import {RivendellApi} from './RivendellApi'; import {AppContext} from './AppContext'; import {DotenvParseOutput} from 'dotenv'; export namespace Rivendell { export enum AppVersionState { NEW = 'NEW', PUBLISHED = 'PUBLISHED', BUILD_FAILED = 'BUILD_FAILED', STARTING = 'STARTING', START_FAILED = 'START_FAILED', RUNNING = 'RUNNING', UPGRADING_RUNTIME = 'UPGRADING_RUNTIME', STOPPING = 'STOPPING', STOP_FAILED = 'STOP_FAILED', STOPPED = 'STOPPED', ABANDONED = 'ABANDONED' } export enum ReviewStatus { NOT_STARTED = 'NOT_STARTED', IN_REVIEW = 'IN_REVIEW', APPROVED = 'APPROVED', NOT_REQUIRED = 'NOT_REQUIRED' } export enum BuildState { NEW = 'NEW', QUEUED = 'QUEUED', RUNNING = 'RUNNING', FINISHED = 'FINISHED' } export enum BuildMode { VALIDATE = 'VALIDATE', PUBLISH = 'PUBLISH' } export enum BuildStatus { SUCCESS = 'SUCCESS', FAILURE = 'FAILURE', ERROR = 'ERROR' } export enum DeploymentStage { TEST = 'TEST', PRODUCTION = 'PRODUCTION' } export enum JobStatus { PENDING = 'PENDING', SCHEDULED = 'SCHEDULED', RUNNING = 'RUNNING', COMPLETE = 'COMPLETE', ERROR = 'ERROR', TERMINATED = 'TERMINATED' } export enum JobTriggerType { IMMEDIATE = 'IMMEDIATE', RECURRING = 'RECURRING' } export enum LogLevel { DEBUG = 'DEBUG', INFO = 'INFO', WARN = 'WARN', ERROR = 'ERROR', NEVER = 'NEVER' } export enum InstallationResolutionType { GUUID = 'GUUID', HEADER = 'HEADER', QUERY_PARAM = 'QUERY_PARAM', JSON_BODY_FIELD = 'JSON_BODY_FIELD' } export interface AppVersionId { appId: string; version: string; } export interface Build { id: number; appVersionId: AppVersionId; mode: BuildMode; state: BuildState; status?: BuildStatus; errors?: string[]; queuedAt?: string; runningAt?: string; finishedAt?: string; dependencySet?: string; } export interface App { id: string; name: string; createdAt: string; updatedAt: string; } export interface AppVersion { id: AppVersionId; manifestJson: string; packageUrl: string; state: string; reviewStatus?: string; stage: string; image: string; accountType: string; createdAt: string; updatedAt: string; } export interface AppInstallation { id: number; appId: string; version: string; accountId: number; trackerId: string; createdAt: string; updatedAt: string; } export interface Job { id: string; accountId: number; trackerId: string; status: JobStatus; definition: JobDefinition; errors: string; triggeredAt: string; scheduledAt: string; startedAt: string; completedAt: string; terminatedAt: string; createdAt: string; updatedAt: string; } export interface JobRuntimeStatus { status: string; } export interface JobDefinition { id: string; appId: string; version: string; function: string; parameters: string; trigger: JobTriggerType; } export interface Webhook { id: string; url: string; attributes: WebhookAttributes; createdAt: string; updatedAt?: string; deletedAt?: string; } export interface WebhookAttributes { appId: string; appVersion: string; appInstallId?: number; taskName: string; internal: boolean; enabled: boolean; installationResolutionType: InstallationResolutionType; installationResolutionKey: string; installationResolutionValue: string; dataSyncId?: string; } export interface AppVersionSearchCriteria { appId?: string; version?: string; appName?: string; states?: AppVersionState[]; reviewStatuses?: ReviewStatus[]; stages?: DeploymentStage[]; categories?: Set; sorts?: Sort[]; } export interface AppInstallationSearchCriteria { appId?: string; version?: string; trackerIds?: string[]; sorts?: Sort[]; } export interface WebhookSearchCriteria { appId?: string; appInstallId?: number; includeInternal?: boolean; includeDisabled?: boolean; } export interface JobSearchCriteria { appId: string; versions?: string[]; trackerIds?: string[]; states?: JobStatus[]; functions?: string[]; minDurationMillis?: number; sort?: Sort[]; limit?: number; start?: number; end?: number; } export interface Upgrade { id: number; status: UpgradeStatus; message?: string; } export enum UpgradeStatus { SCHEDULED = 'SCHEDULED', UPGRADING_LIFECYCLE = 'UPGRADING_LIFECYLES', UPGRADING_LIFECYCLE_FAILED = 'UPGRADING_LIFECYCLE_FAILED', UPDATING_WEBHOOKS = 'UPDATING_WEBHOOKS', UPDATING_WEBHOOKS_FAILED = 'UPGRADING_LIFECYCLE_FAILED', FINALIZING_LIFECYCLE = 'FINALIZING_LIFECYCLE', FINALIZING_LIFECYCLE_FAILED = 'FINALIZING_LIFECYCLE_FAILED', COMPLETE = 'COMPLETE' } export interface AppReview { url: string; } export enum SortDirection { ASC = 'ASC', DESC = 'DESC' } export interface Sort { field: string; direction?: SortDirection; } export enum Audience { USER = 'USER', DEVELOPER = 'DEVELOPER', ZAIUS = 'ZAIUS' } export interface AwsCredentials { accessKeyId: string; secretAccessKey: string; sessionToken: string; expiration: string; } export interface Account { id: number; trackerId: string; name: string; accountType: string; } export interface DeveloperApp { id: string; } export interface Developer { id: string; email: string; role: string; vendor: string; personal_apps: DeveloperApp[]; vendor_apps: DeveloperApp[]; } export interface Shard { id: string; description: string; url: string; live: boolean; } export interface LogLevelRecord { appId: string; version: string; trackerId?: string; logLevel: string; } export interface JwtResponse { jwt: string; } export interface JwtHeader { kid: string; typ: string; alg: string; } export interface JwtPayload { aud: string; exp: number; iss: string; sub: string; } export interface Jwt { token: string; header: JwtHeader; payload: JwtPayload; } function path(shard: string, endpoint: string) { return `${shard}/${endpoint}`.replace(/^\//, ''); } export async function searchAccounts(term: string, shard: string): Promise { const response = await RivendellApi.get( path(shard, `accounts/search?term=${encodeURIComponent(term)}`)); return response.body; } export async function whoami(): Promise { const response = await RivendellApi.get('developers/me'); return response.body; } export async function isAdmin(): Promise { const me = await whoami(); return me.role === 'administrator' || me.role === 'app_administrator'; } /** * Register a new App. * @param appId * @param name * @param accountType * @param personalApp * @param shard */ export async function registerApp( appId: string, name: string, accountType: any, personalApp: boolean, shard: string ): Promise { const response = await RivendellApi.post(path(shard, 'apps'), {appId, name, accountType, personalApp}); return response.body; } /** * Fetch an App by identifier. * @param appId */ export async function fetchApp(appId: string, shard = ''): Promise { const response = await RivendellApi.get(path(shard, `apps/${appId}`)); return response.body; } /** * Get app version review url. * @param appId */ export async function getReviewUrl(appVersion: string): Promise { const response = await RivendellApi.get(`reviews/${appVersion}`); return response.body; } /** * Search for AppVersions using the supplied criteria. * @param criteria */ export async function searchAppVersions(criteria: AppVersionSearchCriteria, shard: string): Promise { const response = await RivendellApi.post(path(shard, 'versions/search'), criteria); return response.body; } /** * Fetch an AppVersion by identifier. * @param appContext */ export async function fetchAppVersion(appContext: AppContext, shard: string): Promise { const response = await RivendellApi.get( path(shard, `versions/${appContext.appId}@${appContext.version}`)); return response.body; } /** * Register a new app version. */ export async function registerAppVersion( appId: string, version: string, packageUrl: string, appEnvValues: DotenvParseOutput, usePreviousAppEnvValues: boolean, shard: string ): Promise { const response = await RivendellApi.post( path(shard, 'versions'), {appId, version, packageUrl, appEnvValues, usePreviousAppEnvValues}); return response.body; } /** * Register a new app version. */ export async function updateAppVersion( appId: string, version: string, packageUrl: string, appEnvValues: DotenvParseOutput, usePreviousAppEnvValues: boolean, shard: string ): Promise { const response = await RivendellApi.put( path(shard, `versions/${appId}@${version}`), {packageUrl, appEnvValues, usePreviousAppEnvValues}); return response.body; } /** * Review an app version. */ export async function reviewAppVersion( appId: string, version: string ): Promise { const appVersionId = `${appId}@${version}`; await RivendellApi.post('reviews', appVersionId); } /** * Search for AppVersions using the supplied criteria. * @param criteria */ export async function searchAppInstallations( criteria: AppInstallationSearchCriteria, shard: string ): Promise { const response = await RivendellApi.post(path(shard, 'installations/search'), criteria); return response.body; } /** * Search for Webhooks using the supplied criteria. * @param criteria */ export async function searchWebhooks(criteria: WebhookSearchCriteria, shard: string): Promise { const response = await RivendellApi.post(path(shard, 'webhooks/search'), criteria); return response.body; } /** * Loads a single AppInstallation. * @param appId * @param trackerId */ export async function fetchAppInstallation(appId: string, trackerId: string, shard = ''): Promise { const response = await searchAppInstallations({appId, trackerIds: [trackerId]}, shard); if (response.length !== 1) { throw new Error(`Installation of ${appId} not found for ${trackerId}`); } return response[0]; } /** * Trigger an installation. */ export async function install( appId: string, version: string, trackerId: string, shard: string) : Promise { const response = await RivendellApi.post( path(shard, 'installations'), {appId, version, trackerId}); return response.body; } /** * Trigger an uninstall. */ export async function uninstall(appInstallationId: number, shard: string): Promise { await RivendellApi.request('DELETE', path(shard, `installations/${appInstallationId}`)); } /** * Request an upgrade. */ export async function upgrade( appInstallationId: number, appId: string, version: string, shard: string ): Promise { const response = await RivendellApi.put( path(shard, `installations/${appInstallationId}`), {appId, version}); return response.body; } export async function fetchUpgrade(upgradeId: number, shard: string): Promise { const response = await RivendellApi.get(path(shard, `upgrades/${upgradeId}`)); return response.body; } /** * Trigger a publish. */ export async function publish(appId: string, version: string, shard: string): Promise { await RivendellApi.request('PATCH', path(shard, `versions/${appId}@${version}`), {state: AppVersionState.RUNNING}); } /** * Trigger a unpublish. */ export async function unpublish(appId: string, version: string, shard: string): Promise { await RivendellApi.request('PATCH', path(shard, `versions/${appId}@${version}`), {state: AppVersionState.STOPPED}); } /** * Check if an app version can be unpublished. */ export async function canUnpublish(appId: string, version: string, shard: string): Promise<{[key: string]: any}> { const response = await RivendellApi.get<{[key: string]: any}>(path(shard, `versions/${appId}@${version}/can_unpublish`)); return response.body; } /** * Trigger a abandon. */ export async function abandon(appId: string, version: string, shard: string): Promise { await RivendellApi.request('PATCH', path(shard, `versions/${appId}@${version}`), {state: AppVersionState.ABANDONED}); } /** * Set logLevel of an app version or an installation */ export async function setLogLevel( shard: string, appId: string, version: string, logLevel: string, timeToLive: number, trackerId?: string ): Promise { await RivendellApi.put(path(shard, `runtime-config/log-levels`), {appId, version, trackerId, logLevel, timeToLive}); } /** * Get logLevel of an app version or an installation */ export async function getLogLevel( shard: string, appId: string, version: string, trackerId?: string ): Promise { const response = await RivendellApi.post( path(shard, `runtime-config/log-levels/search`), {appId, version, trackerId}); return response.body; } /** * Fetch a build by id. */ export async function fetchBuild(id: number): Promise { const response = await RivendellApi.get(`builds/${id}`); return response.body; } /** * Trigger a build. */ export async function build(appId: string, version: string, useRCDependencySet: boolean): Promise { const response = await RivendellApi.post( 'builds', {appId, version, useRCDependencySet} ); return response.body; } /** * Trigger a verification. */ export async function verify(appId: string, version: string): Promise { const response = await RivendellApi.post('verifications', {appId, version}); return response.body; } /** * Fetch temp credentials to upload to s3. */ export async function uploadCredentials( vendor: string, appId: string, version: string, checksum: string ): Promise { const response = await RivendellApi.post('tokens', {vendor, appId, version, checksum}); return response.body; } /** * List shards. */ export async function shards(): Promise { const response = await RivendellApi.get('shards'); const me = await Rivendell.whoami(); return response.body.filter((s) => me.role === 'administrator' || s.live); } /** * Search Jobs. */ export async function searchJobs(criteria: JobSearchCriteria, shard: string): Promise { const response = await RivendellApi.post(path(shard, 'jobs/search'), criteria); return response.body; } /** * Terminate Jobs */ export async function terminateJob(jobId: string, shard: string): Promise { await RivendellApi.delete_(path(shard, `jobs/${jobId}`)); } /** * Trigger a Job */ export async function triggerJob( appId: string, jobName: string, trackerId: string, parameters: string, shard: string ): Promise { const body = {appId, jobName, trackerId, parameters}; const response = await RivendellApi.post(path(shard, 'jobs'), body); return response.body; } /** * Fetch a Job */ export async function fetchJob(jobId: string, shard: string): Promise { const response = await RivendellApi.get(path(shard, `jobs/${jobId}`)); return response.body; } /** * Fetch a jobs runtimeStatus */ export async function fetchJobRuntimeStatus(jobId: string, shard: string): Promise { const response = await RivendellApi.get(path(shard, `jobs/${jobId}/runtimeStatus`)); return response.body; } /** * Get Moria JWT for this developer. */ let cachedJwt: Jwt; export async function jwt(shard: string): Promise { const now = new Date(); if (!cachedJwt || cachedJwt.payload.exp * 1000 - now.getTime() <= 60) { const response = await RivendellApi.get(path(shard, 'tokens/jwt')); cachedJwt = parseJwt(response.body.jwt); } return cachedJwt.token; } function parseJwt(token: string): Jwt { const parts = token.split('.'); return { token, header: JSON.parse(Buffer.from(parts[0], 'base64').toString()), payload: JSON.parse(Buffer.from(parts[1], 'base64').toString()) }; } }