Skip to content

Commit 8988ed6

Browse files
committed
fix: harden formatOverrideTarget fallback and align IsClusterMatched log
Address review feedback on PR #686: - formatOverrideTarget: fall back to "Unknown" when the unstructured target has an empty Kind, so a malformed snapshot doesn't render as `"" "name"` in user-facing error messages. - applyOverrideRules: drop the NewUnexpectedBehaviorError log wrapper on the IsClusterMatched failure path. The outer applyOverrides wrap already tags this as ErrUserError, so classifying the log line as "unexpected" contradicted the user-error classification. - applyOverrides: switch the outer wrap from the inline fmt.Errorf("%w: ...", controller.ErrUserError, ...) form to controller.NewUserError(fmt.Errorf(...)) to match the form used by every other call site in the codebase. Behaviorally identical (same Error() string, same errors.Is, same trim output at workgenerator/controller.go:188-194). - Add TestFormatOverrideTarget covering namespaced, cluster-scoped, and the new empty-Kind fallback (both scopes). - Trim the verbose doc and test comments introduced by this PR. Signed-off-by: Yetkin Timocin <ytimocin@microsoft.com>
1 parent b6d1622 commit 8988ed6

2 files changed

Lines changed: 71 additions & 34 deletions

File tree

pkg/controllers/workgenerator/override.go

Lines changed: 21 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,9 @@ import (
3535
"github.com/kubefleet-dev/kubefleet/pkg/utils/overrider"
3636
)
3737

38-
// fetchClusterResourceOverrideSnapshots returns the ClusterResourceOverrideSnapshots referenced
39-
// by the given binding, keyed by the ResourceIdentifier each snapshot's selectors target. The
40-
// returned map preserves the order in which the binding lists the snapshots, so the caller can
41-
// apply them in a deterministic sequence.
38+
// fetchClusterResourceOverrideSnapshots returns the binding's CRO snapshots keyed by the
39+
// ResourceIdentifier their selectors target. Order matches the binding's snapshot list so
40+
// callers can apply deterministically.
4241
//
4342
// TODO: combine the following two functions into one, as they are very similar.
4443
func (r *Reconciler) fetchClusterResourceOverrideSnapshots(ctx context.Context, resourceBinding placementv1beta1.BindingObj) (map[placementv1beta1.ResourceIdentifier][]*placementv1beta1.ClusterResourceOverrideSnapshot, error) {
@@ -75,10 +74,9 @@ func (r *Reconciler) fetchClusterResourceOverrideSnapshots(ctx context.Context,
7574
return croMap, nil
7675
}
7776

78-
// fetchResourceOverrideSnapshots returns the ResourceOverrideSnapshots referenced by the given
79-
// binding, keyed by the ResourceIdentifier each snapshot's selectors target. The returned map
80-
// preserves the order in which the binding lists the snapshots, so the caller can apply them in
81-
// a deterministic sequence.
77+
// fetchResourceOverrideSnapshots returns the binding's RO snapshots keyed by the
78+
// ResourceIdentifier their selectors target. Order matches the binding's snapshot list so
79+
// callers can apply deterministically.
8280
func (r *Reconciler) fetchResourceOverrideSnapshots(ctx context.Context, resourceBinding placementv1beta1.BindingObj) (map[placementv1beta1.ResourceIdentifier][]*placementv1beta1.ResourceOverrideSnapshot, error) {
8381
roMap := make(map[placementv1beta1.ResourceIdentifier][]*placementv1beta1.ResourceOverrideSnapshot)
8482

@@ -158,8 +156,8 @@ func (r *Reconciler) applyOverrides(resource *placementv1beta1.ResourceContent,
158156
}
159157
if err := applyOverrideRules(resource, cluster, snapshot.Spec.OverrideSpec.Policy.OverrideRules); err != nil {
160158
klog.ErrorS(err, "Failed to apply the override rules", "clusterResourceOverrideSnapshot", klog.KObj(snapshot))
161-
return false, fmt.Errorf("%w: ClusterResourceOverrideSnapshot %q failed to apply on %s: %s",
162-
controller.ErrUserError, snapshot.Name, formatOverrideTarget(&uResource), err.Error())
159+
return false, controller.NewUserError(fmt.Errorf("ClusterResourceOverrideSnapshot %q failed to apply on %s: %s",
160+
snapshot.Name, formatOverrideTarget(&uResource), err.Error()))
163161
}
164162
}
165163
klog.V(2).InfoS("Applied clusterResourceOverrideSnapshots", "resource", klog.KObj(&uResource), "numberOfOverrides", len(croMap[key]))
@@ -182,36 +180,38 @@ func (r *Reconciler) applyOverrides(resource *placementv1beta1.ResourceContent,
182180
}
183181
if err := applyOverrideRules(resource, cluster, snapshot.Spec.OverrideSpec.Policy.OverrideRules); err != nil {
184182
klog.ErrorS(err, "Failed to apply the override rules", "resourceOverrideSnapshot", klog.KObj(snapshot))
185-
return false, fmt.Errorf("%w: ResourceOverrideSnapshot %q failed to apply on %s: %s",
186-
controller.ErrUserError, snapshot.Name, formatOverrideTarget(&uResource), err.Error())
183+
return false, controller.NewUserError(fmt.Errorf("ResourceOverrideSnapshot %q failed to apply on %s: %s",
184+
snapshot.Name, formatOverrideTarget(&uResource), err.Error()))
187185
}
188186
}
189187
klog.V(2).InfoS("Applied resourceOverrideSnapshots", "resource", klog.KObj(&uResource), "numberOfOverrides", len(roMap[key]))
190188
}
191189
return resource.Raw == nil, nil
192190
}
193191

