From 480f791ca34d81f52aefe7ac161849ff1e52cbfd Mon Sep 17 00:00:00 2001 From: Jonathan Jamroga Date: Wed, 7 Aug 2024 08:27:20 -0400 Subject: [PATCH] Allow certain kube markers to be ignored --- integration_test.go | 23 ++++++++ main.go | 6 ++ openapiGenerator.go | 5 +- pkg/markers/constraints.go | 4 ++ pkg/markers/register.go | 16 +++++- pkg/markers/validation.go | 22 +++++++ testdata/golden/test8/openapiv3.yaml | 47 +++++++++++++++ testdata/test8/markers.proto | 85 ++++++++++++++++++++++++++++ 8 files changed, 204 insertions(+), 4 deletions(-) create mode 100644 testdata/golden/test8/openapiv3.yaml create mode 100644 testdata/test8/markers.proto diff --git a/integration_test.go b/integration_test.go index c6dccc0..5e16731 100644 --- a/integration_test.go +++ b/integration_test.go @@ -21,11 +21,14 @@ import ( "path/filepath" "strings" "testing" + "regexp" ) const goldenDir = "testdata/golden/" func TestOpenAPIGeneration(t *testing.T) { + regexp.MustCompile("(:?kubebuilder:altName)") + testcases := []struct { name string id string @@ -109,6 +112,26 @@ func TestOpenAPIGeneration(t *testing.T) { }, wantFiles: []string{"test7/openapiv3.yaml"}, }, + { + name: "Test no markers are ignored when ignored_kube_markers is zero length", + id: "test7", + perPackage: false, + genOpts: "yaml=true,single_file=true,proto_oneof=true,int_native=true,multiline_description=true,disable_kube_markers=true,ignored_kube_markers=", + inputFiles: map[string][]string{ + "test7": {"./testdata/test7/markers.proto"}, + }, + wantFiles: []string{"test7/openapiv3.yaml"}, + }, + { + name: "Test ignored_kube_markers option ignores specific markers", + id: "test8", + perPackage: false, + genOpts: "yaml=true,single_file=true,proto_oneof=true,int_native=true,multiline_description=true,disable_kube_markers=true,ignored_kube_markers=Required", + inputFiles: map[string][]string{ + "test8": {"./testdata/test8/markers.proto"}, + }, + wantFiles: []string{"test8/openapiv3.yaml"}, + }, } for _, tc := range testcases { diff --git a/main.go b/main.go index aefe52a..86382a4 100644 --- a/main.go +++ b/main.go @@ -56,6 +56,7 @@ func generate(request pluginpb.CodeGeneratorRequest) (*pluginpb.CodeGeneratorRes disableKubeMarkers := false var messagesWithEmptySchema []string + var ignoredKubeMarkers []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_markers" { + if len(v) > 0 { + ignoredKubeMarkers = 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, + ignoredKubeMarkers, ) return g.generateOutput(filesToGen) } diff --git a/openapiGenerator.go b/openapiGenerator.go index 3dca79c..0018d69 100644 --- a/openapiGenerator.go +++ b/openapiGenerator.go @@ -121,6 +121,8 @@ 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 + + ignoredKubeMarkers []string } type DescriptionConfiguration struct { @@ -143,8 +145,9 @@ func newOpenAPIGenerator( protoOneof bool, intNative bool, disableKubeMarkers bool, + ignoredKubeMarkers []string, ) *openapiGenerator { - mRegistry, err := markers.NewRegistry() + mRegistry, err := markers.NewRegistry(ignoredKubeMarkers) if err != nil { log.Panicf("error initializing marker registry: %v", err) } diff --git a/pkg/markers/constraints.go b/pkg/markers/constraints.go index b933ae8..5a605d0 100644 --- a/pkg/markers/constraints.go +++ b/pkg/markers/constraints.go @@ -311,6 +311,10 @@ func (m Example) ApplyToSchema(o *openapi3.Schema) { o.Example = m.Value } +type AltName string + +func (a AltName) ApplyToSchema(o *openapi3.Schema) {} + // Schemaless marks a field as being a schemaless object. // // Schemaless objects are not introspected, so you must provide diff --git a/pkg/markers/register.go b/pkg/markers/register.go index 46d4bbf..505e0a3 100644 --- a/pkg/markers/register.go +++ b/pkg/markers/register.go @@ -4,6 +4,9 @@ import ( "reflect" "sigs.k8s.io/controller-tools/pkg/markers" + "regexp" + "fmt" + "strings" ) const ( @@ -17,13 +20,20 @@ type definitionWithHelp struct { } type Registry struct { - mRegistry *markers.Registry + mRegistry *markers.Registry + ignoredKubeMarkersRegex *regexp.Regexp } -func NewRegistry() (*Registry, error) { +func NewRegistry(ignoredKubeMarkers []string) (*Registry, error) { + var ignoredKubeMarkersRegexp *regexp.Regexp + if len(ignoredKubeMarkers) > 0 { + toIgnore := strings.Join(ignoredKubeMarkers, "|") + ignoredKubeMarkersRegexp = regexp.MustCompile(fmt.Sprintf("(?:%s)", toIgnore)) + } mReg := &markers.Registry{} r := &Registry{ - mRegistry: mReg, + mRegistry: mReg, + ignoredKubeMarkersRegex: ignoredKubeMarkersRegexp, } return r, Register(mReg) } diff --git a/pkg/markers/validation.go b/pkg/markers/validation.go index 05aadc3..b272fca 100644 --- a/pkg/markers/validation.go +++ b/pkg/markers/validation.go @@ -141,7 +141,12 @@ func (r *Registry) ApplyRulesToSchema( o *openapi3.Schema, target markers.TargetType, ) error { + for _, rule := range rules { + if r.isIgnoredKubeMarker(rule) { + continue + } + defn := r.mRegistry.Lookup(rule, target) if defn == nil { return fmt.Errorf("no definition found for rule: %s", rule) @@ -164,6 +169,10 @@ func (r *Registry) GetSchemaType( target markers.TargetType, ) Type { for _, rule := range rules { + if r.isIgnoredKubeMarker(rule) { + continue + } + defn := r.mRegistry.Lookup(rule, target) if defn == nil { log.Panicf("no definition found for rule: %s", rule) @@ -183,6 +192,10 @@ func (r *Registry) IsRequired( rules []string, ) bool { for _, rule := range rules { + if r.isIgnoredKubeMarker(rule) { + continue + } + defn := r.mRegistry.Lookup(rule, markers.DescribesField) if defn == nil { log.Panicf("no definition found for rule: %s", rule) @@ -191,5 +204,14 @@ func (r *Registry) IsRequired( return true } } + return false } + +func (r *Registry) isIgnoredKubeMarker(rule string) bool { + if r.ignoredKubeMarkersRegex == nil { + return false + } + + return r.ignoredKubeMarkersRegex.MatchString(rule) +} diff --git a/testdata/golden/test8/openapiv3.yaml b/testdata/golden/test8/openapiv3.yaml new file mode 100644 index 0000000..96a3fa6 --- /dev/null +++ b/testdata/golden/test8/openapiv3.yaml @@ -0,0 +1,47 @@ +components: + schemas: + test7.Msg: + description: This is a top-level message. + properties: + a: + format: int32 + type: integer + blist: + items: + type: string + type: array + nested: + properties: + a: + type: string + b: + type: string + c: + type: string + d: + type: string + defaultValue: + type: string + embedded: + type: string + intOrString: + type: string + schemaless: + description: Schemaless field + type: string + type: object + 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 +info: + title: OpenAPI Spec for Solo APIs. + version: "" +openapi: 3.0.1 +paths: null diff --git a/testdata/test8/markers.proto b/testdata/test8/markers.proto new file mode 100644 index 0000000..fe271f0 --- /dev/null +++ b/testdata/test8/markers.proto @@ -0,0 +1,85 @@ +syntax = "proto3"; + +package test7; + +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; + } +} +