diff --git a/src/cli.ts b/src/cli.ts index 2cdcc04d69..786076e9de 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -8,6 +8,7 @@ import * as sywac from "sywac" import chalk from "chalk" +import { RunCommand } from "./commands/run" import { enumToArray, shutdown } from "./util" import { merge, intersection, reduce } from "lodash" import { @@ -220,17 +221,18 @@ export class GardenCli { const commands = [ new BuildCommand(), new CallCommand(), + new ConfigCommand(), new DeployCommand(), new DevCommand(), new EnvironmentCommand(), + new LoginCommand(), + new LogoutCommand(), new LogsCommand(), + new PushCommand(), + new RunCommand(), + new StatusCommand(), new TestCommand(), - new ConfigCommand(), new ValidateCommand(), - new StatusCommand(), - new PushCommand(), - new LoginCommand(), - new LogoutCommand(), ] const globalOptions = Object.entries(GLOBAL_OPTIONS) diff --git a/src/commands/environment/index.ts b/src/commands/environment/index.ts index 4207eb27d7..5007dda5cf 100644 --- a/src/commands/environment/index.ts +++ b/src/commands/environment/index.ts @@ -13,7 +13,7 @@ import { EnvironmentDestroyCommand } from "./destroy" export class EnvironmentCommand extends Command { name = "environment" alias = "env" - help = "Outputs the status of your environment" + help = "Manage your runtime environment(s)" subCommands = [ new EnvironmentConfigureCommand(), diff --git a/src/commands/run/index.ts b/src/commands/run/index.ts new file mode 100644 index 0000000000..7b5accb495 --- /dev/null +++ b/src/commands/run/index.ts @@ -0,0 +1,39 @@ +/* + * 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 { safeDump } from "js-yaml" +import { PluginContext } from "../../plugin-context" +import { RuntimeContext } from "../../types/service" +import { highlightYaml } from "../../util" +import { Command } from "../base" +import { RunModuleCommand } from "./module" +import { RunServiceCommand } from "./service" +import { RunTestCommand } from "./test" + +export class RunCommand extends Command { + name = "run" + alias = "r" + help = "Run ad-hoc instances of your modules, services and tests" + + subCommands = [ + new RunModuleCommand(), + new RunServiceCommand(), + new RunTestCommand(), + ] + + async action() { } +} + +export function printRuntimeContext(ctx: PluginContext, runtimeContext: RuntimeContext) { + ctx.log.verbose("-----------------------------------\n") + ctx.log.verbose("Environment variables:") + ctx.log.verbose(highlightYaml(safeDump(runtimeContext.envVars))) + ctx.log.verbose("Dependencies:") + ctx.log.verbose(highlightYaml(safeDump(runtimeContext.dependencies))) + ctx.log.verbose("-----------------------------------\n") +} diff --git a/src/commands/run/module.ts b/src/commands/run/module.ts new file mode 100644 index 0000000000..6f09ff9093 --- /dev/null +++ b/src/commands/run/module.ts @@ -0,0 +1,85 @@ +/* + * 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 { PluginContext } from "../../plugin-context" +import { BuildTask } from "../../tasks/build" +import { RunResult } from "../../types/plugin" +import { BooleanParameter, Command, ParameterValues, StringParameter } from "../base" +import { + uniq, + values, + flatten, +} from "lodash" +import { printRuntimeContext } from "./index" + +export const runArgs = { + module: new StringParameter({ + help: "The name of the module to run", + required: true, + }), + // TODO: make this a variadic arg + command: new StringParameter({ + help: "The command to run in the module", + }), +} + +export const runOpts = { + // TODO: we could provide specific parameters like this by adding commands for specific modules, via plugins + //entrypoint: new StringParameter({ help: "Override default entrypoint in module" }), + interactive: new BooleanParameter({ + help: "Set to false to skip interactive mode and just output the command result", + defaultValue: true, + }), + "force-build": new BooleanParameter({ help: "Force rebuild of module" }), +} + +export type Args = ParameterValues +export type Opts = ParameterValues + +export class RunModuleCommand extends Command { + name = "module" + alias = "m" + help = "Run the specified module" + + arguments = runArgs + options = runOpts + + async action(ctx: PluginContext, args: Args, opts: Opts): Promise { + const name = args.module + const module = await ctx.getModule(name) + + const msg = args.command + ? `Running command ${chalk.white(args.command)} in module ${chalk.white(name)}` + : `Running module ${chalk.white(name)}` + + ctx.log.header({ + emoji: "runner", + command: msg, + }) + + await ctx.configureEnvironment() + + const buildTask = new BuildTask(ctx, module, opts["force-build"]) + await ctx.addTask(buildTask) + await ctx.processTasks() + + const command = args.command ? args.command.split(" ") : [] + + // combine all dependencies for all services in the module, to be sure we have all the context we need + const services = values(await module.getServices()) + const depNames = uniq(flatten(services.map(s => s.config.dependencies))) + const deps = values(await ctx.getServices(depNames)) + + const runtimeContext = await module.prepareRuntimeContext(deps) + + printRuntimeContext(ctx, runtimeContext) + + return ctx.runModule({ module, command, runtimeContext, silent: false, interactive: opts.interactive }) + } +} diff --git a/src/commands/run/service.ts b/src/commands/run/service.ts new file mode 100644 index 0000000000..5496d8b539 --- /dev/null +++ b/src/commands/run/service.ts @@ -0,0 +1,65 @@ +/* + * 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 { PluginContext } from "../../plugin-context" +import { BuildTask } from "../../tasks/build" +import { RunResult } from "../../types/plugin" +import { BooleanParameter, Command, ParameterValues, StringParameter } from "../base" +import { printRuntimeContext } from "./index" + +export const runArgs = { + service: new StringParameter({ + help: "The command to run in the module", + required: true, + }), +} + +export const runOpts = { + interactive: new BooleanParameter({ + help: "Set to false to skip interactive mode and just output the command result", + defaultValue: true, + }), + "force-build": new BooleanParameter({ help: "Force rebuild of module" }), +} + +export type Args = ParameterValues +export type Opts = ParameterValues + +export class RunServiceCommand extends Command { + name = "service" + alias = "s" + help = "Run an ad-hoc instance of the specified service" + + arguments = runArgs + options = runOpts + + async action(ctx: PluginContext, args: Args, opts: Opts): Promise { + const name = args.service + const service = await ctx.getService(name) + const module = service.module + + ctx.log.header({ + emoji: "runner", + command: `Running service ${chalk.cyan(name)} in module ${chalk.cyan(module.name)}`, + }) + + await ctx.configureEnvironment() + + const buildTask = new BuildTask(ctx, module, opts["force-build"]) + await ctx.addTask(buildTask) + await ctx.processTasks() + + const dependencies = await service.getDependencies() + const runtimeContext = await module.prepareRuntimeContext(dependencies) + + printRuntimeContext(ctx, runtimeContext) + + return ctx.runService({ service, runtimeContext, silent: false, interactive: opts.interactive }) + } +} diff --git a/src/commands/run/test.ts b/src/commands/run/test.ts new file mode 100644 index 0000000000..1635738e45 --- /dev/null +++ b/src/commands/run/test.ts @@ -0,0 +1,83 @@ +/* + * 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 { ParameterError } from "../../exceptions" +import { PluginContext } from "../../plugin-context" +import { BuildTask } from "../../tasks/build" +import { RunResult } from "../../types/plugin" +import { BooleanParameter, Command, ParameterValues, StringParameter } from "../base" +import { values } from "lodash" +import { printRuntimeContext } from "./index" + +export const runArgs = { + module: new StringParameter({ + help: "The name of the module to run", + required: true, + }), + test: new StringParameter({ + help: "The name of the test to run in the module", + required: true, + }), +} + +export const runOpts = { + interactive: new BooleanParameter({ + help: "Set to false to skip interactive mode and just output the command result", + defaultValue: true, + }), + "force-build": new BooleanParameter({ help: "Force rebuild of module" }), +} + +export type Args = ParameterValues +export type Opts = ParameterValues + +export class RunTestCommand extends Command { + name = "test" + alias = "t" + help = "Run the specified module test" + + arguments = runArgs + options = runOpts + + async action(ctx: PluginContext, args: Args, opts: Opts): Promise { + const moduleName = args.module + const testName = args.test + const module = await ctx.getModule(moduleName) + const config = await module.getConfig() + + const testSpec = config.test[testName] + + if (!testSpec) { + throw new ParameterError(`Could not find test "${testName}" in module ${moduleName}`, { + moduleName, + testName, + availableTests: Object.keys(config.test), + }) + } + + ctx.log.header({ + emoji: "runner", + command: `Running test ${chalk.cyan(testName)} in module ${chalk.cyan(moduleName)}`, + }) + + await ctx.configureEnvironment() + + const buildTask = new BuildTask(ctx, module, opts["force-build"]) + await ctx.addTask(buildTask) + await ctx.processTasks() + + const interactive = opts.interactive + const deps = await ctx.getServices(testSpec.dependencies) + const runtimeContext = await module.prepareRuntimeContext(values(deps)) + + printRuntimeContext(ctx, runtimeContext) + + return ctx.testModule({ module, interactive, runtimeContext, silent: false, testName, testSpec }) + } +} diff --git a/src/garden.ts b/src/garden.ts index e67f90ca40..d6c8c94d2f 100644 --- a/src/garden.ts +++ b/src/garden.ts @@ -31,6 +31,7 @@ import { } from "./plugins" import { Module, + ModuleConfig, ModuleConfigType, } from "./types/module" import { @@ -98,8 +99,8 @@ export interface ModuleMap { [key: string]: T } -export interface ServiceMap { - [key: string]: Service +export interface ServiceMap { + [key: string]: Service } export interface ActionHandlerMap { @@ -123,7 +124,7 @@ export type ModuleActionMap = { } export interface ContextOpts { - config?: object, + config?: GardenConfig, env?: string, logger?: RootLogNode, plugins?: RegisterPluginParam[], @@ -193,8 +194,9 @@ export class Garden { }) } } else { + config = await loadConfig(projectRoot, projectRoot) const templateContext = await getTemplateContext() - parsedConfig = await resolveTemplateStrings(await loadConfig(projectRoot, projectRoot), templateContext) + parsedConfig = await resolveTemplateStrings(config, templateContext) if (!parsedConfig.project) { throw new ConfigurationError(`Path ${projectRoot} does not contain a project configuration`, { @@ -498,7 +500,7 @@ export class Garden { /** * Returns the module with the specified name. Throws error if it doesn't exist. */ - async getModule(name: string, noScan?: boolean): Promise> { + async getModule(name: string, noScan?: boolean): Promise> { return (await this.getModules([name], noScan))[name] } @@ -542,7 +544,7 @@ export class Garden { /** * Returns the service with the specified name. Throws error if it doesn't exist. */ - async getService(name: string, noScan?: boolean): Promise> { + async getService(name: string, noScan?: boolean): Promise> { return (await this.getServices([name], noScan))[name] } diff --git a/src/plugin-context.ts b/src/plugin-context.ts index 9721bfe26e..e4e98c23f3 100644 --- a/src/plugin-context.ts +++ b/src/plugin-context.ts @@ -21,10 +21,7 @@ import { DeployTask } from "./tasks/deploy" import { PrimitiveMap, } from "./types/common" -import { - Module, - TestSpec, -} from "./types/module" +import { Module } from "./types/module" import { BuildResult, BuildStatus, @@ -41,6 +38,10 @@ import { ModuleActions, PluginActionParamsBase, LoginStatusMap, + RunResult, + TestModuleParams, + RunModuleParams, + RunServiceParams, } from "./types/plugin" import { Service, @@ -55,6 +56,7 @@ import { padEnd, } from "lodash" import { + Omit, registerCleanupFunction, sleep, } from "./util" @@ -73,6 +75,8 @@ export interface ContextStatus { services: { [name: string]: ServiceStatus } } +export type OmitBase = Omit + export type WrappedFromGarden = Pick Promise pushModule: (module: T, logEntry?: LogEntry) => Promise - testModule: ( - module: T, testName: string, testSpec: TestSpec, runtimeContext: RuntimeContext, logEntry?: LogEntry, - ) => Promise + runModule: (params: OmitBase>) => Promise, + testModule: (params: OmitBase>) => Promise getTestResult: ( module: T, testName: string, version: TreeVersion, logEntry?: LogEntry, ) => Promise @@ -117,6 +120,7 @@ export interface PluginContext extends PluginContextGuard, WrappedFromGarden { getServiceLogs: ( service: Service, stream: Stream, tail?: boolean, ) => Promise + runService: (params: OmitBase>) => Promise, getConfig: (key: string[]) => Promise setConfig: (key: string[], value: string) => Promise deleteConfig: (key: string[]) => Promise @@ -145,9 +149,11 @@ export function createPluginContext(garden: Garden): PluginContext { function commonParams(handler): PluginActionParamsBase { const providerName = handler["pluginName"] const providerConfig = projectConfig.providers[handler["pluginName"]] + const env = garden.getEnvironment() return { ctx, + env, provider: { name: providerName, config: providerConfig, @@ -206,32 +212,34 @@ export function createPluginContext(garden: Garden): PluginContext { return handler({ ...commonParams(handler), module, logEntry }) }, - testModule: async ( - module: T, testName: string, testSpec: TestSpec, runtimeContext: RuntimeContext, logEntry?: LogEntry, - ) => { + runModule: async (params: OmitBase>) => { + const handler = garden.getModuleActionHandler("runModule", params.module.type) + return handler({ ...commonParams(handler), ...params }) + }, + + testModule: async (params: OmitBase>) => { + const module = params.module + const defaultHandler = garden.getModuleActionHandler("testModule", "generic") const handler = garden.getModuleActionHandler("testModule", module.type, defaultHandler) - const env = garden.getEnvironment() - return handler({ ...commonParams(handler), module, testName, testSpec, runtimeContext, env, logEntry }) + + return handler({ ...commonParams(handler), ...params }) }, getTestResult: async ( module: T, testName: string, version: TreeVersion, logEntry?: LogEntry, ) => { const handler = garden.getModuleActionHandler("getTestResult", module.type, async () => null) - const env = garden.getEnvironment() - return handler({ ...commonParams(handler), module, testName, version, env, logEntry }) + return handler({ ...commonParams(handler), module, testName, version, logEntry }) }, getEnvironmentStatus: async () => { const handlers = garden.getActionHandlers("getEnvironmentStatus") - const env = garden.getEnvironment() - return Bluebird.props(mapValues(handlers, h => h({ ...commonParams(h), env }))) + return Bluebird.props(mapValues(handlers, h => h({ ...commonParams(h) }))) }, configureEnvironment: async () => { const handlers = garden.getActionHandlers("configureEnvironment") - const env = garden.getEnvironment() const statuses = await ctx.getEnvironmentStatus() @@ -248,7 +256,7 @@ export function createPluginContext(garden: Garden): PluginContext { msg: "Configuring...", }) - await handler({ ...commonParams(handler), status, env, logEntry }) + await handler({ ...commonParams(handler), status, logEntry }) logEntry.setSuccess("Configured") }) @@ -257,14 +265,13 @@ export function createPluginContext(garden: Garden): PluginContext { destroyEnvironment: async () => { const handlers = garden.getActionHandlers("destroyEnvironment") - const env = garden.getEnvironment() - await Bluebird.each(values(handlers), h => h({ ...commonParams(h), env })) + await Bluebird.each(values(handlers), h => h({ ...commonParams(h) })) return ctx.getEnvironmentStatus() }, getServiceStatus: async (service: Service) => { const handler = garden.getModuleActionHandler("getServiceStatus", service.module.type) - return handler({ ...commonParams(handler), service, env: garden.getEnvironment() }) + return handler({ ...commonParams(handler), service }) }, deployService: async ( @@ -276,7 +283,7 @@ export function createPluginContext(garden: Garden): PluginContext { runtimeContext = { envVars: {}, dependencies: {} } } - return handler({ ...commonParams(handler), service, runtimeContext, env: garden.getEnvironment(), logEntry }) + return handler({ ...commonParams(handler), service, runtimeContext, logEntry }) }, getServiceOutputs: async (service: Service) => { @@ -287,24 +294,29 @@ export function createPluginContext(garden: Garden): PluginContext { } catch (err) { return {} } - return handler({ ...commonParams(handler), service, env: garden.getEnvironment() }) + return handler({ ...commonParams(handler), service }) }, execInService: async (service: Service, command: string[]) => { const handler = garden.getModuleActionHandler("execInService", service.module.type) - return handler({ ...commonParams(handler), service, command, env: garden.getEnvironment() }) + return handler({ ...commonParams(handler), service, command }) }, getServiceLogs: async (service: Service, stream: Stream, tail?: boolean) => { const handler = garden.getModuleActionHandler("getServiceLogs", service.module.type, dummyLogStreamer) - return handler({ ...commonParams(handler), service, stream, tail, env: garden.getEnvironment() }) + return handler({ ...commonParams(handler), service, stream, tail }) + }, + + runService: async (params: OmitBase>) => { + const handler = garden.getModuleActionHandler("runService", params.service.module.type) + return handler({ ...commonParams(handler), ...params }) }, getConfig: async (key: string[]) => { garden.validateConfigKey(key) // TODO: allow specifying which provider to use for configs const handler = garden.getActionHandler("getConfig") - const value = await handler({ ...commonParams(handler), key, env: garden.getEnvironment() }) + const value = await handler({ ...commonParams(handler), key }) if (value === null) { throw new NotFoundError(`Could not find config key ${key}`, { key }) @@ -316,13 +328,13 @@ export function createPluginContext(garden: Garden): PluginContext { setConfig: async (key: string[], value: string) => { garden.validateConfigKey(key) const handler = garden.getActionHandler("setConfig") - return handler({ ...commonParams(handler), key, value, env: garden.getEnvironment() }) + return handler({ ...commonParams(handler), key, value }) }, deleteConfig: async (key: string[]) => { garden.validateConfigKey(key) const handler = garden.getActionHandler("deleteConfig") - const res = await handler({ ...commonParams(handler), key, env: garden.getEnvironment() }) + const res = await handler({ ...commonParams(handler), key }) if (!res.found) { throw new NotFoundError(`Could not find config key ${key}`, { key }) diff --git a/src/plugins/container.ts b/src/plugins/container.ts index 0760a831f1..bf24294253 100644 --- a/src/plugins/container.ts +++ b/src/plugins/container.ts @@ -21,6 +21,7 @@ import { GardenPlugin, PushModuleParams, ParseModuleParams, + RunServiceParams, } from "../types/plugin" import { Service } from "../types/service" import { DEFAULT_PORT_PROTOCOL } from "../constants" @@ -312,6 +313,19 @@ export const gardenPlugin = (): GardenPlugin => ({ return { pushed: true } }, + + async runService( + { ctx, service, interactive, runtimeContext, silent, timeout }: RunServiceParams, + ) { + return ctx.runModule({ + module: service.module, + command: service.config.command || [], + interactive, + runtimeContext, + silent, + timeout, + }) + }, }, }, }) diff --git a/src/plugins/generic.ts b/src/plugins/generic.ts index 18c5687058..b131eadd34 100644 --- a/src/plugins/generic.ts +++ b/src/plugins/generic.ts @@ -69,12 +69,14 @@ export const genericPlugin = { async testModule({ module, testName, testSpec }: TestModuleParams): Promise { const startedAt = new Date() + const command = testSpec.command const result = await spawn( - testSpec.command[0], testSpec.command.slice(1), { cwd: module.path, ignoreError: true }, + command[0], command.slice(1), { cwd: module.path, ignoreError: true }, ) return { moduleName: module.name, + command, testName, version: await module.getVersion(), success: result.code === 0, diff --git a/src/plugins/kubernetes/actions.ts b/src/plugins/kubernetes/actions.ts index 3a5ae92f15..187e38f4a5 100644 --- a/src/plugins/kubernetes/actions.ts +++ b/src/plugins/kubernetes/actions.ts @@ -23,6 +23,8 @@ import { GetTestResultParams, LoginStatus, PluginActionParamsBase, + RunModuleParams, + RunResult, SetConfigParams, TestModuleParams, TestResult, @@ -192,51 +194,78 @@ export async function execInService( return { code: res.code, output: res.output } } -export async function testModule( - { ctx, provider, module, testName, testSpec, runtimeContext }: TestModuleParams, -): Promise { +export async function runModule( + { ctx, provider, module, command, interactive, runtimeContext, silent, timeout }: RunModuleParams, +): Promise { const context = provider.config.context - const baseEnv = {} - const envVars = { ...baseEnv, ...runtimeContext.envVars, ...testSpec.variables } - const envArgs = Object.entries(envVars).map(([v, k]) => `--env=${k}=${v}`) + const namespace = await getAppNamespace(ctx, provider) + + const envArgs = Object.entries(runtimeContext.envVars).map(([k, v]) => `--env=${k}=${v}`) - // TODO: use the runModule() method - const testCommandStr = testSpec.command.join(" ") + const commandStr = command.join(" ") const image = await module.getLocalImageId() const version = await module.getVersion() - const kubecmd = [ - "run", `run-${module.name}-${Math.round(new Date().getTime())}`, + const opts = [ `--image=${image}`, "--restart=Never", "--command", - "-i", "--tty", "--rm", + "-i", + "--quiet", + ] + + const kubecmd = [ + "run", `run-${module.name}-${Math.round(new Date().getTime())}`, + ...opts, ...envArgs, "--", "/bin/sh", "-c", - testCommandStr, + commandStr, ] const startedAt = new Date() - const timeout = testSpec.timeout || DEFAULT_TEST_TIMEOUT - const res = await kubectl(context, await getAppNamespace(ctx, provider)).tty(kubecmd, { ignoreError: true, timeout }) + const res = await kubectl(context, namespace).tty(kubecmd, { + ignoreError: true, + silent: !interactive || silent, // shouldn't be silent in interactive mode + timeout, + tty: interactive, + }) - const testResult: TestResult = { + return { moduleName: module.name, - testName, + command, version, success: res.code === 0, startedAt, completedAt: new Date(), output: res.output, } +} + +export async function testModule( + { ctx, provider, env, interactive, module, runtimeContext, silent, testName, testSpec }: + TestModuleParams, +): Promise { + const command = testSpec.command + runtimeContext.envVars = { ...runtimeContext.envVars, ...testSpec.variables } + const timeout = testSpec.timeout || DEFAULT_TEST_TIMEOUT + + const result = await runModule({ ctx, provider, env, module, command, interactive, runtimeContext, silent, timeout }) + + const context = provider.config.context + + // store test result + const testResult: TestResult = { + ...result, + testName, + } const ns = getMetadataNamespace(ctx, provider) - const resultKey = getTestResultKey(module, testName, version) + const resultKey = getTestResultKey(module, testName, result.version) const body = { body: { apiVersion: "v1", diff --git a/src/plugins/kubernetes/index.ts b/src/plugins/kubernetes/index.ts index cf3d87bb1a..fec41c4abe 100644 --- a/src/plugins/kubernetes/index.ts +++ b/src/plugins/kubernetes/index.ts @@ -29,6 +29,7 @@ import { getLoginStatus, login, logout, + runModule, } from "./actions" import { deployService } from "./deployment" import { kubernetesSpecHandlers } from "./specs-module" @@ -75,6 +76,7 @@ export function gardenPlugin({ config }: { config: KubernetesConfig }): GardenPl deployService, getServiceOutputs, execInService, + runModule, testModule, getTestResult, getServiceLogs, diff --git a/src/plugins/kubernetes/kubectl.ts b/src/plugins/kubernetes/kubectl.ts index d8a515b6ec..daafcd7a72 100644 --- a/src/plugins/kubernetes/kubectl.ts +++ b/src/plugins/kubernetes/kubectl.ts @@ -6,17 +6,20 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +import chalk from "chalk" import { ChildProcess, spawn } from "child_process" import { extend } from "lodash" import { spawnPty } from "../../util" import { RuntimeError } from "../../exceptions" import { getLogger } from "../../logger" +import hasAnsi = require("has-ansi") export interface KubectlParams { data?: Buffer, ignoreError?: boolean, silent?: boolean, timeout?: number, + tty?: boolean, } export interface KubectlOutput { @@ -58,7 +61,8 @@ export class Kubectl { proc.stdout.on("data", (s) => { if (!silent) { - logger.info(s.toString()) + const str = s.toString() + logger.info(hasAnsi(str) ? str : chalk.white(str)) } out.output += s out.stdout! += s @@ -66,7 +70,8 @@ export class Kubectl { proc.stderr.on("data", (s) => { if (!silent) { - logger.error(s.toString()) + const str = s.toString() + logger.info(hasAnsi(str) ? str : chalk.white(str)) } out.output += s out.stderr! += s @@ -116,11 +121,8 @@ export class Kubectl { return JSON.parse(result.output) } - async tty( - args: string[], - { silent = true, ignoreError = false, timeout = KUBECTL_DEFAULT_TIMEOUT } = {}, - ): Promise { - return spawnPty("kubectl", this.prepareArgs(args), { silent, ignoreError, timeout }) + async tty(args: string[], opts: KubectlParams = {}): Promise { + return spawnPty("kubectl", this.prepareArgs(args), opts) } spawn(args: string[]): ChildProcess { diff --git a/src/plugins/kubernetes/system.ts b/src/plugins/kubernetes/system.ts index da9f824c8d..935ab5c026 100644 --- a/src/plugins/kubernetes/system.ts +++ b/src/plugins/kubernetes/system.ts @@ -26,8 +26,16 @@ export async function getSystemGarden(provider: KubernetesProvider): Promise extends Task { const dependencies = values(await this.ctx.getServices(this.testSpec.dependencies)) const runtimeContext = await this.module.prepareRuntimeContext(dependencies) - const result = await this.ctx.testModule(this.module, this.testName, this.testSpec, runtimeContext) + const result = await this.ctx.testModule({ + interactive: false, + module: this.module, + runtimeContext, + silent: true, + testName: this.testName, + testSpec: this.testSpec, + }) if (result.success) { entry.setSuccess({ msg: chalk.green(`Success`), append: true }) diff --git a/src/types/common.ts b/src/types/common.ts index 2d1e41cf4a..34890db79b 100644 --- a/src/types/common.ts +++ b/src/types/common.ts @@ -25,6 +25,7 @@ export const enumToArray = Enum => ( export const joiPrimitive = () => Joi.alternatives().try(Joi.number(), Joi.string(), Joi.boolean()) export const identifierRegex = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/ +export const envVarRegex = /^[a-zA-Z_][a-zA-Z0-9_]*$/ export const joiIdentifier = () => Joi .string().regex(identifierRegex) @@ -39,6 +40,14 @@ export const joiVariables = () => Joi .object().pattern(/[\w\d]+/i, joiPrimitive()) .default(() => ({}), "{}") +export const joiEnvVarName = () => Joi + .string().regex(envVarRegex) + .description("may contain letters and numbers, cannot start with a number") + +export const joiEnvVars = () => Joi + .object().pattern(envVarRegex, joiPrimitive()) + .default(() => ({}), "{}") + export interface Environment { name: string namespace: string diff --git a/src/types/config.ts b/src/types/config.ts index 2f054dde20..145f35b5f0 100644 --- a/src/types/config.ts +++ b/src/types/config.ts @@ -22,14 +22,16 @@ export interface GardenConfig { version: string dirname: string path: string - module: ModuleConfig - project: ProjectConfig + module?: ModuleConfig + project?: ProjectConfig } export const configSchema = Joi.object() .keys({ // TODO: should this be called apiVersion? version: Joi.string().default("0").only("0"), + dirname: Joi.string(), + path: Joi.string(), module: baseModuleSchema, project: projectSchema, }) diff --git a/src/types/module.ts b/src/types/module.ts index 55726758de..37e4f1aeb0 100644 --- a/src/types/module.ts +++ b/src/types/module.ts @@ -13,6 +13,7 @@ import { DeployTask } from "../tasks/deploy" import { TestTask } from "../tasks/test" import { identifierRegex, + joiEnvVars, joiIdentifier, joiPrimitive, joiVariables, @@ -172,7 +173,7 @@ export class Module { async getDeployTasks( { force = false, forceBuild = false }: { force?: boolean, forceBuild?: boolean }, - ): Promise>>[]> { + ): Promise[]> { const services = await this.getServices() const module = this @@ -196,17 +197,30 @@ export class Module { return tasks } - async prepareRuntimeContext(dependencies: Service[]): Promise { + async prepareRuntimeContext(dependencies: Service[], extraEnvVars: PrimitiveMap = {}): Promise { const { versionString } = await this.getVersion() const envVars = { GARDEN_VERSION: versionString, } - const deps = {} - for (const key in this.ctx.config.variables) { - envVars[key] = this.ctx.config.variables[key] + validate(extraEnvVars, joiEnvVars(), { context: `environment variables for module ${this.name}` }) + + for (const [envVarName, value] of Object.entries(extraEnvVars)) { + if (envVarName.startsWith("GARDEN")) { + throw new ConfigurationError(`Environment variable name cannot start with "GARDEN"`, { + envVarName, + }) + } + envVars[envVarName] = value + } + + for (const [key, value] of Object.entries(this.ctx.config.variables)) { + const envVarName = `GARDEN_VARIABLES_${key.replace(/-/g, "_").toUpperCase()}` + envVars[envVarName] = value } + const deps = {} + for (const dep of dependencies) { const depContext = deps[dep.name] = { version: versionString, diff --git a/src/types/plugin.ts b/src/types/plugin.ts index 16e66b4fa4..4378f06279 100644 --- a/src/types/plugin.ts +++ b/src/types/plugin.ts @@ -32,6 +32,7 @@ export interface Provider { export interface PluginActionParamsBase { ctx: PluginContext + env: Environment provider: Provider logEntry?: LogEntry } @@ -40,32 +41,24 @@ export interface ParseModuleParams extends PluginActi moduleConfig: T["_ConfigType"] } -export interface GetEnvironmentStatusParams extends PluginActionParamsBase { - env: Environment, -} +export interface GetEnvironmentStatusParams extends PluginActionParamsBase { } export interface ConfigureEnvironmentParams extends PluginActionParamsBase { - env: Environment status: EnvironmentStatus } -export interface DestroyEnvironmentParams extends PluginActionParamsBase { - env: Environment, -} +export interface DestroyEnvironmentParams extends PluginActionParamsBase { } export interface GetConfigParams extends PluginActionParamsBase { - env: Environment, key: string[] } export interface SetConfigParams extends PluginActionParamsBase { - env: Environment, key: string[] value: Primitive } export interface DeleteConfigParams extends PluginActionParamsBase { - env: Environment, key: string[] } @@ -96,56 +89,69 @@ export interface PushModuleParams extends PluginActio module: T } +export interface RunModuleParams extends PluginActionParamsBase { + module: T + command: string[] + interactive: boolean + runtimeContext: RuntimeContext + silent: boolean + timeout?: number +} + export interface TestModuleParams extends PluginActionParamsBase { module: T + interactive: boolean + runtimeContext: RuntimeContext + silent: boolean testName: string testSpec: TestSpec - runtimeContext: RuntimeContext - env: Environment } export interface GetTestResultParams extends PluginActionParamsBase { module: T testName: string version: TreeVersion - env: Environment } export interface GetServiceStatusParams extends PluginActionParamsBase { service: Service, - env: Environment, } export interface DeployServiceParams extends PluginActionParamsBase { service: Service, runtimeContext: RuntimeContext, - env: Environment, } export interface GetServiceOutputsParams extends PluginActionParamsBase { service: Service, - env: Environment, } export interface ExecInServiceParams extends PluginActionParamsBase { service: Service, - env: Environment, command: string[], } export interface GetServiceLogsParams extends PluginActionParamsBase { service: Service, - env: Environment, stream: Stream, tail?: boolean, startTime?: Date, } +export interface RunServiceParams extends PluginActionParamsBase { + service: Service + interactive: boolean + runtimeContext: RuntimeContext + silent: boolean + timeout?: number +} + export interface ModuleActionParams { parseModule: ParseModuleParams getModuleBuildStatus: GetModuleBuildStatusParams buildModule: BuildModuleParams pushModule: PushModuleParams + runModule: RunModuleParams testModule: TestModuleParams getTestResult: GetTestResultParams @@ -154,6 +160,7 @@ export interface ModuleActionParams { getServiceOutputs: GetServiceOutputsParams execInService: ExecInServiceParams getServiceLogs: GetServiceLogsParams + runService: RunServiceParams } export interface BuildResult { @@ -169,9 +176,9 @@ export interface PushResult { message?: string } -export interface TestResult { +export interface RunResult { moduleName: string - testName: string + command: string[] version: TreeVersion success: boolean startedAt: Moment | Date @@ -179,6 +186,10 @@ export interface TestResult { output: string } +export interface TestResult extends RunResult { + testName: string +} + export interface BuildStatus { ready: boolean } @@ -236,6 +247,7 @@ export interface ModuleActionOutputs { getModuleBuildStatus: Promise buildModule: Promise pushModule: Promise + runModule: Promise testModule: Promise getTestResult: Promise @@ -244,6 +256,7 @@ export interface ModuleActionOutputs { getServiceOutputs: Promise execInService: Promise getServiceLogs: Promise + runService: Promise } export type PluginActions = { @@ -280,6 +293,7 @@ const moduleActionDescriptions: { [P in ModuleActionName]: PluginActionDescripti getModuleBuildStatus: {}, buildModule: {}, pushModule: {}, + runModule: {}, testModule: {}, getTestResult: {}, @@ -288,6 +302,7 @@ const moduleActionDescriptions: { [P in ModuleActionName]: PluginActionDescripti getServiceOutputs: {}, execInService: {}, getServiceLogs: {}, + runService: {}, } export const pluginActionNames: PluginActionName[] = Object.keys(pluginActionDescriptions) diff --git a/src/types/project.ts b/src/types/project.ts index 6de8282cbb..5a5d8c48c5 100644 --- a/src/types/project.ts +++ b/src/types/project.ts @@ -35,7 +35,6 @@ export interface EnvironmentConfig { } export interface ProjectConfig { - version: string name: string defaultEnvironment: string global: EnvironmentConfig @@ -56,7 +55,6 @@ const defaultGlobal = { } export const projectSchema = Joi.object().keys({ - version: Joi.string().default("0").only("0"), name: joiIdentifier().required(), defaultEnvironment: Joi.string().default("", ""), global: environmentSchema.default(() => defaultGlobal, JSON.stringify(defaultGlobal)), diff --git a/src/types/service.ts b/src/types/service.ts index 63091975c6..917b071a93 100644 --- a/src/types/service.ts +++ b/src/types/service.ts @@ -53,7 +53,7 @@ export type RuntimeContext = { }, } -export class Service { +export class Service { constructor( protected ctx: PluginContext, public module: M, public name: string, public config: M["services"][string], diff --git a/src/util.ts b/src/util.ts index f53cfd1787..2ba749b90b 100644 --- a/src/util.ts +++ b/src/util.ts @@ -28,6 +28,7 @@ import { isArray, isPlainObject, extend, mapValues, pickBy } from "lodash" import highlight from "cli-highlight" import chalk from "chalk" import { FancyConsoleWriter } from "./logger/writers" +import hasAnsi = require("has-ansi") // shim to allow async generator functions (Symbol).asyncIterator = (Symbol).asyncIterator || Symbol.for("Symbol.asyncIterator") @@ -36,6 +37,7 @@ export type HookCallback = (callback?: () => void) => void const exitHookNames: string[] = [] // For debugging/testing/inspection purposes +export type Omit = Pick> export type Nullable = { [P in keyof T]: T[P] | null } export function shutdown(code) { @@ -241,12 +243,14 @@ export function spawnPty( } proc.on("data", (output) => { + const str = output.toString() + if (bufferOutput) { - result.output += output.toString() + result.output += str } if (!silent) { - process.stdout.write(output) + process.stdout.write(hasAnsi(str) ? str : chalk.white(str)) } }) diff --git a/test/helpers.ts b/test/helpers.ts index 40be6ff65d..e29d697bb9 100644 --- a/test/helpers.ts +++ b/test/helpers.ts @@ -1,5 +1,7 @@ import * as td from "testdouble" import { resolve } from "path" +import { PluginContext } from "../src/plugin-context" +import { ContainerModule } from "../src/plugins/container" import { TaskResults } from "../src/task-graph" import { DeleteConfigParams, @@ -10,13 +12,25 @@ import { PluginFactory, SetConfigParams, ModuleActions, + RunModuleParams, + RunServiceParams, } from "../src/types/plugin" import { Garden } from "../src/garden" -import { Module } from "../src/types/module" -import { expect } from "chai" +import { + Module, + ModuleConfig, +} from "../src/types/module" import { mapValues } from "lodash" +import { TreeVersion } from "../src/vcs/base" export const dataDir = resolve(__dirname, "data") +export const testNow = new Date() +export const testModuleVersionString = "1234512345" +export const testModuleVersion: TreeVersion = { + versionString: testModuleVersionString, + latestCommit: testModuleVersionString, + dirtyTimestamp: null, +} export function getDataDir(name: string) { return resolve(dataDir, name) @@ -34,6 +48,10 @@ export const projectRootA = getDataDir("test-project-a") class TestModule extends Module { type = "test" + + async getVersion() { + return testModuleVersion + } } export const testPlugin: PluginFactory = (): GardenPlugin => { @@ -66,7 +84,29 @@ export const testPlugin: PluginFactory = (): GardenPlugin => { async parseModule({ ctx, moduleConfig }: ParseModuleParams) { return new Module(ctx, moduleConfig) }, - + async runModule(params: RunModuleParams) { + const version = await params.module.getVersion() + + return { + moduleName: params.module.name, + command: params.command, + completedAt: testNow, + output: "OK", + version, + startedAt: testNow, + success: true, + } + }, + async runService({ ctx, service, interactive, runtimeContext, silent, timeout}: RunServiceParams) { + return ctx.runModule({ + module: service.module, + command: [service.name], + interactive, + runtimeContext, + silent, + timeout, + }) + }, async getServiceStatus() { return {} }, async deployService() { return {} }, }, @@ -84,19 +124,21 @@ export const testPluginB: PluginFactory = (params) => { } testPluginB.pluginName = "test-plugin-b" -export const makeTestModule = (ctx, name = "test") => { - return new TestModule(ctx, { - type: "test", - name, - path: "bla", - allowPush: false, - variables: {}, - build: { dependencies: [] }, - services: { - testService: { dependencies: [] }, - }, - test: {}, - }) +export const defaultModuleConfig: ModuleConfig = { + type: "test", + name: "test", + path: "bla", + allowPush: false, + variables: {}, + build: { dependencies: [] }, + services: { + testService: { dependencies: [] }, + }, + test: {}, +} + +export const makeTestModule = (ctx: PluginContext, params: Partial = {}) => { + return new TestModule(ctx, { ...defaultModuleConfig, ...params }) } export const makeTestGarden = async (projectRoot: string, extraPlugins: PluginFactory[] = []) => { diff --git a/test/src/commands/run/index.ts b/test/src/commands/run/index.ts new file mode 100644 index 0000000000..16e614b7ad --- /dev/null +++ b/test/src/commands/run/index.ts @@ -0,0 +1,16 @@ +import { RunCommand } from "../../../../src/commands/run" +import { expect } from "chai" + +describe("RunCommand", () => { + it("should do nothing", async () => { + const cmd = new RunCommand() + const res = await cmd.action() + expect(res).to.be.undefined + }) + + it("should contain a set of subcommands", () => { + const cmd = new RunCommand() + const subcommandNames = new Set(cmd.subCommands.map(s => s.name)) + expect(subcommandNames).to.eql(new Set(["module", "service", "test"])) + }) +}) diff --git a/test/src/commands/run/module.ts b/test/src/commands/run/module.ts new file mode 100644 index 0000000000..a2646da72a --- /dev/null +++ b/test/src/commands/run/module.ts @@ -0,0 +1,69 @@ +import { RunModuleCommand } from "../../../../src/commands/run/module" +import { RunResult } from "../../../../src/types/plugin" +import { + makeTestGardenA, + makeTestModule, + testModuleVersion, + testNow, +} from "../../../helpers" +import { expect } from "chai" + +describe("RunModuleCommand", () => { + // TODO: test optional flags + + it("should run a module without a command param", async () => { + const garden = await makeTestGardenA() + const ctx = garden.pluginContext + + garden.addModule(makeTestModule(ctx, { + name: "run-test", + })) + + const cmd = new RunModuleCommand() + const res = await cmd.action( + ctx, + { module: "run-test", command: undefined }, + { interactive: false, "force-build": false }, + ) + + const expected: RunResult = { + moduleName: "run-test", + command: [], + completedAt: testNow, + output: "OK", + version: testModuleVersion, + startedAt: testNow, + success: true, + } + + expect(res).to.eql(expected) + }) + + it("should run a module with a command param", async () => { + const garden = await makeTestGardenA() + const ctx = garden.pluginContext + + garden.addModule(makeTestModule(ctx, { + name: "run-test", + })) + + const cmd = new RunModuleCommand() + const res = await cmd.action( + ctx, + { module: "run-test", command: "my command" }, + { interactive: false, "force-build": false }, + ) + + const expected: RunResult = { + moduleName: "run-test", + command: ["my", "command"], + completedAt: testNow, + output: "OK", + version: testModuleVersion, + startedAt: testNow, + success: true, + } + + expect(res).to.eql(expected) + }) +}) diff --git a/test/src/commands/run/service.ts b/test/src/commands/run/service.ts new file mode 100644 index 0000000000..e7727a0cbe --- /dev/null +++ b/test/src/commands/run/service.ts @@ -0,0 +1,41 @@ +import { RunServiceCommand } from "../../../../src/commands/run/service" +import { RunResult } from "../../../../src/types/plugin" +import { + makeTestGardenA, + makeTestModule, + testModuleVersion, + testNow, +} from "../../../helpers" +import { expect } from "chai" + +describe("RunServiceCommand", () => { + // TODO: test optional flags + + it("should run a service", async () => { + const garden = await makeTestGardenA() + const ctx = garden.pluginContext + + garden.addModule(makeTestModule(ctx, { + name: "run-test", + })) + + const cmd = new RunServiceCommand() + const res = await cmd.action( + ctx, + { service: "testService" }, + { interactive: false, "force-build": false }, + ) + + const expected: RunResult = { + moduleName: "run-test", + command: ["testService"], + completedAt: testNow, + output: "OK", + version: testModuleVersion, + startedAt: testNow, + success: true, + } + + expect(res).to.eql(expected) + }) +}) diff --git a/test/src/garden.ts b/test/src/garden.ts index 2f7a82e881..5249d3f3c4 100644 --- a/test/src/garden.ts +++ b/test/src/garden.ts @@ -4,6 +4,7 @@ import { expect } from "chai" import { dataDir, expectError, + makeTestContextA, makeTestGarden, makeTestGardenA, makeTestModule, @@ -196,17 +197,17 @@ describe("Garden", () => { describe("getService", () => { it("should return the specified service", async () => { - const ctx = await makeTestGardenA() - const service = await ctx.getService("service-b") + const garden = await makeTestGardenA() + const service = await garden.getService("service-b") expect(service.name).to.equal("service-b") }) it("should throw if service is missing", async () => { - const ctx = await makeTestGardenA() + const garden = await makeTestGardenA() try { - await ctx.getServices(["bla"]) + await garden.getServices(["bla"]) } catch (err) { expect(err.type).to.equal("parameter") return @@ -220,36 +221,37 @@ 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 makeTestGardenA() - await ctx.scanModules() + const garden = await makeTestGardenA() + await garden.scanModules() - const modules = await ctx.getModules(undefined, true) + const modules = await garden.getModules(undefined, true) expect(Object.keys(modules)).to.eql(["module-a", "module-b", "module-c"]) }) }) describe("addModule", () => { it("should add the given module and its services to the context", async () => { - const ctx = await makeTestGardenA() + const garden = await makeTestGardenA() - const testModule = makeTestModule(ctx) - await ctx.addModule(testModule) + const testModule = makeTestModule(garden.pluginContext) + await garden.addModule(testModule) - const modules = await ctx.getModules(undefined, true) + const modules = await garden.getModules(undefined, true) expect(Object.keys(modules)).to.eql(["test"]) - const services = await ctx.getServices(undefined, true) + const services = await garden.getServices(undefined, true) expect(Object.keys(services)).to.eql(["testService"]) }) it("should throw when adding module twice without force parameter", async () => { - const ctx = await makeTestGardenA() + const garden = await makeTestGardenA() + const ctx = garden.pluginContext const testModule = makeTestModule(ctx) - await ctx.addModule(testModule) + await garden.addModule(testModule) try { - await ctx.addModule(testModule) + await garden.addModule(testModule) } catch (err) { expect(err.type).to.equal("configuration") return @@ -259,25 +261,27 @@ describe("Garden", () => { }) it("should allow adding module multiple times with force parameter", async () => { - const ctx = await makeTestGardenA() + const garden = await makeTestGardenA() + const ctx = garden.pluginContext const testModule = makeTestModule(ctx) - await ctx.addModule(testModule) - await ctx.addModule(testModule, true) + await garden.addModule(testModule) + await garden.addModule(testModule, true) - const modules = await ctx.getModules(undefined, true) + const modules = await garden.getModules(undefined, true) expect(Object.keys(modules)).to.eql(["test"]) }) it("should throw if a service is added twice without force parameter", async () => { - const ctx = await makeTestGardenA() + const garden = await makeTestGardenA() + const ctx = garden.pluginContext const testModule = makeTestModule(ctx) - const testModuleB = makeTestModule(ctx, "test-b") - await ctx.addModule(testModule) + const testModuleB = makeTestModule(ctx, { name: "test-b" }) + await garden.addModule(testModule) try { - await ctx.addModule(testModuleB) + await garden.addModule(testModuleB) } catch (err) { expect(err.type).to.equal("configuration") return @@ -287,12 +291,13 @@ describe("Garden", () => { }) it("should allow adding service multiple times with force parameter", async () => { - const ctx = await makeTestGardenA() + const garden = await makeTestGardenA() + const ctx = garden.pluginContext const testModule = makeTestModule(ctx) - const testModuleB = makeTestModule(ctx, "test-b") - await ctx.addModule(testModule) - await ctx.addModule(testModuleB, true) + const testModuleB = makeTestModule(ctx, { name: "test-b" }) + await garden.addModule(testModule) + await garden.addModule(testModuleB, true) const services = await ctx.getServices(undefined, true) expect(Object.keys(services)).to.eql(["testService"]) @@ -301,18 +306,18 @@ describe("Garden", () => { describe("resolveModule", () => { it("should return named module", async () => { - const ctx = await makeTestGardenA() - await ctx.scanModules() + const garden = await makeTestGardenA() + await garden.scanModules() - const module = await ctx.resolveModule("module-a") + const module = await garden.resolveModule("module-a") expect(module!.name).to.equal("module-a") }) it("should throw if named module is requested and not available", async () => { - const ctx = await makeTestGardenA() + const garden = await makeTestGardenA() try { - await ctx.resolveModule("module-a") + await garden.resolveModule("module-a") } catch (err) { expect(err.type).to.equal("configuration") return @@ -322,17 +327,17 @@ describe("Garden", () => { }) it("should resolve module by absolute path", async () => { - const ctx = await makeTestGardenA() + const garden = await makeTestGardenA() const path = join(projectRootA, "module-a") - const module = await ctx.resolveModule(path) + const module = await garden.resolveModule(path) expect(module!.name).to.equal("module-a") }) it("should resolve module by relative path to project root", async () => { - const ctx = await makeTestGardenA() + const garden = await makeTestGardenA() - const module = await ctx.resolveModule("./module-a") + const module = await garden.resolveModule("./module-a") expect(module!.name).to.equal("module-a") }) }) diff --git a/test/src/plugins/container.ts b/test/src/plugins/container.ts index 9cee93b568..ebee00103e 100644 --- a/test/src/plugins/container.ts +++ b/test/src/plugins/container.ts @@ -7,6 +7,7 @@ import { ContainerModuleConfig, gardenPlugin, } from "../../../src/plugins/container" +import { Environment } from "../../../src/types/common" import { dataDir, expectError, @@ -22,10 +23,12 @@ describe("container", () => { let garden: Garden let ctx: PluginContext + let env: Environment beforeEach(async () => { garden = await makeTestGarden(projectRoot, [gardenPlugin]) ctx = garden.pluginContext + env = garden.getEnvironment() td.replace(garden.buildDir, "syncDependencyProducts", () => null) }) @@ -35,7 +38,7 @@ describe("container", () => { const provider = { name: "container", config: {} } async function getTestModule(moduleConfig: ContainerModuleConfig) { - return parseModule!({ ctx, provider, moduleConfig }) + return parseModule!({ ctx, env, provider, moduleConfig }) } describe("ContainerModule", () => { @@ -186,7 +189,7 @@ describe("container", () => { variables: {}, } - await parseModule({ ctx, provider, moduleConfig }) + await parseModule({ ctx, env, provider, moduleConfig }) }) it("should fail with invalid port in endpoint spec", async () => { @@ -225,7 +228,7 @@ describe("container", () => { } await expectError( - () => parseModule({ ctx, provider, moduleConfig }), + () => parseModule({ ctx, env, provider, moduleConfig }), "configuration", ) }) @@ -267,7 +270,7 @@ describe("container", () => { } await expectError( - () => parseModule({ ctx, provider, moduleConfig }), + () => parseModule({ ctx, env, provider, moduleConfig }), "configuration", ) }) @@ -306,7 +309,7 @@ describe("container", () => { } await expectError( - () => parseModule({ ctx, provider, moduleConfig }), + () => parseModule({ ctx, env, provider, moduleConfig }), "configuration", ) }) diff --git a/test/src/types/config.ts b/test/src/types/config.ts index 667fab10bc..5faf9d3dff 100644 --- a/test/src/types/config.ts +++ b/test/src/types/config.ts @@ -12,7 +12,6 @@ describe("loadConfig", () => { const parsed = await loadConfig(projectPathA, projectPathA) expect(parsed.project).to.eql({ - version: "0", name: "test-project-a", defaultEnvironment: "local", global: { diff --git a/test/src/types/module.ts b/test/src/types/module.ts index 678fef7e4b..cbc02488f9 100644 --- a/test/src/types/module.ts +++ b/test/src/types/module.ts @@ -12,9 +12,9 @@ describe("Module", () => { it("should create a module instance with the given config", async () => { const ctx = await makeTestContextA() const config = await loadConfig(ctx.projectRoot, modulePathA) - const module = new Module(ctx, config.module) + const module = new Module(ctx, config.module!) - expect(module.name).to.equal(config.module.name) + expect(module.name).to.equal(config.module!.name) expect(omitUndefined(await module.getConfig())).to.eql(config.module) }) @@ -26,9 +26,9 @@ describe("Module", () => { const modulePath = resolve(ctx.projectRoot, "module-a") const config = await loadConfig(ctx.projectRoot, modulePath) - const module = new Module(ctx, config.module) + const module = new Module(ctx, config.module!) - expect(module.name).to.equal(config.module.name) + expect(module.name).to.equal(config.module!.name) expect(await module.getConfig()).to.eql({ allowPush: true, build: { command: "echo OK", dependencies: [] },