diff --git a/src/cli.ts b/src/cli.ts index b255a7b3cc..09f502a954 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -78,7 +78,7 @@ const GLOBAL_OPTIONS = { output: new ChoicesParameter({ alias: "o", choices: Object.keys(OUTPUT_RENDERERS), - help: "output command result in specified format (note: disable progress logging)", + help: "output command result in specified format (note: disables progress logging)", }), } const GLOBAL_OPTIONS_GROUP_NAME = "Global options" @@ -144,7 +144,7 @@ export interface ParseResults { interface SywacParseResults extends ParseResults { output: string - details: any + details: { result: any } } function makeOptSynopsis(key: string, param: Parameter): string { @@ -292,7 +292,7 @@ export class GardenCli { throw new PluginError(`Global option(s) ${dupKeys.join(" ")} cannot be redefined`, {}) } - const action = async argv => { + const action = async (argv, cliContext) => { // Sywac returns positional args and options in a single object which we separate into args and opts const argsForAction = filterByArray(argv, argKeys) const optsForAction = filterByArray(argv, optKeys.concat(globalKeys)) @@ -315,32 +315,12 @@ export class GardenCli { const garden = await Garden.factory(root, { env, logger }) - try { - // TODO: enforce that commands always output DeepPrimitiveMap - const result = await command.action(garden.pluginContext, argsForAction, optsForAction) + // TODO: enforce that commands always output DeepPrimitiveMap + const result = await command.action(garden.pluginContext, argsForAction, optsForAction) - if (output && result !== undefined) { - const renderer = OUTPUT_RENDERERS[output] - console.log(renderer({ success: true, result })) - // Note: this circumvents an issue where the process exits before the output is fully flushed - await sleep(100) - } + // We attach the action result to cli context so that we can process it in the parse method + cliContext.details.result = result - return result - - } catch (error) { - // TODO: we can likely do this better - const result = { error } - - if (output) { - const renderer = OUTPUT_RENDERERS[output] - console.error(renderer(result)) - // Note: this circumvents an issue where the process exits before the output is fully flushed - await sleep(100) - } - - return result - } } // Command specific positional args and options are set inside the builder function @@ -361,24 +341,41 @@ export class GardenCli { } async parse(): Promise { - return this.program.parse().then((result: SywacParseResults) => { - const { argv, errors, code, output } = result - - // --help or --version options were called - if (output && !(errors.length > 0)) { - this.logger.stop() - console.log(output) - process.exit(result.code) - } + const parseResult: SywacParseResults = await this.program.parse() + const { argv, details, errors, code, output: cliOutput } = parseResult + const { result } = details + const { output } = argv + + // --help or --version options were called so we log the cli output and exit + if (cliOutput && errors.length < 1) { + this.logger.stop() + console.log(cliOutput) + process.exit(parseResult.code) + } + // --output option set and there is content to output + if (output && (result !== undefined || errors.length > 0)) { + const renderer = OUTPUT_RENDERERS[output] if (errors.length > 0) { - errors.forEach(err => this.logger.error({ msg: err.message, error: err })) + const msg = errors.join("\n") + console.error(renderer({ result: msg })) + } else { + console.log(renderer({ success: true, result })) + } + // Note: this circumvents an issue where the process exits before the output is fully flushed + await sleep(100) + } + + if (errors.length > 0) { + errors.forEach(err => this.logger.error({ msg: err.message, error: err })) + + if (this.logger.writers.find(w => w instanceof FileWriter)) { this.logger.info(`See ${ERROR_LOG_FILENAME} for detailed error message`) } + } - this.logger.stop() - return { argv, code, errors } - }) + this.logger.stop() + return { argv, code, errors } } } diff --git a/src/commands/environment/destroy.ts b/src/commands/environment/destroy.ts index b7f2769128..6fbe031c6c 100644 --- a/src/commands/environment/destroy.ts +++ b/src/commands/environment/destroy.ts @@ -6,85 +6,25 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import { every, reduce } from "lodash" import { PluginContext } from "../../plugin-context" import { Command } from "../base" -import { EntryStyle } from "../../logger/types" -import { EnvironmentStatus, EnvironmentStatusMap } from "../../types/plugin" -import { LogEntry } from "../../logger" -import { sleep } from "../../util" -import { TimeoutError } from "../../exceptions" - -const WAIT_FOR_SHUTDOWN_TIMEOUT = 600 - -export type LogEntryMap = { [key: string]: LogEntry } - -const providersTerminated = (status: EnvironmentStatusMap): boolean => every(status, s => s.configured === false) +import { EnvironmentStatusMap } from "../../types/plugin" export class EnvironmentDestroyCommand extends Command { name = "destroy" alias = "d" help = "Destroy environment" - async action(ctx: PluginContext) { + async action(ctx: PluginContext): Promise { const { name } = ctx.getEnvironment() ctx.log.header({ emoji: "skull_and_crossbones", command: `Destroying ${name} environment` }) - let result: EnvironmentStatusMap - let logEntries: LogEntryMap = {} - - result = await ctx.destroyEnvironment() - - if (!providersTerminated(result)) { - ctx.log.info("Waiting for providers to terminate") - logEntries = reduce(result, (acc: LogEntryMap, status: EnvironmentStatus, provider: string) => { - if (status.configured) { - acc[provider] = ctx.log.info({ - section: provider, - msg: "Terminating", - entryStyle: EntryStyle.activity, - }) - } - return acc - }, {}) - - result = await this.waitForShutdown(ctx, name, logEntries) - } + const result = await ctx.destroyEnvironment() ctx.log.finish() return result } - async waitForShutdown(ctx: PluginContext, name: string, logEntries: LogEntryMap) { - const startTime = new Date().getTime() - let result: EnvironmentStatusMap - - while (true) { - await sleep(2000) - - result = await ctx.getEnvironmentStatus() - - Object.keys(result).forEach(key => { - if (result[key].configured && logEntries[key]) { - logEntries[key].setSuccess("Terminated") - } - }) - - if (providersTerminated(result)) { - break - } - - const now = new Date().getTime() - if (now - startTime > WAIT_FOR_SHUTDOWN_TIMEOUT * 1000) { - throw new TimeoutError( - `Timed out waiting for ${name} delete to complete`, - { environmentStatus: result }, - ) - } - } - - return result - } } diff --git a/src/plugins/kubernetes/actions.ts b/src/plugins/kubernetes/actions.ts index 16111fcf48..3ea371db57 100644 --- a/src/plugins/kubernetes/actions.ts +++ b/src/plugins/kubernetes/actions.ts @@ -9,7 +9,7 @@ import * as inquirer from "inquirer" import * as Joi from "joi" -import { DeploymentError, NotFoundError } from "../../exceptions" +import { DeploymentError, NotFoundError, TimeoutError } from "../../exceptions" import { ConfigureEnvironmentParams, DeleteConfigParams, @@ -34,7 +34,7 @@ import { ContainerModule, } from "../container" import { values, every, uniq } from "lodash" -import { deserializeKeys, prompt, serializeKeys, splitFirst } from "../../util" +import { deserializeKeys, prompt, serializeKeys, splitFirst, sleep } from "../../util" import { ServiceStatus } from "../../types/service" import { joiIdentifier } from "../../types/common" import { @@ -49,6 +49,7 @@ import { getAllAppNamespaces, } from "./namespace" import { + KUBECTL_DEFAULT_TIMEOUT, kubectl, } from "./kubectl" import { DEFAULT_TEST_TIMEOUT } from "../../constants" @@ -134,22 +135,45 @@ export async function getServiceStatus(params: GetServiceStatusParams KUBECTL_DEFAULT_TIMEOUT * 1000) { + throw new TimeoutError( + `Timed out waiting for namespace ${namespace} delete to complete`, + { status }, + ) + } + } + } export async function getServiceOutputs({ service }: GetServiceOutputsParams) { diff --git a/src/plugins/kubernetes/namespace.ts b/src/plugins/kubernetes/namespace.ts index 9cc6341bd4..ba2964c41c 100644 --- a/src/plugins/kubernetes/namespace.ts +++ b/src/plugins/kubernetes/namespace.ts @@ -8,7 +8,6 @@ import { PluginContext } from "../../plugin-context" import { - apiGetOrNull, coreApi, } from "./api" import { KubernetesProvider } from "./index" @@ -19,17 +18,6 @@ import { import { name as providerName } from "./index" import { AuthenticationError } from "../../exceptions" -export async function namespaceReady(context: string, namespace: string) { - /** - * This is an issue with kubernetes-client where it fetches all namespaces instead of the requested one. - * Is fixed in v4.0.0. See https://github.com/godaddy/kubernetes-client/issues/187 and - * https://github.com/godaddy/kubernetes-client/pull/190 - */ - const allNamespaces = await apiGetOrNull(coreApi(context).namespaces, namespace) - const ns = allNamespaces.items.find(n => n.metadata.name === namespace) - return ns && ns.status.phase === "Active" -} - export async function createNamespace(context: string, namespace: string) { await coreApi(context).namespaces.post({ body: { diff --git a/test/src/commands/environment/destroy.ts b/test/src/commands/environment/destroy.ts index 501898bd0b..f5a8273844 100644 --- a/test/src/commands/environment/destroy.ts +++ b/test/src/commands/environment/destroy.ts @@ -48,19 +48,4 @@ describe("EnvironmentDestroyCommand", () => { expect(result["test-plugin"]["configured"]).to.be.false }) - it("should wait until each provider is no longer configured", async () => { - const garden = await Garden.factory(projectRootB, { plugins: [testProvider] }) - - td.replace( - command, - "waitForShutdown", - async () => ({ - "test-plugin": { configured: false }, - }), - ) - - const result = await command.action(garden.pluginContext) - - expect(result["test-plugin"]["configured"]).to.be.false - }) })