diff --git a/docs/reference/module-types/helm.md b/docs/reference/module-types/helm.md index 881d28231f..cf41810e31 100644 --- a/docs/reference/module-types/helm.md +++ b/docs/reference/module-types/helm.md @@ -703,12 +703,28 @@ The chart version to deploy. ### `values` -Map of values to pass to Helm when rendering the templates. May include arrays and nested objects. +Map of values to pass to Helm when rendering the templates. May include arrays and nested objects. When specified, these take precedence over the values in the `values.yaml` file (or the files specified in `valueFiles`). | Type | Required | Default | | -------- | -------- | ------- | | `object` | No | `{}` | +### `valueFiles` + +Specify value files to use when rendering the Helm chart. These will take precedence over the `values.yaml` file +bundled in the Helm chart, and should be specified in ascending order of precedence. Meaning, the last file in +this list will have the highest precedence. + +If you _also_ specify keys under the `values` field, those will effectively be added as another file at the end +of this list, so they will take precedence over other files listed here. + +Note that the paths here should be relative to the _module_ root, and the files should be contained in +your module directory. + +| Type | Required | Default | +| --------------- | -------- | ------- | +| `array[string]` | No | `[]` | + ## Complete YAML schema ```yaml @@ -768,6 +784,7 @@ tests: env: {} version: values: {} +valueFiles: [] ``` ## Outputs diff --git a/docs/using-garden/using-helm-charts.md b/docs/using-garden/using-helm-charts.md index df7a89ac70..bacda1b83d 100644 --- a/docs/using-garden/using-helm-charts.md +++ b/docs/using-garden/using-helm-charts.md @@ -108,6 +108,53 @@ tests: Instead of the top-level `serviceResource` you can also add a `resource` field with the same schema to any individual task or test specification. This can be useful if you have different containers in the chart that you want to use for different scenarios. +## Providing values to the Helm chart + +In most cases you'll need to provide some parameters to the Helm chart you're using. The simplest way to do this is via the `values`field: + +```yaml +kind: Module +type: helm +name: my-helm-module +... +values: + some: + key: some-value +``` + +This will effectively create a new YAML with the supplied values and pass it to Helm when rendering/deploying the chart. This is particularly handy when you want to template in the values (see the next section for a good example). + +You can also provide you own value files, which will work much the same way. You just need to list the paths to them (relative to the module root, i.e. the directory containing the `garden.yml` file) and they will be supplied to Helm when rendering/deploying. For example: + +```yaml +# garden.yml +kind: Module +type: helm +name: my-helm-module +... +valueFiles: + - values.default.yaml + - values.${environment.name}.yaml +``` + +```yaml +# values.default.yaml +some: + key: default-value +other: + key: other-default +``` + +```yaml +# values.prod.yaml +some: + key: prod-value +``` + +In this example, `some.key` is set to `"prod-value"` for the `prod` environment, and `other.key` maintains the default value set in `values.default.yaml`. + +If you also set the `values` field in the Module configuration, the values there take precedence over both of the value files. + ## Linking container modules and Helm modules When your project also contains one or more `container` modules that build the images used by a `helm` module, you want to make sure the `container`s are built ahead of deploying the Helm chart, and that the correct image tag is used when deploying. The `vote-helm/worker` module and the corresponding `worker-image` module provide a simple example: @@ -132,7 +179,7 @@ type: container name: worker-image ``` -Here the `worker` module specifies the image as a build dependency, and additionally injects the `worker-image` version into the Helm chart via the `values` field. Note that the shape of the chart's `values.yml` file will dictate how exactly you provide the image version/tag to the chart (this example is based on the default template generated by `helm create`), so be sure to consult the reference for the chart in question. +Here the `worker` module specifies the image as a build dependency, and additionally injects the `worker-image` version into the Helm chart via the `values` field. Note that the shape of the chart's `values.yaml` file will dictate how exactly you provide the image version/tag to the chart (this example is based on the default template generated by `helm create`), so be sure to consult the reference for the chart in question. Notice that this can also work if you have multiple containers in a single chart. You just add them all as build dependencies, and the appropriate reference under `values`. diff --git a/garden-service/src/plugins/kubernetes/helm/build.ts b/garden-service/src/plugins/kubernetes/helm/build.ts index 2fe9133910..247fe76e2f 100644 --- a/garden-service/src/plugins/kubernetes/helm/build.ts +++ b/garden-service/src/plugins/kubernetes/helm/build.ts @@ -7,9 +7,8 @@ */ import { HelmModule } from "./config" -import { containsSource, getChartPath, getValuesPath, getBaseModule } from "./common" +import { containsSource, getChartPath, getGardenValuesPath, getBaseModule } from "./common" import { helm } from "./helm-cli" -import { safeLoad } from "js-yaml" import { dumpYaml } from "../../../util/util" import { LogEntry } from "../../../logger/log-entry" import { getNamespace } from "../namespace" @@ -45,16 +44,12 @@ export async function buildHelmModule({ ctx, module, log }: BuildModuleParamsctx const namespace = await getNamespace({ log, @@ -57,7 +58,7 @@ export async function getChartResources(ctx: PluginContext, module: Module, log: "template", "--name", releaseName, "--namespace", namespace, - "--values", valuesPath, + ...await getValueFileArgs(module), chartPath, )) @@ -125,8 +126,24 @@ export async function getChartPath(module: HelmModule) { /** * Get the path to the values file that we generate (garden-values.yml) within the chart directory. */ -export function getValuesPath(chartPath: string) { - return join(chartPath, "garden-values.yml") +export function getGardenValuesPath(chartPath: string) { + return join(chartPath, gardenValuesFilename) +} + +/** + * Get the value files arguments that should be applied to any helm install/render command. + */ +export async function getValueFileArgs(module: HelmModule) { + const chartPath = await getChartPath(module) + const gardenValuesPath = getGardenValuesPath(chartPath) + + // The garden-values.yml file (which is created from the `values` field in the module config) takes precedence, + // so it's added to the end of the list. + const valueFiles = module.spec.valueFiles + .map(f => resolve(module.buildPath, f)) + .concat([gardenValuesPath]) + + return flatten(valueFiles.map(f => ["--values", f])) } /** @@ -276,7 +293,6 @@ async function renderHelmTemplateString( ctx: PluginContext, log: LogEntry, module: Module, chartPath: string, value: string, ): Promise { const tempFilePath = join(chartPath, "templates", cryptoRandomString({ length: 16 })) - const valuesPath = getValuesPath(chartPath) const k8sCtx = ctx const namespace = await getNamespace({ log, @@ -294,7 +310,7 @@ async function renderHelmTemplateString( "template", "--name", releaseName, "--namespace", namespace, - "--values", valuesPath, + ...await getValueFileArgs(module), "-x", tempFilePath, chartPath, )) diff --git a/garden-service/src/plugins/kubernetes/helm/config.ts b/garden-service/src/plugins/kubernetes/helm/config.ts index 4d2554adba..dfa21ee85c 100644 --- a/garden-service/src/plugins/kubernetes/helm/config.ts +++ b/garden-service/src/plugins/kubernetes/helm/config.ts @@ -20,7 +20,7 @@ import { import { Module, FileCopySpec } from "../../../types/module" import { containsSource, getReleaseName } from "./common" import { ConfigurationError } from "../../../exceptions" -import { deline } from "../../../util/string" +import { deline, dedent } from "../../../util/string" import { HotReloadableKind, hotReloadableKinds } from "../hot-reload" import { BaseTestSpec, baseTestSpecSchema } from "../../../config/test" import { BaseTaskSpec, baseTaskSpecSchema } from "../../../config/task" @@ -142,6 +142,7 @@ export interface HelmServiceSpec extends ServiceSpec { tests: HelmTestSpec[] version?: string values: DeepPrimitiveMap + valueFiles: string[] } export type HelmService = Service @@ -209,9 +210,23 @@ export const helmModuleSpecSchema = joi.object().keys({ values: joi.object() .pattern(/.+/, parameterValueSchema) .default(() => ({}), "{}") - .description( - "Map of values to pass to Helm when rendering the templates. May include arrays and nested objects.", - ), + .description(deline` + Map of values to pass to Helm when rendering the templates. May include arrays and nested objects. + When specified, these take precedence over the values in the \`values.yaml\` file (or the files specified + in \`valueFiles\`). + `), + valueFiles: joiArray(joi.string().posixPath({ subPathOnly: true })) + .description(dedent` + Specify value files to use when rendering the Helm chart. These will take precedence over the \`values.yaml\` file + bundled in the Helm chart, and should be specified in ascending order of precedence. Meaning, the last file in + this list will have the highest precedence. + + If you _also_ specify keys under the \`values\` field, those will effectively be added as another file at the end + of this list, so they will take precedence over other files listed here. + + Note that the paths here should be relative to the _module_ root, and the files should be contained in + your module directory. + `), }) export async function validateHelmModule({ moduleConfig }: ConfigureModuleParams) diff --git a/garden-service/src/plugins/kubernetes/helm/deployment.ts b/garden-service/src/plugins/kubernetes/helm/deployment.ts index 522274a152..4240cf4014 100644 --- a/garden-service/src/plugins/kubernetes/helm/deployment.ts +++ b/garden-service/src/plugins/kubernetes/helm/deployment.ts @@ -13,11 +13,11 @@ import { helm } from "./helm-cli" import { HelmModule } from "./config" import { getChartPath, - getValuesPath, getReleaseName, getChartResources, findServiceResource, getServiceResourceSpec, + getValueFileArgs, } from "./common" import { getReleaseStatus, getServiceStatus } from "./status" import { configureHotReload, HotReloadableResource } from "../hot-reload" @@ -46,7 +46,6 @@ export async function deployService( const k8sCtx = ctx const provider = k8sCtx.provider const chartPath = await getChartPath(module) - const valuesPath = getValuesPath(chartPath) const namespace = await getAppNamespace(k8sCtx, log, provider) const context = provider.config.context const releaseName = getReleaseName(module) @@ -59,7 +58,7 @@ export async function deployService( "install", chartPath, "--name", releaseName, "--namespace", namespace, - "--values", valuesPath, + ...await getValueFileArgs(module), // Make sure chart gets purged if it fails to install "--atomic", "--timeout", "600", @@ -74,7 +73,7 @@ export async function deployService( "upgrade", releaseName, chartPath, "--install", "--namespace", namespace, - "--values", valuesPath, + ...await getValueFileArgs(module), ] if (force) { upgradeArgs.push("--force") diff --git a/garden-service/src/plugins/openfaas/openfaas.ts b/garden-service/src/plugins/openfaas/openfaas.ts index 3f2997f2f5..f20e3addf2 100644 --- a/garden-service/src/plugins/openfaas/openfaas.ts +++ b/garden-service/src/plugins/openfaas/openfaas.ts @@ -175,6 +175,7 @@ async function configureProvider( }, securityContext: false, }, + valueFiles: [], }, } diff --git a/garden-service/test/unit/src/plugins/kubernetes/helm/common.ts b/garden-service/test/unit/src/plugins/kubernetes/helm/common.ts index afd72d3488..4d08c6aa09 100644 --- a/garden-service/test/unit/src/plugins/kubernetes/helm/common.ts +++ b/garden-service/test/unit/src/plugins/kubernetes/helm/common.ts @@ -8,10 +8,11 @@ import { getChartResources, getChartPath, getReleaseName, - getValuesPath, + getGardenValuesPath, findServiceResource, getResourceContainer, getBaseModule, + getValueFileArgs, } from "../../../../../../src/plugins/kubernetes/helm/common" import { PluginContext } from "../../../../../../src/plugin-context" import { LogEntry } from "../../../../../../src/logger/log-entry" @@ -22,6 +23,7 @@ import { HotReloadableResource } from "../../../../../../src/plugins/kubernetes/ import { getServiceResourceSpec } from "../../../../../../src/plugins/kubernetes/helm/common" import { ConfigGraph } from "../../../../../../src/config-graph" import { Provider } from "../../../../../../src/config/provider" +import { buildHelmModule } from "../../../../../../src/plugins/kubernetes/helm/build" const helmProvider: Provider = { name: "local-kubernetes", @@ -559,9 +561,30 @@ describe("Helm common functions", () => { }) }) - describe("getValuesPath", () => { + describe("getGardenValuesPath", () => { it("should add garden-values.yml to the specified path", () => { - expect(getValuesPath(ctx.projectRoot)).to.equal(resolve(ctx.projectRoot, "garden-values.yml")) + expect(getGardenValuesPath(ctx.projectRoot)).to.equal(resolve(ctx.projectRoot, "garden-values.yml")) + }) + }) + + describe("getValueFileArgs", () => { + it("should return just garden-values.yml if no valueFiles are configured", async () => { + const module = await graph.getModule("api") + module.spec.valueFiles = [] + const gardenValuesPath = getGardenValuesPath(module.buildPath) + expect(await getValueFileArgs(module)).to.eql(["--values", gardenValuesPath]) + }) + + it("should return a --values arg for each valueFile configured", async () => { + const module = await graph.getModule("api") + module.spec.valueFiles = ["foo.yaml", "bar.yaml"] + const gardenValuesPath = getGardenValuesPath(module.buildPath) + + expect(await getValueFileArgs(module)).to.eql([ + "--values", resolve(module.buildPath, "foo.yaml"), + "--values", resolve(module.buildPath, "bar.yaml"), + "--values", gardenValuesPath, + ]) }) }) @@ -702,6 +725,7 @@ describe("Helm common functions", () => { it("should resolve template string for resource name", async () => { const module = await graph.getModule("postgres") + await buildHelmModule({ ctx, module, log }) const chartResources = await getChartResources(ctx, module, log) module.spec.serviceResource.name = `{{ template "postgresql.master.fullname" . }}` const result = await findServiceResource({ ctx, log, module, chartResources }) diff --git a/garden-service/test/unit/src/plugins/kubernetes/helm/config.ts b/garden-service/test/unit/src/plugins/kubernetes/helm/config.ts index a2e423f6ff..1e7bfd4936 100644 --- a/garden-service/test/unit/src/plugins/kubernetes/helm/config.ts +++ b/garden-service/test/unit/src/plugins/kubernetes/helm/config.ts @@ -91,6 +91,7 @@ describe("validateHelmModule", () => { ], }, }, + valueFiles: [], }, }, ], @@ -122,6 +123,7 @@ describe("validateHelmModule", () => { ], }, }, + valueFiles: [], }, testConfigs: [], type: "helm",