194-
// formatOverrideTarget returns a human-readable identifier for the resource being overridden,
195-
// suitable for inclusion in a user-facing error message (e.g. `Deployment "my-app" in namespace "default"`).
192+
// formatOverrideTarget renders the target as e.g. `Deployment "my-app" in namespace "default"`
193+
// for inclusion in a user-facing error message.
196194
func formatOverrideTarget(target *unstructured.Unstructured) string {
195+
// Fall back to "Unknown" so a snapshot missing `kind` doesn't render as `"" "name"`.
197196
kind := target.GetObjectKind().GroupVersionKind().Kind
197+
if kind == "" {
198+
kind = "Unknown"
199+
}
198200
if ns := target.GetNamespace(); ns != "" {
199201
return fmt.Sprintf("%s %q in namespace %q", kind, target.GetName(), ns)
200202
}
201203
return fmt.Sprintf("%s %q", kind, target.GetName())
202204
}
203205

204-
// applyOverrideRules applies the given override rules to the resource for the cluster. Rules
205-
// whose cluster selector does not match the cluster are skipped. A DeleteOverrideType rule
206-
// clears the resource and ends rule application; otherwise the rule's JSON patches are applied
207-
// in order. The first rule that fails returns its raw error to the caller, which is responsible
208-
// for tagging the failure as a user error.
206+
// applyOverrideRules applies matching rules to the resource. A DeleteOverrideType rule clears
207+
// the resource and stops; otherwise JSON patches apply in order. Errors are returned raw — the
208+
// caller (applyOverrides) tags them as user errors so we don't double-wrap the sentinel.
209209
func applyOverrideRules(resource *placementv1beta1.ResourceContent, cluster *clusterv1beta1.MemberCluster, rules []placementv1beta1.OverrideRule) error {
210210
for _, rule := range rules {
211211
matched, err := overrider.IsClusterMatched(cluster, rule)
212212
if err != nil {
213-
klog.ErrorS(controller.NewUnexpectedBehaviorError(err), "Found an invalid override rule")
214-
return err // should not happen though and should be rejected by the webhook
213+
klog.ErrorS(err, "Found an invalid override rule")
214+
return err
215215
}
216216
if !matched {
217217
continue

pkg/controllers/workgenerator/override_test.go

Lines changed: 50 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -429,10 +429,9 @@ func TestFetchResourceOverrideSnapshot(t *testing.T) {
429429
}
430430

431431
func TestApplyOverrides_clusterScopedResource(t *testing.T) {
432-
// FakeManager.IsClusterScopedResources returns (m.APIResources[gvk] == m.IsClusterScopedResource),
433-
// so a `false` flag combined with a map of namespace-scoped GVKs makes any GVK NOT in the map
434-
// (here: ClusterRole) correctly report as cluster-scoped, while listed GVKs (Deployment) report
435-
// as namespace-scoped. Despite the field name, `false` is the right value for these tests.
432+
// FakeManager.IsClusterScopedResources returns (APIResources[gvk] == IsClusterScopedResource).
433+
// With the flag set to false and APIResources listing namespace-scoped GVKs, listed GVKs
434+
// report as namespace-scoped and unlisted GVKs (ClusterRole) report as cluster-scoped.
436435
fakeInformer := informer.FakeManager{
437436
APIResources: map[schema.GroupVersionKind]bool{
438437
{
@@ -455,8 +454,7 @@ func TestApplyOverrides_clusterScopedResource(t *testing.T) {
455454
croMap map[placementv1beta1.ResourceIdentifier][]*placementv1beta1.ClusterResourceOverrideSnapshot
456455
wantClusterRole rbacv1.ClusterRole
457456
wantErr error
458-
// wantErrSubstr asserts substrings in the returned error message — used to verify
459-
// that the per-resource failure identifies the failing override snapshot and target object.
457+
// wantErrSubstr asserts the failure message names the failing snapshot and target.
460458
wantErrSubstr []string
461459
wantDeleted bool
462460
}{
@@ -977,9 +975,8 @@ func TestApplyOverrides_clusterScopedResource(t *testing.T) {
977975
},
978976
},
979977
{
980-
// Covers the applyOverrideRules → IsClusterMatched error path. An invalid LabelSelector
981-
// Operator causes metav1.LabelSelectorAsSelector to fail; applyOverrideRules returns
982-
// the raw error, and applyOverrides wraps it with the snapshot/target identity.
978+
// Invalid LabelSelector Operator makes IsClusterMatched fail — exercises the
979+
// applyOverrideRules raw-error return that applyOverrides wraps.
983980
name: "invalid cluster label selector in clusterResourceOverride",
984981
clusterRole: rbacv1.ClusterRole{
985982
TypeMeta: clusterRoleType,
@@ -1231,8 +1228,7 @@ func TestApplyOverrides_clusterScopedResource(t *testing.T) {
12311228
}
12321229

12331230
func TestApplyOverrides_namespaceScopedResource(t *testing.T) {
1234-
// See the same setup in TestApplyOverrides_clusterScopedResource above for the rationale
1235-
// behind IsClusterScopedResource: false combined with a map of namespace-scoped GVKs.
1231+
// See TestApplyOverrides_clusterScopedResource for the IsClusterScopedResource: false rationale.
12361232
fakeInformer := informer.FakeManager{
12371233
APIResources: map[schema.GroupVersionKind]bool{
12381234
{
@@ -1256,8 +1252,7 @@ func TestApplyOverrides_namespaceScopedResource(t *testing.T) {
12561252
roMap map[placementv1beta1.ResourceIdentifier][]*placementv1beta1.ResourceOverrideSnapshot
12571253
wantDeployment appsv1.Deployment
12581254
wantErr error
1259-
// wantErrSubstr asserts substrings in the returned error message — used to verify
1260-
// that the per-resource failure identifies the failing override snapshot and target object.
1255+
// wantErrSubstr asserts the failure message names the failing snapshot and target.
12611256
wantErrSubstr []string
12621257
wantDeleted bool
12631258
}{
@@ -3003,3 +2998,45 @@ func TestReplaceClusterLabelKeyVariables(t *testing.T) {
30032998
})
30042999
}
30053000
}
3001+
3002+
func TestFormatOverrideTarget(t *testing.T) {
3003+
tests := map[string]struct {
3004+
kind string
3005+
name string
3006+
namespace string
3007+
want string
3008+
}{
3009+
"namespaced resource": {
3010+
kind: "Deployment",
3011+
name: "my-app",
3012+
namespace: "default",
3013+
want: `Deployment "my-app" in namespace "default"`,
3014+
},
3015+
"cluster-scoped resource": {
3016+
kind: "Namespace",
3017+
name: "app",
3018+
want: `Namespace "app"`,
3019+
},
3020+
"empty kind falls back to Unknown": {
3021+
name: "my-app",
3022+
namespace: "default",
3023+
want: `Unknown "my-app" in namespace "default"`,
3024+
},
3025+
"empty kind, cluster-scoped": {
3026+
name: "app",
3027+
want: `Unknown "app"`,
3028+
},
3029+
}
3030+
3031+
for name, tc := range tests {
3032+
t.Run(name, func(t *testing.T) {
3033+
target := &unstructured.Unstructured{}
3034+
target.SetGroupVersionKind(schema.GroupVersionKind{Kind: tc.kind})
3035+
target.SetName(tc.name)
3036+
target.SetNamespace(tc.namespace)
3037+
if got := formatOverrideTarget(target); got != tc.want {
3038+
t.Errorf("formatOverrideTarget() = %q, want %q", got, tc.want)
3039+
}
3040+
})
3041+
}
3042+
}

0 commit comments

Comments
 (0)