Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(helm): add valueFiles field to specify custom value files #1099

Merged
merged 3 commits into from
Aug 19, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 18 additions & 1 deletion docs/reference/module-types/helm.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -768,6 +784,7 @@ tests:
env: {}
version:
values: {}
valueFiles: []
```

## Outputs
Expand Down
49 changes: 48 additions & 1 deletion docs/using-garden/using-helm-charts.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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`.

Expand Down
11 changes: 3 additions & 8 deletions garden-service/src/plugins/kubernetes/helm/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -45,16 +44,12 @@ export async function buildHelmModule({ ctx, module, log }: BuildModuleParams<He

// create the values.yml file (merge the configured parameters into the default values)
log.debug("Preparing chart...")
const chartValues = safeLoad(await helm(namespace, context, log, "inspect", "values", chartPath)) || {}

// Merge with the base module's values, if applicable
const specValues = baseModule ? jsonMerge(baseModule.spec.values, module.spec.values) : module.spec.values

const mergedValues = jsonMerge(chartValues, specValues)

const valuesPath = getValuesPath(chartPath)
const valuesPath = getGardenValuesPath(chartPath)
log.silly(`Writing chart values to ${valuesPath}`)
await dumpYaml(valuesPath, mergedValues)
await dumpYaml(valuesPath, specValues)

return { fresh: true }
}
Expand Down
32 changes: 24 additions & 8 deletions garden-service/src/plugins/kubernetes/helm/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/

import { find, isEmpty, isPlainObject } from "lodash"
import { join } from "path"
import { find, isEmpty, isPlainObject, flatten } from "lodash"
import { join, resolve } from "path"
import { pathExists, writeFile, remove } from "fs-extra"
import cryptoRandomString = require("crypto-random-string")
import { apply as jsonMerge } from "json-merge-patch"
Expand All @@ -29,6 +29,8 @@ import { KubernetesPluginContext } from "../config"
import { RunResult } from "../../../types/plugin/base"
import { MAX_RUN_RESULT_OUTPUT_LENGTH } from "../constants"

const gardenValuesFilename = "garden-values.yml"

/**
* Returns true if the specified Helm module contains a template (as opposed to just referencing a remote template).
*/
Expand All @@ -42,7 +44,6 @@ export async function containsSource(config: HelmModuleConfig) {
*/
export async function getChartResources(ctx: PluginContext, module: Module, log: LogEntry) {
const chartPath = await getChartPath(module)
const valuesPath = getValuesPath(chartPath)
const k8sCtx = <KubernetesPluginContext>ctx
const namespace = await getNamespace({
log,
Expand All @@ -57,7 +58,7 @@ export async function getChartResources(ctx: PluginContext, module: Module, log:
"template",
"--name", releaseName,
"--namespace", namespace,
"--values", valuesPath,
...await getValueFileArgs(module),
chartPath,
))

Expand Down Expand Up @@ -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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you add a comment saying that the gardenValuesPath takes precedence over the other value files and is therefore appended last.

.map(f => resolve(module.buildPath, f))
.concat([gardenValuesPath])

return flatten(valueFiles.map(f => ["--values", f]))
}

/**
Expand Down Expand Up @@ -276,7 +293,6 @@ async function renderHelmTemplateString(
ctx: PluginContext, log: LogEntry, module: Module, chartPath: string, value: string,
): Promise<string> {
const tempFilePath = join(chartPath, "templates", cryptoRandomString({ length: 16 }))
const valuesPath = getValuesPath(chartPath)
const k8sCtx = <KubernetesPluginContext>ctx
const namespace = await getNamespace({
log,
Expand All @@ -294,7 +310,7 @@ async function renderHelmTemplateString(
"template",
"--name", releaseName,
"--namespace", namespace,
"--values", valuesPath,
...await getValueFileArgs(module),
"-x", tempFilePath,
chartPath,
))
Expand Down
23 changes: 19 additions & 4 deletions garden-service/src/plugins/kubernetes/helm/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -142,6 +142,7 @@ export interface HelmServiceSpec extends ServiceSpec {
tests: HelmTestSpec[]
version?: string
values: DeepPrimitiveMap
valueFiles: string[]
}

export type HelmService = Service<HelmModule, ContainerModule>
Expand Down Expand Up @@ -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`
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't this be deline? Noticed some extra new lines in the generated doc above (although not visible to when rendered as markdown).

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, that'll bunch up all the paragraphs, right? It's too much text for a single paragraph imo.

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<HelmModule>)
Expand Down
7 changes: 3 additions & 4 deletions garden-service/src/plugins/kubernetes/helm/deployment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -46,7 +46,6 @@ export async function deployService(
const k8sCtx = <KubernetesPluginContext>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)
Expand All @@ -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",
Expand All @@ -74,7 +73,7 @@ export async function deployService(
"upgrade", releaseName, chartPath,
"--install",
"--namespace", namespace,
"--values", valuesPath,
...await getValueFileArgs(module),
]
if (force) {
upgradeArgs.push("--force")
Expand Down
1 change: 1 addition & 0 deletions garden-service/src/plugins/openfaas/openfaas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,7 @@ async function configureProvider(
},
securityContext: false,
},
valueFiles: [],
},
}

Expand Down
30 changes: 27 additions & 3 deletions garden-service/test/unit/src/plugins/kubernetes/helm/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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",
Expand Down Expand Up @@ -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,
])
})
})

Expand Down Expand Up @@ -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 })
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ describe("validateHelmModule", () => {
],
},
},
valueFiles: [],
},
},
],
Expand Down Expand Up @@ -122,6 +123,7 @@ describe("validateHelmModule", () => {
],
},
},
valueFiles: [],
},
testConfigs: [],
type: "helm",
Expand Down