|
| 1 | +/* |
| 2 | +Copyright 2026 The Faros Authors. |
| 3 | +
|
| 4 | +Licensed under the Apache License, Version 2.0 (the "License"); |
| 5 | +you may not use this file except in compliance with the License. |
| 6 | +You may obtain a copy of the License at |
| 7 | +
|
| 8 | + http://www.apache.org/licenses/LICENSE-2.0 |
| 9 | +
|
| 10 | +Unless required by applicable law or agreed to in writing, software |
| 11 | +distributed under the License is distributed on an "AS IS" BASIS, |
| 12 | +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 13 | +See the License for the specific language governing permissions and |
| 14 | +limitations under the License. |
| 15 | +*/ |
| 16 | + |
| 17 | +package install |
| 18 | + |
| 19 | +import ( |
| 20 | + "testing" |
| 21 | + |
| 22 | + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" |
| 23 | +) |
| 24 | + |
| 25 | +// schemaWithPointers builds a CRD whose OpenAPIV3Schema populates the |
| 26 | +// pointer-typed fields (Default, *bool flags) that fmt %v renders as |
| 27 | +// memory addresses. Each call allocates fresh, so two structurally |
| 28 | +// identical results live at different addresses — the precise condition |
| 29 | +// that made the old %v-based hash non-deterministic and leaked a new |
| 30 | +// immutable APIResourceSchema on every reconcile (eventually OOM-ing etcd). |
| 31 | +func schemaWithPointers() *apiextensionsv1.CustomResourceDefinition { |
| 32 | + preserve := true |
| 33 | + return &apiextensionsv1.CustomResourceDefinition{ |
| 34 | + Spec: apiextensionsv1.CustomResourceDefinitionSpec{ |
| 35 | + Group: "infrastructure.kedge.faros.sh", |
| 36 | + Names: apiextensionsv1.CustomResourceDefinitionNames{Kind: "Template", Plural: "templates"}, |
| 37 | + Versions: []apiextensionsv1.CustomResourceDefinitionVersion{{ |
| 38 | + Name: "v1alpha1", |
| 39 | + Schema: &apiextensionsv1.CustomResourceValidation{ |
| 40 | + OpenAPIV3Schema: &apiextensionsv1.JSONSchemaProps{ |
| 41 | + Type: "object", |
| 42 | + Properties: map[string]apiextensionsv1.JSONSchemaProps{ |
| 43 | + "image": { |
| 44 | + Type: "string", |
| 45 | + Default: &apiextensionsv1.JSON{Raw: []byte(`"registry.example/img:v1"`)}, |
| 46 | + }, |
| 47 | + "replicas": { |
| 48 | + Type: "integer", |
| 49 | + Default: &apiextensionsv1.JSON{Raw: []byte(`1`)}, |
| 50 | + }, |
| 51 | + }, |
| 52 | + XPreserveUnknownFields: &preserve, |
| 53 | + }, |
| 54 | + }, |
| 55 | + }}, |
| 56 | + }, |
| 57 | + } |
| 58 | +} |
| 59 | + |
| 60 | +// TestSchemaPrefixDeterministic locks the fix: identical schema content |
| 61 | +// must hash to the same name regardless of allocation, even when the |
| 62 | +// schema carries pointer fields. With the old fmt %v hash this failed |
| 63 | +// because %v printed pointer addresses. |
| 64 | +func TestSchemaPrefixDeterministic(t *testing.T) { |
| 65 | + a := schemaPrefix(schemaWithPointers()) |
| 66 | + b := schemaPrefix(schemaWithPointers()) |
| 67 | + if a != b { |
| 68 | + t.Fatalf("schemaPrefix must be deterministic for identical content; got %q vs %q", a, b) |
| 69 | + } |
| 70 | +} |
| 71 | + |
| 72 | +// TestSchemaPrefixSensitiveToContent ensures a real content change still |
| 73 | +// produces a different name (so genuine schema updates get a new schema). |
| 74 | +func TestSchemaPrefixSensitiveToContent(t *testing.T) { |
| 75 | + base := schemaWithPointers() |
| 76 | + changed := schemaWithPointers() |
| 77 | + changed.Spec.Versions[0].Schema.OpenAPIV3Schema.Properties["replicas"] = |
| 78 | + apiextensionsv1.JSONSchemaProps{ |
| 79 | + Type: "integer", |
| 80 | + Default: &apiextensionsv1.JSON{Raw: []byte(`3`)}, // 1 -> 3 |
| 81 | + } |
| 82 | + if schemaPrefix(base) == schemaPrefix(changed) { |
| 83 | + t.Fatal("schemaPrefix must change when schema content changes") |
| 84 | + } |
| 85 | +} |
0 commit comments