mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-27 05:27:21 +00:00
Merge pull request #130783 from jpbetz/versioned-formats
Support emulation versioning of custom resource formats
This commit is contained in:
commit
4666b8cdf6
@ -21,6 +21,7 @@ require (
|
|||||||
go.etcd.io/etcd/client/v3 v3.5.16
|
go.etcd.io/etcd/client/v3 v3.5.16
|
||||||
go.opentelemetry.io/otel v1.33.0
|
go.opentelemetry.io/otel v1.33.0
|
||||||
go.opentelemetry.io/otel/trace v1.33.0
|
go.opentelemetry.io/otel/trace v1.33.0
|
||||||
|
golang.org/x/sync v0.11.0
|
||||||
google.golang.org/grpc v1.68.1
|
google.golang.org/grpc v1.68.1
|
||||||
google.golang.org/protobuf v1.36.5
|
google.golang.org/protobuf v1.36.5
|
||||||
gopkg.in/evanphx/json-patch.v4 v4.12.0
|
gopkg.in/evanphx/json-patch.v4 v4.12.0
|
||||||
@ -110,7 +111,6 @@ require (
|
|||||||
golang.org/x/mod v0.21.0 // indirect
|
golang.org/x/mod v0.21.0 // indirect
|
||||||
golang.org/x/net v0.33.0 // indirect
|
golang.org/x/net v0.33.0 // indirect
|
||||||
golang.org/x/oauth2 v0.27.0 // indirect
|
golang.org/x/oauth2 v0.27.0 // indirect
|
||||||
golang.org/x/sync v0.11.0 // indirect
|
|
||||||
golang.org/x/sys v0.30.0 // indirect
|
golang.org/x/sys v0.30.0 // indirect
|
||||||
golang.org/x/term v0.29.0 // indirect
|
golang.org/x/term v0.29.0 // indirect
|
||||||
golang.org/x/text v0.22.0 // indirect
|
golang.org/x/text v0.22.0 // indirect
|
||||||
|
@ -17,13 +17,23 @@ limitations under the License.
|
|||||||
package validation
|
package validation
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"golang.org/x/sync/singleflight"
|
||||||
|
|
||||||
"k8s.io/apimachinery/pkg/util/sets"
|
"k8s.io/apimachinery/pkg/util/sets"
|
||||||
|
"k8s.io/apimachinery/pkg/util/version"
|
||||||
"k8s.io/kube-openapi/pkg/validation/spec"
|
"k8s.io/kube-openapi/pkg/validation/spec"
|
||||||
)
|
)
|
||||||
|
|
||||||
var supportedFormats = sets.NewString(
|
// supportedVersionedFormats tracks the formats supported by CRD schemas, and the version at which support was introduced.
|
||||||
|
// Formats in CRD schemas are ignored when used in versions where they are not supported.
|
||||||
|
var supportedVersionedFormats = []versionedFormats{
|
||||||
|
{
|
||||||
|
introducedVersion: version.MajorMinor(1, 0),
|
||||||
|
formats: sets.New(
|
||||||
"bsonobjectid", // bson object ID
|
"bsonobjectid", // bson object ID
|
||||||
"uri", // an URI as parsed by Golang net/url.ParseRequestURI
|
"uri", // an URI as parsed by Golang net/url.ParseRequestURI
|
||||||
"email", // an email address as parsed by Golang net/mail.ParseAddress
|
"email", // an email address as parsed by Golang net/mail.ParseAddress
|
||||||
@ -48,18 +58,79 @@ var supportedFormats = sets.NewString(
|
|||||||
"date", // a date string like "2006-01-02" as defined by full-date in RFC3339
|
"date", // a date string like "2006-01-02" as defined by full-date in RFC3339
|
||||||
"duration", // a duration string like "22 ns" as parsed by Golang time.ParseDuration or compatible with Scala duration format
|
"duration", // a duration string like "22 ns" as parsed by Golang time.ParseDuration or compatible with Scala duration format
|
||||||
"datetime", // a date time string like "2014-12-15T19:30:20.000Z" as defined by date-time in RFC3339
|
"datetime", // a date time string like "2014-12-15T19:30:20.000Z" as defined by date-time in RFC3339
|
||||||
)
|
),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
// StripUnsupportedFormatsPostProcess sets unsupported formats to empty string.
|
// StripUnsupportedFormatsPostProcess sets unsupported formats to empty string.
|
||||||
|
// Only supports formats supported by all known version of Kubernetes.
|
||||||
|
// Deprecated: Use StripUnsupportedFormatsPostProcessorForVersion instead.
|
||||||
func StripUnsupportedFormatsPostProcess(s *spec.Schema) error {
|
func StripUnsupportedFormatsPostProcess(s *spec.Schema) error {
|
||||||
|
return legacyPostProcessor(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
// StripUnsupportedFormatsPostProcessorForVersion determines the supported formats at the given compatibility version and
|
||||||
|
// sets unsupported formats to empty string.
|
||||||
|
func StripUnsupportedFormatsPostProcessorForVersion(compatibilityVersion *version.Version) func(s *spec.Schema) error {
|
||||||
|
return func(s *spec.Schema) error {
|
||||||
if len(s.Format) == 0 {
|
if len(s.Format) == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
normalized := strings.Replace(s.Format, "-", "", -1) // go-openapi default format name normalization
|
normalized := strings.ReplaceAll(s.Format, "-", "") // go-openapi default format name normalization
|
||||||
if !supportedFormats.Has(normalized) {
|
if !supportedFormatsAtVersion(compatibilityVersion).supported.Has(normalized) {
|
||||||
s.Format = ""
|
s.Format = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type versionedFormats struct {
|
||||||
|
introducedVersion *version.Version
|
||||||
|
formats sets.Set[string]
|
||||||
|
}
|
||||||
|
type supportedFormats struct {
|
||||||
|
compatibilityVersion *version.Version
|
||||||
|
// supported is a set of formats validated at compatibilityVersion of Kubernetes.
|
||||||
|
supported sets.Set[string]
|
||||||
|
}
|
||||||
|
|
||||||
|
var cacheFormatSets = true
|
||||||
|
|
||||||
|
func supportedFormatsAtVersion(ver *version.Version) *supportedFormats {
|
||||||
|
key := fmt.Sprintf("%d.%d", ver.Major(), ver.Minor())
|
||||||
|
var entry interface{}
|
||||||
|
if entry, ok := baseEnvs.Load(key); ok {
|
||||||
|
return entry.(*supportedFormats)
|
||||||
|
}
|
||||||
|
entry, _, _ = baseEnvsSingleflight.Do(key, func() (interface{}, error) {
|
||||||
|
entry := newFormatsAtVersion(ver, supportedVersionedFormats)
|
||||||
|
if cacheFormatSets {
|
||||||
|
baseEnvs.Store(key, entry)
|
||||||
|
}
|
||||||
|
return entry, nil
|
||||||
|
})
|
||||||
|
return entry.(*supportedFormats)
|
||||||
|
}
|
||||||
|
|
||||||
|
func newFormatsAtVersion(ver *version.Version, versionedFormats []versionedFormats) *supportedFormats {
|
||||||
|
result := &supportedFormats{
|
||||||
|
compatibilityVersion: ver,
|
||||||
|
supported: sets.New[string](),
|
||||||
|
}
|
||||||
|
for _, vf := range versionedFormats {
|
||||||
|
if ver.AtLeast(vf.introducedVersion) {
|
||||||
|
result.supported = result.supported.Union(vf.formats)
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
baseEnvs = sync.Map{}
|
||||||
|
baseEnvsSingleflight = &singleflight.Group{}
|
||||||
|
)
|
||||||
|
|
||||||
|
var legacyPostProcessor = StripUnsupportedFormatsPostProcessorForVersion(version.MajorMinor(1, 0))
|
||||||
|
@ -19,13 +19,122 @@ package validation
|
|||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"k8s.io/apimachinery/pkg/util/sets"
|
||||||
|
"k8s.io/apimachinery/pkg/util/version"
|
||||||
|
"k8s.io/kube-openapi/pkg/validation/spec"
|
||||||
"k8s.io/kube-openapi/pkg/validation/strfmt"
|
"k8s.io/kube-openapi/pkg/validation/strfmt"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestRegistryFormats(t *testing.T) {
|
func TestRegistryFormats(t *testing.T) {
|
||||||
for f := range supportedFormats {
|
for _, sf := range supportedVersionedFormats {
|
||||||
|
for f := range sf.formats {
|
||||||
if !strfmt.Default.ContainsName(f) {
|
if !strfmt.Default.ContainsName(f) {
|
||||||
t.Errorf("expected format %q in strfmt default registry", f)
|
t.Errorf("expected format %q in strfmt default registry", f)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSupportedFormats(t *testing.T) {
|
||||||
|
vf := []versionedFormats{
|
||||||
|
{
|
||||||
|
introducedVersion: version.MajorMinor(1, 0),
|
||||||
|
formats: sets.New(
|
||||||
|
"A",
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
introducedVersion: version.MajorMinor(1, 1),
|
||||||
|
formats: sets.New(
|
||||||
|
"B",
|
||||||
|
"C",
|
||||||
|
),
|
||||||
|
},
|
||||||
|
// Version 1.2 has no new supported formats
|
||||||
|
{
|
||||||
|
introducedVersion: version.MajorMinor(1, 3),
|
||||||
|
formats: sets.New(
|
||||||
|
"D",
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
introducedVersion: version.MajorMinor(1, 3), // same version as previous entry
|
||||||
|
formats: sets.New(
|
||||||
|
"E",
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
introducedVersion: version.MajorMinor(1, 4),
|
||||||
|
formats: sets.New[string](),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
version *version.Version
|
||||||
|
expectedFormats sets.Set[string]
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "version 1.0",
|
||||||
|
version: version.MajorMinor(1, 0),
|
||||||
|
expectedFormats: sets.New("A"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "version 1.1",
|
||||||
|
version: version.MajorMinor(1, 1),
|
||||||
|
expectedFormats: sets.New("A", "B", "C"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "version 1.2",
|
||||||
|
version: version.MajorMinor(1, 2),
|
||||||
|
expectedFormats: sets.New("A", "B", "C"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "version 1.3",
|
||||||
|
version: version.MajorMinor(1, 3),
|
||||||
|
expectedFormats: sets.New("A", "B", "C", "D", "E"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "version 1.4",
|
||||||
|
version: version.MajorMinor(1, 4),
|
||||||
|
expectedFormats: sets.New("A", "B", "C", "D", "E"),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
allFormats := newFormatsAtVersion(version.MajorMinor(0, 0), vf)
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
got := newFormatsAtVersion(tc.version, vf)
|
||||||
|
|
||||||
|
t.Run("newFormatsAtVersion", func(t *testing.T) {
|
||||||
|
if !got.supported.Equal(tc.expectedFormats) {
|
||||||
|
t.Errorf("expected %v, got %v", tc.expectedFormats, got.supported)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(got.supported.Difference(allFormats.supported)) == 0 {
|
||||||
|
t.Errorf("expected allFormats to be a superset of all formats, but was missing %v", allFormats.supported)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("StripUnsupportedFormatsPostProcessorForVersion", func(t *testing.T) {
|
||||||
|
processor := StripUnsupportedFormatsPostProcessorForVersion(tc.version)
|
||||||
|
for f := range allFormats.supported {
|
||||||
|
schema := &spec.Schema{SchemaProps: spec.SchemaProps{Format: f}}
|
||||||
|
err := processor(schema)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
gotFormat := schema.Format
|
||||||
|
if tc.expectedFormats.Has(f) {
|
||||||
|
if gotFormat != f {
|
||||||
|
t.Errorf("expected format %q, got %q", f, gotFormat)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if gotFormat != "" {
|
||||||
|
t.Errorf("expected format to be stripped out, got %q", gotFormat)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -25,6 +25,7 @@ import (
|
|||||||
"k8s.io/apiextensions-apiserver/pkg/features"
|
"k8s.io/apiextensions-apiserver/pkg/features"
|
||||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||||
"k8s.io/apiserver/pkg/cel/common"
|
"k8s.io/apiserver/pkg/cel/common"
|
||||||
|
"k8s.io/apiserver/pkg/cel/environment"
|
||||||
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
||||||
openapierrors "k8s.io/kube-openapi/pkg/validation/errors"
|
openapierrors "k8s.io/kube-openapi/pkg/validation/errors"
|
||||||
"k8s.io/kube-openapi/pkg/validation/spec"
|
"k8s.io/kube-openapi/pkg/validation/spec"
|
||||||
@ -102,7 +103,8 @@ func NewSchemaValidator(customResourceValidation *apiextensions.JSONSchemaProps)
|
|||||||
openapiSchema := &spec.Schema{}
|
openapiSchema := &spec.Schema{}
|
||||||
if customResourceValidation != nil {
|
if customResourceValidation != nil {
|
||||||
// TODO: replace with NewStructural(...).ToGoOpenAPI
|
// TODO: replace with NewStructural(...).ToGoOpenAPI
|
||||||
if err := ConvertJSONSchemaPropsWithPostProcess(customResourceValidation, openapiSchema, StripUnsupportedFormatsPostProcess); err != nil {
|
formatPostProcessor := StripUnsupportedFormatsPostProcessorForVersion(environment.DefaultCompatibilityVersion())
|
||||||
|
if err := ConvertJSONSchemaPropsWithPostProcess(customResourceValidation, openapiSchema, formatPostProcessor); err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -49,6 +49,7 @@ import (
|
|||||||
"k8s.io/apimachinery/pkg/util/version"
|
"k8s.io/apimachinery/pkg/util/version"
|
||||||
"k8s.io/apimachinery/pkg/util/wait"
|
"k8s.io/apimachinery/pkg/util/wait"
|
||||||
utilyaml "k8s.io/apimachinery/pkg/util/yaml"
|
utilyaml "k8s.io/apimachinery/pkg/util/yaml"
|
||||||
|
"k8s.io/apiserver/pkg/cel/environment"
|
||||||
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
||||||
"k8s.io/client-go/dynamic"
|
"k8s.io/client-go/dynamic"
|
||||||
featuregatetesting "k8s.io/component-base/featuregate/testing"
|
featuregatetesting "k8s.io/component-base/featuregate/testing"
|
||||||
@ -1769,7 +1770,8 @@ func newValidator(customResourceValidation *apiextensionsinternal.JSONSchemaProp
|
|||||||
openapiSchema := &spec.Schema{}
|
openapiSchema := &spec.Schema{}
|
||||||
if customResourceValidation != nil {
|
if customResourceValidation != nil {
|
||||||
// TODO: replace with NewStructural(...).ToGoOpenAPI
|
// TODO: replace with NewStructural(...).ToGoOpenAPI
|
||||||
if err := apiservervalidation.ConvertJSONSchemaPropsWithPostProcess(customResourceValidation, openapiSchema, apiservervalidation.StripUnsupportedFormatsPostProcess); err != nil {
|
formatPostProcessor := apiservervalidation.StripUnsupportedFormatsPostProcessorForVersion(environment.DefaultCompatibilityVersion())
|
||||||
|
if err := apiservervalidation.ConvertJSONSchemaPropsWithPostProcess(customResourceValidation, openapiSchema, formatPostProcessor); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -29,6 +29,7 @@ import (
|
|||||||
"github.com/onsi/ginkgo/v2"
|
"github.com/onsi/ginkgo/v2"
|
||||||
"sigs.k8s.io/yaml"
|
"sigs.k8s.io/yaml"
|
||||||
|
|
||||||
|
"k8s.io/apiserver/pkg/cel/environment"
|
||||||
openapiutil "k8s.io/kube-openapi/pkg/util"
|
openapiutil "k8s.io/kube-openapi/pkg/util"
|
||||||
"k8s.io/utils/pointer"
|
"k8s.io/utils/pointer"
|
||||||
|
|
||||||
@ -698,7 +699,8 @@ func convertJSONSchemaProps(in []byte, out *spec.Schema) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
kubeOut := spec.Schema{}
|
kubeOut := spec.Schema{}
|
||||||
if err := validation.ConvertJSONSchemaPropsWithPostProcess(&internal, &kubeOut, validation.StripUnsupportedFormatsPostProcess); err != nil {
|
formatPostProcessor := validation.StripUnsupportedFormatsPostProcessorForVersion(environment.DefaultCompatibilityVersion())
|
||||||
|
if err := validation.ConvertJSONSchemaPropsWithPostProcess(&internal, &kubeOut, formatPostProcessor); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
bs, err := json.Marshal(kubeOut)
|
bs, err := json.Marshal(kubeOut)
|
||||||
|
Loading…
Reference in New Issue
Block a user