Skip to content

Add k8s name related formats #384

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 28 additions & 9 deletions pkg/validation/strfmt/default_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,10 @@ type testableFormat interface {
}

func testStringFormat(t *testing.T, what testableFormat, format, with string, validSamples, invalidSamples []string) {
testStringFormatWithRegistry(t, Default, what, format, with, validSamples, invalidSamples)
}

func testStringFormatWithRegistry(t *testing.T, registry Registry, what testableFormat, format, with string, validSamples, invalidSamples []string) {
// text encoding interface
b := []byte(with)
err := what.UnmarshalText(b)
Expand Down Expand Up @@ -347,24 +351,39 @@ func testStringFormat(t *testing.T, what testableFormat, format, with string, va
assert.Equalf(t, bj, b, "[%s]MarshalJSON: expected %v and %v to be value equal as []byte", format, string(b), with)

// validation with Registry
for _, valid := range append(validSamples, with) {
testValid(t, format, valid)
}

for _, invalid := range invalidSamples {
testInvalid(t, format, invalid)
}
t.Run("valid", func(t *testing.T) {
for _, valid := range append(validSamples, with) {
t.Run(valid, func(t *testing.T) {
testValidWithRegistry(t, registry, format, valid)
})
}
})
t.Run("invalid", func(t *testing.T) {
for _, invalid := range invalidSamples {
t.Run(invalid, func(t *testing.T) {
testInvalidWithRegistry(t, registry, format, invalid)
})
}
})
}

func testValid(t *testing.T, name, value string) {
ok := Default.Validates(name, value)
testValidWithRegistry(t, Default, name, value)
}

func testValidWithRegistry(t *testing.T, registry Registry, name, value string) {
ok := registry.Validates(name, value)
if !ok {
t.Errorf("expected %q of type %s to be valid", value, name)
}
}

func testInvalid(t *testing.T, name, value string) {
ok := Default.Validates(name, value)
testInvalidWithRegistry(t, Default, name, value)
}

func testInvalidWithRegistry(t *testing.T, registry Registry, name, value string) {
ok := registry.Validates(name, value)
if ok {
t.Errorf("expected %q of type %s to be invalid", value, name)
}
Expand Down
39 changes: 39 additions & 0 deletions pkg/validation/strfmt/format.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ package strfmt

import (
"encoding"
"encoding/json"
"fmt"
"reflect"
"strings"
"sync"
Expand Down Expand Up @@ -231,3 +233,40 @@ func (f *defaultFormats) Parse(name, data string) (interface{}, error) {
}
return nil, errors.InvalidTypeName(name)
}

// scan provides a generic implementation of sql.Scanner interface's Scan function for basic string formats.
func scan[T ~string](r *T, raw interface{}) error {
switch v := raw.(type) {
case []byte:
*r = T(v)
case string:
*r = T(v)
default:
return fmt.Errorf("cannot sql.Scan() strfmt.StringFormat from: %#v", v)
}

return nil
}

// unmarshalJSON provides a generic implementation of json.Unmarshaler interface's UnmarshalJSON function for basic string formats.
func unmarshalJSON[T ~string](r *T, data []byte) error {
if string(data) == jsonNull {
return nil
}
var ustr string
if err := json.Unmarshal(data, &ustr); err != nil {
return err
}
*r = T(ustr)
return nil
}

// deepCopy provides a generic implementation of DeepCopy for basic string formats.
func deepCopy[T ~string](r *T) *T {
if r == nil {
return nil
}
out := new(T)
*out = *r
return out
}
153 changes: 153 additions & 0 deletions pkg/validation/strfmt/kubernetes-extensions.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
// Copyright 2024 go-swagger maintainers
//
// 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"
"regexp"
)

// KubernetesExtensions is the formats registry for JSON Schema formats
// extensions defined by the Kubernetes project.
var KubernetesExtensions = NewSeededFormats(nil, nil)

func init() {
// register formats in the KubernetesExtensions registry:
// - k8s.io/short-name
// - k8s.io/long-name
shortName := ShortName("")
KubernetesExtensions.Add("k8s.io/short-name", &shortName, IsShortName)

longName := LongName("")
KubernetesExtensions.Add("k8s.io/long-name", &longName, IsLongName)
}

// ShortName is a name, up to 63 characters long, composed of alphanumeric
// characters and dashes, which cannot begin or end with a dash.
//
// ShortName almost conforms to the definition of a label in DNS (RFC 1123),
// except that uppercase letters are not allowed.
//
// xref: https://github.com/kubernetes/kubernetes/issues/71140
//
// swagger:strfmt k8s.io/short-name
type ShortName string

func (r ShortName) MarshalText() ([]byte, error) {
return []byte(string(r)), nil
}

func (r *ShortName) UnmarshalText(data []byte) error { // validation is performed later on
*r = ShortName(data)
return nil
}

func (r *ShortName) Scan(raw interface{}) error {
return scan[ShortName](r, raw)
}

func (r ShortName) String() string {
return string(r)
}

func (r ShortName) MarshalJSON() ([]byte, error) {
return json.Marshal(string(r))
}

func (r *ShortName) UnmarshalJSON(data []byte) error {
return unmarshalJSON(r, data)
}

func (r *ShortName) DeepCopyInto(out *ShortName) {
*out = *r
}

func (r *ShortName) DeepCopy() *ShortName {
return deepCopy(r)
}

const shortNameFmt string = "[a-z0-9]([-a-z0-9]*[a-z0-9])?"

// ShortNameMaxLength is a label's max length in DNS (RFC 1123)
const ShortNameMaxLength int = 63

var shortNameRegexp = regexp.MustCompile("^" + shortNameFmt + "$")

// IsShortName checks if a string is a valid ShortName.
func IsShortName(value string) bool {
return len(value) <= ShortNameMaxLength &&
shortNameRegexp.MatchString(value)
}

// LongName is a name, up to 253 characters long, composed of dot-separated
// segments; each segment uses only alphanumerics and dashes (no
// leading/trailing).
//
// LongName almost conforms to the definition of a subdomain in DNS (RFC 1123),
// except that uppercase letters are not allowed. and there is no max length of
// limit of 63 for each of the dot separated DNS Labels that make up the
// subdomain.
//
// xref: https://github.com/kubernetes/kubernetes/issues/71140
// xref: https://github.com/kubernetes/kubernetes/issues/79351
//
// swagger:strfmt k8s.io/long-name
type LongName string

func (r LongName) MarshalText() ([]byte, error) {
return []byte(string(r)), nil
}

func (r *LongName) UnmarshalText(data []byte) error { // validation is performed later on
*r = LongName(data)
return nil
}

func (r *LongName) Scan(raw interface{}) error {
return scan[LongName](r, raw)
}

func (r LongName) String() string {
return string(r)
}

func (r LongName) MarshalJSON() ([]byte, error) {
return json.Marshal(string(r))
}

func (r *LongName) UnmarshalJSON(data []byte) error {
return unmarshalJSON(r, data)
}

func (r *LongName) DeepCopyInto(out *LongName) {
*out = *r
}

func (r *LongName) DeepCopy() *LongName {
return deepCopy(r)
}

const longNameFmt string = shortNameFmt + "(\\." + shortNameFmt + ")*"

// LongNameMaxLength is a subdomain's max length in DNS (RFC 1123)
const LongNameMaxLength int = 253

var longNameRegexp = regexp.MustCompile("^" + longNameFmt + "$")

// IsLongName checks if a string is a valid LongName.
func IsLongName(value string) bool {
return len(value) <= LongNameMaxLength &&
longNameRegexp.MatchString(value)
}
77 changes: 77 additions & 0 deletions pkg/validation/strfmt/kubernetes-extensions_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
// Copyright 2024 go-swagger maintainers
//
// 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 (
"strings"
"testing"
)

