diff --git a/garden-service/src/docs/config.ts b/garden-service/src/docs/config.ts index 6d10b331aa..3b5cc02dd6 100644 --- a/garden-service/src/docs/config.ts +++ b/garden-service/src/docs/config.ts @@ -24,7 +24,7 @@ import { execModuleSpecSchema } from "../plugins/exec" import { projectSchema } from "../config/project" import { baseModuleSpecSchema } from "../config/module" import handlebars = require("handlebars") -import { configSchema as localK8sConfigSchema } from "../plugins/kubernetes/local" +import { configSchema as localK8sConfigSchema } from "../plugins/kubernetes/local/config" import { configSchema as k8sConfigSchema } from "../plugins/kubernetes/kubernetes" import { configSchema as openfaasConfigSchema } from "../plugins/openfaas/openfaas" import { openfaasModuleSpecSchema } from "../plugins/openfaas/openfaas" diff --git a/garden-service/src/garden.ts b/garden-service/src/garden.ts index a6d62d2e97..9c7499eba3 100644 --- a/garden-service/src/garden.ts +++ b/garden-service/src/garden.ts @@ -375,6 +375,7 @@ export class Garden { } private async loadPlugin(pluginName: string, config: ProviderConfig) { + this.log.silly(`Loading plugin ${pluginName}`) const factory = this.registeredPlugins[pluginName] if (!factory) { @@ -444,20 +445,28 @@ export class Garden { // call configureProvider action if provided const configureHandler = actions.configureProvider - if (configureHandler) { - const configureOutput = await configureHandler({ config: providerConfig }) - providerConfig = configureOutput.config - } if (plugin.configSchema) { providerConfig = validate(providerConfig, plugin.configSchema, { context: `${pluginName} configuration` }) } + if (configureHandler) { + this.log.silly(`Calling configureProvider on ${pluginName}`) + const configureOutput = await configureHandler({ + config: providerConfig, + projectName: this.projectName, + log: this.log, + }) + providerConfig = configureOutput.config + } + if (providerIndex === -1) { this.environment.providers.push({ name: pluginName, config: providerConfig }) } else { this.environment.providers[providerIndex].config = providerConfig } + + this.log.silly(`Done loading plugin ${pluginName}`) } getPlugin(pluginName: string) { @@ -508,7 +517,7 @@ export class Garden { }) const ctx = this.getPluginContext(configureHandler["pluginName"]) - config = await configureHandler({ ctx, moduleConfig: config }) + config = await configureHandler({ ctx, moduleConfig: config, log: this.log }) // FIXME: We should be able to avoid this config.name = getModuleKey(config.name, config.plugin) diff --git a/garden-service/src/plugins/kubernetes/helm/deployment.ts b/garden-service/src/plugins/kubernetes/helm/deployment.ts index 281093d594..809013b475 100644 --- a/garden-service/src/plugins/kubernetes/helm/deployment.ts +++ b/garden-service/src/plugins/kubernetes/helm/deployment.ts @@ -57,7 +57,6 @@ export async function deployService( "--name", releaseName, "--namespace", namespace, "--values", valuesPath, - "--wait", ] if (force) { installArgs.push("--replace") @@ -70,7 +69,6 @@ export async function deployService( "--install", "--namespace", namespace, "--values", valuesPath, - "--wait", ] if (force) { upgradeArgs.push("--force") diff --git a/garden-service/src/plugins/kubernetes/local.ts b/garden-service/src/plugins/kubernetes/local.ts deleted file mode 100644 index d6762e5c54..0000000000 --- a/garden-service/src/plugins/kubernetes/local.ts +++ /dev/null @@ -1,160 +0,0 @@ -/* - * Copyright (C) 2018 Garden Technologies, Inc. - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. - */ - -import * as execa from "execa" -import { safeLoad } from "js-yaml" -import * as Joi from "joi" -import { join } from "path" -import { GardenPlugin, PluginFactoryParams } from "../../types/plugin/plugin" -import { - gardenPlugin as k8sPlugin, - KubernetesBaseConfig, - kubernetesConfigBase, -} from "./kubernetes" -import { readFile } from "fs-extra" -import { homedir } from "os" -import { getLocalEnvironmentStatus, prepareLocalEnvironment } from "./init" -import { ConfigureProviderParams } from "../../types/plugin/params" - -// TODO: split this into separate plugins to handle Docker for Mac and Minikube - -// note: this is in order of preference, in case neither is set as the current kubectl context -// and none is explicitly configured in the garden.yml -const supportedContexts = ["docker-for-desktop", "minikube"] -const kubeConfigPath = join(homedir(), ".kube", "config") - -async function getKubeConfig(): Promise { - try { - return safeLoad((await readFile(kubeConfigPath)).toString()) - } catch { - return {} - } -} - -/** - * Automatically set docker environment variables for minikube - * TODO: it would be better to explicitly provide those to docker instead of using process.env - */ -async function setMinikubeDockerEnv() { - const minikubeEnv = await execa.stdout("minikube", ["docker-env", "--shell=bash"]) - for (const line of minikubeEnv.split("\n")) { - const matched = line.match(/^export (\w+)="(.+)"$/) - if (matched) { - process.env[matched[1]] = matched[2] - } - } -} - -export interface LocalKubernetesConfig extends KubernetesBaseConfig { - _system?: Symbol -} - -export const configSchema = kubernetesConfigBase - .keys({ - namespace: Joi.string() - .default(undefined, "") - .description( - "Specify which namespace to deploy services to (defaults to the project name). " + - "Note that the framework generates other namespaces as well with this name as a prefix.", - ), - setupIngressController: Joi.string() - .allow("nginx", false, null) - .default("nginx") - .description("Set this to null or false to skip installing/enabling the `nginx` ingress controller."), - _system: Joi.any().meta({ internal: true }), - }) - .description("The provider configuration for the local-kubernetes plugin.") - -export const name = "local-kubernetes" - -export function gardenPlugin({ projectName, log }: PluginFactoryParams): GardenPlugin { - const plugin = k8sPlugin() - - plugin.configSchema = configSchema - - plugin.actions!.configureProvider = async ({ config }: ConfigureProviderParams) => { - let context = config.context - let defaultHostname = config.defaultHostname - - if (!context) { - // automatically detect supported kubectl context if not explicitly configured - const kubeConfig = await getKubeConfig() - const currentContext = kubeConfig["current-context"] - - if (currentContext && supportedContexts.includes(currentContext)) { - // prefer current context if set and supported - context = currentContext - log.debug({ section: name, msg: `Using current context: ${context}` }) - } else if (kubeConfig.contexts) { - const availableContexts = kubeConfig.contexts.map(c => c.name) - - for (const supportedContext of supportedContexts) { - if (availableContexts.includes(supportedContext)) { - context = supportedContext - log.debug({ section: name, msg: `Using detected context: ${context}` }) - break - } - } - } - } - - if (!context) { - context = supportedContexts[0] - log.debug({ section: name, msg: `No kubectl context auto-detected, using default: ${context}` }) - } - - if (context === "minikube") { - await execa("minikube", ["config", "set", "WantUpdateNotification", "false"]) - - if (!defaultHostname) { - // use the nip.io service to give a hostname to the instance, if none is explicitly configured - const minikubeIp = await execa.stdout("minikube", ["ip"]) - defaultHostname = `${projectName}.${minikubeIp}.nip.io` - } - - await Promise.all([ - // TODO: wait for ingress addon to be ready, if it was previously disabled - execa("minikube", ["addons", "enable", "ingress"]), - setMinikubeDockerEnv(), - ]) - } else { - if (!defaultHostname) { - defaultHostname = `${projectName}.local.app.garden` - } - } - - config = { - name: config.name, - context, - defaultHostname, - deploymentRegistry: { - hostname: "foo.garden", // this is not used by this plugin, but required by the base plugin - namespace: "_", - }, - forceSsl: false, - imagePullSecrets: config.imagePullSecrets, - ingressHttpPort: 80, - ingressHttpsPort: 443, - ingressClass: "nginx", - namespace: config.namespace || projectName, - tlsCertificates: config.tlsCertificates, - _system: config._system, - } - - return { name: config.name, config } - } - - // override the environment configuration steps - plugin.actions!.getEnvironmentStatus = getLocalEnvironmentStatus - plugin.actions!.prepareEnvironment = prepareLocalEnvironment - - // no need to push before deploying locally - delete plugin.moduleActions!.container.pushModule - - return plugin -} diff --git a/garden-service/src/plugins/kubernetes/local/config.ts b/garden-service/src/plugins/kubernetes/local/config.ts new file mode 100644 index 0000000000..3f70563ac8 --- /dev/null +++ b/garden-service/src/plugins/kubernetes/local/config.ts @@ -0,0 +1,146 @@ +/* + * Copyright (C) 2018 Garden Technologies, Inc. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +import * as execa from "execa" +import { safeLoad } from "js-yaml" +import * as Joi from "joi" +import { join } from "path" +import { readFile } from "fs-extra" +import { homedir } from "os" +import { KubernetesBaseConfig, kubernetesConfigBase } from "../kubernetes" +import { ConfigureProviderParams } from "../../../types/plugin/params" + +// TODO: split this into separate plugins to handle Docker for Mac and Minikube + +// note: this is in order of preference, in case neither is set as the current kubectl context +// and none is explicitly configured in the garden.yml +const supportedContexts = ["docker-for-desktop", "minikube"] +const kubeConfigPath = join(homedir(), ".kube", "config") + +async function getKubeConfig(): Promise { + try { + return safeLoad((await readFile(kubeConfigPath)).toString()) + } catch { + return {} + } +} + +/** + * Automatically set docker environment variables for minikube + * TODO: it would be better to explicitly provide those to docker instead of using process.env + */ +async function setMinikubeDockerEnv() { + const minikubeEnv = await execa.stdout("minikube", ["docker-env", "--shell=bash"]) + for (const line of minikubeEnv.split("\n")) { + const matched = line.match(/^export (\w+)="(.+)"$/) + if (matched) { + process.env[matched[1]] = matched[2] + } + } +} + +export interface LocalKubernetesConfig extends KubernetesBaseConfig { + _system?: Symbol + setupIngressController: string | null +} + +export const configSchema = kubernetesConfigBase + .keys({ + namespace: Joi.string() + .default(undefined, "") + .description( + "Specify which namespace to deploy services to (defaults to the project name). " + + "Note that the framework generates other namespaces as well with this name as a prefix.", + ), + setupIngressController: Joi.string() + .allow("nginx", false, null) + .default("nginx") + .description("Set this to null or false to skip installing/enabling the `nginx` ingress controller."), + _system: Joi.any().meta({ internal: true }), + }) + .description("The provider configuration for the local-kubernetes plugin.") + +export async function configureProvider({ config, log, projectName }: ConfigureProviderParams) { + let context = config.context + let defaultHostname = config.defaultHostname + let setupIngressController = config.setupIngressController + + if (!context) { + // automatically detect supported kubectl context if not explicitly configured + const kubeConfig = await getKubeConfig() + const currentContext = kubeConfig["current-context"] + + if (currentContext && supportedContexts.includes(currentContext)) { + // prefer current context if set and supported + context = currentContext + log.debug({ section: config.name, msg: `Using current context: ${context}` }) + } else if (kubeConfig.contexts) { + const availableContexts = kubeConfig.contexts.map(c => c.name) + + for (const supportedContext of supportedContexts) { + if (availableContexts.includes(supportedContext)) { + context = supportedContext + log.debug({ section: config.name, msg: `Using detected context: ${context}` }) + break + } + } + } + } + + if (!context) { + context = supportedContexts[0] + log.debug({ section: config.name, msg: `No kubectl context auto-detected, using default: ${context}` }) + } + + if (context === "minikube") { + await execa("minikube", ["config", "set", "WantUpdateNotification", "false"]) + + if (!defaultHostname) { + // use the nip.io service to give a hostname to the instance, if none is explicitly configured + const minikubeIp = await execa.stdout("minikube", ["ip"]) + defaultHostname = `${projectName}.${minikubeIp}.nip.io` + } + + if (config.setupIngressController === "nginx") { + log.silly("Using minikube's ingress addon") + await execa("minikube", ["addons", "enable", "ingress"]) + // make sure the prepare handler doesn't also set up the ingress controller + setupIngressController = null + } + + await setMinikubeDockerEnv() + + } else { + if (!defaultHostname) { + defaultHostname = `${projectName}.local.app.garden` + } + } + + const ingressClass = config.ingressClass || config.setupIngressController || undefined + + config = { + name: config.name, + context, + defaultHostname, + deploymentRegistry: { + hostname: "foo.garden", // this is not used by this plugin, but required by the base plugin + namespace: "_", + }, + forceSsl: false, + imagePullSecrets: config.imagePullSecrets, + ingressHttpPort: 80, + ingressHttpsPort: 443, + ingressClass, + namespace: config.namespace || projectName, + setupIngressController, + tlsCertificates: config.tlsCertificates, + _system: config._system, + } + + return { name: config.name, config } +} diff --git a/garden-service/src/plugins/kubernetes/local/local.ts b/garden-service/src/plugins/kubernetes/local/local.ts new file mode 100644 index 0000000000..b144077e26 --- /dev/null +++ b/garden-service/src/plugins/kubernetes/local/local.ts @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2018 Garden Technologies, Inc. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +import { GardenPlugin } from "../../../types/plugin/plugin" +import { gardenPlugin as k8sPlugin } from "../kubernetes" +import { getLocalEnvironmentStatus, prepareLocalEnvironment } from "../init" +import { configureProvider, configSchema } from "./config" + +export const name = "local-kubernetes" + +export function gardenPlugin(): GardenPlugin { + const plugin = k8sPlugin() + + plugin.configSchema = configSchema + + plugin.actions!.configureProvider = configureProvider + + // override the environment configuration steps + plugin.actions!.getEnvironmentStatus = getLocalEnvironmentStatus + plugin.actions!.prepareEnvironment = prepareLocalEnvironment + + // no need to push before deploying locally + delete plugin.moduleActions!.container.pushModule + + return plugin +} diff --git a/garden-service/src/plugins/kubernetes/status.ts b/garden-service/src/plugins/kubernetes/status.ts index e85c77f347..24e5fe686d 100644 --- a/garden-service/src/plugins/kubernetes/status.ts +++ b/garden-service/src/plugins/kubernetes/status.ts @@ -184,22 +184,24 @@ export async function checkDeploymentStatus( if (event.involvedObject.kind === "Pod") { const logs = await getPodLogs(api, namespace, [event.involvedObject.name]) - out.logs = dedent` - - kubectl -n ${namespace} --context=${api.context} logs ${event.involvedObject.name} + if (logs) { + out.logs = dedent` + + kubectl -n ${namespace} --context=${api.context} logs ${event.involvedObject.name} - ${logs} - ` + ` + logs + } } else { const pods = await getPods(api, namespace, statusRes.spec.selector.matchLabels) const logs = await getPodLogs(api, namespace, pods.map(pod => pod.metadata.name)) - out.logs = dedent` - - kubectl -n ${namespace} --context=${api.context} logs ${obj.kind.toLowerCase()}/${obj.metadata.name} + if (logs) { + out.logs = dedent` + + kubectl -n ${namespace} --context=${api.context} logs ${obj.kind.toLowerCase()}/${obj.metadata.name} - ${logs} - ` + ` + logs + } } return out diff --git a/garden-service/src/plugins/plugins.ts b/garden-service/src/plugins/plugins.ts index 822b78e156..0708f3647e 100644 --- a/garden-service/src/plugins/plugins.ts +++ b/garden-service/src/plugins/plugins.ts @@ -13,7 +13,7 @@ const container = require("./container/container") const gcf = require("./google/google-cloud-functions") const localGcf = require("./local/local-google-cloud-functions") const kubernetes = require("./kubernetes/kubernetes") -const localKubernetes = require("./kubernetes/local") +const localKubernetes = require("./kubernetes/local/local") const npmPackage = require("./npm-package") const gae = require("./google/google-app-engine") const openfaas = require("./openfaas/openfaas") diff --git a/garden-service/src/types/plugin/params.ts b/garden-service/src/types/plugin/params.ts index f3835fd0d5..6ca0f1a662 100644 --- a/garden-service/src/types/plugin/params.ts +++ b/garden-service/src/types/plugin/params.ts @@ -19,7 +19,7 @@ import { EnvironmentStatus, ServiceLogEntry, environmentStatusSchema } from "./o import { moduleConfigSchema } from "../../config/module" import { testConfigSchema } from "../../config/test" import { taskSchema } from "../../config/task" -import { ProviderConfig } from "../../config/project" +import { ProviderConfig, projectNameSchema, providerConfigBaseSchema } from "../../config/project" export interface PluginActionContextParams { ctx: PluginContext @@ -32,6 +32,7 @@ export interface PluginActionParamsBase extends PluginActionContextParams { // Note: not specifying this further because we will later remove it from the API const logEntrySchema = Joi.object() .description("Logging context handler that the handler can use to log messages and progress.") + .required() const actionParamsSchema = Joi.object() .keys({ @@ -73,14 +74,14 @@ const taskActionParamsSchema = moduleActionParamsSchema */ export interface ConfigureProviderParams { config: T + log: LogEntry + projectName: string } export const configureProviderParamsSchema = Joi.object() .keys({ - ctx: pluginContextSchema - .required(), + config: providerConfigBaseSchema.required(), log: logEntrySchema, - moduleConfig: moduleConfigSchema - .required(), + projectName: projectNameSchema, }) export interface GetEnvironmentStatusParams extends PluginActionParamsBase { } @@ -146,14 +147,14 @@ export const describeModuleTypeParamsSchema = Joi.object() export interface ConfigureModuleParams { ctx: PluginContext - logEntry?: LogEntry + log: LogEntry moduleConfig: T["_ConfigType"] } export const configureModuleParamsSchema = Joi.object() .keys({ ctx: pluginContextSchema .required(), - logEntry: logEntrySchema, + log: logEntrySchema, moduleConfig: moduleConfigSchema .required(), }) diff --git a/garden-service/static/kubernetes/system/kubernetes-dashboard/garden.yml b/garden-service/static/kubernetes/system/kubernetes-dashboard/garden.yml index ea5a68338b..24ddc617eb 100644 --- a/garden-service/static/kubernetes/system/kubernetes-dashboard/garden.yml +++ b/garden-service/static/kubernetes/system/kubernetes-dashboard/garden.yml @@ -12,3 +12,6 @@ module: # Don't use this config for remote clusters! enableInsecureLogin: true enableSkipLogin: true + # We should remove this after https://github.com/helm/charts/issues/9776 is resolved. + rbac: + clusterAdminRole: true diff --git a/garden-service/test/src/plugins/container.ts b/garden-service/test/src/plugins/container.ts index 0105fa650e..f197c2cc3b 100644 --- a/garden-service/test/src/plugins/container.ts +++ b/garden-service/test/src/plugins/container.ts @@ -71,7 +71,7 @@ describe("plugins.container", () => { }) async function getTestModule(moduleConfig: ContainerModuleConfig) { - const parsed = await configure({ ctx, moduleConfig }) + const parsed = await configure({ ctx, moduleConfig, log }) const graph = await garden.getConfigGraph() return moduleFromConfig(garden, graph, parsed) } @@ -248,7 +248,7 @@ describe("plugins.container", () => { testConfigs: [], } - const result = await configure({ ctx, moduleConfig }) + const result = await configure({ ctx, moduleConfig, log }) expect(result).to.eql({ allowPublish: false, @@ -358,7 +358,7 @@ describe("plugins.container", () => { moduleConfig.spec.dockerfile = "path/to/non-existing/Dockerfile" await expectError( - () => configure({ ctx, moduleConfig }), + () => configure({ ctx, moduleConfig, log }), "configuration", ) }) @@ -415,7 +415,7 @@ describe("plugins.container", () => { } await expectError( - () => configure({ ctx, moduleConfig }), + () => configure({ ctx, moduleConfig, log }), "configuration", ) }) @@ -467,7 +467,7 @@ describe("plugins.container", () => { } await expectError( - () => configure({ ctx, moduleConfig }), + () => configure({ ctx, moduleConfig, log }), "configuration", ) }) @@ -516,7 +516,7 @@ describe("plugins.container", () => { } await expectError( - () => configure({ ctx, moduleConfig }), + () => configure({ ctx, moduleConfig, log }), "configuration", ) }) diff --git a/garden-service/test/src/plugins/kubernetes/container/ingress.ts b/garden-service/test/src/plugins/kubernetes/container/ingress.ts index 25195057ea..beba849a2d 100644 --- a/garden-service/test/src/plugins/kubernetes/container/ingress.ts +++ b/garden-service/test/src/plugins/kubernetes/container/ingress.ts @@ -356,7 +356,7 @@ describe("createIngresses", () => { } const ctx = await garden.getPluginContext("container") - const parsed = await configure({ ctx, moduleConfig }) + const parsed = await configure({ ctx, moduleConfig, log: garden.log }) const graph = await garden.getConfigGraph() const module = await moduleFromConfig(garden, graph, parsed) diff --git a/garden-service/test/src/plugins/kubernetes/helm/config.ts b/garden-service/test/src/plugins/kubernetes/helm/config.ts index 6ad3a479aa..15fe9b9733 100644 --- a/garden-service/test/src/plugins/kubernetes/helm/config.ts +++ b/garden-service/test/src/plugins/kubernetes/helm/config.ts @@ -6,14 +6,17 @@ import { TestGarden, dataDir, makeTestGarden, expectError } from "../../../../he import { PluginContext } from "../../../../../src/plugin-context" import { validateHelmModule } from "../../../../../src/plugins/kubernetes/helm/config" import { deline } from "../../../../../src/util/string" +import { LogEntry } from "../../../../../src/logger/log-entry" describe("validateHelmModule", () => { let garden: TestGarden let ctx: PluginContext + let log: LogEntry before(async () => { const projectRoot = resolve(dataDir, "test-projects", "helm") garden = await makeTestGarden(projectRoot) + log = garden.log ctx = garden.getPluginContext("local-kubernetes") await garden.resolveModuleConfigs() }) @@ -119,7 +122,7 @@ describe("validateHelmModule", () => { it("should not return a serviceConfig if skipDeploy=true", async () => { const moduleConfig = getModuleConfig("api") moduleConfig.spec.skipDeploy = true - const config = await validateHelmModule({ ctx, moduleConfig }) + const config = await validateHelmModule({ ctx, moduleConfig, log }) expect(config.serviceConfigs).to.eql([]) }) @@ -127,7 +130,7 @@ describe("validateHelmModule", () => { it("should add the module specified under 'base' as a build dependency", async () => { const moduleConfig = getModuleConfig("postgres") moduleConfig.spec.base = "foo" - const config = await validateHelmModule({ ctx, moduleConfig }) + const config = await validateHelmModule({ ctx, moduleConfig, log }) expect(config.build.dependencies).to.eql([ { name: "foo", copy: [{ source: "*", target: "." }] }, @@ -138,7 +141,7 @@ describe("validateHelmModule", () => { const moduleConfig = getModuleConfig("postgres") moduleConfig.build.dependencies = [{ name: "foo", copy: [] }] moduleConfig.spec.base = "foo" - const config = await validateHelmModule({ ctx, moduleConfig }) + const config = await validateHelmModule({ ctx, moduleConfig, log }) expect(config.build.dependencies).to.eql([ { name: "foo", copy: [{ source: "*", target: "." }] }, @@ -150,7 +153,7 @@ describe("validateHelmModule", () => { moduleConfig.spec.tasks = [ { name: "my-task", resource: { kind: "Deployment", containerModule: "foo" } }, ] - const config = await validateHelmModule({ ctx, moduleConfig }) + const config = await validateHelmModule({ ctx, moduleConfig, log }) expect(config.build.dependencies).to.eql([ { name: "foo", copy: [] }, @@ -162,7 +165,7 @@ describe("validateHelmModule", () => { moduleConfig.spec.tests = [ { name: "my-task", resource: { kind: "Deployment", containerModule: "foo" } }, ] - const config = await validateHelmModule({ ctx, moduleConfig }) + const config = await validateHelmModule({ ctx, moduleConfig, log }) expect(config.build.dependencies).to.eql([ { name: "foo", copy: [] }, @@ -173,7 +176,7 @@ describe("validateHelmModule", () => { const moduleConfig = getModuleConfig("api") moduleConfig.spec.base = "foo" await expectError( - () => validateHelmModule({ ctx, moduleConfig }), + () => validateHelmModule({ ctx, moduleConfig, log }), err => expect(err.message).to.equal(deline` Helm module 'api' both contains sources and specifies a base module. Since Helm charts cannot currently be merged, please either remove the sources or @@ -186,7 +189,7 @@ describe("validateHelmModule", () => { const moduleConfig = getModuleConfig("postgres") delete moduleConfig.spec.chart await expectError( - () => validateHelmModule({ ctx, moduleConfig }), + () => validateHelmModule({ ctx, moduleConfig, log }), err => expect(err.message).to.equal(deline` Chart neither specifies a chart name, base module, nor contains chart sources at \`chartPath\`. `),