diff --git a/docs/examples/hello-world.md b/docs/examples/hello-world.md index 244dbd1d61..4e07d75867 100644 --- a/docs/examples/hello-world.md +++ b/docs/examples/hello-world.md @@ -48,12 +48,19 @@ module: name: hello-container services: ... - env: - FUNCTION_ENDPOINT: ${services.hello-function.outputs.endpoint} dependencies: - hello-function + env: + FUNCTION_ENDPOINT: ${modules.hello-function.outputs.endpoint} ``` +Note the added `dependencies` key, which specified `hello-function` as a runtime dependency for the `hello-container` +service. + +Here we also reference the module outputs from the OpenFaaS module using a templated string, in order to inform the +container service where the endpoint for the function is. Please refer to individual provider docs on which values +are exposed under the `outputs` key on different module types. + Test dependencies will be covered further ahead. # Defining ports, endpoints, and health checks diff --git a/docs/reference/module-types/container.md b/docs/reference/module-types/container.md index ba4a19ba8e..59a73dd636 100644 --- a/docs/reference/module-types/container.md +++ b/docs/reference/module-types/container.md @@ -113,14 +113,6 @@ The names of any services that this service depends on at runtime, and the names | Type | Required | | ---- | -------- | | `array[string]` | No -### `module.services[].outputs` -[module](#module) > [services](#module.services[]) > outputs - -Key/value map. Keys must be valid identifiers. - -| Type | Required | -| ---- | -------- | -| `object` | No ### `module.services[].args[]` [module](#module) > [services](#module.services[]) > args @@ -493,7 +485,6 @@ module: services: - name: dependencies: [] - outputs: {} args: daemon: false ingresses: diff --git a/docs/reference/template-strings.md b/docs/reference/template-strings.md index 1721c53341..58eee616c4 100644 --- a/docs/reference/template-strings.md +++ b/docs/reference/template-strings.md @@ -100,17 +100,6 @@ environment: modules: {} -# Retrieve information about services that are defined in the project. -# -# Example: -# my-service: -# outputs: -# ingress: 'http://my-service/path/to/endpoint' -# version: v17ad4cb3fd -# -services: - {} - # A map of all configured plugins/providers for this environment and their configuration. # # Example: diff --git a/examples/hello-world/services/hello-container/garden.yml b/examples/hello-world/services/hello-container/garden.yml index 6de9abca8d..3eef9eccfa 100644 --- a/examples/hello-world/services/hello-container/garden.yml +++ b/examples/hello-world/services/hello-container/garden.yml @@ -18,7 +18,7 @@ module: dependencies: - hello-function env: - FUNCTION_ENDPOINT: ${services.hello-function.outputs.endpoint} + FUNCTION_ENDPOINT: ${modules.hello-function.outputs.endpoint} build: dependencies: - name: hello-npm-package @@ -31,6 +31,6 @@ module: - name: integ args: [npm, run, integ] env: - FUNCTION_ENDPOINT: ${services.hello-function.outputs.endpoint} + FUNCTION_ENDPOINT: ${modules.hello-function.outputs.endpoint} dependencies: - hello-function \ No newline at end of file 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 dccbbe3093..579fde5510 100644 --- a/garden-service/src/actions.ts +++ b/garden-service/src/actions.ts @@ -8,10 +8,19 @@ import Bluebird = require("bluebird") import chalk from "chalk" -import { Garden } from "./garden" -import { PrimitiveMap } from "./config/common" +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, @@ -41,7 +50,6 @@ import { GetSecretParams, GetBuildStatusParams, GetServiceLogsParams, - GetServiceOutputsParams, GetServiceStatusParams, GetTestResultParams, ModuleActionParams, @@ -68,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 @@ -103,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 @@ -122,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", @@ -143,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 @@ -194,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 }) } @@ -295,14 +310,6 @@ export class ActionHelper implements TypeGuard { }) } - async getServiceOutputs(params: ServiceActionHelperParams): Promise { - return this.callServiceHandler({ - params, - actionType: "getServiceOutputs", - defaultHandler: async () => ({}), - }) - } - async execInService(params: ServiceActionHelperParams): Promise { return this.callServiceHandler({ params, actionType: "execInService" }) } @@ -333,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, log, 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 }) @@ -352,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, @@ -390,7 +401,7 @@ export class ActionHelper implements TypeGuard { defaultHandler?: PluginActions[T], }, ): Promise { - const handler = this.garden.getActionHandler({ + const handler = this.getActionHandler({ actionType, pluginName, defaultHandler, @@ -408,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, @@ -428,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, log, module, deps)) - const handlerParams: any = { ...this.commonParams(handler, log), ...params, @@ -463,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, @@ -479,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 18306f79fd..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, log, 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 e1a7cf7ad9..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, log, 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 c3e7ed51a4..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, log, 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/base.ts b/garden-service/src/config/base.ts index f9e13d2356..f21a928db0 100644 --- a/garden-service/src/config/base.ts +++ b/garden-service/src/config/base.ts @@ -123,6 +123,7 @@ export async function loadConfig(projectRoot: string, path: string): Promise Promise> - @schema( - joiIdentifierMap(ServiceContext.getSchema()) - .description("Retrieve information about services that are defined in the project.") - .example({ "my-service": { outputs: exampleOutputs, version: exampleVersion } }), - ) - public services: Map Promise> - @schema( joiIdentifierMap(providerConfigBaseSchema) .description("A map of all configured plugins/providers for this environment and their configuration.") @@ -266,12 +245,6 @@ export class ModuleConfigContext extends ProjectConfigContext { ) public providers: Map - // NOTE: This has some negative performance implications and may not be something we want to support, - // so I'm disabling this feature for now. - // - // @description("Use this to look up values that are configured in the current environment.") - // public config: RemoteConfigContext - @schema( joiIdentifierMap(joiPrimitive()) .description("A map of all variables defined in the project configuration.") @@ -281,7 +254,6 @@ export class ModuleConfigContext extends ProjectConfigContext { constructor( garden: Garden, - log: LogEntry, environment: Environment, moduleConfigs: ModuleConfig[], ) { @@ -293,39 +265,17 @@ export class ModuleConfigContext extends ProjectConfigContext { this.modules = new Map(moduleConfigs.map((config) => <[string, () => Promise]>[config.name, async () => { - const module = await garden.getModule(config.name) - return new ModuleContext(_this, module) - }], - )) - - const serviceNames = flatten(moduleConfigs.map(m => m.serviceConfigs)).map(s => s.name) + // NOTE: This is a temporary hacky solution until we implement module resolution as a TaskGraph task + 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) - this.services = new Map(serviceNames.map((name) => - <[string, () => Promise]>[name, async () => { - const service = await garden.getService(name) - const outputs = { - ...service.config.outputs, - ...await garden.actions.getServiceOutputs({ log, service }), - } - return new ServiceContext(_this, service, outputs) + 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/config/module.ts b/garden-service/src/config/module.ts index 44a8c4de08..e161ee8488 100644 --- a/garden-service/src/config/module.ts +++ b/garden-service/src/config/module.ts @@ -14,6 +14,9 @@ import { joiIdentifier, joiRepositoryUrl, joiUserIdentifier, + PrimitiveMap, + joiIdentifierMap, + joiPrimitive, } from "./common" import { TestConfig, TestSpec } from "./test" import { TaskConfig, TaskSpec } from "./task" @@ -36,6 +39,8 @@ const copySchema = Joi.object() ), }) +export const moduleOutputsSchema = joiIdentifierMap(joiPrimitive()) + export interface BuildDependencyConfig { name: string plugin?: string @@ -121,6 +126,7 @@ export interface ModuleConfig plugin?: string // used to identify modules that are bundled as part of a plugin + outputs: PrimitiveMap serviceConfigs: ServiceConfig[] testConfigs: TestConfig[] taskConfigs: TaskConfig[] diff --git a/garden-service/src/config/service.ts b/garden-service/src/config/service.ts index 510c528a77..02fa20d1a5 100644 --- a/garden-service/src/config/service.ts +++ b/garden-service/src/config/service.ts @@ -32,7 +32,6 @@ export const baseServiceSchema = Joi.object() The names of any services that this service depends on at runtime, and the names of any tasks that should be executed before this service is deployed. `), - outputs: serviceOutputsSchema, }) .unknown(true) .meta({ extendable: true }) diff --git a/garden-service/src/garden.ts b/garden-service/src/garden.ts index 2a1d998e6f..0f976c1bbd 100644 --- a/garden-service/src/garden.ts +++ b/garden-service/src/garden.ts @@ -7,59 +7,39 @@ */ import Bluebird = require("bluebird") -import deline = require("deline") import { parse, relative, resolve, sep, + join, } from "path" 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, @@ -69,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, @@ -96,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" @@ -145,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 @@ -202,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) @@ -352,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) } @@ -448,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, @@ -456,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 || {} @@ -471,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) } } @@ -502,7 +457,7 @@ export class Garden { } } - private getPlugin(pluginName: string) { + getPlugin(pluginName: string) { const plugin = this.loadedPlugins[pluginName] if (!plugin) { @@ -515,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 }) + + // FIXME: We should be able to avoid this + config.name = getModuleKey(config.name, config.plugin) - return Bluebird.map(configs, config => moduleFromConfig(this, config)) + 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) } /** @@ -663,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. */ @@ -827,148 +598,59 @@ export class Garden { return paths })).filter(Boolean) + const rawConfigs: ModuleConfig[] = [...this.pluginModuleConfigs] + await Bluebird.map(modulePaths, async path => { - const config = await this.resolveModule(path) - config && await this.addModule(config) + const config = await this.loadModuleConfig(path) + if (config) { + rawConfigs.push(config) + } }) - this.modulesScanned = true - - const moduleConfigContext = new ModuleConfigContext( - this, this.log, this.environment, Object.values(this.moduleConfigs), - ) + for (const config of rawConfigs) { + this.addModule(config) + } - this.moduleConfigs = await resolveTemplateStrings(this.moduleConfigs, moduleConfigContext) - 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. + */ + hasModule(name: string) { + return !!this.moduleConfigs[name] } - /* - Adds the specified module 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, force = false) { - const validateHandler = await this.getModuleActionHandler({ actionType: "configure", moduleType: config.type }) - const ctx = this.getPluginContext(validateHandler["pluginName"]) - - config = await validateHandler({ ctx, moduleConfig: config }) + private addModule(config: ModuleConfig) { + const key = getModuleKey(config.name, config.plugin) - // FIXME: this is rather clumsy - config.name = getModuleKey(config.name, config.plugin) - - if (!force && this.moduleConfigs[config.name]) { - const pathA = relative(this.projectRoot, this.moduleConfigs[config.name].path) - const pathB = relative(this.projectRoot, config.path) + if (this.moduleConfigs[key]) { + const [pathA, pathB] = [ + 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 ('${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: [] }) - } - } - - this.moduleConfigs[config.name] = config - - // Add to service-module map - for (const serviceConfig of config.serviceConfigs) { - const serviceName = serviceConfig.name - - if (!force && this.serviceNameIndex[serviceName]) { - throw new ConfigurationError(deline` - Service names must be unique - the service name ${serviceName} is declared multiple times - (in '${this.serviceNameIndex[serviceName]}' and '${config.name}')`, - { - serviceName, - moduleA: this.serviceNameIndex[serviceName], - moduleB: config.name, - }, - ) - } - - this.serviceNameIndex[serviceName] = config.name - } - - // Add to task-module map - for (const taskConfig of config.taskConfigs) { - const taskName = taskConfig.name - - if (!force) { - - if (this.serviceNameIndex[taskName]) { - throw new ConfigurationError(deline` - Service and task names must be mutually unique - the task name ${taskName} (declared in - '${config.name}') is also declared as a service name in '${this.serviceNameIndex[taskName]}'`, - { - conflictingName: taskName, - moduleA: config.name, - moduleB: this.serviceNameIndex[taskName], - }) - } - - if (this.taskNameIndex[taskName]) { - throw new ConfigurationError(deline` - Task names must be unique - the task name ${taskName} is declared multiple times (in - '${this.taskNameIndex[taskName]}' and '${config.name}')`, - { - taskName, - moduleA: config.name, - moduleB: this.serviceNameIndex[taskName], - }) - } - - } - - this.taskNameIndex[taskName] = config.name - - } - - if (this.modulesScanned) { - // need to re-run this if adding modules after initial scan - await this.validateDependencies() - } + 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 @@ -1013,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"), } } @@ -1150,5 +714,5 @@ export interface ConfigDump { environmentName: string providers: Provider[] variables: PrimitiveMap - modules: Module[] + modules: ModuleConfig[] } diff --git a/garden-service/src/plugins/container/config.ts b/garden-service/src/plugins/container/config.ts index 4388b16f4a..9db644b21f 100644 --- a/garden-service/src/plugins/container/config.ts +++ b/garden-service/src/plugins/container/config.ts @@ -168,7 +168,6 @@ export const portSchema = Joi.object() Set this to expose the service on the specified port on the host node (may not be supported by all providers).`), }) - .required() const volumeSchema = Joi.object() .keys({ diff --git a/garden-service/src/plugins/google/common.ts b/garden-service/src/plugins/google/common.ts index 65671c20a5..d7aad61f98 100644 --- a/garden-service/src/plugins/google/common.ts +++ b/garden-service/src/plugins/google/common.ts @@ -16,13 +16,9 @@ import { CommonServiceSpec } from "../../config/service" export const GOOGLE_CLOUD_DEFAULT_REGION = "us-central1" -export interface GoogleCloudServiceSpec extends CommonServiceSpec { - project?: string, -} - export interface GoogleCloudModule< M extends ModuleSpec = ModuleSpec, - S extends GoogleCloudServiceSpec = GoogleCloudServiceSpec, + S extends CommonServiceSpec = CommonServiceSpec, T extends ExecTestSpec = ExecTestSpec, > extends Module { } diff --git a/garden-service/src/plugins/google/google-app-engine.ts b/garden-service/src/plugins/google/google-app-engine.ts index 647d453ec0..72a35b1152 100644 --- a/garden-service/src/plugins/google/google-app-engine.ts +++ b/garden-service/src/plugins/google/google-app-engine.ts @@ -6,11 +6,8 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import { - DeployServiceParams, - GetServiceOutputsParams, -} from "../../types/plugin/params" -import { ServiceStatus, Service } from "../../types/service" +import { DeployServiceParams, ConfigureModuleParams } from "../../types/plugin/params" +import { ServiceStatus } from "../../types/service" import { join } from "path" import { gcloud, @@ -20,34 +17,42 @@ import { GOOGLE_CLOUD_DEFAULT_REGION, prepareEnvironment, } from "./common" -import { Provider } from "../../config/project" -import { - ContainerModule, - ContainerModuleSpec, - ContainerServiceSpec, -} from "../container/config" import { dumpYaml } from "../../util/util" -import { - GardenPlugin, -} from "../../types/plugin/plugin" - -export interface GoogleAppEngineServiceSpec extends ContainerServiceSpec { - project?: string -} - -export interface GoogleAppEngineModule extends ContainerModule { } - -function getAppEngineProject(service: Service, provider: Provider) { - return service.spec.project || provider.config["default-project"] || null -} +import { GardenPlugin } from "../../types/plugin/plugin" +import { configureContainerModule } from "../container/container" +import { ContainerModule } from "../container/config" +import { providerConfigBaseSchema } from "../../config/project" +import * as Joi from "joi" + +const configSchema = providerConfigBaseSchema.keys({ + project: Joi.string() + .required() + .description("The GCP project to deploy containers to."), +}) export const gardenPlugin = (): GardenPlugin => ({ + configSchema, actions: { getEnvironmentStatus, prepareEnvironment, }, moduleActions: { container: { + async configure(params: ConfigureModuleParams) { + const config = await configureContainerModule(params) + + // TODO: we may want to pull this from the service status instead, along with other outputs + const project = params.ctx.provider.config.project + const endpoint = `https://${GOOGLE_CLOUD_DEFAULT_REGION}-${project}.cloudfunctions.net/${config.name}` + + config.outputs = { + ...config.outputs || {}, + endpoint, + } + + return config + }, + async getServiceStatus(): Promise { // TODO // const project = this.getProject(service, env) @@ -59,7 +64,7 @@ export const gardenPlugin = (): GardenPlugin => ({ return {} }, - async deployService({ ctx, service, runtimeContext, log }: DeployServiceParams) { + async deployService({ ctx, service, runtimeContext, log }: DeployServiceParams) { log.info({ section: service.name, msg: `Deploying app...`, @@ -92,7 +97,7 @@ export const gardenPlugin = (): GardenPlugin => ({ await dumpYaml(appYamlPath, appYaml) // deploy to GAE - const project = getAppEngineProject(service, ctx.provider) + const project = ctx.provider.config.project await gcloud(project).call([ "app", "deploy", "--quiet", @@ -102,15 +107,6 @@ export const gardenPlugin = (): GardenPlugin => ({ return {} }, - - async getServiceOutputs({ ctx, service }: GetServiceOutputsParams) { - // TODO: we may want to pull this from the service status instead, along with other outputs - const project = getAppEngineProject(service, ctx.provider) - - return { - ingress: `https://${GOOGLE_CLOUD_DEFAULT_REGION}-${project}.cloudfunctions.net/${service.name}`, - } - }, }, }, }) diff --git a/garden-service/src/plugins/google/google-cloud-functions.ts b/garden-service/src/plugins/google/google-cloud-functions.ts index deac2358f7..25c9c44aaa 100644 --- a/garden-service/src/plugins/google/google-cloud-functions.ts +++ b/garden-service/src/plugins/google/google-cloud-functions.ts @@ -14,7 +14,6 @@ import { Module } from "../../types/module" import { ConfigureModuleResult } from "../../types/plugin/outputs" import { DeployServiceParams, - GetServiceOutputsParams, GetServiceStatusParams, ConfigureModuleParams, } from "../../types/plugin/params" @@ -30,21 +29,12 @@ import { gcloud, getEnvironmentStatus, GOOGLE_CLOUD_DEFAULT_REGION, - GoogleCloudServiceSpec, } from "./common" import { GardenPlugin } from "../../types/plugin/plugin" -import { ModuleSpec } from "../../config/module" -import { baseServiceSchema } from "../../config/service" -import { Provider } from "../../config/project" +import { baseServiceSchema, CommonServiceSpec } from "../../config/service" +import { Provider, providerConfigBaseSchema } from "../../config/project" -export interface GcfServiceSpec extends GoogleCloudServiceSpec { - entrypoint?: string, - function: string, - hostname?: string - path: string, -} - -const gcfServiceSchema = baseServiceSchema +const gcfModuleSpecSchema = baseServiceSchema .keys({ entrypoint: Joi.string() .description("The entrypoint for the function (exported name in the function's module)"), @@ -54,34 +44,37 @@ const gcfServiceSchema = baseServiceSchema .description("The path of the module that contains the function."), project: Joi.string() .description("The Google Cloud project name of the function."), + tests: joiArray(execTestSchema), }) .description("Configuration for a Google Cloud Function.") -export const gcfServicesSchema = joiArray(gcfServiceSchema) - .min(1) - .unique("name") - .description("List of configurations for one or more Google Cloud Functions.") - -export interface GcfModuleSpec extends ModuleSpec { - functions: GcfServiceSpec[], +export interface GcfModuleSpec extends CommonServiceSpec { + entrypoint?: string, + function: string, + hostname?: string + path: string, + project?: string, tests: ExecTestSpec[], } -export const gcfModuleSpecSchema = Joi.object() - .keys({ - functions: gcfServicesSchema, - tests: joiArray(execTestSchema), - }) +export type GcfServiceSpec = GcfModuleSpec export interface GcfModule extends Module { } function getGcfProject(service: Service, provider: Provider) { - return service.spec.project || provider.config["default-project"] || null + return service.spec.project || provider.config.defaultProject || null } export async function configureGcfModule( { ctx, moduleConfig }: ConfigureModuleParams, ): Promise> { + // TODO: we may want to pull this from the service status instead, along with other outputs + const { name, spec } = moduleConfig + const project = spec.project || ctx.provider.config.defaultProject + + moduleConfig.outputs = { + endpoint: `https://${GOOGLE_CLOUD_DEFAULT_REGION}-${project}.cloudfunctions.net/${name}`, + } // TODO: check that each function exists at the specified path moduleConfig.spec = validateWithPath({ @@ -92,12 +85,12 @@ export async function configureGcfModule( projectRoot: ctx.projectRoot, }) - moduleConfig.serviceConfigs = moduleConfig.spec.functions.map(f => ({ - name: f.name, - dependencies: f.dependencies, - outputs: f.outputs, - spec: f, - })) + moduleConfig.serviceConfigs = [{ + name, + dependencies: spec.dependencies, + outputs: spec.outputs, + spec, + }] moduleConfig.testConfigs = moduleConfig.spec.tests.map(t => ({ name: t.name, @@ -109,7 +102,13 @@ export async function configureGcfModule( return moduleConfig } +const configSchema = providerConfigBaseSchema.keys({ + project: Joi.string() + .description("The default GCP project to deploy functions to (can be overridden on individual functions)."), +}) + export const gardenPlugin = (): GardenPlugin => ({ + configSchema, actions: { getEnvironmentStatus, prepareEnvironment, @@ -137,15 +136,6 @@ export const gardenPlugin = (): GardenPlugin => ({ return getServiceStatus(params) }, - - async getServiceOutputs({ ctx, service }: GetServiceOutputsParams) { - // TODO: we may want to pull this from the service status instead, along with other outputs - const project = getGcfProject(service, ctx.provider) - - return { - ingress: `https://${GOOGLE_CLOUD_DEFAULT_REGION}-${project}.cloudfunctions.net/${service.name}`, - } - }, }, }, }) diff --git a/garden-service/src/plugins/kubernetes/container/handlers.ts b/garden-service/src/plugins/kubernetes/container/handlers.ts index 3e86d5c2de..a946854750 100644 --- a/garden-service/src/plugins/kubernetes/container/handlers.ts +++ b/garden-service/src/plugins/kubernetes/container/handlers.ts @@ -15,7 +15,7 @@ import { ConfigurationError } from "../../../exceptions" import { configureContainerModule } from "../../container/container" import { KubernetesProvider } from "../kubernetes" import { ConfigureModuleParams } from "../../../types/plugin/params" -import { getContainerServiceStatus, getServiceOutputs } from "./status" +import { getContainerServiceStatus } from "./status" import { getTestResult } from "../test" import { ContainerModule } from "../../container/config" @@ -54,7 +54,6 @@ export const containerHandlers = { deleteService, execInService, getServiceLogs, - getServiceOutputs, getServiceStatus: getContainerServiceStatus, getTestResult, hotReloadService: hotReloadContainer, diff --git a/garden-service/src/plugins/kubernetes/container/status.ts b/garden-service/src/plugins/kubernetes/container/status.ts index 469044e5a0..64e381ff60 100644 --- a/garden-service/src/plugins/kubernetes/container/status.ts +++ b/garden-service/src/plugins/kubernetes/container/status.ts @@ -13,7 +13,7 @@ import { createContainerObjects } from "./deployment" import { KUBECTL_DEFAULT_TIMEOUT } from "../kubectl" import { DeploymentError } from "../../../exceptions" import { sleep } from "../../../util/util" -import { GetServiceOutputsParams, GetServiceStatusParams } from "../../../types/plugin/params" +import { GetServiceStatusParams } from "../../../types/plugin/params" import { ContainerModule } from "../../container/config" import { KubeApi } from "../api" import { compareDeployedObjects } from "../status" @@ -76,9 +76,3 @@ export async function waitForContainerService( await sleep(1000) } } - -export async function getServiceOutputs({ service }: GetServiceOutputsParams) { - return { - host: service.name, - } -} 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, - getServiceOutputs, getServiceStatus, getTestResult, hotReloadService: hotReloadHelmChart, diff --git a/garden-service/src/plugins/kubernetes/helm/status.ts b/garden-service/src/plugins/kubernetes/helm/status.ts index 1a7213e456..d22bbf3850 100644 --- a/garden-service/src/plugins/kubernetes/helm/status.ts +++ b/garden-service/src/plugins/kubernetes/helm/status.ts @@ -7,7 +7,7 @@ */ import { ServiceStatus, ServiceState } from "../../../types/service" -import { GetServiceStatusParams, GetServiceOutputsParams } from "../../../types/plugin/params" +import { GetServiceStatusParams } from "../../../types/plugin/params" import { getExecModuleBuildStatus } from "../../exec" import { compareDeployedObjects } from "../status" import { KubeApi } from "../api" @@ -15,11 +15,10 @@ import { getAppNamespace } from "../namespace" import { LogEntry } from "../../../logger/log-entry" import { helm } from "./helm-cli" import { HelmModule } from "./config" -import { getChartResources, findServiceResource, getReleaseName } from "./common" +import { getChartResources, findServiceResource } from "./common" import { buildHelmModule } from "./build" import { configureHotReload } from "../hot-reload" import { getHotReloadSpec } from "./hot-reload" -import { PrimitiveMap } from "../../../config/common" const helmStatusCodeMap: { [code: number]: ServiceState } = { // see https://github.com/kubernetes/helm/blob/master/_proto/hapi/release/status.proto @@ -71,12 +70,6 @@ export async function getServiceStatus( } } -export async function getServiceOutputs({ module }: GetServiceOutputsParams): Promise { - return { - "release-name": await getReleaseName(module), - } -} - export async function getReleaseStatus( namespace: string, context: string, releaseName: string, log: LogEntry, ): Promise { diff --git a/garden-service/src/plugins/local/local-docker-swarm.ts b/garden-service/src/plugins/local/local-docker-swarm.ts index bc24f45a88..0725a77113 100644 --- a/garden-service/src/plugins/local/local-docker-swarm.ts +++ b/garden-service/src/plugins/local/local-docker-swarm.ts @@ -16,7 +16,6 @@ import { import { DeployServiceParams, ExecInServiceParams, - GetServiceOutputsParams, GetServiceStatusParams, } from "../../types/plugin/params" import { ContainerModule } from "../container/config" @@ -177,12 +176,6 @@ export const gardenPlugin = (): GardenPlugin => ({ return getServiceStatus({ ctx, module, service, runtimeContext, log, hotReload: false }) }, - async getServiceOutputs({ ctx, service }: GetServiceOutputsParams) { - return { - host: getSwarmServiceName(ctx, service.name), - } - }, - async execInService( { ctx, service, command, runtimeContext, log }: ExecInServiceParams, ) { diff --git a/garden-service/src/plugins/local/local-google-cloud-functions.ts b/garden-service/src/plugins/local/local-google-cloud-functions.ts index 497198b244..3afac38007 100644 --- a/garden-service/src/plugins/local/local-google-cloud-functions.ts +++ b/garden-service/src/plugins/local/local-google-cloud-functions.ts @@ -89,6 +89,7 @@ export const gardenPlugin = (): GardenPlugin => ({ }]), }, name: parsed.name, + outputs: {}, path: parsed.path, type: "container", diff --git a/garden-service/src/plugins/openfaas/openfaas.ts b/garden-service/src/plugins/openfaas/openfaas.ts index 7b2d3f15fb..43833272eb 100644 --- a/garden-service/src/plugins/openfaas/openfaas.ts +++ b/garden-service/src/plugins/openfaas/openfaas.ts @@ -41,7 +41,6 @@ import { DeployServiceParams, GetServiceStatusParams, BuildModuleParams, - GetServiceOutputsParams, } from "../../types/plugin/params" import { every, values } from "lodash" import { dumpYaml, findByName } from "../../util/util" @@ -84,6 +83,7 @@ export const openfaasModuleSpecSchema = execModuleSpecSchema .description("The module specification for an OpenFaaS module.") export interface OpenFaasModule extends Module { } +export type OpenFaasModuleConfig = OpenFaasModule["_ConfigType"] export interface OpenFaasService extends Service { } export interface OpenFaasConfig extends Provider { @@ -162,7 +162,7 @@ export function gardenPlugin(): GardenPlugin { }, moduleActions: { openfaas: { - async configure({ moduleConfig }: ConfigureModuleParams): Promise { + async configure({ ctx, moduleConfig }: ConfigureModuleParams): Promise { moduleConfig.spec = validate( moduleConfig.spec, openfaasModuleSpecSchema, @@ -196,6 +196,10 @@ export function gardenPlugin(): GardenPlugin { timeout: t.timeout, })) + moduleConfig.outputs = { + endpoint: await getInternalServiceUrl(ctx, moduleConfig), + } + return moduleConfig }, @@ -218,12 +222,6 @@ export function gardenPlugin(): GardenPlugin { getServiceStatus, - async getServiceOutputs({ ctx, service }: GetServiceOutputsParams) { - return { - endpoint: await getInternalServiceUrl(ctx, service), - } - }, - async getServiceLogs(params: GetServiceLogsParams) { const { ctx, service } = params const k8sProvider = getK8sProvider(ctx) @@ -326,12 +324,12 @@ async function writeStackFile( }) } -async function getServiceStatus({ ctx, service }: GetServiceStatusParams) { +async function getServiceStatus({ ctx, module, service }: GetServiceStatusParams) { const k8sProvider = getK8sProvider(ctx) const ingresses: ServiceIngress[] = [{ hostname: getExternalGatewayHostname(ctx.provider, k8sProvider), - path: getServicePath(service), + path: getServicePath(module), port: k8sProvider.config.ingressHttpPort, protocol: "http", }] @@ -412,8 +410,8 @@ function getK8sProvider(ctx: PluginContext): KubernetesProvider { return provider } -function getServicePath(service: OpenFaasService) { - return join("/", "function", service.name) +function getServicePath(config: OpenFaasModuleConfig) { + return join("/", "function", config.name) } async function getInternalGatewayUrl(ctx: PluginContext) { @@ -444,8 +442,8 @@ function getExternalGatewayUrl(ctx: PluginContext) { return `http://${hostname}:${ingressPort}` } -async function getInternalServiceUrl(ctx: PluginContext, service: OpenFaasService) { - return urlResolve(await getInternalGatewayUrl(ctx), getServicePath(service)) +async function getInternalServiceUrl(ctx: PluginContext, config: OpenFaasModuleConfig) { + return urlResolve(await getInternalGatewayUrl(ctx), getServicePath(config)) } async function getOpenfaasNamespace(ctx: PluginContext, k8sProvider: KubernetesProvider, skipCreate?: boolean) { 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 70cc0be69c..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, log, 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 bc4c6658fa..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, log, 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 d91ce74149..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, log, 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/plugin/outputs.ts b/garden-service/src/types/plugin/outputs.ts index bd4b9b9952..a1c8ca502b 100644 --- a/garden-service/src/types/plugin/outputs.ts +++ b/garden-service/src/types/plugin/outputs.ts @@ -8,7 +8,6 @@ import * as Joi from "joi" import { ModuleVersion, moduleVersionSchema } from "../../vcs/base" -import { PrimitiveMap } from "../../config/common" import { Module } from "../module" import { ServiceStatus } from "../service" import { moduleConfigSchema, ModuleConfig } from "../../config/module" @@ -331,7 +330,6 @@ export interface ServiceActionOutputs { deployService: Promise hotReloadService: Promise deleteService: Promise - getServiceOutputs: Promise execInService: Promise getServiceLogs: Promise<{}> runService: Promise diff --git a/garden-service/src/types/plugin/params.ts b/garden-service/src/types/plugin/params.ts index 485d969a70..f3835fd0d5 100644 --- a/garden-service/src/types/plugin/params.ts +++ b/garden-service/src/types/plugin/params.ts @@ -269,10 +269,6 @@ export const deleteServiceParamsSchema = serviceActionParamsSchema runtimeContext: runtimeContextSchema, }) -export interface GetServiceOutputsParams - extends PluginServiceActionParamsBase { } -export const getServiceOutputsParamsSchema = serviceActionParamsSchema - export interface ExecInServiceParams extends PluginServiceActionParamsBase { command: string[] @@ -338,7 +334,6 @@ export interface ServiceActionParams { deployService: DeployServiceParams hotReloadService: HotReloadServiceParams deleteService: DeleteServiceParams - getServiceOutputs: GetServiceOutputsParams execInService: ExecInServiceParams getServiceLogs: GetServiceLogsParams runService: RunServiceParams diff --git a/garden-service/src/types/plugin/plugin.ts b/garden-service/src/types/plugin/plugin.ts index 5503c2de60..426eeed600 100644 --- a/garden-service/src/types/plugin/plugin.ts +++ b/garden-service/src/types/plugin/plugin.ts @@ -16,7 +16,6 @@ import { } from "../../config/common" import { Module } from "../module" import { serviceStatusSchema } from "../service" -import { serviceOutputsSchema } from "../../config/service" import { LogNode } from "../../logger/log-node" import { ModuleActionParams, @@ -32,7 +31,6 @@ import { getServiceStatusParamsSchema, deployServiceParamsSchema, deleteServiceParamsSchema, - getServiceOutputsParamsSchema, execInServiceParamsSchema, getServiceLogsParamsSchema, runServiceParamsSchema, @@ -232,11 +230,6 @@ export const serviceActionDescriptions: { [P in ServiceActionName]: PluginAction paramsSchema: deleteServiceParamsSchema, resultSchema: serviceStatusSchema, }, - getServiceOutputs: { - description: "DEPRECATED", - paramsSchema: getServiceOutputsParamsSchema, - resultSchema: serviceOutputsSchema, - }, execInService: { description: dedent` Execute the specified command next to a running service, e.g. in a service container. diff --git a/garden-service/src/types/service.ts b/garden-service/src/types/service.ts index 62882d12af..733b13d4cf 100644 --- a/garden-service/src/types/service.ts +++ b/garden-service/src/types/service.ts @@ -16,8 +16,8 @@ import dedent = require("dedent") import { format } from "url" import { moduleVersionSchema } from "../vcs/base" import { Garden } from "../garden" -import { LogEntry } from "../logger/log-entry" import { uniq } from "lodash" +import { ConfigGraph } from "../config-graph" import normalizeUrl = require("normalize-url") export interface Service { @@ -41,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, @@ -210,10 +210,10 @@ export const runtimeContextSchema = Joi.object() }) export async function prepareRuntimeContext( - garden: Garden, log: LogEntry, 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, @@ -243,10 +243,6 @@ export async function prepareRuntimeContext( const depContext = deps[dep.name] const outputs = { - ...await garden.actions.getServiceOutputs({ - log, - service: dep, - }), ...dep.config.outputs, } const serviceEnvName = getEnvVarName(dep.name) @@ -267,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/data/test-projects/duplicate-module/garden.yml b/garden-service/test/data/test-projects/duplicate-module/garden.yml new file mode 100644 index 0000000000..06b1cd1c3b --- /dev/null +++ b/garden-service/test/data/test-projects/duplicate-module/garden.yml @@ -0,0 +1,6 @@ +project: + name: duplicate-module + environments: + - name: local + providers: + - name: test-plugin diff --git a/garden-service/test/data/test-projects/duplicate-module/module-a/garden.yml b/garden-service/test/data/test-projects/duplicate-module/module-a/garden.yml new file mode 100644 index 0000000000..68a7da128f --- /dev/null +++ b/garden-service/test/data/test-projects/duplicate-module/module-a/garden.yml @@ -0,0 +1,3 @@ +module: + name: module-a + type: test diff --git a/garden-service/test/data/test-projects/duplicate-module/module-b/garden.yml b/garden-service/test/data/test-projects/duplicate-module/module-b/garden.yml new file mode 100644 index 0000000000..68a7da128f --- /dev/null +++ b/garden-service/test/data/test-projects/duplicate-module/module-b/garden.yml @@ -0,0 +1,3 @@ +module: + name: module-a + type: test diff --git a/garden-service/test/data/test-projects/duplicate-service-and-task/garden.yml b/garden-service/test/data/test-projects/duplicate-service-and-task/garden.yml new file mode 100644 index 0000000000..06b1cd1c3b --- /dev/null +++ b/garden-service/test/data/test-projects/duplicate-service-and-task/garden.yml @@ -0,0 +1,6 @@ +project: + name: duplicate-module + environments: + - name: local + providers: + - name: test-plugin diff --git a/garden-service/test/data/test-projects/duplicate-service-and-task/module-a/garden.yml b/garden-service/test/data/test-projects/duplicate-service-and-task/module-a/garden.yml new file mode 100644 index 0000000000..aab4ec0ace --- /dev/null +++ b/garden-service/test/data/test-projects/duplicate-service-and-task/module-a/garden.yml @@ -0,0 +1,5 @@ +module: + name: module-a + type: test + services: + - name: dupe diff --git a/garden-service/test/data/test-projects/duplicate-service-and-task/module-b/garden.yml b/garden-service/test/data/test-projects/duplicate-service-and-task/module-b/garden.yml new file mode 100644 index 0000000000..f2dde87c46 --- /dev/null +++ b/garden-service/test/data/test-projects/duplicate-service-and-task/module-b/garden.yml @@ -0,0 +1,5 @@ +module: + name: module-b + type: test + tasks: + - name: dupe diff --git a/garden-service/test/data/test-projects/duplicate-service/garden.yml b/garden-service/test/data/test-projects/duplicate-service/garden.yml new file mode 100644 index 0000000000..06b1cd1c3b --- /dev/null +++ b/garden-service/test/data/test-projects/duplicate-service/garden.yml @@ -0,0 +1,6 @@ +project: + name: duplicate-module + environments: + - name: local + providers: + - name: test-plugin diff --git a/garden-service/test/data/test-projects/duplicate-service/module-a/garden.yml b/garden-service/test/data/test-projects/duplicate-service/module-a/garden.yml new file mode 100644 index 0000000000..aab4ec0ace --- /dev/null +++ b/garden-service/test/data/test-projects/duplicate-service/module-a/garden.yml @@ -0,0 +1,5 @@ +module: + name: module-a + type: test + services: + - name: dupe diff --git a/garden-service/test/data/test-projects/duplicate-service/module-b/garden.yml b/garden-service/test/data/test-projects/duplicate-service/module-b/garden.yml new file mode 100644 index 0000000000..3e4649212c --- /dev/null +++ b/garden-service/test/data/test-projects/duplicate-service/module-b/garden.yml @@ -0,0 +1,5 @@ +module: + name: module-b + type: test + services: + - name: dupe diff --git a/garden-service/test/data/test-projects/duplicate-task/garden.yml b/garden-service/test/data/test-projects/duplicate-task/garden.yml new file mode 100644 index 0000000000..06b1cd1c3b --- /dev/null +++ b/garden-service/test/data/test-projects/duplicate-task/garden.yml @@ -0,0 +1,6 @@ +project: + name: duplicate-module + environments: + - name: local + providers: + - name: test-plugin diff --git a/garden-service/test/data/test-projects/duplicate-task/module-a/garden.yml b/garden-service/test/data/test-projects/duplicate-task/module-a/garden.yml new file mode 100644 index 0000000000..95bf526e42 --- /dev/null +++ b/garden-service/test/data/test-projects/duplicate-task/module-a/garden.yml @@ -0,0 +1,5 @@ +module: + name: module-a + type: test + tasks: + - name: dupe diff --git a/garden-service/test/data/test-projects/duplicate-task/module-b/garden.yml b/garden-service/test/data/test-projects/duplicate-task/module-b/garden.yml new file mode 100644 index 0000000000..f2dde87c46 --- /dev/null +++ b/garden-service/test/data/test-projects/duplicate-task/module-b/garden.yml @@ -0,0 +1,5 @@ +module: + name: module-b + type: test + tasks: + - name: dupe diff --git a/garden-service/test/data/test-projects/source-module/garden.yml b/garden-service/test/data/test-projects/source-module/garden.yml new file mode 100644 index 0000000000..06b1cd1c3b --- /dev/null +++ b/garden-service/test/data/test-projects/source-module/garden.yml @@ -0,0 +1,6 @@ +project: + name: duplicate-module + environments: + - name: local + providers: + - name: test-plugin diff --git a/garden-service/test/data/test-projects/source-module/module-a/garden.yml b/garden-service/test/data/test-projects/source-module/module-a/garden.yml new file mode 100644 index 0000000000..95bf526e42 --- /dev/null +++ b/garden-service/test/data/test-projects/source-module/module-a/garden.yml @@ -0,0 +1,5 @@ +module: + name: module-a + type: test + tasks: + - name: dupe diff --git a/garden-service/test/data/test-projects/source-module/module-b/garden.yml b/garden-service/test/data/test-projects/source-module/module-b/garden.yml new file mode 100644 index 0000000000..0ce6d4d402 --- /dev/null +++ b/garden-service/test/data/test-projects/source-module/module-b/garden.yml @@ -0,0 +1,6 @@ +module: + name: module-b + type: test + services: + - name: service-b + sourceModuleName: module-a diff --git a/garden-service/test/helpers.ts b/garden-service/test/helpers.ts index da7c5048c3..4d248a41b8 100644 --- a/garden-service/test/helpers.ts +++ b/garden-service/test/helpers.ts @@ -99,6 +99,8 @@ export async function configureTestModule({ moduleConfig }: ConfigureModuleParam { context: `test module ${moduleConfig.name}` }, ) + moduleConfig.outputs = { foo: "bar" } + // validate services moduleConfig.serviceConfigs = moduleConfig.spec.services.map(spec => ({ name: spec.name, @@ -223,6 +225,7 @@ const defaultModuleConfig: ModuleConfig = { path: "bla", allowPublish: false, build: { command: [], dependencies: [] }, + outputs: {}, spec: { services: [ { diff --git a/garden-service/test/src/actions.ts b/garden-service/test/src/actions.ts index 65ef6be6c0..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" @@ -24,7 +24,6 @@ import { deployServiceParamsSchema, deleteServiceParamsSchema, hotReloadServiceParamsSchema, - getServiceOutputsParamsSchema, execInServiceParamsSchema, getServiceLogsParamsSchema, runServiceParamsSchema, @@ -47,6 +46,7 @@ describe("ActionHelper", () => { let actions: ActionHelper let module: Module let service: Service + let runtimeContext: RuntimeContext let task: Task before(async () => { @@ -54,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 @@ -255,35 +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("getServiceOutputs", () => { - it("should correctly call the corresponding plugin handler", async () => { - const result = await actions.getServiceOutputs({ log, service }) - expect(result).to.eql({ foo: "bar" }) - }) - }) - 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" }) }) }) @@ -291,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({}) }) }) @@ -343,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 () => ({ @@ -493,11 +556,6 @@ const testPlugin: PluginFactory = async () => ({ return { state: "ready" } }, - getServiceOutputs: async (params) => { - validate(params, getServiceOutputsParamsSchema) - return { foo: "bar" } - }, - execInService: async (params) => { validate(params, execInServiceParamsSchema) return { 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/commands/run/module.ts b/garden-service/test/src/commands/run/module.ts index c65399dcef..60fa383634 100644 --- a/garden-service/test/src/commands/run/module.ts +++ b/garden-service/test/src/commands/run/module.ts @@ -2,7 +2,6 @@ import { RunModuleCommand } from "../../../../src/commands/run/module" import { RunResult } from "../../../../src/types/plugin/outputs" import { makeTestGardenA, - makeTestModule, testModuleVersion, testNow, } from "../../../helpers" @@ -22,21 +21,16 @@ describe("RunModuleCommand", () => { }) it("should run a module without a command param", async () => { - await garden.addModule(makeTestModule({ - name: "run-test", - path: garden.projectRoot, - })) - const cmd = new RunModuleCommand() const { result } = await cmd.action({ garden, log, - args: { module: "run-test", command: [] }, + args: { module: "module-a", command: [] }, opts: { "interactive": false, "force-build": false }, }) const expected: RunResult = { - moduleName: "run-test", + moduleName: "module-a", command: [], completedAt: testNow, output: "OK", @@ -49,21 +43,16 @@ describe("RunModuleCommand", () => { }) it("should run a module with a command param", async () => { - garden.addModule(makeTestModule({ - name: "run-test", - path: garden.projectRoot, - })) - const cmd = new RunModuleCommand() const { result } = await cmd.action({ garden, log, - args: { module: "run-test", command: ["my", "command"] }, + args: { module: "module-a", command: ["my", "command"] }, opts: { "interactive": false, "force-build": false }, }) const expected: RunResult = { - moduleName: "run-test", + moduleName: "module-a", command: ["my", "command"], completedAt: testNow, output: "OK", diff --git a/garden-service/test/src/commands/run/service.ts b/garden-service/test/src/commands/run/service.ts index 134ac6dc1d..ce0aeb8105 100644 --- a/garden-service/test/src/commands/run/service.ts +++ b/garden-service/test/src/commands/run/service.ts @@ -2,7 +2,6 @@ import { RunServiceCommand } from "../../../../src/commands/run/service" import { RunResult } from "../../../../src/types/plugin/outputs" import { makeTestGardenA, - makeTestModule, testModuleVersion, testNow, } from "../../../helpers" @@ -23,22 +22,17 @@ describe("RunServiceCommand", () => { }) it("should run a service", async () => { - garden.addModule(makeTestModule({ - name: "run-test", - serviceConfigs: [{ name: "test-service", dependencies: [], outputs: {}, spec: {} }], - })) - const cmd = new RunServiceCommand() const { result } = await cmd.action({ garden, log, - args: { service: "test-service" }, + args: { service: "service-a" }, opts: { "force-build": false }, }) const expected: RunResult = { - moduleName: "run-test", - command: ["test-service"], + moduleName: "module-a", + command: ["service-a"], completedAt: testNow, output: "OK", version: testModuleVersion, 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/base.ts b/garden-service/test/src/config/base.ts index efb57f3dcd..bd81bd6b6b 100644 --- a/garden-service/test/src/config/base.ts +++ b/garden-service/test/src/config/base.ts @@ -72,6 +72,7 @@ describe("loadConfig", () => { repositoryUrl: undefined, allowPublish: true, build: { command: ["echo", "A"], dependencies: [] }, + outputs: {}, path: modulePathA, spec: { diff --git a/garden-service/test/src/config/config-context.ts b/garden-service/test/src/config/config-context.ts index 8e4cd5c56e..db2e6d354a 100644 --- a/garden-service/test/src/config/config-context.ts +++ b/garden-service/test/src/config/config-context.ts @@ -233,7 +233,6 @@ describe("ModuleConfigContext", () => { await garden.scanModules() c = new ModuleConfigContext( garden, - garden.log, garden.environment, Object.values((garden).moduleConfigs), ) @@ -259,13 +258,17 @@ 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) }) - it("should should resolve the version of a service", async () => { - const { versionString } = (await garden.getModule("module-a")).version - expect(await c.resolve({ key: ["services", "service-a", "version"], nodePath: [] })).to.equal(versionString) + it("should should resolve the outputs of a module", async () => { + expect(await c.resolve({ key: ["modules", "module-a", "outputs", "foo"], nodePath: [] })).to.equal("bar") + }) + + it("should should resolve the version of a module", async () => { + const { versionString } = await garden.resolveVersion("module-a", []) + expect(await c.resolve({ key: ["modules", "module-a", "version"], nodePath: [] })).to.equal(versionString) }) it("should should resolve a project variable", async () => { diff --git a/garden-service/test/src/garden.ts b/garden-service/test/src/garden.ts index 7a2864cdb5..9480003599 100644 --- a/garden-service/test/src/garden.ts +++ b/garden-service/test/src/garden.ts @@ -7,7 +7,6 @@ import { expectError, makeTestGarden, makeTestGardenA, - makeTestModule, projectRootA, stubExtSources, getDataDir, @@ -35,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 () => { @@ -121,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"]) }) @@ -325,152 +144,35 @@ 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"]) }) - }) - - describe("addModule", () => { - it("should add the given module and its services to the context", async () => { - const garden = await makeTestGardenA() - - const testModule = makeTestModule() - await garden.addModule(testModule) - - const modules = await garden.getModules(undefined, true) - expect(getNames(modules)).to.eql(["test"]) - - const services = await garden.getServices(undefined, true) - expect(getNames(services)).to.eql(["test-service"]) - }) - - it("should throw when adding module twice without force parameter", async () => { - const garden = await makeTestGardenA() - - const testModule = makeTestModule() - await garden.addModule(testModule) - - try { - await garden.addModule(testModule) - } catch (err) { - expect(err.type).to.equal("configuration") - return - } - - throw new Error("Expected error") - }) - - it("should allow adding module multiple times with force parameter", async () => { - const garden = await makeTestGardenA() - - let testModule = makeTestModule() - await garden.addModule(testModule) - - testModule = makeTestModule() - await garden.addModule(testModule, true) - - const modules = await garden.getModules(undefined, true) - expect(getNames(modules)).to.eql(["test"]) - }) - - it("should throw if a service is added twice without force parameter", async () => { - const garden = await makeTestGardenA() - - const testModule = makeTestModule() - const testModuleB = makeTestModule({ name: "test-b" }) - await garden.addModule(testModule) - - try { - await garden.addModule(testModuleB) - } catch (err) { - expect(err.type).to.equal("configuration") - return - } - - throw new Error("Expected error") - }) - - it("should allow adding service multiple times with force parameter", async () => { - const garden = await makeTestGardenA() - - const testModule = makeTestModule() - const testModuleB = makeTestModule({ name: "test-b" }) - await garden.addModule(testModule) - await garden.addModule(testModuleB, true) - - const services = await garden.getServices(undefined, true) - expect(getNames(services)).to.eql(["test-service"]) - }) - - it("should automatically add service source modules as build dependencies", async () => { - const garden = await makeTestGardenA() - - const testModule = makeTestModule() - await garden.addModule(testModule) - - const testModuleB = makeTestModule({ - name: "test-b", - spec: { - services: [ - { - name: "test-service-b", - dependencies: [], - sourceModuleName: "test", - }, - ], - }, - serviceConfigs: [ - { - name: "test-service-b", - dependencies: [], - outputs: {}, - sourceModuleName: "test", - spec: {}, - }, - ], - }) - await garden.addModule(testModuleB) + it("should throw when two modules have the same name", async () => { + const garden = await makeTestGarden(resolve(dataDir, "test-projects", "duplicate-module")) - const module = await garden.getModule("test-b") - expect(module.build.dependencies).to.eql([{ name: "test", copy: [] }]) + await expectError( + () => garden.scanModules(), + err => expect(err.message).to.equal( + "Module module-a is declared multiple times (in 'module-a/garden.yml' and 'module-b/garden.yml')", + ), + ) }) }) - 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") }) @@ -479,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, @@ -615,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 27664f19f8..ad4f924aea 100644 --- a/garden-service/test/src/plugins/container.ts +++ b/garden-service/test/src/plugins/container.ts @@ -35,6 +35,7 @@ describe("plugins.container", () => { dependencies: [], }, name: "test", + outputs: {}, path: modulePath, type: "container", @@ -70,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", () => { @@ -134,6 +136,7 @@ describe("plugins.container", () => { dependencies: [], }, name: "test", + outputs: {}, path: modulePath, type: "container", @@ -187,6 +190,7 @@ describe("plugins.container", () => { dependencies: [], }, name: "module-a", + outputs: {}, path: modulePath, type: "container", @@ -247,6 +251,7 @@ describe("plugins.container", () => { allowPublish: false, build: { command: ["echo", "OK"], dependencies: [] }, name: "module-a", + outputs: {}, path: modulePath, type: "container", spec: @@ -362,6 +367,7 @@ describe("plugins.container", () => { dependencies: [], }, name: "module-a", + outputs: {}, path: modulePath, type: "test", @@ -417,6 +423,7 @@ describe("plugins.container", () => { dependencies: [], }, name: "module-a", + outputs: {}, path: modulePath, type: "test", @@ -467,6 +474,7 @@ describe("plugins.container", () => { dependencies: [], }, name: "module-a", + outputs: {}, path: modulePath, type: "test", 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 c8e96bad64..987c65e35f 100644 --- a/garden-service/test/src/plugins/kubernetes/container/ingress.ts +++ b/garden-service/test/src/plugins/kubernetes/container/ingress.ts @@ -337,6 +337,7 @@ describe("createIngresses", () => { dependencies: [], }, name: "test", + outputs: {}, path: "/tmp", type: "container", @@ -355,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 9fed2cb647..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,24 +31,22 @@ 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", name: "api", + outputs: { + "release-name": "api-release", + }, path: resolve(ctx.projectRoot, "api"), repositoryUrl: undefined, serviceConfigs: [ @@ -154,7 +152,6 @@ describe("validateHelmModule", () => { const config = await validateHelmModule({ ctx, moduleConfig }) expect(config.build.dependencies).to.eql([ - { name: "api-image", copy: [] }, { name: "foo", copy: [] }, ]) }) @@ -167,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/plugins/kubernetes/helm/status.ts b/garden-service/test/src/plugins/kubernetes/helm/status.ts deleted file mode 100644 index e27a302f15..0000000000 --- a/garden-service/test/src/plugins/kubernetes/helm/status.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { resolve } from "path" -import { expect } from "chai" - -import { dataDir, makeTestGarden } from "../../../../helpers" -import { getServiceOutputs } from "../../../../../src/plugins/kubernetes/helm/status" - -describe("getServiceOutputs", () => { - it("should output the release name for the chart", async () => { - const projectRoot = resolve(dataDir, "test-projects", "helm") - const garden = await makeTestGarden(projectRoot) - const ctx = garden.getPluginContext("local-kubernetes") - - const service = await garden.getService("api") - const module = service.module - - const result = await getServiceOutputs({ ctx, module, service, log: garden.log }) - - expect(result).to.eql({ "release-name": "api-release" }) - }) -}) 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 () => {