diff --git a/README.md b/README.md index 9c4f004..1e76738 100644 --- a/README.md +++ b/README.md @@ -76,3 +76,6 @@ Other supported options are: * when set to `true`, the native openapi schemas will be used for Integer types instead of Solo wrappers that add Kubernetes extension headers to the schema to treat int as strings. * `disable_kube_markers` * when set to `true`, kubebuilder markers and validations such as PreserveUnknownFields, MinItems, default, and all CEL rules will be omitted from the OpenAPI schema. The Type and Required markers will be maintained. +* `ignored_kube_marker_substrings` + * when set, this list of substrings will be used to identify kubebuilder markers to ignore. When multiple are + supplied, this will function as a logical OR i.e. any rule which contains a provided substring will be ignored \ No newline at end of file diff --git a/changelog/v0.2.5/allow_ignored_kube_markers.yaml b/changelog/v0.2.5/allow_ignored_kube_markers.yaml new file mode 100644 index 0000000..b7f6621 --- /dev/null +++ b/changelog/v0.2.5/allow_ignored_kube_markers.yaml @@ -0,0 +1,7 @@ +changelog: + - type: NEW_FEATURE + issueLink: https://github.com/solo-io/gloo-mesh-enterprise/issues/18119 + resolvesIssue: false + description: | + Allows the user to define one or more kube markers to ignore. This is useful when using protos that contain + unsupported kubebuilder decorators. \ No newline at end of file diff --git a/integration_test.go b/integration_test.go index c6dccc0..6c9e637 100644 --- a/integration_test.go +++ b/integration_test.go @@ -109,6 +109,36 @@ func TestOpenAPIGeneration(t *testing.T) { }, wantFiles: []string{"test7/openapiv3.yaml"}, }, + { + name: "Test no markers are ignored when ignored_kube_markers is zero length", + id: "test8", + perPackage: false, + genOpts: "yaml=true,single_file=true,proto_oneof=true,int_native=true,multiline_description=true,disable_kube_markers=false,ignored_kube_marker_substrings=", + inputFiles: map[string][]string{ + "test8": {"./testdata/test8/markers.proto"}, + }, + wantFiles: []string{"test8/openapiv3.yaml"}, + }, + { + name: "Test ignored_kube_markers option ignores a single marker", + id: "test9", + perPackage: false, + genOpts: "yaml=true,single_file=true,proto_oneof=true,int_native=true,multiline_description=true,disable_kube_markers=false,ignored_kube_marker_substrings=Required", + inputFiles: map[string][]string{ + "test9": {"./testdata/test9/markers.proto"}, + }, + wantFiles: []string{"test9/openapiv3.yaml"}, + }, + { + name: "Test ignored_kube_markers option ignores multiple markers", + id: "test10", + perPackage: false, + genOpts: "yaml=true,single_file=true,proto_oneof=true,int_native=true,multiline_description=true,disable_kube_markers=false,ignored_kube_marker_substrings=Required+example", + inputFiles: map[string][]string{ + "test10": {"./testdata/test10/markers.proto"}, + }, + wantFiles: []string{"test10/openapiv3.yaml"}, + }, } for _, tc := range testcases { diff --git a/main.go b/main.go index aefe52a..fed1349 100644 --- a/main.go +++ b/main.go @@ -56,6 +56,7 @@ func generate(request pluginpb.CodeGeneratorRequest) (*pluginpb.CodeGeneratorRes disableKubeMarkers := false var messagesWithEmptySchema []string + var ignoredKubeMarkerSubstrings []string p := extractParams(request.GetParameter()) for k, v := range p { @@ -147,6 +148,10 @@ func generate(request pluginpb.CodeGeneratorRequest) (*pluginpb.CodeGeneratorRes default: return nil, fmt.Errorf("unknown value '%s' for disable_kube_markers", v) } + } else if k == "ignored_kube_marker_substrings" { + if len(v) > 0 { + ignoredKubeMarkerSubstrings = strings.Split(v, "+") + } } else { return nil, fmt.Errorf("unknown argument '%s' specified", k) } @@ -184,6 +189,7 @@ func generate(request pluginpb.CodeGeneratorRequest) (*pluginpb.CodeGeneratorRes protoOneof, intNative, disableKubeMarkers, + ignoredKubeMarkerSubstrings, ) return g.generateOutput(filesToGen) } diff --git a/openapiGenerator.go b/openapiGenerator.go index 3dca79c..697bd15 100644 --- a/openapiGenerator.go +++ b/openapiGenerator.go @@ -33,6 +33,7 @@ import ( "github.com/solo-io/protoc-gen-openapi/pkg/markers" "github.com/solo-io/protoc-gen-openapi/pkg/protomodel" + "regexp" ) var descriptionExclusionMarkers = []string{"$hide_from_docs", "$hide", "@exclude"} @@ -121,6 +122,10 @@ type openapiGenerator struct { // If set to true, kubebuilder markers and validations such as PreserveUnknownFields, MinItems, default, and all CEL rules will be omitted from the OpenAPI schema. // The Type and Required markers will be maintained. disableKubeMarkers bool + + // when set, this list of substrings will be used to identify kubebuilder markers to ignore. When multiple are + // supplied, this will function as a logical OR i.e. any rule which contains a provided substring will be ignored + ignoredKubeMarkerSubstrings []string } type DescriptionConfiguration struct { @@ -143,25 +148,26 @@ func newOpenAPIGenerator( protoOneof bool, intNative bool, disableKubeMarkers bool, + ignoredKubeMarkers []string, ) *openapiGenerator { mRegistry, err := markers.NewRegistry() if err != nil { log.Panicf("error initializing marker registry: %v", err) } - return &openapiGenerator{ - model: model, - perFile: perFile, - singleFile: singleFile, - yaml: yaml, - useRef: useRef, - descriptionConfiguration: descriptionConfiguration, - enumAsIntOrString: enumAsIntOrString, - customSchemasByMessageName: buildCustomSchemasByMessageName(messagesWithEmptySchema), - protoOneof: protoOneof, - intNative: intNative, - markerRegistry: mRegistry, - disableKubeMarkers: disableKubeMarkers, + model: model, + perFile: perFile, + singleFile: singleFile, + yaml: yaml, + useRef: useRef, + descriptionConfiguration: descriptionConfiguration, + enumAsIntOrString: enumAsIntOrString, + customSchemasByMessageName: buildCustomSchemasByMessageName(messagesWithEmptySchema), + protoOneof: protoOneof, + intNative: intNative, + markerRegistry: mRegistry, + disableKubeMarkers: disableKubeMarkers, + ignoredKubeMarkerSubstrings: ignoredKubeMarkers, } } @@ -663,6 +669,13 @@ func (g *openapiGenerator) parseComments(desc protomodel.CoreDesc) (comments str c := strings.TrimSpace(desc.Location().GetLeadingComments()) blocks := strings.Split(c, "\n\n") + var ignoredKubeMarkersRegexp *regexp.Regexp + if len(g.ignoredKubeMarkerSubstrings) > 0 { + ignoredKubeMarkersRegexp = regexp.MustCompile( + fmt.Sprintf("(?:%s)", strings.Join(g.ignoredKubeMarkerSubstrings, "|")), + ) + } + var sb strings.Builder for i, block := range blocks { if shouldNotRenderDesc(strings.TrimSpace(block)) { @@ -681,7 +694,12 @@ func (g *openapiGenerator) parseComments(desc protomodel.CoreDesc) (comments str if shouldNotRenderDesc(l) { continue } + if strings.HasPrefix(l, markers.Kubebuilder) { + if isIgnoredKubeMarker(ignoredKubeMarkersRegexp, l) { + continue + } + validationRules = append(validationRules, l) continue } @@ -813,3 +831,11 @@ func (g *openapiGenerator) relativeName(desc protomodel.CoreDesc) string { return desc.PackageDesc().Name + "." + typeName } + +func isIgnoredKubeMarker(regexp *regexp.Regexp, l string) bool { + if regexp == nil { + return false + } + + return regexp.MatchString(l) +} diff --git a/testdata/golden/test10/openapiv3.yaml b/testdata/golden/test10/openapiv3.yaml new file mode 100644 index 0000000..cc6146a --- /dev/null +++ b/testdata/golden/test10/openapiv3.yaml @@ -0,0 +1,73 @@ +components: + schemas: + test10.Msg: + description: This is a top-level message. + properties: + a: + exclusiveMaximum: true + exclusiveMinimum: true + format: int32 + maximum: 100 + minimum: 5 + multipleOf: 2 + type: integer + x-kubernetes-validations: + - message: must not equal 27 + rule: self != 27 + blist: + items: + type: string + maxItems: 5 + minItems: 1 + type: array + uniqueItems: true + nested: + maxProperties: 2 + minProperties: 1 + properties: + a: + pattern: ^[a-zA-Z0-9_]*$ + type: string + b: + enum: + - Allow + - Forbid + - Replace + type: string + c: + maxLength: 100 + minLength: 1 + type: string + d: + format: date-time + type: string + defaultValue: + default: forty-two + type: string + embedded: + nullable: true + type: string + x-kubernetes-embedded-resource: true + intOrString: + type: string + x-kubernetes-int-or-string: true + schemaless: + description: Schemaless field + type: object + x-kubernetes-preserve-unknown-fields: true + object: + description: Should maintain valid Type marker and not enumerate subfields. + type: object + x-kubernetes-preserve-unknown-fields: true + recursive: + type: object + x-kubernetes-preserve-unknown-fields: true + val: + x-kubernetes-preserve-unknown-fields: true + type: object + x-kubernetes-preserve-unknown-fields: true +info: + title: OpenAPI Spec for Solo APIs. + version: "" +openapi: 3.0.1 +paths: null diff --git a/testdata/golden/test8/openapiv3.yaml b/testdata/golden/test8/openapiv3.yaml new file mode 100644 index 0000000..a072b99 --- /dev/null +++ b/testdata/golden/test8/openapiv3.yaml @@ -0,0 +1,77 @@ +components: + schemas: + test8.Msg: + description: This is a top-level message. + properties: + a: + exclusiveMaximum: true + exclusiveMinimum: true + format: int32 + maximum: 100 + minimum: 5 + multipleOf: 2 + type: integer + x-kubernetes-validations: + - message: must not equal 27 + rule: self != 27 + blist: + items: + type: string + maxItems: 5 + minItems: 1 + type: array + uniqueItems: true + nested: + maxProperties: 2 + minProperties: 1 + properties: + a: + pattern: ^[a-zA-Z0-9_]*$ + type: string + b: + enum: + - Allow + - Forbid + - Replace + type: string + c: + maxLength: 100 + minLength: 1 + type: string + d: + format: date-time + type: string + defaultValue: + default: forty-two + example: forty-two + type: string + embedded: + nullable: true + type: string + x-kubernetes-embedded-resource: true + intOrString: + type: string + x-kubernetes-int-or-string: true + schemaless: + description: Schemaless field + required: + - a + - b + type: object + x-kubernetes-preserve-unknown-fields: true + object: + description: Should maintain valid Type marker and not enumerate subfields. + type: object + x-kubernetes-preserve-unknown-fields: true + recursive: + type: object + x-kubernetes-preserve-unknown-fields: true + val: + x-kubernetes-preserve-unknown-fields: true + type: object + x-kubernetes-preserve-unknown-fields: true +info: + title: OpenAPI Spec for Solo APIs. + version: "" +openapi: 3.0.1 +paths: null diff --git a/testdata/golden/test9/openapiv3.yaml b/testdata/golden/test9/openapiv3.yaml new file mode 100644 index 0000000..9d5f2d1 --- /dev/null +++ b/testdata/golden/test9/openapiv3.yaml @@ -0,0 +1,74 @@ +components: + schemas: + test9.Msg: + description: This is a top-level message. + properties: + a: + exclusiveMaximum: true + exclusiveMinimum: true + format: int32 + maximum: 100 + minimum: 5 + multipleOf: 2 + type: integer + x-kubernetes-validations: + - message: must not equal 27 + rule: self != 27 + blist: + items: + type: string + maxItems: 5 + minItems: 1 + type: array + uniqueItems: true + nested: + maxProperties: 2 + minProperties: 1 + properties: + a: + pattern: ^[a-zA-Z0-9_]*$ + type: string + b: + enum: + - Allow + - Forbid + - Replace + type: string + c: + maxLength: 100 + minLength: 1 + type: string + d: + format: date-time + type: string + defaultValue: + default: forty-two + example: forty-two + type: string + embedded: + nullable: true + type: string + x-kubernetes-embedded-resource: true + intOrString: + type: string + x-kubernetes-int-or-string: true + schemaless: + description: Schemaless field + type: object + x-kubernetes-preserve-unknown-fields: true + object: + description: Should maintain valid Type marker and not enumerate subfields. + type: object + x-kubernetes-preserve-unknown-fields: true + recursive: + type: object + x-kubernetes-preserve-unknown-fields: true + val: + x-kubernetes-preserve-unknown-fields: true + type: object + x-kubernetes-preserve-unknown-fields: true +info: + title: OpenAPI Spec for Solo APIs. + version: "" +openapi: 3.0.1 +paths: null diff --git a/testdata/test10/markers.proto b/testdata/test10/markers.proto new file mode 100644 index 0000000..353a9fb --- /dev/null +++ b/testdata/test10/markers.proto @@ -0,0 +1,85 @@ +syntax = "proto3"; + +package test10; + +import "struct.proto"; + +// This is a top-level message. +// +// +kubebuilder:pruning:PreserveUnknownFields +message Msg { + // +kubebuilder:pruning:PreserveUnknownFields + Nested nested = 1; + + // +kubebuilder:validation:Maximum=100 + // +kubebuilder:validation:Minimum=5 + // +kubebuilder:validation:ExclusiveMaximum=true + // +kubebuilder:validation:ExclusiveMinimum=true + // +kubebuilder:validation:MultipleOf=2 + // +kubebuilder:validation:XValidation:rule="self != 27",message="must not equal 27" + int32 a = 2; + + // +kubebuilder:validation:MinItems=1 + // +kubebuilder:validation:MaxItems=5 + // +kubebuilder:validation:UniqueItems=true + repeated string blist = 3; + + // +kubebuilder:validation:Type=value + google.protobuf.Value val = 4; + + // Should maintain valid Type marker and not enumerate subfields. + // + // +kubebuilder:validation:Type=object + Nested2 object = 5; + + // +kubebuilder:validation:Type=object + Recursive recursive = 6; + + // This is a nested message. + // + // +kubebuilder:validation:MinProperties=1 + // +kubebuilder:validation:MaxProperties=2 + message Nested { + // +kubebuilder:validation:Pattern="^[a-zA-Z0-9_]*$" + // +kubebuilder:validation:Required + string a = 1; + + // +kubebuilder:validation:Enum=Allow;Forbid;Replace + // +kubebuilder:validation:Required + string b = 2; + + // +kubebuilder:validation:MaxLength=100 + // +kubebuilder:validation:MinLength=1 + string c = 3; + + // +kubebuilder:validation:Format=date-time + string d = 4; + + // +kubebuilder:validation:XIntOrString + string int_or_string = 5; + + // +kubebuilder:default=forty-two + // +kubebuilder:example=forty-two + string default_value = 6; + + // Schemaless field + // + // +kubebuilder:validation:Schemaless + string schemaless = 7; + + // +kubebuilder:validation:EmbeddedResource + // +kubebuilder:validation:Nullable + string embedded = 8; + } + + message Nested2 { + string a = 1; + string b = 2; + int32 c = 3; + } + + message Recursive { + Recursive r = 1; + } +} + diff --git a/testdata/test8/markers.proto b/testdata/test8/markers.proto new file mode 100644 index 0000000..9053939 --- /dev/null +++ b/testdata/test8/markers.proto @@ -0,0 +1,85 @@ +syntax = "proto3"; + +package test8; + +import "struct.proto"; + +// This is a top-level message. +// +// +kubebuilder:pruning:PreserveUnknownFields +message Msg { + // +kubebuilder:pruning:PreserveUnknownFields + Nested nested = 1; + + // +kubebuilder:validation:Maximum=100 + // +kubebuilder:validation:Minimum=5 + // +kubebuilder:validation:ExclusiveMaximum=true + // +kubebuilder:validation:ExclusiveMinimum=true + // +kubebuilder:validation:MultipleOf=2 + // +kubebuilder:validation:XValidation:rule="self != 27",message="must not equal 27" + int32 a = 2; + + // +kubebuilder:validation:MinItems=1 + // +kubebuilder:validation:MaxItems=5 + // +kubebuilder:validation:UniqueItems=true + repeated string blist = 3; + + // +kubebuilder:validation:Type=value + google.protobuf.Value val = 4; + + // Should maintain valid Type marker and not enumerate subfields. + // + // +kubebuilder:validation:Type=object + Nested2 object = 5; + + // +kubebuilder:validation:Type=object + Recursive recursive = 6; + + // This is a nested message. + // + // +kubebuilder:validation:MinProperties=1 + // +kubebuilder:validation:MaxProperties=2 + message Nested { + // +kubebuilder:validation:Pattern="^[a-zA-Z0-9_]*$" + // +kubebuilder:validation:Required + string a = 1; + + // +kubebuilder:validation:Enum=Allow;Forbid;Replace + // +kubebuilder:validation:Required + string b = 2; + + // +kubebuilder:validation:MaxLength=100 + // +kubebuilder:validation:MinLength=1 + string c = 3; + + // +kubebuilder:validation:Format=date-time + string d = 4; + + // +kubebuilder:validation:XIntOrString + string int_or_string = 5; + + // +kubebuilder:default=forty-two + // +kubebuilder:example=forty-two + string default_value = 6; + + // Schemaless field + // + // +kubebuilder:validation:Schemaless + string schemaless = 7; + + // +kubebuilder:validation:EmbeddedResource + // +kubebuilder:validation:Nullable + string embedded = 8; + } + + message Nested2 { + string a = 1; + string b = 2; + int32 c = 3; + } + + message Recursive { + Recursive r = 1; + } +} + diff --git a/testdata/test9/markers.proto b/testdata/test9/markers.proto new file mode 100644 index 0000000..3855ad1 --- /dev/null +++ b/testdata/test9/markers.proto @@ -0,0 +1,85 @@ +syntax = "proto3"; + +package test9; + +import "struct.proto"; + +// This is a top-level message. +// +// +kubebuilder:pruning:PreserveUnknownFields +message Msg { + // +kubebuilder:pruning:PreserveUnknownFields + Nested nested = 1; + + // +kubebuilder:validation:Maximum=100 + // +kubebuilder:validation:Minimum=5 + // +kubebuilder:validation:ExclusiveMaximum=true + // +kubebuilder:validation:ExclusiveMinimum=true + // +kubebuilder:validation:MultipleOf=2 + // +kubebuilder:validation:XValidation:rule="self != 27",message="must not equal 27" + int32 a = 2; + + // +kubebuilder:validation:MinItems=1 + // +kubebuilder:validation:MaxItems=5 + // +kubebuilder:validation:UniqueItems=true + repeated string blist = 3; + + // +kubebuilder:validation:Type=value + google.protobuf.Value val = 4; + + // Should maintain valid Type marker and not enumerate subfields. + // + // +kubebuilder:validation:Type=object + Nested2 object = 5; + + // +kubebuilder:validation:Type=object + Recursive recursive = 6; + + // This is a nested message. + // + // +kubebuilder:validation:MinProperties=1 + // +kubebuilder:validation:MaxProperties=2 + message Nested { + // +kubebuilder:validation:Pattern="^[a-zA-Z0-9_]*$" + // +kubebuilder:validation:Required + string a = 1; + + // +kubebuilder:validation:Enum=Allow;Forbid;Replace + // +kubebuilder:validation:Required + string b = 2; + + // +kubebuilder:validation:MaxLength=100 + // +kubebuilder:validation:MinLength=1 + string c = 3; + + // +kubebuilder:validation:Format=date-time + string d = 4; + + // +kubebuilder:validation:XIntOrString + string int_or_string = 5; + + // +kubebuilder:default=forty-two + // +kubebuilder:example=forty-two + string default_value = 6; + + // Schemaless field + // + // +kubebuilder:validation:Schemaless + string schemaless = 7; + + // +kubebuilder:validation:EmbeddedResource + // +kubebuilder:validation:Nullable + string embedded = 8; + } + + message Nested2 { + string a = 1; + string b = 2; + int32 c = 3; + } + + message Recursive { + Recursive r = 1; + } +} +