Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement var substitution from ConfigMaps and Secrets #275

Merged
merged 2 commits into from
Feb 16, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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