mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-08-04 01:40:07 +00:00
Add ConversionReviewVersions to CustomResourceDefinitionSpec and defdefault it to v1beta1
This commit is contained in:
parent
f7dff4725f
commit
9b28b915f5
@ -66,6 +66,9 @@ func Funcs(codecs runtimeserializer.CodecFactory) []interface{} {
|
|||||||
Strategy: apiextensions.NoneConverter,
|
Strategy: apiextensions.NoneConverter,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if obj.Conversion.Strategy == apiextensions.WebhookConverter && len(obj.Conversion.ConversionReviewVersions) == 0 {
|
||||||
|
obj.Conversion.ConversionReviewVersions = []string{"v1beta1"}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
func(obj *apiextensions.CustomResourceDefinition, c fuzz.Continue) {
|
func(obj *apiextensions.CustomResourceDefinition, c fuzz.Continue) {
|
||||||
c.FuzzNoCustom(obj)
|
c.FuzzNoCustom(obj)
|
||||||
|
@ -84,6 +84,15 @@ type CustomResourceConversion struct {
|
|||||||
|
|
||||||
// `webhookClientConfig` is the instructions for how to call the webhook if strategy is `Webhook`.
|
// `webhookClientConfig` is the instructions for how to call the webhook if strategy is `Webhook`.
|
||||||
WebhookClientConfig *WebhookClientConfig
|
WebhookClientConfig *WebhookClientConfig
|
||||||
|
|
||||||
|
// ConversionReviewVersions is an ordered list of preferred `ConversionReview`
|
||||||
|
// versions the Webhook expects. API server will try to use first version in
|
||||||
|
// the list which it supports. If none of the versions specified in this list
|
||||||
|
// supported by API server, conversion will fail for this object.
|
||||||
|
// If a persisted Webhook configuration specifies allowed versions and does not
|
||||||
|
// include any versions known to the API Server, calls to the webhook will fail.
|
||||||
|
// +optional
|
||||||
|
ConversionReviewVersions []string
|
||||||
}
|
}
|
||||||
|
|
||||||
// WebhookClientConfig contains the information to make a TLS
|
// WebhookClientConfig contains the information to make a TLS
|
||||||
|
@ -68,6 +68,9 @@ func SetDefaults_CustomResourceDefinitionSpec(obj *CustomResourceDefinitionSpec)
|
|||||||
Strategy: NoneConverter,
|
Strategy: NoneConverter,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if obj.Conversion.Strategy == WebhookConverter && len(obj.Conversion.ConversionReviewVersions) == 0 {
|
||||||
|
obj.Conversion.ConversionReviewVersions = []string{SchemeGroupVersion.Version}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// hasPerVersionColumns returns true if a CRD uses per-version columns.
|
// hasPerVersionColumns returns true if a CRD uses per-version columns.
|
||||||
|
@ -91,6 +91,16 @@ type CustomResourceConversion struct {
|
|||||||
// alpha-level and is only honored by servers that enable the CustomResourceWebhookConversion feature.
|
// alpha-level and is only honored by servers that enable the CustomResourceWebhookConversion feature.
|
||||||
// +optional
|
// +optional
|
||||||
WebhookClientConfig *WebhookClientConfig `json:"webhookClientConfig,omitempty" protobuf:"bytes,2,name=webhookClientConfig"`
|
WebhookClientConfig *WebhookClientConfig `json:"webhookClientConfig,omitempty" protobuf:"bytes,2,name=webhookClientConfig"`
|
||||||
|
|
||||||
|
// ConversionReviewVersions is an ordered list of preferred `ConversionReview`
|
||||||
|
// versions the Webhook expects. API server will try to use first version in
|
||||||
|
// the list which it supports. If none of the versions specified in this list
|
||||||
|
// supported by API server, conversion will fail for this object.
|
||||||
|
// If a persisted Webhook configuration specifies allowed versions and does not
|
||||||
|
// include any versions known to the API Server, calls to the webhook will fail.
|
||||||
|
// Default to `['v1beta1']`.
|
||||||
|
// +optional
|
||||||
|
ConversionReviewVersions []string `json:"conversionReviewVersions,omitempty" protobuf:"bytes,3,rep,name=conversionReviewVersions"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// WebhookClientConfig contains the information to make a TLS
|
// WebhookClientConfig contains the information to make a TLS
|
||||||
|
@ -24,12 +24,14 @@ import (
|
|||||||
apiequality "k8s.io/apimachinery/pkg/api/equality"
|
apiequality "k8s.io/apimachinery/pkg/api/equality"
|
||||||
genericvalidation "k8s.io/apimachinery/pkg/api/validation"
|
genericvalidation "k8s.io/apimachinery/pkg/api/validation"
|
||||||
"k8s.io/apimachinery/pkg/util/sets"
|
"k8s.io/apimachinery/pkg/util/sets"
|
||||||
|
utilvalidation "k8s.io/apimachinery/pkg/util/validation"
|
||||||
validationutil "k8s.io/apimachinery/pkg/util/validation"
|
validationutil "k8s.io/apimachinery/pkg/util/validation"
|
||||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||||
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
||||||
"k8s.io/apiserver/pkg/util/webhook"
|
"k8s.io/apiserver/pkg/util/webhook"
|
||||||
|
|
||||||
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
|
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
|
||||||
|
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
|
||||||
apiservervalidation "k8s.io/apiextensions-apiserver/pkg/apiserver/validation"
|
apiservervalidation "k8s.io/apiextensions-apiserver/pkg/apiserver/validation"
|
||||||
apiextensionsfeatures "k8s.io/apiextensions-apiserver/pkg/features"
|
apiextensionsfeatures "k8s.io/apiextensions-apiserver/pkg/features"
|
||||||
)
|
)
|
||||||
@ -113,6 +115,10 @@ func ValidateCustomResourceDefinitionVersion(version *apiextensions.CustomResour
|
|||||||
|
|
||||||
// ValidateCustomResourceDefinitionSpec statically validates
|
// ValidateCustomResourceDefinitionSpec statically validates
|
||||||
func ValidateCustomResourceDefinitionSpec(spec *apiextensions.CustomResourceDefinitionSpec, fldPath *field.Path) field.ErrorList {
|
func ValidateCustomResourceDefinitionSpec(spec *apiextensions.CustomResourceDefinitionSpec, fldPath *field.Path) field.ErrorList {
|
||||||
|
return validateCustomResourceDefinitionSpec(spec, true, fldPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateCustomResourceDefinitionSpec(spec *apiextensions.CustomResourceDefinitionSpec, requireRecognizedVersion bool, fldPath *field.Path) field.ErrorList {
|
||||||
allErrs := field.ErrorList{}
|
allErrs := field.ErrorList{}
|
||||||
|
|
||||||
if len(spec.Group) == 0 {
|
if len(spec.Group) == 0 {
|
||||||
@ -205,7 +211,7 @@ func ValidateCustomResourceDefinitionSpec(spec *apiextensions.CustomResourceDefi
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
allErrs = append(allErrs, ValidateCustomResourceConversion(spec.Conversion, fldPath.Child("conversion"))...)
|
allErrs = append(allErrs, validateCustomResourceConversion(spec.Conversion, requireRecognizedVersion, fldPath.Child("conversion"))...)
|
||||||
|
|
||||||
return allErrs
|
return allErrs
|
||||||
}
|
}
|
||||||
@ -225,8 +231,66 @@ func validateEnumStrings(fldPath *field.Path, value string, accepted []string, r
|
|||||||
return field.ErrorList{field.NotSupported(fldPath, value, accepted)}
|
return field.ErrorList{field.NotSupported(fldPath, value, accepted)}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var acceptedConversionReviewVersion = []string{v1beta1.SchemeGroupVersion.Version}
|
||||||
|
|
||||||
|
func isAcceptedConversionReviewVersion(v string) bool {
|
||||||
|
for _, version := range acceptedConversionReviewVersion {
|
||||||
|
if v == version {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateConversionReviewVersions(versions []string, requireRecognizedVersion bool, fldPath *field.Path) field.ErrorList {
|
||||||
|
allErrs := field.ErrorList{}
|
||||||
|
if len(versions) < 1 {
|
||||||
|
allErrs = append(allErrs, field.Required(fldPath, ""))
|
||||||
|
} else {
|
||||||
|
seen := map[string]bool{}
|
||||||
|
hasAcceptedVersion := false
|
||||||
|
for i, v := range versions {
|
||||||
|
if seen[v] {
|
||||||
|
allErrs = append(allErrs, field.Invalid(fldPath.Index(i), v, "duplicate version"))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[v] = true
|
||||||
|
for _, errString := range utilvalidation.IsDNS1035Label(v) {
|
||||||
|
allErrs = append(allErrs, field.Invalid(fldPath.Index(i), v, errString))
|
||||||
|
}
|
||||||
|
if isAcceptedConversionReviewVersion(v) {
|
||||||
|
hasAcceptedVersion = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if requireRecognizedVersion && !hasAcceptedVersion {
|
||||||
|
allErrs = append(allErrs, field.Invalid(
|
||||||
|
fldPath, versions,
|
||||||
|
fmt.Sprintf("none of the versions accepted by this server. accepted version(s) are %v",
|
||||||
|
strings.Join(acceptedConversionReviewVersion, ", "))))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return allErrs
|
||||||
|
}
|
||||||
|
|
||||||
|
// hasValidConversionReviewVersion return true if there is a valid version or if the list is empty.
|
||||||
|
func hasValidConversionReviewVersionOrEmpty(versions []string) bool {
|
||||||
|
if len(versions) < 1 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
for _, v := range versions {
|
||||||
|
if isAcceptedConversionReviewVersion(v) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
// ValidateCustomResourceConversion statically validates
|
// ValidateCustomResourceConversion statically validates
|
||||||
func ValidateCustomResourceConversion(conversion *apiextensions.CustomResourceConversion, fldPath *field.Path) field.ErrorList {
|
func ValidateCustomResourceConversion(conversion *apiextensions.CustomResourceConversion, fldPath *field.Path) field.ErrorList {
|
||||||
|
return validateCustomResourceConversion(conversion, true, fldPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateCustomResourceConversion(conversion *apiextensions.CustomResourceConversion, requireRecognizedVersion bool, fldPath *field.Path) field.ErrorList {
|
||||||
allErrs := field.ErrorList{}
|
allErrs := field.ErrorList{}
|
||||||
if conversion == nil {
|
if conversion == nil {
|
||||||
return allErrs
|
return allErrs
|
||||||
@ -250,15 +314,22 @@ func ValidateCustomResourceConversion(conversion *apiextensions.CustomResourceCo
|
|||||||
allErrs = append(allErrs, webhook.ValidateWebhookService(fldPath.Child("webhookClientConfig").Child("service"), cc.Service.Name, cc.Service.Namespace, cc.Service.Path)...)
|
allErrs = append(allErrs, webhook.ValidateWebhookService(fldPath.Child("webhookClientConfig").Child("service"), cc.Service.Name, cc.Service.Namespace, cc.Service.Path)...)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if conversion.WebhookClientConfig != nil {
|
allErrs = append(allErrs, validateConversionReviewVersions(conversion.ConversionReviewVersions, requireRecognizedVersion, fldPath.Child("conversionReviewVersions"))...)
|
||||||
allErrs = append(allErrs, field.Forbidden(fldPath.Child("webhookClientConfig"), "should not be set when strategy is not set to Webhook"))
|
} else {
|
||||||
|
if conversion.WebhookClientConfig != nil {
|
||||||
|
allErrs = append(allErrs, field.Forbidden(fldPath.Child("webhookClientConfig"), "should not be set when strategy is not set to Webhook"))
|
||||||
|
}
|
||||||
|
if len(conversion.ConversionReviewVersions) > 0 {
|
||||||
|
allErrs = append(allErrs, field.Forbidden(fldPath.Child("conversionReviewVersions"), "should not be set when strategy is not set to Webhook"))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return allErrs
|
return allErrs
|
||||||
}
|
}
|
||||||
|
|
||||||
// ValidateCustomResourceDefinitionSpecUpdate statically validates
|
// ValidateCustomResourceDefinitionSpecUpdate statically validates
|
||||||
func ValidateCustomResourceDefinitionSpecUpdate(spec, oldSpec *apiextensions.CustomResourceDefinitionSpec, established bool, fldPath *field.Path) field.ErrorList {
|
func ValidateCustomResourceDefinitionSpecUpdate(spec, oldSpec *apiextensions.CustomResourceDefinitionSpec, established bool, fldPath *field.Path) field.ErrorList {
|
||||||
allErrs := ValidateCustomResourceDefinitionSpec(spec, fldPath)
|
requireRecognizedVersion := oldSpec.Conversion == nil || hasValidConversionReviewVersionOrEmpty(oldSpec.Conversion.ConversionReviewVersions)
|
||||||
|
allErrs := validateCustomResourceDefinitionSpec(spec, requireRecognizedVersion, fldPath)
|
||||||
|
|
||||||
if established {
|
if established {
|
||||||
// these effect the storage and cannot be changed therefore
|
// these effect the storage and cannot be changed therefore
|
||||||
|
@ -35,6 +35,9 @@ func required(path ...string) validationMatch {
|
|||||||
func invalid(path ...string) validationMatch {
|
func invalid(path ...string) validationMatch {
|
||||||
return validationMatch{path: field.NewPath(path[0], path[1:]...), errorType: field.ErrorTypeInvalid}
|
return validationMatch{path: field.NewPath(path[0], path[1:]...), errorType: field.ErrorTypeInvalid}
|
||||||
}
|
}
|
||||||
|
func invalidIndex(index int, path ...string) validationMatch {
|
||||||
|
return validationMatch{path: field.NewPath(path[0], path[1:]...).Index(index), errorType: field.ErrorTypeInvalid}
|
||||||
|
}
|
||||||
func unsupported(path ...string) validationMatch {
|
func unsupported(path ...string) validationMatch {
|
||||||
return validationMatch{path: field.NewPath(path[0], path[1:]...), errorType: field.ErrorTypeNotSupported}
|
return validationMatch{path: field.NewPath(path[0], path[1:]...), errorType: field.ErrorTypeNotSupported}
|
||||||
}
|
}
|
||||||
@ -65,7 +68,7 @@ func TestValidateCustomResourceDefinition(t *testing.T) {
|
|||||||
errors []validationMatch
|
errors []validationMatch
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "webhookconfig: blank URL",
|
name: "webhookconfig: both service and URL provided",
|
||||||
resource: &apiextensions.CustomResourceDefinition{
|
resource: &apiextensions.CustomResourceDefinition{
|
||||||
ObjectMeta: metav1.ObjectMeta{Name: "plural.group.com"},
|
ObjectMeta: metav1.ObjectMeta{Name: "plural.group.com"},
|
||||||
Spec: apiextensions.CustomResourceDefinitionSpec{
|
Spec: apiextensions.CustomResourceDefinitionSpec{
|
||||||
@ -109,7 +112,7 @@ func TestValidateCustomResourceDefinition(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "webhookconfig: both service and URL provided",
|
name: "webhookconfig: blank URL",
|
||||||
resource: &apiextensions.CustomResourceDefinition{
|
resource: &apiextensions.CustomResourceDefinition{
|
||||||
ObjectMeta: metav1.ObjectMeta{Name: "plural.group.com"},
|
ObjectMeta: metav1.ObjectMeta{Name: "plural.group.com"},
|
||||||
Spec: apiextensions.CustomResourceDefinitionSpec{
|
Spec: apiextensions.CustomResourceDefinitionSpec{
|
||||||
@ -189,6 +192,207 @@ func TestValidateCustomResourceDefinition(t *testing.T) {
|
|||||||
forbidden("spec", "conversion", "webhookClientConfig"),
|
forbidden("spec", "conversion", "webhookClientConfig"),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "ConversionReviewVersions_should_not_be_set",
|
||||||
|
resource: &apiextensions.CustomResourceDefinition{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{Name: "plural.group.com"},
|
||||||
|
Spec: apiextensions.CustomResourceDefinitionSpec{
|
||||||
|
Group: "group.com",
|
||||||
|
Scope: apiextensions.ResourceScope("Cluster"),
|
||||||
|
Names: apiextensions.CustomResourceDefinitionNames{
|
||||||
|
Plural: "plural",
|
||||||
|
Singular: "singular",
|
||||||
|
Kind: "Plural",
|
||||||
|
ListKind: "PluralList",
|
||||||
|
},
|
||||||
|
Versions: []apiextensions.CustomResourceDefinitionVersion{
|
||||||
|
{
|
||||||
|
Name: "version",
|
||||||
|
Served: true,
|
||||||
|
Storage: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "version2",
|
||||||
|
Served: true,
|
||||||
|
Storage: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Conversion: &apiextensions.CustomResourceConversion{
|
||||||
|
Strategy: apiextensions.ConversionStrategyType("None"),
|
||||||
|
ConversionReviewVersions: []string{"v1beta1"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Status: apiextensions.CustomResourceDefinitionStatus{
|
||||||
|
StoredVersions: []string{"version"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
errors: []validationMatch{
|
||||||
|
forbidden("spec", "conversion", "conversionReviewVersions"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "webhookconfig: invalid ConversionReviewVersion",
|
||||||
|
resource: &apiextensions.CustomResourceDefinition{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{Name: "plural.group.com"},
|
||||||
|
Spec: apiextensions.CustomResourceDefinitionSpec{
|
||||||
|
Group: "group.com",
|
||||||
|
Scope: apiextensions.ResourceScope("Cluster"),
|
||||||
|
Names: apiextensions.CustomResourceDefinitionNames{
|
||||||
|
Plural: "plural",
|
||||||
|
Singular: "singular",
|
||||||
|
Kind: "Plural",
|
||||||
|
ListKind: "PluralList",
|
||||||
|
},
|
||||||
|
Versions: []apiextensions.CustomResourceDefinitionVersion{
|
||||||
|
{
|
||||||
|
Name: "version",
|
||||||
|
Served: true,
|
||||||
|
Storage: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "version2",
|
||||||
|
Served: true,
|
||||||
|
Storage: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Conversion: &apiextensions.CustomResourceConversion{
|
||||||
|
Strategy: apiextensions.ConversionStrategyType("Webhook"),
|
||||||
|
WebhookClientConfig: &apiextensions.WebhookClientConfig{
|
||||||
|
URL: strPtr("https://example.com/webhook"),
|
||||||
|
},
|
||||||
|
ConversionReviewVersions: []string{"invalid-version"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Status: apiextensions.CustomResourceDefinitionStatus{
|
||||||
|
StoredVersions: []string{"version"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
errors: []validationMatch{
|
||||||
|
invalid("spec", "conversion", "conversionReviewVersions"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "webhookconfig: invalid ConversionReviewVersion version string",
|
||||||
|
resource: &apiextensions.CustomResourceDefinition{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{Name: "plural.group.com"},
|
||||||
|
Spec: apiextensions.CustomResourceDefinitionSpec{
|
||||||
|
Group: "group.com",
|
||||||
|
Scope: apiextensions.ResourceScope("Cluster"),
|
||||||
|
Names: apiextensions.CustomResourceDefinitionNames{
|
||||||
|
Plural: "plural",
|
||||||
|
Singular: "singular",
|
||||||
|
Kind: "Plural",
|
||||||
|
ListKind: "PluralList",
|
||||||
|
},
|
||||||
|
Versions: []apiextensions.CustomResourceDefinitionVersion{
|
||||||
|
{
|
||||||
|
Name: "version",
|
||||||
|
Served: true,
|
||||||
|
Storage: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "version2",
|
||||||
|
Served: true,
|
||||||
|
Storage: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Conversion: &apiextensions.CustomResourceConversion{
|
||||||
|
Strategy: apiextensions.ConversionStrategyType("Webhook"),
|
||||||
|
WebhookClientConfig: &apiextensions.WebhookClientConfig{
|
||||||
|
URL: strPtr("https://example.com/webhook"),
|
||||||
|
},
|
||||||
|
ConversionReviewVersions: []string{"0v"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Status: apiextensions.CustomResourceDefinitionStatus{
|
||||||
|
StoredVersions: []string{"version"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
errors: []validationMatch{
|
||||||
|
invalidIndex(0, "spec", "conversion", "conversionReviewVersions"),
|
||||||
|
invalid("spec", "conversion", "conversionReviewVersions"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "webhookconfig: at least one valid ConversionReviewVersion",
|
||||||
|
resource: &apiextensions.CustomResourceDefinition{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{Name: "plural.group.com"},
|
||||||
|
Spec: apiextensions.CustomResourceDefinitionSpec{
|
||||||
|
Group: "group.com",
|
||||||
|
Scope: apiextensions.ResourceScope("Cluster"),
|
||||||
|
Names: apiextensions.CustomResourceDefinitionNames{
|
||||||
|
Plural: "plural",
|
||||||
|
Singular: "singular",
|
||||||
|
Kind: "Plural",
|
||||||
|
ListKind: "PluralList",
|
||||||
|
},
|
||||||
|
Versions: []apiextensions.CustomResourceDefinitionVersion{
|
||||||
|
{
|
||||||
|
Name: "version",
|
||||||
|
Served: true,
|
||||||
|
Storage: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "version2",
|
||||||
|
Served: true,
|
||||||
|
Storage: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Conversion: &apiextensions.CustomResourceConversion{
|
||||||
|
Strategy: apiextensions.ConversionStrategyType("Webhook"),
|
||||||
|
WebhookClientConfig: &apiextensions.WebhookClientConfig{
|
||||||
|
URL: strPtr("https://example.com/webhook"),
|
||||||
|
},
|
||||||
|
ConversionReviewVersions: []string{"invalid-version", "v1beta1"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Status: apiextensions.CustomResourceDefinitionStatus{
|
||||||
|
StoredVersions: []string{"version"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
errors: []validationMatch{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "webhookconfig: duplicate ConversionReviewVersion",
|
||||||
|
resource: &apiextensions.CustomResourceDefinition{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{Name: "plural.group.com"},
|
||||||
|
Spec: apiextensions.CustomResourceDefinitionSpec{
|
||||||
|
Group: "group.com",
|
||||||
|
Scope: apiextensions.ResourceScope("Cluster"),
|
||||||
|
Names: apiextensions.CustomResourceDefinitionNames{
|
||||||
|
Plural: "plural",
|
||||||
|
Singular: "singular",
|
||||||
|
Kind: "Plural",
|
||||||
|
ListKind: "PluralList",
|
||||||
|
},
|
||||||
|
Versions: []apiextensions.CustomResourceDefinitionVersion{
|
||||||
|
{
|
||||||
|
Name: "version",
|
||||||
|
Served: true,
|
||||||
|
Storage: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "version2",
|
||||||
|
Served: true,
|
||||||
|
Storage: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Conversion: &apiextensions.CustomResourceConversion{
|
||||||
|
Strategy: apiextensions.ConversionStrategyType("Webhook"),
|
||||||
|
WebhookClientConfig: &apiextensions.WebhookClientConfig{
|
||||||
|
URL: strPtr("https://example.com/webhook"),
|
||||||
|
},
|
||||||
|
ConversionReviewVersions: []string{"v1beta1", "v1beta1"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Status: apiextensions.CustomResourceDefinitionStatus{
|
||||||
|
StoredVersions: []string{"version"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
errors: []validationMatch{
|
||||||
|
invalidIndex(1, "spec", "conversion", "conversionReviewVersions"),
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "missing_webhookconfig",
|
name: "missing_webhookconfig",
|
||||||
resource: &apiextensions.CustomResourceDefinition{
|
resource: &apiextensions.CustomResourceDefinition{
|
||||||
@ -646,6 +850,10 @@ func TestValidateCustomResourceDefinition(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, tc := range tests {
|
for _, tc := range tests {
|
||||||
|
// duplicate defaulting behaviour
|
||||||
|
if tc.resource.Spec.Conversion != nil && tc.resource.Spec.Conversion.Strategy == apiextensions.WebhookConverter && len(tc.resource.Spec.Conversion.ConversionReviewVersions) == 0 {
|
||||||
|
tc.resource.Spec.Conversion.ConversionReviewVersions = []string{"v1beta1"}
|
||||||
|
}
|
||||||
errs := ValidateCustomResourceDefinition(tc.resource)
|
errs := ValidateCustomResourceDefinition(tc.resource)
|
||||||
seenErrs := make([]bool, len(errs))
|
seenErrs := make([]bool, len(errs))
|
||||||
|
|
||||||
@ -679,6 +887,231 @@ func TestValidateCustomResourceDefinitionUpdate(t *testing.T) {
|
|||||||
resource *apiextensions.CustomResourceDefinition
|
resource *apiextensions.CustomResourceDefinition
|
||||||
errors []validationMatch
|
errors []validationMatch
|
||||||
}{
|
}{
|
||||||
|
{
|
||||||
|
name: "webhookconfig: should pass on invalid ConversionReviewVersion with old invalid versions",
|
||||||
|
resource: &apiextensions.CustomResourceDefinition{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{Name: "plural.group.com", ResourceVersion: "42"},
|
||||||
|
Spec: apiextensions.CustomResourceDefinitionSpec{
|
||||||
|
Group: "group.com",
|
||||||
|
Scope: apiextensions.ResourceScope("Cluster"),
|
||||||
|
Names: apiextensions.CustomResourceDefinitionNames{
|
||||||
|
Plural: "plural",
|
||||||
|
Singular: "singular",
|
||||||
|
Kind: "Plural",
|
||||||
|
ListKind: "PluralList",
|
||||||
|
},
|
||||||
|
Versions: []apiextensions.CustomResourceDefinitionVersion{
|
||||||
|
{
|
||||||
|
Name: "version",
|
||||||
|
Served: true,
|
||||||
|
Storage: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "version2",
|
||||||
|
Served: true,
|
||||||
|
Storage: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Conversion: &apiextensions.CustomResourceConversion{
|
||||||
|
Strategy: apiextensions.ConversionStrategyType("Webhook"),
|
||||||
|
WebhookClientConfig: &apiextensions.WebhookClientConfig{
|
||||||
|
URL: strPtr("https://example.com/webhook"),
|
||||||
|
},
|
||||||
|
ConversionReviewVersions: []string{"invalid-version"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Status: apiextensions.CustomResourceDefinitionStatus{
|
||||||
|
StoredVersions: []string{"version"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
old: &apiextensions.CustomResourceDefinition{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{Name: "plural.group.com", ResourceVersion: "42"},
|
||||||
|
Spec: apiextensions.CustomResourceDefinitionSpec{
|
||||||
|
Group: "group.com",
|
||||||
|
Scope: apiextensions.ResourceScope("Cluster"),
|
||||||
|
Names: apiextensions.CustomResourceDefinitionNames{
|
||||||
|
Plural: "plural",
|
||||||
|
Singular: "singular",
|
||||||
|
Kind: "Plural",
|
||||||
|
ListKind: "PluralList",
|
||||||
|
},
|
||||||
|
Versions: []apiextensions.CustomResourceDefinitionVersion{
|
||||||
|
{
|
||||||
|
Name: "version",
|
||||||
|
Served: true,
|
||||||
|
Storage: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "version2",
|
||||||
|
Served: true,
|
||||||
|
Storage: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Conversion: &apiextensions.CustomResourceConversion{
|
||||||
|
Strategy: apiextensions.ConversionStrategyType("Webhook"),
|
||||||
|
WebhookClientConfig: &apiextensions.WebhookClientConfig{
|
||||||
|
URL: strPtr("https://example.com/webhook"),
|
||||||
|
},
|
||||||
|
ConversionReviewVersions: []string{"invalid-version_0, invalid-version"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Status: apiextensions.CustomResourceDefinitionStatus{
|
||||||
|
StoredVersions: []string{"version"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
errors: []validationMatch{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "webhookconfig: should fail on invalid ConversionReviewVersion with old valid versions",
|
||||||
|
resource: &apiextensions.CustomResourceDefinition{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{Name: "plural.group.com", ResourceVersion: "42"},
|
||||||
|
Spec: apiextensions.CustomResourceDefinitionSpec{
|
||||||
|
Group: "group.com",
|
||||||
|
Scope: apiextensions.ResourceScope("Cluster"),
|
||||||
|
Names: apiextensions.CustomResourceDefinitionNames{
|
||||||
|
Plural: "plural",
|
||||||
|
Singular: "singular",
|
||||||
|
Kind: "Plural",
|
||||||
|
ListKind: "PluralList",
|
||||||
|
},
|
||||||
|
Versions: []apiextensions.CustomResourceDefinitionVersion{
|
||||||
|
{
|
||||||
|
Name: "version",
|
||||||
|
Served: true,
|
||||||
|
Storage: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "version2",
|
||||||
|
Served: true,
|
||||||
|
Storage: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Conversion: &apiextensions.CustomResourceConversion{
|
||||||
|
Strategy: apiextensions.ConversionStrategyType("Webhook"),
|
||||||
|
WebhookClientConfig: &apiextensions.WebhookClientConfig{
|
||||||
|
URL: strPtr("https://example.com/webhook"),
|
||||||
|
},
|
||||||
|
ConversionReviewVersions: []string{"invalid-version"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Status: apiextensions.CustomResourceDefinitionStatus{
|
||||||
|
StoredVersions: []string{"version"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
old: &apiextensions.CustomResourceDefinition{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{Name: "plural.group.com", ResourceVersion: "42"},
|
||||||
|
Spec: apiextensions.CustomResourceDefinitionSpec{
|
||||||
|
Group: "group.com",
|
||||||
|
Scope: apiextensions.ResourceScope("Cluster"),
|
||||||
|
Names: apiextensions.CustomResourceDefinitionNames{
|
||||||
|
Plural: "plural",
|
||||||
|
Singular: "singular",
|
||||||
|
Kind: "Plural",
|
||||||
|
ListKind: "PluralList",
|
||||||
|
},
|
||||||
|
Versions: []apiextensions.CustomResourceDefinitionVersion{
|
||||||
|
{
|
||||||
|
Name: "version",
|
||||||
|
Served: true,
|
||||||
|
Storage: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "version2",
|
||||||
|
Served: true,
|
||||||
|
Storage: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Conversion: &apiextensions.CustomResourceConversion{
|
||||||
|
Strategy: apiextensions.ConversionStrategyType("Webhook"),
|
||||||
|
WebhookClientConfig: &apiextensions.WebhookClientConfig{
|
||||||
|
URL: strPtr("https://example.com/webhook"),
|
||||||
|
},
|
||||||
|
ConversionReviewVersions: []string{"v1beta1", "invalid-version"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Status: apiextensions.CustomResourceDefinitionStatus{
|
||||||
|
StoredVersions: []string{"version"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
errors: []validationMatch{
|
||||||
|
invalid("spec", "conversion", "conversionReviewVersions"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "webhookconfig: should fail on invalid ConversionReviewVersion with missing old versions",
|
||||||
|
resource: &apiextensions.CustomResourceDefinition{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{Name: "plural.group.com", ResourceVersion: "42"},
|
||||||
|
Spec: apiextensions.CustomResourceDefinitionSpec{
|
||||||
|
Group: "group.com",
|
||||||
|
Scope: apiextensions.ResourceScope("Cluster"),
|
||||||
|
Names: apiextensions.CustomResourceDefinitionNames{
|
||||||
|
Plural: "plural",
|
||||||
|
Singular: "singular",
|
||||||
|
Kind: "Plural",
|
||||||
|
ListKind: "PluralList",
|
||||||
|
},
|
||||||
|
Versions: []apiextensions.CustomResourceDefinitionVersion{
|
||||||
|
{
|
||||||
|
Name: "version",
|
||||||
|
Served: true,
|
||||||
|
Storage: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "version2",
|
||||||
|
Served: true,
|
||||||
|
Storage: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Conversion: &apiextensions.CustomResourceConversion{
|
||||||
|
Strategy: apiextensions.ConversionStrategyType("Webhook"),
|
||||||
|
WebhookClientConfig: &apiextensions.WebhookClientConfig{
|
||||||
|
URL: strPtr("https://example.com/webhook"),
|
||||||
|
},
|
||||||
|
ConversionReviewVersions: []string{"invalid-version"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Status: apiextensions.CustomResourceDefinitionStatus{
|
||||||
|
StoredVersions: []string{"version"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
old: &apiextensions.CustomResourceDefinition{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{Name: "plural.group.com", ResourceVersion: "42"},
|
||||||
|
Spec: apiextensions.CustomResourceDefinitionSpec{
|
||||||
|
Group: "group.com",
|
||||||
|
Scope: apiextensions.ResourceScope("Cluster"),
|
||||||
|
Names: apiextensions.CustomResourceDefinitionNames{
|
||||||
|
Plural: "plural",
|
||||||
|
Singular: "singular",
|
||||||
|
Kind: "Plural",
|
||||||
|
ListKind: "PluralList",
|
||||||
|
},
|
||||||
|
Versions: []apiextensions.CustomResourceDefinitionVersion{
|
||||||
|
{
|
||||||
|
Name: "version",
|
||||||
|
Served: true,
|
||||||
|
Storage: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "version2",
|
||||||
|
Served: true,
|
||||||
|
Storage: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Conversion: &apiextensions.CustomResourceConversion{
|
||||||
|
Strategy: apiextensions.ConversionStrategyType("Webhook"),
|
||||||
|
WebhookClientConfig: &apiextensions.WebhookClientConfig{
|
||||||
|
URL: strPtr("https://example.com/webhook"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Status: apiextensions.CustomResourceDefinitionStatus{
|
||||||
|
StoredVersions: []string{"version"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
errors: []validationMatch{
|
||||||
|
invalid("spec", "conversion", "conversionReviewVersions"),
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "unchanged",
|
name: "unchanged",
|
||||||
old: &apiextensions.CustomResourceDefinition{
|
old: &apiextensions.CustomResourceDefinition{
|
||||||
|
@ -60,6 +60,8 @@ type webhookConverter struct {
|
|||||||
restClient *rest.RESTClient
|
restClient *rest.RESTClient
|
||||||
name string
|
name string
|
||||||
nopConverter nopConverter
|
nopConverter nopConverter
|
||||||
|
|
||||||
|
conversionReviewVersions []string
|
||||||
}
|
}
|
||||||
|
|
||||||
func webhookClientConfigForCRD(crd *internal.CustomResourceDefinition) *webhook.ClientConfig {
|
func webhookClientConfigForCRD(crd *internal.CustomResourceDefinition) *webhook.ClientConfig {
|
||||||
@ -96,6 +98,8 @@ func (f *webhookConverterFactory) NewWebhookConverter(validVersions map[schema.G
|
|||||||
restClient: restClient,
|
restClient: restClient,
|
||||||
name: crd.Name,
|
name: crd.Name,
|
||||||
nopConverter: nopConverter{validVersions: validVersions},
|
nopConverter: nopConverter{validVersions: validVersions},
|
||||||
|
|
||||||
|
conversionReviewVersions: crd.Spec.Conversion.ConversionReviewVersions,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -136,6 +140,16 @@ func (c *webhookConverter) Convert(in, out, context interface{}) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// hasConversionReviewVersion check whether a version is accepted by a given webhook.
|
||||||
|
func (c *webhookConverter) hasConversionReviewVersion(v string) bool {
|
||||||
|
for _, b := range c.conversionReviewVersions {
|
||||||
|
if b == v {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
func createConversionReview(obj runtime.Object, apiVersion string) *v1beta1.ConversionReview {
|
func createConversionReview(obj runtime.Object, apiVersion string) *v1beta1.ConversionReview {
|
||||||
listObj, isList := obj.(*unstructured.UnstructuredList)
|
listObj, isList := obj.(*unstructured.UnstructuredList)
|
||||||
var objects []runtime.RawExtension
|
var objects []runtime.RawExtension
|
||||||
@ -216,6 +230,12 @@ func (c *webhookConverter) ConvertToVersion(in runtime.Object, target runtime.Gr
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Currently converter only supports `v1beta1` ConversionReview
|
||||||
|
// TODO: Make CRD webhooks caller capable of sending/receiving multiple ConversionReview versions
|
||||||
|
if !c.hasConversionReviewVersion(v1beta1.SchemeGroupVersion.Version) {
|
||||||
|
return nil, fmt.Errorf("webhook does not accept v1beta1 ConversionReview")
|
||||||
|
}
|
||||||
|
|
||||||
request := createConversionReview(in, toGV.String())
|
request := createConversionReview(in, toGV.String())
|
||||||
if len(request.Request.Objects) == 0 {
|
if len(request.Request.Objects) == 0 {
|
||||||
if !isList {
|
if !isList {
|
||||||
|
Loading…
Reference in New Issue
Block a user