From 1dbbfa0638218597c37c805b5c2705db48d765da Mon Sep 17 00:00:00 2001 From: Joe Betz Date: Fri, 6 Dec 2024 21:00:25 -0500 Subject: [PATCH] Ignore unknown k8s:validation comments tags --- pkg/generators/markers.go | 42 +++++++++++++++++++++++++++++++--- pkg/generators/markers_test.go | 22 +++++++++++++++++- pkg/generators/openapi_test.go | 3 ++- 3 files changed, 62 insertions(+), 5 deletions(-) diff --git a/pkg/generators/markers.go b/pkg/generators/markers.go index c4dd67d3b..00627e9e6 100644 --- a/pkg/generators/markers.go +++ b/pkg/generators/markers.go @@ -20,9 +20,11 @@ import ( "encoding/json" "errors" "fmt" + "reflect" "regexp" "strconv" "strings" + "sync" "k8s.io/gengo/v2/types" openapi "k8s.io/kube-openapi/pkg/common" @@ -61,6 +63,26 @@ func (c *CELTag) Validate() error { return nil } +func isKnownTagCommentKey(key string) bool { + commentTag, _, _ := strings.Cut(key, ":") + _, ok := tagKeys()[commentTag] + return ok +} + +var tagKeys = sync.OnceValue(func() map[string]struct{} { + result := map[string]struct{}{} + t := reflect.TypeOf(commentTags{}) + for i := 0; i < t.NumField(); i++ { + field := t.Field(i) + if jsonTag := field.Tag.Get("json"); jsonTag != "" { + if key, _, _ := strings.Cut(jsonTag, ","); key != "" { + result[key] = struct{}{} + } + } + } + return result +}) + // 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 @@ -385,13 +407,24 @@ func memberWithJSONName(t *types.Type, key string) *types.Member { return nil } +type ParseCommentTagsOptions struct { + IgnoreUnknown bool +} + // Parses the given comments into a CommentTags type. Validates the parsed comment tags, and returns the result. // 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) (*spec.Schema, error) { +func ParseCommentTags(t *types.Type, comments []string, prefix string, opts ...ParseCommentTagsOptions) (*spec.Schema, error) { + options := ParseCommentTagsOptions{} + if len(opts) > 0 { + options = opts[0] + if len(opts) > 1 { + return nil, fmt.Errorf("only one option is allowed") + } + } - markers, err := parseMarkers(comments, prefix) + markers, err := parseMarkers(comments, prefix, options.IgnoreUnknown) if err != nil { return nil, fmt.Errorf("failed to parse marker comments: %w", err) } @@ -597,7 +630,7 @@ func extractCommentTags(marker string, lines []string) (map[string]string, error // Accepts a prefix to filter out markers not related to validation. // The prefix is removed from the key in the returned map. // Empty keys and invalid values will return errors, refs are currently unsupported and will be skipped. -func parseMarkers(markerComments []string, prefix string) (map[string]any, error) { +func parseMarkers(markerComments []string, prefix string, ignoreUnknown bool) (map[string]any, error) { markers, err := extractCommentTags(prefix, markerComments) if err != nil { return nil, err @@ -606,6 +639,9 @@ func parseMarkers(markerComments []string, prefix string) (map[string]any, error // Parse the values as JSON result := map[string]any{} for key, value := range markers { + if ignoreUnknown && !isKnownTagCommentKey(key) { + continue + } var unmarshalled interface{} if len(key) == 0 { diff --git a/pkg/generators/markers_test.go b/pkg/generators/markers_test.go index 28f6e551a..49d1ca329 100644 --- a/pkg/generators/markers_test.go +++ b/pkg/generators/markers_test.go @@ -19,6 +19,7 @@ import ( "testing" "github.com/stretchr/testify/require" + "k8s.io/gengo/v2/types" "k8s.io/kube-openapi/pkg/generators" "k8s.io/kube-openapi/pkg/validation/spec" @@ -626,6 +627,7 @@ func TestCommentTags_Validate(t *testing.T) { comments []string t *types.Type errorMessage string + options generators.ParseCommentTagsOptions }{ { name: "invalid minimum type", @@ -960,11 +962,29 @@ func TestCommentTags_Validate(t *testing.T) { }, errorMessage: `failed to validate property "name": pattern can only be used on string types`, }, + { + name: "ignore unknown field with unparsable value", + comments: []string{ + `+k8s:validation:xyz=a=b`, // a=b is not a valid value + }, + t: &types.Type{ + Kind: types.Struct, + Name: types.Name{Name: "struct"}, + Members: []types.Member{ + { + Name: "name", + Type: types.String, + Tags: `json:"name"`, + }, + }, + }, + options: generators.ParseCommentTagsOptions{IgnoreUnknown: true}, + }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - _, err := generators.ParseCommentTags(tc.t, tc.comments, "+k8s:validation:") + _, err := generators.ParseCommentTags(tc.t, tc.comments, "+k8s:validation:", tc.options) if tc.errorMessage != "" { require.Error(t, err) require.Equal(t, "invalid marker comments: "+tc.errorMessage, err.Error()) diff --git a/pkg/generators/openapi_test.go b/pkg/generators/openapi_test.go index e53292129..55be4c557 100644 --- a/pkg/generators/openapi_test.go +++ b/pkg/generators/openapi_test.go @@ -27,6 +27,7 @@ import ( "github.com/google/go-cmp/cmp" "golang.org/x/tools/go/packages" "golang.org/x/tools/go/packages/packagestest" + "k8s.io/gengo/v2/generator" "k8s.io/gengo/v2/namer" "k8s.io/gengo/v2/parser" @@ -2386,7 +2387,7 @@ func TestMarkerComments(t *testing.T) { // +k8s:validation:pattern="^foo$[0-9]+" StringValue string - // +k8s:validation:maxitems=10 + // +k8s:validation:maxItems=10 // +k8s:validation:minItems=1 // +k8s:validation:uniqueItems ArrayValue []string