Skip to content

Commit 5b3449c

Browse files
authored
Merge pull request #1672 from fluxcd/varsub-always
Introduce `substituteStrategy: Always`
2 parents 3e12665 + 6214269 commit 5b3449c

8 files changed

Lines changed: 250 additions & 10 deletions

File tree

api/v1/kustomization_types.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -271,9 +271,35 @@ type Decryption struct {
271271
SecretRef *meta.LocalObjectReference `json:"secretRef,omitempty"`
272272
}
273273

274+
// SubstituteStrategy defines the strategy for substituting variables in the YAML manifests.
275+
type SubstituteStrategy string
276+
277+
const (
278+
// SubstituteStrategyWithVariables indicates that at least one variable must be defined,
279+
// either through the inline map or through the resolved references to ConfigMaps and Secrets.
280+
SubstituteStrategyWithVariables SubstituteStrategy = "WithVariables"
281+
282+
// SubstituteStrategyAlways indicates that the substitution should always be performed,
283+
// even if no variables are defined. This is useful for cases where the substitution
284+
// expressions have defaults e.g. "${var:=default}".
285+
SubstituteStrategyAlways SubstituteStrategy = "Always"
286+
)
287+
274288
// PostBuild describes which actions to perform on the YAML manifest
275289
// generated by building the kustomize overlay.
276290
type PostBuild struct {
291+
// SubstituteStrategy defines the strategy for substituting variables in the YAML manifests.
292+
// Valid values are:
293+
//
294+
// - WithVariables (the default): require at least one variable to be defined,
295+
// either through the inline map or through the resolved references to ConfigMaps
296+
// and Secrets.
297+
// - Always: perform the substitution even if no variables are defined.
298+
//
299+
// +kubebuilder:validation:Enum=WithVariables;Always
300+
// +optional
301+
SubstituteStrategy SubstituteStrategy `json:"substituteStrategy,omitempty"`
302+
277303
// Substitute holds a map of key/value pairs.
278304
// The variables defined in your YAML manifests that match any of the keys
279305
// defined in the map will be substituted with the set value.
@@ -402,6 +428,14 @@ func (in Kustomization) GetDependsOn() []meta.DependencyReference {
402428
return deps
403429
}
404430

431+
// GetSubstituteStrategy returns the substitute strategy with default.
432+
func (in Kustomization) GetSubstituteStrategy() SubstituteStrategy {
433+
if in.Spec.PostBuild == nil || in.Spec.PostBuild.SubstituteStrategy == "" {
434+
return SubstituteStrategyWithVariables
435+
}
436+
return in.Spec.PostBuild.SubstituteStrategy
437+
}
438+
405439
// GetConditions returns the status conditions of the object.
406440
func (in Kustomization) GetConditions() []metav1.Condition {
407441
return in.Status.Conditions

config/crd/bases/kustomize.toolkit.fluxcd.io_kustomizations.yaml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -546,6 +546,19 @@ spec:
546546
- name
547547
type: object
548548
type: array
549+
substituteStrategy:
550+
description: |-
551+
SubstituteStrategy defines the strategy for substituting variables in the YAML manifests.
552+
Valid values are:
553+
554+
- WithVariables (the default): require at least one variable to be defined,
555+
either through the inline map or through the resolved references to ConfigMaps
556+
and Secrets.
557+
- Always: perform the substitution even if no variables are defined.
558+
enum:
559+
- WithVariables
560+
- Always
561+
type: string
549562
type: object
550563
prune:
551564
description: Prune enables garbage collection.

docs/api/v1/kustomize.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1279,6 +1279,27 @@ generated by building the kustomize overlay.</p>
12791279
<tbody>
12801280
<tr>
12811281
<td>
1282+
<code>substituteStrategy</code><br>
1283+
<em>
1284+
<a href="#kustomize.toolkit.fluxcd.io/v1.SubstituteStrategy">
1285+
SubstituteStrategy
1286+
</a>
1287+
</em>
1288+
</td>
1289+
<td>
1290+
<em>(Optional)</em>
1291+
<p>SubstituteStrategy defines the strategy for substituting variables in the YAML manifests.
1292+
Valid values are:</p>
1293+
<ul>
1294+
<li>WithVariables (the default): require at least one variable to be defined,
1295+
either through the inline map or through the resolved references to ConfigMaps
1296+
and Secrets.</li>
1297+
<li>Always: perform the substitution even if no variables are defined.</li>
1298+
</ul>
1299+
</td>
1300+
</tr>
1301+
<tr>
1302+
<td>
12821303
<code>substitute</code><br>
12831304
<em>
12841305
map[string]string
@@ -1453,6 +1474,13 @@ as if the resource was present but empty, without any variables defined.</p>
14531474
</table>
14541475
</div>
14551476
</div>
1477+
<h3 id="kustomize.toolkit.fluxcd.io/v1.SubstituteStrategy">SubstituteStrategy
1478+
(<code>string</code> alias)</h3>
1479+
<p>
1480+
(<em>Appears on:</em>
1481+
<a href="#kustomize.toolkit.fluxcd.io/v1.PostBuild">PostBuild</a>)
1482+
</p>
1483+
<p>SubstituteStrategy defines the strategy for substituting variables in the YAML manifests.</p>
14561484
<h3 id="kustomize.toolkit.fluxcd.io/v1.VaultConfig">VaultConfig
14571485
</h3>
14581486
<p>VaultConfig is the controller-level configuration that enables and scopes

docs/spec/v1/kustomizations.md

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -829,11 +829,17 @@ labelling or annotating them with:
829829
kustomize.toolkit.fluxcd.io/substitute: disabled
830830
```
831831

832-
Substitution of variables only happens if at least a single variable or resource
833-
to substitute from is defined. This may cause issues if you rely on expressions
832+
By default, substitution of variables only happens if at least a single variable
833+
is available, either defined in-line with `substitute` or resolved from the
834+
ConfigMaps and Secrets referenced in `substituteFrom`. Note that defining a
835+
`substituteFrom` reference is not enough on its own: the referenced ConfigMaps
836+
and Secrets must be resolved and actually contain variables, otherwise the
837+
substitution is still skipped. This may cause issues if you rely on expressions
834838
which should evaluate to a default value, even if no other variables are
835-
configured. To work around this, one can set an arbitrary key/value pair to
836-
enable the substitution of variables. For example:
839+
configured, e.g. `${var:=default}`.
840+
841+
To always perform the substitution regardless of whether any variables are
842+
defined, set `.spec.postBuild.substituteStrategy` to `Always`:
837843

838844
```yaml
839845
apiVersion: kustomize.toolkit.fluxcd.io/v1
@@ -843,10 +849,20 @@ metadata:
843849
spec:
844850
# ...omitted for brevity
845851
postBuild:
846-
substitute:
847-
var_substitution_enabled: "true"
852+
substituteStrategy: Always
848853
```
849854

855+
The `.spec.postBuild.substituteStrategy` field accepts the following values:
856+
857+
- `WithVariables` (default): the substitution is only performed if at least one
858+
variable is available, either defined in-line with `substitute` or resolved
859+
from the ConfigMaps and Secrets referenced in `substituteFrom`. Note that a
860+
`substituteFrom` reference to an empty ConfigMap or Secret yields no variables,
861+
so the substitution is still skipped.
862+
- `Always`: the substitution is always performed, even if no variables are
863+
defined. This is useful when the substitution expressions have defaults, e.g.
864+
`${var:=default}`.
865+
850866
**Note:** When using numbers or booleans as values for variables, they must be
851867
enclosed in double quotes vars to be treated as strings, for more information see
852868
[substitution of numbers and booleans](#post-build-substitution-of-numbers-and-booleans).

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ require (
2727
github.com/fluxcd/pkg/auth v0.54.0
2828
github.com/fluxcd/pkg/cache v0.14.0
2929
github.com/fluxcd/pkg/http/fetch v0.25.0
30-
github.com/fluxcd/pkg/kustomize v1.34.0
30+
github.com/fluxcd/pkg/kustomize v1.35.0
3131
github.com/fluxcd/pkg/runtime v0.110.0
3232
github.com/fluxcd/pkg/ssa v0.76.0
3333
github.com/fluxcd/pkg/tar v1.2.0

go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -219,8 +219,8 @@ github.com/fluxcd/pkg/envsubst v1.7.0 h1:PL9Nj/V2fgaMR9KYZR7mEEw+vlYgP80nFZjOQQK
219219
github.com/fluxcd/pkg/envsubst v1.7.0/go.mod h1:aoWeSIOamhqBZ3bHVj1GDwpdA10DXrI8yYbyjPiFly0=
220220
github.com/fluxcd/pkg/http/fetch v0.25.0 h1:BM2sv9MygBBQ7+G+ISuTEGLVRV4GX9bpKK5UoAKLiQo=
221221
github.com/fluxcd/pkg/http/fetch v0.25.0/go.mod h1:UOpeGiEBB+I7jW8uMBdptnvWa4zlyGnGfGwUxtzM4vo=
222-
github.com/fluxcd/pkg/kustomize v1.34.0 h1:plmUt9VcvD/mtdfYhyxmoCi51OiZ4KBBlGATn20NJRs=
223-
github.com/fluxcd/pkg/kustomize v1.34.0/go.mod h1:M1jIz03OQ86QmcsO+2W3y0+X4B0dbh2QLUF8h3gzyJE=
222+
github.com/fluxcd/pkg/kustomize v1.35.0 h1:Hl3Y6WPPo1btDyUTrT+CS3b4GbAqCuE+bZebpIG2cxg=
223+
github.com/fluxcd/pkg/kustomize v1.35.0/go.mod h1:M1jIz03OQ86QmcsO+2W3y0+X4B0dbh2QLUF8h3gzyJE=
224224
github.com/fluxcd/pkg/runtime v0.110.0 h1:ziGAuoQ3OVSEqmMXS6doZWi2LcF7exEKPe69dun5RNg=
225225
github.com/fluxcd/pkg/runtime v0.110.0/go.mod h1:r3X179AZAuUquYAKYFDxCJk5yYpAKTJatfNWTLlA9/s=
226226
github.com/fluxcd/pkg/sourceignore v0.18.0 h1:WU2tPKasG9AM7/H/LlqdjULyaSknnZBTrpHsDDtOuns=

internal/controller/kustomization_controller.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -814,8 +814,10 @@ func (r *KustomizationReconciler) build(ctx context.Context,
814814

815815
// run variable substitutions
816816
if obj.Spec.PostBuild != nil {
817+
always := obj.GetSubstituteStrategy() == kustomizev1.SubstituteStrategyAlways
817818
outRes, err := generator.SubstituteVariables(ctx, r.Client, u, res,
818-
generator.SubstituteWithStrict(r.StrictSubstitutions))
819+
generator.SubstituteWithStrict(r.StrictSubstitutions),
820+
generator.SubstituteWithAlways(always))
819821
if err != nil {
820822
return nil, fmt.Errorf("post build failed for '%s/%s': %w", res.GetGvk(), res.GetName(), err)
821823
}

internal/controller/kustomization_varsub_test.go

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -350,6 +350,153 @@ metadata:
350350
})
351351
}
352352

353+
func TestKustomizationReconciler_VarsubAlways(t *testing.T) {
354+
ctx := context.Background()
355+
356+
g := NewWithT(t)
357+
id := "vars-" + randStringRunes(5)
358+
revision := "v1.0.0/" + randStringRunes(7)
359+
360+
err := createNamespace(id)
361+
g.Expect(err).NotTo(HaveOccurred(), "failed to create test namespace")
362+
363+
err = createKubeConfigSecret(id)
364+
g.Expect(err).NotTo(HaveOccurred(), "failed to create kubeconfig secret")
365+
366+
// The manifest only relies on substitution expressions with defaults and
367+
// defines no variables through substitute or substituteFrom.
368+
manifests := func(name string) []testserver.File {
369+
return []testserver.File{
370+
{
371+
Name: "service-account.yaml",
372+
Body: fmt.Sprintf(`
373+
apiVersion: v1
374+
kind: ServiceAccount
375+
metadata:
376+
name: %[1]s
377+
namespace: %[1]s
378+
labels:
379+
color: "${color:=blue}"
380+
shape: "${shape:=square}"
381+
`, name),
382+
},
383+
}
384+
}
385+
386+
artifact, err := testServer.ArtifactFromFiles(manifests(id))
387+
g.Expect(err).NotTo(HaveOccurred())
388+
389+
repositoryName := types.NamespacedName{
390+
Name: randStringRunes(5),
391+
Namespace: id,
392+
}
393+
394+
err = applyGitRepository(repositoryName, artifact, revision)
395+
g.Expect(err).NotTo(HaveOccurred())
396+
397+
resultSA := &corev1.ServiceAccount{}
398+
399+
t.Run("WithVariables strategy skips substitution without variables", func(t *testing.T) {
400+
g := NewWithT(t)
401+
402+
name := id + "-with-variables"
403+
inputK := &kustomizev1.Kustomization{
404+
ObjectMeta: metav1.ObjectMeta{
405+
Name: name,
406+
Namespace: id,
407+
},
408+
Spec: kustomizev1.KustomizationSpec{
409+
KubeConfig: &meta.KubeConfigReference{
410+
SecretRef: &meta.SecretKeyReference{
411+
Name: "kubeconfig",
412+
},
413+
},
414+
Interval: metav1.Duration{Duration: reconciliationInterval},
415+
Path: "./",
416+
Prune: true,
417+
TargetNamespace: id,
418+
SourceRef: kustomizev1.CrossNamespaceSourceReference{
419+
Kind: sourcev1.GitRepositoryKind,
420+
Name: repositoryName.Name,
421+
},
422+
// PostBuild without variables and the default WithVariables strategy.
423+
PostBuild: &kustomizev1.PostBuild{},
424+
Wait: true,
425+
},
426+
}
427+
g.Expect(k8sClient.Create(ctx, inputK)).Should(Succeed())
428+
defer func() { g.Expect(k8sClient.Delete(ctx, inputK)).To(Succeed()) }()
429+
430+
// The ServiceAccount fails to apply because the unresolved expressions
431+
// are invalid label values, proving substitution was skipped.
432+
g.Eventually(func() bool {
433+
resultK := &kustomizev1.Kustomization{}
434+
_ = k8sClient.Get(ctx, client.ObjectKeyFromObject(inputK), resultK)
435+
for _, c := range resultK.Status.Conditions {
436+
if c.Reason == meta.ReconciliationFailedReason && c.Status == metav1.ConditionFalse {
437+
return true
438+
}
439+
}
440+
return false
441+
}, timeout, interval).Should(BeTrue())
442+
})
443+
444+
t.Run("Always strategy substitutes defaults without variables", func(t *testing.T) {
445+
g := NewWithT(t)
446+
447+
name := id + "-always"
448+
inputK := &kustomizev1.Kustomization{
449+
ObjectMeta: metav1.ObjectMeta{
450+
Name: name,
451+
Namespace: id,
452+
},
453+
Spec: kustomizev1.KustomizationSpec{
454+
KubeConfig: &meta.KubeConfigReference{
455+
SecretRef: &meta.SecretKeyReference{
456+
Name: "kubeconfig",
457+
},
458+
},
459+
Interval: metav1.Duration{Duration: reconciliationInterval},
460+
Path: "./",
461+
Prune: true,
462+
SourceRef: kustomizev1.CrossNamespaceSourceReference{
463+
Kind: sourcev1.GitRepositoryKind,
464+
Name: repositoryName.Name,
465+
},
466+
// PostBuild with no variables but the Always strategy.
467+
PostBuild: &kustomizev1.PostBuild{
468+
SubstituteStrategy: kustomizev1.SubstituteStrategyAlways,
469+
},
470+
HealthChecks: []meta.NamespacedObjectKindReference{
471+
{
472+
APIVersion: "v1",
473+
Kind: "ServiceAccount",
474+
Name: id,
475+
Namespace: id,
476+
},
477+
},
478+
},
479+
}
480+
g.Expect(k8sClient.Create(ctx, inputK)).Should(Succeed())
481+
defer func() { g.Expect(k8sClient.Delete(ctx, inputK)).To(Succeed()) }()
482+
483+
g.Eventually(func() bool {
484+
resultK := &kustomizev1.Kustomization{}
485+
_ = k8sClient.Get(ctx, client.ObjectKeyFromObject(inputK), resultK)
486+
for _, c := range resultK.Status.Conditions {
487+
if c.Reason == meta.ReconciliationSucceededReason {
488+
return true
489+
}
490+
}
491+
return false
492+
}, timeout, interval).Should(BeTrue())
493+
494+
g.Expect(k8sClient.Get(ctx, types.NamespacedName{Name: id, Namespace: id}, resultSA)).Should(Succeed())
495+
g.Expect(resultSA.Labels["color"]).To(Equal("blue"))
496+
g.Expect(resultSA.Labels["shape"]).To(Equal("square"))
497+
})
498+
}
499+
353500
func TestKustomizationReconciler_VarsubNumberBool(t *testing.T) {
354501
ctx := context.Background()
355502

0 commit comments

Comments
 (0)