Skip to content

Commit

Permalink
dra api: implement semver attribute value type
Browse files Browse the repository at this point in the history
This adds support for semantic version comparison to the CEL support in the
"named resources" structured parameter model. For example, it can be used to
check that an instance supports a certain API level.

To minimize the risk, the new "semver" type is only defined in the CEL
environment for DRA expressions, not in the base library.

Validation of semver strings is done with the regular expression from
semver.org. The actual evaluation at runtime then uses semver/v4.
  • Loading branch information
pohly committed Mar 5, 2024
1 parent fe0f8ce commit 3b5eeec
Show file tree
Hide file tree
Showing 24 changed files with 644 additions and 194 deletions.
1 change: 1 addition & 0 deletions api/api-rules/violation_exceptions.list
Expand Up @@ -57,6 +57,7 @@ API rule violation: names_match,k8s.io/api/resource/v1alpha2,NamedResourcesAttri
API rule violation: names_match,k8s.io/api/resource/v1alpha2,NamedResourcesAttributeValue,QuantityValue
API rule violation: names_match,k8s.io/api/resource/v1alpha2,NamedResourcesAttributeValue,StringSliceValue
API rule violation: names_match,k8s.io/api/resource/v1alpha2,NamedResourcesAttributeValue,StringValue
API rule violation: names_match,k8s.io/api/resource/v1alpha2,NamedResourcesAttributeValue,VersionValue
API rule violation: names_match,k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1,JSONSchemaProps,Ref
API rule violation: names_match,k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1,JSONSchemaProps,Schema
API rule violation: names_match,k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1,JSONSchemaProps,XEmbeddedResource
Expand Down
4 changes: 4 additions & 0 deletions api/openapi-spec/swagger.json

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

