Skip to content

Commit afae843

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 afae843

12 files changed

Lines changed: 778 additions & 30 deletions

File tree

charts/fleet-crd/templates/crds.yaml

Lines changed: 88 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,34 @@ 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+
- ''
3241+
type: string
32013242
yaml:
32023243
description: 'YAML options, if using raw YAML these are names
32033244
that map to
@@ -8708,17 +8749,30 @@ spec:
87088749
description: ServiceAccount which will be used to perform this deployment.
87098750
nullable: true
87108751
type: string
8752+
targetCustomizationMode:
8753+
default: FirstMatch
8754+
description: 'TargetCustomizationMode controls how targetCustomizations
8755+
from fleet.yaml
8756+
8757+
are evaluated. "FirstMatch" (default) stops at the first matching
8758+
entry.
8759+
8760+
"AllMatches" applies all matching entries in order, merging them.'
8761+
enum:
8762+
- FirstMatch
8763+
- AllMatches
8764+
type: string
87118765
targetRestrictions:
87128766
description: TargetRestrictions is an allow list, which controls
87138767
if a bundledeployment is created for a target.
87148768
items:
87158769
description: 'BundleTargetRestriction is used internally by Fleet
87168770
and should not be modified.
87178771
8718-
It acts as an allow list, to prevent the creation of BundleDeployments
8719-
from
8772+
It acts as an allow list, restricting BundleDeployment creation
8773+
to only those clusters
87208774
8721-
Targets created by TargetCustomizations in fleet.yaml.'
8775+
that are explicitly listed in the GitRepo targets.'
87228776
properties:
87238777
clusterGroup:
87248778
nullable: true
@@ -9445,6 +9499,34 @@ spec:
94459499
this deployment.
94469500
nullable: true
94479501
type: string
9502+
source:
9503+
description: 'Source indicates the origin of this target.
9504+
9505+
"customization" - target comes from fleet.yaml targetCustomizations
9506+
9507+
"gitrepo" - target comes from GitRepo.Spec.Targets (via
9508+
targets file)
9509+
9510+
9511+
If empty, provenance is determined by position in the Targets
9512+
array:
9513+
9514+
- First N targets are customizations where N = len(Targets)
9515+
- len(TargetRestrictions)
9516+
9517+
- Remaining targets are GitRepo targets
9518+
9519+
9520+
This field enables explicit provenance tracking for better
9521+
maintainability
9522+
9523+
while maintaining backward compatibility with Bundles created
9524+
before this field existed.'
9525+
enum:
9526+
- customization
9527+
- gitrepo
9528+
- ''
9529+
type: string
94489530
yaml:
94499531
description: 'YAML options, if using raw YAML these are names
94509532
that map to

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 {

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

internal/bundlereader/read_test.go

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
package bundlereader
2+
3+
import (
4+
"context"
5+
"testing"
6+
7+
"github.com/stretchr/testify/assert"
8+
"github.com/stretchr/testify/require"
9+
10+
fleet "github.com/rancher/fleet/pkg/apis/fleet.cattle.io/v1alpha1"
11+
)
12+
13+
// TestBundleFromDir_TargetCustomizationModePropagate verifies that the
14+
// targetCustomizationMode field from fleet.yaml is correctly unmarshalled into
15+
// bundle.Spec.TargetCustomizationMode via the embedded BundleSpec.
16+
func TestBundleFromDir_TargetCustomizationModePropagate(t *testing.T) {
17+
dir := t.TempDir()
18+
19+
tests := []struct {
20+
name string
21+
yaml string
22+
wantMode fleet.TargetCustomizationMode
23+
}{
24+
{
25+
name: "AllMatches is propagated to BundleSpec",
26+
yaml: `targetCustomizationMode: AllMatches`,
27+
wantMode: fleet.TargetCustomizationModeAllMatches,
28+
},
29+
{
30+
name: "FirstMatch is propagated to BundleSpec",
31+
yaml: `targetCustomizationMode: FirstMatch`,
32+
wantMode: fleet.TargetCustomizationModeFirstMatch,
33+
},
34+
{
35+
name: "omitted mode propagates as empty string (controller uses default)",
36+
yaml: `namespace: test`,
37+
wantMode: "",
38+
},
39+
}
40+
41+
for _, tt := range tests {
42+
t.Run(tt.name, func(t *testing.T) {
43+
bundle, _, err := bundleFromDir(context.Background(), "test", dir, []byte(tt.yaml), nil)
44+
require.NoError(t, err)
45+
assert.Equal(t, tt.wantMode, bundle.Spec.TargetCustomizationMode)
46+
})
47+
}
48+
}

internal/cmd/controller/helmops/reconciler/helmop_controller.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,7 @@ func (r *HelmOpReconciler) calculateBundle(helmop *fleet.HelmOp) *fleet.Bundle {
227227
{
228228
Name: "default",
229229
ClusterGroup: "default",
230+
Source: "gitrepo",
230231
},
231232
}
232233
}

0 commit comments

Comments
 (0)