diff --git a/api/v1beta1/kustomization_types.go b/api/v1beta1/kustomization_types.go index 9cb40efb..b01b3ae6 100644 --- a/api/v1beta1/kustomization_types.go +++ b/api/v1beta1/kustomization_types.go @@ -33,6 +33,7 @@ const ( KustomizationKind = "Kustomization" KustomizationFinalizer = "finalizers.fluxcd.io" MaxConditionMessageLength = 20000 + DisabledValue = "disabled" ) // KustomizationSpec defines the desired state of a kustomization. diff --git a/controllers/kustomization_controller.go b/controllers/kustomization_controller.go index ea92fdf9..eb15a599 100644 --- a/controllers/kustomization_controller.go +++ b/controllers/kustomization_controller.go @@ -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) @@ -532,6 +532,21 @@ 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() @@ -539,12 +554,6 @@ func (r *KustomizationReconciler) build(ctx context.Context, kustomization kusto 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 diff --git a/controllers/kustomization_gc.go b/controllers/kustomization_gc.go index cdb8f54c..b17d93e3 100644 --- a/controllers/kustomization_gc.go +++ b/controllers/kustomization_gc.go @@ -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 { diff --git a/controllers/kustomization_generator.go b/controllers/kustomization_generator.go index 82c98293..8ec0e076 100644 --- a/controllers/kustomization_generator.go +++ b/controllers/kustomization_generator.go @@ -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" @@ -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 @@ -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 -} diff --git a/controllers/kustomization_varsub.go b/controllers/kustomization_varsub.go new file mode 100644 index 00000000..dd5a8618 --- /dev/null +++ b/controllers/kustomization_varsub.go @@ -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 +} diff --git a/docs/spec/v1beta1/kustomization.md b/docs/spec/v1beta1/kustomization.md index 0c461a43..f10823b9 100644 --- a/docs/spec/v1beta1/kustomization.md +++ b/docs/spec/v1beta1/kustomization.md @@ -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):