Skip to content

Commit

Permalink
add marker comment support for annotating cluster scopedness
Browse files Browse the repository at this point in the history
  • Loading branch information
alexzielenski committed Apr 12, 2024
1 parent dc4e619 commit a5e08fc
Show file tree
Hide file tree
Showing 6 changed files with 85 additions and 4 deletions.
39 changes: 36 additions & 3 deletions pkg/generators/markers.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,13 @@ func (c *CELTag) Validate() error {
return nil
}

type KindScope string

var (
KindScopeNamespaced KindScope = "Namespaced"
KindScopeCluster KindScope = "Cluster"
)

// 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
Expand All @@ -77,7 +84,8 @@ func (c *CELTag) Validate() error {
type commentTags struct {
spec.SchemaProps

CEL []CELTag `json:"cel,omitempty"`
CEL []CELTag `json:"cel,omitempty"`
Scope *KindScope `json:"scope,omitempty"`

// Future markers can all be parsed into this centralized struct...
// Optional bool `json:"optional,omitempty"`
Expand All @@ -91,9 +99,30 @@ func (c commentTags) ValidationSchema() (*spec.Schema, error) {
SchemaProps: c.SchemaProps,
}

if len(c.CEL) > 0 {
celRules := c.CEL

if c.Scope != nil {
switch *c.Scope {
case KindScopeCluster:
celRules = append(celRules, CELTag{
Rule: "self.metadata.namespace.size() == 0",
Message: "not allowed on this type",
Reason: "FieldValueForbidden",
})
case KindScopeNamespaced:
celRules = append(celRules, CELTag{
Rule: "self.metadata.namespace.size() > 0",
Message: "",
Reason: "FieldValueRequired",
})
default:
return nil, fmt.Errorf("invalid scope %q", *c.Scope)
}
}

if len(celRules) > 0 {
// Convert the CELTag to a map[string]interface{} via JSON
celTagJSON, err := json.Marshal(c.CEL)
celTagJSON, err := json.Marshal(celRules)
if err != nil {
return nil, fmt.Errorf("failed to marshal CEL tag: %w", err)
}
Expand Down Expand Up @@ -164,6 +193,10 @@ func (c commentTags) Validate() error {
err = errors.Join(err, fmt.Errorf("invalid CEL tag at index %d: %w", i, celError))
}

if c.Scope != nil && *c.Scope != KindScopeNamespaced && *c.Scope != KindScopeCluster {
err = errors.Join(err, fmt.Errorf("invalid scope %q", *c.Scope))
}

return err
}

Expand Down
39 changes: 39 additions & 0 deletions pkg/generators/markers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -494,6 +494,45 @@ func TestParseCommentTags(t *testing.T) {
},
expectedError: `failed to parse marker comments: concatenations to key 'cel[0]:message' must be consecutive with its assignment`,
},
{
name: "namespaced scope",
t: structKind,
comments: []string{
`+k8s:validation:scope>Namespaced`,
},
expected: &spec.Schema{
VendorExtensible: spec.VendorExtensible{
Extensions: map[string]interface{}{
"x-kubernetes-validations": []interface{}{
map[string]interface{}{
"rule": "self.metadata.namespace.size() > 0",
"reason": "FieldValueRequired",
},
},
},
},
},
},
{
name: "cluster scope",
t: structKind,
comments: []string{
`+k8s:validation:scope>Cluster`,
},
expected: &spec.Schema{
VendorExtensible: spec.VendorExtensible{
Extensions: map[string]interface{}{
"x-kubernetes-validations": []interface{}{
map[string]interface{}{
"rule": "self.metadata.namespace.size() == 0",
"message": "not allowed on this type",
"reason": "FieldValueForbidden",
},
},
},
},
},
},
}

for _, tc := range cases {
Expand Down
2 changes: 1 addition & 1 deletion test/integration/pkg/generated/openapi_generated.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions test/integration/testdata/golden.v2.json
Original file line number Diff line number Diff line change
Expand Up @@ -1285,6 +1285,10 @@
{
"message": "foo",
"rule": "self == oldSelf"
},
{
"reason": "FieldValueRequired",
"rule": "self.metadata.namespace.size() \u003e 0"
}
]
},
Expand Down
4 changes: 4 additions & 0 deletions test/integration/testdata/golden.v3.json
Original file line number Diff line number Diff line change
Expand Up @@ -1247,6 +1247,10 @@
{
"message": "foo",
"rule": "self == oldSelf"
},
{
"reason": "FieldValueRequired",
"rule": "self.metadata.namespace.size() \u003e 0"
}
]
},
Expand Down
1 change: 1 addition & 0 deletions test/integration/testdata/valuevalidation/alpha.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
// +k8s:openapi-gen=true
// +k8s:validation:cel[0]:rule="self == oldSelf"
// +k8s:validation:cel[0]:message="foo"
// +k8s:validation:scope>Namespaced
type Foo struct {
// +k8s:validation:maxLength=5
// +k8s:validation:minLength=1
Expand Down

0 comments on commit a5e08fc

Please sign in to comment.