diff --git a/garden-service/src/actions.ts b/garden-service/src/actions.ts index 79d2bb0b61..7d4b4c9f3e 100644 --- a/garden-service/src/actions.ts +++ b/garden-service/src/actions.ts @@ -97,6 +97,7 @@ import { DeleteServiceTask, deletedServiceStatuses } from "./tasks/delete-servic import { realpath, writeFile } from "fs-extra" import { relative, join } from "path" import { getArtifactKey } from "./util/artifacts" +import { AugmentGraphResult, AugmentGraphParams } from "./types/plugin/provider/augmentGraph" const maxArtifactLogLines = 5 // max number of artifacts to list in console after task+test runs @@ -181,7 +182,7 @@ export class ActionRouter implements TypeGuard { const handlerParams: PluginActionParams["configureProvider"] = { ...omit(params, ["pluginName"]), - base: this.wrapBase(handler.base), + base: this.wrapBase(handler!.base), } const result = (handler)(handlerParams) @@ -191,6 +192,17 @@ export class ActionRouter implements TypeGuard { return result } + async augmentGraph(params: RequirePluginName>): Promise { + const { pluginName } = params + + return this.callActionHandler({ + actionType: "augmentGraph", + pluginName, + params: omit(params, ["pluginName"]), + defaultHandler: async () => ({ addBuildDependencies: [], addRuntimeDependencies: [], addModules: [] }), + }) + } + async getEnvironmentStatus( params: RequirePluginName> & { ctx?: PluginContext } ): Promise { @@ -283,7 +295,11 @@ export class ActionRouter implements TypeGuard { } async build(params: ModuleActionRouterParams>): Promise { - return this.callModuleHandler({ params, actionType: "build" }) + return this.callModuleHandler({ + params, + actionType: "build", + defaultHandler: async () => ({}), + }) } async publishModule( @@ -516,7 +532,7 @@ export class ActionRouter implements TypeGuard { log: LogEntry serviceNames?: string[] }): Promise { - const graph = await this.garden.getConfigGraph() + const graph = await this.garden.getConfigGraph(log) const services = await graph.getServices(serviceNames) const tasks = services.map( @@ -540,7 +556,7 @@ export class ActionRouter implements TypeGuard { forceBuild = false, log, }: DeployServicesParams): Promise { - const graph = await this.garden.getConfigGraph() + const graph = await this.garden.getConfigGraph(log) const services = await graph.getServices(serviceNames) return processServices({ @@ -566,7 +582,7 @@ export class ActionRouter implements TypeGuard { * Deletes all services and cleans up the specified environment. */ async deleteEnvironment(log: LogEntry) { - const graph = await this.garden.getConfigGraph() + const graph = await this.garden.getConfigGraph(log) const servicesLog = log.info({ msg: chalk.white("Deleting services..."), status: "active" }) @@ -678,7 +694,7 @@ export class ActionRouter implements TypeGuard { }) const handlerParams: PluginActionParams[T] = { - ...(await this.commonParams(handler, params.log)), + ...(await this.commonParams(handler!, params.log)), ...(params), } @@ -746,7 +762,7 @@ export class ActionRouter implements TypeGuard { if (runtimeContext && (await getRuntimeTemplateReferences(module)).length > 0) { log.silly(`Resolving runtime template strings for service '${service.name}'`) const configContext = await this.garden.getModuleConfigContext(runtimeContext) - const graph = await this.garden.getConfigGraph({ configContext }) + const graph = await this.garden.getConfigGraph(log, { configContext }) service = await graph.getService(service.name) module = service.module @@ -803,7 +819,7 @@ export class ActionRouter implements TypeGuard { if (runtimeContext && (await getRuntimeTemplateReferences(module)).length > 0) { log.silly(`Resolving runtime template strings for task '${task.name}'`) const configContext = await this.garden.getModuleConfigContext(runtimeContext) - const graph = await this.garden.getConfigGraph({ configContext }) + const graph = await this.garden.getConfigGraph(log, { configContext }) task = await graph.getTask(task.name) module = task.module @@ -971,15 +987,17 @@ export class ActionRouter implements TypeGuard { /** * Get the last configured handler for the specified action (and optionally module type). */ - private async getActionHandler({ + async getActionHandler({ actionType, pluginName, defaultHandler, + throwIfMissing = true, }: { actionType: T pluginName: string defaultHandler?: PluginActionHandlers[T] - }): Promise { + throwIfMissing?: boolean + }): Promise { const handlers = Object.values(await this.getActionHandlers(actionType, pluginName)) // Since we only allow retrieving by plugin name, the length is always either 0 or 1 @@ -995,6 +1013,10 @@ export class ActionRouter implements TypeGuard { ) } + if (!throwIfMissing) { + return null + } + const errorDetails = { requestedHandlerType: actionType, environment: this.garden.environmentName, @@ -1013,9 +1035,9 @@ export class ActionRouter implements TypeGuard { } /** - * Get the last configured handler for the specified action. + * Get the configured handler for the specified action. */ - private async getModuleActionHandler({ + async getModuleActionHandler({ actionType, moduleType, pluginName, diff --git a/garden-service/src/commands/build.ts b/garden-service/src/commands/build.ts index 551d83cfbc..1b9505063b 100644 --- a/garden-service/src/commands/build.ts +++ b/garden-service/src/commands/build.ts @@ -15,12 +15,13 @@ import { StringsParameter, PrepareParams, } from "./base" -import { BuildTask } from "../tasks/build" +import { getBuildTasks } from "../tasks/build" import { TaskResults } from "../task-graph" import dedent = require("dedent") import { processModules } from "../process" import { printHeader } from "../logger/util" import { startServer, GardenServer } from "../server/server" +import { flatten } from "lodash" const buildArguments = { modules: new StringsParameter({ @@ -81,24 +82,25 @@ export class BuildCommand extends Command { await garden.clearBuilds() - const graph = await garden.getConfigGraph() + const graph = await garden.getConfigGraph(log) const modules = await graph.getModules(args.modules) const moduleNames = modules.map((m) => m.name) const results = await processModules({ garden, - graph: await garden.getConfigGraph(), + graph: await garden.getConfigGraph(log), log, footerLog, modules, watch: opts.watch, - handler: async (_, module) => [new BuildTask({ garden, log, module, force: opts.force })], + handler: async (_, module) => getBuildTasks({ garden, log, module, force: opts.force }), changeHandler: async (_, module) => { const dependantModules = (await graph.getDependants("build", module.name, true)).build - return [module] + const tasks = [module] .concat(dependantModules) .filter((m) => moduleNames.includes(m.name)) - .map((m) => new BuildTask({ garden, log, module: m, force: true })) + .map((m) => getBuildTasks({ garden, log, module: m, force: true })) + return flatten(await Promise.all(tasks)) }, }) diff --git a/garden-service/src/commands/call.ts b/garden-service/src/commands/call.ts index 0c2d708cb5..727d0dabe3 100644 --- a/garden-service/src/commands/call.ts +++ b/garden-service/src/commands/call.ts @@ -52,7 +52,7 @@ export class CallCommand extends Command { let [serviceName, path] = splitFirst(args.serviceAndPath, "/") // TODO: better error when service doesn't exist - const graph = await garden.getConfigGraph() + const graph = await garden.getConfigGraph(log) const service = await graph.getService(serviceName) // No need for full context, since we're just checking if the service is running. const runtimeContext = emptyRuntimeContext diff --git a/garden-service/src/commands/delete.ts b/garden-service/src/commands/delete.ts index b78092b8a0..5c97b8d455 100644 --- a/garden-service/src/commands/delete.ts +++ b/garden-service/src/commands/delete.ts @@ -126,7 +126,7 @@ export class DeleteServiceCommand extends Command { ` async action({ garden, log, headerLog, args }: CommandParams): Promise { - const graph = await garden.getConfigGraph() + const graph = await garden.getConfigGraph(log) const services = await graph.getServices(args.services) if (services.length === 0) { diff --git a/garden-service/src/commands/deploy.ts b/garden-service/src/commands/deploy.ts index 4e28f09e03..7a70fc9e92 100644 --- a/garden-service/src/commands/deploy.ts +++ b/garden-service/src/commands/deploy.ts @@ -102,7 +102,7 @@ export class DeployCommand extends Command { this.server.setGarden(garden) } - const initGraph = await garden.getConfigGraph() + const initGraph = await garden.getConfigGraph(log) const services = await initGraph.getServices(args.services) if (services.length === 0) { diff --git a/garden-service/src/commands/dev.ts b/garden-service/src/commands/dev.ts index 1cfa0966ef..3c0f03186c 100644 --- a/garden-service/src/commands/dev.ts +++ b/garden-service/src/commands/dev.ts @@ -105,7 +105,7 @@ export class DevCommand extends Command { async action({ garden, log, footerLog, opts }: CommandParams): Promise { this.server.setGarden(garden) - const graph = await garden.getConfigGraph() + const graph = await garden.getConfigGraph(log) const modules = await graph.getModules() if (modules.length === 0) { diff --git a/garden-service/src/commands/exec.ts b/garden-service/src/commands/exec.ts index db0bd6ee2f..cff93fb469 100644 --- a/garden-service/src/commands/exec.ts +++ b/garden-service/src/commands/exec.ts @@ -72,7 +72,7 @@ export class ExecCommand extends Command { "runner" ) - const graph = await garden.getConfigGraph() + const graph = await garden.getConfigGraph(log) const service = await graph.getService(serviceName) const actions = await garden.getActionRouter() const result = await actions.execInService({ diff --git a/garden-service/src/commands/get/get-config.ts b/garden-service/src/commands/get/get-config.ts index 51c09404be..7e2870acc9 100644 --- a/garden-service/src/commands/get/get-config.ts +++ b/garden-service/src/commands/get/get-config.ts @@ -14,7 +14,7 @@ export class GetConfigCommand extends Command { help = "Outputs the fully resolved configuration for this project and environment." async action({ garden, log }: CommandParams): Promise> { - const config = await garden.dumpConfig() + const config = await garden.dumpConfig(log) // TODO: do a nicer print of this by default log.info({ data: config }) diff --git a/garden-service/src/commands/get/get-graph.ts b/garden-service/src/commands/get/get-graph.ts index 22d93faf83..b782a2f60a 100644 --- a/garden-service/src/commands/get/get-graph.ts +++ b/garden-service/src/commands/get/get-graph.ts @@ -19,7 +19,7 @@ export class GetGraphCommand extends Command { help = "Outputs the dependency relationships specified in this project's garden.yml files." async action({ garden, log }: CommandParams): Promise> { - const graph = await garden.getConfigGraph() + const graph = await garden.getConfigGraph(log) const renderedGraph = graph.render() const output: GraphOutput = { nodes: renderedGraph.nodes, diff --git a/garden-service/src/commands/get/get-status.ts b/garden-service/src/commands/get/get-status.ts index 63c81800d3..a57a1ce328 100644 --- a/garden-service/src/commands/get/get-status.ts +++ b/garden-service/src/commands/get/get-status.ts @@ -50,7 +50,7 @@ export class GetStatusCommand extends Command { let result: AllEnvironmentStatus if (opts.output) { - const graph = await garden.getConfigGraph() + const graph = await garden.getConfigGraph(log) result = await Bluebird.props({ ...status, tests: getTestStatuses(garden, graph, log), diff --git a/garden-service/src/commands/get/get-task-result.ts b/garden-service/src/commands/get/get-task-result.ts index e9f510c614..e386952570 100644 --- a/garden-service/src/commands/get/get-task-result.ts +++ b/garden-service/src/commands/get/get-task-result.ts @@ -43,7 +43,7 @@ export class GetTaskResultCommand extends Command { }: CommandParams): Promise> { const taskName = args.name - const graph: ConfigGraph = await garden.getConfigGraph() + const graph: ConfigGraph = await garden.getConfigGraph(log) const task = await graph.getTask(taskName) const actions = await garden.getActionRouter() diff --git a/garden-service/src/commands/get/get-tasks.ts b/garden-service/src/commands/get/get-tasks.ts index df4fb1948c..40307d2e05 100644 --- a/garden-service/src/commands/get/get-tasks.ts +++ b/garden-service/src/commands/get/get-tasks.ts @@ -61,7 +61,7 @@ export class GetTasksCommand extends Command { } async action({ args, garden, log }: CommandParams): Promise { - const graph = await garden.getConfigGraph() + const graph = await garden.getConfigGraph(log) const tasks = await graph.getTasks(args.tasks) const taskModuleNames = uniq(tasks.map((t) => t.module.name)) const modules = sortBy(await graph.getModules(taskModuleNames), (m) => m.name) diff --git a/garden-service/src/commands/get/get-test-result.ts b/garden-service/src/commands/get/get-test-result.ts index 50c360183a..9df5144fcd 100644 --- a/garden-service/src/commands/get/get-test-result.ts +++ b/garden-service/src/commands/get/get-test-result.ts @@ -55,7 +55,7 @@ export class GetTestResultCommand extends Command { "heavy_check_mark" ) - const graph = await garden.getConfigGraph() + const graph = await garden.getConfigGraph(log) const actions = await garden.getActionRouter() const module = await graph.getModule(moduleName) diff --git a/garden-service/src/commands/link/module.ts b/garden-service/src/commands/link/module.ts index cfc87aec60..b8d8a09229 100644 --- a/garden-service/src/commands/link/module.ts +++ b/garden-service/src/commands/link/module.ts @@ -50,7 +50,7 @@ export class LinkModuleCommand extends Command { const sourceType = "module" const { module: moduleName, path } = args - const graph = await garden.getConfigGraph() + const graph = await garden.getConfigGraph(log) const moduleToLink = await graph.getModule(moduleName) const isRemote = [moduleToLink].filter(hasRemoteSource)[0] diff --git a/garden-service/src/commands/logs.ts b/garden-service/src/commands/logs.ts index 023bbfbf98..dab1606a9b 100644 --- a/garden-service/src/commands/logs.ts +++ b/garden-service/src/commands/logs.ts @@ -63,7 +63,7 @@ export class LogsCommand extends Command { async action({ garden, log, args, opts }: CommandParams): Promise> { const { follow, tail } = opts - const graph = await garden.getConfigGraph() + const graph = await garden.getConfigGraph(log) const services = await graph.getServices(args.services) const result: ServiceLogEntry[] = [] diff --git a/garden-service/src/commands/publish.ts b/garden-service/src/commands/publish.ts index 6d5c388b4a..49e312b350 100644 --- a/garden-service/src/commands/publish.ts +++ b/garden-service/src/commands/publish.ts @@ -64,7 +64,7 @@ export class PublishCommand extends Command { }: CommandParams): Promise> { printHeader(headerLog, "Publish modules", "rocket") - const graph = await garden.getConfigGraph() + const graph = await garden.getConfigGraph(log) const modules = await graph.getModules(args.modules) const results = await publishModules(garden, log, modules, !!opts["force-build"], !!opts["allow-dirty"]) diff --git a/garden-service/src/commands/run/module.ts b/garden-service/src/commands/run/module.ts index abc9ef2eba..22aa60e733 100644 --- a/garden-service/src/commands/run/module.ts +++ b/garden-service/src/commands/run/module.ts @@ -11,7 +11,7 @@ import { RunResult } from "../../types/plugin/base" import { BooleanParameter, Command, CommandParams, StringParameter, CommandResult, StringsParameter } from "../base" import { printRuntimeContext } from "./run" import { printHeader } from "../../logger/util" -import { BuildTask } from "../../tasks/build" +import { getBuildTasks } from "../../tasks/build" import { dedent, deline } from "../../util/string" import { prepareRuntimeContext } from "../../runtime-context" @@ -74,7 +74,7 @@ export class RunModuleCommand extends Command { async action({ garden, log, headerLog, args, opts }: CommandParams): Promise> { const moduleName = args.module - const graph = await garden.getConfigGraph() + const graph = await garden.getConfigGraph(log) const module = await graph.getModule(moduleName) const msg = args.arguments @@ -85,13 +85,13 @@ export class RunModuleCommand extends Command { const actions = await garden.getActionRouter() - const buildTask = new BuildTask({ + const buildTasks = await getBuildTasks({ garden, log, module, force: opts["force-build"], }) - await garden.processTasks([buildTask]) + await garden.processTasks(buildTasks) const dependencies = await graph.getDependencies("build", module.name, false) diff --git a/garden-service/src/commands/run/service.ts b/garden-service/src/commands/run/service.ts index 369e9f7543..011d14d299 100644 --- a/garden-service/src/commands/run/service.ts +++ b/garden-service/src/commands/run/service.ts @@ -50,7 +50,7 @@ export class RunServiceCommand extends Command { async action({ garden, log, headerLog, args, opts }: CommandParams): Promise> { const serviceName = args.service - const graph = await garden.getConfigGraph() + const graph = await garden.getConfigGraph(log) const service = await graph.getService(serviceName) const module = service.module diff --git a/garden-service/src/commands/run/task.ts b/garden-service/src/commands/run/task.ts index 756494a752..3f2507132f 100644 --- a/garden-service/src/commands/run/task.ts +++ b/garden-service/src/commands/run/task.ts @@ -51,7 +51,7 @@ export class RunTaskCommand extends Command { args, opts, }: CommandParams): Promise> { - const graph = await garden.getConfigGraph() + const graph = await garden.getConfigGraph(log) const task = await graph.getTask(args.task) const msg = `Running task ${chalk.white(task.name)}` diff --git a/garden-service/src/commands/run/test.ts b/garden-service/src/commands/run/test.ts index f406a74cce..66e4d9adba 100644 --- a/garden-service/src/commands/run/test.ts +++ b/garden-service/src/commands/run/test.ts @@ -64,7 +64,7 @@ export class RunTestCommand extends Command { const moduleName = args.module const testName = args.test - const graph = await garden.getConfigGraph() + const graph = await garden.getConfigGraph(log) const module = await graph.getModule(moduleName) const testConfig = findByName(module.testConfigs, testName) diff --git a/garden-service/src/commands/scan.ts b/garden-service/src/commands/scan.ts index b4125105b5..c404ca31eb 100644 --- a/garden-service/src/commands/scan.ts +++ b/garden-service/src/commands/scan.ts @@ -17,7 +17,8 @@ export class ScanCommand extends Command { help = "Scans your project and outputs an overview of all modules." async action({ garden, log }: CommandParams): Promise> { - const modules = (await garden.resolveModuleConfigs()).map((m) => { + const graph = await garden.getConfigGraph(log) + const modules = (await graph.getModules()).map((m) => { return omit(m, ["_ConfigType", "cacheContext", "serviceNames", "taskNames"]) }) diff --git a/garden-service/src/commands/test.ts b/garden-service/src/commands/test.ts index d239ead6eb..b1f18805eb 100644 --- a/garden-service/src/commands/test.ts +++ b/garden-service/src/commands/test.ts @@ -101,7 +101,7 @@ export class TestCommand extends Command { this.server.setGarden(garden) } - const graph = await garden.getConfigGraph() + const graph = await garden.getConfigGraph(log) let modules: Module[] if (args.modules) { diff --git a/garden-service/src/commands/update-remote/modules.ts b/garden-service/src/commands/update-remote/modules.ts index 8b4627ffc9..47e97dbc0a 100644 --- a/garden-service/src/commands/update-remote/modules.ts +++ b/garden-service/src/commands/update-remote/modules.ts @@ -58,7 +58,7 @@ export async function updateRemoteModules({ args: ParameterValues }): Promise> { const { modules: moduleNames } = args - const graph = await garden.getConfigGraph() + const graph = await garden.getConfigGraph(log) const modules = await graph.getModules(moduleNames) const moduleSources = ( diff --git a/garden-service/src/commands/validate.ts b/garden-service/src/commands/validate.ts index 703618b626..3790439f07 100644 --- a/garden-service/src/commands/validate.ts +++ b/garden-service/src/commands/validate.ts @@ -18,10 +18,10 @@ export class ValidateCommand extends Command { Throws an error and exits with code 1 if something's not right in your garden.yml files. ` - async action({ garden, headerLog }: CommandParams): Promise { + async action({ garden, log, headerLog }: CommandParams): Promise { printHeader(headerLog, "Validate", "heavy_check_mark") - const graph = await garden.getConfigGraph() + const graph = await garden.getConfigGraph(log) await graph.getModules() return {} diff --git a/garden-service/src/config/base.ts b/garden-service/src/config/base.ts index 23fb1d1a1b..551925dd1c 100644 --- a/garden-service/src/config/base.ts +++ b/garden-service/src/config/base.ts @@ -115,7 +115,12 @@ function prepareProjectConfig(spec: any, path: string, configPath: string): Proj return spec } -function prepareModuleResource(spec: any, path: string, configPath: string, projectRoot: string): ModuleResource { +export function prepareModuleResource( + spec: any, + path: string, + configPath: string, + projectRoot: string +): ModuleResource { /** * We allow specifying modules by name only as a shorthand: * dependencies: diff --git a/garden-service/src/config/config-context.ts b/garden-service/src/config/config-context.ts index c698173aa7..6cd0581b05 100644 --- a/garden-service/src/config/config-context.ts +++ b/garden-service/src/config/config-context.ts @@ -527,12 +527,12 @@ export class ModuleConfigContext extends ProviderConfigContext { async (opts: ContextResolveOpts) => { // NOTE: This is a temporary hacky solution until we implement module resolution as a TaskGraph task const stackKey = "modules." + config.name - const resolvedConfig = await garden.resolveModuleConfig(config.name, { + const resolvedConfig = await garden.resolveModuleConfig(garden.log, config.name, { configContext: _this, ...opts, stack: [...(opts.stack || []), stackKey], }) - const version = await garden.resolveVersion(resolvedConfig.name, resolvedConfig.build.dependencies) + const version = await garden.resolveVersion(resolvedConfig, resolvedConfig.build.dependencies) const buildPath = await garden.buildDir.buildPath(config) return new ModuleContext(_this, resolvedConfig, buildPath, version) diff --git a/garden-service/src/config/module.ts b/garden-service/src/config/module.ts index 86ba89bbb9..be2bdc05e3 100644 --- a/garden-service/src/config/module.ts +++ b/garden-service/src/config/module.ts @@ -74,12 +74,12 @@ export interface BaseBuildSpec { export interface ModuleSpec {} -export interface BaseModuleSpec { - apiVersion: string +export interface AddModuleSpec { + apiVersion?: string name: string path: string - allowPublish: boolean - build: BaseBuildSpec + allowPublish?: boolean + build?: BaseBuildSpec description?: string include?: string[] exclude?: string[] @@ -87,6 +87,12 @@ export interface BaseModuleSpec { repositoryUrl?: string } +export interface BaseModuleSpec extends AddModuleSpec { + apiVersion: string + allowPublish: boolean + build: BaseBuildSpec +} + export const baseBuildSpecSchema = joi .object() .keys({ @@ -191,10 +197,12 @@ export interface ModuleConfig< spec: M } +export const modulePathSchema = joi.string().description("The filesystem path of the module.") + export const moduleConfigSchema = baseModuleSpecSchema .keys({ outputs: joiVariables().description("The outputs defined by the module (referenceable in other module configs)."), - path: joi.string().description("The filesystem path of the module."), + path: modulePathSchema, configPath: joi.string().description("The filesystem path of the module config file."), plugin: joiIdentifier() .meta({ internal: true }) diff --git a/garden-service/src/garden.ts b/garden-service/src/garden.ts index 205dcc74e4..635aff9f8e 100644 --- a/garden-service/src/garden.ts +++ b/garden-service/src/garden.ts @@ -8,7 +8,7 @@ import Bluebird from "bluebird" import { parse, relative, resolve, dirname } from "path" -import { flatten, isString, cloneDeep, sortBy, fromPairs, keyBy } from "lodash" +import { flatten, isString, sortBy, fromPairs, keyBy } from "lodash" const AsyncLock = require("async-lock") import { TreeCache } from "./cache" @@ -25,14 +25,14 @@ import { ConfigGraph } from "./config-graph" import { TaskGraph, TaskResults, ProcessTasksOpts } from "./task-graph" import { getLogger } from "./logger/logger" import { PluginActionHandlers, GardenPlugin } from "./types/plugin/plugin" -import { validate, PrimitiveMap, validateWithPath } from "./config/common" -import { resolveTemplateStrings } from "./template-string" -import { loadConfig, findProjectConfig } from "./config/base" +import { validate, PrimitiveMap } from "./config/common" +import { loadConfig, findProjectConfig, prepareModuleResource } from "./config/base" import { BaseTask } from "./tasks/base" import { LocalConfigStore, ConfigStore, GlobalConfigStore } from "./config-store" import { getLinkedSources, ExternalSourceType } from "./util/ext-source-util" -import { BuildDependencyConfig, ModuleConfig, ModuleResource, moduleConfigSchema } from "./config/module" -import { ModuleConfigContext, ContextResolveOpts } from "./config/config-context" +import { BuildDependencyConfig, ModuleConfig, ModuleResource } from "./config/module" +import { resolveModuleConfig, ModuleConfigResolveOpts } from "./resolve-module" +import { ModuleConfigContext } from "./config/config-context" import { createPluginContext, CommandInfo } from "./plugin-context" import { ModuleAndRuntimeActionHandlers, RegisterPluginParam } from "./types/plugin/plugin" import { SUPPORTED_PLATFORMS, SupportedPlatform, DEFAULT_GARDEN_DIR_NAME } from "./constants" @@ -47,9 +47,9 @@ import { ActionRouter } from "./actions" import { detectCycles, cyclesToString, Dependency } from "./util/validate-dependencies" import chalk from "chalk" import { RuntimeContext } from "./runtime-context" -import { deline } from "./util/string" -import { loadPlugins, getModuleTypeBases } from "./plugins" import { ensureDir } from "fs-extra" +import { loadPlugins, getDependencyOrder } from "./plugins" +import { deline } from "./util/string" export interface ActionHandlerMap { [actionName: string]: PluginActionHandlers[T] @@ -83,10 +83,6 @@ export interface GardenOpts { plugins?: RegisterPluginParam[] } -interface ModuleConfigResolveOpts extends ContextResolveOpts { - configContext?: ModuleConfigContext -} - export interface GardenParams { artifactsPath: string buildDir: BuildDir @@ -560,152 +556,118 @@ export class Garden { * plugin handlers). * Scans for modules in the project root and remote/linked sources if it hasn't already been done. */ - async resolveModuleConfigs(keys?: string[], opts: ModuleConfigResolveOpts = {}): Promise { - const actions = await this.getActionRouter() - await this.resolveProviders() + private async resolveModuleConfigs( + log: LogEntry, + keys?: string[], + opts: ModuleConfigResolveOpts = {} + ): Promise { + const providers = await this.resolveProviders() const configs = await this.getRawModuleConfigs(keys) - keys ? this.log.silly(`Resolving module configs ${keys.join(", ")}`) : this.log.silly(`Resolving module configs`) + keys ? log.silly(`Resolving module configs ${keys.join(", ")}`) : this.log.silly(`Resolving module configs`) if (!opts.configContext) { opts.configContext = await this.getModuleConfigContext() } - const moduleTypeDefinitions = await this.getModuleTypeDefinitions() - - return Bluebird.map(configs, async (config) => { - config = await resolveTemplateStrings(cloneDeep(config), opts.configContext!, opts) - const description = moduleTypeDefinitions[config.type] + // Resolve the project module configs + const moduleConfigs = await Bluebird.map(configs, (config) => resolveModuleConfig(this, config, opts)) + const actions = await this.getActionRouter() - if (!description) { - const configPath = relative(this.projectRoot, config.configPath || config.path) + let graph: ConfigGraph | undefined = undefined - throw new ConfigurationError( - deline` - Unrecognized module type '${config.type}' (defined at ${configPath}). - Are you missing a provider configuration? - `, - { config, configuredModuleTypes: Object.keys(moduleTypeDefinitions) } - ) - } + // Walk through all plugins in dependency order, and allow them to augment the graph + for (const provider of getDependencyOrder(providers, this.registeredPlugins)) { + // Skip the routine if the provider doesn't have the handler + const handler = await actions.getActionHandler({ + actionType: "augmentGraph", + pluginName: provider.name, + throwIfMissing: false, + }) - // Validate the module-type specific spec - if (description.schema) { - config.spec = validateWithPath({ - config: config.spec, - schema: description.schema, - name: config.name, - path: config.path, - projectRoot: this.projectRoot, - }) + if (!handler) { + continue } - /* - We allow specifying modules by name only as a shorthand: - - dependencies: - - foo-module - - name: foo-module // same as the above - */ - if (config.build && config.build.dependencies) { - config.build.dependencies = config.build.dependencies.map((dep) => - typeof dep === "string" ? { name: dep, copy: [] } : dep - ) + // We clear the graph below whenever an augmentGraph handler adds/modifies modules, and re-init here + if (!graph) { + graph = new ConfigGraph(this, moduleConfigs) } - // Validate the base config schema - config = validateWithPath({ - config, - schema: moduleConfigSchema, - configType: "module", - name: config.name, - path: config.path, - projectRoot: this.projectRoot, + const { addBuildDependencies, addRuntimeDependencies, addModules } = await actions.augmentGraph({ + pluginName: provider.name, + log, + providers, + modules: await graph.getModules(), }) - if (config.repositoryUrl) { - config.path = await this.loadExtSourcePath({ - name: config.name, - repositoryUrl: config.repositoryUrl, - sourceType: "module", - }) - } - - const configureResult = await actions.configureModule({ - moduleConfig: config, - log: this.log, + // Resolve module configs from specs and add to the list + await Bluebird.map(addModules || [], async (spec) => { + const moduleConfig = prepareModuleResource(spec, spec.path, spec.path, this.projectRoot) + moduleConfigs.push(await resolveModuleConfig(this, moduleConfig, opts)) + graph = undefined }) - config = configureResult.moduleConfig - - // Validate the module outputs against the outputs schema - if (description.moduleOutputsSchema) { - config.outputs = validateWithPath({ - config: config.outputs, - schema: description.moduleOutputsSchema, - configType: `outputs for module`, - name: config.name, - path: config.path, - projectRoot: this.projectRoot, - ErrorClass: PluginError, - }) - } - - // Validate the configure handler output (incl. module outputs) against the module type's bases - const bases = getModuleTypeBases(moduleTypeDefinitions[config.type], moduleTypeDefinitions) - - for (const base of bases) { - if (base.schema) { - this.log.silly(`Validating '${config.name}' config against '${base.name}' schema`) - - config.spec = validateWithPath({ - config: config.spec, - schema: base.schema.unknown(true), - path: this.projectRoot, - projectRoot: this.projectRoot, - configType: `configuration for module '${config.name}' (base schema from '${base.name}' plugin)`, - ErrorClass: ConfigurationError, - }) + // Note: For both kinds of dependencies we only validate that `by` resolves correctly, since the rest + // (i.e. whether all `on` references exist + circular deps) will be validated when initiating the ConfigGraph. + for (const dependency of addBuildDependencies || []) { + const by = findByName(moduleConfigs, dependency.by) + + if (!by) { + throw new PluginError( + deline` + Provider '${provider.name}' added a build dependency by module '${dependency.by}' on '${dependency.on}' + but module '${dependency.by}' could not be found. + `, + { provider, dependency } + ) } - if (base.moduleOutputsSchema) { - this.log.silly(`Validating '${config.name}' module outputs against '${base.name}' schema`) - - config.outputs = validateWithPath({ - config: config.outputs, - schema: base.moduleOutputsSchema.unknown(true), - path: this.projectRoot, - projectRoot: this.projectRoot, - configType: `outputs for module '${config.name}' (base schema from '${base.name}' plugin)`, - ErrorClass: PluginError, - }) - } + // TODO: allow copy directives on build dependencies? + by.build.dependencies.push({ name: dependency.on, copy: [] }) + graph = undefined } - // FIXME: We should be able to avoid this - config.name = getModuleKey(config.name, config.plugin) + for (const dependency of addRuntimeDependencies || []) { + let found = false - if (config.plugin) { - for (const serviceConfig of config.serviceConfigs) { - serviceConfig.name = getModuleKey(serviceConfig.name, config.plugin) - } - for (const taskConfig of config.taskConfigs) { - taskConfig.name = getModuleKey(taskConfig.name, config.plugin) + for (const moduleConfig of moduleConfigs) { + for (const serviceConfig of moduleConfig.serviceConfigs) { + if (serviceConfig.name === dependency.by) { + serviceConfig.dependencies.push(dependency.on) + found = true + } + } + for (const taskConfig of moduleConfig.taskConfigs) { + if (taskConfig.name === dependency.by) { + taskConfig.dependencies.push(dependency.on) + found = true + } + } } - for (const testConfig of config.testConfigs) { - testConfig.name = getModuleKey(testConfig.name, config.plugin) + + if (!found) { + throw new PluginError( + deline` + Provider '${provider.name}' added a runtime dependency by '${dependency.by}' on '${dependency.on}' + but service or task '${dependency.by}' could not be found. + `, + { provider, dependency } + ) } + + graph = undefined } + } - return config - }) + return moduleConfigs } /** * Returns the module with the specified name. Throws error if it doesn't exist. */ - async resolveModuleConfig(name: string, opts: ModuleConfigResolveOpts = {}): Promise { - return (await this.resolveModuleConfigs([name], opts))[0] + async resolveModuleConfig(log: LogEntry, name: string, opts: ModuleConfigResolveOpts = {}): Promise { + return (await this.resolveModuleConfigs(log, [name], opts))[0] } /** @@ -713,8 +675,8 @@ export class Garden { * The graph instance is immutable and represents the configuration at the point of calling this method. * For long-running processes, you need to call this again when any module or configuration has been updated. */ - async getConfigGraph(opts: ModuleConfigResolveOpts = {}) { - const modules = await this.resolveModuleConfigs(undefined, opts) + async getConfigGraph(log: LogEntry, opts: ModuleConfigResolveOpts = {}) { + const modules = await this.resolveModuleConfigs(log, undefined, opts) return new ConfigGraph(this, modules) } @@ -723,7 +685,12 @@ export class Garden { * The combined version is a either the latest dirty module version (if any), or the hash of the module version * and the versions of its dependencies (in sorted order). */ - async resolveVersion(moduleName: string, moduleDependencies: (Module | BuildDependencyConfig)[], force = false) { + async resolveVersion( + moduleConfig: ModuleConfig, + moduleDependencies: (Module | BuildDependencyConfig)[], + force = false + ) { + const moduleName = moduleConfig.name this.log.silly(`Resolving version for module ${moduleName}`) const depModuleNames = moduleDependencies.map((m) => m.name) @@ -738,12 +705,11 @@ export class Garden { } } - const config = await this.resolveModuleConfig(moduleName) const dependencyKeys = moduleDependencies.map((dep) => getModuleKey(dep.name, dep.plugin)) const dependencies = await this.getRawModuleConfigs(dependencyKeys) - const cacheContexts = dependencies.concat([config]).map((c) => getModuleCacheContext(c)) + const cacheContexts = dependencies.concat([moduleConfig]).map((c) => getModuleCacheContext(c)) - const version = await this.vcs.resolveVersion(this.log, config, dependencies) + const version = await this.vcs.resolveVersion(this.log, moduleConfig, dependencies) this.cache.set(cacheKey, version, ...cacheContexts) return version @@ -888,12 +854,12 @@ export class Garden { /** * This dumps the full project configuration including all modules. */ - public async dumpConfig(): Promise { + public async dumpConfig(log: LogEntry): Promise { return { environmentName: this.environmentName, providers: await this.resolveProviders(), variables: this.variables, - moduleConfigs: sortBy(await this.resolveModuleConfigs(), "name"), + moduleConfigs: sortBy(await this.resolveModuleConfigs(log), "name"), projectRoot: this.projectRoot, } } diff --git a/garden-service/src/plugins.ts b/garden-service/src/plugins.ts index 4e7d3f1f99..72d5c8c816 100644 --- a/garden-service/src/plugins.ts +++ b/garden-service/src/plugins.ts @@ -17,36 +17,6 @@ import { validate } from "./config/common" import { LogEntry } from "./logger/log-entry" export function loadPlugins(log: LogEntry, registeredPlugins: PluginMap, configs: ProviderConfig[]) { - let graph = new DepGraph() - - for (const plugin of Object.values(registeredPlugins)) { - graph.addNode(plugin.name) - - if (plugin.base) { - graph.addNode(plugin.base) - graph.addDependency(plugin.name, plugin.base) - } - - for (const dependency of plugin.dependencies || []) { - graph.addNode(dependency) - graph.addDependency(plugin.name, dependency) - } - } - - let ordered: string[] - - try { - ordered = graph.overallOrder() - } catch (err) { - if (err.cyclePath) { - throw new PluginError(`Found a circular dependency between registered plugins: ${err.cyclePath.join(" -> ")}`, { - cyclePath: err.cyclePath, - }) - } else { - throw err - } - } - const loadedPlugins: PluginMap = {} const loadPlugin = (name: string) => { @@ -106,10 +76,8 @@ export function loadPlugins(log: LogEntry, registeredPlugins: PluginMap, configs return plugin } - // Load plugins in dependency order (concat() makes sure we're not mutating the original array, because JS...) - const orderedConfigs = configs.concat().sort((a, b) => { - return ordered.indexOf(a.name) - ordered.indexOf(b.name) - }) + // Load plugins in dependency order + const orderedConfigs = getDependencyOrder(configs, registeredPlugins) for (const config of orderedConfigs) { const plugin = loadPlugin(config.name) @@ -129,6 +97,46 @@ export function loadPlugins(log: LogEntry, registeredPlugins: PluginMap, configs return Object.values(resolveModuleDefinitions(resolvedPlugins, configs)) } +/** + * Returns the given provider configs in dependency order. + */ +export function getDependencyOrder(configs: T[], registeredPlugins: PluginMap): T[] { + let graph = new DepGraph() + + for (const plugin of Object.values(registeredPlugins)) { + graph.addNode(plugin.name) + + if (plugin.base) { + graph.addNode(plugin.base) + graph.addDependency(plugin.name, plugin.base) + } + + for (const dependency of plugin.dependencies || []) { + graph.addNode(dependency) + graph.addDependency(plugin.name, dependency) + } + } + + let ordered: string[] + + try { + ordered = graph.overallOrder() + } catch (err) { + if (err.cyclePath) { + throw new PluginError(`Found a circular dependency between registered plugins: ${err.cyclePath.join(" -> ")}`, { + cyclePath: err.cyclePath, + }) + } else { + throw err + } + } + + // Note: concat() makes sure we're not mutating the original array, because JS... + return configs.concat().sort((a, b) => { + return ordered.indexOf(a.name) - ordered.indexOf(b.name) + }) +} + // Takes a plugin and resolves it against its base plugin, if applicable function resolvePlugin(plugin: GardenPlugin, loadedPlugins: PluginMap, configs: ProviderConfig[]): GardenPlugin { if (!plugin.base) { diff --git a/garden-service/src/plugins/container/helpers.ts b/garden-service/src/plugins/container/helpers.ts index 21e289a4aa..c2e6ced87a 100644 --- a/garden-service/src/plugins/container/helpers.ts +++ b/garden-service/src/plugins/container/helpers.ts @@ -281,7 +281,7 @@ const helpers = { } }, - async hasDockerfile(module: ContainerModule): Promise { + async hasDockerfile(module: ContainerModule): Promise { // If we explicitly set a Dockerfile, we take that to mean you want it to be built. // If the file turns out to be missing, this will come up in the build handler. const dockerfileSourcePath = helpers.getDockerfileSourcePath(module) diff --git a/garden-service/src/plugins/plugins.ts b/garden-service/src/plugins/plugins.ts index 1a586bef48..4b713f407e 100644 --- a/garden-service/src/plugins/plugins.ts +++ b/garden-service/src/plugins/plugins.ts @@ -11,6 +11,7 @@ export const builtinPlugins = [ require("./exec"), require("./container/container"), require("./google/google-cloud-functions"), + require("./hadolint/hadolint"), require("./local/local-google-cloud-functions"), require("./kubernetes/kubernetes"), require("./kubernetes/local/local"), diff --git a/garden-service/src/plugins/terraform/terraform.ts b/garden-service/src/plugins/terraform/terraform.ts index 6d77196468..21493e20b1 100644 --- a/garden-service/src/plugins/terraform/terraform.ts +++ b/garden-service/src/plugins/terraform/terraform.ts @@ -91,8 +91,6 @@ export const gardenPlugin = createGardenPlugin({ schema, handlers: { configure: configureTerraformModule, - // FIXME: it should not be strictly necessary to provide this handler - build: async () => ({}), getServiceStatus: getTerraformStatus, deployService: deployTerraform, }, diff --git a/garden-service/src/process.ts b/garden-service/src/process.ts index 854a8947e3..5dde0b723d 100644 --- a/garden-service/src/process.ts +++ b/garden-service/src/process.ts @@ -187,7 +187,7 @@ export async function processModules({ }) garden.events.on("moduleSourcesChanged", async (event) => { - graph = await garden.getConfigGraph() + graph = await garden.getConfigGraph(log) const changedModuleNames = event.names.filter((moduleName) => !!modulesByName[moduleName]) if (changedModuleNames.length === 0) { @@ -231,7 +231,7 @@ async function validateConfigChange( ): Promise { try { const nextGarden = await Garden.factory(garden.projectRoot, garden.opts) - await nextGarden.getConfigGraph() + await nextGarden.getConfigGraph(log) } catch (error) { if (error instanceof ConfigurationError) { const msg = dedent` diff --git a/garden-service/src/resolve-module.ts b/garden-service/src/resolve-module.ts new file mode 100644 index 0000000000..d55d77b714 --- /dev/null +++ b/garden-service/src/resolve-module.ts @@ -0,0 +1,161 @@ +/* + * 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 { cloneDeep } from "lodash" +import { validateWithPath } from "./config/common" +import { resolveTemplateStrings } from "./template-string" +import { ContextResolveOpts, ModuleConfigContext } from "./config/config-context" +import { relative } from "path" +import { Garden } from "./garden" +import { ConfigurationError, PluginError } from "./exceptions" +import { deline } from "./util/string" +import { getModuleKey } from "./types/module" +import { getModuleTypeBases } from "./plugins" +import { ModuleConfig, moduleConfigSchema } from "./config/module" + +export interface ModuleConfigResolveOpts extends ContextResolveOpts { + configContext?: ModuleConfigContext +} + +export async function resolveModuleConfig( + garden: Garden, + config: ModuleConfig, + opts: ModuleConfigResolveOpts +): Promise { + if (!opts.configContext) { + opts.configContext = await garden.getModuleConfigContext() + } + + config = await resolveTemplateStrings(cloneDeep(config), opts.configContext, opts) + + const moduleTypeDefinitions = await garden.getModuleTypeDefinitions() + const description = moduleTypeDefinitions[config.type] + + if (!description) { + const configPath = relative(garden.projectRoot, config.configPath || config.path) + + throw new ConfigurationError( + deline` + Unrecognized module type '${config.type}' (defined at ${configPath}). + Are you missing a provider configuration? + `, + { config, configuredModuleTypes: Object.keys(moduleTypeDefinitions) } + ) + } + + // Validate the module-type specific spec + if (description.schema) { + config.spec = validateWithPath({ + config: config.spec, + schema: description.schema, + name: config.name, + path: config.path, + projectRoot: garden.projectRoot, + }) + } + + /* + We allow specifying modules by name only as a shorthand: + + dependencies: + - foo-module + - name: foo-module // same as the above + */ + if (config.build && config.build.dependencies) { + config.build.dependencies = config.build.dependencies.map((dep) => + typeof dep === "string" ? { name: dep, copy: [] } : dep + ) + } + + // Validate the base config schema + config = validateWithPath({ + config, + schema: moduleConfigSchema, + configType: "module", + name: config.name, + path: config.path, + projectRoot: garden.projectRoot, + }) + + if (config.repositoryUrl) { + config.path = await garden.loadExtSourcePath({ + name: config.name, + repositoryUrl: config.repositoryUrl, + sourceType: "module", + }) + } + + const actions = await garden.getActionRouter() + const configureResult = await actions.configureModule({ + moduleConfig: config, + log: garden.log, + }) + + config = configureResult.moduleConfig + + // Validate the module outputs against the outputs schema + if (description.moduleOutputsSchema) { + config.outputs = validateWithPath({ + config: config.outputs, + schema: description.moduleOutputsSchema, + configType: `outputs for module`, + name: config.name, + path: config.path, + projectRoot: garden.projectRoot, + ErrorClass: PluginError, + }) + } + + // Validate the configure handler output (incl. module outputs) against the module type's bases + const bases = getModuleTypeBases(moduleTypeDefinitions[config.type], moduleTypeDefinitions) + + for (const base of bases) { + if (base.schema) { + garden.log.silly(`Validating '${config.name}' config against '${base.name}' schema`) + + config.spec = validateWithPath({ + config: config.spec, + schema: base.schema.unknown(true), + path: garden.projectRoot, + projectRoot: garden.projectRoot, + configType: `configuration for module '${config.name}' (base schema from '${base.name}' plugin)`, + ErrorClass: ConfigurationError, + }) + } + + if (base.moduleOutputsSchema) { + garden.log.silly(`Validating '${config.name}' module outputs against '${base.name}' schema`) + + config.outputs = validateWithPath({ + config: config.outputs, + schema: base.moduleOutputsSchema.unknown(true), + path: garden.projectRoot, + projectRoot: garden.projectRoot, + configType: `outputs for module '${config.name}' (base schema from '${base.name}' plugin)`, + ErrorClass: PluginError, + }) + } + } + + // FIXME: We should be able to avoid this + config.name = getModuleKey(config.name, config.plugin) + + if (config.plugin) { + for (const serviceConfig of config.serviceConfigs) { + serviceConfig.name = getModuleKey(serviceConfig.name, config.plugin) + } + for (const taskConfig of config.taskConfigs) { + taskConfig.name = getModuleKey(taskConfig.name, config.plugin) + } + for (const testConfig of config.testConfigs) { + testConfig.name = getModuleKey(testConfig.name, config.plugin) + } + } + + return config +} diff --git a/garden-service/src/tasks/build.ts b/garden-service/src/tasks/build.ts index 31e45e238d..03f7268b5e 100644 --- a/garden-service/src/tasks/build.ts +++ b/garden-service/src/tasks/build.ts @@ -14,6 +14,7 @@ import { BaseTask, TaskType } from "../tasks/base" import { Garden } from "../garden" import { LogEntry } from "../logger/log-entry" import { StageBuildTask } from "./stage-build" +import { some } from "lodash" export interface BuildTaskParams { garden: Garden @@ -39,7 +40,7 @@ export class BuildTask extends BaseTask { } async getDependencies() { - const dg = await this.garden.getConfigGraph() + const dg = await this.garden.getConfigGraph(this.log) const deps = (await dg.getDependencies("build", this.getName(), false)).build const stageBuildTask = new StageBuildTask({ @@ -124,3 +125,41 @@ export class BuildTask extends BaseTask { return result } } + +/** + * Use this method to get the build tasks for a module. This is needed to be able to avoid an unnecessary build step + * when there is no build handler and no dependency files to copy. + */ +export async function getBuildTasks(params: BuildTaskParams): Promise<(BuildTask | StageBuildTask)[]> { + // We need to see if a build step is necessary for the module. If it is, return a build task for the module. + // Otherwise, return a build task for each of the module's dependencies. + // We do this to avoid displaying no-op build steps in the stack graph. + + const { garden, module } = params + + // We need to build if there is a copy statement on any of the build dependencies. + let needsBuild = some(module.build.dependencies, (d) => d.copy && d.copy.length > 0) + + if (!needsBuild) { + // We also need to build if there is a build handler for the module type + const actions = await garden.getActionRouter() + try { + await actions.getModuleActionHandler({ + actionType: "build", + moduleType: module.type, + }) + + needsBuild = true + } catch { + // No build handler for the module type. + } + } + + const buildTask = new BuildTask(params) + + if (needsBuild) { + return [buildTask] + } else { + return buildTask.getDependencies() + } +} diff --git a/garden-service/src/tasks/deploy.ts b/garden-service/src/tasks/deploy.ts index a0f729946b..b3b2621419 100644 --- a/garden-service/src/tasks/deploy.ts +++ b/garden-service/src/tasks/deploy.ts @@ -14,7 +14,7 @@ import { BaseTask, TaskType, getServiceStatuses, getRunTaskResults } from "./bas import { Service, ServiceStatus, getLinkUrl } from "../types/service" import { Garden } from "../garden" import { TaskTask, getTaskVersion } from "./task" -import { BuildTask } from "./build" +import { getBuildTasks } from "./build" import { ConfigGraph } from "../config-graph" import { startPortProxies } from "../proxy" import { TaskResults } from "../task-graph" @@ -129,7 +129,7 @@ export class DeployTask extends BaseTask { }) }) - const buildTask = new BuildTask({ + const buildTasks = await getBuildTasks({ garden: this.garden, log: this.log, module: this.service.module, @@ -138,7 +138,7 @@ export class DeployTask extends BaseTask { hotReloadServiceNames: this.hotReloadServiceNames, }) - return [statusTask, ...deployTasks, ...taskTasks, buildTask] + return [statusTask, ...deployTasks, ...taskTasks, ...buildTasks] } } diff --git a/garden-service/src/tasks/helpers.ts b/garden-service/src/tasks/helpers.ts index d993eef81c..2df6e20254 100644 --- a/garden-service/src/tasks/helpers.ts +++ b/garden-service/src/tasks/helpers.ts @@ -6,7 +6,8 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import { intersection, uniq } from "lodash" +import Bluebird from "bluebird" +import { intersection, uniq, flatten } from "lodash" import { DeployTask } from "./deploy" import { Garden } from "../garden" import { Module } from "../types/module" @@ -14,7 +15,7 @@ import { Service } from "../types/service" import { DependencyGraphNode, ConfigGraph } from "../config-graph" import { LogEntry } from "../logger/log-entry" import { BaseTask } from "./base" -import { BuildTask } from "./build" +import { BuildTask, getBuildTasks } from "./build" export async function getDependantTasksForModule({ garden, @@ -37,20 +38,20 @@ export async function getDependantTasksForModule({ fromWatch?: boolean includeDependants?: boolean }): Promise { - let buildTasks: BuildTask[] = [] + let buildTasks: BaseTask[] = [] let dependantBuildModules: Module[] = [] let services: Service[] = [] if (!includeDependants) { buildTasks.push( - new BuildTask({ + ...(await getBuildTasks({ garden, log, module, force: forceBuild, fromWatch, hotReloadServiceNames, - }) + })) ) services = await graph.getServices(module.serviceNames) } else { @@ -69,31 +70,30 @@ export async function getDependantTasksForModule({ const dependants = await graph.getDependantsForModule(module, dependantFilterFn) buildTasks.push( - new BuildTask({ + ...(await getBuildTasks({ garden, log, module, force: true, fromWatch, hotReloadServiceNames, - }) + })) ) dependantBuildModules = dependants.build services = (await graph.getServices(module.serviceNames)).concat(dependants.service) } } - buildTasks.push( - ...dependantBuildModules.map( - (m) => - new BuildTask({ - garden, - log, - module: m, - force: forceBuild, - fromWatch, - hotReloadServiceNames, - }) + const dependantBuildTasks = flatten( + await Bluebird.map(dependantBuildModules, (m) => + getBuildTasks({ + garden, + log, + module: m, + force: forceBuild, + fromWatch, + hotReloadServiceNames, + }) ) ) @@ -111,7 +111,7 @@ export async function getDependantTasksForModule({ }) ) - const outputTasks = [...buildTasks, ...deployTasks] + const outputTasks = [...buildTasks, ...dependantBuildTasks, ...deployTasks] log.silly(`getDependantTasksForModule called for module ${module.name}, returning the following tasks:`) log.silly(` ${outputTasks.map((t) => t.getKey()).join(", ")}`) diff --git a/garden-service/src/tasks/publish.ts b/garden-service/src/tasks/publish.ts index d1537ae652..4a86d4db48 100644 --- a/garden-service/src/tasks/publish.ts +++ b/garden-service/src/tasks/publish.ts @@ -7,7 +7,7 @@ */ import chalk from "chalk" -import { BuildTask } from "./build" +import { getBuildTasks } from "./build" import { Module } from "../types/module" import { PublishResult } from "../types/plugin/module/publishModule" import { BaseTask, TaskType } from "../tasks/base" @@ -37,14 +37,12 @@ export class PublishTask extends BaseTask { if (!this.module.allowPublish) { return [] } - return [ - new BuildTask({ - garden: this.garden, - log: this.log, - module: this.module, - force: this.forceBuild, - }), - ] + return getBuildTasks({ + garden: this.garden, + log: this.log, + module: this.module, + force: this.forceBuild, + }) } getName() { diff --git a/garden-service/src/tasks/resolve-provider.ts b/garden-service/src/tasks/resolve-provider.ts index a4dcd9fd9a..35ec48313b 100644 --- a/garden-service/src/tasks/resolve-provider.ts +++ b/garden-service/src/tasks/resolve-provider.ts @@ -184,7 +184,7 @@ export class ResolveProviderTask extends BaseTask { defaultHandler: async () => defaultEnvironmentStatus, }) - let status = await handler({ ctx, log: this.log }) + let status = await handler!({ ctx, log: this.log }) this.log.silly(`${pluginName} status: ${status.ready ? "ready" : "not ready"}`) @@ -204,7 +204,7 @@ export class ResolveProviderTask extends BaseTask { pluginName, defaultHandler: async () => ({ status }), }) - const result = await prepareHandler({ ctx, log: this.log, force: this.forceInit, status }) + const result = await prepareHandler!({ ctx, log: this.log, force: this.forceInit, status }) status = result.status diff --git a/garden-service/src/tasks/stage-build.ts b/garden-service/src/tasks/stage-build.ts index 877d6339cf..baaac2f975 100644 --- a/garden-service/src/tasks/stage-build.ts +++ b/garden-service/src/tasks/stage-build.ts @@ -33,7 +33,7 @@ export class StageBuildTask extends BaseTask { } async getDependencies() { - const dg = await this.garden.getConfigGraph() + const dg = await this.garden.getConfigGraph(this.log) const deps = (await dg.getDependencies("build", this.getName(), false)).build return Bluebird.map(deps, async (m: Module) => { @@ -65,7 +65,7 @@ export class StageBuildTask extends BaseTask { }) } - const graph = await this.garden.getConfigGraph() + const graph = await this.garden.getConfigGraph(log || this.log) await this.garden.buildDir.syncFromSrc(this.module, log || this.log) await this.garden.buildDir.syncDependencyProducts(this.module, graph, log || this.log) diff --git a/garden-service/src/tasks/task.ts b/garden-service/src/tasks/task.ts index fd6eb59977..148a4e43b7 100644 --- a/garden-service/src/tasks/task.ts +++ b/garden-service/src/tasks/task.ts @@ -16,7 +16,7 @@ import { LogEntry } from "../logger/log-entry" import { prepareRuntimeContext } from "../runtime-context" import { ConfigGraph } from "../config-graph" import { ModuleVersion } from "../vcs/vcs" -import { BuildTask } from "./build" +import { getBuildTasks } from "./build" import { RunTaskResult } from "../types/plugin/task/runTask" import { TaskResults } from "../task-graph" import { GetTaskResultTask } from "./get-task-result" @@ -53,14 +53,14 @@ export class TaskTask extends BaseTask { } async getDependencies(): Promise { - const buildTask = new BuildTask({ + const buildTasks = await getBuildTasks({ garden: this.garden, log: this.log, module: this.task.module, force: this.forceBuild, }) - const dg = await this.garden.getConfigGraph() + const dg = await this.garden.getConfigGraph(this.log) const deps = await dg.getDependencies("task", this.getName(), false) const deployTasks = deps.service.map((service) => { @@ -93,7 +93,7 @@ export class TaskTask extends BaseTask { version: this.version, }) - return [buildTask, ...deployTasks, ...taskTasks, resultTask] + return [...buildTasks, ...deployTasks, ...taskTasks, resultTask] } getName() { @@ -174,5 +174,5 @@ export class TaskTask extends BaseTask { export async function getTaskVersion(garden: Garden, graph: ConfigGraph, task: Task): Promise { const { module } = task const moduleDeps = await graph.resolveDependencyModules(module.build.dependencies, task.config.dependencies) - return garden.resolveVersion(module.name, moduleDeps) + return garden.resolveVersion(module, moduleDeps) } diff --git a/garden-service/src/tasks/test.ts b/garden-service/src/tasks/test.ts index fbccf43e2f..eef839371f 100644 --- a/garden-service/src/tasks/test.ts +++ b/garden-service/src/tasks/test.ts @@ -22,7 +22,7 @@ import { Garden } from "../garden" import { LogEntry } from "../logger/log-entry" import { ConfigGraph } from "../config-graph" import { makeTestTaskName } from "./helpers" -import { BuildTask } from "./build" +import { getBuildTasks } from "./build" import { TaskTask } from "./task" import { TaskResults } from "../task-graph" @@ -75,7 +75,7 @@ export class TestTask extends BaseTask { const dg = this.graph const deps = await dg.getDependencies("test", this.getName(), false) - const buildTask = new BuildTask({ + const buildTasks = await getBuildTasks({ garden: this.garden, log: this.log, module: this.module, @@ -105,7 +105,7 @@ export class TestTask extends BaseTask { }) ) - return [buildTask, ...serviceTasks, ...taskTasks] + return [...buildTasks, ...serviceTasks, ...taskTasks] } getName() { @@ -249,5 +249,5 @@ export async function getTestVersion( // Don't include the module itself in the dependencies here .filter((m) => m.name !== module.name) - return garden.resolveVersion(module.name, moduleDeps) + return garden.resolveVersion(module, moduleDeps) } diff --git a/garden-service/src/types/module.ts b/garden-service/src/types/module.ts index 30310ef560..ddc10e3858 100644 --- a/garden-service/src/types/module.ts +++ b/garden-service/src/types/module.ts @@ -19,6 +19,7 @@ import { joiArray, joiIdentifier, joiIdentifierMap, joi } from "../config/common import { ConfigGraph } from "../config-graph" import Bluebird from "bluebird" import { getConfigFilePath } from "../util/fs" +import { getModuleTypeBases } from "../plugins" export interface FileCopySpec { source: string @@ -48,6 +49,7 @@ export interface Module< taskNames: string[] taskDependencyNames: string[] + compatibleTypes: string[] _ConfigType: ModuleConfig } @@ -60,6 +62,9 @@ export const moduleSchema = moduleConfigSchema.keys({ .string() .required() .description("The path to the build metadata directory for the module."), + compatibleTypes: joiArray(joiIdentifier()) + .required() + .description("A list of types that this module is compatible with (i.e. the module type itself + all bases)."), configPath: joi .string() .required() @@ -92,7 +97,9 @@ export interface ModuleConfigMap { export async function moduleFromConfig(garden: Garden, graph: ConfigGraph, config: ModuleConfig): Promise { const configPath = await getConfigFilePath(config.path) - const version = await garden.resolveVersion(config.name, config.build.dependencies) + const version = await garden.resolveVersion(config, config.build.dependencies) + const moduleTypes = await garden.getModuleTypeDefinitions() + const compatibleTypes = [config.type, ...getModuleTypeBases(moduleTypes[config.type], moduleTypes).map((t) => t.name)] const module: Module = { ...cloneDeep(config), @@ -115,6 +122,7 @@ export async function moduleFromConfig(garden: Garden, graph: ConfigGraph, confi flatten(config.taskConfigs.map((taskConfig) => taskConfig.dependencies).filter((deps) => !!deps)) ), + compatibleTypes, _ConfigType: config, } diff --git a/garden-service/src/types/plugin/base.ts b/garden-service/src/types/plugin/base.ts index 574bcd2acb..5cd19f51a7 100644 --- a/garden-service/src/types/plugin/base.ts +++ b/garden-service/src/types/plugin/base.ts @@ -39,7 +39,7 @@ export interface PluginModuleActionParamsBase extends module: T } export const moduleActionParamsSchema = actionParamsSchema.keys({ - module: moduleSchema, + module: joi.lazy(() => moduleSchema), }) export interface PluginServiceActionParamsBase @@ -70,13 +70,18 @@ export const runBaseParams = { } export interface RunResult { + // FIXME: this field can always be inferred moduleName: string + // FIXME: this field is overly specific, consider replacing with more generic metadata field(s) command: string[] + // FIXME: this field can always be inferred version: string success: boolean + // FIXME: we should avoid native Date objects startedAt: Date completedAt: Date log: string + // DEPRECATED output?: string } diff --git a/garden-service/src/types/plugin/plugin.ts b/garden-service/src/types/plugin/plugin.ts index a87656f343..8dd2265b20 100644 --- a/garden-service/src/types/plugin/plugin.ts +++ b/garden-service/src/types/plugin/plugin.ts @@ -40,6 +40,7 @@ import { dedent } from "../../util/string" import { pluginCommandSchema, PluginCommand } from "./command" import { getPortForward, GetPortForwardParams, GetPortForwardResult } from "./service/getPortForward" import { StopPortForwardParams, stopPortForward } from "./service/stopPortForward" +import { AugmentGraphResult, AugmentGraphParams, augmentGraph } from "./provider/augmentGraph" export interface ActionHandlerParamsBase { base?: ActionHandler @@ -104,6 +105,7 @@ export interface PluginActionDescription { export interface PluginActionParams { configureProvider: ConfigureProviderParams + augmentGraph: AugmentGraphParams getEnvironmentStatus: GetEnvironmentStatusParams prepareEnvironment: PrepareEnvironmentParams @@ -118,6 +120,7 @@ export interface PluginActionParams { export interface PluginActionOutputs { configureProvider: ConfigureProviderResult + augmentGraph: AugmentGraphResult getEnvironmentStatus: EnvironmentStatus prepareEnvironment: PrepareEnvironmentResult @@ -132,6 +135,8 @@ export interface PluginActionOutputs { const _pluginActionDescriptions: { [P in PluginActionName]: PluginActionDescription } = { configureProvider, + augmentGraph, + getEnvironmentStatus, prepareEnvironment, cleanupEnvironment, diff --git a/garden-service/src/types/plugin/provider/augmentGraph.ts b/garden-service/src/types/plugin/provider/augmentGraph.ts new file mode 100644 index 0000000000..145ee49b01 --- /dev/null +++ b/garden-service/src/types/plugin/provider/augmentGraph.ts @@ -0,0 +1,106 @@ +/* + * 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 { PluginActionParamsBase, actionParamsSchema } from "../base" +import { dedent } from "../../../util/string" +import { joi, joiArray, joiIdentifier } from "../../../config/common" +import { baseModuleSpecSchema, AddModuleSpec, modulePathSchema } from "../../../config/module" +import { Provider, providerSchema } from "../../../config/provider" +import { moduleSchema, Module } from "../../module" + +export interface AugmentGraphParams extends PluginActionParamsBase { + modules: Module[] + providers: Provider[] +} + +interface AddDependency { + by: string + on: string +} + +export interface AugmentGraphResult { + addBuildDependencies?: AddDependency[] + addRuntimeDependencies?: AddDependency[] + addModules?: AddModuleSpec[] +} + +const addModuleSchema = baseModuleSpecSchema.keys({ + path: modulePathSchema, +}) + +export const augmentGraph = { + description: dedent` + Add modules and/or dependency relationships to the project stack graph. See the individual output fields for + details. + + The handler receives all configured providers and their configs, as well as all previously defined modules + in the project, including all modules added by any \`augmentGraph\` handlers defined by other providers + that this provider depends on. Which is to say, all the \`augmentGraph\` handlers are called and their outputs + applied in dependency order. + + Note that this handler is called frequently when resolving module configuration, so it should return quickly + and avoid any external I/O. + `, + paramsSchema: actionParamsSchema.keys({ + modules: joiArray(joi.lazy(() => moduleSchema)).description( + dedent` + A list of all previously defined modules in the project, including all modules added by any \`augmentGraph\` + handlers defined by other providers that this provider depends on. + ` + ), + providers: joiArray(providerSchema).description("All configured providers in the project."), + }), + resultSchema: joi.object().keys({ + addBuildDependencies: joiArray( + joi.object().keys({ + by: joiIdentifier().description( + "The _dependant_, i.e. the module that should have a build dependency on `on`." + ), + on: joiIdentifier().description("The _dependency, i.e. the module that `by` should depend on."), + }) + ).description( + dedent` + Add build dependencies between two modules, where \`by\` depends on \`on\`. + + Both modules must be previously defined in the project, added by one of the providers that this provider depends + on, _or_ it can be one of the modules specified in \`addModules\`. + + The most common use case for this field is to make an existing module depend on one of the modules specified + in \`addModules\`. + ` + ), + addRuntimeDependencies: joiArray( + joi.object().keys({ + by: joiIdentifier().description( + "The _dependant_, i.e. the service or task that should have a runtime dependency on `on`." + ), + on: joiIdentifier().description("The _dependency, i.e. the service or task that `by` should depend on."), + }) + ).description( + dedent` + Add runtime dependencies between two services or tasks, where \`by\` depends on \`on\`. + + Both services/tasks must be previously defined in the project, added by one of the providers that this provider + depends on, _or_ it can be defined in one of the modules specified in \`addModules\`. + + The most common use case for this field is to make an existing service or task depend on one of the + services/tasks specified under \`addModules\`. + ` + ), + addModules: joiArray(addModuleSchema).description( + dedent` + Add modules (of any defined kind) to the stack graph. Each should be a module spec in the same format as + a normal module specified in a \`garden.yml\` config file (which will later be passed to the appropriate + \`configure\` handler(s) for the module type), with the addition of \`path\` being required. + + The added modules can be referenced in \`addBuildDependencies\`, and their services/tasks can be referenced + in \`addRuntimeDependencies\`. + ` + ), + }), +} diff --git a/garden-service/src/util/validate-dependencies.ts b/garden-service/src/util/validate-dependencies.ts index 20f43c0bd4..4b97609122 100644 --- a/garden-service/src/util/validate-dependencies.ts +++ b/garden-service/src/util/validate-dependencies.ts @@ -18,6 +18,7 @@ import { ModuleConfig } from "../config/module" import { deline } from "./string" export function validateDependencies(moduleConfigs: ModuleConfig[], serviceNames: string[], taskNames: string[]): void { + // TODO: indicate in errors when modules are added by providers const missingDepsError = detectMissingDependencies(moduleConfigs, serviceNames, taskNames) const circularDepsError = detectCircularModuleDependencies(moduleConfigs) @@ -69,7 +70,7 @@ export function detectMissingDependencies( for (const [configKey, entityName] of runtimeDepTypes) { for (const config of m[configKey]) { - for (const missingRuntimeDep of config.dependencies.filter((d) => !runtimeNames.has(d))) { + for (const missingRuntimeDep of config.dependencies.filter((d: string) => !runtimeNames.has(d))) { missingDepDescriptions.push(deline` ${entityName} '${config.name}' (in module '${m.name}'): Unknown service or task '${missingRuntimeDep}' referenced in dependencies.`) diff --git a/garden-service/src/watch.ts b/garden-service/src/watch.ts index bc76765631..90402e4350 100644 --- a/garden-service/src/watch.ts +++ b/garden-service/src/watch.ts @@ -256,7 +256,7 @@ export class Watcher extends EventEmitter { private async updateModules() { this.log.silly(`Watcher: Updating list of modules`) - const graph = await this.garden.getConfigGraph() + const graph = await this.garden.getConfigGraph(this.log) this.modules = await graph.getModules() } diff --git a/garden-service/test/integ/src/plugins/kubernetes/container/build.ts b/garden-service/test/integ/src/plugins/kubernetes/container/build.ts index bb92c78ddf..7f44f920de 100644 --- a/garden-service/test/integ/src/plugins/kubernetes/container/build.ts +++ b/garden-service/test/integ/src/plugins/kubernetes/container/build.ts @@ -38,7 +38,7 @@ describe("k8sBuildContainer", () => { } garden = await makeTestGarden(root, { environmentName }) - graph = await garden.getConfigGraph() + graph = await garden.getConfigGraph(garden.log) provider = await garden.resolveProvider("local-kubernetes") ctx = garden.getPluginContext(provider) diff --git a/garden-service/test/integ/src/plugins/kubernetes/container/container.ts b/garden-service/test/integ/src/plugins/kubernetes/container/container.ts index ae7ba5bf06..22cc58772a 100644 --- a/garden-service/test/integ/src/plugins/kubernetes/container/container.ts +++ b/garden-service/test/integ/src/plugins/kubernetes/container/container.ts @@ -24,7 +24,7 @@ describe("kubernetes container module handlers", () => { before(async () => { const root = getDataDir("test-projects", "container") garden = await makeTestGarden(root) - graph = await garden.getConfigGraph() + graph = await garden.getConfigGraph(garden.log) provider = await garden.resolveProvider("local-kubernetes") }) diff --git a/garden-service/test/integ/src/plugins/kubernetes/container/logs.ts b/garden-service/test/integ/src/plugins/kubernetes/container/logs.ts index 3d2c58b74f..cdb5b94a0f 100644 --- a/garden-service/test/integ/src/plugins/kubernetes/container/logs.ts +++ b/garden-service/test/integ/src/plugins/kubernetes/container/logs.ts @@ -19,7 +19,7 @@ describe("kubernetes", () => { before(async () => { const root = getDataDir("test-projects", "container") garden = await makeTestGarden(root) - graph = await garden.getConfigGraph() + graph = await garden.getConfigGraph(garden.log) provider = await garden.resolveProvider("local-kubernetes") ctx = garden.getPluginContext(provider) }) diff --git a/garden-service/test/integ/src/plugins/kubernetes/helm/common.ts b/garden-service/test/integ/src/plugins/kubernetes/helm/common.ts index e0ec28494f..580ad79c07 100644 --- a/garden-service/test/integ/src/plugins/kubernetes/helm/common.ts +++ b/garden-service/test/integ/src/plugins/kubernetes/helm/common.ts @@ -50,12 +50,12 @@ describe("Helm common functions", () => { const provider = await garden.resolveProvider("local-kubernetes") ctx = garden.getPluginContext(provider) log = garden.log - graph = await garden.getConfigGraph() + graph = await garden.getConfigGraph(garden.log) await buildModules() }) beforeEach(async () => { - graph = await garden.getConfigGraph() + graph = await garden.getConfigGraph(garden.log) }) async function buildModules() { diff --git a/garden-service/test/integ/src/plugins/kubernetes/helm/config.ts b/garden-service/test/integ/src/plugins/kubernetes/helm/config.ts index 92432e2626..01d6f8a83a 100644 --- a/garden-service/test/integ/src/plugins/kubernetes/helm/config.ts +++ b/garden-service/test/integ/src/plugins/kubernetes/helm/config.ts @@ -19,7 +19,7 @@ describe("validateHelmModule", () => { garden = await getHelmTestGarden() const provider = await garden.resolveProvider("local-kubernetes") ctx = garden.getPluginContext(provider) - await garden.resolveModuleConfigs() + await garden["resolveModuleConfigs"](garden.log) moduleConfigs = cloneDeep((garden).moduleConfigs) }) @@ -32,8 +32,8 @@ describe("validateHelmModule", () => { } it("should validate a Helm module", async () => { - const config = await garden.resolveModuleConfig("api") - const graph = await garden.getConfigGraph() + const config = await garden.resolveModuleConfig(garden.log, "api") + const graph = await garden.getConfigGraph(garden.log) const imageModule = await graph.getModule("api-image") const { versionString } = imageModule.version @@ -124,14 +124,14 @@ describe("validateHelmModule", () => { it("should not return a serviceConfig if skipDeploy=true", async () => { patchModuleConfig("api", { spec: { skipDeploy: true } }) - const config = await garden.resolveModuleConfig("api") + const config = await garden.resolveModuleConfig(garden.log, "api") expect(config.serviceConfigs).to.eql([]) }) it("should add the module specified under 'base' as a build dependency", async () => { patchModuleConfig("postgres", { spec: { base: "foo" } }) - const config = await garden.resolveModuleConfig("postgres") + const config = await garden.resolveModuleConfig(garden.log, "postgres") expect(config.build.dependencies).to.eql([{ name: "foo", copy: [{ source: "*", target: "." }] }]) }) @@ -141,7 +141,7 @@ describe("validateHelmModule", () => { build: { dependencies: [{ name: "foo", copy: [] }] }, spec: { base: "foo" }, }) - const config = await garden.resolveModuleConfig("postgres") + const config = await garden.resolveModuleConfig(garden.log, "postgres") expect(config.build.dependencies).to.eql([{ name: "foo", copy: [{ source: "*", target: "." }] }]) }) @@ -157,7 +157,7 @@ describe("validateHelmModule", () => { ], }, }) - const config = await garden.resolveModuleConfig("api") + const config = await garden.resolveModuleConfig(garden.log, "api") expect(config.build.dependencies).to.eql([{ name: "foo", copy: [] }]) }) @@ -173,7 +173,7 @@ describe("validateHelmModule", () => { ], }, }) - const config = await garden.resolveModuleConfig("api") + const config = await garden.resolveModuleConfig(garden.log, "api") expect(config.build.dependencies).to.eql([{ name: "foo", copy: [] }]) }) @@ -182,7 +182,7 @@ describe("validateHelmModule", () => { patchModuleConfig("api", { spec: { base: "foo" } }) await expectError( - () => garden.resolveModuleConfig("api"), + () => garden.resolveModuleConfig(garden.log, "api"), (err) => expect(err.message).to.equal(deline` Helm module 'api' both contains sources and specifies a base module. @@ -196,7 +196,7 @@ describe("validateHelmModule", () => { patchModuleConfig("postgres", { spec: { chart: null } }) await expectError( - () => garden.resolveModuleConfig("postgres"), + () => garden.resolveModuleConfig(garden.log, "postgres"), (err) => expect(err.message).to.equal(deline` Module 'postgres' neither specifies a chart name, base module, nor contains chart sources at \`chartPath\`. diff --git a/garden-service/test/integ/src/plugins/kubernetes/helm/hot-reload.ts b/garden-service/test/integ/src/plugins/kubernetes/helm/hot-reload.ts index 86a2e304f1..4d9f6c1719 100644 --- a/garden-service/test/integ/src/plugins/kubernetes/helm/hot-reload.ts +++ b/garden-service/test/integ/src/plugins/kubernetes/helm/hot-reload.ts @@ -15,7 +15,7 @@ describe("getHotReloadSpec", () => { }) beforeEach(async () => { - graph = await garden.getConfigGraph() + graph = await garden.getConfigGraph(garden.log) }) it("should retrieve the hot reload spec on the service's source module", async () => { diff --git a/garden-service/test/integ/src/plugins/kubernetes/helm/run.ts b/garden-service/test/integ/src/plugins/kubernetes/helm/run.ts index c09ebf2594..26ae38428c 100644 --- a/garden-service/test/integ/src/plugins/kubernetes/helm/run.ts +++ b/garden-service/test/integ/src/plugins/kubernetes/helm/run.ts @@ -16,7 +16,7 @@ describe("runHelmTask", () => { }) beforeEach(async () => { - graph = await garden.getConfigGraph() + graph = await garden.getConfigGraph(garden.log) }) it("should run a basic task", async () => { diff --git a/garden-service/test/integ/src/plugins/kubernetes/helm/test.ts b/garden-service/test/integ/src/plugins/kubernetes/helm/test.ts index 6bd26aba93..e84e728a88 100644 --- a/garden-service/test/integ/src/plugins/kubernetes/helm/test.ts +++ b/garden-service/test/integ/src/plugins/kubernetes/helm/test.ts @@ -17,7 +17,7 @@ describe("testHelmModule", () => { }) beforeEach(async () => { - graph = await garden.getConfigGraph() + graph = await garden.getConfigGraph(garden.log) }) it("should run a basic test", async () => { diff --git a/garden-service/test/integ/src/plugins/kubernetes/util.ts b/garden-service/test/integ/src/plugins/kubernetes/util.ts index 8925b4ffd3..796f4af7ee 100644 --- a/garden-service/test/integ/src/plugins/kubernetes/util.ts +++ b/garden-service/test/integ/src/plugins/kubernetes/util.ts @@ -22,7 +22,7 @@ describe("util", () => { before(async () => { const root = getDataDir("test-projects", "container") garden = await makeTestGarden(root) - graph = await garden.getConfigGraph() + graph = await garden.getConfigGraph(garden.log) provider = (await garden.resolveProvider("local-kubernetes")) as Provider api = await KubeApi.factory(garden.log, provider) }) diff --git a/garden-service/test/unit/src/actions.ts b/garden-service/test/unit/src/actions.ts index ecb1c230bb..99da2bd1b4 100644 --- a/garden-service/test/unit/src/actions.ts +++ b/garden-service/test/unit/src/actions.ts @@ -60,7 +60,7 @@ describe("ActionRouter", () => { }) log = garden.log actions = await garden.getActionRouter() - const graph = await garden.getConfigGraph() + const graph = await garden.getConfigGraph(garden.log) module = await graph.getModule("module-a") service = await graph.getService("service-a") runtimeContext = await prepareRuntimeContext({ @@ -79,11 +79,15 @@ describe("ActionRouter", () => { task = await graph.getTask("task-a") }) + after(async () => { + await garden.close() + }) + // Note: The test plugins below implicitly validate input params for each of the tests describe("environment actions", () => { describe("configureProvider", () => { it("should configure the provider", async () => { - const config = { foo: "bar" } + const config = { name: "test-plugin", foo: "bar" } const result = await actions.configureProvider({ pluginName: "test-plugin", log, @@ -95,6 +99,40 @@ describe("ActionRouter", () => { }) expect(result).to.eql({ config, + moduleConfigs: [], + }) + }) + }) + + describe("augmentGraph", () => { + it("should return modules and/or dependency relations to add to the stack graph", async () => { + const graph = await garden.getConfigGraph(garden.log) + const modules = await graph.getModules() + const providers = await garden.resolveProviders() + const result = await actions.augmentGraph({ + log, + pluginName: "test-plugin", + modules, + providers, + }) + + const name = "added-by-test-plugin" + + expect(result).to.eql({ + addBuildDependencies: [{ by: name, on: "module-b" }], + addRuntimeDependencies: [{ by: name, on: "service-b" }], + addModules: [ + { + apiVersion: DEFAULT_API_VERSION, + kind: "Module", + name, + type: "test", + path: garden.projectRoot, + services: [{ name }], + allowPublish: true, + build: { dependencies: [] }, + }, + ], }) }) }) @@ -579,7 +617,7 @@ describe("ActionRouter", () => { it("should copy artifacts exported by the handler to the artifacts directory", async () => { await emptyDir(garden.artifactsPath) - const graph = await garden.getConfigGraph() + const graph = await garden.getConfigGraph(garden.log) const _task = await graph.getTask("task-a") _task.spec.artifacts = [ @@ -646,8 +684,8 @@ describe("ActionRouter", () => { const pluginName = "test-plugin-b" const handler = await actionsA["getActionHandler"]({ actionType: "prepareEnvironment", pluginName }) - expect(handler.actionType).to.equal("prepareEnvironment") - expect(handler.pluginName).to.equal(pluginName) + expect(handler!.actionType).to.equal("prepareEnvironment") + expect(handler!.pluginName).to.equal(pluginName) }) it("should throw if no handler is available", async () => { @@ -1244,7 +1282,7 @@ describe("ActionRouter", () => { }, }) - const graph = await garden.getConfigGraph() + const graph = await garden.getConfigGraph(garden.log) const moduleA = await graph.getModule("module-a") const base = Object.assign( @@ -1283,7 +1321,7 @@ describe("ActionRouter", () => { }, }) - const graph = await garden.getConfigGraph() + const graph = await garden.getConfigGraph(garden.log) const serviceA = await graph.getService("service-a") const base = Object.assign( @@ -1326,7 +1364,7 @@ describe("ActionRouter", () => { garden["moduleConfigs"]["module-a"].spec.foo = "${runtime.services.service-b.outputs.foo}" - const graph = await garden.getConfigGraph() + const graph = await garden.getConfigGraph(garden.log) const serviceA = await graph.getService("service-a") const serviceB = await graph.getService("service-b") @@ -1378,7 +1416,7 @@ describe("ActionRouter", () => { garden["moduleConfigs"]["module-a"].spec.services[0].foo = "${runtime.services.service-b.outputs.foo}" - const graph = await garden.getConfigGraph() + const graph = await garden.getConfigGraph(garden.log) const serviceA = await graph.getService("service-a") const _runtimeContext = await prepareRuntimeContext({ @@ -1429,7 +1467,7 @@ describe("ActionRouter", () => { }, }) - const graph = await garden.getConfigGraph() + const graph = await garden.getConfigGraph(garden.log) const taskA = await graph.getTask("task-a") const base = Object.assign( @@ -1489,7 +1527,7 @@ describe("ActionRouter", () => { garden["moduleConfigs"]["module-a"].spec.tasks[0].foo = "${runtime.services.service-b.outputs.foo}" - const graph = await garden.getConfigGraph() + const graph = await garden.getConfigGraph(garden.log) const taskA = await graph.getTask("task-a") const serviceB = await graph.getService("service-b") @@ -1552,7 +1590,7 @@ describe("ActionRouter", () => { garden["moduleConfigs"]["module-a"].spec.tasks[0].foo = "${runtime.services.service-b.outputs.foo}" - const graph = await garden.getConfigGraph() + const graph = await garden.getConfigGraph(garden.log) const taskA = await graph.getTask("task-a") const _runtimeContext = await prepareRuntimeContext({ @@ -1618,6 +1656,11 @@ const testPlugin = createGardenPlugin({ dependencies: ["base"], handlers: { + configureProvider: async (params) => { + validate(params, pluginActionDescriptions.configureProvider.paramsSchema) + return { config: params.config } + }, + getEnvironmentStatus: async (params) => { validate(params, pluginActionDescriptions.getEnvironmentStatus.paramsSchema) return { @@ -1626,6 +1669,35 @@ const testPlugin = createGardenPlugin({ } }, + augmentGraph: async (params) => { + validate(params, pluginActionDescriptions.augmentGraph.paramsSchema) + + const moduleName = "added-by-" + params.ctx.provider.name + + return { + addBuildDependencies: [{ by: moduleName, on: "module-b" }], + addRuntimeDependencies: [{ by: moduleName, on: "service-b" }], + addModules: [ + { + kind: "Module", + name: moduleName, + type: "test", + path: projectRootA, + services: [ + { + name: moduleName, + }, + ], + }, + ], + } + }, + + getDebugInfo: async (params) => { + validate(params, pluginActionDescriptions.getDebugInfo.paramsSchema) + return { info: {} } + }, + prepareEnvironment: async (params) => { validate(params, pluginActionDescriptions.prepareEnvironment.paramsSchema) return { status: { ready: true, outputs: {} } } diff --git a/garden-service/test/unit/src/build-dir.ts b/garden-service/test/unit/src/build-dir.ts index 39ec839002..6cbf0cf8e2 100644 --- a/garden-service/test/unit/src/build-dir.ts +++ b/garden-service/test/unit/src/build-dir.ts @@ -45,14 +45,14 @@ describe("BuildDir", () => { }) it("should ensure that a module's build subdir exists before returning from buildPath", async () => { - const moduleA = await garden.resolveModuleConfig("module-a") + const moduleA = await garden.resolveModuleConfig(garden.log, "module-a") const buildPath = await garden.buildDir.buildPath(moduleA) expect(await pathExists(buildPath)).to.eql(true) }) describe("syncFromSrc", () => { it("should sync sources to the build dir", async () => { - const graph = await garden.getConfigGraph() + const graph = await garden.getConfigGraph(garden.log) const moduleA = await graph.getModule("module-a") await garden.buildDir.syncFromSrc(moduleA, garden.log) const buildDirA = await garden.buildDir.buildPath(moduleA) @@ -65,7 +65,7 @@ describe("BuildDir", () => { }) it("should not sync sources for local exec modules", async () => { - const graph = await garden.getConfigGraph() + const graph = await garden.getConfigGraph(garden.log) const moduleE = await graph.getModule("module-e") await garden.buildDir.syncFromSrc(moduleE, garden.log) // This is the dir Garden would have synced the sources into @@ -75,7 +75,7 @@ describe("BuildDir", () => { }) it("should respect the file list in the module's version", async () => { - const graph = await garden.getConfigGraph() + const graph = await garden.getConfigGraph(garden.log) const moduleA = await graph.getModule("module-a") moduleA.version.files = [await getConfigFilePath(moduleA.path)] @@ -88,7 +88,7 @@ describe("BuildDir", () => { }) it("should delete files that are not being synced from the module source directory", async () => { - const graph = await garden.getConfigGraph() + const graph = await garden.getConfigGraph(garden.log) const moduleA = await graph.getModule("module-a") const buildDirA = await garden.buildDir.buildPath(moduleA) @@ -108,7 +108,7 @@ describe("BuildDir", () => { const log = garden.log try { - const graph = await garden.getConfigGraph() + const graph = await garden.getConfigGraph(garden.log) const modules = await graph.getModules() const tasks = modules.map( (module) => @@ -122,8 +122,8 @@ describe("BuildDir", () => { await garden.processTasks(tasks) - const moduleD = await garden.resolveModuleConfig("module-d") - const moduleF = await garden.resolveModuleConfig("module-f") + const moduleD = await garden.resolveModuleConfig(garden.log, "module-d") + const moduleF = await garden.resolveModuleConfig(garden.log, "module-f") const buildDirD = await garden.buildDir.buildPath(moduleD) const buildDirF = await garden.buildDir.buildPath(moduleF) @@ -153,7 +153,7 @@ describe("BuildDir", () => { describe("buildPath", () => { it("should ensure the build path and return it", async () => { - const graph = await garden.getConfigGraph() + const graph = await garden.getConfigGraph(garden.log) const moduleA = await graph.getModule("module-a") const buildDirA = await garden.buildDir.buildPath(moduleA) @@ -162,7 +162,7 @@ describe("BuildDir", () => { }) it("should return the module path for a local exec modules", async () => { - const graph = await garden.getConfigGraph() + const graph = await garden.getConfigGraph(garden.log) const moduleE = await graph.getModule("module-e") const buildDirE = await garden.buildDir.buildPath(moduleE) diff --git a/garden-service/test/unit/src/commands/get/get-config.ts b/garden-service/test/unit/src/commands/get/get-config.ts index d98f403566..449b703e0c 100644 --- a/garden-service/test/unit/src/commands/get/get-config.ts +++ b/garden-service/test/unit/src/commands/get/get-config.ts @@ -27,7 +27,7 @@ describe("GetConfigCommand", () => { environmentName: garden.environmentName, providers, variables: garden.variables, - moduleConfigs: sortBy(await garden.resolveModuleConfigs(), "name"), + moduleConfigs: sortBy(await garden["resolveModuleConfigs"](log), "name"), projectRoot: garden.projectRoot, } diff --git a/garden-service/test/unit/src/commands/get/get-debug-info.ts b/garden-service/test/unit/src/commands/get/get-debug-info.ts index 253c3ef523..6618c58912 100644 --- a/garden-service/test/unit/src/commands/get/get-debug-info.ts +++ b/garden-service/test/unit/src/commands/get/get-debug-info.ts @@ -97,7 +97,7 @@ describe("GetDebugInfoCommand", () => { // we first check if the main garden.yml exists expect(await pathExists(await getConfigFilePath(gardenDebugTmp))).to.equal(true) - const graph = await garden.getConfigGraph() + const graph = await garden.getConfigGraph(garden.log) // Check that each module config files have been copied over and // the folder structure is maintained diff --git a/garden-service/test/unit/src/commands/get/get-task-result.ts b/garden-service/test/unit/src/commands/get/get-task-result.ts index 6ee419b035..97f64b76b3 100644 --- a/garden-service/test/unit/src/commands/get/get-task-result.ts +++ b/garden-service/test/unit/src/commands/get/get-task-result.ts @@ -111,7 +111,7 @@ describe("GetTaskResultCommand", () => { it("should include paths to artifacts if artifacts exist", async () => { const name = "task-a" - const graph = await garden.getConfigGraph() + const graph = await garden.getConfigGraph(garden.log) const module = await graph.getModule("module-a") const artifactKey = getArtifactKey("task", name, module.version.versionString) const metadataPath = join(garden.artifactsPath, `.metadata.${artifactKey}.json`) diff --git a/garden-service/test/unit/src/commands/get/get-test-result.ts b/garden-service/test/unit/src/commands/get/get-test-result.ts index 9fec8e6ef0..ab0b5c2120 100644 --- a/garden-service/test/unit/src/commands/get/get-test-result.ts +++ b/garden-service/test/unit/src/commands/get/get-test-result.ts @@ -113,7 +113,7 @@ describe("GetTestResultCommand", () => { it("should include paths to artifacts if artifacts exist", async () => { const name = "unit" - const graph = await garden.getConfigGraph() + const graph = await garden.getConfigGraph(garden.log) const module = await graph.getModule("module-a") const artifactKey = getArtifactKey("test", name, module.version.versionString) const metadataPath = join(garden.artifactsPath, `.metadata.${artifactKey}.json`) diff --git a/garden-service/test/unit/src/config-graph.ts b/garden-service/test/unit/src/config-graph.ts index 6144fb1367..7d54dcaa20 100644 --- a/garden-service/test/unit/src/config-graph.ts +++ b/garden-service/test/unit/src/config-graph.ts @@ -11,14 +11,14 @@ describe("ConfigGraph", () => { before(async () => { gardenA = await makeTestGardenA() - graphA = await gardenA.getConfigGraph() + graphA = await gardenA.getConfigGraph(gardenA.log) }) it("should throw when two services have the same name", async () => { const garden = await makeTestGarden(resolve(dataDir, "test-projects", "duplicate-service")) await expectError( - () => garden.getConfigGraph(), + () => garden.getConfigGraph(garden.log), (err) => expect(err.message).to.equal( "Service names must be unique - the service name 'dupe' is declared multiple times " + @@ -31,7 +31,7 @@ describe("ConfigGraph", () => { const garden = await makeTestGarden(resolve(dataDir, "test-projects", "duplicate-task")) await expectError( - () => garden.getConfigGraph(), + () => garden.getConfigGraph(garden.log), (err) => expect(err.message).to.equal( "Task names must be unique - the task name 'dupe' is declared multiple times " + @@ -44,7 +44,7 @@ describe("ConfigGraph", () => { const garden = await makeTestGarden(resolve(dataDir, "test-projects", "duplicate-service-and-task")) await expectError( - () => garden.getConfigGraph(), + () => garden.getConfigGraph(garden.log), (err) => expect(err.message).to.equal( "Service and task names must be mutually unique - the name 'dupe' is used for a task " + @@ -55,7 +55,7 @@ describe("ConfigGraph", () => { it("should automatically add service source modules as module build dependencies", async () => { const garden = await makeTestGarden(resolve(dataDir, "test-projects", "source-module")) - const graph = await garden.getConfigGraph() + const graph = await garden.getConfigGraph(garden.log) const module = await graph.getModule("module-b") expect(module.build.dependencies).to.eql([{ name: "module-a", copy: [] }]) }) diff --git a/garden-service/test/unit/src/config/config-context.ts b/garden-service/test/unit/src/config/config-context.ts index a571a1d630..132db347e8 100644 --- a/garden-service/test/unit/src/config/config-context.ts +++ b/garden-service/test/unit/src/config/config-context.ts @@ -280,7 +280,8 @@ describe("ModuleConfigContext", () => { }) it("should should resolve the version of a module", async () => { - const { versionString } = await garden.resolveVersion("module-a", []) + const config = await garden.resolveModuleConfig(garden.log, "module-a") + const { versionString } = await garden.resolveVersion(config, []) expect(await c.resolve({ key: ["modules", "module-a", "version"], nodePath: [], opts: {} })).to.equal(versionString) }) @@ -288,11 +289,6 @@ describe("ModuleConfigContext", () => { expect(await c.resolve({ key: ["modules", "module-a", "outputs", "foo"], nodePath: [], opts: {} })).to.equal("bar") }) - it("should should resolve the version of a module", async () => { - const { versionString } = await garden.resolveVersion("module-a", []) - expect(await c.resolve({ key: ["modules", "module-a", "version"], nodePath: [], opts: {} })).to.equal(versionString) - }) - it("should should resolve a project variable", async () => { expect(await c.resolve({ key: ["variables", "some"], nodePath: [], opts: {} })).to.equal("variable") }) @@ -314,7 +310,7 @@ describe("ModuleConfigContext", () => { let serviceA: Service before(async () => { - const graph = await garden.getConfigGraph() + const graph = await garden.getConfigGraph(garden.log) serviceA = await graph.getService("service-a") const serviceB = await graph.getService("service-b") const taskB = await graph.getTask("task-b") diff --git a/garden-service/test/unit/src/garden.ts b/garden-service/test/unit/src/garden.ts index 5fc1f74324..be41337c33 100644 --- a/garden-service/test/unit/src/garden.ts +++ b/garden-service/test/unit/src/garden.ts @@ -19,7 +19,7 @@ import { resetLocalConfig, testGitUrl, } from "../../helpers" -import { getNames, findByName } from "../../../src/util/util" +import { getNames, findByName, deepOmitUndefined } from "../../../src/util/util" import { MOCK_CONFIG } from "../../../src/cli/cli" import { LinkedSource } from "../../../src/config-store" import { ModuleVersion } from "../../../src/vcs/vcs" @@ -27,7 +27,7 @@ import { getModuleCacheContext } from "../../../src/types/module" import { createGardenPlugin, GardenPlugin } from "../../../src/types/plugin/plugin" import { ConfigureProviderParams } from "../../../src/types/plugin/provider/configureProvider" import { ProjectConfig } from "../../../src/config/project" -import { ModuleConfig, baseModuleSpecSchema } from "../../../src/config/module" +import { ModuleConfig, baseModuleSpecSchema, baseBuildSpecSchema } from "../../../src/config/module" import { DEFAULT_API_VERSION } from "../../../src/constants" import { providerConfigBaseSchema } from "../../../src/config/provider" import { keyBy, set } from "lodash" @@ -1450,7 +1450,7 @@ describe("Garden", () => { const garden = await TestGarden.factory(projectRootA, { config: projectConfig, plugins: [test] }) - const graph = await garden.getConfigGraph() + const graph = await garden.getConfigGraph(garden.log) expect(await graph.getModule("test--foo")).to.exist }) @@ -1976,7 +1976,7 @@ describe("Garden", () => { const garden = await makeTestGardenA() await garden.scanModules() - const modules = await garden.resolveModuleConfigs() + const modules = await garden["resolveModuleConfigs"](garden.log) expect(getNames(modules).sort()).to.eql(["module-a", "module-b", "module-c"]) }) @@ -1984,7 +1984,7 @@ describe("Garden", () => { const garden = await makeTestGarden(resolve(dataDir, "test-projects", "multiple-module-config")) await garden.scanModules() - const modules = await garden.resolveModuleConfigs() + const modules = await garden["resolveModuleConfigs"](garden.log) expect(getNames(modules).sort()).to.eql([ "module-a1", "module-a2", @@ -1998,7 +1998,7 @@ describe("Garden", () => { it("should scan and add modules for projects with external project sources", async () => { const garden = await makeExtProjectSourcesGarden() await garden.scanModules() - const modules = await garden.resolveModuleConfigs() + const modules = await garden["resolveModuleConfigs"](garden.log) expect(getNames(modules).sort()).to.eql(["module-a", "module-b", "module-c"]) }) @@ -2016,14 +2016,14 @@ describe("Garden", () => { it("should scan and add modules with config files with yaml and yml extensions", async () => { const garden = await makeTestGarden(getDataDir("test-project-yaml-file-extensions")) - const modules = await garden.resolveModuleConfigs() + const modules = await garden["resolveModuleConfigs"](garden.log) expect(getNames(modules).sort()).to.eql(["module-yaml", "module-yml"]) }) it("should respect the modules.include and modules.exclude fields, if specified", async () => { const projectRoot = getDataDir("test-projects", "project-include-exclude") const garden = await makeTestGarden(projectRoot) - const moduleConfigs = await garden.resolveModuleConfigs() + const moduleConfigs = await garden["resolveModuleConfigs"](garden.log) // Should NOT include "nope" and "module-c" expect(getNames(moduleConfigs).sort()).to.eql(["module-a", "module-b"]) @@ -2032,7 +2032,7 @@ describe("Garden", () => { it("should respect .gitignore and .gardenignore files", async () => { const projectRoot = getDataDir("test-projects", "dotignore") const garden = await makeTestGarden(projectRoot) - const moduleConfigs = await garden.resolveModuleConfigs() + const moduleConfigs = await garden["resolveModuleConfigs"](garden.log) expect(getNames(moduleConfigs).sort()).to.eql(["module-a"]) }) @@ -2040,7 +2040,7 @@ describe("Garden", () => { it("should respect custom dotignore files", async () => { const projectRoot = getDataDir("test-projects", "dotignore") const garden = await makeTestGarden(projectRoot) - const moduleConfigs = await garden.resolveModuleConfigs() + const moduleConfigs = await garden["resolveModuleConfigs"](garden.log) expect(getNames(moduleConfigs).sort()).to.eql(["module-a"]) }) @@ -2068,7 +2068,7 @@ describe("Garden", () => { const projectRoot = resolve(dataDir, "test-projects", "module-self-ref") const garden = await makeTestGarden(projectRoot) await expectError( - () => garden.resolveModuleConfigs(), + () => garden["resolveModuleConfigs"](garden.log), (err) => expect(err.message).to.equal( "Invalid template string ${modules.module-a.version}: " + @@ -2081,7 +2081,7 @@ describe("Garden", () => { const projectRoot = resolve(dataDir, "test-project-ext-module-sources") const garden = await makeExtModuleSourcesGarden() - const module = await garden.resolveModuleConfig("module-a") + const module = await garden.resolveModuleConfig(garden.log, "module-a") expect(module!.path).to.equal(join(projectRoot, ".garden", "sources", "module", `module-a--${testGitUrlHash}`)) }) @@ -2090,7 +2090,7 @@ describe("Garden", () => { const projectRoot = getDataDir("test-projects", "non-string-template-values") const garden = await makeTestGarden(projectRoot) - const module = await garden.resolveModuleConfig("module-a") + const module = await garden.resolveModuleConfig(garden.log, "module-a") // We template in the value for the module's allowPublish field to test this expect(module.allowPublish).to.equal(false) @@ -2100,7 +2100,7 @@ describe("Garden", () => { const projectRoot = getDataDir("test-projects", "1067-module-ref-within-file") const garden = await makeTestGarden(projectRoot) // This should just complete successfully - await garden.resolveModuleConfigs() + await garden["resolveModuleConfigs"](garden.log) }) it("should throw if a module type is not recognized", async () => { @@ -2110,7 +2110,7 @@ describe("Garden", () => { config.type = "foo" await expectError( - () => garden.resolveModuleConfigs(), + () => garden["resolveModuleConfigs"](garden.log), (err) => expect(err.message).to.equal( "Unrecognized module type 'foo' (defined at module-a/garden.yml). Are you missing a provider configuration?" @@ -2153,7 +2153,7 @@ describe("Garden", () => { } await expectError( - () => garden.resolveModuleConfigs(), + () => garden["resolveModuleConfigs"](garden.log), (err) => expect(stripAnsi(err.message)).to.equal(deline` Error validating module 'foo' (/garden.yml): key "bla" is not allowed at path [bla] @@ -2203,34 +2203,185 @@ describe("Garden", () => { } await expectError( - () => garden.resolveModuleConfigs(), + () => garden["resolveModuleConfigs"](garden.log), (err) => expect(stripAnsi(err.message)).to.equal(deline` Error validating outputs for module 'foo' (/garden.yml): key .foo must be a string `) ) }) + }) - context("module type has a base", () => { - it("should throw if the configure handler output doesn't match the module type's base schema", async () => { - const base = { - name: "base", + context("module type has a base", () => { + it("should throw if the configure handler output doesn't match the module type's base schema", async () => { + const base = { + name: "base", + createModuleTypes: [ + { + name: "base", + docs: "base", + schema: joi.object().keys({ base: joi.string().required() }), + handlers: {}, + }, + ], + } + const foo = { + name: "foo", + dependencies: ["base"], + createModuleTypes: [ + { + name: "foo", + base: "base", + docs: "foo", + schema: joi.object().keys({ foo: joi.string().required() }), + handlers: { + configure: async ({ moduleConfig }) => ({ + moduleConfig: { + ...moduleConfig, + spec: { + ...moduleConfig.spec, + foo: "bar", + }, + }, + }), + }, + }, + ], + } + + const garden = await Garden.factory(pathFoo, { + plugins: [base, foo], + config: { + ...projectConfigFoo, + providers: [...projectConfigFoo.providers, { name: "base" }], + }, + }) + + garden["moduleConfigs"] = { + foo: { + apiVersion: DEFAULT_API_VERSION, + name: "foo", + type: "foo", + allowPublish: false, + build: { dependencies: [] }, + outputs: {}, + path: pathFoo, + serviceConfigs: [], + taskConfigs: [], + testConfigs: [], + spec: { foo: "bar" }, + }, + } + + await expectError( + () => garden["resolveModuleConfigs"](garden.log), + (err) => + expect(stripAnsi(err.message)).to.equal(deline` + Error validating configuration for module 'foo' + (base schema from 'base' plugin) (/garden.yml): key .base is required + `) + ) + }) + + it("should throw if the module outputs don't match the base's declared outputs schema", async () => { + const base = { + name: "base", + createModuleTypes: [ + { + name: "base", + docs: "base", + moduleOutputsSchema: joi.object().keys({ foo: joi.string() }), + handlers: {}, + }, + ], + } + const foo = { + name: "foo", + dependencies: ["base"], + createModuleTypes: [ + { + name: "foo", + base: "base", + docs: "foo", + handlers: { + configure: async ({ moduleConfig }) => ({ + moduleConfig: { + ...moduleConfig, + outputs: { foo: 123 }, + }, + }), + }, + }, + ], + } + + const garden = await Garden.factory(pathFoo, { + plugins: [base, foo], + config: { + ...projectConfigFoo, + providers: [...projectConfigFoo.providers, { name: "base" }], + }, + }) + + garden["moduleConfigs"] = { + foo: { + apiVersion: DEFAULT_API_VERSION, + name: "foo", + type: "foo", + allowPublish: false, + build: { dependencies: [] }, + outputs: {}, + path: pathFoo, + serviceConfigs: [], + taskConfigs: [], + testConfigs: [], + spec: {}, + }, + } + + await expectError( + () => garden["resolveModuleConfigs"](garden.log), + (err) => + expect(stripAnsi(err.message)).to.equal(deline` + Error validating outputs for module 'foo' (base schema from 'base' plugin) (/garden.yml): + key .foo must be a string + `) + ) + }) + + context("module type's base has a base", () => { + it("should throw if the configure handler output doesn't match the schema of the base's base", async () => { + const baseA = { + name: "base-a", createModuleTypes: [ { - name: "base", - docs: "base", + name: "base-a", + docs: "base-a", schema: joi.object().keys({ base: joi.string().required() }), handlers: {}, }, ], } + const baseB = { + name: "base-b", + dependencies: ["base-a"], + createModuleTypes: [ + { + name: "base-b", + docs: "base-b", + base: "base-a", + schema: joi.object().keys({ foo: joi.string() }), + handlers: {}, + }, + ], + } const foo = { name: "foo", - dependencies: ["base"], + dependencies: ["base-b"], createModuleTypes: [ { name: "foo", - base: "base", + base: "base-b", docs: "foo", schema: joi.object().keys({ foo: joi.string().required() }), handlers: { @@ -2249,10 +2400,10 @@ describe("Garden", () => { } const garden = await Garden.factory(pathFoo, { - plugins: [base, foo], + plugins: [baseA, baseB, foo], config: { ...projectConfigFoo, - providers: [...projectConfigFoo.providers, { name: "base" }], + providers: [...projectConfigFoo.providers, { name: "base-a" }, { name: "base-b" }], }, }) @@ -2273,34 +2424,46 @@ describe("Garden", () => { } await expectError( - () => garden.resolveModuleConfigs(), + () => garden["resolveModuleConfigs"](garden.log), (err) => expect(stripAnsi(err.message)).to.equal(deline` Error validating configuration for module 'foo' - (base schema from 'base' plugin) (/garden.yml): key .base is required + (base schema from 'base-a' plugin) (/garden.yml): key .base is required `) ) }) - it("should throw if the module outputs don't match the base's declared outputs schema", async () => { - const base = { - name: "base", + it("should throw if the module outputs don't match the base's base's declared outputs schema", async () => { + const baseA = { + name: "base-a", createModuleTypes: [ { - name: "base", - docs: "base", + name: "base-a", + docs: "base-a", moduleOutputsSchema: joi.object().keys({ foo: joi.string() }), handlers: {}, }, ], } + const baseB = { + name: "base-b", + dependencies: ["base-a"], + createModuleTypes: [ + { + name: "base-b", + docs: "base-b", + base: "base-a", + handlers: {}, + }, + ], + } const foo = { name: "foo", - dependencies: ["base"], + dependencies: ["base-b"], createModuleTypes: [ { name: "foo", - base: "base", + base: "base-b", docs: "foo", handlers: { configure: async ({ moduleConfig }) => ({ @@ -2315,10 +2478,10 @@ describe("Garden", () => { } const garden = await Garden.factory(pathFoo, { - plugins: [base, foo], + plugins: [baseA, baseB, foo], config: { ...projectConfigFoo, - providers: [...projectConfigFoo.providers, { name: "base" }], + providers: [...projectConfigFoo.providers, { name: "base-a" }, { name: "base-b" }], }, }) @@ -2339,176 +2502,512 @@ describe("Garden", () => { } await expectError( - () => garden.resolveModuleConfigs(), + () => garden["resolveModuleConfigs"](garden.log), (err) => expect(stripAnsi(err.message)).to.equal(deline` - Error validating outputs for module 'foo' (base schema from 'base' plugin) (/garden.yml): + Error validating outputs for module 'foo' (base schema from 'base-a' plugin) (/garden.yml): key .foo must be a string `) ) }) + }) - context("module type's base has a base", () => { - it("should throw if the configure handler output doesn't match the schema of the base's base", async () => { - const baseA = { - name: "base-a", - createModuleTypes: [ - { - name: "base-a", - docs: "base-a", - schema: joi.object().keys({ base: joi.string().required() }), - handlers: {}, + context("when a provider has an augmentGraph handler", () => { + it("should correctly add and resolve modules from the handler", async () => { + const foo = { + name: "foo", + createModuleTypes: [ + { + name: "foo", + docs: "foo", + schema: joi.object().keys({ foo: joi.string(), build: baseBuildSpecSchema }), + handlers: { + configure: async ({ moduleConfig }) => { + return { moduleConfig } + }, }, - ], - } - const baseB = { - name: "base-b", - dependencies: ["base-a"], - createModuleTypes: [ - { - name: "base-b", - docs: "base-b", - base: "base-a", - schema: joi.object().keys({ foo: joi.string() }), - handlers: {}, + }, + ], + handlers: { + augmentGraph: async () => { + return { + addModules: [ + { + kind: "Module", + type: "foo", + name: "foo", + foo: "bar", + path: "/tmp", + }, + ], + } + }, + }, + } + + const garden = await Garden.factory(pathFoo, { + plugins: [foo], + config: projectConfigFoo, + }) + + const moduleConfigs = await garden["resolveModuleConfigs"](garden.log) + + expect(deepOmitUndefined(moduleConfigs[0])).to.eql({ + apiVersion: "garden.io/v0", + kind: "Module", + allowPublish: true, + build: { dependencies: [] }, + name: "foo", + outputs: {}, + configPath: "/tmp", + path: "/tmp", + serviceConfigs: [], + spec: { foo: "bar", build: { dependencies: [] } }, + testConfigs: [], + type: "foo", + taskConfigs: [], + }) + }) + + it("should apply returned build dependency relationships", async () => { + const foo = { + name: "foo", + createModuleTypes: [ + { + name: "foo", + docs: "foo", + schema: joi.object().keys({ foo: joi.string(), build: baseBuildSpecSchema }), + handlers: { + configure: async ({ moduleConfig }) => { + return { moduleConfig } + }, }, - ], - } - const foo = { + }, + ], + handlers: { + augmentGraph: async () => { + return { + addBuildDependencies: [{ by: "foo", on: "bar" }], + } + }, + }, + } + + const garden = await Garden.factory(pathFoo, { + plugins: [foo], + config: projectConfigFoo, + }) + + garden["moduleConfigs"] = { + foo: { + apiVersion: DEFAULT_API_VERSION, name: "foo", - dependencies: ["base-b"], - createModuleTypes: [ - { - name: "foo", - base: "base-b", - docs: "foo", - schema: joi.object().keys({ foo: joi.string().required() }), - handlers: { - configure: async ({ moduleConfig }) => ({ - moduleConfig: { - ...moduleConfig, - spec: { - ...moduleConfig.spec, - foo: "bar", - }, + type: "foo", + allowPublish: false, + build: { dependencies: [] }, + outputs: {}, + path: "/tmp", + serviceConfigs: [], + taskConfigs: [], + testConfigs: [], + spec: {}, + }, + bar: { + apiVersion: DEFAULT_API_VERSION, + name: "bar", + type: "foo", + allowPublish: false, + build: { dependencies: [] }, + outputs: {}, + path: "/tmp", + serviceConfigs: [], + taskConfigs: [], + testConfigs: [], + spec: {}, + }, + } + + const moduleConfigs = await garden["resolveModuleConfigs"](garden.log) + const fooModule = deepOmitUndefined(findByName(moduleConfigs, "foo")!) + + expect(fooModule).to.eql({ + apiVersion: "garden.io/v0", + kind: "Module", + allowPublish: false, + build: { dependencies: [{ name: "bar", copy: [] }] }, + name: "foo", + outputs: {}, + path: "/tmp", + serviceConfigs: [], + spec: { build: { dependencies: [] } }, + testConfigs: [], + type: "foo", + taskConfigs: [], + }) + }) + + it("should add modules before applying dependencies", async () => { + const foo = { + name: "foo", + createModuleTypes: [ + { + name: "foo", + docs: "foo", + schema: joi.object().keys({ foo: joi.string(), build: baseBuildSpecSchema }), + handlers: { + configure: async ({ moduleConfig }) => { + moduleConfig.serviceConfigs = [ + { + name: moduleConfig.name, }, - }), + ] + return { moduleConfig } }, }, - ], - } + }, + ], + handlers: { + augmentGraph: async () => { + return { + addModules: [ + { + kind: "Module", + type: "foo", + name: "foo", + foo: "bar", + path: "/tmp", + }, + { + kind: "Module", + type: "foo", + name: "bar", + foo: "bar", + path: "/tmp", + }, + ], + // These wouldn't work unless build deps are set in right order + addBuildDependencies: [{ by: "foo", on: "bar" }], + addRuntimeDependencies: [{ by: "foo", on: "bar" }], + } + }, + }, + } - const garden = await Garden.factory(pathFoo, { - plugins: [baseA, baseB, foo], - config: { - ...projectConfigFoo, - providers: [...projectConfigFoo.providers, { name: "base-a" }, { name: "base-b" }], + const garden = await Garden.factory(pathFoo, { + plugins: [foo], + config: projectConfigFoo, + }) + + const moduleConfigs = await garden["resolveModuleConfigs"](garden.log) + const fooModule = deepOmitUndefined(findByName(moduleConfigs, "foo")!) + + expect(fooModule).to.eql({ + apiVersion: "garden.io/v0", + kind: "Module", + allowPublish: true, + build: { dependencies: [{ name: "bar", copy: [] }] }, + name: "foo", + outputs: {}, + configPath: "/tmp", + path: "/tmp", + serviceConfigs: [ + { + name: "foo", + dependencies: ["bar"], + hotReloadable: false, }, - }) + ], + spec: { foo: "bar", build: { dependencies: [] } }, + testConfigs: [], + type: "foo", + taskConfigs: [], + }) + }) + + // TODO: Complete this once we've gotten rid of the -- prefix business + it.skip("should flag added modules as added by the plugin", async () => { + throw "TODO" + }) - garden["moduleConfigs"] = { - foo: { - apiVersion: DEFAULT_API_VERSION, + it("should throw if a build dependency's `by` reference can't be resolved", async () => { + const foo = { + name: "foo", + createModuleTypes: [ + { name: "foo", - type: "foo", - allowPublish: false, - build: { dependencies: [] }, - outputs: {}, - path: pathFoo, - serviceConfigs: [], - taskConfigs: [], - testConfigs: [], - spec: { foo: "bar" }, + docs: "foo", + schema: joi.object().keys({ foo: joi.string(), build: baseBuildSpecSchema }), + handlers: { + configure: async ({ moduleConfig }) => { + return { moduleConfig } + }, + }, }, - } + ], + handlers: { + augmentGraph: async () => { + return { + addBuildDependencies: [{ by: "foo", on: "bar" }], + } + }, + }, + } - await expectError( - () => garden.resolveModuleConfigs(), - (err) => - expect(stripAnsi(err.message)).to.equal(deline` - Error validating configuration for module 'foo' - (base schema from 'base-a' plugin) (/garden.yml): key .base is required - `) - ) + const garden = await Garden.factory(pathFoo, { + plugins: [foo], + config: projectConfigFoo, }) - it("should throw if the module outputs don't match the base's base's declared outputs schema", async () => { - const baseA = { - name: "base-a", - createModuleTypes: [ - { - name: "base-a", - docs: "base-a", - moduleOutputsSchema: joi.object().keys({ foo: joi.string() }), - handlers: {}, - }, - ], - } - const baseB = { - name: "base-b", - dependencies: ["base-a"], - createModuleTypes: [ - { - name: "base-b", - docs: "base-b", - base: "base-a", - handlers: {}, - }, - ], - } - const foo = { - name: "foo", - dependencies: ["base-b"], - createModuleTypes: [ - { - name: "foo", - base: "base-b", - docs: "foo", - handlers: { - configure: async ({ moduleConfig }) => ({ - moduleConfig: { - ...moduleConfig, - outputs: { foo: 123 }, + await expectError( + () => garden["resolveModuleConfigs"](garden.log), + (err) => + expect(stripAnsi(err.message)).to.equal(deline` + Provider 'foo' added a build dependency by module 'foo' on 'bar' but module 'foo' could not be found. + `) + ) + }) + + it("should apply returned runtime dependency relationships", async () => { + const foo = { + name: "foo", + createModuleTypes: [ + { + name: "foo", + docs: "foo", + schema: joi.object().keys({ foo: joi.string(), build: baseBuildSpecSchema }), + handlers: { + configure: async ({ moduleConfig }) => { + moduleConfig.serviceConfigs = [ + { + name: moduleConfig.name, }, - }), + ] + return { moduleConfig } }, }, - ], - } + }, + ], + handlers: { + augmentGraph: async () => { + return { + addRuntimeDependencies: [{ by: "foo", on: "bar" }], + } + }, + }, + } - const garden = await Garden.factory(pathFoo, { - plugins: [baseA, baseB, foo], - config: { - ...projectConfigFoo, - providers: [...projectConfigFoo.providers, { name: "base-a" }, { name: "base-b" }], + const garden = await Garden.factory(pathFoo, { + plugins: [foo], + config: projectConfigFoo, + }) + + garden["moduleConfigs"] = { + foo: { + apiVersion: DEFAULT_API_VERSION, + name: "foo", + type: "foo", + allowPublish: false, + build: { dependencies: [] }, + outputs: {}, + path: "/tmp", + serviceConfigs: [], + taskConfigs: [], + testConfigs: [], + spec: {}, + }, + bar: { + apiVersion: DEFAULT_API_VERSION, + name: "bar", + type: "foo", + allowPublish: false, + build: { dependencies: [] }, + outputs: {}, + path: "/tmp", + serviceConfigs: [], + taskConfigs: [], + testConfigs: [], + spec: {}, + }, + } + + const moduleConfigs = await garden["resolveModuleConfigs"](garden.log) + const fooModule = deepOmitUndefined(findByName(moduleConfigs, "foo")!) + + expect(fooModule).to.eql({ + apiVersion: "garden.io/v0", + kind: "Module", + allowPublish: false, + build: { dependencies: [] }, + name: "foo", + outputs: {}, + path: "/tmp", + serviceConfigs: [ + { + name: "foo", + dependencies: ["bar"], + hotReloadable: false, }, - }) + ], + spec: { build: { dependencies: [] } }, + testConfigs: [], + type: "foo", + taskConfigs: [], + }) + }) - garden["moduleConfigs"] = { - foo: { - apiVersion: DEFAULT_API_VERSION, + it("should throw if a runtime dependency's `by` reference can't be resolved", async () => { + const foo = { + name: "foo", + createModuleTypes: [ + { name: "foo", - type: "foo", - allowPublish: false, - build: { dependencies: [] }, - outputs: {}, - path: pathFoo, - serviceConfigs: [], - taskConfigs: [], - testConfigs: [], - spec: {}, + docs: "foo", + schema: joi.object().keys({ foo: joi.string(), build: baseBuildSpecSchema }), + handlers: { + configure: async ({ moduleConfig }) => { + moduleConfig.serviceConfigs = [ + { + name: moduleConfig.name, + }, + ] + return { moduleConfig } + }, + }, }, - } + ], + handlers: { + augmentGraph: async () => { + return { + addRuntimeDependencies: [{ by: "bar", on: "foo" }], + } + }, + }, + } - await expectError( - () => garden.resolveModuleConfigs(), - (err) => - expect(stripAnsi(err.message)).to.equal(deline` - Error validating outputs for module 'foo' (base schema from 'base-a' plugin) (/garden.yml): - key .foo must be a string + const garden = await Garden.factory(pathFoo, { + plugins: [foo], + config: projectConfigFoo, + }) + + garden["moduleConfigs"] = { + foo: { + apiVersion: DEFAULT_API_VERSION, + name: "foo", + type: "foo", + allowPublish: false, + build: { dependencies: [] }, + outputs: {}, + path: "/tmp", + serviceConfigs: [], + taskConfigs: [], + testConfigs: [], + spec: {}, + }, + } + + await expectError( + () => garden["resolveModuleConfigs"](garden.log), + (err) => + expect(stripAnsi(err.message)).to.equal(deline` + Provider 'foo' added a runtime dependency by 'bar' on 'foo' + but service or task 'bar' could not be found. `) - ) + ) + }) + + it("should process augmentGraph handlers in dependency order", async () => { + // Ensure modules added by the dependency are in place before adding dependencies in dependant. + const foo = { + name: "foo", + dependencies: [], + createModuleTypes: [ + { + name: "foo", + docs: "foo", + schema: joi.object().keys({ foo: joi.string(), build: baseBuildSpecSchema }), + handlers: { + configure: async ({ moduleConfig }) => { + return { moduleConfig } + }, + }, + }, + ], + handlers: { + augmentGraph: async () => { + return { + addModules: [ + { + kind: "Module", + type: "foo", + name: "foo", + foo: "bar", + path: "/tmp", + }, + ], + } + }, + }, + } + + const bar = { + name: "bar", + dependencies: ["foo"], + handlers: { + augmentGraph: async () => { + return { + // This doesn't work unless providers are processed in right order + addBuildDependencies: [{ by: "foo", on: "bar" }], + } + }, + }, + } + + const config = { + ...projectConfigFoo, + providers: [...projectConfigFoo.providers, { name: "bar" }], + } + + // First test correct order + let garden = await Garden.factory(pathFoo, { + plugins: [foo, bar], + config, }) + + const moduleConfigs = await garden["resolveModuleConfigs"](garden.log) + const fooModule = deepOmitUndefined(findByName(moduleConfigs, "foo")!) + + expect(fooModule).to.eql({ + apiVersion: "garden.io/v0", + kind: "Module", + allowPublish: true, + build: { dependencies: [{ name: "bar", copy: [] }] }, + name: "foo", + outputs: {}, + configPath: "/tmp", + path: "/tmp", + serviceConfigs: [], + spec: { foo: "bar", build: { dependencies: [] } }, + testConfigs: [], + type: "foo", + taskConfigs: [], + }) + + // Then test wrong order and make sure it throws + foo.dependencies = ["bar"] + bar.dependencies = [] + + garden = await Garden.factory(pathFoo, { + plugins: [foo, bar], + config, + }) + + await expectError( + () => garden["resolveModuleConfigs"](garden.log), + (err) => + expect(stripAnsi(err.message)).to.equal(deline` + Provider 'bar' added a build dependency by module 'foo' on 'bar' but module 'foo' could not be found. + `) + ) }) }) }) @@ -2518,15 +3017,15 @@ describe("Garden", () => { it("should return result from cache if available", async () => { const garden = await makeTestGardenA() - const module = await garden.resolveModuleConfig("module-a") + const config = await garden.resolveModuleConfig(garden.log, "module-a") const version: ModuleVersion = { versionString: "banana", dependencyVersions: {}, files: [], } - garden.cache.set(["moduleVersions", module.name], version, getModuleCacheContext(module)) + garden.cache.set(["moduleVersions", config.name], version, getModuleCacheContext(config)) - const result = await garden.resolveVersion("module-a", []) + const result = await garden.resolveVersion(config, []) expect(result).to.eql(version) }) @@ -2537,6 +3036,7 @@ describe("Garden", () => { garden.cache.delete(["moduleVersions", "module-b"]) + const config = await garden.resolveModuleConfig(garden.log, "module-b") const resolveStub = td.replace(garden.vcs, "resolveVersion") const version: ModuleVersion = { versionString: "banana", @@ -2546,22 +3046,22 @@ describe("Garden", () => { td.when(resolveStub(), { ignoreExtraArgs: true }).thenResolve(version) - const result = await garden.resolveVersion("module-b", []) + const result = await garden.resolveVersion(config, []) expect(result).to.eql(version) }) it("should ignore cache if force=true", async () => { const garden = await makeTestGardenA() - const module = await garden.resolveModuleConfig("module-a") + const config = await garden.resolveModuleConfig(garden.log, "module-a") const version: ModuleVersion = { versionString: "banana", dependencyVersions: {}, files: [], } - garden.cache.set(["moduleVersions", module.name], version, getModuleCacheContext(module)) + garden.cache.set(["moduleVersions", config.name], version, getModuleCacheContext(config)) - const result = await garden.resolveVersion("module-a", [], true) + const result = await garden.resolveVersion(config, [], true) expect(result).to.not.eql(version) }) diff --git a/garden-service/test/unit/src/plugins/container/container.ts b/garden-service/test/unit/src/plugins/container/container.ts index 2adb12c689..b67ec10f13 100644 --- a/garden-service/test/unit/src/plugins/container/container.ts +++ b/garden-service/test/unit/src/plugins/container/container.ts @@ -82,7 +82,7 @@ describe("plugins.container", () => { async function getTestModule(moduleConfig: ContainerModuleConfig) { const parsed = await configure({ ctx, moduleConfig, log }) - const graph = await garden.getConfigGraph() + const graph = await garden.getConfigGraph(garden.log) return moduleFromConfig(garden, graph, parsed.moduleConfig) } diff --git a/garden-service/test/unit/src/plugins/container/helpers.ts b/garden-service/test/unit/src/plugins/container/helpers.ts index 3634a88efa..1b553746e9 100644 --- a/garden-service/test/unit/src/plugins/container/helpers.ts +++ b/garden-service/test/unit/src/plugins/container/helpers.ts @@ -74,7 +74,7 @@ describe("containerHelpers", () => { async function getTestModule(moduleConfig: ContainerModuleConfig) { const parsed = await configure({ ctx, moduleConfig, log }) - const graph = await garden.getConfigGraph() + const graph = await garden.getConfigGraph(garden.log) return moduleFromConfig(garden, graph, parsed.moduleConfig) } @@ -384,14 +384,14 @@ describe("containerHelpers", () => { describe("hasDockerfile", () => { it("should return true if module config explicitly sets a Dockerfile", async () => { - const graph = await garden.getConfigGraph() + const graph = await garden.getConfigGraph(garden.log) const module = await graph.getModule("module-a") module.spec.dockerfile = "Dockerfile" expect(await helpers.hasDockerfile(module)).to.be.true }) it("should return true if module sources include a Dockerfile", async () => { - const graph = await garden.getConfigGraph() + const graph = await garden.getConfigGraph(garden.log) const module = await graph.getModule("module-a") const dockerfilePath = join(module.path, "Dockerfile") @@ -402,7 +402,7 @@ describe("containerHelpers", () => { }) it("should return false if no Dockerfile is specified or included in sources", async () => { - const graph = await garden.getConfigGraph() + const graph = await garden.getConfigGraph(garden.log) const module = await graph.getModule("module-a") const dockerfilePath = join(module.path, "Dockerfile") diff --git a/garden-service/test/unit/src/plugins/exec.ts b/garden-service/test/unit/src/plugins/exec.ts index eecc9876ae..4bd2388a09 100644 --- a/garden-service/test/unit/src/plugins/exec.ts +++ b/garden-service/test/unit/src/plugins/exec.ts @@ -25,7 +25,7 @@ describe("exec plugin", () => { beforeEach(async () => { garden = await makeTestGarden(projectRoot, { plugins: [gardenPlugin] }) - graph = await garden.getConfigGraph() + graph = await garden.getConfigGraph(garden.log) log = garden.log await garden.clearBuilds() }) @@ -160,8 +160,8 @@ describe("exec plugin", () => { }) it("should propagate task logs to runtime outputs", async () => { - const _garden = await makeTestGarden(await getDataDir("test-projects", "exec-task-outputs")) - const _graph = await _garden.getConfigGraph() + const _garden = await makeTestGarden(getDataDir("test-projects", "exec-task-outputs")) + const _graph = await _garden.getConfigGraph(_garden.log) const taskB = await _graph.getTask("task-b") const taskTask = new TaskTask({ @@ -182,7 +182,7 @@ describe("exec plugin", () => { it("should copy artifacts after task runs", async () => { const _garden = await makeTestGarden(getDataDir("test-projects", "exec-artifacts")) - const _graph = await _garden.getConfigGraph() + const _graph = await _garden.getConfigGraph(_garden.log) const task = await _graph.getTask("task-a") const taskTask = new TaskTask({ @@ -204,7 +204,7 @@ describe("exec plugin", () => { it("should copy artifacts after test runs", async () => { const _garden = await makeTestGarden(getDataDir("test-projects", "exec-artifacts")) - const _graph = await _garden.getConfigGraph() + const _graph = await _garden.getConfigGraph(_garden.log) const module = await _graph.getModule("module-a") const testTask = new TestTask({ diff --git a/garden-service/test/unit/src/plugins/kubernetes/container/ingress.ts b/garden-service/test/unit/src/plugins/kubernetes/container/ingress.ts index 25d6d6147d..dbeaf68c24 100644 --- a/garden-service/test/unit/src/plugins/kubernetes/container/ingress.ts +++ b/garden-service/test/unit/src/plugins/kubernetes/container/ingress.ts @@ -386,7 +386,7 @@ describe("createIngressResources", () => { const provider = await garden.resolveProvider("container") const ctx = garden.getPluginContext(provider) const parsed = await configure({ ctx, moduleConfig, log: garden.log }) - const graph = await garden.getConfigGraph() + const graph = await garden.getConfigGraph(garden.log) const module = await moduleFromConfig(garden, graph, parsed.moduleConfig) return { diff --git a/garden-service/test/unit/src/plugins/kubernetes/container/service.ts b/garden-service/test/unit/src/plugins/kubernetes/container/service.ts index a70c8a3bac..cec0b09a5d 100644 --- a/garden-service/test/unit/src/plugins/kubernetes/container/service.ts +++ b/garden-service/test/unit/src/plugins/kubernetes/container/service.ts @@ -16,7 +16,7 @@ describe("createServiceResources", () => { }) it("should return service resources", async () => { - const graph = await garden.getConfigGraph() + const graph = await garden.getConfigGraph(garden.log) const service = await graph.getService("service-a") const resources = await createServiceResources(service, "my-namespace") @@ -50,7 +50,7 @@ describe("createServiceResources", () => { }) it("should add annotations if configured", async () => { - const graph = await garden.getConfigGraph() + const graph = await garden.getConfigGraph(garden.log) const service: ContainerService = await graph.getService("service-a") service.spec.annotations = { my: "annotation" } @@ -88,7 +88,7 @@ describe("createServiceResources", () => { }) it("should create a NodePort service if a nodePort is specified", async () => { - const graph = await garden.getConfigGraph() + const graph = await garden.getConfigGraph(garden.log) const service: ContainerService = await graph.getService("service-a") service.spec.ports[0].nodePort = 12345 @@ -125,7 +125,7 @@ describe("createServiceResources", () => { }) it("should create a NodePort service without nodePort set if nodePort is specified as true", async () => { - const graph = await garden.getConfigGraph() + const graph = await garden.getConfigGraph(garden.log) const service: ContainerService = await graph.getService("service-a") service.spec.ports[0].nodePort = true diff --git a/garden-service/test/unit/src/plugins/terraform/terraform.ts b/garden-service/test/unit/src/plugins/terraform/terraform.ts index 098d42264b..2d4464ad19 100644 --- a/garden-service/test/unit/src/plugins/terraform/terraform.ts +++ b/garden-service/test/unit/src/plugins/terraform/terraform.ts @@ -66,7 +66,7 @@ describe("Terraform module type", () => { }) async function runTestTask() { - const graph = await garden.getConfigGraph() + const graph = await garden.getConfigGraph(garden.log) const task = await graph.getTask("test-task") const taskTask = new TaskTask({ diff --git a/garden-service/test/unit/src/runtime-context.ts b/garden-service/test/unit/src/runtime-context.ts index c041982842..61fa8ae45f 100644 --- a/garden-service/test/unit/src/runtime-context.ts +++ b/garden-service/test/unit/src/runtime-context.ts @@ -10,7 +10,7 @@ describe("prepareRuntimeContext", () => { before(async () => { garden = await makeTestGardenA() - graph = await garden.getConfigGraph() + graph = await garden.getConfigGraph(garden.log) }) it("should add the module version to the output envVars", async () => { diff --git a/garden-service/test/unit/src/server/server.ts b/garden-service/test/unit/src/server/server.ts index daa9178f85..cd6d7e7f01 100644 --- a/garden-service/test/unit/src/server/server.ts +++ b/garden-service/test/unit/src/server/server.ts @@ -77,7 +77,7 @@ describe("startServer", () => { .post("/api") .send({ command: "get.config" }) .expect(200) - const config = await garden.dumpConfig() + const config = await garden.dumpConfig(garden.log) expect(res.body.result).to.eql(deepOmitUndefined(config)) }) @@ -203,7 +203,7 @@ describe("startServer", () => { const id = uuid.v4() garden - .dumpConfig() + .dumpConfig(garden.log) .then((config) => { onMessage((req) => { expect(req).to.eql({ diff --git a/garden-service/test/unit/src/tasks/deploy.ts b/garden-service/test/unit/src/tasks/deploy.ts index b591751fa0..1c96579667 100644 --- a/garden-service/test/unit/src/tasks/deploy.ts +++ b/garden-service/test/unit/src/tasks/deploy.ts @@ -119,7 +119,7 @@ describe("DeployTask", () => { }, } - const graph = await garden.getConfigGraph() + const graph = await garden.getConfigGraph(garden.log) const testService = await graph.getService("test-service") const deployTask = new DeployTask({ diff --git a/garden-service/test/unit/src/tasks/get-service-status.ts b/garden-service/test/unit/src/tasks/get-service-status.ts index 1950fe9e64..ce0b49dab5 100644 --- a/garden-service/test/unit/src/tasks/get-service-status.ts +++ b/garden-service/test/unit/src/tasks/get-service-status.ts @@ -112,7 +112,7 @@ describe("GetServiceStatusTask", () => { }, } - const graph = await garden.getConfigGraph() + const graph = await garden.getConfigGraph(garden.log) const testService = await graph.getService("test-service") const statusTask = new GetServiceStatusTask({ diff --git a/garden-service/test/unit/src/tasks/helpers.ts b/garden-service/test/unit/src/tasks/helpers.ts index ceaf8b3921..4cda93bbf4 100644 --- a/garden-service/test/unit/src/tasks/helpers.ts +++ b/garden-service/test/unit/src/tasks/helpers.ts @@ -26,7 +26,7 @@ describe("TaskHelpers", () => { before(async () => { garden = await makeTestGarden(resolve(dataDir, "test-project-dependants")) - graph = await garden.getConfigGraph() + graph = await garden.getConfigGraph(garden.log) log = garden.log }) @@ -37,7 +37,7 @@ describe("TaskHelpers", () => { describe("getDependantTasksForModule", () => { it("returns the correct set of tasks for the changed module", async () => { const module = await graph.getModule("good-morning") - await garden.getConfigGraph() + await garden.getConfigGraph(garden.log) const tasks = await getDependantTasksForModule({ garden, diff --git a/garden-service/test/unit/src/tasks/test.ts b/garden-service/test/unit/src/tasks/test.ts index 03fd96e71b..9bb322c9de 100644 --- a/garden-service/test/unit/src/tasks/test.ts +++ b/garden-service/test/unit/src/tasks/test.ts @@ -15,7 +15,7 @@ describe("TestTask", () => { beforeEach(async () => { garden = await makeTestGarden(resolve(dataDir, "test-project-test-deps")) - graph = await garden.getConfigGraph() + graph = await garden.getConfigGraph(garden.log) log = garden.log }) @@ -39,15 +39,20 @@ describe("TestTask", () => { files: [], } - td.when(resolveVersion("module-a", [])).thenResolve(versionA) - td.when(resolveVersion("module-b", [])).thenResolve(versionB) + const configA = await garden.resolveModuleConfig(garden.log, "module-a") + const configB = await garden.resolveModuleConfig(garden.log, "module-b") + + td.when(resolveVersion(configA, [])).thenResolve(versionA) + td.when(resolveVersion(configB, [])).thenResolve(versionB) const moduleB = await graph.getModule("module-b") - td.when(resolveVersion("module-a", [moduleB])).thenResolve(versionA) + td.when(resolveVersion(configA, [moduleB])).thenResolve(versionA) const moduleA = await graph.getModule("module-a") + td.when(resolveVersion(moduleA, [moduleB])).thenResolve(versionA) + const testConfig = moduleA.testConfigs[0] const task = await TestTask.factory({ diff --git a/garden-service/test/unit/src/util/validate-dependencies.ts b/garden-service/test/unit/src/util/validate-dependencies.ts index 7054836ee3..3dbb6a7206 100644 --- a/garden-service/test/unit/src/util/validate-dependencies.ts +++ b/garden-service/test/unit/src/util/validate-dependencies.ts @@ -19,7 +19,8 @@ import { flatten } from "lodash" * execution of scanModules). */ async function scanAndGetConfigs(garden: Garden) { - const moduleConfigs: ModuleConfig[] = await garden.resolveModuleConfigs() + const moduleConfigs: ModuleConfig[] = await garden["resolveModuleConfigs"](garden.log) + const serviceNames = flatten(moduleConfigs.map((m) => m.serviceConfigs.map((s) => s.name))) const taskNames = flatten(moduleConfigs.map((m) => m.taskConfigs.map((s) => s.name))) diff --git a/garden-service/test/unit/src/vcs/vcs.ts b/garden-service/test/unit/src/vcs/vcs.ts index 9cb9f9bc6d..dbe9cbdb9b 100644 --- a/garden-service/test/unit/src/vcs/vcs.ts +++ b/garden-service/test/unit/src/vcs/vcs.ts @@ -67,7 +67,7 @@ describe("VcsHandler", () => { describe("getTreeVersion", () => { it("should sort the list of files in the returned version", async () => { const getFiles = td.replace(handlerA, "getFiles") - const moduleConfig = await gardenA.resolveModuleConfig("module-a") + const moduleConfig = await gardenA.resolveModuleConfig(gardenA.log, "module-a") td.when( getFiles({ log: gardenA.log, @@ -86,7 +86,7 @@ describe("VcsHandler", () => { it("should not include the module config file in the file list", async () => { const getFiles = td.replace(handlerA, "getFiles") - const moduleConfig = await gardenA.resolveModuleConfig("module-a") + const moduleConfig = await gardenA.resolveModuleConfig(gardenA.log, "module-a") td.when( getFiles({ log: gardenA.log, @@ -106,7 +106,7 @@ describe("VcsHandler", () => { it("should respect the include field, if specified", async () => { const projectRoot = getDataDir("test-projects", "include-exclude") const garden = await makeTestGarden(projectRoot) - const moduleConfig = await garden.resolveModuleConfig("module-a") + const moduleConfig = await garden.resolveModuleConfig(garden.log, "module-a") const handler = new GitHandler(garden.gardenDirPath, garden.dotIgnoreFiles) const version = await handler.getTreeVersion(gardenA.log, moduleConfig) @@ -117,7 +117,7 @@ describe("VcsHandler", () => { it("should respect the exclude field, if specified", async () => { const projectRoot = getDataDir("test-projects", "include-exclude") const garden = await makeTestGarden(projectRoot) - const moduleConfig = await garden.resolveModuleConfig("module-b") + const moduleConfig = await garden.resolveModuleConfig(garden.log, "module-b") const handler = new GitHandler(garden.gardenDirPath, garden.dotIgnoreFiles) const version = await handler.getTreeVersion(garden.log, moduleConfig) @@ -128,7 +128,7 @@ describe("VcsHandler", () => { it("should respect both include and exclude fields, if specified", async () => { const projectRoot = getDataDir("test-projects", "include-exclude") const garden = await makeTestGarden(projectRoot) - const moduleConfig = await garden.resolveModuleConfig("module-c") + const moduleConfig = await garden.resolveModuleConfig(garden.log, "module-c") const handler = new GitHandler(garden.gardenDirPath, garden.dotIgnoreFiles) const version = await handler.getTreeVersion(garden.log, moduleConfig) @@ -139,7 +139,7 @@ describe("VcsHandler", () => { it("should not be affected by changes to the module's garden.yml that don't affect the module config", async () => { const projectRoot = getDataDir("test-projects", "multiple-module-config") const garden = await makeTestGarden(projectRoot) - const moduleConfigA1 = await garden.resolveModuleConfig("module-a1") + const moduleConfigA1 = await garden.resolveModuleConfig(garden.log, "module-a1") const configPath = moduleConfigA1.configPath! const orgConfig = await readFile(configPath) @@ -156,7 +156,7 @@ describe("VcsHandler", () => { describe("resolveTreeVersion", () => { it("should return the version from a version file if it exists", async () => { - const moduleConfig = await gardenA.resolveModuleConfig("module-a") + const moduleConfig = await gardenA.resolveModuleConfig(gardenA.log, "module-a") const result = await handlerA.resolveTreeVersion(gardenA.log, moduleConfig) expect(result).to.eql({ @@ -166,7 +166,7 @@ describe("VcsHandler", () => { }) it("should call getTreeVersion if there is no version file", async () => { - const moduleConfig = await gardenA.resolveModuleConfig("module-b") + const moduleConfig = await gardenA.resolveModuleConfig(gardenA.log, "module-b") const version = { contentHash: "qwerty", @@ -188,8 +188,10 @@ describe("VcsHandler", () => { before(async () => { const templateGarden = await makeTestGarden(getDataDir("test-project-variable-versioning")) - moduleABefore = await templateGarden.resolveModuleConfig("module-a") // uses the echo-string variable - moduleBBefore = await templateGarden.resolveModuleConfig("module-b") // does not use the echo-string variable + // uses the echo-string variable + moduleABefore = await templateGarden.resolveModuleConfig(templateGarden.log, "module-a") + // does not use the echo-string variable + moduleBBefore = await templateGarden.resolveModuleConfig(templateGarden.log, "module-b") const configContext = new ModuleConfigContext( templateGarden, @@ -198,10 +200,10 @@ describe("VcsHandler", () => { await templateGarden.getRawModuleConfigs() ) - moduleAAfter = await templateGarden.resolveModuleConfig("module-a", { + moduleAAfter = await templateGarden.resolveModuleConfig(templateGarden.log, "module-a", { configContext, }) - moduleBAfter = await templateGarden.resolveModuleConfig("module-b", { + moduleBAfter = await templateGarden.resolveModuleConfig(templateGarden.log, "module-b", { configContext, }) }) @@ -244,7 +246,7 @@ describe("VcsHandler", () => { describe("hashVersions", () => { it("is stable with respect to key order in moduleConfig", async () => { - const originalConfig = await gardenA.resolveModuleConfig("module-a") + const originalConfig = await gardenA.resolveModuleConfig(gardenA.log, "module-a") const stirredConfig = cloneDeep(originalConfig) delete stirredConfig.name stirredConfig.name = originalConfig.name @@ -253,7 +255,7 @@ describe("VcsHandler", () => { }) it("is stable with respect to named version order", async () => { - const config = await gardenA.resolveModuleConfig("module-a") + const config = await gardenA.resolveModuleConfig(gardenA.log, "module-a") expect(getVersionString(config, [namedVersionA, namedVersionB, namedVersionC])).to.eql( getVersionString(config, [namedVersionB, namedVersionA, namedVersionC]) @@ -264,7 +266,7 @@ describe("VcsHandler", () => { describe("resolveVersion", () => { it("should return module version if there are no dependencies", async () => { - const module = await gardenA.resolveModuleConfig("module-a") + const module = await gardenA.resolveModuleConfig(gardenA.log, "module-a") const result = await handlerA.resolveVersion(gardenA.log, module, []) expect(result).to.eql({ @@ -275,7 +277,11 @@ describe("VcsHandler", () => { }) it("should hash together the version of the module and all dependencies", async () => { - const [moduleA, moduleB, moduleC] = await gardenA.resolveModuleConfigs(["module-a", "module-b", "module-c"]) + const [moduleA, moduleB, moduleC] = await gardenA["resolveModuleConfigs"](gardenA.log, [ + "module-a", + "module-b", + "module-c", + ]) const versionStringB = "qwerty" const versionB = { @@ -306,13 +312,13 @@ describe("VcsHandler", () => { }) it("should not include module's garden.yml in version file list", async () => { - const moduleConfig = await gardenA.resolveModuleConfig("module-a") + const moduleConfig = await gardenA.resolveModuleConfig(gardenA.log, "module-a") const version = await handlerA.resolveVersion(gardenA.log, moduleConfig, []) expect(version.files).to.not.include(moduleConfig.configPath!) }) it("should be affected by changes to the module's config", async () => { - const moduleConfig = await gardenA.resolveModuleConfig("module-a") + const moduleConfig = await gardenA.resolveModuleConfig(gardenA.log, "module-a") const version1 = await handlerA.resolveVersion(gardenA.log, moduleConfig, []) moduleConfig.name = "foo" const version2 = await handlerA.resolveVersion(gardenA.log, moduleConfig, []) @@ -322,7 +328,7 @@ describe("VcsHandler", () => { it("should not be affected by changes to the module's garden.yml that don't affect the module config", async () => { const projectRoot = getDataDir("test-projects", "multiple-module-config") const garden = await makeTestGarden(projectRoot) - const moduleConfigA1 = await garden.resolveModuleConfig("module-a1") + const moduleConfigA1 = await garden.resolveModuleConfig(garden.log, "module-a1") const configPath = moduleConfigA1.configPath! const orgConfig = await readFile(configPath) diff --git a/garden-service/test/unit/src/watch.ts b/garden-service/test/unit/src/watch.ts index 54b8c7a852..b76bbde457 100644 --- a/garden-service/test/unit/src/watch.ts +++ b/garden-service/test/unit/src/watch.ts @@ -36,7 +36,7 @@ describe("Watcher", () => { doubleModulePath = resolve(garden.projectRoot, "double-module") includeModulePath = resolve(garden.projectRoot, "with-include") moduleContext = pathToCacheContext(modulePath) - await garden.startWatcher(await garden.getConfigGraph(), 10) + await garden.startWatcher(await garden.getConfigGraph(garden.log), 10) }) beforeEach(async () => { @@ -321,7 +321,7 @@ describe("Watcher", () => { // This is not an issue in practice because there are specific commands just for linking // so the user will always have a new instance of Garden when they run their next command. garden = await makeExtModuleSourcesGarden() - await garden.startWatcher(await garden.getConfigGraph()) + await garden.startWatcher(await garden.getConfigGraph(garden.log)) }) after(async () => { @@ -384,7 +384,7 @@ describe("Watcher", () => { // This is not an issue in practice because there are specific commands just for linking // so the user will always have a new instance of Garden when they run their next command. garden = await makeExtProjectSourcesGarden() - await garden.startWatcher(await garden.getConfigGraph()) + await garden.startWatcher(await garden.getConfigGraph(garden.log)) }) after(async () => {