-
Notifications
You must be signed in to change notification settings - Fork 267
/
config.ts
296 lines (260 loc) · 11 KB
/
config.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
/*
* Copyright (C) 2018-2020 Garden Technologies, Inc. <info@garden.io>
*
* 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 {
joiPrimitive,
joiArray,
joiIdentifier,
joiUserIdentifier,
DeepPrimitiveMap,
joi,
joiModuleIncludeDirective,
} from "../../../config/common"
import { GardenModule } from "../../../types/module"
import { containsSource } from "./common"
import { ConfigurationError } from "../../../exceptions"
import { deline, dedent } from "../../../util/string"
import { Service } from "../../../types/service"
import { ContainerModule } from "../../container/config"
import { baseBuildSpecSchema } from "../../../config/module"
import { ConfigureModuleParams, ConfigureModuleResult } from "../../../types/plugin/module/configure"
import {
serviceResourceSchema,
kubernetesTaskSchema,
kubernetesTestSchema,
ServiceResourceSpec,
KubernetesTestSpec,
KubernetesTaskSpec,
namespaceSchema,
containerModuleSchema,
hotReloadArgsSchema,
} from "../config"
import { posix } from "path"
import { runPodSpecWhitelist } from "../run"
export const defaultHelmTimeout = 300
// A Helm Module always maps to a single Service
export type HelmModuleSpec = HelmServiceSpec
export interface HelmModule
extends GardenModule<HelmModuleSpec, HelmServiceSpec, KubernetesTestSpec, KubernetesTaskSpec> {}
export type HelmModuleConfig = HelmModule["_config"]
export interface HelmServiceSpec {
base?: string
chart?: string
chartPath: string
dependencies: string[]
namespace?: string
releaseName?: string
repo?: string
serviceResource?: ServiceResourceSpec
skipDeploy: boolean
tasks: KubernetesTaskSpec[]
tests: KubernetesTestSpec[]
timeout: number
version?: string
values: DeepPrimitiveMap
valueFiles: string[]
}
export type HelmService = Service<HelmModule, ContainerModule>
const parameterValueSchema = () =>
joi
.alternatives(
joiPrimitive(),
joi.array().items(joi.link("#parameterValue")),
joi.object().pattern(/.+/, joi.link("#parameterValue"))
)
.id("parameterValue")
export const helmModuleOutputsSchema = () =>
joi.object().keys({
"release-name": joi.string().required().description("The Helm release name of the service."),
})
const helmServiceResourceSchema = () =>
serviceResourceSchema().keys({
name: joi.string().description(
dedent`The name of the resource to sync to. If the chart contains a single resource of the specified Kind,
this can be omitted.
This can include a Helm template string, e.g. '{{ template "my-chart.fullname" . }}'.
This allows you to easily match the dynamic names given by Helm. In most cases you should copy this
directly from the template in question in order to match it. Note that you may need to add single quotes around
the string for the YAML to be parsed correctly.`
),
containerModule: containerModuleSchema(),
hotReloadArgs: hotReloadArgsSchema(),
})
const runPodSpecWhitelistDescription = runPodSpecWhitelist.map((f) => `* \`${f}\``).join("\n")
const helmTaskSchema = () =>
kubernetesTaskSchema().keys({
resource: helmServiceResourceSchema().description(
dedent`The Deployment, DaemonSet or StatefulSet that Garden should use to execute this task.
If not specified, the \`serviceResource\` configured on the module will be used. If neither is specified,
an error will be thrown.
The following pod spec fields from the service resource will be used (if present) when executing the task:
${runPodSpecWhitelistDescription}`
),
})
const helmTestSchema = () =>
kubernetesTestSchema().keys({
resource: helmServiceResourceSchema().description(
dedent`The Deployment, DaemonSet or StatefulSet that Garden should use to execute this test suite.
If not specified, the \`serviceResource\` configured on the module will be used. If neither is specified,
an error will be thrown.
The following pod spec fields from the service resource will be used (if present) when executing the test suite:
${runPodSpecWhitelistDescription}`
),
})
export const helmModuleSpecSchema = () =>
joi.object().keys({
base: joiUserIdentifier()
.description(
deline`The name of another \`helm\` module to use as a base for this one. Use this to re-use a Helm chart across
multiple services. For example, you might have an organization-wide base chart for certain types of services.
If set, this module will by default inherit the following properties from the base module:
\`serviceResource\`, \`values\`
Each of those can be overridden in this module. They will be merged with a JSON Merge Patch (RFC 7396).`
)
.example("my-base-chart"),
build: baseBuildSpecSchema(),
chart: joi
.string()
.description(
deline`A valid Helm chart name or URI (same as you'd input to \`helm install\`).
Required if the module doesn't contain the Helm chart itself.`
)
.example("ingress-nginx"),
chartPath: joi
.posixPath()
.subPathOnly()
.description(
deline`The path, relative to the module path, to the chart sources (i.e. where the Chart.yaml file is, if any).
Not used when \`base\` is specified.`
)
.default("."),
dependencies: joiArray(joiIdentifier()).description(
"List of names of services that should be deployed before this chart."
),
namespace: namespaceSchema(),
releaseName: joiIdentifier().description(
"Optionally override the release name used when installing (defaults to the module name)."
),
repo: joi.string().description("The repository URL to fetch the chart from."),
serviceResource: helmServiceResourceSchema().description(
deline`The Deployment, DaemonSet or StatefulSet that Garden should regard as the _Garden service_ in this module
(not to be confused with Kubernetes Service resources).
Because a Helm chart can contain any number of Kubernetes resources, this needs to be specified for certain
Garden features and commands to work, such as hot-reloading.
We currently map a Helm chart to a single Garden service, because all the resources in a Helm chart are
deployed at once.`
),
skipDeploy: joi
.boolean()
.default(false)
.description(
deline`Set this to true if the chart should only be built, but not deployed as a service.
Use this, for example, if the chart should only be used as a base for other modules.`
),
include: joiModuleIncludeDirective(dedent`
If neither \`include\` nor \`exclude\` is set, and the module has local chart sources, Garden
automatically sets \`include\` to: \`["*", "charts/**/*", "templates/**/*"]\`.
If neither \`include\` nor \`exclude\` is set and the module specifies a remote chart, Garden
automatically sets \`ìnclude\` to \`[]\`.
`),
tasks: joiArray(helmTaskSchema()).description("The task definitions for this module."),
tests: joiArray(helmTestSchema()).description("The test suite definitions for this module."),
timeout: joi
.number()
.integer()
.default(defaultHelmTimeout)
.description(
"Time in seconds to wait for Helm to complete any individual Kubernetes operation (like Jobs for hooks)."
),
version: joi.string().description("The chart version to deploy."),
values: joi
.object()
.pattern(/.+/, parameterValueSchema())
.default(() => ({})).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.posixPath().subPathOnly()).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 configureHelmModule({
moduleConfig,
}: ConfigureModuleParams<HelmModule>): Promise<ConfigureModuleResult<HelmModule>> {
const { base, chartPath, dependencies, serviceResource, skipDeploy, tasks, tests } = moduleConfig.spec
const sourceModuleName = serviceResource ? serviceResource.containerModule : undefined
if (!skipDeploy) {
moduleConfig.serviceConfigs = [
{
name: moduleConfig.name,
dependencies,
disabled: moduleConfig.disabled,
// Note: We can't tell here if the source module supports hot-reloading,
// so we catch it in the handler if need be.
hotReloadable: !!sourceModuleName,
sourceModuleName,
spec: moduleConfig.spec,
},
]
}
const containsSources = await containsSource(moduleConfig)
if (base) {
if (containsSources) {
throw new ConfigurationError(
deline`
Helm module '${moduleConfig.name}' both contains sources and specifies a base module.
Since Helm charts cannot currently be merged, please either remove the sources or
the \`base\` reference in your module config.
`,
{ moduleConfig }
)
}
// We copy the chart on build
moduleConfig.build.dependencies.push({ name: base, copy: [{ source: "*", target: "." }] })
}
moduleConfig.taskConfigs = tasks.map((spec) => {
if (spec.resource && spec.resource.containerModule) {
moduleConfig.build.dependencies.push({ name: spec.resource.containerModule, copy: [] })
}
return {
name: spec.name,
cacheResult: spec.cacheResult,
dependencies: spec.dependencies,
disabled: moduleConfig.disabled,
timeout: spec.timeout,
spec,
}
})
moduleConfig.testConfigs = tests.map((spec) => {
if (spec.resource && spec.resource.containerModule) {
moduleConfig.build.dependencies.push({ name: spec.resource.containerModule, copy: [] })
}
return {
name: spec.name,
dependencies: spec.dependencies,
disabled: moduleConfig.disabled,
timeout: spec.timeout,
spec,
}
})
const valueFiles = moduleConfig.spec.valueFiles
// Automatically set the include if not explicitly set
if (!(moduleConfig.include || moduleConfig.exclude)) {
moduleConfig.include = containsSources
? ["*", "charts/**/*", "templates/**/*", ...valueFiles]
: ["*.yaml", "*.yml", ...valueFiles]
moduleConfig.include = moduleConfig.include.map((path) => posix.join(chartPath, path))
}
return { moduleConfig }
}