Skip to content

Commit

Permalink
Merge pull request #1130 from fluxcd/StrictPostBuildSubstitutions
Browse files Browse the repository at this point in the history
Add `StrictPostBuildSubstitutions` feature flag
  • Loading branch information
stefanprodan authored Apr 9, 2024
2 parents b2daff1 + b810013 commit fa5cebb
Show file tree
Hide file tree
Showing 7 changed files with 174 additions and 39 deletions.
15 changes: 9 additions & 6 deletions docs/spec/v1/kustomizations.md
Original file line number Diff line number Diff line change
Expand Up @@ -564,8 +564,7 @@ kind: Kustomization
metadata:
name: apps
spec:
interval: 5m
path: "./apps/"
# ...omitted for brevity
postBuild:
substitute:
cluster_env: "prod"
Expand Down Expand Up @@ -605,6 +604,11 @@ will print out `${var}`.
All the undefined variables in the format `${var}` will be substituted with an
empty string unless a default value is provided e.g. `${var:=default}`.

**Note:** It is recommended to set the `--feature-gates=StrictPostBuildSubstitutions=true`
controller flag, so that the post-build substitutions will fail if a
variable without a default value is declared in files but is
missing from the input vars.

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

Expand All @@ -624,6 +628,7 @@ kind: Kustomization
metadata:
name: apps
spec:
# ...omitted for brevity
postBuild:
substitute:
var_substitution_enabled: "true"
Expand All @@ -635,13 +640,11 @@ enclosed in double quotes vars to be treated as strings, for more information se

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):
and the Flux CLI:

