Skip to content

Commit

Permalink
Allow disabling var substitution for certain resources
Browse files Browse the repository at this point in the history
Signed-off-by: Stefan Prodan <[email protected]>
  • Loading branch information
stefanprodan committed Feb 16, 2021
1 parent 0ac1f9e commit 1f8cb01
Show file tree
Hide file tree
Showing 6 changed files with 131 additions and 73 deletions.
1 change: 1 addition & 0 deletions api/v1beta1/kustomization_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ const (
KustomizationKind = "Kustomization"
KustomizationFinalizer = "finalizers.fluxcd.io"
MaxConditionMessageLength = 20000
DisabledValue = "disabled"
)

// KustomizationSpec defines the desired state of a kustomization.
Expand Down
27 changes: 18 additions & 9 deletions controllers/kustomization_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -517,9 +517,9 @@ func (r *KustomizationReconciler) build(ctx context.Context, kustomization kusto
return nil, fmt.Errorf("kustomize build failed: %w", err)
}

// check if resources are encrypted and decrypt them before generating the final YAML
if kustomization.Spec.Decryption != nil {
for _, res := range m.Resources() {
for _, res := range m.Resources() {
// check if resources are encrypted and decrypt them before generating the final YAML
if kustomization.Spec.Decryption != nil {
outRes, err := dec.Decrypt(res)
if err != nil {
return nil, fmt.Errorf("decryption failed for '%s': %w", res.GetName(), err)
Expand All @@ -532,19 +532,28 @@ func (r *KustomizationReconciler) build(ctx context.Context, kustomization kusto
}
}
}

// run variable substitutions
if kustomization.Spec.PostBuild != nil {
outRes, err := substituteVariables(ctx, r.Client, kustomization, res)
if err != nil {
return nil, fmt.Errorf("var substitution failed for '%s': %w", res.GetName(), err)
}

if outRes != nil {
_, err = m.Replace(res)
if err != nil {
return nil, err
}
}
}
}

resources, err := m.AsYaml()
if err != nil {
return nil, fmt.Errorf("kustomize build failed: %w", err)
}

// run post-build actions
resources, err = runPostBuildActions(ctx, r.Client, kustomization, resources)
if err != nil {
return nil, fmt.Errorf("post-build actions failed: %w", err)
}

manifestsFile := filepath.Join(dirPath, fmt.Sprintf("%s.yaml", kustomization.GetUID()))
if err := fs.WriteFile(manifestsFile, resources); err != nil {
return nil, err
Expand Down
3 changes: 1 addition & 2 deletions controllers/kustomization_gc.go
Original file line number Diff line number Diff line change
Expand Up @@ -144,9 +144,8 @@ func (kgc *KustomizeGarbageCollector) isStale(obj unstructured.Unstructured) boo

func (kgc *KustomizeGarbageCollector) shouldSkip(obj unstructured.Unstructured) bool {
key := fmt.Sprintf("%s/prune", kustomizev1.GroupVersion.Group)
val := "disabled"

return obj.GetLabels()[key] == val || obj.GetAnnotations()[key] == val
return obj.GetLabels()[key] == kustomizev1.DisabledValue || obj.GetAnnotations()[key] == kustomizev1.DisabledValue
}

func (kgc *KustomizeGarbageCollector) matchingLabels(name, namespace string) client.MatchingLabels {
Expand Down
80 changes: 18 additions & 62 deletions controllers/kustomization_generator.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,11 @@ import (
"encoding/json"
"fmt"
"io/ioutil"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/types"
"os"
"path/filepath"
"sigs.k8s.io/controller-runtime/pkg/client"
"strings"

"github.com/drone/envsubst"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/kustomize/api/filesys"
"sigs.k8s.io/kustomize/api/k8sdeps/kunstruct"
"sigs.k8s.io/kustomize/api/konfig"
Expand Down Expand Up @@ -253,15 +250,26 @@ func (kg *KustomizeGenerator) checksum(ctx context.Context, dirPath string) (str
return "", fmt.Errorf("kustomize build failed: %w", err)
}

resources, err := m.AsYaml()
if err != nil {
return "", fmt.Errorf("kustomize build failed: %w", err)
// run variable substitutions
if kg.kustomization.Spec.PostBuild != nil {
for _, res := range m.Resources() {
outRes, err := substituteVariables(ctx, kg.Client, kg.kustomization, res)
if err != nil {
return "", fmt.Errorf("var substitution failed for '%s': %w", res.GetName(), err)
}

if outRes != nil {
_, err = m.Replace(res)
if err != nil {
return "", err
}
}
}
}

// run post-build actions
resources, err = runPostBuildActions(ctx, kg.Client, kg.kustomization, resources)
resources, err := m.AsYaml()
if err != nil {
return "", fmt.Errorf("post-build actions failed: %w", err)
return "", fmt.Errorf("kustomize build failed: %w", err)
}

return fmt.Sprintf("%x", sha1.Sum(resources)), nil
Expand Down Expand Up @@ -346,55 +354,3 @@ func buildKustomization(fs filesys.FileSystem, dirPath string) (resmap.ResMap, e
k := krusty.MakeKustomizer(fs, buildOptions)
return k.Run(dirPath)
}

// runPostBuildActions runs actions on the multi-doc YAML manifest generated by kustomize build
func runPostBuildActions(ctx context.Context, kubeClient client.Client, kustomization kustomizev1.Kustomization, manifests []byte) ([]byte, error) {
if kustomization.Spec.PostBuild == nil {
return manifests, nil
}

vars := make(map[string]string)

// load vars from ConfigMaps and Secrets data keys
for _, reference := range kustomization.Spec.PostBuild.SubstituteFrom {
namespacedName := types.NamespacedName{Namespace: kustomization.Namespace, Name: reference.Name}
switch reference.Kind {
case "ConfigMap":
resource := &corev1.ConfigMap{}
if err := kubeClient.Get(ctx, namespacedName, resource); err != nil {
return nil, fmt.Errorf("substitute from 'ConfigMap/%s' error: %w", reference.Name, err)
}
for k, v := range resource.Data {
vars[k] = strings.Replace(v, "\n", "", -1)
}
case "Secret":
resource := &corev1.Secret{}
if err := kubeClient.Get(ctx, namespacedName, resource); err != nil {
return nil, fmt.Errorf("substitute from 'Secret/%s' error: %w", reference.Name, err)
}
for k, v := range resource.Data {
vars[k] = strings.Replace(string(v), "\n", "", -1)
}
}
}

// load in-line vars (overrides the ones from resources)
if kustomization.Spec.PostBuild.Substitute != nil {
for k, v := range kustomization.Spec.PostBuild.Substitute {
vars[k] = strings.Replace(v, "\n", "", -1)
}
}

// run bash variable substitutions
if len(vars) > 0 {
output, err := envsubst.Eval(string(manifests), func(s string) string {
return vars[s]
})
if err != nil {
return nil, fmt.Errorf("variable substitution failed: %w", err)
}
manifests = []byte(output)
}

return manifests, nil
}
86 changes: 86 additions & 0 deletions controllers/kustomization_varsub.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package controllers

import (
"context"
"fmt"
"strings"

"github.com/drone/envsubst"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/kustomize/api/resource"
"sigs.k8s.io/yaml"

kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1beta1"
)

// substituteVariables replaces the vars with their values in the specified resource.
// If a resource is labeled or annotated with
// 'kustomize.toolkit.fluxcd.io/substitute: disabled' the substitution is skipped.
func substituteVariables(ctx context.Context, kubeClient client.Client, kustomization kustomizev1.Kustomization, res *resource.Resource) (*resource.Resource, error) {
resData, err := res.AsYAML()
if err != nil {
return nil, err
}

key := fmt.Sprintf("%s/substitute", kustomizev1.GroupVersion.Group)

if res.GetLabels()[key] == kustomizev1.DisabledValue || res.GetAnnotations()[key] == kustomizev1.DisabledValue {
return nil, nil
}

vars := make(map[string]string)

// load vars from ConfigMaps and Secrets data keys
for _, reference := range kustomization.Spec.PostBuild.SubstituteFrom {
namespacedName := types.NamespacedName{Namespace: kustomization.Namespace, Name: reference.Name}
switch reference.Kind {
case "ConfigMap":
resource := &corev1.ConfigMap{}
if err := kubeClient.Get(ctx, namespacedName, resource); err != nil {
return nil, fmt.Errorf("substitute from 'ConfigMap/%s' error: %w", reference.Name, err)
}
for k, v := range resource.Data {
vars[k] = strings.Replace(v, "\n", "", -1)
}
case "Secret":
resource := &corev1.Secret{}
if err := kubeClient.Get(ctx, namespacedName, resource); err != nil {
return nil, fmt.Errorf("substitute from 'Secret/%s' error: %w", reference.Name, err)
}
for k, v := range resource.Data {
vars[k] = strings.Replace(string(v), "\n", "", -1)
}
}
}

// load in-line vars (overrides the ones from resources)
if kustomization.Spec.PostBuild.Substitute != nil {
for k, v := range kustomization.Spec.PostBuild.Substitute {
vars[k] = strings.Replace(v, "\n", "", -1)
}
}

// run bash variable substitutions
if len(vars) > 0 {
output, err := envsubst.Eval(string(resData), func(s string) string {
return vars[s]
})
if err != nil {
return nil, fmt.Errorf("variable substitution failed: %w", err)
}

jsonData, err := yaml.YAMLToJSON([]byte(output))
if err != nil {
return nil, fmt.Errorf("YAMLToJSON: %w", err)
}

err = res.UnmarshalJSON(jsonData)
if err != nil {
return nil, fmt.Errorf("UnmarshalJSON: %w", err)
}
}

return res, nil
}
7 changes: 7 additions & 0 deletions docs/spec/v1beta1/kustomization.md
Original file line number Diff line number Diff line change
Expand Up @@ -748,6 +748,13 @@ Note that if you want to avoid var substitutions in scripts embedded in ConfigMa
you must use the format `$var` instead of `${var}`. All the undefined variables in the format `${var}`
will be substituted with string empty, unless a default is provided e.g. `${var:=default}`.

You can disable the variable substitution for certain resources by either
labeling or annotating them with:

```yaml
kustomize.toolkit.fluxcd.io/substitute: disabled
```

You can replicate the controller post-build substitutions locally using
[kustomize](https://github.com/kubernetes-sigs/kustomize)
and Drone's [envsubst](https://github.com/drone/envsubst):
Expand Down

0 comments on commit 1f8cb01

Please sign in to comment.