mirror of https://github.com/kubevela/kubevela.git
				
				
				
			Feat: Support Native Cue in HealthPolicy and CustomStatus (#6859)
* Feat: Support Native Cue in HealthPolicy and CustomStatus Signed-off-by: Brian Kane <briankane1@gmail.com> * Feat: Support Native Cue in HealthPolicy and CustomStatus - Fix PR Comments & Bugs Signed-off-by: Brian Kane <briankane1@gmail.com> --------- Signed-off-by: Brian Kane <briankane1@gmail.com>
This commit is contained in:
		
							parent
							
								
									3aa94842fb
								
							
						
					
					
						commit
						a5de74ec1e
					
				|  | @ -27,14 +27,22 @@ import ( | |||
| 
 | ||||
| const ( | ||||
| 	// status is the path to the status field in the metadata
 | ||||
| 	status = "attributes.status.details" | ||||
| 	status       = "attributes.status.details" | ||||
| 	healthPolicy = "attributes.status.healthPolicy" | ||||
| 	customStatus = "attributes.status.customStatus" | ||||
| 	// localFieldPrefix is the prefix for local fields not output to the status
 | ||||
| 	localFieldPrefix = "$" | ||||
| ) | ||||
| 
 | ||||
| // EncodeMetadata encodes native CUE in the metadata fields to a CUE string literal
 | ||||
| func EncodeMetadata(field *ast.Field) error { | ||||
| 	if err := marshalStatusDetailsField(field); err != nil { | ||||
| 	if err := marshalField[*ast.StructLit](field, healthPolicy, validateHealthPolicyField); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	if err := marshalField[*ast.StructLit](field, customStatus, validateCustomStatusField); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	if err := marshalField[*ast.StructLit](field, status, validateStatusField); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	return nil | ||||
|  | @ -42,65 +50,75 @@ func EncodeMetadata(field *ast.Field) error { | |||
| 
 | ||||
| // DecodeMetadata decodes a CUE string literal in the metadata fields to native CUE expressions
 | ||||
| func DecodeMetadata(field *ast.Field) error { | ||||
| 	if err := unmarshalStatusDetailsField(field); err != nil { | ||||
| 	if err := unmarshalField[*ast.StructLit](field, healthPolicy, validateHealthPolicyField); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	if err := unmarshalField[*ast.StructLit](field, customStatus, validateCustomStatusField); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	if err := unmarshalField[*ast.StructLit](field, status, validateStatusField); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| func marshalStatusDetailsField(field *ast.Field) error { | ||||
| 	if statusField, ok := GetFieldByPath(field, status); ok { | ||||
| func marshalField[T ast.Node](field *ast.Field, key string, validator func(T) error) error { | ||||
| 	if statusField, ok := GetFieldByPath(field, key); ok { | ||||
| 		switch expr := statusField.Value.(type) { | ||||
| 		case *ast.BasicLit: | ||||
| 			if expr.Kind != token.STRING { | ||||
| 				return fmt.Errorf("expected status field to be string, got %v", expr.Kind) | ||||
| 				return fmt.Errorf("expected %s field to be string, got %v", key, expr.Kind) | ||||
| 			} | ||||
| 			if err := ValidateCueStringLiteral[*ast.StructLit](expr, validateStatusField); err != nil { | ||||
| 				return fmt.Errorf("status.details field failed validation: %w", err) | ||||
| 			if err := ValidateCueStringLiteral[T](expr, validator); err != nil { | ||||
| 				return fmt.Errorf("%s field failed validation: %w", key, err) | ||||
| 			} | ||||
| 			return nil | ||||
| 
 | ||||
| 		case *ast.StructLit: | ||||
| 			v, _ := statusField.Value.(*ast.StructLit) | ||||
| 			err := validateStatusField(v) | ||||
| 			structLit := expr | ||||
| 			v, ok := ast.Node(structLit).(T) | ||||
| 			if !ok { | ||||
| 				return fmt.Errorf("%s field: cannot convert *ast.StructLit to expected type", key) | ||||
| 			} | ||||
| 			err := validator(v) | ||||
| 			if err != nil { | ||||
| 				return err | ||||
| 			} | ||||
| 			strLit, err := StringifyStructLitAsCueString(v) | ||||
| 			strLit, err := StringifyStructLitAsCueString(structLit) | ||||
| 			if err != nil { | ||||
| 				return err | ||||
| 			} | ||||
| 			UpdateNodeByPath(field, status, strLit) | ||||
| 			UpdateNodeByPath(field, key, strLit) | ||||
| 			return nil | ||||
| 
 | ||||
| 		default: | ||||
| 			return fmt.Errorf("unexpected type for status field: %T", expr) | ||||
| 			return fmt.Errorf("unexpected type for %s field: %T", key, expr) | ||||
| 		} | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| func unmarshalStatusDetailsField(field *ast.Field) error { | ||||
| 	if statusField, ok := GetFieldByPath(field, status); ok { | ||||
| func unmarshalField[T ast.Node](field *ast.Field, key string, validator func(T) error) error { | ||||
| 	if statusField, ok := GetFieldByPath(field, key); ok { | ||||
| 		basicLit, ok := statusField.Value.(*ast.BasicLit) | ||||
| 		if !ok || basicLit.Kind != token.STRING { | ||||
| 			return fmt.Errorf("status.details field is not a string literal") | ||||
| 			return fmt.Errorf("%s field is not a string literal", key) | ||||
| 		} | ||||
| 
 | ||||
| 		err := ValidateCueStringLiteral[*ast.StructLit](basicLit, validateStatusField) | ||||
| 		err := ValidateCueStringLiteral[T](basicLit, validator) | ||||
| 		if err != nil { | ||||
| 			return fmt.Errorf("status field failed validation: %w", err) | ||||
| 			return fmt.Errorf("%s field failed validation: %w", key, err) | ||||
| 		} | ||||
| 
 | ||||
| 		unquoted := strings.TrimSpace(TrimCueRawString(basicLit.Value)) | ||||
| 		expr, err := parser.ParseExpr("-", WrapCueStruct(unquoted)) | ||||
| 		if err != nil { | ||||
| 			return fmt.Errorf("unexpected error re-parsing validated string: %w", err) | ||||
| 			return fmt.Errorf("unexpected error re-parsing validated %s string: %w", key, err) | ||||
| 		} | ||||
| 
 | ||||
| 		structLit, ok := expr.(*ast.StructLit) | ||||
| 		if !ok { | ||||
| 			return fmt.Errorf("expected struct after validation") | ||||
| 			return fmt.Errorf("expected struct after validation in field %s", key) | ||||
| 		} | ||||
| 
 | ||||
| 		statusField.Value = structLit | ||||
|  | @ -134,3 +152,55 @@ func validateStatusField(sl *ast.StructLit) error { | |||
| 	} | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| func validateCustomStatusField(sl *ast.StructLit) error { | ||||
| 	validator := func(expr ast.Expr) error { | ||||
| 		switch v := expr.(type) { | ||||
| 		case *ast.BasicLit: | ||||
| 			if v.Kind != token.STRING { | ||||
| 				return fmt.Errorf("customStatus field 'message' must be a string, got %v", v.Kind) | ||||
| 			} | ||||
| 		case *ast.Interpolation, *ast.CallExpr, *ast.SelectorExpr, *ast.Ident, *ast.BinaryExpr, *ast.ParenExpr, | ||||
| 			*ast.ListLit, *ast.IndexExpr, *ast.SliceExpr, *ast.Comprehension: | ||||
| 		default: | ||||
| 			return fmt.Errorf("customStatus field 'message' must be a string expression, got %T", v) | ||||
| 		} | ||||
| 		return nil | ||||
| 	} | ||||
| 
 | ||||
| 	found, err := FindAndValidateField(sl, "message", validator) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	if !found { | ||||
| 		return fmt.Errorf("customStatus must contain a 'message' field") | ||||
| 	} | ||||
| 
 | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| func validateHealthPolicyField(sl *ast.StructLit) error { | ||||
| 	validator := func(expr ast.Expr) error { | ||||
| 		switch v := expr.(type) { | ||||
| 		case *ast.Ident: | ||||
| 		case *ast.BasicLit: | ||||
| 			if v.Kind != token.TRUE && v.Kind != token.FALSE { | ||||
| 				return fmt.Errorf("healthPolicy field 'isHealth' must be a boolean literal (true/false), got %v", v.Kind) | ||||
| 			} | ||||
| 		case *ast.BinaryExpr, *ast.UnaryExpr, *ast.CallExpr, *ast.SelectorExpr, *ast.ParenExpr: | ||||
| 		default: | ||||
| 			return fmt.Errorf("healthPolicy field 'isHealth' must be a boolean expression, got %T", v) | ||||
| 		} | ||||
| 		return nil | ||||
| 	} | ||||
| 
 | ||||
| 	found, err := FindAndValidateField(sl, "isHealth", validator) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	if !found { | ||||
| 		return fmt.Errorf("healthPolicy must contain an 'isHealth' field") | ||||
| 	} | ||||
| 
 | ||||
| 	return nil | ||||
| } | ||||
|  |  | |||
|  | @ -229,3 +229,891 @@ func TestMarshalAndUnmarshalMetadata(t *testing.T) { | |||
| 		}) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func TestMarshalAndUnmarshalHealthPolicy(t *testing.T) { | ||||
| 	tests := []struct { | ||||
| 		name               string | ||||
| 		input              string | ||||
| 		expectMarshalErr   string | ||||
| 		expectUnmarshalErr string | ||||
| 		expectContains     string | ||||
| 	}{ | ||||
| 		{ | ||||
| 			name: "valid healthPolicy with boolean literal", | ||||
| 			input: ` | ||||
| 				attributes: { | ||||
| 					status: { | ||||
| 						healthPolicy: { | ||||
| 							isHealth: true | ||||
| 						} | ||||
| 					} | ||||
| 				} | ||||
| 			`, | ||||
| 			expectContains: "isHealth", | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "valid healthPolicy with binary expression", | ||||
| 			input: ` | ||||
| 				attributes: { | ||||
| 					status: { | ||||
| 						healthPolicy: { | ||||
| 							isHealth: context.output.status.phase == "Running" | ||||
| 						} | ||||
| 					} | ||||
| 				} | ||||
| 			`, | ||||
| 			expectContains: "isHealth", | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "valid healthPolicy with complex expression", | ||||
| 			input: ` | ||||
| 				attributes: { | ||||
| 					status: { | ||||
| 						healthPolicy: { | ||||
| 							isHealth: context.output.status.ready && context.output.status.replicas > 0 | ||||
| 						} | ||||
| 					} | ||||
| 				} | ||||
| 			`, | ||||
| 			expectContains: "isHealth", | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "valid healthPolicy with selector expression", | ||||
| 			input: ` | ||||
| 				attributes: { | ||||
| 					status: { | ||||
| 						healthPolicy: { | ||||
| 							isHealth: context.output.status.conditions[0].status | ||||
| 						} | ||||
| 					} | ||||
| 				} | ||||
| 			`, | ||||
| 			expectContains: "isHealth", | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "valid healthPolicy with call expression", | ||||
| 			input: ` | ||||
| 				attributes: { | ||||
| 					status: { | ||||
| 						healthPolicy: { | ||||
| 							isHealth: len(context.output.status.conditions) > 0 | ||||
| 						} | ||||
| 					} | ||||
| 				} | ||||
| 			`, | ||||
| 			expectContains: "isHealth", | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "healthPolicy missing isHealth field", | ||||
| 			input: ` | ||||
| 				attributes: { | ||||
| 					status: { | ||||
| 						healthPolicy: { | ||||
| 							someOtherField: true | ||||
| 						} | ||||
| 					} | ||||
| 				} | ||||
| 			`, | ||||
| 			expectMarshalErr: "healthPolicy must contain an 'isHealth' field", | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "healthPolicy with invalid isHealth type (struct)", | ||||
| 			input: ` | ||||
| 				attributes: { | ||||
| 					status: { | ||||
| 						healthPolicy: { | ||||
| 							isHealth: { nested: true } | ||||
| 						} | ||||
| 					} | ||||
| 				} | ||||
| 			`, | ||||
| 			expectMarshalErr: "healthPolicy field 'isHealth' must be a boolean expression", | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "healthPolicy with invalid isHealth type (list)", | ||||
| 			input: ` | ||||
| 				attributes: { | ||||
| 					status: { | ||||
| 						healthPolicy: { | ||||
| 							isHealth: [true, false] | ||||
| 						} | ||||
| 					} | ||||
| 				} | ||||
| 			`, | ||||
| 			expectMarshalErr: "healthPolicy field 'isHealth' must be a boolean expression", | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "valid stringified healthPolicy round trip", | ||||
| 			input: ` | ||||
| 				attributes: { | ||||
| 					status: { | ||||
| 						healthPolicy: #""" | ||||
| 							isHealth: context.output.status.phase == "Running" | ||||
| 						"""# | ||||
| 					} | ||||
| 				} | ||||
| 			`, | ||||
| 			expectContains: "isHealth", | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "malformed stringified healthPolicy fails validation", | ||||
| 			input: ` | ||||
| 				attributes: { | ||||
| 					status: { | ||||
| 						healthPolicy: #""" | ||||
| 							invalid cue: abc | ||||
| 						"""# | ||||
| 					} | ||||
| 				} | ||||
| 			`, | ||||
| 			expectMarshalErr: "invalid cue content in string literal", | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "healthPolicy as plain string is valid", | ||||
| 			input: ` | ||||
| 				attributes: { | ||||
| 					status: { | ||||
| 						healthPolicy: "isHealth: true" | ||||
| 					} | ||||
| 				} | ||||
| 			`, | ||||
| 			expectContains: "isHealth", | ||||
| 		}, | ||||
| 	} | ||||
| 
 | ||||
| 	for _, tt := range tests { | ||||
| 		tt := tt | ||||
| 		t.Run(tt.name, func(t *testing.T) { | ||||
| 			file, err := parser.ParseFile("-", tt.input) | ||||
| 			require.NoError(t, err) | ||||
| 
 | ||||
| 			var rootField *ast.Field | ||||
| 			for _, decl := range file.Decls { | ||||
| 				if f, ok := decl.(*ast.Field); ok { | ||||
| 					rootField = f | ||||
| 					break | ||||
| 				} | ||||
| 			} | ||||
| 			require.NotNil(t, rootField) | ||||
| 
 | ||||
| 			err = EncodeMetadata(rootField) | ||||
| 			if tt.expectMarshalErr != "" { | ||||
| 				require.Error(t, err) | ||||
| 				require.Contains(t, err.Error(), tt.expectMarshalErr) | ||||
| 				return | ||||
| 			} else { | ||||
| 				require.NoError(t, err) | ||||
| 			} | ||||
| 
 | ||||
| 			err = DecodeMetadata(rootField) | ||||
| 			if tt.expectUnmarshalErr != "" { | ||||
| 				require.Error(t, err) | ||||
| 				require.Contains(t, err.Error(), tt.expectUnmarshalErr) | ||||
| 				return | ||||
| 			} else { | ||||
| 				require.NoError(t, err) | ||||
| 			} | ||||
| 
 | ||||
| 			if tt.expectContains != "" { | ||||
| 				healthPolicyField, ok := GetFieldByPath(rootField, "attributes.status.healthPolicy") | ||||
| 				require.True(t, ok) | ||||
| 
 | ||||
| 				switch v := healthPolicyField.Value.(type) { | ||||
| 				case *ast.BasicLit: | ||||
| 					require.Contains(t, v.Value, tt.expectContains) | ||||
| 				case *ast.StructLit: | ||||
| 					out, err := format.Node(v) | ||||
| 					require.NoError(t, err) | ||||
| 					require.Contains(t, string(out), tt.expectContains) | ||||
| 				default: | ||||
| 					t.Fatalf("unexpected healthPolicy value type: %T", v) | ||||
| 				} | ||||
| 			} | ||||
| 		}) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func TestMarshalAndUnmarshalCustomStatus(t *testing.T) { | ||||
| 	tests := []struct { | ||||
| 		name               string | ||||
| 		input              string | ||||
| 		expectMarshalErr   string | ||||
| 		expectUnmarshalErr string | ||||
| 		expectContains     string | ||||
| 	}{ | ||||
| 		{ | ||||
| 			name: "valid customStatus with string message", | ||||
| 			input: ` | ||||
| 				attributes: { | ||||
| 					status: { | ||||
| 						customStatus: { | ||||
| 							message: "Service is healthy" | ||||
| 						} | ||||
| 					} | ||||
| 				} | ||||
| 			`, | ||||
| 			expectContains: "message", | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "valid customStatus with interpolation", | ||||
| 			input: ` | ||||
| 				attributes: { | ||||
| 					status: { | ||||
| 						customStatus: { | ||||
| 							message: "\(context.output.metadata.name) is running" | ||||
| 						} | ||||
| 					} | ||||
| 				} | ||||
| 			`, | ||||
| 			expectContains: "message", | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "valid customStatus with selector expression", | ||||
| 			input: ` | ||||
| 				attributes: { | ||||
| 					status: { | ||||
| 						customStatus: { | ||||
| 							message: context.output.status.message | ||||
| 						} | ||||
| 					} | ||||
| 				} | ||||
| 			`, | ||||
| 			expectContains: "message", | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "valid customStatus with binary expression", | ||||
| 			input: ` | ||||
| 				attributes: { | ||||
| 					status: { | ||||
| 						customStatus: { | ||||
| 							message: "Replicas: " + context.output.status.replicas | ||||
| 						} | ||||
| 					} | ||||
| 				} | ||||
| 			`, | ||||
| 			expectContains: "message", | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "valid customStatus with call expression", | ||||
| 			input: ` | ||||
| 				attributes: { | ||||
| 					status: { | ||||
| 						customStatus: { | ||||
| 							message: strconv.FormatInt(context.output.status.replicas, 10) | ||||
| 						} | ||||
| 					} | ||||
| 				} | ||||
| 			`, | ||||
| 			expectContains: "message", | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "valid customStatus with list expression", | ||||
| 			input: ` | ||||
| 				attributes: { | ||||
| 					status: { | ||||
| 						customStatus: { | ||||
| 							message: [for c in context.output.status.conditions if c.type == "Ready" { c.message }][0] | ||||
| 						} | ||||
| 					} | ||||
| 				} | ||||
| 			`, | ||||
| 			expectContains: "message", | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "customStatus missing message field", | ||||
| 			input: ` | ||||
| 				attributes: { | ||||
| 					status: { | ||||
| 						customStatus: { | ||||
| 							someOtherField: "value" | ||||
| 						} | ||||
| 					} | ||||
| 				} | ||||
| 			`, | ||||
| 			expectMarshalErr: "customStatus must contain a 'message' field", | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "customStatus with invalid message type (struct)", | ||||
| 			input: ` | ||||
| 				attributes: { | ||||
| 					status: { | ||||
| 						customStatus: { | ||||
| 							message: { nested: "value" } | ||||
| 						} | ||||
| 					} | ||||
| 				} | ||||
| 			`, | ||||
| 			expectMarshalErr: "customStatus field 'message' must be a string expression", | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "customStatus with integer literal message", | ||||
| 			input: ` | ||||
| 				attributes: { | ||||
| 					status: { | ||||
| 						customStatus: { | ||||
| 							message: 42 | ||||
| 						} | ||||
| 					} | ||||
| 				} | ||||
| 			`, | ||||
| 			expectMarshalErr: "customStatus field 'message' must be a string", | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "valid stringified customStatus round trip", | ||||
| 			input: ` | ||||
| 				attributes: { | ||||
| 					status: { | ||||
| 						customStatus: #""" | ||||
| 							message: "Pod \(context.output.metadata.name) is running" | ||||
| 						"""# | ||||
| 					} | ||||
| 				} | ||||
| 			`, | ||||
| 			expectContains: "message", | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "malformed stringified customStatus fails validation", | ||||
| 			input: ` | ||||
| 				attributes: { | ||||
| 					status: { | ||||
| 						customStatus: #""" | ||||
| 							invalid cue: abc | ||||
| 						"""# | ||||
| 					} | ||||
| 				} | ||||
| 			`, | ||||
| 			expectMarshalErr: "invalid cue content in string literal", | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "customStatus with additional fields alongside message", | ||||
| 			input: ` | ||||
| 				attributes: { | ||||
| 					status: { | ||||
| 						customStatus: { | ||||
| 							message: "Service is healthy" | ||||
| 							severity: "info" | ||||
| 							timestamp: context.output.metadata.creationTimestamp | ||||
| 						} | ||||
| 					} | ||||
| 				} | ||||
| 			`, | ||||
| 			expectContains: "message", | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "customStatus as plain string is valid", | ||||
| 			input: ` | ||||
| 				attributes: { | ||||
| 					status: { | ||||
| 						customStatus: "message: \"Hello\"" | ||||
| 					} | ||||
| 				} | ||||
| 			`, | ||||
| 			expectContains: "message", | ||||
| 		}, | ||||
| 	} | ||||
| 
 | ||||
| 	for _, tt := range tests { | ||||
| 		tt := tt | ||||
| 		t.Run(tt.name, func(t *testing.T) { | ||||
| 			file, err := parser.ParseFile("-", tt.input) | ||||
| 			require.NoError(t, err) | ||||
| 
 | ||||
| 			var rootField *ast.Field | ||||
| 			for _, decl := range file.Decls { | ||||
| 				if f, ok := decl.(*ast.Field); ok { | ||||
| 					rootField = f | ||||
| 					break | ||||
| 				} | ||||
| 			} | ||||
| 			require.NotNil(t, rootField) | ||||
| 
 | ||||
| 			err = EncodeMetadata(rootField) | ||||
| 			if tt.expectMarshalErr != "" { | ||||
| 				require.Error(t, err) | ||||
| 				require.Contains(t, err.Error(), tt.expectMarshalErr) | ||||
| 				return | ||||
| 			} else { | ||||
| 				require.NoError(t, err) | ||||
| 			} | ||||
| 
 | ||||
| 			err = DecodeMetadata(rootField) | ||||
| 			if tt.expectUnmarshalErr != "" { | ||||
| 				require.Error(t, err) | ||||
| 				require.Contains(t, err.Error(), tt.expectUnmarshalErr) | ||||
| 				return | ||||
| 			} else { | ||||
| 				require.NoError(t, err) | ||||
| 			} | ||||
| 
 | ||||
| 			if tt.expectContains != "" { | ||||
| 				customStatusField, ok := GetFieldByPath(rootField, "attributes.status.customStatus") | ||||
| 				require.True(t, ok) | ||||
| 
 | ||||
| 				switch v := customStatusField.Value.(type) { | ||||
| 				case *ast.BasicLit: | ||||
| 					require.Contains(t, v.Value, tt.expectContains) | ||||
| 				case *ast.StructLit: | ||||
| 					out, err := format.Node(v) | ||||
| 					require.NoError(t, err) | ||||
| 					require.Contains(t, string(out), tt.expectContains) | ||||
| 				default: | ||||
| 					t.Fatalf("unexpected customStatus value type: %T", v) | ||||
| 				} | ||||
| 			} | ||||
| 		}) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func TestHealthPolicyEdgeCases(t *testing.T) { | ||||
| 	tests := []struct { | ||||
| 		name               string | ||||
| 		input              string | ||||
| 		expectMarshalErr   string | ||||
| 		expectUnmarshalErr string | ||||
| 	}{ | ||||
| 		{ | ||||
| 			name: "healthPolicy with unary expression", | ||||
| 			input: ` | ||||
| 				attributes: { | ||||
| 					status: { | ||||
| 						healthPolicy: { | ||||
| 							isHealth: !context.output.status.failed | ||||
| 						} | ||||
| 					} | ||||
| 				} | ||||
| 			`, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "healthPolicy with parenthesized expression", | ||||
| 			input: ` | ||||
| 				attributes: { | ||||
| 					status: { | ||||
| 						healthPolicy: { | ||||
| 							isHealth: (context.output.status.phase == "Running" || context.output.status.phase == "Succeeded") | ||||
| 						} | ||||
| 					} | ||||
| 				} | ||||
| 			`, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "healthPolicy with nested binary expressions", | ||||
| 			input: ` | ||||
| 				attributes: { | ||||
| 					status: { | ||||
| 						healthPolicy: { | ||||
| 							isHealth: context.output.status.ready && (context.output.status.replicas > 0 || context.output.status.phase == "Ready") | ||||
| 						} | ||||
| 					} | ||||
| 				} | ||||
| 			`, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "healthPolicy with string literal should fail", | ||||
| 			input: ` | ||||
| 				attributes: { | ||||
| 					status: { | ||||
| 						healthPolicy: { | ||||
| 							isHealth: "true" | ||||
| 						} | ||||
| 					} | ||||
| 				} | ||||
| 			`, | ||||
| 			expectMarshalErr: "healthPolicy field 'isHealth' must be a boolean literal (true/false)", | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "healthPolicy with comprehension should fail", | ||||
| 			input: ` | ||||
| 				attributes: { | ||||
| 					status: { | ||||
| 						healthPolicy: { | ||||
| 							isHealth: [for c in context.output.status.conditions if c.type == "Ready" { c.status }][0] | ||||
| 						} | ||||
| 					} | ||||
| 				} | ||||
| 			`, | ||||
| 			expectMarshalErr: "healthPolicy field 'isHealth' must be a boolean expression", | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "healthPolicy with empty struct should fail", | ||||
| 			input: ` | ||||
| 				attributes: { | ||||
| 					status: { | ||||
| 						healthPolicy: {} | ||||
| 					} | ||||
| 				} | ||||
| 			`, | ||||
| 			expectMarshalErr: "healthPolicy must contain an 'isHealth' field", | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "healthPolicy with additional fields is allowed", | ||||
| 			input: ` | ||||
| 				attributes: { | ||||
| 					status: { | ||||
| 						healthPolicy: { | ||||
| 							isHealth: true | ||||
| 							reason: "always healthy" | ||||
| 						} | ||||
| 					} | ||||
| 				} | ||||
| 			`, | ||||
| 		}, | ||||
| 	} | ||||
| 
 | ||||
| 	for _, tt := range tests { | ||||
| 		tt := tt | ||||
| 		t.Run(tt.name, func(t *testing.T) { | ||||
| 			file, err := parser.ParseFile("-", tt.input) | ||||
| 			require.NoError(t, err) | ||||
| 
 | ||||
| 			var rootField *ast.Field | ||||
| 			for _, decl := range file.Decls { | ||||
| 				if f, ok := decl.(*ast.Field); ok { | ||||
| 					rootField = f | ||||
| 					break | ||||
| 				} | ||||
| 			} | ||||
| 			require.NotNil(t, rootField) | ||||
| 
 | ||||
| 			err = EncodeMetadata(rootField) | ||||
| 			if tt.expectMarshalErr != "" { | ||||
| 				require.Error(t, err) | ||||
| 				require.Contains(t, err.Error(), tt.expectMarshalErr) | ||||
| 				return | ||||
| 			} else { | ||||
| 				require.NoError(t, err) | ||||
| 			} | ||||
| 
 | ||||
| 			err = DecodeMetadata(rootField) | ||||
| 			if tt.expectUnmarshalErr != "" { | ||||
| 				require.Error(t, err) | ||||
| 				require.Contains(t, err.Error(), tt.expectUnmarshalErr) | ||||
| 				return | ||||
| 			} else { | ||||
| 				require.NoError(t, err) | ||||
| 			} | ||||
| 		}) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func TestCustomStatusEdgeCases(t *testing.T) { | ||||
| 	tests := []struct { | ||||
| 		name               string | ||||
| 		input              string | ||||
| 		expectMarshalErr   string | ||||
| 		expectUnmarshalErr string | ||||
| 	}{ | ||||
| 		{ | ||||
| 			name: "customStatus with comprehension expression", | ||||
| 			input: ` | ||||
| 				attributes: { | ||||
| 					status: { | ||||
| 						customStatus: { | ||||
| 							message: [for i, v in context.output.status.conditions { "\(i): \(v.message)" }][0] | ||||
| 						} | ||||
| 					} | ||||
| 				} | ||||
| 			`, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "customStatus with index expression", | ||||
| 			input: ` | ||||
| 				attributes: { | ||||
| 					status: { | ||||
| 						customStatus: { | ||||
| 							message: context.output.status.conditions[0].message | ||||
| 						} | ||||
| 					} | ||||
| 				} | ||||
| 			`, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "customStatus with slice expression", | ||||
| 			input: ` | ||||
| 				attributes: { | ||||
| 					status: { | ||||
| 						customStatus: { | ||||
| 							message: context.output.status.message[0:10] | ||||
| 						} | ||||
| 					} | ||||
| 				} | ||||
| 			`, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "customStatus with parenthesized expression", | ||||
| 			input: ` | ||||
| 				attributes: { | ||||
| 					status: { | ||||
| 						customStatus: { | ||||
| 							message: ("Status: " + context.output.status.phase) | ||||
| 						} | ||||
| 					} | ||||
| 				} | ||||
| 			`, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "customStatus with nested interpolation", | ||||
| 			input: ` | ||||
| 				attributes: { | ||||
| 					status: { | ||||
| 						customStatus: { | ||||
| 							message: "Pod \(context.output.metadata.name) has \(context.output.status.replicas) replicas" | ||||
| 						} | ||||
| 					} | ||||
| 				} | ||||
| 			`, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "customStatus with boolean literal should fail", | ||||
| 			input: ` | ||||
| 				attributes: { | ||||
| 					status: { | ||||
| 						customStatus: { | ||||
| 							message: true | ||||
| 						} | ||||
| 					} | ||||
| 				} | ||||
| 			`, | ||||
| 			expectMarshalErr: "customStatus field 'message' must be a string", | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "customStatus with empty struct should fail", | ||||
| 			input: ` | ||||
| 				attributes: { | ||||
| 					status: { | ||||
| 						customStatus: {} | ||||
| 					} | ||||
| 				} | ||||
| 			`, | ||||
| 			expectMarshalErr: "customStatus must contain a 'message' field", | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "customStatus with only non-message fields should fail", | ||||
| 			input: ` | ||||
| 				attributes: { | ||||
| 					status: { | ||||
| 						customStatus: { | ||||
| 							severity: "error" | ||||
| 							code: 500 | ||||
| 						} | ||||
| 					} | ||||
| 				} | ||||
| 			`, | ||||
| 			expectMarshalErr: "customStatus must contain a 'message' field", | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "customStatus with field expression should fail", | ||||
| 			input: ` | ||||
| 				attributes: { | ||||
| 					status: { | ||||
| 						customStatus: { | ||||
| 							message: { template: "Hello" } | ||||
| 						} | ||||
| 					} | ||||
| 				} | ||||
| 			`, | ||||
| 			expectMarshalErr: "customStatus field 'message' must be a string expression", | ||||
| 		}, | ||||
| 	} | ||||
| 
 | ||||
| 	for _, tt := range tests { | ||||
| 		tt := tt | ||||
| 		t.Run(tt.name, func(t *testing.T) { | ||||
| 			file, err := parser.ParseFile("-", tt.input) | ||||
| 			require.NoError(t, err) | ||||
| 
 | ||||
| 			var rootField *ast.Field | ||||
| 			for _, decl := range file.Decls { | ||||
| 				if f, ok := decl.(*ast.Field); ok { | ||||
| 					rootField = f | ||||
| 					break | ||||
| 				} | ||||
| 			} | ||||
| 			require.NotNil(t, rootField) | ||||
| 
 | ||||
| 			err = EncodeMetadata(rootField) | ||||
| 			if tt.expectMarshalErr != "" { | ||||
| 				require.Error(t, err) | ||||
| 				require.Contains(t, err.Error(), tt.expectMarshalErr) | ||||
| 				return | ||||
| 			} else { | ||||
| 				require.NoError(t, err) | ||||
| 			} | ||||
| 
 | ||||
| 			err = DecodeMetadata(rootField) | ||||
| 			if tt.expectUnmarshalErr != "" { | ||||
| 				require.Error(t, err) | ||||
| 				require.Contains(t, err.Error(), tt.expectUnmarshalErr) | ||||
| 				return | ||||
| 			} else { | ||||
| 				require.NoError(t, err) | ||||
| 			} | ||||
| 		}) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func TestBackwardCompatibility(t *testing.T) { | ||||
| 	tests := []struct { | ||||
| 		name  string | ||||
| 		input string | ||||
| 	}{ | ||||
| 		{ | ||||
| 			name: "existing worker component healthPolicy format", | ||||
| 			input: ` | ||||
| 				attributes: { | ||||
| 					status: { | ||||
| 						healthPolicy: #""" | ||||
| 							isHealth: context.output.status.readyReplicas > 0 && context.output.status.readyReplicas == context.output.status.replicas | ||||
| 						"""# | ||||
| 					} | ||||
| 				} | ||||
| 			`, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "existing worker component customStatus format", | ||||
| 			input: ` | ||||
| 				attributes: { | ||||
| 					status: { | ||||
| 						customStatus: #""" | ||||
| 							appName:     context.appName | ||||
| 							internal:    "\($appName) is running" | ||||
| 							exposeType:  *"" | string | ||||
| 							if context.outputs.service != _|_ { | ||||
| 								exposeType: context.outputs.service.spec.type | ||||
| 							} | ||||
| 							if exposeType == "ClusterIP" { | ||||
| 								message: "\(appName) has ClusterIP service" | ||||
| 							} | ||||
| 							if exposeType == "NodePort" { | ||||
| 								message: "\(appName) has NodePort service" | ||||
| 							} | ||||
| 							if exposeType == "LoadBalancer" { | ||||
| 								message: "\(appName) has LoadBalancer service" | ||||
| 							} | ||||
| 							if exposeType == "" { | ||||
| 								message: internal | ||||
| 							} | ||||
| 						"""# | ||||
| 					} | ||||
| 				} | ||||
| 			`, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "complex multi-field definition with both healthPolicy and customStatus", | ||||
| 			input: ` | ||||
| 				attributes: { | ||||
| 					status: { | ||||
| 						healthPolicy: #""" | ||||
| 							isHealth: context.output.status.phase == "Running" || (context.output.status.phase == "Succeeded" && context.output.spec.restartPolicy == "Never") | ||||
| 						"""# | ||||
| 						customStatus: #""" | ||||
| 							ready: [ for c in context.output.status.conditions if c.type == "Ready" { c.status }][0] == "True" | ||||
| 							message: "Pod \(context.output.metadata.name): phase=\(context.output.status.phase), ready=\(ready)" | ||||
| 						"""# | ||||
| 					} | ||||
| 				} | ||||
| 			`, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "simple string format for healthPolicy", | ||||
| 			input: ` | ||||
| 				attributes: { | ||||
| 					status: { | ||||
| 						healthPolicy: "isHealth: true" | ||||
| 					} | ||||
| 				} | ||||
| 			`, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "simple string format for customStatus", | ||||
| 			input: ` | ||||
| 				attributes: { | ||||
| 					status: { | ||||
| 						customStatus: "message: \"Service is healthy\"" | ||||
| 					} | ||||
| 				} | ||||
| 			`, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "healthPolicy with list comprehensions and complex conditions", | ||||
| 			input: ` | ||||
| 				attributes: { | ||||
| 					status: { | ||||
| 						healthPolicy: #""" | ||||
| 							conditions: [ for c in context.output.status.conditions if c.type == "Ready" || c.type == "ContainersReady" { c.status }] | ||||
| 							isHealth: len($conditions) > 0 && ![ for c in conditions if c != "True" { c }] != [] | ||||
| 						"""# | ||||
| 					} | ||||
| 				} | ||||
| 			`, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "customStatus with nested conditionals and string interpolation", | ||||
| 			input: ` | ||||
| 				attributes: { | ||||
| 					status: { | ||||
| 						customStatus: #""" | ||||
| 							phase: context.output.status.phase | ||||
| 							replicas: context.output.status.replicas | ||||
| 							readyReplicas: *0 | int | ||||
| 							if context.output.status.readyReplicas != _|_ { | ||||
| 								readyReplicas: context.output.status.readyReplicas | ||||
| 							} | ||||
| 							if phase == "Running" { | ||||
| 								if readyReplicas == replicas { | ||||
| 									message: "All \(replicas) replicas are ready" | ||||
| 								} | ||||
| 								if readyReplicas < replicas { | ||||
| 									message: "Only \(readyReplicas) of \(replicas) replicas are ready" | ||||
| 								} | ||||
| 							} | ||||
| 							if phase != "Running" { | ||||
| 								message: "Deployment is in phase: \(phase)" | ||||
| 							} | ||||
| 						"""# | ||||
| 					} | ||||
| 				} | ||||
| 			`, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "preserving local fields that start with $", | ||||
| 			input: ` | ||||
| 				attributes: { | ||||
| 					status: { | ||||
| 						customStatus: #""" | ||||
| 							internal: "internal state" | ||||
| 							compute: context.output.status.replicas * 2 | ||||
| 							debugInfo: { | ||||
| 								phase: context.output.status.phase | ||||
| 								replicas: context.output.status.replicas | ||||
| 							} | ||||
| 							message: "Status: \(internal), computed: \(compute)" | ||||
| 						"""# | ||||
| 					} | ||||
| 				} | ||||
| 			`, | ||||
| 		}, | ||||
| 	} | ||||
| 
 | ||||
| 	for _, tt := range tests { | ||||
| 		tt := tt | ||||
| 		t.Run(tt.name, func(t *testing.T) { | ||||
| 			file, err := parser.ParseFile("-", tt.input) | ||||
| 			require.NoError(t, err) | ||||
| 
 | ||||
| 			var rootField *ast.Field | ||||
| 			for _, decl := range file.Decls { | ||||
| 				if f, ok := decl.(*ast.Field); ok { | ||||
| 					rootField = f | ||||
| 					break | ||||
| 				} | ||||
| 			} | ||||
| 			require.NotNil(t, rootField) | ||||
| 
 | ||||
| 			err = EncodeMetadata(rootField) | ||||
| 			require.NoError(t, err) | ||||
| 
 | ||||
| 			err = DecodeMetadata(rootField) | ||||
| 			require.NoError(t, err) | ||||
| 		}) | ||||
| 	} | ||||
| } | ||||
|  |  | |||
|  | @ -104,50 +104,40 @@ func StringifyStructLitAsCueString(structLit *ast.StructLit) (*ast.BasicLit, err | |||
| 		}, nil | ||||
| 	} | ||||
| 
 | ||||
| 	formatted, err := format.Node(structLit) | ||||
| 	if err != nil { | ||||
| 		return nil, fmt.Errorf("failed to format struct: %w", err) | ||||
| 	} | ||||
| 
 | ||||
| 	content := string(formatted) | ||||
| 
 | ||||
| 	content = strings.TrimSpace(content) | ||||
| 	if strings.HasPrefix(content, "{") && strings.HasSuffix(content, "}") { | ||||
| 		content = strings.TrimPrefix(content, "{") | ||||
| 		content = strings.TrimSuffix(content, "}") | ||||
| 		content = strings.Trim(content, "\n") | ||||
| 	} | ||||
| 
 | ||||
| 	if content == "" { | ||||
| 		return &ast.BasicLit{ | ||||
| 			Kind:  token.STRING, | ||||
| 			Value: `"{}"`, | ||||
| 		}, nil | ||||
| 	} | ||||
| 	lines := strings.Split(content, "\n") | ||||
| 
 | ||||
| 	var sb strings.Builder | ||||
| 	sb.WriteString(`#"""`) | ||||
| 	sb.WriteString("\n") | ||||
| 
 | ||||
| 	for _, elt := range structLit.Elts { | ||||
| 		field, ok := elt.(*ast.Field) | ||||
| 		if !ok { | ||||
| 			continue | ||||
| 		} | ||||
| 
 | ||||
| 		var labelStr, valueStr string | ||||
| 
 | ||||
| 		switch l := field.Label.(type) { | ||||
| 		case *ast.Ident: | ||||
| 			labelStr = l.Name | ||||
| 		case *ast.BasicLit: | ||||
| 			labelStr = strings.Trim(l.Value, `"`) | ||||
| 		default: | ||||
| 			labelStr = "<unknown>" | ||||
| 		} | ||||
| 
 | ||||
| 		for _, attr := range field.Attrs { | ||||
| 			sb.WriteString("  " + attr.Text + "\n") | ||||
| 		} | ||||
| 
 | ||||
| 		b, err := format.Node(field.Value) | ||||
| 		if err != nil { | ||||
| 			valueStr = "<complex>" | ||||
| 		} else { | ||||
| 			lines := strings.Split(string(b), "\n") | ||||
| 			for i := range lines { | ||||
| 				lines[i] = "  " + lines[i] | ||||
| 			} | ||||
| 			valueStr = strings.Join(lines, "\n") | ||||
| 		} | ||||
| 
 | ||||
| 		sb.WriteString(fmt.Sprintf("  %s: %s\n", labelStr, valueStr)) | ||||
| 	} | ||||
| 
 | ||||
| 	sb.WriteString(strings.Join(lines, "\n")) | ||||
| 	sb.WriteString("\n") | ||||
| 	sb.WriteString(`"""#`) | ||||
| 	val := strings.ReplaceAll(sb.String(), "\t", "  ") | ||||
| 
 | ||||
| 	result := strings.ReplaceAll(sb.String(), "\t", "  ") | ||||
| 
 | ||||
| 	return &ast.BasicLit{ | ||||
| 		Kind:  token.STRING, | ||||
| 		Value: val, | ||||
| 		Value: result, | ||||
| 	}, nil | ||||
| } | ||||
| 
 | ||||
|  | @ -177,20 +167,26 @@ func ValidateCueStringLiteral[T ast.Node](lit *ast.BasicLit, validator func(T) e | |||
| 	return validator(node) | ||||
| } | ||||
| 
 | ||||
| // TrimCueRawString trims a CUE raw string literal
 | ||||
| // TrimCueRawString trims a CUE raw string literal and handles escape sequences
 | ||||
| func TrimCueRawString(s string) string { | ||||
| 	s = strings.TrimSpace(s) | ||||
| 	if strings.HasPrefix(s, `#"""`) && strings.HasSuffix(s, `"""#`) { | ||||
| 		return strings.TrimSuffix(strings.TrimPrefix(s, `#"""`), `"""#`) | ||||
| 	switch { | ||||
| 	case strings.HasPrefix(s, `#"""`) && strings.HasSuffix(s, `"""#`): | ||||
| 		s = strings.TrimSuffix(strings.TrimPrefix(s, `#"""`), `"""#`) | ||||
| 	case strings.HasPrefix(s, `"""`) && strings.HasSuffix(s, `"""`): | ||||
| 		s = strings.TrimSuffix(strings.TrimPrefix(s, `"""`), `"""`) | ||||
| 	default: | ||||
| 		fallback, err := strconv.Unquote(s) | ||||
| 		if err == nil { | ||||
| 			s = fallback | ||||
| 		} | ||||
| 	} | ||||
| 	if strings.HasPrefix(s, `"""`) && strings.HasSuffix(s, `"""`) { | ||||
| 		return strings.TrimSuffix(strings.TrimPrefix(s, `"""`), `"""`) | ||||
| 	} | ||||
| 	fallback, err := strconv.Unquote(s) | ||||
| 	if err != nil { | ||||
| 		return s | ||||
| 	} | ||||
| 	return fallback | ||||
| 
 | ||||
| 	// Handle escape sequences for backward compatibility with existing definitions
 | ||||
| 	s = strings.ReplaceAll(s, "\\t", "  ") | ||||
| 	s = strings.ReplaceAll(s, "\\\\", "\\") | ||||
| 
 | ||||
| 	return s | ||||
| } | ||||
| 
 | ||||
| // WrapCueStruct wraps a string in a CUE struct format
 | ||||
|  | @ -198,6 +194,48 @@ func WrapCueStruct(s string) string { | |||
| 	return fmt.Sprintf("{\n%s\n}", s) | ||||
| } | ||||
| 
 | ||||
| // FindAndValidateField searches for a field at the top level or within top-level if statements
 | ||||
| func FindAndValidateField(sl *ast.StructLit, fieldName string, validator fieldValidator) (found bool, err error) { | ||||
| 	// First check top-level fields
 | ||||
| 	for _, elt := range sl.Elts { | ||||
| 		if field, ok := elt.(*ast.Field); ok { | ||||
| 			label := GetFieldLabel(field.Label) | ||||
| 			if label == fieldName { | ||||
| 				found = true | ||||
| 				if validator != nil { | ||||
| 					err = validator(field.Value) | ||||
| 				} | ||||
| 				return found, err | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	// If not found at top level, check within top-level if statements
 | ||||
| 	for _, elt := range sl.Elts { | ||||
| 		if comp, ok := elt.(*ast.Comprehension); ok { | ||||
| 			// Check if this comprehension has if clauses (conditional fields)
 | ||||
| 			hasIfClause := false | ||||
| 			for _, clause := range comp.Clauses { | ||||
| 				if _, ok := clause.(*ast.IfClause); ok { | ||||
| 					hasIfClause = true | ||||
| 					break | ||||
| 				} | ||||
| 			} | ||||
| 
 | ||||
| 			// If it has an if clause and the value is a struct, search within it
 | ||||
| 			if hasIfClause { | ||||
| 				if structLit, ok := comp.Value.(*ast.StructLit); ok { | ||||
| 					if innerFound, innerErr := FindAndValidateField(structLit, fieldName, validator); innerFound { | ||||
| 						return true, innerErr | ||||
| 					} | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	return found, err | ||||
| } | ||||
| 
 | ||||
| func lookupTopLevelField(node ast.Node, key string) (*ast.Field, ast.Expr, bool) { | ||||
| 	switch n := node.(type) { | ||||
| 	case *ast.Field: | ||||
|  | @ -220,6 +258,9 @@ func lookupTopLevelField(node ast.Node, key string) (*ast.Field, ast.Expr, bool) | |||
| 	return nil, nil, false | ||||
| } | ||||
| 
 | ||||
| // fieldValidator is a function that validates a field's value
 | ||||
| type fieldValidator func(ast.Expr) error | ||||
| 
 | ||||
| func traversePath(val ast.Expr, pathParts []string, lastField *ast.Field) (ast.Node, *ast.Field, bool) { | ||||
| 	currentField := lastField | ||||
| 	currentVal := val | ||||
|  |  | |||
|  | @ -281,6 +281,25 @@ func TestStringifyStructLitAsCueString(t *testing.T) { | |||
| 			}`, | ||||
| 			shouldFail: true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "Conditional fields with if statements", | ||||
| 			input: ` | ||||
| 			{ | ||||
| 				if context.status.healthy { | ||||
| 					message: "Healthy! (\(context.status.details.replicaReadyRatio * 100)% pods running)" | ||||
| 				} | ||||
| 
 | ||||
| 				if !context.status.healthy { | ||||
| 					message: "Unhealthy! (\(context.status.details.replicaReadyRatio * 100)% pods running)" | ||||
| 				} | ||||
| 			}`, | ||||
| 			contains: []string{ | ||||
| 				`if context.status.healthy {`, | ||||
| 				`message: "Healthy! (\(context.status.details.replicaReadyRatio*100)% pods running)"`, | ||||
| 				`if !context.status.healthy {`, | ||||
| 				`message: "Unhealthy! (\(context.status.details.replicaReadyRatio*100)% pods running)"`, | ||||
| 			}, | ||||
| 		}, | ||||
| 	} | ||||
| 
 | ||||
| 	for _, tt := range tests { | ||||
|  | @ -536,3 +555,270 @@ func TestWrapCueStruct(t *testing.T) { | |||
| 		}) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func TestFindAndValidateField(t *testing.T) { | ||||
| 	tests := []struct { | ||||
| 		name               string | ||||
| 		input              string | ||||
| 		fieldName          string | ||||
| 		expectedFound      bool | ||||
| 		expectedErrMessage string | ||||
| 		validator          fieldValidator | ||||
| 	}{ | ||||
| 		{ | ||||
| 			name: "find field at top level without validator", | ||||
| 			input: `{ | ||||
| 				field1: "value1" | ||||
| 				field2: "value2" | ||||
| 			}`, | ||||
| 			fieldName:     "field1", | ||||
| 			expectedFound: true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "field not found at any level", | ||||
| 			input: `{ | ||||
| 				field1: "value1" | ||||
| 				field2: "value2" | ||||
| 			}`, | ||||
| 			fieldName:     "field3", | ||||
| 			expectedFound: false, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "find field at top level with validator that passes", | ||||
| 			input: `{ | ||||
| 				message: "hello world" | ||||
| 				other: "value" | ||||
| 			}`, | ||||
| 			fieldName:     "message", | ||||
| 			expectedFound: true, | ||||
| 			validator: func(expr ast.Expr) error { | ||||
| 				if basicLit, ok := expr.(*ast.BasicLit); ok { | ||||
| 					if basicLit.Value != `"hello world"` { | ||||
| 						return fmt.Errorf("expected hello world, got %s", basicLit.Value) | ||||
| 					} | ||||
| 				} | ||||
| 				return nil | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "find field at top level with validator that fails", | ||||
| 			input: `{ | ||||
| 				message: "hello world" | ||||
| 				other: "value" | ||||
| 			}`, | ||||
| 			fieldName:          "message", | ||||
| 			expectedFound:      true, | ||||
| 			expectedErrMessage: "expected goodbye, got", | ||||
| 			validator: func(expr ast.Expr) error { | ||||
| 				if basicLit, ok := expr.(*ast.BasicLit); ok { | ||||
| 					if basicLit.Value != `"goodbye"` { | ||||
| 						return fmt.Errorf("expected goodbye, got %s", basicLit.Value) | ||||
| 					} | ||||
| 				} | ||||
| 				return nil | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "find field in simple if statement", | ||||
| 			input: `{ | ||||
| 				otherField: "value" | ||||
| 				if condition { | ||||
| 					message: "found in if" | ||||
| 				} | ||||
| 			}`, | ||||
| 			fieldName:     "message", | ||||
| 			expectedFound: true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "find field in nested if statements", | ||||
| 			input: `{ | ||||
| 				otherField: "value" | ||||
| 				if condition1 { | ||||
| 					if condition2 { | ||||
| 						message: "deeply nested" | ||||
| 					} | ||||
| 				} | ||||
| 			}`, | ||||
| 			fieldName:     "message", | ||||
| 			expectedFound: true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "find field in if-else chain", | ||||
| 			input: `{ | ||||
| 				status: "running" | ||||
| 				if status == "running" { | ||||
| 					message: "service is running" | ||||
| 				} | ||||
| 				if status == "stopped" { | ||||
| 					message: "service is stopped" | ||||
| 				} | ||||
| 			}`, | ||||
| 			fieldName:     "message", | ||||
| 			expectedFound: true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "find field in complex nested conditionals", | ||||
| 			input: `{ | ||||
| 				phase: context.output.status.phase | ||||
| 				replicas: context.output.status.replicas | ||||
| 				readyReplicas: *0 | int | ||||
| 				if context.output.status.readyReplicas != _|_ { | ||||
| 					readyReplicas: context.output.status.readyReplicas | ||||
| 				} | ||||
| 				if phase == "Running" { | ||||
| 					if readyReplicas == replicas { | ||||
| 						message: "All replicas are ready" | ||||
| 					} | ||||
| 					if readyReplicas < replicas { | ||||
| 						message: "Some replicas are not ready" | ||||
| 					} | ||||
| 				} | ||||
| 				if phase != "Running" { | ||||
| 					message: "Deployment is not running" | ||||
| 				} | ||||
| 			}`, | ||||
| 			fieldName:     "message", | ||||
| 			expectedFound: true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "find field in if with validator", | ||||
| 			input: `{ | ||||
| 				condition: true | ||||
| 				if condition { | ||||
| 					isHealth: true | ||||
| 				} | ||||
| 			}`, | ||||
| 			fieldName:     "isHealth", | ||||
| 			expectedFound: true, | ||||
| 			validator: func(expr ast.Expr) error { | ||||
| 				if basicLit, ok := expr.(*ast.BasicLit); ok { | ||||
| 					if basicLit.Value != "true" { | ||||
| 						return fmt.Errorf("expected true, got %s", basicLit.Value) | ||||
| 					} | ||||
| 				} | ||||
| 				return nil | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "find field in nested if with failing validator", | ||||
| 			input: `{ | ||||
| 				condition: true | ||||
| 				if condition { | ||||
| 					if nestedCondition { | ||||
| 						isHealth: false | ||||
| 					} | ||||
| 				} | ||||
| 			}`, | ||||
| 			fieldName:          "isHealth", | ||||
| 			expectedFound:      true, | ||||
| 			expectedErrMessage: "expected true, got", | ||||
| 			validator: func(expr ast.Expr) error { | ||||
| 				if basicLit, ok := expr.(*ast.BasicLit); ok { | ||||
| 					if basicLit.Value != "true" { | ||||
| 						return fmt.Errorf("expected true, got %s", basicLit.Value) | ||||
| 					} | ||||
| 				} | ||||
| 				return nil | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "field not found in comprehension without if clause", | ||||
| 			input: `{ | ||||
| 				items: [for x in list { value: x }] | ||||
| 			}`, | ||||
| 			fieldName:     "message", | ||||
| 			expectedFound: false, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "find field with complex expressions in if", | ||||
| 			input: `{ | ||||
| 				replicas: context.output.status.replicas | ||||
| 				readyReplicas: context.output.status.readyReplicas | ||||
| 				if (replicas | *0) != (readyReplicas | *0) { | ||||
| 					message: "not ready" | ||||
| 				} | ||||
| 			}`, | ||||
| 			fieldName:     "message", | ||||
| 			expectedFound: true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "find field with quoted label in if statement", | ||||
| 			input: `{ | ||||
| 				condition: true | ||||
| 				if condition { | ||||
| 					"field-with-dash": "value" | ||||
| 				} | ||||
| 			}`, | ||||
| 			fieldName:     "field-with-dash", | ||||
| 			expectedFound: true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "find field in multiple nested levels", | ||||
| 			input: `{ | ||||
| 				level1: "value" | ||||
| 				if condition1 { | ||||
| 					level2: "value" | ||||
| 					if condition2 { | ||||
| 						level3: "value" | ||||
| 						if condition3 { | ||||
| 							message: "deeply nested field" | ||||
| 						} | ||||
| 					} | ||||
| 				} | ||||
| 			}`, | ||||
| 			fieldName:     "message", | ||||
| 			expectedFound: true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "empty struct with if statements", | ||||
| 			input: `{ | ||||
| 				if condition { | ||||
| 				} | ||||
| 			}`, | ||||
| 			fieldName:     "message", | ||||
| 			expectedFound: false, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "field exists in both top level and if statement (finds top level first)", | ||||
| 			input: `{ | ||||
| 				message: "top level" | ||||
| 				if condition { | ||||
| 					message: "in if" | ||||
| 				} | ||||
| 			}`, | ||||
| 			fieldName:     "message", | ||||
| 			expectedFound: true, | ||||
| 			validator: func(expr ast.Expr) error { | ||||
| 				if basicLit, ok := expr.(*ast.BasicLit); ok { | ||||
| 					if basicLit.Value == `"top level"` { | ||||
| 						return nil | ||||
| 					} | ||||
| 					return fmt.Errorf("expected top level message, got %s", basicLit.Value) | ||||
| 				} | ||||
| 				return fmt.Errorf("expected string literal") | ||||
| 			}, | ||||
| 		}, | ||||
| 	} | ||||
| 
 | ||||
| 	for _, tt := range tests { | ||||
| 		tt := tt | ||||
| 		t.Run(tt.name, func(t *testing.T) { | ||||
| 			expr, err := parser.ParseExpr("-", tt.input) | ||||
| 			require.NoError(t, err) | ||||
| 
 | ||||
| 			structLit, ok := expr.(*ast.StructLit) | ||||
| 			require.True(t, ok, "input should parse as struct literal") | ||||
| 
 | ||||
| 			found, err := FindAndValidateField(structLit, tt.fieldName, tt.validator) | ||||
| 
 | ||||
| 			require.Equal(t, tt.expectedFound, found, "found result should match expected") | ||||
| 
 | ||||
| 			if tt.expectedErrMessage != "" { | ||||
| 				require.Error(t, err) | ||||
| 				require.Contains(t, err.Error(), tt.expectedErrMessage) | ||||
| 			} else { | ||||
| 				require.NoError(t, err) | ||||
| 			} | ||||
| 		}) | ||||
| 	} | ||||
| } | ||||
|  |  | |||
|  | @ -280,13 +280,13 @@ func TestCueNativeStatusFromCueString(t *testing.T) { | |||
| 	        } | ||||
| 	      } | ||||
| 	      status: { | ||||
| 	        customStatus: #""" | ||||
| 	        customStatus: { | ||||
| 	          message: "\(context.output.status.readyReplicas) / \(context.output.status.replicas) replicas are ready" | ||||
| 	        """# | ||||
| 	        } | ||||
| 
 | ||||
| 	        healthPolicy: #""" | ||||
| 	        healthPolicy: { | ||||
| 	          isHealth: context.output.status.readyReplicas == context.output.status.replicas | ||||
| 	        """# | ||||
| 	        } | ||||
| 
 | ||||
| 	        details: { | ||||
| 	          $temp: context.output.status.replicas | ||||
|  |  | |||
|  | @ -29,11 +29,14 @@ import ( | |||
| 	. "github.com/onsi/gomega" | ||||
| 	corev1 "k8s.io/api/core/v1" | ||||
| 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | ||||
| 	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" | ||||
| 	pkgruntime "k8s.io/apimachinery/pkg/runtime" | ||||
| 	"sigs.k8s.io/controller-runtime/pkg/client" | ||||
| 
 | ||||
| 	"github.com/oam-dev/kubevela/apis/core.oam.dev/common" | ||||
| 	"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1" | ||||
| 	"github.com/oam-dev/kubevela/apis/types" | ||||
| 	pkgdef "github.com/oam-dev/kubevela/pkg/definition" | ||||
| 	"github.com/oam-dev/kubevela/pkg/oam" | ||||
| 	"github.com/oam-dev/kubevela/pkg/oam/util" | ||||
| 	utilcommon "github.com/oam-dev/kubevela/pkg/utils/common" | ||||
|  | @ -260,4 +263,90 @@ var _ = Describe("ComponentDefinition Normal tests", func() { | |||
| 		By("Verify application is running") | ||||
| 		verifyApplicationPhase(context.TODO(), newApp.Namespace, newApp.Name, common.ApplicationRunning) | ||||
| 	}) | ||||
| 
 | ||||
| 	Context("Definition Retrieval and CUE Parsing Validation", func() { | ||||
| 		It("should successfully parse all definitions loaded from helm chart templates", func() { | ||||
| 			By("Loading all definition YAML files from charts/vela-core/templates/defwithtemplate") | ||||
| 			_, file, _, _ := runtime.Caller(0) | ||||
| 			definitionDir := filepath.Join(file, "../../../charts/vela-core/templates/defwithtemplate") | ||||
| 
 | ||||
| 			files, err := filepath.Glob(filepath.Join(definitionDir, "*.yaml")) | ||||
| 			Expect(err).To(BeNil()) | ||||
| 			Expect(len(files)).To(BeNumerically(">", 0)) | ||||
| 
 | ||||
| 			By(fmt.Sprintf("Found %d definition YAML files to test", len(files))) | ||||
| 
 | ||||
| 			// Install all definitions
 | ||||
| 			installedDefinitions := []string{} | ||||
| 			for _, definitionFile := range files { | ||||
| 				definitionName := strings.TrimSuffix(filepath.Base(definitionFile), ".yaml") | ||||
| 				By(fmt.Sprintf("Installing definition from %s", definitionName)) | ||||
| 
 | ||||
| 				err := testdef.InstallDefinitionFromYAML(ctx, k8sClient, definitionFile, func(s string) string { | ||||
| 					// Replace helm template placeholders like the existing notification test
 | ||||
| 					s = strings.ReplaceAll(s, `{{ include "systemDefinitionNamespace" . }}`, namespace) | ||||
| 					s = strings.ReplaceAll(s, `{{- include "systemDefinitionNamespace" . }}`, namespace) | ||||
| 					return s | ||||
| 				}) | ||||
| 				if err != nil { | ||||
| 					// Some definitions might fail to install due to dependencies, that's ok
 | ||||
| 					fmt.Printf("Warning: Failed to install definition from %s: %v\n", definitionFile, err) | ||||
| 				} else { | ||||
| 					installedDefinitions = append(installedDefinitions, definitionName) | ||||
| 				} | ||||
| 			} | ||||
| 
 | ||||
| 			By(fmt.Sprintf("Successfully installed %d definitions, now testing CUE parsing", len(installedDefinitions))) | ||||
| 
 | ||||
| 			// Test CUE parsing on all installed ComponentDefinitions
 | ||||
| 			By("Testing CUE parsing for ComponentDefinitions") | ||||
| 			var componentDefs v1beta1.ComponentDefinitionList | ||||
| 			Expect(k8sClient.List(ctx, &componentDefs, &client.ListOptions{Namespace: namespace})).Should(Succeed()) | ||||
| 
 | ||||
| 			componentErrorCount := 0 | ||||
| 			for i := range componentDefs.Items { | ||||
| 				unstructuredObj, err := pkgruntime.DefaultUnstructuredConverter.ToUnstructured(&componentDefs.Items[i]) | ||||
| 				if err != nil { | ||||
| 					componentErrorCount++ | ||||
| 					fmt.Printf("ERROR: ComponentDefinition %s failed to convert to unstructured: %v\n", componentDefs.Items[i].Name, err) | ||||
| 					continue | ||||
| 				} | ||||
| 
 | ||||
| 				def := &pkgdef.Definition{Unstructured: unstructured.Unstructured{Object: unstructuredObj}} | ||||
| 				_, err = def.ToCUEString() | ||||
| 				if err != nil { | ||||
| 					componentErrorCount++ | ||||
| 					fmt.Printf("ERROR: ComponentDefinition %s failed CUE parsing: %v\n", componentDefs.Items[i].Name, err) | ||||
| 				} | ||||
| 			} | ||||
| 
 | ||||
| 			// Test CUE parsing on all installed TraitDefinitions
 | ||||
| 			By("Testing CUE parsing for TraitDefinitions") | ||||
| 			var traitDefs v1beta1.TraitDefinitionList | ||||
| 			Expect(k8sClient.List(ctx, &traitDefs, &client.ListOptions{Namespace: namespace})).Should(Succeed()) | ||||
| 
 | ||||
| 			traitErrorCount := 0 | ||||
| 			for i := range traitDefs.Items { | ||||
| 				unstructuredObj, err := pkgruntime.DefaultUnstructuredConverter.ToUnstructured(&traitDefs.Items[i]) | ||||
| 				if err != nil { | ||||
| 					traitErrorCount++ | ||||
| 					fmt.Printf("ERROR: TraitDefinition %s failed to convert to unstructured: %v\n", traitDefs.Items[i].Name, err) | ||||
| 					continue | ||||
| 				} | ||||
| 
 | ||||
| 				def := &pkgdef.Definition{Unstructured: unstructured.Unstructured{Object: unstructuredObj}} | ||||
| 				_, err = def.ToCUEString() | ||||
| 				if err != nil { | ||||
| 					traitErrorCount++ | ||||
| 					fmt.Printf("ERROR: TraitDefinition %s failed CUE parsing: %v\n", traitDefs.Items[i].Name, err) | ||||
| 				} | ||||
| 			} | ||||
| 
 | ||||
| 			By(fmt.Sprintf("CUE parsing results: %d ComponentDefinitions tested, %d TraitDefinitions tested", | ||||
| 				len(componentDefs.Items), len(traitDefs.Items))) | ||||
| 
 | ||||
| 			totalErrors := componentErrorCount + traitErrorCount | ||||
| 			Expect(totalErrors).To(Equal(0), fmt.Sprintf("%d definitions failed CUE parsing with updated logic", totalErrors)) | ||||
| 		}) | ||||
| 	}) | ||||
| }) | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue