diff --git a/pkg/generators/markers.go b/pkg/generators/markers.go index 7f0fe985a..a7ed201a4 100644 --- a/pkg/generators/markers.go +++ b/pkg/generators/markers.go @@ -91,9 +91,14 @@ func (c commentTags) ValidationSchema() (*spec.Schema, error) { SchemaProps: c.SchemaProps, } - if len(c.CEL) > 0 { + ccel := append([]CELTag{}, c.CEL...) + if _, exists := NameFormats[c.Format]; exists { + ccel = append([]CELTag{{Rule: "!format." + c.Format + "().validate(self).hasValue()", MessageExpression: "format." + c.Format + "().validate(self).value()"}}, ccel...) + } + + if len(ccel) > 0 { // Convert the CELTag to a map[string]interface{} via JSON - celTagJSON, err := json.Marshal(c.CEL) + celTagJSON, err := json.Marshal(ccel) if err != nil { return nil, fmt.Errorf("failed to marshal CEL tag: %w", err) } @@ -108,6 +113,44 @@ func (c commentTags) ValidationSchema() (*spec.Schema, error) { return &res, nil } +var Formats = map[string]struct{}{ + "bsonobjectid": struct{}{}, // bson object ID + "uri": struct{}{}, // an URI as parsed by Golang net/url.ParseRequestURI + "email": struct{}{}, // an email address as parsed by Golang net/mail.ParseAddress + "hostname": struct{}{}, // a valid representation for an Internet host name, as defined by RFC 1034, section 3.1 [RFC1034]. + "ipv4": struct{}{}, // an IPv4 IP as parsed by Golang net.ParseIP + "ipv6": struct{}{}, // an IPv6 IP as parsed by Golang net.ParseIP + "cidr": struct{}{}, // a CIDR as parsed by Golang net.ParseCIDR + "mac": struct{}{}, // a MAC address as parsed by Golang net.ParseMAC + "uuid": struct{}{}, // an UUID that allows uppercase defined by the regex (?i)^[0-9a-f]{8}-?[0-9a-f]{4}-?[0-9a-f]{4}-?[0-9a-f]{4}-?[0-9a-f]{12}$ + "uuid3": struct{}{}, // an UUID3 that allows uppercase defined by the regex (?i)^[0-9a-f]{8}-?[0-9a-f]{4}-?3[0-9a-f]{3}-?[0-9a-f]{4}-?[0-9a-f]{12}$ + "uuid4": struct{}{}, // an UUID4 that allows uppercase defined by the regex (?i)^[0-9a-f]{8}-?[0-9a-f]{4}-?4[0-9a-f]{3}-?[89ab][0-9a-f]{3}-?[0-9a-f]{12}$ + "uuid5": struct{}{}, // an UUID6 that allows uppercase defined by the regex (?i)^[0-9a-f]{8}-?[0-9a-f]{4}-?5[0-9a-f]{3}-?[89ab][0-9a-f]{3}-?[0-9a-f]{12}$ + "isbn": struct{}{}, // an ISBN10 or ISBN13 number string like "0321751043" or "978-0321751041" + "isbn10": struct{}{}, // an ISBN10 number string like "0321751043" + "isbn13": struct{}{}, // an ISBN13 number string like "978-0321751041" + "creditcard": struct{}{}, // a credit card number defined by the regex ^(?:4[0-9]{12}(?:[0-9]{3})?|5[1-5][0-9]{14}|6(?:011|5[0-9][0-9])[0-9]{12}|3[47][0-9]{13}|3(?:0[0-5]|[68][0-9])[0-9]{11}|(?:2131|1800|35\\d{3})\\d{11})$ with any non digit characters mixed in + "ssn": struct{}{}, // a U.S. social security number following the regex ^\\d{3}[- ]?\\d{2}[- ]?\\d{4}$ + "hexcolor": struct{}{}, // an hexadecimal color code like "#FFFFFF", following the regex ^#?([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$ + "rgbcolor": struct{}{}, // an RGB color code like rgb like "rgb(255,255,2559" + "byte": struct{}{}, // base64 encoded binary data + "password": struct{}{}, // any kind of string + "date": struct{}{}, // a date string like "2006-01-02" as defined by full-date in RFC3339 + "duration": struct{}{}, // a duration string like "22 ns" as parsed by Golang time.ParseDuration or compatible with Scala duration format + "datetime": struct{}{}, // a date time string like "2014-12-15T19:30:20.000Z" as defined by date-time in RFC3339 +} + +var NameFormats = map[string]struct{}{ + "dns1123Label": struct{}{}, + "dns1123Subdomain": struct{}{}, + "httpPath": struct{}{}, + "qualifiedName": struct{}{}, + "wildcardDNS1123Subdomain": struct{}{}, + "cIdentifier": struct{}{}, + "dns1035Label": struct{}{}, + "labelValue": struct{}{}, +} + // validates the parameters in a CommentTags instance. Returns any errors encountered. func (c commentTags) Validate() error { @@ -164,6 +207,14 @@ func (c commentTags) Validate() error { err = errors.Join(err, fmt.Errorf("invalid CEL tag at index %d: %w", i, celError)) } + if c.Format != "" { + _, ok := NameFormats[c.Format] + _, alsoOk := Formats[c.Format] + if !ok && !alsoOk { + err = errors.Join(err, fmt.Errorf("invalid nameFormat: %v", c.Format)) + } + } + return err } @@ -226,6 +277,9 @@ func (c commentTags) ValidateType(t *types.Type) error { if c.ExclusiveMaximum && !isInt && !isFloat { err = errors.Join(err, fmt.Errorf("exclusiveMaximum can only be used on numeric types")) } + if c.Format != "" && !isString { + err = errors.Join(err, fmt.Errorf("Format can only be used on string types")) + } return err } diff --git a/pkg/generators/markers_test.go b/pkg/generators/markers_test.go index 36f0ff10d..812888889 100644 --- a/pkg/generators/markers_test.go +++ b/pkg/generators/markers_test.go @@ -16,6 +16,7 @@ limitations under the License. package generators_test import ( + "fmt" "testing" "github.com/stretchr/testify/require" @@ -512,6 +513,101 @@ func TestParseCommentTags(t *testing.T) { } } +func TestFormat(t *testing.T) { + + cases := []struct { + t *types.Type + name string + comments []string + expected *spec.Schema + expectedError string + }{} + + cases = append(cases, struct { + t *types.Type + name string + comments []string + expected *spec.Schema + expectedError string + }{ + t: types.String, + name: "invalid format", + comments: []string{ + "+k8s:validation:format=5", + }, + expectedError: "failed to unmarshal marker comments: json: cannot unmarshal number into Go struct field commentTags.format of type string", + }) + + for formatName := range generators.NameFormats { + + cases = append(cases, struct { + t *types.Type + name string + comments []string + expected *spec.Schema + expectedError string + }{ + t: types.String, + name: formatName, + comments: []string{ + fmt.Sprintf("+k8s:validation:format=\"%s\"", formatName), + }, + expected: &spec.Schema{ + VendorExtensible: spec.VendorExtensible{ + Extensions: spec.Extensions{ + "x-kubernetes-validations": []interface{}{ + map[string]interface{}{ + "messageExpression": "format." + formatName + "().validate(self).value()", + "rule": "!format." + formatName + "().validate(self).hasValue()", + }, + }, + }, + }, + SchemaProps: spec.SchemaProps{ + Format: formatName, + }, + }, + }) + } + + for formatName := range generators.Formats { + + cases = append(cases, struct { + t *types.Type + name string + comments []string + expected *spec.Schema + expectedError string + }{ + t: types.String, + name: formatName, + comments: []string{ + fmt.Sprintf("+k8s:validation:format=\"%s\"", formatName), + }, + expected: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Format: formatName, + }, + }, + }) + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + actual, err := generators.ParseCommentTags(tc.t, tc.comments, "+k8s:validation:") + if tc.expectedError != "" { + require.Error(t, err) + require.Regexp(t, tc.expectedError, err.Error()) + return + } else { + require.NoError(t, err) + } + + require.Equal(t, tc.expected, actual) + }) + } +} + // Test comment tag validation function func TestCommentTags_Validate(t *testing.T) { @@ -753,6 +849,14 @@ func TestCommentTags_Validate(t *testing.T) { t: types.String, errorMessage: "", }, + { + name: "format not supported", + comments: []string{ + `+k8s:validation:format="not-supported"`, + }, + t: types.String, + errorMessage: "invalid nameFormat: not-supported", + }, } for _, tc := range testCases { diff --git a/pkg/generators/openapi.go b/pkg/generators/openapi.go index 1ffcf9094..f1595c053 100644 --- a/pkg/generators/openapi.go +++ b/pkg/generators/openapi.go @@ -401,11 +401,7 @@ func (g openAPITypeWriter) generateValueValidations(vs *spec.SchemaProps) error g.Do("MaxProperties: $.ptrTo|raw$[int64]($.spec.MaxProperties$),\n", args) } if len(vs.Pattern) > 0 { - p, err := json.Marshal(vs.Pattern) - if err != nil { - return err - } - g.Do("Pattern: $.$,\n", string(p)) + g.Do("Pattern: $.$,\n", fmt.Sprintf("%#v", vs.Pattern)) } if vs.MultipleOf != nil { g.Do("MultipleOf: $.ptrTo|raw$[float64]($.spec.MultipleOf$),\n", args) diff --git a/pkg/generators/openapi_test.go b/pkg/generators/openapi_test.go index 4ffdf6c92..75379fc98 100644 --- a/pkg/generators/openapi_test.go +++ b/pkg/generators/openapi_test.go @@ -2455,7 +2455,7 @@ func TestMarkerComments(t *testing.T) { Default: "", MinLength: ptr.To[int64](1), MaxLength: ptr.To[int64](10), - Pattern: "^foo$[0-9]+", + Pattern: ` + fmt.Sprintf("%#v", "^foo$[0-9]+") + `, Type: []string{"string"}, Format: "", }, @@ -2813,6 +2813,177 @@ func TestRequired(t *testing.T) { }) } +func TestFormatMarkerComments(t *testing.T) { + + inputFile := ` +package foo + +// +k8s:openapi-gen=true +type Blah struct { + // +k8s:validation:format="dns1123Label" + dns string + // +k8s:validation:format="dns1123Subdomain" + subdomain string + // +k8s:validation:format="httpPath" + path string + // +k8s:validation:format="qualifiedName" + qualified string + // +k8s:validation:format="wildcardDNS1123Subdomain" + wildcard string + // +k8s:validation:format="cIdentifier" + identifier string + // +k8s:validation:format="dns1035Label" + label string + // +k8s:validation:format="labelValue" + value string +} + ` + packagestest.TestAll(t, func(t *testing.T, x packagestest.Exporter) { + e := packagestest.Export(t, x, []packagestest.Module{{ + Name: "example.com/base/foo", + Files: map[string]interface{}{ + "foo.go": inputFile, + }, + }}) + defer e.Cleanup() + + callErr, funcErr, _, funcBuffer, imports := testOpenAPITypeWriter(t, e.Config) + if funcErr != nil { + t.Fatalf("Unexpected funcErr: %v", funcErr) + } + if callErr != nil { + t.Fatalf("Unexpected callErr: %v", callErr) + } + expImports := []string{ + `foo "example.com/base/foo"`, + `common "k8s.io/kube-openapi/pkg/common"`, + `spec "k8s.io/kube-openapi/pkg/validation/spec"`, + } + if !cmp.Equal(imports, expImports) { + t.Errorf("wrong imports:\n%s", cmp.Diff(expImports, imports)) + } + + if formatted, err := format.Source(funcBuffer.Bytes()); err != nil { + t.Fatalf("%v\n%v", err, string(funcBuffer.Bytes())) + } else { + formatted_expected, ree := format.Source([]byte(`func schema_examplecom_base_foo_Blah(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "dns": { + VendorExtensible: spec.VendorExtensible{ + Extensions: spec.Extensions{ + "x-kubernetes-validations": []interface{}{map[string]interface{}{"messageExpression": "format.dns1123Label().validate(self).value()", "rule": "!format.dns1123Label().validate(self).hasValue()"}}, + }, + }, + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "subdomain": { + VendorExtensible: spec.VendorExtensible{ + Extensions: spec.Extensions{ + "x-kubernetes-validations": []interface{}{map[string]interface{}{"messageExpression": "format.dns1123Subdomain().validate(self).value()", "rule": "!format.dns1123Subdomain().validate(self).hasValue()"}}, + }, + }, + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "path": { + VendorExtensible: spec.VendorExtensible{ + Extensions: spec.Extensions{ + "x-kubernetes-validations": []interface{}{map[string]interface{}{"messageExpression": "format.httpPath().validate(self).value()", "rule": "!format.httpPath().validate(self).hasValue()"}}, + }, + }, + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "qualified": { + VendorExtensible: spec.VendorExtensible{ + Extensions: spec.Extensions{ + "x-kubernetes-validations": []interface{}{map[string]interface{}{"messageExpression": "format.qualifiedName().validate(self).value()", "rule": "!format.qualifiedName().validate(self).hasValue()"}}, + }, + }, + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "wildcard": { + VendorExtensible: spec.VendorExtensible{ + Extensions: spec.Extensions{ + "x-kubernetes-validations": []interface{}{map[string]interface{}{"messageExpression": "format.wildcardDNS1123Subdomain().validate(self).value()", "rule": "!format.wildcardDNS1123Subdomain().validate(self).hasValue()"}}, + }, + }, + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "identifier": { + VendorExtensible: spec.VendorExtensible{ + Extensions: spec.Extensions{ + "x-kubernetes-validations": []interface{}{map[string]interface{}{"messageExpression": "format.cIdentifier().validate(self).value()", "rule": "!format.cIdentifier().validate(self).hasValue()"}}, + }, + }, + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "label": { + VendorExtensible: spec.VendorExtensible{ + Extensions: spec.Extensions{ + "x-kubernetes-validations": []interface{}{map[string]interface{}{"messageExpression": "format.dns1035Label().validate(self).value()", "rule": "!format.dns1035Label().validate(self).hasValue()"}}, + }, + }, + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "value": { + VendorExtensible: spec.VendorExtensible{ + Extensions: spec.Extensions{ + "x-kubernetes-validations": []interface{}{map[string]interface{}{"messageExpression": "format.labelValue().validate(self).value()", "rule": "!format.labelValue().validate(self).hasValue()"}}, + }, + }, + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + }, + Required: []string{"dns", "subdomain", "path", "qualified", "wildcard", "identifier", "label", "value"}, + }, + }, + } + } + + `)) + if ree != nil { + t.Fatal(ree) + } + assertEqual(t, string(formatted_expected), string(formatted)) + } + }) +} + func TestMarkerCommentsCustomDefsV3(t *testing.T) { inputFile := ` package foo diff --git a/test/integration/pkg/generated/openapi_generated.go b/test/integration/pkg/generated/openapi_generated.go index 993ef5815..64f33edf1 100644 --- a/test/integration/pkg/generated/openapi_generated.go +++ b/test/integration/pkg/generated/openapi_generated.go @@ -67,6 +67,7 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA "k8s.io/kube-openapi/test/integration/testdata/valuevalidation.Foo3": schema_test_integration_testdata_valuevalidation_Foo3(ref), "k8s.io/kube-openapi/test/integration/testdata/valuevalidation.Foo4": valuevalidation.Foo4{}.OpenAPIDefinition(), "k8s.io/kube-openapi/test/integration/testdata/valuevalidation.Foo5": schema_test_integration_testdata_valuevalidation_Foo5(ref), + "k8s.io/kube-openapi/test/integration/testdata/valuevalidation.NameFormats": schema_test_integration_testdata_valuevalidation_NameFormats(ref), } } @@ -1166,3 +1167,112 @@ func schema_test_integration_testdata_valuevalidation_Foo5(ref common.ReferenceC }, }) } + +func schema_test_integration_testdata_valuevalidation_NameFormats(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "dns": { + VendorExtensible: spec.VendorExtensible{ + Extensions: spec.Extensions{ + "x-kubernetes-validations": []interface{}{map[string]interface{}{"messageExpression": "format.dns1123Label().validate(self).value()", "rule": "!format.dns1123Label().validate(self).hasValue()"}}, + }, + }, + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "subdomain": { + VendorExtensible: spec.VendorExtensible{ + Extensions: spec.Extensions{ + "x-kubernetes-validations": []interface{}{map[string]interface{}{"messageExpression": "format.dns1123Subdomain().validate(self).value()", "rule": "!format.dns1123Subdomain().validate(self).hasValue()"}}, + }, + }, + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "path": { + VendorExtensible: spec.VendorExtensible{ + Extensions: spec.Extensions{ + "x-kubernetes-validations": []interface{}{map[string]interface{}{"messageExpression": "format.httpPath().validate(self).value()", "rule": "!format.httpPath().validate(self).hasValue()"}}, + }, + }, + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "qualified": { + VendorExtensible: spec.VendorExtensible{ + Extensions: spec.Extensions{ + "x-kubernetes-validations": []interface{}{map[string]interface{}{"messageExpression": "format.qualifiedName().validate(self).value()", "rule": "!format.qualifiedName().validate(self).hasValue()"}}, + }, + }, + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "wildcard": { + VendorExtensible: spec.VendorExtensible{ + Extensions: spec.Extensions{ + "x-kubernetes-validations": []interface{}{map[string]interface{}{"messageExpression": "format.wildcardDNS1123Subdomain().validate(self).value()", "rule": "!format.wildcardDNS1123Subdomain().validate(self).hasValue()"}}, + }, + }, + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "identifier": { + VendorExtensible: spec.VendorExtensible{ + Extensions: spec.Extensions{ + "x-kubernetes-validations": []interface{}{map[string]interface{}{"messageExpression": "format.cIdentifier().validate(self).value()", "rule": "!format.cIdentifier().validate(self).hasValue()"}}, + }, + }, + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "label": { + VendorExtensible: spec.VendorExtensible{ + Extensions: spec.Extensions{ + "x-kubernetes-validations": []interface{}{map[string]interface{}{"messageExpression": "format.dns1035Label().validate(self).value()", "rule": "!format.dns1035Label().validate(self).hasValue()"}}, + }, + }, + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "value": { + VendorExtensible: spec.VendorExtensible{ + Extensions: spec.Extensions{ + "x-kubernetes-validations": []interface{}{map[string]interface{}{"messageExpression": "format.labelValue().validate(self).value()", "rule": "!format.labelValue().validate(self).hasValue()"}}, + }, + }, + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + }, + Required: []string{"dns", "subdomain", "path", "qualified", "wildcard", "identifier", "label", "value"}, + }, + }, + } +} diff --git a/test/integration/testdata/golden.v2.json b/test/integration/testdata/golden.v2.json index 08f757e23..563f36480 100644 --- a/test/integration/testdata/golden.v2.json +++ b/test/integration/testdata/golden.v2.json @@ -613,12 +613,12 @@ "schemes": [ "https" ], - "operationId": "create-valuevalidation.Foo", + "operationId": "create-valuevalidation.NameFormats", "responses": { "201": { "description": "Created", "schema": { - "$ref": "#/definitions/valuevalidation.Foo" + "$ref": "#/definitions/valuevalidation.NameFormats" } }, "404": { @@ -736,6 +736,28 @@ } } } + }, + "/test/valuevalidation/nameformats": { + "get": { + "produces": [ + "application/json" + ], + "schemes": [ + "https" + ], + "operationId": "get-valuevalidation.NameFormats", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/valuevalidation.NameFormats" + } + }, + "404": { + "$ref": "#/responses/NotFound" + } + } + } } }, "definitions": { @@ -1326,6 +1348,101 @@ "rule": "self == oldSelf" } ] + }, + "valuevalidation.NameFormats": { + "type": "object", + "required": [ + "dns", + "subdomain", + "path", + "qualified", + "wildcard", + "identifier", + "label", + "value" + ], + "properties": { + "dns": { + "type": "string", + "default": "", + "x-kubernetes-validations": [ + { + "messageExpression": "format.dns1123Label().validate(self).value()", + "rule": "!format.dns1123Label().validate(self).hasValue()" + } + ] + }, + "identifier": { + "type": "string", + "default": "", + "x-kubernetes-validations": [ + { + "messageExpression": "format.cIdentifier().validate(self).value()", + "rule": "!format.cIdentifier().validate(self).hasValue()" + } + ] + }, + "label": { + "type": "string", + "default": "", + "x-kubernetes-validations": [ + { + "messageExpression": "format.dns1035Label().validate(self).value()", + "rule": "!format.dns1035Label().validate(self).hasValue()" + } + ] + }, + "path": { + "type": "string", + "default": "", + "x-kubernetes-validations": [ + { + "messageExpression": "format.httpPath().validate(self).value()", + "rule": "!format.httpPath().validate(self).hasValue()" + } + ] + }, + "qualified": { + "type": "string", + "default": "", + "x-kubernetes-validations": [ + { + "messageExpression": "format.qualifiedName().validate(self).value()", + "rule": "!format.qualifiedName().validate(self).hasValue()" + } + ] + }, + "subdomain": { + "type": "string", + "default": "", + "x-kubernetes-validations": [ + { + "messageExpression": "format.dns1123Subdomain().validate(self).value()", + "rule": "!format.dns1123Subdomain().validate(self).hasValue()" + } + ] + }, + "value": { + "type": "string", + "default": "", + "x-kubernetes-validations": [ + { + "messageExpression": "format.labelValue().validate(self).value()", + "rule": "!format.labelValue().validate(self).hasValue()" + } + ] + }, + "wildcard": { + "type": "string", + "default": "", + "x-kubernetes-validations": [ + { + "messageExpression": "format.wildcardDNS1123Subdomain().validate(self).value()", + "rule": "!format.wildcardDNS1123Subdomain().validate(self).hasValue()" + } + ] + } + } } }, "responses": { diff --git a/test/integration/testdata/golden.v2.report b/test/integration/testdata/golden.v2.report index bc1a71217..85e7eecf0 100644 --- a/test/integration/testdata/golden.v2.report +++ b/test/integration/testdata/golden.v2.report @@ -38,4 +38,12 @@ API rule violation: names_match,k8s.io/kube-openapi/test/integration/testdata/va API rule violation: names_match,k8s.io/kube-openapi/test/integration/testdata/valuevalidation,Foo,MapValue API rule violation: names_match,k8s.io/kube-openapi/test/integration/testdata/valuevalidation,Foo,NumberValue API rule violation: names_match,k8s.io/kube-openapi/test/integration/testdata/valuevalidation,Foo,StringValue +API rule violation: names_match,k8s.io/kube-openapi/test/integration/testdata/valuevalidation,NameFormats,dns +API rule violation: names_match,k8s.io/kube-openapi/test/integration/testdata/valuevalidation,NameFormats,identifier +API rule violation: names_match,k8s.io/kube-openapi/test/integration/testdata/valuevalidation,NameFormats,label +API rule violation: names_match,k8s.io/kube-openapi/test/integration/testdata/valuevalidation,NameFormats,path +API rule violation: names_match,k8s.io/kube-openapi/test/integration/testdata/valuevalidation,NameFormats,qualified +API rule violation: names_match,k8s.io/kube-openapi/test/integration/testdata/valuevalidation,NameFormats,subdomain +API rule violation: names_match,k8s.io/kube-openapi/test/integration/testdata/valuevalidation,NameFormats,value +API rule violation: names_match,k8s.io/kube-openapi/test/integration/testdata/valuevalidation,NameFormats,wildcard API rule violation: omitempty_match_case,k8s.io/kube-openapi/test/integration/testdata/listtype,Item,C diff --git a/test/integration/testdata/golden.v3.json b/test/integration/testdata/golden.v3.json index 7a3b832a8..4fd367ce6 100644 --- a/test/integration/testdata/golden.v3.json +++ b/test/integration/testdata/golden.v3.json @@ -557,14 +557,14 @@ }, "/test/valuevalidation": { "post": { - "operationId": "create-valuevalidation.Foo", + "operationId": "create-valuevalidation.NameFormats", "responses": { "201": { "description": "Created", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/valuevalidation.Foo" + "$ref": "#/components/schemas/valuevalidation.NameFormats" } } } @@ -674,6 +674,26 @@ } } } + }, + "/test/valuevalidation/nameformats": { + "get": { + "operationId": "get-valuevalidation.NameFormats", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/valuevalidation.NameFormats" + } + } + } + }, + "404": { + "$ref": "#/components/responses/NotFound" + } + } + } } }, "components": { @@ -1277,6 +1297,101 @@ }, "valuevalidation.Foo5": { "type": "object" + }, + "valuevalidation.NameFormats": { + "type": "object", + "required": [ + "dns", + "subdomain", + "path", + "qualified", + "wildcard", + "identifier", + "label", + "value" + ], + "properties": { + "dns": { + "type": "string", + "default": "", + "x-kubernetes-validations": [ + { + "messageExpression": "format.dns1123Label().validate(self).value()", + "rule": "!format.dns1123Label().validate(self).hasValue()" + } + ] + }, + "identifier": { + "type": "string", + "default": "", + "x-kubernetes-validations": [ + { + "messageExpression": "format.cIdentifier().validate(self).value()", + "rule": "!format.cIdentifier().validate(self).hasValue()" + } + ] + }, + "label": { + "type": "string", + "default": "", + "x-kubernetes-validations": [ + { + "messageExpression": "format.dns1035Label().validate(self).value()", + "rule": "!format.dns1035Label().validate(self).hasValue()" + } + ] + }, + "path": { + "type": "string", + "default": "", + "x-kubernetes-validations": [ + { + "messageExpression": "format.httpPath().validate(self).value()", + "rule": "!format.httpPath().validate(self).hasValue()" + } + ] + }, + "qualified": { + "type": "string", + "default": "", + "x-kubernetes-validations": [ + { + "messageExpression": "format.qualifiedName().validate(self).value()", + "rule": "!format.qualifiedName().validate(self).hasValue()" + } + ] + }, + "subdomain": { + "type": "string", + "default": "", + "x-kubernetes-validations": [ + { + "messageExpression": "format.dns1123Subdomain().validate(self).value()", + "rule": "!format.dns1123Subdomain().validate(self).hasValue()" + } + ] + }, + "value": { + "type": "string", + "default": "", + "x-kubernetes-validations": [ + { + "messageExpression": "format.labelValue().validate(self).value()", + "rule": "!format.labelValue().validate(self).hasValue()" + } + ] + }, + "wildcard": { + "type": "string", + "default": "", + "x-kubernetes-validations": [ + { + "messageExpression": "format.wildcardDNS1123Subdomain().validate(self).value()", + "rule": "!format.wildcardDNS1123Subdomain().validate(self).hasValue()" + } + ] + } + } } }, "responses": { diff --git a/test/integration/testdata/valuevalidation/nameformats.go b/test/integration/testdata/valuevalidation/nameformats.go new file mode 100644 index 000000000..8a89002de --- /dev/null +++ b/test/integration/testdata/valuevalidation/nameformats.go @@ -0,0 +1,25 @@ +package valuevalidation + +// Dummy type to test the openapi-gen API rule checker. +// The API rule violations are in format of: +// -> +k8s:validation:[validation rule]=[value] + +// +k8s:openapi-gen=true +type NameFormats struct { + // +k8s:validation:format="dns1123Label" + dns string + // +k8s:validation:format="dns1123Subdomain" + subdomain string + // +k8s:validation:format="httpPath" + path string + // +k8s:validation:format="qualifiedName" + qualified string + // +k8s:validation:format="wildcardDNS1123Subdomain" + wildcard string + // +k8s:validation:format="cIdentifier" + identifier string + // +k8s:validation:format="dns1035Label" + label string + // +k8s:validation:format="labelValue" + value string +} diff --git a/test/integration/testutil/testutil.go b/test/integration/testutil/testutil.go index 38e9eafdf..cfa8d6902 100644 --- a/test/integration/testutil/testutil.go +++ b/test/integration/testutil/testutil.go @@ -108,6 +108,7 @@ func CreateWebServices(includeV2SchemaAnnotation bool) []*restful.WebService { addRoutes(w, buildRouteForType(w, "structtype", "DeclaredAtomicStruct")...) addRoutes(w, buildRouteForType(w, "defaults", "Defaulted")...) addRoutes(w, buildRouteForType(w, "valuevalidation", "Foo")...) + addRoutes(w, buildRouteForType(w, "valuevalidation", "NameFormats")...) return []*restful.WebService{w} }