Expand Up @@ -228,6 +228,10 @@
}
],
"description": "StringSliceValue is an array of strings."
},
"version": {
"description": "VersionValue is a semantic version according to semver.org spec 2.0.0.",
"type": "string"
}
},
"required": [
Expand Down
14 changes: 2 additions & 12 deletions pkg/apis/resource/namedresources.go
Expand Up @@ -59,7 +59,8 @@ type NamedResourcesAttributeValue struct {
StringValue *string
// StringSliceValue is an array of strings.
StringSliceValue *NamedResourcesStringSlice
// TODO: VersionValue *SemVersion
// VersionValue is a semantic version according to semver.org spec 2.0.0.
VersionValue *string
}

// NamedResourcesIntSlice contains a slice of 64-bit integers.
Expand All @@ -74,17 +75,6 @@ type NamedResourcesStringSlice struct {
Strings []string
}

// TODO
//
// A wrapper around https://pkg.go.dev/github.com/blang/semver/v4#Version which
// is encoded as a string. During decoding, it validates that the string
// can be parsed using tolerant parsing (currently trims spaces, removes a "v" prefix,
// adds a 0 patch number to versions with only major and minor components specified,
// and removes leading 0s).
// type SemVersion struct {
// semver.Version
//}

// NamedResourcesRequest is used in ResourceRequestModel.
type NamedResourcesRequest struct {
// Selector is a CEL expression which must evaluate to true if a
Expand Down
Expand Up @@ -18,6 +18,7 @@ package validation

import (
"fmt"
"regexp"

"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/apimachinery/pkg/util/validation/field"
Expand Down Expand Up @@ -63,6 +64,27 @@ func validateInstances(instances []resource.NamedResourcesInstance, fldPath *fie
return allErrs
}

var (
numericIdentifier = `(0|[1-9]\d*)`

preReleaseIdentifier = `(0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)`

buildIdentifier = `[0-9a-zA-Z-]+`

semverRe = regexp.MustCompile(`^` +

// dot-separated version segments (e.g. 1.2.3)
numericIdentifier + `\.` + numericIdentifier + `\.` + numericIdentifier +

// optional dot-separated prerelease segments (e.g. -alpha.PRERELEASE.1)
`(-` + preReleaseIdentifier + `(\.` + preReleaseIdentifier + `)*)?` +

// optional dot-separated build identifier segments (e.g. +build.id.20240305)
`(\+` + buildIdentifier + `(\.` + buildIdentifier + `)*)?` +

`$`)
)

func validateAttributes(attributes []resource.NamedResourcesAttribute, fldPath *field.Path) field.ErrorList {
var allErrs field.ErrorList
attributeNames := sets.New[string]()
Expand Down Expand Up @@ -95,7 +117,12 @@ func validateAttributes(attributes []resource.NamedResourcesAttribute, fldPath *
if attribute.StringSliceValue != nil {
entries.Insert("stringSlice")
}
// TODO: VersionValue
if attribute.VersionValue != nil {
entries.Insert("version")
if !semverRe.MatchString(*attribute.VersionValue) {
allErrs = append(allErrs, field.Invalid(idxPath.Child("version"), *attribute.VersionValue, fmt.Sprintf("a semantic version string must start with major/minor/patch non-negative integer version numbers, optionally followed by pre-release identifiers after a hyphen and build metadata after a plus sign - regexp used for validation is `%s`", semverRe)))
}
}

switch len(entries) {
case 0:
Expand Down
Expand Up @@ -75,7 +75,13 @@ func TestValidateResources(t *testing.T) {
"string-slice": {
resources: testResources([]resourceapi.NamedResourcesInstance{{Name: goodName, Attributes: []resourceapi.NamedResourcesAttribute{{Name: goodName, NamedResourcesAttributeValue: resourceapi.NamedResourcesAttributeValue{StringSliceValue: &resourceapi.NamedResourcesStringSlice{Strings: []string{"hello"}}}}}}}),
},
// TODO: semver
"version-okay": {
resources: testResources([]resourceapi.NamedResourcesInstance{{Name: goodName, Attributes: []resourceapi.NamedResourcesAttribute{{Name: goodName, NamedResourcesAttributeValue: resourceapi.NamedResourcesAttributeValue{VersionValue: ptr.To("1.0.0-beta.1")}}}}}),
},
"version-bad": {
wantFailures: field.ErrorList{field.Invalid(field.NewPath("instances").Index(0).Child("attributes").Index(0).Child("version"), "1.0", "a semantic version string must start with major/minor/patch non-negative integer version numbers, optionally followed by pre-release identifiers after a hyphen and build metadata after a plus sign - regexp used for validation is `^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$`")},
resources: testResources([]resourceapi.NamedResourcesInstance{{Name: goodName, Attributes: []resourceapi.NamedResourcesAttribute{{Name: goodName, NamedResourcesAttributeValue: resourceapi.NamedResourcesAttributeValue{VersionValue: ptr.To("1.0")}}}}}),
},
"empty-attribute": {
wantFailures: field.ErrorList{field.Required(field.NewPath("instances").Index(0).Child("attributes").Index(0), "exactly one value must be set")},
resources: testResources([]resourceapi.NamedResourcesInstance{{Name: goodName, Attributes: []resourceapi.NamedResourcesAttribute{{Name: goodName}}}}),
Expand Down Expand Up @@ -132,7 +138,9 @@ func TestValidateSelector(t *testing.T) {
"stringslice": {
selector: `attributes.stringslice["name"].isSorted()`,
},
// TODO: semver
"version": {
selector: `attributes.version["name"].isGreaterThan(semver("1.0.0"))`,
},
}

for name, scenario := range scenarios {
Expand Down
2 changes: 2 additions & 0 deletions pkg/apis/resource/v1alpha2/zz_generated.conversion.go

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

5 changes: 5 additions & 0 deletions pkg/apis/resource/zz_generated.deepcopy.go

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

14 changes: 14 additions & 0 deletions pkg/generated/openapi/zz_generated.openapi.go

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

0 comments on commit 3b5eeec

Please sign in to comment.