Skip to content

Commit 2d6f9e5

Browse files
feat: simplify selection count check in NamespaceWithResourceSelectors selection scope (#488)
1 parent 881ede3 commit 2d6f9e5

2 files changed

Lines changed: 44 additions & 106 deletions

File tree

pkg/utils/controller/resource_selector_resolver.go

Lines changed: 26 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -248,22 +248,18 @@ func (rs *ResourceSelectorResolver) gatherSelectedResource(placementKey types.Na
248248
Kind: selector.Kind,
249249
}
250250
if gvk == utils.NamespaceGVK && placementKey.Namespace == "" && selector.SelectionScope == placementv1beta1.NamespaceWithResourceSelectors {
251-
namespaces, err := rs.fetchSelectedNamespaces(selector, placementKey.Name)
251+
namespace, found, err := rs.fetchSelectedNamespace(selector, placementKey.Name)
252252
if err != nil {
253253
return nil, err
254254
}
255255
// NamespaceWithResourceSelectors mode requires exactly one namespace
256-
if len(namespaces) == 0 {
256+
if !found {
257257
err := fmt.Errorf("invalid clusterResourcePlacement %s: NamespaceWithResourceSelectors mode requires exactly one namespace, but no namespaces were selected", placementKey.Name)
258258
klog.ErrorS(err, "No namespaces selected with NamespaceWithResourceSelectors mode", "placement", placementKey.Name)
259259
return nil, NewUserError(err)
260260
}
261-
if len(namespaces) > 1 {
262-
err := fmt.Errorf("invalid clusterResourcePlacement %s: NamespaceWithResourceSelectors mode requires exactly one namespace, but %d namespaces were selected", placementKey.Name, len(namespaces))
263-
klog.ErrorS(err, "Multiple namespaces selected with NamespaceWithResourceSelectors mode", "placement", placementKey.Name, "namespaceCount", len(namespaces))
264-
return nil, NewUserError(err)
265-
}
266-
selectedNamespace = namespaces[0]
261+
// Note: CEL validation ensures that at most one namespace can be selected.
262+
selectedNamespace = namespace
267263
break
268264
}
269265
}
@@ -591,32 +587,37 @@ func (rs *ResourceSelectorResolver) fetchResources(selector placementv1beta1.Res
591587
return selectedObjs, nil
592588
}
593589

594-
// fetchSelectedNamespaces retrieves the namespace names based on the namespace selector.
595-
func (rs *ResourceSelectorResolver) fetchSelectedNamespaces(selector placementv1beta1.ResourceSelectorTerm, placementName string) ([]string, error) {
596-
klog.V(2).InfoS("start to fetch selected namespaces", "selector", selector, "placement", placementName)
590+
// fetchSelectedNamespace retrieves the namespace name based on the namespace selector.
591+
// This function assumes the selector is a name-based selector (not label-based) as guaranteed by CEL validation.
592+
// Returns the namespace name if found and not skipped, or empty string if not found/skipped.
593+
func (rs *ResourceSelectorResolver) fetchSelectedNamespace(selector placementv1beta1.ResourceSelectorTerm, placementName string) (string, bool, error) {
594+
klog.V(2).InfoS("start to fetch selected namespace", "selector", selector, "placement", placementName)
597595

598596
// Use fetchResources to get namespace objects (handles label selector, name selector, deletion timestamps)
599597
placementKey := types.NamespacedName{Name: placementName}
600598
objs, err := rs.fetchResources(selector, placementKey)
601599
if err != nil {
602-
return nil, err
600+
return "", false, err
603601
}
604602

605-
// Filter by skipped namespaces and extract names
606-
namespaceNames := make([]string, 0, len(objs))
607-
for _, obj := range objs {
608-
ns, err := meta.Accessor(obj)
609-
if err != nil {
610-
return nil, NewUnexpectedBehaviorError(fmt.Errorf("failed to access the namespace object: %w", err))
611-
}
612-
if !utils.ShouldPropagateNamespace(ns.GetName(), rs.SkippedNamespaces) {
613-
klog.V(2).InfoS("skip namespace that is not allowed to propagate", "namespace", ns.GetName(), "placement", placementName)
614-
continue
615-
}
616-
namespaceNames = append(namespaceNames, ns.GetName())
603+
// Since CEL validation ensures name-based selection only, we expect at most 1 namespace object
604+
if len(objs) == 0 {
605+
return "", false, nil
606+
}
607+
608+
// We should have exactly 1 namespace object due to CEL validation constraints
609+
ns, err := meta.Accessor(objs[0])
610+
if err != nil {
611+
return "", false, NewUnexpectedBehaviorError(fmt.Errorf("failed to access the namespace object: %w", err))
612+
}
613+
614+
// Check if this namespace should be propagated
615+
if !utils.ShouldPropagateNamespace(ns.GetName(), rs.SkippedNamespaces) {
616+
klog.V(2).InfoS("skip namespace that is not allowed to propagate", "namespace", ns.GetName(), "placement", placementName)
617+
return "", false, nil
617618
}
618619

619-
return namespaceNames, nil
620+
return ns.GetName(), true, nil
620621
}
621622

622623
func (rs *ResourceSelectorResolver) ShouldPropagateObj(namespace, placementName string, obj runtime.Object) (bool, error) {

pkg/utils/controller/resource_selector_resolver_test.go

Lines changed: 18 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -1493,56 +1493,6 @@ func TestGatherSelectedResource(t *testing.T) {
14931493
// Should select only the namespace, not its child resources
14941494
want: []*unstructured.Unstructured{testNamespace},
14951495
},
1496-
{
1497-
name: "should error when selecting multiple namespaces with NamespaceWithResourceSelectors mode using label selector",
1498-
placementName: types.NamespacedName{Name: "test-placement"},
1499-
selectors: []fleetv1beta1.ResourceSelectorTerm{
1500-
{
1501-
Group: "",
1502-
Version: "v1",
1503-
Kind: "Namespace",
1504-
LabelSelector: &metav1.LabelSelector{
1505-
MatchLabels: map[string]string{
1506-
"environment": "test",
1507-
},
1508-
},
1509-
SelectionScope: fleetv1beta1.NamespaceWithResourceSelectors,
1510-
},
1511-
{
1512-
Group: "apps",
1513-
Version: "v1",
1514-
Kind: "Deployment",
1515-
Name: "test-deployment",
1516-
},
1517-
},
1518-
resourceConfig: utils.NewResourceConfig(false), // default deny list
1519-
informerManager: func() *testinformer.FakeManager {
1520-
// Create a second test namespace with the same label
1521-
testNamespace2 := &unstructured.Unstructured{
1522-
Object: map[string]interface{}{
1523-
"apiVersion": "v1",
1524-
"kind": "Namespace",
1525-
"metadata": map[string]interface{}{
1526-
"name": "test-ns-2",
1527-
"labels": map[string]interface{}{
1528-
"environment": "test",
1529-
},
1530-
},
1531-
},
1532-
}
1533-
testNamespace2.SetGroupVersionKind(utils.NamespaceGVK)
1534-
1535-
return &testinformer.FakeManager{
1536-
Listers: map[schema.GroupVersionResource]*testinformer.FakeLister{
1537-
utils.NamespaceGVR: {Objects: []runtime.Object{testNamespace, testNamespace2}},
1538-
utils.DeploymentGVR: {Objects: []runtime.Object{testDeployment}},
1539-
},
1540-
NamespaceScopedResources: []schema.GroupVersionResource{utils.DeploymentGVR},
1541-
}
1542-
}(),
1543-
// Should error because multiple namespaces match the label selector
1544-
wantError: ErrUserError,
1545-
},
15461496
{
15471497
name: "should error when selecting zero namespaces with NamespaceWithResourceSelectors mode",
15481498
placementName: types.NamespacedName{Name: "test-placement"},
@@ -2201,7 +2151,7 @@ func TestSortResources(t *testing.T) {
22012151
}
22022152
}
22032153

2204-
func TestFetchSelectedNamespaces(t *testing.T) {
2154+
func TestFetchSelectedNamespace(t *testing.T) {
22052155
testNs1 := &unstructured.Unstructured{
22062156
Object: map[string]interface{}{
22072157
"apiVersion": "v1",
@@ -2246,7 +2196,8 @@ func TestFetchSelectedNamespaces(t *testing.T) {
22462196
selector fleetv1beta1.ResourceSelectorTerm
22472197
skippedNamespaces map[string]bool
22482198
informerManager *testinformer.FakeManager
2249-
want []string
2199+
want string
2200+
wantFound bool
22502201
wantErr bool
22512202
}{
22522203
{
@@ -2263,43 +2214,25 @@ func TestFetchSelectedNamespaces(t *testing.T) {
22632214
utils.NamespaceGVR: {Objects: []runtime.Object{testNs1, testNs2}},
22642215
},
22652216
},
2266-
want: []string{"test-ns-1"},
2267-
},
2268-
{
2269-
name: "select namespaces by label selector",
2270-
selector: fleetv1beta1.ResourceSelectorTerm{
2271-
Group: "",
2272-
Version: "v1",
2273-
Kind: "Namespace",
2274-
LabelSelector: &metav1.LabelSelector{
2275-
MatchLabels: map[string]string{"env": "prod"},
2276-
},
2277-
},
2278-
skippedNamespaces: nil,
2279-
informerManager: &testinformer.FakeManager{
2280-
Listers: map[schema.GroupVersionResource]*testinformer.FakeLister{
2281-
utils.NamespaceGVR: {Objects: []runtime.Object{testNs1, testNs2}},
2282-
},
2283-
},
2284-
want: []string{"test-ns-1"},
2217+
want: "test-ns-1",
2218+
wantFound: true,
22852219
},
22862220
{
22872221
name: "filter out skipped namespaces",
22882222
selector: fleetv1beta1.ResourceSelectorTerm{
22892223
Group: "",
22902224
Version: "v1",
22912225
Kind: "Namespace",
2292-
LabelSelector: &metav1.LabelSelector{
2293-
MatchLabels: map[string]string{},
2294-
},
2226+
Name: "kube-system", // Use name-based selection instead of label selector
22952227
},
22962228
skippedNamespaces: map[string]bool{"kube-system": true},
22972229
informerManager: &testinformer.FakeManager{
22982230
Listers: map[schema.GroupVersionResource]*testinformer.FakeLister{
2299-
utils.NamespaceGVR: {Objects: []runtime.Object{testNs1, skippedNs}},
2231+
utils.NamespaceGVR: {Objects: []runtime.Object{testNs1, testNs2, skippedNs}},
23002232
},
23012233
},
2302-
want: []string{"test-ns-1"},
2234+
want: "", // Empty because kube-system is skipped
2235+
wantFound: false,
23032236
},
23042237
{
23052238
name: "no namespaces match selector",
@@ -2315,7 +2248,8 @@ func TestFetchSelectedNamespaces(t *testing.T) {
23152248
utils.NamespaceGVR: {Objects: []runtime.Object{testNs1, testNs2}},
23162249
},
23172250
},
2318-
want: []string{},
2251+
want: "",
2252+
wantFound: false,
23192253
},
23202254
}
23212255

@@ -2328,13 +2262,16 @@ func TestFetchSelectedNamespaces(t *testing.T) {
23282262
RestMapper: newFakeRESTMapper(),
23292263
}
23302264

2331-
got, err := rsr.fetchSelectedNamespaces(tt.selector, "test-placement")
2265+
gotNamespace, gotFound, err := rsr.fetchSelectedNamespace(tt.selector, "test-placement")
23322266
if (err != nil) != tt.wantErr {
2333-
t.Errorf("fetchSelectedNamespaces() error = %v, wantErr %v", err, tt.wantErr)
2267+
t.Errorf("fetchSelectedNamespace() error = %v, wantErr %v", err, tt.wantErr)
23342268
return
23352269
}
2336-
if diff := cmp.Diff(tt.want, got); diff != "" {
2337-
t.Errorf("fetchSelectedNamespaces() mismatch (-want +got):\n%s", diff)
2270+
if gotNamespace != tt.want {
2271+
t.Errorf("fetchSelectedNamespace() namespace = %v, want %v", gotNamespace, tt.want)
2272+
}
2273+
if gotFound != tt.wantFound {
2274+
t.Errorf("fetchSelectedNamespace() found = %v, wantFound %v", gotFound, tt.wantFound)
23382275
}
23392276
})
23402277
}

0 commit comments

Comments
 (0)