var goodShortName = []string{
"a", "ab", "abc", "a1", "a-1", "a--1--2--b",
"0", "01", "012", "1a", "1-a", "1--a--b--2",
strings.Repeat("a", 63),
}
var badShortName = []string{
"", "A", "ABC", "aBc", "A1", "A-1", "1-A",
"-", "-a", "-1",
"_", "a_", "_a", "a_b", "1_", "_1", "1_2",
".", "a.", ".a", "a.b", "1.", ".1", "1.2",
" ", "a ", " a", "a b", "1 ", " 1", "1 2",
strings.Repeat("a", 64),
}

var prefixOnlyShortName = []string{
"a-", "1-",
}

func TestIsShortName(t *testing.T) {
v := ShortName("a")
testStringFormatWithRegistry(t, KubernetesExtensions, &v, "k8s.io/short-name", "a", goodShortName, append(badShortName, prefixOnlyShortName...))
}

var goodLongName = []string{
"a", "ab", "abc", "a1", "a-1", "a--1--2--b",
"0", "01", "012", "1a", "1-a", "1--a--b--2",
"a.a", "ab.a", "abc.a", "a1.a", "a-1.a", "a--1--2--b.a",
"a.1", "ab.1", "abc.1", "a1.1", "a-1.1", "a--1--2--b.1",
"0.a", "01.a", "012.a", "1a.a", "1-a.a", "1--a--b--2",
"0.1", "01.1", "012.1", "1a.1", "1-a.1", "1--a--b--2.1",
"a.b.c.d.e", "aa.bb.cc.dd.ee", "1.2.3.4.5", "11.22.33.44.55",
strings.Repeat("a", 253),
}
var badLongName = []string{
"", "A", "ABC", "aBc", "A1", "A-1", "1-A",
"-", "-a", "-1",
"_", "a_", "_a", "a_b", "1_", "_1", "1_2",
".", "a.", ".a", "a..b", "1.", ".1", "1..2",
" ", "a ", " a", "a b", "1 ", " 1", "1 2",
"A.a", "aB.a", "ab.A", "A1.a", "a1.A",
"A.1", "aB.1", "A1.1", "1A.1",
"0.A", "01.A", "012.A", "1A.a", "1a.A",
"A.B.C.D.E", "AA.BB.CC.DD.EE", "a.B.c.d.e", "aa.bB.cc.dd.ee",
"a@b", "a,b", "a_b", "a;b",
"a:b", "a%b", "a?b", "a$b",
strings.Repeat("a", 254),
}

var prefixOnlyLongName = []string{
"a-", "1-",
}

func TestFormatLongName(t *testing.T) {
v := LongName("a")
testStringFormatWithRegistry(t, KubernetesExtensions, &v, "k8s.io/long-name", "a", goodLongName, append(badLongName, prefixOnlyLongName...))
}