Skip to content

Commit

Permalink
Merge pull request #275 from fluxcd/substitute-from
Browse files Browse the repository at this point in the history
Implement var substitution from ConfigMaps and Secrets
  • Loading branch information
stefanprodan committed Feb 16, 2021
2 parents 947bd57 + 401fec6 commit 5f966d0
Show file tree
Hide file tree
Showing 10 changed files with 356 additions and 66 deletions.
24 changes: 24 additions & 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 Expand Up @@ -166,6 +167,29 @@ type PostBuild struct {
// e.g. ${var:=default}, ${var:position} and ${var/substring/replacement}.
// +optional
Substitute map[string]string `json:"substitute,omitempty"`

// SubstituteFrom holds references to ConfigMaps and Secrets containing
// the variables and their values to be substituted in the YAML manifests.
// The ConfigMap and the Secret data keys represent the var names and they
// must match the vars declared in the manifests for the substitution to happen.
// +optional
SubstituteFrom []SubstituteReference `json:"substituteFrom,omitempty"`
}

// SubstituteReference contains a reference to a resource containing
// the variables name and value.
type SubstituteReference struct {
// Kind of the values referent, valid values are ('Secret', 'ConfigMap').
// +kubebuilder:validation:Enum=Secret;ConfigMap
// +required
Kind string `json:"kind"`

// Name of the values referent. Should reside in the same namespace as the
// referring resource.
// +kubebuilder:validation:MinLength=1
// +kubebuilder:validation:MaxLength=253
// +required
Name string `json:"name"`
}

// KustomizationStatus defines the observed state of a kustomization.
Expand Down
20 changes: 20 additions & 0 deletions api/v1beta1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

28 changes: 28 additions & 0 deletions config/crd/bases/kustomize.toolkit.fluxcd.io_kustomizations.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,34 @@ spec:
support for bash string replacement functions e.g. ${var:=default},
${var:position} and ${var/substring/replacement}.
type: object
substituteFrom:
description: SubstituteFrom holds references to ConfigMaps and
Secrets containing the variables and their values to be substituted
in the YAML manifests. The ConfigMap and the Secret data keys
represent the var names and they must match the vars declared
in the manifests for the substitution to happen.
items:
description: SubstituteReference contains a reference to a resource
containing the variables name and value.
properties:
kind:
description: Kind of the values referent, valid values are
('Secret', 'ConfigMap').
enum:
- Secret
- ConfigMap
type: string
name:
description: Name of the values referent. Should reside
in the same namespace as the referring resource.
maxLength: 253
minLength: 1
type: string
required:
- kind
- name
type: object
type: array
type: object
prune:
description: Prune enables garbage collection.
Expand Down
63 changes: 36 additions & 27 deletions controllers/kustomization_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -298,19 +298,20 @@ func (r *KustomizationReconciler) reconcile(
), err
}

// generate kustomization.yaml and calculate the manifests checksum
checksum, err := r.generate(kustomization, dirPath)
// create any necessary kube-clients for impersonation
impersonation := NewKustomizeImpersonation(kustomization, r.Client, r.StatusPoller, dirPath)
kubeClient, statusPoller, err := impersonation.GetClient(ctx)
if err != nil {
return kustomizev1.KustomizationNotReady(
kustomization,
source.GetArtifact().Revision,
kustomizev1.BuildFailedReason,
meta.ReconciliationFailedReason,
err.Error(),
), err
), fmt.Errorf("failed to build kube client: %w", err)
}

// build the kustomization and generate the GC snapshot
snapshot, err := r.build(kustomization, checksum, dirPath)
// generate kustomization.yaml and calculate the manifests checksum
checksum, err := r.generate(ctx, kubeClient, kustomization, dirPath)
if err != nil {
return kustomizev1.KustomizationNotReady(
kustomization,
Expand All @@ -320,16 +321,15 @@ func (r *KustomizationReconciler) reconcile(
), err
}

// create any necessary kube-clients for impersonation
impersonation := NewKustomizeImpersonation(kustomization, r.Client, r.StatusPoller, dirPath)
client, statusPoller, err := impersonation.GetClient(ctx)
// build the kustomization and generate the GC snapshot
snapshot, err := r.build(ctx, kustomization, checksum, dirPath)
if err != nil {
return kustomizev1.KustomizationNotReady(
kustomization,
source.GetArtifact().Revision,
meta.ReconciliationFailedReason,
kustomizev1.BuildFailedReason,
err.Error(),
), fmt.Errorf("failed to build kube client: %w", err)
), err
}

// dry-run apply
Expand All @@ -355,7 +355,7 @@ func (r *KustomizationReconciler) reconcile(
}

