Skip to content

Commit 8843961

Browse files
authored
Merge branch 'main' into docsaurs_mermaid
2 parents 1338878 + 6eb5d69 commit 8843961

File tree

92 files changed

+9450
-317
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

92 files changed

+9450
-317
lines changed

.golangci.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ linters:
3434
- builtin$
3535
- examples$
3636
settings:
37+
goconst:
38+
ignore-string-values: ["true","false"]
3739
staticcheck:
3840
checks:
3941
- -QF1008

pkg/cel/compatibility.go

Lines changed: 350 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,350 @@
1+
// Copyright 2025 The Kubernetes Authors.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package cel
16+
17+
import (
18+
"fmt"
19+
"strings"
20+
21+
"github.com/google/cel-go/cel"
22+
apiservercel "k8s.io/apiserver/pkg/cel"
23+
)
24+
25+
const (
26+
// TypeNamePrefix is the prefix used for CEL type names when converting OpenAPI schemas.
27+
// Used to namespace custom types and avoid conflicts with built-in CEL types.
28+
// Example: "__type_schema.spec.ports" for a ports field in the schema resource.
29+
TypeNamePrefix = "__type_"
30+
)
31+
32+
// AreTypesStructurallyCompatible checks if an output type from an expected or executed CEL expression is compatible
33+
// with an expected type.
34+
//
35+
// This performs deep structural comparison:
36+
// - For primitives: checks kind equality
37+
// - For lists: recursively checks element type compatibility
38+
// - For maps: recursively checks key and value type compatibility
39+
// - For structs: uses DeclTypeProvider to introspect fields and check all required fields exist with compatible types
40+
// - For map → struct and struct → map compatibility if fields/keys are structurally compatible
41+
//
42+
// The provider is required for introspecting struct field information.
43+
// Returns true if types are compatible, false otherwise. If false, the error describes why.
44+
func AreTypesStructurallyCompatible(output, expected *cel.Type, provider *DeclTypeProvider) (bool, error) {
45+
if output == nil || expected == nil {
46+
return false, fmt.Errorf("nil type(s): output=%v, expected=%v", output, expected)
47+
}
48+
49+
// Dynamic type is compatible with anything
50+
if expected.Kind() == cel.DynKind || output.Kind() == cel.DynKind {
51+
return true, nil
52+
}
53+
54+
// Unwrap optional output if available
55+
if output.Kind() == cel.OpaqueKind && output.TypeName() == "optional_type" {
56+
return AreTypesStructurallyCompatible(output.Parameters()[0], expected, provider)
57+
}
58+
59+
switch {
60+
case expected.Kind() == cel.StructKind && output.Kind() == cel.MapKind:
61+
return areMapTypesAssignableToStruct(output, expected, provider)
62+
case expected.Kind() == cel.MapKind && output.Kind() == cel.StructKind:
63+
return areStructTypesAssignableToMap(output, expected, provider)
64+
case expected.Kind() == cel.ListKind:
65+
return areListTypesCompatible(output, expected, provider)
66+
case expected.Kind() == cel.MapKind:
67+
return areMapTypesCompatible(output, expected, provider)
68+
case expected.Kind() == cel.StructKind:
69+
return areStructTypesCompatible(output, expected, provider)
70+
default:
71+
// Kinds must match otherwise
72+
if output.Kind() != expected.Kind() {
73+
return false, fmt.Errorf("type kind mismatch: got %q, expected %q", output.String(), expected.String())
74+
}
75+
// primitives: kind equality already checked
76+
return true, nil
77+
}
78+
}
79+
80+
// areListTypesCompatible checks if list element types are structurally compatible.
81+
func areListTypesCompatible(output, expected *cel.Type, provider *DeclTypeProvider) (bool, error) {
82+
outputParams := output.Parameters()
83+
expectedParams := expected.Parameters()
84+
85+
// Both must have element type parameters
86+
if len(outputParams) == 0 || len(expectedParams) == 0 {
87+
if len(outputParams) != len(expectedParams) {
88+
return false, fmt.Errorf("list parameter count mismatch: got %d, expected %d", len(outputParams), len(expectedParams))
89+
}
90+
return true, nil
91+
}
92+
93+
// Recursively check element type compatibility
94+
compatible, err := AreTypesStructurallyCompatible(outputParams[0], expectedParams[0], provider)
95+
if !compatible {
96+
return false, fmt.Errorf("list element type incompatible: %w", err)
97+
}
98+
return true, nil
99+
}
100+
101+
// areMapTypesCompatible checks if map key and value types are structurally compatible.
102+
func areMapTypesCompatible(output, expected *cel.Type, provider *DeclTypeProvider) (bool, error) {
103+
outputParams := output.Parameters()
104+
expectedParams := expected.Parameters()
105+
106+
// Both must have key and value type parameters
107+
if len(outputParams) < 2 || len(expectedParams) < 2 {
108+
if len(outputParams) != len(expectedParams) {
109+
return false, fmt.Errorf("map parameter count mismatch: got %d, expected %d", len(outputParams), len(expectedParams))
110+
}
111+
return true, nil
112+
}
113+
114+
// Check key type compatibility
115+
compatible, err := AreTypesStructurallyCompatible(outputParams[0], expectedParams[0], provider)
116+
if !compatible {
117+
return false, fmt.Errorf("map key type incompatible: %w", err)
118+
}
119+
120+
// Check value type compatibility
121+
compatible, err = AreTypesStructurallyCompatible(outputParams[1], expectedParams[1], provider)
122+
if !compatible {
123+
return false, fmt.Errorf("map value type incompatible: %w", err)
124+
}
125+
return true, nil
126+
}
127+
128+
// areStructTypesCompatible checks if struct types are structurally compatible
129+
// by introspecting their fields using the DeclTypeProvider.
130+
func areStructTypesCompatible(output, expected *cel.Type, provider *DeclTypeProvider) (bool, error) {
131+
if provider == nil {
132+
// Without provider, we can't introspect fields - fall back to kind check only
133+
return true, nil
134+
}
135+
136+
// Resolve DeclTypes by walking through nested type paths
137+
expectedDecl := resolveDeclTypeFromPath(expected.String(), provider)
138+
outputDecl := resolveDeclTypeFromPath(output.String(), provider)
139+
140+
// If we can't resolve both types, we can't do structural comparison
141+
// Fall back to accepting it (permissive - could make this stricter)
142+
if expectedDecl == nil || outputDecl == nil {
143+
return true, nil
144+
}
145+
146+
// Check that output has all required fields of expected
147+
return areStructFieldsCompatible(outputDecl, expectedDecl, provider)
148+
}
149+
150+
// resolveDeclTypeFromPath resolves a DeclType by walking through a nested path.
151+
// For example, "[email protected]" would:
152+
// 1. Strip TypeNamePrefix and look up "ingressroute" in the provider
153+
// 2. Find the "spec" field
154+
// 3. Find the "routes" field
155+
// 4. Get the list element type (@idx)
156+
// 5. Find the "middlewares" field
157+
func resolveDeclTypeFromPath(typePath string, provider *DeclTypeProvider) *apiservercel.DeclType {
158+
if provider == nil || typePath == "" {
159+
return nil
160+
}
161+
162+
// Split the path into segments
163+
segments := strings.Split(typePath, ".")
164+
if len(segments) == 0 {
165+
return nil
166+
}
167+
168+
// Get the root name - keep it as-is (with or without prefix)
169+
rootName := segments[0]
170+
171+
// Look up the root type in the provider
172+
// Try first with the name as-is, then try without prefix if it has one
173+
currentDecl, found := provider.FindDeclType(rootName)
174+
if !found && strings.HasPrefix(rootName, TypeNamePrefix) {
175+
// Try without prefix for backwards compatibility
176+
shortName := strings.TrimPrefix(rootName, TypeNamePrefix)
177+
currentDecl, found = provider.FindDeclType(shortName)
178+
}
179+
if !found {
180+
return nil
181+
}
182+
183+
// Walk through remaining path segments
184+
for i := 1; i < len(segments); i++ {
185+
segment := segments[i]
186+
187+
// Handle list element type (@idx) and map value type (@elem)
188+
// These are KRO conventions used in DeclTypeProvider, not CEL built-ins
189+
if segment == "@idx" || segment == "@elem" {
190+
if currentDecl.ElemType != nil {
191+
currentDecl = currentDecl.ElemType
192+
} else {
193+
return nil
194+
}
195+
continue
196+
}
197+
198+
// Handle array index notation like "routes[0]" - strip the index
199+
if idx := strings.Index(segment, "["); idx != -1 {
200+
segment = segment[:idx]
201+
}
202+
203+
// Look up field in current struct
204+
if currentDecl.Fields == nil {
205+
return nil
206+
}
207+
208+
field, exists := currentDecl.Fields[segment]
209+
if !exists {
210+
return nil
211+
}
212+
213+
currentDecl = field.Type
214+
if currentDecl == nil {
215+
return nil
216+
}
217+
}
218+
219+
return currentDecl
220+
}
221+
222+
// areStructFieldsCompatible checks if output struct is a subset of expected struct.
223+
// The output type can have fewer fields than expected (subset semantics), but cannot have extra fields.
224+
// For each field that exists in output:
225+
// 1. The field must exist in expected
226+
// 2. The field type must be compatible
227+
func areStructFieldsCompatible(output, expected *apiservercel.DeclType, provider *DeclTypeProvider) (bool, error) {
228+
if expected == nil {
229+
return true, nil
230+
}
231+
// PreserveUnknownFields is set on the expected type, so everything we would pass from output would be okay
232+
if expected.Metadata[XKubernetesPreserveUnknownFields] == "true" {
233+
return true, nil
234+
}
235+
236+
if output == nil {
237+
return false, fmt.Errorf("output type is nil")
238+
}
239+
240+
outputFields := output.Fields
241+
if outputFields == nil {
242+
// Output has no fields - this is a valid subset of any expected type
243+
return true, nil
244+
}
245+
246+
expectedFields := expected.Fields
247+
if expectedFields == nil {
248+
// Expected has no fields, but output does - incompatible
249+
if len(outputFields) > 0 {
250+
return false, fmt.Errorf("output has fields but expected type has none")
251+
}
252+
return true, nil
253+
}
254+
255+
// Check each field in output exists in expected with compatible type
256+
for fieldName, outputField := range outputFields {
257+
expectedField, exists := expectedFields[fieldName]
258+
259+
// Output has a field that expected doesn't have - not a subset
260+
if !exists {
261+
return false, fmt.Errorf("field %q exists in output but not in expected type", fieldName)
262+
}
263+
264+
// Field exists in both - check type compatibility recursively
265+
expectedFieldType := expectedField.Type
266+
outputFieldType := outputField.Type
267+
268+
if expectedFieldType == nil || outputFieldType == nil {
269+
continue
270+
}
271+
272+
// Recursively compare field types
273+
expectedCELType := expectedFieldType.CelType()
274+
outputCELType := outputFieldType.CelType()
275+
276+
compatible, err := AreTypesStructurallyCompatible(outputCELType, expectedCELType, provider)
277+
if !compatible {
278+
return false, fmt.Errorf("field %q has incompatible type: %w", fieldName, err)
279+
}
280+
}
281+
282+
return true, nil
283+
}
284+
285+
func areMapTypesAssignableToStruct(outputMap, expectedStruct *cel.Type, provider *DeclTypeProvider) (bool, error) {
286+
expectedDecl := resolveDeclTypeFromPath(expectedStruct.String(), provider)
287+
if expectedDecl == nil || expectedDecl.Fields == nil {
288+
return true, nil
289+
}
290+
291+
// map parameters are [keyType, valueType]
292+
params := outputMap.Parameters()
293+
if len(params) < 2 {
294+
return false, fmt.Errorf("map must have key and value types")
295+
}
296+
297+
keyType := params[0]
298+
valType := params[1]
299+
300+
// keys must be strings to match struct field names
301+
if keyType.Kind() != cel.StringKind {
302+
return false, fmt.Errorf("map keys must be strings to assign to struct")
303+
}
304+
305+
for fieldName, expectedField := range expectedDecl.Fields {
306+
expectedFieldCEL := expectedField.Type.CelType()
307+
if expectedFieldCEL == nil {
308+
continue
309+
}
310+
compatible, err := AreTypesStructurallyCompatible(valType, expectedFieldCEL, provider)
311+
if !compatible {
312+
return false, fmt.Errorf("map value incompatible with struct field %q: %w", fieldName, err)
313+
}
314+
}
315+
316+
return true, nil
317+
}
318+
319+
func areStructTypesAssignableToMap(outputStruct, expectedMap *cel.Type, provider *DeclTypeProvider) (bool, error) {
320+
outputDecl := resolveDeclTypeFromPath(outputStruct.String(), provider)
321+
if outputDecl == nil || outputDecl.Fields == nil {
322+
return true, nil
323+
}
324+
325+
params := expectedMap.Parameters()
326+
if len(params) < 2 {
327+
return false, fmt.Errorf("expected map must have key and value types")
328+
}
329+
keyType := params[0]
330+
valType := params[1]
331+
332+
// struct field names map to string keys
333+
if keyType.Kind() != cel.StringKind {
334+
return false, fmt.Errorf("map key type must be string when assigning struct → map")
335+
}
336+
337+
for fieldName, outputField := range outputDecl.Fields {
338+
outputCEL := outputField.Type.CelType()
339+
if outputCEL == nil {
340+
continue
341+
}
342+
343+
compatible, err := AreTypesStructurallyCompatible(outputCEL, valType, provider)
344+
if !compatible {
345+
return false, fmt.Errorf("struct field %q incompatible with map value type: %w", fieldName, err)
346+
}
347+
}
348+
349+
return true, nil
350+
}

0 commit comments

Comments
 (0)