From b2509e9c5af57dfae0b96b57bb76e8ffe8e4f409 Mon Sep 17 00:00:00 2001 From: Jon Edvald Date: Tue, 5 Nov 2019 23:35:17 +0100 Subject: [PATCH] refactor(core): allow plugins to augment the module graph Providers are now able to augment the stack graph using a new `augmentGraph` handler, which is called in dependency order for each provider that specifies it. This allows providers to add modules (and by extension services, tasks, tests), and to add dependency relations between existing modules and services/tasks. See the new augmentGraph plugin handler for details on usage. Also made a small refactor to no longer require a build handler for every module type, and to avoid unnecessary build tasks when a module doesn't require building. --- garden-service/src/actions.ts | 46 +- garden-service/src/commands/build.ts | 14 +- garden-service/src/commands/call.ts | 2 +- garden-service/src/commands/delete.ts | 2 +- garden-service/src/commands/deploy.ts | 2 +- garden-service/src/commands/dev.ts | 2 +- garden-service/src/commands/exec.ts | 2 +- garden-service/src/commands/get/get-config.ts | 2 +- garden-service/src/commands/get/get-graph.ts | 2 +- garden-service/src/commands/get/get-status.ts | 2 +- .../src/commands/get/get-task-result.ts | 2 +- garden-service/src/commands/get/get-tasks.ts | 2 +- .../src/commands/get/get-test-result.ts | 2 +- garden-service/src/commands/link/module.ts | 2 +- garden-service/src/commands/logs.ts | 2 +- garden-service/src/commands/publish.ts | 2 +- garden-service/src/commands/run/module.ts | 8 +- garden-service/src/commands/run/service.ts | 2 +- garden-service/src/commands/run/task.ts | 2 +- garden-service/src/commands/run/test.ts | 2 +- garden-service/src/commands/scan.ts | 3 +- garden-service/src/commands/test.ts | 2 +- .../src/commands/update-remote/modules.ts | 2 +- garden-service/src/commands/validate.ts | 4 +- garden-service/src/config/base.ts | 7 +- garden-service/src/config/config-context.ts | 4 +- garden-service/src/config/module.ts | 18 +- garden-service/src/garden.ts | 234 ++--- garden-service/src/plugins.ts | 76 +- .../src/plugins/container/helpers.ts | 2 +- garden-service/src/plugins/plugins.ts | 1 + .../src/plugins/terraform/terraform.ts | 2 - garden-service/src/process.ts | 4 +- garden-service/src/resolve-module.ts | 161 ++++ garden-service/src/tasks/build.ts | 41 +- garden-service/src/tasks/deploy.ts | 6 +- garden-service/src/tasks/helpers.ts | 38 +- garden-service/src/tasks/publish.ts | 16 +- garden-service/src/tasks/resolve-provider.ts | 4 +- garden-service/src/tasks/stage-build.ts | 4 +- garden-service/src/tasks/task.ts | 10 +- garden-service/src/tasks/test.ts | 8 +- garden-service/src/types/module.ts | 10 +- garden-service/src/types/plugin/base.ts | 7 +- garden-service/src/types/plugin/plugin.ts | 5 + .../src/types/plugin/provider/augmentGraph.ts | 106 +++ .../src/util/validate-dependencies.ts | 3 +- garden-service/src/watch.ts | 2 +- .../src/plugins/kubernetes/container/build.ts | 2 +- .../plugins/kubernetes/container/container.ts | 2 +- .../src/plugins/kubernetes/container/logs.ts | 2 +- .../src/plugins/kubernetes/helm/common.ts | 4 +- .../src/plugins/kubernetes/helm/config.ts | 20 +- .../src/plugins/kubernetes/helm/hot-reload.ts | 2 +- .../integ/src/plugins/kubernetes/helm/run.ts | 2 +- .../integ/src/plugins/kubernetes/helm/test.ts | 2 +- .../test/integ/src/plugins/kubernetes/util.ts | 2 +- garden-service/test/unit/src/actions.ts | 96 +- garden-service/test/unit/src/build-dir.ts | 20 +- .../test/unit/src/commands/get/get-config.ts | 2 +- .../unit/src/commands/get/get-debug-info.ts | 2 +- .../unit/src/commands/get/get-task-result.ts | 2 +- .../unit/src/commands/get/get-test-result.ts | 2 +- garden-service/test/unit/src/config-graph.ts | 10 +- .../test/unit/src/config/config-context.ts | 10 +- garden-service/test/unit/src/garden.ts | 866 ++++++++++++++---- .../unit/src/plugins/container/container.ts | 2 +- .../unit/src/plugins/container/helpers.ts | 8 +- garden-service/test/unit/src/plugins/exec.ts | 10 +- .../plugins/kubernetes/container/ingress.ts | 2 +- .../plugins/kubernetes/container/service.ts | 8 +- .../unit/src/plugins/terraform/terraform.ts | 2 +- .../test/unit/src/runtime-context.ts | 2 +- garden-service/test/unit/src/server/server.ts | 4 +- garden-service/test/unit/src/tasks/deploy.ts | 2 +- .../test/unit/src/tasks/get-service-status.ts | 2 +- garden-service/test/unit/src/tasks/helpers.ts | 4 +- garden-service/test/unit/src/tasks/test.ts | 13 +- .../unit/src/util/validate-dependencies.ts | 3 +- garden-service/test/unit/src/vcs/vcs.ts | 44 +- garden-service/test/unit/src/watch.ts | 6 +- 81 files changed, 1478 insertions(+), 564 deletions(-) create mode 100644 garden-service/src/resolve-module.ts create mode 100644 garden-service/src/types/plugin/provider/augmentGraph.ts 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 () => {