// prune
err = r.prune(ctx, client, kustomization, checksum)
err = r.prune(ctx, kubeClient, kustomization, checksum)
if err != nil {
return kustomizev1.KustomizationNotReady(
kustomization,
Expand Down Expand Up @@ -490,12 +490,12 @@ func (r *KustomizationReconciler) getSource(ctx context.Context, kustomization k
return source, nil
}

func (r *KustomizationReconciler) generate(kustomization kustomizev1.Kustomization, dirPath string) (string, error) {
gen := NewGenerator(kustomization)
return gen.WriteFile(dirPath)
func (r *KustomizationReconciler) generate(ctx context.Context, kubeClient client.Client, kustomization kustomizev1.Kustomization, dirPath string) (string, error) {
gen := NewGenerator(kustomization, kubeClient)
return gen.WriteFile(ctx, dirPath)
}

func (r *KustomizationReconciler) build(kustomization kustomizev1.Kustomization, checksum, dirPath string) (*kustomizev1.Snapshot, error) {
func (r *KustomizationReconciler) build(ctx context.Context, kustomization kustomizev1.Kustomization, checksum, dirPath string) (*kustomizev1.Snapshot, error) {
timeout := kustomization.GetTimeout()
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
Expand All @@ -517,9 +517,9 @@ func (r *KustomizationReconciler) build(kustomization kustomizev1.Kustomization,
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(kustomization kustomizev1.Kustomization,
}
}
}

// 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(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 Expand Up @@ -678,15 +687,15 @@ func (r *KustomizationReconciler) applyWithRetry(ctx context.Context, kustomizat
return changeSet, nil
}

func (r *KustomizationReconciler) prune(ctx context.Context, client client.Client, kustomization kustomizev1.Kustomization, newChecksum string) error {
func (r *KustomizationReconciler) prune(ctx context.Context, kubeClient client.Client, kustomization kustomizev1.Kustomization, newChecksum string) error {
if !kustomization.Spec.Prune || kustomization.Status.Snapshot == nil {
return nil
}
if kustomization.DeletionTimestamp.IsZero() && kustomization.Status.Snapshot.Checksum == newChecksum {
return nil
}

gc := NewGarbageCollector(client, *kustomization.Status.Snapshot, newChecksum, logr.FromContext(ctx))
gc := NewGarbageCollector(kubeClient, *kustomization.Status.Snapshot, newChecksum, logr.FromContext(ctx))

if output, ok := gc.Prune(kustomization.GetTimeout(),
kustomization.GetName(),
Expand Down
38 changes: 38 additions & 0 deletions controllers/kustomization_controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,32 @@ var _ = Describe("KustomizationReconciler", func() {
Expect(k8sClient.Status().Update(context.Background(), repository)).Should(Succeed())
defer k8sClient.Delete(context.Background(), repository)

configName := types.NamespacedName{
Name: fmt.Sprintf("%s", randStringRunes(5)),
Namespace: namespace.Name,
}
config := &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: configName.Name,
Namespace: configName.Namespace,
},
Data: map[string]string{"zone": "\naz-1a\n"},
}
Expect(k8sClient.Create(context.Background(), config)).Should(Succeed())

secretName := types.NamespacedName{
Name: fmt.Sprintf("%s", randStringRunes(5)),
Namespace: namespace.Name,
}
secret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: secretName.Name,
Namespace: secretName.Namespace,
},
StringData: map[string]string{"zone": "\naz-1b\n"},
}
Expect(k8sClient.Create(context.Background(), secret)).Should(Succeed())

kName := types.NamespacedName{
Name: fmt.Sprintf("%s", randStringRunes(5)),
Namespace: namespace.Name,
Expand All @@ -173,6 +199,16 @@ var _ = Describe("KustomizationReconciler", func() {
Validation: "client",
PostBuild: &kustomizev1.PostBuild{
Substitute: map[string]string{"region": "eu-central-1"},
SubstituteFrom: []kustomizev1.SubstituteReference{
{
Kind: "ConfigMap",
Name: configName.Name,
},
{
Kind: "Secret",
Name: secretName.Name,
},
},
},
HealthChecks: []meta.NamespacedObjectKindReference{
{
Expand Down Expand Up @@ -213,6 +249,7 @@ var _ = Describe("KustomizationReconciler", func() {
Expect(k8sClient.Get(context.Background(), types.NamespacedName{Name: "test", Namespace: "test"}, sa)).Should(Succeed())
Expect(sa.Labels["environment"]).To(Equal("dev"))
Expect(sa.Labels["region"]).To(Equal("eu-central-1"))
Expect(sa.Labels["zone"]).To(Equal("az-1b"))
},
Entry("namespace-sa", refTestCase{
artifacts: []testserver.File{
Expand All @@ -236,6 +273,7 @@ metadata:
labels:
environment: ${env:=dev}
region: "${region}"
zone: "${zone}"
`,
},
},
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
Loading

0 comments on commit 5f966d0

Please sign in to comment.