From 6c209af82d7565ea901566a71679df52e37a36e1 Mon Sep 17 00:00:00 2001 From: Jon Edvald Date: Thu, 31 Jan 2019 20:50:06 +0100 Subject: [PATCH] fix(core): using module version in templates didn't work with watch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This is a pretty major refactor, where resolving raw configs is deferred to usage time, and the handling of resolved configuration is moved to the ConfigGraph class (née DependencyGraph). Basically the Garden class now holds the raw configuration, and it is fully resolved when building a new instance of ConfigGraph, which should then be done whenever module sources are modified (otherwise resolved template strings, e.g. version tags, may be stale). Also moved some handling of actions to the ActionHandler to further reduce the size of the Garden class. --- garden-service/bin/add-version-files.ts | 3 +- garden-service/src/actions.ts | 212 +++++- garden-service/src/build-dir.ts | 3 +- garden-service/src/commands/build.ts | 12 +- garden-service/src/commands/call.ts | 8 +- garden-service/src/commands/delete.ts | 8 +- garden-service/src/commands/deploy.ts | 16 +- garden-service/src/commands/dev.ts | 17 +- garden-service/src/commands/exec.ts | 13 +- garden-service/src/commands/get/get-graph.ts | 6 +- garden-service/src/commands/get/get-tasks.ts | 5 +- garden-service/src/commands/link/module.ts | 5 +- garden-service/src/commands/logs.ts | 10 +- garden-service/src/commands/publish.ts | 3 +- garden-service/src/commands/run/module.ts | 8 +- garden-service/src/commands/run/service.ts | 7 +- garden-service/src/commands/run/task.ts | 5 +- garden-service/src/commands/run/test.ts | 8 +- garden-service/src/commands/scan.ts | 11 +- garden-service/src/commands/test.ts | 17 +- .../src/commands/update-remote/modules.ts | 5 +- garden-service/src/commands/validate.ts | 3 +- .../{dependency-graph.ts => config-graph.ts} | 281 ++++++-- garden-service/src/config/config-context.ts | 36 +- garden-service/src/garden.ts | 616 +++--------------- .../src/plugins/kubernetes/helm/build.ts | 10 +- .../src/plugins/kubernetes/helm/handlers.ts | 2 - garden-service/src/process.ts | 18 +- garden-service/src/tasks/base.ts | 2 +- garden-service/src/tasks/build.ts | 4 +- garden-service/src/tasks/deploy.ts | 25 +- garden-service/src/tasks/helpers.ts | 40 +- garden-service/src/tasks/hot-reload.ts | 13 +- garden-service/src/tasks/publish.ts | 2 +- garden-service/src/tasks/push.ts | 4 +- garden-service/src/tasks/task.ts | 16 +- garden-service/src/tasks/test.ts | 42 +- garden-service/src/types/module.ts | 26 +- garden-service/src/types/service.ts | 14 +- .../module-a/garden.yml | 5 +- garden-service/test/src/actions.ts | 91 ++- garden-service/test/src/build-dir.ts | 7 +- .../test/src/commands/get/get-config.ts | 2 +- garden-service/test/src/config-graph.ts | 185 ++++++ .../test/src/config/config-context.ts | 4 +- garden-service/test/src/garden.ts | 353 +--------- garden-service/test/src/plugins/container.ts | 3 +- garden-service/test/src/plugins/exec.ts | 9 +- .../plugins/kubernetes/container/ingress.ts | 3 +- .../src/plugins/kubernetes/helm/common.ts | 65 +- .../src/plugins/kubernetes/helm/config.ts | 17 +- .../src/plugins/kubernetes/helm/hot-reload.ts | 14 +- garden-service/test/src/task-graph.ts | 2 +- garden-service/test/src/tasks/helpers.ts | 17 +- garden-service/test/src/tasks/test.ts | 8 +- .../test/src/util/validate-dependencies.ts | 21 +- garden-service/test/src/vcs/base.ts | 16 +- garden-service/test/src/watch.ts | 2 +- 58 files changed, 1090 insertions(+), 1270 deletions(-) rename garden-service/src/{dependency-graph.ts => config-graph.ts} (58%) create mode 100644 garden-service/test/src/config-graph.ts diff --git a/garden-service/bin/add-version-files.ts b/garden-service/bin/add-version-files.ts index 852c5c7cee..d1f10acc18 100755 --- a/garden-service/bin/add-version-files.ts +++ b/garden-service/bin/add-version-files.ts @@ -17,7 +17,8 @@ async function addVersionFiles() { const staticPath = resolve(__dirname, "..", "static") const garden = await Garden.factory(staticPath) - const modules = await garden.getModules() + const graph = await garden.getConfigGraph() + const modules = await graph.getModules() return Bluebird.map(modules, async (module) => { const path = module.path diff --git a/garden-service/src/actions.ts b/garden-service/src/actions.ts index b75409ba70..579fde5510 100644 --- a/garden-service/src/actions.ts +++ b/garden-service/src/actions.ts @@ -8,9 +8,19 @@ import Bluebird = require("bluebird") import chalk from "chalk" -import { Garden } from "./garden" +import { Garden, ActionHandlerMap, ModuleActionHandlerMap, PluginActionMap, ModuleActionMap } from "./garden" import { Module } from "./types/module" -import { ModuleActions, ServiceActions, PluginActions, TaskActions } from "./types/plugin/plugin" +import { + ModuleActions, + ServiceActions, + PluginActions, + TaskActions, + ModuleAndRuntimeActions, + pluginActionDescriptions, + moduleActionDescriptions, + pluginActionNames, + moduleActionNames, +} from "./types/plugin/plugin" import { BuildResult, BuildStatus, @@ -66,15 +76,16 @@ import { ServiceStatus, prepareRuntimeContext, } from "./types/service" -import { mapValues, values, keyBy, omit } from "lodash" +import { mapValues, values, keyBy, omit, pickBy, fromPairs } from "lodash" import { Omit } from "./util/util" -import { RuntimeContext } from "./types/service" import { processServices, ProcessResults } from "./process" import { getDependantTasksForModule } from "./tasks/helpers" import { LogEntry } from "./logger/log-entry" import { createPluginContext } from "./plugin-context" import { CleanupEnvironmentParams } from "./types/plugin/params" -import { ConfigurationError } from "./exceptions" +import { ConfigurationError, PluginError, ParameterError } from "./exceptions" +import { defaultProvider } from "./config/project" +import { validate } from "./config/common" type TypeGuard = { readonly [P in keyof (PluginActionParams | ModuleActionParams)]: (...args: any[]) => Promise @@ -101,17 +112,23 @@ type ModuleActionHelperParams = // additionally make runtimeContext param optional type ServiceActionHelperParams = - Omit - & { runtimeContext?: RuntimeContext, pluginName?: string } + Omit + & { pluginName?: string } type TaskActionHelperParams = Omit - & { runtimeContext?: RuntimeContext, pluginName?: string } + & { pluginName?: string } type RequirePluginName = T & { pluginName: string } export class ActionHelper implements TypeGuard { - constructor(private garden: Garden) { } + private readonly actionHandlers: PluginActionMap + private readonly moduleActionHandlers: ModuleActionMap + + constructor(private garden: Garden) { + this.actionHandlers = fromPairs(pluginActionNames.map(n => [n, {}])) + this.moduleActionHandlers = fromPairs(moduleActionNames.map(n => [n, {}])) + } //=========================================================================== //region Environment Actions @@ -120,7 +137,7 @@ export class ActionHelper implements TypeGuard { async getEnvironmentStatus( { pluginName, log }: ActionHelperParams, ): Promise { - const handlers = this.garden.getActionHandlers("getEnvironmentStatus", pluginName) + const handlers = this.getActionHandlers("getEnvironmentStatus", pluginName) const logEntry = log.debug({ msg: "Getting status...", status: "active", @@ -141,7 +158,7 @@ export class ActionHelper implements TypeGuard { { force = false, pluginName, log, allowUserInput = false }: { force?: boolean, pluginName?: string, log: LogEntry, allowUserInput?: boolean }, ) { - const handlers = this.garden.getActionHandlers("prepareEnvironment", pluginName) + const handlers = this.getActionHandlers("prepareEnvironment", pluginName) // FIXME: We're calling getEnvironmentStatus before preparing the environment. // Results in 404 errors for unprepared/missing services. // See: https://github.com/garden-io/garden/issues/353 @@ -192,7 +209,7 @@ export class ActionHelper implements TypeGuard { async cleanupEnvironment( { pluginName, log }: ActionHelperParams, ): Promise { - const handlers = this.garden.getActionHandlers("cleanupEnvironment", pluginName) + const handlers = this.getActionHandlers("cleanupEnvironment", pluginName) await Bluebird.each(values(handlers), h => h({ ...this.commonParams(h, log) })) return this.getEnvironmentStatus({ pluginName, log }) } @@ -323,11 +340,12 @@ export class ActionHelper implements TypeGuard { async getStatus({ log }: { log: LogEntry }): Promise { const envStatus: EnvironmentStatusMap = await this.getEnvironmentStatus({ log }) - const services = keyBy(await this.garden.getServices(), "name") + const graph = await this.garden.getConfigGraph() + const services = keyBy(await graph.getServices(), "name") const serviceStatus = await Bluebird.props(mapValues(services, async (service: Service) => { - const serviceDependencies = await this.garden.getServices(service.config.dependencies) - const runtimeContext = await prepareRuntimeContext(this.garden, service.module, serviceDependencies) + const serviceDependencies = await graph.getServices(service.config.dependencies) + const runtimeContext = await prepareRuntimeContext(this.garden, graph, service.module, serviceDependencies) // TODO: The status will be reported as "outdated" if the service was deployed with hot-reloading enabled. // Once hot-reloading is a toggle, as opposed to an API/CLI flag, we can resolve that issue. return this.getServiceStatus({ log, service, runtimeContext, hotReload: false }) @@ -342,16 +360,19 @@ export class ActionHelper implements TypeGuard { async deployServices( { serviceNames, force = false, forceBuild = false, log }: DeployServicesParams, ): Promise { - const services = await this.garden.getServices(serviceNames) + const graph = await this.garden.getConfigGraph() + const services = await graph.getServices(serviceNames) return processServices({ services, garden: this.garden, + graph, log, watch: false, - handler: async (module) => getDependantTasksForModule({ + handler: async (_, module) => getDependantTasksForModule({ garden: this.garden, log, + graph, module, hotReloadServiceNames: [], force, @@ -380,7 +401,7 @@ export class ActionHelper implements TypeGuard { defaultHandler?: PluginActions[T], }, ): Promise { - const handler = this.garden.getActionHandler({ + const handler = this.getActionHandler({ actionType, pluginName, defaultHandler, @@ -398,7 +419,7 @@ export class ActionHelper implements TypeGuard { ): Promise { // the type system is messing me up here, not sure why I need the any cast... - j.e. const { module, pluginName } = params - const handler = await this.garden.getModuleActionHandler({ + const handler = await this.getModuleActionHandler({ moduleType: module.type, actionType, pluginName, @@ -418,20 +439,16 @@ export class ActionHelper implements TypeGuard { { params, actionType, defaultHandler }: { params: ServiceActionHelperParams, actionType: T, defaultHandler?: ServiceActions[T] }, ): Promise { - const { log, service } = params + const { log, service, runtimeContext } = params const module = service.module - const handler = await this.garden.getModuleActionHandler({ + const handler = await this.getModuleActionHandler({ moduleType: module.type, actionType, pluginName: params.pluginName, defaultHandler, }) - // TODO: figure out why this doesn't compile without the casts - const deps = await this.garden.getServices(service.config.dependencies) - const runtimeContext = ((params).runtimeContext || await prepareRuntimeContext(this.garden, module, deps)) - const handlerParams: any = { ...this.commonParams(handler, log), ...params, @@ -453,7 +470,7 @@ export class ActionHelper implements TypeGuard { const { task } = params const module = task.module - const handler = await this.garden.getModuleActionHandler({ + const handler = await this.getModuleActionHandler({ moduleType: module.type, actionType, pluginName: params.pluginName, @@ -469,6 +486,149 @@ export class ActionHelper implements TypeGuard { return (handler)(handlerParams) } + + public addActionHandler( + pluginName: string, actionType: T, handler: PluginActions[T], + ) { + const plugin = this.garden.getPlugin(pluginName) + const schema = pluginActionDescriptions[actionType].resultSchema + + const wrapped = async (...args) => { + const result = await handler.apply(plugin, args) + return validate(result, schema, { context: `${actionType} output from plugin ${pluginName}` }) + } + wrapped["actionType"] = actionType + wrapped["pluginName"] = pluginName + + this.actionHandlers[actionType][pluginName] = wrapped + } + + public addModuleActionHandler( + pluginName: string, actionType: T, moduleType: string, handler: ModuleActions[T], + ) { + const plugin = this.garden.getPlugin(pluginName) + const schema = moduleActionDescriptions[actionType].resultSchema + + const wrapped = async (...args) => { + const result = await handler.apply(plugin, args) + return validate(result, schema, { context: `${actionType} output from plugin ${pluginName}` }) + } + wrapped["actionType"] = actionType + wrapped["pluginName"] = pluginName + wrapped["moduleType"] = moduleType + + if (!this.moduleActionHandlers[actionType]) { + this.moduleActionHandlers[actionType] = {} + } + + if (!this.moduleActionHandlers[actionType][moduleType]) { + this.moduleActionHandlers[actionType][moduleType] = {} + } + + this.moduleActionHandlers[actionType][moduleType][pluginName] = wrapped + } + + /** + * Get a handler for the specified action. + */ + public getActionHandlers(actionType: T, pluginName?: string): ActionHandlerMap { + return this.filterActionHandlers(this.actionHandlers[actionType], pluginName) + } + + /** + * Get a handler for the specified module action. + */ + public getModuleActionHandlers( + { actionType, moduleType, pluginName }: + { actionType: T, moduleType: string, pluginName?: string }, + ): ModuleActionHandlerMap { + return this.filterActionHandlers((this.moduleActionHandlers[actionType] || {})[moduleType], pluginName) + } + + private filterActionHandlers(handlers, pluginName?: string) { + // make sure plugin is loaded + if (!!pluginName) { + this.garden.getPlugin(pluginName) + } + + if (handlers === undefined) { + handlers = {} + } + + return !pluginName ? handlers : pickBy(handlers, (handler) => handler["pluginName"] === pluginName) + } + + /** + * Get the last configured handler for the specified action (and optionally module type). + */ + public getActionHandler( + { actionType, pluginName, defaultHandler }: + { actionType: T, pluginName?: string, defaultHandler?: PluginActions[T] }, + ): PluginActions[T] { + + const handlers = Object.values(this.getActionHandlers(actionType, pluginName)) + + if (handlers.length) { + return handlers[handlers.length - 1] + } else if (defaultHandler) { + defaultHandler["pluginName"] = defaultProvider.name + return defaultHandler + } + + const errorDetails = { + requestedHandlerType: actionType, + environment: this.garden.environment.name, + pluginName, + } + + if (pluginName) { + throw new PluginError(`Plugin '${pluginName}' does not have a '${actionType}' handler.`, errorDetails) + } else { + throw new ParameterError( + `No '${actionType}' handler configured in environment '${this.garden.environment.name}'. ` + + `Are you missing a provider configuration?`, + errorDetails, + ) + } + } + + /** + * Get the last configured handler for the specified action. + */ + public getModuleActionHandler( + { actionType, moduleType, pluginName, defaultHandler }: + { actionType: T, moduleType: string, pluginName?: string, defaultHandler?: ModuleAndRuntimeActions[T] }, + ): ModuleAndRuntimeActions[T] { + + const handlers = Object.values(this.getModuleActionHandlers({ actionType, moduleType, pluginName })) + + if (handlers.length) { + return handlers[handlers.length - 1] + } else if (defaultHandler) { + defaultHandler["pluginName"] = defaultProvider.name + return defaultHandler + } + + const errorDetails = { + requestedHandlerType: actionType, + requestedModuleType: moduleType, + environment: this.garden.environment.name, + pluginName, + } + + if (pluginName) { + throw new PluginError( + `Plugin '${pluginName}' does not have a '${actionType}' handler for module type '${moduleType}'.`, + errorDetails, + ) + } else { + throw new ParameterError( + `No '${actionType}' handler configured for module type '${moduleType}' in environment ` + + `'${this.garden.environment.name}'. Are you missing a provider configuration?`, + errorDetails, + ) + } + } } const dummyLogStreamer = async ({ service, log }: GetServiceLogsParams) => { diff --git a/garden-service/src/build-dir.ts b/garden-service/src/build-dir.ts index 0ceb6a80a1..52502ef4a5 100644 --- a/garden-service/src/build-dir.ts +++ b/garden-service/src/build-dir.ts @@ -29,6 +29,7 @@ import { zip } from "lodash" import * as execa from "execa" import { platform } from "os" import { toCygwinPath } from "./util/util" +import { ModuleConfig } from "./config/module" // Lazily construct a directory of modules inside which all build steps are performed. @@ -43,7 +44,7 @@ export class BuildDir { return new BuildDir(projectRoot, buildDirPath) } - async syncFromSrc(module: Module) { + async syncFromSrc(module: ModuleConfig) { await this.sync( resolve(this.projectRoot, module.path) + sep, await this.buildPath(module.name), diff --git a/garden-service/src/commands/build.ts b/garden-service/src/commands/build.ts index 7ac82a07f9..7c99285ae9 100644 --- a/garden-service/src/commands/build.ts +++ b/garden-service/src/commands/build.ts @@ -18,7 +18,6 @@ import { BuildTask } from "../tasks/build" import { TaskResults } from "../task-graph" import dedent = require("dedent") import { processModules } from "../process" -import { Module } from "../types/module" import { logHeader } from "../logger/util" const buildArguments = { @@ -67,19 +66,20 @@ export class BuildCommand extends Command { ): Promise> { await garden.clearBuilds() - const modules = await garden.getModules(args.modules) - const dependencyGraph = await garden.getDependencyGraph() + const graph = await garden.getConfigGraph() + const modules = await graph.getModules(args.modules) const moduleNames = modules.map(m => m.name) const results = await processModules({ garden, + graph: await garden.getConfigGraph(), log, logFooter, modules, watch: opts.watch, - handler: async (module) => [new BuildTask({ garden, log, module, force: opts.force })], - changeHandler: async (module: Module) => { - const dependantModules = (await dependencyGraph.getDependants("build", module.name, true)).build + handler: async (_, module) => [new BuildTask({ garden, log, module, force: opts.force })], + changeHandler: async (_, module) => { + const dependantModules = (await graph.getDependants("build", module.name, true)).build return [module].concat(dependantModules) .filter(m => moduleNames.includes(m.name)) .map(m => new BuildTask({ garden, log, module: m, force: true })) diff --git a/garden-service/src/commands/call.ts b/garden-service/src/commands/call.ts index 9739fba75f..4fd651e5a3 100644 --- a/garden-service/src/commands/call.ts +++ b/garden-service/src/commands/call.ts @@ -19,7 +19,7 @@ import { import { splitFirst } from "../util/util" import { ParameterError, RuntimeError } from "../exceptions" import { find, includes, pick } from "lodash" -import { ServiceIngress, getIngressUrl } from "../types/service" +import { ServiceIngress, getIngressUrl, getServiceRuntimeContext } from "../types/service" import dedent = require("dedent") const callArgs = { @@ -53,8 +53,10 @@ export class CallCommand extends Command { let [serviceName, path] = splitFirst(args.serviceAndPath, "/") // TODO: better error when service doesn't exist - const service = await garden.getService(serviceName) - const status = await garden.actions.getServiceStatus({ service, log, hotReload: false }) + const graph = await garden.getConfigGraph() + const service = await graph.getService(serviceName) + const runtimeContext = await getServiceRuntimeContext(garden, graph, service) + const status = await garden.actions.getServiceStatus({ service, log, hotReload: false, runtimeContext }) if (!includes(["ready", "outdated"], status.state)) { throw new RuntimeError(`Service ${service.name} is not running`, { diff --git a/garden-service/src/commands/delete.ts b/garden-service/src/commands/delete.ts index 6c4cefcf85..4b6a155848 100644 --- a/garden-service/src/commands/delete.ts +++ b/garden-service/src/commands/delete.ts @@ -20,7 +20,7 @@ import { } from "./base" import { NotFoundError } from "../exceptions" import dedent = require("dedent") -import { ServiceStatus } from "../types/service" +import { ServiceStatus, getServiceRuntimeContext } from "../types/service" import { logHeader } from "../logger/util" export class DeleteCommand extends Command { @@ -129,7 +129,8 @@ export class DeleteServiceCommand extends Command { ` async action({ garden, log, args }: CommandParams): Promise { - const services = await garden.getServices(args.services) + const graph = await garden.getConfigGraph() + const services = await graph.getServices(args.services) if (services.length === 0) { log.warn({ msg: "No services found. Aborting." }) @@ -141,7 +142,8 @@ export class DeleteServiceCommand extends Command { const result: { [key: string]: ServiceStatus } = {} await Bluebird.map(services, async service => { - result[service.name] = await garden.actions.deleteService({ log, service }) + const runtimeContext = await getServiceRuntimeContext(garden, graph, service) + result[service.name] = await garden.actions.deleteService({ log, service, runtimeContext }) }) return { result } diff --git a/garden-service/src/commands/deploy.ts b/garden-service/src/commands/deploy.ts index ccad509ed8..de3358d660 100644 --- a/garden-service/src/commands/deploy.ts +++ b/garden-service/src/commands/deploy.ts @@ -81,7 +81,8 @@ export class DeployCommand extends Command { } async action({ garden, log, logFooter, args, opts }: CommandParams): Promise> { - const services = await garden.getServices(args.services) + const initGraph = await garden.getConfigGraph() + const services = await initGraph.getServices(args.services) if (services.length === 0) { log.error({ msg: "No services found. Aborting." }) @@ -95,19 +96,19 @@ export class DeployCommand extends Command { throw new ParameterError(`Must specify --watch flag when requesting hot-reloading`, { opts }) } - const hotReloadServices = await garden.getServices(hotReloadServiceNames) - // TODO: make this a task await garden.actions.prepareEnvironment({ log }) const results = await processServices({ garden, + graph: initGraph, log, logFooter, services, watch, - handler: async (module) => getDependantTasksForModule({ + handler: async (graph, module) => getDependantTasksForModule({ garden, + graph, log, module, fromWatch: false, @@ -115,15 +116,16 @@ export class DeployCommand extends Command { force: opts.force, forceBuild: opts["force-build"], }), - changeHandler: async (module) => { + changeHandler: async (graph, module) => { const tasks: BaseTask[] = await getDependantTasksForModule({ - garden, log, module, hotReloadServiceNames, force: true, forceBuild: opts["force-build"], + garden, graph, log, module, hotReloadServiceNames, force: true, forceBuild: opts["force-build"], fromWatch: true, includeDependants: true, }) + const hotReloadServices = await graph.getServices(hotReloadServiceNames) const hotReloadTasks = hotReloadServices .filter(service => service.module.name === module.name || service.sourceModule.name === module.name) - .map(service => new HotReloadTask({ garden, log, service, force: true })) + .map(service => new HotReloadTask({ garden, graph, log, service, force: true })) tasks.push(...hotReloadTasks) diff --git a/garden-service/src/commands/dev.ts b/garden-service/src/commands/dev.ts index 4c97d051c2..541bf7e33f 100644 --- a/garden-service/src/commands/dev.ts +++ b/garden-service/src/commands/dev.ts @@ -29,6 +29,7 @@ import { processModules } from "../process" import { Module } from "../types/module" import { getTestTasks } from "../tasks/test" import { HotReloadTask } from "../tasks/hot-reload" +import { ConfigGraph } from "../config-graph" const ansiBannerPath = join(STATIC_DIR, "garden-banner-2.txt") @@ -76,7 +77,8 @@ export class DevCommand extends Command { async action({ garden, log, logFooter, opts }: CommandParams): Promise { await garden.actions.prepareEnvironment({ log }) - const modules = await garden.getModules() + const graph = await garden.getConfigGraph() + const modules = await graph.getModules() if (modules.length === 0) { logFooter && logFooter.setState({ msg: "" }) @@ -86,32 +88,32 @@ export class DevCommand extends Command { } const hotReloadServiceNames = opts["hot-reload"] || [] - const hotReloadServices = await garden.getServices(hotReloadServiceNames) - const dependencyGraph = await garden.getDependencyGraph() const tasksForModule = (watch: boolean) => { - return async (module: Module) => { + return async (updatedGraph: ConfigGraph, module: Module) => { const tasks: BaseTask[] = [] if (watch) { + const hotReloadServices = await updatedGraph.getServices(hotReloadServiceNames) const hotReloadTasks = hotReloadServices .filter(service => service.module.name === module.name || service.sourceModule.name === module.name) - .map(service => new HotReloadTask({ garden, log, service, force: true })) + .map(service => new HotReloadTask({ garden, graph: updatedGraph, log, service, force: true })) tasks.push(...hotReloadTasks) } const testModules: Module[] = watch - ? (await dependencyGraph.withDependantModules([module])) + ? (await updatedGraph.withDependantModules([module])) : [module] tasks.push(...flatten( - await Bluebird.map(testModules, m => getTestTasks({ garden, log, module: m })), + await Bluebird.map(testModules, m => getTestTasks({ garden, log, module: m, graph: updatedGraph })), )) tasks.push(...await getDependantTasksForModule({ garden, log, + graph: updatedGraph, module, fromWatch: watch, hotReloadServiceNames, @@ -126,6 +128,7 @@ export class DevCommand extends Command { const results = await processModules({ garden, + graph, log, logFooter, modules, diff --git a/garden-service/src/commands/exec.ts b/garden-service/src/commands/exec.ts index 1756ae878e..2eb636fb60 100644 --- a/garden-service/src/commands/exec.ts +++ b/garden-service/src/commands/exec.ts @@ -19,6 +19,7 @@ import { BooleanParameter, } from "./base" import dedent = require("dedent") +import { getServiceRuntimeContext } from "../types/service" const runArgs = { service: new StringParameter({ @@ -72,8 +73,16 @@ export class ExecCommand extends Command { command: `Running command ${chalk.cyan(command.join(" "))} in service ${chalk.cyan(serviceName)}`, }) - const service = await garden.getService(serviceName) - const result = await garden.actions.execInService({ log, service, command, interactive: opts.interactive }) + const graph = await garden.getConfigGraph() + const service = await graph.getService(serviceName) + const runtimeContext = await getServiceRuntimeContext(garden, graph, service) + const result = await garden.actions.execInService({ + log, + service, + command, + interactive: opts.interactive, + runtimeContext, + }) return { result } } diff --git a/garden-service/src/commands/get/get-graph.ts b/garden-service/src/commands/get/get-graph.ts index 7870843ee2..8b3d031bde 100644 --- a/garden-service/src/commands/get/get-graph.ts +++ b/garden-service/src/commands/get/get-graph.ts @@ -7,7 +7,7 @@ */ import * as yaml from "js-yaml" -import { RenderedEdge, RenderedNode } from "../../dependency-graph" +import { RenderedEdge, RenderedNode } from "../../config-graph" import { highlightYaml } from "../../util/util" import { Command, @@ -25,8 +25,8 @@ 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 dependencyGraph = await garden.getDependencyGraph() - const renderedGraph = dependencyGraph.render() + const graph = await garden.getConfigGraph() + const renderedGraph = graph.render() const output: GraphOutput = { nodes: renderedGraph.nodes, relationships: renderedGraph.relationships } const yamlGraph = yaml.safeDump(renderedGraph, { noRefs: true, skipInvalid: true }) diff --git a/garden-service/src/commands/get/get-tasks.ts b/garden-service/src/commands/get/get-tasks.ts index 686d52226c..526bcb6cc2 100644 --- a/garden-service/src/commands/get/get-tasks.ts +++ b/garden-service/src/commands/get/get-tasks.ts @@ -65,9 +65,10 @@ export class GetTasksCommand extends Command { } async action({ args, garden, log }: CommandParams): Promise { - const tasks = await garden.getTasks(args.tasks) + const graph = await garden.getConfigGraph() + const tasks = await graph.getTasks(args.tasks) const taskModuleNames = uniq(tasks.map(t => t.module.name)) - const modules = sortBy(await garden.getModules(taskModuleNames), m => m.name) + const modules = sortBy(await graph.getModules(taskModuleNames), m => m.name) const taskListing: any[] = [] let logStr = "" diff --git a/garden-service/src/commands/link/module.ts b/garden-service/src/commands/link/module.ts index a9f1699e92..6dd8808f29 100644 --- a/garden-service/src/commands/link/module.ts +++ b/garden-service/src/commands/link/module.ts @@ -59,11 +59,12 @@ export class LinkModuleCommand extends Command { const sourceType = "module" const { module: moduleName, path } = args - const moduleToLink = await garden.getModule(moduleName) + const graph = await garden.getConfigGraph() + const moduleToLink = await graph.getModule(moduleName) const isRemote = [moduleToLink].filter(hasRemoteSource)[0] if (!isRemote) { - const modulesWithRemoteSource = (await garden.getModules()).filter(hasRemoteSource).sort() + const modulesWithRemoteSource = (await graph.getModules()).filter(hasRemoteSource).sort() throw new ParameterError( `Expected module(s) ${chalk.underline(moduleName)} to have a remote source.` + diff --git a/garden-service/src/commands/logs.ts b/garden-service/src/commands/logs.ts index a789f098b1..3107cb70e3 100644 --- a/garden-service/src/commands/logs.ts +++ b/garden-service/src/commands/logs.ts @@ -17,7 +17,7 @@ import { import chalk from "chalk" import { ServiceLogEntry } from "../types/plugin/outputs" import Bluebird = require("bluebird") -import { Service } from "../types/service" +import { Service, getServiceRuntimeContext } from "../types/service" import Stream from "ts-stream" import { LoggerType } from "../logger/logger" import dedent = require("dedent") @@ -68,7 +68,8 @@ export class LogsCommand extends Command { async action({ garden, log, args, opts }: CommandParams): Promise> { const { follow, tail } = opts - const services = await garden.getServices(args.services) + const graph = await garden.getConfigGraph() + const services = await graph.getServices(args.services) const result: ServiceLogEntry[] = [] const stream = new Stream() @@ -96,10 +97,11 @@ export class LogsCommand extends Command { await Bluebird.map(services, async (service: Service) => { const voidLog = log.placeholder(LogLevel.silly, { childEntriesInheritLevel: true }) - const status = await garden.actions.getServiceStatus({ log: voidLog, service, hotReload: false }) + const runtimeContext = await getServiceRuntimeContext(garden, graph, service) + const status = await garden.actions.getServiceStatus({ log: voidLog, service, hotReload: false, runtimeContext }) if (status.state === "ready" || status.state === "outdated") { - await garden.actions.getServiceLogs({ log, service, stream, follow, tail }) + await garden.actions.getServiceLogs({ log, service, stream, follow, tail, runtimeContext }) } else { await stream.write({ serviceName: service.name, diff --git a/garden-service/src/commands/publish.ts b/garden-service/src/commands/publish.ts index a0ff471461..d1cdeceede 100644 --- a/garden-service/src/commands/publish.ts +++ b/garden-service/src/commands/publish.ts @@ -64,7 +64,8 @@ export class PublishCommand extends Command { async action({ garden, log, args, opts }: CommandParams): Promise> { logHeader({ log, emoji: "rocket", command: "Publish modules" }) - const modules = await garden.getModules(args.modules) + const graph = await garden.getConfigGraph() + 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 bb6cfc9ece..cb46f02012 100644 --- a/garden-service/src/commands/run/module.ts +++ b/garden-service/src/commands/run/module.ts @@ -74,7 +74,9 @@ export class RunModuleCommand extends Command { async action({ garden, log, args, opts }: CommandParams): Promise> { const moduleName = args.module - const module = await garden.getModule(moduleName) + + const graph = await garden.getConfigGraph() + const module = await graph.getModule(moduleName) const msg = args.command ? `Running command ${chalk.white(args.command.join(" "))} in module ${chalk.white(moduleName)}` @@ -96,9 +98,9 @@ export class RunModuleCommand extends Command { // combine all dependencies for all services in the module, to be sure we have all the context we need const depNames = uniq(flatten(module.serviceConfigs.map(s => s.dependencies))) - const deps = await garden.getServices(depNames) + const deps = await graph.getServices(depNames) - const runtimeContext = await prepareRuntimeContext(garden, module, deps) + const runtimeContext = await prepareRuntimeContext(garden, graph, module, deps) printRuntimeContext(log, runtimeContext) diff --git a/garden-service/src/commands/run/service.ts b/garden-service/src/commands/run/service.ts index 9703120dfc..2f398b8b72 100644 --- a/garden-service/src/commands/run/service.ts +++ b/garden-service/src/commands/run/service.ts @@ -55,7 +55,8 @@ export class RunServiceCommand extends Command { async action({ garden, log, args, opts }: CommandParams): Promise> { const serviceName = args.service - const service = await garden.getService(serviceName) + const graph = await garden.getConfigGraph() + const service = await graph.getService(serviceName) const module = service.module logHeader({ @@ -70,8 +71,8 @@ export class RunServiceCommand extends Command { await garden.addTask(buildTask) await garden.processTasks() - const dependencies = await garden.getServices(module.serviceDependencyNames) - const runtimeContext = await prepareRuntimeContext(garden, module, dependencies) + const dependencies = await graph.getServices(module.serviceDependencyNames) + const runtimeContext = await prepareRuntimeContext(garden, graph, module, dependencies) printRuntimeContext(log, runtimeContext) diff --git a/garden-service/src/commands/run/task.ts b/garden-service/src/commands/run/task.ts index 0f5556f0b5..6ec1543710 100644 --- a/garden-service/src/commands/run/task.ts +++ b/garden-service/src/commands/run/task.ts @@ -50,7 +50,8 @@ export class RunTaskCommand extends Command { options = runOpts async action({ garden, log, args, opts }: CommandParams): Promise> { - const task = await garden.getTask(args.task) + const graph = await garden.getConfigGraph() + const task = await graph.getTask(args.task) const msg = `Running task ${chalk.white(task.name)}` @@ -58,7 +59,7 @@ export class RunTaskCommand extends Command { await garden.actions.prepareEnvironment({ log }) - const taskTask = new TaskTask({ garden, task, log, force: true, forceBuild: opts["force-build"] }) + const taskTask = new TaskTask({ garden, graph, task, log, force: true, forceBuild: opts["force-build"] }) await garden.addTask(taskTask) const result = (await garden.processTasks())[taskTask.getBaseKey()] diff --git a/garden-service/src/commands/run/test.ts b/garden-service/src/commands/run/test.ts index 34a4459285..6802eb1f49 100644 --- a/garden-service/src/commands/run/test.ts +++ b/garden-service/src/commands/run/test.ts @@ -69,7 +69,9 @@ export class RunTestCommand extends Command { async action({ garden, log, args, opts }: CommandParams): Promise> { const moduleName = args.module const testName = args.test - const module = await garden.getModule(moduleName) + + const graph = await garden.getConfigGraph() + const module = await graph.getModule(moduleName) const testConfig = findByName(module.testConfigs, testName) @@ -94,8 +96,8 @@ export class RunTestCommand extends Command { await garden.processTasks() const interactive = opts.interactive - const deps = await garden.getServices(testConfig.dependencies) - const runtimeContext = await prepareRuntimeContext(garden, module, deps) + const deps = await graph.getDependencies("test", testConfig.name, false) + const runtimeContext = await prepareRuntimeContext(garden, graph, module, deps.service) printRuntimeContext(log, runtimeContext) diff --git a/garden-service/src/commands/scan.ts b/garden-service/src/commands/scan.ts index d14587b422..64d73af9a0 100644 --- a/garden-service/src/commands/scan.ts +++ b/garden-service/src/commands/scan.ts @@ -21,15 +21,10 @@ 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.getModules()) + const modules = (await garden.resolveModuleConfigs()) .map(m => { - m.services.forEach(s => { - delete s.module - delete s.sourceModule - }) - m.tasks.forEach(w => delete w.module) return omit(m, [ - "_ConfigType", "cacheContext", "serviceConfigs", "serviceNames", "taskConfigs", "taskNames", + "_ConfigType", "cacheContext", "serviceNames", "taskNames", ]) }) @@ -37,7 +32,7 @@ export class ScanCommand extends Command { const shortOutput = { modules: modules.map(m => { - m.services!.map(s => delete s.spec) + m.serviceConfigs!.map(s => delete s.spec) return omit(m, ["spec"]) }), } diff --git a/garden-service/src/commands/test.ts b/garden-service/src/commands/test.ts index 0b35dab907..5a8c62fb9c 100644 --- a/garden-service/src/commands/test.ts +++ b/garden-service/src/commands/test.ts @@ -81,13 +81,13 @@ export class TestCommand extends Command { } async action({ garden, log, logFooter, args, opts }: CommandParams): Promise> { - const dependencyGraph = await garden.getDependencyGraph() + const graph = await garden.getConfigGraph() let modules: Module[] if (args.modules) { - modules = await dependencyGraph.withDependantModules(await garden.getModules(args.modules)) + modules = await graph.withDependantModules(await graph.getModules(args.modules)) } else { // All modules are included in this case, so there's no need to compute dependants. - modules = await garden.getModules() + modules = await graph.getModules() } await garden.actions.prepareEnvironment({ log }) @@ -98,16 +98,19 @@ export class TestCommand extends Command { const results = await processModules({ garden, + graph, log, logFooter, modules, watch: opts.watch, - handler: async (module) => getTestTasks({ garden, log, module, name, force, forceBuild }), - changeHandler: async (module) => { - const modulesToProcess = await dependencyGraph.withDependantModules([module]) + handler: async (updatedGraph, module) => getTestTasks({ + garden, log, graph: updatedGraph, module, name, force, forceBuild, + }), + changeHandler: async (updatedGraph, module) => { + const modulesToProcess = await updatedGraph.withDependantModules([module]) return flatten(await Bluebird.map( modulesToProcess, - m => getTestTasks({ garden, log, module: m, name, force, forceBuild }))) + m => getTestTasks({ garden, log, graph: updatedGraph, module: m, name, force, forceBuild }))) }, }) diff --git a/garden-service/src/commands/update-remote/modules.ts b/garden-service/src/commands/update-remote/modules.ts index e5d0c8e1b9..cf917443a4 100644 --- a/garden-service/src/commands/update-remote/modules.ts +++ b/garden-service/src/commands/update-remote/modules.ts @@ -49,7 +49,8 @@ export class UpdateRemoteModulesCommand extends Command { logHeader({ log, emoji: "hammer_and_wrench", command: "update-remote modules" }) const { modules: moduleNames } = args - const modules = await garden.getModules(moduleNames) + const graph = await garden.getConfigGraph() + const modules = await graph.getModules(moduleNames) const moduleSources = modules .filter(hasRemoteSource) @@ -59,7 +60,7 @@ export class UpdateRemoteModulesCommand extends Command { const diff = difference(moduleNames, names) if (diff.length > 0) { - const modulesWithRemoteSource = (await garden.getModules()).filter(hasRemoteSource).sort() + const modulesWithRemoteSource = (await graph.getModules()).filter(hasRemoteSource).sort() throw new ParameterError( `Expected module(s) ${chalk.underline(diff.join(","))} to have a remote source.`, diff --git a/garden-service/src/commands/validate.ts b/garden-service/src/commands/validate.ts index 91a4c48787..4c637ab693 100644 --- a/garden-service/src/commands/validate.ts +++ b/garden-service/src/commands/validate.ts @@ -25,7 +25,8 @@ export class ValidateCommand extends Command { async action({ garden, log }: CommandParams): Promise { logHeader({ log, emoji: "heavy_check_mark", command: "validate" }) - await garden.getModules() + const graph = await garden.getConfigGraph() + await graph.getModules() return {} } diff --git a/garden-service/src/dependency-graph.ts b/garden-service/src/config-graph.ts similarity index 58% rename from garden-service/src/dependency-graph.ts rename to garden-service/src/config-graph.ts index 54f997597b..4739ee1e3f 100644 --- a/garden-service/src/dependency-graph.ts +++ b/garden-service/src/config-graph.ts @@ -8,18 +8,23 @@ import * as Bluebird from "bluebird" const toposort = require("toposort") -import { flatten, fromPairs, pick, uniq } from "lodash" +import { flatten, pick, uniq, find, sortBy } from "lodash" import { Garden } from "./garden" -import { BuildDependencyConfig } from "./config/module" -import { Module, getModuleKey } from "./types/module" -import { Service } from "./types/service" -import { Task } from "./types/task" +import { BuildDependencyConfig, ModuleConfig } from "./config/module" +import { Module, getModuleKey, moduleFromConfig } from "./types/module" +import { Service, serviceFromConfig } from "./types/service" +import { Task, taskFromConfig } from "./types/task" import { TestConfig } from "./config/test" -import { uniqByName } from "./util/util" +import { uniqByName, pickKeys } from "./util/util" +import { ConfigurationError } from "./exceptions" +import { deline } from "./util/string" +import { validateDependencies } from "./util/validate-dependencies" +import { ServiceConfig } from "./config/service" +import { TaskConfig } from "./config/task" // Each of these types corresponds to a Task class (e.g. BuildTask, DeployTask, ...). export type DependencyGraphNodeType = "build" | "service" | "task" | "test" - | "push" | "publish" // these two types are currently not represented in DependencyGraph + | "push" | "publish" // these two types are currently not represented in the graph // The primary output type (for dependencies and dependants). export type DependencyRelations = { @@ -39,103 +44,215 @@ type DependencyRelationNames = { export type DependencyRelationFilterFn = (DependencyGraphNode) => boolean // Output types for rendering/logging - export type RenderedGraph = { nodes: RenderedNode[], relationships: RenderedEdge[] } - export type RenderedEdge = { dependant: RenderedNode, dependency: RenderedNode } - export type RenderedNode = { type: RenderedNodeType, name: string } - export type RenderedNodeType = "build" | "deploy" | "runTask" | "test" | "push" | "publish" /** * A graph data structure that facilitates querying (recursive or non-recursive) of the project's dependency and * dependant relationships. + * + * This should be initialized with fully resolved and validated ModuleConfigs. */ -export class DependencyGraph { +export class ConfigGraph { + private dependencyGraph: { [key: string]: DependencyGraphNode } + private moduleConfigs: { [key: string]: ModuleConfig } - index: { [key: string]: DependencyGraphNode } - private garden: Garden - private serviceMap: { [key: string]: Service } - private taskMap: { [key: string]: Task } - private testConfigMap: { [key: string]: TestConfig } - private testConfigModuleMap: { [key: string]: Module } + private serviceConfigs: { [key: string]: { moduleKey: string, config: ServiceConfig } } + private taskConfigs: { [key: string]: { moduleKey: string, config: TaskConfig } } + private testConfigs: { [key: string]: { moduleKey: string, config: TestConfig } } - static async factory(garden: Garden) { - const modules = await garden.getModules() - const { services, tasks } = await garden.getServicesAndTasks() - return new DependencyGraph(garden, modules, services, tasks) - } + constructor(private garden: Garden, moduleConfigs: ModuleConfig[]) { + this.garden = garden + this.dependencyGraph = {} + this.moduleConfigs = {} + this.serviceConfigs = {} + this.taskConfigs = {} + this.testConfigs = {} + + for (const moduleConfig of moduleConfigs) { + const moduleKey = this.keyForModule(moduleConfig) + this.moduleConfigs[moduleKey] = moduleConfig + + // Add services + for (const serviceConfig of moduleConfig.serviceConfigs) { + const serviceName = serviceConfig.name + + if (this.taskConfigs[serviceName]) { + throw serviceTaskConflict(serviceName, this.taskConfigs[serviceName].moduleKey, moduleKey) + } - constructor(garden: Garden, modules: Module[], services: Service[], tasks: Task[]) { + if (this.serviceConfigs[serviceName]) { + const [moduleA, moduleB] = [moduleKey, this.serviceConfigs[serviceName].moduleKey].sort() + + throw new ConfigurationError(deline` + Service names must be unique - the service name '${serviceName}' is declared multiple times + (in modules '${moduleA}' and '${moduleB}')`, + { + serviceName, + moduleA, + moduleB, + }, + ) + } - this.garden = garden - this.index = {} + // Make sure service source modules are added as build dependencies for the module + const { sourceModuleName } = serviceConfig + if (sourceModuleName && !find(moduleConfig.build.dependencies, ["name", sourceModuleName])) { + moduleConfig.build.dependencies.push({ name: sourceModuleName, copy: [] }) + } + + this.serviceConfigs[serviceName] = { moduleKey, config: serviceConfig } + } + + // Add tasks + for (const taskConfig of moduleConfig.taskConfigs) { + const taskName = taskConfig.name + + if (this.serviceConfigs[taskName]) { + throw serviceTaskConflict(taskName, moduleKey, this.serviceConfigs[taskName].moduleKey) + } + + if (this.taskConfigs[taskName]) { + const [moduleA, moduleB] = [moduleKey, this.taskConfigs[taskName].moduleKey].sort() + + throw new ConfigurationError(deline` + Task names must be unique - the task name '${taskName}' is declared multiple times (in modules + '${moduleA}' and '${moduleB}')`, + { + taskName, + moduleA, + moduleB, + }) + } - this.serviceMap = fromPairs(services.map(s => [s.name, s])) - this.taskMap = fromPairs(tasks.map(w => [w.name, w])) - this.testConfigMap = {} - this.testConfigModuleMap = {} + this.taskConfigs[taskName] = { moduleKey, config: taskConfig } + } + } - for (const module of modules) { + this.validateDependencies() - const moduleKey = this.keyForModule(module) + for (const moduleConfig of moduleConfigs) { + const moduleKey = this.keyForModule(moduleConfig) + this.moduleConfigs[moduleKey] = moduleConfig // Build dependencies const buildNode = this.getNode("build", moduleKey, moduleKey) - for (const buildDep of module.build.dependencies) { + for (const buildDep of moduleConfig.build.dependencies) { const buildDepKey = getModuleKey(buildDep.name, buildDep.plugin) this.addRelation(buildNode, "build", buildDepKey, buildDepKey) } // Service dependencies - for (const serviceConfig of module.serviceConfigs) { + for (const serviceConfig of moduleConfig.serviceConfigs) { const serviceNode = this.getNode("service", serviceConfig.name, moduleKey) this.addRelation(serviceNode, "build", moduleKey, moduleKey) for (const depName of serviceConfig.dependencies) { - if (this.serviceMap[depName]) { - this.addRelation(serviceNode, "service", depName, this.keyForModule(this.serviceMap[depName].module)) + if (this.serviceConfigs[depName]) { + this.addRelation(serviceNode, "service", depName, this.serviceConfigs[depName].moduleKey) } else { - this.addRelation(serviceNode, "task", depName, this.keyForModule(this.taskMap[depName].module)) + this.addRelation(serviceNode, "task", depName, this.taskConfigs[depName].moduleKey) } } } // Task dependencies - for (const taskConfig of module.taskConfigs) { + for (const taskConfig of moduleConfig.taskConfigs) { const taskNode = this.getNode("task", taskConfig.name, moduleKey) this.addRelation(taskNode, "build", moduleKey, moduleKey) for (const depName of taskConfig.dependencies) { - if (this.serviceMap[depName]) { - this.addRelation(taskNode, "service", depName, this.keyForModule(this.serviceMap[depName].module)) + if (this.serviceConfigs[depName]) { + this.addRelation(taskNode, "service", depName, this.serviceConfigs[depName].moduleKey) } else { - this.addRelation(taskNode, "task", depName, this.keyForModule(this.taskMap[depName].module)) + this.addRelation(taskNode, "task", depName, this.taskConfigs[depName].moduleKey) } } } // Test dependencies - for (const testConfig of module.testConfigs) { - const testConfigName = `${module.name}.${testConfig.name}` - this.testConfigMap[testConfigName] = testConfig - this.testConfigModuleMap[testConfigName] = module + for (const testConfig of moduleConfig.testConfigs) { + const testConfigName = `${moduleConfig.name}.${testConfig.name}` + + this.testConfigs[testConfigName] = { moduleKey, config: testConfig } + const testNode = this.getNode("test", testConfigName, moduleKey) this.addRelation(testNode, "build", moduleKey, moduleKey) for (const depName of testConfig.dependencies) { - if (this.serviceMap[depName]) { - this.addRelation(testNode, "service", depName, this.keyForModule(this.serviceMap[depName].module)) + if (this.serviceConfigs[depName]) { + this.addRelation(testNode, "service", depName, this.serviceConfigs[depName].moduleKey) } else { - this.addRelation(testNode, "task", depName, this.keyForModule(this.taskMap[depName].module)) + this.addRelation(testNode, "task", depName, this.taskConfigs[depName].moduleKey) } } } - } } // Convenience method used in the constructor above. - keyForModule(module: Module | BuildDependencyConfig) { - return getModuleKey(module.name, module.plugin) + keyForModule(config: ModuleConfig | BuildDependencyConfig) { + return getModuleKey(config.name, config.plugin) + } + + private validateDependencies() { + validateDependencies( + Object.values(this.moduleConfigs), + Object.keys(this.serviceConfigs), + Object.keys(this.taskConfigs)) + } + + /** + * Returns the Service with the specified name. Throws error if it doesn't exist. + */ + async getModule(name: string): Promise { + return (await this.getModules([name]))[0] + } + + /** + * Returns the Service with the specified name. Throws error if it doesn't exist. + */ + async getService(name: string): Promise { + return (await this.getServices([name]))[0] + } + + /** + * Returns the Task with the specified name. Throws error if it doesn't exist. + */ + async getTask(name: string): Promise { + return (await this.getTasks([name]))[0] + } + + /* + Returns all modules defined in this configuration graph, or the ones specified. + */ + async getModules(names?: string[]): Promise { + const configs = Object.values( + names ? pickKeys(this.moduleConfigs, names, "module") : this.moduleConfigs, + ) + + return Bluebird.map(configs, config => moduleFromConfig(this.garden, this, config)) + } + + /* + Returns all services defined in this configuration graph, or the ones specified. + */ + async getServices(names?: string[]): Promise { + const entries = Object.values( + names ? pickKeys(this.serviceConfigs, names, "service") : this.serviceConfigs, + ) + + return Bluebird.map(entries, async (e) => serviceFromConfig(this, await this.getModule(e.moduleKey), e.config)) + } + + /* + Returns all tasks defined in this configuration graph, or the ones specified. + */ + async getTasks(names?: string[]): Promise { + const entries = Object.values( + names ? pickKeys(this.taskConfigs, names, "task") : this.taskConfigs, + ) + + return Bluebird.map(entries, async (e) => taskFromConfig(await this.getModule(e.moduleKey), e.config)) } /* @@ -151,7 +268,7 @@ export class DependencyGraph { // We call getModules to ensure that the returned modules have up-to-date versions. const dependantModules = await this.modulesForRelations( await this.mergeRelations(...dependants)) - return this.garden.getModules(uniq(modules.concat(dependantModules).map(m => m.name))) + return this.getModules(uniq(modules.concat(dependantModules).map(m => m.name))) } /** @@ -166,7 +283,7 @@ export class DependencyGraph { } /** - * Returns all dependencies of a node in DependencyGraph. As noted above, each DependencyGraphNodeType corresponds + * Returns all dependencies of a node in the graph. As noted above, each DependencyGraphNodeType corresponds * to a Task class (e.g. BuildTask, DeployTask, ...), and name corresponds to the value returned by its getName * instance method. * @@ -179,7 +296,7 @@ export class DependencyGraph { } /** - * Returns all dependants of a node in DependencyGraph. As noted above, each DependencyGraphNodeType corresponds + * Returns all dependants of a node in the graph. As noted above, each DependencyGraphNodeType corresponds * to a Task class (e.g. BuildTask, DeployTask, ...), and name corresponds to the value returned by its getName * instance method. * @@ -238,10 +355,33 @@ export class DependencyGraph { relations.build, relations.service.map(s => s.module), relations.task.map(w => w.module), - relations.test.map(t => this.testConfigModuleMap[t.name]), + await this.getModules(relations.test.map(t => this.testConfigs[t.name].moduleKey)), ]).map(m => m.name)) // We call getModules to ensure that the returned modules have up-to-date versions. - return this.garden.getModules(moduleNames) + return this.getModules(moduleNames) + } + + /** + * Given the provided lists of build and runtime (service/task) dependencies, return a list of all + * modules required to satisfy those dependencies. + */ + async resolveDependencyModules( + buildDependencies: BuildDependencyConfig[], runtimeDependencies: string[], + ): Promise { + const moduleNames = buildDependencies.map(d => getModuleKey(d.name, d.plugin)) + const serviceNames = runtimeDependencies.filter(d => this.serviceConfigs[d]) + const taskNames = runtimeDependencies.filter(d => this.taskConfigs[d]) + + const buildDeps = await this.getDependenciesForMany("build", moduleNames, true) + const serviceDeps = await this.getDependenciesForMany("service", serviceNames, true) + const taskDeps = await this.getDependenciesForMany("task", taskNames, true) + + const modules = [ + ...(await this.getModules(moduleNames)), + ...(await this.modulesForRelations(await this.mergeRelations(buildDeps, serviceDeps, taskDeps))), + ] + + return sortBy(uniqByName(modules), "name") } private async toRelations(nodes): Promise { @@ -255,17 +395,17 @@ export class DependencyGraph { private async relationsFromNames(names: DependencyRelationNames): Promise { return Bluebird.props({ - build: this.garden.getModules(names.build), - service: this.garden.getServices(names.service), - task: this.garden.getTasks(names.task), - test: Object.values(pick(this.testConfigMap, names.test)), + build: this.getModules(names.build), + service: this.getServices(names.service), + task: this.getTasks(names.task), + test: Object.values(pick(this.testConfigs, names.test)).map(t => t.config), }) } private getDependencyNodes( nodeType: DependencyGraphNodeType, name: string, recursive: boolean, filterFn?: DependencyRelationFilterFn, ): DependencyGraphNode[] { - const node = this.index[nodeKey(nodeType, name)] + const node = this.dependencyGraph[nodeKey(nodeType, name)] if (node) { if (recursive) { return node.recursiveDependencies(filterFn) @@ -280,7 +420,7 @@ export class DependencyGraph { private getDependantNodes( nodeType: DependencyGraphNodeType, name: string, recursive: boolean, filterFn?: DependencyRelationFilterFn, ): DependencyGraphNode[] { - const node = this.index[nodeKey(nodeType, name)] + const node = this.dependencyGraph[nodeKey(nodeType, name)] if (node) { if (recursive) { return node.recursiveDependants(filterFn) @@ -309,18 +449,18 @@ export class DependencyGraph { // Idempotent. private getNode(type: DependencyGraphNodeType, name: string, moduleName: string) { const key = nodeKey(type, name) - const existingNode = this.index[key] + const existingNode = this.dependencyGraph[key] if (existingNode) { return existingNode } else { const newNode = new DependencyGraphNode(type, name, moduleName) - this.index[key] = newNode + this.dependencyGraph[key] = newNode return newNode } } render(): RenderedGraph { - const nodes = Object.values(this.index) + const nodes = Object.values(this.dependencyGraph) let edges: { dependant: DependencyGraphNode, dependency: DependencyGraphNode }[] = [] let simpleEdges: string[][] = [] for (const dependant of nodes) { @@ -435,3 +575,14 @@ export class DependencyGraphNode { function nodeKey(type: DependencyGraphNodeType, name: string) { return `${type}.${name}` } + +function serviceTaskConflict(conflictingName: string, moduleWithTask: string, moduleWithService: string) { + return new ConfigurationError(deline` + Service and task names must be mutually unique - the name '${conflictingName}' is used for a task in + '${moduleWithTask}' and for a service in '${moduleWithService}'`, + { + conflictingName, + moduleWithTask, + moduleWithService, + }) +} diff --git a/garden-service/src/config/config-context.ts b/garden-service/src/config/config-context.ts index c7d05a048f..8006138847 100644 --- a/garden-service/src/config/config-context.ts +++ b/garden-service/src/config/config-context.ts @@ -7,7 +7,6 @@ */ import { isString } from "lodash" -import { Module } from "../types/module" import { PrimitiveMap, isPrimitive, Primitive, joiIdentifierMap, joiStringMap, joiPrimitive } from "./common" import { Provider, Environment, providerConfigBaseSchema } from "./project" import { ModuleConfig } from "./module" @@ -15,6 +14,7 @@ import { ConfigurationError } from "../exceptions" import { resolveTemplateString } from "../template-string" import * as Joi from "joi" import { Garden } from "../garden" +import { ModuleVersion } from "../vcs/base" export type ContextKey = string[] @@ -130,6 +130,7 @@ export abstract class ConfigContext { } this._resolvedValues[path] = value + return value } } @@ -210,12 +211,12 @@ class ModuleContext extends ConfigContext { @schema(Joi.string().description("The current version of the module.").example(exampleVersion)) public version: string - constructor(root: ConfigContext, module: Module) { + constructor(root: ConfigContext, moduleConfig: ModuleConfig, buildPath: string, version: ModuleVersion) { super(root) - this.buildPath = module.buildPath - this.outputs = module.outputs - this.path = module.path - this.version = module.version.versionString + this.buildPath = buildPath + this.outputs = moduleConfig.outputs + this.path = moduleConfig.path + this.version = version.versionString } } @@ -265,29 +266,16 @@ export class ModuleConfigContext extends ProjectConfigContext { this.modules = new Map(moduleConfigs.map((config) => <[string, () => Promise]>[config.name, async () => { // NOTE: This is a temporary hacky solution until we implement module resolution as a TaskGraph task - if (!garden.hasModule(config.name)) { - await garden.addModule(config) - } - const module = await garden.getModule(config.name) - return new ModuleContext(_this, module) + const resolvedConfig = await garden.resolveModuleConfig(config.name) + const version = await garden.resolveVersion(resolvedConfig.name, resolvedConfig.build.dependencies) + const buildPath = await garden.buildDir.buildPath(config.name) + + return new ModuleContext(_this, resolvedConfig, buildPath, version) }], )) this.providers = new Map(environment.providers.map(p => <[string, Provider]>[p.name, p])) - // this.config = new SecretsContextNode(ctx) - this.variables = environment.variables } } - -// class RemoteConfigContext extends ConfigContext { -// constructor(private ctx: PluginContext) { -// super() -// } - -// async resolve({ key }: ResolveParams) { -// const { value } = await this.ctx.getSecret({ key }) -// return value === null ? undefined : value -// } -// } diff --git a/garden-service/src/garden.ts b/garden-service/src/garden.ts index 0f46eba318..0f976c1bbd 100644 --- a/garden-service/src/garden.ts +++ b/garden-service/src/garden.ts @@ -7,7 +7,6 @@ */ import Bluebird = require("bluebird") -import deline = require("deline") import { parse, relative, @@ -18,49 +17,29 @@ import { import { extend, flatten, - intersection, isString, - fromPairs, merge, keyBy, cloneDeep, - pick, - pickBy, sortBy, - difference, - find, findIndex, } from "lodash" const AsyncLock = require("async-lock") import { TreeCache } from "./cache" -import { - builtinPlugins, - fixedPlugins, -} from "./plugins/plugins" -import { Module, moduleFromConfig, getModuleCacheContext, getModuleKey, ModuleConfigMap } from "./types/module" -import { - moduleActionDescriptions, - moduleActionNames, - pluginActionDescriptions, - pluginModuleSchema, - pluginSchema, -} from "./types/plugin/plugin" -import { Environment, SourceConfig, defaultProvider, ProviderConfig, Provider } from "./config/project" +import { builtinPlugins, fixedPlugins } from "./plugins/plugins" +import { Module, getModuleCacheContext, getModuleKey, ModuleConfigMap } from "./types/module" +import { moduleActionNames, pluginModuleSchema, pluginSchema } from "./types/plugin/plugin" +import { Environment, SourceConfig, ProviderConfig, Provider } from "./config/project" import { findByName, getIgnorer, getNames, scanDirectory, pickKeys, - throwOnMissingNames, - uniqByName, Ignorer, } from "./util/util" -import { - DEFAULT_NAMESPACE, - MODULE_CONFIG_FILENAME, -} from "./constants" +import { DEFAULT_NAMESPACE, MODULE_CONFIG_FILENAME } from "./constants" import { ConfigurationError, ParameterError, @@ -70,24 +49,11 @@ import { import { VcsHandler, ModuleVersion } from "./vcs/base" import { GitHandler } from "./vcs/git" import { BuildDir } from "./build-dir" -import { DependencyGraph } from "./dependency-graph" -import { - TaskGraph, - TaskResults, -} from "./task-graph" -import { - getLogger, -} from "./logger/logger" -import { - pluginActionNames, - PluginActions, - PluginFactory, - GardenPlugin, - ModuleActions, -} from "./types/plugin/plugin" +import { ConfigGraph } from "./config-graph" +import { TaskGraph, TaskResults } from "./task-graph" +import { getLogger } from "./logger/logger" +import { pluginActionNames, PluginActions, PluginFactory, GardenPlugin } from "./types/plugin/plugin" import { joiIdentifier, validate, PrimitiveMap } from "./config/common" -import { Service, serviceFromConfig } from "./types/service" -import { Task } from "./types/task" import { resolveTemplateStrings } from "./template-string" import { configSchema, @@ -97,11 +63,7 @@ import { } from "./config/base" import { BaseTask } from "./tasks/base" import { LocalConfigStore } from "./config-store" -import { validateDependencies } from "./util/validate-dependencies" -import { - getLinkedSources, - ExternalSourceType, -} from "./util/ext-source-util" +import { getLinkedSources, ExternalSourceType } from "./util/ext-source-util" import { BuildDependencyConfig, ModuleConfig } from "./config/module" import { ProjectConfigContext, ModuleConfigContext } from "./config/config-context" import { ActionHelper } from "./actions" @@ -146,16 +108,11 @@ const scanLock = new AsyncLock() export class Garden { public readonly log: LogEntry - public readonly actionHandlers: PluginActionMap - public readonly moduleActionHandlers: ModuleActionMap - public dependencyGraph: DependencyGraph - private readonly loadedPlugins: { [key: string]: GardenPlugin } private moduleConfigs: ModuleConfigMap + private pluginModuleConfigs: ModuleConfig[] private modulesScanned: boolean private readonly registeredPlugins: { [key: string]: PluginFactory } - private readonly serviceNameIndex: { [key: string]: string } // service name -> module name - private readonly taskNameIndex: { [key: string]: string } // task name -> module name private readonly taskGraph: TaskGraph private readonly watcher: Watcher @@ -203,12 +160,9 @@ export class Garden { } this.moduleConfigs = {} - this.serviceNameIndex = {} - this.taskNameIndex = {} this.loadedPlugins = {} + this.pluginModuleConfigs = [] this.registeredPlugins = {} - this.actionHandlers = fromPairs(pluginActionNames.map(n => [n, {}])) - this.moduleActionHandlers = fromPairs(moduleActionNames.map(n => [n, {}])) this.taskGraph = new TaskGraph(this, this.log) this.actions = new ActionHelper(this) @@ -353,8 +307,8 @@ export class Garden { * Enables the file watcher for the project. * Make sure to stop it using `.close()` when cleaning up or when watching is no longer needed. */ - async startWatcher() { - const modules = await this.getModules() + async startWatcher(graph: ConfigGraph) { + const modules = await graph.getModules() this.watcher.start(modules) } @@ -449,7 +403,7 @@ export class Garden { this.loadedPlugins[pluginName] = plugin for (const modulePath of plugin.modules || []) { - let moduleConfig = await this.resolveModule(modulePath) + let moduleConfig = await this.loadModuleConfig(modulePath) if (!moduleConfig) { throw new PluginError(`Could not load module "${modulePath}" specified in plugin "${pluginName}"`, { pluginName, @@ -457,14 +411,14 @@ export class Garden { }) } moduleConfig.plugin = pluginName - await this.addModule(moduleConfig) + this.pluginModuleConfigs.push(moduleConfig) } const actions = plugin.actions || {} for (const actionType of pluginActionNames) { const handler = actions[actionType] - handler && this.addActionHandler(pluginName, actionType, handler) + handler && this.actions.addActionHandler(pluginName, actionType, handler) } const moduleActions = plugin.moduleActions || {} @@ -472,7 +426,7 @@ export class Garden { for (const moduleType of Object.keys(moduleActions)) { for (const actionType of moduleActionNames) { const handler = moduleActions[moduleType][actionType] - handler && this.addModuleActionHandler(pluginName, actionType, moduleType, handler) + handler && this.actions.addModuleActionHandler(pluginName, actionType, moduleType, handler) } } @@ -503,7 +457,7 @@ export class Garden { } } - private getPlugin(pluginName: string) { + getPlugin(pluginName: string) { const plugin = this.loadedPlugins[pluginName] if (!plugin) { @@ -516,123 +470,57 @@ export class Garden { return plugin } - private addActionHandler( - pluginName: string, actionType: T, handler: PluginActions[T], - ) { - const plugin = this.getPlugin(pluginName) - const schema = pluginActionDescriptions[actionType].resultSchema - - const wrapped = async (...args) => { - const result = await handler.apply(plugin, args) - return validate(result, schema, { context: `${actionType} output from plugin ${pluginName}` }) - } - wrapped["actionType"] = actionType - wrapped["pluginName"] = pluginName - - this.actionHandlers[actionType][pluginName] = wrapped - } - - private addModuleActionHandler( - pluginName: string, actionType: T, moduleType: string, handler: ModuleActions[T], - ) { - const plugin = this.getPlugin(pluginName) - const schema = moduleActionDescriptions[actionType].resultSchema - - const wrapped = async (...args) => { - const result = await handler.apply(plugin, args) - return validate(result, schema, { context: `${actionType} output from plugin ${pluginName}` }) - } - wrapped["actionType"] = actionType - wrapped["pluginName"] = pluginName - wrapped["moduleType"] = moduleType - - if (!this.moduleActionHandlers[actionType]) { - this.moduleActionHandlers[actionType] = {} - } - - if (!this.moduleActionHandlers[actionType][moduleType]) { - this.moduleActionHandlers[actionType][moduleType] = {} - } - - this.moduleActionHandlers[actionType][moduleType][pluginName] = wrapped - } - - /* - Returns all modules that are registered in this context. - Scans for modules in the project root if it hasn't already been done. + /** + * Returns module configs that are registered in this context, fully resolved and configured (via their respective + * plugin handlers). + * Scans for modules in the project root and remote/linked sources if it hasn't already been done. */ - async getModules(names?: string[], noScan?: boolean): Promise { - if (!this.modulesScanned && !noScan) { + async resolveModuleConfigs(keys?: string[], configContext?: ModuleConfigContext): Promise { + if (!this.modulesScanned) { await this.scanModules() } - let configs: ModuleConfig[] + const configs: ModuleConfig[] = Object.values( + keys ? pickKeys(this.moduleConfigs, keys, "module") : this.moduleConfigs, + ) - if (!!names) { - configs = [] - const missing: string[] = [] + if (!configContext) { + configContext = new ModuleConfigContext(this, this.environment, Object.values(this.moduleConfigs)) + } - for (const name of names) { - const module = this.moduleConfigs[name] + return Bluebird.map(configs, async (config) => { + config = await resolveTemplateStrings(cloneDeep(config), configContext!) - if (!module) { - missing.push(name) - } else { - configs.push(module) - } - } + const configureHandler = await this.actions.getModuleActionHandler({ + actionType: "configure", + moduleType: config.type, + }) + const ctx = this.getPluginContext(configureHandler["pluginName"]) - if (missing.length) { - throw new ParameterError(`Could not find module(s): ${missing.join(", ")}`, { - missing, - available: Object.keys(this.moduleConfigs), - }) - } - } else { - configs = Object.values(this.moduleConfigs) - } + config = await configureHandler({ ctx, moduleConfig: config }) - return Bluebird.map(configs, config => moduleFromConfig(this, config)) + // FIXME: We should be able to avoid this + config.name = getModuleKey(config.name, config.plugin) + + return config + }) } /** * Returns the module with the specified name. Throws error if it doesn't exist. */ - async getModule(name: string, noScan?: boolean): Promise { - return (await this.getModules([name], noScan))[0] - } - - async getDependencyGraph() { - if (!this.dependencyGraph) { - this.dependencyGraph = await DependencyGraph.factory(this) - } - - return this.dependencyGraph + async resolveModuleConfig(name: string, configContext?: ModuleConfigContext): Promise { + return (await this.resolveModuleConfigs([name], configContext))[0] } /** - * Given the provided lists of build and runtime (service/task) dependencies, return a list of all - * modules required to satisfy those dependencies. + * Resolve the raw module configs and return a new instance of ConfigGraph. + * 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 resolveDependencyModules( - buildDependencies: BuildDependencyConfig[], runtimeDependencies: string[], - ): Promise { - const moduleNames = buildDependencies.map(d => getModuleKey(d.name, d.plugin)) - const dg = await this.getDependencyGraph() - - const serviceNames = runtimeDependencies.filter(d => this.serviceNameIndex[d]) - const taskNames = runtimeDependencies.filter(d => this.taskNameIndex[d]) - - const buildDeps = await dg.getDependenciesForMany("build", moduleNames, true) - const serviceDeps = await dg.getDependenciesForMany("service", serviceNames, true) - const taskDeps = await dg.getDependenciesForMany("task", taskNames, true) - - const modules = [ - ...(await this.getModules(moduleNames)), - ...(await dg.modulesForRelations(await dg.mergeRelations(buildDeps, serviceDeps, taskDeps))), - ] - - return sortBy(uniqByName(modules), "name") + async getConfigGraph() { + const modules = await this.resolveModuleConfigs() + return new ConfigGraph(this, modules) } /** @@ -664,124 +552,6 @@ export class Garden { return version } - async getServiceOrTask(name: string, noScan?: boolean): Promise | Task> { - const service = (await this.getServices([name], noScan))[0] - const task = (await this.getTasks([name], noScan))[0] - - if (!service && !task) { - throw new ParameterError(`Could not find service or task named ${name}`, { - missing: [name], - availableServices: Object.keys(this.serviceNameIndex), - availableTasks: Object.keys(this.taskNameIndex), - }) - } - - return service || task - } - - /** - * Returns the service with the specified name. Throws error if it doesn't exist. - */ - async getService(name: string, noScan?: boolean): Promise> { - const service = (await this.getServices([name], noScan))[0] - - if (!service) { - throw new ParameterError(`Could not find service ${name}`, { - missing: [name], - available: Object.keys(this.serviceNameIndex), - }) - } - - return service - } - - async getTask(name: string, noScan?: boolean): Promise { - const task = (await this.getTasks([name], noScan))[0] - - if (!task) { - throw new ParameterError(`Could not find task ${name}`, { - missing: [name], - available: Object.keys(this.taskNameIndex), - }) - } - - return task - } - - /* - Returns all services that are registered in this context, or the ones specified. - If the names parameter is used and task names are included in it, they will be - ignored. Scans for modules and services in the project root if it hasn't already - been done. - */ - async getServices(names?: string[], noScan?: boolean): Promise { - const services = (await this.getServicesAndTasks(names, noScan)).services - if (names) { - const taskNames = Object.keys(this.taskNameIndex) - throwOnMissingNames(difference(names, taskNames), services, "service") - } - return services - } - - /* - Returns all tasks that are registered in this context, or the ones specified. - If the names parameter is used and service names are included in it, they will be - ignored. Scans for modules and services in the project root if it hasn't already - been done. - */ - async getTasks(names?: string[], noScan?: boolean): Promise { - const tasks = (await this.getServicesAndTasks(names, noScan)).tasks - if (names) { - const serviceNames = Object.keys(this.serviceNameIndex) - throwOnMissingNames(difference(names, serviceNames), tasks, "task") - } - return tasks - } - - async getServicesAndTasks(names?: string[], noScan?: boolean) { - if (!this.modulesScanned && !noScan) { - await this.scanModules() - } - - let pickedServices: { [key: string]: string } - let pickedTasks: { [key: string]: string } - - if (names) { - const serviceNames = Object.keys(this.serviceNameIndex) - const taskNames = Object.keys(this.taskNameIndex) - pickedServices = pick(this.serviceNameIndex, intersection(names, serviceNames)) - pickedTasks = pick(this.taskNameIndex, intersection(names, taskNames)) - } else { - pickedServices = this.serviceNameIndex - pickedTasks = this.taskNameIndex - } - - return Bluebird.props({ - services: Bluebird.map(Object.entries(pickedServices), async ([serviceName, moduleName]): - Promise => { - - const module = await this.getModule(moduleName) - const config = findByName(module.serviceConfigs, serviceName)! - - return serviceFromConfig(this, module, config) - }), - - tasks: Bluebird.map(Object.entries(pickedTasks), async ([taskName, moduleName]): - Promise => { - - const module = await this.getModule(moduleName) - const config = findByName(module.taskConfigs, taskName)! - - return { - name: taskName, - config, - module, - spec: config.spec, - } - }), - }) - } - /* Scans the project root for modules and adds them to the context. */ @@ -828,44 +598,23 @@ export class Garden { return paths })).filter(Boolean) - const rawConfigs: ModuleConfig[] = [] + const rawConfigs: ModuleConfig[] = [...this.pluginModuleConfigs] await Bluebird.map(modulePaths, async path => { - const config = await this.resolveModule(path) + const config = await this.loadModuleConfig(path) if (config) { rawConfigs.push(config) } }) - this.modulesScanned = true - - // Resolve template strings - const moduleConfigContext = new ModuleConfigContext( - this, - this.environment, - [...Object.values(this.moduleConfigs), ...rawConfigs], - ) - const resolvedConfigs = await resolveTemplateStrings(rawConfigs, moduleConfigContext) - - // Configure and validate the modules - await Bluebird.map(resolvedConfigs, async (config) => { - // Need this check here, because the module might have been added while resolving the template strings - if (!this.hasModule(config.name)) { - await this.addModule(config) - } - }) + for (const config of rawConfigs) { + this.addModule(config) + } - this.validateDependencies() + this.modulesScanned = true }) } - private validateDependencies() { - validateDependencies( - Object.values(this.moduleConfigs), - Object.keys(this.serviceNameIndex), - Object.keys(this.taskNameIndex)) - } - /** * Returns true if a module has been configured in this project with the specified name. */ @@ -873,120 +622,35 @@ export class Garden { return !!this.moduleConfigs[name] } - /* - Adds the specified module config to the context - - @param force - add the module again, even if it's already registered + /** + * Add a module config to the context, after validating and calling the appropriate configure plugin handler. + * Template strings should be resolved on the config before calling this. */ - async addModule(config: ModuleConfig) { - const configureHandler = await this.getModuleActionHandler({ actionType: "configure", moduleType: config.type }) - const ctx = this.getPluginContext(configureHandler["pluginName"]) + private addModule(config: ModuleConfig) { + const key = getModuleKey(config.name, config.plugin) - config = await configureHandler({ ctx, moduleConfig: config }) - - // FIXME: this is rather clumsy - config.name = getModuleKey(config.name, config.plugin) - - if (this.moduleConfigs[config.name]) { + if (this.moduleConfigs[key]) { const [pathA, pathB] = [ - relative(this.projectRoot, join(this.moduleConfigs[config.name].path, MODULE_CONFIG_FILENAME)), + relative(this.projectRoot, join(this.moduleConfigs[key].path, MODULE_CONFIG_FILENAME)), relative(this.projectRoot, join(config.path, MODULE_CONFIG_FILENAME)), ].sort() throw new ConfigurationError( - `Module ${config.name} is declared multiple times (in '${pathA}' and '${pathB}')`, + `Module ${key} is declared multiple times (in '${pathA}' and '${pathB}')`, { pathA, pathB }, ) } - // Make sure service source modules are added as build dependencies for the module - for (const serviceConfig of config.serviceConfigs) { - const { sourceModuleName } = serviceConfig - - if (sourceModuleName && !find(config.build.dependencies, ["name", sourceModuleName])) { - config.build.dependencies.push({ name: sourceModuleName, copy: [] }) - } - } - - // Add to service-module map - for (const serviceConfig of config.serviceConfigs) { - const serviceName = serviceConfig.name - - if (this.taskNameIndex[serviceName]) { - throw serviceTaskConflict(serviceName, this.taskNameIndex[serviceName], config.name) - } - - if (this.serviceNameIndex[serviceName]) { - const [moduleA, moduleB] = [config.name, this.serviceNameIndex[serviceName]].sort() - - throw new ConfigurationError(deline` - Service names must be unique - the service name '${serviceName}' is declared multiple times - (in modules '${moduleA}' and '${moduleB}')`, - { - serviceName, - moduleA, - moduleB, - }, - ) - } - - this.serviceNameIndex[serviceName] = config.name - } - - // Add to task-module map - for (const taskConfig of config.taskConfigs) { - const taskName = taskConfig.name - - if (this.serviceNameIndex[taskName]) { - throw serviceTaskConflict(taskName, config.name, this.serviceNameIndex[taskName]) - } - - if (this.taskNameIndex[taskName]) { - const [moduleA, moduleB] = [config.name, this.taskNameIndex[taskName]].sort() - - throw new ConfigurationError(deline` - Task names must be unique - the task name '${taskName}' is declared multiple times (in modules - '${moduleA}' and '${moduleB}')`, - { - taskName, - moduleA, - moduleB, - }) - } - - this.taskNameIndex[taskName] = config.name - } - - this.moduleConfigs[config.name] = config - - return config + this.moduleConfigs[key] = config } - /* - Maps the provided name or locator to a Module. We first look for a module in the - project with the provided name. If it does not exist, we treat it as a path - (resolved with the project path as a base path) and attempt to load the module - from there. + /** + * Load a module from the specified directory and return the config, or null if no module is found. + * + * @param path Directory containing the module */ - async resolveModule(nameOrLocation: string): Promise { - const parsedPath = parse(nameOrLocation) - - if (parsedPath.dir === "") { - // Looks like a name - const existingModule = this.moduleConfigs[nameOrLocation] - - if (!existingModule) { - throw new ConfigurationError(`Module ${nameOrLocation} could not be found`, { - name: nameOrLocation, - }) - } - - return existingModule - } - - // Looks like a path - const path = resolve(this.projectRoot, nameOrLocation) - const config = await loadConfig(this.projectRoot, path) + private async loadModuleConfig(path: string): Promise { + const config = await loadConfig(this.projectRoot, resolve(this.projectRoot, path)) if (!config || !config.module) { return null @@ -1031,133 +695,15 @@ export class Garden { return path } - /** - * Get a handler for the specified action. - */ - public getActionHandlers(actionType: T, pluginName?: string): ActionHandlerMap { - return this.filterActionHandlers(this.actionHandlers[actionType], pluginName) - } - - /** - * Get a handler for the specified module action. - */ - public getModuleActionHandlers( - { actionType, moduleType, pluginName }: - { actionType: T, moduleType: string, pluginName?: string }, - ): ModuleActionHandlerMap { - return this.filterActionHandlers((this.moduleActionHandlers[actionType] || {})[moduleType], pluginName) - } - - private filterActionHandlers(handlers, pluginName?: string) { - // make sure plugin is loaded - if (!!pluginName) { - this.getPlugin(pluginName) - } - - if (handlers === undefined) { - handlers = {} - } - - return !pluginName ? handlers : pickBy(handlers, (handler) => handler["pluginName"] === pluginName) - } - - /** - * Get the last configured handler for the specified action (and optionally module type). - */ - public getActionHandler( - { actionType, pluginName, defaultHandler }: - { actionType: T, pluginName?: string, defaultHandler?: PluginActions[T] }, - ): PluginActions[T] { - - const handlers = Object.values(this.getActionHandlers(actionType, pluginName)) - - if (handlers.length) { - return handlers[handlers.length - 1] - } else if (defaultHandler) { - defaultHandler["pluginName"] = defaultProvider.name - return defaultHandler - } - - const errorDetails = { - requestedHandlerType: actionType, - environment: this.environment.name, - pluginName, - } - - if (pluginName) { - throw new PluginError(`Plugin '${pluginName}' does not have a '${actionType}' handler.`, errorDetails) - } else { - throw new ParameterError( - `No '${actionType}' handler configured in environment '${this.environment.name}'. ` + - `Are you missing a provider configuration?`, - errorDetails, - ) - } - } - - /** - * Get the last configured handler for the specified action. - */ - public getModuleActionHandler( - { actionType, moduleType, pluginName, defaultHandler }: - { actionType: T, moduleType: string, pluginName?: string, defaultHandler?: ModuleAndRuntimeActions[T] }, - ): ModuleAndRuntimeActions[T] { - - const handlers = Object.values(this.getModuleActionHandlers({ actionType, moduleType, pluginName })) - - if (handlers.length) { - return handlers[handlers.length - 1] - } else if (defaultHandler) { - defaultHandler["pluginName"] = defaultProvider.name - return defaultHandler - } - - const errorDetails = { - requestedHandlerType: actionType, - requestedModuleType: moduleType, - environment: this.environment.name, - pluginName, - } - - if (pluginName) { - throw new PluginError( - `Plugin '${pluginName}' does not have a '${actionType}' handler for module type '${moduleType}'.`, - errorDetails, - ) - } else { - throw new ParameterError( - `No '${actionType}' handler configured for module type '${moduleType}' in environment ` + - `'${this.environment.name}'. Are you missing a provider configuration?`, - errorDetails, - ) - } - } - /** * This dumps the full project configuration including all modules. */ public async dumpConfig(): Promise { - const modules = await this.getModules() - - // Remove circular references and superfluous keys. - for (const module of modules) { - delete module._ConfigType - delete module.buildDependencies - - for (const service of module.services) { - delete service.module - delete service.sourceModule - } - for (const task of module.tasks) { - delete task.module - } - } - return { environmentName: this.environment.name, providers: this.environment.providers, variables: this.environment.variables, - modules: sortBy(modules, "name"), + modules: sortBy(Object.values(this.moduleConfigs), "name"), } } @@ -1168,17 +714,5 @@ export interface ConfigDump { environmentName: string providers: Provider[] variables: PrimitiveMap - modules: Module[] -} - -function serviceTaskConflict(conflictingName: string, moduleWithTask: string, moduleWithService: string) { - return new ConfigurationError(deline` - Service and task names must be mutually unique - the name '${conflictingName}' is used for a task in - '${moduleWithTask}' and for a service in '${moduleWithService}'`, - { - conflictingName, - moduleWithTask, - moduleWithService, - }) - + modules: ModuleConfig[] } diff --git a/garden-service/src/plugins/kubernetes/helm/build.ts b/garden-service/src/plugins/kubernetes/helm/build.ts index abe93b527f..eeaee2b315 100644 --- a/garden-service/src/plugins/kubernetes/helm/build.ts +++ b/garden-service/src/plugins/kubernetes/helm/build.ts @@ -13,15 +13,11 @@ import { containsSource, getChartPath, getValuesPath, getBaseModule } from "./co import { helm } from "./helm-cli" import { safeLoad } from "js-yaml" import { dumpYaml } from "../../../util/util" -import { join } from "path" -import { GARDEN_BUILD_VERSION_FILENAME } from "../../../constants" -import { writeModuleVersionFile } from "../../../vcs/base" import { LogEntry } from "../../../logger/log-entry" import { getNamespace } from "../namespace" import { apply as jsonMerge } from "json-merge-patch" export async function buildHelmModule({ ctx, module, log }: BuildModuleParams): Promise { - const buildPath = module.buildPath const namespace = await getNamespace({ ctx, provider: ctx.provider, skipCreate: true }) const context = ctx.provider.config.context const baseModule = getBaseModule(module) @@ -51,13 +47,9 @@ export async function buildHelmModule({ ctx, module, log }: BuildModuleParams> = { // TODO: add execInService handler deleteService, deployService, - getBuildStatus: getExecModuleBuildStatus, getServiceLogs, getServiceStatus, getTestResult, diff --git a/garden-service/src/process.ts b/garden-service/src/process.ts index 88e37d1876..6245e15bb2 100644 --- a/garden-service/src/process.ts +++ b/garden-service/src/process.ts @@ -18,11 +18,13 @@ import { isModuleLinked } from "./util/ext-source-util" import { Garden } from "./garden" import { LogEntry } from "./logger/log-entry" import { startServer } from "./server/server" +import { ConfigGraph } from "./config-graph" -export type ProcessHandler = (module: Module) => Promise +export type ProcessHandler = (graph: ConfigGraph, module: Module) => Promise interface ProcessParams { garden: Garden + graph: ConfigGraph log: LogEntry logFooter?: LogEntry watch: boolean @@ -45,7 +47,7 @@ export interface ProcessResults { } export async function processServices( - { garden, log, logFooter, services, watch, handler, changeHandler }: ProcessServicesParams, + { garden, graph, log, logFooter, services, watch, handler, changeHandler }: ProcessServicesParams, ): Promise { const modules = Array.from(new Set(services.map(s => s.module))) @@ -53,6 +55,7 @@ export async function processServices( return processModules({ modules, garden, + graph, log, logFooter, watch, @@ -62,7 +65,7 @@ export async function processServices( } export async function processModules( - { garden, log, logFooter, modules, watch, handler, changeHandler }: ProcessModulesParams, + { garden, graph, log, logFooter, modules, watch, handler, changeHandler }: ProcessModulesParams, ): Promise { log.debug("Starting processModules") @@ -81,7 +84,7 @@ export async function processModules( } for (const module of modules) { - const tasks = await handler(module) + const tasks = await handler(graph, module) await Bluebird.map(tasks, t => garden.addTask(t)) } @@ -110,7 +113,7 @@ export async function processModules( const modulesByName = keyBy(modules, "name") - await garden.startWatcher() + await garden.startWatcher(graph) const restartPromise = new Promise((resolve) => { garden.events.on("_restart", () => { @@ -140,7 +143,10 @@ export async function processModules( return } - await Bluebird.map(changeHandler!(changedModule), (task) => garden.addTask(task)) + // Update the config graph + graph = await garden.getConfigGraph() + + await Bluebird.map(changeHandler!(graph, changedModule), (task) => garden.addTask(task)) await garden.processTasks() }) }) diff --git a/garden-service/src/tasks/base.ts b/garden-service/src/tasks/base.ts index b763d0c434..29f064ad8d 100644 --- a/garden-service/src/tasks/base.ts +++ b/garden-service/src/tasks/base.ts @@ -10,7 +10,7 @@ import { TaskResults } from "../task-graph" import { ModuleVersion } from "../vcs/base" import { v1 as uuidv1 } from "uuid" import { Garden } from "../garden" -import { DependencyGraphNodeType } from "../dependency-graph" +import { DependencyGraphNodeType } from "../config-graph" import { LogEntry } from "../logger/log-entry" export class TaskDefinitionError extends Error { } diff --git a/garden-service/src/tasks/build.ts b/garden-service/src/tasks/build.ts index 736db0af60..41d29a309d 100644 --- a/garden-service/src/tasks/build.ts +++ b/garden-service/src/tasks/build.ts @@ -12,7 +12,7 @@ import { Module, getModuleKey } from "../types/module" import { BuildResult } from "../types/plugin/outputs" import { BaseTask } from "../tasks/base" import { Garden } from "../garden" -import { DependencyGraphNodeType } from "../dependency-graph" +import { DependencyGraphNodeType } from "../config-graph" import { LogEntry } from "../logger/log-entry" export interface BuildTaskParams { @@ -40,7 +40,7 @@ export class BuildTask extends BaseTask { } async getDependencies(): Promise { - const dg = await this.garden.getDependencyGraph() + const dg = await this.garden.getConfigGraph() const deps = (await dg.getDependencies(this.depType, this.getName(), false)).build return Bluebird.map(deps, async (m: Module) => { diff --git a/garden-service/src/tasks/deploy.ts b/garden-service/src/tasks/deploy.ts index 638849708c..e4ddee6205 100644 --- a/garden-service/src/tasks/deploy.ts +++ b/garden-service/src/tasks/deploy.ts @@ -11,18 +11,15 @@ import chalk from "chalk" import { includes } from "lodash" import { LogEntry } from "../logger/log-entry" import { BaseTask } from "./base" -import { - Service, - ServiceStatus, - prepareRuntimeContext, -} from "../types/service" +import { Service, ServiceStatus, getServiceRuntimeContext } from "../types/service" import { Garden } from "../garden" import { PushTask } from "./push" import { TaskTask } from "./task" -import { DependencyGraphNodeType } from "../dependency-graph" +import { DependencyGraphNodeType, ConfigGraph } from "../config-graph" export interface DeployTaskParams { garden: Garden + graph: ConfigGraph service: Service force: boolean forceBuild: boolean @@ -35,15 +32,17 @@ export class DeployTask extends BaseTask { type = "deploy" depType: DependencyGraphNodeType = "service" + private graph: ConfigGraph private service: Service private forceBuild: boolean private fromWatch: boolean private hotReloadServiceNames: string[] constructor( - { garden, log, service, force, forceBuild, fromWatch = false, hotReloadServiceNames = [] }: DeployTaskParams, + { garden, graph, log, service, force, forceBuild, fromWatch = false, hotReloadServiceNames = [] }: DeployTaskParams, ) { super({ garden, log, force, version: service.module.version }) + this.graph = graph this.service = service this.forceBuild = forceBuild this.fromWatch = fromWatch @@ -51,8 +50,7 @@ export class DeployTask extends BaseTask { } async getDependencies() { - - const dg = await this.garden.getDependencyGraph() + const dg = this.graph // We filter out service dependencies on services configured for hot reloading (if any) const deps = await dg.getDependencies(this.depType, this.getName(), false, @@ -61,6 +59,7 @@ export class DeployTask extends BaseTask { const deployTasks = await Bluebird.map(deps.service, async (service) => { return new DeployTask({ garden: this.garden, + graph: this.graph, log: this.log, service, force: false, @@ -78,6 +77,7 @@ export class DeployTask extends BaseTask { task, garden: this.garden, log: this.log, + graph: this.graph, force: false, forceBuild: this.forceBuild, }) @@ -115,10 +115,13 @@ export class DeployTask extends BaseTask { let version = this.version const hotReload = includes(this.hotReloadServiceNames, this.service.name) + const runtimeContext = await getServiceRuntimeContext(this.garden, this.graph, this.service) + const status = await this.garden.actions.getServiceStatus({ service: this.service, log, hotReload, + runtimeContext, }) const { versionString } = version @@ -138,13 +141,11 @@ export class DeployTask extends BaseTask { log.setState(`Deploying version ${versionString}...`) - const dependencies = await this.garden.getServices(this.service.config.dependencies) - let result: ServiceStatus try { result = await this.garden.actions.deployService({ service: this.service, - runtimeContext: await prepareRuntimeContext(this.garden, this.service.module, dependencies), + runtimeContext, log, force: this.force, hotReload, diff --git a/garden-service/src/tasks/helpers.ts b/garden-service/src/tasks/helpers.ts index 248702b561..2a08f7d60e 100644 --- a/garden-service/src/tasks/helpers.ts +++ b/garden-service/src/tasks/helpers.ts @@ -12,16 +12,23 @@ import { BuildTask } from "./build" import { Garden } from "../garden" import { Module } from "../types/module" import { Service } from "../types/service" -import { DependencyGraphNode } from "../dependency-graph" +import { DependencyGraphNode, ConfigGraph } from "../config-graph" import { LogEntry } from "../logger/log-entry" import { BaseTask } from "./base" export async function getDependantTasksForModule( - { garden, log, module, hotReloadServiceNames, force = false, forceBuild = false, + { garden, log, graph, module, hotReloadServiceNames, force = false, forceBuild = false, fromWatch = false, includeDependants = false }: { - garden: Garden, log: LogEntry, module: Module, hotReloadServiceNames: string[], force?: boolean, - forceBuild?: boolean, fromWatch?: boolean, includeDependants?: boolean, + garden: Garden, + log: LogEntry, + graph: ConfigGraph, + module: Module, + hotReloadServiceNames: string[], + force?: boolean, + forceBuild?: boolean, + fromWatch?: boolean, + includeDependants?: boolean, }, ): Promise { @@ -31,26 +38,25 @@ export async function getDependantTasksForModule( if (!includeDependants) { buildTasks.push(new BuildTask({ garden, log, module, force: forceBuild, fromWatch, hotReloadServiceNames })) - services = module.services + services = await graph.getServices(module.serviceNames) } else { - const hotReloadModuleNames = await getModuleNames(garden, hotReloadServiceNames) - const dg = await garden.getDependencyGraph() + const hotReloadModuleNames = await getModuleNames(graph, hotReloadServiceNames) const dependantFilterFn = (dependantNode: DependencyGraphNode) => !hotReloadModuleNames.includes(dependantNode.moduleName) if (intersection(module.serviceNames, hotReloadServiceNames).length) { // Hot reloading is enabled for one or more of module's services. - const serviceDeps = await dg.getDependantsForMany("service", module.serviceNames, true, dependantFilterFn) + const serviceDeps = await graph.getDependantsForMany("service", module.serviceNames, true, dependantFilterFn) dependantBuildModules = serviceDeps.build services = serviceDeps.service } else { - const dependants = await dg.getDependantsForModule(module, dependantFilterFn) + const dependants = await graph.getDependantsForModule(module, dependantFilterFn) buildTasks.push(new BuildTask({ garden, log, module, force: true, fromWatch, hotReloadServiceNames })) dependantBuildModules = dependants.build - services = module.services.concat(dependants.service) + services = (await graph.getServices(module.serviceNames)).concat(dependants.service) } } @@ -58,7 +64,15 @@ export async function getDependantTasksForModule( .map(m => new BuildTask({ garden, log, module: m, force: forceBuild, fromWatch, hotReloadServiceNames }))) const deployTasks = services - .map(service => new DeployTask({ garden, log, service, force, forceBuild, fromWatch, hotReloadServiceNames })) + .map(service => new DeployTask({ + garden, + log, + graph, + service, + force, + forceBuild, + fromWatch, hotReloadServiceNames, + })) const outputTasks = [...buildTasks, ...deployTasks] log.silly(`getDependantTasksForModule called for module ${module.name}, returning the following tasks:`) @@ -67,7 +81,7 @@ export async function getDependantTasksForModule( return outputTasks } -async function getModuleNames(garden: Garden, hotReloadServiceNames: string[]) { - const services = await garden.getServices(hotReloadServiceNames) +async function getModuleNames(dg: ConfigGraph, hotReloadServiceNames: string[]) { + const services = await dg.getServices(hotReloadServiceNames) return uniq(services.map(s => s.module.name)) } diff --git a/garden-service/src/tasks/hot-reload.ts b/garden-service/src/tasks/hot-reload.ts index 73ac355864..f367560660 100644 --- a/garden-service/src/tasks/hot-reload.ts +++ b/garden-service/src/tasks/hot-reload.ts @@ -9,12 +9,13 @@ import chalk from "chalk" import { LogEntry } from "../logger/log-entry" import { BaseTask } from "./base" -import { Service } from "../types/service" +import { Service, getServiceRuntimeContext } from "../types/service" import { Garden } from "../garden" -import { DependencyGraphNodeType } from "../dependency-graph" +import { DependencyGraphNodeType, ConfigGraph } from "../config-graph" interface Params { garden: Garden + graph: ConfigGraph force: boolean service: Service log: LogEntry @@ -24,12 +25,14 @@ export class HotReloadTask extends BaseTask { type = "hot-reload" depType: DependencyGraphNodeType = "service" + private graph: ConfigGraph private service: Service constructor( - { garden, log, service, force }: Params, + { garden, graph, log, service, force }: Params, ) { super({ garden, log, force, version: service.module.version }) + this.graph = graph this.service = service } @@ -48,8 +51,10 @@ export class HotReloadTask extends BaseTask { status: "active", }) + const runtimeContext = await getServiceRuntimeContext(this.garden, this.graph, this.service) + try { - await this.garden.actions.hotReloadService({ log, service: this.service }) + await this.garden.actions.hotReloadService({ log, service: this.service, runtimeContext }) } catch (err) { log.setError() throw err diff --git a/garden-service/src/tasks/publish.ts b/garden-service/src/tasks/publish.ts index f5e5fbe179..c0fa9f273e 100644 --- a/garden-service/src/tasks/publish.ts +++ b/garden-service/src/tasks/publish.ts @@ -12,7 +12,7 @@ import { Module } from "../types/module" import { PublishResult } from "../types/plugin/outputs" import { BaseTask } from "../tasks/base" import { Garden } from "../garden" -import { DependencyGraphNodeType } from "../dependency-graph" +import { DependencyGraphNodeType } from "../config-graph" import { LogEntry } from "../logger/log-entry" export interface PublishTaskParams { diff --git a/garden-service/src/tasks/push.ts b/garden-service/src/tasks/push.ts index 1cdccee266..f6d9ac2baf 100644 --- a/garden-service/src/tasks/push.ts +++ b/garden-service/src/tasks/push.ts @@ -12,7 +12,7 @@ import { Module } from "../types/module" import { PushResult } from "../types/plugin/outputs" import { BaseTask } from "../tasks/base" import { Garden } from "../garden" -import { DependencyGraphNodeType } from "../dependency-graph" +import { DependencyGraphNodeType } from "../config-graph" import { LogEntry } from "../logger/log-entry" export interface PushTaskParams { @@ -63,7 +63,7 @@ export class PushTask extends BaseTask { async process(): Promise { // avoid logging stuff if there is no push handler const defaultHandler = async () => ({ pushed: false }) - const handler = await this.garden.getModuleActionHandler({ + const handler = await this.garden.actions.getModuleActionHandler({ moduleType: this.module.type, actionType: "pushModule", defaultHandler, diff --git a/garden-service/src/tasks/task.ts b/garden-service/src/tasks/task.ts index 2b14ebda70..01c35e7ad7 100644 --- a/garden-service/src/tasks/task.ts +++ b/garden-service/src/tasks/task.ts @@ -15,11 +15,12 @@ import { DeployTask } from "./deploy" import { LogEntry } from "../logger/log-entry" import { RunTaskResult } from "../types/plugin/outputs" import { prepareRuntimeContext } from "../types/service" -import { DependencyGraphNodeType } from "../dependency-graph" +import { DependencyGraphNodeType, ConfigGraph } from "../config-graph" export interface TaskTaskParams { garden: Garden log: LogEntry + graph: ConfigGraph task: Task force: boolean forceBuild: boolean @@ -29,11 +30,13 @@ export class TaskTask extends BaseTask { // ... to be renamed soon. type = "task" depType: DependencyGraphNodeType = "task" + private graph: ConfigGraph private task: Task private forceBuild: boolean - constructor({ garden, log, task, force, forceBuild }: TaskTaskParams) { + constructor({ garden, log, graph, task, force, forceBuild }: TaskTaskParams) { super({ garden, log, force, version: task.module.version }) + this.graph = graph this.task = task this.forceBuild = forceBuild } @@ -47,7 +50,7 @@ export class TaskTask extends BaseTask { // ... to be renamed soon. force: this.forceBuild, }) - const dg = await this.garden.getDependencyGraph() + const dg = await this.garden.getConfigGraph() const deps = await dg.getDependencies(this.depType, this.getName(), false) const deployTasks = deps.service.map(service => { @@ -55,6 +58,7 @@ export class TaskTask extends BaseTask { // ... to be renamed soon. service, log: this.log, garden: this.garden, + graph: this.graph, force: false, forceBuild: false, }) @@ -65,6 +69,7 @@ export class TaskTask extends BaseTask { // ... to be renamed soon. task, log: this.log, garden: this.garden, + graph: this.graph, force: false, forceBuild: false, }) @@ -93,9 +98,8 @@ export class TaskTask extends BaseTask { // ... to be renamed soon. }) // combine all dependencies for all services in the module, to be sure we have all the context we need - const dg = await this.garden.getDependencyGraph() - const serviceDeps = (await dg.getDependencies(this.depType, this.getName(), false)).service - const runtimeContext = await prepareRuntimeContext(this.garden, module, serviceDeps) + const serviceDeps = (await this.graph.getDependencies(this.depType, this.getName(), false)).service + const runtimeContext = await prepareRuntimeContext(this.garden, this.graph, module, serviceDeps) let result: RunTaskResult try { diff --git a/garden-service/src/tasks/test.ts b/garden-service/src/tasks/test.ts index 132234b2dd..eb17e94a84 100644 --- a/garden-service/src/tasks/test.ts +++ b/garden-service/src/tasks/test.ts @@ -18,7 +18,7 @@ import { BaseTask, TaskParams } from "../tasks/base" import { prepareRuntimeContext } from "../types/service" import { Garden } from "../garden" import { LogEntry } from "../logger/log-entry" -import { DependencyGraphNodeType } from "../dependency-graph" +import { DependencyGraphNodeType, ConfigGraph } from "../config-graph" class TestError extends Error { toString() { @@ -29,6 +29,7 @@ class TestError extends Error { export interface TestTaskParams { garden: Garden log: LogEntry + graph: ConfigGraph module: Module testConfig: TestConfig force: boolean @@ -40,20 +41,22 @@ export class TestTask extends BaseTask { depType: DependencyGraphNodeType = "test" private module: Module + private graph: ConfigGraph private testConfig: TestConfig private forceBuild: boolean - constructor({ garden, log, module, testConfig, force, forceBuild, version }: TestTaskParams & TaskParams) { + constructor({ garden, graph, log, module, testConfig, force, forceBuild, version }: TestTaskParams & TaskParams) { super({ garden, log, force, version }) this.module = module + this.graph = graph this.testConfig = testConfig this.force = force this.forceBuild = forceBuild } static async factory(initArgs: TestTaskParams): Promise { - const { garden, module, testConfig } = initArgs - const version = await getTestVersion(garden, module, testConfig) + const { garden, graph, module, testConfig } = initArgs + const version = await getTestVersion(garden, graph, module, testConfig) return new TestTask({ ...initArgs, version }) } @@ -64,7 +67,7 @@ export class TestTask extends BaseTask { return [] } - const dg = await this.garden.getDependencyGraph() + const dg = this.graph const services = (await dg.getDependencies(this.depType, this.getName(), false)).service const deps: BaseTask[] = [new BuildTask({ @@ -77,6 +80,7 @@ export class TestTask extends BaseTask { for (const service of services) { deps.push(new DeployTask({ garden: this.garden, + graph: this.graph, log: this.log, service, force: false, @@ -114,8 +118,8 @@ export class TestTask extends BaseTask { status: "active", }) - const dependencies = await getTestDependencies(this.garden, this.testConfig) - const runtimeContext = await prepareRuntimeContext(this.garden, this.module, dependencies) + const dependencies = await getTestDependencies(this.graph, this.testConfig) + const runtimeContext = await prepareRuntimeContext(this.garden, this.graph, this.module, dependencies) let result: TestResult try { @@ -156,13 +160,22 @@ export class TestTask extends BaseTask { } export async function getTestTasks( - { garden, log, module, name, force = false, forceBuild = false }: - { garden: Garden, log: LogEntry, module: Module, name?: string, force?: boolean, forceBuild?: boolean }, + { garden, log, graph, module, name, force = false, forceBuild = false }: + { + garden: Garden, + log: LogEntry, + graph: ConfigGraph, + module: Module, + name?: string, + force?: boolean, + forceBuild?: boolean, + }, ) { const configs = module.testConfigs.filter(test => !name || test.name === name) return Bluebird.map(configs, test => TestTask.factory({ garden, + graph, log, force, forceBuild, @@ -171,14 +184,17 @@ export async function getTestTasks( })) } -async function getTestDependencies(garden: Garden, testConfig: TestConfig) { - return garden.getServices(testConfig.dependencies) +async function getTestDependencies(graph: ConfigGraph, testConfig: TestConfig) { + const deps = await graph.getDependencies("test", testConfig.name, false) + return deps.service } /** * Determine the version of the test run, based on the version of the module and each of its dependencies. */ -async function getTestVersion(garden: Garden, module: Module, testConfig: TestConfig): Promise { - const moduleDeps = await garden.resolveDependencyModules(module.build.dependencies, testConfig.dependencies) +async function getTestVersion( + garden: Garden, graph: ConfigGraph, module: Module, testConfig: TestConfig, +): Promise { + const moduleDeps = await graph.resolveDependencyModules(module.build.dependencies, testConfig.dependencies) return garden.resolveVersion(module.name, moduleDeps) } diff --git a/garden-service/src/types/module.ts b/garden-service/src/types/module.ts index d4e761b74d..f619da6479 100644 --- a/garden-service/src/types/module.ts +++ b/garden-service/src/types/module.ts @@ -11,14 +11,13 @@ import { getNames } from "../util/util" import { TestSpec } from "../config/test" import { ModuleSpec, ModuleConfig, moduleConfigSchema } from "../config/module" import { ServiceSpec } from "../config/service" -import { Task, taskFromConfig } from "./task" -import { TaskSpec, taskSchema } from "../config/task" +import { TaskSpec } from "../config/task" import { ModuleVersion, moduleVersionSchema } from "../vcs/base" import { pathToCacheContext } from "../cache" import { Garden } from "../garden" -import { serviceFromConfig, Service, serviceSchema } from "./service" import * as Joi from "joi" import { joiArray, joiIdentifier, joiIdentifierMap } from "../config/common" +import { ConfigGraph } from "../config-graph" import * as Bluebird from "bluebird" export interface FileCopySpec { @@ -37,11 +36,9 @@ export interface Module< buildDependencies: ModuleMap - services: Service>[] serviceNames: string[] serviceDependencyNames: string[] - tasks: Task>[] taskNames: string[] taskDependencyNames: string[] @@ -59,18 +56,12 @@ export const moduleSchema = moduleConfigSchema buildDependencies: joiIdentifierMap(Joi.lazy(() => moduleSchema)) .required() .description("A map of all modules referenced under \`build.dependencies\`."), - services: joiArray(Joi.lazy(() => serviceSchema)) - .required() - .description("A list of all the services that the module provides."), serviceNames: joiArray(joiIdentifier()) .required() .description("The names of the services that the module provides."), serviceDependencyNames: joiArray(joiIdentifier()) .required() .description("The names of all the services and tasks that the services in this module depend on."), - tasks: joiArray(Joi.lazy(() => taskSchema)) - .required() - .description("A list of all the tasks that the module provides."), taskNames: joiArray(joiIdentifier()) .required() .description("The names of the tasks that the module provides."), @@ -87,7 +78,7 @@ export interface ModuleConfigMap { [key: string]: T } -export async function moduleFromConfig(garden: Garden, config: ModuleConfig): Promise { +export async function moduleFromConfig(garden: Garden, graph: ConfigGraph, config: ModuleConfig): Promise { const module: Module = { ...cloneDeep(config), @@ -96,13 +87,11 @@ export async function moduleFromConfig(garden: Garden, config: ModuleConfig): Pr buildDependencies: {}, - services: [], serviceNames: getNames(config.serviceConfigs), serviceDependencyNames: uniq(flatten(config.serviceConfigs .map(serviceConfig => serviceConfig.dependencies) .filter(deps => !!deps))), - tasks: [], taskNames: getNames(config.taskConfigs), taskDependencyNames: uniq(flatten(config.taskConfigs .map(taskConfig => taskConfig.dependencies) @@ -112,17 +101,10 @@ export async function moduleFromConfig(garden: Garden, config: ModuleConfig): Pr } const buildDependencyModules = await Bluebird.map( - module.build.dependencies, d => garden.getModule(getModuleKey(d.name, d.plugin)), + module.build.dependencies, d => graph.getModule(getModuleKey(d.name, d.plugin)), ) module.buildDependencies = keyBy(buildDependencyModules, "name") - module.services = await Bluebird.map( - config.serviceConfigs, - serviceConfig => serviceFromConfig(garden, module, serviceConfig), - ) - - module.tasks = config.taskConfigs.map(taskConfig => taskFromConfig(module, taskConfig)) - return module } diff --git a/garden-service/src/types/service.ts b/garden-service/src/types/service.ts index 36429a634a..733b13d4cf 100644 --- a/garden-service/src/types/service.ts +++ b/garden-service/src/types/service.ts @@ -17,6 +17,7 @@ import { format } from "url" import { moduleVersionSchema } from "../vcs/base" import { Garden } from "../garden" import { uniq } from "lodash" +import { ConfigGraph } from "../config-graph" import normalizeUrl = require("normalize-url") export interface Service { @@ -40,9 +41,9 @@ export const serviceSchema = Joi.object() }) export async function serviceFromConfig - (garden: Garden, module: M, config: ServiceConfig): Promise> { + (graph: ConfigGraph, module: M, config: ServiceConfig): Promise> { - const sourceModule = config.sourceModuleName ? await garden.getModule(config.sourceModuleName) : module + const sourceModule = config.sourceModuleName ? await graph.getModule(config.sourceModuleName) : module return { name: config.name, @@ -209,10 +210,10 @@ export const runtimeContextSchema = Joi.object() }) export async function prepareRuntimeContext( - garden: Garden, module: Module, serviceDependencies: Service[], + garden: Garden, graph: ConfigGraph, module: Module, serviceDependencies: Service[], ): Promise { const buildDepKeys = module.build.dependencies.map(dep => getModuleKey(dep.name, dep.plugin)) - const buildDependencies: Module[] = await garden.getModules(buildDepKeys) + const buildDependencies: Module[] = await graph.getModules(buildDepKeys) const { versionString } = module.version const envVars = { GARDEN_VERSION: versionString, @@ -262,6 +263,11 @@ export async function prepareRuntimeContext( } } +export async function getServiceRuntimeContext(garden: Garden, graph: ConfigGraph, service: Service) { + const deps = await graph.getDependencies("service", service.name, false) + return prepareRuntimeContext(garden, graph, service.module, deps.service) +} + export function getIngressUrl(ingress: ServiceIngress) { return normalizeUrl(format({ protocol: ingress.protocol, diff --git a/garden-service/test/data/test-project-container/module-a/garden.yml b/garden-service/test/data/test-project-container/module-a/garden.yml index 66fdf0e799..09faaa1709 100644 --- a/garden-service/test/data/test-project-container/module-a/garden.yml +++ b/garden-service/test/data/test-project-container/module-a/garden.yml @@ -13,7 +13,4 @@ module: tcpPort: http tasks: - name: task-a - command: [echo, A] - dependencies: - - task-b - - service-b + args: [echo, A] diff --git a/garden-service/test/src/actions.ts b/garden-service/test/src/actions.ts index 23b2fcbf2e..522e92b9db 100644 --- a/garden-service/test/src/actions.ts +++ b/garden-service/test/src/actions.ts @@ -1,12 +1,12 @@ import { Garden } from "../../src/garden" -import { makeTestGardenA } from "../helpers" +import { makeTestGardenA, expectError } from "../helpers" import { PluginFactory, PluginActions, ModuleAndRuntimeActions } from "../../src/types/plugin/plugin" import { validate } from "../../src/config/common" import { ActionHelper } from "../../src/actions" import { expect } from "chai" import { omit } from "lodash" import { Module } from "../../src/types/module" -import { Service } from "../../src/types/service" +import { Service, RuntimeContext, getServiceRuntimeContext } from "../../src/types/service" import { Task } from "../../src/types/task" import Stream from "ts-stream" import { ServiceLogEntry } from "../../src/types/plugin/outputs" @@ -46,6 +46,7 @@ describe("ActionHelper", () => { let actions: ActionHelper let module: Module let service: Service + let runtimeContext: RuntimeContext let task: Task before(async () => { @@ -53,9 +54,11 @@ describe("ActionHelper", () => { garden = await makeTestGardenA(plugins) log = garden.log actions = garden.actions - module = await garden.getModule("module-a") - service = await garden.getService("service-a") - task = await garden.getTask("task-a") + const graph = await garden.getConfigGraph() + module = await graph.getModule("module-a") + service = await graph.getService("service-a") + runtimeContext = await getServiceRuntimeContext(garden, graph, service) + task = await graph.getTask("task-a") }) // Note: The test plugins below implicitly validate input params for each of the tests @@ -254,28 +257,34 @@ describe("ActionHelper", () => { describe("service actions", () => { describe("getServiceStatus", () => { it("should correctly call the corresponding plugin handler", async () => { - const result = await actions.getServiceStatus({ log, service, hotReload: false }) + const result = await actions.getServiceStatus({ log, service, runtimeContext, hotReload: false }) expect(result).to.eql({ state: "ready" }) }) }) describe("deployService", () => { it("should correctly call the corresponding plugin handler", async () => { - const result = await actions.deployService({ log, service, force: true, hotReload: false }) + const result = await actions.deployService({ log, service, runtimeContext, force: true, hotReload: false }) expect(result).to.eql({ state: "ready" }) }) }) describe("deleteService", () => { it("should correctly call the corresponding plugin handler", async () => { - const result = await actions.deleteService({ log, service }) + const result = await actions.deleteService({ log, service, runtimeContext }) expect(result).to.eql({ state: "ready" }) }) }) describe("execInService", () => { it("should correctly call the corresponding plugin handler", async () => { - const result = await actions.execInService({ log, service, command: ["foo"], interactive: false }) + const result = await actions.execInService({ + log, + service, + runtimeContext, + command: ["foo"], + interactive: false, + }) expect(result).to.eql({ code: 0, output: "bla bla" }) }) }) @@ -283,7 +292,7 @@ describe("ActionHelper", () => { describe("getServiceLogs", () => { it("should correctly call the corresponding plugin handler", async () => { const stream = new Stream() - const result = await actions.getServiceLogs({ log, service, stream, follow: false, tail: -1 }) + const result = await actions.getServiceLogs({ log, service, runtimeContext, stream, follow: false, tail: -1 }) expect(result).to.eql({}) }) }) @@ -335,6 +344,68 @@ describe("ActionHelper", () => { }) }) }) + + describe("getActionHandlers", () => { + it("should return all handlers for a type", async () => { + const handlers = actions.getActionHandlers("prepareEnvironment") + + expect(Object.keys(handlers)).to.eql([ + "test-plugin", + "test-plugin-b", + ]) + }) + }) + + describe("getModuleActionHandlers", () => { + it("should return all handlers for a type", async () => { + const handlers = actions.getModuleActionHandlers({ actionType: "build", moduleType: "exec" }) + + expect(Object.keys(handlers)).to.eql([ + "exec", + ]) + }) + }) + + describe("getActionHandler", () => { + it("should return last configured handler for specified action type", async () => { + const gardenA = await makeTestGardenA() + const handler = gardenA.actions.getActionHandler({ actionType: "prepareEnvironment" }) + + expect(handler["actionType"]).to.equal("prepareEnvironment") + expect(handler["pluginName"]).to.equal("test-plugin-b") + }) + + it("should optionally filter to only handlers for the specified module type", async () => { + const gardenA = await makeTestGardenA() + const handler = gardenA.actions.getActionHandler({ actionType: "prepareEnvironment" }) + + expect(handler["actionType"]).to.equal("prepareEnvironment") + expect(handler["pluginName"]).to.equal("test-plugin-b") + }) + + it("should throw if no handler is available", async () => { + const gardenA = await makeTestGardenA() + await expectError(() => gardenA.actions.getActionHandler({ actionType: "cleanupEnvironment" }), "parameter") + }) + }) + + describe("getModuleActionHandler", () => { + it("should return last configured handler for specified module action type", async () => { + const gardenA = await makeTestGardenA() + const handler = gardenA.actions.getModuleActionHandler({ actionType: "deployService", moduleType: "test" }) + + expect(handler["actionType"]).to.equal("deployService") + expect(handler["pluginName"]).to.equal("test-plugin-b") + }) + + it("should throw if no handler is available", async () => { + const gardenA = await makeTestGardenA() + await expectError( + () => gardenA.actions.getModuleActionHandler({ actionType: "execInService", moduleType: "container" }), + "parameter", + ) + }) + }) }) const testPlugin: PluginFactory = async () => ({ diff --git a/garden-service/test/src/build-dir.ts b/garden-service/test/src/build-dir.ts index 0881a21ed3..faf9e0fcc9 100644 --- a/garden-service/test/src/build-dir.ts +++ b/garden-service/test/src/build-dir.ts @@ -40,14 +40,14 @@ describe("BuildDir", () => { it("should ensure that a module's build subdir exists before returning from buildPath", async () => { const garden = await makeGarden() await garden.buildDir.clear() - const moduleA = await garden.getModule("module-a") + const moduleA = await garden.resolveModuleConfig("module-a") const buildPath = await garden.buildDir.buildPath(moduleA.name) expect(await pathExists(buildPath)).to.eql(true) }) it("should sync sources to the build dir", async () => { const garden = await makeGarden() - const moduleA = await garden.getModule("module-a") + const moduleA = await garden.resolveModuleConfig("module-a") await garden.buildDir.syncFromSrc(moduleA) const buildDirA = await garden.buildDir.buildPath(moduleA.name) @@ -67,7 +67,8 @@ describe("BuildDir", () => { try { await garden.clearBuilds() - const modules = await garden.getModules() + const graph = await garden.getConfigGraph() + const modules = await graph.getModules() await Bluebird.map(modules, async (module) => { return garden.addTask(new BuildTask({ diff --git a/garden-service/test/src/commands/get/get-config.ts b/garden-service/test/src/commands/get/get-config.ts index f85f03f2f4..e5de286df7 100644 --- a/garden-service/test/src/commands/get/get-config.ts +++ b/garden-service/test/src/commands/get/get-config.ts @@ -24,7 +24,7 @@ describe("GetConfigCommand", () => { environmentName: garden.environment.name, providers: garden.environment.providers, variables: garden.environment.variables, - modules: sortBy(await garden.getModules(), "name"), + modules: sortBy(await garden.resolveModuleConfigs(), "name"), } expect(isSubset(config, res.result)).to.be.true diff --git a/garden-service/test/src/config-graph.ts b/garden-service/test/src/config-graph.ts new file mode 100644 index 0000000000..fb1a611494 --- /dev/null +++ b/garden-service/test/src/config-graph.ts @@ -0,0 +1,185 @@ +import { resolve } from "path" +import { expect } from "chai" +import { makeTestGardenA, makeTestGarden, dataDir, expectError } from "../helpers" +import { getNames } from "../../src/util/util" +import { ConfigGraph } from "../../src/config-graph" +import { Garden } from "../../src/garden" + +describe("ConfigGraph", () => { + let gardenA: Garden + let graphA: ConfigGraph + + before(async () => { + gardenA = await makeTestGardenA() + graphA = await gardenA.getConfigGraph() + }) + + 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(), + err => expect(err.message).to.equal( + "Service names must be unique - the service name 'dupe' is declared multiple times " + + "(in modules 'module-a' and 'module-b')", + ), + ) + }) + + it("should throw when two tasks have the same name", async () => { + const garden = await makeTestGarden(resolve(dataDir, "test-projects", "duplicate-task")) + + await expectError( + () => garden.getConfigGraph(), + err => expect(err.message).to.equal( + "Task names must be unique - the task name 'dupe' is declared multiple times " + + "(in modules 'module-a' and 'module-b')", + ), + ) + }) + + it("should throw when a service and a task have the same name", async () => { + const garden = await makeTestGarden(resolve(dataDir, "test-projects", "duplicate-service-and-task")) + + await expectError( + () => garden.getConfigGraph(), + err => expect(err.message).to.equal( + "Service and task names must be mutually unique - the name 'dupe' is used for a task " + + "in 'module-b' and for a service in 'module-a'", + ), + ) + }) + + 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 module = await graph.getModule("module-b") + expect(module.build.dependencies).to.eql([{ name: "module-a", copy: [] }]) + }) + + describe("getModules", () => { + it("should scan and return all registered modules in the context", async () => { + const modules = await graphA.getModules() + expect(getNames(modules).sort()).to.eql(["module-a", "module-b", "module-c"]) + }) + + it("should optionally return specified modules in the context", async () => { + const modules = await graphA.getModules(["module-b", "module-c"]) + expect(getNames(modules).sort()).to.eql(["module-b", "module-c"]) + }) + + it("should throw if named module is missing", async () => { + try { + await graphA.getModules(["bla"]) + } catch (err) { + expect(err.type).to.equal("parameter") + return + } + + throw new Error("Expected error") + }) + }) + + describe("getServices", () => { + it("should scan for modules and return all registered services in the context", async () => { + const services = await graphA.getServices() + + expect(getNames(services).sort()).to.eql(["service-a", "service-b", "service-c"]) + }) + + it("should optionally return specified services in the context", async () => { + const services = await graphA.getServices(["service-b", "service-c"]) + + expect(getNames(services).sort()).to.eql(["service-b", "service-c"]) + }) + + it("should throw if named service is missing", async () => { + try { + await graphA.getServices(["bla"]) + } catch (err) { + expect(err.type).to.equal("parameter") + return + } + + throw new Error("Expected error") + }) + }) + + describe("getService", () => { + it("should return the specified service", async () => { + const service = await graphA.getService("service-b") + + expect(service.name).to.equal("service-b") + }) + + it("should throw if service is missing", async () => { + try { + await graphA.getService("bla") + } catch (err) { + expect(err.type).to.equal("parameter") + return + } + + throw new Error("Expected error") + }) + }) + + describe("getTasks", () => { + it("should scan for modules and return all registered tasks in the context", async () => { + const tasks = await graphA.getTasks() + expect(getNames(tasks).sort()).to.eql(["task-a", "task-b", "task-c"]) + }) + + it("should optionally return specified tasks in the context", async () => { + const tasks = await graphA.getTasks(["task-b", "task-c"]) + expect(getNames(tasks).sort()).to.eql(["task-b", "task-c"]) + }) + + it("should throw if named task is missing", async () => { + try { + await graphA.getTasks(["bla"]) + } catch (err) { + expect(err.type).to.equal("parameter") + return + } + + throw new Error("Expected error") + }) + }) + + describe("getTask", () => { + it("should return the specified task", async () => { + const task = await graphA.getTask("task-b") + + expect(task.name).to.equal("task-b") + }) + + it("should throw if task is missing", async () => { + try { + await graphA.getTask("bla") + } catch (err) { + expect(err.type).to.equal("parameter") + return + } + + throw new Error("Expected error") + }) + }) + + describe("resolveDependencyModules", () => { + it("should resolve build dependencies", async () => { + const modules = await graphA.resolveDependencyModules([{ name: "module-c", copy: [] }], []) + expect(getNames(modules)).to.eql(["module-a", "module-b", "module-c"]) + }) + + it("should resolve service dependencies", async () => { + const modules = await graphA.resolveDependencyModules([], ["service-b"]) + expect(getNames(modules)).to.eql(["module-a", "module-b"]) + }) + + it("should combine module and service dependencies", async () => { + const modules = await graphA.resolveDependencyModules([{ name: "module-b", copy: [] }], ["service-c"]) + expect(getNames(modules)).to.eql(["module-a", "module-b", "module-c"]) + }) + }) +}) diff --git a/garden-service/test/src/config/config-context.ts b/garden-service/test/src/config/config-context.ts index ecaa4805d6..db2e6d354a 100644 --- a/garden-service/test/src/config/config-context.ts +++ b/garden-service/test/src/config/config-context.ts @@ -258,7 +258,7 @@ describe("ModuleConfigContext", () => { }) it("should should resolve the version of a module", async () => { - const { versionString } = (await garden.getModule("module-a")).version + const { versionString } = await garden.resolveVersion("module-a", []) expect(await c.resolve({ key: ["modules", "module-a", "version"], nodePath: [] })).to.equal(versionString) }) @@ -267,7 +267,7 @@ describe("ModuleConfigContext", () => { }) it("should should resolve the version of a module", async () => { - const { versionString } = (await garden.getModule("module-a")).version + const { versionString } = await garden.resolveVersion("module-a", []) expect(await c.resolve({ key: ["modules", "module-a", "version"], nodePath: [] })).to.equal(versionString) }) diff --git a/garden-service/test/src/garden.ts b/garden-service/test/src/garden.ts index 5b89ca2c76..9480003599 100644 --- a/garden-service/test/src/garden.ts +++ b/garden-service/test/src/garden.ts @@ -34,8 +34,8 @@ describe("Garden", () => { it("should initialize and add the action handlers for a plugin", async () => { const garden = await makeTestGardenA() - expect(garden.actionHandlers.prepareEnvironment["test-plugin"]).to.be.ok - expect(garden.actionHandlers.prepareEnvironment["test-plugin-b"]).to.be.ok + expect((garden).actions.actionHandlers.prepareEnvironment["test-plugin"]).to.be.ok + expect((garden).actions.actionHandlers.prepareEnvironment["test-plugin-b"]).to.be.ok }) it("should initialize with MOCK_CONFIG", async () => { @@ -120,193 +120,13 @@ describe("Garden", () => { }) }) - describe("getModules", () => { - it("should scan and return all registered modules in the context", async () => { - const garden = await makeTestGardenA() - const modules = await garden.getModules() - - expect(getNames(modules).sort()).to.eql(["module-a", "module-b", "module-c"]) - }) - - it("should optionally return specified modules in the context", async () => { - const garden = await makeTestGardenA() - const modules = await garden.getModules(["module-b", "module-c"]) - - expect(getNames(modules).sort()).to.eql(["module-b", "module-c"]) - }) - - it("should throw if named module is missing", async () => { - const garden = await makeTestGardenA() - - try { - await garden.getModules(["bla"]) - } catch (err) { - expect(err.type).to.equal("parameter") - return - } - - throw new Error("Expected error") - }) - }) - - describe("getServicesAndTasks", () => { - it("should scan for modules and return all registered services and tasks in the context", async () => { - const garden = await makeTestGardenA() - const { services, tasks } = await garden.getServicesAndTasks() - - expect(getNames(services).sort()).to.eql(["service-a", "service-b", "service-c"]) - expect(getNames(tasks).sort()).to.eql(["task-a", "task-b", "task-c"]) - }) - - it("should optionally return specified services and tasks in the context", async () => { - const garden = await makeTestGardenA() - const { services, tasks } = await garden.getServicesAndTasks(["service-b", "service-c", "task-a"]) - - expect(getNames(services).sort()).to.eql(["service-b", "service-c"]) - expect(getNames(tasks).sort()).to.eql(["task-a"]) - }) - - it("should not throw if a named service or task is missing", async () => { - const garden = await makeTestGardenA() - - await garden.getServicesAndTasks(["not", "real"]) - }) - - }) - - describe("getServices", () => { - it("should scan for modules and return all registered services in the context", async () => { - const garden = await makeTestGardenA() - const services = await garden.getServices() - - expect(getNames(services).sort()).to.eql(["service-a", "service-b", "service-c"]) - }) - - it("should optionally return specified services in the context", async () => { - const garden = await makeTestGardenA() - const services = await garden.getServices(["service-b", "service-c"]) - - expect(getNames(services).sort()).to.eql(["service-b", "service-c"]) - }) - - it("should throw if named service is missing", async () => { - const garden = await makeTestGardenA() - - try { - await garden.getServices(["bla"]) - } catch (err) { - expect(err.type).to.equal("parameter") - return - } - - throw new Error("Expected error") - }) - }) - - describe("getService", () => { - it("should return the specified service", async () => { - const garden = await makeTestGardenA() - const service = await garden.getService("service-b") - - expect(service.name).to.equal("service-b") - }) - - it("should throw if service is missing", async () => { - const garden = await makeTestGardenA() - - try { - await garden.getServices(["bla"]) - } catch (err) { - expect(err.type).to.equal("parameter") - return - } - - throw new Error("Expected error") - }) - }) - - describe("getTasks", () => { - it("should scan for modules and return all registered tasks in the context", async () => { - const garden = await makeTestGardenA() - const tasks = await garden.getTasks() - - expect(getNames(tasks).sort()).to.eql(["task-a", "task-b", "task-c"]) - }) - - it("should optionally return specified tasks in the context", async () => { - const garden = await makeTestGardenA() - const tasks = await garden.getTasks(["task-b", "task-c"]) - - expect(getNames(tasks).sort()).to.eql(["task-b", "task-c"]) - }) - - it("should throw if named task is missing", async () => { - const garden = await makeTestGardenA() - - try { - await garden.getTasks(["bla"]) - } catch (err) { - expect(err.type).to.equal("parameter") - return - } - - throw new Error("Expected error") - }) - }) - - describe("getTask", () => { - it("should return the specified task", async () => { - const garden = await makeTestGardenA() - const task = await garden.getTask("task-b") - - expect(task.name).to.equal("task-b") - }) - - it("should throw if task is missing", async () => { - const garden = await makeTestGardenA() - - try { - await garden.getTasks(["bla"]) - } catch (err) { - expect(err.type).to.equal("parameter") - return - } - - throw new Error("Expected error") - }) - }) - - describe("getServiceOrTask", () => { - it("should return the specified service or task", async () => { - const garden = await makeTestGardenA() - const service = await garden.getServiceOrTask("service-a") - const task = await garden.getServiceOrTask("task-a") - - expect(service.name).to.equal("service-a") - expect(task.name).to.equal("task-a") - }) - - it("should throw if no matching service or task was found", async () => { - const garden = await makeTestGardenA() - - try { - await garden.getServiceOrTask("bla") - } catch (err) { - expect(err.type).to.equal("parameter") - return - } - - throw new Error("Expected error") - }) - }) - describe("scanModules", () => { // TODO: assert that gitignore in project root is respected it("should scan the project root for modules and add to the context", async () => { const garden = await makeTestGardenA() await garden.scanModules() - const modules = await garden.getModules(undefined, true) + const modules = await garden.resolveModuleConfigs() expect(getNames(modules).sort()).to.eql(["module-a", "module-b", "module-c"]) }) @@ -324,7 +144,7 @@ describe("Garden", () => { await garden.scanModules() - const modules = await garden.getModules(undefined, true) + const modules = await garden.resolveModuleConfigs() expect(getNames(modules).sort()).to.eql(["module-a", "module-b", "module-c"]) }) @@ -338,85 +158,21 @@ describe("Garden", () => { ), ) }) - - it("should throw when two services have the same name", async () => { - const garden = await makeTestGarden(resolve(dataDir, "test-projects", "duplicate-service")) - - await expectError( - () => garden.scanModules(), - err => expect(err.message).to.equal( - "Service names must be unique - the service name 'dupe' is declared multiple times " + - "(in modules 'module-a' and 'module-b')", - ), - ) - }) - - it("should throw when two tasks have the same name", async () => { - const garden = await makeTestGarden(resolve(dataDir, "test-projects", "duplicate-task")) - - await expectError( - () => garden.scanModules(), - err => expect(err.message).to.equal( - "Task names must be unique - the task name 'dupe' is declared multiple times " + - "(in modules 'module-a' and 'module-b')", - ), - ) - }) - - it("should throw when a service and a task have the same name", async () => { - const garden = await makeTestGarden(resolve(dataDir, "test-projects", "duplicate-service-and-task")) - - await expectError( - () => garden.scanModules(), - err => expect(err.message).to.equal( - "Service and task names must be mutually unique - the name 'dupe' is used for a task " + - "in 'module-b' and for a service in 'module-a'", - ), - ) - }) - - it("should automatically add service source modules as build dependencies", async () => { - const garden = await makeTestGarden(resolve(dataDir, "test-projects", "source-module")) - - const module = await garden.getModule("module-b") - expect(module.build.dependencies).to.eql([{ name: "module-a", copy: [] }]) - }) }) - describe("resolveModule", () => { - it("should return named module", async () => { - const garden = await makeTestGardenA() - await garden.scanModules() - - const module = await garden.resolveModule("module-a") - expect(module!.name).to.equal("module-a") - }) - - it("should throw if named module is requested and not available", async () => { - const garden = await makeTestGardenA() - - try { - await garden.resolveModule("module-a") - } catch (err) { - expect(err.type).to.equal("configuration") - return - } - - throw new Error("Expected error") - }) - + describe("loadModuleConfig", () => { it("should resolve module by absolute path", async () => { const garden = await makeTestGardenA() const path = join(projectRootA, "module-a") - const module = await garden.resolveModule(path) + const module = await (garden).loadModuleConfig(path) expect(module!.name).to.equal("module-a") }) it("should resolve module by relative path to project root", async () => { const garden = await makeTestGardenA() - const module = await garden.resolveModule("./module-a") + const module = await (garden).loadModuleConfig("./module-a") expect(module!.name).to.equal("module-a") }) @@ -425,108 +181,19 @@ describe("Garden", () => { const garden = await makeTestGarden(projectRoot) stubGitCli() - const module = await garden.resolveModule("./module-a") + const module = await (garden).loadModuleConfig("./module-a") const repoUrlHash = hashRepoUrl(module!.repositoryUrl!) expect(module!.path).to.equal(join(projectRoot, ".garden", "sources", "module", `module-a--${repoUrlHash}`)) }) }) - describe("getActionHandlers", () => { - it("should return all handlers for a type", async () => { - const garden = await makeTestGardenA() - - const handlers = garden.getActionHandlers("prepareEnvironment") - - expect(Object.keys(handlers)).to.eql([ - "test-plugin", - "test-plugin-b", - ]) - }) - }) - - describe("getModuleActionHandlers", () => { - it("should return all handlers for a type", async () => { - const garden = await makeTestGardenA() - - const handlers = garden.getModuleActionHandlers({ actionType: "build", moduleType: "exec" }) - - expect(Object.keys(handlers)).to.eql([ - "exec", - ]) - }) - }) - - describe("getActionHandler", () => { - it("should return last configured handler for specified action type", async () => { - const garden = await makeTestGardenA() - - const handler = garden.getActionHandler({ actionType: "prepareEnvironment" }) - - expect(handler["actionType"]).to.equal("prepareEnvironment") - expect(handler["pluginName"]).to.equal("test-plugin-b") - }) - - it("should optionally filter to only handlers for the specified module type", async () => { - const garden = await makeTestGardenA() - - const handler = garden.getActionHandler({ actionType: "prepareEnvironment" }) - - expect(handler["actionType"]).to.equal("prepareEnvironment") - expect(handler["pluginName"]).to.equal("test-plugin-b") - }) - - it("should throw if no handler is available", async () => { - const garden = await makeTestGardenA() - await expectError(() => garden.getActionHandler({ actionType: "cleanupEnvironment" }), "parameter") - }) - }) - - describe("getModuleActionHandler", () => { - it("should return last configured handler for specified module action type", async () => { - const garden = await makeTestGardenA() - - const handler = garden.getModuleActionHandler({ actionType: "deployService", moduleType: "test" }) - - expect(handler["actionType"]).to.equal("deployService") - expect(handler["pluginName"]).to.equal("test-plugin-b") - }) - - it("should throw if no handler is available", async () => { - const garden = await makeTestGardenA() - await expectError( - () => garden.getModuleActionHandler({ actionType: "execInService", moduleType: "container" }), - "parameter", - ) - }) - }) - - describe("resolveModuleDependencies", () => { - it("should resolve build dependencies", async () => { - const garden = await makeTestGardenA() - const modules = await garden.resolveDependencyModules([{ name: "module-c", copy: [] }], []) - expect(getNames(modules)).to.eql(["module-a", "module-b", "module-c"]) - }) - - it("should resolve service dependencies", async () => { - const garden = await makeTestGardenA() - const modules = await garden.resolveDependencyModules([], ["service-b"]) - expect(getNames(modules)).to.eql(["module-a", "module-b"]) - }) - - it("should combine module and service dependencies", async () => { - const garden = await makeTestGardenA() - const modules = await garden.resolveDependencyModules([{ name: "module-b", copy: [] }], ["service-c"]) - expect(getNames(modules)).to.eql(["module-a", "module-b", "module-c"]) - }) - }) - describe("resolveVersion", () => { beforeEach(() => td.reset()) it("should return result from cache if available", async () => { const garden = await makeTestGardenA() - const module = await garden.getModule("module-a") + const module = await garden.resolveModuleConfig("module-a") const version: ModuleVersion = { versionString: "banana", dirtyTimestamp: 987654321, @@ -561,7 +228,7 @@ describe("Garden", () => { it("should ignore cache if force=true", async () => { const garden = await makeTestGardenA() - const module = await garden.getModule("module-a") + const module = await garden.resolveModuleConfig("module-a") const version: ModuleVersion = { versionString: "banana", dirtyTimestamp: 987654321, diff --git a/garden-service/test/src/plugins/container.ts b/garden-service/test/src/plugins/container.ts index 301d885c7b..ad4f924aea 100644 --- a/garden-service/test/src/plugins/container.ts +++ b/garden-service/test/src/plugins/container.ts @@ -71,7 +71,8 @@ describe("plugins.container", () => { async function getTestModule(moduleConfig: ContainerModuleConfig) { const parsed = await configure({ ctx, moduleConfig }) - return moduleFromConfig(garden, parsed) + const graph = await garden.getConfigGraph() + return moduleFromConfig(garden, graph, parsed) } describe("getLocalImageId", () => { diff --git a/garden-service/test/src/plugins/exec.ts b/garden-service/test/src/plugins/exec.ts index ed69e05193..127c228a50 100644 --- a/garden-service/test/src/plugins/exec.ts +++ b/garden-service/test/src/plugins/exec.ts @@ -5,6 +5,7 @@ import { gardenPlugin } from "../../../src/plugins/exec" import { GARDEN_BUILD_VERSION_FILENAME } from "../../../src/constants" import { LogEntry } from "../../../src/logger/log-entry" import { keyBy } from "lodash" +import { ConfigGraph } from "../../../src/config-graph" import { writeModuleVersionFile, readModuleVersionFile, @@ -19,16 +20,18 @@ describe("exec plugin", () => { const moduleName = "module-a" let garden: Garden + let graph: ConfigGraph let log: LogEntry beforeEach(async () => { garden = await makeTestGarden(projectRoot, { exec: gardenPlugin }) log = garden.log + graph = await garden.getConfigGraph() await garden.clearBuilds() }) it("should correctly parse exec modules", async () => { - const modules = keyBy(await garden.getModules(), "name") + const modules = keyBy(await graph.getModules(), "name") const { "module-a": moduleA, "module-b": moduleB, @@ -126,7 +129,7 @@ describe("exec plugin", () => { describe("getBuildStatus", () => { it("should read a build version file if it exists", async () => { - const module = await garden.getModule(moduleName) + const module = await graph.getModule(moduleName) const version = module.version const buildPath = module.buildPath const versionFilePath = join(buildPath, GARDEN_BUILD_VERSION_FILENAME) @@ -141,7 +144,7 @@ describe("exec plugin", () => { describe("build", () => { it("should write a build version file after building", async () => { - const module = await garden.getModule(moduleName) + const module = await graph.getModule(moduleName) const version = module.version const buildPath = module.buildPath const versionFilePath = join(buildPath, GARDEN_BUILD_VERSION_FILENAME) diff --git a/garden-service/test/src/plugins/kubernetes/container/ingress.ts b/garden-service/test/src/plugins/kubernetes/container/ingress.ts index c8147118a8..987c65e35f 100644 --- a/garden-service/test/src/plugins/kubernetes/container/ingress.ts +++ b/garden-service/test/src/plugins/kubernetes/container/ingress.ts @@ -356,7 +356,8 @@ describe("createIngresses", () => { const ctx = await garden.getPluginContext("container") const parsed = await configure({ ctx, moduleConfig }) - const module = await moduleFromConfig(garden, parsed) + const graph = await garden.getConfigGraph() + const module = await moduleFromConfig(garden, graph, parsed) return { name: spec.name, diff --git a/garden-service/test/src/plugins/kubernetes/helm/common.ts b/garden-service/test/src/plugins/kubernetes/helm/common.ts index dce638d10a..e7eef849b4 100644 --- a/garden-service/test/src/plugins/kubernetes/helm/common.ts +++ b/garden-service/test/src/plugins/kubernetes/helm/common.ts @@ -20,15 +20,18 @@ import { find } from "lodash" import { deline } from "../../../../../src/util/string" import { HotReloadableResource } from "../../../../../src/plugins/kubernetes/hot-reload" import { getServiceResourceSpec } from "../../../../../src/plugins/kubernetes/helm/common" +import { ConfigGraph } from "../../../../../src/config-graph" describe("Helm common functions", () => { let garden: TestGarden + let graph: ConfigGraph let ctx: PluginContext let log: LogEntry before(async () => { const projectRoot = resolve(dataDir, "test-projects", "helm") garden = await makeTestGarden(projectRoot) + graph = await garden.getConfigGraph() ctx = garden.getPluginContext("local-kubernetes") log = garden.log await buildModules() @@ -39,7 +42,7 @@ describe("Helm common functions", () => { }) async function buildModules() { - const modules = await garden.getModules() + const modules = await graph.getModules() for (const module of modules) { await garden.addTask(new BuildTask({ garden, log, module, force: false })) } @@ -54,20 +57,20 @@ describe("Helm common functions", () => { describe("containsSource", () => { it("should return true if the specified module contains chart sources", async () => { - const module = await garden.getModule("api") + const module = await graph.getModule("api") expect(await containsSource(module)).to.be.true }) it("should return false if the specified module does not contain chart sources", async () => { - const module = await garden.getModule("postgres") + const module = await graph.getModule("postgres") expect(await containsSource(module)).to.be.false }) }) describe("getChartResources", () => { it("should render and return resources for a local template", async () => { - const module = await garden.getModule("api") - const imageModule = await garden.getModule("api-image") + const module = await graph.getModule("api") + const imageModule = await graph.getModule("api-image") const resources = await getChartResources(ctx, module, log) expect(resources).to.eql([ @@ -188,7 +191,7 @@ describe("Helm common functions", () => { }) it("should render and return resources for a remote template", async () => { - const module = await garden.getModule("postgres") + const module = await graph.getModule("postgres") const resources = await getChartResources(ctx, module, log) expect(resources).to.eql([ @@ -434,14 +437,14 @@ describe("Helm common functions", () => { describe("getBaseModule", () => { it("should return undefined if no base module is specified", async () => { - const module = await garden.getModule("api") + const module = await graph.getModule("api") expect(await getBaseModule(module)).to.be.undefined }) it("should return the resolved base module if specified", async () => { - const module = await garden.getModule("api") - const baseModule = await garden.getModule("postgres") + const module = await graph.getModule("api") + const baseModule = await graph.getModule("postgres") module.spec.base = baseModule.name module.buildDependencies = { postgres: baseModule } @@ -450,7 +453,7 @@ describe("Helm common functions", () => { }) it("should throw if the base module isn't in the build dependency map", async () => { - const module = await garden.getModule("api") + const module = await graph.getModule("api") module.spec.base = "postgres" @@ -464,8 +467,8 @@ describe("Helm common functions", () => { }) it("should throw if the base module isn't a Helm module", async () => { - const module = await garden.getModule("api") - const baseModule = await garden.getModule("postgres") + const module = await graph.getModule("api") + const baseModule = await graph.getModule("postgres") baseModule.type = "foo" @@ -485,7 +488,7 @@ describe("Helm common functions", () => { describe("getChartPath", () => { context("module has chart sources", () => { it("should return the chart path in the build directory", async () => { - const module = await garden.getModule("api") + const module = await graph.getModule("api") expect(await getChartPath(module)).to.equal( resolve(ctx.projectRoot, ".garden", "build", "api"), ) @@ -494,7 +497,7 @@ describe("Helm common functions", () => { context("module references remote chart", () => { it("should construct the chart path based on the chart name", async () => { - const module = await garden.getModule("postgres") + const module = await graph.getModule("postgres") expect(await getChartPath(module)).to.equal( resolve(ctx.projectRoot, ".garden", "build", "postgres", "postgresql"), ) @@ -510,26 +513,26 @@ describe("Helm common functions", () => { describe("getReleaseName", () => { it("should return the module name if not overridden in config", async () => { - const module = await garden.getModule("api") + const module = await graph.getModule("api") delete module.spec.releaseName expect(getReleaseName(module)).to.equal("api") }) it("should return the configured release name if any", async () => { - const module = await garden.getModule("api") + const module = await graph.getModule("api") expect(getReleaseName(module)).to.equal("api-release") }) }) describe("getServiceResourceSpec", () => { it("should return the spec on the given module if it has no base module", async () => { - const module = await garden.getModule("api") + const module = await graph.getModule("api") expect(await getServiceResourceSpec(module)).to.eql(module.spec.serviceResource) }) it("should return the spec on the base module if there is none on the module", async () => { - const module = await garden.getModule("api") - const baseModule = await garden.getModule("postgres") + const module = await graph.getModule("api") + const baseModule = await graph.getModule("postgres") module.spec.base = "postgres" delete module.spec.serviceResource module.buildDependencies = { postgres: baseModule } @@ -537,8 +540,8 @@ describe("Helm common functions", () => { }) it("should merge the specs if both module and base have specs", async () => { - const module = await garden.getModule("api") - const baseModule = await garden.getModule("postgres") + const module = await graph.getModule("api") + const baseModule = await graph.getModule("postgres") module.spec.base = "postgres" module.buildDependencies = { postgres: baseModule } expect(await getServiceResourceSpec(module)).to.eql({ @@ -549,7 +552,7 @@ describe("Helm common functions", () => { }) it("should throw if there is no base module and the module has no serviceResource spec", async () => { - const module = await garden.getModule("api") + const module = await graph.getModule("api") delete module.spec.serviceResource await expectError( () => getServiceResourceSpec(module), @@ -562,8 +565,8 @@ describe("Helm common functions", () => { }) it("should throw if there is a base module but neither module has a spec", async () => { - const module = await garden.getModule("api") - const baseModule = await garden.getModule("postgres") + const module = await graph.getModule("api") + const baseModule = await graph.getModule("postgres") module.spec.base = "postgres" module.buildDependencies = { postgres: baseModule } delete module.spec.serviceResource @@ -581,7 +584,7 @@ describe("Helm common functions", () => { describe("findServiceResource", () => { it("should return the resource specified by serviceResource", async () => { - const module = await garden.getModule("api") + const module = await graph.getModule("api") const chartResources = await getChartResources(ctx, module, log) const result = await findServiceResource({ ctx, log, module, chartResources }) const expected = find(chartResources, r => r.kind === "Deployment") @@ -589,7 +592,7 @@ describe("Helm common functions", () => { }) it("should throw if no resourceSpec or serviceResource is specified", async () => { - const module = await garden.getModule("api") + const module = await graph.getModule("api") const chartResources = await getChartResources(ctx, module, log) delete module.spec.serviceResource await expectError( @@ -603,7 +606,7 @@ describe("Helm common functions", () => { }) it("should throw if no resource of the specified kind is in the chart", async () => { - const module = await garden.getModule("api") + const module = await graph.getModule("api") const chartResources = await getChartResources(ctx, module, log) const resourceSpec = { ...module.spec.serviceResource, @@ -616,7 +619,7 @@ describe("Helm common functions", () => { }) it("should throw if matching resource is not found by name", async () => { - const module = await garden.getModule("api") + const module = await graph.getModule("api") const chartResources = await getChartResources(ctx, module, log) const resourceSpec = { ...module.spec.serviceResource, @@ -629,7 +632,7 @@ describe("Helm common functions", () => { }) it("should throw if no name is specified and multiple resources are matched", async () => { - const module = await garden.getModule("api") + const module = await graph.getModule("api") const chartResources = await getChartResources(ctx, module, log) const deployment = find(chartResources, r => r.kind === "Deployment") chartResources.push(deployment!) @@ -644,7 +647,7 @@ describe("Helm common functions", () => { }) it("should resolve template string for resource name", async () => { - const module = await garden.getModule("postgres") + const module = await graph.getModule("postgres") const chartResources = await getChartResources(ctx, module, log) module.spec.serviceResource.name = `{{ template "postgresql.master.fullname" . }}` const result = await findServiceResource({ ctx, log, module, chartResources }) @@ -655,7 +658,7 @@ describe("Helm common functions", () => { describe("getResourceContainer", () => { async function getDeployment() { - const module = await garden.getModule("api") + const module = await graph.getModule("api") const chartResources = await getChartResources(ctx, module, log) return find(chartResources, r => r.kind === "Deployment")! } diff --git a/garden-service/test/src/plugins/kubernetes/helm/config.ts b/garden-service/test/src/plugins/kubernetes/helm/config.ts index 01dbaf2224..0c891e3c55 100644 --- a/garden-service/test/src/plugins/kubernetes/helm/config.ts +++ b/garden-service/test/src/plugins/kubernetes/helm/config.ts @@ -15,7 +15,7 @@ describe("validateHelmModule", () => { const projectRoot = resolve(dataDir, "test-projects", "helm") garden = await makeTestGarden(projectRoot) ctx = garden.getPluginContext("local-kubernetes") - await garden.getModules() + await garden.resolveModuleConfigs() }) after(async () => { @@ -31,20 +31,15 @@ describe("validateHelmModule", () => { } it("should validate a Helm module", async () => { - const moduleConfig = getModuleConfig("api") - const config = await validateHelmModule({ ctx, moduleConfig }) - const imageModule = await garden.getModule("api-image") + const config = await garden.resolveModuleConfig("api") + const graph = await garden.getConfigGraph() + const imageModule = await graph.getModule("api-image") const { versionString } = imageModule.version expect(config).to.eql({ allowPublish: true, build: { - dependencies: [ - { - name: "api-image", - copy: [], - }, - ], + dependencies: [], command: [], }, description: "The API backend for the voting UI", @@ -157,7 +152,6 @@ describe("validateHelmModule", () => { const config = await validateHelmModule({ ctx, moduleConfig }) expect(config.build.dependencies).to.eql([ - { name: "api-image", copy: [] }, { name: "foo", copy: [] }, ]) }) @@ -170,7 +164,6 @@ describe("validateHelmModule", () => { const config = await validateHelmModule({ ctx, moduleConfig }) expect(config.build.dependencies).to.eql([ - { name: "api-image", copy: [] }, { name: "foo", copy: [] }, ]) }) diff --git a/garden-service/test/src/plugins/kubernetes/helm/hot-reload.ts b/garden-service/test/src/plugins/kubernetes/helm/hot-reload.ts index 594784cf42..b79c4d4db3 100644 --- a/garden-service/test/src/plugins/kubernetes/helm/hot-reload.ts +++ b/garden-service/test/src/plugins/kubernetes/helm/hot-reload.ts @@ -4,14 +4,16 @@ import { expect } from "chai" import { dataDir, makeTestGarden, TestGarden, expectError } from "../../../../helpers" import { getHotReloadSpec } from "../../../../../src/plugins/kubernetes/helm/hot-reload" import { deline } from "../../../../../src/util/string" +import { ConfigGraph } from "../../../../../src/config-graph" describe("getHotReloadSpec", () => { let garden: TestGarden + let graph: ConfigGraph before(async () => { const projectRoot = resolve(dataDir, "test-projects", "helm") garden = await makeTestGarden(projectRoot) - await garden.getModules() + graph = await garden.getConfigGraph() }) after(async () => { @@ -19,7 +21,7 @@ describe("getHotReloadSpec", () => { }) it("should retrieve the hot reload spec on the service's source module", async () => { - const service = await garden.getService("api") + const service = await graph.getService("api") expect(getHotReloadSpec(service)).to.eql({ sync: [{ source: "*", @@ -29,7 +31,7 @@ describe("getHotReloadSpec", () => { }) it("should throw if the module doesn't specify serviceResource.containerModule", async () => { - const service = await garden.getService("api") + const service = await graph.getService("api") delete service.module.spec.serviceResource.containerModule await expectError( () => getHotReloadSpec(service), @@ -40,8 +42,8 @@ describe("getHotReloadSpec", () => { }) it("should throw if the referenced module is not a container module", async () => { - const service = await garden.getService("api") - const otherModule = await garden.getModule("postgres") + const service = await graph.getService("api") + const otherModule = await graph.getModule("postgres") service.sourceModule = otherModule await expectError( () => getHotReloadSpec(service), @@ -54,7 +56,7 @@ describe("getHotReloadSpec", () => { }) it("should throw if the referenced module is not configured for hot reloading", async () => { - const service = await garden.getService("api") + const service = await graph.getService("api") delete service.sourceModule.spec.hotReload await expectError( () => getHotReloadSpec(service), diff --git a/garden-service/test/src/task-graph.ts b/garden-service/test/src/task-graph.ts index 86a39e4fea..fe78637833 100644 --- a/garden-service/test/src/task-graph.ts +++ b/garden-service/test/src/task-graph.ts @@ -8,7 +8,7 @@ import { } from "../../src/task-graph" import { makeTestGarden, freezeTime } from "../helpers" import { Garden } from "../../src/garden" -import { DependencyGraphNodeType } from "../../src/dependency-graph" +import { DependencyGraphNodeType } from "../../src/config-graph" const projectRoot = join(__dirname, "..", "data", "test-project-empty") diff --git a/garden-service/test/src/tasks/helpers.ts b/garden-service/test/src/tasks/helpers.ts index bef2b488cb..bb1492a81d 100644 --- a/garden-service/test/src/tasks/helpers.ts +++ b/garden-service/test/src/tasks/helpers.ts @@ -7,6 +7,7 @@ import { makeTestGarden, dataDir } from "../../helpers" import { getDependantTasksForModule } from "../../../src/tasks/helpers" import { BaseTask } from "../../../src/tasks/base" import { LogEntry } from "../../../src/logger/log-entry" +import { ConfigGraph } from "../../../src/config-graph" async function sortedBaseKeysdependencyTasks(tasks: BaseTask[]): Promise { const dependencies = await Bluebird.map(tasks, async (t) => t.getDependencies(), { concurrency: 1 }) @@ -20,10 +21,12 @@ function sortedBaseKeys(tasks: BaseTask[]): string[] { describe("TaskHelpers", () => { let garden: Garden + let graph: ConfigGraph let log: LogEntry before(async () => { garden = await makeTestGarden(resolve(dataDir, "test-project-dependants")) + graph = await garden.getConfigGraph() log = garden.log }) @@ -34,11 +37,11 @@ describe("TaskHelpers", () => { describe("getDependantTasksForModule", () => { it("returns the correct set of tasks for the changed module", async () => { - const module = await garden.getModule("good-morning") - await garden.getDependencyGraph() + const module = await graph.getModule("good-morning") + await garden.getConfigGraph() const tasks = await getDependantTasksForModule({ - garden, log, module, hotReloadServiceNames: [], force: true, forceBuild: true, + garden, graph, log, module, hotReloadServiceNames: [], force: true, forceBuild: true, fromWatch: false, includeDependants: false, }) @@ -142,9 +145,9 @@ describe("TaskHelpers", () => { for (const { moduleName, expected, dependencyTasks } of expectedBaseKeysByChangedModule) { it(`returns the correct set of tasks for ${moduleName} and its dependants`, async () => { - const module = await garden.getModule(moduleName) + const module = await graph.getModule(moduleName) const tasks = await getDependantTasksForModule({ - garden, log, module, hotReloadServiceNames: [], force: true, forceBuild: true, + garden, graph, log, module, hotReloadServiceNames: [], force: true, forceBuild: true, fromWatch: true, includeDependants: true, }) expect(sortedBaseKeys(tasks)).to.eql(expected.sort()) @@ -213,9 +216,9 @@ describe("TaskHelpers", () => { for (const { moduleName, expected, dependencyTasks } of expectedBaseKeysByChangedModule) { it(`returns the correct set of tasks for ${moduleName} and its dependants`, async () => { - const module = await garden.getModule(moduleName) + const module = await graph.getModule(moduleName) const tasks = await getDependantTasksForModule({ - garden, log, module, hotReloadServiceNames: ["good-morning"], force: true, forceBuild: true, + garden, graph, log, module, hotReloadServiceNames: ["good-morning"], force: true, forceBuild: true, fromWatch: true, includeDependants: true, }) expect(sortedBaseKeys(tasks)).to.eql(expected.sort()) diff --git a/garden-service/test/src/tasks/test.ts b/garden-service/test/src/tasks/test.ts index 038f6198b2..1021c846f0 100644 --- a/garden-service/test/src/tasks/test.ts +++ b/garden-service/test/src/tasks/test.ts @@ -5,13 +5,16 @@ import * as td from "testdouble" import { Garden } from "../../../src/garden" import { dataDir, makeTestGarden } from "../../helpers" import { LogEntry } from "../../../src/logger/log-entry" +import { ConfigGraph } from "../../../src/config-graph" describe("TestTask", () => { let garden: Garden + let graph: ConfigGraph let log: LogEntry beforeEach(async () => { garden = await makeTestGarden(resolve(dataDir, "test-project-test-deps")) + graph = await garden.getConfigGraph() log = garden.log }) @@ -31,15 +34,16 @@ describe("TestTask", () => { }, } - const moduleB = await garden.getModule("module-b") + const moduleB = await graph.getModule("module-b") td.when(resolveVersion("module-a", [moduleB])).thenResolve(version) - const moduleA = await garden.getModule("module-a") + const moduleA = await graph.getModule("module-a") const testConfig = moduleA.testConfigs[0] const task = await TestTask.factory({ garden, + graph, log, module: moduleA, testConfig, diff --git a/garden-service/test/src/util/validate-dependencies.ts b/garden-service/test/src/util/validate-dependencies.ts index 6295536f3a..60ff2f2e06 100644 --- a/garden-service/test/src/util/validate-dependencies.ts +++ b/garden-service/test/src/util/validate-dependencies.ts @@ -8,6 +8,8 @@ import { import { makeTestGarden, dataDir } from "../../helpers" import { ModuleConfig } from "../../../src/config/module" import { ConfigurationError } from "../../../src/exceptions" +import { Garden } from "../../../src/garden" +import { flatten } from "lodash" /** * Here, we cast the garden arg to any in order to access the private moduleConfigs property. @@ -16,16 +18,15 @@ import { ConfigurationError } from "../../../src/exceptions" * test the validation methods below (which normally throw their exceptions during the * execution of scanModules). */ -async function scanAndGetConfigs(garden: any) { - try { - await garden.scanModules() - } finally { - const moduleConfigs: ModuleConfig[] = Object.values(garden.moduleConfigs) - return { - moduleConfigs, - serviceNames: Object.keys(garden.serviceNameIndex), - taskNames: Object.keys(garden.taskNameIndex), - } +async function scanAndGetConfigs(garden: Garden) { + const moduleConfigs: ModuleConfig[] = await garden.resolveModuleConfigs() + const serviceNames = flatten(moduleConfigs.map(m => m.serviceConfigs.map(s => s.name))) + const taskNames = flatten(moduleConfigs.map(m => m.taskConfigs.map(s => s.name))) + + return { + moduleConfigs, + serviceNames, + taskNames, } } diff --git a/garden-service/test/src/vcs/base.ts b/garden-service/test/src/vcs/base.ts index 0de5ce5a0a..b84917aad3 100644 --- a/garden-service/test/src/vcs/base.ts +++ b/garden-service/test/src/vcs/base.ts @@ -44,7 +44,7 @@ describe("VcsHandler", () => { describe("resolveTreeVersion", () => { it("should return the version from a version file if it exists", async () => { - const module = await garden.getModule("module-a") + const module = await garden.resolveModuleConfig("module-a") const result = await handler.resolveTreeVersion(module.path) expect(result).to.eql({ @@ -54,7 +54,7 @@ describe("VcsHandler", () => { }) it("should call getTreeVersion if there is no version file", async () => { - const module = await garden.getModule("module-b") + const module = await garden.resolveModuleConfig("module-b") const version = { latestCommit: "qwerty", @@ -69,7 +69,7 @@ describe("VcsHandler", () => { describe("resolveVersion", () => { it("should return module version if there are no dependencies", async () => { - const module = await garden.getModule("module-a") + const module = await garden.resolveModuleConfig("module-a") const result = await handler.resolveVersion(module, []) @@ -81,7 +81,7 @@ describe("VcsHandler", () => { }) it("should return module version if there are no dependencies and properly handle a dirty timestamp", async () => { - const module = await garden.getModule("module-b") + const module = await garden.resolveModuleConfig("module-b") const latestCommit = "abcdef" const version = { latestCommit, @@ -100,7 +100,7 @@ describe("VcsHandler", () => { }) it("should return the dirty version if there is a single one", async () => { - const [moduleA, moduleB, moduleC] = await garden.getModules(["module-a", "module-b", "module-c"]) + const [moduleA, moduleB, moduleC] = await garden.resolveModuleConfigs(["module-a", "module-b", "module-c"]) const versionB = { latestCommit: "qwerty", @@ -126,7 +126,7 @@ describe("VcsHandler", () => { }) it("should return the latest dirty version if there are multiple", async () => { - const [moduleA, moduleB, moduleC] = await garden.getModules(["module-a", "module-b", "module-c"]) + const [moduleA, moduleB, moduleC] = await garden.resolveModuleConfigs(["module-a", "module-b", "module-c"]) const versionB = { latestCommit: "qwerty", @@ -152,7 +152,7 @@ describe("VcsHandler", () => { }) it("should hash together the version of the module and all dependencies if none are dirty", async () => { - const [moduleA, moduleB, moduleC] = await garden.getModules(["module-a", "module-b", "module-c"]) + const [moduleA, moduleB, moduleC] = await garden.resolveModuleConfigs(["module-a", "module-b", "module-c"]) const versionStringB = "qwerty" const versionB = { @@ -182,7 +182,7 @@ describe("VcsHandler", () => { "should hash together the dirty versions and add the timestamp if there are multiple with same timestamp", async () => { - const [moduleA, moduleB, moduleC] = await garden.getModules(["module-a", "module-b", "module-c"]) + const [moduleA, moduleB, moduleC] = await garden.resolveModuleConfigs(["module-a", "module-b", "module-c"]) const versionStringB = "qwerty" const versionB = { diff --git a/garden-service/test/src/watch.ts b/garden-service/test/src/watch.ts index 7f7b7ec9f6..c0b8cf7797 100644 --- a/garden-service/test/src/watch.ts +++ b/garden-service/test/src/watch.ts @@ -13,7 +13,7 @@ describe("Watcher", () => { garden = await makeTestGarden(resolve(dataDir, "test-project-watch")) modulePath = resolve(garden.projectRoot, "module-a") moduleContext = pathToCacheContext(modulePath) - await garden.startWatcher() + await garden.startWatcher(await garden.getConfigGraph()) }) beforeEach(async () => {