diff --git a/go.mod b/go.mod index bb920c317..e7dad5eac 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.21 require ( github.com/NYTimes/gziphandler v1.1.1 + github.com/blang/semver/v4 v4.0.0 github.com/emicklei/go-restful/v3 v3.11.0 github.com/go-openapi/jsonreference v0.20.1 github.com/go-openapi/swag v0.23.0 diff --git a/go.sum b/go.sum index a75dd2d91..8942329e1 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,7 @@ github.com/NYTimes/gziphandler v1.1.1 h1:ZUDjpQae29j0ryrS0u/B8HZfJBtBQHjqw2rQ2cqUQ3I= github.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMomdKFjzJNB0c= +github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= +github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= diff --git a/pkg/validation/strfmt/semver.go b/pkg/validation/strfmt/semver.go new file mode 100644 index 000000000..c2cfa3d38 --- /dev/null +++ b/pkg/validation/strfmt/semver.go @@ -0,0 +1,83 @@ +/* +Copyright 2025 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package strfmt + +import ( + "encoding/json" + + "github.com/blang/semver/v4" +) + +func init() { + semver := Semver("") + Default.Add("semver", &semver, isSemver) +} + +// Semver represents a semantic version string that follows the semver.org specification. +// +// swagger:strfmt semver +type Semver string + +// MarshalText turns this instance into text +func (s Semver) MarshalText() ([]byte, error) { + return []byte(s), nil +} + +// UnmarshalText hydrates this instance from text +func (s *Semver) UnmarshalText(data []byte) error { + *(s) = Semver(data) + return nil +} + +// String converts this value to a string +func (s Semver) String() string { + return string(s) +} + +// MarshalJSON returns the Semver as JSON +func (s Semver) MarshalJSON() ([]byte, error) { + return json.Marshal(string(s)) +} + +// UnmarshalJSON sets the Semver from JSON +func (s *Semver) UnmarshalJSON(data []byte) error { + var str string + if err := json.Unmarshal(data, &str); err != nil { + return err + } + return s.UnmarshalText([]byte(str)) +} + +// DeepCopyInto copies the receiver into out. out must be non-nil. +func (s *Semver) DeepCopyInto(out *Semver) { + *out = *s +} + +// DeepCopy creates a deep copy of Semver +func (s *Semver) DeepCopy() *Semver { + if s == nil { + return nil + } + out := new(Semver) + s.DeepCopyInto(out) + return out +} + +func isSemver(str string) bool { + _, err := semver.Parse(str) + return err == nil +} diff --git a/pkg/validation/strfmt/semver_test.go b/pkg/validation/strfmt/semver_test.go new file mode 100644 index 000000000..59aac8591 --- /dev/null +++ b/pkg/validation/strfmt/semver_test.go @@ -0,0 +1,99 @@ +/* +Copyright 2025 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package strfmt + +import ( + "encoding/json" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestSemver(t *testing.T) { + validSemvers := []string{ + "1.0.0", + "2.3.4", + "1.0.0-alpha", + "1.0.0-alpha.1", + "1.0.0-0.3.7", + "1.0.0-x.7.z.92", + "1.0.0-beta+exp.sha.5114f85", + "1.0.0+20130313144700", + "1.0.0-beta+exp.sha.5114f85", + "11.200.300-alpha+meta", + } + + invalidSemvers := []string{ + "", // empty + "1", // missing minor and patch + "1.0", // missing patch + "1.a.2", // non-numeric version parts + "1.0.0beta", // prerelease without hyphen + "v1.0.0", // with v prefix + "1.0.0-", // empty prerelease + "1.0.0+", // empty build metadata + "1.0.0-+", // empty prerelease and build metadata + "1.0.0-alpha_1", // invalid character in prerelease + "1.0.0+alpha_1", // invalid character in build metadata + "-1.0.0", // negative major version + "1.-2.0", // negative minor version + "1.0.-3", // negative patch version + } + + for _, v := range validSemvers { + t.Run(v, func(t *testing.T) { + assert.True(t, isSemver(v), "Expected %q to be a valid semver", v) + }) + } + + for _, v := range invalidSemvers { + t.Run(v, func(t *testing.T) { + assert.False(t, isSemver(v), "Expected %q to be an invalid semver", v) + }) + } +} + +func TestDeepCopySemver(t *testing.T) { + semver := Semver("1.0.0-alpha+001") + in := &semver + + out := new(Semver) + in.DeepCopyInto(out) + assert.Equal(t, in, out) + + out2 := in.DeepCopy() + assert.Equal(t, in, out2) + + var inNil *Semver + out3 := inNil.DeepCopy() + assert.Nil(t, out3) +} + +func TestSemverJSON(t *testing.T) { + semver := Semver("1.0.0-alpha+001") + + // Test marshaling + data, err := json.Marshal(semver) + assert.NoError(t, err) + assert.Equal(t, `"1.0.0-alpha+001"`, string(data)) + + // Test unmarshaling + var s Semver + err = json.Unmarshal(data, &s) + assert.NoError(t, err) + assert.Equal(t, semver, s) +} diff --git a/pkg/validation/validate/helpers.go b/pkg/validation/validate/helpers.go index 67514a189..e327fcf87 100644 --- a/pkg/validation/validate/helpers.go +++ b/pkg/validation/validate/helpers.go @@ -60,6 +60,7 @@ const ( stringFormatISBN13 = "isbn13" stringFormatMAC = "mac" stringFormatRGBColor = "rgbcolor" + stringFormatSemver = "semver" stringFormatSSN = "ssn" stringFormatURI = "uri" stringFormatUUID = "uuid" diff --git a/pkg/validation/validate/type.go b/pkg/validation/validate/type.go index 6469719dd..0de3a7f5c 100644 --- a/pkg/validation/validate/type.go +++ b/pkg/validation/validate/type.go @@ -19,6 +19,7 @@ import ( "strings" "github.com/go-openapi/swag" + "k8s.io/kube-openapi/pkg/validation/errors" "k8s.io/kube-openapi/pkg/validation/spec" "k8s.io/kube-openapi/pkg/validation/strfmt" @@ -71,6 +72,8 @@ func (t *typeValidator) schemaInfoForType(data interface{}) (string, string) { return stringType, stringFormatRGBColor case strfmt.SSN, *strfmt.SSN: return stringType, stringFormatSSN + case strfmt.Semver, *strfmt.Semver: + return stringType, stringFormatSemver case strfmt.URI, *strfmt.URI: return stringType, stringFormatURI case strfmt.UUID, *strfmt.UUID: diff --git a/pkg/validation/validate/type_test.go b/pkg/validation/validate/type_test.go index b28ae2260..7ea72c3cc 100644 --- a/pkg/validation/validate/type_test.go +++ b/pkg/validation/validate/type_test.go @@ -19,6 +19,7 @@ import ( "time" "github.com/stretchr/testify/assert" + "k8s.io/kube-openapi/pkg/validation/strfmt" ) @@ -125,6 +126,11 @@ func TestType_schemaInfoForType(t *testing.T) { expectedJSONType: stringType, expectedSwaggerFormat: "ssn", }, + { + value: strfmt.Semver("1.0.0-alpha+001"), + expectedJSONType: stringType, + expectedSwaggerFormat: "semver", + }, { value: strfmt.HexColor("#FFFFFF"), expectedJSONType: stringType,