Skip to content

Commit 4bf96e3

Browse files
committed
feat: add TargetCustomizationMode with FirstMatch and AllMatches options
Introduces TargetCustomizationMode on BundleSpec and fleet.yaml. The default "FirstMatch" preserves existing behaviour. "AllMatches" applies all matching targetCustomizations in order, merging their options into the base deployment options. Customization targets are now identified via hasMatchingRestriction() rather than relying on their position in the Targets slice.
1 parent aa2ccc8 commit 4bf96e3

14 files changed

Lines changed: 794 additions & 32 deletions

File tree

charts/fleet-crd/templates/crds.yaml

Lines changed: 90 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2461,17 +2461,30 @@ spec:
24612461
description: ServiceAccount which will be used to perform this deployment.
24622462
nullable: true
24632463
type: string
2464+
targetCustomizationMode:
2465+
default: FirstMatch
2466+
description: 'TargetCustomizationMode controls how targetCustomizations
2467+
from fleet.yaml
2468+
2469+
are evaluated. "FirstMatch" (default) stops at the first matching
2470+
entry.
2471+
2472+
"AllMatches" applies all matching entries in order, merging them.'
2473+
enum:
2474+
- FirstMatch
2475+
- AllMatches
2476+
type: string
24642477
targetRestrictions:
24652478
description: TargetRestrictions is an allow list, which controls
24662479
if a bundledeployment is created for a target.
24672480
items:
24682481
description: 'BundleTargetRestriction is used internally by Fleet
24692482
and should not be modified.
24702483
2471-
It acts as an allow list, to prevent the creation of BundleDeployments
2472-
from
2484+
It acts as an allow list, restricting BundleDeployment creation
2485+
to only those clusters
24732486
2474-
Targets created by TargetCustomizations in fleet.yaml.'
2487+
that are explicitly listed in the GitRepo targets.'
24752488
properties:
24762489
clusterGroup:
24772490
nullable: true
@@ -3198,6 +3211,35 @@ spec:
31983211
this deployment.
31993212
nullable: true
32003213
type: string
3214+
source:
3215+
description: 'Source indicates the origin of this target.
3216+
3217+
"customization" - target comes from fleet.yaml targetCustomizations
3218+
3219+
"gitrepo" - target comes from GitRepo.Spec.Targets (via
3220+
targets file)
3221+
3222+
3223+
If empty, provenance is determined by position in the Targets
3224+
array:
3225+
3226+
- First N targets are customizations where N = len(Targets)
3227+
- len(TargetRestrictions)
3228+
3229+
- Remaining targets are GitRepo targets
3230+
3231+
3232+
This field enables explicit provenance tracking for better
3233+
maintainability
3234+
3235+
while maintaining backward compatibility with Bundles created
3236+
before this field existed.'
3237+
enum:
3238+
- customization
3239+
- gitrepo
3240+
- helmop
3241+
- ''
3242+
type: string
32013243
yaml:
32023244
description: 'YAML options, if using raw YAML these are names
32033245
that map to
@@ -8708,17 +8750,30 @@ spec:
87088750
description: ServiceAccount which will be used to perform this deployment.
87098751
nullable: true
87108752
type: string
8753+
targetCustomizationMode:
8754+
default: FirstMatch
8755+
description: 'TargetCustomizationMode controls how targetCustomizations
8756+
from fleet.yaml
8757+
8758+
are evaluated. "FirstMatch" (default) stops at the first matching
8759+
entry.
8760+
8761+
"AllMatches" applies all matching entries in order, merging them.'
8762+
enum:
8763+
- FirstMatch
8764+
- AllMatches
8765+
type: string
87118766
targetRestrictions:
87128767
description: TargetRestrictions is an allow list, which controls
87138768
if a bundledeployment is created for a target.
87148769
items:
87158770
description: 'BundleTargetRestriction is used internally by Fleet
87168771
and should not be modified.
87178772
8718-
It acts as an allow list, to prevent the creation of BundleDeployments
8719-
from
8773+
It acts as an allow list, restricting BundleDeployment creation
8774+
to only those clusters
87208775
8721-
Targets created by TargetCustomizations in fleet.yaml.'
8776+
that are explicitly listed in the GitRepo targets.'
87228777
properties:
87238778
clusterGroup:
87248779
nullable: true
@@ -9445,6 +9500,35 @@ spec:
94459500
this deployment.
94469501
nullable: true
94479502
type: string
9503+
source:
9504+
description: 'Source indicates the origin of this target.
9505+
9506+
"customization" - target comes from fleet.yaml targetCustomizations
9507+
9508+
"gitrepo" - target comes from GitRepo.Spec.Targets (via
9509+
targets file)
9510+
9511+
9512+
If empty, provenance is determined by position in the Targets
9513+
array:
9514+
9515+
- First N targets are customizations where N = len(Targets)
9516+
- len(TargetRestrictions)
9517+
9518+
- Remaining targets are GitRepo targets
9519+
9520+
9521+
This field enables explicit provenance tracking for better
9522+
maintainability
9523+
9524+
while maintaining backward compatibility with Bundles created
9525+
before this field existed.'
9526+
enum:
9527+
- customization
9528+
- gitrepo
9529+
- helmop
9530+
- ''
9531+
type: string
94489532
yaml:
94499533
description: 'YAML options, if using raw YAML these are names
94509534
that map to

