diff --git a/garden-service/src/cli/cli.ts b/garden-service/src/cli/cli.ts index 33c20bbf0e..4a03404191 100644 --- a/garden-service/src/cli/cli.ts +++ b/garden-service/src/cli/cli.ts @@ -276,7 +276,15 @@ export class GardenCli { const log = logger.placeholder() const footerLog = logger.placeholder() - const contextOpts: GardenOpts = { environmentName: env, log } + const contextOpts: GardenOpts = { + commandInfo: { + name: command.getFullName(), + args: parsedArgs, + opts: parsedOpts, + }, + environmentName: env, + log, + } if (command.noProject) { contextOpts.config = MOCK_CONFIG } diff --git a/garden-service/src/commands/plugins.ts b/garden-service/src/commands/plugins.ts index 5872fb0479..0214b5339a 100644 --- a/garden-service/src/commands/plugins.ts +++ b/garden-service/src/commands/plugins.ts @@ -15,6 +15,7 @@ import { LogEntry } from "../logger/log-entry" import { Garden } from "../garden" import { Command, CommandResult, CommandParams, StringParameter } from "./base" import * as Bluebird from "bluebird" +import { printHeader } from "../logger/util" const pluginArgs = { plugin: new StringParameter({ @@ -53,8 +54,8 @@ export class PluginsCommand extends Command { arguments = pluginArgs async action({ garden, log, args }: CommandParams): Promise { - const providers = await garden.resolveProviders() - const configuredPlugins = providers.map(p => p.name) + const providerConfigs = await garden.getRawProviderConfigs() + const configuredPlugins = providerConfigs.map(p => p.name) if (!args.command) { // We're listing commands, not executing one @@ -77,6 +78,12 @@ export class PluginsCommand extends Command { } } + if (command.title) { + const environmentName = garden.environmentName + const title = typeof command.title === "function" ? await command.title({ environmentName }) : command.title + printHeader(log, title, "gear") + } + const provider = await garden.resolveProvider(args.plugin) const ctx = await garden.getPluginContext(provider) diff --git a/garden-service/src/garden.ts b/garden-service/src/garden.ts index ea5a1d3f15..16ec02444c 100644 --- a/garden-service/src/garden.ts +++ b/garden-service/src/garden.ts @@ -33,7 +33,7 @@ import { LocalConfigStore, ConfigStore, GlobalConfigStore } from "./config-store import { getLinkedSources, ExternalSourceType } from "./util/ext-source-util" import { BuildDependencyConfig, ModuleConfig, ModuleResource, moduleConfigSchema } from "./config/module" import { ModuleConfigContext, ContextResolveOpts } from "./config/config-context" -import { createPluginContext } from "./plugin-context" +import { createPluginContext, CommandInfo } from "./plugin-context" import { ModuleAndRuntimeActions, Plugins, RegisterPluginParam } from "./types/plugin/plugin" import { SUPPORTED_PLATFORMS, SupportedPlatform, DEFAULT_GARDEN_DIR_NAME } from "./constants" import { platform, arch } from "os" @@ -71,6 +71,7 @@ export type ModuleActionMap = { export interface GardenOpts { config?: ProjectConfig, + commandInfo?: CommandInfo, gardenDirPath?: string, environmentName?: string, log?: LogEntry, @@ -244,7 +245,7 @@ export class Garden { } getPluginContext(provider: Provider) { - return createPluginContext(this, provider) + return createPluginContext(this, provider, this.opts.commandInfo) } async clearBuilds() { diff --git a/garden-service/src/plugin-context.ts b/garden-service/src/plugin-context.ts index 5c88653a7a..bc6f0219d0 100644 --- a/garden-service/src/plugin-context.ts +++ b/garden-service/src/plugin-context.ts @@ -10,9 +10,8 @@ import { Garden } from "./garden" import { cloneDeep } from "lodash" import { projectNameSchema, projectSourcesSchema, environmentNameSchema } from "./config/project" import { Provider, providerSchema, ProviderConfig } from "./config/provider" -import { configStoreSchema } from "./config-store" import { deline } from "./util/string" -import { joi } from "./config/common" +import { joi, joiVariables, PrimitiveMap } from "./config/common" type WrappedFromGarden = Pick +export interface CommandInfo { + name: string + args: PrimitiveMap + opts: PrimitiveMap +} + export interface PluginContext extends WrappedFromGarden { + command?: CommandInfo provider: Provider } @@ -34,31 +39,44 @@ export interface PluginContext extend export const pluginContextSchema = joi.object() .options({ presence: "required" }) .keys({ - projectName: projectNameSchema, - projectRoot: joi.string() - .description("The absolute path of the project root."), + command: joi.object() + .optional() + .keys({ + name: joi.string() + .required() + .description("The command name currently being executed."), + args: joiVariables() + .required() + .description("The positional arguments passed to the command."), + opts: joiVariables() + .required() + .description("The optional flags passed to the command."), + }) + .description("Information about the command being executed, if applicable."), + environmentName: environmentNameSchema, gardenDirPath: joi.string() .description(deline` The absolute path of the project's Garden dir. This is the directory the contains builds, logs and other meta data. A custom path can be set when initialising the Garden class. Defaults to \`.garden\`. `), + projectName: projectNameSchema, + projectRoot: joi.string() + .description("The absolute path of the project root."), projectSources: projectSourcesSchema, - configStore: configStoreSchema, - environmentName: environmentNameSchema, provider: providerSchema .description("The provider being used for this context."), workingCopyId: joi.string() .description("A unique ID assigned to the current project working copy."), }) -export function createPluginContext(garden: Garden, provider: Provider): PluginContext { +export function createPluginContext(garden: Garden, provider: Provider, command?: CommandInfo): PluginContext { return { + command, environmentName: garden.environmentName, + gardenDirPath: garden.gardenDirPath, projectName: garden.projectName, projectRoot: garden.projectRoot, - gardenDirPath: garden.gardenDirPath, projectSources: cloneDeep(garden.projectSources), - configStore: garden.configStore, provider, workingCopyId: garden.workingCopyId, } diff --git a/garden-service/src/plugins/kubernetes/commands/cleanup-cluster-registry.ts b/garden-service/src/plugins/kubernetes/commands/cleanup-cluster-registry.ts index 2dff246d1b..6306014c67 100644 --- a/garden-service/src/plugins/kubernetes/commands/cleanup-cluster-registry.ts +++ b/garden-service/src/plugins/kubernetes/commands/cleanup-cluster-registry.ts @@ -33,6 +33,8 @@ export const cleanupClusterRegistry: PluginCommand = { name: "cleanup-cluster-registry", description: "Clean up unused images in the in-cluster registry and cache.", + title: "Cleaning up caches and unused images from the in-cluster registry", + handler: async ({ ctx, log }) => { const result = {} diff --git a/garden-service/src/plugins/kubernetes/commands/cluster-init.ts b/garden-service/src/plugins/kubernetes/commands/cluster-init.ts index 52c8d61cef..1d239afa4b 100644 --- a/garden-service/src/plugins/kubernetes/commands/cluster-init.ts +++ b/garden-service/src/plugins/kubernetes/commands/cluster-init.ts @@ -14,29 +14,27 @@ export const clusterInit: PluginCommand = { name: "cluster-init", description: "Initialize or update cluster-wide Garden services.", - handler: async ({ ctx, log }) => { - const entry = log.info({ - msg: chalk.bold.magenta( - `Initializing/updating cluster-wide services for ${chalk.white(ctx.environmentName)} environment`, - ), - }) + title: ({ environmentName }) => { + return `Initializing/updating cluster-wide services for ${chalk.white(environmentName)} environment` + }, + handler: async ({ ctx, log }) => { const status = await getEnvironmentStatus({ ctx, log }) let result = {} if (status.ready) { - entry.info("All services already initialized!") + log.info("All services already initialized!") } else { result = await prepareSystem({ ctx, - log: entry, + log, force: true, status, clusterInit: true, }) } - log.info(chalk.green("Done!")) + log.info(chalk.green("\nDone!")) return { result } }, diff --git a/garden-service/src/plugins/kubernetes/commands/uninstall-garden-services.ts b/garden-service/src/plugins/kubernetes/commands/uninstall-garden-services.ts index cdb9dd928c..9358cd89f2 100644 --- a/garden-service/src/plugins/kubernetes/commands/uninstall-garden-services.ts +++ b/garden-service/src/plugins/kubernetes/commands/uninstall-garden-services.ts @@ -16,22 +16,20 @@ export const uninstallGardenServices: PluginCommand = { name: "uninstall-garden-services", description: "Clean up all installed cluster-wide Garden services.", - handler: async ({ ctx, log }) => { - const entry = log.info({ - msg: chalk.bold.magenta( - `Removing cluster-wide services for ${chalk.white(ctx.environmentName)} environment`, - ), - }) + title: ({ environmentName }) => { + return `Removing cluster-wide services for ${chalk.white(environmentName)} environment` + }, + handler: async ({ ctx, log }) => { const k8sCtx = ctx const variables = getKubernetesSystemVariables(k8sCtx.provider.config) const sysGarden = await getSystemGarden(k8sCtx, variables || {}, log) const actions = await sysGarden.getActionHelper() - const result = await actions.deleteEnvironment(entry) + const result = await actions.deleteEnvironment(log) - log.info(chalk.green("Done!")) + log.info(chalk.green("\nDone!")) return { result } }, diff --git a/garden-service/src/plugins/kubernetes/init.ts b/garden-service/src/plugins/kubernetes/init.ts index 6b9480949f..0de173b61f 100644 --- a/garden-service/src/plugins/kubernetes/init.ts +++ b/garden-service/src/plugins/kubernetes/init.ts @@ -157,21 +157,29 @@ export async function prepareSystem( // If we require manual init and system services are ready OR outdated but none are *missing*, we warn // in the prepareEnvironment handler, instead of flagging as not ready here. This avoids blocking users where // there's variance in configuration between users of the same cluster, that often doesn't affect usage. - if (!clusterInit && remoteCluster && combinedState === "outdated" && !serviceStates.includes("missing")) { - log.warn({ - symbol: "warning", - msg: chalk.yellow(deline` - One or more cluster-wide system services are outdated or their configuration does not match your current - configuration. You may want to run \`garden --env=${ctx.environmentName} plugins kubernetes cluster-init\` - to update them, or contact a cluster admin to do so. - `), - }) + if (!clusterInit && remoteCluster) { + if (combinedState === "outdated" && !serviceStates.includes("missing")) { + log.warn({ + symbol: "warning", + msg: chalk.yellow(deline` + One or more cluster-wide system services are outdated or their configuration does not match your current + configuration. You may want to run \`garden --env=${ctx.environmentName} plugins kubernetes cluster-init\` + to update them, or contact a cluster admin to do so. + `), + }) + } return {} } // We require manual init if we're installing any system services to remote clusters, to avoid conflicts // between users or unnecessary work. if (!clusterInit && remoteCluster && !systemReady) { + // Special-case so that this doesn't error when attempting to run the cluster init + const initCommandName = `plugins ${ctx.provider.name} cluster-init` + if (ctx.command && ctx.command.name === initCommandName) { + return {} + } + throw new KubernetesError(deline` One or more cluster-wide system services are missing or not ready. You need to run \`garden --env=${ctx.environmentName} plugins kubernetes cluster-init\` diff --git a/garden-service/src/plugins/kubernetes/system.ts b/garden-service/src/plugins/kubernetes/system.ts index bb592fda1a..d254ba1d96 100644 --- a/garden-service/src/plugins/kubernetes/system.ts +++ b/garden-service/src/plugins/kubernetes/system.ts @@ -68,6 +68,7 @@ export async function getSystemGarden( providers: [sysProvider], variables, }, + commandInfo: ctx.command, log: log.info({ section: "garden-system", msg: "Initializing...", status: "active", indent: 1 }), }) } diff --git a/garden-service/src/tasks/resolve-provider.ts b/garden-service/src/tasks/resolve-provider.ts index 4e0f4e52be..ff5452b187 100644 --- a/garden-service/src/tasks/resolve-provider.ts +++ b/garden-service/src/tasks/resolve-provider.ts @@ -18,7 +18,6 @@ import { ModuleConfig } from "../config/module" import { GardenPlugin } from "../types/plugin/plugin" import { validateWithPath } from "../config/common" import * as Bluebird from "bluebird" -import { createPluginContext } from "../plugin-context" import { defaultEnvironmentStatus } from "../types/plugin/provider/getEnvironmentStatus" interface Params extends TaskParams { @@ -136,7 +135,7 @@ export class ResolveProviderTask extends BaseTask { private async ensurePrepared(tmpProvider: Provider) { const pluginName = tmpProvider.name const actions = await this.garden.getActionHelper() - const ctx = createPluginContext(this.garden, tmpProvider) + const ctx = this.garden.getPluginContext(tmpProvider) const log = this.log.placeholder() diff --git a/garden-service/src/types/plugin/command.ts b/garden-service/src/types/plugin/command.ts index 550218b752..3d552df489 100644 --- a/garden-service/src/types/plugin/command.ts +++ b/garden-service/src/types/plugin/command.ts @@ -38,6 +38,7 @@ export interface PluginCommandHandler { export interface PluginCommand { name: string description: string + title?: string | ((params: { environmentName: string }) => string | Promise) // TODO: allow arguments handler: PluginCommandHandler } @@ -51,6 +52,8 @@ export const pluginCommandSchema = joi.object() .required() .max(80) .description("A one-line description of the command (max 80 chars)."), + title: joi.alternatives(joi.string(), joi.func()) + .description("A heading to print ahead of calling the command handler, or a function that returns it."), handler: joi.func() // TODO: see if we can define/output the function schema somehow .description("The command handler."),