From af2af06ffa45f3ce462319129cd2257fbef57120 Mon Sep 17 00:00:00 2001 From: Jon Edvald Date: Tue, 11 Sep 2018 14:37:34 -0700 Subject: [PATCH] refactor: tighten plugin context API considerably This is a fairly major refactor, geared at stabilising and limiting the surface area of the plugin API, and getting closer to only passing primitive values to plugin actions, as opposed to framework code. The only user facing change here is changing the currently unused `get/set/delete config` to more expressly handle user secrets management, dropping the notion of namespaced configuration in favour of plugin-managed secrets. Other notable changes: - Most of PluginContext has been split into a separate ActionHelper, which is only accessible internally via the `garden.actions`. Plugins can thus no longer affect workflows imperatively. - Because of the above, we now pass Module objects to actions directly, simplifying flows somewhat and getting rid of some race condition issues. - A lot of internal code that used to receive a PluginContext, now just receives a Garden instance. - The PluginContext is now initialised with a provider, and includes it, removing the separate `env` and `provider` action params. - I discovered that Command args and opts were not type-safe anymore, so I needed to fix a couple of things there along the way, and cleaned up a bit as well. - I added thorough documentation to the plugin schema, along with full input parameter schemas for every action, as well as tests for the ActionHelper and those schemas. - Some handlers have been renamed, and some schemas updated slightly for clarity and consistency. Next steps: Generate documentation for providers and better docs for plugin module types via the `describeType` action handler. --- docs/reference/commands.md | 51 +- docs/reference/config.md | 14 +- examples/hello-world/garden.yml | 2 + garden-cli/src/actions.ts | 417 +++++++++++++ garden-cli/src/cli/cli.ts | 3 +- garden-cli/src/commands/base.ts | 26 +- garden-cli/src/commands/build.ts | 28 +- garden-cli/src/commands/call.ts | 21 +- garden-cli/src/commands/create/module.ts | 32 +- garden-cli/src/commands/create/project.ts | 37 +- garden-cli/src/commands/delete.ts | 63 +- garden-cli/src/commands/deploy.ts | 39 +- garden-cli/src/commands/dev.ts | 23 +- garden-cli/src/commands/exec.ts | 24 +- garden-cli/src/commands/get.ts | 49 +- garden-cli/src/commands/init.ts | 19 +- garden-cli/src/commands/link/module.ts | 21 +- garden-cli/src/commands/link/source.ts | 22 +- garden-cli/src/commands/login.ts | 10 +- garden-cli/src/commands/logout.ts | 9 +- garden-cli/src/commands/logs.ts | 19 +- garden-cli/src/commands/push.ts | 25 +- garden-cli/src/commands/run/module.ts | 40 +- garden-cli/src/commands/run/run.ts | 16 +- garden-cli/src/commands/run/service.ts | 29 +- garden-cli/src/commands/run/test.ts | 35 +- garden-cli/src/commands/scan.ts | 8 +- garden-cli/src/commands/set.ts | 48 +- garden-cli/src/commands/test.ts | 45 +- garden-cli/src/commands/unlink/module.ts | 24 +- garden-cli/src/commands/unlink/source.ts | 24 +- garden-cli/src/commands/update-remote/all.ts | 8 +- .../src/commands/update-remote/modules.ts | 23 +- .../src/commands/update-remote/sources.ts | 24 +- garden-cli/src/commands/validate.ts | 7 +- garden-cli/src/config/common.ts | 16 - garden-cli/src/config/config-context.ts | 26 +- garden-cli/src/config/project.ts | 104 ++-- garden-cli/src/garden.ts | 178 ++---- garden-cli/src/plugin-context.ts | 549 ++---------------- garden-cli/src/plugins/container.ts | 26 +- garden-cli/src/plugins/generic.ts | 18 +- garden-cli/src/plugins/google/common.ts | 18 +- .../src/plugins/google/google-app-engine.ts | 18 +- .../plugins/google/google-cloud-functions.ts | 28 +- garden-cli/src/plugins/kubernetes/actions.ts | 153 ++--- garden-cli/src/plugins/kubernetes/api.ts | 4 +- .../src/plugins/kubernetes/deployment.ts | 19 +- garden-cli/src/plugins/kubernetes/helm.ts | 60 +- .../src/plugins/kubernetes/kubernetes.ts | 36 +- garden-cli/src/plugins/kubernetes/local.ts | 63 +- garden-cli/src/plugins/kubernetes/status.ts | 29 +- .../src/plugins/local/local-docker-swarm.ts | 35 +- .../local/local-google-cloud-functions.ts | 4 +- garden-cli/src/plugins/openfaas.ts | 91 ++- garden-cli/src/plugins/plugins.ts | 1 - garden-cli/src/process.ts | 17 +- garden-cli/src/task-graph.ts | 14 +- garden-cli/src/tasks/base.ts | 4 + garden-cli/src/tasks/build.ts | 30 +- garden-cli/src/tasks/deploy.ts | 38 +- garden-cli/src/tasks/push.ts | 18 +- garden-cli/src/tasks/test.ts | 46 +- garden-cli/src/types/module.ts | 35 +- garden-cli/src/types/plugin/outputs.ts | 63 +- garden-cli/src/types/plugin/params.ts | 224 +++++-- garden-cli/src/types/plugin/plugin.ts | 266 ++++++++- garden-cli/src/types/service.ts | 57 +- garden-cli/src/util/ext-source-util.ts | 27 +- garden-cli/src/watch.ts | 32 +- garden-cli/test/helpers.ts | 76 ++- garden-cli/test/src/actions.ts | 533 +++++++++++++++++ garden-cli/test/src/build-dir.ts | 4 +- garden-cli/test/src/commands/build.ts | 4 - garden-cli/test/src/commands/call.ts | 22 +- garden-cli/test/src/commands/create/module.ts | 28 +- .../test/src/commands/create/project.ts | 44 +- garden-cli/test/src/commands/delete.ts | 53 +- garden-cli/test/src/commands/deploy.ts | 10 +- garden-cli/test/src/commands/get.ts | 30 +- garden-cli/test/src/commands/link.ts | 17 +- garden-cli/test/src/commands/login.ts | 10 +- garden-cli/test/src/commands/logout.ts | 10 +- garden-cli/test/src/commands/push.ts | 46 +- garden-cli/test/src/commands/run/module.ts | 10 +- garden-cli/test/src/commands/run/service.ts | 3 - garden-cli/test/src/commands/scan.ts | 3 +- garden-cli/test/src/commands/set.ts | 27 +- garden-cli/test/src/commands/test.ts | 4 - garden-cli/test/src/commands/unlink.ts | 22 +- garden-cli/test/src/commands/update-remote.ts | 26 +- garden-cli/test/src/commands/validate.ts | 6 +- garden-cli/test/src/config/config-context.ts | 6 +- garden-cli/test/src/garden.ts | 70 +-- garden-cli/test/src/plugin-context.ts | 89 --- garden-cli/test/src/plugins/container.ts | 43 +- garden-cli/test/src/plugins/generic.ts | 19 +- .../test/src/plugins/kubernetes/ingress.ts | 7 +- garden-cli/test/src/task-graph.ts | 59 +- garden-cli/test/src/tasks/test.ts | 25 +- garden-cli/test/src/util/ext-source-util.ts | 86 +-- garden-cli/test/src/vcs/base.ts | 24 +- garden-cli/test/src/watch.ts | 4 +- 103 files changed, 2780 insertions(+), 2272 deletions(-) create mode 100644 garden-cli/src/actions.ts create mode 100644 garden-cli/test/src/actions.ts delete mode 100644 garden-cli/test/src/plugin-context.ts diff --git a/docs/reference/commands.md b/docs/reference/commands.md index f5560cc475..240c5abc42 100644 --- a/docs/reference/commands.md +++ b/docs/reference/commands.md @@ -135,28 +135,29 @@ Examples: | Argument | Alias | Type | Description | | -------- | ----- | ---- | ----------- | - | `--name` | | boolean | Assigns a custom name to the module. (Defaults to name of the current directory.) + | `--name` | | string | Assigns a custom name to the module. (Defaults to name of the current directory.) | `--type` | | `container` `google-cloud-function` `npm-package` | Type of module. -### garden delete config +### garden delete secret -Delete a configuration variable from the environment. +Delete a secret from the environment. -Returns with an error if the provided key could not be found in the configuration. +Returns with an error if the provided key could not be found by the provider. Examples: - garden delete config somekey - garden del config some.nested.key + garden delete secret kubernetes somekey + garden del secret local-kubernetes some-other-key ##### Usage - garden delete config + garden delete secret ##### Arguments | Argument | Required | Description | | -------- | -------- | ----------- | + | `provider` | Yes | The name of the provider to remove the secret from. | `key` | Yes | The key of the configuration variable. Separate with dots to get a nested key (e.g. key.nested). ### garden delete environment @@ -275,26 +276,27 @@ Examples: | `service` | Yes | The service to exec the command in. | `command` | Yes | The command to run. -### garden get config +### garden get secret -Get a configuration variable from the environment. +Get a secret from the environment. -Returns with an error if the provided key could not be found in the configuration. +Returns with an error if the provided key could not be found. Examples: - garden get config somekey - garden get config some.nested.key + garden get secret kubernetes somekey + garden get secret local-kubernetes some-other-key ##### Usage - garden get config + garden get secret ##### Arguments | Argument | Required | Description | | -------- | -------- | ----------- | - | `key` | Yes | The key of the configuration variable. Separate with dots to get a nested key (e.g. key.nested). + | `provider` | Yes | The name of the provider to read the secret from. + | `key` | Yes | The key of the configuration variable. ### garden get status @@ -553,29 +555,32 @@ Scans your project and outputs an overview of all modules. garden scan -### garden set config +### garden set secret -Set a configuration variable in the environment. +Set a secret value for a provider in an environment. -These configuration values can be referenced in module templates, for example as environment variables. +These secrets are handled by each provider, and may for example be exposed as environment +variables for services or mounted as files, depending on how the provider is implemented +and configured. -_Note: The value is always stored as a string._ +_Note: The value is currently always stored as a string._ Examples: - garden set config somekey myvalue - garden set config some.nested.key myvalue + garden set secret kubernetes somekey myvalue + garden set secret local-kubernets somekey myvalue ##### Usage - garden set config + garden set secret ##### Arguments | Argument | Required | Description | | -------- | -------- | ----------- | - | `key` | Yes | The key of the configuration variable. Separate with dots to get a nested key (e.g. key.nested). - | `value` | Yes | The value of the configuration variable. + | `provider` | Yes | The name of the provider to store the secret with. + | `key` | Yes | A unique identifier for the secret. + | `value` | Yes | The value of the secret. ### garden test diff --git a/docs/reference/config.md b/docs/reference/config.md index d8404f8706..4a839c5175 100644 --- a/docs/reference/config.md +++ b/docs/reference/config.md @@ -127,12 +127,6 @@ project: # # Optional. environmentDefaults: - # Specify the provider that should store configuration variables for this environment. Use - # this when you configure multiple providers that can manage configuration. - # - # Optional. - configurationHandler: - # A list of providers that should be used for this environment, and their configuration. # Please refer to individual plugins/providers for details on how to configure them. # @@ -161,13 +155,7 @@ project: # # Optional. environments: - - # Specify the provider that should store configuration variables for this environment. Use - # this when you configure multiple providers that can manage configuration. - # - # Optional. - configurationHandler: - - # A list of providers that should be used for this environment, and their configuration. + - # A list of providers that should be used for this environment, and their configuration. # Please refer to individual plugins/providers for details on how to configure them. # # Optional. diff --git a/examples/hello-world/garden.yml b/examples/hello-world/garden.yml index 60d3074810..e090b1db79 100644 --- a/examples/hello-world/garden.yml +++ b/examples/hello-world/garden.yml @@ -1,6 +1,8 @@ project: name: hello-world environmentDefaults: + providers: + - name: npm-package variables: my-variable: hello-variable environments: diff --git a/garden-cli/src/actions.ts b/garden-cli/src/actions.ts new file mode 100644 index 0000000000..7e8a0d493d --- /dev/null +++ b/garden-cli/src/actions.ts @@ -0,0 +1,417 @@ +/* + * Copyright (C) 2018 Garden Technologies, Inc. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +import Bluebird = require("bluebird") +import chalk from "chalk" +import { Garden } from "./garden" +import { PrimitiveMap } from "./config/common" +import { Module } from "./types/module" +import { ModuleActions, ServiceActions, PluginActions } from "./types/plugin/plugin" +import { + BuildResult, + BuildStatus, + DeleteSecretResult, + EnvironmentStatusMap, + ExecInServiceResult, + GetSecretResult, + GetServiceLogsResult, + LoginStatusMap, + ModuleActionOutputs, + PushResult, + RunResult, + ServiceActionOutputs, + SetSecretResult, + TestResult, + PluginActionOutputs, +} from "./types/plugin/outputs" +import { + BuildModuleParams, + DeleteSecretParams, + DeployServiceParams, + DeleteServiceParams, + ExecInServiceParams, + GetSecretParams, + GetBuildStatusParams, + GetServiceLogsParams, + GetServiceOutputsParams, + GetServiceStatusParams, + GetTestResultParams, + ModuleActionParams, + PluginActionContextParams, + PluginActionParams, + PluginActionParamsBase, + PluginServiceActionParamsBase, + PushModuleParams, + RunModuleParams, + RunServiceParams, + ServiceActionParams, + SetSecretParams, + TestModuleParams, + GetLoginStatusParams, + LoginParams, + LogoutParams, + GetEnvironmentStatusParams, + PluginModuleActionParamsBase, +} from "./types/plugin/params" +import { + Service, + ServiceStatus, + prepareRuntimeContext, +} from "./types/service" +import { mapValues, values, keyBy, omit } from "lodash" +import { Omit } from "./util/util" +import { RuntimeContext } from "./types/service" +import { processServices, ProcessResults } from "./process" +import { getDeployTasks } from "./tasks/deploy" +import { LogEntry } from "./logger/log-entry" +import { createPluginContext } from "./plugin-context" +import { CleanupEnvironmentParams } from "./types/plugin/params" + +type TypeGuard = { + readonly [P in keyof (PluginActionParams | ModuleActionParams)]: (...args: any[]) => Promise +} + +export interface ContextStatus { + providers: EnvironmentStatusMap + services: { [name: string]: ServiceStatus } +} + +export interface DeployServicesParams { + serviceNames?: string[], + force?: boolean + forceBuild?: boolean +} + +// avoid having to specify common params on each action helper call +type ActionHelperParams = + Omit & { pluginName?: string } +type ModuleActionHelperParams = + Omit & { pluginName?: string } +// additionally make runtimeContext param optional +type ServiceActionHelperParams = + Omit + & { runtimeContext?: RuntimeContext, pluginName?: string } + +type RequirePluginName = T & { pluginName: string } + +export class ActionHelper implements TypeGuard { + constructor(private garden: Garden) { } + + //=========================================================================== + //region Environment Actions + //=========================================================================== + + async getEnvironmentStatus( + { pluginName }: ActionHelperParams, + ): Promise { + const handlers = this.garden.getActionHandlers("getEnvironmentStatus", pluginName) + return Bluebird.props(mapValues(handlers, h => h({ ...this.commonParams(h) }))) + } + + async prepareEnvironment( + { force = false, pluginName, logEntry }: + { force?: boolean, pluginName?: string, logEntry?: LogEntry }, + ): Promise { + const handlers = this.garden.getActionHandlers("prepareEnvironment", pluginName) + + const statuses = await this.getEnvironmentStatus({}) + + const result = await Bluebird.props(mapValues(handlers, async (handler, name) => { + const status = statuses[name] || { ready: false } + + if (status.ready && !force) { + return status + } + + const envLogEntry = (logEntry || this.garden.log).info({ + status: "active", + section: name, + msg: "Configuring...", + }) + + const res = await handler({ ...this.commonParams(handler), force, status, logEntry: envLogEntry }) + + envLogEntry.setSuccess("Configured") + + return res + })) + + return result + } + + async cleanupEnvironment( + { pluginName }: ActionHelperParams, + ): Promise { + const handlers = this.garden.getActionHandlers("cleanupEnvironment", pluginName) + await Bluebird.each(values(handlers), h => h({ ...this.commonParams(h) })) + return this.getEnvironmentStatus({ pluginName }) + } + + async login({ pluginName }: ActionHelperParams): Promise { + const handlers = this.garden.getActionHandlers("login", pluginName) + await Bluebird.each(values(handlers), h => h({ ...this.commonParams(h) })) + return this.getLoginStatus({ pluginName }) + } + + async logout({ pluginName }: ActionHelperParams): Promise { + const handlers = this.garden.getActionHandlers("logout", pluginName) + await Bluebird.each(values(handlers), h => h({ ...this.commonParams(h) })) + return this.getLoginStatus({ pluginName }) + } + + async getLoginStatus({ pluginName }: ActionHelperParams): Promise { + const handlers = this.garden.getActionHandlers("getLoginStatus", pluginName) + return Bluebird.props(mapValues(handlers, h => h({ ...this.commonParams(h) }))) + } + + async getSecret(params: RequirePluginName>): Promise { + const { pluginName } = params + return this.callActionHandler({ actionType: "getSecret", pluginName, params: omit(params, ["pluginName"]) }) + } + + async setSecret(params: RequirePluginName>): Promise { + const { pluginName } = params + return this.callActionHandler({ actionType: "setSecret", pluginName, params: omit(params, ["pluginName"]) }) + } + + async deleteSecret(params: RequirePluginName>): Promise { + const { pluginName } = params + return this.callActionHandler({ actionType: "deleteSecret", pluginName, params: omit(params, ["pluginName"]) }) + } + + //endregion + + //=========================================================================== + //region Module Actions + //=========================================================================== + + async getBuildStatus( + params: ModuleActionHelperParams>, + ): Promise { + return this.callModuleHandler({ + params, + actionType: "getBuildStatus", + defaultHandler: async () => ({ ready: false }), + }) + } + + async build(params: ModuleActionHelperParams>): Promise { + await this.garden.buildDir.syncDependencyProducts(params.module) + return this.callModuleHandler({ params, actionType: "build" }) + } + + async pushModule(params: ModuleActionHelperParams>): Promise { + return this.callModuleHandler({ params, actionType: "pushModule", defaultHandler: dummyPushHandler }) + } + + async runModule(params: ModuleActionHelperParams>): Promise { + return this.callModuleHandler({ params, actionType: "runModule" }) + } + + async testModule(params: ModuleActionHelperParams>): Promise { + return this.callModuleHandler({ params, actionType: "testModule" }) + } + + async getTestResult( + params: ModuleActionHelperParams>, + ): Promise { + return this.callModuleHandler({ + params, + actionType: "getTestResult", + defaultHandler: async () => null, + }) + } + + //endregion + + //=========================================================================== + //region Service Actions + //=========================================================================== + + async getServiceStatus(params: ServiceActionHelperParams): Promise { + return this.callServiceHandler({ params, actionType: "getServiceStatus" }) + } + + async deployService(params: ServiceActionHelperParams): Promise { + return this.callServiceHandler({ params, actionType: "deployService" }) + } + + async deleteService(params: ServiceActionHelperParams): Promise { + const logEntry = this.garden.log.info({ + section: params.service.name, + msg: "Deleting...", + status: "active", + }) + return this.callServiceHandler({ + params: { ...params, logEntry }, + actionType: "deleteService", + defaultHandler: dummyDeleteServiceHandler, + }) + } + + async getServiceOutputs(params: ServiceActionHelperParams): Promise { + return this.callServiceHandler({ + params, + actionType: "getServiceOutputs", + defaultHandler: async () => ({}), + }) + } + + async execInService(params: ServiceActionHelperParams): Promise { + return this.callServiceHandler({ params, actionType: "execInService" }) + } + + async getServiceLogs(params: ServiceActionHelperParams): Promise { + return this.callServiceHandler({ params, actionType: "getServiceLogs", defaultHandler: dummyLogStreamer }) + } + + async runService(params: ServiceActionHelperParams): Promise { + return this.callServiceHandler({ params, actionType: "runService" }) + } + + //endregion + + //=========================================================================== + //region Helper Methods + //=========================================================================== + + async getStatus(): Promise { + const envStatus: EnvironmentStatusMap = await this.getEnvironmentStatus({}) + const services = keyBy(await this.garden.getServices(), "name") + + const serviceStatus = await Bluebird.props(mapValues(services, async (service: Service) => { + const dependencies = await this.garden.getServices(service.config.dependencies) + const runtimeContext = await prepareRuntimeContext(this.garden, service.module, dependencies) + return this.getServiceStatus({ service, runtimeContext }) + })) + + return { + providers: envStatus, + services: serviceStatus, + } + } + + async deployServices( + { serviceNames, force = false, forceBuild = false }: DeployServicesParams, + ): Promise { + const services = await this.garden.getServices(serviceNames) + + return processServices({ + services, + garden: this.garden, + watch: false, + handler: async (module) => getDeployTasks({ + garden: this.garden, + module, + serviceNames, + force, + forceBuild, + includeDependants: false, + }), + }) + } + + //endregion + + // TODO: find a nicer way to do this (like a type-safe wrapper function) + private commonParams(handler, logEntry?: LogEntry): PluginActionParamsBase { + return { + ctx: createPluginContext(this.garden, handler["pluginName"]), + // TODO: find a better way for handlers to log during execution + logEntry, + } + } + + private async callActionHandler( + { params, actionType, pluginName, defaultHandler }: + { + params: ActionHelperParams, + actionType: T, + pluginName?: string, + defaultHandler?: PluginActions[T], + }, + ): Promise { + const handler = this.garden.getActionHandler({ + actionType, + pluginName, + defaultHandler, + }) + const handlerParams: PluginActionParams[T] = { + ...this.commonParams(handler), + ...params, + } + return (handler)(handlerParams) + } + + private async callModuleHandler>( + { params, actionType, defaultHandler }: + { params: ModuleActionHelperParams, actionType: T, defaultHandler?: ModuleActions[T] }, + ): Promise { + // the type system is messing me up here, not sure why I need the any cast... - j.e. + const { module, pluginName } = params + const handler = await this.garden.getModuleActionHandler({ + moduleType: module.type, + actionType, + pluginName, + defaultHandler, + }) + const handlerParams: any = { + ...this.commonParams(handler), + ...omit(params, ["module"]), + module: omit(module, ["_ConfigType"]), + } + // TODO: figure out why this doesn't compile without the function cast + return (handler)(handlerParams) + } + + private async callServiceHandler( + { params, actionType, defaultHandler }: + { params: ServiceActionHelperParams, actionType: T, defaultHandler?: ServiceActions[T] }, + ): Promise { + const { service } = params + const module = service.module + + const handler = await this.garden.getModuleActionHandler({ + moduleType: module.type, + actionType, + pluginName: params.pluginName, + defaultHandler, + }) + + // TODO: figure out why this doesn't compile without the casts + const deps = await this.garden.getServices(service.config.dependencies) + const runtimeContext = ((params).runtimeContext || await prepareRuntimeContext(this.garden, module, deps)) + + const handlerParams: any = { + ...this.commonParams(handler), + ...params, + module, + runtimeContext, + } + + return (handler)(handlerParams) + } +} + +const dummyLogStreamer = async ({ service, logEntry }: GetServiceLogsParams) => { + logEntry && logEntry.warn({ + section: service.name, + msg: chalk.yellow(`No handler for log retrieval available for module type ${service.module.type}`), + }) + return {} +} + +const dummyPushHandler = async ({ module }: PushModuleParams) => { + return { pushed: false, message: chalk.yellow(`No push handler available for module type ${module.type}`) } +} + +const dummyDeleteServiceHandler = async ({ module, logEntry }: DeleteServiceParams) => { + const msg = `No delete service handler available for module type ${module.type}` + logEntry && logEntry.setError(msg) + return {} +} diff --git a/garden-cli/src/cli/cli.ts b/garden-cli/src/cli/cli.ts index 8d5fe30ca6..f8df74fcd3 100644 --- a/garden-cli/src/cli/cli.ts +++ b/garden-cli/src/cli/cli.ts @@ -234,10 +234,9 @@ export class GardenCli { garden = await Garden.factory(root, contextOpts) // TODO: enforce that commands always output DeepPrimitiveMap result = await command.action({ - ctx: garden.getPluginContext(), + garden, args: parsedArgs, opts: parsedOpts, - garden, }) } while (result.restartRequired) diff --git a/garden-cli/src/commands/base.ts b/garden-cli/src/commands/base.ts index 624745d5ed..e10bebb9ad 100644 --- a/garden-cli/src/commands/base.ts +++ b/garden-cli/src/commands/base.ts @@ -10,7 +10,6 @@ import { GardenError, RuntimeError, } from "../exceptions" -import { PluginContext } from "../plugin-context" import { TaskResults } from "../task-graph" import { LoggerType } from "../logger/logger" import { ProcessResults } from "../process" @@ -70,7 +69,17 @@ export class StringParameter extends Parameter { } } -export class StringsParameter extends Parameter { +// Separating this from StringParameter for now because we can't set the output type based on the required flag +// FIXME: Maybe use a Required type to enforce presence, rather that an option flag? +export class StringOption extends Parameter { + type = "string" + + validate(input?: string) { + return input + } +} + +export class StringsParameter extends Parameter { type = "array:string" // Sywac returns [undefined] if input is empty so we coerce that into undefined. @@ -163,7 +172,7 @@ export class EnvironmentOption extends StringParameter { } export type Parameters = { [key: string]: Parameter } -export type ParameterValues = { [P in keyof T]: T["_valueType"] } +export type ParameterValues = { [P in keyof T]: T[P]["_valueType"] } export interface CommandConstructor { new(parent?: Command): Command @@ -176,9 +185,8 @@ export interface CommandResult { } export interface CommandParams { - ctx: PluginContext - args: T - opts: U + args: ParameterValues + opts: ParameterValues garden: Garden } @@ -224,7 +232,7 @@ export abstract class Command> { const failed = Object.values(results.taskResults).filter(r => !!r.error).length @@ -235,9 +243,9 @@ export async function handleTaskResults( return { errors: [error] } } - ctx.log.info("") + garden.log.info("") if (!results.restartRequired) { - ctx.log.header({ emoji: "heavy_check_mark", command: `Done!` }) + garden.log.header({ emoji: "heavy_check_mark", command: `Done!` }) } return { result: results.taskResults, diff --git a/garden-cli/src/commands/build.ts b/garden-cli/src/commands/build.ts index b6fb333882..a4f9c5cc0e 100644 --- a/garden-cli/src/commands/build.ts +++ b/garden-cli/src/commands/build.ts @@ -12,7 +12,6 @@ import { CommandResult, CommandParams, handleTaskResults, - ParameterValues, StringsParameter, } from "./base" import { BuildTask } from "../tasks/build" @@ -22,21 +21,21 @@ import { processModules } from "../process" import { computeAutoReloadDependants, withDependants } from "../watch" import { Module } from "../types/module" -export const buildArguments = { +const buildArguments = { module: new StringsParameter({ help: "Specify module(s) to build. Use comma separator to specify multiple modules.", }), } -export const buildOptions = { +const buildOptions = { force: new BooleanParameter({ help: "Force rebuild of module(s)." }), watch: new BooleanParameter({ help: "Watch for changes in module(s) and auto-build.", alias: "w" }), } -export type BuildArguments = ParameterValues -export type BuildOptions = ParameterValues +type BuildArguments = typeof buildArguments +type BuildOptions = typeof buildOptions -export class BuildCommand extends Command { +export class BuildCommand extends Command { name = "build" help = "Build your modules." @@ -56,30 +55,29 @@ export class BuildCommand extends Command, + { args, opts, garden }: CommandParams, ): Promise> { await garden.clearBuilds() - const autoReloadDependants = await computeAutoReloadDependants(ctx) - const modules = await ctx.getModules(args.module) + const autoReloadDependants = await computeAutoReloadDependants(garden) + const modules = await garden.getModules(args.module) const moduleNames = modules.map(m => m.name) - ctx.log.header({ emoji: "hammer", command: "Build" }) + garden.log.header({ emoji: "hammer", command: "Build" }) const results = await processModules({ - ctx, garden, modules, watch: opts.watch, - handler: async (module) => [new BuildTask({ ctx, module, force: opts.force })], + handler: async (module) => [new BuildTask({ garden, module, force: opts.force })], changeHandler: async (module: Module) => { - return (await withDependants(ctx, [module], autoReloadDependants)) + return (await withDependants(garden, [module], autoReloadDependants)) .filter(m => moduleNames.includes(m.name)) - .map(m => new BuildTask({ ctx, module: m, force: true })) + .map(m => new BuildTask({ garden, module: m, force: true })) }, }) - return handleTaskResults(ctx, "build", results) + return handleTaskResults(garden, "build", results) } } diff --git a/garden-cli/src/commands/call.ts b/garden-cli/src/commands/call.ts index a7c2e1e7af..d2885684f3 100644 --- a/garden-cli/src/commands/call.ts +++ b/garden-cli/src/commands/call.ts @@ -14,7 +14,6 @@ import { Command, CommandResult, CommandParams, - ParameterValues, StringParameter, } from "./base" import { splitFirst } from "../util/util" @@ -23,16 +22,16 @@ import { pick, find } from "lodash" import { ServiceEndpoint, getEndpointUrl } from "../types/service" import dedent = require("dedent") -export const callArgs = { +const callArgs = { serviceAndPath: new StringParameter({ help: "The name of the service(s) to call followed by the endpoint path (e.g. my-container/somepath).", required: true, }), } -export type Args = ParameterValues +type Args = typeof callArgs -export class CallCommand extends Command { +export class CallCommand extends Command { name = "call" help = "Call a service endpoint." @@ -50,12 +49,12 @@ export class CallCommand extends Command { arguments = callArgs - async action({ ctx, args }: CommandParams): Promise { + async action({ garden, args }: CommandParams): Promise { let [serviceName, path] = splitFirst(args.serviceAndPath, "/") // TODO: better error when service doesn't exist - const service = await ctx.getService(serviceName) - const status = await ctx.getServiceStatus({ serviceName }) + const service = await garden.getService(serviceName) + const status = await garden.actions.getServiceStatus({ service }) if (status.state !== "ready") { throw new RuntimeError(`Service ${service.name} is not running`, { @@ -120,7 +119,7 @@ export class CallCommand extends Command { // TODO: support POST requests with request body const method = "get" - const entry = ctx.log.info({ + const entry = garden.log.info({ msg: chalk.cyan(`Sending ${matchedEndpoint.protocol.toUpperCase()} GET request to `) + url + "\n", status: "active", }) @@ -142,18 +141,18 @@ export class CallCommand extends Command { try { res = await req entry.setSuccess() - ctx.log.info(chalk.green(`${res.status} ${res.statusText}\n`)) + garden.log.info(chalk.green(`${res.status} ${res.statusText}\n`)) } catch (err) { res = err.response entry.setError() const error = res ? `${res.status} ${res.statusText}` : err.message - ctx.log.info(chalk.red(error + "\n")) + garden.log.info(chalk.red(error + "\n")) return {} } const resStr = isObject(res.data) ? JSON.stringify(res.data, null, 2) : res.data - res.data && ctx.log.info(chalk.white(resStr) + "\n") + res.data && garden.log.info(chalk.white(resStr) + "\n") return { result: { diff --git a/garden-cli/src/commands/create/module.ts b/garden-cli/src/commands/create/module.ts index ac62cefd9d..8e3246b7ae 100644 --- a/garden-cli/src/commands/create/module.ts +++ b/garden-cli/src/commands/create/module.ts @@ -13,8 +13,6 @@ import { Command, CommandResult, StringParameter, - ParameterValues, - BooleanParameter, ChoicesParameter, CommandParams, } from "../base" @@ -28,8 +26,8 @@ import { prompts } from "./prompts" import { validate, joiIdentifier } from "../../config/common" import { ensureDir } from "fs-extra" -export const createModuleOptions = { - name: new BooleanParameter({ +const createModuleOptions = { + name: new StringParameter({ help: "Assigns a custom name to the module. (Defaults to name of the current directory.)", }), type: new ChoicesParameter({ @@ -38,14 +36,14 @@ export const createModuleOptions = { }), } -export const createModuleArguments = { +const createModuleArguments = { "module-dir": new StringParameter({ help: "Directory of the module. (Defaults to current directory.)", }), } -export type Args = ParameterValues -export type Opts = ParameterValues +type Args = typeof createModuleArguments +type Opts = typeof createModuleOptions interface CreateModuleResult extends CommandResult { result: { @@ -53,7 +51,7 @@ interface CreateModuleResult extends CommandResult { } } -export class CreateModuleCommand extends Command { +export class CreateModuleCommand extends Command { name = "module" alias = "m" help = "Creates a new Garden module." @@ -73,10 +71,10 @@ export class CreateModuleCommand extends Command): Promise { + async action({ garden, args, opts }: CommandParams): Promise { let errors: GardenBaseError[] = [] - const moduleRoot = join(ctx.projectRoot, (args["module-dir"] || "").trim()) + const moduleRoot = join(garden.projectRoot, (args["module-dir"] || "").trim()) const moduleName = validate( opts.name || basename(moduleRoot), joiIdentifier(), @@ -85,23 +83,23 @@ export class CreateModuleCommand extends Commandopts.type if (!availableModuleTypes.includes(type)) { throw new ParameterError("Module type not available", {}) } } else { // Prompt for type - ctx.log.info("---------") - ctx.log.stop() + garden.log.info("---------") + garden.log.stop() type = (await prompts.addConfigForModule(moduleName)).type - ctx.log.info("---------") + garden.log.info("---------") if (!type) { return { result: {} } } @@ -109,7 +107,7 @@ export class CreateModuleCommand extends Command -export type Opts = ParameterValues +type Args = typeof createProjectArguments +type Opts = typeof createProjectOptions const flatten = (acc, val) => acc.concat(val) @@ -63,7 +62,7 @@ interface CreateProjectResult extends CommandResult { } } -export class CreateProjectCommand extends Command { +export class CreateProjectCommand extends Command { name = "project" alias = "p" help = "Creates a new Garden project." @@ -87,11 +86,11 @@ export class CreateProjectCommand extends Command): Promise { + async action({ garden, args, opts }: CommandParams): Promise { let moduleConfigs: ModuleConfigOpts[] = [] let errors: GardenBaseError[] = [] - const projectRoot = args["project-dir"] ? join(ctx.projectRoot, args["project-dir"].trim()) : ctx.projectRoot + const projectRoot = args["project-dir"] ? join(garden.projectRoot, args["project-dir"].trim()) : garden.projectRoot const moduleParentDirs = await Bluebird.map(opts["module-dirs"] || [], (dir: string) => resolve(projectRoot, dir)) const projectName = validate( opts.name || basename(projectRoot), @@ -101,11 +100,11 @@ export class CreateProjectCommand extends Command 0) { // If module-dirs option provided we scan for modules in the parent dir(s) and add them one by one @@ -128,13 +127,13 @@ export class CreateProjectCommand extends Command prepareNewModuleConfig(name, type, join(projectRoot, name))) } - ctx.log.info("---------") - const task = ctx.log.info({ msg: "Setting up project", status: "active" }) + garden.log.info("---------") + const taskLog = garden.log.info({ msg: "Setting up project", status: "active" }) for (const module of moduleConfigs) { await ensureDir(module.path) try { - await dumpConfig(module, moduleSchema, ctx.log) + await dumpConfig(module, moduleSchema, garden.log) } catch (err) { errors.push(err) } @@ -147,19 +146,19 @@ export class CreateProjectCommand extends Command - -// TODO: add --all option to remove all configs +type DeleteSecretArgs = typeof deleteSecretArgs -export class DeleteConfigCommand extends Command { - name = "config" - help = "Delete a configuration variable from the environment." +export class DeleteSecretCommand extends Command { + name = "secret" + help = "Delete a secret from the environment." description = dedent` - Returns with an error if the provided key could not be found in the configuration. + Returns with an error if the provided key could not be found by the provider. Examples: - garden delete config somekey - garden del config some.nested.key + garden delete secret kubernetes somekey + garden del secret local-kubernetes some-other-key ` - arguments = deleteConfigArgs + arguments = deleteSecretArgs - async action({ ctx, args }: CommandParams): Promise> { - const key = args.key.split(".") - const result = await ctx.deleteConfig({ key }) + async action({ garden, args }: CommandParams): Promise> { + const key = args.key! + const result = await garden.actions.deleteSecret({ pluginName: args.provider!, key }) if (result.found) { - ctx.log.info(`Deleted config key ${args.key}`) + garden.log.info(`Deleted config key ${args.key}`) } else { throw new NotFoundError(`Could not find config key ${args.key}`, { key }) } @@ -90,25 +91,25 @@ export class DeleteEnvironmentCommand extends Command { resources. ` - async action({ ctx }: CommandParams): Promise> { - const { name } = ctx.getEnvironment() - ctx.log.header({ emoji: "skull_and_crossbones", command: `Deleting ${name} environment` }) + async action({ garden }: CommandParams): Promise> { + const { name } = garden.environment + garden.log.header({ emoji: "skull_and_crossbones", command: `Deleting ${name} environment` }) - const result = await ctx.destroyEnvironment({}) + const result = await garden.actions.cleanupEnvironment({}) - ctx.log.finish() + garden.log.finish() return { result } } } -export const deleteServiceArgs = { +const deleteServiceArgs = { service: new StringsParameter({ help: "The name of the service(s) to delete. Use comma as separator to specify multiple services.", required: true, }), } -export type DeleteServiceArgs = ParameterValues +type DeleteServiceArgs = typeof deleteServiceArgs export class DeleteServiceCommand extends Command { name = "service" @@ -125,23 +126,23 @@ export class DeleteServiceCommand extends Command { garden delete service my-service # deletes my-service ` - async action({ ctx, args }: CommandParams): Promise { - const services = await ctx.getServices(args.service) + async action({ garden, args }: CommandParams): Promise { + const services = await garden.getServices(args.service) if (services.length === 0) { - ctx.log.warn({ msg: "No services found. Aborting." }) + garden.log.warn({ msg: "No services found. Aborting." }) return { result: {} } } - ctx.log.header({ emoji: "skull_and_crossbones", command: `Delete service` }) + garden.log.header({ emoji: "skull_and_crossbones", command: `Delete service` }) const result: { [key: string]: ServiceStatus } = {} await Bluebird.map(services, async service => { - result[service.name] = await ctx.deleteService({ serviceName: service.name }) + result[service.name] = await garden.actions.deleteService({ service }) }) - ctx.log.finish() + garden.log.finish() return { result } } } diff --git a/garden-cli/src/commands/deploy.ts b/garden-cli/src/commands/deploy.ts index 8243697f45..177c342a06 100644 --- a/garden-cli/src/commands/deploy.ts +++ b/garden-cli/src/commands/deploy.ts @@ -12,7 +12,6 @@ import { CommandParams, CommandResult, handleTaskResults, - ParameterValues, StringsParameter, } from "./base" import { getDeployTasks } from "../tasks/deploy" @@ -20,23 +19,23 @@ import { TaskResults } from "../task-graph" import { processServices } from "../process" import { getNames } from "../util/util" -export const deployArgs = { +const deployArgs = { service: new StringsParameter({ help: "The name of the service(s) to deploy (skip to deploy all services). " + "Use comma as separator to specify multiple services.", }), } -export const deployOpts = { +const deployOpts = { force: new BooleanParameter({ help: "Force redeploy of service(s)." }), "force-build": new BooleanParameter({ help: "Force rebuild of module(s)." }), watch: new BooleanParameter({ help: "Watch for changes in module(s) and auto-deploy.", alias: "w" }), } -export type Args = ParameterValues -export type Opts = ParameterValues +type Args = typeof deployArgs +type Opts = typeof deployOpts -export class DeployCommand extends Command { +export class DeployCommand extends Command { name = "deploy" help = "Deploy service(s) to your environment." @@ -59,34 +58,42 @@ export class DeployCommand extends Command arguments = deployArgs options = deployOpts - async action({ garden, ctx, args, opts }: CommandParams): Promise> { - - const services = await ctx.getServices(args.service) + async action({ garden, args, opts }: CommandParams): Promise> { + const services = await garden.getServices(args.service) const serviceNames = getNames(services) if (services.length === 0) { - ctx.log.warn({ msg: "No services found. Aborting." }) + garden.log.warn({ msg: "No services found. Aborting." }) return { result: {} } } - ctx.log.header({ emoji: "rocket", command: "Deploy" }) + garden.log.header({ emoji: "rocket", command: "Deploy" }) // TODO: make this a task - await ctx.configureEnvironment({}) + await garden.actions.prepareEnvironment({}) const results = await processServices({ - ctx, garden, services, watch: opts.watch, handler: async (module) => getDeployTasks({ - ctx, module, serviceNames, force: opts.force, forceBuild: opts["force-build"], includeDependants: false, + garden, + module, + serviceNames, + force: opts.force, + forceBuild: opts["force-build"], + includeDependants: false, }), changeHandler: async (module) => getDeployTasks({ - ctx, module, serviceNames, force: true, forceBuild: true, includeDependants: true, + garden, + module, + serviceNames, + force: true, + forceBuild: true, + includeDependants: true, }), }) - return handleTaskResults(ctx, "deploy", results) + return handleTaskResults(garden, "deploy", results) } } diff --git a/garden-cli/src/commands/dev.ts b/garden-cli/src/commands/dev.ts index f54acd93ae..9e0eeed4fb 100644 --- a/garden-cli/src/commands/dev.ts +++ b/garden-cli/src/commands/dev.ts @@ -44,23 +44,23 @@ export class DevCommand extends Command { garden dev ` - async action({ garden, ctx }: CommandParams): Promise { + async action({ garden }: CommandParams): Promise { // print ANSI banner image const data = await readFile(ansiBannerPath) console.log(data.toString()) - ctx.log.info(chalk.gray.italic(`\nGood ${getGreetingTime()}! Let's get your environment wired up...\n`)) + garden.log.info(chalk.gray.italic(`\nGood ${getGreetingTime()}! Let's get your environment wired up...\n`)) - await ctx.configureEnvironment({}) + await garden.actions.prepareEnvironment({}) - const autoReloadDependants = await computeAutoReloadDependants(ctx) - const modules = await ctx.getModules() + const autoReloadDependants = await computeAutoReloadDependants(garden) + const modules = await garden.getModules() if (modules.length === 0) { if (modules.length === 0) { - ctx.log.info({ msg: "No modules found in project." }) + garden.log.info({ msg: "No modules found in project." }) } - ctx.log.info({ msg: "Aborting..." }) + garden.log.info({ msg: "Aborting..." }) return {} } @@ -68,19 +68,19 @@ export class DevCommand extends Command { return async (module: Module) => { const testModules: Module[] = watch - ? (await withDependants(ctx, [module], autoReloadDependants)) + ? (await withDependants(garden, [module], autoReloadDependants)) : [module] const testTasks: Task[] = flatten(await Bluebird.map( - testModules, m => getTestTasks({ ctx, module: m }))) + testModules, m => getTestTasks({ garden, module: m }))) const deployTasks = await getDeployTasks({ - ctx, module, force: watch, forceBuild: watch, includeDependants: watch, + garden, module, force: watch, forceBuild: watch, includeDependants: watch, }) const tasks = testTasks.concat(deployTasks) if (tasks.length === 0) { - return [new BuildTask({ ctx, module, force: watch })] + return [new BuildTask({ garden, module, force: watch })] } else { return tasks } @@ -89,7 +89,6 @@ export class DevCommand extends Command { } await processModules({ - ctx, garden, modules, watch: true, diff --git a/garden-cli/src/commands/exec.ts b/garden-cli/src/commands/exec.ts index 56877cf434..8271a53228 100644 --- a/garden-cli/src/commands/exec.ts +++ b/garden-cli/src/commands/exec.ts @@ -13,33 +13,32 @@ import { Command, CommandResult, CommandParams, - ParameterValues, StringParameter, + StringsParameter, } from "./base" import dedent = require("dedent") -export const runArgs = { +const runArgs = { service: new StringParameter({ help: "The service to exec the command in.", required: true, }), - command: new StringParameter({ + command: new StringsParameter({ help: "The command to run.", required: true, }), } -export const runOpts = { +const runOpts = { // interactive: new BooleanParameter({ // help: "Set to false to skip interactive mode and just output the command result", // defaultValue: true, // }), } -export type Args = ParameterValues -// export type Opts = ParameterValues +type Args = typeof runArgs -export class ExecCommand extends Command { +export class ExecCommand extends Command { name = "exec" alias = "e" help = "Executes a command (such as an interactive shell) in a running service." @@ -59,16 +58,17 @@ export class ExecCommand extends Command { options = runOpts loggerType = LoggerType.basic - async action({ ctx, args }: CommandParams): Promise> { + async action({ garden, args }: CommandParams): Promise> { const serviceName = args.service - const command = args.command.split(" ") + const command = args.command || [] - ctx.log.header({ + garden.log.header({ emoji: "runner", - command: `Running command ${chalk.cyan(args.command)} in service ${chalk.cyan(serviceName)}`, + command: `Running command ${chalk.cyan(command.join(" "))} in service ${chalk.cyan(serviceName)}`, }) - const result = await ctx.execInService({ serviceName, command }) + const service = await garden.getService(serviceName) + const result = await garden.actions.execInService({ service, command }) return { result } } diff --git a/garden-cli/src/commands/get.ts b/garden-cli/src/commands/get.ts index e54ad76ec2..c888952a2c 100644 --- a/garden-cli/src/commands/get.ts +++ b/garden-cli/src/commands/get.ts @@ -8,66 +8,69 @@ import * as yaml from "js-yaml" import { NotFoundError } from "../exceptions" -import { ContextStatus } from "../plugin-context" import { highlightYaml } from "../util/util" import { Command, CommandResult, CommandParams, - ParameterValues, StringParameter, } from "./base" import dedent = require("dedent") +import { ContextStatus } from "../actions" export class GetCommand extends Command { name = "get" - help = "Retrieve and output data and objects, e.g. configuration variables, status info etc." + help = "Retrieve and output data and objects, e.g. secrets, status info etc." subCommands = [ - GetConfigCommand, + GetSecretCommand, GetStatusCommand, ] async action() { return {} } } -export const getConfigArgs = { +const getSecretArgs = { + provider: new StringParameter({ + help: "The name of the provider to read the secret from.", + required: true, + }), key: new StringParameter({ - help: "The key of the configuration variable. Separate with dots to get a nested key (e.g. key.nested).", + help: "The key of the configuration variable.", required: true, }), } -export type GetArgs = ParameterValues +type GetArgs = typeof getSecretArgs // TODO: allow omitting key to return all configs -export class GetConfigCommand extends Command { - name = "config" - help = "Get a configuration variable from the environment." +export class GetSecretCommand extends Command { + name = "secret" + help = "Get a secret from the environment." description = dedent` - Returns with an error if the provided key could not be found in the configuration. + Returns with an error if the provided key could not be found. Examples: - garden get config somekey - garden get config some.nested.key + garden get secret kubernetes somekey + garden get secret local-kubernetes some-other-key ` - arguments = getConfigArgs + arguments = getSecretArgs - async action({ ctx, args }: CommandParams): Promise { - const key = args.key.split(".") - const { value } = await ctx.getConfig({ key }) + async action({ garden, args }: CommandParams): Promise { + const key = args.key + const { value } = await garden.actions.getSecret({ pluginName: args.provider, key }) if (value === null || value === undefined) { - throw new NotFoundError(`Could not find config key ${args.key}`, { key }) + throw new NotFoundError(`Could not find config key ${key}`, { key }) } - ctx.log.info(value) + garden.log.info(value) - return { [args.key]: value } + return { [key]: value } } } @@ -75,12 +78,12 @@ export class GetStatusCommand extends Command { name = "status" help = "Outputs the status of your environment." - async action({ ctx }: CommandParams): Promise> { - const status = await ctx.getStatus() + async action({ garden }: CommandParams): Promise> { + const status = await garden.actions.getStatus() const yamlStatus = yaml.safeDump(status, { noRefs: true, skipInvalid: true }) // TODO: do a nicer print of this by default and add --yaml/--json options (maybe globally) for exporting - ctx.log.info(highlightYaml(yamlStatus)) + garden.log.info(highlightYaml(yamlStatus)) return { result: status } } diff --git a/garden-cli/src/commands/init.ts b/garden-cli/src/commands/init.ts index be61a6395c..08f43a9326 100644 --- a/garden-cli/src/commands/init.ts +++ b/garden-cli/src/commands/init.ts @@ -12,7 +12,6 @@ import { Command, CommandResult, CommandParams, - ParameterValues, } from "./base" import dedent = require("dedent") @@ -27,13 +26,13 @@ export class InitCommand extends Command { async action() { return {} } } -export const initEnvOptions = { +const initEnvOptions = { force: new BooleanParameter({ help: "Force initalization of environment, ignoring the environment status check." }), } -export type InitEnvOpts = ParameterValues +type InitEnvOpts = typeof initEnvOptions -export class InitEnvironmentCommand extends Command { +export class InitEnvironmentCommand extends Command<{}, InitEnvOpts> { name = "environment" alias = "env" help = "Initializes your environment." @@ -51,14 +50,14 @@ export class InitEnvironmentCommand extends Command { options = initEnvOptions - async action({ ctx, opts }: CommandParams<{}, InitEnvOpts>): Promise> { - const { name } = ctx.getEnvironment() - ctx.log.header({ emoji: "gear", command: `Initializing ${name} environment` }) + async action({ garden, opts }: CommandParams<{}, InitEnvOpts>): Promise> { + const { name } = garden.environment + garden.log.header({ emoji: "gear", command: `Initializing ${name} environment` }) - const result = await ctx.configureEnvironment({ force: opts.force }) + const result = await garden.actions.prepareEnvironment({ force: opts.force }) - ctx.log.info("") - ctx.log.header({ emoji: "heavy_check_mark", command: `Done!` }) + garden.log.info("") + garden.log.header({ emoji: "heavy_check_mark", command: `Done!` }) return { result } } diff --git a/garden-cli/src/commands/link/module.ts b/garden-cli/src/commands/link/module.ts index 658b8442e4..a576c3d113 100644 --- a/garden-cli/src/commands/link/module.ts +++ b/garden-cli/src/commands/link/module.ts @@ -14,7 +14,6 @@ import { ParameterError } from "../../exceptions" import { Command, CommandResult, - ParameterValues, StringParameter, PathParameter, CommandParams, @@ -27,7 +26,7 @@ import { hasRemoteSource, } from "../../util/ext-source-util" -export const linkModuleArguments = { +const linkModuleArguments = { module: new StringParameter({ help: "Name of the module to link.", required: true, @@ -38,9 +37,9 @@ export const linkModuleArguments = { }), } -export type LinkModuleArguments = ParameterValues +type Args = typeof linkModuleArguments -export class LinkModuleCommand extends Command { +export class LinkModuleCommand extends Command { name = "module" help = "Link a module to a local directory." arguments = linkModuleArguments @@ -55,17 +54,17 @@ export class LinkModuleCommand extends Command { garden link module my-module path/to/my-module # links my-module to its local version at the given path ` - async action({ ctx, args }: CommandParams): Promise> { - ctx.log.header({ emoji: "link", command: "link module" }) + async action({ garden, args }: CommandParams): Promise> { + garden.log.header({ emoji: "link", command: "link module" }) const sourceType = "module" const { module: moduleName, path } = args - const moduleToLink = await ctx.getModule(moduleName) + const moduleToLink = await garden.getModule(moduleName) const isRemote = [moduleToLink].filter(hasRemoteSource)[0] if (!isRemote) { - const modulesWithRemoteSource = (await ctx.getModules()).filter(hasRemoteSource).sort() + const modulesWithRemoteSource = (await garden.getModules()).filter(hasRemoteSource).sort() throw new ParameterError( `Expected module(s) ${chalk.underline(moduleName)} to have a remote source.` + @@ -77,14 +76,14 @@ export class LinkModuleCommand extends Command { ) } - const absPath = resolve(ctx.projectRoot, path) + const absPath = resolve(garden.projectRoot, path) const linkedModuleSources = await addLinkedSources({ - ctx, + garden, sourceType, sources: [{ name: moduleName, path: absPath }], }) - ctx.log.info(`Linked module ${moduleName}`) + garden.log.info(`Linked module ${moduleName}`) return { result: linkedModuleSources } diff --git a/garden-cli/src/commands/link/source.ts b/garden-cli/src/commands/link/source.ts index 5d827309f4..da31980f0d 100644 --- a/garden-cli/src/commands/link/source.ts +++ b/garden-cli/src/commands/link/source.ts @@ -14,7 +14,6 @@ import { ParameterError } from "../../exceptions" import { Command, CommandResult, - ParameterValues, StringParameter, PathParameter, } from "../base" @@ -22,7 +21,7 @@ import { addLinkedSources } from "../../util/ext-source-util" import { LinkedSource } from "../../config-store" import { CommandParams } from "../base" -export const linkSourceArguments = { +const linkSourceArguments = { source: new StringParameter({ help: "Name of the source to link as declared in the project config.", required: true, @@ -33,9 +32,9 @@ export const linkSourceArguments = { }), } -export type LinkSourceArguments = ParameterValues +type Args = typeof linkSourceArguments -export class LinkSourceCommand extends Command { +export class LinkSourceCommand extends Command { name = "source" help = "Link a remote source to a local directory." arguments = linkSourceArguments @@ -50,17 +49,16 @@ export class LinkSourceCommand extends Command { garden link source my-source path/to/my-source # links my-source to its local version at the given path ` - async action({ ctx, args }: CommandParams): Promise> { - - ctx.log.header({ emoji: "link", command: "link source" }) + async action({ garden, args }: CommandParams): Promise> { + garden.log.header({ emoji: "link", command: "link source" }) const sourceType = "project" const { source: sourceName, path } = args - const projectSourceToLink = ctx.projectSources.find(src => src.name === sourceName) + const projectSourceToLink = garden.projectSources.find(src => src.name === sourceName) if (!projectSourceToLink) { - const availableRemoteSources = ctx.projectSources.map(s => s.name).sort() + const availableRemoteSources = garden.projectSources.map(s => s.name).sort() throw new ParameterError( `Remote source ${chalk.underline(sourceName)} not found in project config.` + @@ -72,15 +70,15 @@ export class LinkSourceCommand extends Command { ) } - const absPath = resolve(ctx.projectRoot, path) + const absPath = resolve(garden.projectRoot, path) const linkedProjectSources = await addLinkedSources({ - ctx, + garden, sourceType, sources: [{ name: sourceName, path: absPath }], }) - ctx.log.info(`Linked source ${sourceName}`) + garden.log.info(`Linked source ${sourceName}`) return { result: linkedProjectSources } } diff --git a/garden-cli/src/commands/login.ts b/garden-cli/src/commands/login.ts index abcc81776c..21d3d10bf9 100644 --- a/garden-cli/src/commands/login.ts +++ b/garden-cli/src/commands/login.ts @@ -26,13 +26,13 @@ export class LoginCommand extends Command { garden login ` - async action({ ctx }: CommandParams): Promise> { - ctx.log.header({ emoji: "unlock", command: "Login" }) - ctx.log.info({ msg: "Logging in...", status: "active" }) + async action({ garden }: CommandParams): Promise> { + garden.log.header({ emoji: "unlock", command: "Login" }) + garden.log.info({ msg: "Logging in...", status: "active" }) - const result = await ctx.login({}) + const result = await garden.actions.login({}) - ctx.log.info("\nLogin success!") + garden.log.info("\nLogin success!") return { result } } diff --git a/garden-cli/src/commands/logout.ts b/garden-cli/src/commands/logout.ts index 7c3aa6d3b0..69d8eafc83 100644 --- a/garden-cli/src/commands/logout.ts +++ b/garden-cli/src/commands/logout.ts @@ -24,13 +24,12 @@ export class LogoutCommand extends Command { garden logout ` - async action({ ctx }: CommandParams): Promise> { + async action({ garden }: CommandParams): Promise> { + garden.log.header({ emoji: "lock", command: "Logout" }) - ctx.log.header({ emoji: "lock", command: "Logout" }) + const entry = garden.log.info({ msg: "Logging out...", status: "active" }) - const entry = ctx.log.info({ msg: "Logging out...", status: "active" }) - - const result = await ctx.logout({}) + const result = await garden.actions.logout({}) entry.setSuccess("Logged out successfully") diff --git a/garden-cli/src/commands/logs.ts b/garden-cli/src/commands/logs.ts index f75369b1c3..e5196f3b5b 100644 --- a/garden-cli/src/commands/logs.ts +++ b/garden-cli/src/commands/logs.ts @@ -11,7 +11,6 @@ import { Command, CommandResult, CommandParams, - ParameterValues, StringsParameter, } from "./base" import chalk from "chalk" @@ -22,23 +21,23 @@ import Stream from "ts-stream" import { LoggerType } from "../logger/logger" import dedent = require("dedent") -export const logsArgs = { +const logsArgs = { service: new StringsParameter({ help: "The name of the service(s) to logs (skip to logs all services). " + "Use comma as separator to specify multiple services.", }), } -export const logsOpts = { +const logsOpts = { tail: new BooleanParameter({ help: "Continuously stream new logs from the service(s).", alias: "t" }), // TODO // since: new MomentParameter({ help: "Retrieve logs from the specified point onwards" }), } -export type Args = ParameterValues -export type Opts = ParameterValues +type Args = typeof logsArgs +type Opts = typeof logsOpts -export class LogsCommand extends Command { +export class LogsCommand extends Command { name = "logs" help = "Retrieves the most recent logs for the specified service(s)." @@ -56,9 +55,9 @@ export class LogsCommand extends Command { options = logsOpts loggerType = LoggerType.basic - async action({ ctx, args, opts }: CommandParams): Promise> { + async action({ garden, args, opts }: CommandParams): Promise> { const tail = opts.tail - const services = await ctx.getServices(args.service) + const services = await garden.getServices(args.service) const result: ServiceLogEntry[] = [] const stream = new Stream() @@ -75,7 +74,7 @@ export class LogsCommand extends Command { } catch { } } - ctx.log.info({ + garden.log.info({ section: entry.serviceName, msg: [timestamp, chalk.white(entry.msg)], }) @@ -88,7 +87,7 @@ export class LogsCommand extends Command { // NOTE: This will work differently when we have Elasticsearch set up for logging, but is // quite servicable for now. await Bluebird.map(services, async (service: Service) => { - await ctx.getServiceLogs({ serviceName: service.name, stream, tail }) + await garden.actions.getServiceLogs({ service, stream, tail }) }) return { result } diff --git a/garden-cli/src/commands/push.ts b/garden-cli/src/commands/push.ts index d3bc41450a..5759dde434 100644 --- a/garden-cli/src/commands/push.ts +++ b/garden-cli/src/commands/push.ts @@ -12,10 +12,8 @@ import { CommandParams, CommandResult, handleTaskResults, - ParameterValues, StringsParameter, } from "./base" -import { PluginContext } from "../plugin-context" import { Module } from "../types/module" import { PushTask } from "../tasks/push" import { RuntimeError } from "../exceptions" @@ -23,14 +21,14 @@ import { TaskResults } from "../task-graph" import { Garden } from "../garden" import dedent = require("dedent") -export const pushArgs = { +const pushArgs = { module: new StringsParameter({ help: "The name of the module(s) to push (skip to push all modules). " + "Use comma as separator to specify multiple modules.", }), } -export const pushOpts = { +const pushOpts = { "force-build": new BooleanParameter({ help: "Force rebuild of module(s) before pushing.", }), @@ -39,10 +37,10 @@ export const pushOpts = { }), } -export type Args = ParameterValues -export type Opts = ParameterValues +type Args = typeof pushArgs +type Opts = typeof pushOpts -export class PushCommand extends Command { +export class PushCommand extends Command { name = "push" help = "Build and push built module(s) to remote registry." @@ -61,20 +59,19 @@ export class PushCommand extends Command { arguments = pushArgs options = pushOpts - async action({ garden, ctx, args, opts }: CommandParams): Promise> { - ctx.log.header({ emoji: "rocket", command: "Push modules" }) + async action({ garden, args, opts }: CommandParams): Promise> { + garden.log.header({ emoji: "rocket", command: "Push modules" }) - const modules = await ctx.getModules(args.module) + const modules = await garden.getModules(args.module) - const results = await pushModules(garden, ctx, modules, !!opts["force-build"], !!opts["allow-dirty"]) + const results = await pushModules(garden, modules, !!opts["force-build"], !!opts["allow-dirty"]) - return handleTaskResults(ctx, "push", { taskResults: results }) + return handleTaskResults(garden, "push", { taskResults: results }) } } export async function pushModules( garden: Garden, - ctx: PluginContext, modules: Module[], forceBuild: boolean, allowDirty: boolean, @@ -90,7 +87,7 @@ export async function pushModules( ) } - const task = new PushTask({ ctx, module, forceBuild }) + const task = new PushTask({ garden, module, forceBuild }) await garden.addTask(task) } diff --git a/garden-cli/src/commands/run/module.ts b/garden-cli/src/commands/run/module.ts index b7f3c323f2..a08ebd03af 100644 --- a/garden-cli/src/commands/run/module.ts +++ b/garden-cli/src/commands/run/module.ts @@ -13,9 +13,9 @@ import { BooleanParameter, Command, CommandParams, - ParameterValues, StringParameter, CommandResult, + StringsParameter, } from "../base" import { uniq, @@ -25,18 +25,18 @@ import { printRuntimeContext } from "./run" import dedent = require("dedent") import { prepareRuntimeContext } from "../../types/service" -export const runArgs = { +const runArgs = { module: new StringParameter({ help: "The name of the module to run.", required: true, }), // TODO: make this a variadic arg - command: new StringParameter({ + command: new StringsParameter({ help: "The command to run in the module.", }), } -export const runOpts = { +const runOpts = { // TODO: we could provide specific parameters like this by adding commands for specific modules, via plugins //entrypoint: new StringParameter({ help: "Override default entrypoint in module" }), interactive: new BooleanParameter({ @@ -46,10 +46,10 @@ export const runOpts = { "force-build": new BooleanParameter({ help: "Force rebuild of module before running." }), } -export type Args = ParameterValues -export type Opts = ParameterValues +type Args = typeof runArgs +type Opts = typeof runOpts -export class RunModuleCommand extends Command { +export class RunModuleCommand extends Command { name = "module" alias = "m" help = "Run an ad-hoc instance of a module." @@ -67,39 +67,39 @@ export class RunModuleCommand extends Command { arguments = runArgs options = runOpts - async action({ garden, ctx, args, opts }: CommandParams): Promise> { + async action({ garden, args, opts }: CommandParams): Promise> { const moduleName = args.module - const module = await ctx.getModule(moduleName) + const module = await garden.getModule(moduleName) const msg = args.command - ? `Running command ${chalk.white(args.command)} in module ${chalk.white(moduleName)}` + ? `Running command ${chalk.white(args.command.join(" "))} in module ${chalk.white(moduleName)}` : `Running module ${chalk.white(moduleName)}` - ctx.log.header({ + garden.log.header({ emoji: "runner", command: msg, }) - await ctx.configureEnvironment({}) + await garden.actions.prepareEnvironment({}) - const buildTask = new BuildTask({ ctx, module, force: opts["force-build"] }) + const buildTask = new BuildTask({ garden, module, force: opts["force-build"] }) await garden.addTask(buildTask) await garden.processTasks() - const command = args.command ? args.command.split(" ") : [] + const command = args.command || [] // combine all dependencies for all services in the module, to be sure we have all the context we need const depNames = uniq(flatten(module.serviceConfigs.map(s => s.dependencies))) - const deps = await ctx.getServices(depNames) + const deps = await garden.getServices(depNames) - const runtimeContext = await prepareRuntimeContext(ctx, module, deps) + const runtimeContext = await prepareRuntimeContext(garden, module, deps) - printRuntimeContext(ctx, runtimeContext) + printRuntimeContext(garden, runtimeContext) - ctx.log.info("") + garden.log.info("") - const result = await ctx.runModule({ - moduleName, + const result = await garden.actions.runModule({ + module, command, runtimeContext, silent: false, diff --git a/garden-cli/src/commands/run/run.ts b/garden-cli/src/commands/run/run.ts index 291863c5c8..506b474c7d 100644 --- a/garden-cli/src/commands/run/run.ts +++ b/garden-cli/src/commands/run/run.ts @@ -7,13 +7,13 @@ */ import { safeDump } from "js-yaml" -import { PluginContext } from "../../plugin-context" import { RuntimeContext } from "../../types/service" import { highlightYaml } from "../../util/util" import { Command } from "../base" import { RunModuleCommand } from "./module" import { RunServiceCommand } from "./service" import { RunTestCommand } from "./test" +import { Garden } from "../../garden" export class RunCommand extends Command { name = "run" @@ -29,11 +29,11 @@ export class RunCommand extends Command { async action() { return {} } } -export function printRuntimeContext(ctx: PluginContext, runtimeContext: RuntimeContext) { - ctx.log.verbose("-----------------------------------\n") - ctx.log.verbose("Environment variables:") - ctx.log.verbose(highlightYaml(safeDump(runtimeContext.envVars))) - ctx.log.verbose("Dependencies:") - ctx.log.verbose(highlightYaml(safeDump(runtimeContext.dependencies))) - ctx.log.verbose("-----------------------------------\n") +export function printRuntimeContext(garden: Garden, runtimeContext: RuntimeContext) { + garden.log.verbose("-----------------------------------\n") + garden.log.verbose("Environment variables:") + garden.log.verbose(highlightYaml(safeDump(runtimeContext.envVars))) + garden.log.verbose("Dependencies:") + garden.log.verbose(highlightYaml(safeDump(runtimeContext.dependencies))) + garden.log.verbose("-----------------------------------\n") } diff --git a/garden-cli/src/commands/run/service.ts b/garden-cli/src/commands/run/service.ts index b9bc8a5b6b..e09eaf11b7 100644 --- a/garden-cli/src/commands/run/service.ts +++ b/garden-cli/src/commands/run/service.ts @@ -14,28 +14,27 @@ import { Command, CommandParams, CommandResult, - ParameterValues, StringParameter, } from "../base" import { printRuntimeContext } from "./run" import dedent = require("dedent") import { prepareRuntimeContext } from "../../types/service" -export const runArgs = { +const runArgs = { service: new StringParameter({ help: "The service to run", required: true, }), } -export const runOpts = { +const runOpts = { "force-build": new BooleanParameter({ help: "Force rebuild of module" }), } -export type Args = ParameterValues -export type Opts = ParameterValues +type Args = typeof runArgs +type Opts = typeof runOpts -export class RunServiceCommand extends Command { +export class RunServiceCommand extends Command { name = "service" alias = "s" help = "Run an ad-hoc instance of the specified service" @@ -51,28 +50,28 @@ export class RunServiceCommand extends Command { arguments = runArgs options = runOpts - async action({ garden, ctx, args, opts }: CommandParams): Promise> { + async action({ garden, args, opts }: CommandParams): Promise> { const serviceName = args.service - const service = await ctx.getService(serviceName) + const service = await garden.getService(serviceName) const module = service.module - ctx.log.header({ + garden.log.header({ emoji: "runner", command: `Running service ${chalk.cyan(serviceName)} in module ${chalk.cyan(module.name)}`, }) - await ctx.configureEnvironment({}) + await garden.actions.prepareEnvironment({}) - const buildTask = new BuildTask({ ctx, module, force: opts["force-build"] }) + const buildTask = new BuildTask({ garden, module, force: opts["force-build"] }) await garden.addTask(buildTask) await garden.processTasks() - const dependencies = await ctx.getServices(module.serviceDependencyNames) - const runtimeContext = await prepareRuntimeContext(ctx, module, dependencies) + const dependencies = await garden.getServices(module.serviceDependencyNames) + const runtimeContext = await prepareRuntimeContext(garden, module, dependencies) - printRuntimeContext(ctx, runtimeContext) + printRuntimeContext(garden, runtimeContext) - const result = await ctx.runService({ serviceName, runtimeContext, silent: false, interactive: true }) + const result = await garden.actions.runService({ service, runtimeContext, silent: false, interactive: true }) return { result } } diff --git a/garden-cli/src/commands/run/test.ts b/garden-cli/src/commands/run/test.ts index 5cbce7bcbd..c2506c7703 100644 --- a/garden-cli/src/commands/run/test.ts +++ b/garden-cli/src/commands/run/test.ts @@ -19,14 +19,13 @@ import { Command, CommandParams, CommandResult, - ParameterValues, StringParameter, } from "../base" import { printRuntimeContext } from "./run" import dedent = require("dedent") import { prepareRuntimeContext } from "../../types/service" -export const runArgs = { +const runArgs = { module: new StringParameter({ help: "The name of the module to run.", required: true, @@ -37,7 +36,7 @@ export const runArgs = { }), } -export const runOpts = { +const runOpts = { interactive: new BooleanParameter({ help: "Set to false to skip interactive mode and just output the command result.", defaultValue: true, @@ -45,10 +44,10 @@ export const runOpts = { "force-build": new BooleanParameter({ help: "Force rebuild of module before running." }), } -export type Args = ParameterValues -export type Opts = ParameterValues +type Args = typeof runArgs +type Opts = typeof runOpts -export class RunTestCommand extends Command { +export class RunTestCommand extends Command { name = "test" alias = "t" help = "Run the specified module test." @@ -65,10 +64,10 @@ export class RunTestCommand extends Command { arguments = runArgs options = runOpts - async action({ garden, ctx, args, opts }: CommandParams): Promise> { + async action({ garden, args, opts }: CommandParams): Promise> { const moduleName = args.module const testName = args.test - const module = await ctx.getModule(moduleName) + const module = await garden.getModule(moduleName) const testConfig = findByName(module.testConfigs, testName) @@ -80,24 +79,30 @@ export class RunTestCommand extends Command { }) } - ctx.log.header({ + garden.log.header({ emoji: "runner", command: `Running test ${chalk.cyan(testName)} in module ${chalk.cyan(moduleName)}`, }) - await ctx.configureEnvironment({}) + await garden.actions.prepareEnvironment({}) - const buildTask = new BuildTask({ ctx, module, force: opts["force-build"] }) + const buildTask = new BuildTask({ garden, module, force: opts["force-build"] }) await garden.addTask(buildTask) await garden.processTasks() const interactive = opts.interactive - const deps = await ctx.getServices(testConfig.dependencies) - const runtimeContext = await prepareRuntimeContext(ctx, module, deps) + const deps = await garden.getServices(testConfig.dependencies) + const runtimeContext = await prepareRuntimeContext(garden, module, deps) - printRuntimeContext(ctx, runtimeContext) + printRuntimeContext(garden, runtimeContext) - const result = await ctx.testModule({ moduleName, interactive, runtimeContext, silent: false, testConfig }) + const result = await garden.actions.testModule({ + module, + interactive, + runtimeContext, + silent: false, + testConfig, + }) return { result } } diff --git a/garden-cli/src/commands/scan.ts b/garden-cli/src/commands/scan.ts index 5df84dafdb..14f8c591bc 100644 --- a/garden-cli/src/commands/scan.ts +++ b/garden-cli/src/commands/scan.ts @@ -20,8 +20,8 @@ export class ScanCommand extends Command { name = "scan" help = "Scans your project and outputs an overview of all modules." - async action({ ctx }: CommandParams): Promise> { - let modules = (await ctx.getModules()) + async action({ garden }: CommandParams): Promise> { + const modules = (await garden.getModules()) .map(m => { m.services.forEach(s => delete s.module) return omit(m, ["_ConfigType", "cacheContext", "serviceConfigs", "serviceNames"]) @@ -31,12 +31,12 @@ export class ScanCommand extends Command { const shortOutput = { modules: modules.map(m => { - m.services.map(s => delete s.spec) + m.services!.map(s => delete s.spec) return omit(m, ["spec"]) }), } - ctx.log.info(highlightYaml(safeDump(shortOutput, { noRefs: true, skipInvalid: true, sortKeys: true }))) + garden.log.info(highlightYaml(safeDump(shortOutput, { noRefs: true, skipInvalid: true, sortKeys: true }))) return { result: output } } diff --git a/garden-cli/src/commands/set.ts b/garden-cli/src/commands/set.ts index b7235573d0..8397f8b791 100644 --- a/garden-cli/src/commands/set.ts +++ b/garden-cli/src/commands/set.ts @@ -6,64 +6,68 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import { SetConfigResult } from "../types/plugin/outputs" +import { SetSecretResult } from "../types/plugin/outputs" import { Command, CommandResult, CommandParams, - ParameterValues, StringParameter, } from "./base" import dedent = require("dedent") export class SetCommand extends Command { name = "set" - help = "Set or modify data, e.g. configuration variables." + help = "Set or modify data, e.g. secrets." subCommands = [ - SetConfigCommand, + SetSecretCommand, ] async action() { return {} } } -export const setConnfigArgs = { - // TODO: specify and validate config key schema here +const setSecretArgs = { + provider: new StringParameter({ + help: "The name of the provider to store the secret with.", + required: true, + }), key: new StringParameter({ - help: "The key of the configuration variable. Separate with dots to get a nested key (e.g. key.nested).", + help: "A unique identifier for the secret.", required: true, }), value: new StringParameter({ - help: "The value of the configuration variable.", + help: "The value of the secret.", required: true, }), } -export type SetArgs = ParameterValues +type SetArgs = typeof setSecretArgs -// TODO: allow reading key/value pairs from a file +// TODO: allow storing data from files -export class SetConfigCommand extends Command { - name = "config" - help = "Set a configuration variable in the environment." +export class SetSecretCommand extends Command { + name = "secret" + help = "Set a secret value for a provider in an environment." description = dedent` - These configuration values can be referenced in module templates, for example as environment variables. + These secrets are handled by each provider, and may for example be exposed as environment + variables for services or mounted as files, depending on how the provider is implemented + and configured. - _Note: The value is always stored as a string._ + _Note: The value is currently always stored as a string._ Examples: - garden set config somekey myvalue - garden set config some.nested.key myvalue + garden set secret kubernetes somekey myvalue + garden set secret local-kubernets somekey myvalue ` - arguments = setConnfigArgs + arguments = setSecretArgs - async action({ ctx, args }: CommandParams): Promise> { - const key = args.key.split(".") - const result = await ctx.setConfig({ key, value: args.value }) - ctx.log.info(`Set config key ${args.key}`) + async action({ garden, args }: CommandParams): Promise> { + const key = args.key + const result = await garden.actions.setSecret({ pluginName: args.provider, key, value: args.value }) + garden.log.info(`Set config key ${args.key}`) return { result } } } diff --git a/garden-cli/src/commands/test.ts b/garden-cli/src/commands/test.ts index f35d8e56db..3003cb93e6 100644 --- a/garden-cli/src/commands/test.ts +++ b/garden-cli/src/commands/test.ts @@ -8,15 +8,13 @@ import * as Bluebird from "bluebird" import { flatten } from "lodash" -import { PluginContext } from "../plugin-context" import { BooleanParameter, Command, CommandParams, CommandResult, handleTaskResults, - ParameterValues, - StringParameter, + StringOption, StringsParameter, } from "./base" import { TaskResults } from "../task-graph" @@ -24,16 +22,17 @@ import { processModules } from "../process" import { Module } from "../types/module" import { TestTask } from "../tasks/test" import { computeAutoReloadDependants, withDependants } from "../watch" +import { Garden } from "../garden" -export const testArgs = { +const testArgs = { module: new StringsParameter({ help: "The name of the module(s) to deploy (skip to test all modules). " + "Use comma as separator to specify multiple modules.", }), } -export const testOpts = { - name: new StringParameter({ +const testOpts = { + name: new StringOption({ help: "Only run tests with the specfied name (e.g. unit or integ).", alias: "n", }), @@ -42,10 +41,10 @@ export const testOpts = { watch: new BooleanParameter({ help: "Watch for changes in module(s) and auto-test.", alias: "w" }), } -export type Args = ParameterValues -export type Opts = ParameterValues +type Args = typeof testArgs +type Opts = typeof testOpts -export class TestCommand extends Command { +export class TestCommand extends Command { name = "test" help = "Test all or specified modules." @@ -68,49 +67,47 @@ export class TestCommand extends Command { arguments = testArgs options = testOpts - async action({ garden, ctx, args, opts }: CommandParams): Promise> { - - const autoReloadDependants = await computeAutoReloadDependants(ctx) + async action({ garden, args, opts }: CommandParams): Promise> { + const autoReloadDependants = await computeAutoReloadDependants(garden) let modules: Module[] if (args.module) { - modules = await withDependants(ctx, await ctx.getModules(args.module), autoReloadDependants) + modules = await withDependants(garden, await garden.getModules(args.module), autoReloadDependants) } else { // All modules are included in this case, so there's no need to compute dependants. - modules = await ctx.getModules() + modules = await garden.getModules() } - ctx.log.header({ + garden.log.header({ emoji: "thermometer", command: `Running tests`, }) - await ctx.configureEnvironment({}) + await garden.actions.prepareEnvironment({}) const name = opts.name const force = opts.force const forceBuild = opts["force-build"] const results = await processModules({ - ctx, garden, modules, watch: opts.watch, - handler: async (module) => getTestTasks({ ctx, module, name, force, forceBuild }), + handler: async (module) => getTestTasks({ garden, module, name, force, forceBuild }), changeHandler: async (module) => { - const modulesToProcess = await withDependants(ctx, [module], autoReloadDependants) + const modulesToProcess = await withDependants(garden, [module], autoReloadDependants) return flatten(await Bluebird.map( modulesToProcess, - m => getTestTasks({ ctx, module: m, name, force, forceBuild }))) + m => getTestTasks({ garden, module: m, name, force, forceBuild }))) }, }) - return handleTaskResults(ctx, "test", results) + return handleTaskResults(garden, "test", results) } } export async function getTestTasks( - { ctx, module, name, force = false, forceBuild = false }: - { ctx: PluginContext, module: Module, name?: string, force?: boolean, forceBuild?: boolean }, + { garden, module, name, force = false, forceBuild = false }: + { garden: Garden, module: Module, name?: string, force?: boolean, forceBuild?: boolean }, ) { const tasks: Promise[] = [] @@ -119,10 +116,10 @@ export async function getTestTasks( continue } tasks.push(TestTask.factory({ + garden, force, forceBuild, testConfig: test, - ctx, module, })) } diff --git a/garden-cli/src/commands/unlink/module.ts b/garden-cli/src/commands/unlink/module.ts index 0834b6b6b0..929f04b4ac 100644 --- a/garden-cli/src/commands/unlink/module.ts +++ b/garden-cli/src/commands/unlink/module.ts @@ -12,7 +12,6 @@ import { Command, CommandResult, StringsParameter, - ParameterValues, BooleanParameter, CommandParams, } from "../base" @@ -22,23 +21,23 @@ import { LinkedSource, } from "../../config-store" -export const unlinkModuleArguments = { +const unlinkModuleArguments = { module: new StringsParameter({ help: "Name of the module(s) to unlink. Use comma separator to specify multiple modules.", }), } -export const unlinkModuleOptions = { +const unlinkModuleOptions = { all: new BooleanParameter({ help: "Unlink all modules.", alias: "a", }), } -type Args = ParameterValues -type Opts = ParameterValues +type Args = typeof unlinkModuleArguments +type Opts = typeof unlinkModuleOptions -export class UnlinkModuleCommand extends Command { +export class UnlinkModuleCommand extends Command { name = "module" help = "Unlink a previously linked remote module from its local directory." arguments = unlinkModuleArguments @@ -54,23 +53,22 @@ export class UnlinkModuleCommand extends Command): Promise> { - - ctx.log.header({ emoji: "chains", command: "unlink module" }) + async action({ garden, args, opts }: CommandParams): Promise> { + garden.log.header({ emoji: "chains", command: "unlink module" }) const sourceType = "module" const { module = [] } = args if (opts.all) { - await ctx.localConfigStore.set([localConfigKeys.linkedModuleSources], []) - ctx.log.info("Unlinked all modules") + await garden.localConfigStore.set([localConfigKeys.linkedModuleSources], []) + garden.log.info("Unlinked all modules") return { result: [] } } - const linkedModuleSources = await removeLinkedSources({ ctx, sourceType, names: module }) + const linkedModuleSources = await removeLinkedSources({ garden, sourceType, names: module }) - ctx.log.info(`Unlinked module(s) ${module}`) + garden.log.info(`Unlinked module(s) ${module}`) return { result: linkedModuleSources } } diff --git a/garden-cli/src/commands/unlink/source.ts b/garden-cli/src/commands/unlink/source.ts index 7130bd2b8f..8b68bf434d 100644 --- a/garden-cli/src/commands/unlink/source.ts +++ b/garden-cli/src/commands/unlink/source.ts @@ -12,7 +12,6 @@ import { Command, CommandResult, StringsParameter, - ParameterValues, BooleanParameter, CommandParams, } from "../base" @@ -22,23 +21,23 @@ import { LinkedSource, } from "../../config-store" -export const unlinkSourceArguments = { +const unlinkSourceArguments = { source: new StringsParameter({ help: "Name of the source(s) to unlink. Use comma separator to specify multiple sources.", }), } -export const unlinkSourceOptions = { +const unlinkSourceOptions = { all: new BooleanParameter({ help: "Unlink all sources.", alias: "a", }), } -type Args = ParameterValues -type Opts = ParameterValues +type Args = typeof unlinkSourceArguments +type Opts = typeof unlinkSourceOptions -export class UnlinkSourceCommand extends Command { +export class UnlinkSourceCommand extends Command { name = "source" help = "Unlink a previously linked remote source from its local directory." arguments = unlinkSourceArguments @@ -54,23 +53,22 @@ export class UnlinkSourceCommand extends Command): Promise> { - - ctx.log.header({ emoji: "chains", command: "unlink source" }) + async action({ garden, args, opts }: CommandParams): Promise> { + garden.log.header({ emoji: "chains", command: "unlink source" }) const sourceType = "project" const { source = [] } = args if (opts.all) { - await ctx.localConfigStore.set([localConfigKeys.linkedProjectSources], []) - ctx.log.info("Unlinked all sources") + await garden.localConfigStore.set([localConfigKeys.linkedProjectSources], []) + garden.log.info("Unlinked all sources") return { result: [] } } - const linkedProjectSources = await removeLinkedSources({ ctx, sourceType, names: source }) + const linkedProjectSources = await removeLinkedSources({ garden, sourceType, names: source }) - ctx.log.info(`Unlinked source(s) ${source}`) + garden.log.info(`Unlinked source(s) ${source}`) return { result: linkedProjectSources } } diff --git a/garden-cli/src/commands/update-remote/all.ts b/garden-cli/src/commands/update-remote/all.ts index bc332704ad..90ced17490 100644 --- a/garden-cli/src/commands/update-remote/all.ts +++ b/garden-cli/src/commands/update-remote/all.ts @@ -32,15 +32,15 @@ export class UpdateRemoteAllCommand extends Command { garden update-remote all # update all remote sources and modules in the project ` - async action({ garden, ctx }: CommandParams): Promise> { + async action({ garden }: CommandParams): Promise> { - ctx.log.header({ emoji: "hammer_and_wrench", command: "update-remote all" }) + garden.log.header({ emoji: "hammer_and_wrench", command: "update-remote all" }) const sourcesCmd = new UpdateRemoteSourcesCommand() const modulesCmd = new UpdateRemoteModulesCommand() - const { result: projectSources } = await sourcesCmd.action({ garden, ctx, args: { source: undefined }, opts: {} }) - const { result: moduleSources } = await modulesCmd.action({ garden, ctx, args: { module: undefined }, opts: {} }) + const { result: projectSources } = await sourcesCmd.action({ garden, args: { source: undefined }, opts: {} }) + const { result: moduleSources } = await modulesCmd.action({ garden, args: { module: undefined }, opts: {} }) return { result: { projectSources: projectSources!, moduleSources: moduleSources! } } } diff --git a/garden-cli/src/commands/update-remote/modules.ts b/garden-cli/src/commands/update-remote/modules.ts index ed9207f1e2..a377ca259a 100644 --- a/garden-cli/src/commands/update-remote/modules.ts +++ b/garden-cli/src/commands/update-remote/modules.ts @@ -13,7 +13,6 @@ import chalk from "chalk" import { Command, StringsParameter, - ParameterValues, CommandResult, CommandParams, } from "../base" @@ -22,15 +21,15 @@ import { ParameterError } from "../../exceptions" import { pruneRemoteSources } from "./helpers" import { hasRemoteSource } from "../../util/ext-source-util" -export const updateRemoteModulesArguments = { +const updateRemoteModulesArguments = { module: new StringsParameter({ help: "Name of the remote module(s) to update. Use comma separator to specify multiple modules.", }), } -export type UpdateRemoteModulesArguments = ParameterValues +type Args = typeof updateRemoteModulesArguments -export class UpdateRemoteModulesCommand extends Command { +export class UpdateRemoteModulesCommand extends Command { name = "modules" help = "Update remote modules." arguments = updateRemoteModulesArguments @@ -45,11 +44,13 @@ export class UpdateRemoteModulesCommand extends Command): Promise> { - ctx.log.header({ emoji: "hammer_and_wrench", command: "update-remote modules" }) + async action( + { garden, args }: CommandParams, + ): Promise> { + garden.log.header({ emoji: "hammer_and_wrench", command: "update-remote modules" }) const { module } = args - const modules = await ctx.getModules(module) + const modules = await garden.getModules(module) const moduleSources = modules .filter(hasRemoteSource) @@ -59,13 +60,13 @@ export class UpdateRemoteModulesCommand extends Command 0) { - const modulesWithRemoteSource = (await ctx.getModules()).filter(hasRemoteSource).sort() + const modulesWithRemoteSource = (await garden.getModules()).filter(hasRemoteSource).sort() throw new ParameterError( `Expected module(s) ${chalk.underline(diff.join(","))} to have a remote source.`, { modulesWithRemoteSource, - input: module.sort(), + input: module ? module.sort() : undefined, }, ) } @@ -73,10 +74,10 @@ export class UpdateRemoteModulesCommand extends Command +type Args = typeof updateRemoteSourcesArguments -export class UpdateRemoteSourcesCommand extends Command { +export class UpdateRemoteSourcesCommand extends Command { name = "sources" help = "Update remote sources." arguments = updateRemoteSourcesArguments @@ -43,13 +42,14 @@ export class UpdateRemoteSourcesCommand extends Command): Promise> { - - ctx.log.header({ emoji: "hammer_and_wrench", command: "update-remote sources" }) + async action( + { garden, args }: CommandParams, + ): Promise> { + garden.log.header({ emoji: "hammer_and_wrench", command: "update-remote sources" }) const { source } = args - const projectSources = ctx.projectSources + const projectSources = garden.projectSources .filter(src => source ? source.includes(src.name) : true) const names = projectSources.map(src => src.name) @@ -60,8 +60,8 @@ export class UpdateRemoteSourcesCommand extends Command s.name).sort(), - input: source.sort(), + remoteSources: garden.projectSources.map(s => s.name).sort(), + input: source ? source.sort() : undefined, }, ) } @@ -69,10 +69,10 @@ export class UpdateRemoteSourcesCommand extends Command { + async action({ garden }: CommandParams): Promise { + garden.log.header({ emoji: "heavy_check_mark", command: "validate" }) - ctx.log.header({ emoji: "heavy_check_mark", command: "validate" }) - - await ctx.getModules() + await garden.getModules() return {} } diff --git a/garden-cli/src/config/common.ts b/garden-cli/src/config/common.ts index 30126cb01c..399d7f397b 100644 --- a/garden-cli/src/config/common.ts +++ b/garden-cli/src/config/common.ts @@ -9,7 +9,6 @@ import { JoiObject } from "joi" import * as Joi from "joi" import * as uuid from "uuid" -import { EnvironmentConfig } from "../config/project" import { ConfigurationError, LocalConfigError } from "../exceptions" import chalk from "chalk" @@ -91,21 +90,6 @@ export const joiRepositoryUrl = () => Joi ) .example("# or git+https://github.com/organization/some-module.git#v2.0") -export const remoteSourceSchema = Joi.object() - .keys({ - name: joiIdentifier() - .required() - .description("The name of the source to import"), - repositoryUrl: joiRepositoryUrl() - .required(), - }) - -export interface Environment { - name: string - namespace: string - config: EnvironmentConfig, -} - export function isPrimitive(value: any) { return typeof value === "string" || typeof value === "number" || typeof value === "boolean" } diff --git a/garden-cli/src/config/config-context.ts b/garden-cli/src/config/config-context.ts index 1245259fb7..b717003b3c 100644 --- a/garden-cli/src/config/config-context.ts +++ b/garden-cli/src/config/config-context.ts @@ -9,13 +9,13 @@ import { isString, flatten } from "lodash" import { Module } from "../types/module" import { PrimitiveMap, isPrimitive, Primitive, joiIdentifierMap, joiStringMap, joiPrimitive } from "./common" -import { ProviderConfig, EnvironmentConfig, providerConfigBase } from "./project" -import { PluginContext } from "../plugin-context" +import { Provider, Environment, providerConfigBaseSchema } from "./project" import { ModuleConfig } from "./module" import { ConfigurationError } from "../exceptions" import { Service } from "../types/service" import { resolveTemplateString } from "../template-string" import * as Joi from "joi" +import { Garden } from "../garden" export type ContextKey = string[] @@ -252,11 +252,11 @@ export class ModuleConfigContext extends ProjectConfigContext { public services: Map Promise> @schema( - joiIdentifierMap(providerConfigBase) + joiIdentifierMap(providerConfigBaseSchema) .description("A map of all configured plugins/providers for this environment and their configuration.") .example({ kubernetes: { name: "local-kubernetes", context: "my-kube-context" } }), ) - public providers: Map + public providers: Map // NOTE: This has some negative performance implications and may not be something we want to support, // so I'm disabling this feature for now. @@ -272,19 +272,19 @@ export class ModuleConfigContext extends ProjectConfigContext { public variables: PrimitiveMap constructor( - ctx: PluginContext, - environmentConfig: EnvironmentConfig, + garden: Garden, + environment: Environment, moduleConfigs: ModuleConfig[], ) { super() const _this = this - this.environment = new EnvironmentContext(_this, environmentConfig.name) + this.environment = new EnvironmentContext(_this, environment.name) this.modules = new Map(moduleConfigs.map((config) => <[string, () => Promise]>[config.name, async () => { - const module = await ctx.getModule(config.name) + const module = await garden.getModule(config.name) return new ModuleContext(_this, module) }], )) @@ -293,20 +293,20 @@ export class ModuleConfigContext extends ProjectConfigContext { this.services = new Map(serviceNames.map((name) => <[string, () => Promise]>[name, async () => { - const service = await ctx.getService(name) + const service = await garden.getService(name) const outputs = { ...service.config.outputs, - ...await ctx.getServiceOutputs({ serviceName: service.name }), + ...await garden.actions.getServiceOutputs({ service }), } return new ServiceContext(_this, service, outputs) }], )) - this.providers = new Map(environmentConfig.providers.map(p => <[string, ProviderConfig]>[p.name, p])) + this.providers = new Map(environment.providers.map(p => <[string, Provider]>[p.name, p])) // this.config = new SecretsContextNode(ctx) - this.variables = environmentConfig.variables + this.variables = environment.variables } } @@ -316,7 +316,7 @@ export class ModuleConfigContext extends ProjectConfigContext { // } // async resolve({ key }: ResolveParams) { -// const { value } = await this.ctx.getConfig({ key }) +// const { value } = await this.ctx.getSecret({ key }) // return value === null ? undefined : value // } // } diff --git a/garden-cli/src/config/project.ts b/garden-cli/src/config/project.ts index 6cac463d59..f2ca5426db 100644 --- a/garden-cli/src/config/project.ts +++ b/garden-cli/src/config/project.ts @@ -13,7 +13,7 @@ import { joiIdentifier, joiVariables, Primitive, - remoteSourceSchema, + joiRepositoryUrl, } from "./common" export interface ProviderConfig { @@ -21,26 +21,71 @@ export interface ProviderConfig { [key: string]: any } +export const providerConfigBaseSchema = Joi.object() + .keys({ + name: joiIdentifier().required() + .description("The name of the provider plugin to configure.") + .example("local-kubernetes"), + }) + .unknown(true) + .meta({ extendable: true }) + +export interface Provider { + name: string + config: T +} + export interface CommonEnvironmentConfig { - configurationHandler?: string providers: ProviderConfig[] // further validated by each plugin variables: { [key: string]: Primitive } } -export interface EnvironmentConfig extends CommonEnvironmentConfig { +export const environmentConfigSchema = Joi.object() + .keys({ + providers: joiArray(providerConfigBaseSchema) + .unique("name") + .description( + "A list of providers that should be used for this environment, and their configuration. " + + "Please refer to individual plugins/providers for details on how to configure them.", + ), + variables: joiVariables() + .description("A key/value map of variables that modules can reference when using this environment."), + }) + +export interface Environment extends CommonEnvironmentConfig { name: string } +export const environmentSchema = environmentConfigSchema + .keys({ + name: Joi.string() + .required() + .description("The name of the current environment."), + }) + export interface SourceConfig { name: string repositoryUrl: string } +export const projectSourceSchema = Joi.object() + .keys({ + name: joiIdentifier() + .required() + .description("The name of the source to import"), + repositoryUrl: joiRepositoryUrl() + .required(), + }) + +export const projectSourcesSchema = joiArray(projectSourceSchema) + .unique("name") + .description("A list of remote sources to import into project") + export interface ProjectConfig { name: string defaultEnvironment: string environmentDefaults: CommonEnvironmentConfig - environments: EnvironmentConfig[] + environments: Environment[] sources?: SourceConfig[] } @@ -48,7 +93,7 @@ export const defaultProviders = [ { name: "container" }, ] -export const defaultEnvironments: EnvironmentConfig[] = [ +export const defaultEnvironments: Environment[] = [ { name: "local", providers: [ @@ -60,61 +105,42 @@ export const defaultEnvironments: EnvironmentConfig[] = [ }, ] -export const providerConfigBase = Joi.object() - .keys({ - name: joiIdentifier().required() - .description("The name of the provider plugin to configure.") - .example("local-kubernetes"), - }) - .unknown(true) - .meta({ extendable: true }) - -export const environmentSchema = Joi.object().keys({ - configurationHandler: joiIdentifier() - .description( - "Specify the provider that should store configuration variables for this environment. " + - "Use this when you configure multiple providers that can manage configuration.", - ), - providers: joiArray(providerConfigBase) - .unique("name") - .description( - "A list of providers that should be used for this environment, and their configuration. " + - "Please refer to individual plugins/providers for details on how to configure them.", - ), - variables: joiVariables() - .description("A key/value map of variables that modules can reference when using this environment."), -}) - const environmentDefaults = { providers: [], variables: {}, } +export const projectNameSchema = joiIdentifier() + .required() + .description("The name of the project.") + .example("my-sweet-project") + export const projectSchema = Joi.object() .keys({ - name: joiIdentifier() - .required() - .description("The name of the project.") - .example("my-sweet-project"), + name: projectNameSchema, defaultEnvironment: Joi.string() .default("", "") .description("The default environment to use when calling commands without the `--env` parameter."), - environmentDefaults: environmentSchema + environmentDefaults: environmentConfigSchema .default(() => environmentDefaults, safeDump(environmentDefaults)) .example(environmentDefaults) .description( "Default environment settings, that are inherited (but can be overridden) by each configured environment", ), - environments: joiArray(environmentSchema.keys({ name: joiIdentifier().required() })) + environments: joiArray(environmentConfigSchema.keys({ name: joiIdentifier().required() })) .unique("name") .default(() => ({ ...defaultEnvironments }), safeDump(defaultEnvironments)) .description("A list of environments to configure for the project.") .example(defaultEnvironments), - sources: joiArray(remoteSourceSchema) - .unique("name") - .description("A list of remote sources to import into project"), + sources: projectSourcesSchema, }) .required() .description( "The configuration for a Garden project. This should be specified in the garden.yml file in your project root.", ) + +// this is used for default handlers in the action handler +export const defaultProvider: Provider = { + name: "_default", + config: {}, +} diff --git a/garden-cli/src/garden.ts b/garden-cli/src/garden.ts index b823436a08..7f03263520 100644 --- a/garden-cli/src/garden.ts +++ b/garden-cli/src/garden.ts @@ -25,14 +25,9 @@ import { sortBy, uniqBy, } from "lodash" -import * as Joi from "joi" - const AsyncLock = require("async-lock") + import { TreeCache } from "./cache" -import { - PluginContext, - createPluginContext, -} from "./plugin-context" import { builtinPlugins, fixedPlugins, @@ -44,10 +39,9 @@ import { pluginActionDescriptions, pluginModuleSchema, pluginSchema, - Provider, RegisterPluginParam, } from "./types/plugin/plugin" -import { EnvironmentConfig, SourceConfig } from "./config/project" +import { Environment, SourceConfig, defaultProvider } from "./config/project" import { findByName, getIgnorer, @@ -83,11 +77,7 @@ import { GardenPlugin, ModuleActions, } from "./types/plugin/plugin" -import { - Environment, - joiIdentifier, - validate, -} from "./config/common" +import { joiIdentifier, validate } from "./config/common" import { Service } from "./types/service" import { resolveTemplateStrings } from "./template-string" import { @@ -107,13 +97,16 @@ import { BuildDependencyConfig, ModuleConfig } from "./config/module" import { ProjectConfigContext, ModuleConfigContext } from "./config/config-context" import { FileWriter } from "./logger/writers/file-writer" import { LogLevel } from "./logger/log-node" +import { ActionHelper } from "./actions" +import { createPluginContext } from "./plugin-context" +import { ModuleAndServiceActions } from "./types/plugin/plugin" export interface ActionHandlerMap { [actionName: string]: PluginActions[T] } -export interface ModuleActionHandlerMap> { - [actionName: string]: ModuleActions[T] +export interface ModuleActionHandlerMap { + [actionName: string]: ModuleAndServiceActions[T] } export type PluginActionMap = { @@ -123,9 +116,9 @@ export type PluginActionMap = { } export type ModuleActionMap = { - [A in keyof ModuleActions]: { + [A in keyof ModuleAndServiceActions]: { [moduleType: string]: { - [pluginName: string]: ModuleActions[A], + [pluginName: string]: ModuleAndServiceActions[A], }, } } @@ -151,24 +144,21 @@ export class Garden { public readonly moduleActionHandlers: ModuleActionMap private readonly loadedPlugins: { [key: string]: GardenPlugin } - private moduleConfigs: ModuleConfigMap + private moduleConfigs: ModuleConfigMap private modulesScanned: boolean private readonly registeredPlugins: { [key: string]: PluginFactory } private readonly serviceNameIndex: { [key: string]: string } private readonly taskGraph: TaskGraph - private readonly configKeyNamespaces: string[] - private readonly pluginContext: PluginContext public readonly localConfigStore: LocalConfigStore public readonly vcs: VcsHandler public readonly cache: TreeCache + public readonly actions: ActionHelper constructor( public readonly projectRoot: string, public readonly projectName: string, - public readonly environmentName: string, - private readonly namespace: string, - public readonly environmentConfig: EnvironmentConfig, + public readonly environment: Environment, public readonly projectSources: SourceConfig[] = [], public readonly buildDir: BuildDir, logger?: Logger, @@ -187,12 +177,8 @@ export class Garden { this.actionHandlers = fromPairs(pluginActionNames.map(n => [n, {}])) this.moduleActionHandlers = fromPairs(moduleActionNames.map(n => [n, {}])) - this.environmentConfig = environmentConfig - - this.configKeyNamespaces = ["project"] - - this.pluginContext = this.getPluginContext() - this.taskGraph = new TaskGraph(this.pluginContext) + this.taskGraph = new TaskGraph(this) + this.actions = new ActionHelper(this) } static async factory(currentDirectory: string, { env, config, logger, plugins = [] }: ContextOpts = {}) { @@ -235,45 +221,47 @@ export class Garden { } const parts = env.split(".") - const environment = parts[0] + const environmentName = parts[0] const namespace = parts.slice(1).join(".") || DEFAULT_NAMESPACE - const envConfig = findByName(environments, environment) + const environmentConfig = findByName(environments, environmentName) - if (!envConfig) { - throw new ParameterError(`Project ${projectName} does not specify environment ${environment}`, { + if (!environmentConfig) { + throw new ParameterError(`Project ${projectName} does not specify environment ${environmentName}`, { projectName, env, definedEnvironments: getNames(environments), }) } - if (!envConfig.providers || envConfig.providers.length === 0) { - throw new ConfigurationError(`Environment '${environment}' does not specify any providers`, { + if (!environmentConfig.providers || environmentConfig.providers.length === 0) { + throw new ConfigurationError(`Environment '${environmentName}' does not specify any providers`, { projectName, env, - envConfig, + environmentConfig, }) } if (namespace.startsWith("garden-")) { throw new ParameterError(`Namespace cannot start with "garden-"`, { - environment, + environmentConfig, namespace, }) } + const fixedProviders = fixedPlugins.map(name => ({ name })) + const mergedProviders = merge( - {}, + fixedProviders, keyBy(environmentDefaults.providers, "name"), - keyBy(envConfig.providers, "name"), + keyBy(environmentConfig.providers, "name"), ) // Resolve the project configuration based on selected environment - const projectEnvConfig: EnvironmentConfig = { - name: environment, + const environment: Environment = { + name: environmentConfig.name, providers: Object.values(mergedProviders), - variables: merge({}, environmentDefaults.variables, envConfig.variables), + variables: merge({}, environmentDefaults.variables, environmentConfig.variables), } const buildDir = await BuildDir.factory(projectRoot) @@ -295,8 +283,6 @@ export class Garden { projectRoot, projectName, environment, - namespace, - projectEnvConfig, projectSources, buildDir, logger, @@ -307,30 +293,17 @@ export class Garden { garden.registerPlugin(plugin) } - // Load fixed plugins (e.g. built-in module types) - for (const plugin of fixedPlugins) { - await garden.loadPlugin(plugin, {}) - } - // Load configured plugins // Validate configuration - for (const provider of projectEnvConfig.providers) { + for (const provider of environment.providers) { await garden.loadPlugin(provider.name, provider) } return garden } - getEnvironment(): Environment { - return { - name: this.environmentName, - namespace: this.namespace, - config: this.environmentConfig, - } - } - - getPluginContext() { - return createPluginContext(this) + getPluginContext(providerName: string) { + return createPluginContext(this, providerName) } async clearBuilds() { @@ -367,10 +340,11 @@ export class Garden { try { pluginModule = require(moduleNameOrLocation) } catch (error) { - throw new ConfigurationError(`Unable to load plugin "${moduleNameOrLocation}" (could not load module)`, { - message: error.message, - moduleNameOrLocation, - }) + throw new ConfigurationError( + `Unable to load plugin "${moduleNameOrLocation}" (could not load module: ${error.message})`, { + message: error.message, + moduleNameOrLocation, + }) } try { @@ -438,13 +412,11 @@ export class Garden { this.loadedPlugins[pluginName] = plugin // allow plugins to extend their own config (that gets passed to action handlers) - if (plugin.config) { - const providerConfig = findByName(this.environmentConfig.providers, pluginName) - if (providerConfig) { - extend(providerConfig, plugin.config) - } else { - this.environmentConfig.providers.push(plugin.config) - } + const providerConfig = findByName(this.environment.providers, pluginName) + if (providerConfig) { + extend(providerConfig, plugin.config, config) + } else { + this.environment.providers.push(extend({ name: pluginName }, plugin.config, config)) } for (const modulePath of plugin.modules || []) { @@ -505,8 +477,8 @@ export class Garden { this.actionHandlers[actionType][pluginName] = wrapped } - private addModuleActionHandler>( - pluginName: string, actionType: T, moduleType: string, handler: ModuleActions[T], + private addModuleActionHandler( + pluginName: string, actionType: T, moduleType: string, handler: ModuleActions[T], ) { const plugin = this.getPlugin(pluginName) const schema = moduleActionDescriptions[actionType].resultSchema @@ -714,7 +686,7 @@ export class Garden { await this.detectCircularDependencies() const moduleConfigContext = new ModuleConfigContext( - this.pluginContext, this.environmentConfig, Object.values(this.moduleConfigs), + this, this.environment, Object.values(this.moduleConfigs), ) this.moduleConfigs = await resolveTemplateStrings(this.moduleConfigs, moduleConfigContext) }) @@ -733,14 +705,10 @@ export class Garden { @param force - add the module again, even if it's already registered */ async addModule(config: ModuleConfig, force = false) { - const parseHandler = await this.getModuleActionHandler({ actionType: "parseModule", moduleType: config.type }) - const env = this.getEnvironment() - const provider: Provider = { - name: parseHandler["pluginName"], - config: this.environmentConfig.providers[parseHandler["pluginName"]], - } + const validateHandler = await this.getModuleActionHandler({ actionType: "validate", moduleType: config.type }) + const ctx = this.getPluginContext(validateHandler["pluginName"]) - config = await parseHandler({ env, provider, moduleConfig: config }) + config = await validateHandler({ ctx, moduleConfig: config }) // FIXME: this is rather clumsy config.name = getModuleKey(config.name, config.plugin) @@ -838,7 +806,7 @@ export class Garden { sourceType: ExternalSourceType, }): Promise { - const linkedSources = await getLinkedSources(this.pluginContext, sourceType) + const linkedSources = await getLinkedSources(this, sourceType) const linked = findByName(linkedSources, name) @@ -861,7 +829,7 @@ export class Garden { /** * Get a handler for the specified module action. */ - public getModuleActionHandlers>( + public getModuleActionHandlers( { actionType, moduleType, pluginName }: { actionType: T, moduleType: string, pluginName?: string }, ): ModuleActionHandlerMap { @@ -894,12 +862,13 @@ export class Garden { if (handlers.length) { return handlers[handlers.length - 1] } else if (defaultHandler) { + defaultHandler["pluginName"] = defaultProvider.name return defaultHandler } const errorDetails = { requestedHandlerType: actionType, - environment: this.environmentName, + environment: this.environment.name, pluginName, } @@ -907,7 +876,7 @@ export class Garden { throw new PluginError(`Plugin '${pluginName}' does not have a '${actionType}' handler.`, errorDetails) } else { throw new ParameterError( - `No '${actionType}' handler configured in environment '${this.environmentName}'. ` + + `No '${actionType}' handler configured in environment '${this.environment.name}'. ` + `Are you missing a provider configuration?`, errorDetails, ) @@ -917,23 +886,24 @@ export class Garden { /** * Get the last configured handler for the specified action. */ - public getModuleActionHandler( + public getModuleActionHandler( { actionType, moduleType, pluginName, defaultHandler }: - { actionType: T, moduleType: string, pluginName?: string, defaultHandler?: ModuleActions[T] }, - ): ModuleActions[T] { + { actionType: T, moduleType: string, pluginName?: string, defaultHandler?: ModuleAndServiceActions[T] }, + ): ModuleAndServiceActions[T] { const handlers = Object.values(this.getModuleActionHandlers({ actionType, moduleType, pluginName })) if (handlers.length) { return handlers[handlers.length - 1] } else if (defaultHandler) { + defaultHandler["pluginName"] = defaultProvider.name return defaultHandler } const errorDetails = { requestedHandlerType: actionType, requestedModuleType: moduleType, - environment: this.environmentName, + environment: this.environment.name, pluginName, } @@ -945,39 +915,11 @@ export class Garden { } else { throw new ParameterError( `No '${actionType}' handler configured for module type '${moduleType}' in environment ` + - `'${this.environmentName}'. Are you missing a provider configuration?`, + `'${this.environment.name}'. Are you missing a provider configuration?`, errorDetails, ) } } - /** - * Validates the specified config key, making sure it's properly formatted and matches defined keys. - */ - public validateConfigKey(key: string[]) { - try { - validate(key, Joi.array().items(joiIdentifier())) - } catch (err) { - throw new ParameterError( - `Invalid config key: ${key.join(".")} (must be a dot delimited string of identifiers)`, - { key }, - ) - } - - if (!this.configKeyNamespaces.includes(key[0])) { - throw new ParameterError( - `Invalid config key namespace ${key[0]} (must be one of ${this.configKeyNamespaces.join(", ")})`, - { key, validNamespaces: this.configKeyNamespaces }, - ) - } - - if (key[0] === "project") { - // we allow any custom key under the project namespace - return - } else { - // TODO: validate built-in (garden) and plugin config keys - } - } - //endregion } diff --git a/garden-cli/src/plugin-context.ts b/garden-cli/src/plugin-context.ts index b82fef4d41..a24e141b0c 100644 --- a/garden-cli/src/plugin-context.ts +++ b/garden-cli/src/plugin-context.ts @@ -6,523 +6,82 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import Bluebird = require("bluebird") -import chalk from "chalk" -import { CacheContext } from "./cache" import { Garden } from "./garden" -import { PrimitiveMap } from "./config/common" -import { Module } from "./types/module" +import { mapValues, keyBy, cloneDeep } from "lodash" +import * as Joi from "joi" import { - ModuleActions, Provider, - ServiceActions, -} from "./types/plugin/plugin" -import { - BuildResult, - BuildStatus, - DeleteConfigResult, - EnvironmentStatusMap, - ExecInServiceResult, - GetConfigResult, - GetServiceLogsResult, - LoginStatusMap, - ModuleActionOutputs, - PushResult, - RunResult, - ServiceActionOutputs, - SetConfigResult, - TestResult, -} from "./types/plugin/outputs" -import { - BuildModuleParams, - DeleteConfigParams, - DeployServiceParams, - DeleteServiceParams, - ExecInServiceParams, - GetConfigParams, - GetModuleBuildStatusParams, - GetServiceLogsParams, - GetServiceOutputsParams, - GetServiceStatusParams, - GetTestResultParams, - ModuleActionParams, - PluginActionContextParams, - PluginActionParams, - PluginActionParamsBase, - PluginModuleActionParamsBase, - PluginServiceActionParamsBase, - PushModuleParams, - RunModuleParams, - RunServiceParams, - ServiceActionParams, - SetConfigParams, - TestModuleParams, - GetLoginStatusParams, - LoginParams, - LogoutParams, - GetEnvironmentStatusParams, - DestroyEnvironmentParams, -} from "./types/plugin/params" -import { - Service, - ServiceStatus, - prepareRuntimeContext, -} from "./types/service" -import { - mapValues, - toPairs, - values, - keyBy, - omit, -} from "lodash" -import { Omit } from "./util/util" -import { RuntimeContext } from "./types/service" -import { processServices, ProcessResults } from "./process" -import { getDeployTasks } from "./tasks/deploy" - -export type PluginContextGuard = { - readonly [P in keyof (PluginActionParams | ModuleActionParams)]: (...args: any[]) => Promise -} - -export interface ContextStatus { - providers: EnvironmentStatusMap - services: { [name: string]: ServiceStatus } -} - -export interface DeployServicesParams { - serviceNames?: string[], - force?: boolean - forceBuild?: boolean -} - -export type PluginContextParams = - Omit & { pluginName?: string } -export type PluginContextModuleParams = - Omit & { moduleName: string, pluginName?: string } -export type PluginContextServiceParams = - Omit - & { serviceName: string, runtimeContext?: RuntimeContext, pluginName?: string } - -export type WrappedFromGarden = Pick -export interface PluginContext extends PluginContextGuard, WrappedFromGarden { - providers: { [name: string]: Provider } - - getEnvironmentStatus: (params: PluginContextParams) => Promise - configureEnvironment: (params: { force?: boolean, pluginName?: string }) => Promise - destroyEnvironment: (params: PluginContextParams) => Promise - getConfig: (params: PluginContextParams) => Promise - setConfig: (params: PluginContextParams) => Promise - deleteConfig: (params: PluginContextParams) => Promise - getLoginStatus: (params: PluginContextParams) => Promise - login: (params: PluginContextParams) => Promise - logout: (params: PluginContextParams) => Promise - - getModuleBuildStatus: (params: PluginContextModuleParams>) - => Promise - buildModule: (params: PluginContextModuleParams>) - => Promise - pushModule: (params: PluginContextModuleParams>) - => Promise - runModule: (params: PluginContextModuleParams>) - => Promise, - testModule: (params: PluginContextModuleParams>) - => Promise - getTestResult: (params: PluginContextModuleParams>) - => Promise - - getServiceStatus: (params: PluginContextServiceParams>) - => Promise - deployService: (params: PluginContextServiceParams>) - => Promise - deleteService: (params: PluginContextServiceParams>) - => Promise - getServiceOutputs: (params: PluginContextServiceParams>) - => Promise - execInService: (params: PluginContextServiceParams>) - => Promise - getServiceLogs: (params: PluginContextServiceParams>) - => Promise - runService: (params: PluginContextServiceParams>) - => Promise, +const providerSchema = Joi.object() + .options({ presence: "required" }) + .keys({ + name: joiIdentifier() + .description("The name of the provider (plugin)."), + config: providerConfigBaseSchema, + }) - invalidateCache: (context: CacheContext) => void - invalidateCacheUp: (context: CacheContext) => void - invalidateCacheDown: (context: CacheContext) => void - stageBuild: (moduleName: string) => Promise - getStatus: () => Promise - deployServices: (params: DeployServicesParams) => Promise +export interface PluginContext extends WrappedFromGarden { + provider: Provider + providers: { [name: string]: Provider } } -export function createPluginContext(garden: Garden): PluginContext { - function wrap(f) { - return f.bind(garden) - } +// NOTE: this is used more for documentation than validation, outside of internal testing +// TODO: validate the output from createPluginContext against this schema (in tests) +export const pluginContextSchema = Joi.object() + .options({ presence: "required" }) + .keys({ + projectName: projectNameSchema, + projectRoot: Joi.string() + .uri({ relativeOnly: true }) + .description("The absolute path of the project root."), + projectSources: projectSourcesSchema, + localConfigStore: Joi.object() + .description("Helper class for managing local configuration for plugins."), + environment: environmentSchema, + provider: providerSchema + .description("The provider being used for this context."), + providers: joiIdentifierMap(providerSchema) + .description("Map of all configured providers for the current environment and project."), + }) - const projectConfig = { ...garden.environmentConfig } +export function createPluginContext(garden: Garden, providerName: string): PluginContext { + const projectConfig = cloneDeep(garden.environment) const providerConfigs = keyBy(projectConfig.providers, "name") - const providers = mapValues(providerConfigs, (config, name) => ({ - name, - config, - })) - - function getProvider(handler): Provider { - return providers[handler["pluginName"]] - } + const providers = mapValues(providerConfigs, (config, name) => ({ name, config })) + let provider = providers[providerName] - // TODO: find a nicer way to do this (like a type-safe wrapper function) - function commonParams(handler): PluginActionParamsBase { - return { - ctx: createPluginContext(garden), - env: garden.getEnvironment(), - provider: getProvider(handler), - } + if (providerName === "_default") { + provider = defaultProvider } - async function getModuleAndHandler( - { moduleName, actionType, pluginName, defaultHandler }: - { moduleName: string, actionType: T, pluginName?: string, defaultHandler?: (ModuleActions & ServiceActions)[T] }, - ): Promise<{ handler: (ModuleActions & ServiceActions)[T], module: Module }> { - const module = await garden.getModule(moduleName) - const handler = garden.getModuleActionHandler({ - actionType, - moduleType: module.type, - pluginName, - defaultHandler, - }) - - return { handler, module } - } - - async function callModuleHandler>( - { params, actionType, defaultHandler }: - { params: PluginContextModuleParams, actionType: T, defaultHandler?: ModuleActions[T] }, - ): Promise { - const { moduleName, pluginName } = params - const { module, handler } = await getModuleAndHandler({ - moduleName, - actionType, - pluginName, - defaultHandler, - }) - const handlerParams: ModuleActionParams[T] = { - ...commonParams(handler), - ...omit(params, ["moduleName"]), - module, - } - // TODO: figure out why this doesn't compile without the function cast - return (handler)(handlerParams) + if (!provider) { + throw new PluginError(`Could not find provider '${providerName}'`, { providerName, providers }) } - async function callServiceHandler( - { params, actionType, defaultHandler }: - { params: PluginContextServiceParams, actionType: T, defaultHandler?: ServiceActions[T] }, - ): Promise { - const service = await garden.getService(params.serviceName) - - const { module, handler } = await getModuleAndHandler({ - moduleName: service.module.name, - actionType, - pluginName: params.pluginName, - defaultHandler, - }) - - service.module = module - - // TODO: figure out why this doesn't compile without the casts - const deps = await garden.getServices(service.config.dependencies) - const runtimeContext = ((params).runtimeContext || await prepareRuntimeContext(ctx, module, deps)) - - const handlerParams: any = { - ...commonParams(handler), - ...omit(params, ["moduleName"]), - module, - service, - runtimeContext, - } - - return (handler)(handlerParams) - } - - const ctx: PluginContext = { + return { projectName: garden.projectName, projectRoot: garden.projectRoot, - projectSources: garden.projectSources, - log: garden.log, - environmentConfig: projectConfig, + projectSources: cloneDeep(garden.projectSources), + environment: projectConfig, localConfigStore: garden.localConfigStore, - vcs: garden.vcs, + provider, providers, - - getEnvironment: wrap(garden.getEnvironment), - getModules: wrap(garden.getModules), - getModule: wrap(garden.getModule), - getServices: wrap(garden.getServices), - getService: wrap(garden.getService), - resolveModuleDependencies: wrap(garden.resolveModuleDependencies), - resolveVersion: wrap(garden.resolveVersion), - - //=========================================================================== - //region Environment Actions - //=========================================================================== - - getEnvironmentStatus: async ({ pluginName }: PluginContextParams) => { - const handlers = garden.getActionHandlers("getEnvironmentStatus", pluginName) - return Bluebird.props(mapValues(handlers, h => h({ ...commonParams(h) }))) - }, - - configureEnvironment: async ({ force = false, pluginName }: { force?: boolean, pluginName?: string }) => { - const handlers = garden.getActionHandlers("configureEnvironment", pluginName) - - const statuses = await ctx.getEnvironmentStatus({}) - - await Bluebird.each(toPairs(handlers), async ([name, handler]) => { - const status = statuses[name] || { configured: false } - - if (status.configured && !force) { - return - } - - const logEntry = garden.log.info({ - status: "active", - section: name, - msg: "Configuring...", - }) - - await handler({ ...commonParams(handler), force, status, logEntry }) - - logEntry.setSuccess("Configured") - }) - return ctx.getEnvironmentStatus({}) - }, - - destroyEnvironment: async ({ pluginName }: PluginContextParams) => { - const handlers = garden.getActionHandlers("destroyEnvironment", pluginName) - await Bluebird.each(values(handlers), h => h({ ...commonParams(h) })) - return ctx.getEnvironmentStatus({}) - }, - - getConfig: async ({ key, pluginName }: PluginContextParams) => { - garden.validateConfigKey(key) - // TODO: allow specifying which provider to use for configs - const handler = garden.getActionHandler({ actionType: "getConfig", pluginName }) - return handler({ ...commonParams(handler), key }) - }, - - setConfig: async ({ key, value, pluginName }: PluginContextParams) => { - garden.validateConfigKey(key) - const handler = garden.getActionHandler({ actionType: "setConfig", pluginName }) - return handler({ ...commonParams(handler), key, value }) - }, - - deleteConfig: async ({ key, pluginName }: PluginContextParams) => { - garden.validateConfigKey(key) - const handler = garden.getActionHandler({ actionType: "deleteConfig", pluginName }) - return handler({ ...commonParams(handler), key }) - }, - - getLoginStatus: async ({ pluginName }: PluginContextParams) => { - const handlers = garden.getActionHandlers("getLoginStatus", pluginName) - return Bluebird.props(mapValues(handlers, h => h({ ...commonParams(h) }))) - }, - - login: async ({ pluginName }: PluginContextParams) => { - const handlers = garden.getActionHandlers("login", pluginName) - await Bluebird.each(values(handlers), h => h({ ...commonParams(h) })) - return ctx.getLoginStatus({}) - }, - - logout: async ({ pluginName }: PluginContextParams) => { - const handlers = garden.getActionHandlers("logout", pluginName) - await Bluebird.each(values(handlers), h => h({ ...commonParams(h) })) - return ctx.getLoginStatus({}) - }, - - //endregion - - //=========================================================================== - //region Module Actions - //=========================================================================== - - getModuleBuildStatus: async ( - params: PluginContextModuleParams>, - ) => { - return callModuleHandler({ - params, - actionType: "getModuleBuildStatus", - defaultHandler: async () => ({ ready: false }), - }) - }, - - buildModule: async (params: PluginContextModuleParams>) => { - const module = await garden.getModule(params.moduleName) - await garden.buildDir.syncDependencyProducts(module) - return callModuleHandler({ params, actionType: "buildModule" }) - }, - - pushModule: async (params: PluginContextModuleParams>) => { - return callModuleHandler({ params, actionType: "pushModule", defaultHandler: dummyPushHandler }) - }, - - runModule: async (params: PluginContextModuleParams>) => { - return callModuleHandler({ params, actionType: "runModule" }) - }, - - testModule: async (params: PluginContextModuleParams>) => { - return callModuleHandler({ params, actionType: "testModule" }) - }, - - getTestResult: async (params: PluginContextModuleParams>) => { - return callModuleHandler({ - params, - actionType: "getTestResult", - defaultHandler: async () => null, - }) - }, - - //endregion - - //=========================================================================== - //region Service Actions - //=========================================================================== - - getServiceStatus: async (params: PluginContextServiceParams) => { - return callServiceHandler({ params, actionType: "getServiceStatus" }) - }, - - deployService: async (params: PluginContextServiceParams) => { - return callServiceHandler({ params, actionType: "deployService" }) - }, - - deleteService: async (params: PluginContextServiceParams) => { - const logEntry = garden.log.info({ - section: params.serviceName, - msg: "Deleting...", - status: "active", - }) - return callServiceHandler({ - params: { ...params, logEntry }, - actionType: "deleteService", - defaultHandler: dummyDeleteServiceHandler, - }) - }, - - getServiceOutputs: async (params: PluginContextServiceParams) => { - return callServiceHandler({ - params, - actionType: "getServiceOutputs", - defaultHandler: async () => ({}), - }) - }, - - execInService: async (params: PluginContextServiceParams) => { - return callServiceHandler({ params, actionType: "execInService" }) - }, - - getServiceLogs: async (params: PluginContextServiceParams) => { - return callServiceHandler({ params, actionType: "getServiceLogs", defaultHandler: dummyLogStreamer }) - }, - - runService: async (params: PluginContextServiceParams) => { - return callServiceHandler({ params, actionType: "runService" }) - }, - - //endregion - - //=========================================================================== - //region Helper Methods - //=========================================================================== - - invalidateCache: (context: CacheContext) => { - garden.cache.invalidate(context) - }, - - invalidateCacheUp: (context: CacheContext) => { - garden.cache.invalidateUp(context) - }, - - invalidateCacheDown: (context: CacheContext) => { - garden.cache.invalidateDown(context) - }, - - stageBuild: async (moduleName: string) => { - const module = await garden.getModule(moduleName) - await garden.buildDir.syncDependencyProducts(module) - }, - - getStatus: async () => { - const envStatus: EnvironmentStatusMap = await ctx.getEnvironmentStatus({}) - const services = keyBy(await ctx.getServices(), "name") - - const serviceStatus = await Bluebird.props(mapValues(services, async (service: Service) => { - const dependencies = await ctx.getServices(service.config.dependencies) - const runtimeContext = await prepareRuntimeContext(ctx, service.module, dependencies) - return ctx.getServiceStatus({ serviceName: service.name, runtimeContext }) - })) - - return { - providers: envStatus, - services: serviceStatus, - } - }, - - deployServices: async ({ serviceNames, force = false, forceBuild = false }: DeployServicesParams) => { - const services = await ctx.getServices(serviceNames) - - return processServices({ - services, - garden, - ctx, - watch: false, - handler: async (module) => getDeployTasks({ - ctx, module, serviceNames, force, forceBuild, includeDependants: false, - }), - }) - }, - - //endregion - } - - return ctx -} - -const dummyLogStreamer = async ({ ctx, service }: GetServiceLogsParams) => { - ctx.log.warn({ - section: service.name, - msg: chalk.yellow(`No handler for log retrieval available for module type ${service.module.type}`), - }) - return {} -} - -const dummyPushHandler = async ({ module }: PushModuleParams) => { - return { pushed: false, message: chalk.yellow(`No push handler available for module type ${module.type}`) } -} - -const dummyDeleteServiceHandler = async ({ ctx, module, logEntry }: DeleteServiceParams) => { - const msg = `No delete service handler available for module type ${module.type}` - if (logEntry) { - logEntry.setError(msg) - } else { - try { - ctx.log.error(msg) - } catch (err) { - console.log("FAIL", err) - } } - return {} } diff --git a/garden-cli/src/plugins/container.ts b/garden-cli/src/plugins/container.ts index 796fb26427..a00c8d5583 100644 --- a/garden-cli/src/plugins/container.ts +++ b/garden-cli/src/plugins/container.ts @@ -25,10 +25,9 @@ import { } from "../types/plugin/plugin" import { BuildModuleParams, - GetModuleBuildStatusParams, - ParseModuleParams, + GetBuildStatusParams, + ValidateModuleParams, PushModuleParams, - RunServiceParams, } from "../types/plugin/params" import { Service, endpointHostnameSchema } from "../types/service" import { DEFAULT_PORT_PROTOCOL } from "../constants" @@ -268,7 +267,7 @@ export const helpers = { }, } -export async function parseContainerModule({ moduleConfig }: ParseModuleParams) { +export async function validateContainerModule({ moduleConfig }: ValidateModuleParams) { moduleConfig.spec = validate(moduleConfig.spec, containerModuleSpecSchema, { context: `module ${moduleConfig.name}` }) // validate services @@ -341,9 +340,9 @@ export async function parseContainerModule({ moduleConfig }: ParseModuleParams ({ moduleActions: { container: { - parseModule: parseContainerModule, + validate: validateContainerModule, - async getModuleBuildStatus({ module, logEntry }: GetModuleBuildStatusParams) { + async getBuildStatus({ module, logEntry }: GetBuildStatusParams) { const identifier = await helpers.imageExistsLocally(module) if (identifier) { @@ -357,7 +356,7 @@ export const gardenPlugin = (): GardenPlugin => ({ return { ready: !!identifier } }, - async buildModule({ module, logEntry }: BuildModuleParams) { + async build({ module, logEntry }: BuildModuleParams) { const buildPath = module.buildPath const image = await getImage(module) @@ -410,19 +409,6 @@ export const gardenPlugin = (): GardenPlugin => ({ return { pushed: true } }, - - async runService( - { ctx, service, interactive, runtimeContext, silent, timeout }: RunServiceParams, - ) { - return ctx.runModule({ - moduleName: service.module.name, - command: service.spec.command || [], - interactive, - runtimeContext, - silent, - timeout, - }) - }, }, }, }) diff --git a/garden-cli/src/plugins/generic.ts b/garden-cli/src/plugins/generic.ts index 3a9d16e196..6532cddb21 100644 --- a/garden-cli/src/plugins/generic.ts +++ b/garden-cli/src/plugins/generic.ts @@ -21,13 +21,13 @@ import { Module } from "../types/module" import { BuildResult, BuildStatus, - ParseModuleResult, + ValidateModuleResult, TestResult, } from "../types/plugin/outputs" import { BuildModuleParams, - GetModuleBuildStatusParams, - ParseModuleParams, + GetBuildStatusParams, + ValidateModuleParams, TestModuleParams, } from "../types/plugin/params" import { BaseServiceSpec } from "../config/service" @@ -70,8 +70,8 @@ export const genericModuleSpecSchema = Joi.object() export interface GenericModule extends Module { } export async function parseGenericModule( - { moduleConfig }: ParseModuleParams, -): Promise { + { moduleConfig }: ValidateModuleParams, +): Promise { moduleConfig.spec = validate(moduleConfig.spec, genericModuleSpecSchema, { context: `module ${moduleConfig.name}` }) moduleConfig.testConfigs = moduleConfig.spec.tests.map(t => ({ @@ -84,7 +84,7 @@ export async function parseGenericModule( return moduleConfig } -export async function getGenericModuleBuildStatus({ module }: GetModuleBuildStatusParams): Promise { +export async function getGenericModuleBuildStatus({ module }: GetBuildStatusParams): Promise { const buildVersionFilePath = join(module.buildPath, GARDEN_BUILD_VERSION_FILENAME) let builtVersion: ModuleVersion | null = null @@ -160,9 +160,9 @@ export async function testGenericModule({ module, testConfig }: TestModuleParams export const genericPlugin: GardenPlugin = { moduleActions: { generic: { - parseModule: parseGenericModule, - getModuleBuildStatus: getGenericModuleBuildStatus, - buildModule: buildGenericModule, + validate: parseGenericModule, + getBuildStatus: getGenericModuleBuildStatus, + build: buildGenericModule, testModule: testGenericModule, }, }, diff --git a/garden-cli/src/plugins/google/common.ts b/garden-cli/src/plugins/google/common.ts index 7e00044aa8..df63ace505 100644 --- a/garden-cli/src/plugins/google/common.ts +++ b/garden-cli/src/plugins/google/common.ts @@ -7,14 +7,14 @@ */ import { Module } from "../../types/module" -import { ConfigureEnvironmentParams } from "../../types/plugin/params" +import { PrepareEnvironmentParams } from "../../types/plugin/params" import { Service } from "../../types/service" import { ConfigurationError } from "../../exceptions" import { GenericTestSpec } from "../generic" import { GCloud } from "./gcloud" -import { Provider } from "../../types/plugin/plugin" import { ModuleSpec } from "../../config/module" import { BaseServiceSpec } from "../../config/service" +import { Provider } from "../../config/project" export const GOOGLE_CLOUD_DEFAULT_REGION = "us-central1" @@ -32,7 +32,7 @@ export async function getEnvironmentStatus() { let sdkInfo const output = { - configured: true, + ready: true, detail: { sdkInstalled: true, sdkInitialized: true, @@ -44,24 +44,24 @@ export async function getEnvironmentStatus() { try { sdkInfo = output.detail.sdkInfo = await gcloud().json(["info"]) } catch (err) { - output.configured = false + output.ready = false output.detail.sdkInstalled = false } if (!sdkInfo.config.account) { - output.configured = false + output.ready = false output.detail.sdkInitialized = false } if (!sdkInfo.installation.components.beta) { - output.configured = false + output.ready = false output.detail.betaComponentsInstalled = false } return output } -export async function configureEnvironment({ ctx, status }: ConfigureEnvironmentParams) { +export async function prepareEnvironment({ status, logEntry }: PrepareEnvironmentParams) { if (!status.detail.sdkInstalled) { throw new ConfigurationError( "Google Cloud SDK is not installed. " + @@ -71,7 +71,7 @@ export async function configureEnvironment({ ctx, status }: ConfigureEnvironment } if (!status.detail.betaComponentsInstalled) { - ctx.log.info({ + logEntry && logEntry.info({ section: "google-cloud-functions", msg: `Installing gcloud SDK beta components...`, }) @@ -80,7 +80,7 @@ export async function configureEnvironment({ ctx, status }: ConfigureEnvironment } if (!status.detail.sdkInitialized) { - ctx.log.info({ + logEntry && logEntry.info({ section: "google-cloud-functions", msg: `Initializing SDK...`, }) diff --git a/garden-cli/src/plugins/google/google-app-engine.ts b/garden-cli/src/plugins/google/google-app-engine.ts index 2a9985e072..d29255376c 100644 --- a/garden-cli/src/plugins/google/google-app-engine.ts +++ b/garden-cli/src/plugins/google/google-app-engine.ts @@ -19,7 +19,7 @@ import { import { getEnvironmentStatus, GOOGLE_CLOUD_DEFAULT_REGION, - configureEnvironment, + prepareEnvironment, } from "./common" import { ContainerModule, @@ -40,7 +40,7 @@ export interface GoogleAppEngineModule extends ContainerModule ({ actions: { getEnvironmentStatus, - configureEnvironment, + prepareEnvironment, }, moduleActions: { container: { @@ -55,8 +55,8 @@ export const gardenPlugin = (): GardenPlugin => ({ return {} }, - async deployService({ ctx, service, runtimeContext, provider }: DeployServiceParams) { - ctx.log.info({ + async deployService({ ctx, service, runtimeContext, logEntry }: DeployServiceParams) { + logEntry && logEntry.info({ section: service.name, msg: `Deploying app...`, }) @@ -72,7 +72,7 @@ export const gardenPlugin = (): GardenPlugin => ({ if (config.healthCheck) { if (config.healthCheck.tcpPort || config.healthCheck.command) { - ctx.log.warn({ + logEntry && logEntry.warn({ section: service.name, msg: "GAE only supports httpGet health checks", }) @@ -88,20 +88,20 @@ export const gardenPlugin = (): GardenPlugin => ({ await dumpYaml(appYamlPath, appYaml) // deploy to GAE - const project = getProject(service, provider) + const project = getProject(service, ctx.provider) await gcloud(project).call([ "app", "deploy", "--quiet", ], { cwd: service.module.path }) - ctx.log.info({ section: service.name, msg: `App deployed` }) + logEntry && logEntry.info({ section: service.name, msg: `App deployed` }) return {} }, - async getServiceOutputs({ service, provider }: GetServiceOutputsParams) { + async getServiceOutputs({ ctx, service }: GetServiceOutputsParams) { // TODO: we may want to pull this from the service status instead, along with other outputs - const project = getProject(service, provider) + const project = getProject(service, ctx.provider) return { endpoint: `https://${GOOGLE_CLOUD_DEFAULT_REGION}-${project}.cloudfunctions.net/${service.name}`, diff --git a/garden-cli/src/plugins/google/google-cloud-functions.ts b/garden-cli/src/plugins/google/google-cloud-functions.ts index 23efea18ef..969d7419d8 100644 --- a/garden-cli/src/plugins/google/google-cloud-functions.ts +++ b/garden-cli/src/plugins/google/google-cloud-functions.ts @@ -11,12 +11,12 @@ import { validate, } from "../../config/common" import { Module } from "../../types/module" -import { ParseModuleResult } from "../../types/plugin/outputs" +import { ValidateModuleResult } from "../../types/plugin/outputs" import { DeployServiceParams, GetServiceOutputsParams, GetServiceStatusParams, - ParseModuleParams, + ValidateModuleParams, } from "../../types/plugin/params" import { ServiceState, ServiceStatus, endpointHostnameSchema } from "../../types/service" import { @@ -26,7 +26,7 @@ import * as Joi from "joi" import { GARDEN_ANNOTATION_KEYS_VERSION } from "../../constants" import { GenericTestSpec, genericTestSchema } from "../generic" import { - configureEnvironment, + prepareEnvironment, gcloud, getEnvironmentStatus, getProject, @@ -76,8 +76,8 @@ const gcfModuleSpecSchema = Joi.object() export interface GcfModule extends Module { } export async function parseGcfModule( - { moduleConfig }: ParseModuleParams, -): Promise> { + { moduleConfig }: ValidateModuleParams, +): Promise> { // TODO: check that each function exists at the specified path moduleConfig.spec = validate( moduleConfig.spec, gcfModuleSpecSchema, { context: `module ${moduleConfig.name}` }, @@ -103,17 +103,17 @@ export async function parseGcfModule( export const gardenPlugin = (): GardenPlugin => ({ actions: { getEnvironmentStatus, - configureEnvironment, + prepareEnvironment, }, moduleActions: { "google-cloud-function": { - parseModule: parseGcfModule, + validate: parseGcfModule, async deployService( - { ctx, provider, module, service, env, runtimeContext }: DeployServiceParams, + { ctx, module, service, runtimeContext, logEntry }: DeployServiceParams, ) { // TODO: provide env vars somehow to function - const project = getProject(service, provider) + const project = getProject(service, ctx.provider) const functionPath = resolve(service.module.path, service.spec.path) const entrypoint = service.spec.entrypoint || service.name @@ -126,12 +126,12 @@ export const gardenPlugin = (): GardenPlugin => ({ "--trigger-http", ]) - return getServiceStatus({ ctx, provider, module, service, env, runtimeContext }) + return getServiceStatus({ ctx, module, service, runtimeContext, logEntry }) }, - async getServiceOutputs({ service, provider }: GetServiceOutputsParams) { + async getServiceOutputs({ ctx, service }: GetServiceOutputsParams) { // TODO: we may want to pull this from the service status instead, along with other outputs - const project = getProject(service, provider) + const project = getProject(service, ctx.provider) return { endpoint: `https://${GOOGLE_CLOUD_DEFAULT_REGION}-${project}.cloudfunctions.net/${service.name}`, @@ -142,9 +142,9 @@ export const gardenPlugin = (): GardenPlugin => ({ }) export async function getServiceStatus( - { service, provider }: GetServiceStatusParams, + { ctx, service }: GetServiceStatusParams, ): Promise { - const project = getProject(service, provider) + const project = getProject(service, ctx.provider) const functions: any[] = await gcloud(project).json(["beta", "functions", "list"]) const providerId = `projects/${project}/locations/${GOOGLE_CLOUD_DEFAULT_REGION}/functions/${service.name}` diff --git a/garden-cli/src/plugins/kubernetes/actions.ts b/garden-cli/src/plugins/kubernetes/actions.ts index 6d54ceb557..0ea8840308 100644 --- a/garden-cli/src/plugins/kubernetes/actions.ts +++ b/garden-cli/src/plugins/kubernetes/actions.ts @@ -17,23 +17,24 @@ import { DeploymentError, NotFoundError, TimeoutError, ConfigurationError } from import { GetServiceLogsResult, LoginStatus } from "../../types/plugin/outputs" import { RunResult, TestResult } from "../../types/plugin/outputs" import { - ConfigureEnvironmentParams, - DeleteConfigParams, - DestroyEnvironmentParams, + PrepareEnvironmentParams, + DeleteSecretParams, + CleanupEnvironmentParams, ExecInServiceParams, - GetConfigParams, + GetSecretParams, GetEnvironmentStatusParams, GetServiceLogsParams, GetServiceOutputsParams, GetTestResultParams, PluginActionParamsBase, RunModuleParams, - SetConfigParams, + SetSecretParams, TestModuleParams, DeleteServiceParams, + RunServiceParams, } from "../../types/plugin/params" import { ModuleVersion } from "../../vcs/base" -import { ContainerModule, helpers, parseContainerModule } from "../container" +import { ContainerModule, helpers, validateContainerModule } from "../container" import { deserializeValues, serializeValues, splitFirst, sleep } from "../../util/util" import { joiIdentifier } from "../../config/common" import { KubeApi } from "./api" @@ -47,15 +48,15 @@ import { DEFAULT_TEST_TIMEOUT } from "../../constants" import { KubernetesProvider, name as providerName } from "./kubernetes" import { deleteContainerService, getContainerServiceStatus } from "./deployment" import { ServiceStatus } from "../../types/service" -import { ParseModuleParams } from "../../types/plugin/params" +import { ValidateModuleParams } from "../../types/plugin/params" const MAX_STORED_USERNAMES = 5 -export async function parseModule(params: ParseModuleParams) { - const config = await parseContainerModule(params) +export async function validate(params: ValidateModuleParams) { + const config = await validateContainerModule(params) // validate endpoint specs - const provider: KubernetesProvider = params.provider + const provider: KubernetesProvider = params.ctx.provider for (const serviceConfig of config.serviceConfigs) { for (const endpointSpec of serviceConfig.spec.endpoints) { @@ -78,8 +79,8 @@ export async function parseModule(params: ParseModuleParams) { } } -export async function getEnvironmentStatus({ ctx, provider }: GetEnvironmentStatusParams) { - const context = provider.config.context +export async function getEnvironmentStatus({ ctx }: GetEnvironmentStatusParams) { + const context = ctx.provider.config.context try { // TODO: use API instead of kubectl (I just couldn't find which API call to make) @@ -100,25 +101,25 @@ export async function getEnvironmentStatus({ ctx, provider }: GetEnvironmentStat } await Bluebird.all([ - getMetadataNamespace(ctx, provider), - getAppNamespace(ctx, provider), + getMetadataNamespace(ctx, ctx.provider), + getAppNamespace(ctx, ctx.provider), ]) return { - configured: true, + ready: true, detail: {}, } } -export async function configureEnvironment({ }: ConfigureEnvironmentParams) { +export async function prepareEnvironment({ }: PrepareEnvironmentParams) { // this happens implicitly in the `getEnvironmentStatus()` function return {} } -export async function destroyEnvironment({ ctx, provider }: DestroyEnvironmentParams) { - const api = new KubeApi(provider) - const namespace = await getAppNamespace(ctx, provider) - const entry = ctx.log.info({ +export async function cleanupEnvironment({ ctx, logEntry }: CleanupEnvironmentParams) { + const api = new KubeApi(ctx.provider) + const namespace = await getAppNamespace(ctx, ctx.provider) + const entry = logEntry && logEntry.info({ section: "kubernetes", msg: `Deleting namespace ${namespace} (this may take a while)`, status: "active", @@ -129,7 +130,7 @@ export async function destroyEnvironment({ ctx, provider }: DestroyEnvironmentPa // TODO: any cast is required until https://github.com/kubernetes-client/javascript/issues/52 is fixed await api.core.deleteNamespace(namespace, {}) } catch (err) { - entry.setError(err.message) + entry && entry.setError(err.message) const availableNamespaces = await getAllGardenNamespaces(api) throw new NotFoundError(err, { namespace, availableNamespaces }) } @@ -157,10 +158,10 @@ export async function destroyEnvironment({ ctx, provider }: DestroyEnvironmentPa } export async function deleteService(params: DeleteServiceParams): Promise { - const { ctx, logEntry, provider, service } = params - const namespace = await getAppNamespace(ctx, provider) + const { ctx, logEntry, service } = params + const namespace = await getAppNamespace(ctx, ctx.provider) - await deleteContainerService({ logEntry, namespace, provider, serviceName: service.name }) + await deleteContainerService({ provider: ctx.provider, logEntry, namespace, serviceName: service.name }) return getContainerServiceStatus(params) } @@ -171,12 +172,11 @@ export async function getServiceOutputs({ service }: GetServiceOutputsParams, -) { - const api = new KubeApi(provider) - const status = await getContainerServiceStatus({ ctx, provider, module, service, env, runtimeContext }) - const namespace = await getAppNamespace(ctx, provider) +export async function execInService(params: ExecInServiceParams) { + const { ctx, service, command } = params + const api = new KubeApi(ctx.provider) + const status = await getContainerServiceStatus(params) + const namespace = await getAppNamespace(ctx, ctx.provider) // TODO: this check should probably live outside of the plugin if (!status.state || status.state !== "ready") { @@ -218,10 +218,10 @@ export async function execInService( } export async function runModule( - { ctx, provider, module, command, interactive, runtimeContext, silent, timeout }: RunModuleParams, + { ctx, module, command, interactive, runtimeContext, silent, timeout }: RunModuleParams, ): Promise { - const context = provider.config.context - const namespace = await getAppNamespace(ctx, provider) + const context = ctx.provider.config.context + const namespace = await getAppNamespace(ctx, ctx.provider) const envArgs = Object.entries(runtimeContext.envVars).map(([k, v]) => `--env=${k}=${v}`) @@ -269,8 +269,24 @@ export async function runModule( } } +export async function runService( + { ctx, service, interactive, runtimeContext, silent, timeout, logEntry }: + RunServiceParams, +) { + return runModule({ + ctx, + module: service.module, + command: service.spec.command || [], + interactive, + runtimeContext, + silent, + timeout, + logEntry, + }) +} + export async function testModule( - { ctx, provider, env, interactive, module, runtimeContext, silent, testConfig }: + { ctx, interactive, module, runtimeContext, silent, testConfig, logEntry }: TestModuleParams, ): Promise { const testName = testConfig.name @@ -278,9 +294,18 @@ export async function testModule( runtimeContext.envVars = { ...runtimeContext.envVars, ...testConfig.spec.env } const timeout = testConfig.timeout || DEFAULT_TEST_TIMEOUT - const result = await runModule({ ctx, provider, env, module, command, interactive, runtimeContext, silent, timeout }) + const result = await runModule({ + ctx, + module, + command, + interactive, + runtimeContext, + silent, + timeout, + logEntry, + }) - const api = new KubeApi(provider) + const api = new KubeApi(ctx.provider) // store test result const testResult: TestResult = { @@ -288,7 +313,7 @@ export async function testModule( testName, } - const ns = await getMetadataNamespace(ctx, provider) + const ns = await getMetadataNamespace(ctx, ctx.provider) const resultKey = getTestResultKey(module, testName, result.version) const body = { apiVersion: "v1", @@ -316,10 +341,10 @@ export async function testModule( } export async function getTestResult( - { ctx, provider, module, testName, version }: GetTestResultParams, + { ctx, module, testName, version }: GetTestResultParams, ) { - const api = new KubeApi(provider) - const ns = await getMetadataNamespace(ctx, provider) + const api = new KubeApi(ctx.provider) + const ns = await getMetadataNamespace(ctx, ctx.provider) const resultKey = getTestResultKey(module, testName, version) try { @@ -335,9 +360,9 @@ export async function getTestResult( } export async function getServiceLogs( - { ctx, provider, service, stream, tail }: GetServiceLogsParams, + { ctx, service, stream, tail }: GetServiceLogsParams, ) { - const context = provider.config.context + const context = ctx.provider.config.context const resourceType = service.spec.daemon ? "daemonset" : "deployment" const kubectlArgs = ["logs", `${resourceType}/${service.name}`, "--timestamps=true"] @@ -346,7 +371,7 @@ export async function getServiceLogs( kubectlArgs.push("--follow") } - const namespace = await getAppNamespace(ctx, provider) + const namespace = await getAppNamespace(ctx, ctx.provider) const proc = kubectl(context, namespace).spawn(kubectlArgs) let timestamp: Date @@ -374,12 +399,12 @@ export async function getServiceLogs( }) } -export async function getConfig({ ctx, provider, key }: GetConfigParams) { - const api = new KubeApi(provider) - const ns = await getMetadataNamespace(ctx, provider) +export async function getSecret({ ctx, key }: GetSecretParams) { + const api = new KubeApi(ctx.provider) + const ns = await getMetadataNamespace(ctx, ctx.provider) try { - const res = await api.core.readNamespacedSecret(key.join("."), ns) + const res = await api.core.readNamespacedSecret(key, ns) return { value: Buffer.from(res.body.data.value, "base64").toString() } } catch (err) { if (err.code === 404) { @@ -390,17 +415,16 @@ export async function getConfig({ ctx, provider, key }: GetConfigParams) { } } -export async function setConfig({ ctx, provider, key, value }: SetConfigParams) { +export async function setSecret({ ctx, key, value }: SetSecretParams) { // we store configuration in a separate metadata namespace, so that configs aren't cleared when wiping the namespace - const api = new KubeApi(provider) - const ns = await getMetadataNamespace(ctx, provider) - const name = key.join(".") + const api = new KubeApi(ctx.provider) + const ns = await getMetadataNamespace(ctx, ctx.provider) const body = { body: { apiVersion: "v1", kind: "Secret", metadata: { - name, + name: key, annotations: { "garden.io/generated": "true", }, @@ -414,7 +438,7 @@ export async function setConfig({ ctx, provider, key, value }: SetConfigParams) await api.core.createNamespacedSecret(ns, body) } catch (err) { if (err.code === 409) { - await api.core.patchNamespacedSecret(name, ns, body) + await api.core.patchNamespacedSecret(key, ns, body) } else { throw err } @@ -423,13 +447,12 @@ export async function setConfig({ ctx, provider, key, value }: SetConfigParams) return {} } -export async function deleteConfig({ ctx, provider, key }: DeleteConfigParams) { - const api = new KubeApi(provider) - const ns = await getMetadataNamespace(ctx, provider) - const name = key.join(".") +export async function deleteSecret({ ctx, key }: DeleteSecretParams) { + const api = new KubeApi(ctx.provider) + const ns = await getMetadataNamespace(ctx, ctx.provider) try { - await api.core.deleteNamespacedSecret(name, ns, {}) + await api.core.deleteNamespacedSecret(key, ns, {}) } catch (err) { if (err.code === 404) { return { found: false } @@ -449,8 +472,8 @@ export async function getLoginStatus({ ctx }: PluginActionParamsBase): Promise { - const entry = ctx.log.info({ section: "kubernetes", msg: "Logging in..." }) +export async function login({ ctx, logEntry }: PluginActionParamsBase): Promise { + const entry = logEntry && logEntry.info({ section: "kubernetes", msg: "Logging in..." }) const localConfig = await ctx.localConfigStore.get() let currentUsername @@ -462,7 +485,7 @@ export async function login({ ctx }: PluginActionParamsBase): Promise { - const entry = ctx.log.info({ section: "kubernetes", msg: "Logging out..." }) +export async function logout({ ctx, logEntry }: PluginActionParamsBase): Promise { + const entry = logEntry && logEntry.info({ section: "kubernetes", msg: "Logging out..." }) const localConfig = await ctx.localConfigStore.get() const k8sConfig = localConfig.kubernetes || {} if (k8sConfig.username) { await ctx.localConfigStore.delete([providerName, "username"]) - entry.setSuccess("Logged out") + entry && entry.setSuccess("Logged out") } else { - entry.setSuccess("Already logged out") + entry && entry.setSuccess("Already logged out") } return { loggedIn: false } } diff --git a/garden-cli/src/plugins/kubernetes/api.ts b/garden-cli/src/plugins/kubernetes/api.ts index 3b05c2b568..3f00833930 100644 --- a/garden-cli/src/plugins/kubernetes/api.ts +++ b/garden-cli/src/plugins/kubernetes/api.ts @@ -81,7 +81,7 @@ export class KubeApi { constructor(public provider: KubernetesProvider) { this.context = provider.config.context - const config = getConfig(this.context) + const config = getSecret(this.context) for (const [name, cls] of Object.entries(apiTypes)) { const api = new cls(config.getCurrentCluster().server) @@ -207,7 +207,7 @@ export class KubeApi { } } -function getConfig(context: string): KubeConfig { +function getSecret(context: string): KubeConfig { if (!kubeConfigStr) { const kubeConfigPath = process.env.KUBECONFIG || join(homedir(), ".kube", "config") kubeConfigStr = readFileSync(kubeConfigPath).toString() diff --git a/garden-cli/src/plugins/kubernetes/deployment.ts b/garden-cli/src/plugins/kubernetes/deployment.ts index d5d8720c07..9852b8c78e 100644 --- a/garden-cli/src/plugins/kubernetes/deployment.ts +++ b/garden-cli/src/plugins/kubernetes/deployment.ts @@ -28,7 +28,6 @@ import { KubernetesObject } from "./helm" import { PluginContext } from "../../plugin-context" import { KubernetesProvider } from "./kubernetes" import { GARDEN_ANNOTATION_KEYS_VERSION } from "../../constants" -import { Provider } from "../../types/plugin/plugin" import { KubeApi } from "./api" import { LogEntry } from "../../logger/log-entry" @@ -44,13 +43,13 @@ interface KubeEnvVar { } export async function getContainerServiceStatus( - { ctx, provider, module, service, runtimeContext }: GetServiceStatusParams, + { ctx, module, service, runtimeContext }: GetServiceStatusParams, ): Promise { // TODO: hash and compare all the configuration files (otherwise internal changes don't get deployed) const version = module.version - const objects = await createContainerObjects(ctx, provider, service, runtimeContext) - const matched = await compareDeployedObjects(ctx, provider, objects) - const api = new KubeApi(provider) + const objects = await createContainerObjects(ctx, service, runtimeContext) + const matched = await compareDeployedObjects(ctx, objects) + const api = new KubeApi(ctx.provider) const endpoints = await getEndpoints(service, api) return { @@ -61,10 +60,11 @@ export async function getContainerServiceStatus( } export async function deployContainerService(params: DeployServiceParams): Promise { - const { ctx, provider, service, runtimeContext, force, logEntry } = params + const { ctx, service, runtimeContext, force, logEntry } = params + const provider = ctx.provider const namespace = await getAppNamespace(ctx, provider) - const objects = await createContainerObjects(ctx, provider, service, runtimeContext) + const objects = await createContainerObjects(ctx, service, runtimeContext) // TODO: use Helm instead of kubectl apply const pruneSelector = "service=" + service.name @@ -75,9 +75,10 @@ export async function deployContainerService(params: DeployServiceParams> = { - async parseModule({ moduleConfig }: ParseModuleParams): Promise { +export const helmHandlers: Partial> = { + async validate({ moduleConfig }: ValidateModuleParams): Promise { moduleConfig.spec = validate( moduleConfig.spec, helmModuleSpecSchema, @@ -133,19 +130,20 @@ export const helmHandlers: Partial> = { return moduleConfig }, - getModuleBuildStatus: getGenericModuleBuildStatus, - buildModule, + getBuildStatus: getGenericModuleBuildStatus, + build, getServiceStatus, async deployService( - { ctx, provider, module, service, logEntry }: DeployServiceParams, + { ctx, module, service, logEntry }: DeployServiceParams, ): Promise { + const provider = ctx.provider const chartPath = await getChartPath(module) const valuesPath = getValuesPath(chartPath) const releaseName = getReleaseName(ctx, service) - const namespace = await getAppNamespace(ctx, provider) + const namespace = await getAppNamespace(ctx, ctx.provider) - const releaseStatus = await getReleaseStatus(provider, releaseName) + const releaseStatus = await getReleaseStatus(ctx.provider, releaseName) if (releaseStatus.state === "missing") { await helm(provider, @@ -164,23 +162,23 @@ export const helmHandlers: Partial> = { ) } - const objects = await getChartObjects(ctx, provider, service) + const objects = await getChartObjects(ctx, service) await waitForObjects({ ctx, provider, service, objects, logEntry }) return {} }, async deleteService(params: DeleteServiceParams): Promise { - const { ctx, logEntry, provider, service } = params + const { ctx, logEntry, service } = params const releaseName = getReleaseName(ctx, service) - await helm(provider, "delete", "--purge", releaseName) + await helm(ctx.provider, "delete", "--purge", releaseName) logEntry && logEntry.setSuccess("Service deleted") return await getServiceStatus(params) }, } -async function buildModule({ ctx, provider, module, logEntry }: BuildModuleParams): Promise { +async function build({ ctx, module, logEntry }: BuildModuleParams): Promise { const buildPath = module.buildPath const config = module @@ -197,13 +195,13 @@ async function buildModule({ ctx, provider, module, logEntry }: BuildModuleParam fetchArgs.push("--repo", config.spec.repo) } logEntry && logEntry.setState("Fetching chart...") - await helm(provider, ...fetchArgs) + await helm(ctx.provider, ...fetchArgs) const chartPath = await getChartPath(module) // create the values.yml file (merge the configured parameters into the default values) logEntry && logEntry.setState("Preparing chart...") - const values = safeLoad(await helm(provider, "inspect", "values", chartPath)) || {} + const values = safeLoad(await helm(ctx.provider, "inspect", "values", chartPath)) || {} Object.entries(flattenValues(config.spec.parameters)) .map(([k, v]) => set(values, k, v)) @@ -211,10 +209,6 @@ async function buildModule({ ctx, provider, module, logEntry }: BuildModuleParam const valuesPath = getValuesPath(chartPath) dumpYaml(valuesPath, values) - // make sure the template renders okay - const services = await ctx.getServices(module.serviceNames) - await getChartObjects(ctx, provider, services[0]) - // keep track of which version has been built const buildVersionFilePath = join(buildPath, GARDEN_BUILD_VERSION_FILENAME) const version = module.version @@ -243,13 +237,13 @@ function getValuesPath(chartPath: string) { return join(chartPath, "garden-values.yml") } -async function getChartObjects(ctx: PluginContext, provider: Provider, service: Service) { +async function getChartObjects(ctx: PluginContext, service: Service) { const chartPath = await getChartPath(service.module) const valuesPath = getValuesPath(chartPath) - const namespace = await getAppNamespace(ctx, provider) + const namespace = await getAppNamespace(ctx, ctx.provider) const releaseName = getReleaseName(ctx, service) - const objects = safeLoadAll(await helm(provider, + const objects = safeLoadAll(await helm(ctx.provider, "template", "--name", releaseName, "--namespace", namespace, @@ -266,17 +260,17 @@ async function getChartObjects(ctx: PluginContext, provider: Provider, service: } async function getServiceStatus( - { ctx, env, provider, service, module, logEntry }: GetServiceStatusParams, + { ctx, service, module, logEntry }: GetServiceStatusParams, ): Promise { // need to build to be able to check the status - const buildStatus = await getGenericModuleBuildStatus({ ctx, env, provider, module, logEntry }) + const buildStatus = await getGenericModuleBuildStatus({ ctx, module, logEntry }) if (!buildStatus.ready) { - await buildModule({ ctx, env, provider, module, logEntry }) + await build({ ctx, module, logEntry }) } // first check if the installed objects on the cluster match the current code - const objects = await getChartObjects(ctx, provider, service) - const matched = await compareDeployedObjects(ctx, provider, objects) + const objects = await getChartObjects(ctx, service) + const matched = await compareDeployedObjects(ctx, objects) if (!matched) { return { state: "outdated" } @@ -284,8 +278,8 @@ async function getServiceStatus( // then check if the rollout is complete const version = module.version - const api = new KubeApi(provider) - const namespace = await getAppNamespace(ctx, provider) + const api = new KubeApi(ctx.provider) + const namespace = await getAppNamespace(ctx, ctx.provider) const { ready } = await checkObjectStatus(api, namespace, objects) // TODO: set state to "unhealthy" if any status is "unhealthy" diff --git a/garden-cli/src/plugins/kubernetes/kubernetes.ts b/garden-cli/src/plugins/kubernetes/kubernetes.ts index ef1543fa78..d0c1dec41c 100644 --- a/garden-cli/src/plugins/kubernetes/kubernetes.ts +++ b/garden-cli/src/plugins/kubernetes/kubernetes.ts @@ -14,31 +14,26 @@ import { joiIdentifier, validate, } from "../../config/common" +import { GardenPlugin } from "../../types/plugin/plugin" +import { Provider, providerConfigBaseSchema, ProviderConfig } from "../../config/project" import { - GardenPlugin, - Provider, -} from "../../types/plugin/plugin" -import { - ProviderConfig, - providerConfigBase, -} from "../../config/project" -import { - configureEnvironment, - deleteConfig, - destroyEnvironment, + prepareEnvironment, + deleteSecret, + cleanupEnvironment, deleteService, execInService, - getConfig, + getSecret, getEnvironmentStatus, getServiceLogs, getServiceOutputs, getTestResult, - setConfig, + setSecret, testModule, getLoginStatus, login, logout, runModule, + runService, } from "./actions" import { deployContainerService, getContainerServiceStatus } from "./deployment" import { helmHandlers } from "./helm" @@ -68,7 +63,7 @@ export interface KubernetesConfig extends ProviderConfig { tlsCertificates: IngressTlsCertificate[] } -export interface KubernetesProvider extends Provider { } +export type KubernetesProvider = Provider const secretRef = Joi.object() .keys({ @@ -103,7 +98,7 @@ const tlsCertificateSchema = Joi.object() .example({ name: "my-tls-secret", namespace: "default" }), }) -const kubernetesConfigBase = providerConfigBase +const kubernetesConfigBase = providerConfigBaseSchema .keys({ context: Joi.string() .required() @@ -154,11 +149,11 @@ export function gardenPlugin({ config }: { config: KubernetesConfig }): GardenPl config, actions: { getEnvironmentStatus, - configureEnvironment, - destroyEnvironment, - getConfig, - setConfig, - deleteConfig, + prepareEnvironment, + cleanupEnvironment, + getSecret, + setSecret, + deleteSecret, getLoginStatus, login, logout, @@ -172,6 +167,7 @@ export function gardenPlugin({ config }: { config: KubernetesConfig }): GardenPl execInService, runModule, testModule, + runService, getTestResult, getServiceLogs, }, diff --git a/garden-cli/src/plugins/kubernetes/local.ts b/garden-cli/src/plugins/kubernetes/local.ts index cdb9dcbfab..94b6478925 100644 --- a/garden-cli/src/plugins/kubernetes/local.ts +++ b/garden-cli/src/plugins/kubernetes/local.ts @@ -12,22 +12,21 @@ import { every, values } from "lodash" import * as Joi from "joi" import { join } from "path" import { PluginError } from "../../exceptions" -import { Environment, validate } from "../../config/common" -import { GardenPlugin, Provider } from "../../types/plugin/plugin" -import { ConfigureEnvironmentParams, GetEnvironmentStatusParams } from "../../types/plugin/params" -import { providerConfigBase } from "../../config/project" -import { findByName } from "../../util/util" -import { configureEnvironment, getEnvironmentStatus } from "./actions" +import { GardenPlugin } from "../../types/plugin/plugin" +import { GetEnvironmentStatusParams, PrepareEnvironmentParams } from "../../types/plugin/params" +import { getEnvironmentStatus, prepareEnvironment } from "./actions" +import { validate } from "../../config/common" import { gardenPlugin as k8sPlugin, KubernetesConfig, - KubernetesProvider, } from "./kubernetes" import { getSystemGarden, isSystemGarden } from "./system" import { readFile } from "fs-extra" import { LogEntry } from "../../logger/log-entry" import { homedir } from "os" import { helm } from "./helm" +import { PluginContext } from "../../plugin-context" +import { providerConfigBaseSchema } from "../../config/project" // TODO: split this into separate plugins to handle Docker for Mac and Minikube @@ -38,37 +37,34 @@ const kubeConfigPath = join(homedir(), ".kube", "config") // extend the environment configuration to also set up an ingress controller and dashboard export async function getLocalEnvironmentStatus( - { ctx, provider, env, logEntry }: GetEnvironmentStatusParams, + { ctx, logEntry }: GetEnvironmentStatusParams, ) { - const status = await getEnvironmentStatus({ ctx, provider, env, logEntry }) + const status = await getEnvironmentStatus({ ctx, logEntry }) - if (!isSystemGarden(provider)) { - const sysGarden = await getSystemGarden(provider) - const sysStatus = await sysGarden.getPluginContext().getStatus() + if (!isSystemGarden(ctx.provider)) { + const sysGarden = await getSystemGarden(ctx.provider) + const sysStatus = await sysGarden.actions.getStatus() - status.detail.systemReady = sysStatus.providers[provider.name].configured && + status.detail.systemReady = sysStatus.providers[ctx.provider.config.name].ready && every(values(sysStatus.services).map(s => s.state === "ready")) // status.detail.systemServicesStatus = sysStatus.services } - status.configured = every(values(status.detail)) + status.ready = every(values(status.detail)) return status } async function configureSystemEnvironment( - { provider, env, force, logEntry }: - { provider: LocalKubernetesProvider, env: Environment, force: boolean, logEntry?: LogEntry }, + { ctx, force, logEntry }: + { ctx: PluginContext, force: boolean, logEntry?: LogEntry }, ) { + const provider = ctx.provider const sysGarden = await getSystemGarden(provider) - const sysCtx = sysGarden.getPluginContext() - const sysProvider: KubernetesProvider = { - name: provider.name, - config: findByName(sysGarden.environmentConfig.providers, provider.name)!, - } + const sysCtx = sysGarden.getPluginContext(provider.name) // TODO: need to add logic here to wait for tiller to be ready - await helm(sysProvider, + await helm(sysCtx.provider, "init", "--wait", "--service-account", "default", "--upgrade", @@ -76,14 +72,11 @@ async function configureSystemEnvironment( const sysStatus = await getEnvironmentStatus({ ctx: sysCtx, - provider: sysProvider, - env, + logEntry, }) - await configureEnvironment({ + await prepareEnvironment({ ctx: sysCtx, - env: sysGarden.getEnvironment(), - provider: sysProvider, force, status: sysStatus, logEntry, @@ -91,7 +84,7 @@ async function configureSystemEnvironment( // only deploy services if configured to do so (minikube bundles the required services as addons) if (!provider.config._systemServices || provider.config._systemServices.length > 0) { - const results = await sysCtx.deployServices({ + const results = await sysGarden.actions.deployServices({ serviceNames: provider.config._systemServices, }) @@ -106,12 +99,12 @@ async function configureSystemEnvironment( } async function configureLocalEnvironment( - { ctx, provider, env, force, status, logEntry }: ConfigureEnvironmentParams, + { ctx, force, status, logEntry }: PrepareEnvironmentParams, ) { - await configureEnvironment({ ctx, provider, env, force, status, logEntry }) + await prepareEnvironment({ ctx, force, status, logEntry }) - if (!isSystemGarden(provider)) { - await configureSystemEnvironment({ provider, env, force, logEntry }) + if (!isSystemGarden(ctx.provider)) { + await configureSystemEnvironment({ ctx, force, logEntry }) } return {} @@ -144,9 +137,7 @@ export interface LocalKubernetesConfig extends KubernetesConfig { _systemServices?: string[] } -type LocalKubernetesProvider = Provider - -const configSchema = providerConfigBase +const configSchema = providerConfigBaseSchema .keys({ context: Joi.string() .description("The kubectl context to use to connect to the Kubernetes cluster."), @@ -235,7 +226,7 @@ export async function gardenPlugin({ projectName, config, logEntry }): Promise { - const existingObjects = await Bluebird.map(objects, obj => getDeployedObject(ctx, provider, obj)) +export async function compareDeployedObjects(ctx: PluginContext, objects: KubernetesObject[]): Promise { + const existingObjects = await Bluebird.map(objects, obj => getDeployedObject(ctx, ctx.provider, obj)) for (let [obj, existingSpec] of zip(objects, existingObjects)) { if (existingSpec && obj) { diff --git a/garden-cli/src/plugins/local/local-docker-swarm.ts b/garden-cli/src/plugins/local/local-docker-swarm.ts index 954090ff02..720fc5b40b 100644 --- a/garden-cli/src/plugins/local/local-docker-swarm.ts +++ b/garden-cli/src/plugins/local/local-docker-swarm.ts @@ -38,19 +38,19 @@ const pluginName = "local-docker-swarm" export const gardenPlugin = (): GardenPlugin => ({ actions: { getEnvironmentStatus, - configureEnvironment, + prepareEnvironment, }, moduleActions: { container: { getServiceStatus, async deployService( - { ctx, provider, module, service, runtimeContext, env }: DeployServiceParams, + { ctx, module, service, runtimeContext, logEntry }: DeployServiceParams, ) { // TODO: split this method up and test const { versionString } = service.module.version - ctx.log.info({ section: service.name, msg: `Deploying version ${versionString}` }) + logEntry && logEntry.info({ section: service.name, msg: `Deploying version ${versionString}` }) const identifier = await helpers.getLocalImageId(module) const ports = service.spec.ports.map(p => { @@ -85,8 +85,7 @@ export const gardenPlugin = (): GardenPlugin => ({ const opts: any = { Name: getSwarmServiceName(ctx, service.name), Labels: { - environment: env.name, - namespace: env.namespace, + environment: ctx.environment.name, provider: pluginName, }, TaskTemplate: { @@ -117,7 +116,7 @@ export const gardenPlugin = (): GardenPlugin => ({ } const docker = getDocker() - const serviceStatus = await getServiceStatus({ ctx, provider, service, env, module, runtimeContext }) + const serviceStatus = await getServiceStatus({ ctx, service, module, runtimeContext, logEntry }) let swarmServiceStatus let serviceId @@ -125,14 +124,14 @@ export const gardenPlugin = (): GardenPlugin => ({ const swarmService = await docker.getService(serviceStatus.providerId) swarmServiceStatus = await swarmService.inspect() opts.version = parseInt(swarmServiceStatus.Version.Index, 10) - ctx.log.verbose({ + logEntry && logEntry.verbose({ section: service.name, msg: `Updating existing Swarm service (version ${opts.version})`, }) await swarmService.update(opts) serviceId = serviceStatus.providerId } else { - ctx.log.verbose({ + logEntry && logEntry.verbose({ section: service.name, msg: `Creating new Swarm service`, }) @@ -168,12 +167,12 @@ export const gardenPlugin = (): GardenPlugin => ({ } } - ctx.log.info({ + logEntry && logEntry.info({ section: service.name, msg: `Ready`, }) - return getServiceStatus({ ctx, provider, module, service, env, runtimeContext }) + return getServiceStatus({ ctx, module, service, runtimeContext, logEntry }) }, async getServiceOutputs({ ctx, service }: GetServiceOutputsParams) { @@ -183,9 +182,15 @@ export const gardenPlugin = (): GardenPlugin => ({ }, async execInService( - { ctx, provider, env, service, command, runtimeContext }: ExecInServiceParams, + { ctx, service, command, runtimeContext, logEntry }: ExecInServiceParams, ) { - const status = await getServiceStatus({ ctx, provider, service, env, module: service.module, runtimeContext }) + const status = await getServiceStatus({ + ctx, + service, + module: service.module, + runtimeContext, + logEntry, + }) if (!status.state || status.state !== "ready") { throw new DeploymentError(`Service ${service.name} is not running`, { @@ -222,13 +227,13 @@ async function getEnvironmentStatus() { await docker.swarmInspect() return { - configured: true, + ready: true, } } catch (err) { if (err.statusCode === 503) { // swarm has not been initialized return { - configured: false, + ready: false, services: [], } } else { @@ -237,7 +242,7 @@ async function getEnvironmentStatus() { } } -async function configureEnvironment() { +async function prepareEnvironment() { await getDocker().swarmInit({}) return {} } diff --git a/garden-cli/src/plugins/local/local-google-cloud-functions.ts b/garden-cli/src/plugins/local/local-google-cloud-functions.ts index 72c7ee9ce0..8221717f1d 100644 --- a/garden-cli/src/plugins/local/local-google-cloud-functions.ts +++ b/garden-cli/src/plugins/local/local-google-cloud-functions.ts @@ -6,7 +6,7 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import { ParseModuleParams } from "../../types/plugin/params" +import { ValidateModuleParams } from "../../types/plugin/params" import { join } from "path" import { GcfModule, @@ -33,7 +33,7 @@ export const gardenPlugin = (): GardenPlugin => ({ moduleActions: { "google-cloud-function": { - async parseModule(params: ParseModuleParams) { + async validate(params: ValidateModuleParams) { const parsed = await parseGcfModule(params) // convert the module and services to containers to run locally diff --git a/garden-cli/src/plugins/openfaas.ts b/garden-cli/src/plugins/openfaas.ts index 22ce8b5499..a2f4b09604 100644 --- a/garden-cli/src/plugins/openfaas.ts +++ b/garden-cli/src/plugins/openfaas.ts @@ -13,14 +13,13 @@ import { STATIC_DIR } from "../constants" import { PluginError, ConfigurationError } from "../exceptions" import { Garden } from "../garden" import { PluginContext } from "../plugin-context" -import { processServices } from "../process" import { joiArray, validate, PrimitiveMap } from "../config/common" import { Module } from "../types/module" -import { ParseModuleResult } from "../types/plugin/outputs" +import { ValidateModuleResult } from "../types/plugin/outputs" import { - ConfigureEnvironmentParams, + PrepareEnvironmentParams, GetEnvironmentStatusParams, - ParseModuleParams, + ValidateModuleParams, DeleteServiceParams, } from "../types/plugin/params" import { @@ -51,10 +50,9 @@ import { KubeApi } from "./kubernetes/api" import { waitForObjects, checkDeploymentStatus } from "./kubernetes/status" import { systemSymbol } from "./kubernetes/system" import { BaseServiceSpec } from "../config/service" -import { getDeployTasks } from "../tasks/deploy" -import { GardenPlugin, Provider } from "../types/plugin/plugin" +import { GardenPlugin } from "../types/plugin/plugin" import { deleteContainerService } from "./kubernetes/deployment" -import { ProviderConfig, providerConfigBase } from "../config/project" +import { Provider, providerConfigBaseSchema } from "../config/project" const systemProjectPath = join(STATIC_DIR, "openfaas", "system") const stackFilename = "stack.yml" @@ -85,11 +83,11 @@ export const openfaasModuleSpecSchame = genericModuleSpecSchema export interface OpenFaasModule extends Module { } export interface OpenFaasService extends Service { } -export interface OpenFaasConfig extends ProviderConfig { +export interface OpenFaasConfig extends Provider { hostname: string } -const configSchema = providerConfigBase +const configSchema = providerConfigBaseSchema .keys({ hostname: Joi.string() .hostname() @@ -108,38 +106,25 @@ export function gardenPlugin({ config }: { config: OpenFaasConfig }): GardenPlug return { modules: [join(STATIC_DIR, "openfaas", "openfaas-builder")], actions: { - async getEnvironmentStatus({ ctx, provider }: GetEnvironmentStatusParams) { - const ofGarden = await getOpenFaasGarden(ctx, provider) - const status = await ofGarden.getPluginContext().getStatus() - const envReady = every(values(status.providers).map(s => s.configured)) + async getEnvironmentStatus({ ctx }: GetEnvironmentStatusParams) { + const ofGarden = await getOpenFaasGarden(ctx) + const status = await ofGarden.actions.getStatus() + const envReady = every(values(status.providers).map(s => s.ready)) const servicesReady = every(values(status.services).map(s => s.state === "ready")) return { - configured: envReady && servicesReady, + ready: envReady && servicesReady, detail: status, } }, - async configureEnvironment({ ctx, provider, force }: ConfigureEnvironmentParams) { + async prepareEnvironment({ ctx, force }: PrepareEnvironmentParams) { // TODO: refactor to dedupe similar code in local-kubernetes - const ofGarden = await getOpenFaasGarden(ctx, provider) - const ofCtx = ofGarden.getPluginContext() + const ofGarden = await getOpenFaasGarden(ctx) - await ofCtx.configureEnvironment({ force }) - - const services = await ofCtx.getServices() - const deployTasksForModule = async (module) => getDeployTasks({ - ctx: ofCtx, module, force, forceBuild: false, includeDependants: false, - }) - - const results = await processServices({ - garden: ofGarden, - ctx: ofCtx, - services, - watch: false, - handler: deployTasksForModule, - }) + await ofGarden.actions.prepareEnvironment({ force }) + const results = await ofGarden.actions.deployServices({}) const failed = values(results.taskResults).filter(r => !!r.error).length if (failed) { @@ -153,14 +138,14 @@ export function gardenPlugin({ config }: { config: OpenFaasConfig }): GardenPlug }, moduleActions: { openfaas: { - async parseModule({ moduleConfig }: ParseModuleParams): Promise { + async validate({ moduleConfig }: ValidateModuleParams): Promise { moduleConfig.spec = validate( moduleConfig.spec, openfaasModuleSpecSchame, { context: `module ${moduleConfig.name}` }, ) - // stack.yml is populated in the buildModule handler below + // stack.yml is populated in the build handler below moduleConfig.build.command = ["./faas-cli", "build", "-f", stackFilename] moduleConfig.build.dependencies.push({ @@ -192,13 +177,13 @@ export function gardenPlugin({ config }: { config: OpenFaasConfig }): GardenPlug return moduleConfig }, - getModuleBuildStatus: getGenericModuleBuildStatus, + getBuildStatus: getGenericModuleBuildStatus, - async buildModule(params: BuildModuleParams) { - const { ctx, provider, module } = params + async build(params: BuildModuleParams) { + const { ctx, module } = params // prepare the stack.yml file, before handing off the build to the generic handler - await writeStackFile(ctx, provider, module, {}) + await writeStackFile(ctx, module, {}) return buildGenericModule(params) }, @@ -215,10 +200,10 @@ export function gardenPlugin({ config }: { config: OpenFaasConfig }): GardenPlug }, async deployService(params: DeployServiceParams): Promise { - const { ctx, provider, module, service, logEntry, runtimeContext } = params + const { ctx, module, service, logEntry, runtimeContext } = params // write the stack file again with environment variables - await writeStackFile(ctx, provider, module, runtimeContext.envVars) + await writeStackFile(ctx, module, runtimeContext.envVars) // use faas-cli to do the deployment await execa("./faas-cli", ["deploy", "-f", stackFilename], { @@ -243,7 +228,7 @@ export function gardenPlugin({ config }: { config: OpenFaasConfig }): GardenPlug const provider = getK8sProvider(ctx) const namespace = await getAppNamespace(ctx, provider) - await deleteContainerService({ logEntry, namespace, provider, serviceName: service.name }) + await deleteContainerService({ provider, logEntry, namespace, serviceName: service.name }) return await getServiceStatus(params) }, @@ -253,7 +238,7 @@ export function gardenPlugin({ config }: { config: OpenFaasConfig }): GardenPlug } async function writeStackFile( - ctx: PluginContext, provider: OpenFaasProvider, module: OpenFaasModule, envVars: PrimitiveMap, + ctx: PluginContext, module: OpenFaasModule, envVars: PrimitiveMap, ) { const image = getImageName(module) @@ -262,7 +247,7 @@ async function writeStackFile( return dumpYaml(stackPath, { provider: { name: "faas", - gateway: getExternalGatewayUrl(ctx, provider), + gateway: getExternalGatewayUrl(ctx), }, functions: { [module.name]: { @@ -275,11 +260,11 @@ async function writeStackFile( }) } -async function getServiceStatus({ ctx, provider, service }: GetServiceStatusParams) { +async function getServiceStatus({ ctx, service }: GetServiceStatusParams) { const k8sProvider = getK8sProvider(ctx) const endpoints: ServiceEndpoint[] = [{ - hostname: getExternalGatewayHostname(provider, k8sProvider), + hostname: getExternalGatewayHostname(ctx.provider, k8sProvider), path: getServicePath(service), port: k8sProvider.config.ingressHttpPort, protocol: "http", @@ -366,8 +351,8 @@ function getServicePath(service: OpenFaasService) { } async function getInternalGatewayUrl(ctx: PluginContext) { - const provider = getK8sProvider(ctx) - const namespace = await getOpenfaasNamespace(ctx, provider, true) + const k8sProvider = getK8sProvider(ctx) + const namespace = await getOpenfaasNamespace(ctx, k8sProvider, true) return `http://gateway.${namespace}.svc.cluster.local:8080` } @@ -378,7 +363,7 @@ function getExternalGatewayHostname(provider: OpenFaasProvider, k8sProvider: Kub throw new ConfigurationError( `openfaas: Must configure hostname if no default hostname is configured on Kubernetes provider.`, { - config: provider.config, + config: provider, }, ) } @@ -386,9 +371,9 @@ function getExternalGatewayHostname(provider: OpenFaasProvider, k8sProvider: Kub return hostname } -function getExternalGatewayUrl(ctx: PluginContext, provider: OpenFaasProvider) { +function getExternalGatewayUrl(ctx: PluginContext) { const k8sProvider = getK8sProvider(ctx) - const hostname = getExternalGatewayHostname(provider, k8sProvider) + const hostname = getExternalGatewayHostname(ctx.provider, k8sProvider) const ingressPort = k8sProvider.config.ingressHttpPort return `http://${hostname}:${ingressPort}` } @@ -397,18 +382,18 @@ async function getInternalServiceUrl(ctx: PluginContext, service: OpenFaasServic return urlResolve(await getInternalGatewayUrl(ctx), getServicePath(service)) } -async function getOpenfaasNamespace(ctx: PluginContext, provider: KubernetesProvider, skipCreate?: boolean) { - return getNamespace({ ctx, provider, skipCreate, suffix: "openfaas" }) +async function getOpenfaasNamespace(ctx: PluginContext, k8sProvider: KubernetesProvider, skipCreate?: boolean) { + return getNamespace({ ctx, provider: k8sProvider, skipCreate, suffix: "openfaas" }) } -export async function getOpenFaasGarden(ctx: PluginContext, provider: OpenFaasProvider): Promise { +export async function getOpenFaasGarden(ctx: PluginContext): Promise { // TODO: figure out good way to retrieve namespace from kubernetes plugin through an exposed interface // (maybe allow plugins to expose arbitrary data on the Provider object?) const k8sProvider = getK8sProvider(ctx) const namespace = await getOpenfaasNamespace(ctx, k8sProvider, true) const functionNamespace = await getAppNamespace(ctx, k8sProvider) - const hostname = getExternalGatewayHostname(provider, k8sProvider) + const hostname = getExternalGatewayHostname(ctx.provider, k8sProvider) // TODO: allow passing variables/parameters here to be parsed as part of the garden.yml project config // (this would allow us to use a garden.yml for the project config, instead of speccing it here) diff --git a/garden-cli/src/plugins/plugins.ts b/garden-cli/src/plugins/plugins.ts index 668f7bde29..7c6e346706 100644 --- a/garden-cli/src/plugins/plugins.ts +++ b/garden-cli/src/plugins/plugins.ts @@ -27,5 +27,4 @@ export const builtinPlugins: RegisterPluginParam[] = [ export const fixedPlugins = [ "generic", "container", - "npm-package", ] diff --git a/garden-cli/src/process.ts b/garden-cli/src/process.ts index 8019c31489..35c68d3b0a 100644 --- a/garden-cli/src/process.ts +++ b/garden-cli/src/process.ts @@ -14,14 +14,12 @@ import { Task } from "./tasks/base" import { TaskResults } from "./task-graph" import { FSWatcher } from "./watch" import { registerCleanupFunction } from "./util/util" -import { PluginContext } from "./plugin-context" import { isModuleLinked } from "./util/ext-source-util" import { Garden } from "./garden" export type ProcessHandler = (module: Module) => Promise interface ProcessParams { - ctx: PluginContext garden: Garden, watch: boolean handler: ProcessHandler @@ -43,14 +41,13 @@ export interface ProcessResults { } export async function processServices( - { ctx, garden, services, watch, handler, changeHandler }: ProcessServicesParams, + { garden, services, watch, handler, changeHandler }: ProcessServicesParams, ): Promise { const modules = Array.from(new Set(services.map(s => s.module))) return processModules({ modules, - ctx, garden, watch, handler, @@ -59,12 +56,12 @@ export async function processServices( } export async function processModules( - { ctx, garden, modules, watch, handler, changeHandler }: ProcessModulesParams, + { garden, modules, watch, handler, changeHandler }: ProcessModulesParams, ): Promise { for (const module of modules) { const tasks = await handler(module) - if (isModuleLinked(module, ctx)) { - ctx.log.info( + if (isModuleLinked(module, garden)) { + garden.log.info( chalk.gray(`Reading module ${chalk.cyan(module.name)} from linked local path ${chalk.white(module.path)}`), ) } @@ -84,19 +81,19 @@ export async function processModules( changeHandler = handler } - const watcher = new FSWatcher(ctx) + const watcher = new FSWatcher(garden) const restartPromise = new Promise(async (resolve) => { await watcher.watchModules(modules, async (changedModule: Module | null, configChanged: boolean) => { if (configChanged) { - ctx.log.debug({ msg: `Config changed, reloading.` }) + garden.log.debug({ msg: `Config changed, reloading.` }) resolve() return } if (changedModule) { - ctx.log.debug({ msg: `Files changed for module ${changedModule.name}` }) + garden.log.debug({ msg: `Files changed for module ${changedModule.name}` }) await Bluebird.map(changeHandler!(changedModule), task => { garden.addTask(task) diff --git a/garden-cli/src/task-graph.ts b/garden-cli/src/task-graph.ts index b8571b5798..162c449752 100644 --- a/garden-cli/src/task-graph.ts +++ b/garden-cli/src/task-graph.ts @@ -12,8 +12,8 @@ import { merge, padEnd, pick } from "lodash" import { Task, TaskDefinitionError } from "./tasks/base" import { LogEntry } from "./logger/log-entry" -import { PluginContext } from "./plugin-context" import { toGardenError } from "./exceptions" +import { Garden } from "./garden" class TaskGraphError extends Error { } @@ -58,7 +58,7 @@ export class TaskGraph { private resultCache: ResultCache private opQueue: OperationQueue - constructor(private ctx: PluginContext, private concurrency: number = DEFAULT_CONCURRENCY) { + constructor(private garden: Garden, private concurrency: number = DEFAULT_CONCURRENCY) { this.roots = new TaskNodeMap() this.index = new TaskNodeMap() this.inProgress = new TaskNodeMap() @@ -265,7 +265,7 @@ export class TaskGraph { // Logging private logTask(node: TaskNode) { - const entry = this.ctx.log.debug({ + const entry = this.garden.log.debug({ section: "tasks", msg: `Processing task ${taskStyle(node.getKey())}`, status: "active", @@ -283,12 +283,12 @@ export class TaskGraph { private initLogging() { if (!Object.keys(this.logEntryMap).length) { - const header = this.ctx.log.debug("Processing tasks...") - const counter = this.ctx.log.debug({ + const header = this.garden.log.debug("Processing tasks...") + const counter = this.garden.log.debug({ msg: remainingTasksToStr(this.index.length), status: "active", }) - const inProgress = this.ctx.log.debug(inProgressToStr(this.inProgress.getNodes())) + const inProgress = this.garden.log.debug(inProgressToStr(this.inProgress.getNodes())) this.logEntryMap = { ...this.logEntryMap, header, @@ -302,7 +302,7 @@ export class TaskGraph { const divider = padEnd("", 80, "—") const error = toGardenError(err) const msg = `\nFailed ${node.getDescription()}. Here is the output:\n${divider}\n${error.message}\n${divider}\n` - this.ctx.log.error({ msg, error }) + this.garden.log.error({ msg, error }) } } diff --git a/garden-cli/src/tasks/base.ts b/garden-cli/src/tasks/base.ts index c6eb1ecceb..65ddf12dd8 100644 --- a/garden-cli/src/tasks/base.ts +++ b/garden-cli/src/tasks/base.ts @@ -9,16 +9,19 @@ import { TaskResults } from "../task-graph" import { ModuleVersion } from "../vcs/base" import { v1 as uuidv1 } from "uuid" +import { Garden } from "../garden" export class TaskDefinitionError extends Error { } export interface TaskParams { + garden: Garden force?: boolean version: ModuleVersion } export abstract class Task { abstract type: string + garden: Garden id: string force: boolean version: ModuleVersion @@ -26,6 +29,7 @@ export abstract class Task { dependencies: Task[] constructor(initArgs: TaskParams) { + this.garden = initArgs.garden this.dependencies = [] this.id = uuidv1() // uuidv1 is timestamp-based this.force = !!initArgs.force diff --git a/garden-cli/src/tasks/build.ts b/garden-cli/src/tasks/build.ts index 185673c219..a0f31eb45a 100644 --- a/garden-cli/src/tasks/build.ts +++ b/garden-cli/src/tasks/build.ts @@ -8,13 +8,13 @@ import * as Bluebird from "bluebird" import chalk from "chalk" -import { PluginContext } from "../plugin-context" import { Module } from "../types/module" import { BuildResult } from "../types/plugin/outputs" import { Task } from "../tasks/base" +import { Garden } from "../garden" export interface BuildTaskParams { - ctx: PluginContext + garden: Garden module: Module force: boolean } @@ -22,19 +22,21 @@ export interface BuildTaskParams { export class BuildTask extends Task { type = "build" - private ctx: PluginContext private module: Module - constructor({ ctx, force, module }: BuildTaskParams) { - super({ force, version: module.version }) - this.ctx = ctx + constructor({ garden, force, module }: BuildTaskParams) { + super({ garden, force, version: module.version }) this.module = module } async getDependencies(): Promise { - const deps = await this.ctx.resolveModuleDependencies(this.module.build.dependencies, []) + const deps = await this.garden.resolveModuleDependencies(this.module.build.dependencies, []) return Bluebird.map(deps, async (m: Module) => { - return new BuildTask({ ctx: this.ctx, module: m, force: this.force }) + return new BuildTask({ + garden: this.garden, + module: m, + force: this.force, + }) }) } @@ -47,15 +49,15 @@ export class BuildTask extends Task { } async process(): Promise { - const moduleName = this.module.name + const module = this.module - if (!this.force && (await this.ctx.getModuleBuildStatus({ moduleName })).ready) { + if (!this.force && (await this.garden.actions.getBuildStatus({ module })).ready) { // this is necessary in case other modules depend on files from this one - await this.ctx.stageBuild(moduleName) + await this.garden.buildDir.syncDependencyProducts(this.module) return { fresh: false } } - const logEntry = this.ctx.log.info({ + const logEntry = this.garden.log.info({ section: this.module.name, msg: "Building", status: "active", @@ -63,8 +65,8 @@ export class BuildTask extends Task { let result: BuildResult try { - result = await this.ctx.buildModule({ - moduleName, + result = await this.garden.actions.build({ + module, logEntry, }) } catch (err) { diff --git a/garden-cli/src/tasks/deploy.ts b/garden-cli/src/tasks/deploy.ts index 26ea75a583..4a0c4dce9d 100644 --- a/garden-cli/src/tasks/deploy.ts +++ b/garden-cli/src/tasks/deploy.ts @@ -10,7 +10,6 @@ import { flatten } from "lodash" import * as Bluebird from "bluebird" import chalk from "chalk" import { LogEntry } from "../logger/log-entry" -import { PluginContext } from "../plugin-context" import { BuildTask } from "./build" import { Task } from "./base" import { @@ -21,9 +20,10 @@ import { import { Module } from "../types/module" import { withDependants, computeAutoReloadDependants } from "../watch" import { getNames } from "../util/util" +import { Garden } from "../garden" export interface DeployTaskParams { - ctx: PluginContext + garden: Garden service: Service force: boolean forceBuild: boolean @@ -33,14 +33,12 @@ export interface DeployTaskParams { export class DeployTask extends Task { type = "deploy" - private ctx: PluginContext private service: Service private forceBuild: boolean private logEntry?: LogEntry - constructor({ ctx, service, force, forceBuild, logEntry }: DeployTaskParams) { - super({ force, version: service.module.version }) - this.ctx = ctx + constructor({ garden, service, force, forceBuild, logEntry }: DeployTaskParams) { + super({ garden, force, version: service.module.version }) this.service = service this.forceBuild = forceBuild this.logEntry = logEntry @@ -48,19 +46,19 @@ export class DeployTask extends Task { async getDependencies() { const serviceDeps = this.service.config.dependencies - const services = await this.ctx.getServices(serviceDeps) + const services = await this.garden.getServices(serviceDeps) const deps: Task[] = await Bluebird.map(services, async (service) => { return new DeployTask({ + garden: this.garden, service, - ctx: this.ctx, force: false, forceBuild: this.forceBuild, }) }) deps.push(new BuildTask({ - ctx: this.ctx, + garden: this.garden, module: this.service.module, force: this.forceBuild, })) @@ -77,7 +75,7 @@ export class DeployTask extends Task { } async process(): Promise { - const logEntry = (this.logEntry || this.ctx.log).info({ + const logEntry = (this.logEntry || this.garden.log).info({ section: this.service.name, msg: "Checking status", status: "active", @@ -85,7 +83,7 @@ export class DeployTask extends Task { // TODO: get version from build task results const { versionString } = await this.service.module.version - const status = await this.ctx.getServiceStatus({ serviceName: this.service.name, logEntry }) + const status = await this.garden.actions.getServiceStatus({ service: this.service, logEntry }) if ( !this.force && @@ -102,13 +100,13 @@ export class DeployTask extends Task { logEntry.setState("Deploying") - const dependencies = await this.ctx.getServices(this.service.config.dependencies) + const dependencies = await this.garden.getServices(this.service.config.dependencies) let result: ServiceStatus try { - result = await this.ctx.deployService({ - serviceName: this.service.name, - runtimeContext: await prepareRuntimeContext(this.ctx, this.service.module, dependencies), + result = await this.garden.actions.deployService({ + service: this.service, + runtimeContext: await prepareRuntimeContext(this.garden, this.service.module, dependencies), logEntry, force: this.force, }) @@ -123,24 +121,24 @@ export class DeployTask extends Task { } export async function getDeployTasks( - { ctx, module, serviceNames, force = false, forceBuild = false, includeDependants = false }: + { garden, module, serviceNames, force = false, forceBuild = false, includeDependants = false }: { - ctx: PluginContext, module: Module, serviceNames?: string[] | null, + garden: Garden, module: Module, serviceNames?: string[] | null, force?: boolean, forceBuild?: boolean, includeDependants?: boolean, }, ) { const modulesToProcess = includeDependants - ? (await withDependants(ctx, [module], await computeAutoReloadDependants(ctx))) + ? (await withDependants(garden, [module], await computeAutoReloadDependants(garden))) : [module] const moduleServices = flatten(await Bluebird.map( modulesToProcess, - m => ctx.getServices(getNames(m.serviceConfigs)))) + m => garden.getServices(getNames(m.serviceConfigs)))) const servicesToProcess = serviceNames ? moduleServices.filter(s => serviceNames.includes(s.name)) : moduleServices - return servicesToProcess.map(service => new DeployTask({ ctx, service, force, forceBuild })) + return servicesToProcess.map(service => new DeployTask({ garden, service, force, forceBuild })) } diff --git a/garden-cli/src/tasks/push.ts b/garden-cli/src/tasks/push.ts index 78718c3f03..1063bf6327 100644 --- a/garden-cli/src/tasks/push.ts +++ b/garden-cli/src/tasks/push.ts @@ -7,14 +7,14 @@ */ import chalk from "chalk" -import { PluginContext } from "../plugin-context" import { BuildTask } from "./build" import { Module } from "../types/module" import { PushResult } from "../types/plugin/outputs" import { Task } from "../tasks/base" +import { Garden } from "../garden" export interface PushTaskParams { - ctx: PluginContext + garden: Garden module: Module forceBuild: boolean } @@ -22,13 +22,11 @@ export interface PushTaskParams { export class PushTask extends Task { type = "push" - private ctx: PluginContext private module: Module private forceBuild: boolean - constructor({ ctx, module, forceBuild }: PushTaskParams) { - super({ version: module.version }) - this.ctx = ctx + constructor({ garden, module, forceBuild }: PushTaskParams) { + super({ garden, version: module.version }) this.module = module this.forceBuild = forceBuild } @@ -38,7 +36,7 @@ export class PushTask extends Task { return [] } return [new BuildTask({ - ctx: this.ctx, + garden: this.garden, module: this.module, force: this.forceBuild, })] @@ -55,7 +53,7 @@ export class PushTask extends Task { async process(): Promise { if (!this.module.allowPush) { - this.ctx.log.info({ + this.garden.log.info({ section: this.module.name, msg: "Push disabled", status: "active", @@ -63,7 +61,7 @@ export class PushTask extends Task { return { pushed: false } } - const logEntry = this.ctx.log.info({ + const logEntry = this.garden.log.info({ section: this.module.name, msg: "Pushing", status: "active", @@ -71,7 +69,7 @@ export class PushTask extends Task { let result: PushResult try { - result = await this.ctx.pushModule({ moduleName: this.module.name, logEntry }) + result = await this.garden.actions.pushModule({ module: this.module, logEntry }) } catch (err) { logEntry.setError() throw err diff --git a/garden-cli/src/tasks/test.ts b/garden-cli/src/tasks/test.ts index ab854a5463..c33e3d7534 100644 --- a/garden-cli/src/tasks/test.ts +++ b/garden-cli/src/tasks/test.ts @@ -8,7 +8,6 @@ import * as Bluebird from "bluebird" import chalk from "chalk" -import { PluginContext } from "../plugin-context" import { Module } from "../types/module" import { TestConfig } from "../config/test" import { ModuleVersion } from "../vcs/base" @@ -17,6 +16,7 @@ import { DeployTask } from "./deploy" import { TestResult } from "../types/plugin/outputs" import { Task, TaskParams } from "../tasks/base" import { prepareRuntimeContext } from "../types/service" +import { Garden } from "../garden" class TestError extends Error { toString() { @@ -25,7 +25,7 @@ class TestError extends Error { } export interface TestTaskParams { - ctx: PluginContext + garden: Garden module: Module testConfig: TestConfig force: boolean @@ -35,14 +35,12 @@ export interface TestTaskParams { export class TestTask extends Task { type = "test" - private ctx: PluginContext private module: Module private testConfig: TestConfig private forceBuild: boolean - constructor({ ctx, module, testConfig, force, forceBuild, version }: TestTaskParams & TaskParams) { - super({ force, version }) - this.ctx = ctx + constructor({ garden, module, testConfig, force, forceBuild, version }: TestTaskParams & TaskParams) { + super({ garden, force, version }) this.module = module this.testConfig = testConfig this.force = force @@ -50,8 +48,8 @@ export class TestTask extends Task { } static async factory(initArgs: TestTaskParams): Promise { - const { ctx, module, testConfig } = initArgs - const version = await getTestVersion(ctx, module, testConfig) + const { garden, module, testConfig } = initArgs + const version = await getTestVersion(garden, module, testConfig) return new TestTask({ ...initArgs, version }) } @@ -62,18 +60,18 @@ export class TestTask extends Task { return [] } - const services = await this.ctx.getServices(this.testConfig.dependencies) + const services = await this.garden.getServices(this.testConfig.dependencies) const deps: Task[] = [new BuildTask({ - ctx: this.ctx, + garden: this.garden, module: this.module, force: this.forceBuild, })] for (const service of services) { deps.push(new DeployTask({ + garden: this.garden, service, - ctx: this.ctx, force: false, forceBuild: this.forceBuild, })) @@ -95,7 +93,7 @@ export class TestTask extends Task { const testResult = await this.getTestResult() if (testResult && testResult.success) { - const passedEntry = this.ctx.log.info({ + const passedEntry = this.garden.log.info({ section: this.module.name, msg: `${this.testConfig.name} tests`, }) @@ -103,20 +101,20 @@ export class TestTask extends Task { return testResult } - const entry = this.ctx.log.info({ + const entry = this.garden.log.info({ section: this.module.name, msg: `Running ${this.testConfig.name} tests`, status: "active", }) - const dependencies = await getTestDependencies(this.ctx, this.testConfig) - const runtimeContext = await prepareRuntimeContext(this.ctx, this.module, dependencies) + const dependencies = await getTestDependencies(this.garden, this.testConfig) + const runtimeContext = await prepareRuntimeContext(this.garden, this.module, dependencies) let result: TestResult try { - result = await this.ctx.testModule({ + result = await this.garden.actions.testModule({ interactive: false, - moduleName: this.module.name, + module: this.module, runtimeContext, silent: true, testConfig: this.testConfig, @@ -140,22 +138,22 @@ export class TestTask extends Task { return null } - return this.ctx.getTestResult({ - moduleName: this.module.name, + return this.garden.actions.getTestResult({ + module: this.module, testName: this.testConfig.name, version: this.version, }) } } -async function getTestDependencies(ctx: PluginContext, testConfig: TestConfig) { - return ctx.getServices(testConfig.dependencies) +async function getTestDependencies(garden: Garden, testConfig: TestConfig) { + return garden.getServices(testConfig.dependencies) } /** * Determine the version of the test run, based on the version of the module and each of its dependencies. */ -async function getTestVersion(ctx: PluginContext, module: Module, testConfig: TestConfig): Promise { - const moduleDeps = await ctx.resolveModuleDependencies(module.build.dependencies, testConfig.dependencies) - return ctx.resolveVersion(module.name, moduleDeps) +async function getTestVersion(garden: Garden, module: Module, testConfig: TestConfig): Promise { + const moduleDeps = await garden.resolveModuleDependencies(module.build.dependencies, testConfig.dependencies) + return garden.resolveVersion(module.name, moduleDeps) } diff --git a/garden-cli/src/types/module.ts b/garden-cli/src/types/module.ts index 66d5ba410f..903a067a4f 100644 --- a/garden-cli/src/types/module.ts +++ b/garden-cli/src/types/module.ts @@ -9,12 +9,14 @@ import { flatten, uniq } from "lodash" import { getNames } from "../util/util" import { TestSpec } from "../config/test" -import { ModuleSpec, ModuleConfig } from "../config/module" +import { ModuleSpec, ModuleConfig, moduleConfigSchema } from "../config/module" import { ServiceSpec } from "../config/service" -import { ModuleVersion } from "../vcs/base" -import { CacheContext, pathToCacheContext } from "../cache" +import { ModuleVersion, moduleVersionSchema } from "../vcs/base" +import { pathToCacheContext } from "../cache" import { Garden } from "../garden" -import { serviceFromConfig, Service } from "./service" +import { serviceFromConfig, Service, serviceSchema } from "./service" +import * as Joi from "joi" +import { joiArray, joiIdentifier } from "../config/common" export interface BuildCopySpec { source: string @@ -28,7 +30,6 @@ export interface Module< > extends ModuleConfig { buildPath: string version: ModuleVersion - cacheContext: CacheContext services: Service>[] serviceNames: string[] @@ -37,11 +38,30 @@ export interface Module< _ConfigType: ModuleConfig } -export interface ModuleMap { +export const moduleSchema = moduleConfigSchema + .keys({ + buildPath: Joi.string() + .required() + .uri({ relativeOnly: true }) + .description("The path to the build staging directory for the module."), + version: moduleVersionSchema + .required(), + services: joiArray(Joi.lazy(() => serviceSchema)) + .required() + .description("A list of all the services that the module provides."), + serviceNames: joiArray(joiIdentifier()) + .required() + .description("The names of the services that the module provides."), + serviceDependencyNames: joiArray(joiIdentifier()) + .required() + .description("The names of all the services that the services in this module depend on."), + }) + +export interface ModuleMap { [key: string]: T } -export interface ModuleConfigMap { +export interface ModuleConfigMap { [key: string]: T } @@ -51,7 +71,6 @@ export async function moduleFromConfig(garden: Garden, config: ModuleConfig): Pr buildPath: await garden.buildDir.buildPath(config.name), version: await garden.resolveVersion(config.name, config.build.dependencies), - cacheContext: pathToCacheContext(config.path), services: [], serviceNames: getNames(config.serviceConfigs), diff --git a/garden-cli/src/types/plugin/outputs.ts b/garden-cli/src/types/plugin/outputs.ts index fe30090555..082ce9327d 100644 --- a/garden-cli/src/types/plugin/outputs.ts +++ b/garden-cli/src/types/plugin/outputs.ts @@ -14,13 +14,13 @@ import { ServiceStatus } from "../service" import { moduleConfigSchema, ModuleConfig } from "../../config/module" export interface EnvironmentStatus { - configured: boolean + ready: boolean detail?: any } export const environmentStatusSchema = Joi.object() .keys({ - configured: Joi.boolean() + ready: Joi.boolean() .required() .description("Set to true if the environment is fully configured for a provider."), detail: Joi.object() @@ -33,19 +33,19 @@ export type EnvironmentStatusMap = { [key: string]: EnvironmentStatus, } -export interface ConfigureEnvironmentResult { } +export interface PrepareEnvironmentResult { } -export const configureEnvironmentResultSchema = Joi.object().keys({}) +export const prepareEnvironmentResultSchema = Joi.object().keys({}) -export interface DestroyEnvironmentResult { } +export interface CleanupEnvironmentResult { } -export const destroyEnvironmentResultSchema = Joi.object().keys({}) +export const cleanupEnvironmentResultSchema = Joi.object().keys({}) -export interface GetConfigResult { +export interface GetSecretResult { value: string | null } -export const getConfigResultSchema = Joi.object() +export const getSecretResultSchema = Joi.object() .keys({ value: Joi.string() .allow(null) @@ -53,15 +53,15 @@ export const getConfigResultSchema = Joi.object() .description("The config value found for the specified key (as string), or null if not found."), }) -export interface SetConfigResult { } +export interface SetSecretResult { } -export const setConfigResultSchema = Joi.object().keys({}) +export const setSecretResultSchema = Joi.object().keys({}) -export interface DeleteConfigResult { +export interface DeleteSecretResult { found: boolean } -export const deleteConfigResultSchema = Joi.object() +export const deleteSecretResultSchema = Joi.object() .keys({ found: Joi.boolean() .required() @@ -128,10 +128,27 @@ export interface GetServiceLogsResult { } export const getServiceLogsResultSchema = Joi.object().keys({}) -export type ParseModuleResult = +export interface ModuleTypeDescription { + docs: string + schema: object +} + +export const moduleTypeDescriptionSchema = Joi.object() + .keys({ + docs: Joi.string() + .required() + .description("Documentation for the module type, in markdown format."), + schema: Joi.object() + .required() + .description( + "A valid OpenAPI schema describing the configuration keys for the `module` field in the module's `garden.yml`.", + ), + }) + +export type ValidateModuleResult = ModuleConfig -export const parseModuleResultSchema = moduleConfigSchema +export const validateModuleResultSchema = moduleConfigSchema export interface BuildResult { buildLog?: string @@ -144,6 +161,7 @@ export interface BuildResult { export const buildModuleResultSchema = Joi.object() .keys({ buildLog: Joi.string() + .allow("") .description("The full log from the build."), fetched: Joi.boolean() .description("Set to true if the build was fetched from a remote registry."), @@ -228,12 +246,12 @@ export const buildStatusSchema = Joi.object() export interface PluginActionOutputs { getEnvironmentStatus: Promise - configureEnvironment: Promise - destroyEnvironment: Promise + prepareEnvironment: Promise + cleanupEnvironment: Promise - getConfig: Promise - setConfig: Promise - deleteConfig: Promise + getSecret: Promise + setSecret: Promise + deleteSecret: Promise getLoginStatus: Promise login: Promise @@ -251,9 +269,10 @@ export interface ServiceActionOutputs { } export interface ModuleActionOutputs extends ServiceActionOutputs { - parseModule: Promise - getModuleBuildStatus: Promise - buildModule: Promise + describeType: Promise + validate: Promise + getBuildStatus: Promise + build: Promise pushModule: Promise runModule: Promise testModule: Promise diff --git a/garden-cli/src/types/plugin/params.ts b/garden-cli/src/types/plugin/params.ts index 00bfe0ad84..d31dc2b269 100644 --- a/garden-cli/src/types/plugin/params.ts +++ b/garden-cli/src/types/plugin/params.ts @@ -8,101 +8,151 @@ import Stream from "ts-stream" import { LogEntry } from "../../logger/log-entry" -import { PluginContext } from "../../plugin-context" -import { ModuleVersion } from "../../vcs/base" -import { - Environment, - Primitive, -} from "../../config/common" -import { Module } from "../module" -import { - RuntimeContext, - Service, -} from "../service" -import { - Provider, -} from "./plugin" -import { - EnvironmentStatus, - ServiceLogEntry, -} from "./outputs" +import { PluginContext, pluginContextSchema } from "../../plugin-context" +import { ModuleVersion, moduleVersionSchema } from "../../vcs/base" +import { Primitive, joiPrimitive, joiArray } from "../../config/common" +import { Module, moduleSchema } from "../module" +import { RuntimeContext, Service, serviceSchema, runtimeContextSchema } from "../service" +import { EnvironmentStatus, ServiceLogEntry, environmentStatusSchema } from "./outputs" +import * as Joi from "joi" +import { moduleConfigSchema } from "../../config/module" +import { testConfigSchema } from "../../config/test" export interface PluginActionContextParams { ctx: PluginContext - env: Environment - provider: Provider } export interface PluginActionParamsBase extends PluginActionContextParams { logEntry?: LogEntry } +// Note: not specifying this further because we will later remove it from the API +const logEntrySchema = Joi.object() + .description("Logging context handler that the handler can use to log messages and progress.") + +const actionParamsSchema = Joi.object() + .keys({ + ctx: pluginContextSchema + .required(), + logEntry: logEntrySchema, + }) + export interface PluginModuleActionParamsBase extends PluginActionParamsBase { module: T } +const moduleActionParamsSchema = actionParamsSchema + .keys({ + module: moduleSchema, + }) export interface PluginServiceActionParamsBase extends PluginModuleActionParamsBase { runtimeContext?: RuntimeContext service: Service } +const serviceActionParamsSchema = moduleActionParamsSchema + .keys({ + runtimeContext: runtimeContextSchema + .optional(), + service: serviceSchema, + }) -export interface ParseModuleParams { - env: Environment - provider: Provider +/** + * Plugin actions + */ +export interface DescribeModuleTypeParams { } +export const describeModuleTypeParamsSchema = Joi.object() + .keys({}) + +export interface ValidateModuleParams { + ctx: PluginContext logEntry?: LogEntry moduleConfig: T["_ConfigType"] } +export const validateModuleParamsSchema = Joi.object() + .keys({ + ctx: pluginContextSchema + .required(), + logEntry: logEntrySchema, + moduleConfig: moduleConfigSchema + .required(), + }) -export interface GetEnvironmentStatusParams extends PluginActionParamsBase { -} +export interface GetEnvironmentStatusParams extends PluginActionParamsBase { } +export const getEnvironmentStatusParamsSchema = actionParamsSchema -export interface ConfigureEnvironmentParams extends PluginActionParamsBase { +export interface PrepareEnvironmentParams extends PluginActionParamsBase { status: EnvironmentStatus force: boolean } +export const prepareEnvironmentParamsSchema = actionParamsSchema + .keys({ + status: environmentStatusSchema, + force: Joi.boolean() + .description("Force re-configuration of the environment."), + }) -export interface DestroyEnvironmentParams extends PluginActionParamsBase { +export interface CleanupEnvironmentParams extends PluginActionParamsBase { } +export const cleanupEnvironmentParamsSchema = actionParamsSchema -export interface GetConfigParams extends PluginActionParamsBase { - key: string[] +export interface GetSecretParams extends PluginActionParamsBase { + key: string } +export const getSecretParamsSchema = actionParamsSchema + .keys({ + key: Joi.string() + .description("A unique identifier for the secret."), + }) -export interface SetConfigParams extends PluginActionParamsBase { - key: string[] +export interface SetSecretParams extends PluginActionParamsBase { + key: string value: Primitive } +export const setSecretParamsSchema = getSecretParamsSchema + .keys({ + value: joiPrimitive() + .description("The value of the secret."), + }) -export interface DeleteConfigParams extends PluginActionParamsBase { - key: string[] +export interface DeleteSecretParams extends PluginActionParamsBase { + key: string } +export const deleteSecretParamsSchema = getSecretParamsSchema export interface GetLoginStatusParams extends PluginActionParamsBase { } +export const getLoginStatusParamsSchema = actionParamsSchema + export interface LoginParams extends PluginActionParamsBase { } +export const loginParamsSchema = actionParamsSchema + export interface LogoutParams extends PluginActionParamsBase { } +export const logoutParamsSchema = actionParamsSchema export interface PluginActionParams { getEnvironmentStatus: GetEnvironmentStatusParams - configureEnvironment: ConfigureEnvironmentParams - destroyEnvironment: DestroyEnvironmentParams + prepareEnvironment: PrepareEnvironmentParams + cleanupEnvironment: CleanupEnvironmentParams - getConfig: GetConfigParams - setConfig: SetConfigParams - deleteConfig: DeleteConfigParams + getSecret: GetSecretParams + setSecret: SetSecretParams + deleteSecret: DeleteSecretParams getLoginStatus: GetLoginStatusParams login: LoginParams logout: LogoutParams } -export interface GetModuleBuildStatusParams extends PluginModuleActionParamsBase { -} +/** + * Module actions + */ +export interface GetBuildStatusParams extends PluginModuleActionParamsBase { } +export const getBuildStatusParamsSchema = moduleActionParamsSchema -export interface BuildModuleParams extends PluginModuleActionParamsBase { -} +export interface BuildModuleParams extends PluginModuleActionParamsBase { } +export const buildModuleParamsSchema = moduleActionParamsSchema -export interface PushModuleParams extends PluginModuleActionParamsBase { -} +export interface PushModuleParams extends PluginModuleActionParamsBase { } +export const pushModuleParamsSchema = moduleActionParamsSchema export interface RunModuleParams extends PluginModuleActionParamsBase { command: string[] @@ -111,6 +161,23 @@ export interface RunModuleParams extends PluginModule silent: boolean timeout?: number } +const runBaseParams = { + interactive: Joi.boolean() + .description("Whether to run the module interactively (i.e. attach to the terminal)."), + runtimeContext: runtimeContextSchema, + silent: Joi.boolean() + .description("Set to false if the output should not be logged to the console."), + timeout: Joi.number() + .optional() + .description("If set, how long to run the command before timing out."), +} +const runModuleBaseSchema = moduleActionParamsSchema + .keys(runBaseParams) +export const runModuleParamsSchema = runModuleBaseSchema + .keys({ + command: joiArray(Joi.string()) + .description("The command to run in the module."), + }) export interface TestModuleParams extends PluginModuleActionParamsBase { interactive: boolean @@ -118,39 +185,83 @@ export interface TestModuleParams extends PluginModul silent: boolean testConfig: T["testConfigs"][0] } +export const testModuleParamsSchema = runModuleBaseSchema + .keys({ + testConfig: testConfigSchema, + }) export interface GetTestResultParams extends PluginModuleActionParamsBase { testName: string version: ModuleVersion } +export const getTestResultParamsSchema = moduleActionParamsSchema + .keys({ + testName: Joi.string() + .description("A unique name to identify the test run."), + version: moduleVersionSchema, + }) +/** + * Service actions + */ export interface GetServiceStatusParams extends PluginServiceActionParamsBase { runtimeContext: RuntimeContext } +export const getServiceStatusParamsSchema = serviceActionParamsSchema + .keys({ + runtimeContext: runtimeContextSchema, + }) export interface DeployServiceParams extends PluginServiceActionParamsBase { - force?: boolean, + force: boolean runtimeContext: RuntimeContext } +export const deployServiceParamsSchema = serviceActionParamsSchema + .keys({ + force: Joi.boolean() + .description("Whether to force a re-deploy, even if the service is already deployed."), + runtimeContext: runtimeContextSchema, + }) export interface DeleteServiceParams extends PluginServiceActionParamsBase { runtimeContext: RuntimeContext } +export const deleteServiceParamsSchema = serviceActionParamsSchema + .keys({ + runtimeContext: runtimeContextSchema, + }) -export interface GetServiceOutputsParams extends PluginServiceActionParamsBase { -} +export interface GetServiceOutputsParams extends PluginServiceActionParamsBase { } +export const getServiceOutputsParamsSchema = serviceActionParamsSchema export interface ExecInServiceParams extends PluginServiceActionParamsBase { - command: string[], + command: string[] runtimeContext: RuntimeContext } +export const execInServiceParamsSchema = serviceActionParamsSchema + .keys({ + command: joiArray(Joi.string()) + .description("The command to run alongside the service."), + runtimeContext: runtimeContextSchema, + }) export interface GetServiceLogsParams extends PluginServiceActionParamsBase { runtimeContext: RuntimeContext - stream: Stream, - tail?: boolean, - startTime?: Date, + stream: Stream + tail: boolean + startTime?: Date } +export const getServiceLogsParamsSchema = serviceActionParamsSchema + .keys({ + runtimeContext: runtimeContextSchema, + stream: Joi.object() + .description("A Stream object, to write the logs to."), + tail: Joi.boolean() + .description("Whether to keep listening for logs until aborted."), + startTime: Joi.date() + .optional() + .description("If set, only return logs that are as new or newer than this date."), + }) export interface RunServiceParams extends PluginServiceActionParamsBase { interactive: boolean @@ -158,6 +269,8 @@ export interface RunServiceParams extends PluginServi silent: boolean timeout?: number } +export const runServiceParamsSchema = serviceActionParamsSchema + .keys(runBaseParams) export interface ServiceActionParams { getServiceStatus: GetServiceStatusParams @@ -169,10 +282,11 @@ export interface ServiceActionParams { runService: RunServiceParams } -export interface ModuleActionParams extends ServiceActionParams { - parseModule: ParseModuleParams - getModuleBuildStatus: GetModuleBuildStatusParams - buildModule: BuildModuleParams +export interface ModuleActionParams { + describeType: DescribeModuleTypeParams, + validate: ValidateModuleParams + getBuildStatus: GetBuildStatusParams + build: BuildModuleParams pushModule: PushModuleParams runModule: RunModuleParams testModule: TestModuleParams diff --git a/garden-cli/src/types/plugin/plugin.ts b/garden-cli/src/types/plugin/plugin.ts index 3388cb1d18..56bc3e75e8 100644 --- a/garden-cli/src/types/plugin/plugin.ts +++ b/garden-cli/src/types/plugin/plugin.ts @@ -8,48 +8,71 @@ import * as Joi from "joi" import { mapValues } from "lodash" +import dedent = require("dedent") import { joiArray, joiIdentifier, joiIdentifierMap, - PrimitiveMap, } from "../../config/common" import { Module } from "../module" import { serviceStatusSchema } from "../service" import { serviceOutputsSchema } from "../../config/service" import { LogNode } from "../../logger/log-node" +import { Provider } from "../../config/project" +import { + prepareEnvironmentParamsSchema, + cleanupEnvironmentParamsSchema, + getSecretParamsSchema, + setSecretParamsSchema, + deleteSecretParamsSchema, + getLoginStatusParamsSchema, + loginParamsSchema, + logoutParamsSchema, + getServiceStatusParamsSchema, + deployServiceParamsSchema, + deleteServiceParamsSchema, + getServiceOutputsParamsSchema, + execInServiceParamsSchema, + getServiceLogsParamsSchema, + runServiceParamsSchema, + describeModuleTypeParamsSchema, + validateModuleParamsSchema, + getBuildStatusParamsSchema, + buildModuleParamsSchema, + pushModuleParamsSchema, + runModuleParamsSchema, + testModuleParamsSchema, + getTestResultParamsSchema, +} from "./params" import { buildModuleResultSchema, buildStatusSchema, - configureEnvironmentResultSchema, - deleteConfigResultSchema, - destroyEnvironmentResultSchema, + prepareEnvironmentResultSchema, + deleteSecretResultSchema, + cleanupEnvironmentResultSchema, environmentStatusSchema, execInServiceResultSchema, - getConfigResultSchema, + getSecretResultSchema, getServiceLogsResultSchema, getTestResultSchema, loginStatusSchema, ModuleActionOutputs, - parseModuleResultSchema, + moduleTypeDescriptionSchema, PluginActionOutputs, pushModuleResultSchema, runResultSchema, ServiceActionOutputs, - setConfigResultSchema, + setSecretResultSchema, testResultSchema, + validateModuleResultSchema, } from "./outputs" import { ModuleActionParams, PluginActionParams, ServiceActionParams, + getEnvironmentStatusParamsSchema, } from "./params" -export interface Provider { - name: string - config: T -} - export type PluginActions = { [P in keyof PluginActionParams]: (params: PluginActionParams[P]) => PluginActionOutputs[P] } @@ -62,91 +85,274 @@ export type ModuleActions = { [P in keyof ModuleActionParams]: (params: ModuleActionParams[P]) => ModuleActionOutputs[P] } +export type ModuleAndServiceActions = ModuleActions & ServiceActions + export type PluginActionName = keyof PluginActions export type ServiceActionName = keyof ServiceActions export type ModuleActionName = keyof ModuleActions export interface PluginActionDescription { - description?: string - resultSchema: Joi.Schema, + description: string + paramsSchema: Joi.Schema + resultSchema: Joi.Schema } export const pluginActionDescriptions: { [P in PluginActionName]: PluginActionDescription } = { getEnvironmentStatus: { + description: dedent` + Check if the current environment is ready for use by this plugin. Use this action in combination + with \`prepareEnvironment\` to avoid unnecessary work on startup. + + Called before \`prepareEnvironment\`. If this returns \`{ ready: true }\`, the + \`prepareEnvironment\` action is not called. + `, + paramsSchema: getEnvironmentStatusParamsSchema, resultSchema: environmentStatusSchema, }, - configureEnvironment: { - resultSchema: configureEnvironmentResultSchema, + prepareEnvironment: { + description: dedent` + Make sure the environment is set up for this plugin. Use this action to do any bootstrapping required + before deploying services. + + Called ahead of any service runtime actions (such as \`deployService\`, + \`runModule\` and \`testModule\`), unless \`getEnvironmentStatus\` returns \`{ ready: true }\`. + `, + paramsSchema: prepareEnvironmentParamsSchema, + resultSchema: prepareEnvironmentResultSchema, }, - destroyEnvironment: { - resultSchema: destroyEnvironmentResultSchema, + cleanupEnvironment: { + description: dedent` + Clean up any runtime components, services etc. that this plugin has deployed in the environment. + + Called by the \`garden delete environment\` command. + `, + paramsSchema: cleanupEnvironmentParamsSchema, + resultSchema: cleanupEnvironmentResultSchema, }, - getConfig: { - resultSchema: getConfigResultSchema, + getSecret: { + description: dedent` + Retrieve a secret value for this plugin in the current environment (as set via \`setSecret\`). + `, + paramsSchema: getSecretParamsSchema, + resultSchema: getSecretResultSchema, }, - setConfig: { - resultSchema: setConfigResultSchema, + setSecret: { + description: dedent` + Set a secret for this plugin in the current environment. These variables are + not used by the Garden framework, but the plugin may expose them to services etc. at runtime + (e.g. as environment variables or mounted in containers). + `, + paramsSchema: setSecretParamsSchema, + resultSchema: setSecretResultSchema, }, - deleteConfig: { - resultSchema: deleteConfigResultSchema, + deleteSecret: { + description: dedent` + Remove a secret for this plugin in the current environment (as set via \`setSecret\`). + `, + paramsSchema: deleteSecretParamsSchema, + resultSchema: deleteSecretResultSchema, }, getLoginStatus: { + description: dedent` + Check if the user needs to log in for the current environment. Use this in combination with + \`login\` if the plugin requires some form of authentication or identification before use. + + Called before \`login\`, if a \`login\` handler is specified. If this returns \`{ loggedIn: true }\`, + the \`login\` action is not performed. + `, + paramsSchema: getLoginStatusParamsSchema, resultSchema: loginStatusSchema, }, login: { + description: dedent` + Execute an authentication flow needed before using this plugin in the current environment. + + Unlike other action handlers, any configured \`login\` handlers are called synchronously and sequentially, + so the handler can, for example, trigger external authentication flows, request input from stdin or call + other CLIs that have a sequential flow requiring user input. + + Called by the \`garden login\` command. + `, + paramsSchema: loginParamsSchema, resultSchema: loginStatusSchema, }, logout: { + description: dedent` + Clear any authentication previously performed for this plugin in the current environment. + + Called by the \`garden login\` command. + `, + paramsSchema: logoutParamsSchema, resultSchema: loginStatusSchema, }, } export const serviceActionDescriptions: { [P in ServiceActionName]: PluginActionDescription } = { getServiceStatus: { + description: dedent` + Check and return the current runtime status of a service. + + Called ahead of any actions that expect a service to be running, as well as the + \`garden get status\` command. + `, + paramsSchema: getServiceStatusParamsSchema, resultSchema: serviceStatusSchema, }, deployService: { + description: dedent` + Deploy the specified service. This should wait until the service is ready and accessible, + and fail if the service doesn't reach a ready state. + + Called by the \`garden deploy\` and \`garden dev\` commands. + `, + paramsSchema: deployServiceParamsSchema, resultSchema: serviceStatusSchema, }, deleteService: { + description: dedent` + Terminate a deployed service. This should wait until the service is no longer running. + + Called by the \`garden delete service\` command. + `, + paramsSchema: deleteServiceParamsSchema, resultSchema: serviceStatusSchema, }, getServiceOutputs: { + description: "DEPRECATED", + paramsSchema: getServiceOutputsParamsSchema, resultSchema: serviceOutputsSchema, }, execInService: { + description: dedent` + Execute the specified command next to a running service, e.g. in a service container. + + Called by the \`garden exec\` command. + `, + paramsSchema: execInServiceParamsSchema, resultSchema: execInServiceResultSchema, }, getServiceLogs: { + description: dedent` + Retrieve a stream of logs for the specified service, optionally waiting listening for new logs. + + Called by the \`garden logs\` command. + `, + paramsSchema: getServiceLogsParamsSchema, resultSchema: getServiceLogsResultSchema, }, runService: { + description: dedent` + Run an ad-hoc instance of the specified service. This should wait until the service completes + execution, and should ideally attach it to the terminal (i.e. pipe the output from the service + to the console, as well as pipe the input from the console to the running service). + + Called by the \`garden run service\` command. + `, + paramsSchema: runServiceParamsSchema, resultSchema: runResultSchema, }, } -export const moduleActionDescriptions: { [P in ModuleActionName]: PluginActionDescription } = { - parseModule: { - resultSchema: parseModuleResultSchema, +export const moduleActionDescriptions: { [P in ModuleActionName | ServiceActionName]: PluginActionDescription } = { + // TODO: implement this method (it is currently not defined or used) + describeType: { + description: dedent` + Return documentation and a schema description of the module type. + + The documentation should be in markdown format. A reference for the module type is automatically + generated based on the provided schema, and a section appended to the provided documentation. + + The schema should be a valid OpenAPI schema describing the configuration keys that the user + should use under the \`module\` key in a \`garden.yml\` configuration file. Note that the schema + should not specify the built-in fields (such as \`name\`, \`type\` and \`description\`). + + Used when auto-generating framework documentation. + `, + paramsSchema: describeModuleTypeParamsSchema, + resultSchema: moduleTypeDescriptionSchema, }, - getModuleBuildStatus: { + validate: { + description: dedent` + Validate and optionally transform the given module configuration. + + Note that this does not need to perform structural schema validation (the framework does that + automatically), but should in turn perform semantic validation to make sure the configuration is sane. + + This can and should also be used to further specify the semantics of the module, including service + configuration and test configuration. Since services and tests are not specified using built-in + framework configuration fields, this action needs to specify those via the \`serviceConfigs\` and + \`testConfigs\` output keys. + `, + paramsSchema: validateModuleParamsSchema, + resultSchema: validateModuleResultSchema, + }, + + getBuildStatus: { + description: dedent` + Check and return the build status of a module, i.e. whether the current version been built. + + Called before running the \`build\` action, which is not run if this returns \`{ ready: true }\`. + `, + paramsSchema: getBuildStatusParamsSchema, resultSchema: buildStatusSchema, }, - buildModule: { + build: { + description: dedent` + Build the current version of a module. This must wait until the build is complete before returning. + `, + paramsSchema: buildModuleParamsSchema, resultSchema: buildModuleResultSchema, }, + pushModule: { + description: dedent` + Push the build for current version of a module to a remote, such as a registry or an artifact store. + `, + paramsSchema: pushModuleParamsSchema, resultSchema: pushModuleResultSchema, }, + runModule: { + description: dedent` + Run an ad-hoc instance of the specified module. This should wait until the execution completes, + and should ideally attach it to the terminal (i.e. pipe the output from the service + to the console, as well as pipe the input from the console to the running service). + + Called by the \`garden run module\` command. + `, + paramsSchema: runModuleParamsSchema, resultSchema: runResultSchema, }, + testModule: { + description: dedent` + Run the specified test for a module. + + This should complete the test run and return the logs from the test run, and signal whether the + tests completed successfully. + + It should also store the test results and provide the accompanying \`getTestResult\` handler, + so that the same version does not need to be tested multiple times. + + Note that the version string provided to this handler may be a hash of the module's version, as + well as any runtime dependencies configured for the test, so it may not match the current version + of the module itself. + `, + paramsSchema: testModuleParamsSchema, resultSchema: testResultSchema, }, getTestResult: { + description: dedent` + Retrieve the test result for the specified version. Use this along with the \`testModule\` handler + to avoid testing the same code repeatedly. + + Note that the version string provided to this handler may be a hash of the module's version, as + well as any runtime dependencies configured for the test, so it may not match the current version + of the module itself. + `, + paramsSchema: getTestResultParamsSchema, resultSchema: getTestResultSchema, }, @@ -164,7 +370,7 @@ export interface GardenPlugin { modules?: string[] actions?: Partial - moduleActions?: { [moduleType: string]: Partial } + moduleActions?: { [moduleType: string]: Partial } } export interface PluginFactoryParams { diff --git a/garden-cli/src/types/service.ts b/garden-cli/src/types/service.ts index d7a9d6bac4..d3b88077cc 100644 --- a/garden-cli/src/types/service.ts +++ b/garden-cli/src/types/service.ts @@ -7,14 +7,15 @@ */ import * as Joi from "joi" -import { PluginContext } from "../plugin-context" import { getEnvVarName } from "../util/util" -import { PrimitiveMap } from "../config/common" +import { PrimitiveMap, joiIdentifier, joiEnvVars, joiIdentifierMap, joiPrimitive } from "../config/common" import { Module, getModuleKey } from "./module" -import { serviceOutputsSchema, ServiceConfig } from "../config/service" +import { serviceOutputsSchema, ServiceConfig, serviceConfigSchema } from "../config/service" import { validate } from "../config/common" import dedent = require("dedent") import { format } from "url" +import { moduleVersionSchema } from "../vcs/base" +import { Garden } from "../garden" import normalizeUrl = require("normalize-url") export interface Service { @@ -24,6 +25,17 @@ export interface Service { spec: M["serviceConfigs"][0]["spec"] } +export const serviceSchema = Joi.object() + .options({ presence: "required" }) + .keys({ + name: joiIdentifier() + .description("The name of the service."), + module: Joi.object().unknown(true), // This causes a stack overflow: Joi.lazy(() => moduleSchema), + config: serviceConfigSchema, + spec: Joi.object() + .description("The raw configuration of the service (specific to each plugin)."), + }) + export function serviceFromConfig(module: M, config: ServiceConfig): Service { return { name: config.name, @@ -141,24 +153,40 @@ export type RuntimeContext = { outputs: PrimitiveMap, }, }, - module: { - name: string, - type: string, - version: string, - }, } +const runtimeDependencySchema = Joi.object() + .keys({ + version: moduleVersionSchema, + outputs: joiEnvVars() + .description("The outputs provided by the service (e.g. endpoint URLs etc.)."), + }) + +export const runtimeContextSchema = Joi.object() + .options({ presence: "required" }) + .keys({ + envVars: Joi.object().pattern(/.+/, joiPrimitive()) + .default(() => ({}), "{}") + .unknown(false) + .description( + "Key/value map of environment variables. Keys must be valid POSIX environment variable names " + + "(must be uppercase) and values must be primitives.", + ), + dependencies: joiIdentifierMap(runtimeDependencySchema) + .description("Map of all the services that this service or test depends on, and their metadata."), + }) + export async function prepareRuntimeContext( - ctx: PluginContext, module: Module, serviceDependencies: Service[], + garden: Garden, module: Module, serviceDependencies: Service[], ): Promise { const buildDepKeys = module.build.dependencies.map(dep => getModuleKey(dep.name, dep.plugin)) - const buildDependencies: Module[] = await ctx.getModules(buildDepKeys) + const buildDependencies: Module[] = await garden.getModules(buildDepKeys) const { versionString } = module.version const envVars = { GARDEN_VERSION: versionString, } - for (const [key, value] of Object.entries(ctx.environmentConfig.variables)) { + for (const [key, value] of Object.entries(garden.environment.variables)) { const envVarName = `GARDEN_VARIABLES_${key.replace(/-/g, "_").toUpperCase()}` envVars[envVarName] = value } @@ -181,7 +209,7 @@ export async function prepareRuntimeContext( } const depContext = deps[dep.name] - const outputs = { ...await ctx.getServiceOutputs({ serviceName: dep.name }), ...dep.config.outputs } + const outputs = { ...await garden.actions.getServiceOutputs({ service: dep }), ...dep.config.outputs } const serviceEnvName = getEnvVarName(dep.name) validate(outputs, serviceOutputsSchema, { context: `outputs for service ${dep.name}` }) @@ -197,11 +225,6 @@ export async function prepareRuntimeContext( return { envVars, dependencies: deps, - module: { - name: module.name, - type: module.type, - version: versionString, - }, } } diff --git a/garden-cli/src/util/ext-source-util.ts b/garden-cli/src/util/ext-source-util.ts index 0d9ef606fb..b4a2350686 100644 --- a/garden-cli/src/util/ext-source-util.ts +++ b/garden-cli/src/util/ext-source-util.ts @@ -21,8 +21,8 @@ import { } from "../config-store" import { ParameterError } from "../exceptions" import { Module } from "../types/module" -import { PluginContext } from "../plugin-context" import { join } from "path" +import { Garden } from "../garden" export type ExternalSourceType = "project" | "module" @@ -49,7 +49,6 @@ export function hashRepoUrl(url: string) { export function hasRemoteSource(module: Module): boolean { return !!module.repositoryUrl } - export function getConfigKey(type: ExternalSourceType): string { return type === "project" ? localConfigKeys.linkedProjectSources : localConfigKeys.linkedModuleSources } @@ -58,37 +57,37 @@ export function getConfigKey(type: ExternalSourceType): string { * Check if any module is linked, including those within an external project source. * Returns true if module path is not under the project root or alternatively if the module is a Garden module. */ -export function isModuleLinked(module: Module, ctx: PluginContext) { +export function isModuleLinked(module: Module, garden: Garden) { const isPluginModule = !!module.plugin - return !pathIsInside(module.path, ctx.projectRoot) && !isPluginModule + return !pathIsInside(module.path, garden.projectRoot) && !isPluginModule } export async function getLinkedSources( - ctx: PluginContext, + garden: Garden, type: ExternalSourceType, ): Promise { - const localConfig = await ctx.localConfigStore.get() + const localConfig = await garden.localConfigStore.get() return (type === "project" ? localConfig.linkedProjectSources : localConfig.linkedModuleSources) || [] } -export async function addLinkedSources({ ctx, sourceType, sources }: { - ctx: PluginContext, +export async function addLinkedSources({ garden, sourceType, sources }: { + garden: Garden, sourceType: ExternalSourceType, sources: LinkedSource[], }): Promise { - const linked = uniqBy([...await getLinkedSources(ctx, sourceType), ...sources], "name") - await ctx.localConfigStore.set([getConfigKey(sourceType)], linked) + const linked = uniqBy([...await getLinkedSources(garden, sourceType), ...sources], "name") + await garden.localConfigStore.set([getConfigKey(sourceType)], linked) return linked } -export async function removeLinkedSources({ ctx, sourceType, names }: { - ctx: PluginContext, +export async function removeLinkedSources({ garden, sourceType, names }: { + garden: Garden, sourceType: ExternalSourceType, names: string[], }): Promise { - const currentlyLinked = await getLinkedSources(ctx, sourceType) + const currentlyLinked = await getLinkedSources(garden, sourceType) const currentNames = currentlyLinked.map(s => s.name) for (const name of names) { @@ -103,6 +102,6 @@ export async function removeLinkedSources({ ctx, sourceType, names }: { } const linked = currentlyLinked.filter(({ name }) => !names.includes(name)) - await ctx.localConfigStore.set([getConfigKey(sourceType)], linked) + await garden.localConfigStore.set([getConfigKey(sourceType)], linked) return linked } diff --git a/garden-cli/src/watch.ts b/garden-cli/src/watch.ts index 80e3f06e34..39e70ccb3f 100644 --- a/garden-cli/src/watch.ts +++ b/garden-cli/src/watch.ts @@ -16,9 +16,9 @@ import { import { basename, parse, relative } from "path" import { pathToCacheContext } from "./cache" import { Module, getModuleKey } from "./types/module" -import { PluginContext } from "./plugin-context" import { getIgnorer, scanDirectory } from "./util/util" import { MODULE_CONFIG_FILENAME } from "./constants" +import { Garden } from "./garden" export type AutoReloadDependants = { [key: string]: Module[] } export type ChangeHandler = (module: Module | null, configChanged: boolean) => Promise @@ -28,7 +28,7 @@ export type ChangeHandler = (module: Module | null, configChanged: boolean) => P Each module is represented at most once in the output. */ export async function withDependants( - ctx: PluginContext, + garden: Garden, modules: Module[], autoReloadDependants: AutoReloadDependants, ): Promise { @@ -47,14 +47,14 @@ export async function withDependants( } // we retrieve the modules again to be sure we have the latest versions - return ctx.getModules(Array.from(moduleSet)) + return garden.getModules(Array.from(moduleSet)) } -export async function computeAutoReloadDependants(ctx: PluginContext): Promise { +export async function computeAutoReloadDependants(garden: Garden): Promise { const dependants = {} - for (const module of await ctx.getModules()) { - const depModules: Module[] = await uniqueDependencyModules(ctx, module) + for (const module of await garden.getModules()) { + const depModules: Module[] = await uniqueDependencyModules(garden, module) for (const dep of depModules) { set(dependants, [dep.name, module.name], module) } @@ -63,28 +63,28 @@ export async function computeAutoReloadDependants(ctx: PluginContext): Promise { +async function uniqueDependencyModules(garden: Garden, module: Module): Promise { const buildDeps = module.build.dependencies.map(d => getModuleKey(d.name, d.plugin)) - const serviceDeps = (await ctx.getServices(module.serviceDependencyNames)).map(s => s.module.name) - return ctx.getModules(uniq(buildDeps.concat(serviceDeps))) + const serviceDeps = (await garden.getServices(module.serviceDependencyNames)).map(s => s.module.name) + return garden.getModules(uniq(buildDeps.concat(serviceDeps))) } export class FSWatcher { private watcher - constructor(private ctx: PluginContext) { + constructor(private garden: Garden) { } async watchModules(modules: Module[], changeHandler: ChangeHandler) { - const projectRoot = this.ctx.projectRoot - const ignorer = await getIgnorer(this.ctx.projectRoot) + const projectRoot = this.garden.projectRoot + const ignorer = await getIgnorer(projectRoot) const onFileChanged = this.makeFileChangedHandler(modules, changeHandler) this.watcher = watch(projectRoot, { ignored: (path, _) => { - const relpath = relative(this.ctx.projectRoot, path) + const relpath = relative(projectRoot, path) return relpath && ignorer.ignores(relpath) }, ignoreInitial: true, @@ -128,7 +128,7 @@ export class FSWatcher { const scanOpts = { filter: (path) => { - const relPath = relative(this.ctx.projectRoot, path) + const relPath = relative(this.garden.projectRoot, path) return !ignorer.ignores(relPath) }, } @@ -198,11 +198,11 @@ export class FSWatcher { private invalidateCached(module: Module) { // invalidate the cache for anything attached to the module path or upwards in the directory tree const cacheContext = pathToCacheContext(module.path) - this.ctx.invalidateCacheUp(cacheContext) + this.garden.cache.invalidateUp(cacheContext) } private async invalidateCachedForAll() { - for (const module of await this.ctx.getModules()) { + for (const module of await this.garden.getModules()) { this.invalidateCached(module) } } diff --git a/garden-cli/test/helpers.ts b/garden-cli/test/helpers.ts index f5e0d83d56..3e038c042b 100644 --- a/garden-cli/test/helpers.ts +++ b/garden-cli/test/helpers.ts @@ -25,12 +25,12 @@ import { Garden } from "../src/garden" import { ModuleConfig } from "../src/config/module" import { mapValues, fromPairs } from "lodash" import { - DeleteConfigParams, - GetConfigParams, - ParseModuleParams, + DeleteSecretParams, + GetSecretParams, + ValidateModuleParams, RunModuleParams, RunServiceParams, - SetConfigParams, + SetSecretParams, } from "../src/types/plugin/params" import { helpers, @@ -62,6 +62,20 @@ export async function profileBlock(description: string, block: () => Promise { @@ -69,23 +83,22 @@ export const testPlugin: PluginFactory = (): GardenPlugin => { return { actions: { - async configureEnvironment() { + async prepareEnvironment() { return {} }, - async setConfig({ key, value }: SetConfigParams) { - _config[key.join(".")] = value + async setSecret({ key, value }: SetSecretParams) { + _config[key] = value return {} }, - async getConfig({ key }: GetConfigParams) { - return { value: _config[key.join(".")] || null } + async getSecret({ key }: GetSecretParams) { + return { value: _config[key] || null } }, - async deleteConfig({ key }: DeleteConfigParams) { - const k = key.join(".") - if (_config[k]) { - delete _config[k] + async deleteSecret({ key }: DeleteSecretParams) { + if (_config[key]) { + delete _config[key] return { found: true } } else { return { found: false } @@ -96,7 +109,7 @@ export const testPlugin: PluginFactory = (): GardenPlugin => { test: { testModule: testGenericModule, - async parseModule({ moduleConfig }: ParseModuleParams) { + async validate({ moduleConfig }: ValidateModuleParams) { moduleConfig.spec = validate( moduleConfig.spec, containerModuleSpecSchema, @@ -121,25 +134,13 @@ export const testPlugin: PluginFactory = (): GardenPlugin => { return moduleConfig }, - buildModule: buildGenericModule, - - async runModule(params: RunModuleParams) { - const version = await params.module.version - - return { - moduleName: params.module.name, - command: params.command, - completedAt: testNow, - output: "OK", - version, - startedAt: testNow, - success: true, - } - }, + build: buildGenericModule, + runModule, async runService({ ctx, service, interactive, runtimeContext, silent, timeout }: RunServiceParams) { - return ctx.runModule({ - moduleName: service.module.name, + return runModule({ + ctx, + module: service.module, command: [service.name], interactive, runtimeContext, @@ -208,23 +209,16 @@ export const makeTestGarden = async (projectRoot: string, extraPlugins: PluginFa return Garden.factory(projectRoot, { plugins }) } -export const makeTestContext = async (projectRoot: string, extraPlugins: PluginFactory[] = []) => { - const garden = await makeTestGarden(projectRoot, extraPlugins) - return garden.getPluginContext() -} - export const makeTestGardenA = async (extraPlugins: PluginFactory[] = []) => { return makeTestGarden(projectRootA, extraPlugins) } -export const makeTestContextA = async (extraPlugins: PluginFactory[] = []) => { - const garden = await makeTestGardenA(extraPlugins) - return garden.getPluginContext() -} - export function stubAction( garden: Garden, pluginName: string, type: T, handler?: PluginActions[T], ) { + if (handler) { + handler["pluginName"] = pluginName + } return td.replace(garden["actionHandlers"][type], pluginName, handler) } diff --git a/garden-cli/test/src/actions.ts b/garden-cli/test/src/actions.ts new file mode 100644 index 0000000000..5d19a928f8 --- /dev/null +++ b/garden-cli/test/src/actions.ts @@ -0,0 +1,533 @@ +import { Garden } from "../../src/garden" +import { makeTestGardenA } from "../helpers" +import { PluginFactory, PluginActions, ModuleAndServiceActions } from "../../src/types/plugin/plugin" +import { validate } from "../../src/config/common" +import { ActionHelper } from "../../src/actions" +import { expect } from "chai" +import { omit } from "lodash" +import { Module } from "../../src/types/module" +import { Service } from "../../src/types/service" +import Stream from "ts-stream" +import { ServiceLogEntry } from "../../src/types/plugin/outputs" +import { + describeModuleTypeParamsSchema, + validateModuleParamsSchema, + getBuildStatusParamsSchema, + buildModuleParamsSchema, + pushModuleParamsSchema, + runModuleParamsSchema, + testModuleParamsSchema, + getTestResultParamsSchema, + getServiceStatusParamsSchema, + deployServiceParamsSchema, + deleteServiceParamsSchema, + getServiceOutputsParamsSchema, + execInServiceParamsSchema, + getServiceLogsParamsSchema, + runServiceParamsSchema, + getEnvironmentStatusParamsSchema, + prepareEnvironmentParamsSchema, + cleanupEnvironmentParamsSchema, + getSecretParamsSchema, + setSecretParamsSchema, + deleteSecretParamsSchema, + loginParamsSchema, + logoutParamsSchema, + getLoginStatusParamsSchema, +} from "../../src/types/plugin/params" + +const now = new Date() + +describe("ActionHelper", () => { + let garden: Garden + let actions: ActionHelper + let module: Module + let service: Service + + before(async () => { + garden = await makeTestGardenA([testPlugin, testPluginB]) + actions = garden.actions + module = await garden.getModule("module-a") + service = await garden.getService("service-a") + }) + + // Note: The test plugins below implicitly validate input params for each of the tests + describe("environment actions", () => { + describe("getEnvironmentStatus", () => { + it("should return a map of statuses for providers that have a getEnvironmentStatus handler", async () => { + const result = await actions.getEnvironmentStatus({}) + expect(result).to.eql({ + "test-plugin": { ready: true }, + "test-plugin-b": { ready: true }, + }) + }) + + it("should optionally filter to single plugin", async () => { + const result = await actions.getEnvironmentStatus({ pluginName: "test-plugin" }) + expect(result).to.eql({ + "test-plugin": { ready: true }, + }) + }) + }) + + describe("prepareEnvironment", () => { + it("should prepare the environment for each configured provider", async () => { + const result = await actions.prepareEnvironment({}) + expect(result).to.eql({ + "test-plugin": { ready: true }, + "test-plugin-b": { ready: true }, + }) + }) + + it("should optionally filter to single plugin", async () => { + const result = await actions.prepareEnvironment({ pluginName: "test-plugin" }) + expect(result).to.eql({ + "test-plugin": { ready: true }, + }) + }) + }) + + describe("cleanupEnvironment", () => { + it("should clean up environment for each configured provider", async () => { + const result = await actions.cleanupEnvironment({}) + expect(result).to.eql({ + "test-plugin": { ready: true }, + "test-plugin-b": { ready: true }, + }) + }) + + it("should optionally filter to single plugin", async () => { + const result = await actions.cleanupEnvironment({ pluginName: "test-plugin" }) + expect(result).to.eql({ + "test-plugin": { ready: true }, + }) + }) + }) + + describe("getSecret", () => { + it("should retrieve a secret from the specified provider", async () => { + const result = await actions.getSecret({ pluginName: "test-plugin", key: "foo" }) + expect(result).to.eql({ value: "foo" }) + }) + }) + + describe("setSecret", () => { + it("should set a secret via the specified provider", async () => { + const result = await actions.setSecret({ pluginName: "test-plugin", key: "foo", value: "boo" }) + expect(result).to.eql({}) + }) + }) + + describe("deleteSecret", () => { + it("should delete a secret from the specified provider", async () => { + const result = await actions.deleteSecret({ pluginName: "test-plugin", key: "foo" }) + expect(result).to.eql({ found: true }) + }) + }) + + describe("login", () => { + it("should run the login procedure for each provider", async () => { + const result = await actions.login({}) + expect(result).to.eql({ + "test-plugin": { loggedIn: true }, + "test-plugin-b": { loggedIn: true }, + }) + }) + + it("should optionally filter to single plugin", async () => { + const result = await actions.login({ pluginName: "test-plugin" }) + expect(result).to.eql({ + "test-plugin": { loggedIn: true }, + }) + }) + }) + + describe("logout", () => { + it("should run the logout procedure for each provider", async () => { + const result = await actions.logout({}) + expect(result).to.eql({ + "test-plugin": { loggedIn: true }, + "test-plugin-b": { loggedIn: true }, + }) + }) + + it("should optionally filter to single plugin", async () => { + const result = await actions.logout({ pluginName: "test-plugin" }) + expect(result).to.eql({ + "test-plugin": { loggedIn: true }, + }) + }) + }) + + describe("getLoginStatus", () => { + it("should get the login status for each provider", async () => { + const result = await actions.getLoginStatus({}) + expect(result).to.eql({ + "test-plugin": { loggedIn: true }, + "test-plugin-b": { loggedIn: true }, + }) + }) + + it("should optionally filter to single plugin", async () => { + const result = await actions.getLoginStatus({ pluginName: "test-plugin" }) + expect(result).to.eql({ + "test-plugin": { loggedIn: true }, + }) + }) + }) + }) + + describe("module actions", () => { + describe("getBuildStatus", () => { + it("should correctly call the corresponding plugin handler", async () => { + const result = await actions.getBuildStatus({ module }) + expect(result).to.eql({ + ready: true, + }) + }) + }) + + describe("build", () => { + it("should correctly call the corresponding plugin handler", async () => { + const result = await actions.build({ module }) + expect(result).to.eql({}) + }) + }) + + describe("pushModule", () => { + it("should correctly call the corresponding plugin handler", async () => { + const result = await actions.pushModule({ module }) + expect(result).to.eql({ + pushed: true, + }) + }) + }) + + describe("runModule", () => { + it("should correctly call the corresponding plugin handler", async () => { + const command = ["npm", "run"] + const result = await actions.runModule({ + module, + command, + interactive: true, + runtimeContext: { + envVars: { FOO: "bar" }, + dependencies: {}, + }, + silent: false, + }) + expect(result).to.eql({ + moduleName: module.name, + command, + completedAt: now, + output: "bla bla", + success: true, + startedAt: now, + version: module.version, + }) + }) + }) + + describe("testModule", () => { + it("should correctly call the corresponding plugin handler", async () => { + const result = await actions.testModule({ + module, + interactive: true, + runtimeContext: { + envVars: { FOO: "bar" }, + dependencies: {}, + }, + silent: false, + testConfig: { + name: "test", + dependencies: [], + timeout: 1234, + spec: {}, + }, + }) + expect(result).to.eql({ + moduleName: module.name, + command: [], + completedAt: now, + output: "bla bla", + success: true, + startedAt: now, + testName: "test", + version: module.version, + }) + }) + }) + + describe("getTestResult", () => { + it("should correctly call the corresponding plugin handler", async () => { + const result = await actions.getTestResult({ + module, + testName: "test", + version: module.version, + }) + expect(result).to.eql({ + moduleName: module.name, + command: [], + completedAt: now, + output: "bla bla", + success: true, + startedAt: now, + testName: "test", + version: module.version, + }) + }) + }) + }) + + describe("service actions", () => { + describe("getServiceStatus", () => { + it("should correctly call the corresponding plugin handler", async () => { + const result = await actions.getServiceStatus({ service }) + expect(result).to.eql({ state: "ready" }) + }) + }) + + describe("deployService", () => { + it("should correctly call the corresponding plugin handler", async () => { + const result = await actions.deployService({ service, force: true }) + expect(result).to.eql({ state: "ready" }) + }) + }) + + describe("deleteService", () => { + it("should correctly call the corresponding plugin handler", async () => { + const result = await actions.deleteService({ service }) + expect(result).to.eql({ state: "ready" }) + }) + }) + + describe("getServiceOutputs", () => { + it("should correctly call the corresponding plugin handler", async () => { + const result = await actions.getServiceOutputs({ service }) + expect(result).to.eql({ foo: "bar" }) + }) + }) + + describe("execInService", () => { + it("should correctly call the corresponding plugin handler", async () => { + const result = await actions.execInService({ service, command: ["foo"] }) + expect(result).to.eql({ code: 0, output: "bla bla" }) + }) + }) + + describe("getServiceLogs", () => { + it("should correctly call the corresponding plugin handler", async () => { + const stream = new Stream() + const result = await actions.getServiceLogs({ service, stream, tail: false }) + expect(result).to.eql({}) + }) + }) + + describe("runService", () => { + it("should correctly call the corresponding plugin handler", async () => { + const result = await actions.runService({ + service, + interactive: true, + runtimeContext: { + envVars: { FOO: "bar" }, + dependencies: {}, + }, + silent: false, + }) + expect(result).to.eql({ + moduleName: service.module.name, + command: ["foo"], + completedAt: now, + output: "bla bla", + success: true, + startedAt: now, + version: service.module.version, + }) + }) + }) + }) +}) + +const testPlugin: PluginFactory = async () => ({ + actions: { + getEnvironmentStatus: async (params) => { + validate(params, getEnvironmentStatusParamsSchema) + return { + ready: true, + } + }, + + prepareEnvironment: async (params) => { + validate(params, prepareEnvironmentParamsSchema) + return {} + }, + + cleanupEnvironment: async (params) => { + validate(params, cleanupEnvironmentParamsSchema) + return {} + }, + + getSecret: async (params) => { + validate(params, getSecretParamsSchema) + return { value: params.key } + }, + + setSecret: async (params) => { + validate(params, setSecretParamsSchema) + return {} + }, + + deleteSecret: async (params) => { + validate(params, deleteSecretParamsSchema) + return { found: true } + }, + + login: async (params) => { + validate(params, loginParamsSchema) + return { loggedIn: true } + }, + + logout: async (params) => { + validate(params, logoutParamsSchema) + return { loggedIn: true } + }, + + getLoginStatus: async (params) => { + validate(params, getLoginStatusParamsSchema) + return { loggedIn: true } + }, + }, + moduleActions: { + test: { + describeType: async (params) => { + validate(params, describeModuleTypeParamsSchema) + return { + docs: "bla bla bla", + schema: {}, + } + }, + + validate: async (params) => { + validate(params, validateModuleParamsSchema) + + const serviceConfigs = params.moduleConfig.spec.services.map(spec => ({ + name: spec.name, + dependencies: spec.dependencies || [], + outputs: {}, + spec, + })) + + return { + ...params.moduleConfig, + serviceConfigs, + } + }, + + getBuildStatus: async (params) => { + validate(params, getBuildStatusParamsSchema) + return { ready: true } + }, + + build: async (params) => { + validate(params, buildModuleParamsSchema) + return {} + }, + + pushModule: async (params) => { + validate(params, pushModuleParamsSchema) + return { pushed: true } + }, + + runModule: async (params) => { + validate(params, runModuleParamsSchema) + return { + moduleName: params.module.name, + command: params.command, + completedAt: now, + output: "bla bla", + success: true, + startedAt: now, + version: params.module.version, + } + }, + + testModule: async (params) => { + validate(params, testModuleParamsSchema) + return { + moduleName: params.module.name, + command: [], + completedAt: now, + output: "bla bla", + success: true, + startedAt: now, + testName: params.testConfig.name, + version: params.module.version, + } + }, + + getTestResult: async (params) => { + validate(params, getTestResultParamsSchema) + return { + moduleName: params.module.name, + command: [], + completedAt: now, + output: "bla bla", + success: true, + startedAt: now, + testName: params.testName, + version: params.module.version, + } + }, + + getServiceStatus: async (params) => { + validate(params, getServiceStatusParamsSchema) + return { state: "ready" } + }, + + deployService: async (params) => { + validate(params, deployServiceParamsSchema) + return { state: "ready" } + }, + + deleteService: async (params) => { + validate(params, deleteServiceParamsSchema) + return { state: "ready" } + }, + + getServiceOutputs: async (params) => { + validate(params, getServiceOutputsParamsSchema) + return { foo: "bar" } + }, + + execInService: async (params) => { + validate(params, execInServiceParamsSchema) + return { + code: 0, + output: "bla bla", + } + }, + + getServiceLogs: async (params) => { + validate(params, getServiceLogsParamsSchema) + return {} + }, + + runService: async (params) => { + validate(params, runServiceParamsSchema) + return { + moduleName: params.module.name, + command: ["foo"], + completedAt: now, + output: "bla bla", + success: true, + startedAt: now, + version: params.module.version, + } + }, + }, + }, +}) +testPlugin.pluginName = "test-plugin" + +const testPluginB: PluginFactory = async (params) => omit(await testPlugin(params), ["moduleActions"]) +testPluginB.pluginName = "test-plugin-b" diff --git a/garden-cli/test/src/build-dir.ts b/garden-cli/test/src/build-dir.ts index 0f9e080d7f..9f8fbaae3c 100644 --- a/garden-cli/test/src/build-dir.ts +++ b/garden-cli/test/src/build-dir.ts @@ -71,7 +71,9 @@ describe("BuildDir", () => { await Bluebird.map(modules, async (module) => { return garden.addTask(new BuildTask({ - module, ctx: garden.getPluginContext(), force: false, + garden, + module, + force: true, })) }) diff --git a/garden-cli/test/src/commands/build.ts b/garden-cli/test/src/commands/build.ts index 375b745430..789b0c93bc 100644 --- a/garden-cli/test/src/commands/build.ts +++ b/garden-cli/test/src/commands/build.ts @@ -6,12 +6,10 @@ import { taskResultOutputs } from "../../helpers" describe("commands.build", () => { it("should build all modules in a project", async () => { const garden = await makeTestGardenA() - const ctx = garden.getPluginContext() const command = new BuildCommand() const { result } = await command.action({ garden, - ctx, args: { module: undefined }, opts: { watch: false, force: true }, }) @@ -25,12 +23,10 @@ describe("commands.build", () => { it("should optionally build single module and its dependencies", async () => { const garden = await makeTestGardenA() - const ctx = garden.getPluginContext() const command = new BuildCommand() const { result } = await command.action({ garden, - ctx, args: { module: ["module-b"] }, opts: { watch: false, force: true }, }) diff --git a/garden-cli/test/src/commands/call.ts b/garden-cli/test/src/commands/call.ts index a2fa319379..f3e8511e29 100644 --- a/garden-cli/test/src/commands/call.ts +++ b/garden-cli/test/src/commands/call.ts @@ -2,7 +2,7 @@ import { join } from "path" import { Garden } from "../../../src/garden" import { CallCommand } from "../../../src/commands/call" import { expect } from "chai" -import { parseContainerModule } from "../../../src/plugins/container" +import { validateContainerModule } from "../../../src/plugins/container" import { PluginFactory } from "../../../src/types/plugin/plugin" import { GetServiceStatusParams } from "../../../src/types/plugin/params" import { ServiceStatus } from "../../../src/types/service" @@ -39,7 +39,7 @@ const testProvider: PluginFactory = () => { return { moduleActions: { - container: { parseModule: parseContainerModule, getServiceStatus }, + container: { validate: validateContainerModule, getServiceStatus }, }, } } @@ -60,14 +60,13 @@ describe("commands.call", () => { it("should find the endpoint for a service and call it with the specified path", async () => { const garden = await Garden.factory(projectRootB, { plugins: [testProvider] }) - const ctx = garden.getPluginContext() const command = new CallCommand() nock("http://service-a.test-project-b.local.app.garden:32000") .get("/path-a") .reply(200, "bla") - const { result } = await command.action({ garden, ctx, args: { serviceAndPath: "service-a/path-a" }, opts: {} }) + const { result } = await command.action({ garden, args: { serviceAndPath: "service-a/path-a" }, opts: {} }) expect(result.url).to.equal("http://service-a.test-project-b.local.app.garden:32000/path-a") expect(result.serviceName).to.equal("service-a") @@ -79,14 +78,13 @@ describe("commands.call", () => { it("should default to the path '/' if that is exposed if no path is requested", async () => { const garden = await Garden.factory(projectRootB, { plugins: [testProvider] }) - const ctx = garden.getPluginContext() const command = new CallCommand() nock("http://service-a.test-project-b.local.app.garden:32000") .get("/path-a") .reply(200, "bla") - const { result } = await command.action({ garden, ctx, args: { serviceAndPath: "service-a" }, opts: {} }) + const { result } = await command.action({ garden, args: { serviceAndPath: "service-a" }, opts: {} }) expect(result.url).to.equal("http://service-a.test-project-b.local.app.garden:32000/path-a") expect(result.serviceName).to.equal("service-a") @@ -97,14 +95,13 @@ describe("commands.call", () => { it("should otherwise use the first defined endpoint if no path is requested", async () => { const garden = await Garden.factory(projectRootB, { plugins: [testProvider] }) - const ctx = garden.getPluginContext() const command = new CallCommand() nock("http://service-b.test-project-b.local.app.garden:32000") .get("/") .reply(200, "bla") - const { result } = await command.action({ garden, ctx, args: { serviceAndPath: "service-b" }, opts: {} }) + const { result } = await command.action({ garden, args: { serviceAndPath: "service-b" }, opts: {} }) expect(result.url).to.equal("http://service-b.test-project-b.local.app.garden:32000/") expect(result.serviceName).to.equal("service-b") @@ -115,11 +112,10 @@ describe("commands.call", () => { it("should error if service isn't running", async () => { const garden = await Garden.factory(projectRootB, { plugins: [testProvider] }) - const ctx = garden.getPluginContext() const command = new CallCommand() try { - await command.action({ garden, ctx, args: { serviceAndPath: "service-d/path-d" }, opts: {} }) + await command.action({ garden, args: { serviceAndPath: "service-d/path-d" }, opts: {} }) } catch (err) { expect(err.type).to.equal("runtime") return @@ -130,11 +126,10 @@ describe("commands.call", () => { it("should error if service has no endpoints", async () => { const garden = await Garden.factory(projectRootB, { plugins: [testProvider] }) - const ctx = garden.getPluginContext() const command = new CallCommand() try { - await command.action({ garden, ctx, args: { serviceAndPath: "service-c/path-c" }, opts: {} }) + await command.action({ garden, args: { serviceAndPath: "service-c/path-c" }, opts: {} }) } catch (err) { expect(err.type).to.equal("parameter") return @@ -145,11 +140,10 @@ describe("commands.call", () => { it("should error if service has no matching endpoints", async () => { const garden = await Garden.factory(projectRootB, { plugins: [testProvider] }) - const ctx = garden.getPluginContext() const command = new CallCommand() try { - await command.action({ garden, ctx, args: { serviceAndPath: "service-a/bla" }, opts: {} }) + await command.action({ garden, args: { serviceAndPath: "service-a/bla" }, opts: {} }) } catch (err) { expect(err.type).to.equal("parameter") return diff --git a/garden-cli/test/src/commands/create/module.ts b/garden-cli/test/src/commands/create/module.ts index 05d0662077..5fa50f2c36 100644 --- a/garden-cli/test/src/commands/create/module.ts +++ b/garden-cli/test/src/commands/create/module.ts @@ -26,10 +26,8 @@ const replaceAddConfigForModule = (returnVal?: ModuleTypeMap) => { } describe("CreateModuleCommand", () => { - afterEach(async () => { await remove(join(projectRoot, "new-module")) - td.reset() }) const cmd = new CreateModuleCommand() @@ -37,71 +35,63 @@ describe("CreateModuleCommand", () => { it("should add a module config to the current directory", async () => { replaceAddConfigForModule() const garden = await makeTestGarden(projectRoot) - const ctx = garden.getPluginContext() - const { result } = await cmd.action({ garden, ctx, args: { "module-dir": "" }, opts: { name: "", type: "" } }) + const { result } = await cmd.action({ garden, args: { "module-dir": "" }, opts: { name: "", type: "" } }) expect(pick(result.module, ["name", "type", "path"])).to.eql({ name: "test-project-create-command", type: "container", - path: ctx.projectRoot, + path: garden.projectRoot, }) }) // garden create module new-module it("should add a module config to new-module directory", async () => { replaceAddConfigForModule() const garden = await makeTestGarden(projectRoot) - const ctx = garden.getPluginContext() const { result } = await cmd.action({ garden, - ctx, args: { "module-dir": "new-module" }, opts: { name: "", type: "" }, }) expect(pick(result.module, ["name", "type", "path"])).to.eql({ name: "new-module", type: "container", - path: join(ctx.projectRoot, "new-module"), + path: join(garden.projectRoot, "new-module"), }) }) // garden create module --name=my-module it("should optionally name the module my-module", async () => { replaceAddConfigForModule() const garden = await makeTestGarden(projectRoot) - const ctx = garden.getPluginContext() const { result } = await cmd.action({ garden, - ctx, args: { "module-dir": "" }, opts: { name: "my-module", type: "" }, }) expect(pick(result.module, ["name", "type", "path"])).to.eql({ name: "my-module", type: "container", - path: ctx.projectRoot, + path: garden.projectRoot, }) }) // garden create module --type=google-cloud-function it("should optionally create a module of a specific type (without prompting)", async () => { const garden = await makeTestGarden(projectRoot) - const ctx = garden.getPluginContext() const { result } = await cmd.action({ garden, - ctx, args: { "module-dir": "" }, opts: { name: "", type: "google-cloud-function" }, }) expect(pick(result.module, ["name", "type", "path"])).to.eql({ name: "test-project-create-command", type: "google-cloud-function", - path: ctx.projectRoot, + path: garden.projectRoot, }) }) // garden create module ___ it("should throw if module name is invalid when inherited from current directory", async () => { replaceAddConfigForModule() const garden = await makeTestGarden(projectRoot) - const ctx = garden.getPluginContext() await expectError( - async () => await cmd.action({ garden, ctx, args: { "module-dir": "___" }, opts: { name: "", type: "" } }), + async () => await cmd.action({ garden, args: { "module-dir": "___" }, opts: { name: "", type: "" } }), "configuration", ) }) @@ -109,9 +99,8 @@ describe("CreateModuleCommand", () => { it("should throw if module name is invalid when explicitly specified", async () => { replaceAddConfigForModule() const garden = await makeTestGarden(projectRoot) - const ctx = garden.getPluginContext() await expectError( - async () => await cmd.action({ garden, ctx, args: { "module-dir": "" }, opts: { name: "___", type: "" } }), + async () => await cmd.action({ garden, args: { "module-dir": "" }, opts: { name: "___", type: "" } }), "configuration", ) }) @@ -119,9 +108,8 @@ describe("CreateModuleCommand", () => { it("should throw if invalid type provided", async () => { replaceAddConfigForModule() const garden = await makeTestGarden(projectRoot) - const ctx = garden.getPluginContext() await expectError( - async () => await cmd.action({ garden, ctx, args: { "module-dir": "" }, opts: { name: "", type: "banana" } }), + async () => await cmd.action({ garden, args: { "module-dir": "" }, opts: { name: "", type: "banana" } }), "parameter", ) }) diff --git a/garden-cli/test/src/commands/create/project.ts b/garden-cli/test/src/commands/create/project.ts index 2568313d74..f340ca9978 100644 --- a/garden-cli/test/src/commands/create/project.ts +++ b/garden-cli/test/src/commands/create/project.ts @@ -38,37 +38,33 @@ const replaceAddConfigForModule = (returnVal?: ModuleTypeMap) => { } describe("CreateProjectCommand", () => { - const projectRoot = join(__dirname, "../../..", "data", "test-project-create-command") const cmd = new CreateProjectCommand() afterEach(async () => { await remove(join(projectRoot, "new-project")) - td.reset() }) // garden create project it("should create a project in the current directory", async () => { replaceRepeatAddModule() const garden = await makeTestGarden(projectRoot) - const ctx = garden.getPluginContext() const { result } = await cmd.action({ garden, - ctx, args: { "project-dir": "" }, - opts: { name: "", "module-dirs": undefined }, + opts: { name: "", "module-dirs": [] }, }) const modules = result.moduleConfigs.map(m => pick(m, ["name", "type", "path"])) const project = pick(result.projectConfig, ["name", "path"]) expect({ modules, project }).to.eql({ modules: [ - { type: "container", name: "module-a", path: join(ctx.projectRoot, "module-a") }, - { type: "container", name: "module-b", path: join(ctx.projectRoot, "module-b") }, + { type: "container", name: "module-a", path: join(garden.projectRoot, "module-a") }, + { type: "container", name: "module-b", path: join(garden.projectRoot, "module-b") }, ], project: { name: "test-project-create-command", - path: ctx.projectRoot, + path: garden.projectRoot, }, }) }) @@ -76,77 +72,67 @@ describe("CreateProjectCommand", () => { it("should create a project in directory new-project", async () => { replaceRepeatAddModule() const garden = await makeTestGarden(projectRoot) - const ctx = garden.getPluginContext() const { result } = await cmd.action({ garden, - ctx, args: { "project-dir": "new-project" }, - opts: { name: "", "module-dirs": undefined }, + opts: { name: "", "module-dirs": [] }, }) expect(pick(result.projectConfig, ["name", "path"])).to.eql({ name: "new-project", - path: join(ctx.projectRoot, "new-project"), + path: join(garden.projectRoot, "new-project"), }) }) // garden create project --name=my-project it("should optionally create a project named my-project", async () => { replaceRepeatAddModule() const garden = await makeTestGarden(projectRoot) - const ctx = garden.getPluginContext() const { result } = await cmd.action({ garden, - ctx, args: { "project-dir": "" }, - opts: { name: "my-project", "module-dirs": undefined }, + opts: { name: "my-project", "module-dirs": [] }, }) expect(pick(result.projectConfig, ["name", "path"])).to.eql({ name: "my-project", - path: join(ctx.projectRoot), + path: join(garden.projectRoot), }) }) // garden create project --module-dirs=. it("should optionally create module configs for modules in current directory", async () => { replaceAddConfigForModule() const garden = await makeTestGarden(projectRoot) - const ctx = garden.getPluginContext() const { result } = await cmd.action({ garden, - ctx, args: { "project-dir": "" }, opts: { name: "", "module-dirs": ["."] }, }) expect(result.moduleConfigs.map(m => pick(m, ["name", "type", "path"]))).to.eql([ - { type: "container", name: "module-a", path: join(ctx.projectRoot, "module-a") }, - { type: "container", name: "module-b", path: join(ctx.projectRoot, "module-b") }, + { type: "container", name: "module-a", path: join(garden.projectRoot, "module-a") }, + { type: "container", name: "module-b", path: join(garden.projectRoot, "module-b") }, ]) }) // garden create project --module-dirs=module-a,module-b it("should optionally create module configs for modules in specified directories", async () => { replaceAddConfigForModule() const garden = await makeTestGarden(projectRoot) - const ctx = garden.getPluginContext() const { result } = await cmd.action({ garden, - ctx, args: { "project-dir": "" }, opts: { name: "", "module-dirs": ["module-a", "module-b"] }, }) expect(result.moduleConfigs.map(m => pick(m, ["name", "type", "path"]))).to.eql([ - { type: "container", name: "child-module-a", path: join(ctx.projectRoot, "module-a", "child-module-a") }, - { type: "container", name: "child-module-b", path: join(ctx.projectRoot, "module-b", "child-module-b") }, + { type: "container", name: "child-module-a", path: join(garden.projectRoot, "module-a", "child-module-a") }, + { type: "container", name: "child-module-b", path: join(garden.projectRoot, "module-b", "child-module-b") }, ]) }) // garden create project ___ it("should throw if project name is invalid when inherited from current directory", async () => { replaceRepeatAddModule() const garden = await makeTestGarden(projectRoot) - const ctx = garden.getPluginContext() await expectError( async () => await cmd.action({ garden, - ctx, args: { "project-dir": "___" }, - opts: { name: "", "module-dirs": undefined }, + opts: { name: "", "module-dirs": [] }, }), "configuration", ) @@ -155,13 +141,11 @@ describe("CreateProjectCommand", () => { it("should throw if project name is invalid when explicitly specified", async () => { replaceRepeatAddModule() const garden = await makeTestGarden(projectRoot) - const ctx = garden.getPluginContext() await expectError( async () => await cmd.action({ garden, - ctx, args: { "project-dir": "" }, - opts: { name: "___", "module-dirs": undefined }, + opts: { name: "___", "module-dirs": [] }, }), "configuration", ) diff --git a/garden-cli/test/src/commands/delete.ts b/garden-cli/test/src/commands/delete.ts index 0e4e75edfd..37dedb5551 100644 --- a/garden-cli/test/src/commands/delete.ts +++ b/garden-cli/test/src/commands/delete.ts @@ -1,5 +1,5 @@ import { - DeleteConfigCommand, + DeleteSecretCommand, DeleteEnvironmentCommand, DeleteServiceCommand, } from "../../../src/commands/delete" @@ -11,40 +11,30 @@ import { expect } from "chai" import { ServiceStatus } from "../../../src/types/service" import { DeleteServiceParams } from "../../../src/types/plugin/params" -describe("DeleteConfigCommand", () => { - it("should delete a config variable", async () => { +describe("DeleteSecretCommand", () => { + const pluginName = "test-plugin" + const provider = pluginName + + it("should delete a secret", async () => { const garden = await makeTestGardenA() - const ctx = garden.getPluginContext() - const command = new DeleteConfigCommand() + const command = new DeleteSecretCommand() - const key = ["project", "mykey"] + const key = "mykey" const value = "myvalue" - await ctx.setConfig({ key, value }) - - await command.action({ garden, ctx, args: { key: "project.mykey" }, opts: {} }) + await garden.actions.setSecret({ key, value, pluginName }) - expect(await ctx.getConfig({ key })).to.eql({ value: null }) - }) + await command.action({ garden, args: { provider, key }, opts: {} }) - it("should throw on invalid key", async () => { - const garden = await makeTestGardenA() - const ctx = garden.getPluginContext() - const command = new DeleteConfigCommand() - - await expectError( - async () => await command.action({ garden, ctx, args: { key: "bla.mykey" }, opts: {} }), - "parameter", - ) + expect(await garden.actions.getSecret({ pluginName, key })).to.eql({ value: null }) }) it("should throw on missing key", async () => { const garden = await makeTestGardenA() - const ctx = garden.getPluginContext() - const command = new DeleteConfigCommand() + const command = new DeleteSecretCommand() await expectError( - async () => await command.action({ garden, ctx, args: { key: "project.mykey" }, opts: {} }), + async () => await command.action({ garden, args: { provider, key: "foo" }, opts: {} }), "not-found", ) }) @@ -56,8 +46,8 @@ describe("DeleteEnvironmentCommand", () => { const testEnvStatuses: { [key: string]: EnvironmentStatus } = {} - const destroyEnvironment = async () => { - testEnvStatuses[name] = { configured: false } + const cleanupEnvironment = async () => { + testEnvStatuses[name] = { ready: false } return {} } @@ -67,7 +57,7 @@ describe("DeleteEnvironmentCommand", () => { return { actions: { - destroyEnvironment, + cleanupEnvironment, getEnvironmentStatus, }, } @@ -80,11 +70,10 @@ describe("DeleteEnvironmentCommand", () => { it("should destroy environment", async () => { const garden = await Garden.factory(projectRootB, { plugins: [testProvider] }) - const ctx = garden.getPluginContext() - const { result } = await command.action({ garden, ctx, args: {}, opts: {} }) + const { result } = await command.action({ garden, args: {}, opts: {} }) - expect(result!["test-plugin"]["configured"]).to.be.false + expect(result!["test-plugin"]["ready"]).to.be.false }) }) @@ -121,9 +110,8 @@ describe("DeleteServiceCommand", () => { it("should return the status of the deleted service", async () => { const garden = await Garden.factory(projectRootB, { plugins: [testProvider] }) - const ctx = garden.getPluginContext() - const { result } = await command.action({ garden, ctx, args: { service: ["service-a"] }, opts: {} }) + const { result } = await command.action({ garden, args: { service: ["service-a"] }, opts: {} }) expect(result).to.eql({ "service-a": { state: "unknown", endpoints: [] }, }) @@ -131,9 +119,8 @@ describe("DeleteServiceCommand", () => { it("should return the status of the deleted services", async () => { const garden = await Garden.factory(projectRootB, { plugins: [testProvider] }) - const ctx = garden.getPluginContext() - const { result } = await command.action({ garden, ctx, args: { service: ["service-a", "service-b"] }, opts: {} }) + const { result } = await command.action({ garden, args: { service: ["service-a", "service-b"] }, opts: {} }) expect(result).to.eql({ "service-a": { state: "unknown", endpoints: [] }, "service-b": { state: "unknown", endpoints: [] }, diff --git a/garden-cli/test/src/commands/deploy.ts b/garden-cli/test/src/commands/deploy.ts index 39c8518582..12856a7dfe 100644 --- a/garden-cli/test/src/commands/deploy.ts +++ b/garden-cli/test/src/commands/deploy.ts @@ -2,7 +2,7 @@ import { join } from "path" import { Garden } from "../../../src/garden" import { DeployCommand } from "../../../src/commands/deploy" import { expect } from "chai" -import { parseContainerModule } from "../../../src/plugins/container" +import { validateContainerModule } from "../../../src/plugins/container" import { buildGenericModule } from "../../../src/plugins/generic" import { PluginFactory, @@ -48,8 +48,8 @@ const testProvider: PluginFactory = () => { return { moduleActions: { container: { - parseModule: parseContainerModule, - buildModule: buildGenericModule, + validate: validateContainerModule, + build: buildGenericModule, deployService, getServiceStatus, }, @@ -67,12 +67,10 @@ describe("DeployCommand", () => { it("should build and deploy all modules in a project", async () => { const garden = await Garden.factory(projectRootB, { plugins: [testProvider] }) - const ctx = garden.getPluginContext() const command = new DeployCommand() const { result } = await command.action({ garden, - ctx, args: { service: undefined, }, @@ -96,12 +94,10 @@ describe("DeployCommand", () => { it("should optionally build and deploy single service and its dependencies", async () => { const garden = await Garden.factory(projectRootB, { plugins: [testProvider] }) - const ctx = garden.getPluginContext() const command = new DeployCommand() const { result } = await command.action({ garden, - ctx, args: { service: ["service-b"], }, diff --git a/garden-cli/test/src/commands/get.ts b/garden-cli/test/src/commands/get.ts index 12dc1aae4c..8ef644621e 100644 --- a/garden-cli/test/src/commands/get.ts +++ b/garden-cli/test/src/commands/get.ts @@ -1,38 +1,28 @@ import { expect } from "chai" import { expectError, makeTestGardenA } from "../../helpers" -import { GetConfigCommand } from "../../../src/commands/get" +import { GetSecretCommand } from "../../../src/commands/get" + +describe("GetSecretCommand", () => { + const pluginName = "test-plugin" + const provider = pluginName -describe("GetConfigCommand", () => { it("should get a config variable", async () => { const garden = await makeTestGardenA() - const ctx = garden.getPluginContext() - const command = new GetConfigCommand() + const command = new GetSecretCommand() - await ctx.setConfig({ key: ["project", "mykey"], value: "myvalue" }) + await garden.actions.setSecret({ pluginName, key: "project.mykey", value: "myvalue" }) - const res = await command.action({ garden, ctx, args: { key: "project.mykey" }, opts: {} }) + const res = await command.action({ garden, args: { provider, key: "project.mykey" }, opts: {} }) expect(res).to.eql({ "project.mykey": "myvalue" }) }) - it("should throw on invalid key", async () => { - const garden = await makeTestGardenA() - const ctx = garden.getPluginContext() - const command = new GetConfigCommand() - - await expectError( - async () => await command.action({ garden, ctx, args: { key: "bla.mykey" }, opts: {} }), - "parameter", - ) - }) - it("should throw on missing key", async () => { const garden = await makeTestGardenA() - const ctx = garden.getPluginContext() - const command = new GetConfigCommand() + const command = new GetSecretCommand() await expectError( - async () => await command.action({ garden, ctx, args: { key: "project.mykey" }, opts: {} }), + async () => await command.action({ garden, args: { provider, key: "project.mykey" }, opts: {} }), "not-found", ) }) diff --git a/garden-cli/test/src/commands/link.ts b/garden-cli/test/src/commands/link.ts index 48627f847f..13bbe48571 100644 --- a/garden-cli/test/src/commands/link.ts +++ b/garden-cli/test/src/commands/link.ts @@ -9,13 +9,11 @@ import { stubExtSources, makeTestGarden, } from "../../helpers" -import { PluginContext } from "../../../src/plugin-context" import { LinkSourceCommand } from "../../../src/commands/link/source" import { Garden } from "../../../src/garden" describe("LinkCommand", () => { let garden: Garden - let ctx: PluginContext describe("LinkModuleCommand", () => { const cmd = new LinkModuleCommand() @@ -23,7 +21,6 @@ describe("LinkCommand", () => { beforeEach(async () => { garden = await makeTestGarden(projectRoot) - ctx = garden.getPluginContext() stubExtSources(garden) }) @@ -34,7 +31,6 @@ describe("LinkCommand", () => { it("should link external modules", async () => { await cmd.action({ garden, - ctx, args: { module: "module-a", path: join(projectRoot, "mock-local-path", "module-a"), @@ -42,7 +38,7 @@ describe("LinkCommand", () => { opts: {}, }) - const { linkedModuleSources } = await ctx.localConfigStore.get() + const { linkedModuleSources } = await garden.localConfigStore.get() expect(linkedModuleSources).to.eql([ { name: "module-a", path: join(projectRoot, "mock-local-path", "module-a") }, @@ -52,7 +48,6 @@ describe("LinkCommand", () => { it("should handle relative paths", async () => { await cmd.action({ garden, - ctx, args: { module: "module-a", path: join("mock-local-path", "module-a"), @@ -60,7 +55,7 @@ describe("LinkCommand", () => { opts: {}, }) - const { linkedModuleSources } = await ctx.localConfigStore.get() + const { linkedModuleSources } = await garden.localConfigStore.get() expect(linkedModuleSources).to.eql([ { name: "module-a", path: join(projectRoot, "mock-local-path", "module-a") }, @@ -72,7 +67,6 @@ describe("LinkCommand", () => { async () => ( await cmd.action({ garden, - ctx, args: { module: "banana", path: "", @@ -91,7 +85,6 @@ describe("LinkCommand", () => { beforeEach(async () => { garden = await makeTestGarden(projectRoot) - ctx = garden.getPluginContext() stubExtSources(garden) }) @@ -102,7 +95,6 @@ describe("LinkCommand", () => { it("should link external sources", async () => { await cmd.action({ garden, - ctx, args: { source: "source-a", path: join(projectRoot, "mock-local-path", "source-a"), @@ -110,7 +102,7 @@ describe("LinkCommand", () => { opts: {}, }) - const { linkedProjectSources } = await ctx.localConfigStore.get() + const { linkedProjectSources } = await garden.localConfigStore.get() expect(linkedProjectSources).to.eql([ { name: "source-a", path: join(projectRoot, "mock-local-path", "source-a") }, @@ -120,7 +112,6 @@ describe("LinkCommand", () => { it("should handle relative paths", async () => { await cmd.action({ garden, - ctx, args: { source: "source-a", path: join("mock-local-path", "source-a"), @@ -128,7 +119,7 @@ describe("LinkCommand", () => { opts: {}, }) - const { linkedProjectSources } = await ctx.localConfigStore.get() + const { linkedProjectSources } = await garden.localConfigStore.get() expect(linkedProjectSources).to.eql([ { name: "source-a", path: join(projectRoot, "mock-local-path", "source-a") }, diff --git a/garden-cli/test/src/commands/login.ts b/garden-cli/test/src/commands/login.ts index 7c49e68c7b..f60aed1826 100644 --- a/garden-cli/test/src/commands/login.ts +++ b/garden-cli/test/src/commands/login.ts @@ -1,27 +1,19 @@ import { expect } from "chai" import { makeTestGardenA, stubAction } from "../../helpers" -import * as td from "testdouble" import { LoginCommand } from "../../../src/commands/login" describe("LoginCommand", () => { - - afterEach(async () => { - td.reset() - }) - const command = new LoginCommand() it("should log in to provider", async () => { const garden = await makeTestGardenA() - const ctx = garden.getPluginContext() stubAction(garden, "test-plugin", "login", async () => ({ loggedIn: true })) stubAction(garden, "test-plugin", "getLoginStatus", async () => ({ loggedIn: true })) - const { result } = await command.action({ garden, ctx, args: {}, opts: {} }) + const { result } = await command.action({ garden, args: {}, opts: {} }) expect(result).to.eql({ "test-plugin": { loggedIn: true } }) }) - }) diff --git a/garden-cli/test/src/commands/logout.ts b/garden-cli/test/src/commands/logout.ts index a49f6870de..efb2a2bdb8 100644 --- a/garden-cli/test/src/commands/logout.ts +++ b/garden-cli/test/src/commands/logout.ts @@ -1,27 +1,19 @@ import { expect } from "chai" import { makeTestGardenA, stubAction } from "../../helpers" -import * as td from "testdouble" import { LogoutCommand } from "../../../src/commands/logout" describe("LogoutCommand", () => { - - afterEach(async () => { - td.reset() - }) - const command = new LogoutCommand() it("should log out from a provider", async () => { const garden = await makeTestGardenA() - const ctx = garden.getPluginContext() stubAction(garden, "test-plugin", "logout", async () => ({ loggedIn: false })) stubAction(garden, "test-plugin", "getLoginStatus", async () => ({ loggedIn: false })) - const { result } = await command.action({ garden, ctx, args: {}, opts: {} }) + const { result } = await command.action({ garden, args: {}, opts: {} }) expect(result).to.eql({ "test-plugin": { loggedIn: false } }) }) - }) diff --git a/garden-cli/test/src/commands/push.ts b/garden-cli/test/src/commands/push.ts index a6ee78ed61..7dd42c7b09 100644 --- a/garden-cli/test/src/commands/push.ts +++ b/garden-cli/test/src/commands/push.ts @@ -4,7 +4,7 @@ import { join } from "path" import { expect } from "chai" import * as td from "testdouble" import { Garden } from "../../../src/garden" -import { parseContainerModule } from "../../../src/plugins/container" +import { validateContainerModule } from "../../../src/plugins/container" import { PluginFactory } from "../../../src/types/plugin/plugin" import { PushCommand } from "../../../src/commands/push" import { makeTestGardenA } from "../../helpers" @@ -13,11 +13,11 @@ import { ModuleVersion } from "../../../src/vcs/base" const projectRootB = join(__dirname, "..", "..", "data", "test-project-b") -const getModuleBuildStatus = async () => { +const getBuildStatus = async () => { return { ready: true } } -const buildModule = async () => { +const build = async () => { return { fresh: true } } @@ -29,9 +29,9 @@ const testProvider: PluginFactory = () => { return { moduleActions: { container: { - parseModule: parseContainerModule, - getModuleBuildStatus, - buildModule, + validate: validateContainerModule, + getBuildStatus, + build, pushModule, }, }, @@ -44,9 +44,9 @@ const testProviderB: PluginFactory = () => { return { moduleActions: { container: { - parseModule: parseContainerModule, - getModuleBuildStatus, - buildModule, + validate: validateContainerModule, + getBuildStatus, + build, }, }, } @@ -58,9 +58,9 @@ const testProviderNoPush: PluginFactory = () => { return { moduleActions: { container: { - parseModule: parseContainerModule, - getModuleBuildStatus, - buildModule, + validate: validateContainerModule, + getBuildStatus, + build, }, }, } @@ -71,19 +71,18 @@ testProviderNoPush.pluginName = "test-plugin" async function getTestGarden() { const garden = await Garden.factory(projectRootB, { plugins: [testProvider] }) await garden.clearBuilds() - return { garden, ctx: garden.getPluginContext() } + return garden } describe("PushCommand", () => { // TODO: Verify that services don't get redeployed when same version is already deployed. it("should build and push modules in a project", async () => { - const { garden, ctx } = await getTestGarden() + const garden = await getTestGarden() const command = new PushCommand() const { result } = await command.action({ garden, - ctx, args: { module: undefined, }, @@ -103,12 +102,11 @@ describe("PushCommand", () => { }) it("should optionally force new build", async () => { - const { garden, ctx } = await getTestGarden() + const garden = await getTestGarden() const command = new PushCommand() const { result } = await command.action({ garden, - ctx, args: { module: undefined, }, @@ -128,12 +126,11 @@ describe("PushCommand", () => { }) it("should optionally build selected module", async () => { - const { garden, ctx } = await getTestGarden() + const garden = await getTestGarden() const command = new PushCommand() const { result } = await command.action({ garden, - ctx, args: { module: ["module-a"], }, @@ -150,12 +147,11 @@ describe("PushCommand", () => { }) it("should respect allowPush flag", async () => { - const { garden, ctx } = await getTestGarden() + const garden = await getTestGarden() const command = new PushCommand() const { result } = await command.action({ garden, - ctx, args: { module: ["module-c"], }, @@ -172,14 +168,12 @@ describe("PushCommand", () => { it("should fail gracefully if module does not have a provider for push", async () => { const garden = await makeTestGardenA() - const ctx = garden.getPluginContext() await garden.clearBuilds() const command = new PushCommand() const { result } = await command.action({ garden, - ctx, args: { module: ["module-a"], }, @@ -209,16 +203,14 @@ describe("PushCommand", () => { dependencyVersions: {}, } }) - garden = (await getTestGarden()).garden + garden = await getTestGarden() }) it("should throw if module is dirty", async () => { - const ctx = garden.getPluginContext() const command = new PushCommand() await expectError(() => command.action({ garden, - ctx, args: { module: ["module-a"], }, @@ -230,12 +222,10 @@ describe("PushCommand", () => { }) it("should optionally allow pushing dirty commits", async () => { - const ctx = garden.getPluginContext() const command = new PushCommand() const { result } = await command.action({ garden, - ctx, args: { module: ["module-a"], }, diff --git a/garden-cli/test/src/commands/run/module.ts b/garden-cli/test/src/commands/run/module.ts index 29cb14e8ff..eccbc17cd0 100644 --- a/garden-cli/test/src/commands/run/module.ts +++ b/garden-cli/test/src/commands/run/module.ts @@ -20,8 +20,6 @@ describe("RunModuleCommand", () => { }) it("should run a module without a command param", async () => { - const ctx = garden.getPluginContext() - await garden.addModule(makeTestModule({ name: "run-test", path: garden.projectRoot, @@ -30,8 +28,7 @@ describe("RunModuleCommand", () => { const cmd = new RunModuleCommand() const { result } = await cmd.action({ garden, - ctx, - args: { module: "run-test", command: undefined }, + args: { module: "run-test", command: [] }, opts: { interactive: false, "force-build": false }, }) @@ -49,8 +46,6 @@ describe("RunModuleCommand", () => { }) it("should run a module with a command param", async () => { - const ctx = garden.getPluginContext() - garden.addModule(makeTestModule({ name: "run-test", path: garden.projectRoot, @@ -59,8 +54,7 @@ describe("RunModuleCommand", () => { const cmd = new RunModuleCommand() const { result } = await cmd.action({ garden, - ctx, - args: { module: "run-test", command: "my command" }, + args: { module: "run-test", command: ["my", "command"] }, opts: { interactive: false, "force-build": false }, }) diff --git a/garden-cli/test/src/commands/run/service.ts b/garden-cli/test/src/commands/run/service.ts index 7ef0e0d798..f8d6a4ade3 100644 --- a/garden-cli/test/src/commands/run/service.ts +++ b/garden-cli/test/src/commands/run/service.ts @@ -20,8 +20,6 @@ describe("RunServiceCommand", () => { }) it("should run a service", async () => { - const ctx = garden.getPluginContext() - garden.addModule(makeTestModule({ name: "run-test", serviceConfigs: [{ name: "test-service", dependencies: [], outputs: {}, spec: {} }], @@ -30,7 +28,6 @@ describe("RunServiceCommand", () => { const cmd = new RunServiceCommand() const { result } = await cmd.action({ garden, - ctx, args: { service: "test-service" }, opts: { "force-build": false }, }) diff --git a/garden-cli/test/src/commands/scan.ts b/garden-cli/test/src/commands/scan.ts index bdf05cc9cc..6e667ae569 100644 --- a/garden-cli/test/src/commands/scan.ts +++ b/garden-cli/test/src/commands/scan.ts @@ -6,10 +6,9 @@ describe("ScanCommand", () => { for (const [name, path] of Object.entries(getExampleProjects())) { it(`should successfully scan the ${name} project`, async () => { const garden = await Garden.factory(path) - const ctx = garden.getPluginContext() const command = new ScanCommand() - await command.action({ garden, ctx, args: {}, opts: {} }) + await command.action({ garden, args: {}, opts: {} }) }) } }) diff --git a/garden-cli/test/src/commands/set.ts b/garden-cli/test/src/commands/set.ts index e5a2e2c8e2..e9c17d4045 100644 --- a/garden-cli/test/src/commands/set.ts +++ b/garden-cli/test/src/commands/set.ts @@ -1,26 +1,17 @@ import { expect } from "chai" -import { SetConfigCommand } from "../../../src/commands/set" -import { expectError, makeTestGardenA } from "../../helpers" +import { SetSecretCommand } from "../../../src/commands/set" +import { makeTestGardenA } from "../../helpers" + +describe("SetSecretCommand", () => { + const pluginName = "test-plugin" + const provider = pluginName -describe("SetConfigCommand", () => { it("should set a config variable", async () => { const garden = await makeTestGardenA() - const ctx = garden.getPluginContext() - const command = new SetConfigCommand() - - await command.action({ garden, ctx, args: { key: "project.mykey", value: "myvalue" }, opts: {} }) + const command = new SetSecretCommand() - expect(await ctx.getConfig({ key: ["project", "mykey"] })).to.eql({ value: "myvalue" }) - }) - - it("should throw on invalid key", async () => { - const garden = await makeTestGardenA() - const ctx = garden.getPluginContext() - const command = new SetConfigCommand() + await command.action({ garden, args: { provider, key: "mykey", value: "myvalue" }, opts: {} }) - await expectError( - async () => await command.action({ garden, ctx, args: { key: "bla.mykey", value: "ble" }, opts: {} }), - "parameter", - ) + expect(await garden.actions.getSecret({ pluginName, key: "mykey" })).to.eql({ value: "myvalue" }) }) }) diff --git a/garden-cli/test/src/commands/test.ts b/garden-cli/test/src/commands/test.ts index 26d94e79ed..25560e6e6c 100644 --- a/garden-cli/test/src/commands/test.ts +++ b/garden-cli/test/src/commands/test.ts @@ -6,12 +6,10 @@ import { makeTestGardenA, taskResultOutputs } from "../../helpers" describe("commands.test", () => { it("should run all tests in a simple project", async () => { const garden = await makeTestGardenA() - const ctx = garden.getPluginContext() const command = new TestCommand() const { result } = await command.action({ garden, - ctx, args: { module: undefined }, opts: { name: undefined, force: true, "force-build": true, watch: false }, }) @@ -43,12 +41,10 @@ describe("commands.test", () => { it("should optionally test single module", async () => { const garden = await makeTestGardenA() - const ctx = garden.getPluginContext() const command = new TestCommand() const { result } = await command.action({ garden, - ctx, args: { module: ["module-a"] }, opts: { name: undefined, force: true, "force-build": true, watch: false }, }) diff --git a/garden-cli/test/src/commands/unlink.ts b/garden-cli/test/src/commands/unlink.ts index 64cc546748..3fd8789f05 100644 --- a/garden-cli/test/src/commands/unlink.ts +++ b/garden-cli/test/src/commands/unlink.ts @@ -9,14 +9,12 @@ import { cleanProject, makeTestGarden, } from "../../helpers" -import { PluginContext } from "../../../src/plugin-context" import { LinkSourceCommand } from "../../../src/commands/link/source" import { UnlinkSourceCommand } from "../../../src/commands/unlink/source" import { Garden } from "../../../src/garden" describe("UnlinkCommand", () => { let garden: Garden - let ctx: PluginContext describe("UnlinkModuleCommand", () => { const projectRoot = getDataDir("test-project-ext-module-sources") @@ -25,12 +23,10 @@ describe("UnlinkCommand", () => { beforeEach(async () => { garden = await makeTestGarden(projectRoot) - ctx = garden.getPluginContext() stubExtSources(garden) await linkCmd.action({ garden, - ctx, args: { module: "module-a", path: join(projectRoot, "mock-local-path", "module-a"), @@ -39,7 +35,6 @@ describe("UnlinkCommand", () => { }) await linkCmd.action({ garden, - ctx, args: { module: "module-b", path: join(projectRoot, "mock-local-path", "module-b"), @@ -48,7 +43,6 @@ describe("UnlinkCommand", () => { }) await linkCmd.action({ garden, - ctx, args: { module: "module-c", path: join(projectRoot, "mock-local-path", "module-c"), @@ -64,11 +58,10 @@ describe("UnlinkCommand", () => { it("should unlink the provided modules", async () => { await unlinkCmd.action({ garden, - ctx, args: { module: ["module-a", "module-b"] }, opts: { all: false }, }) - const { linkedModuleSources } = await ctx.localConfigStore.get() + const { linkedModuleSources } = await garden.localConfigStore.get() expect(linkedModuleSources).to.eql([ { name: "module-c", path: join(projectRoot, "mock-local-path", "module-c") }, ]) @@ -77,11 +70,10 @@ describe("UnlinkCommand", () => { it("should unlink all modules", async () => { await unlinkCmd.action({ garden, - ctx, args: { module: undefined }, opts: { all: true }, }) - const { linkedModuleSources } = await ctx.localConfigStore.get() + const { linkedModuleSources } = await garden.localConfigStore.get() expect(linkedModuleSources).to.eql([]) }) }) @@ -93,12 +85,10 @@ describe("UnlinkCommand", () => { beforeEach(async () => { garden = await makeTestGarden(projectRoot) - ctx = garden.getPluginContext() stubExtSources(garden) await linkCmd.action({ garden, - ctx, args: { source: "source-a", path: join(projectRoot, "mock-local-path", "source-a"), @@ -107,7 +97,6 @@ describe("UnlinkCommand", () => { }) await linkCmd.action({ garden, - ctx, args: { source: "source-b", path: join(projectRoot, "mock-local-path", "source-b"), @@ -116,7 +105,6 @@ describe("UnlinkCommand", () => { }) await linkCmd.action({ garden, - ctx, args: { source: "source-c", path: join(projectRoot, "mock-local-path", "source-c"), @@ -132,11 +120,10 @@ describe("UnlinkCommand", () => { it("should unlink the provided sources", async () => { await unlinkCmd.action({ garden, - ctx, args: { source: ["source-a", "source-b"] }, opts: { all: false }, }) - const { linkedProjectSources } = await ctx.localConfigStore.get() + const { linkedProjectSources } = await garden.localConfigStore.get() expect(linkedProjectSources).to.eql([ { name: "source-c", path: join(projectRoot, "mock-local-path", "source-c") }, ]) @@ -145,11 +132,10 @@ describe("UnlinkCommand", () => { it("should unlink all sources", async () => { await unlinkCmd.action({ garden, - ctx, args: { source: undefined }, opts: { all: true }, }) - const { linkedProjectSources } = await ctx.localConfigStore.get() + const { linkedProjectSources } = await garden.localConfigStore.get() expect(linkedProjectSources).to.eql([]) }) }) diff --git a/garden-cli/test/src/commands/update-remote.ts b/garden-cli/test/src/commands/update-remote.ts index bd933df436..c27f16705e 100644 --- a/garden-cli/test/src/commands/update-remote.ts +++ b/garden-cli/test/src/commands/update-remote.ts @@ -9,10 +9,8 @@ import { expect } from "chai" import { join } from "path" import { mkdirp, pathExists } from "fs-extra" -import * as td from "testdouble" import { getDataDir, expectError, stubExtSources, stubGitCli, makeTestGarden } from "../../helpers" -import { PluginContext } from "../../../src/plugin-context" import { UpdateRemoteSourcesCommand } from "../../../src/commands/update-remote/sources" import { UpdateRemoteModulesCommand } from "../../../src/commands/update-remote/modules" import { Garden } from "../../../src/garden" @@ -20,11 +18,9 @@ import { Garden } from "../../../src/garden" describe("UpdateRemoteCommand", () => { describe("UpdateRemoteSourcesCommand", () => { let garden: Garden - let ctx: PluginContext beforeEach(async () => { garden = await makeTestGarden(projectRoot) - ctx = garden.getPluginContext() stubGitCli() }) @@ -32,26 +28,26 @@ describe("UpdateRemoteCommand", () => { const cmd = new UpdateRemoteSourcesCommand() it("should update all project sources", async () => { - const { result } = await cmd.action({ garden, ctx, args: { source: undefined }, opts: {} }) + const { result } = await cmd.action({ garden, args: { source: undefined }, opts: {} }) expect(result!.map(s => s.name).sort()).to.eql(["source-a", "source-b", "source-c"]) }) it("should update the specified project sources", async () => { - const { result } = await cmd.action({ garden, ctx, args: { source: ["source-a"] }, opts: {} }) + const { result } = await cmd.action({ garden, args: { source: ["source-a"] }, opts: {} }) expect(result!.map(s => s.name).sort()).to.eql(["source-a"]) }) it("should remove stale remote project sources", async () => { const stalePath = join(projectRoot, ".garden", "sources", "project", "stale-source") await mkdirp(stalePath) - await cmd.action({ garden, ctx, args: { source: undefined }, opts: {} }) + await cmd.action({ garden, args: { source: undefined }, opts: {} }) expect(await pathExists(stalePath)).to.be.false }) it("should throw if project source is not found", async () => { await expectError( async () => ( - await cmd.action({ garden, ctx, args: { source: ["banana"] }, opts: {} }) + await cmd.action({ garden, args: { source: ["banana"] }, opts: {} }) ), "parameter", ) @@ -60,42 +56,36 @@ describe("UpdateRemoteCommand", () => { describe("UpdateRemoteModulesCommand", () => { let garden: Garden - let ctx: PluginContext beforeEach(async () => { garden = await makeTestGarden(projectRoot) - ctx = garden.getPluginContext() stubExtSources(garden) }) - afterEach(async () => { - td.reset() - }) - const projectRoot = getDataDir("test-project-ext-module-sources") const cmd = new UpdateRemoteModulesCommand() it("should update all modules sources", async () => { - const { result } = await cmd.action({ garden, ctx, args: { module: undefined }, opts: {} }) + const { result } = await cmd.action({ garden, args: { module: undefined }, opts: {} }) expect(result!.map(s => s.name).sort()).to.eql(["module-a", "module-b", "module-c"]) }) it("should update the specified module sources", async () => { - const { result } = await cmd.action({ garden, ctx, args: { module: ["module-a"] }, opts: {} }) + const { result } = await cmd.action({ garden, args: { module: ["module-a"] }, opts: {} }) expect(result!.map(s => s.name).sort()).to.eql(["module-a"]) }) it("should remove stale remote module sources", async () => { const stalePath = join(projectRoot, ".garden", "sources", "module", "stale-source") await mkdirp(stalePath) - await cmd.action({ garden, ctx, args: { module: undefined }, opts: {} }) + await cmd.action({ garden, args: { module: undefined }, opts: {} }) expect(await pathExists(stalePath)).to.be.false }) it("should throw if project source is not found", async () => { await expectError( async () => ( - await cmd.action({ garden, ctx, args: { module: ["banana"] }, opts: {} }) + await cmd.action({ garden, args: { module: ["banana"] }, opts: {} }) ), "parameter", ) diff --git a/garden-cli/test/src/commands/validate.ts b/garden-cli/test/src/commands/validate.ts index 47b93bfba3..cdbcb4a07a 100644 --- a/garden-cli/test/src/commands/validate.ts +++ b/garden-cli/test/src/commands/validate.ts @@ -8,10 +8,9 @@ describe("commands.validate", () => { for (const [name, path] of Object.entries(getExampleProjects())) { it(`should successfully validate the ${name} project`, async () => { const garden = await Garden.factory(path) - const ctx = garden.getPluginContext() const command = new ValidateCommand() - await command.action({ garden, ctx, args: {}, opts: {} }) + await command.action({ garden, args: {}, opts: {} }) }) } @@ -24,9 +23,8 @@ describe("commands.validate", () => { it("should fail validating the bad-module project", async () => { const root = join(__dirname, "data", "validate", "bad-module") const garden = await Garden.factory(root) - const ctx = garden.getPluginContext() const command = new ValidateCommand() - await expectError(async () => await command.action({ garden, ctx, args: {}, opts: {} }), "configuration") + await expectError(async () => await command.action({ garden, args: {}, opts: {} }), "configuration") }) }) diff --git a/garden-cli/test/src/config/config-context.ts b/garden-cli/test/src/config/config-context.ts index 0255088b96..67623a50f8 100644 --- a/garden-cli/test/src/config/config-context.ts +++ b/garden-cli/test/src/config/config-context.ts @@ -232,8 +232,8 @@ describe("ModuleConfigContext", () => { garden = await makeTestGardenA() await garden.scanModules() c = new ModuleConfigContext( - garden.getPluginContext(), - garden.environmentConfig, + garden, + garden.environment, Object.values((garden).moduleConfigs), ) }) @@ -249,7 +249,7 @@ describe("ModuleConfigContext", () => { }) it("should should resolve the environment config", async () => { - expect(await c.resolve({ key: ["environment", "name"], nodePath: [] })).to.equal(garden.environmentName) + expect(await c.resolve({ key: ["environment", "name"], nodePath: [] })).to.equal(garden.environment.name) }) it("should should resolve the path of a module", async () => { diff --git a/garden-cli/test/src/garden.ts b/garden-cli/test/src/garden.ts index 13d0b59126..cb769b899d 100644 --- a/garden-cli/test/src/garden.ts +++ b/garden-cli/test/src/garden.ts @@ -10,8 +10,6 @@ import { makeTestGardenA, makeTestModule, projectRootA, - testPlugin, - testPluginB, stubExtSources, getDataDir, cleanProject, @@ -23,6 +21,7 @@ import { MOCK_CONFIG } from "../../src/cli/cli" import { LinkedSource } from "../../src/config-store" import { ModuleVersion } from "../../src/vcs/base" import { hashRepoUrl } from "../../src/util/ext-source-util" +import { getModuleCacheContext } from "../../src/types/module" describe("Garden", () => { beforeEach(async () => { @@ -37,8 +36,8 @@ describe("Garden", () => { it("should initialize and add the action handlers for a plugin", async () => { const garden = await makeTestGardenA() - expect(garden.actionHandlers.configureEnvironment["test-plugin"]).to.be.ok - expect(garden.actionHandlers.configureEnvironment["test-plugin-b"]).to.be.ok + expect(garden.actionHandlers.prepareEnvironment["test-plugin"]).to.be.ok + expect(garden.actionHandlers.prepareEnvironment["test-plugin-b"]).to.be.ok }) it("should initialize with MOCK_CONFIG", async () => { @@ -63,9 +62,11 @@ describe("Garden", () => { const garden = await makeTestGardenA() expect(garden.projectName).to.equal("test-project-a") - expect(garden.environmentConfig).to.eql({ + expect(garden.environment).to.eql({ name: "local", providers: [ + { name: "generic" }, + { name: "container" }, { name: "test-plugin" }, { name: "test-plugin-b" }, ], @@ -86,9 +87,11 @@ describe("Garden", () => { delete process.env.TEST_PROVIDER_TYPE delete process.env.TEST_VARIABLE - expect(garden.environmentConfig).to.eql({ + expect(garden.environment).to.eql({ name: "local", providers: [ + { name: "generic" }, + { name: "container" }, { name: "test-plugin" }, ], variables: { @@ -98,26 +101,6 @@ describe("Garden", () => { }) }) - it("should optionally set a namespace with the dot separator", async () => { - const garden = await Garden.factory( - projectRootA, { env: "local.mynamespace", plugins: [testPlugin, testPluginB] }, - ) - - const { name, namespace } = garden.getEnvironment() - expect(name).to.equal("local") - expect(namespace).to.equal("mynamespace") - }) - - it("should split environment and namespace on the first dot", async () => { - const garden = await Garden.factory( - projectRootA, { env: "local.mynamespace.2", plugins: [testPlugin, testPluginB] }, - ) - - const { name, namespace } = garden.getEnvironment() - expect(name).to.equal("local") - expect(namespace).to.equal("mynamespace.2") - }) - it("should throw if the specified environment isn't configured", async () => { await expectError(async () => Garden.factory(projectRootA, { env: "bla" }), "parameter") }) @@ -149,16 +132,6 @@ describe("Garden", () => { }) }) - describe("getEnvironment", () => { - it("should get the active environment for the context", async () => { - const garden = await makeTestGardenA() - - const { name, namespace } = garden.getEnvironment() - expect(name).to.equal("local") - expect(namespace).to.equal("default") - }) - }) - describe("getModules", () => { it("should scan and return all registered modules in the context", async () => { const garden = await makeTestGardenA() @@ -241,11 +214,6 @@ describe("Garden", () => { describe("scanModules", () => { // TODO: assert that gitignore in project root is respected - - afterEach(() => { - td.reset() - }) - it("should scan the project root for modules and add to the context", async () => { const garden = await makeTestGardenA() await garden.scanModules() @@ -435,7 +403,7 @@ describe("Garden", () => { it("should return all handlers for a type", async () => { const garden = await makeTestGardenA() - const handlers = garden.getActionHandlers("configureEnvironment") + const handlers = garden.getActionHandlers("prepareEnvironment") expect(Object.keys(handlers)).to.eql([ "test-plugin", @@ -448,7 +416,7 @@ describe("Garden", () => { it("should return all handlers for a type", async () => { const garden = await makeTestGardenA() - const handlers = garden.getModuleActionHandlers({ actionType: "buildModule", moduleType: "generic" }) + const handlers = garden.getModuleActionHandlers({ actionType: "build", moduleType: "generic" }) expect(Object.keys(handlers)).to.eql([ "generic", @@ -460,24 +428,24 @@ describe("Garden", () => { it("should return last configured handler for specified action type", async () => { const garden = await makeTestGardenA() - const handler = garden.getActionHandler({ actionType: "configureEnvironment" }) + const handler = garden.getActionHandler({ actionType: "prepareEnvironment" }) - expect(handler["actionType"]).to.equal("configureEnvironment") + expect(handler["actionType"]).to.equal("prepareEnvironment") expect(handler["pluginName"]).to.equal("test-plugin-b") }) it("should optionally filter to only handlers for the specified module type", async () => { const garden = await makeTestGardenA() - const handler = garden.getActionHandler({ actionType: "configureEnvironment" }) + const handler = garden.getActionHandler({ actionType: "prepareEnvironment" }) - expect(handler["actionType"]).to.equal("configureEnvironment") + expect(handler["actionType"]).to.equal("prepareEnvironment") expect(handler["pluginName"]).to.equal("test-plugin-b") }) it("should throw if no handler is available", async () => { const garden = await makeTestGardenA() - await expectError(() => garden.getActionHandler({ actionType: "destroyEnvironment" }), "parameter") + await expectError(() => garden.getActionHandler({ actionType: "cleanupEnvironment" }), "parameter") }) }) @@ -531,7 +499,7 @@ describe("Garden", () => { dirtyTimestamp: 987654321, dependencyVersions: {}, } - garden.cache.set(["moduleVersions", module.name], version, module.cacheContext) + garden.cache.set(["moduleVersions", module.name], version, getModuleCacheContext(module)) const result = await garden.resolveVersion("module-a", []) @@ -566,7 +534,7 @@ describe("Garden", () => { dirtyTimestamp: 987654321, dependencyVersions: {}, } - garden.cache.set(["moduleVersions", module.name], version, module.cacheContext) + garden.cache.set(["moduleVersions", module.name], version, getModuleCacheContext(module)) const result = await garden.resolveVersion("module-a", [], true) @@ -575,7 +543,6 @@ describe("Garden", () => { }) describe("loadExtSourcePath", () => { - let projectRoot: string const makeGardenContext = async (root) => { @@ -585,7 +552,6 @@ describe("Garden", () => { } afterEach(async () => { - td.reset() await cleanProject(projectRoot) }) diff --git a/garden-cli/test/src/plugin-context.ts b/garden-cli/test/src/plugin-context.ts deleted file mode 100644 index 43a25ce5a0..0000000000 --- a/garden-cli/test/src/plugin-context.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { expect } from "chai" -import { PluginContext } from "../../src/plugin-context" -import { expectError } from "../helpers" -import { Garden } from "../../src/garden" -import { makeTestGardenA } from "../helpers" - -describe("PluginContext", () => { - let garden: Garden - let ctx: PluginContext - - beforeEach(async () => { - garden = await makeTestGardenA() - ctx = garden.getPluginContext() - }) - - describe("setConfig", () => { - it("should set a valid key in the 'project' namespace", async () => { - const key = ["project", "my", "variable"] - const value = "myvalue" - - await ctx.setConfig({ key, value }) - expect(await ctx.getConfig({ key })).to.eql({ value }) - }) - - it("should throw with an invalid namespace in the key", async () => { - const key = ["bla", "my", "variable"] - const value = "myvalue" - - await expectError(async () => await ctx.setConfig({ key, value }), "parameter") - }) - - it("should throw with malformatted key", async () => { - const key = ["project", "!4215"] - const value = "myvalue" - - await expectError(async () => await ctx.setConfig({ key, value }), "parameter") - }) - }) - - describe("getConfig", () => { - it("should get a valid key in the 'project' namespace", async () => { - const key = ["project", "my", "variable"] - const value = "myvalue" - - await ctx.setConfig({ key, value }) - expect(await ctx.getConfig({ key })).to.eql({ value }) - }) - - it("should throw with an invalid namespace in the key", async () => { - const key = ["bla", "my", "variable"] - - await expectError(async () => await ctx.getConfig({ key }), "parameter") - }) - - it("should throw with malformatted key", async () => { - const key = ["project", "!4215"] - - await expectError(async () => await ctx.getConfig({ key }), "parameter") - }) - }) - - describe("deleteConfig", () => { - it("should delete a valid key in the 'project' namespace", async () => { - const key = ["project", "my", "variable"] - const value = "myvalue" - - await ctx.setConfig({ key, value }) - expect(await ctx.deleteConfig({ key })).to.eql({ found: true }) - }) - - it("should return {found:false} if key does not exist", async () => { - const key = ["project", "my", "variable"] - - expect(await ctx.deleteConfig({ key })).to.eql({ found: false }) - }) - - it("should throw with an invalid namespace in the key", async () => { - const key = ["bla", "my", "variable"] - - await expectError(async () => await ctx.deleteConfig({ key }), "parameter") - }) - - it("should throw with malformatted key", async () => { - const key = ["project", "!4215"] - - await expectError(async () => await ctx.deleteConfig({ key }), "parameter") - }) - }) -}) diff --git a/garden-cli/test/src/plugins/container.ts b/garden-cli/test/src/plugins/container.ts index 3abf21f40d..d0f2ab7337 100644 --- a/garden-cli/test/src/plugins/container.ts +++ b/garden-cli/test/src/plugins/container.ts @@ -8,7 +8,6 @@ import { gardenPlugin, helpers, } from "../../../src/plugins/container" -import { Environment } from "../../../src/config/common" import { dataDir, expectError, @@ -21,19 +20,17 @@ describe("plugins.container", () => { const modulePath = resolve(dataDir, "test-project-container", "module-a") const handler = gardenPlugin() - const parseModule = handler.moduleActions!.container!.parseModule! - const buildModule = handler.moduleActions!.container!.buildModule! + const validate = handler.moduleActions!.container!.validate! + const build = handler.moduleActions!.container!.build! const pushModule = handler.moduleActions!.container!.pushModule! - const getModuleBuildStatus = handler.moduleActions!.container!.getModuleBuildStatus! + const getBuildStatus = handler.moduleActions!.container!.getBuildStatus! let garden: Garden let ctx: PluginContext - let env: Environment beforeEach(async () => { garden = await makeTestGarden(projectRoot, [gardenPlugin]) - ctx = garden.getPluginContext() - env = garden.getEnvironment() + ctx = garden.getPluginContext("container") td.replace(garden.buildDir, "syncDependencyProducts", () => null) @@ -44,10 +41,8 @@ describe("plugins.container", () => { })) }) - const provider = { name: "container", config: {} } - async function getTestModule(moduleConfig: ContainerModuleConfig) { - const parsed = await parseModule({ env, provider, moduleConfig }) + const parsed = await validate({ ctx, moduleConfig }) return moduleFromConfig(garden, parsed) } @@ -193,7 +188,7 @@ describe("plugins.container", () => { }) describe("DockerModuleHandler", () => { - describe("parseModule", () => { + describe("validate", () => { it("should validate and parse a container module", async () => { const moduleConfig: ContainerModuleConfig = { allowPush: false, @@ -249,7 +244,7 @@ describe("plugins.container", () => { testConfigs: [], } - const result = await parseModule({ env, provider, moduleConfig }) + const result = await validate({ ctx, moduleConfig }) expect(result).to.eql({ allowPush: false, @@ -375,7 +370,7 @@ describe("plugins.container", () => { } await expectError( - () => parseModule({ env, provider, moduleConfig }), + () => validate({ ctx, moduleConfig }), "configuration", ) }) @@ -419,7 +414,7 @@ describe("plugins.container", () => { } await expectError( - () => parseModule({ env, provider, moduleConfig }), + () => validate({ ctx, moduleConfig }), "configuration", ) }) @@ -460,13 +455,13 @@ describe("plugins.container", () => { } await expectError( - () => parseModule({ env, provider, moduleConfig }), + () => validate({ ctx, moduleConfig }), "configuration", ) }) }) - describe("getModuleBuildStatus", () => { + describe("getBuildStatus", () => { it("should return ready:true if build exists locally", async () => { const module = td.object(await getTestModule({ allowPush: false, @@ -491,7 +486,7 @@ describe("plugins.container", () => { td.replace(helpers, "imageExistsLocally", async () => true) - const result = await getModuleBuildStatus({ ctx, env, provider, module }) + const result = await getBuildStatus({ ctx, module }) expect(result).to.eql({ ready: true }) }) @@ -519,12 +514,12 @@ describe("plugins.container", () => { td.replace(helpers, "imageExistsLocally", async () => false) - const result = await getModuleBuildStatus({ ctx, env, provider, module }) + const result = await getBuildStatus({ ctx, module }) expect(result).to.eql({ ready: false }) }) }) - describe("buildModule", () => { + describe("build", () => { it("pull image if image tag is set and the module doesn't container a Dockerfile", async () => { const module = td.object(await getTestModule({ allowPush: false, @@ -552,7 +547,7 @@ describe("plugins.container", () => { td.replace(helpers, "pullImage", async () => null) td.replace(helpers, "imageExistsLocally", async () => false) - const result = await buildModule({ ctx, env, provider, module }) + const result = await build({ ctx, module }) expect(result).to.eql({ fetched: true }) }) @@ -586,7 +581,7 @@ describe("plugins.container", () => { const dockerCli = td.replace(helpers, "dockerCli") - const result = await buildModule({ ctx, env, provider, module }) + const result = await build({ ctx, module }) expect(result).to.eql({ fresh: true, @@ -623,7 +618,7 @@ describe("plugins.container", () => { td.replace(helpers, "hasDockerfile", async () => false) - const result = await pushModule({ ctx, env, provider, module }) + const result = await pushModule({ ctx, module }) expect(result).to.eql({ pushed: false }) }) @@ -656,7 +651,7 @@ describe("plugins.container", () => { const dockerCli = td.replace(helpers, "dockerCli") - const result = await pushModule({ ctx, env, provider, module }) + const result = await pushModule({ ctx, module }) expect(result).to.eql({ pushed: true }) td.verify(dockerCli(module, "tag some/image:12345 some/image:12345"), { times: 0 }) @@ -692,7 +687,7 @@ describe("plugins.container", () => { const dockerCli = td.replace(helpers, "dockerCli") - const result = await pushModule({ ctx, env, provider, module }) + const result = await pushModule({ ctx, module }) expect(result).to.eql({ pushed: true }) td.verify(dockerCli(module, "tag some/image:12345 some/image:1.1")) diff --git a/garden-cli/test/src/plugins/generic.ts b/garden-cli/test/src/plugins/generic.ts index 19ea0bc4d9..d99031a3a6 100644 --- a/garden-cli/test/src/plugins/generic.ts +++ b/garden-cli/test/src/plugins/generic.ts @@ -4,10 +4,7 @@ import { resolve, } from "path" import { Garden } from "../../../src/garden" -import { PluginContext } from "../../../src/plugin-context" -import { - gardenPlugin, -} from "../../../src/plugins/generic" +import { gardenPlugin } from "../../../src/plugins/generic" import { GARDEN_BUILD_VERSION_FILENAME } from "../../../src/constants" import { writeModuleVersionFile, @@ -23,37 +20,35 @@ describe("generic plugin", () => { const moduleName = "module-a" let garden: Garden - let ctx: PluginContext beforeEach(async () => { garden = await makeTestGarden(projectRoot, [gardenPlugin]) - ctx = garden.getPluginContext() await garden.clearBuilds() }) - describe("getModuleBuildStatus", () => { + describe("getBuildStatus", () => { it("should read a build version file if it exists", async () => { - const module = await ctx.getModule(moduleName) + const module = await garden.getModule(moduleName) const version = module.version const buildPath = module.buildPath const versionFilePath = join(buildPath, GARDEN_BUILD_VERSION_FILENAME) await writeModuleVersionFile(versionFilePath, version) - const result = await ctx.getModuleBuildStatus({ moduleName }) + const result = await garden.actions.getBuildStatus({ module }) expect(result.ready).to.be.true }) }) - describe("buildModule", () => { + describe("build", () => { it("should write a build version file after building", async () => { - const module = await ctx.getModule(moduleName) + const module = await garden.getModule(moduleName) const version = module.version const buildPath = module.buildPath const versionFilePath = join(buildPath, GARDEN_BUILD_VERSION_FILENAME) - await ctx.buildModule({ moduleName }) + await garden.actions.build({ module }) const versionFileContents = await readModuleVersionFile(versionFilePath) diff --git a/garden-cli/test/src/plugins/kubernetes/ingress.ts b/garden-cli/test/src/plugins/kubernetes/ingress.ts index ee2ba70a3e..372a5f5c24 100644 --- a/garden-cli/test/src/plugins/kubernetes/ingress.ts +++ b/garden-cli/test/src/plugins/kubernetes/ingress.ts @@ -282,7 +282,7 @@ const wildcardDomainCertSecret = { describe("createIngresses", () => { const projectRoot = resolve(dataDir, "test-project-container") const handler = gardenPlugin() - const parseModule = handler.moduleActions!.container!.parseModule! + const validate = handler.moduleActions!.container!.validate! let garden: Garden @@ -340,9 +340,8 @@ describe("createIngresses", () => { testConfigs: [], } - const env = garden.getEnvironment() - const provider = { name: "container", config: {} } - const parsed = await parseModule({ env, provider, moduleConfig }) + const ctx = await garden.getPluginContext("container") + const parsed = await validate({ ctx, moduleConfig }) const module = await moduleFromConfig(garden, parsed) return { diff --git a/garden-cli/test/src/task-graph.ts b/garden-cli/test/src/task-graph.ts index 0fd188f7ff..2f0d16b312 100644 --- a/garden-cli/test/src/task-graph.ts +++ b/garden-cli/test/src/task-graph.ts @@ -7,6 +7,7 @@ import { TaskResults, } from "../../src/task-graph" import { makeTestGarden } from "../helpers" +import { Garden } from "../../src/garden" const projectRoot = join(__dirname, "..", "data", "test-project-empty") @@ -26,11 +27,13 @@ class TestTask extends Task { throwError: boolean constructor( + garden: Garden, name: string, dependencies?: Task[], options?: TestTaskOptions, ) { super({ + garden, version: { versionString: "12345-6789", dirtyTimestamp: 6789, @@ -81,15 +84,14 @@ class TestTask extends Task { describe("task-graph", () => { describe("TaskGraph", () => { - async function getContext() { - const garden = await makeTestGarden(projectRoot) - return garden.getPluginContext() + async function getGarden() { + return makeTestGarden(projectRoot) } it("should successfully process a single task without dependencies", async () => { - const ctx = await getContext() - const graph = new TaskGraph(ctx) - const task = new TestTask("a") + const garden = await getGarden() + const graph = new TaskGraph(garden) + const task = new TestTask(garden, "a") await graph.addTask(task) const results = await graph.processTasks() @@ -110,8 +112,8 @@ describe("task-graph", () => { }) it("should process multiple tasks in dependency order", async () => { - const ctx = await getContext() - const graph = new TaskGraph(ctx) + const garden = await getGarden() + const graph = new TaskGraph(garden) const callbackResults = {} const resultOrder: string[] = [] @@ -123,10 +125,10 @@ describe("task-graph", () => { const opts = { callback } - const taskA = new TestTask("a", [], opts) - const taskB = new TestTask("b", [taskA], opts) - const taskC = new TestTask("c", [taskB], opts) - const taskD = new TestTask("d", [taskB, taskC], opts) + const taskA = new TestTask(garden, "a", [], opts) + const taskB = new TestTask(garden, "b", [taskA], opts) + const taskC = new TestTask(garden, "c", [taskB], opts) + const taskD = new TestTask(garden, "d", [taskB, taskC], opts) // we should be able to add tasks multiple times and in any order await graph.addTask(taskC) @@ -202,8 +204,8 @@ describe("task-graph", () => { }) it("should recursively cancel a task's dependants when it throws an error", async () => { - const ctx = await getContext() - const graph = new TaskGraph(ctx) + const garden = await getGarden() + const graph = new TaskGraph(garden) const resultOrder: string[] = [] @@ -213,10 +215,10 @@ describe("task-graph", () => { const opts = { callback } - const taskA = new TestTask("a", [], opts) - const taskB = new TestTask("b", [taskA], { callback, throwError: true }) - const taskC = new TestTask("c", [taskB], opts) - const taskD = new TestTask("d", [taskB, taskC], opts) + const taskA = new TestTask(garden, "a", [], opts) + const taskB = new TestTask(garden, "b", [taskA], { callback, throwError: true }) + const taskC = new TestTask(garden, "c", [taskB], opts) + const taskD = new TestTask(garden, "d", [taskB, taskC], opts) await graph.addTask(taskA) await graph.addTask(taskB) @@ -243,8 +245,8 @@ describe("task-graph", () => { it.skip( "should process a task as an inheritor of an existing, in-progress task when they have the same base key", async () => { - const ctx = await getContext() - const graph = new TaskGraph(ctx) + const garden = await getGarden() + const graph = new TaskGraph(garden) let callbackResults = {} let resultOrder: string[] = [] @@ -282,13 +284,18 @@ describe("task-graph", () => { callbackResults[key] = result } - const dependencyA = new TestTask("dependencyA", [], { callback: defaultCallback }) - const dependencyB = new TestTask("dependencyB", [], { callback: defaultCallback }) - const parentTask = new TestTask("sharedName", [dependencyA, dependencyB], { callback: parentCallback, id: "1" }) - const dependantA = new TestTask("dependantA", [parentTask], { callback: defaultCallback }) - const dependantB = new TestTask("dependantB", [parentTask], { callback: defaultCallback }) + const dependencyA = new TestTask(garden, "dependencyA", [], { callback: defaultCallback }) + const dependencyB = new TestTask(garden, "dependencyB", [], { callback: defaultCallback }) + const parentTask = new TestTask( + garden, + "sharedName", + [dependencyA, dependencyB], + { callback: parentCallback, id: "1" }, + ) + const dependantA = new TestTask(garden, "dependantA", [parentTask], { callback: defaultCallback }) + const dependantB = new TestTask(garden, "dependantB", [parentTask], { callback: defaultCallback }) - const inheritorTask = new TestTask( + const inheritorTask = new TestTask(garden, "sharedName", [dependencyA, dependencyB], { callback: defaultCallback, id: "2" }, ) diff --git a/garden-cli/test/src/tasks/test.ts b/garden-cli/test/src/tasks/test.ts index d5997a6149..6196e4223a 100644 --- a/garden-cli/test/src/tasks/test.ts +++ b/garden-cli/test/src/tasks/test.ts @@ -2,26 +2,19 @@ import { expect } from "chai" import { resolve } from "path" import { TestTask } from "../../../src/tasks/test" import * as td from "testdouble" -import { VcsHandler } from "../../../src/vcs/base" -import { - dataDir, - makeTestGarden, -} from "../../helpers" +import { Garden } from "../../../src/garden" +import { dataDir, makeTestGarden } from "../../helpers" describe("TestTask", () => { + let garden: Garden + beforeEach(async () => { - td.replace(VcsHandler.prototype, "resolveTreeVersion", async () => ({ - latestCommit: "abcdefg1234", - dirtyTimestamp: null, - })) + garden = await makeTestGarden(resolve(dataDir, "test-project-test-deps")) }) it("should correctly resolve version for tests with dependencies", async () => { process.env.TEST_VARIABLE = "banana" - const garden = await makeTestGarden(resolve(dataDir, "test-project-test-deps")) - const ctx = garden.getPluginContext() - const resolveVersion = td.replace(garden, "resolveVersion") const version = { @@ -35,13 +28,15 @@ describe("TestTask", () => { }, } - td.when(resolveVersion("module-a", [{ name: "module-b", copy: [] }])).thenResolve(version) + const moduleB = await garden.getModule("module-b") + + td.when(resolveVersion("module-a", [moduleB])).thenResolve(version) - const moduleA = await ctx.getModule("module-a") + const moduleA = await garden.getModule("module-a") const testConfig = moduleA.testConfigs[0] const task = await TestTask.factory({ - ctx, + garden, module: moduleA, testConfig, force: true, diff --git a/garden-cli/test/src/util/ext-source-util.ts b/garden-cli/test/src/util/ext-source-util.ts index 9cf9e5bcd0..14c93a598f 100644 --- a/garden-cli/test/src/util/ext-source-util.ts +++ b/garden-cli/test/src/util/ext-source-util.ts @@ -8,22 +8,22 @@ import { getRemoteSourcePath, hashRepoUrl, } from "../../../src/util/ext-source-util" -import { makeTestContextA, cleanProject, expectError } from "../../helpers" -import { PluginContext } from "../../../src/plugin-context" +import { cleanProject, expectError, makeTestGardenA } from "../../helpers" +import { Garden } from "../../../src/garden" describe("ext-source-util", () => { - let ctx: PluginContext + let garden: Garden const sources = [{ name: "name-a", path: "path-a" }, { name: "name-b", path: "path-b" }] - describe("getExtSourcesDirName", () => { - beforeEach(async () => { - ctx = await makeTestContextA() - }) + beforeEach(async () => { + garden = await makeTestGardenA() + }) - afterEach(async () => { - await cleanProject(ctx.projectRoot) - }) + afterEach(async () => { + await cleanProject(garden.projectRoot) + }) + describe("getExtSourcesDirName", () => { it("should return the relative path to the remote projects directory", () => { const dirName = getRemoteSourcesDirname("project") expect(dirName).to.equal(".garden/sources/project") @@ -54,96 +54,72 @@ describe("ext-source-util", () => { }) describe("getLinkedSources", () => { - beforeEach(async () => { - ctx = await makeTestContextA() - }) - - afterEach(async () => { - await cleanProject(ctx.projectRoot) - }) - it("should get linked project sources", async () => { - await ctx.localConfigStore.set(["linkedProjectSources"], sources) - expect(await getLinkedSources(ctx, "project")).to.eql(sources) + await garden.localConfigStore.set(["linkedProjectSources"], sources) + expect(await getLinkedSources(garden, "project")).to.eql(sources) }) it("should get linked module sources", async () => { - await ctx.localConfigStore.set(["linkedModuleSources"], sources) - expect(await getLinkedSources(ctx, "module")).to.eql(sources) + await garden.localConfigStore.set(["linkedModuleSources"], sources) + expect(await getLinkedSources(garden, "module")).to.eql(sources) }) }) describe("addLinkedSources", () => { - beforeEach(async () => { - ctx = await makeTestContextA() - }) - - afterEach(async () => { - await cleanProject(ctx.projectRoot) - }) - it("should add linked project sources to local config", async () => { - await addLinkedSources({ ctx, sourceType: "project", sources }) - expect(await ctx.localConfigStore.get(["linkedProjectSources"])).to.eql(sources) + await addLinkedSources({ garden, sourceType: "project", sources }) + expect(await garden.localConfigStore.get(["linkedProjectSources"])).to.eql(sources) }) it("should add linked module sources to local config", async () => { - await addLinkedSources({ ctx, sourceType: "module", sources }) - expect(await ctx.localConfigStore.get(["linkedModuleSources"])).to.eql(sources) + await addLinkedSources({ garden, sourceType: "module", sources }) + expect(await garden.localConfigStore.get(["linkedModuleSources"])).to.eql(sources) }) it("should append sources to local config if key already has value", async () => { - const { localConfigStore } = ctx + const { localConfigStore } = garden await localConfigStore.set(["linkedModuleSources"], sources) const newSources = [{ name: "name-c", path: "path-c" }] - await addLinkedSources({ ctx, sourceType: "module", sources: newSources }) + await addLinkedSources({ garden, sourceType: "module", sources: newSources }) - expect(await ctx.localConfigStore.get(["linkedModuleSources"])).to.eql(sources.concat(newSources)) + expect(await garden.localConfigStore.get(["linkedModuleSources"])).to.eql(sources.concat(newSources)) }) }) describe("removeLinkedSources", () => { - beforeEach(async () => { - ctx = await makeTestContextA() - }) - - afterEach(async () => { - await cleanProject(ctx.projectRoot) - }) - it("should remove linked project sources from local config", async () => { - await ctx.localConfigStore.set(["linkedModuleSources"], sources) + await garden.localConfigStore.set(["linkedModuleSources"], sources) const names = ["name-a"] - await removeLinkedSources({ ctx, sourceType: "module", names }) + await removeLinkedSources({ garden, sourceType: "module", names }) - expect(await ctx.localConfigStore.get(["linkedModuleSources"])).to.eql([{ + expect(await garden.localConfigStore.get(["linkedModuleSources"])).to.eql([{ name: "name-b", path: "path-b", }]) }) it("should remove linked module sources from local config", async () => { - await ctx.localConfigStore.set(["linkedProjectSources"], sources) + await garden.localConfigStore.set(["linkedProjectSources"], sources) const names = ["name-a"] - await removeLinkedSources({ ctx, sourceType: "project", names }) + await removeLinkedSources({ garden, sourceType: "project", names }) - expect(await ctx.localConfigStore.get(["linkedProjectSources"])).to.eql([{ + expect(await garden.localConfigStore.get(["linkedProjectSources"])).to.eql([{ name: "name-b", path: "path-b", }]) }) it("should remove multiple sources from local config", async () => { - await ctx.localConfigStore.set(["linkedModuleSources"], sources.concat({ name: "name-c", path: "path-c" })) + await garden.localConfigStore.set(["linkedModuleSources"], sources.concat({ name: "name-c", path: "path-c" })) const names = ["name-a", "name-b"] - await removeLinkedSources({ ctx, sourceType: "module", names }) + await removeLinkedSources({ garden, sourceType: "module", names }) - expect(await ctx.localConfigStore.get(["linkedModuleSources"])).to.eql([{ + expect(await garden.localConfigStore.get(["linkedModuleSources"])).to.eql([{ name: "name-c", path: "path-c", }]) }) @@ -151,7 +127,7 @@ describe("ext-source-util", () => { it("should throw if source not currently linked", async () => { const names = ["banana"] await expectError( - async () => await removeLinkedSources({ ctx, sourceType: "project", names }), + async () => await removeLinkedSources({ garden, sourceType: "project", names }), "parameter", ) diff --git a/garden-cli/test/src/vcs/base.ts b/garden-cli/test/src/vcs/base.ts index 3c4bf18cb3..0de5ce5a0a 100644 --- a/garden-cli/test/src/vcs/base.ts +++ b/garden-cli/test/src/vcs/base.ts @@ -1,7 +1,7 @@ import { VcsHandler, NEW_MODULE_VERSION, TreeVersions, TreeVersion } from "../../../src/vcs/base" -import { projectRootA, makeTestContextA } from "../../helpers" -import { PluginContext } from "../../../src/plugin-context" +import { projectRootA, makeTestGardenA } from "../../helpers" import { expect } from "chai" +import { Garden } from "../../../src/garden" class TestVcsHandler extends VcsHandler { name = "test" @@ -29,7 +29,7 @@ class TestVcsHandler extends VcsHandler { } describe("VcsHandler", () => { let handler: TestVcsHandler - let ctx: PluginContext + let garden: Garden // note: module-a has a version file with this content const versionA = { @@ -39,12 +39,12 @@ describe("VcsHandler", () => { beforeEach(async () => { handler = new TestVcsHandler(projectRootA) - ctx = await makeTestContextA() + garden = await makeTestGardenA() }) describe("resolveTreeVersion", () => { it("should return the version from a version file if it exists", async () => { - const module = await ctx.getModule("module-a") + const module = await garden.getModule("module-a") const result = await handler.resolveTreeVersion(module.path) expect(result).to.eql({ @@ -54,7 +54,7 @@ describe("VcsHandler", () => { }) it("should call getTreeVersion if there is no version file", async () => { - const module = await ctx.getModule("module-b") + const module = await garden.getModule("module-b") const version = { latestCommit: "qwerty", @@ -69,7 +69,7 @@ describe("VcsHandler", () => { describe("resolveVersion", () => { it("should return module version if there are no dependencies", async () => { - const module = await ctx.getModule("module-a") + const module = await garden.getModule("module-a") const result = await handler.resolveVersion(module, []) @@ -81,7 +81,7 @@ describe("VcsHandler", () => { }) it("should return module version if there are no dependencies and properly handle a dirty timestamp", async () => { - const module = await ctx.getModule("module-b") + const module = await garden.getModule("module-b") const latestCommit = "abcdef" const version = { latestCommit, @@ -100,7 +100,7 @@ describe("VcsHandler", () => { }) it("should return the dirty version if there is a single one", async () => { - const [moduleA, moduleB, moduleC] = await ctx.getModules(["module-a", "module-b", "module-c"]) + const [moduleA, moduleB, moduleC] = await garden.getModules(["module-a", "module-b", "module-c"]) const versionB = { latestCommit: "qwerty", @@ -126,7 +126,7 @@ describe("VcsHandler", () => { }) it("should return the latest dirty version if there are multiple", async () => { - const [moduleA, moduleB, moduleC] = await ctx.getModules(["module-a", "module-b", "module-c"]) + const [moduleA, moduleB, moduleC] = await garden.getModules(["module-a", "module-b", "module-c"]) const versionB = { latestCommit: "qwerty", @@ -152,7 +152,7 @@ describe("VcsHandler", () => { }) it("should hash together the version of the module and all dependencies if none are dirty", async () => { - const [moduleA, moduleB, moduleC] = await ctx.getModules(["module-a", "module-b", "module-c"]) + const [moduleA, moduleB, moduleC] = await garden.getModules(["module-a", "module-b", "module-c"]) const versionStringB = "qwerty" const versionB = { @@ -182,7 +182,7 @@ describe("VcsHandler", () => { "should hash together the dirty versions and add the timestamp if there are multiple with same timestamp", async () => { - const [moduleA, moduleB, moduleC] = await ctx.getModules(["module-a", "module-b", "module-c"]) + const [moduleA, moduleB, moduleC] = await garden.getModules(["module-a", "module-b", "module-c"]) const versionStringB = "qwerty" const versionB = { diff --git a/garden-cli/test/src/watch.ts b/garden-cli/test/src/watch.ts index 99842c09c6..ef90659124 100644 --- a/garden-cli/test/src/watch.ts +++ b/garden-cli/test/src/watch.ts @@ -19,9 +19,9 @@ describe("watch", () => { describe("computeAutoReloadDependants", () => { it("should include build and service dependants of requested modules", async () => { - const ctx = (await makeTestGarden(projectRoot)).getPluginContext() + const garden = await makeTestGarden(projectRoot) const dependants = dependantModuleNames( - await computeAutoReloadDependants(ctx)) + await computeAutoReloadDependants(garden)) expect(dependants).to.eql({ "module-a": ["module-b"],