From 04b5417d390d9235148101aa340645d8ef2ae6e5 Mon Sep 17 00:00:00 2001 From: Jon Edvald Date: Fri, 13 Apr 2018 10:29:48 +0200 Subject: [PATCH] refactor: split GardenContext into Garden and PluginContext This is to reduce the size and scope of the GardenContext object, as well as to reduce visibility and access for plugin actions. I'm doing this now because I'm about to make further changes to plugin flows, and wanted to be done with this first. --- src/build-dir.ts | 9 +- src/cli.ts | 7 +- src/commands/base.ts | 4 +- src/commands/build.ts | 6 +- src/commands/call.ts | 16 +- src/commands/config/delete.ts | 17 +- src/commands/config/get.ts | 17 +- src/commands/config/set.ts | 17 +- src/commands/deploy.ts | 12 +- src/commands/dev.ts | 18 +- src/commands/environment/configure.ts | 20 +- src/commands/environment/destroy.ts | 16 +- src/commands/logs.ts | 21 +- src/commands/status.ts | 25 +- src/commands/test.ts | 8 +- src/commands/validate.ts | 4 +- src/garden.ts | 194 +++---------- src/plugin-context.ts | 265 ++++++++++++++++++ src/plugins/container.ts | 8 +- src/plugins/google/google-cloud-functions.ts | 4 +- src/plugins/kubernetes/ingress.ts | 4 +- src/plugins/kubernetes/namespace.ts | 6 +- src/plugins/kubernetes/status.ts | 6 +- src/plugins/kubernetes/system-global.ts | 10 +- src/plugins/local/local-docker-swarm.ts | 4 +- .../local/local-google-cloud-functions.ts | 9 +- src/task-graph.ts | 4 +- src/tasks/build.ts | 4 +- src/tasks/deploy.ts | 11 +- src/tasks/test.ts | 4 +- src/types/module.ts | 4 +- src/types/plugin.ts | 7 +- src/types/service.ts | 8 +- src/vcs/base.ts | 4 +- src/vcs/git.ts | 2 +- test/helpers.ts | 21 +- test/plugin-context.ts | 109 +++++++ test/src/build-dir.ts | 69 ++--- test/src/commands/call.ts | 12 +- test/src/commands/deploy.ts | 8 +- test/src/commands/environment/destroy.ts | 10 +- test/src/commands/test.ts | 4 +- test/src/commands/validate.ts | 8 +- test/src/{context.ts => garden.ts} | 192 +++---------- test/src/task-graph.ts | 3 +- test/src/tasks/deploy.ts | 13 +- 46 files changed, 651 insertions(+), 573 deletions(-) create mode 100644 src/plugin-context.ts create mode 100644 test/plugin-context.ts rename test/src/{context.ts => garden.ts} (73%) 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 }, )