Skip to content

Commit 903eb2b

Browse files
committed
refactor based on feedbacks
1 parent 642a44a commit 903eb2b

2 files changed

Lines changed: 64 additions & 27 deletions

File tree

pkg/validate/validate.go

Lines changed: 14 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ package validate
1818

1919
import (
2020
"context"
21-
"fmt"
2221

2322
ext "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
2423
extv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
@@ -60,41 +59,29 @@ func SchemaValidate(ctx context.Context, resources []*unstructured.Unstructured,
6059
return nil, errors.Wrap(err, "cannot create schema validators")
6160
}
6261

63-
// Index old resources by GVK+name+namespace so each resource can be paired
64-
// with its previous state. On duplicate keys the last entry wins.
65-
oldByKey := make(map[string]*unstructured.Unstructured, len(oldResources))
66-
for _, o := range oldResources {
67-
oldByKey[resourceKey(o)] = o
68-
}
69-
7062
result := &ValidationResult{
7163
Resources: make([]ResourceValidationResult, 0, len(resources)),
7264
}
7365
for _, r := range resources {
74-
result.Resources = append(result.Resources, validateResource(ctx, r, objectOf(oldByKey[resourceKey(r)]), schemaValidators, structurals, crds))
66+
// Find this resource's previous state, if supplied, so CEL transition
67+
// rules (those referencing oldSelf) can be evaluated. A resource is
68+
// matched to its old state by GroupVersionKind, namespace, and name;
69+
// with no match oldObject stays nil and transition rules are skipped,
70+
// exactly as on a Kubernetes create.
71+
gvk, namespace, name := r.GroupVersionKind(), r.GetNamespace(), getResourceName(r)
72+
var oldObject map[string]any
73+
for _, o := range oldResources {
74+
if o.GroupVersionKind() == gvk && o.GetNamespace() == namespace && getResourceName(o) == name {
75+
oldObject = o.Object
76+
break
77+
}
78+
}
79+
result.Resources = append(result.Resources, validateResource(ctx, r, oldObject, schemaValidators, structurals, crds))
7580
}
7681
result.Summary = computeSummary(result.Resources)
7782
return result, nil
7883
}
7984

80-
// resourceKey identifies a resource by GroupVersionKind, name, and namespace.
81-
// It is used to match a resource under validation to its previous state so CEL
82-
// transition rules see the right old object.
83-
func resourceKey(r *unstructured.Unstructured) string {
84-
gvk := r.GetObjectKind().GroupVersionKind()
85-
return fmt.Sprintf("%s-%s-%s", gvk.String(), getResourceName(r), r.GetNamespace())
86-
}
87-
88-
// objectOf returns the unstructured content of u, or nil when u is nil. It
89-
// keeps the nil check for an unmatched old resource in one place so callers can
90-
// pass the result straight to the CEL validator's oldObject argument.
91-
func objectOf(u *unstructured.Unstructured) map[string]any {
92-
if u == nil {
93-
return nil
94-
}
95-
return u.Object
96-
}
97-
9885
// validateResource runs every check (schema, CEL, unknown fields, defaulting)
9986
// against a single resource and returns its ResourceValidationResult. It is
10087
// the per-resource decomposition of SchemaValidate; pulling it out keeps the

pkg/validate/validate_test.go

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,56 @@ var testCRDNoMatchingVersion = &extv1.CustomResourceDefinition{
180180
},
181181
}
182182

183+
// TestSchemaValidateDistinguishesAmbiguousKeys is a regression test for
184+
// old-resource matching. A naive "<gvk>-<name>-<namespace>" string key collides
185+
// whenever a name or namespace contains the "-" separator: (name=app,
186+
// namespace=test-1) and (name=app-test, namespace=1) both render as
187+
// "<gvk>-app-test-1". Under such a collision a resource gets matched to the
188+
// WRONG previous state and its CEL transition rule is evaluated against the
189+
// wrong old object. SchemaValidate matches on GroupVersionKind, namespace, and
190+
// name as separate values, which keeps these two resources distinct.
191+
func TestSchemaValidateDistinguishesAmbiguousKeys(t *testing.T) {
192+
mk := func(name, namespace, param string) *unstructured.Unstructured {
193+
return &unstructured.Unstructured{Object: map[string]any{
194+
"apiVersion": "test.org/v1alpha1",
195+
"kind": "TestTransition",
196+
"metadata": map[string]any{"name": name, "namespace": namespace},
197+
"spec": map[string]any{"param": param},
198+
}}
199+
}
200+
201+
// newA and newB collide to "<gvk>-app-test-1" under a naive string key, as
202+
// do their matching old resources.
203+
newA := mk("app", "test-1", "keep") // unchanged vs oldA -> should be Valid
204+
newB := mk("app-test", "1", "changed") // changed vs oldB -> should be Invalid
205+
oldA := mk("app", "test-1", "keep")
206+
oldB := mk("app-test", "1", "original")
207+
208+
result, err := SchemaValidate(
209+
t.Context(),
210+
[]*unstructured.Unstructured{newA, newB},
211+
[]*unstructured.Unstructured{oldA, oldB},
212+
[]*extv1.CustomResourceDefinition{testCRDWithTransition},
213+
)
214+
if err != nil {
215+
t.Fatalf("SchemaValidate() unexpected error: %v", err)
216+
}
217+
218+
// Matching on separate fields, each resource sees its own old state. A naive
219+
// string key would map both to the last old resource (oldB), wrongly
220+
// flipping newA to Invalid and yielding Valid=0, Invalid=2.
221+
want := ValidationSummary{Total: 2, Valid: 1, Invalid: 1}
222+
if diff := cmp.Diff(want, result.Summary); diff != "" {
223+
t.Errorf("Summary mismatch (-want +got):\n%s", diff)
224+
}
225+
if result.Resources[0].Status != ValidationStatusValid {
226+
t.Errorf("newA (app/test-1) Status = %q; want Valid — matched to the wrong old resource?", result.Resources[0].Status)
227+
}
228+
if result.Resources[1].Status != ValidationStatusInvalid {
229+
t.Errorf("newB (app-test/1) Status = %q; want Invalid", result.Resources[1].Status)
230+
}
231+
}
232+
183233
func TestSchemaValidate(t *testing.T) {
184234
validResource := &unstructured.Unstructured{Object: map[string]any{
185235
"apiVersion": "test.org/v1alpha1",

0 commit comments

Comments
 (0)