diff --git a/examples/hello-world/garden.yml b/examples/hello-world/garden.yml index 415e2d0815..1c1fc85628 100644 --- a/examples/hello-world/garden.yml +++ b/examples/hello-world/garden.yml @@ -2,18 +2,18 @@ project: name: hello-world global: providers: - container: {} - npm-package: {} + - name: container + - name: npm-package variables: my-variable: hello-variable environments: - local: + - name: local providers: - local-kubernetes: + - name: local-kubernetes context: docker-for-desktop - local-google-cloud-functions: {} - dev: + - name: local-google-cloud-functions + - name: dev providers: - google-app-engine: {} - google-cloud-functions: + - name: google-app-engine + - name: google-cloud-functions default-project: garden-hello-world diff --git a/examples/hello-world/services/hello-container/garden.yml b/examples/hello-world/services/hello-container/garden.yml index 193f92eb15..4995b649ba 100644 --- a/examples/hello-world/services/hello-container/garden.yml +++ b/examples/hello-world/services/hello-container/garden.yml @@ -2,10 +2,10 @@ module: description: Hello world container service type: container services: - hello-container: + - name: hello-container command: [npm, start] ports: - http: + - name: http containerPort: 8080 endpoints: - paths: [/hello] @@ -20,9 +20,9 @@ module: dependencies: - hello-npm-package test: - unit: + - name: unit command: [npm, test] - integ: + - name: integ command: [npm, run, integ] dependencies: - hello-function diff --git a/examples/hello-world/services/hello-function/garden.yml b/examples/hello-world/services/hello-function/garden.yml index 05d810aacf..00a094f0d2 100644 --- a/examples/hello-world/services/hello-function/garden.yml +++ b/examples/hello-world/services/hello-function/garden.yml @@ -3,10 +3,10 @@ module: name: hello-function type: google-cloud-function services: - hello-function: + - name: hello-function entrypoint: helloFunction test: - unit: + - name: unit command: [npm, test] build: dependencies: diff --git a/examples/multi-container/garden.yml b/examples/multi-container/garden.yml index dbc16e471e..b26c444405 100644 --- a/examples/multi-container/garden.yml +++ b/examples/multi-container/garden.yml @@ -1,7 +1,10 @@ project: name: multi-container environments: - dev: + - name: local providers: - kubernetes: + - name: local-kubernetes + - name: dev + providers: + - name: kubernetes context: my-dev-context diff --git a/examples/multi-container/services/jworker/garden.yml b/examples/multi-container/services/jworker/garden.yml index 822dea4e10..b903169726 100644 --- a/examples/multi-container/services/jworker/garden.yml +++ b/examples/multi-container/services/jworker/garden.yml @@ -2,6 +2,6 @@ module: description: worker type: container services: - javaworker: + - name: javaworker dependencies: - redis diff --git a/examples/multi-container/services/postgres/garden.yml b/examples/multi-container/services/postgres/garden.yml index 5bd43631ae..3efc1ae745 100644 --- a/examples/multi-container/services/postgres/garden.yml +++ b/examples/multi-container/services/postgres/garden.yml @@ -3,10 +3,10 @@ module: type: container image: postgres:9.4 services: - db: + - name: db volumes: - name: data containerPath: /db-data ports: - db: + - name: db containerPort: 5432 diff --git a/examples/multi-container/services/redis/garden.yml b/examples/multi-container/services/redis/garden.yml index cebb395213..9aa35eb61a 100644 --- a/examples/multi-container/services/redis/garden.yml +++ b/examples/multi-container/services/redis/garden.yml @@ -3,8 +3,8 @@ module: type: container image: redis:alpine services: - redis: + - name: redis ports: - redis: + - name: redis protocol: TCP containerPort: 6379 diff --git a/examples/multi-container/services/result/garden.yml b/examples/multi-container/services/result/garden.yml index fcbfd40dce..7bd4f30644 100644 --- a/examples/multi-container/services/result/garden.yml +++ b/examples/multi-container/services/result/garden.yml @@ -2,12 +2,12 @@ module: description: Results presentation service type: container services: - result: + - name: result command: [nodemon, server.js] endpoints: - paths: [/] port: ui ports: - ui: + - name: ui protocol: TCP containerPort: 80 diff --git a/examples/multi-container/services/vote/garden.yml b/examples/multi-container/services/vote/garden.yml index 8fd03b9456..30416901c5 100644 --- a/examples/multi-container/services/vote/garden.yml +++ b/examples/multi-container/services/vote/garden.yml @@ -2,13 +2,13 @@ module: description: voting service type: container services: - vote: + - name: vote command: [python, app.py] endpoints: - paths: [/vote/] port: interface ports: - interface: + - name: interface protocol: TCP containerPort: 80 dependencies: diff --git a/garden.yml b/garden.yml index b1fdad37a5..24b4eb7a36 100644 --- a/garden.yml +++ b/garden.yml @@ -1,7 +1,7 @@ project: name: garden-framework environments: - local: + - name: local providers: - container: {} - local-kubernetes: {} + - name: container + - name: local-kubernetes diff --git a/gulpfile.ts b/gulpfile.ts index 572ea69166..2f76120fbf 100644 --- a/gulpfile.ts +++ b/gulpfile.ts @@ -85,7 +85,7 @@ gulp.task("add-version-files", (cb) => { proc.on("close", () => { const results = JSON.parse(output) - for (const module of Object.values(results.result)) { + for (const module of results.result) { const relPath = relative(__dirname, module.path) const versionFilePath = join(__dirname, destDir, relPath, ".garden-version") writeFileSync(versionFilePath, JSON.stringify(module.version)) diff --git a/src/commands/build.ts b/src/commands/build.ts index a5b73620e2..4bae8bce77 100644 --- a/src/commands/build.ts +++ b/src/commands/build.ts @@ -9,7 +9,6 @@ import { PluginContext } from "../plugin-context" import { BooleanParameter, Command, ParameterValues, StringParameter } from "./base" import { BuildTask } from "../tasks/build" -import { values } from "lodash" import { TaskResults } from "../task-graph" export const buildArguments = { @@ -36,7 +35,7 @@ export class BuildCommand extends Command { await ctx.clearBuilds() const names = args.module ? args.module.split(",") : undefined - const modules = values(await ctx.getModules(names)) + const modules = await ctx.getModules(names) ctx.log.header({ emoji: "hammer", command: "build" }) diff --git a/src/commands/deploy.ts b/src/commands/deploy.ts index 3d6ad8644f..d283e6ef13 100644 --- a/src/commands/deploy.ts +++ b/src/commands/deploy.ts @@ -10,7 +10,6 @@ import { PluginContext } from "../plugin-context" import { DeployTask } from "../tasks/deploy" import { BooleanParameter, Command, ParameterValues, StringParameter } from "./base" import { TaskResults } from "../task-graph" -import { values } from "lodash" export const deployArgs = { service: new StringParameter({ @@ -39,7 +38,7 @@ export class DeployCommand extends Command const names = args.service ? args.service.split(",") : undefined const services = await ctx.getServices(names) - if (Object.keys(services).length === 0) { + if (services.length === 0) { ctx.log.warn({ msg: "No services found. Aborting." }) return {} } @@ -53,10 +52,10 @@ export class DeployCommand extends Command const force = opts.force const forceBuild = opts["force-build"] - const modules = Array.from(new Set(values(services).map(s => s.module))) + const modules = Array.from(new Set(services.map(s => s.module))) const result = await ctx.processModules(modules, watch, async (module) => { - const servicesToDeploy = values(await module.getServices()).filter(s => !!services[s.name]) + const servicesToDeploy = await module.getServices() for (const service of servicesToDeploy) { await ctx.addTask(new DeployTask(ctx, service, force, forceBuild)) } diff --git a/src/commands/dev.ts b/src/commands/dev.ts index 2166420ba4..2c928ae8cc 100644 --- a/src/commands/dev.ts +++ b/src/commands/dev.ts @@ -15,7 +15,6 @@ import { STATIC_DIR } from "../constants" import { spawnSync } from "child_process" import chalk from "chalk" import Bluebird = require("bluebird") -import { values } from "lodash" import moment = require("moment") const imgcatPath = join(STATIC_DIR, "imgcat") @@ -38,7 +37,7 @@ export class DevCommand extends Command { await ctx.configureEnvironment() - const modules = values(await ctx.getModules()) + const modules = await ctx.getModules() if (modules.length === 0) { if (modules.length === 0) { diff --git a/src/commands/logs.ts b/src/commands/logs.ts index e09eb45712..2af7051343 100644 --- a/src/commands/logs.ts +++ b/src/commands/logs.ts @@ -11,7 +11,6 @@ import { BooleanParameter, Command, ParameterValues, StringParameter } from "./b import chalk from "chalk" import { ServiceLogEntry } from "../types/plugin" import Bluebird = require("bluebird") -import { values } from "lodash" import { Service } from "../types/service" import Stream from "ts-stream" @@ -53,7 +52,7 @@ export class LogsCommand extends Command { // NOTE: This will work differently when we have Elasticsearch set up for logging, but is // quite servicable for now. - await Bluebird.map(values(services), async (service: Service) => { + await Bluebird.map(services, async (service: Service) => { await ctx.getServiceLogs(service, stream, opts.tail) }) diff --git a/src/commands/push.ts b/src/commands/push.ts index 4d72730555..41ca6d18c2 100644 --- a/src/commands/push.ts +++ b/src/commands/push.ts @@ -7,7 +7,6 @@ */ import chalk from "chalk" -import { values } from "lodash" import { BooleanParameter, Command, ParameterValues, StringParameter } from "./base" import { PluginContext } from "../plugin-context" import { Module } from "../types/module" @@ -47,7 +46,7 @@ export class PushCommand extends Command { const names = args.module ? args.module.split(",") : undefined const modules = await ctx.getModules(names) - const result = await pushModules(ctx, values(modules), !!opts["force-build"], !!opts["allow-dirty"]) + const result = await pushModules(ctx, modules, !!opts["force-build"], !!opts["allow-dirty"]) ctx.log.info({ msg: "" }) ctx.log.info({ emoji: "heavy_check_mark", msg: chalk.green("Done!\n") }) diff --git a/src/commands/run/module.ts b/src/commands/run/module.ts index 6f09ff9093..29daecf798 100644 --- a/src/commands/run/module.ts +++ b/src/commands/run/module.ts @@ -13,7 +13,6 @@ import { RunResult } from "../../types/plugin" import { BooleanParameter, Command, ParameterValues, StringParameter } from "../base" import { uniq, - values, flatten, } from "lodash" import { printRuntimeContext } from "./index" @@ -72,9 +71,9 @@ export class RunModuleCommand extends Command { const command = args.command ? args.command.split(" ") : [] // combine all dependencies for all services in the module, to be sure we have all the context we need - const services = values(await module.getServices()) + const services = await module.getServices() const depNames = uniq(flatten(services.map(s => s.config.dependencies))) - const deps = values(await ctx.getServices(depNames)) + const deps = await ctx.getServices(depNames) const runtimeContext = await module.prepareRuntimeContext(deps) diff --git a/src/commands/run/test.ts b/src/commands/run/test.ts index 1635738e45..693468782a 100644 --- a/src/commands/run/test.ts +++ b/src/commands/run/test.ts @@ -11,8 +11,11 @@ import { ParameterError } from "../../exceptions" import { PluginContext } from "../../plugin-context" import { BuildTask } from "../../tasks/build" import { RunResult } from "../../types/plugin" +import { + findByName, + getNames, +} from "../../util" import { BooleanParameter, Command, ParameterValues, StringParameter } from "../base" -import { values } from "lodash" import { printRuntimeContext } from "./index" export const runArgs = { @@ -51,13 +54,13 @@ export class RunTestCommand extends Command { const module = await ctx.getModule(moduleName) const config = await module.getConfig() - const testSpec = config.test[testName] + const testSpec = findByName(config.test, testName) if (!testSpec) { throw new ParameterError(`Could not find test "${testName}" in module ${moduleName}`, { moduleName, testName, - availableTests: Object.keys(config.test), + availableTests: getNames(config.test), }) } @@ -74,10 +77,10 @@ export class RunTestCommand extends Command { const interactive = opts.interactive const deps = await ctx.getServices(testSpec.dependencies) - const runtimeContext = await module.prepareRuntimeContext(values(deps)) + const runtimeContext = await module.prepareRuntimeContext(deps) printRuntimeContext(ctx, runtimeContext) - return ctx.testModule({ module, interactive, runtimeContext, silent: false, testName, testSpec }) + return ctx.testModule({ module, interactive, runtimeContext, silent: false, testSpec }) } } diff --git a/src/commands/scan.ts b/src/commands/scan.ts index 3483e61705..72e65adde1 100644 --- a/src/commands/scan.ts +++ b/src/commands/scan.ts @@ -8,11 +8,11 @@ import { safeDump } from "js-yaml" import { PluginContext } from "../plugin-context" +import { DeepPrimitiveMap } from "../types/common" import { highlightYaml } from "../util" import { Command } from "./base" import Bluebird = require("bluebird") import { - mapValues, omit, } from "lodash" @@ -23,21 +23,22 @@ export class ScanCommand extends Command { async action(ctx: PluginContext) { const modules = await ctx.getModules() - const output = await Bluebird.props(mapValues(modules, async (m) => { + const output = await Bluebird.map(modules, async (m) => { + const config = await m.getConfig() return { name: m.name, type: m.type, path: m.path, - description: m.config.description, + description: config.description, version: await m.getVersion(), - config: m.config, + config, } - })) + }) - const shortOutput = mapValues(output, m => omit(m, ["config"])) + const shortOutput = output.map(m => omit(m, ["config"])) ctx.log.info(highlightYaml(safeDump(shortOutput, { noRefs: true, skipInvalid: true }))) - return output + return output } } diff --git a/src/commands/test.ts b/src/commands/test.ts index 591f961a13..306729b446 100644 --- a/src/commands/test.ts +++ b/src/commands/test.ts @@ -42,7 +42,7 @@ export class TestCommand extends Command { async action(ctx: PluginContext, args: Args, opts: Opts): Promise { const names = args.module ? args.module.split(",") : undefined - const modules = values(await ctx.getModules(names)) + const modules = await ctx.getModules(names) ctx.log.header({ emoji: "thermometer", diff --git a/src/garden.ts b/src/garden.ts index d6c8c94d2f..095a8115a7 100644 --- a/src/garden.ts +++ b/src/garden.ts @@ -13,12 +13,13 @@ import { sep, } from "path" import { + extend, isString, values, fromPairs, merge, pick, - isEmpty, + keyBy, } from "lodash" import * as Joi from "joi" import { @@ -40,11 +41,11 @@ import { pluginSchema, RegisterPluginParam, } from "./types/plugin" +import { EnvironmentConfig } from "./types/project" import { - EnvironmentConfig, -} from "./types/project" -import { + findByName, getIgnorer, + getNames, scanDirectory, } from "./util" import { @@ -217,17 +218,17 @@ export class Garden { const environment = parts[0] const namespace = parts.slice(1).join(".") || DEFAULT_NAMESPACE - const envConfig = parsedConfig.project.environments[environment] + const envConfig = findByName(parsedConfig.project.environments, environment) if (!envConfig) { throw new ParameterError(`Project ${projectName} does not specify environment ${environment}`, { projectName, env, - definedEnvironments: Object.keys(parsedConfig.project.environments), + definedEnvironments: getNames(parsedConfig.project.environments), }) } - if (!envConfig.providers || isEmpty(envConfig.providers)) { + if (!envConfig.providers || envConfig.providers.length === 0) { throw new ConfigurationError(`Environment '${environment}' does not specify any providers`, { projectName, env, @@ -242,10 +243,26 @@ export class Garden { }) } - // Resolve the project configuration based on selected environment - const projectConfig = merge({}, globalConfig, envConfig) + const mergedProviders = merge( + {}, + keyBy(globalConfig.providers, "name"), + keyBy(envConfig.providers, "name"), + ) - const garden = new Garden(projectRoot, projectName, environment, namespace, projectConfig, localConfigStore, logger) + // Resolve the project configuration based on selected environment + const projectEnvConfig: EnvironmentConfig = { + name: environment, + providers: values(mergedProviders), + variables: merge({}, globalConfig.variables, envConfig.variables), + } + + const garden = new Garden( + projectRoot, projectName, + environment, namespace, + projectEnvConfig, + localConfigStore, + logger, + ) // Register plugins for (const plugin of builtinPlugins.concat(plugins)) { @@ -258,8 +275,8 @@ export class Garden { // Load configured plugins // Validate configuration - for (const [providerName, provider] of Object.entries(projectConfig.providers)) { - await garden.loadPlugin(providerName, provider) + for (const provider of projectEnvConfig.providers) { + await garden.loadPlugin(provider.name, provider) } return garden @@ -376,9 +393,14 @@ export class Garden { this.loadedPlugins[pluginName] = plugin - // allow plugins to override their own config (that gets passed to action handlers) + // allow plugins to extend their own config (that gets passed to action handlers) if (plugin.config) { - this.config.providers[pluginName] = plugin.config + const providerConfig = findByName(this.config.providers, pluginName) + if (providerConfig) { + extend(providerConfig, plugin.config) + } else { + this.config.providers.push(plugin.config) + } } for (const modulePath of plugin.modules || []) { @@ -465,16 +487,16 @@ export class Garden { Returns all modules that are registered in this context. Scans for modules in the project root if it hasn't already been done. */ - async getModules(names?: string[], noScan?: boolean): Promise> { + async getModules(names?: string[], noScan?: boolean): Promise { if (!this.modulesScanned && !noScan) { await this.scanModules() } if (!names) { - return this.modules + return values(this.modules) } - const output = {} + const output: Module[] = [] const missing: string[] = [] for (const name of names) { @@ -483,7 +505,7 @@ export class Garden { if (!module) { missing.push(name) } else { - output[name] = module + output.push(module) } } @@ -501,33 +523,33 @@ export class Garden { * Returns the module with the specified name. Throws error if it doesn't exist. */ async getModule(name: string, noScan?: boolean): Promise> { - return (await this.getModules([name], noScan))[name] + return (await this.getModules([name], noScan))[0] } /* Returns all services that are registered in this context. Scans for modules and services in the project root if it hasn't already been done. */ - async getServices(names?: string[], noScan?: boolean): Promise { + async getServices(names?: string[], noScan?: boolean): Promise { // TODO: deduplicate (this is almost the same as getModules() if (!this.modulesScanned && !noScan) { await this.scanModules() } if (!names) { - return this.services + return values(this.services) } - const output = {} + const output: Service[] = [] const missing: string[] = [] for (const name of names) { - const module = this.services[name] + const service = this.services[name] - if (!module) { + if (!service) { missing.push(name) } else { - output[name] = module + output.push(service) } } @@ -545,7 +567,7 @@ export class Garden { * Returns the service with the specified name. Throws error if it doesn't exist. */ async getService(name: string, noScan?: boolean): Promise> { - return (await this.getServices([name], noScan))[name] + return (await this.getServices([name], noScan))[0] } /* @@ -604,7 +626,9 @@ export class Garden { this.modules[config.name] = module // Add to service-module map - for (const serviceName in config.services || {}) { + for (const service of config.services || []) { + const serviceName = service.name + if (!force && this.services[serviceName]) { throw new ConfigurationError( `Service names must be unique - ${serviceName} is declared multiple times ` + diff --git a/src/plugin-context.ts b/src/plugin-context.ts index e4e98c23f3..b1b74a9856 100644 --- a/src/plugin-context.ts +++ b/src/plugin-context.ts @@ -54,6 +54,7 @@ import { toPairs, values, padEnd, + keyBy, } from "lodash" import { Omit, @@ -144,11 +145,12 @@ export function createPluginContext(garden: Garden): PluginContext { } const projectConfig = { ...garden.config } + const providerConfigs = keyBy(projectConfig.providers, "name") // TODO: find a nicer way to do this (like a type-safe wrapper function) function commonParams(handler): PluginActionParamsBase { const providerName = handler["pluginName"] - const providerConfig = projectConfig.providers[handler["pluginName"]] + const providerConfig = providerConfigs[providerName] const env = garden.getEnvironment() return { @@ -352,20 +354,20 @@ export function createPluginContext(garden: Garden): PluginContext { const envStatus: EnvironmentStatusMap = await ctx.getEnvironmentStatus() const services = await ctx.getServices() - const serviceStatus = await Bluebird.props( - mapValues(services, (service: Service) => ctx.getServiceStatus(service)), + const serviceStatus = await Bluebird.map( + services, (service: Service) => ctx.getServiceStatus(service), ) return { providers: envStatus, - services: serviceStatus, + services: keyBy(serviceStatus, "name"), } }, deployServices: async ({ names, force = false, forceBuild = false, logEntry }) => { const services = await ctx.getServices(names) - for (const service of values(services)) { + for (const service of services) { const task = new DeployTask(ctx, service, force, forceBuild, logEntry) await ctx.addTask(task) } diff --git a/src/plugins/container.ts b/src/plugins/container.ts index bf24294253..aaa9e124e5 100644 --- a/src/plugins/container.ts +++ b/src/plugins/container.ts @@ -11,7 +11,11 @@ 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" +import { + joiIdentifier, + joiArray, + validate, +} from "../types/common" import { existsSync } from "fs" import { join } from "path" import { ConfigurationError } from "../exceptions" @@ -23,9 +27,13 @@ import { ParseModuleParams, RunServiceParams, } from "../types/plugin" -import { Service } from "../types/service" +import { + Service, + ServiceConfig, +} from "../types/service" import { DEFAULT_PORT_PROTOCOL } from "../constants" import { splitFirst } from "../util" +import { keyBy } from "lodash" export interface ServiceEndpointSpec { paths?: string[] @@ -37,6 +45,7 @@ export interface ServiceEndpointSpec { export type ServicePortProtocol = "TCP" | "UDP" export interface ServicePortSpec { + name: string protocol: ServicePortProtocol containerPort: number hostPort?: number @@ -59,13 +68,12 @@ export interface ServiceHealthCheckSpec { tcpPort?: string, } -export interface ContainerServiceConfig { +export interface ContainerServiceConfig extends ServiceConfig { command?: string[], daemon: boolean - dependencies: string[], endpoints: ServiceEndpointSpec[], healthCheck?: ServiceHealthCheckSpec, - ports: { [portName: string]: ServicePortSpec }, + ports: ServicePortSpec[], volumes: ServiceVolumeSpec[], } @@ -104,7 +112,7 @@ const portSchema = Joi.object() const volumeSchema = Joi.object() .keys({ - name: Joi.string().required(), + name: joiIdentifier(), containerPath: Joi.string().required(), hostPath: Joi.string(), }) @@ -113,17 +121,17 @@ const serviceSchema = baseServiceSchema .keys({ command: Joi.array().items(Joi.string()), daemon: Joi.boolean().default(false), - endpoints: Joi.array().items(endpointSchema).default(() => [], "[]"), + endpoints: joiArray(endpointSchema), healthCheck: healthCheckSchema, - ports: Joi.object().pattern(identifierRegex, portSchema).default(() => ({}), "{}"), - volumes: Joi.array().items(volumeSchema).default(() => [], "[]"), + ports: joiArray(portSchema).unique("name"), + volumes: joiArray(volumeSchema).unique("name"), }) const containerSchema = baseModuleSchema.keys({ type: Joi.string().allow("container").required(), path: Joi.string().required(), image: Joi.string(), - services: Joi.object().pattern(identifierRegex, serviceSchema).default(() => ({}), "{}"), + services: joiArray(serviceSchema).unique("name"), }) export class ContainerService extends Service { } @@ -205,14 +213,16 @@ export const gardenPlugin = (): GardenPlugin => ({ } // validate services - for (const [name, service] of Object.entries(module.services)) { + for (const service of module.services) { // make sure ports are correctly configured - const definedPorts = Object.keys(service.ports) + const name = service.name + const definedPorts = service.ports + const portsByName = keyBy(service.ports, "name") for (const endpoint of service.endpoints) { const endpointPort = endpoint.port - if (!service.ports[endpointPort]) { + if (!portsByName[endpointPort]) { throw new ConfigurationError( `Service ${name} does not define port ${endpointPort} defined in endpoint`, { definedPorts, endpointPort }, @@ -223,7 +233,7 @@ export const gardenPlugin = (): GardenPlugin => ({ if (service.healthCheck && service.healthCheck.httpGet) { const healthCheckHttpPort = service.healthCheck.httpGet.port - if (!service.ports[healthCheckHttpPort]) { + if (!portsByName[healthCheckHttpPort]) { throw new ConfigurationError( `Service ${name} does not define port ${healthCheckHttpPort} defined in httpGet health check`, { definedPorts, healthCheckHttpPort }, @@ -234,7 +244,7 @@ export const gardenPlugin = (): GardenPlugin => ({ if (service.healthCheck && service.healthCheck.tcpPort) { const healthCheckTcpPort = service.healthCheck.tcpPort - if (!service.ports[healthCheckTcpPort]) { + if (!portsByName[healthCheckTcpPort]) { throw new ConfigurationError( `Service ${name} does not define port ${healthCheckTcpPort} defined in tcpPort health check`, { definedPorts, healthCheckTcpPort }, diff --git a/src/plugins/generic.ts b/src/plugins/generic.ts index b131eadd34..c2d97dc860 100644 --- a/src/plugins/generic.ts +++ b/src/plugins/generic.ts @@ -67,7 +67,7 @@ export const genericPlugin = { } }, - async testModule({ module, testName, testSpec }: TestModuleParams): Promise { + async testModule({ module, testSpec }: TestModuleParams): Promise { const startedAt = new Date() const command = testSpec.command const result = await spawn( @@ -77,7 +77,7 @@ export const genericPlugin = { return { moduleName: module.name, command, - testName, + testName: testSpec.name, version: await module.getVersion(), success: result.code === 0, startedAt, diff --git a/src/plugins/google/common.ts b/src/plugins/google/common.ts index a58f54cfe5..be1954c9cd 100644 --- a/src/plugins/google/common.ts +++ b/src/plugins/google/common.ts @@ -6,12 +6,14 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import { Environment } from "../../types/common" import { Module, ModuleConfig } from "../../types/module" import { Service, ServiceConfig } from "../../types/service" import { ConfigurationError } from "../../exceptions" import { GCloud } from "./gcloud" -import { ConfigureEnvironmentParams } from "../../types/plugin" +import { + ConfigureEnvironmentParams, + Provider, +} from "../../types/plugin" export const GOOGLE_CLOUD_DEFAULT_REGION = "us-central1" @@ -87,9 +89,6 @@ export function gcloud(project?: string, account?: string) { return new GCloud({ project, account }) } -export function getProject(providerName: string, service: Service, env: Environment) { - // TODO: this is very contrived - we should rethink this a bit and pass - // provider configuration when calling the plugin - const provider = env.config.providers[providerName] - return service.config.project || provider["default-project"] || null +export function getProject(service: Service, provider: Provider) { + return service.config.project || provider.config["default-project"] || null } diff --git a/src/plugins/google/google-app-engine.ts b/src/plugins/google/google-app-engine.ts index 5f9f052708..d7b3d3a5c5 100644 --- a/src/plugins/google/google-app-engine.ts +++ b/src/plugins/google/google-app-engine.ts @@ -51,7 +51,7 @@ export const gardenPlugin = (): GardenPlugin => ({ return {} }, - async deployService({ ctx, service, runtimeContext, env }: DeployServiceParams) { + async deployService({ ctx, service, runtimeContext, provider }: DeployServiceParams) { ctx.log.info({ section: service.name, msg: `Deploying app...`, @@ -84,7 +84,7 @@ export const gardenPlugin = (): GardenPlugin => ({ dumpYaml(appYamlPath, appYaml) // deploy to GAE - const project = getProject("google-app-engine", service, env) + const project = getProject(service, provider) await gcloud(project).call([ "app", "deploy", "--quiet", @@ -93,9 +93,9 @@ export const gardenPlugin = (): GardenPlugin => ({ ctx.log.info({ section: service.name, msg: `App deployed` }) }, - async getServiceOutputs({ service, env }: GetServiceOutputsParams) { + async getServiceOutputs({ service, provider }: GetServiceOutputsParams) { // TODO: we may want to pull this from the service status instead, along with other outputs - const project = getProject("google-app-engine", service, env) + const project = getProject(service, provider) return { endpoint: `https://${GOOGLE_CLOUD_DEFAULT_REGION}-${project}.cloudfunctions.net/${service.name}`, diff --git a/src/plugins/google/google-cloud-functions.ts b/src/plugins/google/google-cloud-functions.ts index 1b3874c2f6..287dc1dbb6 100644 --- a/src/plugins/google/google-cloud-functions.ts +++ b/src/plugins/google/google-cloud-functions.ts @@ -6,7 +6,10 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import { identifierRegex, validate } from "../../types/common" +import { + joiArray, + validate, +} from "../../types/common" import { baseServiceSchema, Module, ModuleConfig } from "../../types/module" import { ServiceConfig, @@ -43,19 +46,15 @@ export interface GoogleCloudFunctionsServiceConfig extends ServiceConfig { export interface GoogleCloudFunctionsModuleConfig extends ModuleConfig { } -export const gcfServicesSchema = Joi.object() - .pattern(identifierRegex, baseServiceSchema.keys({ - entrypoint: Joi.string(), - path: Joi.string().default("."), - project: Joi.string(), - })) - .default(() => ({}), "{}") +export const gcfServicesSchema = joiArray(baseServiceSchema.keys({ + entrypoint: Joi.string(), + path: Joi.string().default("."), + project: Joi.string(), +})).unique("name") export class GoogleCloudFunctionsModule extends Module { } export class GoogleCloudFunctionsService extends Service { } -const pluginName = "google-cloud-functions" - export const gardenPlugin = (): GardenPlugin => ({ actions: { getEnvironmentStatus, @@ -79,7 +78,7 @@ export const gardenPlugin = (): GardenPlugin => ({ { ctx, provider, service, env }: DeployServiceParams, ) { // TODO: provide env vars somehow to function - const project = getProject(pluginName, service, env) + const project = getProject(service, provider) const functionPath = resolve(service.module.path, service.config.path) const entrypoint = service.config.entrypoint || service.name @@ -95,9 +94,9 @@ export const gardenPlugin = (): GardenPlugin => ({ return getServiceStatus({ ctx, provider, service, env }) }, - async getServiceOutputs({ service, env }: GetServiceOutputsParams) { + async getServiceOutputs({ service, provider }: GetServiceOutputsParams) { // TODO: we may want to pull this from the service status instead, along with other outputs - const project = getProject(pluginName, service, env) + const project = getProject(service, provider) return { endpoint: `https://${GOOGLE_CLOUD_DEFAULT_REGION}-${project}.cloudfunctions.net/${service.name}`, @@ -108,9 +107,9 @@ export const gardenPlugin = (): GardenPlugin => ({ }) export async function getServiceStatus( - { service, env }: GetServiceStatusParams, + { service, provider }: GetServiceStatusParams, ): Promise { - const project = getProject(pluginName, service, env) + const project = getProject(service, provider) const functions: any[] = await gcloud(project).json(["beta", "functions", "list"]) const providerId = `projects/${project}/locations/${GOOGLE_CLOUD_DEFAULT_REGION}/functions/${service.name}` diff --git a/src/plugins/kubernetes/actions.ts b/src/plugins/kubernetes/actions.ts index 3ea371db57..184ba8afea 100644 --- a/src/plugins/kubernetes/actions.ts +++ b/src/plugins/kubernetes/actions.ts @@ -271,9 +271,10 @@ export async function runModule( } export async function testModule( - { ctx, provider, env, interactive, module, runtimeContext, silent, testName, testSpec }: + { ctx, provider, env, interactive, module, runtimeContext, silent, testSpec }: TestModuleParams, ): Promise { + const testName = testSpec.name const command = testSpec.command runtimeContext.envVars = { ...runtimeContext.envVars, ...testSpec.variables } const timeout = testSpec.timeout || DEFAULT_TEST_TIMEOUT diff --git a/src/plugins/kubernetes/deployment.ts b/src/plugins/kubernetes/deployment.ts index 34fa57282a..cc1bf6398f 100644 --- a/src/plugins/kubernetes/deployment.ts +++ b/src/plugins/kubernetes/deployment.ts @@ -12,7 +12,11 @@ import { ContainerService, ContainerServiceConfig, } from "../container" -import { toPairs, extend } from "lodash" +import { + toPairs, + extend, + keyBy, +} from "lodash" import { RuntimeContext, ServiceStatus, @@ -178,9 +182,11 @@ export async function createDeployment(service: ContainerService, runtimeContext failureThreshold: 3, } + const portsByName = keyBy(config.ports, "name") + if (config.healthCheck.httpGet) { const httpGet: any = extend({}, config.healthCheck.httpGet) - httpGet.port = config.ports[httpGet.port].containerPort + httpGet.port = portsByName[httpGet.port].containerPort container.readinessProbe.httpGet = httpGet container.livenessProbe.httpGet = httpGet @@ -189,7 +195,7 @@ export async function createDeployment(service: ContainerService, runtimeContext container.livenessProbe.exec = container.readinessProbe.exec } else if (config.healthCheck.tcpPort) { container.readinessProbe.tcpSocket = { - port: config.ports[config.healthCheck.tcpPort].containerPort, + port: portsByName[config.healthCheck.tcpPort].containerPort, } container.livenessProbe.tcpSocket = container.readinessProbe.tcpSocket } else { @@ -252,7 +258,7 @@ export async function createDeployment(service: ContainerService, runtimeContext container.volumeMounts = volumeMounts } - const ports = Object.values(config.ports) + const ports = config.ports for (const port of ports) { container.ports.push({ diff --git a/src/plugins/kubernetes/index.ts b/src/plugins/kubernetes/index.ts index fec41c4abe..361e8adfae 100644 --- a/src/plugins/kubernetes/index.ts +++ b/src/plugins/kubernetes/index.ts @@ -12,6 +12,10 @@ import { GardenPlugin, Provider, } from "../../types/plugin" +import { + ProviderConfig, + providerConfigBase, +} from "../../types/project" import { configureEnvironment, @@ -36,7 +40,7 @@ import { kubernetesSpecHandlers } from "./specs-module" export const name = "kubernetes" -export interface KubernetesConfig { +export interface KubernetesConfig extends ProviderConfig { context: string ingressHostname: string ingressClass: string @@ -46,7 +50,7 @@ export interface KubernetesConfig { export interface KubernetesProvider extends Provider { } -const configSchema = Joi.object().keys({ +const configSchema = providerConfigBase.keys({ context: Joi.string().required(), ingressHostname: Joi.string().hostname().required(), ingressClass: Joi.string(), diff --git a/src/plugins/kubernetes/ingress.ts b/src/plugins/kubernetes/ingress.ts index 17be874542..f27d29cfe5 100644 --- a/src/plugins/kubernetes/ingress.ts +++ b/src/plugins/kubernetes/ingress.ts @@ -7,6 +7,7 @@ */ import { PluginContext } from "../../plugin-context" +import { findByName } from "../../util" import { ContainerService } from "../container" import { KubernetesProvider } from "./index" @@ -24,7 +25,7 @@ export async function createIngress(ctx: PluginContext, provider: KubernetesProv const backend = { serviceName: service.name, - servicePort: service.config.ports[e.port].containerPort, + servicePort: findByName(service.config.ports, e.port)!.containerPort, } rule.http = { diff --git a/src/plugins/kubernetes/local.ts b/src/plugins/kubernetes/local.ts index 6a06d99395..f195c9a916 100644 --- a/src/plugins/kubernetes/local.ts +++ b/src/plugins/kubernetes/local.ts @@ -17,11 +17,16 @@ import { GardenPlugin, GetEnvironmentStatusParams, } from "../../types/plugin" +import { providerConfigBase } from "../../types/project" +import { findByName } from "../../util" import { configureEnvironment, getEnvironmentStatus, } from "./actions" -import { gardenPlugin as k8sPlugin } from "./index" +import { + gardenPlugin as k8sPlugin, + KubernetesConfig, +} from "./index" import { getSystemGarden, isSystemGarden, @@ -55,7 +60,7 @@ async function configureLocalEnvironment( const sysGarden = await getSystemGarden(provider) const sysProvider = { name: provider.name, - config: sysGarden.config.providers[provider.name], + config: findByName(sysGarden.config.providers, provider.name), } const sysStatus = await getEnvironmentStatus({ ctx: sysGarden.pluginContext, @@ -75,7 +80,7 @@ async function configureLocalEnvironment( export const name = "local-kubernetes" -const configSchema = Joi.object().keys({ +const configSchema = providerConfigBase.keys({ context: Joi.string().default("docker-for-desktop"), _system: Joi.any(), }) @@ -83,7 +88,8 @@ const configSchema = Joi.object().keys({ export function gardenPlugin({ config }): GardenPlugin { config = validate(config, configSchema, { context: "kubernetes provider config" }) - const k8sConfig = { + const k8sConfig: KubernetesConfig = { + name: config.name, context: config.context, ingressHostname: "local.app.garden", ingressClass: "nginx", diff --git a/src/plugins/kubernetes/service.ts b/src/plugins/kubernetes/service.ts index 3541c38989..d7b3b14b80 100644 --- a/src/plugins/kubernetes/service.ts +++ b/src/plugins/kubernetes/service.ts @@ -35,11 +35,11 @@ export async function createServices(service: ContainerService) { // first add internally exposed (ClusterIP) service const internalPorts: any = [] - const ports = Object.entries(service.config.ports) + const ports = service.config.ports - for (const [portName, portSpec] of ports) { + for (const portSpec of ports) { internalPorts.push({ - name: portName, + name: portSpec.name, protocol: portSpec.protocol, targetPort: portSpec.containerPort, port: portSpec.containerPort, @@ -52,12 +52,12 @@ export async function createServices(service: ContainerService) { // optionally add a NodePort service for externally open ports, if applicable // TODO: explore nicer ways to do this - const exposedPorts = ports.filter(([_, portSpec]) => portSpec.nodePort) + const exposedPorts = ports.filter(portSpec => portSpec.nodePort) if (exposedPorts.length > 0) { - addService(service.name + "-nodeport", "NodePort", exposedPorts.map(([portName, portSpec]) => ({ + addService(service.name + "-nodeport", "NodePort", exposedPorts.map(portSpec => ({ // TODO: do the parsing and defaults when loading the yaml - name: portName, + name: portSpec.name, protocol: portSpec.protocol, port: portSpec.containerPort, nodePort: portSpec.nodePort, diff --git a/src/plugins/kubernetes/specs-module.ts b/src/plugins/kubernetes/specs-module.ts index f444c25656..f8d6f69430 100644 --- a/src/plugins/kubernetes/specs-module.ts +++ b/src/plugins/kubernetes/specs-module.ts @@ -10,8 +10,8 @@ import Bluebird = require("bluebird") import * as Joi from "joi" import { GARDEN_ANNOTATION_KEYS_VERSION } from "../../constants" import { + joiArray, joiIdentifier, - joiIdentifierMap, validate, } from "../../types/common" import { @@ -51,11 +51,11 @@ const k8sSpecSchema = Joi.object().keys({ }).required(), }).unknown(true) -const specsServicesSchema = joiIdentifierMap(baseServiceSchema.keys({ +const specsServicesSchema = joiArray(baseServiceSchema.keys({ specs: Joi.array().items(k8sSpecSchema).required(), // TODO: support spec files as well // specFiles: Joi.array().items(Joi.string()), -})) +})).unique("name") export const kubernetesSpecHandlers = { parseModule: async ({ ctx, moduleConfig }: ParseModuleParams): Promise => { diff --git a/src/plugins/kubernetes/system.ts b/src/plugins/kubernetes/system.ts index 935ab5c026..16eeaf7e34 100644 --- a/src/plugins/kubernetes/system.ts +++ b/src/plugins/kubernetes/system.ts @@ -31,23 +31,25 @@ export async function getSystemGarden(provider: KubernetesProvider): Promise ({ ctx.log.info({ section: service.name, msg: `Deploying version ${versionString}` }) const identifier = await service.module.getLocalImageId() - const ports = Object.values(service.config.ports).map(p => { + const ports = service.config.ports.map(p => { const port: any = { Protocol: p.protocol ? p.protocol.toLowerCase() : "tcp", TargetPort: p.containerPort, diff --git a/src/plugins/local/local-google-cloud-functions.ts b/src/plugins/local/local-google-cloud-functions.ts index 017265119a..1fa37106dd 100644 --- a/src/plugins/local/local-google-cloud-functions.ts +++ b/src/plugins/local/local-google-cloud-functions.ts @@ -32,7 +32,6 @@ import { ServicePortProtocol, } from "../container" import { validate } from "../../types/common" -import { mapValues } from "lodash" const baseContainerName = "local-google-cloud-functions.local-gcf-container" const emulatorBaseModulePath = join(STATIC_DIR, "local-gcf-container") @@ -101,10 +100,11 @@ export const gardenPlugin = (): GardenPlugin => ({ }) async function getEmulatorModule(ctx: PluginContext, module: GoogleCloudFunctionsModule) { - const services = mapValues(module.services, (s, name) => { - const functionEntrypoint = s.entrypoint || name + const services = module.services.map((s) => { + const functionEntrypoint = s.entrypoint || s.name return { + name: s.name, command: ["/app/start.sh", functionEntrypoint], daemon: false, dependencies: s.dependencies, @@ -112,9 +112,13 @@ async function getEmulatorModule(ctx: PluginContext, module: GoogleCloudFunction port: "http", }], healthCheck: { tcpPort: "http" }, - ports: { - http: { protocol: "TCP", containerPort: 8010 }, - }, + ports: [ + { + name: "http", + protocol: "TCP", + containerPort: 8010, + }, + ], volumes: [], } }) diff --git a/src/tasks/deploy.ts b/src/tasks/deploy.ts index 757cd7a125..6a1e70f83c 100644 --- a/src/tasks/deploy.ts +++ b/src/tasks/deploy.ts @@ -9,7 +9,6 @@ import { LogEntry } from "../logger" import { PluginContext } from "../plugin-context" import { BuildTask } from "./build" -import { values } from "lodash" import { Task } from "../types/task" import { Service, @@ -33,7 +32,7 @@ export class DeployTask> extends Task { async getDependencies() { const serviceDeps = this.service.config.dependencies const services = await this.ctx.getServices(serviceDeps) - const deps: Task[] = values(services).map((s) => { + const deps: Task[] = services.map((s) => { return new DeployTask(this.ctx, s, this.force, this.forceBuild) }) diff --git a/src/tasks/test.ts b/src/tasks/test.ts index 230671da12..581776b206 100644 --- a/src/tasks/test.ts +++ b/src/tasks/test.ts @@ -14,7 +14,6 @@ import { TestResult } from "../types/plugin" import { Task } from "../types/task" import { EntryStyle } from "../logger/types" import chalk from "chalk" -import { values } from "lodash" class TestError extends Error { toString() { @@ -27,7 +26,7 @@ export class TestTask extends Task { constructor( private ctx: PluginContext, - private module: T, private testName: string, private testSpec: TestSpec, + private module: T, private testSpec: TestSpec, private force: boolean, private forceBuild: boolean, ) { super() @@ -44,8 +43,7 @@ export class TestTask extends Task { const services = await this.ctx.getServices(this.testSpec.dependencies) - for (const serviceName of Object.keys(services)) { - const service = services[serviceName] + for (const service of services) { deps.push(new DeployTask(this.ctx, service, false, this.forceBuild)) } @@ -53,11 +51,11 @@ export class TestTask extends Task { } getName() { - return `${this.module.name}.${this.testName}` + return `${this.module.name}.${this.testSpec.name}` } getDescription() { - return `running ${this.testName} tests in module ${this.module.name}` + return `running ${this.testSpec.name} tests in module ${this.module.name}` } async process(): Promise { @@ -68,7 +66,7 @@ export class TestTask extends Task { if (testResult && testResult.success) { const passedEntry = this.ctx.log.info({ section: this.module.name, - msg: `${this.testName} tests`, + msg: `${this.testSpec.name} tests`, }) passedEntry.setSuccess({ msg: chalk.green("Already passed"), append: true }) return testResult @@ -77,11 +75,11 @@ export class TestTask extends Task { const entry = this.ctx.log.info({ section: this.module.name, - msg: `Running ${this.testName} tests`, + msg: `Running ${this.testSpec.name} tests`, entryStyle: EntryStyle.activity, }) - const dependencies = values(await this.ctx.getServices(this.testSpec.dependencies)) + const dependencies = await this.ctx.getServices(this.testSpec.dependencies) const runtimeContext = await this.module.prepareRuntimeContext(dependencies) const result = await this.ctx.testModule({ @@ -89,7 +87,6 @@ export class TestTask extends Task { module: this.module, runtimeContext, silent: true, - testName: this.testName, testSpec: this.testSpec, }) @@ -108,7 +105,7 @@ export class TestTask extends Task { return null } - const testResult = await this.ctx.getTestResult(this.module, this.testName, await this.module.getVersion()) + const testResult = await this.ctx.getTestResult(this.module, this.testSpec.name, await this.module.getVersion()) return testResult && testResult.success && testResult } } diff --git a/src/types/common.ts b/src/types/common.ts index 34890db79b..1add686594 100644 --- a/src/types/common.ts +++ b/src/types/common.ts @@ -48,6 +48,10 @@ export const joiEnvVars = () => Joi .object().pattern(envVarRegex, joiPrimitive()) .default(() => ({}), "{}") +export const joiArray = (schema) => Joi + .array().items(schema) + .default(() => [], "[]") + export interface Environment { name: string namespace: string diff --git a/src/types/config.ts b/src/types/config.ts index 145f35b5f0..3a4249a14e 100644 --- a/src/types/config.ts +++ b/src/types/config.ts @@ -7,6 +7,10 @@ */ import { join, parse, relative, sep } from "path" +import { + findByName, + getNames, +} from "../util" import { baseModuleSchema, ModuleConfig } from "./module" import { joiIdentifier, validate } from "./common" import { ConfigurationError } from "../exceptions" @@ -14,7 +18,6 @@ import * as Joi from "joi" import * as yaml from "js-yaml" import { readFileSync } from "fs" import { defaultEnvironments, ProjectConfig, projectSchema } from "./project" -import { extend } from "lodash" const CONFIG_FILENAME = "garden.yml" @@ -80,18 +83,22 @@ export async function loadConfig(projectRoot: string, path: string): Promise [], "[]"), + variables: joiVariables(), + timeout: Joi.number(), +}) const versionFileSchema = Joi.object().keys({ versionString: Joi.string().required(), @@ -91,12 +95,38 @@ export interface ModuleConfig { description?: string name: string path: string - services: { [name: string]: T } - test: TestConfig + services: T[] + test: TestSpec[] type: string variables: PrimitiveMap } +export const baseServiceSchema = Joi.object() + .keys({ + dependencies: Joi.array().items((joiIdentifier())).default(() => [], "[]"), + }) + .options({ allowUnknown: true }) + +export const baseDependencySchema = Joi.object().keys({ + name: joiIdentifier().required(), + copy: Joi.array().items(copySchema).default(() => [], "[]"), +}) + +export const baseModuleSchema = Joi.object().keys({ + type: joiIdentifier().required(), + name: joiIdentifier(), + description: Joi.string(), + variables: joiVariables(), + services: joiArray(baseServiceSchema).unique("name"), + allowPush: Joi.boolean() + .default(true, "Set to false to disable pushing this module to remote registries"), + build: Joi.object().keys({ + command: Joi.string(), + dependencies: Joi.array().items(baseDependencySchema).default(() => [], "[]"), + }).default(() => ({ dependencies: [] }), "{}"), + test: joiArray(baseTestSpecSchema).unique("name"), +}).required().unknown(true) + export class Module { public name: string public type: string @@ -121,7 +151,7 @@ export class Module { const config = extend({}, this.config) config.build = await resolveTemplateStrings(config.build, templateContext) - config.test = await resolveTemplateStrings(config.test, templateContext) + config.test = await Bluebird.map(config.test, t => resolveTemplateStrings(t, templateContext)) config.variables = await resolveTemplateStrings(config.variables, templateContext) return config @@ -178,7 +208,7 @@ export class Module { } // TODO: Detect circular dependencies - const modules = await this.ctx.getModules() + const modules = keyBy(await this.ctx.getModules(), "name") const deps: Module[] = [] for (let dependencyConfig of this.config.build.dependencies) { @@ -200,8 +230,8 @@ export class Module { return deps } - async getServices(): Promise { - const serviceNames = keys(this.services || {}) + async getServices(): Promise { + const serviceNames = getNames(this.services) return this.ctx.getServices(serviceNames) } @@ -211,7 +241,7 @@ export class Module { const services = await this.getServices() const module = this - return values(services).map(s => new DeployTask(module.ctx, s, force, forceBuild)) + return services.map(s => new DeployTask(module.ctx, s, force, forceBuild)) } async getTestTasks( @@ -220,12 +250,11 @@ export class Module { const tasks: TestTask>[] = [] const config = await this.getConfig() - for (const testName of Object.keys(config.test)) { - if (group && testName !== group) { + for (const test of config.test) { + if (group && test.name !== group) { continue } - const testSpec = config.test[testName] - tasks.push(new TestTask>(this.ctx, this, testName, testSpec, force, forceBuild)) + tasks.push(new TestTask>(this.ctx, this, test, force, forceBuild)) } return tasks @@ -279,40 +308,3 @@ export class Module { } export type ModuleConfigType = T["_ConfigType"] - -export const baseServiceSchema = Joi.object() - .keys({ - dependencies: Joi.array().items((joiIdentifier())).default(() => [], "[]"), - }) - .options({ allowUnknown: true }) - -export const baseServicesSchema = Joi.object() - .pattern(identifierRegex, baseServiceSchema) - .default(() => ({}), "{}") - -export const baseTestSpecSchema = Joi.object().keys({ - command: Joi.array().items(Joi.string()).required(), - dependencies: Joi.array().items(Joi.string()).default(() => [], "[]"), - variables: joiVariables(), - timeout: Joi.number(), -}) - -export const baseDependencySchema = Joi.object().keys({ - name: joiIdentifier().required(), - copy: Joi.array().items(copySchema).default(() => [], "[]"), -}) - -export const baseModuleSchema = Joi.object().keys({ - type: joiIdentifier().required(), - name: joiIdentifier(), - description: Joi.string(), - variables: joiVariables(), - services: baseServicesSchema, - allowPush: Joi.boolean() - .default(true, "Set to false to disable pushing this module to remote registries"), - build: Joi.object().keys({ - command: Joi.string(), - dependencies: Joi.array().items(baseDependencySchema).default(() => [], "[]"), - }).default(() => ({ dependencies: [] }), "{}"), - test: Joi.object().pattern(/[\w\d]+/i, baseTestSpecSchema).default(() => ({}), "{}"), -}).required().unknown(true) diff --git a/src/types/plugin.ts b/src/types/plugin.ts index 4378f06279..e2d9a1dab1 100644 --- a/src/types/plugin.ts +++ b/src/types/plugin.ts @@ -103,7 +103,6 @@ export interface TestModuleParams extends PluginActio interactive: boolean runtimeContext: RuntimeContext silent: boolean - testName: string testSpec: TestSpec } diff --git a/src/types/project.ts b/src/types/project.ts index 5a5d8c48c5..1b99c597b1 100644 --- a/src/types/project.ts +++ b/src/types/project.ts @@ -8,44 +8,58 @@ import * as Joi from "joi" import { - identifierRegex, + joiArray, joiIdentifier, joiVariables, Primitive, } from "./common" -export const defaultProviders = { - container: {}, -} - -export const defaultEnvironments = { - local: { - providers: { - "local-kubernetes": { - context: "docker-for-desktop", - }, - }, - }, +export interface ProviderConfig { + name: string + [key: string]: any } -export interface EnvironmentConfig { +export interface CommonEnvironmentConfig { configurationHandler?: string - providers: { [key: string]: any } // further validated by each plugin + providers: ProviderConfig[] // further validated by each plugin variables: { [key: string]: Primitive } } +export interface EnvironmentConfig extends CommonEnvironmentConfig { + name: string +} + export interface ProjectConfig { name: string defaultEnvironment: string - global: EnvironmentConfig - environments: { [key: string]: EnvironmentConfig } + global: CommonEnvironmentConfig + environments: EnvironmentConfig[] } -export const providerConfigBase = Joi.object().unknown(true) +export const defaultProviders = [ + { name: "container" }, +] + +export const defaultEnvironments: EnvironmentConfig[] = [ + { + name: "local", + providers: [ + { + name: "local-kubernetes", + context: "docker-for-desktop", + }, + ], + variables: {}, + }, +] + +export const providerConfigBase = Joi.object().keys({ + name: joiIdentifier().required(), +}).unknown(true) export const environmentSchema = Joi.object().keys({ configurationHandler: joiIdentifier(), - providers: Joi.object().pattern(identifierRegex, providerConfigBase), + providers: joiArray(providerConfigBase).unique("name"), variables: joiVariables(), }) @@ -58,7 +72,7 @@ export const projectSchema = Joi.object().keys({ name: joiIdentifier().required(), defaultEnvironment: Joi.string().default("", ""), global: environmentSchema.default(() => defaultGlobal, JSON.stringify(defaultGlobal)), - environments: Joi.object() - .pattern(identifierRegex, environmentSchema) + environments: joiArray(environmentSchema.keys({ name: joiIdentifier().required() })) + .unique("name") .default(() => ({ ...defaultEnvironments }), JSON.stringify(defaultEnvironments)), }).required() diff --git a/src/types/service.ts b/src/types/service.ts index 917b071a93..5d58a35c2b 100644 --- a/src/types/service.ts +++ b/src/types/service.ts @@ -8,6 +8,7 @@ import Bluebird = require("bluebird") import { PluginContext } from "../plugin-context" +import { findByName } from "../util" import { Module } from "./module" import { PrimitiveMap } from "./common" import { ConfigurationError } from "../exceptions" @@ -26,6 +27,7 @@ export interface ServiceEndpoint { } export interface ServiceConfig { + name: string dependencies: string[] } @@ -56,14 +58,14 @@ export type RuntimeContext = { export class Service { constructor( protected ctx: PluginContext, public module: M, - public name: string, public config: M["services"][string], + public name: string, public config: M["services"][0], ) { } static async factory, M extends Module>( this: (new (ctx: PluginContext, module: M, name: string, config: S["config"]) => S), ctx: PluginContext, module: M, name: string, ) { - const config = module.services[name] + const config = findByName(module.services, name) if (!config) { throw new ConfigurationError(`Could not find service ${name} in module ${module.name}`, { module, name }) diff --git a/src/util.ts b/src/util.ts index 19e0150af7..e291dd950c 100644 --- a/src/util.ts +++ b/src/util.ts @@ -18,6 +18,7 @@ import * as inquirer from "inquirer" import { spawn as _spawn } from "child_process" import { existsSync, readFileSync, writeFileSync } from "fs" import { join } from "path" +import { find } from "lodash" import { getLogger, RootLogNode } from "./logger" import { TimeoutError, @@ -384,3 +385,15 @@ export async function loadYamlFile(path: string): Promise { const fileData = await readFile(path) return yaml.safeLoad(fileData.toString()) } + +export interface ObjectWithName { + name: string +} + +export function getNames(array: T[]) { + return array.map(v => v.name) +} + +export function findByName(array: T[], name: string): T | undefined { + return find(array, ["name", name]) +} diff --git a/static/kubernetes/system/default-backend/garden.yml b/static/kubernetes/system/default-backend/garden.yml index 42c252374c..dffcedeb60 100644 --- a/static/kubernetes/system/default-backend/garden.yml +++ b/static/kubernetes/system/default-backend/garden.yml @@ -4,9 +4,9 @@ module: type: container image: gcr.io/google_containers/defaultbackend:1.3 services: - default-backend: + - name: default-backend ports: - http: + - name: http containerPort: 8080 # restart: Always healthCheck: diff --git a/static/kubernetes/system/ingress-controller/garden.yml b/static/kubernetes/system/ingress-controller/garden.yml index 5663ead401..1873c16d33 100644 --- a/static/kubernetes/system/ingress-controller/garden.yml +++ b/static/kubernetes/system/ingress-controller/garden.yml @@ -4,7 +4,7 @@ module: type: container image: quay.io/kubernetes-ingress-controller/nginx-ingress-controller:0.9.0-beta.19 services: - ingress-controller: + - name: ingress-controller command: - /nginx-ingress-controller - --default-backend-service=garden-system/default-backend @@ -15,7 +15,7 @@ module: # - --configmap=garden-system/nginx-configuration daemon: true ports: - http: + - name: http containerPort: 80 nodePort: 32000 healthCheck: diff --git a/static/kubernetes/system/kubernetes-dashboard/garden.yml b/static/kubernetes/system/kubernetes-dashboard/garden.yml index 16335f3bcb..90d903ce87 100644 --- a/static/kubernetes/system/kubernetes-dashboard/garden.yml +++ b/static/kubernetes/system/kubernetes-dashboard/garden.yml @@ -3,7 +3,7 @@ module: name: kubernetes-dashboard type: kubernetes-specs services: - kubernetes-dashboard: + - name: kubernetes-dashboard specs: # ------------------- Dashboard Secret ------------------- # - apiVersion: v1 diff --git a/test/data/test-project-a/garden.yml b/test/data/test-project-a/garden.yml index bbeaab2dea..ed931cfda4 100644 --- a/test/data/test-project-a/garden.yml +++ b/test/data/test-project-a/garden.yml @@ -4,8 +4,8 @@ project: variables: some: variable environments: - local: + - name: local providers: - test-plugin: {} - test-plugin-b: {} - other: {} + - name: test-plugin + - name: test-plugin-b + - name: other diff --git a/test/data/test-project-a/module-a/garden.yml b/test/data/test-project-a/module-a/garden.yml index 8320321324..8f189f3d35 100644 --- a/test/data/test-project-a/module-a/garden.yml +++ b/test/data/test-project-a/module-a/garden.yml @@ -2,9 +2,9 @@ module: name: module-a type: generic services: - service-a: {} + - name: service-a build: command: echo A test: - unit: + - name: unit command: [echo, OK] diff --git a/test/data/test-project-a/module-b/garden.yml b/test/data/test-project-a/module-b/garden.yml index 24cfb6ad68..2f1dad50a5 100644 --- a/test/data/test-project-a/module-b/garden.yml +++ b/test/data/test-project-a/module-b/garden.yml @@ -2,7 +2,7 @@ module: name: module-b type: generic services: - service-b: + - name: service-b dependencies: - service-a build: @@ -10,5 +10,5 @@ module: dependencies: - module-a test: - unit: + - name: unit command: [echo, OK] diff --git a/test/data/test-project-a/module-c/garden.yml b/test/data/test-project-a/module-c/garden.yml index fdaed4cf6a..8a5162c30d 100644 --- a/test/data/test-project-a/module-c/garden.yml +++ b/test/data/test-project-a/module-c/garden.yml @@ -2,10 +2,10 @@ module: name: module-c type: generic services: - service-c: {} + - name: service-c build: dependencies: - module-b test: - unit: + - name: unit command: [echo, OK] diff --git a/test/data/test-project-b/garden.yml b/test/data/test-project-b/garden.yml index 153a3a3832..fc289ee552 100644 --- a/test/data/test-project-b/garden.yml +++ b/test/data/test-project-b/garden.yml @@ -1,6 +1,6 @@ project: name: test-project-b environments: - local: + - name: local providers: - test-plugin: {} + - name: test-plugin diff --git a/test/data/test-project-b/module-a/garden.yml b/test/data/test-project-b/module-a/garden.yml index 7a71880688..2498a31afc 100644 --- a/test/data/test-project-b/module-a/garden.yml +++ b/test/data/test-project-b/module-a/garden.yml @@ -2,7 +2,7 @@ module: name: module-a type: generic services: - service-a: + - name: service-a endpoints: - paths: [/path-a] containerPort: 8080 diff --git a/test/data/test-project-b/module-b/garden.yml b/test/data/test-project-b/module-b/garden.yml index 4fc7b9b6c1..c7b20ba9f6 100644 --- a/test/data/test-project-b/module-b/garden.yml +++ b/test/data/test-project-b/module-b/garden.yml @@ -2,7 +2,7 @@ module: name: module-b type: generic services: - service-b: + - name: service-b endpoints: - paths: [/path-b] containerPort: 8080 diff --git a/test/data/test-project-b/module-c/garden.yml b/test/data/test-project-b/module-c/garden.yml index 644230eb8a..97a5ea50c6 100644 --- a/test/data/test-project-b/module-c/garden.yml +++ b/test/data/test-project-b/module-c/garden.yml @@ -3,7 +3,7 @@ module: type: generic allowPush: false services: - service-c: + - name: service-c endpoints: - paths: [/path-c] containerPort: 8080 diff --git a/test/data/test-project-container/garden.yml b/test/data/test-project-container/garden.yml index dd3d2e9f6e..51767fa146 100644 --- a/test/data/test-project-container/garden.yml +++ b/test/data/test-project-container/garden.yml @@ -1,7 +1,7 @@ project: name: container-module-test-project environments: - local: + - name: local providers: - test-plugin: {} - container: {} + - name: test-plugin + - name: container diff --git a/test/data/test-project-container/module-a/garden.yml b/test/data/test-project-container/module-a/garden.yml index 40f5fed413..0f480b1c47 100644 --- a/test/data/test-project-container/module-a/garden.yml +++ b/test/data/test-project-container/module-a/garden.yml @@ -2,9 +2,9 @@ module: name: module-a type: container services: - service-a: + - name: service-a ports: - http: + - name: http containerPort: 8080 endpoints: - paths: [/] diff --git a/test/data/test-project-missing-provider/garden.yml b/test/data/test-project-missing-provider/garden.yml index 4b3c5930c5..5e5576b329 100644 --- a/test/data/test-project-missing-provider/garden.yml +++ b/test/data/test-project-missing-provider/garden.yml @@ -1,7 +1,7 @@ project: name: build-test-project environments: - test: + - name: test providers: - test-plugin: {} - test-plugin-b: {} + - name: test-plugin + - name: test-plugin-b diff --git a/test/data/test-project-templated/garden.yml b/test/data/test-project-templated/garden.yml index e0647f955c..ad55a33764 100644 --- a/test/data/test-project-templated/garden.yml +++ b/test/data/test-project-templated/garden.yml @@ -5,6 +5,6 @@ project: some: ${local.env.TEST_VARIABLE} service-a-build-command: echo OK environments: - local: + - name: local providers: - test-plugin: {} + - name: test-plugin diff --git a/test/data/test-project-templated/module-a/garden.yml b/test/data/test-project-templated/module-a/garden.yml index 97919223f4..b547eea158 100644 --- a/test/data/test-project-templated/module-a/garden.yml +++ b/test/data/test-project-templated/module-a/garden.yml @@ -2,10 +2,10 @@ module: name: module-a type: generic services: - service-a: + - name: service-a command: echo ${local.env.TEST_VARIABLE} build: command: ${variables.service-a-build-command} test: - unit: + - name: unit command: [echo, OK] diff --git a/test/data/test-project-templated/module-b/garden.yml b/test/data/test-project-templated/module-b/garden.yml index 9c7672a3b3..9a0772734f 100644 --- a/test/data/test-project-templated/module-b/garden.yml +++ b/test/data/test-project-templated/module-b/garden.yml @@ -2,12 +2,12 @@ module: name: module-b type: generic services: - service-b: + - name: service-b command: echo ${dependencies.service-a.version} dependencies: - service-a build: command: ${variables.service-a-build-command} test: - unit: + - name: unit command: [echo, "${config.project.my.variable}"] diff --git a/test/helpers.ts b/test/helpers.ts index e29d697bb9..d3e411d1e3 100644 --- a/test/helpers.ts +++ b/test/helpers.ts @@ -1,7 +1,6 @@ import * as td from "testdouble" import { resolve } from "path" import { PluginContext } from "../src/plugin-context" -import { ContainerModule } from "../src/plugins/container" import { TaskResults } from "../src/task-graph" import { DeleteConfigParams, @@ -131,10 +130,13 @@ export const defaultModuleConfig: ModuleConfig = { allowPush: false, variables: {}, build: { dependencies: [] }, - services: { - testService: { dependencies: [] }, - }, - test: {}, + services: [ + { + name: "testService", + dependencies: [], + }, + ], + test: [], } export const makeTestModule = (ctx: PluginContext, params: Partial = {}) => { diff --git a/test/src/build-dir.ts b/test/src/build-dir.ts index 8826ba6b9c..240019ec0d 100644 --- a/test/src/build-dir.ts +++ b/test/src/build-dir.ts @@ -2,9 +2,9 @@ const nodetree = require("nodetree") import { join } from "path" import { pathExists, readdir } from "fs-extra" import { expect } from "chai" -import { values } from "lodash" import { BuildTask } from "../../src/tasks/build" import { makeTestGarden } from "../helpers" +import { keyBy } from "lodash" /* Module dependency diagram for test-project-build-products @@ -40,16 +40,14 @@ describe("BuildDir", () => { it("should ensure that a module's build subdir exists before returning from buildPath", async () => { const garden = await makeGarden() await garden.buildDir.clear() - const modules = await garden.getModules() - const moduleA = modules["module-a"] + const moduleA = await garden.getModule("module-a") const buildPath = await garden.buildDir.buildPath(moduleA) expect(await pathExists(buildPath)).to.eql(true) }) it("should sync sources to the build dir", async () => { const garden = await makeGarden() - const modules = await garden.getModules() - const moduleA = modules["module-a"] + const moduleA = await garden.getModule("module-a") await garden.buildDir.syncFromSrc(moduleA) const buildDirA = await garden.buildDir.buildPath(moduleA) @@ -71,14 +69,15 @@ describe("BuildDir", () => { await garden.clearBuilds() const modules = await garden.getModules() - for (const module of values(modules)) { + for (const module of modules) { await garden.addTask(new BuildTask(garden.pluginContext, module, false)) } await garden.processTasks() + const modulesByName = keyBy(modules, "name") - const buildDirD = await garden.buildDir.buildPath(modules["module-d"]) - const buildDirE = await garden.buildDir.buildPath(modules["module-e"]) + const buildDirD = await garden.buildDir.buildPath(modulesByName["module-d"]) + const buildDirE = await garden.buildDir.buildPath(modulesByName["module-e"]) // All these destinations should be populated now. const buildProductDestinations = [ diff --git a/test/src/commands/data/auto-reload-project/garden.yml b/test/src/commands/data/auto-reload-project/garden.yml index 70b96901ca..35c2ec9b15 100644 --- a/test/src/commands/data/auto-reload-project/garden.yml +++ b/test/src/commands/data/auto-reload-project/garden.yml @@ -1,7 +1,7 @@ project: name: auto-reload-project environments: - local: + - name: local providers: - test: + - name: test type: test-plugin diff --git a/test/src/commands/data/auto-reload-project/module-b/garden.yml b/test/src/commands/data/auto-reload-project/module-b/garden.yml index 35ee078b3e..9d3fb9bcd5 100644 --- a/test/src/commands/data/auto-reload-project/module-b/garden.yml +++ b/test/src/commands/data/auto-reload-project/module-b/garden.yml @@ -2,7 +2,7 @@ module: name: module-b type: generic services: - service-b: + - name: service-b endpoints: - paths: [/path-b] containerPort: 8080 diff --git a/test/src/commands/data/auto-reload-project/module-c/garden.yml b/test/src/commands/data/auto-reload-project/module-c/garden.yml index e5fefe9260..a4e081519e 100644 --- a/test/src/commands/data/auto-reload-project/module-c/garden.yml +++ b/test/src/commands/data/auto-reload-project/module-c/garden.yml @@ -2,7 +2,7 @@ module: name: module-c type: generic services: - service-c: + - name: service-c endpoints: - paths: [/path-c] containerPort: 8080 diff --git a/test/src/commands/data/auto-reload-project/module-d/garden.yml b/test/src/commands/data/auto-reload-project/module-d/garden.yml index b6a2edd0ae..d3ecd4902c 100644 --- a/test/src/commands/data/auto-reload-project/module-d/garden.yml +++ b/test/src/commands/data/auto-reload-project/module-d/garden.yml @@ -2,7 +2,7 @@ module: name: module-d type: generic services: - service-d: + - name: service-d dependencies: - service-b endpoints: diff --git a/test/src/commands/data/auto-reload-project/module-e/garden.yml b/test/src/commands/data/auto-reload-project/module-e/garden.yml index 054c493123..484c94061f 100644 --- a/test/src/commands/data/auto-reload-project/module-e/garden.yml +++ b/test/src/commands/data/auto-reload-project/module-e/garden.yml @@ -2,7 +2,7 @@ module: name: module-e type: generic services: - service-e: + - name: service-e dependencies: - service-b - service-c diff --git a/test/src/commands/data/auto-reload-project/module-f/garden.yml b/test/src/commands/data/auto-reload-project/module-f/garden.yml index 171c51fed8..8a8aa784cd 100644 --- a/test/src/commands/data/auto-reload-project/module-f/garden.yml +++ b/test/src/commands/data/auto-reload-project/module-f/garden.yml @@ -2,7 +2,7 @@ module: name: module-f type: generic services: - service-f: + - name: service-f dependencies: - service-c endpoints: diff --git a/test/src/garden.ts b/test/src/garden.ts index 5249d3f3c4..dabb4f2a15 100644 --- a/test/src/garden.ts +++ b/test/src/garden.ts @@ -4,7 +4,6 @@ import { expect } from "chai" import { dataDir, expectError, - makeTestContextA, makeTestGarden, makeTestGardenA, makeTestModule, @@ -12,6 +11,7 @@ import { testPlugin, testPluginB, } from "../helpers" +import { getNames } from "../../src/util" describe("Garden", () => { describe("factory", () => { @@ -44,10 +44,11 @@ describe("Garden", () => { expect(ctx.projectName).to.equal("test-project-a") expect(ctx.config).to.eql({ - providers: { - "test-plugin": {}, - "test-plugin-b": {}, - }, + name: "local", + providers: [ + { name: "test-plugin" }, + { name: "test-plugin-b" }, + ], variables: { some: "variable", }, @@ -66,9 +67,10 @@ describe("Garden", () => { delete process.env.TEST_VARIABLE expect(ctx.config).to.eql({ - providers: { - "test-plugin": {}, - }, + name: "local", + providers: [ + { name: "test-plugin" }, + ], variables: { some: "banana", "service-a-build-command": "echo OK", @@ -142,14 +144,14 @@ describe("Garden", () => { const ctx = await makeTestGardenA() const modules = await ctx.getModules() - expect(Object.keys(modules)).to.eql(["module-a", "module-b", "module-c"]) + expect(getNames(modules)).to.eql(["module-a", "module-b", "module-c"]) }) it("should optionally return specified modules in the context", async () => { const ctx = await makeTestGardenA() const modules = await ctx.getModules(["module-b", "module-c"]) - expect(Object.keys(modules)).to.eql(["module-b", "module-c"]) + expect(getNames(modules)).to.eql(["module-b", "module-c"]) }) it("should throw if named module is missing", async () => { @@ -171,14 +173,14 @@ describe("Garden", () => { const ctx = await makeTestGardenA() const services = await ctx.getServices() - expect(Object.keys(services)).to.eql(["service-a", "service-b", "service-c"]) + expect(getNames(services)).to.eql(["service-a", "service-b", "service-c"]) }) it("should optionally return specified services in the context", async () => { const ctx = await makeTestGardenA() const services = await ctx.getServices(["service-b", "service-c"]) - expect(Object.keys(services)).to.eql(["service-b", "service-c"]) + expect(getNames(services)).to.eql(["service-b", "service-c"]) }) it("should throw if named service is missing", async () => { @@ -225,7 +227,7 @@ describe("Garden", () => { await garden.scanModules() const modules = await garden.getModules(undefined, true) - expect(Object.keys(modules)).to.eql(["module-a", "module-b", "module-c"]) + expect(getNames(modules)).to.eql(["module-a", "module-b", "module-c"]) }) }) @@ -237,10 +239,10 @@ describe("Garden", () => { await garden.addModule(testModule) const modules = await garden.getModules(undefined, true) - expect(Object.keys(modules)).to.eql(["test"]) + expect(getNames(modules)).to.eql(["test"]) const services = await garden.getServices(undefined, true) - expect(Object.keys(services)).to.eql(["testService"]) + expect(getNames(services)).to.eql(["testService"]) }) it("should throw when adding module twice without force parameter", async () => { @@ -269,7 +271,7 @@ describe("Garden", () => { await garden.addModule(testModule, true) const modules = await garden.getModules(undefined, true) - expect(Object.keys(modules)).to.eql(["test"]) + expect(getNames(modules)).to.eql(["test"]) }) it("should throw if a service is added twice without force parameter", async () => { @@ -300,7 +302,7 @@ describe("Garden", () => { await garden.addModule(testModuleB, true) const services = await ctx.getServices(undefined, true) - expect(Object.keys(services)).to.eql(["testService"]) + expect(getNames(services)).to.eql(["testService"]) }) }) @@ -355,10 +357,11 @@ describe("Garden", () => { expect(result.environment).to.eql({ name: "local", config: { - providers: { - "test-plugin": {}, - "test-plugin-b": {}, - }, + name: "local", + providers: [ + { name: "test-plugin" }, + { name: "test-plugin-b" }, + ], variables: { some: "variable", }, @@ -378,10 +381,11 @@ describe("Garden", () => { expect(result.environment).to.eql({ name: "local", config: { - providers: { - "test-plugin": {}, - "test-plugin-b": {}, - }, + name: "local", + providers: [ + { name: "test-plugin" }, + { name: "test-plugin-b" }, + ], variables: { some: "variable", }, diff --git a/test/src/plugins/container.ts b/test/src/plugins/container.ts index ebee00103e..48c8f0b57e 100644 --- a/test/src/plugins/container.ts +++ b/test/src/plugins/container.ts @@ -52,8 +52,8 @@ describe("container", () => { name: "test", path: modulePath, image: "some/image:1.1", - services: {}, - test: {}, + services: [], + test: [], type: "container", variables: {}, }) @@ -72,8 +72,8 @@ describe("container", () => { name: "test", path: modulePath, image: "some/image:1.1", - services: {}, - test: {}, + services: [], + test: [], type: "container", variables: {}, }) @@ -93,8 +93,8 @@ describe("container", () => { name: "test", path: modulePath, image: "some/image:1.1", - services: {}, - test: {}, + services: [], + test: [], type: "container", variables: {}, }) @@ -111,8 +111,8 @@ describe("container", () => { name: "test", path: modulePath, image: "some/image", - services: {}, - test: {}, + services: [], + test: [], type: "container", variables: {}, }) @@ -129,8 +129,8 @@ describe("container", () => { }, name: "test", path: modulePath, - services: {}, - test: {}, + services: [], + test: [], type: "container", variables: {}, }) @@ -152,39 +152,36 @@ describe("container", () => { }, name: "module-a", path: modulePath, - services: { - "service-a": { - command: ["echo"], - dependencies: [], - daemon: false, - endpoints: [ - { - paths: ["/"], - port: "http", - }, - ], - healthCheck: { - httpGet: { - path: "/health", - port: "http", - }, + services: [{ + name: "service-a", + command: ["echo"], + dependencies: [], + daemon: false, + endpoints: [ + { + paths: ["/"], + port: "http", }, - ports: { - http: { - protocol: "TCP", - containerPort: 8080, - }, + ], + healthCheck: { + httpGet: { + path: "/health", + port: "http", }, - volumes: [], - }, - }, - test: { - unit: { - command: ["echo", "OK"], - dependencies: [], - variables: {}, }, - }, + ports: [{ + name: "http", + protocol: "TCP", + containerPort: 8080, + }], + volumes: [], + }], + test: [{ + name: "unit", + command: ["echo", "OK"], + dependencies: [], + variables: {}, + }], type: "test", variables: {}, } @@ -201,28 +198,26 @@ describe("container", () => { }, name: "module-a", path: modulePath, - services: { - "service-a": { - command: ["echo"], - dependencies: [], - daemon: false, - endpoints: [ - { - paths: ["/"], - port: "bla", - }, - ], - ports: {}, - volumes: [], - }, - }, - test: { - unit: { - command: ["echo", "OK"], - dependencies: [], - variables: {}, - }, - }, + services: [{ + name: "service-a", + command: ["echo"], + dependencies: [], + daemon: false, + endpoints: [ + { + paths: ["/"], + port: "bla", + }, + ], + ports: [], + volumes: [], + }], + test: [{ + name: "unit", + command: ["echo", "OK"], + dependencies: [], + variables: {}, + }], type: "test", variables: {}, } @@ -242,29 +237,22 @@ describe("container", () => { }, name: "module-a", path: modulePath, - services: { - "service-a": { - command: ["echo"], - dependencies: [], - daemon: false, - endpoints: [], - healthCheck: { - httpGet: { - path: "/", - port: "bla", - }, + services: [{ + name: "service-a", + command: ["echo"], + dependencies: [], + daemon: false, + endpoints: [], + healthCheck: { + httpGet: { + path: "/", + port: "bla", }, - ports: {}, - volumes: [], }, - }, - test: { - unit: { - command: ["echo", "OK"], - dependencies: [], - variables: {}, - }, - }, + ports: [], + volumes: [], + }], + test: [], type: "test", variables: {}, } @@ -284,26 +272,19 @@ describe("container", () => { }, name: "module-a", path: modulePath, - services: { - "service-a": { - command: ["echo"], - dependencies: [], - daemon: false, - endpoints: [], - healthCheck: { - tcpPort: "bla", - }, - ports: {}, - volumes: [], - }, - }, - test: { - unit: { - command: ["echo", "OK"], - dependencies: [], - variables: {}, + services: [{ + name: "service-a", + command: ["echo"], + dependencies: [], + daemon: false, + endpoints: [], + healthCheck: { + tcpPort: "bla", }, - }, + ports: [], + volumes: [], + }], + test: [], type: "test", variables: {}, } @@ -324,8 +305,8 @@ describe("container", () => { }, name: "test", path: modulePath, - services: {}, - test: {}, + services: [], + test: [], type: "container", variables: {}, })) @@ -345,8 +326,8 @@ describe("container", () => { }, name: "test", path: modulePath, - services: {}, - test: {}, + services: [], + test: [], type: "container", variables: {}, })) @@ -369,8 +350,8 @@ describe("container", () => { name: "test", path: modulePath, image: "some/image", - services: {}, - test: {}, + services: [], + test: [], type: "container", variables: {}, })) @@ -392,8 +373,8 @@ describe("container", () => { name: "test", path: modulePath, image: "some/image", - services: {}, - test: {}, + services: [], + test: [], type: "container", variables: {}, })) @@ -422,8 +403,8 @@ describe("container", () => { name: "test", path: modulePath, image: "some/image", - services: {}, - test: {}, + services: [], + test: [], type: "container", variables: {}, })) @@ -444,8 +425,8 @@ describe("container", () => { name: "test", path: modulePath, image: "some/image:1.1", - services: {}, - test: {}, + services: [], + test: [], type: "container", variables: {}, })) @@ -471,8 +452,8 @@ describe("container", () => { name: "test", path: modulePath, image: "some/image:1.1", - services: {}, - test: {}, + services: [], + test: [], type: "container", variables: {}, })) diff --git a/test/src/tasks/deploy.ts b/test/src/tasks/deploy.ts index 821491e843..eaf6fe8748 100644 --- a/test/src/tasks/deploy.ts +++ b/test/src/tasks/deploy.ts @@ -41,6 +41,7 @@ describe("DeployTask", () => { const { versionString } = await serviceA.module.getVersion() expect(actionParams.service.config).to.eql({ + name: "service-b", command: `echo ${versionString}`, dependencies: ["service-a"], }) diff --git a/test/src/types/config.ts b/test/src/types/config.ts index 5faf9d3dff..744bd35a04 100644 --- a/test/src/types/config.ts +++ b/test/src/types/config.ts @@ -15,18 +15,24 @@ describe("loadConfig", () => { name: "test-project-a", defaultEnvironment: "local", global: { + providers: [], variables: { some: "variable" }, }, - environments: { - local: { - providers: { - "test-plugin": {}, - "test-plugin-b": {}, - }, + environments: [ + { + name: "local", + providers: [ + { name: "test-plugin" }, + { name: "test-plugin-b" }, + ], variables: {}, }, - other: { variables: {} }, - }, + { + name: "other", + providers: [], + variables: {}, + }, + ], }) }) @@ -37,15 +43,14 @@ describe("loadConfig", () => { name: "module-a", type: "generic", allowPush: true, - services: { "service-a": { dependencies: [] } }, + services: [{ name: "service-a", dependencies: [] }], build: { command: "echo A", dependencies: [] }, - test: { - unit: { - command: ["echo", "OK"], - dependencies: [], - variables: {}, - }, - }, + test: [{ + name: "unit", + command: ["echo", "OK"], + dependencies: [], + variables: {}, + }], path: modulePathA, variables: {}, }) diff --git a/test/src/types/module.ts b/test/src/types/module.ts index cbc02488f9..81a41c9382 100644 --- a/test/src/types/module.ts +++ b/test/src/types/module.ts @@ -34,9 +34,12 @@ describe("Module", () => { build: { command: "echo OK", dependencies: [] }, name: "module-a", path: modulePath, - services: - { "service-a": { command: "echo \${local.env.TEST_VARIABLE}", dependencies: [] } }, - test: { unit: { command: ["echo", "OK"], dependencies: [], variables: {} } }, + services: [ + { name: "service-a", command: "echo \${local.env.TEST_VARIABLE}", dependencies: [] }, + ], + test: [ + { name: "unit", command: ["echo", "OK"], dependencies: [], variables: {} }, + ], type: "generic", variables: {}, }) diff --git a/test/src/types/service.ts b/test/src/types/service.ts index c4702187e7..3c0d77ba99 100644 --- a/test/src/types/service.ts +++ b/test/src/types/service.ts @@ -7,12 +7,12 @@ describe("Service", () => { describe("factory", () => { it("should create a Service instance with the given config", async () => { const ctx = await makeTestContextA() - const module = (await ctx.getModules(["module-a"]))["module-a"] + const module = await ctx.getModule("module-a") const service = await Service.factory(ctx, module, "service-a") expect(service.name).to.equal("service-a") - expect(service.config).to.eql(module.services["service-a"]) + expect(service.config).to.eql(module.services[0]) }) it("should resolve template strings", async () => { @@ -22,11 +22,11 @@ describe("Service", () => { const ctx = await makeTestContext(resolve(dataDir, "test-project-templated")) await ctx.setConfig(["project", "my", "variable"], "OK") - const module = (await ctx.getModules(["module-a"]))["module-a"] + const module = await ctx.getModule("module-a") const service = await Service.factory(ctx, module, "service-a") - expect(service.config).to.eql({ command: "echo banana", dependencies: [] }) + expect(service.config).to.eql({ name: "service-a", command: "echo banana", dependencies: [] }) }) }) @@ -61,6 +61,7 @@ describe("Service", () => { const resolved = await serviceB.resolveConfig() expect(resolved.config).to.eql({ + name: "service-b", command: `echo ${(await serviceA.module.getVersion()).versionString}`, dependencies: ["service-a"], })