integrationtests/cli/apply/apply_online_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ data:
105105
{
106106
Name: "default",
107107
ClusterGroup: "default",
108+
Source: "gitrepo",
108109
},
109110
},
110111
},

integrationtests/cli/apply/targetsfile_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ var _ = Describe("Fleet apply targets", func() {
5959
)
6060

6161
BeforeEach(func() {
62-
targets = []fleet.BundleTarget{{Name: "target1", ClusterName: "test1"}}
62+
targets = []fleet.BundleTarget{{Name: "target1", ClusterName: "test1", Source: "gitrepo"}}
6363
targetRestrictions = []fleet.BundleTargetRestriction{{Name: "target1", ClusterName: "test1"}}
6464
file := createTargetsFile(targets, targetRestrictions)
6565
options = apply.Options{TargetsFile: file.Name()}
@@ -81,7 +81,7 @@ var _ = Describe("Fleet apply targets", func() {
8181
)
8282

8383
BeforeEach(func() {
84-
targets = []fleet.BundleTarget{{Name: "target1", ClusterName: "test1"}}
84+
targets = []fleet.BundleTarget{{Name: "target1", ClusterName: "test1", Source: "gitrepo"}}
8585
targetRestrictions = []fleet.BundleTargetRestriction{{Name: "target1", ClusterName: "test1"}}
8686
file := createTargetsFile(targets, targetRestrictions)
8787
options = apply.Options{TargetsFile: file.Name()}

integrationtests/controller/bundle/bundle_targets_test.go

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -588,6 +588,73 @@ var _ = Describe("Bundle targets", Ordered, func() {
588588
}
589589
})
590590
})
591+
592+
// AllMatches mode: every matching targetCustomization is applied in order,
593+
// so a cluster that matches two customizations receives the merged options
594+
// from both, whereas FirstMatch would stop at the first.
595+
When("TargetCustomizationMode is AllMatches and two customizations match cluster one", func() {
596+
BeforeEach(func() {
597+
bundleName = "all-matches-mode"
598+
bdLabels = map[string]string{
599+
"fleet.cattle.io/bundle-name": bundleName,
600+
"fleet.cattle.io/bundle-namespace": namespace,
601+
}
602+
expectedNumberOfBundleDeployments = 3
603+
604+
// Customization 1: only cluster group "one" → sets region
605+
// Customization 2: all clusters → sets env
606+
// GitRepo target: all clusters (no extra values)
607+
targetsInGitRepo := []v1alpha1.BundleTarget{
608+
{ClusterGroup: "all"},
609+
}
610+
targets = []v1alpha1.BundleTarget{
611+
{
612+
BundleDeploymentOptions: v1alpha1.BundleDeploymentOptions{
613+
Helm: &v1alpha1.HelmOptions{
614+
Values: &v1alpha1.GenericMap{Data: map[string]interface{}{"region": "us-west"}},
615+
},
616+
},
617+
ClusterGroup: "one",
618+
},
619+
{
620+
BundleDeploymentOptions: v1alpha1.BundleDeploymentOptions{
621+
Helm: &v1alpha1.HelmOptions{
622+
Values: &v1alpha1.GenericMap{Data: map[string]interface{}{"env": "prod"}},
623+
},
624+
},
625+
ClusterGroup: "all",
626+
},
627+
}
628+
targetRestrictions = make([]v1alpha1.BundleTarget, len(targetsInGitRepo))
629+
copy(targetRestrictions, targetsInGitRepo)
630+
targets = append(targets, targetsInGitRepo...)
631+
})
632+
633+
JustBeforeEach(func() {
634+
// The outer JustBeforeEach already created the bundle with FirstMatch
635+
// (the default). Patch it to AllMatches so the reconciler re-evaluates.
636+
mod := bundle.DeepCopy()
637+
mod.Spec.TargetCustomizationMode = v1alpha1.TargetCustomizationModeAllMatches
638+
Expect(k8sClient.Patch(ctx, mod, client.MergeFrom(bundle))).ToNot(HaveOccurred())
639+
bundle = mod
640+
})
641+
642+
It("merges all matching customizations into cluster one's BundleDeployment", func() {
643+
bdList := verifyBundlesDeploymentsAreCreated(expectedNumberOfBundleDeployments, bdLabels, bundleName)
644+
for _, bd := range bdList.Items {
645+
values, _ := loadValues(bd)
646+
if strings.Contains(bd.Namespace, "cluster-one") {
647+
// cluster "one" matches both customizations: both keys must be present
648+
Expect(values).To(HaveKeyWithValue("region", "us-west"), "cluster-one should have region from cust1")
649+
Expect(values).To(HaveKeyWithValue("env", "prod"), "cluster-one should have env from cust2")
650+
} else {
651+
// other clusters match only cust2
652+
Expect(values).ToNot(HaveKey("region"), "non-one clusters should not have region")
653+
Expect(values).To(HaveKeyWithValue("env", "prod"), "non-one clusters should have env from cust2")
654+
}
655+
}
656+
})
657+
})
591658
})
592659

593660
func verifyBundlesDeploymentsAreCreated(numBundleDeployments int, bdLabels map[string]string, bundleName string) *v1alpha1.BundleDeploymentList {

integrationtests/helmops/controller/controller_test.go

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -406,6 +406,7 @@ var _ = Describe("HelmOps controller", func() {
406406
{
407407
Name: "default",
408408
ClusterGroup: "default",
409+
Source: "helmop",
409410
},
410411
}
411412

@@ -455,6 +456,7 @@ var _ = Describe("HelmOps controller", func() {
455456
{
456457
Name: "default",
457458
ClusterGroup: "default",
459+
Source: "helmop",
458460
},
459461
}
460462

@@ -485,6 +487,7 @@ var _ = Describe("HelmOps controller", func() {
485487
{
486488
Name: "default",
487489
ClusterGroup: "default",
490+
Source: "helmop",
488491
},
489492
}
490493
checkBundleIsAsExpected(g, *bundle, helmop, t)
@@ -587,6 +590,7 @@ var _ = Describe("HelmOps controller", func() {
587590
{
588591
Name: "default",
589592
ClusterGroup: "default",
593+
Source: "helmop",
590594
},
591595
}
592596

@@ -652,6 +656,7 @@ var _ = Describe("HelmOps controller", func() {
652656
{
653657
Name: "default",
654658
ClusterGroup: "default",
659+
Source: "helmop",
655660
},
656661
}
657662
// the original helmop has no version defined.
@@ -676,6 +681,7 @@ var _ = Describe("HelmOps controller", func() {
676681
{
677682
Name: "default",
678683
ClusterGroup: "default",
684+
Source: "helmop",
679685
},
680686
}
681687
// the original helmop has no version defined.
@@ -704,6 +710,7 @@ var _ = Describe("HelmOps controller", func() {
704710
{
705711
Name: "default",
706712
ClusterGroup: "default",
713+
Source: "helmop",
707714
},
708715
}
709716
// the original helmop has no version defined.
@@ -1023,6 +1030,7 @@ var _ = Describe("HelmOps controller", func() {
10231030
{
10241031
Name: "default",
10251032
ClusterGroup: "default",
1033+
Source: "helmop",
10261034
},
10271035
}
10281036
// the original helmop has no version defined.
@@ -1202,6 +1210,7 @@ var _ = Describe("HelmOps controller", func() {
12021210
{
12031211
Name: "default",
12041212
ClusterGroup: "default",
1213+
Source: "helmop",
12051214
},
12061215
}
12071216
// the original helmop has no version defined.
@@ -1276,6 +1285,7 @@ var _ = Describe("HelmOps controller", func() {
12761285
{
12771286
Name: "default",
12781287
ClusterGroup: "default",
1288+
Source: "helmop",
12791289
},
12801290
}
12811291
// the original helmop has no version defined.
@@ -1316,7 +1326,7 @@ var _ = Describe("HelmOps controller", func() {
13161326
ns := types.NamespacedName{Name: helmop.Name, Namespace: helmop.Namespace}
13171327
err := k8sClient.Get(ctx, ns, bundle)
13181328
g.Expect(err).ToNot(HaveOccurred())
1319-
t := []fleet.BundleTarget{{Name: "default", ClusterGroup: "default"}}
1329+
t := []fleet.BundleTarget{{Name: "default", ClusterGroup: "default", Source: "helmop"}}
13201330
helmop.Spec.Helm.Version = "0.2.0"
13211331
checkBundleIsAsExpected(g, *bundle, helmop, t)
13221332
}).Should(Succeed())
@@ -1350,7 +1360,7 @@ var _ = Describe("HelmOps controller", func() {
13501360
ns := types.NamespacedName{Name: helmop.Name, Namespace: helmop.Namespace}
13511361
err := k8sClient.Get(ctx, ns, bundle)
13521362
g.Expect(err).ToNot(HaveOccurred())
1353-
t := []fleet.BundleTarget{{Name: "default", ClusterGroup: "default"}}
1363+
t := []fleet.BundleTarget{{Name: "default", ClusterGroup: "default", Source: "helmop"}}
13541364
helmop.Spec.Helm.Version = "0.2.0"
13551365
checkBundleIsAsExpected(g, *bundle, helmop, t)
13561366
}).Should(Succeed())

internal/bundlereader/read.go

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,11 @@ func bundleFromDir(ctx context.Context, name, baseDir string, bundleData []byte,
184184
})
185185
}
186186

187-
fy.Targets = append(fy.Targets, fy.TargetCustomizations...)
187+
// Mark targetCustomizations with explicit source
188+
for _, tc := range fy.TargetCustomizations {
189+
tc.Source = "customization"
190+
fy.Targets = append(fy.Targets, tc)
191+
}
188192

189193
meta, err := readMetadata(bundleData)
190194
if err != nil {
@@ -244,6 +248,7 @@ func bundleFromDir(ctx context.Context, name, baseDir string, bundleData []byte,
244248
ClusterSelector: target.ClusterSelector,
245249
ClusterGroup: target.ClusterGroup,
246250
ClusterGroupSelector: target.ClusterGroupSelector,
251+
Source: "gitrepo", // OverrideTargets replace GitRepo targets
247252
})
248253
bundle.Spec.TargetRestrictions = append(bundle.Spec.TargetRestrictions, fleet.BundleTargetRestriction(target))
249254
}
@@ -259,6 +264,7 @@ func bundleFromDir(ctx context.Context, name, baseDir string, bundleData []byte,
259264
{
260265
Name: "default",
261266
ClusterGroup: "default",
267+
Source: "gitrepo",
262268
},
263269
}
264270
}
@@ -328,7 +334,11 @@ func appendTargets(def *fleet.Bundle, targetsFile string) (*fleet.Bundle, error)
328334
return nil, err
329335
}
330336

331-
def.Spec.Targets = append(def.Spec.Targets, spec.Targets...)
337+
// Mark GitRepo targets with explicit source
338+
for _, target := range spec.Targets {
339+
target.Source = "gitrepo"
340+
def.Spec.Targets = append(def.Spec.Targets, target)
341+
}
332342
def.Spec.TargetRestrictions = append(def.Spec.TargetRestrictions, spec.TargetRestrictions...)
333343

334344
return def, nil

0 commit comments

Comments
 (0)