```console
$ go install github.com/drone/envsubst/cmd/envsubst
$ export cluster_region=eu-central-1
$ kustomize build ./apps/ | $GOPATH/bin/envsubst
$ kustomize build ./apps/ | flux envsubst --strict
---
apiVersion: v1
kind: Namespace
Expand Down
4 changes: 2 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ require (
github.com/fluxcd/pkg/apis/kustomize v1.4.0
github.com/fluxcd/pkg/apis/meta v1.4.0
github.com/fluxcd/pkg/http/fetch v0.10.0
github.com/fluxcd/pkg/kustomize v1.8.0
github.com/fluxcd/pkg/kustomize v1.9.0
github.com/fluxcd/pkg/runtime v0.46.0
github.com/fluxcd/pkg/ssa v0.38.0
github.com/fluxcd/pkg/tar v0.6.0
Expand Down Expand Up @@ -96,12 +96,12 @@ require (
github.com/docker/docker v24.0.9+incompatible // indirect
github.com/docker/go-connections v0.4.0 // indirect
github.com/docker/go-units v0.4.0 // indirect
github.com/drone/envsubst v1.0.3 // indirect
github.com/emicklei/go-restful/v3 v3.11.0 // indirect
github.com/evanphx/json-patch v5.7.0+incompatible // indirect
github.com/evanphx/json-patch/v5 v5.8.0 // indirect
github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f // indirect
github.com/fatih/color v1.16.0 // indirect
github.com/fluxcd/pkg/envsubst v1.0.0 // indirect
github.com/fluxcd/pkg/sourceignore v0.6.0 // indirect
github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/getsops/gopgagent v0.0.0-20170926210634-4d7ea76ff71a // indirect
Expand Down
8 changes: 4 additions & 4 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -116,8 +116,6 @@ github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKoh
github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=
github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw=
github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/drone/envsubst v1.0.3 h1:PCIBwNDYjs50AsLZPYdfhSATKaRg/FJmDc2D6+C2x8g=
github.com/drone/envsubst v1.0.3/go.mod h1:N2jZmlMufstn1KEqvbHjw40h1KyTmnVzHcSc9bFiJ2g=
github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g=
github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
Expand All @@ -143,10 +141,12 @@ github.com/fluxcd/pkg/apis/kustomize v1.4.0 h1:SXoGN9M31fW5tO+wpKMnyHXbjxGUqDo7Y
github.com/fluxcd/pkg/apis/kustomize v1.4.0/go.mod h1:bZklVWB11tELMss89qYzgg4ClzhFzp0Hm4/8EiHgKew=
github.com/fluxcd/pkg/apis/meta v1.4.0 h1:nNdgB6FFHP3cubxZCViaCFDUVlAbpq9+hvKEIveOGMg=
github.com/fluxcd/pkg/apis/meta v1.4.0/go.mod h1:81sZ01ShTuLc1C3M1dFJNkINareBysvmrO1b8zJFFKs=
github.com/fluxcd/pkg/envsubst v1.0.0 h1:LD86BRNSCGJrvyrH2aX5/pit7RfbFpkzRXogwcazLVk=
github.com/fluxcd/pkg/envsubst v1.0.0/go.mod h1:VAcb4OxcRdsDix1TRtr/mtTqFGHmNQaOvXQO2REArFQ=
github.com/fluxcd/pkg/http/fetch v0.10.0 h1:Uh1ZrPa4B4EDgi+NFrY7qP6g9vg1O6JHKg3+iJLtt1w=
github.com/fluxcd/pkg/http/fetch v0.10.0/go.mod h1:zZOsAqn7iODap40PVq29mcCPEKjDodYvamEaoN6tV/Q=
github.com/fluxcd/pkg/kustomize v1.8.0 h1:Vf1UwnoP3yScaLi/QrDjgN2d2nI6LcmX4tNRoH+sypY=
github.com/fluxcd/pkg/kustomize v1.8.0/go.mod h1:yszv9tkYrnC01mcGPct8+bdxpTyxf69k1kmSvk7w0zs=
github.com/fluxcd/pkg/kustomize v1.9.0 h1:bqS3mXiK1q5TpUtIO5I5b+v/0r96NGJBiearKGUhicA=
github.com/fluxcd/pkg/kustomize v1.9.0/go.mod h1:PBerk0KzZN/IXaGociVp4MSMvsUQB0jR1P2SqSdixz0=
github.com/fluxcd/pkg/runtime v0.46.0 h1:+pxFwTk8j8lZIS9Vyc8EJbgvmFp9JqeT6pfLo/0iP98=
github.com/fluxcd/pkg/runtime v0.46.0/go.mod h1:d9BaIjqoHL71fYeZsssrt08UFONGN2WQRaJ/Ay2d1Cc=
github.com/fluxcd/pkg/sourceignore v0.6.0 h1:kD6QXL/upPEX66UpR669yK1Bxr/GtjzmZiqBeYpunUQ=
Expand Down
6 changes: 4 additions & 2 deletions internal/controller/kustomization_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ type KustomizationReconciler struct {
KubeConfigOpts runtimeClient.KubeConfigOptions
ConcurrentSSA int
DisallowedFieldManagers []string
StrictSubstitutions bool
}

// KustomizationReconcilerOptions contains options for the KustomizationReconciler.
Expand Down Expand Up @@ -622,9 +623,10 @@ func (r *KustomizationReconciler) build(ctx context.Context,

// run variable substitutions
if obj.Spec.PostBuild != nil {
outRes, err := generator.SubstituteVariables(ctx, r.Client, u, res, false)
outRes, err := generator.SubstituteVariables(ctx, r.Client, u, res,
generator.SubstituteWithStrict(r.StrictSubstitutions))
if err != nil {
return nil, fmt.Errorf("var substitution failed for '%s': %w", res.GetName(), err)
return nil, fmt.Errorf("post build failed for '%s': %w", res.GetName(), err)
}

if outRes != nil {
Expand Down
165 changes: 140 additions & 25 deletions internal/controller/kustomization_varsub_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -366,10 +366,11 @@ func TestKustomizationReconciler_VarsubNumberBool(t *testing.T) {
manifests := func(name string) []testserver.File {
return []testserver.File{
{
Name: "service-account.yaml",
Name: "templates.yaml",
Body: fmt.Sprintf(`
apiVersion: v1
kind: ServiceAccount
---
apiVersion: source.toolkit.fluxcd.io/v1
kind: GitRepository
metadata:
name: %[1]s
namespace: %[1]s
Expand All @@ -379,6 +380,29 @@ metadata:
annotations:
id: ${q}${number}${q}
enabled: ${q}${boolean}${q}
spec:
interval: ${number}m
url: https://host/repo
---
apiVersion: v1
kind: ConfigMap
metadata:
name: %[1]s
namespace: %[1]s
data:
id: ${q}${number}${q}
text: |
This variable is escaped $${var}
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus at
nisl sem. Nullam nec dui ipsum. Nam vehicula volutpat ipsum, ac fringilla
nisl convallis sed. Aliquam porttitor turpis finibus, finibus velit ut,
imperdiet mauris. Cras nec neque nulla. Maecenas semper nulla et elit
dictum sagittis. Quisque tincidunt non diam non ullamcorper. Curabitur
pretium urna odio, vitae ullamcorper purus mollis sit amet. Nam ac lectus
ac arcu varius feugiat id fringilla massa.
\?
`, name),
},
}
Expand Down Expand Up @@ -423,35 +447,126 @@ metadata:
"boolean": "true",
},
},
Wait: true,
Wait: false,
},
}
g.Expect(k8sClient.Create(ctx, inputK)).Should(Succeed())

resultSA := &corev1.ServiceAccount{}
g.Eventually(func() bool {
resultK := &kustomizev1.Kustomization{}
_ = k8sClient.Get(ctx, client.ObjectKeyFromObject(inputK), resultK)
for _, c := range resultK.Status.Conditions {
if c.Reason == kustomizev1.ReconciliationSucceededReason {
return true
}
}
return false
}, timeout, interval).Should(BeTrue())

resultRepo := &sourcev1.GitRepository{}
g.Expect(k8sClient.Get(ctx, types.NamespacedName{Name: id, Namespace: id}, resultRepo)).Should(Succeed())
g.Expect(resultRepo.Labels["id"]).To(Equal("123"))
g.Expect(resultRepo.Annotations["id"]).To(Equal("123"))
g.Expect(resultRepo.Labels["enabled"]).To(Equal("true"))
g.Expect(resultRepo.Annotations["enabled"]).To(Equal("true"))

resultCM := &corev1.ConfigMap{}
g.Expect(k8sClient.Get(ctx, types.NamespacedName{Name: id, Namespace: id}, resultCM)).Should(Succeed())
g.Expect(resultCM.Data["id"]).To(Equal("123"))
g.Expect(resultCM.Data["text"]).To(ContainSubstring(`${var}`))
g.Expect(resultCM.Data["text"]).ToNot(ContainSubstring(`$${var}`))
g.Expect(resultCM.Data["text"]).To(ContainSubstring(`\?`))
}

ensureReconciles := func(nameSuffix string) {
t.Run("reconciles successfully"+nameSuffix, func(t *testing.T) {
g.Eventually(func() bool {
resultK := &kustomizev1.Kustomization{}
_ = k8sClient.Get(ctx, client.ObjectKeyFromObject(inputK), resultK)
for _, c := range resultK.Status.Conditions {
if c.Reason == kustomizev1.ReconciliationSucceededReason {
return true
}
}
return false
}, timeout, interval).Should(BeTrue())
func TestKustomizationReconciler_VarsubStrict(t *testing.T) {
reconciler.StrictSubstitutions = true
defer func() {
reconciler.StrictSubstitutions = false
}()

g.Expect(k8sClient.Get(ctx, types.NamespacedName{Name: id, Namespace: id}, resultSA)).Should(Succeed())
})
ctx := context.Background()

g := NewWithT(t)
id := "vars-" + randStringRunes(5)
revision := "v1.0.0/" + randStringRunes(7)

err := createNamespace(id)
g.Expect(err).NotTo(HaveOccurred(), "failed to create test namespace")

err = createKubeConfigSecret(id)
g.Expect(err).NotTo(HaveOccurred(), "failed to create kubeconfig secret")

manifests := func(name string) []testserver.File {
return []testserver.File{
{
Name: "service-account.yaml",
Body: fmt.Sprintf(`
apiVersion: v1
kind: ServiceAccount
metadata:
name: %[1]s
namespace: %[1]s
labels:
default: ${default:=test}
missing: ${missing}
`, name),
},
}
}

ensureReconciles(" with optional ConfigMap")
t.Run("replaces vars from optional ConfigMap", func(t *testing.T) {
g.Expect(resultSA.Labels["id"]).To(Equal("123"))
g.Expect(resultSA.Annotations["id"]).To(Equal("123"))
g.Expect(resultSA.Labels["enabled"]).To(Equal("true"))
g.Expect(resultSA.Annotations["enabled"]).To(Equal("true"))
artifact, err := testServer.ArtifactFromFiles(manifests(id))
g.Expect(err).NotTo(HaveOccurred())

repositoryName := types.NamespacedName{
Name: randStringRunes(5),
Namespace: id,
}

err = applyGitRepository(repositoryName, artifact, revision)
g.Expect(err).NotTo(HaveOccurred())

inputK := &kustomizev1.Kustomization{
ObjectMeta: metav1.ObjectMeta{
Name: id,
Namespace: id,
},
Spec: kustomizev1.KustomizationSpec{
KubeConfig: &meta.KubeConfigReference{
SecretRef: meta.SecretKeyReference{
Name: "kubeconfig",
},
},
Interval: metav1.Duration{Duration: reconciliationInterval},
Path: "./",
Prune: true,
SourceRef: kustomizev1.CrossNamespaceSourceReference{
Kind: sourcev1.GitRepositoryKind,
Name: repositoryName.Name,
},
PostBuild: &kustomizev1.PostBuild{
Substitute: map[string]string{
"test": "test",
},
},
Wait: true,
},
}
g.Expect(k8sClient.Create(ctx, inputK)).Should(Succeed())

var resultK kustomizev1.Kustomization
t.Run("fails to reconcile", func(t *testing.T) {
g.Eventually(func() bool {
_ = k8sClient.Get(context.Background(), client.ObjectKeyFromObject(inputK), &resultK)
for _, c := range resultK.Status.Conditions {
if c.Reason == kustomizev1.BuildFailedReason {
return true
}
}
return false
}, timeout, interval).Should(BeTrue())
})

ready := apimeta.FindStatusCondition(resultK.Status.Conditions, meta.ReadyCondition)
g.Expect(ready.Message).To(ContainSubstring("variable not set"))
g.Expect(k8sClient.Delete(context.Background(), &resultK)).To(Succeed())
}
8 changes: 8 additions & 0 deletions internal/features/features.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,11 @@ const (
// DisableFailFastBehavior controls whether the fail-fast behavior when
// waiting for resources to become ready should be disabled.
DisableFailFastBehavior = "DisableFailFastBehavior"

// StrictPostBuildSubstitutions controls whether the post-build substitutions
// should fail if a variable without a default value is declared in files
// but is missing from the input vars.
StrictPostBuildSubstitutions = "StrictPostBuildSubstitutions"
)

var features = map[string]bool{
Expand All @@ -51,6 +56,9 @@ var features = map[string]bool{
// DisableFailFastBehavior
// opt-in from v1.1
DisableFailFastBehavior: false,
// StrictPostBuildSubstitutions
// opt-in from v1.3
StrictPostBuildSubstitutions: false,
}

// FeatureGates contains a list of all supported feature gates and
Expand Down
7 changes: 7 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,12 @@ func main() {
failFast = false
}

strictSubstitutions, err := features.Enabled(features.StrictPostBuildSubstitutions)
if err != nil {
setupLog.Error(err, "unable to check feature gate "+features.StrictPostBuildSubstitutions)
os.Exit(1)
}

if err = (&controller.KustomizationReconciler{
ControllerName: controllerName,
DefaultServiceAccount: defaultServiceAccount,
Expand All @@ -242,6 +248,7 @@ func main() {
PollingOpts: pollingOpts,
StatusPoller: polling.NewStatusPoller(mgr.GetClient(), mgr.GetRESTMapper(), pollingOpts),
DisallowedFieldManagers: disallowedFieldManagers,
StrictSubstitutions: strictSubstitutions,
}).SetupWithManager(ctx, mgr, controller.KustomizationReconcilerOptions{
DependencyRequeueInterval: requeueDependency,
HTTPRetry: httpRetry,
Expand Down

0 comments on commit fa5cebb

Please sign in to comment.