diff --git a/src/build-dir.ts b/src/build-dir.ts index b6db3450bd..81e49ce77f 100644 --- a/src/build-dir.ts +++ b/src/build-dir.ts @@ -22,7 +22,6 @@ import * as Rsync from "rsync" import { GARDEN_DIR_NAME } from "./constants" import { execRsyncCmd } from "./util" import { Module } from "./types/module" -import { Garden } from "./garden" // Lazily construct a directory of modules inside which all build steps are performed. @@ -30,11 +29,9 @@ const buildDirRelPath = join(GARDEN_DIR_NAME, "build") export class BuildDir { buildDirPath: string - private ctx: Garden - constructor(ctx: Garden) { - this.ctx = ctx - this.buildDirPath = join(ctx.projectRoot, buildDirRelPath) + constructor(private projectRoot: string) { + this.buildDirPath = join(projectRoot, buildDirRelPath) } // Synchronous, so it can run in Garden's constructor. @@ -44,7 +41,7 @@ export class BuildDir { async syncFromSrc(module: T) { await this.sync( - resolve(this.ctx.projectRoot, module.path), + resolve(this.projectRoot, module.path), this.buildDirPath) } diff --git a/src/cli.ts b/src/cli.ts index ca22dbe4be..163f5c7417 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -17,6 +17,7 @@ import { ParameterValues, Parameter, StringParameter, + EnvironmentOption, } from "./commands/base" import { ValidateCommand } from "./commands/validate" import { InternalError, PluginError } from "./exceptions" @@ -42,6 +43,7 @@ const GLOBAL_OPTIONS = { help: "override project root directory (defaults to working directory)", defaultValue: process.cwd(), }), + env: new EnvironmentOption(), verbose: new BooleanParameter({ alias: "v", help: "verbose logging", @@ -263,6 +265,7 @@ export class GardenCli { const argsForAction = filterByArray(argv, argKeys) const optsForAction = filterByArray(argv, optKeys.concat(globalKeys)) const root = resolve(process.cwd(), optsForAction.root) + const env = optsForAction.env // Update logger config if (argv.silent) { @@ -277,8 +280,8 @@ export class GardenCli { ) } - const ctx = await Garden.factory(root, { logger, plugins: defaultPlugins }) - return command.action(ctx, argsForAction, optsForAction) + const garden = await Garden.factory(root, { env, logger, plugins: defaultPlugins }) + return command.action(garden.pluginContext, argsForAction, optsForAction) } // Command specific positional args and options are set inside the builder function diff --git a/src/commands/base.ts b/src/commands/base.ts index 81d5cc7dde..b0228908c2 100644 --- a/src/commands/base.ts +++ b/src/commands/base.ts @@ -6,7 +6,7 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import { Garden } from "../garden" +import { PluginContext } from "../plugin-context" export class ValidationError extends Error { } @@ -132,5 +132,5 @@ export abstract class Command, opts: ParameterValues): Promise + abstract async action(ctx: PluginContext, args: ParameterValues, opts: ParameterValues): Promise } diff --git a/src/commands/build.ts b/src/commands/build.ts index a9a50120d4..4f7fd65be2 100644 --- a/src/commands/build.ts +++ b/src/commands/build.ts @@ -6,8 +6,8 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +import { PluginContext } from "../plugin-context" import { BooleanParameter, Command, ParameterValues, StringParameter } from "./base" -import { Garden } from "../garden" import { BuildTask } from "../tasks/build" import { values } from "lodash" import { TaskResults } from "../task-graph" @@ -32,8 +32,8 @@ export class BuildCommand extends Command { - await ctx.buildDir.clear() + async action(ctx: PluginContext, args: BuildArguments, opts: BuildOptions): Promise { + await ctx.clearBuilds() const names = args.module ? args.module.split(",") : undefined const modules = await ctx.getModules(names) diff --git a/src/commands/call.ts b/src/commands/call.ts index 675fc96133..16ebe4bb9e 100644 --- a/src/commands/call.ts +++ b/src/commands/call.ts @@ -9,12 +9,12 @@ import { resolve } from "url" import Axios from "axios" import chalk from "chalk" -import { Command, EnvironmentOption, ParameterValues, StringParameter } from "./base" -import { Garden } from "../garden" +import { Command, ParameterValues, StringParameter } from "./base" import { splitFirst } from "../util" import { ParameterError, RuntimeError } from "../exceptions" import { EntryStyle } from "../logger/types" import { pick } from "lodash" +import { PluginContext } from "../plugin-context" export const callArgs = { serviceAndPath: new StringParameter({ @@ -23,25 +23,15 @@ export const callArgs = { }), } -export const options = { - env: new EnvironmentOption({ - help: "The environment (and optionally namespace) to call to", - }), -} - export type Args = ParameterValues -export type Opts = ParameterValues export class CallCommand extends Command { name = "call" help = "Call a service endpoint" arguments = callArgs - options = options - - async action(ctx: Garden, args: Args, opts: Opts) { - opts.env && ctx.setEnvironment(opts.env) + async action(ctx: PluginContext, args: Args) { let [serviceName, path] = splitFirst(args.serviceAndPath, "/") path = "/" + path diff --git a/src/commands/config/delete.ts b/src/commands/config/delete.ts index 4d4a5a5b8d..8817558eb4 100644 --- a/src/commands/config/delete.ts +++ b/src/commands/config/delete.ts @@ -6,8 +6,8 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import { Command, EnvironmentOption, ParameterValues, StringParameter } from "../base" -import { Garden } from "../../garden" +import { PluginContext } from "../../plugin-context" +import { Command, ParameterValues, StringParameter } from "../base" import { NotFoundError } from "../../exceptions" export const configDeleteArgs = { @@ -17,27 +17,18 @@ export const configDeleteArgs = { }), } -export const configDeleteOpts = { - env: new EnvironmentOption({ - help: "Set the environment (and optionally namespace) to delete the config variable from", - }), -} - export type DeleteArgs = ParameterValues -export type DeleteOpts = ParameterValues // TODO: add --all option to remove all configs -export class ConfigDeleteCommand extends Command { +export class ConfigDeleteCommand extends Command { name = "delete" alias = "del" help = "Delete a configuration variable" arguments = configDeleteArgs - options = configDeleteOpts - async action(ctx: Garden, args: DeleteArgs, opts: DeleteOpts) { - opts.env && ctx.setEnvironment(opts.env) + async action(ctx: PluginContext, args: DeleteArgs) { const res = await ctx.deleteConfig(args.key.split(".")) if (res.found) { diff --git a/src/commands/config/get.ts b/src/commands/config/get.ts index 577ba8e128..3add628325 100644 --- a/src/commands/config/get.ts +++ b/src/commands/config/get.ts @@ -6,8 +6,8 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import { Command, EnvironmentOption, ParameterValues, StringParameter } from "../base" -import { Garden } from "../../garden" +import { PluginContext } from "../../plugin-context" +import { Command, ParameterValues, StringParameter } from "../base" export const configGetArgs = { key: new StringParameter({ @@ -16,26 +16,17 @@ export const configGetArgs = { }), } -export const configGetOpts = { - env: new EnvironmentOption({ - help: "Get the environment (and optionally namespace) where the config should be stored", - }), -} - export type GetArgs = ParameterValues -export type GetOpts = ParameterValues // TODO: allow omitting key to return all configs -export class ConfigGetCommand extends Command { +export class ConfigGetCommand extends Command { name = "get" help = "Get a configuration variable" arguments = configGetArgs - options = configGetOpts - async action(ctx: Garden, args: GetArgs, opts: GetOpts) { - opts.env && ctx.setEnvironment(opts.env) + async action(ctx: PluginContext, args: GetArgs) { const res = await ctx.getConfig(args.key.split(".")) ctx.log.info(res) diff --git a/src/commands/config/set.ts b/src/commands/config/set.ts index 744c4e63e9..f9f13dd493 100644 --- a/src/commands/config/set.ts +++ b/src/commands/config/set.ts @@ -6,8 +6,8 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import { Command, EnvironmentOption, ParameterValues, StringParameter } from "../base" -import { Garden } from "../../garden" +import { PluginContext } from "../../plugin-context" +import { Command, ParameterValues, StringParameter } from "../base" export const configSetArgs = { key: new StringParameter({ @@ -20,26 +20,17 @@ export const configSetArgs = { }), } -export const configSetOpts = { - env: new EnvironmentOption({ - help: "Set the environment (and optionally namespace) where the config should be stored", - }), -} - export type SetArgs = ParameterValues -export type SetOpts = ParameterValues // TODO: allow reading key/value pairs from a file -export class ConfigSetCommand extends Command { +export class ConfigSetCommand extends Command { name = "set" help = "Set a configuration variable" arguments = configSetArgs - options = configSetOpts - async action(ctx: Garden, args: SetArgs, opts: SetOpts) { - opts.env && ctx.setEnvironment(opts.env) + async action(ctx: PluginContext, args: SetArgs) { await ctx.setConfig(args.key.split("."), args.value) ctx.log.info(`Set config key ${args.key}`) return { ok: true } diff --git a/src/commands/deploy.ts b/src/commands/deploy.ts index aeacaa0763..fb7edda73b 100644 --- a/src/commands/deploy.ts +++ b/src/commands/deploy.ts @@ -6,8 +6,8 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import { BooleanParameter, Command, EnvironmentOption, ParameterValues, StringParameter } from "./base" -import { Garden } from "../garden" +import { PluginContext } from "../plugin-context" +import { BooleanParameter, Command, ParameterValues, StringParameter } from "./base" import { DeployTask } from "../tasks/deploy" import { values } from "lodash" import { Service } from "../types/service" @@ -22,9 +22,6 @@ export const deployArgs = { } export const deployOpts = { - env: new EnvironmentOption({ - help: "Set the environment (and optionally namespace) to deploy to", - }), force: new BooleanParameter({ help: "Force redeploy of service(s)" }), "force-build": new BooleanParameter({ help: "Force rebuild of module(s)" }), } @@ -39,10 +36,9 @@ export class DeployCommand extends Command arguments = deployArgs options = deployOpts - async action(ctx: Garden, args: Args, opts: Opts): Promise { + async action(ctx: PluginContext, args: Args, opts: Opts): Promise { ctx.log.header({ emoji: "rocket", command: "Deploy" }) - opts.env && ctx.setEnvironment(opts.env) const names = args.service ? args.service.split(",") : undefined const services = await ctx.getServices(names) @@ -56,7 +52,7 @@ export class DeployCommand extends Command } export async function deployServices( - ctx: Garden, + ctx: PluginContext, services: Service[], force: boolean, forceBuild: boolean, diff --git a/src/commands/dev.ts b/src/commands/dev.ts index 2521ea4082..5cc8f2f010 100644 --- a/src/commands/dev.ts +++ b/src/commands/dev.ts @@ -6,8 +6,8 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import { Command, EnvironmentOption, ParameterValues } from "./base" -import { Garden } from "../garden" +import { PluginContext } from "../plugin-context" +import { Command } from "./base" import { join } from "path" import { STATIC_DIR } from "../constants" import { spawnSync } from "child_process" @@ -19,19 +19,11 @@ import { sleep } from "../util" const imgcatPath = join(__dirname, "..", "..", "bin", "imgcat") const bannerPath = join(STATIC_DIR, "garden-banner-1-half.png") -export const options = { - env: new EnvironmentOption(), -} - -export type Opts = ParameterValues - -export class DevCommand extends Command { +export class DevCommand extends Command { name = "dev" help = "Starts the garden development console" - options = options - - async action(ctx: Garden, _args, opts: Opts) { + async action(ctx: PluginContext) { try { spawnSync(imgcatPath, [bannerPath], { stdio: "inherit", @@ -44,8 +36,6 @@ export class DevCommand extends Command { // console.log(chalk.bold(` garden - dev\n`)) console.log(chalk.gray.italic(` Good afternoon, Jon! Let's get your environment wired up...\n`)) - opts.env && ctx.setEnvironment(opts.env) - await ctx.configureEnvironment() const services = values(await ctx.getServices()) diff --git a/src/commands/environment/configure.ts b/src/commands/environment/configure.ts index aa463efde4..5a389fb2f9 100644 --- a/src/commands/environment/configure.ts +++ b/src/commands/environment/configure.ts @@ -6,26 +6,16 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import { Command, EnvironmentOption, ParameterValues } from "../base" -import { Garden } from "../../garden" +import { PluginContext } from "../../plugin-context" +import { EnvironmentStatusMap } from "../../types/plugin" +import { Command } from "../base" -export const options = { - env: new EnvironmentOption({ - help: "Set the environment (and optionally namespace) to configure", - }), -} - -export type Opts = ParameterValues - -export class EnvironmentConfigureCommand extends Command { +export class EnvironmentConfigureCommand extends Command { name = "configure" alias = "config" help = "Configures your environment" - options = options - - async action(ctx: Garden, _args, opts: Opts) { - opts.env && ctx.setEnvironment(opts.env) + async action(ctx: PluginContext): Promise { const { name } = ctx.getEnvironment() ctx.log.header({ emoji: "gear", command: `Configuring ${name} environment` }) diff --git a/src/commands/environment/destroy.ts b/src/commands/environment/destroy.ts index 4a62823b43..1494e100d1 100644 --- a/src/commands/environment/destroy.ts +++ b/src/commands/environment/destroy.ts @@ -7,24 +7,17 @@ */ import { every, reduce } from "lodash" +import { PluginContext } from "../../plugin-context" -import { Command, EnvironmentOption, ParameterValues } from "../base" +import { Command } from "../base" import { EntryStyle } from "../../logger/types" import { EnvironmentStatus, EnvironmentStatusMap } from "../../types/plugin" -import { Garden } from "../../garden" import { LogEntry } from "../../logger" import { sleep } from "../../util" import { TimeoutError } from "../../exceptions" const WAIT_FOR_SHUTDOWN_TIMEOUT = 600 -export const options = { - env: new EnvironmentOption({ - help: "Set the environment (and optionally namespace) to destroy", - }), -} - -export type Opts = ParameterValues export type LogEntryMap = { [key: string]: LogEntry } const providersTerminated = (status: EnvironmentStatusMap): boolean => every(status, s => s.configured === false) @@ -34,8 +27,7 @@ export class EnvironmentDestroyCommand extends Command { alias = "d" help = "Destroy environment" - async action(ctx: Garden, _args, opts: Opts) { - opts.env && ctx.setEnvironment(opts.env) + async action(ctx: PluginContext) { const { name } = ctx.getEnvironment() ctx.log.header({ emoji: "skull_and_crossbones", command: `Destroying ${name} environment` }) @@ -65,7 +57,7 @@ export class EnvironmentDestroyCommand extends Command { return result } - async waitForShutdown(ctx: Garden, name: string, logEntries: LogEntryMap) { + async waitForShutdown(ctx: PluginContext, name: string, logEntries: LogEntryMap) { const startTime = new Date().getTime() let result: EnvironmentStatusMap diff --git a/src/commands/logs.ts b/src/commands/logs.ts index f74c6aae38..e09eb45712 100644 --- a/src/commands/logs.ts +++ b/src/commands/logs.ts @@ -6,10 +6,10 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import { BooleanParameter, Command, EnvironmentOption, ParameterValues, StringParameter } from "./base" -import { Garden } from "../garden" +import { PluginContext } from "../plugin-context" +import { BooleanParameter, Command, ParameterValues, StringParameter } from "./base" import chalk from "chalk" -import { GetServiceLogsParams, ServiceLogEntry } from "../types/plugin" +import { ServiceLogEntry } from "../types/plugin" import Bluebird = require("bluebird") import { values } from "lodash" import { Service } from "../types/service" @@ -23,7 +23,6 @@ export const logsArgs = { } export const logsOpts = { - env: new EnvironmentOption(), 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" }), @@ -39,9 +38,7 @@ export class LogsCommand extends Command { arguments = logsArgs options = logsOpts - async action(ctx: Garden, args: Args, opts: Opts) { - opts.env && ctx.setEnvironment(opts.env) - const env = ctx.getEnvironment() + async action(ctx: PluginContext, args: Args, opts: Opts) { const names = args.service ? args.service.split(",") : undefined const services = await ctx.getServices(names) @@ -57,17 +54,9 @@ 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(values(services), async (service: Service) => { - const handler = ctx.getActionHandler("getServiceLogs", service.module.type, dummyLogStreamer) - await handler({ ctx, service, env, stream, tail: opts.tail }) + await ctx.getServiceLogs(service, stream, opts.tail) }) return result } } - -async function dummyLogStreamer({ ctx, service }: GetServiceLogsParams) { - ctx.log.warn({ - section: service.name, - msg: chalk.yellow(`No handler for log retrieval available for module type ${service.module.type}`), - }) -} diff --git a/src/commands/status.ts b/src/commands/status.ts index 93045c3263..6575ed5c30 100644 --- a/src/commands/status.ts +++ b/src/commands/status.ts @@ -9,30 +9,21 @@ import Bluebird = require("bluebird") import { mapValues } from "lodash" import * as yaml from "js-yaml" -import { Command, EnvironmentOption, ParameterValues } from "./base" -import { Garden } from "../garden" +import { PluginContext } from "../plugin-context" +import { + EnvironmentStatusMap, +} from "../types/plugin" +import { Command } from "./base" import { Service } from "../types/service" import { highlightYaml } from "../util" -export const options = { - env: new EnvironmentOption({ - help: "The environment (and optionally namespace) to check", - }), -} - -export type Opts = ParameterValues - -export class StatusCommand extends Command { +export class StatusCommand extends Command { name = "status" alias = "s" help = "Outputs the status of your environment" - options = options - - async action(ctx: Garden, _args, opts: Opts) { - opts.env && ctx.setEnvironment(opts.env) - - const envStatus = await ctx.getEnvironmentStatus() + async action(ctx: PluginContext) { + const envStatus: EnvironmentStatusMap = await ctx.getEnvironmentStatus() const services = await ctx.getServices() const serviceStatus = await Bluebird.props( diff --git a/src/commands/test.ts b/src/commands/test.ts index 87eb406e74..86cf363be4 100644 --- a/src/commands/test.ts +++ b/src/commands/test.ts @@ -6,8 +6,8 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import { BooleanParameter, Command, EnvironmentOption, ParameterValues, StringParameter } from "./base" -import { Garden } from "../garden" +import { PluginContext } from "../plugin-context" +import { BooleanParameter, Command, ParameterValues, StringParameter } from "./base" import { values, padEnd } from "lodash" import { TestTask } from "../tasks/test" import { splitFirst } from "../util" @@ -22,7 +22,6 @@ export const testArgs = { } export const testOpts = { - env: new EnvironmentOption(), group: new StringParameter({ help: "Only run tests with the specfied group (e.g. unit or integ)", alias: "g", @@ -41,7 +40,7 @@ export class TestCommand extends Command { arguments = testArgs options = testOpts - async action(ctx: Garden, args: Args, opts: Opts): Promise { + async action(ctx: PluginContext, args: Args, opts: Opts): Promise { const names = args.module ? args.module.split(",") : undefined const modules = await ctx.getModules(names) @@ -50,7 +49,6 @@ export class TestCommand extends Command { command: `Running tests`, }) - opts.env && ctx.setEnvironment(opts.env) await ctx.configureEnvironment() for (const module of values(modules)) { diff --git a/src/commands/validate.ts b/src/commands/validate.ts index d9263f93b0..8dc42248e1 100644 --- a/src/commands/validate.ts +++ b/src/commands/validate.ts @@ -6,14 +6,14 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +import { PluginContext } from "../plugin-context" import { Command } from "./base" -import { Garden } from "../garden" export class ValidateCommand extends Command { name = "validate" help = "Check your garden configuration for errors" - async action(ctx: Garden) { + async action(ctx: PluginContext) { ctx.log.header({ emoji: "heavy_check_mark", command: "validate" }) diff --git a/src/garden.ts b/src/garden.ts index 8522e4f611..0c5d02566e 100644 --- a/src/garden.ts +++ b/src/garden.ts @@ -7,36 +7,36 @@ */ import { parse, relative, resolve } from "path" -import Bluebird = require("bluebird") -import { values, mapValues, fromPairs, toPairs } from "lodash" +import { values, fromPairs } from "lodash" import * as Joi from "joi" -import { Module, ModuleConfigType, TestSpec } from "./types/module" +import { + PluginContext, + createPluginContext, +} from "./plugin-context" +import { Module, ModuleConfigType } from "./types/module" import { ProjectConfig } from "./types/project" import { getIgnorer, scanDirectory } from "./util" import { DEFAULT_NAMESPACE, MODULE_CONFIG_FILENAME } from "./constants" -import { ConfigurationError, NotFoundError, ParameterError, PluginError } from "./exceptions" +import { + ConfigurationError, + ParameterError, + PluginError, +} from "./exceptions" import { VcsHandler } from "./vcs/base" import { GitHandler } from "./vcs/git" import { BuildDir } from "./build-dir" import { Task, TaskGraph, TaskResults } from "./task-graph" -import { getLogger, LogEntry, RootLogNode } from "./logger" +import { getLogger, RootLogNode } from "./logger" import { - BuildStatus, pluginActionNames, PluginActions, PluginFactory, Plugin, - BuildResult, - TestResult, - ExecInServiceResult, - DeleteConfigResult, - EnvironmentStatusMap, } from "./types/plugin" import { GenericModuleHandler } from "./plugins/generic" -import { Environment, joiIdentifier, PrimitiveMap, validate } from "./types/common" -import { Service, ServiceContext, ServiceStatus } from "./types/service" +import { Environment, joiIdentifier, validate } from "./types/common" +import { Service } from "./types/service" import { TemplateStringContext, getTemplateContext, resolveTemplateStrings } from "./template-string" -import { EntryStyle } from "./logger/types" import { loadConfig } from "./types/config" export interface ModuleMap { [key: string]: T } @@ -50,6 +50,7 @@ export type PluginActionMap = { } export interface ContextOpts { + env?: string, logger?: RootLogNode, plugins?: PluginFactory[], } @@ -64,9 +65,8 @@ export class Garden { public readonly actionHandlers: PluginActionMap public readonly projectName: string public readonly plugins: { [key: string]: Plugin } + public readonly pluginContext: PluginContext - // TODO: We may want to use the _ prefix for private properties even if it's not idiomatic TS, - // because we're supporting plain-JS plugins as well. private environment: string private namespace: string private readonly modules: ModuleMap @@ -78,14 +78,14 @@ export class Garden { vcs: VcsHandler constructor( - public projectRoot: string, public projectConfig: ProjectConfig, logger?: RootLogNode, + public projectRoot: string, public projectConfig: ProjectConfig, + env?: string, logger?: RootLogNode, ) { this.modulesScanned = false this.log = logger || getLogger() // TODO: Support other VCS options. - this.vcs = new GitHandler(this) - this.taskGraph = new TaskGraph(this) - this.buildDir = new BuildDir(this) + this.vcs = new GitHandler(this.projectRoot) + this.buildDir = new BuildDir(this.projectRoot) this.modules = {} this.services = {} @@ -99,10 +99,13 @@ export class Garden { this.configKeyNamespaces = ["project"] - this.setEnvironment(this.projectConfig.defaultEnvironment) + this.setEnvironment(env || this.projectConfig.defaultEnvironment) + + this.pluginContext = createPluginContext(this) + this.taskGraph = new TaskGraph(this.pluginContext) } - static async factory(projectRoot: string, { logger, plugins = [] }: ContextOpts = {}) { + static async factory(projectRoot: string, { env, logger, plugins = [] }: ContextOpts = {}) { // const localConfig = new LocalConfig(projectRoot) const templateContext = await getTemplateContext() const config = await resolveTemplateStrings(await loadConfig(projectRoot, projectRoot), templateContext) @@ -115,7 +118,7 @@ export class Garden { }) } - const ctx = new Garden(projectRoot, projectConfig, logger) + const ctx = new Garden(projectRoot, projectConfig, env, logger) // Load configured plugins plugins = builtinPlugins.concat(plugins) @@ -181,6 +184,10 @@ export class Garden { } } + async clearBuilds() { + return this.buildDir.clear() + } + async addTask(task: Task) { await this.taskGraph.addTask(task) } @@ -220,7 +227,7 @@ export class Garden { Returns all modules that are registered in this context. Scans for modules in the project root if it hasn't already been done. */ - async getModules(names?: string[], noScan?: boolean) { + async getModules(names?: string[], noScan?: boolean): Promise> { if (!this.modulesScanned && !noScan) { await this.scanModules() } @@ -361,7 +368,7 @@ export class Garden { ) } - this.services[serviceName] = await Service.factory(this, module, serviceName) + this.services[serviceName] = await Service.factory(this.pluginContext, module, serviceName) } } @@ -398,8 +405,7 @@ export class Garden { return null } - const parseHandler = this.getActionHandler("parseModule", moduleConfig.type) - return parseHandler({ ctx: this, config: moduleConfig }) + return this.pluginContext.parseModule(moduleConfig) } async getTemplateContext(extraContext: TemplateStringContext = {}): Promise { @@ -409,7 +415,7 @@ export class Garden { return { ...await getTemplateContext(), config: async (key: string[]) => { - return _this.getConfig(key) + return _this.pluginContext.getConfig(key) }, variables: this.projectConfig.variables, environment: { name: env.name, config: env.config }, @@ -417,136 +423,6 @@ export class Garden { } } - //=========================================================================== - //region Plugin actions - //=========================================================================== - - async getModuleBuildPath(module: T): Promise { - return await this.buildDir.buildPath(module) - } - - async getModuleBuildStatus(module: T): Promise { - const defaultHandler = this.actionHandlers["getModuleBuildStatus"]["generic"] - const handler = this.getActionHandler("getModuleBuildStatus", module.type, defaultHandler) - return handler({ ctx: this, module }) - } - - async buildModule(module: T, logEntry?: LogEntry): Promise { - await this.buildDir.syncDependencyProducts(module) - const defaultHandler = this.actionHandlers["buildModule"]["generic"] - const handler = this.getActionHandler("buildModule", module.type, defaultHandler) - return handler({ ctx: this, module, logEntry }) - } - - async testModule(module: T, testSpec: TestSpec, logEntry?: LogEntry): Promise { - const defaultHandler = this.actionHandlers["testModule"]["generic"] - const handler = this.getEnvActionHandler("testModule", module.type, defaultHandler) - const env = this.getEnvironment() - return handler({ ctx: this, module, testSpec, env, logEntry }) - } - - async getTestResult(module: T, version: string, logEntry?: LogEntry): Promise { - const handler = this.getEnvActionHandler("getTestResult", module.type, async () => null) - const env = this.getEnvironment() - return handler({ ctx: this, module, version, env, logEntry }) - } - - async getEnvironmentStatus() { - const handlers = this.getEnvActionHandlers("getEnvironmentStatus") - const env = this.getEnvironment() - return Bluebird.props(mapValues(handlers, h => h({ ctx: this, env }))) - } - - async configureEnvironment() { - const handlers = this.getEnvActionHandlers("configureEnvironment") - const env = this.getEnvironment() - const _this = this - - await Bluebird.each(toPairs(handlers), async ([name, handler]) => { - const logEntry = _this.log.info({ - entryStyle: EntryStyle.activity, - section: name, - msg: "Configuring...", - }) - - await handler({ ctx: this, env, logEntry }) - - logEntry.setSuccess("Configured") - }) - return this.getEnvironmentStatus() - } - - async destroyEnvironment(): Promise { - const handlers = this.getEnvActionHandlers("destroyEnvironment") - const env = this.getEnvironment() - await Bluebird.each(values(handlers), h => h({ ctx: this, env })) - return this.getEnvironmentStatus() - } - - async getServiceStatus(service: Service): Promise { - const handler = this.getEnvActionHandler("getServiceStatus", service.module.type) - return handler({ ctx: this, service, env: this.getEnvironment() }) - } - - async deployService(service: Service, serviceContext?: ServiceContext, logEntry?: LogEntry) { - const handler = this.getEnvActionHandler("deployService", service.module.type) - - if (!serviceContext) { - serviceContext = { envVars: {}, dependencies: {} } - } - - return handler({ ctx: this, service, serviceContext, env: this.getEnvironment(), logEntry }) - } - - async getServiceOutputs(service: Service): Promise { - // TODO: We might want to generally allow for "default handlers" - let handler: PluginActions["getServiceOutputs"] - try { - handler = this.getEnvActionHandler("getServiceOutputs", service.module.type) - } catch (err) { - return {} - } - return handler({ ctx: this, service, env: this.getEnvironment() }) - } - - async execInService(service: Service, command: string[]): Promise { - const handler = this.getEnvActionHandler("execInService", service.module.type) - return handler({ ctx: this, service, command, env: this.getEnvironment() }) - } - - async getConfig(key: string[]): Promise { - this.validateConfigKey(key) - // TODO: allow specifying which provider to use for configs - const handler = this.getEnvActionHandler("getConfig") - const value = await handler({ ctx: this, key, env: this.getEnvironment() }) - - if (value === null) { - throw new NotFoundError(`Could not find config key ${key}`, { key }) - } else { - return value - } - } - - async setConfig(key: string[], value: string) { - this.validateConfigKey(key) - const handler = this.getEnvActionHandler("setConfig") - return handler({ ctx: this, key, value, env: this.getEnvironment() }) - } - - async deleteConfig(key: string[]): Promise { - this.validateConfigKey(key) - const handler = this.getEnvActionHandler("deleteConfig") - const res = await handler({ ctx: this, key, env: this.getEnvironment() }) - - if (!res.found) { - throw new NotFoundError(`Could not find config key ${key}`, { key }) - } else { - return res - } - } - - //endregion - //=========================================================================== //region Internal helpers //=========================================================================== @@ -676,7 +552,7 @@ export class Garden { /** * Validates the specified config key, making sure it's properly formatted and matches defined keys. */ - private validateConfigKey(key: string[]) { + public validateConfigKey(key: string[]) { try { validate(key, Joi.array().items(joiIdentifier())) } catch (err) { diff --git a/src/plugin-context.ts b/src/plugin-context.ts new file mode 100644 index 0000000000..695e9c1204 --- /dev/null +++ b/src/plugin-context.ts @@ -0,0 +1,265 @@ +/* + * 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 chalk from "chalk" +import { Stream } from "ts-stream" +import { NotFoundError } from "./exceptions" +import { + Garden, +} from "./garden" +import { + LogEntry, +} from "./logger" +import { EntryStyle } from "./logger/types" +import { + PrimitiveMap, +} from "./types/common" +import { + Module, + TestSpec, +} from "./types/module" +import { + BuildResult, + BuildStatus, + DeleteConfigResult, + EnvironmentStatusMap, + ExecInServiceResult, + GetServiceLogsParams, + PluginActionParams, + PluginActions, + ServiceLogEntry, + TestResult, +} from "./types/plugin" +import { + Service, + ServiceContext, + ServiceStatus, +} from "./types/service" +import Bluebird = require("bluebird") +import { + mapValues, + toPairs, + values, +} from "lodash" + +export type PluginContextGuard = { + readonly [P in keyof PluginActionParams]: (...args: any[]) => Promise +} + +export type WrappedFromGarden = Pick + +export interface PluginContext extends PluginContextGuard, WrappedFromGarden { + parseModule: (config: T["_ConfigType"]) => Promise + getModuleBuildPath: (module: T) => Promise + getModuleBuildStatus: (module: T) => Promise + buildModule: (module: T, logEntry?: LogEntry) => Promise + testModule: (module: T, testSpec: TestSpec, logEntry?: LogEntry) => Promise + getTestResult: (module: T, version: string, logEntry?: LogEntry) => Promise + getEnvironmentStatus: () => Promise + configureEnvironment: () => Promise + destroyEnvironment: () => Promise + getServiceStatus: (service: Service) => Promise + deployService: ( + service: Service, serviceContext?: ServiceContext, logEntry?: LogEntry, + ) => Promise + getServiceOutputs: (service: Service) => Promise + execInService: (service: Service, command: string[]) => Promise + getServiceLogs: ( + service: Service, stream: Stream, tail?: boolean, + ) => Promise + getConfig: (key: string[]) => Promise + setConfig: (key: string[], value: string) => Promise + deleteConfig: (key: string[]) => Promise +} + +export function createPluginContext(garden: Garden): PluginContext { + function wrap(f) { + return f.bind(garden) + } + + const ctx: PluginContext = { + projectName: garden.projectName, + projectRoot: garden.projectRoot, + log: garden.log, + projectConfig: { ...garden.projectConfig }, + vcs: garden.vcs, + + // TODO: maybe we should move some of these here + clearBuilds: wrap(garden.clearBuilds), + getEnvironment: wrap(garden.getEnvironment), + getModules: wrap(garden.getModules), + getServices: wrap(garden.getServices), + getService: wrap(garden.getService), + getTemplateContext: wrap(garden.getTemplateContext), + addTask: wrap(garden.addTask), + processTasks: wrap(garden.processTasks), + + resolveModule: async (nameOrLocation: string) => { + const module = await garden.resolveModule(nameOrLocation) + return module ? module : null + }, + + parseModule: async (config: T["_ConfigType"]) => { + const handler = garden.getActionHandler("parseModule", config.type) + return handler({ ctx, config }) + }, + + getModuleBuildPath: async (module: T) => { + return await garden.buildDir.buildPath(module) + }, + + getModuleBuildStatus: async (module: T) => { + const defaultHandler = garden.actionHandlers["getModuleBuildStatus"]["generic"] + const handler = garden.getActionHandler("getModuleBuildStatus", module.type, defaultHandler) + return handler({ ctx, module }) + }, + + buildModule: async (module: T, logEntry?: LogEntry) => { + await garden.buildDir.syncDependencyProducts(module) + const defaultHandler = garden.actionHandlers["buildModule"]["generic"] + const handler = garden.getActionHandler("buildModule", module.type, defaultHandler) + return handler({ ctx, module, logEntry }) + }, + + testModule: async (module: T, testSpec: TestSpec, logEntry?: LogEntry) => { + const defaultHandler = garden.actionHandlers["testModule"]["generic"] + const handler = garden.getEnvActionHandler("testModule", module.type, defaultHandler) + const env = garden.getEnvironment() + return handler({ ctx, module, testSpec, env, logEntry }) + }, + + getTestResult: async (module: T, version: string, logEntry?: LogEntry) => { + const handler = garden.getEnvActionHandler("getTestResult", module.type, async () => null) + const env = garden.getEnvironment() + return handler({ ctx, module, version, env, logEntry }) + }, + + getEnvironmentStatus: async () => { + const handlers = garden.getEnvActionHandlers("getEnvironmentStatus") + const env = garden.getEnvironment() + return Bluebird.props(mapValues(handlers, h => h({ ctx, env }))) + }, + + configureEnvironment: async () => { + const handlers = garden.getEnvActionHandlers("configureEnvironment") + const env = garden.getEnvironment() + + await Bluebird.each(toPairs(handlers), async ([name, handler]) => { + const logEntry = garden.log.info({ + entryStyle: EntryStyle.activity, + section: name, + msg: "Configuring...", + }) + + await handler({ ctx, env, logEntry }) + + logEntry.setSuccess("Configured") + }) + return ctx.getEnvironmentStatus() + }, + + destroyEnvironment: async () => { + const handlers = garden.getEnvActionHandlers("destroyEnvironment") + const env = garden.getEnvironment() + await Bluebird.each(values(handlers), h => h({ ctx, env })) + return ctx.getEnvironmentStatus() + }, + + getServiceStatus: async (service: Service) => { + const handler = garden.getEnvActionHandler("getServiceStatus", service.module.type) + return handler({ ctx, service, env: garden.getEnvironment() }) + }, + + deployService: async ( + service: Service, serviceContext?: ServiceContext, logEntry?: LogEntry, + ) => { + const handler = garden.getEnvActionHandler("deployService", service.module.type) + + if (!serviceContext) { + serviceContext = { envVars: {}, dependencies: {} } + } + + return handler({ ctx, service, serviceContext, env: garden.getEnvironment(), logEntry }) + }, + + getServiceOutputs: async (service: Service) => { + // TODO: We might want to generally allow for "default handlers" + let handler: PluginActions["getServiceOutputs"] + try { + handler = garden.getEnvActionHandler("getServiceOutputs", service.module.type) + } catch (err) { + return {} + } + return handler({ ctx, service, env: garden.getEnvironment() }) + }, + + execInService: async (service: Service, command: string[]) => { + const handler = garden.getEnvActionHandler("execInService", service.module.type) + return handler({ ctx, service, command, env: garden.getEnvironment() }) + }, + + getServiceLogs: async (service: Service, stream: Stream, tail?: boolean) => { + const handler = garden.getEnvActionHandler("getServiceLogs", service.module.type, dummyLogStreamer) + return handler({ ctx, service, stream, tail, env: garden.getEnvironment() }) + }, + + getConfig: async (key: string[]) => { + garden.validateConfigKey(key) + // TODO: allow specifying which provider to use for configs + const handler = garden.getEnvActionHandler("getConfig") + const value = await handler({ ctx, key, env: garden.getEnvironment() }) + + if (value === null) { + throw new NotFoundError(`Could not find config key ${key}`, { key }) + } else { + return value + } + }, + + setConfig: async (key: string[], value: string) => { + garden.validateConfigKey(key) + const handler = garden.getEnvActionHandler("setConfig") + return handler({ ctx, key, value, env: garden.getEnvironment() }) + }, + + deleteConfig: async (key: string[]) => { + garden.validateConfigKey(key) + const handler = garden.getEnvActionHandler("deleteConfig") + const res = await handler({ ctx, key, env: garden.getEnvironment() }) + + if (!res.found) { + throw new NotFoundError(`Could not find config key ${key}`, { key }) + } else { + return res + } + }, + } + + 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}`), + }) +} diff --git a/src/plugins/container.ts b/src/plugins/container.ts index 75f2a7baca..aaae2eca2f 100644 --- a/src/plugins/container.ts +++ b/src/plugins/container.ts @@ -8,6 +8,7 @@ import * as Joi from "joi" import * as childProcess from "child-process-promise" +import { PluginContext } from "../plugin-context" import { baseModuleSchema, baseServiceSchema, Module, ModuleConfig } from "../types/module" import { LogSymbolType } from "../logger/types" import { identifierRegex, validate } from "../types/common" @@ -15,7 +16,6 @@ import { existsSync } from "fs" import { join } from "path" import { ConfigurationError } from "../exceptions" import { BuildModuleParams, GetModuleBuildStatusParams, Plugin } from "../types/plugin" -import { Garden } from "../garden" import { Service } from "../types/service" import { DEFAULT_PORT_PROTOCOL } from "../constants" @@ -121,7 +121,7 @@ export class ContainerService extends Service { } export class ContainerModule extends Module { image?: string - constructor(ctx: Garden, config: T) { + constructor(ctx: PluginContext, config: T) { super(ctx, config) this.image = config.image @@ -131,7 +131,7 @@ export class ContainerModule { name = "container-module" supportedModuleTypes = ["container"] - async parseModule({ ctx, config }: { ctx: Garden, config: ContainerModuleConfig }) { + async parseModule({ ctx, config }: { ctx: PluginContext, config: ContainerModuleConfig }) { config = validate(config, containerSchema, `module ${config.name}`) const module = new ContainerModule(ctx, config) diff --git a/src/plugins/google/google-cloud-functions.ts b/src/plugins/google/google-cloud-functions.ts index ab4f981d51..6dd9244e2b 100644 --- a/src/plugins/google/google-cloud-functions.ts +++ b/src/plugins/google/google-cloud-functions.ts @@ -6,9 +6,9 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +import { PluginContext } from "../../plugin-context" import { identifierRegex, validate } from "../../types/common" import { baseServiceSchema, Module, ModuleConfig } from "../../types/module" -import { Garden } from "../../garden" import { ServiceConfig, ServiceState, ServiceStatus } from "../../types/service" import { resolve } from "path" import * as Joi from "joi" @@ -39,7 +39,7 @@ export class GoogleCloudFunctionsProvider extends GoogleCloudProviderBase { const type = service.config.daemon ? "daemonsets" : "deployments" const hostname = getServiceHostname(ctx, service) @@ -179,7 +179,7 @@ export async function checkDeploymentStatus( export async function waitForDeployment( { ctx, service, logEntry, env }: - { ctx: Garden, service: ContainerService, logEntry?: LogEntry, env?: Environment }, + { ctx: PluginContext, service: ContainerService, logEntry?: LogEntry, env?: Environment }, ) { // NOTE: using `kubectl rollout status` here didn't pan out, since it just times out when errors occur. let loops = 0 diff --git a/src/plugins/kubernetes/system-global.ts b/src/plugins/kubernetes/system-global.ts index e09ac4f6c1..a9afeaeb92 100644 --- a/src/plugins/kubernetes/system-global.ts +++ b/src/plugins/kubernetes/system-global.ts @@ -8,7 +8,7 @@ import { join } from "path" import { STATIC_DIR } from "../../constants" -import { Garden } from "../../garden" +import { PluginContext } from "../../plugin-context" import { Environment } from "../../types/common" import { ConfigureEnvironmentParams, @@ -37,7 +37,7 @@ const dashboardSpecPath = join(dashboardModulePath, "dashboard.yml") export const localIngressPort = 32000 -export async function getGlobalSystemStatus(ctx: Garden, env: Environment) { +export async function getGlobalSystemStatus(ctx: PluginContext, env: Environment) { const gardenEnv = getSystemEnv(env) const systemNamespaceReady = namespaceReady(GARDEN_GLOBAL_SYSTEM_NAMESPACE) @@ -119,19 +119,19 @@ function getSystemEnv(env: Environment): Environment { return { name: env.name, namespace: GARDEN_GLOBAL_SYSTEM_NAMESPACE, config: { providers: {} } } } -async function getIngressControllerService(ctx: Garden) { +async function getIngressControllerService(ctx: PluginContext) { const module = await ctx.resolveModule(ingressControllerModulePath) return ContainerService.factory(ctx, module, "ingress-controller") } -async function getDefaultBackendService(ctx: Garden) { +async function getDefaultBackendService(ctx: PluginContext) { const module = await ctx.resolveModule(defaultBackendModulePath) return ContainerService.factory(ctx, module, "default-backend") } -async function getDashboardService(ctx: Garden) { +async function getDashboardService(ctx: PluginContext) { // TODO: implement raw kubernetes module load this module the same way as the ones above const module = new ContainerModule(ctx, { version: "0", diff --git a/src/plugins/local/local-docker-swarm.ts b/src/plugins/local/local-docker-swarm.ts index c6be5ec2ec..9db6134670 100644 --- a/src/plugins/local/local-docker-swarm.ts +++ b/src/plugins/local/local-docker-swarm.ts @@ -10,6 +10,7 @@ import * as Docker from "dockerode" import { exec } from "child-process-promise" import { Memoize } from "typescript-memoize" import { DeploymentError } from "../../exceptions" +import { PluginContext } from "../../plugin-context" import { DeployServiceParams, ExecInServiceParams, GetServiceOutputsParams, GetServiceStatusParams, Plugin, @@ -18,7 +19,6 @@ import { ContainerModule } from "../container" import { sortBy, map } from "lodash" import { sleep } from "../../util" import { ServiceState, ServiceStatus } from "../../types/service" -import { Garden } from "../../garden" // should this be configurable and/or global across providers? const DEPLOY_TIMEOUT = 30 @@ -261,7 +261,7 @@ export class LocalDockerSwarmProvider implements Plugin { return { code: 0, output: "", stdout: res.stdout, stderr: res.stderr } } - private getSwarmServiceName(ctx: Garden, serviceName: string) { + private getSwarmServiceName(ctx: PluginContext, serviceName: string) { return `${ctx.projectName}--${serviceName}` } diff --git a/src/plugins/local/local-google-cloud-functions.ts b/src/plugins/local/local-google-cloud-functions.ts index ac6df13a9a..c8ed47456d 100644 --- a/src/plugins/local/local-google-cloud-functions.ts +++ b/src/plugins/local/local-google-cloud-functions.ts @@ -6,6 +6,7 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +import { PluginContext } from "../../plugin-context" import { ServiceStatus } from "../../types/service" import { join, relative, resolve } from "path" import * as escapeStringRegexp from "escape-string-regexp" @@ -19,7 +20,6 @@ import { GetServiceStatusParams, ParseModuleParams, Plugin, } from "../../types/plugin" -import { Garden } from "../../garden" import { STATIC_DIR } from "../../constants" import { ContainerModule, ContainerService } from "../container" import { validate } from "../../types/common" @@ -131,14 +131,13 @@ export class LocalGoogleCloudFunctionsProvider implements Plugin) { + async getServiceLogs({ ctx, stream, tail }: GetServiceLogsParams) { const emulator = await this.getEmulatorService(ctx) - const handler = ctx.getActionHandler("getServiceLogs", "container") // TODO: filter to only relevant function logs - return handler({ ctx, service: emulator, env, stream, tail }) + return ctx.getServiceLogs(emulator, stream, tail) } - private async getEmulatorService(ctx: Garden) { + private async getEmulatorService(ctx: PluginContext) { const module = await ctx.resolveModule(emulatorModulePath) if (!module) { diff --git a/src/task-graph.ts b/src/task-graph.ts index 16eaf4796a..6398122b21 100644 --- a/src/task-graph.ts +++ b/src/task-graph.ts @@ -8,11 +8,11 @@ import * as Bluebird from "bluebird" import chalk from "chalk" -import { Garden } from "./garden" import { pick } from "lodash" import { EntryStyle, LogSymbolType } from "./logger/types" import { LogEntry } from "./logger" +import { PluginContext } from "./plugin-context" class TaskDefinitionError extends Error { } class TaskGraphError extends Error { } @@ -68,7 +68,7 @@ export class TaskGraph { private inProgress: TaskNodeMap private logEntryMap: LogEntryMap - constructor(private ctx: Garden, private concurrency: number = DEFAULT_CONCURRENCY) { + constructor(private ctx: PluginContext, private concurrency: number = DEFAULT_CONCURRENCY) { this.roots = new TaskNodeMap() this.index = new TaskNodeMap() this.inProgress = new TaskNodeMap() diff --git a/src/tasks/build.ts b/src/tasks/build.ts index 6a35302f77..20256fd262 100644 --- a/src/tasks/build.ts +++ b/src/tasks/build.ts @@ -6,9 +6,9 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +import { PluginContext } from "../plugin-context" import { Task } from "../task-graph" import { Module } from "../types/module" -import { Garden } from "../garden" import { EntryStyle } from "../logger/types" import chalk from "chalk" import { round } from "lodash" @@ -17,7 +17,7 @@ import { BuildResult } from "../types/plugin" export class BuildTask extends Task { type = "build" - constructor(private ctx: Garden, private module: T, private force: boolean) { + constructor(private ctx: PluginContext, private module: T, private force: boolean) { super() } diff --git a/src/tasks/deploy.ts b/src/tasks/deploy.ts index 9749ebe6db..7f61d24685 100644 --- a/src/tasks/deploy.ts +++ b/src/tasks/deploy.ts @@ -6,11 +6,14 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +import { PluginContext } from "../plugin-context" import { Task } from "../task-graph" -import { Garden } from "../garden" import { BuildTask } from "./build" import { values } from "lodash" -import { Service } from "../types/service" +import { + Service, + ServiceStatus, +} from "../types/service" import { EntryStyle } from "../logger/types" import chalk from "chalk" @@ -18,7 +21,7 @@ export class DeployTask> extends Task { type = "deploy" constructor( - private ctx: Garden, + private ctx: PluginContext, private service: T, private force: boolean, private forceBuild: boolean) { @@ -41,7 +44,7 @@ export class DeployTask> extends Task { return this.service.name } - async process() { + async process(): Promise { const entry = this.ctx.log.info({ section: this.service.name, msg: "Checking status", diff --git a/src/tasks/test.ts b/src/tasks/test.ts index 07297a0bd9..2333546e4a 100644 --- a/src/tasks/test.ts +++ b/src/tasks/test.ts @@ -6,12 +6,12 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +import { PluginContext } from "../plugin-context" import { Task } from "../task-graph" import { Module, TestSpec } from "../types/module" import { BuildTask } from "./build" import { TestResult } from "../types/plugin" import { DeployTask } from "./deploy" -import { Garden } from "../garden" import { EntryStyle } from "../logger/types" import chalk from "chalk" @@ -19,7 +19,7 @@ export class TestTask extends Task { type = "test" constructor( - private ctx: Garden, + private ctx: PluginContext, private module: T, private testType: string, private testSpec: TestSpec, private force: boolean, private forceBuild: boolean, ) { diff --git a/src/types/module.ts b/src/types/module.ts index da5f3310b8..6063331d3b 100644 --- a/src/types/module.ts +++ b/src/types/module.ts @@ -7,10 +7,10 @@ */ import * as Joi from "joi" +import { PluginContext } from "../plugin-context" import { identifierRegex, joiIdentifier, joiVariables, PrimitiveMap } from "./common" import { ConfigurationError } from "../exceptions" import Bluebird = require("bluebird") -import { Garden } from "../garden" import { ServiceConfig } from "./service" import { resolveTemplateStrings, TemplateStringContext } from "../template-string" import { Memoize } from "typescript-memoize" @@ -60,7 +60,7 @@ export class Module { _ConfigType: T - constructor(private ctx: Garden, private config: T) { + constructor(private ctx: PluginContext, private config: T) { this.name = config.name this.type = config.type this.path = config.path diff --git a/src/types/plugin.ts b/src/types/plugin.ts index 4fe24259a6..cd5489a411 100644 --- a/src/types/plugin.ts +++ b/src/types/plugin.ts @@ -6,8 +6,9 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import { Module, TestSpec } from "./module" import { Garden } from "../garden" +import { PluginContext } from "../plugin-context" +import { Module, TestSpec } from "./module" import { Environment, Primitive, PrimitiveMap } from "./common" import { Nullable } from "../util" import { Service, ServiceContext, ServiceStatus } from "./service" @@ -16,7 +17,7 @@ import { Stream } from "ts-stream" import { Moment } from "moment" export interface PluginActionParamsBase { - ctx: Garden + ctx: PluginContext logEntry?: LogEntry } @@ -233,4 +234,4 @@ export interface Plugin extends Partial> { configKeys?: string[] } -export type PluginFactory = (ctx: Garden) => Plugin +export type PluginFactory = (garden: Garden) => Plugin diff --git a/src/types/service.ts b/src/types/service.ts index c9506bab5b..97b6cb6508 100644 --- a/src/types/service.ts +++ b/src/types/service.ts @@ -8,9 +8,9 @@ import Bluebird = require("bluebird") import * as Joi from "joi" +import { PluginContext } from "../plugin-context" import { Module } from "./module" import { joiPrimitive, PrimitiveMap, validate } from "./common" -import { Garden } from "../garden" import { ConfigurationError } from "../exceptions" import { resolveTemplateStrings, TemplateOpts, TemplateStringContext } from "../template-string" @@ -58,13 +58,13 @@ const serviceOutputsSchema = Joi.object().pattern(/.+/, joiPrimitive()) export class Service { constructor( - protected ctx: Garden, public module: M, + protected ctx: PluginContext, public module: M, public name: string, public config: M["services"][string], ) { } static async factory, M extends Module>( - this: (new (ctx: Garden, module: M, name: string, config: S["config"]) => S), - ctx: Garden, module: M, name: string, + this: (new (ctx: PluginContext, module: M, name: string, config: S["config"]) => S), + ctx: PluginContext, module: M, name: string, ) { const config = module.services[name] diff --git a/src/vcs/base.ts b/src/vcs/base.ts index d65f82b5bf..f77d87926a 100644 --- a/src/vcs/base.ts +++ b/src/vcs/base.ts @@ -6,12 +6,10 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import { Garden } from "../garden" - export const NEW_MODULE_VERSION = "0000000000" export abstract class VcsHandler { - constructor(protected ctx: Garden) { } + constructor(protected projectRoot: string) { } abstract async getTreeVersion(directories): Promise abstract async sortVersions(versions: string[]): Promise diff --git a/src/vcs/git.ts b/src/vcs/git.ts index ce9c33ea3c..55aa96479b 100644 --- a/src/vcs/git.ts +++ b/src/vcs/git.ts @@ -97,6 +97,6 @@ export class GitHandler extends VcsHandler { } private async git(args) { - return exec("git " + args, { cwd: this.ctx.projectRoot }) + return exec("git " + args, { cwd: this.projectRoot }) } } diff --git a/test/helpers.ts b/test/helpers.ts index e1bb7e4368..29d15d5952 100644 --- a/test/helpers.ts +++ b/test/helpers.ts @@ -1,5 +1,6 @@ import * as td from "testdouble" import { resolve } from "path" +import { PluginContext } from "../src/plugin-context" import { DeleteConfigParams, GetConfigParams, ParseModuleParams, Plugin, PluginActions, PluginFactory, @@ -77,24 +78,34 @@ export const makeTestModule = (ctx, name = "test") => { }) } -export const makeTestContext = async (projectRoot: string, extraPlugins: PluginFactory[] = []) => { +export const makeTestGarden = async (projectRoot: string, extraPlugins: PluginFactory[] = []) => { const testPlugins: PluginFactory[] = [ (_ctx) => new TestPlugin(), (_ctx) => new TestPluginB(), ] const plugins: PluginFactory[] = testPlugins.concat(extraPlugins) - return await Garden.factory(projectRoot, { plugins }) + return Garden.factory(projectRoot, { plugins }) +} + +export const makeTestContext = async (projectRoot: string, extraPlugins: PluginFactory[] = []) => { + const garden = await makeTestGarden(projectRoot, extraPlugins) + return garden.pluginContext +} + +export const makeTestGardenA = async (extraPlugins: PluginFactory[] = []) => { + return makeTestGarden(projectRootA, extraPlugins) } export const makeTestContextA = async (extraPlugins: PluginFactory[] = []) => { - return makeTestContext(projectRootA, extraPlugins) + const garden = await makeTestGardenA(extraPlugins) + return garden.pluginContext } export function stubPluginAction> ( - ctx: Garden, pluginName: string, type: T, handler?: PluginActions[T], + garden: Garden, pluginName: string, type: T, handler?: PluginActions[T], ) { - return td.replace(ctx["actionHandlers"][type], pluginName, handler) + return td.replace(garden["actionHandlers"][type], pluginName, handler) } export async function expectError(fn: Function, typeOrCallback: string | ((err: any) => void)) { diff --git a/test/plugin-context.ts b/test/plugin-context.ts new file mode 100644 index 0000000000..fe31bc5081 --- /dev/null +++ b/test/plugin-context.ts @@ -0,0 +1,109 @@ +import { expect } from "chai" +import { + expectError, + makeTestContextA, +} from "./helpers" + +describe("PluginContext", () => { + describe("setConfig", () => { + it("should set a valid key in the 'project' namespace", async () => { + const ctx = await makeTestContextA() + + const key = ["project", "my", "variable"] + const value = "myvalue" + + await ctx.setConfig(key, value) + expect(await ctx.getConfig(key)).to.equal(value) + }) + + it("should throw with an invalid namespace in the key", async () => { + const ctx = await makeTestContextA() + + 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 ctx = await makeTestContextA() + + 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 ctx = await makeTestContextA() + + const key = ["project", "my", "variable"] + const value = "myvalue" + + await ctx.setConfig(key, value) + expect(await ctx.getConfig(key)).to.equal(value) + }) + + it("should throw if key does not exist", async () => { + const ctx = await makeTestContextA() + + const key = ["project", "my", "variable"] + + await expectError(async () => await ctx.getConfig(key), "not-found") + }) + + it("should throw with an invalid namespace in the key", async () => { + const ctx = await makeTestContextA() + + const key = ["bla", "my", "variable"] + + await expectError(async () => await ctx.getConfig(key), "parameter") + }) + + it("should throw with malformatted key", async () => { + const ctx = await makeTestContextA() + + 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 ctx = await makeTestContextA() + + 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 ctx = await makeTestContextA() + + const key = ["project", "my", "variable"] + + await expectError(async () => await ctx.deleteConfig(key), "not-found") + }) + + it("should throw with an invalid namespace in the key", async () => { + const ctx = await makeTestContextA() + + const key = ["bla", "my", "variable"] + + await expectError(async () => await ctx.deleteConfig(key), "parameter") + }) + + it("should throw with malformatted key", async () => { + const ctx = await makeTestContextA() + + const key = ["project", "!4215"] + + await expectError(async () => await ctx.deleteConfig(key), "parameter") + }) + }) +}) diff --git a/test/src/build-dir.ts b/test/src/build-dir.ts index cfbf021709..1c2c508bf8 100644 --- a/test/src/build-dir.ts +++ b/test/src/build-dir.ts @@ -3,10 +3,11 @@ import { pathExists, readdir } from "fs-extra" import { expect } from "chai" const nodetree = require("nodetree") import { values } from "lodash" -import { defaultPlugins } from "../../src/plugins"; -import { Garden } from "../../src/garden"; -import { BuildTask } from "../../src/tasks/build"; -import { makeTestContext } from "../helpers"; +import { defaultPlugins } from "../../src/plugins" +import { BuildTask } from "../../src/tasks/build" +import { + makeTestGarden, +} from "../helpers" /* Module dependency diagram for test-project-build-products @@ -20,47 +21,47 @@ import { makeTestContext } from "../helpers"; const projectRoot = join(__dirname, "..", "data", "test-project-build-products") -const makeContext = async () => { - return await makeTestContext(projectRoot, defaultPlugins) +const makeGarden = async () => { + return await makeTestGarden(projectRoot, defaultPlugins) } describe("BuildDir", () => { it("should have ensured the existence of the build dir when Garden was initialized", async () => { - const ctx = await makeContext() - const buildDirExists = await pathExists(ctx.buildDir.buildDirPath) + const garden = await makeGarden() + const buildDirExists = await pathExists(garden.buildDir.buildDirPath) expect(buildDirExists).to.eql(true) }) it("should clear the build dir when requested", async () => { - const ctx = await makeContext() - await ctx.buildDir.clear() - const nodeCount = await readdir(ctx.buildDir.buildDirPath) + const garden = await makeGarden() + await garden.buildDir.clear() + const nodeCount = await readdir(garden.buildDir.buildDirPath) expect(nodeCount).to.eql([]) }) it("should ensure that a module's build subdir exists before returning from buildPath", async () => { - const ctx = await makeContext() - await ctx.buildDir.clear() - const modules = await ctx.getModules() + const garden = await makeGarden() + await garden.buildDir.clear() + const modules = await garden.getModules() const moduleA = modules["module-a"] - const buildPath = await ctx.buildDir.buildPath(moduleA) + const buildPath = await garden.buildDir.buildPath(moduleA) expect(await pathExists(buildPath)).to.eql(true) }) it("should sync sources to the build dir", async () => { - const ctx = await makeContext() - const modules = await ctx.getModules() + const garden = await makeGarden() + const modules = await garden.getModules() const moduleA = modules["module-a"] - await ctx.buildDir.syncFromSrc(moduleA) - const buildDirA = await ctx.buildDir.buildPath(moduleA) + await garden.buildDir.syncFromSrc(moduleA) + const buildDirA = await garden.buildDir.buildPath(moduleA) const copiedPaths = [ join(buildDirA, "garden.yml"), - join(buildDirA, "some-dir", "some-file") + join(buildDirA, "some-dir", "some-file"), ] - const buildDirPrettyPrint = nodetree(ctx.buildDir.buildDirPath) + const buildDirPrettyPrint = nodetree(garden.buildDir.buildDirPath) for (const p of copiedPaths) { expect(await pathExists(p)).to.eql(true, buildDirPrettyPrint) @@ -68,26 +69,26 @@ describe("BuildDir", () => { }) it("should sync dependency products to their specified destinations", async () => { - const ctx = await makeContext() + const garden = await makeGarden() try { - await ctx.buildDir.clear() - const modules = await ctx.getModules() + await garden.clearBuilds() + const modules = await garden.getModules() for (const module of values(modules)) { - await ctx.addTask(new BuildTask(ctx, module, false)) + await garden.addTask(new BuildTask(garden.pluginContext, module, false)) } - await ctx.processTasks() + await garden.processTasks() - const buildDirD = await ctx.buildDir.buildPath(modules["module-d"]) - const buildDirE = await ctx.buildDir.buildPath(modules["module-e"]) + const buildDirD = await garden.buildDir.buildPath(modules["module-d"]) + const buildDirE = await garden.buildDir.buildPath(modules["module-e"]) // All these destinations should be populated now. const buildProductDestinations = [ - join(buildDirD, 'a', 'a.txt'), - join(buildDirD, 'b', 'build', 'b1.txt'), - join(buildDirD, 'b', 'build', 'build_subdir', 'b2.txt'), - join(buildDirE, 'd', 'build', 'd.txt') + join(buildDirD, "a", "a.txt"), + join(buildDirD, "b", "build", "b1.txt"), + join(buildDirD, "b", "build", "build_subdir", "b2.txt"), + join(buildDirE, "d", "build", "d.txt"), ] for (const p of buildProductDestinations) { @@ -95,10 +96,10 @@ describe("BuildDir", () => { } // This file was not requested by module-d's garden.yml's copy directive for module-b. - const notCopiedPath = join(buildDirD, 'B', 'build', 'unused.txt') + const notCopiedPath = join(buildDirD, "B", "build", "unused.txt") expect(await pathExists(notCopiedPath)).to.eql(false) } catch (e) { - const buildDirPrettyPrint = nodetree(ctx.buildDir.buildDirPath) + const buildDirPrettyPrint = nodetree(garden.buildDir.buildDirPath) console.log(buildDirPrettyPrint) throw e } diff --git a/test/src/commands/call.ts b/test/src/commands/call.ts index 970247ee60..ac3b7b0758 100644 --- a/test/src/commands/call.ts +++ b/test/src/commands/call.ts @@ -44,7 +44,8 @@ describe("commands.call", () => { }) it("should find the endpoint for a service and call it with the specified path", async () => { - const ctx = await Garden.factory(projectRootB, { plugins: [() => new TestProvider()] }) + const garden = await Garden.factory(projectRootB, { plugins: [() => new TestProvider()] }) + const ctx = garden.pluginContext const command = new CallCommand() nock("http://service-a.test-project-b.local.app.garden:32000") @@ -70,7 +71,8 @@ describe("commands.call", () => { }) it("should error if service isn't running", async () => { - const ctx = await Garden.factory(projectRootB, { plugins: [() => new TestProvider()] }) + const garden = await Garden.factory(projectRootB, { plugins: [() => new TestProvider()] }) + const ctx = garden.pluginContext const command = new CallCommand() try { @@ -92,7 +94,8 @@ describe("commands.call", () => { }) it("should error if service has no endpoints", async () => { - const ctx = await Garden.factory(projectRootB, { plugins: [() => new TestProvider()] }) + const garden = await Garden.factory(projectRootB, { plugins: [() => new TestProvider()] }) + const ctx = garden.pluginContext const command = new CallCommand() try { @@ -114,7 +117,8 @@ describe("commands.call", () => { }) it("should error if service has no matching endpoints", async () => { - const ctx = await Garden.factory(projectRootB, { plugins: [() => new TestProvider()] }) + const garden = await Garden.factory(projectRootB, { plugins: [() => new TestProvider()] }) + const ctx = garden.pluginContext const command = new CallCommand() try { diff --git a/test/src/commands/deploy.ts b/test/src/commands/deploy.ts index f51994f64d..87bcf89c63 100644 --- a/test/src/commands/deploy.ts +++ b/test/src/commands/deploy.ts @@ -35,7 +35,8 @@ describe("commands.deploy", () => { // TODO: Verify that services don't get redeployed when same version is already deployed. it("should build and deploy all modules in a project", async () => { - const ctx = await Garden.factory(projectRootB, { plugins: defaultPlugins.concat([() => new TestProvider()]) }) + const garden = await Garden.factory(projectRootB, { plugins: defaultPlugins.concat([() => new TestProvider()]) }) + const ctx = garden.pluginContext const command = new DeployCommand() const result = await command.action( @@ -43,7 +44,6 @@ describe("commands.deploy", () => { service: "", }, { - env: "local", force: false, "force-build": true, }, @@ -60,7 +60,8 @@ describe("commands.deploy", () => { }) it("should optionally build and deploy single service and its dependencies", async () => { - const ctx = await Garden.factory(projectRootB, { plugins: defaultPlugins.concat([() => new TestProvider()]) }) + const garden = await Garden.factory(projectRootB, { plugins: defaultPlugins.concat([() => new TestProvider()]) }) + const ctx = garden.pluginContext const command = new DeployCommand() const result = await command.action( @@ -69,7 +70,6 @@ describe("commands.deploy", () => { service: "service-b", }, { - env: "local", force: false, "force-build": true, }, diff --git a/test/src/commands/environment/destroy.ts b/test/src/commands/environment/destroy.ts index 80f0b6b58f..cd67c63c43 100644 --- a/test/src/commands/environment/destroy.ts +++ b/test/src/commands/environment/destroy.ts @@ -6,14 +6,12 @@ import { defaultPlugins } from "../../../../src/plugins" import { DestroyEnvironmentParams, EnvironmentStatus, - EnvironmentStatusMap, GetEnvironmentStatusParams, Plugin, } from "../../../../src/types/plugin" import { EnvironmentDestroyCommand } from "../../../../src/commands/environment/destroy" import { Garden } from "../../../../src/garden" import { Module } from "../../../../src/types/module" -import { sleep } from "../../../../src/util" class TestProvider implements Plugin { name = "test-plugin" @@ -45,17 +43,17 @@ describe("EnvironmentDestroyCommand", () => { const command = new EnvironmentDestroyCommand() it("should destroy environment", async () => { - const ctx = await Garden.factory(projectRootB, { + const garden = await Garden.factory(projectRootB, { plugins: defaultPlugins.concat([() => new TestProvider()]), }) - const result = await command.action(ctx, {}, { env: undefined }) + const result = await command.action(garden.pluginContext, {}, { env: undefined }) expect(result["test-plugin"]["configured"]).to.be.false }) it("should wait until each provider is no longer configured", async () => { - const ctx = await Garden.factory(projectRootB, { + const garden = await Garden.factory(projectRootB, { plugins: defaultPlugins.concat([() => new TestProviderSlow()]), }) @@ -67,7 +65,7 @@ describe("EnvironmentDestroyCommand", () => { }), ) - const result = await command.action(ctx, {}, { env: undefined }) + const result = await command.action(garden.pluginContext, {}, { env: undefined }) expect(result["test-plugin"]["configured"]).to.be.false }) diff --git a/test/src/commands/test.ts b/test/src/commands/test.ts index 904eb6a948..82275513fe 100644 --- a/test/src/commands/test.ts +++ b/test/src/commands/test.ts @@ -11,7 +11,7 @@ describe("commands.test", () => { const result = await command.action( ctx, { module: undefined }, - { env: "local.test", group: undefined, force: true, "force-build": true }, + { group: undefined, force: true, "force-build": true }, ) expect(isSubset(result, { @@ -46,7 +46,7 @@ describe("commands.test", () => { const result = await command.action( ctx, { module: "module-a" }, - { env: "local.test", group: undefined, force: true, "force-build": true }, + { group: undefined, force: true, "force-build": true }, ) expect(isSubset(result, { diff --git a/test/src/commands/validate.ts b/test/src/commands/validate.ts index 78b4340a59..5f6c913e71 100644 --- a/test/src/commands/validate.ts +++ b/test/src/commands/validate.ts @@ -7,10 +7,10 @@ import { expectError } from "../../helpers" describe("commands.validate", () => { it("should successfully validate the hello-world project", async () => { const root = join(__dirname, "..", "..", "..", "examples", "hello-world") - const ctx = await Garden.factory(root, { plugins: defaultPlugins }) + const garden = await Garden.factory(root, { plugins: defaultPlugins }) const command = new ValidateCommand() - await command.action(ctx) + await command.action(garden.pluginContext) }) it("should fail validating the bad-project project", async () => { @@ -21,9 +21,9 @@ describe("commands.validate", () => { it("should fail validating the bad-module project", async () => { const root = join(__dirname, "data", "validate", "bad-module") - const ctx = await Garden.factory(root, { plugins: defaultPlugins }) + const garden = await Garden.factory(root, { plugins: defaultPlugins }) const command = new ValidateCommand() - await expectError(async () => await command.action(ctx), "configuration") + await expectError(async () => await command.action(garden.pluginContext), "configuration") }) }) diff --git a/test/src/context.ts b/test/src/garden.ts similarity index 73% rename from test/src/context.ts rename to test/src/garden.ts index 6f3ada1a3e..9b1a1ca17b 100644 --- a/test/src/context.ts +++ b/test/src/garden.ts @@ -2,7 +2,7 @@ import { join } from "path" import { Garden } from "../../src/garden" import { expect } from "chai" import { - expectError, makeTestContext, makeTestContextA, makeTestModule, projectRootA, + expectError, makeTestGarden, makeTestGardenA, makeTestModule, projectRootA, TestPlugin, } from "../helpers" @@ -12,7 +12,7 @@ describe("Garden", () => { }) it("should initialize add the action handlers for a plugin", async () => { - const ctx = await makeTestContextA() + const ctx = await makeTestGardenA() expect(ctx.plugins["test-plugin"]).to.be.ok expect(ctx.actionHandlers.configureEnvironment["test-plugin"]).to.be.ok @@ -37,7 +37,7 @@ describe("Garden", () => { }) it("should parse the config from the project root", async () => { - const ctx = await makeTestContextA() + const ctx = await makeTestGardenA() const config = ctx.projectConfig expect(config).to.eql({ @@ -69,7 +69,7 @@ describe("Garden", () => { const projectRoot = join(__dirname, "..", "data", "test-project-templated") - const ctx = await makeTestContext(projectRoot) + const ctx = await makeTestGarden(projectRoot) const config = ctx.projectConfig delete process.env.TEST_PROVIDER_TYPE @@ -97,7 +97,7 @@ describe("Garden", () => { describe("setEnvironment", () => { it("should set the active environment for the context", async () => { - const ctx = await makeTestContextA() + const ctx = await makeTestGardenA() const { name, namespace } = ctx.setEnvironment("local") expect(name).to.equal("local") @@ -109,7 +109,7 @@ describe("Garden", () => { }) it("should optionally set a namespace with the dot separator", async () => { - const ctx = await makeTestContextA() + const ctx = await makeTestGardenA() const { name, namespace } = ctx.setEnvironment("local.mynamespace") expect(name).to.equal("local") @@ -117,7 +117,7 @@ describe("Garden", () => { }) it("should split environment and namespace on the first dot", async () => { - const ctx = await makeTestContextA() + const ctx = await makeTestGardenA() const { name, namespace } = ctx.setEnvironment("local.mynamespace.2") expect(name).to.equal("local") @@ -125,7 +125,7 @@ describe("Garden", () => { }) it("should throw if the specified environment isn't configured", async () => { - const ctx = await makeTestContextA() + const ctx = await makeTestGardenA() try { ctx.setEnvironment("bla") @@ -138,7 +138,7 @@ describe("Garden", () => { }) it("should throw if namespace starts with 'garden-'", async () => { - const ctx = await makeTestContextA() + const ctx = await makeTestGardenA() try { ctx.setEnvironment("local.garden-bla") @@ -153,7 +153,7 @@ describe("Garden", () => { describe("getEnvironment", () => { it("should get the active environment for the context", async () => { - const ctx = await makeTestContextA() + const ctx = await makeTestGardenA() const { name, namespace } = ctx.setEnvironment("other") expect(name).to.equal("other") @@ -165,7 +165,7 @@ describe("Garden", () => { }) it("should return default environment if none has been explicitly set", async () => { - const ctx = await makeTestContextA() + const ctx = await makeTestGardenA() const { name, namespace } = ctx.getEnvironment() expect(name).to.equal("local") @@ -175,21 +175,21 @@ describe("Garden", () => { describe("getModules", () => { it("should scan and return all registered modules in the context", async () => { - const ctx = await makeTestContextA() + const ctx = await makeTestGardenA() const modules = await ctx.getModules() expect(Object.keys(modules)).to.eql(["module-a", "module-b", "module-c"]) }) it("should optionally return specified modules in the context", async () => { - const ctx = await makeTestContextA() + const ctx = await makeTestGardenA() const modules = await ctx.getModules(["module-b", "module-c"]) expect(Object.keys(modules)).to.eql(["module-b", "module-c"]) }) it("should throw if named module is missing", async () => { - const ctx = await makeTestContextA() + const ctx = await makeTestGardenA() try { await ctx.getModules(["bla"]) @@ -204,21 +204,21 @@ describe("Garden", () => { describe("getServices", () => { it("should scan for modules and return all registered services in the context", async () => { - const ctx = await makeTestContextA() + const ctx = await makeTestGardenA() const services = await ctx.getServices() expect(Object.keys(services)).to.eql(["service-a", "service-b", "service-c"]) }) it("should optionally return specified services in the context", async () => { - const ctx = await makeTestContextA() + const ctx = await makeTestGardenA() const services = await ctx.getServices(["service-b", "service-c"]) expect(Object.keys(services)).to.eql(["service-b", "service-c"]) }) it("should throw if named service is missing", async () => { - const ctx = await makeTestContextA() + const ctx = await makeTestGardenA() try { await ctx.getServices(["bla"]) @@ -233,14 +233,14 @@ describe("Garden", () => { describe("getService", () => { it("should return the specified service", async () => { - const ctx = await makeTestContextA() + const ctx = await makeTestGardenA() const service = await ctx.getService("service-b") expect(service.name).to.equal("service-b") }) it("should throw if service is missing", async () => { - const ctx = await makeTestContextA() + const ctx = await makeTestGardenA() try { await ctx.getServices(["bla"]) @@ -257,7 +257,7 @@ describe("Garden", () => { // TODO: assert that gitignore in project root is respected it("should scan the project root for modules and add to the context", async () => { - const ctx = await makeTestContextA() + const ctx = await makeTestGardenA() await ctx.scanModules() const modules = await ctx.getModules(undefined, true) @@ -267,7 +267,7 @@ describe("Garden", () => { describe("addModule", () => { it("should add the given module and its services to the context", async () => { - const ctx = await makeTestContextA() + const ctx = await makeTestGardenA() const testModule = makeTestModule(ctx) await ctx.addModule(testModule) @@ -280,7 +280,7 @@ describe("Garden", () => { }) it("should throw when adding module twice without force parameter", async () => { - const ctx = await makeTestContextA() + const ctx = await makeTestGardenA() const testModule = makeTestModule(ctx) await ctx.addModule(testModule) @@ -296,7 +296,7 @@ describe("Garden", () => { }) it("should allow adding module multiple times with force parameter", async () => { - const ctx = await makeTestContextA() + const ctx = await makeTestGardenA() const testModule = makeTestModule(ctx) await ctx.addModule(testModule) @@ -307,7 +307,7 @@ describe("Garden", () => { }) it("should throw if a service is added twice without force parameter", async () => { - const ctx = await makeTestContextA() + const ctx = await makeTestGardenA() const testModule = makeTestModule(ctx) const testModuleB = makeTestModule(ctx, "test-b") @@ -324,7 +324,7 @@ describe("Garden", () => { }) it("should allow adding service multiple times with force parameter", async () => { - const ctx = await makeTestContextA() + const ctx = await makeTestGardenA() const testModule = makeTestModule(ctx) const testModuleB = makeTestModule(ctx, "test-b") @@ -338,15 +338,15 @@ describe("Garden", () => { describe("resolveModule", () => { it("should return named module", async () => { - const ctx = await makeTestContextA() + const ctx = await makeTestGardenA() await ctx.scanModules() const module = await ctx.resolveModule("module-a") - expect(module.name).to.equal("module-a") + expect(module!.name).to.equal("module-a") }) it("should throw if named module is requested and not available", async () => { - const ctx = await makeTestContextA() + const ctx = await makeTestGardenA() try { await ctx.resolveModule("module-a") @@ -359,24 +359,24 @@ describe("Garden", () => { }) it("should resolve module by absolute path", async () => { - const ctx = await makeTestContextA() + const ctx = await makeTestGardenA() const path = join(projectRootA, "module-a") const module = await ctx.resolveModule(path) - expect(module.name).to.equal("module-a") + expect(module!.name).to.equal("module-a") }) it("should resolve module by relative path to project root", async () => { - const ctx = await makeTestContextA() + const ctx = await makeTestGardenA() const module = await ctx.resolveModule("./module-a") - expect(module.name).to.equal("module-a") + expect(module!.name).to.equal("module-a") }) }) describe("getTemplateContext", () => { it("should return the basic project context without parameters", async () => { - const ctx = await makeTestContextA() + const ctx = await makeTestGardenA() const result = await ctx.getTemplateContext() @@ -396,7 +396,7 @@ describe("Garden", () => { }) it("should extend the basic project context if specified", async () => { - const ctx = await makeTestContextA() + const ctx = await makeTestGardenA() const result = await ctx.getTemplateContext({ my: "things" }) @@ -419,7 +419,7 @@ describe("Garden", () => { describe("getActionHandlers", () => { it("should return all handlers for a type", async () => { - const ctx = await makeTestContextA() + const ctx = await makeTestGardenA() const handlers = ctx.getActionHandlers("parseModule") @@ -431,7 +431,7 @@ describe("Garden", () => { }) it("should optionally limit to handlers for specific module type", async () => { - const ctx = await makeTestContextA() + const ctx = await makeTestGardenA() const handlers = ctx.getActionHandlers("parseModule", "generic") @@ -444,7 +444,7 @@ describe("Garden", () => { describe("getActionHandler", () => { it("should return last configured handler for specified action type", async () => { - const ctx = await makeTestContextA() + const ctx = await makeTestGardenA() const handler = ctx.getActionHandler("parseModule") @@ -453,7 +453,7 @@ describe("Garden", () => { }) it("should optionally filter to only handlers for the specified module type", async () => { - const ctx = await makeTestContextA() + const ctx = await makeTestGardenA() const handler = ctx.getActionHandler("parseModule", "test") @@ -462,7 +462,7 @@ describe("Garden", () => { }) it("should throw if no handler is available", async () => { - const ctx = await makeTestContextA() + const ctx = await makeTestGardenA() try { ctx.getActionHandler("deployService", "container") @@ -477,7 +477,7 @@ describe("Garden", () => { describe("getEnvActionHandlers", () => { it("should return all handlers for a type that are configured for the set environment", async () => { - const ctx = await makeTestContextA() + const ctx = await makeTestGardenA() ctx.setEnvironment("local") const handlers = ctx.getEnvActionHandlers("configureEnvironment") @@ -485,7 +485,7 @@ describe("Garden", () => { }) it("should optionally limit to handlers that support a specific module type", async () => { - const ctx = await makeTestContextA() + const ctx = await makeTestGardenA() ctx.setEnvironment("local") const handlers = ctx.getEnvActionHandlers("configureEnvironment", "test") @@ -493,7 +493,7 @@ describe("Garden", () => { }) it("should throw if environment has not been set", async () => { - const ctx = await makeTestContextA() + const ctx = await makeTestGardenA() try { ctx.getEnvActionHandlers("configureEnvironment", "container") @@ -505,7 +505,7 @@ describe("Garden", () => { describe("getEnvActionHandler", () => { it("should return last configured handler for specified action type", async () => { - const ctx = await makeTestContextA() + const ctx = await makeTestGardenA() ctx.setEnvironment("local") const handler = ctx.getEnvActionHandler("configureEnvironment") @@ -515,7 +515,7 @@ describe("Garden", () => { }) it("should optionally filter to only handlers for the specified module type", async () => { - const ctx = await makeTestContextA() + const ctx = await makeTestGardenA() ctx.setEnvironment("local") const handler = ctx.getEnvActionHandler("deployService", "test") @@ -525,7 +525,7 @@ describe("Garden", () => { }) it("should throw if no handler is available", async () => { - const ctx = await makeTestContextA() + const ctx = await makeTestGardenA() ctx.setEnvironment("local") try { @@ -538,106 +538,4 @@ describe("Garden", () => { throw new Error("Expected error") }) }) - - describe("setConfig", () => { - it("should set a valid key in the 'project' namespace", async () => { - const ctx = await makeTestContextA() - - const key = ["project", "my", "variable"] - const value = "myvalue" - - await ctx.setConfig(key, value) - expect(await ctx.getConfig(key)).to.equal(value) - }) - - it("should throw with an invalid namespace in the key", async () => { - const ctx = await makeTestContextA() - - 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 ctx = await makeTestContextA() - - 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 ctx = await makeTestContextA() - - const key = ["project", "my", "variable"] - const value = "myvalue" - - await ctx.setConfig(key, value) - expect(await ctx.getConfig(key)).to.equal(value) - }) - - it("should throw if key does not exist", async () => { - const ctx = await makeTestContextA() - - const key = ["project", "my", "variable"] - - await expectError(async () => await ctx.getConfig(key), "not-found") - }) - - it("should throw with an invalid namespace in the key", async () => { - const ctx = await makeTestContextA() - - const key = ["bla", "my", "variable"] - - await expectError(async () => await ctx.getConfig(key), "parameter") - }) - - it("should throw with malformatted key", async () => { - const ctx = await makeTestContextA() - - 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 ctx = await makeTestContextA() - - 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 ctx = await makeTestContextA() - - const key = ["project", "my", "variable"] - - await expectError(async () => await ctx.deleteConfig(key), "not-found") - }) - - it("should throw with an invalid namespace in the key", async () => { - const ctx = await makeTestContextA() - - const key = ["bla", "my", "variable"] - - await expectError(async () => await ctx.deleteConfig(key), "parameter") - }) - - it("should throw with malformatted key", async () => { - const ctx = await makeTestContextA() - - const key = ["project", "!4215"] - - await expectError(async () => await ctx.deleteConfig(key), "parameter") - }) - }) }) diff --git a/test/src/task-graph.ts b/test/src/task-graph.ts index 706688c038..ca498ec9e3 100644 --- a/test/src/task-graph.ts +++ b/test/src/task-graph.ts @@ -31,7 +31,8 @@ describe("task-graph", () => { describe("TaskGraph", async () => { const projectRoot = join(__dirname, "..", "data", "test-project-empty") - const ctx = await Garden.factory(projectRoot, { plugins: defaultPlugins }) + const garden = await Garden.factory(projectRoot, { plugins: defaultPlugins }) + const ctx = garden.pluginContext it("should successfully process a single task without dependencies", async () => { const graph = new TaskGraph(ctx) diff --git a/test/src/tasks/deploy.ts b/test/src/tasks/deploy.ts index 8fe9ba2378..2807968a14 100644 --- a/test/src/tasks/deploy.ts +++ b/test/src/tasks/deploy.ts @@ -1,7 +1,11 @@ import { expect } from "chai" import { resolve } from "path" import * as td from "testdouble" -import { dataDir, makeTestContext, stubPluginAction } from "../../helpers" +import { + dataDir, + makeTestGarden, + stubPluginAction, +} from "../../helpers" import { DeployTask } from "../../../src/tasks/deploy" describe("DeployTask", () => { @@ -13,7 +17,8 @@ describe("DeployTask", () => { process.env.TEST_VARIABLE = "banana" process.env.TEST_PROVIDER_TYPE = "test-plugin-b" - const ctx = await makeTestContext(resolve(dataDir, "test-project-templated")) + const garden = await makeTestGarden(resolve(dataDir, "test-project-templated")) + const ctx = garden.pluginContext await ctx.setConfig(["project", "my", "variable"], "OK") const serviceA = await ctx.getService("service-a") @@ -23,12 +28,12 @@ describe("DeployTask", () => { let actionParams: any = {} stubPluginAction( - ctx, "test-plugin-b", "getServiceStatus", + garden, "test-plugin-b", "getServiceStatus", async () => ({}), ) stubPluginAction( - ctx, "test-plugin-b", "deployService", + garden, "test-plugin-b", "deployService", async (params) => { actionParams = params }, )