Skip to content

Commit 9dc20e9

Browse files
authored
Merge pull request #253 from fluxcd/envsubst
[RFC] Add support for variable substitutions
2 parents 1e30988 + acaaafc commit 9dc20e9

11 files changed

Lines changed: 262 additions & 1 deletion

api/v1beta1/kustomization_types.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,11 @@ type KustomizationSpec struct {
6868
// +optional
6969
Path string `json:"path,omitempty"`
7070

71+
// PostBuild describes which actions to perform on the YAML manifest
72+
// generated by building the kustomize overlay.
73+
// +optional
74+
PostBuild *PostBuild `json:"postBuild,omitempty"`
75+
7176
// Prune enables garbage collection.
7277
// +required
7378
Prune bool `json:"prune"`
@@ -150,6 +155,19 @@ type KubeConfig struct {
150155
SecretRef meta.LocalObjectReference `json:"secretRef,omitempty"`
151156
}
152157

158+
// PostBuild describes which actions to perform on the YAML manifest
159+
// generated by building the kustomize overlay.
160+
type PostBuild struct {
161+
// Substitute holds a map of key/value pairs.
162+
// The variables defined in your YAML manifests
163+
// that match any of the keys defined in the map
164+
// will be substituted with the set value.
165+
// Includes support for bash string replacement functions
166+
// e.g. ${var:=default}, ${var:position} and ${var/substring/replacement}.
167+
// +optional
168+
Substitute map[string]string `json:"substitute,omitempty"`
169+
}
170+
153171
// KustomizationStatus defines the observed state of a kustomization.
154172
type KustomizationStatus struct {
155173
// ObservedGeneration is the last reconciled generation.

api/v1beta1/zz_generated.deepcopy.go

Lines changed: 27 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

config/crd/bases/kustomize.toolkit.fluxcd.io_kustomizations.yaml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,20 @@ spec:
250250
for. Defaults to 'None', which translates to the root path of the
251251
SourceRef.
252252
type: string
253+
postBuild:
254+
description: PostBuild describes which actions to perform on the YAML
255+
manifest generated by building the kustomize overlay.
256+
properties:
257+
substitute:
258+
additionalProperties:
259+
type: string
260+
description: Substitute holds a map of key/value pairs. The variables
261+
defined in your YAML manifests that match any of the keys defined
262+
in the map will be substituted with the set value. Includes
263+
support for bash string replacement functions e.g. ${var:=default},
264+
${var:position} and ${var/substring/replacement}.
265+
type: object
266+
type: object
253267
prune:
254268
description: Prune enables garbage collection.
255269
type: boolean

controllers/kustomization_controller.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -527,6 +527,12 @@ func (r *KustomizationReconciler) build(kustomization kustomizev1.Kustomization,
527527
return nil, fmt.Errorf("kustomize build failed: %w", err)
528528
}
529529

530+
// run post-build actions
531+
resources, err = runPostBuildActions(kustomization, resources)
532+
if err != nil {
533+
return nil, fmt.Errorf("post-build actions failed: %w", err)
534+
}
535+
530536
manifestsFile := filepath.Join(dirPath, fmt.Sprintf("%s.yaml", kustomization.GetUID()))
531537
if err := fs.WriteFile(manifestsFile, resources); err != nil {
532538
return nil, err

controllers/kustomization_controller_test.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,9 @@ var _ = Describe("KustomizationReconciler", func() {
171171
Suspend: false,
172172
Timeout: nil,
173173
Validation: "client",
174+
PostBuild: &kustomizev1.PostBuild{
175+
Substitute: map[string]string{"region": "eu-central-1"},
176+
},
174177
HealthChecks: []meta.NamespacedObjectKindReference{
175178
{
176179
APIVersion: "v1",
@@ -205,6 +208,11 @@ var _ = Describe("KustomizationReconciler", func() {
205208
Expect(k8sClient.Get(context.Background(), types.NamespacedName{Name: "test"}, ns)).Should(Succeed())
206209
Expect(ns.Labels[fmt.Sprintf("%s/name", kustomizev1.GroupVersion.Group)]).To(Equal(kName.Name))
207210
Expect(ns.Labels[fmt.Sprintf("%s/namespace", kustomizev1.GroupVersion.Group)]).To(Equal(kName.Namespace))
211+
212+
sa := &corev1.ServiceAccount{}
213+
Expect(k8sClient.Get(context.Background(), types.NamespacedName{Name: "test", Namespace: "test"}, sa)).Should(Succeed())
214+
Expect(sa.Labels["environment"]).To(Equal("dev"))
215+
Expect(sa.Labels["region"]).To(Equal("eu-central-1"))
208216
},
209217
Entry("namespace-sa", refTestCase{
210218
artifacts: []testserver.File{
@@ -225,6 +233,9 @@ kind: ServiceAccount
225233
metadata:
226234
name: test
227235
namespace: test
236+
labels:
237+
environment: ${env:=dev}
238+
region: "${region}"
228239
`,
229240
},
230241
},

controllers/kustomization_generator.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import (
2525
"path/filepath"
2626
"strings"
2727

28+
"github.com/drone/envsubst"
2829
"sigs.k8s.io/kustomize/api/filesys"
2930
"sigs.k8s.io/kustomize/api/k8sdeps/kunstruct"
3031
"sigs.k8s.io/kustomize/api/konfig"
@@ -251,6 +252,12 @@ func (kg *KustomizeGenerator) checksum(dirPath string) (string, error) {
251252
return "", fmt.Errorf("kustomize build failed: %w", err)
252253
}
253254

255+
// run post-build actions
256+
resources, err = runPostBuildActions(kg.kustomization, resources)
257+
if err != nil {
258+
return "", fmt.Errorf("post-build actions failed: %w", err)
259+
}
260+
254261
return fmt.Sprintf("%x", sha1.Sum(resources)), nil
255262
}
256263

@@ -333,3 +340,24 @@ func buildKustomization(fs filesys.FileSystem, dirPath string) (resmap.ResMap, e
333340
k := krusty.MakeKustomizer(fs, buildOptions)
334341
return k.Run(dirPath)
335342
}
343+
344+
// runPostBuildActions runs actions on the multi-doc YAML manifest generated by kustomize build
345+
func runPostBuildActions(kustomization kustomizev1.Kustomization, manifests []byte) ([]byte, error) {
346+
if kustomization.Spec.PostBuild == nil {
347+
return manifests, nil
348+
}
349+
350+
// run bash variable substitutions
351+
vars := kustomization.Spec.PostBuild.Substitute
352+
if vars != nil && len(vars) > 0 {
353+
output, err := envsubst.Eval(string(manifests), func(s string) string {
354+
return vars[s]
355+
})
356+
if err != nil {
357+
return nil, fmt.Errorf("variable substitution failed: %w", err)
358+
}
359+
manifests = []byte(output)
360+
}
361+
362+
return manifests, nil
363+
}

docs/api/kustomize.md

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,21 @@ Defaults to ‘None’, which translates to the root path of the SourceR
158158
</tr>
159159
<tr>
160160
<td>
161+
<code>postBuild</code><br>
162+
<em>
163+
<a href="#kustomize.toolkit.fluxcd.io/v1beta1.PostBuild">
164+
PostBuild
165+
</a>
166+
</em>
167+
</td>
168+
<td>
169+
<em>(Optional)</em>
170+
<p>PostBuild describes which actions to perform on the YAML manifest
171+
generated by building the kustomize overlay.</p>
172+
</td>
173+
</tr>
174+
<tr>
175+
<td>
161176
<code>prune</code><br>
162177
<em>
163178
bool
@@ -586,6 +601,21 @@ Defaults to &lsquo;None&rsquo;, which translates to the root path of the SourceR
586601
</tr>
587602
<tr>
588603
<td>
604+
<code>postBuild</code><br>
605+
<em>
606+
<a href="#kustomize.toolkit.fluxcd.io/v1beta1.PostBuild">
607+
PostBuild
608+
</a>
609+
</em>
610+
</td>
611+
<td>
612+
<em>(Optional)</em>
613+
<p>PostBuild describes which actions to perform on the YAML manifest
614+
generated by building the kustomize overlay.</p>
615+
</td>
616+
</tr>
617+
<tr>
618+
<td>
589619
<code>prune</code><br>
590620
<em>
591621
bool
@@ -837,6 +867,45 @@ Snapshot
837867
</table>
838868
</div>
839869
</div>
870+
<h3 id="kustomize.toolkit.fluxcd.io/v1beta1.PostBuild">PostBuild
871+
</h3>
872+
<p>
873+
(<em>Appears on:</em>
874+
<a href="#kustomize.toolkit.fluxcd.io/v1beta1.KustomizationSpec">KustomizationSpec</a>)
875+
</p>
876+
<p>PostBuild describes which actions to perform on the YAML manifest
877+
generated by building the kustomize overlay.</p>
878+
<div class="md-typeset__scrollwrap">
879+
<div class="md-typeset__table">
880+
<table>
881+
<thead>
882+
<tr>
883+
<th>Field</th>
884+
<th>Description</th>
885+
</tr>
886+
</thead>
887+
<tbody>
888+
<tr>
889+
<td>
890+
<code>substitute</code><br>
891+
<em>
892+
map[string]string
893+
</em>
894+
</td>
895+
<td>
896+
<em>(Optional)</em>
897+
<p>Substitute holds a map of key/value pairs.
898+
The variables defined in your YAML manifests
899+
that match any of the keys defined in the map
900+
will be substituted with the set value.
901+
Includes support for bash string replacement functions
902+
e.g. ${var:=default}, ${var:position} and ${var/substring/replacement}.</p>
903+
</td>
904+
</tr>
905+
</tbody>
906+
</table>
907+
</div>
908+
</div>
840909
<h3 id="kustomize.toolkit.fluxcd.io/v1beta1.Snapshot">Snapshot
841910
</h3>
842911
<p>

docs/spec/v1beta1/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ of Kubernetes objects generated with Kustomize.
1414
+ [Kustomization dependencies](kustomization.md#kustomization-dependencies)
1515
+ [Role-based access control](kustomization.md#role-based-access-control)
1616
+ [Override kustomize config](kustomization.md#override-kustomize-config)
17+
+ [Variable substitution](kustomization.md#variable-substitution)
1718
+ [Targeting remote clusters](kustomization.md#remote-clusters--cluster-api)
1819
+ [Secrets decryption](kustomization.md#secrets-decryption)
1920
+ [Status](kustomization.md#status)

docs/spec/v1beta1/kustomization.md

Lines changed: 85 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ type KustomizationSpec struct {
3030
// value to retry failures.
3131
// +optional
3232
RetryInterval *metav1.Duration `json:"retryInterval,omitempty"`
33-
33+
3434
// The KubeConfig for reconciling the Kustomization on a remote cluster.
3535
// When specified, KubeConfig takes precedence over ServiceAccountName.
3636
// +optional
@@ -42,6 +42,11 @@ type KustomizationSpec struct {
4242
// +optional
4343
Path string `json:"path,omitempty"`
4444

45+
// PostBuild describes which actions to perform on the YAML manifest
46+
// generated by building the kustomize overlay.
47+
// +optional
48+
PostBuild *PostBuild `json:"postBuild,omitempty"`
49+
4550
// Enables garbage collection.
4651
// +required
4752
Prune bool `json:"prune"`
@@ -147,6 +152,21 @@ type Image struct {
147152
}
148153
```
149154

155+
The post-build section defines which actions to perform on the YAML manifest after kustomize build:
156+
157+
```go
158+
type PostBuild struct {
159+
// Substitute holds a map of key/value pairs.
160+
// The variables defined in your YAML manifests
161+
// that match any of the keys defined in the map
162+
// will be substituted with the set value.
163+
// Includes support for bash string replacement functions
164+
// e.g. ${var:=default}, ${var:position} and ${var/substring/replacement}.
165+
// +optional
166+
Substitute map[string]string `json:"substitute,omitempty"`
167+
}
168+
```
169+
150170
The status sub-resource records the result of the last reconciliation:
151171

152172
```go
@@ -663,6 +683,70 @@ spec:
663683
digest: sha256:24a0c4b4a4c0eb97a1aabb8e29f18e917d05abfe1b7a7c07857230879ce7d3d3
664684
```
665685

686+
## Variable substitution
687+
688+
With `spec.postBuild.substitute` you can provide a map of key/value pairs holding the
689+
variables to be substituted in the final YAML manifest, after kustomize build.
690+
691+
This offers basic templating for your manifests including support
692+
for [bash string replacement functions](https://github.com/drone/envsubst) e.g.:
693+
694+
- `${var:=default}`
695+
- `${var:position}`
696+
- `${var:position:length}`
697+
- `${var/substring/replacement}`
698+
699+
Assuming you have manifests with the following variables:
700+
701+
```yaml
702+
apiVersion: v1
703+
kind: Namespace
704+
metadata:
705+
name: apps
706+
labels:
707+
environment: ${cluster_env:=dev}
708+
region: "${cluster_region}"
709+
```
710+
711+
You can specify the variables and their values in the Kustomization definition under
712+
the `substitute` post build section:
713+
714+
````yaml
715+
apiVersion: kustomize.toolkit.fluxcd.io/v1beta1
716+
kind: Kustomization
717+
metadata:
718+
name: apps
719+
spec:
720+
interval: 5m
721+
path: "./apps/"
722+
postBuild:
723+
substitute:
724+
cluster_env: "prod"
725+
cluster_region: "eu-central-1"
726+
````
727+
728+
Note that you should prefix the variables that get replaced by kustomize-controller
729+
to avoid conflicts with any existing scripts embedded in ConfigMaps or container commands.
730+
731+
You can replicate the controller post-build substitutions locally using
732+
[kustomize](https://github.com/kubernetes-sigs/kustomize)
733+
and Drone's [envsubst](https://github.com/drone/envsubst):
734+
735+
```console
736+
$ go install github.com/drone/envsubst/cmd/envsubst
737+
738+
$ export cluster_region=eu-central-1
739+
$ kustomize build ./apps/ | $GOPATH/bin/envsubst
740+
---
741+
apiVersion: v1
742+
kind: Namespace
743+
metadata:
744+
name: apps
745+
labels:
746+
environment: dev
747+
region: eu-central-1
748+
```
749+
666750
## Remote Clusters / Cluster-API
667751

668752
If the `kubeConfig` field is set, objects will be applied, health-checked, pruned, and deleted for the default

0 commit comments

Comments
 (0)