import chalk from 'chalk'; import {command, flag, help, namespace, option, param} from 'oo-cli'; import {terminal} from 'terminal-kit'; import {AppContext} from '../../lib/AppContext'; import {die} from '../../lib/die'; import {formatVersionState} from '../../lib/formatVersionState'; import {handleInterrupt} from '../../lib/handleInterrupt'; import {TerminalSpinner} from '../../lib/TerminalSpinner'; import {Rivendell} from '../../lib/Rivendell'; import AppVersionState = Rivendell.AppVersionState; import SortDirection = Rivendell.SortDirection; import AppInstallation = Rivendell.AppInstallation; import {formatError} from '../../lib/formatError'; import {applicableShards} from '../../lib/Shards'; import { AppManifest } from '@zaiusinc/app-sdk'; const UNPUBLISHABLE_STATES = [AppVersionState.RUNNING, AppVersionState.START_FAILED, AppVersionState.STOP_FAILED]; @namespace('directory') export class UnpublishCommand { @flag('f') public force!: boolean; @flag('no-progress') public noProgress!: boolean; @param @help('The App ID and version (e.g. my_app@1.0.0)') public appVersion!: string; @option('a') @help('The availability zone that will be targeted (default: us)') public availability: string = ''; @command @help('Unpublish an app version (uninstall everywhere and undeploy)') public async unpublish() { handleInterrupt(); if (!/[\w-]+@\d+\.\d+\.\d+(?:-\w+\.\d+)?/.test(this.appVersion)) { die('appVersion is required in the format of app@1.0.0'); } const [appId, version] = this.appVersion.split('@'); try { const appVersion = await Rivendell.fetchAppVersion({appId, version}, 'us'); if (!UNPUBLISHABLE_STATES.includes(appVersion.state as AppVersionState)) { throw new Error(`AppVersion must be in an unpublishable state. ` + `Currently: ${formatVersionState(appVersion.state as AppVersionState)}`); } const manifest = JSON.parse(appVersion.manifestJson) as AppManifest; const shards = await applicableShards(this.availability, manifest); const errors: string[] = []; for (const shard of shards) { if (!(await this.uninstallEverywhere(appId, version, shard))) { process.exit(); } try { await Rivendell.unpublish(appId, version, shard); console.log(chalk.green(`${this.appVersion} is being unpublished in ${shard}.`)); await this.watchForCompletion({appId, version}, shard); } catch (error) { errors.push(`${formatError(error)} (${shard})`); } } if (errors.length) { console.log(chalk.red(errors.join('\n'))); } } catch (e) { die(formatError(e)); } } private async uninstallEverywhere(appId: string, version: string, shard: string): Promise { return new Promise((resolve, reject) => { const criteria = { appId, version, sorts: [{ field: 'trackerId', direction: SortDirection.DESC }] }; Rivendell.searchAppInstallations(criteria, shard) .then(async (installations) => { if (!installations.length) { resolve(true); } else { const doIt = async () => { try { for (const appInstallation of installations) { await this.uninstall(appInstallation, shard); } resolve(true); } catch (e) { reject(e); } }; if (this.force) { await doIt(); } else { const trackers = installations.map((installation) => installation.trackerId); console.log(`${this.appVersion} is currently installed in ${trackers.length} account(s) in ${shard}:`); console.log(` (${trackers.join(', ')})\n`); console.log(chalk.red('Are you sure you want to uninstall from all these accounts? [y/n]')); terminal.yesOrNo({yes: ['y', 'yes'], no: ['n', 'no']}, async (err, yes) => { if (err) { reject(err); } else if (yes) { await doIt(); } else { console.log(`Aborted. No changes made to ${this.appVersion}.`); resolve(false); } }); } } }) .catch((e) => reject(e)); }); } private async uninstall(appInstallation: AppInstallation, shard: string) { await Rivendell.uninstall(appInstallation.id, shard); console.log(chalk.gray(`Uninstalled ${appInstallation.appId} from ${appInstallation.trackerId} in ${shard}`)); } private async watchForCompletion(context: AppContext, shard: string) { console.log(chalk.gray(`Watching for unpublish to complete in ${shard}... CTRL+C to stop checking.`)); return new Promise((resolve, reject) => { const spinner = this.noProgress ? null : new TerminalSpinner().start(''); let attempts = 0; const checkStatus = () => { attempts++; Rivendell.fetchAppVersion(context, shard) .then((appVersion) => { const state = appVersion.state as AppVersionState; if (state === AppVersionState.STOPPED) { if (spinner) { spinner.stop(); } clearInterval(interval); console.log(chalk.green(`${context.appId}@${context.version} has been unpublished in ${shard}.`)); resolve(); } else { if (attempts > 300) { die('Timed out waiting for completion'); } if (spinner) { spinner.update(`Status: ${formatVersionState(state)}`); } } }) .catch((e) => { clearInterval(interval); if (spinner) { spinner.stop(); } reject(e); }); }; const interval = setInterval(checkStatus, 2000); checkStatus(); }); } }