diff --git a/pkg/generators/markers.go b/pkg/generators/markers.go index 9b568a1a8..5e18da183 100644 --- a/pkg/generators/markers.go +++ b/pkg/generators/markers.go @@ -62,8 +62,20 @@ func (c *CELTag) Validate() error { return nil } -// CommentTags represents the parsed comment tags for a given type. These types are then used to generate schema validations. -type CommentTags struct { +// commentTags represents the parsed comment tags for a given type. These types are then used to generate schema validations. +// These only include the newer prefixed tags. The older tags are still supported, +// but are not included in this struct. Comment Tags are transformed into a +// *spec.Schema, which is then combined with the older marker comments to produce +// the generated OpenAPI spec. +// +// List of tags not included in this struct: +// +// - +optional +// - +default +// - +listType +// - +listMapKeys +// - +mapType +type commentTags struct { spec.SchemaProps CEL []CELTag `json:"cel,omitempty"` @@ -73,8 +85,36 @@ type CommentTags struct { // Default any `json:"default,omitempty"` } +// Returns the schema for the given CommentTags instance. +// This is the final authoritative schema for the comment tags +func (c commentTags) ValidationSchema() (*spec.Schema, error) { + res := spec.Schema{ + SchemaProps: c.SchemaProps, + } + + if res.AllOf != nil { + res.AllOf = append([]spec.Schema{}, res.AllOf...) + } + + if len(c.CEL) > 0 { + // Convert the CELTag to a map[string]interface{} via JSON + celTagJSON, err := json.Marshal(c.CEL) + if err != nil { + return nil, fmt.Errorf("failed to marshal CEL tag: %w", err) + } + var celTagMap []interface{} + if err := json.Unmarshal(celTagJSON, &celTagMap); err != nil { + return nil, fmt.Errorf("failed to unmarshal CEL tag: %w", err) + } + + res.VendorExtensible.AddExtension("x-kubernetes-validations", celTagMap) + } + + return &res, nil +} + // validates the parameters in a CommentTags instance. Returns any errors encountered. -func (c CommentTags) Validate() error { +func (c commentTags) Validate() error { var err error @@ -133,60 +173,63 @@ func (c CommentTags) Validate() error { } // Performs type-specific validation for CommentTags porameters. Accepts a Type instance and returns any errors encountered during validation. -func (c CommentTags) ValidateType(t *types.Type) error { +func (c commentTags) ValidateType(t *types.Type) error { var err error resolvedType := resolveAliasAndPtrType(t) typeString, _ := openapi.OpenAPITypeFormat(resolvedType.String()) // will be empty for complicated types - isNoValidate := resolvedType.Kind == types.Interface || resolvedType.Kind == types.Struct - if !isNoValidate { + // Structs and interfaces may dynamically be any type, so we cant validate them + // easily. We may be able to if we check that they don't implement all the + // override functions, but for now we just skip them. + if resolvedType.Kind == types.Interface || resolvedType.Kind == types.Struct { + return nil + } - isArray := resolvedType.Kind == types.Slice || resolvedType.Kind == types.Array - isMap := resolvedType.Kind == types.Map - isString := typeString == "string" - isInt := typeString == "integer" - isFloat := typeString == "number" + isArray := resolvedType.Kind == types.Slice || resolvedType.Kind == types.Array + isMap := resolvedType.Kind == types.Map + isString := typeString == "string" + isInt := typeString == "integer" + isFloat := typeString == "number" - if c.MaxItems != nil && !isArray { - err = errors.Join(err, fmt.Errorf("maxItems can only be used on array types")) - } - if c.MinItems != nil && !isArray { - err = errors.Join(err, fmt.Errorf("minItems can only be used on array types")) - } - if c.UniqueItems && !isArray { - err = errors.Join(err, fmt.Errorf("uniqueItems can only be used on array types")) - } - if c.MaxProperties != nil && !isMap { - err = errors.Join(err, fmt.Errorf("maxProperties can only be used on map types")) - } - if c.MinProperties != nil && !isMap { - err = errors.Join(err, fmt.Errorf("minProperties can only be used on map types")) - } - if c.MinLength != nil && !isString { - err = errors.Join(err, fmt.Errorf("minLength can only be used on string types")) - } - if c.MaxLength != nil && !isString { - err = errors.Join(err, fmt.Errorf("maxLength can only be used on string types")) - } - if c.Pattern != "" && !isString { - err = errors.Join(err, fmt.Errorf("pattern can only be used on string types")) - } - if c.Minimum != nil && !isInt && !isFloat { - err = errors.Join(err, fmt.Errorf("minimum can only be used on numeric types")) - } - if c.Maximum != nil && !isInt && !isFloat { - err = errors.Join(err, fmt.Errorf("maximum can only be used on numeric types")) - } - if c.MultipleOf != nil && !isInt && !isFloat { - err = errors.Join(err, fmt.Errorf("multipleOf can only be used on numeric types")) - } - if c.ExclusiveMinimum && !isInt && !isFloat { - err = errors.Join(err, fmt.Errorf("exclusiveMinimum can only be used on numeric types")) - } - if c.ExclusiveMaximum && !isInt && !isFloat { - err = errors.Join(err, fmt.Errorf("exclusiveMaximum can only be used on numeric types")) - } + if c.MaxItems != nil && !isArray { + err = errors.Join(err, fmt.Errorf("maxItems can only be used on array types")) + } + if c.MinItems != nil && !isArray { + err = errors.Join(err, fmt.Errorf("minItems can only be used on array types")) + } + if c.UniqueItems && !isArray { + err = errors.Join(err, fmt.Errorf("uniqueItems can only be used on array types")) + } + if c.MaxProperties != nil && !isMap { + err = errors.Join(err, fmt.Errorf("maxProperties can only be used on map types")) + } + if c.MinProperties != nil && !isMap { + err = errors.Join(err, fmt.Errorf("minProperties can only be used on map types")) + } + if c.MinLength != nil && !isString { + err = errors.Join(err, fmt.Errorf("minLength can only be used on string types")) + } + if c.MaxLength != nil && !isString { + err = errors.Join(err, fmt.Errorf("maxLength can only be used on string types")) + } + if c.Pattern != "" && !isString { + err = errors.Join(err, fmt.Errorf("pattern can only be used on string types")) + } + if c.Minimum != nil && !isInt && !isFloat { + err = errors.Join(err, fmt.Errorf("minimum can only be used on numeric types")) + } + if c.Maximum != nil && !isInt && !isFloat { + err = errors.Join(err, fmt.Errorf("maximum can only be used on numeric types")) + } + if c.MultipleOf != nil && !isInt && !isFloat { + err = errors.Join(err, fmt.Errorf("multipleOf can only be used on numeric types")) + } + if c.ExclusiveMinimum && !isInt && !isFloat { + err = errors.Join(err, fmt.Errorf("exclusiveMinimum can only be used on numeric types")) + } + if c.ExclusiveMaximum && !isInt && !isFloat { + err = errors.Join(err, fmt.Errorf("exclusiveMaximum can only be used on numeric types")) } return err @@ -196,27 +239,28 @@ func (c CommentTags) ValidateType(t *types.Type) error { // Accepts an optional type to validate against, and a prefix to filter out markers not related to validation. // Accepts a prefix to filter out markers not related to validation. // Returns any errors encountered while parsing or validating the comment tags. -func ParseCommentTags(t *types.Type, comments []string, prefix string) (CommentTags, error) { - - markers, err := parseMarkers(comments, prefix) - if err != nil { - return CommentTags{}, fmt.Errorf("failed to parse marker comments: %w", err) - } - nested, err := nestMarkers(markers) +func ParseCommentTags(t *types.Type, comments []string, prefix string) (*spec.Schema, error) { + // Parse the comment tags + commentTags, err := parseCommentTagsFromLines(comments, prefix) if err != nil { - return CommentTags{}, fmt.Errorf("invalid marker comments: %w", err) + return nil, err } - // Parse the map into a CommentTags type by marshalling and unmarshalling - // as JSON in leiu of an unstructured converter. - out, err := json.Marshal(nested) - if err != nil { - return CommentTags{}, fmt.Errorf("failed to marshal marker comments: %w", err) - } + // If t is an alias then parse each of the underlying types' comment tags + // and merge them into a single schema. Aliases closest to t should take + // precedence (e.g. minLength specified in the alias closest to t should override + // any minLength specified in an alias further away from t). + currentT := t + for currentT != nil { + aliasCommentTags, err := parseCommentTagsFromLines(currentT.CommentLines, prefix) + if err != nil { + return nil, err + } + + mergeCommentTags(&commentTags, aliasCommentTags) - var commentTags CommentTags - if err = json.Unmarshal(out, &commentTags); err != nil { - return CommentTags{}, fmt.Errorf("failed to unmarshal marker comments: %w", err) + // Move to the next type in the chain + currentT = currentT.Underlying } // Validate the parsed comment tags @@ -227,10 +271,10 @@ func ParseCommentTags(t *types.Type, comments []string, prefix string) (CommentT } if validationErrors != nil { - return CommentTags{}, fmt.Errorf("invalid marker comments: %w", validationErrors) + return nil, fmt.Errorf("invalid marker comments: %w", validationErrors) } - return commentTags, nil + return commentTags.ValidationSchema() } var ( @@ -240,6 +284,81 @@ var ( valueRawString = regexp.MustCompile(fmt.Sprintf(`^(%s*)>(.*)$`, allowedKeyCharacterSet)) ) +func parseCommentTagsFromLines(comments []string, prefix string) (commentTags, error) { + if len(comments) == 0 { + return commentTags{}, nil + } + + markers, err := parseMarkers(comments, prefix) + if err != nil { + return commentTags{}, fmt.Errorf("failed to parse marker comments: %w", err) + } + nested, err := nestMarkers(markers) + if err != nil { + return commentTags{}, fmt.Errorf("invalid marker comments: %w", err) + } + + // Parse the map into a CommentTags type by marshalling and unmarshalling + // as JSON in leiu of an unstructured converter. + out, err := json.Marshal(nested) + if err != nil { + return commentTags{}, fmt.Errorf("failed to marshal marker comments: %w", err) + } + + var result commentTags + if err = json.Unmarshal(out, &result); err != nil { + return commentTags{}, fmt.Errorf("failed to unmarshal marker comments: %w", err) + } + return result, nil +} + +// Writes src values into dst if dst is nil, and src is non-nil +// Does not override anythng already set in dst +func mergeCommentTags(dst *commentTags, src commentTags) { + if src.MinLength != nil && dst.MinLength == nil { + dst.MinLength = src.MinLength + } + if src.MaxLength != nil && dst.MaxLength == nil { + dst.MaxLength = src.MaxLength + } + if src.MinItems != nil && dst.MinItems == nil { + dst.MinItems = src.MinItems + } + if src.MaxItems != nil && dst.MaxItems == nil { + dst.MaxItems = src.MaxItems + } + if src.MinProperties != nil && dst.MinProperties == nil { + dst.MinProperties = src.MinProperties + } + if src.MaxProperties != nil && dst.MaxProperties == nil { + dst.MaxProperties = src.MaxProperties + } + if src.Minimum != nil && dst.Minimum == nil { + dst.Minimum = src.Minimum + } + if src.Maximum != nil && dst.Maximum == nil { + dst.Maximum = src.Maximum + } + if src.MultipleOf != nil && dst.MultipleOf == nil { + dst.MultipleOf = src.MultipleOf + } + if src.Pattern != "" && dst.Pattern == "" { + dst.Pattern = src.Pattern + } + if src.ExclusiveMinimum && !dst.ExclusiveMinimum { + dst.ExclusiveMinimum = true + } + if src.ExclusiveMaximum && !dst.ExclusiveMaximum { + dst.ExclusiveMaximum = true + } + if src.UniqueItems && !dst.UniqueItems { + dst.UniqueItems = true + } + if len(src.CEL) > 0 { + dst.CEL = append(src.CEL, dst.CEL...) + } +} + // extractCommentTags parses comments for lines of the form: // // 'marker' + "key=value" diff --git a/pkg/generators/markers_test.go b/pkg/generators/markers_test.go index 1b7260b39..d8dba0183 100644 --- a/pkg/generators/markers_test.go +++ b/pkg/generators/markers_test.go @@ -35,7 +35,7 @@ func TestParseCommentTags(t *testing.T) { t *types.Type name string comments []string - expected generators.CommentTags + expected *spec.Schema // regex pattern matching the error, or empty string/unset if no error // is expected @@ -59,7 +59,7 @@ func TestParseCommentTags(t *testing.T) { "exclusiveMaximum=true", "not+k8s:validation:Minimum=0.0", }, - expected: generators.CommentTags{ + expected: &spec.Schema{ SchemaProps: spec.SchemaProps{ Maximum: ptr.To(20.0), Minimum: ptr.To(10.0), @@ -74,8 +74,9 @@ func TestParseCommentTags(t *testing.T) { }, }, { - t: structKind, - name: "empty", + t: structKind, + name: "empty", + expected: &spec.Schema{}, }, { t: types.Float64, @@ -83,7 +84,7 @@ func TestParseCommentTags(t *testing.T) { comments: []string{ "+k8s:validation:minimum=10.0", }, - expected: generators.CommentTags{ + expected: &spec.Schema{ SchemaProps: spec.SchemaProps{ Minimum: ptr.To(10.0), }, @@ -96,7 +97,7 @@ func TestParseCommentTags(t *testing.T) { "+k8s:validation:minimum=10.0", "+k8s:validation:maximum=20.0", }, - expected: generators.CommentTags{ + expected: &spec.Schema{ SchemaProps: spec.SchemaProps{ Maximum: ptr.To(20.0), Minimum: ptr.To(10.0), @@ -119,6 +120,7 @@ func TestParseCommentTags(t *testing.T) { comments: []string{ "+ignored=30.0", }, + expected: &spec.Schema{}, }, { t: types.Float64, @@ -134,7 +136,7 @@ func TestParseCommentTags(t *testing.T) { comments: []string{ `+k8s:validation:minimum="asdf"`, }, - expectedError: `failed to unmarshal marker comments: json: cannot unmarshal string into Go struct field CommentTags.minimum of type float64`, + expectedError: `failed to unmarshal marker comments: json: cannot unmarshal string into Go struct field commentTags.minimum of type float64`, }, { @@ -152,6 +154,7 @@ func TestParseCommentTags(t *testing.T) { comments: []string{ "+k8s:validation:pattern=ref(asdf)", }, + expected: &spec.Schema{}, }, { t: types.Float64, @@ -160,11 +163,15 @@ func TestParseCommentTags(t *testing.T) { `+k8s:validation:cel[0]:rule="oldSelf == self"`, `+k8s:validation:cel[0]:message="immutable field"`, }, - expected: generators.CommentTags{ - CEL: []generators.CELTag{ - { - Rule: "oldSelf == self", - Message: "immutable field", + expected: &spec.Schema{ + VendorExtensible: spec.VendorExtensible{ + Extensions: map[string]interface{}{ + "x-kubernetes-validations": []interface{}{ + map[string]interface{}{ + "rule": "oldSelf == self", + "message": "immutable field", + }, + }, }, }, }, @@ -192,16 +199,20 @@ func TestParseCommentTags(t *testing.T) { `+k8s:validation:cel[1]:optionalOldSelf=true`, `+k8s:validation:cel[1]:message="must be greater than 5"`, }, - expected: generators.CommentTags{ - CEL: []generators.CELTag{ - { - Rule: "oldSelf == self", - Message: "immutable field", - }, - { - Rule: "self > 5", - Message: "must be greater than 5", - OptionalOldSelf: ptr.To(true), + expected: &spec.Schema{ + VendorExtensible: spec.VendorExtensible{ + Extensions: map[string]interface{}{ + "x-kubernetes-validations": []interface{}{ + map[string]interface{}{ + "rule": "oldSelf == self", + "message": "immutable field", + }, + map[string]interface{}{ + "rule": "self > 5", + "optionalOldSelf": true, + "message": "must be greater than 5", + }, + }, }, }, }, @@ -217,17 +228,21 @@ func TestParseCommentTags(t *testing.T) { `+k8s:validation:cel[1]:optionalOldSelf=true`, `+k8s:validation:cel[1]:message="must be greater than 5"`, }, - expected: generators.CommentTags{ - CEL: []generators.CELTag{ - { - Rule: "oldSelf == self", - MessageExpression: "self + ' must be equal to old value'", - OptionalOldSelf: ptr.To(true), - }, - { - Rule: "self > 5", - Message: "must be greater than 5", - OptionalOldSelf: ptr.To(true), + expected: &spec.Schema{ + VendorExtensible: spec.VendorExtensible{ + Extensions: map[string]interface{}{ + "x-kubernetes-validations": []interface{}{ + map[string]interface{}{ + "rule": "oldSelf == self", + "optionalOldSelf": true, + "messageExpression": "self + ' must be equal to old value'", + }, + map[string]interface{}{ + "rule": "self > 5", + "optionalOldSelf": true, + "message": "must be greater than 5", + }, + }, }, }, }, @@ -304,15 +319,19 @@ func TestParseCommentTags(t *testing.T) { `+k8s:validation:cel[0]:rule="string rule [1]"`, `+k8s:validation:pattern="self[3] == 'hi'"`, }, - expected: generators.CommentTags{ - CEL: []generators.CELTag{ - { - Rule: "string rule [1]", - Message: "[3]string rule [1]", - }, - }, + expected: &spec.Schema{ SchemaProps: spec.SchemaProps{ - Pattern: "self[3] == 'hi'", + Pattern: `self[3] == 'hi'`, + }, + VendorExtensible: spec.VendorExtensible{ + Extensions: map[string]interface{}{ + "x-kubernetes-validations": []interface{}{ + map[string]interface{}{ + "rule": "string rule [1]", + "message": "[3]string rule [1]", + }, + }, + }, }, }, }, @@ -324,16 +343,20 @@ func TestParseCommentTags(t *testing.T) { `+k8s:validation:cel[0]:rule> raw string rule [1]`, `+k8s:validation:pattern>"self[3] == 'hi'"`, }, - expected: generators.CommentTags{ - CEL: []generators.CELTag{ - { - Rule: "raw string rule [1]", - Message: "[3]raw string message with subscirpt [3]\"", - }, - }, + expected: &spec.Schema{ SchemaProps: spec.SchemaProps{ Pattern: `"self[3] == 'hi'"`, }, + VendorExtensible: spec.VendorExtensible{ + Extensions: map[string]interface{}{ + "x-kubernetes-validations": []interface{}{ + map[string]interface{}{ + "rule": "raw string rule [1]", + "message": "[3]raw string message with subscirpt [3]\"", + }, + }, + }, + }, }, }, { @@ -369,16 +392,20 @@ func TestParseCommentTags(t *testing.T) { `+k8s:validation:cel[1]:message="must be greater than 5"`, `+k8s:validation:cel[1]:optionalOldSelf`, }, - expected: generators.CommentTags{ - CEL: []generators.CELTag{ - { - Rule: "oldSelf == self", - Message: "cant change", - }, - { - Rule: "self > 5", - Message: "must be greater than 5", - OptionalOldSelf: ptr.To(true), + expected: &spec.Schema{ + VendorExtensible: spec.VendorExtensible{ + Extensions: map[string]interface{}{ + "x-kubernetes-validations": []interface{}{ + map[string]interface{}{ + "rule": "oldSelf == self", + "message": "cant change", + }, + map[string]interface{}{ + "rule": "self > 5", + "message": "must be greater than 5", + "optionalOldSelf": true, + }, + }, }, }, }, @@ -390,11 +417,15 @@ func TestParseCommentTags(t *testing.T) { `+k8s:validation:cel[0]:rule> raw string rule`, `+k8s:validation:cel[0]:message="raw string message"`, }, - expected: generators.CommentTags{ - CEL: []generators.CELTag{ - { - Rule: "raw string rule", - Message: "raw string message", + expected: &spec.Schema{ + VendorExtensible: spec.VendorExtensible{ + Extensions: map[string]interface{}{ + "x-kubernetes-validations": []interface{}{ + map[string]interface{}{ + "rule": "raw string rule", + "message": "raw string message", + }, + }, }, }, }, @@ -408,11 +439,15 @@ func TestParseCommentTags(t *testing.T) { `+k8s:validation:cel[0]:rule> : self.field == self.name + ' is odd'`, `+k8s:validation:cel[0]:message>raw string message`, }, - expected: generators.CommentTags{ - CEL: []generators.CELTag{ - { - Rule: "self.length() % 2 == 0\n? self.field == self.name + ' is even'\n: self.field == self.name + ' is odd'", - Message: "raw string message", + expected: &spec.Schema{ + VendorExtensible: spec.VendorExtensible{ + Extensions: map[string]interface{}{ + "x-kubernetes-validations": []interface{}{ + map[string]interface{}{ + "rule": "self.length() % 2 == 0\n? self.field == self.name + ' is even'\n: self.field == self.name + ' is odd'", + "message": "raw string message", + }, + }, }, }, }, @@ -426,11 +461,15 @@ func TestParseCommentTags(t *testing.T) { `+k8s:validation:cel[0]:rule> ? self.field == self.name + ' is even'`, `+k8s:validation:cel[0]:rule> : self.field == self.name + ' is odd'`, }, - expected: generators.CommentTags{ - CEL: []generators.CELTag{ - { - Rule: "self.length() % 2 == 0\n? self.field == self.name + ' is even'\n: self.field == self.name + ' is odd'", - Message: "raw string message", + expected: &spec.Schema{ + VendorExtensible: spec.VendorExtensible{ + Extensions: map[string]interface{}{ + "x-kubernetes-validations": []interface{}{ + map[string]interface{}{ + "rule": "self.length() % 2 == 0\n? self.field == self.name + ' is even'\n: self.field == self.name + ' is odd'", + "message": "raw string message", + }, + }, }, }, }, @@ -455,6 +494,69 @@ func TestParseCommentTags(t *testing.T) { }, expectedError: `failed to parse marker comments: concatenations to key 'cel[0]:message' must be consecutive with its assignment`, }, + { + name: "alias type without any comments", + t: &types.Type{Kind: types.Slice, Elem: &types.Type{Kind: types.Alias, Name: types.Name{Name: "PersistentVolumeAccessMode"}, Underlying: types.String}}, + comments: []string{ + `+k8s:validation:cel[0]:rule>!self.exists(c, c == "ReadWriteOncePod") || self.size() == 1`, + `+k8s:validation:cel[0]:message>may not use ReadWriteOncePod with other access modes`, + `+k8s:validation:cel[0]:reason>FieldForbidden`, + }, + expected: &spec.Schema{ + VendorExtensible: spec.VendorExtensible{ + Extensions: map[string]interface{}{ + "x-kubernetes-validations": []interface{}{ + map[string]interface{}{ + "rule": `!self.exists(c, c == "ReadWriteOncePod") || self.size() == 1`, + "message": "may not use ReadWriteOncePod with other access modes", + "reason": "FieldForbidden", + }, + }, + }, + }, + }, + }, + { + name: "alias type with comments", + t: &types.Type{ + Kind: types.Alias, + Name: types.Name{Name: "PersistentVolumeAccessModeList"}, + CommentLines: []string{ + `+k8s:validation:cel[0]:rule>self.all(c, ["ReadWriteOncePod","ReadOnlyMany","ReadWriteMany","ReadWriteOnce"].contains(c))`, + `+k8s:validation:cel[0]:message>must follow enum`, + }, + Underlying: &types.Type{ + Kind: types.Slice, + Elem: &types.Type{ + Kind: types.Alias, + Name: types.Name{Name: "PersistentVolumeAccessMode"}, + Underlying: types.String, + }, + }, + }, + comments: []string{ + `+k8s:validation:cel[0]:rule>!self.exists(c, c == "ReadWriteOncePod") || self.size() == 1`, + `+k8s:validation:cel[0]:message>may not use ReadWriteOncePod with other access modes`, + `+k8s:validation:cel[0]:reason>FieldForbidden`, + }, + expected: &spec.Schema{ + VendorExtensible: spec.VendorExtensible{ + Extensions: map[string]interface{}{ + "x-kubernetes-validations": []interface{}{ + map[string]interface{}{ + "rule": `self.all(c, ["ReadWriteOncePod","ReadOnlyMany","ReadWriteMany","ReadWriteOnce"].contains(c))`, + "message": "must follow enum", + }, + map[string]interface{}{ + "rule": `!self.exists(c, c == "ReadWriteOncePod") || self.size() == 1`, + "message": "may not use ReadWriteOncePod with other access modes", + "reason": "FieldForbidden", + }, + }, + }, + }, + }, + }, } for _, tc := range cases { diff --git a/pkg/generators/openapi.go b/pkg/generators/openapi.go index 757009251..93395eb5f 100644 --- a/pkg/generators/openapi.go +++ b/pkg/generators/openapi.go @@ -412,6 +412,22 @@ func (g openAPITypeWriter) generateValueValidations(vs *spec.SchemaProps) error if vs.UniqueItems { g.Do("UniqueItems: true,\n", nil) } + + allOfSchemas := vs.AllOf + if len(allOfSchemas) > 0 { + g.Do("AllOf: []spec.Schema{\n", nil) + for _, s := range allOfSchemas { + g.Do("spec.Schema{\n", nil) + err := g.generateValueValidations(&s.SchemaProps) + if err != nil { + return err + } + g.Do("},\n", nil) + } + + g.Do("},\n", nil) + } + return nil } @@ -419,7 +435,7 @@ func (g openAPITypeWriter) generate(t *types.Type) error { // Only generate for struct type and ignore the rest switch t.Kind { case types.Struct: - overrides, err := ParseCommentTags(t, t.CommentLines, markerPrefix) + validationSchema, err := ParseCommentTags(t, nil, markerPrefix) if err != nil { return err } @@ -444,12 +460,12 @@ func (g openAPITypeWriter) generate(t *types.Type) error { g.generateDescription(t.CommentLines) g.Do("Type:$.type|raw${}.OpenAPISchemaType(),\n"+ "Format:$.type|raw${}.OpenAPISchemaFormat(),\n", args) - err = g.generateValueValidations(&overrides.SchemaProps) + err = g.generateValueValidations(&validationSchema.SchemaProps) if err != nil { return err } g.Do("},\n", nil) - if err := g.generateStructExtensions(t, overrides); err != nil { + if err := g.generateStructExtensions(t, validationSchema.Extensions); err != nil { return err } g.Do("},\n", nil) @@ -463,12 +479,12 @@ func (g openAPITypeWriter) generate(t *types.Type) error { g.generateDescription(t.CommentLines) g.Do("OneOf:common.GenerateOpenAPIV3OneOfSchema($.type|raw${}.OpenAPIV3OneOfTypes()),\n"+ "Format:$.type|raw${}.OpenAPISchemaFormat(),\n", args) - err = g.generateValueValidations(&overrides.SchemaProps) + err = g.generateValueValidations(&validationSchema.SchemaProps) if err != nil { return err } g.Do("},\n", nil) - if err := g.generateStructExtensions(t, overrides); err != nil { + if err := g.generateStructExtensions(t, validationSchema.Extensions); err != nil { return err } g.Do("},\n", nil) @@ -480,12 +496,12 @@ func (g openAPITypeWriter) generate(t *types.Type) error { g.generateDescription(t.CommentLines) g.Do("Type:$.type|raw${}.OpenAPISchemaType(),\n"+ "Format:$.type|raw${}.OpenAPISchemaFormat(),\n", args) - err = g.generateValueValidations(&overrides.SchemaProps) + err = g.generateValueValidations(&validationSchema.SchemaProps) if err != nil { return err } g.Do("},\n", nil) - if err := g.generateStructExtensions(t, overrides); err != nil { + if err := g.generateStructExtensions(t, validationSchema.Extensions); err != nil { return err } g.Do("},\n", nil) @@ -498,12 +514,12 @@ func (g openAPITypeWriter) generate(t *types.Type) error { g.generateDescription(t.CommentLines) g.Do("Type:$.type|raw${}.OpenAPISchemaType(),\n"+ "Format:$.type|raw${}.OpenAPISchemaFormat(),\n", args) - err = g.generateValueValidations(&overrides.SchemaProps) + err = g.generateValueValidations(&validationSchema.SchemaProps) if err != nil { return err } g.Do("},\n", nil) - if err := g.generateStructExtensions(t, overrides); err != nil { + if err := g.generateStructExtensions(t, validationSchema.Extensions); err != nil { return err } g.Do("},\n", nil) @@ -517,7 +533,7 @@ func (g openAPITypeWriter) generate(t *types.Type) error { g.Do("return $.OpenAPIDefinition|raw${\nSchema: spec.Schema{\nSchemaProps: spec.SchemaProps{\n", args) g.generateDescription(t.CommentLines) g.Do("Type: []string{\"object\"},\n", nil) - err = g.generateValueValidations(&overrides.SchemaProps) + err = g.generateValueValidations(&validationSchema.SchemaProps) if err != nil { return err } @@ -541,7 +557,7 @@ func (g openAPITypeWriter) generate(t *types.Type) error { g.Do("Required: []string{\"$.$\"},\n", strings.Join(required, "\",\"")) } g.Do("},\n", nil) - if err := g.generateStructExtensions(t, overrides); err != nil { + if err := g.generateStructExtensions(t, validationSchema.Extensions); err != nil { return err } g.Do("},\n", nil) @@ -574,7 +590,7 @@ func (g openAPITypeWriter) generate(t *types.Type) error { return nil } -func (g openAPITypeWriter) generateStructExtensions(t *types.Type, tags CommentTags) error { +func (g openAPITypeWriter) generateStructExtensions(t *types.Type, otherExtensions map[string]interface{}) error { extensions, errors := parseExtensions(t.CommentLines) // Initially, we will only log struct extension errors. if len(errors) > 0 { @@ -590,11 +606,11 @@ func (g openAPITypeWriter) generateStructExtensions(t *types.Type, tags CommentT } // TODO(seans3): Validate struct extensions here. - g.emitExtensions(extensions, unions, tags.CEL) + g.emitExtensions(extensions, unions, otherExtensions) return nil } -func (g openAPITypeWriter) generateMemberExtensions(m *types.Member, parent *types.Type, tags CommentTags) error { +func (g openAPITypeWriter) generateMemberExtensions(m *types.Member, parent *types.Type, otherExtensions map[string]interface{}) error { extensions, parseErrors := parseExtensions(m.CommentLines) validationErrors := validateMemberExtensions(extensions, m) errors := append(parseErrors, validationErrors...) @@ -605,13 +621,13 @@ func (g openAPITypeWriter) generateMemberExtensions(m *types.Member, parent *typ klog.V(2).Infof("%s %s\n", errorPrefix, e) } } - g.emitExtensions(extensions, nil, tags.CEL) + g.emitExtensions(extensions, nil, otherExtensions) return nil } -func (g openAPITypeWriter) emitExtensions(extensions []extension, unions []union, celRules []CELTag) { +func (g openAPITypeWriter) emitExtensions(extensions []extension, unions []union, otherExtensions map[string]interface{}) { // If any extensions exist, then emit code to create them. - if len(extensions) == 0 && len(unions) == 0 && len(celRules) == 0 { + if len(extensions) == 0 && len(unions) == 0 && len(otherExtensions) == 0 { return } g.Do("VendorExtensible: spec.VendorExtensible{\nExtensions: spec.Extensions{\n", nil) @@ -635,42 +651,13 @@ func (g openAPITypeWriter) emitExtensions(extensions []extension, unions []union g.Do("},\n", nil) } - if len(celRules) > 0 { - g.Do("\"x-kubernetes-validations\": []interface{}{\n", nil) - for _, rule := range celRules { - g.Do("map[string]interface{}{\n", nil) - - g.Do("\"rule\": $.$,\n", fmt.Sprintf("%#v", rule.Rule)) - - if len(rule.Message) > 0 { - g.Do("\"message\": $.$,\n", fmt.Sprintf("%#v", rule.Message)) - } - - if len(rule.MessageExpression) > 0 { - g.Do("\"messageExpression\": $.$,\n", fmt.Sprintf("%#v", rule.MessageExpression)) - } - - if rule.OptionalOldSelf != nil && *rule.OptionalOldSelf { - g.Do("\"optionalOldSelf\": $.ptrTo|raw$[bool](true),\n", generator.Args{ - "ptrTo": &types.Type{ - Name: types.Name{ - Package: "k8s.io/utils/ptr", - Name: "To", - }}, - }) - } - - if len(rule.Reason) > 0 { - g.Do("\"reason\": $.$,\n", fmt.Sprintf("%#v", rule.Reason)) - } - - if len(rule.FieldPath) > 0 { - g.Do("\"fieldPath\": $.$,\n", fmt.Sprintf("%#v", rule.FieldPath)) - } - - g.Do("},\n", nil) + if len(otherExtensions) > 0 { + for k, v := range otherExtensions { + g.Do("$.key$: $.value$,\n", map[string]interface{}{ + "key": fmt.Sprintf("%#v", k), + "value": fmt.Sprintf("%#v", v), + }) } - g.Do("},\n", nil) } g.Do("},\n},\n", nil) @@ -857,7 +844,7 @@ func (g openAPITypeWriter) generateProperty(m *types.Member, parent *types.Type) if name == "" { return nil } - overrides, err := ParseCommentTags(m.Type, m.CommentLines, markerPrefix) + validationSchema, err := ParseCommentTags(m.Type, m.CommentLines, markerPrefix) if err != nil { return err } @@ -865,7 +852,7 @@ func (g openAPITypeWriter) generateProperty(m *types.Member, parent *types.Type) return err } g.Do("\"$.$\": {\n", name) - if err := g.generateMemberExtensions(m, parent, overrides); err != nil { + if err := g.generateMemberExtensions(m, parent, validationSchema.Extensions); err != nil { return err } g.Do("SchemaProps: spec.SchemaProps{\n", nil) @@ -884,7 +871,7 @@ func (g openAPITypeWriter) generateProperty(m *types.Member, parent *types.Type) if err := g.generateDefault(m.CommentLines, m.Type, omitEmpty, parent); err != nil { return fmt.Errorf("failed to generate default in %v: %v: %v", parent, m.Name, err) } - err = g.generateValueValidations(&overrides.SchemaProps) + err = g.generateValueValidations(&validationSchema.SchemaProps) if err != nil { return err } diff --git a/pkg/generators/openapi_test.go b/pkg/generators/openapi_test.go index 2ae47b227..9d6b4e0f2 100644 --- a/pkg/generators/openapi_test.go +++ b/pkg/generators/openapi_test.go @@ -2045,7 +2045,7 @@ func TestCELMarkerComments(t *testing.T) { assert.NoError(funcErr) assert.NoError(callErr) - assert.ElementsMatch(imports, []string{`foo "base/foo"`, `common "k8s.io/kube-openapi/pkg/common"`, `spec "k8s.io/kube-openapi/pkg/validation/spec"`, `ptr "k8s.io/utils/ptr"`}) + assert.ElementsMatch(imports, []string{`foo "base/foo"`, `common "k8s.io/kube-openapi/pkg/common"`, `spec "k8s.io/kube-openapi/pkg/validation/spec"`}) if formatted, err := format.Source(funcBuffer.Bytes()); err != nil { t.Fatalf("%v\n%v", err, string(funcBuffer.Bytes())) @@ -2059,17 +2059,7 @@ func TestCELMarkerComments(t *testing.T) { "Field": { VendorExtensible: spec.VendorExtensible{ Extensions: spec.Extensions{ - "x-kubernetes-validations": []interface{}{ - map[string]interface{}{ - "rule": "self.length() > 0", - "message": "string message", - }, - map[string]interface{}{ - "rule": "self.length() % 2 == 0", - "messageExpression": "self + ' hello'", - "optionalOldSelf": ptr.To[bool](true), - }, - }, + "x-kubernetes-validations": []interface{}{map[string]interface{}{"message": "string message", "rule": "self.length() > 0"}, map[string]interface{}{"messageExpression": "self + ' hello'", "optionalOldSelf": true, "rule": "self.length() % 2 == 0"}}, }, }, SchemaProps: spec.SchemaProps{ @@ -2082,12 +2072,7 @@ func TestCELMarkerComments(t *testing.T) { }, VendorExtensible: spec.VendorExtensible{ Extensions: spec.Extensions{ - "x-kubernetes-validations": []interface{}{ - map[string]interface{}{ - "rule": "self == oldSelf", - "message": "message1", - }, - }, + "x-kubernetes-validations": []interface{}{map[string]interface{}{"message": "message1", "rule": "self == oldSelf"}}, }, }, },