diff --git a/src/build-dir.ts b/src/build-dir.ts index d1152f38fa..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 { GardenContext } from "./context" // Lazily construct a directory of modules inside which all build steps are performed. @@ -30,21 +29,19 @@ const buildDirRelPath = join(GARDEN_DIR_NAME, "build") export class BuildDir { buildDirPath: string - private ctx: GardenContext - constructor(ctx: GardenContext) { - this.ctx = ctx - this.buildDirPath = join(ctx.projectRoot, buildDirRelPath) + constructor(private projectRoot: string) { + this.buildDirPath = join(projectRoot, buildDirRelPath) } - // Synchronous, so it can run in GardenContext's constructor. + // Synchronous, so it can run in Garden's constructor. init() { ensureDirSync(this.buildDirPath) } 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 f7db6245a9..163f5c7417 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -17,10 +17,11 @@ import { ParameterValues, Parameter, StringParameter, + EnvironmentOption, } from "./commands/base" import { ValidateCommand } from "./commands/validate" import { InternalError, PluginError } from "./exceptions" -import { GardenContext } from "./context" +import { Garden } from "./garden" import { FileWriter } from "./logger/writers" import { getLogger, RootLogNode } from "./logger" import { resolve } from "path" @@ -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 GardenContext.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 3657862ee3..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 { GardenContext } from "../context" +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 690781f150..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 { GardenContext } from "../context" 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 e4e1290baf..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 { GardenContext } from "../context" +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: GardenContext, 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 4db43924ac..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 { GardenContext } from "../../context" +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: GardenContext, 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 df0d91b651..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 { GardenContext } from "../../context" +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: GardenContext, 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 c6b2e5dfa6..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 { GardenContext } from "../../context" +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: GardenContext, 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 b374e687dc..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 { GardenContext } from "../context" +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: GardenContext, 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: GardenContext, + ctx: PluginContext, services: Service[], force: boolean, forceBuild: boolean, diff --git a/src/commands/dev.ts b/src/commands/dev.ts index 0410ed82cd..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 { GardenContext } from "../context" +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: GardenContext, _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 7fa631d841..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 { GardenContext } from "../../context" +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: GardenContext, _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 882618ea0d..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 { GardenContext } from "../../context" 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: GardenContext, _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: GardenContext, 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 e046ceb53a..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 { GardenContext } from "../context" +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: GardenContext, 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 19b1c8bac5..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 { GardenContext } from "../context" +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: GardenContext, _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 ce26b10fa1..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 { GardenContext } from "../context" +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: GardenContext, 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 61efca00dd..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 { GardenContext } from "../context" export class ValidateCommand extends Command { name = "validate" help = "Check your garden configuration for errors" - async action(ctx: GardenContext) { + async action(ctx: PluginContext) { ctx.log.header({ emoji: "heavy_check_mark", command: "validate" }) diff --git a/src/context.ts b/src/garden.ts similarity index 69% rename from src/context.ts rename to src/garden.ts index d0049e7fa2..0c5d02566e 100644 --- a/src/context.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[], } @@ -58,15 +59,14 @@ const builtinPlugins: PluginFactory[] = [ () => new GenericModuleHandler(), ] -export class GardenContext { +export class Garden { public buildDir: BuildDir public readonly log: RootLogNode 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 GardenContext { 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 GardenContext { 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 GardenContext { }) } - const ctx = new GardenContext(projectRoot, projectConfig, logger) + const ctx = new Garden(projectRoot, projectConfig, env, logger) // Load configured plugins plugins = builtinPlugins.concat(plugins) @@ -181,6 +184,10 @@ export class GardenContext { } } + async clearBuilds() { + return this.buildDir.clear() + } + async addTask(task: Task) { await this.taskGraph.addTask(task) } @@ -220,7 +227,7 @@ export class GardenContext { 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 GardenContext { ) } - 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 GardenContext { 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 GardenContext { 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 GardenContext { } } - //=========================================================================== - //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 GardenContext { /** * 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/logger/index.ts b/src/logger/index.ts index 35b0eb0114..2ebb6de7a3 100644 --- a/src/logger/index.ts +++ b/src/logger/index.ts @@ -248,7 +248,7 @@ export class RootLogNode extends LogNode { } public header( - { command, emoji, level = LogLevel.verbose }: { command: string, emoji?: string, level?: LogLevel }, + { command, emoji, level = LogLevel.info }: { command: string, emoji?: string, level?: LogLevel }, ): LogEntry { const msg = combine([ [chalk.bold.magenta(command)], @@ -260,7 +260,7 @@ export class RootLogNode extends LogNode { } public finish( - { showDuration = true, level = LogLevel.verbose }: { showDuration?: boolean, level?: LogLevel } = {}, + { showDuration = true, level = LogLevel.info }: { showDuration?: boolean, level?: LogLevel } = {}, ): LogEntry { const msg = combine([ [`\n${nodeEmoji.get("sparkles")} Finished`], 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 e3cb81c904..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 { GardenContext } from "../context" 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: GardenContext, 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: GardenContext, 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 c4c8059e5e..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 { GardenContext } from "../../context" 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 + * + * 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 * as K8s from "kubernetes-client" + +import { DEFAULT_CONTEXT } from "./kubectl" + +const cachedParams = {} + +function getParams(namespace?: string) { + let params = cachedParams[namespace || ""] + + if (!params) { + const config = K8s.config.loadKubeconfig() + params = K8s.config.fromKubeconfig(config, DEFAULT_CONTEXT) + + params.promises = true + params.namespace = namespace + + cachedParams[namespace || ""] = params + } + + return params +} + +export function coreApi(namespace?: string): any { + return new K8s.Core(getParams(namespace)) +} + +export function extensionsApi(namespace?: string): any { + return new K8s.Extensions(getParams(namespace)) +} + +export async function apiPostOrPut(api: any, name: string, body: object) { + try { + await api.post(body) + } catch (err) { + if (err.code === 409) { + await api(name).put(body) + } else { + throw err + } + } +} + +export async function apiGetOrNull(api: any, name: string) { + try { + return await api(name).get() + } catch (err) { + if (err.code === 404) { + return null + } else { + throw err + } + } +} diff --git a/src/plugins/kubernetes/deployment.ts b/src/plugins/kubernetes/deployment.ts index e0b1508651..d8a1a29541 100644 --- a/src/plugins/kubernetes/deployment.ts +++ b/src/plugins/kubernetes/deployment.ts @@ -6,9 +6,28 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import { ContainerService, ContainerServiceConfig } from "../container" +import { DeployServiceParams } from "../../types/plugin" +import { + ContainerModule, + ContainerService, + ContainerServiceConfig, +} from "../container" import { toPairs, extend } from "lodash" -import { ServiceContext } from "../../types/service" +import { + ServiceContext, + ServiceStatus, +} from "../../types/service" +import { + createIngress, + getServiceHostname, +} from "./ingress" +import { apply } from "./kubectl" +import { getAppNamespace } from "./namespace" +import { createServices } from "./service" +import { + checkDeploymentStatus, + waitForDeployment, +} from "./status" const DEFAULT_CPU_REQUEST = 0.01 const DEFAULT_CPU_LIMIT = 0.5 @@ -21,6 +40,33 @@ interface KubeEnvVar { valueFrom?: { fieldRef: { fieldPath: string } } } +export async function deployService( + { ctx, service, env, serviceContext, exposePorts = false, logEntry }: DeployServiceParams, +): Promise { + const namespace = getAppNamespace(ctx, env) + + const deployment = await createDeployment(service, serviceContext, exposePorts) + await apply(deployment, { namespace }) + + // TODO: automatically clean up Services and Ingresses if they should no longer exist + + const kubeservices = await createServices(service, exposePorts) + + for (let kubeservice of kubeservices) { + await apply(kubeservice, { namespace }) + } + + const ingress = await createIngress(service, getServiceHostname(ctx, service)) + + if (ingress !== null) { + await apply(ingress, { namespace }) + } + + await waitForDeployment({ ctx, service, logEntry, env }) + + return checkDeploymentStatus({ ctx, service, env }) +} + export async function createDeployment( service: ContainerService, serviceContext: ServiceContext, exposePorts: boolean, ) { diff --git a/src/plugins/kubernetes/index.ts b/src/plugins/kubernetes/index.ts index 9fd37476ee..a1b9490033 100644 --- a/src/plugins/kubernetes/index.ts +++ b/src/plugins/kubernetes/index.ts @@ -6,9 +6,6 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import * as Docker from "dockerode" -import { Memoize } from "typescript-memoize" -import * as K8s from "kubernetes-client" import { DeploymentError, NotFoundError } from "../../exceptions" import { ConfigureEnvironmentParams, DeleteConfigParams, @@ -22,31 +19,38 @@ import { TestModuleParams, TestResult, } from "../../types/plugin" import { - ContainerModule, ContainerService, ServiceEndpointSpec, + ContainerModule, } from "../container" import { values, every, map, extend } from "lodash" -import { Environment } from "../../types/common" -import { deserializeKeys, serializeKeys, sleep, splitFirst } from "../../util" -import { Service, ServiceProtocol, ServiceStatus } from "../../types/service" -import { join } from "path" -import { createServices } from "./service" -import { createIngress } from "./ingress" -import { createDeployment } from "./deployment" -import { DEFAULT_CONTEXT, Kubectl, KUBECTL_DEFAULT_TIMEOUT } from "./kubectl" -import { DEFAULT_TEST_TIMEOUT, STATIC_DIR } from "../../constants" -import { LogEntry } from "../../logger" -import { GardenContext } from "../../context" +import { deserializeKeys, serializeKeys, splitFirst } from "../../util" +import { ServiceStatus } from "../../types/service" +import { + apiGetOrNull, + apiPostOrPut, + coreApi, +} from "./api" +import { + createNamespace, + getAppNamespace, + getMetadataNamespace, +} from "./namespace" +import { + deployService, +} from "./deployment" +import { + kubectl, +} from "./kubectl" +import { DEFAULT_TEST_TIMEOUT } from "../../constants" import * as split from "split" import moment = require("moment") -import { EntryStyle, LogSymbolType } from "../../logger/types" - -const GARDEN_SYSTEM_NAMESPACE = "garden-system" - -const ingressControllerModulePath = join(STATIC_DIR, "garden-ingress-controller") -const defaultBackendModulePath = join(STATIC_DIR, "garden-default-backend") -const dashboardModulePath = join(STATIC_DIR, "garden-dashboard") -const dashboardSpecPath = join(dashboardModulePath, "dashboard.yml") -const localIngressPort = 32000 +import { EntryStyle } from "../../logger/types" +import { + checkDeploymentStatus, +} from "./status" +import { + configureGlobalSystem, + getGlobalSystemStatus, +} from "./system-global" export class KubernetesProvider implements Plugin { name = "kubernetes" @@ -61,7 +65,7 @@ export class KubernetesProvider implements Plugin { async getEnvironmentStatus({ ctx, env }: GetEnvironmentStatusParams) { try { // TODO: use API instead of kubectl (I just couldn't find which API call to make) - await this.kubectl().call(["version"]) + await kubectl().call(["version"]) } catch (err) { // TODO: catch error properly if (err.output) { @@ -70,52 +74,26 @@ export class KubernetesProvider implements Plugin { throw err } - const gardenEnv = this.getSystemEnv(env) - - const ingressControllerService = await this.getIngressControllerService(ctx) - const defaultBackendService = await this.getDefaultBackendService(ctx) - const dashboardService = await this.getDashboardService(ctx) - - const ingressControllerStatus = await this.checkDeploymentStatus({ - ctx, - service: ingressControllerService, - env: gardenEnv, - }) - const defaultBackendStatus = await this.checkDeploymentStatus({ - ctx, - service: defaultBackendService, - env: gardenEnv, - }) - const dashboardStatus = await this.checkDeploymentStatus({ - ctx, - service: dashboardService, - env: gardenEnv, - }) + const globalSystemStatus = await getGlobalSystemStatus(ctx, env) const statusDetail = { systemNamespaceReady: false, namespaceReady: false, metadataNamespaceReady: false, - dashboardReady: dashboardStatus.state === "ready", - ingressControllerReady: ingressControllerStatus.state === "ready", - defaultBackendReady: defaultBackendStatus.state === "ready", + ...globalSystemStatus, } - const metadataNamespace = this.getMetadataNamespaceName(ctx) - const namespacesStatus = await this.coreApi().namespaces().get() + const metadataNamespace = getMetadataNamespace(ctx) + const namespacesStatus = await coreApi().namespaces().get() for (const n of namespacesStatus.items) { - if (n.metadata.name === this.getNamespaceName(ctx, env) && n.status.phase === "Active") { + if (n.metadata.name === getAppNamespace(ctx, env) && n.status.phase === "Active") { statusDetail.namespaceReady = true } if (n.metadata.name === metadataNamespace && n.status.phase === "Active") { statusDetail.metadataNamespaceReady = true } - - if (n.metadata.name === GARDEN_SYSTEM_NAMESPACE && n.status.phase === "Active") { - statusDetail.systemNamespaceReady = true - } } let configured = every(values(statusDetail)) @@ -126,105 +104,44 @@ export class KubernetesProvider implements Plugin { } } - async configureEnvironment({ ctx, env, logEntry }: ConfigureEnvironmentParams) { + async configureEnvironment(params: ConfigureEnvironmentParams) { // TODO: use Helm 3 when it's released instead of this custom/manual stuff + const { ctx, env, logEntry } = params const status = await this.getEnvironmentStatus({ ctx, env }) if (status.configured) { return } - if (!status.detail.systemNamespaceReady) { - logEntry && logEntry.setState({ section: "kubernetes", msg: `Creating garden system namespace` }) - await this.coreApi().namespaces.post({ - body: { - apiVersion: "v1", - kind: "Namespace", - metadata: { - name: GARDEN_SYSTEM_NAMESPACE, - annotations: { - "garden.io/generated": "true", - }, - }, - }, - }) - } + await configureGlobalSystem(params, status) if (!status.detail.namespaceReady) { - const ns = this.getNamespaceName(ctx, env) + const ns = getAppNamespace(ctx, env) logEntry && logEntry.setState({ section: "kubernetes", msg: `Creating namespace ${ns}` }) - await this.coreApi().namespaces.post({ - body: { - apiVersion: "v1", - kind: "Namespace", - metadata: { - name: ns, - annotations: { - "garden.io/generated": "true", - }, - }, - }, - }) + await createNamespace(ns) } if (!status.detail.metadataNamespaceReady) { - const ns = this.getMetadataNamespaceName(ctx) + const ns = getMetadataNamespace(ctx) logEntry && logEntry.setState({ section: "kubernetes", msg: `Creating namespace ${ns}` }) - await this.coreApi().namespaces.post({ - body: { - apiVersion: "v1", - kind: "Namespace", - metadata: { - name: ns, - annotations: { - "garden.io/generated": "true", - }, - }, - }, - }) - } - - if (!status.detail.dashboardReady) { - logEntry && logEntry.setState({ section: "kubernetes", msg: `Configuring dashboard` }) - // TODO: deploy this as a service - await this.kubectl(GARDEN_SYSTEM_NAMESPACE).call(["apply", "-f", dashboardSpecPath]) - } - - if (!status.detail.ingressControllerReady) { - logEntry && logEntry.setState({ section: "kubernetes", msg: `Configuring ingress controller` }) - const gardenEnv = this.getSystemEnv(env) - await this.deployService({ - ctx, - service: await this.getDefaultBackendService(ctx), - serviceContext: { envVars: {}, dependencies: {} }, - env: gardenEnv, - logEntry, - }) - await this.deployService({ - ctx, - service: await this.getIngressControllerService(ctx), - serviceContext: { envVars: {}, dependencies: {} }, - env: gardenEnv, - exposePorts: true, - logEntry, - }) + await createNamespace(ns) } } async getServiceStatus({ ctx, service }: GetServiceStatusParams): Promise { // TODO: hash and compare all the configuration files (otherwise internal changes don't get deployed) - return await this.checkDeploymentStatus({ ctx, service }) + return await checkDeploymentStatus({ ctx, service }) } async destroyEnvironment({ ctx, env }: DestroyEnvironmentParams) { - const namespace = this.getNamespaceName(ctx, env) + const namespace = getAppNamespace(ctx, env) const entry = ctx.log.info({ section: "kubernetes", msg: `Deleting namespace ${namespace}`, entryStyle: EntryStyle.activity, }) try { - await this.coreApi().namespace(namespace).delete(namespace) + await coreApi().namespace(namespace).delete(namespace) entry.setSuccess("Finished") } catch (err) { entry.setError(err.message) @@ -232,31 +149,8 @@ export class KubernetesProvider implements Plugin { } } - async deployService( - { ctx, service, env, serviceContext, exposePorts = false, logEntry }: DeployServiceParams, - ) { - const namespace = this.getNamespaceName(ctx, env) - - const deployment = await createDeployment(service, serviceContext, exposePorts) - await this.apply(deployment, { namespace }) - - // TODO: automatically clean up Services and Ingresses if they should no longer exist - - const kubeservices = await createServices(service, exposePorts) - - for (let kubeservice of kubeservices) { - await this.apply(kubeservice, { namespace }) - } - - const ingress = await createIngress(service, this.getServiceHostname(ctx, service)) - - if (ingress !== null) { - await this.apply(ingress, { namespace }) - } - - await this.waitForDeployment({ ctx, service, logEntry, env }) - - return this.getServiceStatus({ ctx, service, env }) + async deployService(params: DeployServiceParams) { + return deployService(params) } async getServiceOutputs({ service }: GetServiceOutputsParams) { @@ -267,7 +161,7 @@ export class KubernetesProvider implements Plugin { async execInService({ ctx, service, env, command }: ExecInServiceParams) { const status = await this.getServiceStatus({ ctx, service, env }) - const namespace = this.getNamespaceName(ctx, env) + const namespace = getAppNamespace(ctx, env) // TODO: this check should probably live outside of the plugin if (!status.state || status.state !== "ready") { @@ -278,7 +172,7 @@ export class KubernetesProvider implements Plugin { } // get a running pod - let res = await this.coreApi(namespace).namespaces.pods.get({ + let res = await coreApi(namespace).namespaces.pods.get({ qs: { labelSelector: `service=${service.name}`, }, @@ -293,7 +187,7 @@ export class KubernetesProvider implements Plugin { } // exec in the pod via kubectl - res = await this.kubectl(namespace).tty(["exec", "-it", pod.metadata.name, "--", ...command]) + res = await kubectl(namespace).tty(["exec", "-it", pod.metadata.name, "--", ...command]) return { code: res.code, output: res.output } } @@ -327,7 +221,7 @@ export class KubernetesProvider implements Plugin { const startedAt = new Date() const timeout = testSpec.timeout || DEFAULT_TEST_TIMEOUT - const res = await this.kubectl(this.getNamespaceName(ctx)).tty(kubecmd, { ignoreError: true, timeout }) + const res = await kubectl(getAppNamespace(ctx)).tty(kubecmd, { ignoreError: true, timeout }) const testResult: TestResult = { version, @@ -337,7 +231,7 @@ export class KubernetesProvider implements Plugin { output: res.output, } - const ns = this.getMetadataNamespaceName(ctx) + const ns = getMetadataNamespace(ctx) const resultKey = `test-result--${module.name}--${version}` const body = { body: { @@ -354,15 +248,15 @@ export class KubernetesProvider implements Plugin { }, } - await apiPostOrPut(this.coreApi(ns).namespaces.configmaps, resultKey, body) + await apiPostOrPut(coreApi(ns).namespaces.configmaps, resultKey, body) return testResult } async getTestResult({ ctx, module, version }: GetTestResultParams) { - const ns = this.getMetadataNamespaceName(ctx) + const ns = getMetadataNamespace(ctx) const resultKey = getTestResultKey(module, version) - const res = await apiGetOrNull(this.coreApi(ns).namespaces.configmaps, resultKey) + const res = await apiGetOrNull(coreApi(ns).namespaces.configmaps, resultKey) return res && deserializeKeys(res.data) } @@ -375,7 +269,7 @@ export class KubernetesProvider implements Plugin { kubectlArgs.push("--follow") } - const proc = this.kubectl(this.getNamespaceName(ctx)).spawn(kubectlArgs) + const proc = kubectl(getAppNamespace(ctx)).spawn(kubectlArgs) proc.stdout .pipe(split()) @@ -400,14 +294,14 @@ export class KubernetesProvider implements Plugin { } async getConfig({ ctx, key }: GetConfigParams) { - const ns = this.getMetadataNamespaceName(ctx) - const res = await apiGetOrNull(this.coreApi(ns).namespaces.secrets, key.join(".")) + const ns = getMetadataNamespace(ctx) + const res = await apiGetOrNull(coreApi(ns).namespaces.secrets, key.join(".")) return res && Buffer.from(res.data.value, "base64").toString() } async setConfig({ ctx, key, value }: SetConfigParams) { // we store configuration in a separate metadata namespace, so that configs aren't cleared when wiping the namespace - const ns = this.getMetadataNamespaceName(ctx) + const ns = getMetadataNamespace(ctx) const body = { body: { apiVersion: "v1", @@ -423,13 +317,13 @@ export class KubernetesProvider implements Plugin { }, } - await apiPostOrPut(this.coreApi(ns).namespaces.secrets, key.join("."), body) + await apiPostOrPut(coreApi(ns).namespaces.secrets, key.join("."), body) } async deleteConfig({ ctx, key }: DeleteConfigParams) { - const ns = this.getMetadataNamespaceName(ctx) + const ns = getMetadataNamespace(ctx) try { - await this.coreApi(ns).namespaces.secrets(key.join(".")).delete() + await coreApi(ns).namespaces.secrets(key.join(".")).delete() } catch (err) { if (err.code === 404) { return { found: false } @@ -441,357 +335,8 @@ export class KubernetesProvider implements Plugin { } //endregion - - //=========================================================================== - //region Internal helpers - //=========================================================================== - - private getNamespaceName(ctx: GardenContext, env?: Environment) { - const currentEnv = env || ctx.getEnvironment() - if (currentEnv.namespace === GARDEN_SYSTEM_NAMESPACE) { - return currentEnv.namespace - } - return `garden--${ctx.projectName}--${currentEnv.namespace}` - } - - private getMetadataNamespaceName(ctx: GardenContext) { - const env = ctx.getEnvironment() - return `garden-metadata--${ctx.projectName}--${env.namespace}` - } - - private async getIngressControllerService(ctx: GardenContext) { - const module = await ctx.resolveModule(ingressControllerModulePath) - - return ContainerService.factory(ctx, module, "ingress-controller") - } - - private async getDefaultBackendService(ctx: GardenContext) { - const module = await ctx.resolveModule(defaultBackendModulePath) - - return ContainerService.factory(ctx, module, "default-backend") - } - - private async getDashboardService(ctx: GardenContext) { - // TODO: implement raw kubernetes module load this module the same way as the ones above - const module = new ContainerModule(ctx, { - version: "0", - name: "garden-dashboard", - type: "container", - path: dashboardModulePath, - services: { - "kubernetes-dashboard": { - daemon: false, - dependencies: [], - endpoints: [], - ports: {}, - volumes: [], - }, - }, - variables: {}, - build: { dependencies: [] }, - test: {}, - }) - - return Service.factory(ctx, module, "kubernetes-dashboard") - } - - protected getProjectHostname() { - // TODO: for remote Garden environments, this will depend on the configured project - // TODO: make configurable for the generic kubernetes plugin - return "local.app.garden" - } - - protected getServiceHostname(ctx: GardenContext, service: ContainerService) { - return `${service.name}.${ctx.projectName}.${this.getProjectHostname()}` - } - - async checkDeploymentStatus( - { ctx, service, resourceVersion, env }: - { ctx: GardenContext, service: ContainerService, resourceVersion?: number, env?: Environment }, - ): Promise { - const type = service.config.daemon ? "daemonsets" : "deployments" - const hostname = this.getServiceHostname(ctx, service) - - const namespace = this.getNamespaceName(ctx, env) - - const endpoints = service.config.endpoints.map((e: ServiceEndpointSpec) => { - // TODO: this should be HTTPS, once we've set up TLS termination at the ingress controller level - const protocol: ServiceProtocol = "http" - - return { - protocol, - hostname, - port: localIngressPort, - url: `${protocol}://${hostname}:${localIngressPort}`, - paths: e.paths, - } - }) - - const out: ServiceStatus = { - endpoints, - runningReplicas: 0, - detail: { resourceVersion }, - } - - let statusRes - let status - - try { - statusRes = await this.extensionsApi(namespace).namespaces[type](service.name).get() - } catch (err) { - if (err.code === 404) { - // service is not running - return out - } else { - throw err - } - } - - status = statusRes.status - - if (!resourceVersion) { - resourceVersion = out.detail.resourceVersion = parseInt(statusRes.metadata.resourceVersion, 10) - } - - out.version = statusRes.metadata.annotations["garden.io/version"] - - // TODO: try to come up with something more efficient. may need to wait for newer k8s version. - // note: the resourceVersion parameter does not appear to work... - const eventsRes = await this.coreApi(namespace).namespaces.events.get() - - // const eventsRes = await this.kubeApi( - // "GET", - // [ - // "apis", apiSection, "v1beta1", - // "watch", - // "namespaces", namespace, - // type + "s", service.fullName, - // ], - // { resourceVersion, watch: "false" }, - // ) - - // look for errors and warnings in the events for the service, abort if we find any - const events = eventsRes.items - - for (let event of events) { - const eventVersion = parseInt(event.metadata.resourceVersion, 10) - - if ( - eventVersion <= resourceVersion || - (!event.metadata.name.startsWith(service.name + ".") && !event.metadata.name.startsWith(service.name + "-")) - ) { - continue - } - - if (eventVersion > resourceVersion) { - out.detail.resourceVersion = eventVersion - } - - if (event.type === "Warning" || event.type === "Error") { - if (event.reason === "Unhealthy") { - // still waiting on readiness probe - continue - } - out.state = "unhealthy" - out.lastError = `${event.reason} - ${event.message}` - return out - } - - let message = event.message - - if (event.reason === event.reason.toUpperCase()) { - // some events like ingress events are formatted this way - message = `${event.reason} ${message}` - } - - if (message) { - out.detail.lastMessage = message - } - } - - // See `https://github.com/kubernetes/kubernetes/blob/master/pkg/kubectl/rollout_status.go` for a reference - // for this logic. - let available = 0 - out.state = "ready" - let statusMsg = "" - - if (statusRes.metadata.generation > status.observedGeneration) { - statusMsg = `Waiting for spec update to be observed...` - out.state = "deploying" - } else if (service.config.daemon) { - const desired = status.desiredNumberScheduled || 0 - const updated = status.updatedNumberScheduled || 0 - available = status.numberAvailable || 0 - - if (updated < desired) { - statusMsg = `${updated} out of ${desired} new pods updated...` - out.state = "deploying" - } else if (available < desired) { - statusMsg = `${available} out of ${desired} updated pods available...` - out.state = "deploying" - } - } else { - const desired = 1 // TODO: service.count[env.name] || 1 - const updated = status.updatedReplicas || 0 - const replicas = status.replicas || 0 - available = status.availableReplicas || 0 - - if (updated < desired) { - statusMsg = `Waiting for rollout: ${updated} out of ${desired} new replicas updated...` - out.state = "deploying" - } else if (replicas > updated) { - statusMsg = `Waiting for rollout: ${replicas - updated} old replicas pending termination...` - out.state = "deploying" - } else if (available < updated) { - statusMsg = `Waiting for rollout: ${available} out of ${updated} updated replicas available...` - out.state = "deploying" - } - } - - out.runningReplicas = available - out.lastMessage = statusMsg - - return out - } - - async waitForDeployment( - { ctx, service, logEntry, env }: - { ctx: GardenContext, 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 - let resourceVersion - let lastMessage - let lastDetailMessage - const startTime = new Date().getTime() - - logEntry && ctx.log.verbose({ - symbol: LogSymbolType.info, - section: service.name, - msg: `Waiting for service to be ready...`, - }) - - while (true) { - await sleep(2000 + 1000 * loops) - - const status = await this.checkDeploymentStatus({ ctx, service, resourceVersion, env }) - - if (status.lastError) { - throw new DeploymentError(`Error deploying ${service.name}: ${status.lastError}`, { - serviceName: service.name, - status, - }) - } - - if (status.detail.lastMessage && status.detail.lastMessage !== lastDetailMessage) { - lastDetailMessage = status.detail.lastMessage - logEntry && ctx.log.verbose({ - symbol: LogSymbolType.info, - section: service.name, - msg: status.detail.lastMessage, - }) - } - - if (status.lastMessage && status.lastMessage !== lastMessage) { - lastMessage = status.lastMessage - logEntry && ctx.log.verbose({ - symbol: LogSymbolType.info, - section: service.name, - msg: status.lastMessage, - }) - } - - if (status.state === "ready") { - break - } - - resourceVersion = status.detail.resourceVersion - - const now = new Date().getTime() - - if (now - startTime > KUBECTL_DEFAULT_TIMEOUT * 1000) { - throw new Error(`Timed out waiting for ${service.name} to deploy`) - } - } - - logEntry && ctx.log.verbose({ symbol: LogSymbolType.info, section: service.name, msg: `Service deployed` }) - } - - // sadly the TS definitions are no good for this one - @Memoize() - protected coreApi(namespace?: string): any { - const config = K8s.config.loadKubeconfig() - const params: any = K8s.config.fromKubeconfig(config, DEFAULT_CONTEXT) - - params.promises = true - params.namespace = namespace - - return new K8s.Core(params) - } - - @Memoize() - protected extensionsApi(namespace?: string): any { - const config = K8s.config.loadKubeconfig() - const params: any = K8s.config.fromKubeconfig(config, DEFAULT_CONTEXT) - - params.promises = true - params.namespace = namespace - - return new K8s.Extensions(params) - } - - @Memoize() - public kubectl(namespace?: string) { - return new Kubectl({ context: DEFAULT_CONTEXT, namespace }) - } - - @Memoize() - protected getDocker() { - return new Docker() - } - - protected async apply(obj: any, { force = false, namespace }: { force?: boolean, namespace?: string } = {}) { - const data = Buffer.from(JSON.stringify(obj)) - - let args = ["apply"] - force && args.push("--force") - args.push("-f") - args.push("-") - - await this.kubectl(namespace).call(args, { data }) - } - - private getSystemEnv(env: Environment): Environment { - return { name: env.name, namespace: GARDEN_SYSTEM_NAMESPACE, config: { providers: {} } } - } - - //endregion } function getTestResultKey(module: ContainerModule, version: string) { return `test-result--${module.name}--${version}` } - -async function apiPostOrPut(api: any, name: string, body: object) { - try { - await api.post(body) - } catch (err) { - if (err.code === 409) { - await api(name).put(body) - } else { - throw err - } - } -} - -async function apiGetOrNull(api: any, name: string) { - try { - return await api(name).get() - } catch (err) { - if (err.code === 404) { - return null - } else { - throw err - } - } -} diff --git a/src/plugins/kubernetes/ingress.ts b/src/plugins/kubernetes/ingress.ts index 4dd25f9cee..f0e69b3a08 100644 --- a/src/plugins/kubernetes/ingress.ts +++ b/src/plugins/kubernetes/ingress.ts @@ -6,6 +6,7 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +import { PluginContext } from "../../plugin-context" import { ContainerService } from "../container" export async function createIngress(service: ContainerService, externalHostname: string) { @@ -52,3 +53,12 @@ export async function createIngress(service: ContainerService, externalHostname: }, } } + +export function getProjectHostname() { + // TODO: make configurable + return "local.app.garden" +} + +export function getServiceHostname(ctx: PluginContext, service: ContainerService) { + return `${service.name}.${ctx.projectName}.${getProjectHostname()}` +} diff --git a/src/plugins/kubernetes/kubectl.ts b/src/plugins/kubernetes/kubectl.ts index 7affd2e5af..274bfdb647 100644 --- a/src/plugins/kubernetes/kubectl.ts +++ b/src/plugins/kubernetes/kubectl.ts @@ -144,3 +144,18 @@ export class Kubectl { return ops.concat(args) } } + +export function kubectl(namespace?: string) { + return new Kubectl({ context: DEFAULT_CONTEXT, namespace }) +} + +export async function apply(obj: any, { force = false, namespace }: { force?: boolean, namespace?: string } = {}) { + const data = Buffer.from(JSON.stringify(obj)) + + let args = ["apply"] + force && args.push("--force") + args.push("-f") + args.push("-") + + await kubectl(namespace).call(args, { data }) +} diff --git a/src/plugins/kubernetes/modules.ts b/src/plugins/kubernetes/modules.ts new file mode 100644 index 0000000000..a58ab75a9a --- /dev/null +++ b/src/plugins/kubernetes/modules.ts @@ -0,0 +1,30 @@ +/* + * 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 * as Joi from "joi" +import { identifierRegex } from "../../types/common" +import { + baseServiceSchema, + Module, + ModuleConfig, +} from "../../types/module" +import { ServiceConfig } from "../../types/service" + +export interface KubernetesRawServiceConfig extends ServiceConfig { + specs: string[] +} + +export interface KubernetesRawModuleConfig extends ModuleConfig { } + +export const k8sRawServicesSchema = Joi.object() + .pattern(identifierRegex, baseServiceSchema.keys({ + specs: Joi.array().items(Joi.string()).required(), + })) + .default(() => ({}), "{}") + +export class KubernetesRawModule extends Module { } diff --git a/src/plugins/kubernetes/namespace.ts b/src/plugins/kubernetes/namespace.ts new file mode 100644 index 0000000000..fdc0f71bea --- /dev/null +++ b/src/plugins/kubernetes/namespace.ts @@ -0,0 +1,48 @@ +/* + * 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 { PluginContext } from "../../plugin-context" +import { Environment } from "../../types/common" +import { + apiGetOrNull, + coreApi, +} from "./api" +import { GARDEN_GLOBAL_SYSTEM_NAMESPACE } from "./system-global" + +export async function namespaceReady(namespace: string) { + const ns = await apiGetOrNull(coreApi().namespaces, namespace) + return ns && ns.status.phase === "Active" +} + +export async function createNamespace(namespace: string) { + await coreApi().namespaces.post({ + body: { + apiVersion: "v1", + kind: "Namespace", + metadata: { + name: namespace, + annotations: { + "garden.io/generated": "true", + }, + }, + }, + }) +} + +export function getAppNamespace(ctx: PluginContext, env?: Environment) { + const currentEnv = env || ctx.getEnvironment() + if (currentEnv.namespace === GARDEN_GLOBAL_SYSTEM_NAMESPACE) { + return currentEnv.namespace + } + return `garden--${ctx.projectName}--${currentEnv.namespace}` +} + +export function getMetadataNamespace(ctx: PluginContext) { + const env = ctx.getEnvironment() + return `garden-metadata--${ctx.projectName}--${env.namespace}` +} diff --git a/src/plugins/kubernetes/status.ts b/src/plugins/kubernetes/status.ts new file mode 100644 index 0000000000..3411334531 --- /dev/null +++ b/src/plugins/kubernetes/status.ts @@ -0,0 +1,241 @@ +/* + * 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 { DeploymentError } from "../../exceptions" +import { LogEntry } from "../../logger" +import { LogSymbolType } from "../../logger/types" +import { PluginContext } from "../../plugin-context" +import { Environment } from "../../types/common" +import { + ServiceProtocol, + ServiceStatus, +} from "../../types/service" +import { sleep } from "../../util" +import { + ContainerService, + ServiceEndpointSpec, +} from "../container" +import { + coreApi, + extensionsApi, +} from "./api" +import { getServiceHostname } from "./ingress" +import { KUBECTL_DEFAULT_TIMEOUT } from "./kubectl" +import { getAppNamespace } from "./namespace" +import { localIngressPort } from "./system-global" + +export async function checkDeploymentStatus( + { ctx, service, resourceVersion, env }: + { ctx: PluginContext, service: ContainerService, resourceVersion?: number, env?: Environment }, +): Promise { + const type = service.config.daemon ? "daemonsets" : "deployments" + const hostname = getServiceHostname(ctx, service) + const namespace = getAppNamespace(ctx, env) + + const endpoints = service.config.endpoints.map((e: ServiceEndpointSpec) => { + // TODO: this should be HTTPS, once we've set up TLS termination at the ingress controller level + const protocol: ServiceProtocol = "http" + + return { + protocol, + hostname, + port: localIngressPort, + url: `${protocol}://${hostname}:${localIngressPort}`, + paths: e.paths, + } + }) + + const out: ServiceStatus = { + endpoints, + runningReplicas: 0, + detail: { resourceVersion }, + } + + let statusRes + let status + + try { + statusRes = await extensionsApi(namespace).namespaces[type](service.name).get() + } catch (err) { + if (err.code === 404) { + // service is not running + return out + } else { + throw err + } + } + + status = statusRes.status + + if (!resourceVersion) { + resourceVersion = out.detail.resourceVersion = parseInt(statusRes.metadata.resourceVersion, 10) + } + + out.version = statusRes.metadata.annotations["garden.io/version"] + + // TODO: try to come up with something more efficient. may need to wait for newer k8s version. + // note: the resourceVersion parameter does not appear to work... + const eventsRes = await coreApi(namespace).namespaces.events.get() + + // const eventsRes = await this.kubeApi( + // "GET", + // [ + // "apis", apiSection, "v1beta1", + // "watch", + // "namespaces", namespace, + // type + "s", service.fullName, + // ], + // { resourceVersion, watch: "false" }, + // ) + + // look for errors and warnings in the events for the service, abort if we find any + const events = eventsRes.items + + for (let event of events) { + const eventVersion = parseInt(event.metadata.resourceVersion, 10) + + if ( + eventVersion <= resourceVersion || + (!event.metadata.name.startsWith(service.name + ".") && !event.metadata.name.startsWith(service.name + "-")) + ) { + continue + } + + if (eventVersion > resourceVersion) { + out.detail.resourceVersion = eventVersion + } + + if (event.type === "Warning" || event.type === "Error") { + if (event.reason === "Unhealthy") { + // still waiting on readiness probe + continue + } + out.state = "unhealthy" + out.lastError = `${event.reason} - ${event.message}` + return out + } + + let message = event.message + + if (event.reason === event.reason.toUpperCase()) { + // some events like ingress events are formatted this way + message = `${event.reason} ${message}` + } + + if (message) { + out.detail.lastMessage = message + } + } + + // See `https://github.com/kubernetes/kubernetes/blob/master/pkg/kubectl/rollout_status.go` for a reference + // for this logic. + let available = 0 + out.state = "ready" + let statusMsg = "" + + if (statusRes.metadata.generation > status.observedGeneration) { + statusMsg = `Waiting for spec update to be observed...` + out.state = "deploying" + } else if (service.config.daemon) { + const desired = status.desiredNumberScheduled || 0 + const updated = status.updatedNumberScheduled || 0 + available = status.numberAvailable || 0 + + if (updated < desired) { + statusMsg = `${updated} out of ${desired} new pods updated...` + out.state = "deploying" + } else if (available < desired) { + statusMsg = `${available} out of ${desired} updated pods available...` + out.state = "deploying" + } + } else { + const desired = 1 // TODO: service.count[env.name] || 1 + const updated = status.updatedReplicas || 0 + const replicas = status.replicas || 0 + available = status.availableReplicas || 0 + + if (updated < desired) { + statusMsg = `Waiting for rollout: ${updated} out of ${desired} new replicas updated...` + out.state = "deploying" + } else if (replicas > updated) { + statusMsg = `Waiting for rollout: ${replicas - updated} old replicas pending termination...` + out.state = "deploying" + } else if (available < updated) { + statusMsg = `Waiting for rollout: ${available} out of ${updated} updated replicas available...` + out.state = "deploying" + } + } + + out.runningReplicas = available + out.lastMessage = statusMsg + + return out +} + +export async function waitForDeployment( + { ctx, service, logEntry, env }: + { 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 + let resourceVersion = undefined + let lastMessage + let lastDetailMessage + const startTime = new Date().getTime() + + logEntry && ctx.log.verbose({ + symbol: LogSymbolType.info, + section: service.name, + msg: `Waiting for service to be ready...`, + }) + + while (true) { + await sleep(2000 + 1000 * loops) + + const status = await checkDeploymentStatus({ ctx, service, resourceVersion, env }) + + if (status.lastError) { + throw new DeploymentError(`Error deploying ${service.name}: ${status.lastError}`, { + serviceName: service.name, + status, + }) + } + + if (status.detail.lastMessage && (!lastDetailMessage || status.detail.lastMessage !== lastDetailMessage)) { + lastDetailMessage = status.detail.lastMessage + logEntry && ctx.log.verbose({ + symbol: LogSymbolType.info, + section: service.name, + msg: status.detail.lastMessage, + }) + } + + if (status.lastMessage && (!lastMessage && status.lastMessage !== lastMessage)) { + lastMessage = status.lastMessage + logEntry && ctx.log.verbose({ + symbol: LogSymbolType.info, + section: service.name, + msg: status.lastMessage, + }) + } + + if (status.state === "ready") { + break + } + + resourceVersion = status.detail.resourceVersion + + const now = new Date().getTime() + + if (now - startTime > KUBECTL_DEFAULT_TIMEOUT * 1000) { + throw new Error(`Timed out waiting for ${service.name} to deploy`) + } + } + + logEntry && ctx.log.verbose({ symbol: LogSymbolType.info, section: service.name, msg: `Service deployed` }) +} diff --git a/src/plugins/kubernetes/system-global.ts b/src/plugins/kubernetes/system-global.ts new file mode 100644 index 0000000000..a9afeaeb92 --- /dev/null +++ b/src/plugins/kubernetes/system-global.ts @@ -0,0 +1,156 @@ +/* + * 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 { join } from "path" +import { STATIC_DIR } from "../../constants" +import { PluginContext } from "../../plugin-context" +import { Environment } from "../../types/common" +import { + ConfigureEnvironmentParams, + EnvironmentStatus, +} from "../../types/plugin" +import { Service } from "../../types/service" +import { + ContainerModule, + ContainerService, +} from "../container" +import { deployService } from "./deployment" +import { kubectl } from "./kubectl" +import { checkDeploymentStatus } from "./status" +import { + createNamespace, + namespaceReady, +} from "./namespace" + +export const GARDEN_GLOBAL_SYSTEM_NAMESPACE = "garden-system" + +const globalSystemProjectPath = join(STATIC_DIR, "kubernetes", "system-global") +const ingressControllerModulePath = join(globalSystemProjectPath, "ingress-controller") +const defaultBackendModulePath = join(globalSystemProjectPath, "default-backend") +const dashboardModulePath = join(globalSystemProjectPath, "kubernetes-dashboard") +const dashboardSpecPath = join(dashboardModulePath, "dashboard.yml") + +export const localIngressPort = 32000 + +export async function getGlobalSystemStatus(ctx: PluginContext, env: Environment) { + const gardenEnv = getSystemEnv(env) + + const systemNamespaceReady = namespaceReady(GARDEN_GLOBAL_SYSTEM_NAMESPACE) + + if (!systemNamespaceReady) { + return { + systemNamespaceReady, + dashboardReady: false, + ingressControllerReady: false, + defaultBackendReady: false, + } + } + + const ingressControllerService = await getIngressControllerService(ctx) + const defaultBackendService = await getDefaultBackendService(ctx) + const dashboardService = await getDashboardService(ctx) + + const ingressControllerStatus = await checkDeploymentStatus({ + ctx, + service: ingressControllerService, + env: gardenEnv, + }) + const defaultBackendStatus = await checkDeploymentStatus({ + ctx, + service: defaultBackendService, + env: gardenEnv, + }) + const dashboardStatus = await checkDeploymentStatus({ + ctx, + service: dashboardService, + env: gardenEnv, + }) + + return { + systemNamespaceReady, + dashboardReady: dashboardStatus.state === "ready", + ingressControllerReady: ingressControllerStatus.state === "ready", + defaultBackendReady: defaultBackendStatus.state === "ready", + } +} + +export async function configureGlobalSystem( + { ctx, env, logEntry }: ConfigureEnvironmentParams, status: EnvironmentStatus, +) { + if (!status.detail.systemNamespaceReady) { + logEntry && logEntry.setState({ section: "kubernetes", msg: `Creating garden system namespace` }) + await createNamespace(GARDEN_GLOBAL_SYSTEM_NAMESPACE) + } + + if (!status.detail.dashboardReady) { + logEntry && logEntry.setState({ section: "kubernetes", msg: `Configuring dashboard` }) + // TODO: deploy this as a service + await kubectl(GARDEN_GLOBAL_SYSTEM_NAMESPACE).call(["apply", "-f", dashboardSpecPath]) + } + + if (!status.detail.ingressControllerReady) { + logEntry && logEntry.setState({ section: "kubernetes", msg: `Configuring ingress controller` }) + const gardenEnv = getSystemEnv(env) + + await deployService({ + ctx, + service: await getDefaultBackendService(ctx), + serviceContext: { envVars: {}, dependencies: {} }, + env: gardenEnv, + logEntry, + }) + await deployService({ + ctx, + service: await getIngressControllerService(ctx), + serviceContext: { envVars: {}, dependencies: {} }, + env: gardenEnv, + exposePorts: true, + logEntry, + }) + } +} + +function getSystemEnv(env: Environment): Environment { + return { name: env.name, namespace: GARDEN_GLOBAL_SYSTEM_NAMESPACE, config: { providers: {} } } +} + +async function getIngressControllerService(ctx: PluginContext) { + const module = await ctx.resolveModule(ingressControllerModulePath) + + return ContainerService.factory(ctx, module, "ingress-controller") +} + +async function getDefaultBackendService(ctx: PluginContext) { + const module = await ctx.resolveModule(defaultBackendModulePath) + + return ContainerService.factory(ctx, module, "default-backend") +} + +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", + name: "garden-dashboard", + type: "container", + path: dashboardModulePath, + services: { + "kubernetes-dashboard": { + daemon: false, + dependencies: [], + endpoints: [], + ports: {}, + volumes: [], + }, + }, + variables: {}, + build: { dependencies: [] }, + test: {}, + }) + + return Service.factory(ctx, module, "kubernetes-dashboard") +} diff --git a/src/plugins/local/local-docker-swarm.ts b/src/plugins/local/local-docker-swarm.ts index 63e7eaf68c..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 { GardenContext } from "../../context" // 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: GardenContext, 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 eb257811fd..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 { GardenContext } from "../../context" 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: GardenContext) { + 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 8873a09bfe..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 { GardenContext } from "./context" 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: GardenContext, 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 cdb66b6603..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 { GardenContext } from "../context" 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: GardenContext, 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 e76460ab0c..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 { GardenContext } from "../context" 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: GardenContext, + 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 e4fff931e3..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 { GardenContext } from "../context" import { EntryStyle } from "../logger/types" import chalk from "chalk" @@ -19,7 +19,7 @@ export class TestTask extends Task { type = "test" constructor( - private ctx: GardenContext, + 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 58b0849dec..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 { GardenContext } from "../context" 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: GardenContext, 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 d145e6860c..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 { Garden } from "../garden" +import { PluginContext } from "../plugin-context" import { Module, TestSpec } from "./module" -import { GardenContext } from "../context" 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: GardenContext + ctx: PluginContext logEntry?: LogEntry } @@ -233,4 +234,4 @@ export interface Plugin extends Partial> { configKeys?: string[] } -export type PluginFactory = (ctx: GardenContext) => Plugin +export type PluginFactory = (garden: Garden) => Plugin diff --git a/src/types/service.ts b/src/types/service.ts index 698fbf05ef..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 { GardenContext } from "../context" 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: GardenContext, 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: GardenContext, module: M, name: string, config: S["config"]) => S), - ctx: GardenContext, 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 0a7a4390f9..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 { GardenContext } from "../context" - export const NEW_MODULE_VERSION = "0000000000" export abstract class VcsHandler { - constructor(protected ctx: GardenContext) { } + 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/static/garden-default-backend/garden.yml b/static/kubernetes/system-global/default-backend/garden.yml similarity index 100% rename from static/garden-default-backend/garden.yml rename to static/kubernetes/system-global/default-backend/garden.yml diff --git a/static/garden-ingress-controller/garden.yml b/static/kubernetes/system-global/ingress-controller/garden.yml similarity index 100% rename from static/garden-ingress-controller/garden.yml rename to static/kubernetes/system-global/ingress-controller/garden.yml diff --git a/static/garden-dashboard/dashboard.yml b/static/kubernetes/system-global/kubernetes-dashboard/dashboard.yml similarity index 97% rename from static/garden-dashboard/dashboard.yml rename to static/kubernetes/system-global/kubernetes-dashboard/dashboard.yml index 0a9e2908b5..b05adb0c12 100644 --- a/static/garden-dashboard/dashboard.yml +++ b/static/kubernetes/system-global/kubernetes-dashboard/dashboard.yml @@ -45,6 +45,8 @@ metadata: kind: Role apiVersion: rbac.authorization.k8s.io/v1 metadata: + labels: + k8s-app: kubernetes-dashboard name: kubernetes-dashboard-minimal namespace: garden-system rules: @@ -80,6 +82,8 @@ rules: apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: + labels: + k8s-app: kubernetes-dashboard name: kubernetes-dashboard-minimal namespace: garden-system roleRef: @@ -157,13 +161,13 @@ apiVersion: v1 metadata: labels: k8s-app: kubernetes-dashboard - name: kubernetes-dashboard + name: dashboard namespace: garden-system spec: type: NodePort ports: - port: 443 - nodePort: 32001 + nodePort: 3200 targetPort: 8443 selector: k8s-app: kubernetes-dashboard diff --git a/static/garden-dashboard/garden.yml b/static/kubernetes/system-global/kubernetes-dashboard/garden.yml similarity index 65% rename from static/garden-dashboard/garden.yml rename to static/kubernetes/system-global/kubernetes-dashboard/garden.yml index f2a8d9ca48..b456491cd4 100644 --- a/static/garden-dashboard/garden.yml +++ b/static/kubernetes/system-global/kubernetes-dashboard/garden.yml @@ -1,8 +1,7 @@ module: description: Kubernetes dashboard configuration name: k8s-dashboard - # TODO: add support for raw kubernetes specs as services - type: kubernetes + type: kubernetes-raw services: kubernetes-dashboard: specs: [dashboard.yml] diff --git a/test/helpers.ts b/test/helpers.ts index b582b3aa2b..29d15d5952 100644 --- a/test/helpers.ts +++ b/test/helpers.ts @@ -1,11 +1,12 @@ import * as td from "testdouble" import { resolve } from "path" +import { PluginContext } from "../src/plugin-context" import { DeleteConfigParams, GetConfigParams, ParseModuleParams, Plugin, PluginActions, PluginFactory, SetConfigParams, } from "../src/types/plugin" -import { GardenContext } from "../src/context" +import { Garden } from "../src/garden" import { Module } from "../src/types/module" import { expect } from "chai" @@ -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 GardenContext.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: GardenContext, 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 ea3a137cdd..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 { GardenContext } from "../../src/context"; -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 GardenContext was initialized", async () => { - const ctx = await makeContext() - const buildDirExists = await pathExists(ctx.buildDir.buildDirPath) + it("should have ensured the existence of the build dir when Garden was initialized", async () => { + 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 2d8677c981..ac3b7b0758 100644 --- a/test/src/commands/call.ts +++ b/test/src/commands/call.ts @@ -1,5 +1,5 @@ import { join } from "path" -import { GardenContext } from "../../../src/context" +import { Garden } from "../../../src/garden" import { CallCommand } from "../../../src/commands/call" import { expect } from "chai" import { GetServiceStatusParams, Plugin } from "../../../src/types/plugin" @@ -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 GardenContext.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 GardenContext.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 GardenContext.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 GardenContext.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 eb87b45ecf..87bcf89c63 100644 --- a/test/src/commands/deploy.ts +++ b/test/src/commands/deploy.ts @@ -1,5 +1,5 @@ import { join } from "path" -import { GardenContext } from "../../../src/context" +import { Garden } from "../../../src/garden" import { DeployCommand } from "../../../src/commands/deploy" import { expect } from "chai" import { DeployServiceParams, GetServiceStatusParams, Plugin } from "../../../src/types/plugin" @@ -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 GardenContext.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 GardenContext.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 976e62e27d..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 { GardenContext } from "../../../../src/context" +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 GardenContext.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 GardenContext.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 717813ec1e..5f6c913e71 100644 --- a/test/src/commands/validate.ts +++ b/test/src/commands/validate.ts @@ -1,5 +1,5 @@ import { join } from "path" -import { GardenContext } from "../../../src/context" +import { Garden } from "../../../src/garden" import { ValidateCommand } from "../../../src/commands/validate" import { defaultPlugins } from "../../../src/plugins" import { expectError } from "../../helpers" @@ -7,23 +7,23 @@ 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 GardenContext.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 () => { const root = join(__dirname, "data", "validate", "bad-project") - await expectError(async () => await GardenContext.factory(root), "configuration") + await expectError(async () => await Garden.factory(root), "configuration") }) it("should fail validating the bad-module project", async () => { const root = join(__dirname, "data", "validate", "bad-module") - const ctx = await GardenContext.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 72% rename from test/src/context.ts rename to test/src/garden.ts index 93fc107820..9b1a1ca17b 100644 --- a/test/src/context.ts +++ b/test/src/garden.ts @@ -1,18 +1,18 @@ import { join } from "path" -import { GardenContext } from "../../src/context" +import { Garden } from "../../src/garden" import { expect } from "chai" import { - expectError, makeTestContext, makeTestContextA, makeTestModule, projectRootA, + expectError, makeTestGarden, makeTestGardenA, makeTestModule, projectRootA, TestPlugin, } from "../helpers" -describe("GardenContext", () => { +describe("Garden", () => { it("should throw when initializing with missing plugins", async () => { - await expectError(async () => await GardenContext.factory(projectRootA), "configuration") + await expectError(async () => await Garden.factory(projectRootA), "configuration") }) 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 @@ -22,7 +22,7 @@ describe("GardenContext", () => { it("should throw if registering same plugin twice", async () => { try { - await GardenContext.factory(projectRootA, { + await Garden.factory(projectRootA, { plugins: [ (_ctx) => new TestPlugin(), (_ctx) => new TestPlugin(), @@ -37,7 +37,7 @@ describe("GardenContext", () => { }) 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("GardenContext", () => { 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("GardenContext", () => { 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("GardenContext", () => { }) 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("GardenContext", () => { }) 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("GardenContext", () => { }) 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("GardenContext", () => { }) 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("GardenContext", () => { 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("GardenContext", () => { }) 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("GardenContext", () => { 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("GardenContext", () => { 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("GardenContext", () => { 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("GardenContext", () => { // 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("GardenContext", () => { 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("GardenContext", () => { }) 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("GardenContext", () => { }) 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("GardenContext", () => { }) 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("GardenContext", () => { }) 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("GardenContext", () => { 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("GardenContext", () => { }) 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("GardenContext", () => { }) 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("GardenContext", () => { 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("GardenContext", () => { }) 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("GardenContext", () => { 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("GardenContext", () => { }) 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("GardenContext", () => { }) 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("GardenContext", () => { 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("GardenContext", () => { }) 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("GardenContext", () => { }) 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("GardenContext", () => { 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("GardenContext", () => { }) 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("GardenContext", () => { }) 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("GardenContext", () => { 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 adc4e3dfd9..ca498ec9e3 100644 --- a/test/src/task-graph.ts +++ b/test/src/task-graph.ts @@ -1,7 +1,7 @@ import { join } from "path" import { expect } from "chai" import { Task, TaskGraph, TaskResults } from "../../src/task-graph" -import { GardenContext } from "../../src/context" +import { Garden } from "../../src/garden" import { defaultPlugins } from "../../src/plugins" describe("task-graph", () => { @@ -31,7 +31,8 @@ describe("task-graph", () => { describe("TaskGraph", async () => { const projectRoot = join(__dirname, "..", "data", "test-project-empty") - const ctx = await GardenContext.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 }, )