2019-01-12 04:58:27 +00:00
/ *
Copyright 2017 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 validation
import (
"fmt"
"reflect"
"strings"
apiequality "k8s.io/apimachinery/pkg/api/equality"
genericvalidation "k8s.io/apimachinery/pkg/api/validation"
"k8s.io/apimachinery/pkg/util/sets"
2019-04-07 17:07:55 +00:00
utilvalidation "k8s.io/apimachinery/pkg/util/validation"
2019-01-12 04:58:27 +00:00
validationutil "k8s.io/apimachinery/pkg/util/validation"
"k8s.io/apimachinery/pkg/util/validation/field"
"k8s.io/apiserver/pkg/util/webhook"
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
2019-04-07 17:07:55 +00:00
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
2019-01-12 04:58:27 +00:00
)
var (
printerColumnDatatypes = sets . NewString ( "integer" , "number" , "string" , "boolean" , "date" )
customResourceColumnDefinitionFormats = sets . NewString ( "int32" , "int64" , "float" , "double" , "byte" , "date" , "date-time" , "password" )
)
// ValidateCustomResourceDefinition statically validates
func ValidateCustomResourceDefinition ( obj * apiextensions . CustomResourceDefinition ) field . ErrorList {
nameValidationFn := func ( name string , prefix bool ) [ ] string {
ret := genericvalidation . NameIsDNSSubdomain ( name , prefix )
requiredName := obj . Spec . Names . Plural + "." + obj . Spec . Group
if name != requiredName {
ret = append ( ret , fmt . Sprintf ( ` must be spec.names.plural+"."+spec.group ` ) )
}
return ret
}
allErrs := genericvalidation . ValidateObjectMeta ( & obj . ObjectMeta , false , nameValidationFn , field . NewPath ( "metadata" ) )
allErrs = append ( allErrs , ValidateCustomResourceDefinitionSpec ( & obj . Spec , field . NewPath ( "spec" ) ) ... )
allErrs = append ( allErrs , ValidateCustomResourceDefinitionStatus ( & obj . Status , field . NewPath ( "status" ) ) ... )
allErrs = append ( allErrs , ValidateCustomResourceDefinitionStoredVersions ( obj . Status . StoredVersions , obj . Spec . Versions , field . NewPath ( "status" ) . Child ( "storedVersions" ) ) ... )
return allErrs
}
// ValidateCustomResourceDefinitionUpdate statically validates
func ValidateCustomResourceDefinitionUpdate ( obj , oldObj * apiextensions . CustomResourceDefinition ) field . ErrorList {
allErrs := genericvalidation . ValidateObjectMetaUpdate ( & obj . ObjectMeta , & oldObj . ObjectMeta , field . NewPath ( "metadata" ) )
allErrs = append ( allErrs , ValidateCustomResourceDefinitionSpecUpdate ( & obj . Spec , & oldObj . Spec , apiextensions . IsCRDConditionTrue ( oldObj , apiextensions . Established ) , field . NewPath ( "spec" ) ) ... )
allErrs = append ( allErrs , ValidateCustomResourceDefinitionStatus ( & obj . Status , field . NewPath ( "status" ) ) ... )
allErrs = append ( allErrs , ValidateCustomResourceDefinitionStoredVersions ( obj . Status . StoredVersions , obj . Spec . Versions , field . NewPath ( "status" ) . Child ( "storedVersions" ) ) ... )
return allErrs
}
// ValidateCustomResourceDefinitionStoredVersions statically validates
func ValidateCustomResourceDefinitionStoredVersions ( storedVersions [ ] string , versions [ ] apiextensions . CustomResourceDefinitionVersion , fldPath * field . Path ) field . ErrorList {
if len ( storedVersions ) == 0 {
return field . ErrorList { field . Invalid ( fldPath , storedVersions , "must have at least one stored version" ) }
}
allErrs := field . ErrorList { }
storedVersionsMap := map [ string ] int { }
for i , v := range storedVersions {
storedVersionsMap [ v ] = i
}
for _ , v := range versions {
_ , ok := storedVersionsMap [ v . Name ]
if v . Storage && ! ok {
allErrs = append ( allErrs , field . Invalid ( fldPath , v , "must have the storage version " + v . Name ) )
}
if ok {
delete ( storedVersionsMap , v . Name )
}
}
for v , i := range storedVersionsMap {
allErrs = append ( allErrs , field . Invalid ( fldPath . Index ( i ) , v , "must appear in spec.versions" ) )
}
return allErrs
}
// ValidateUpdateCustomResourceDefinitionStatus statically validates
func ValidateUpdateCustomResourceDefinitionStatus ( obj , oldObj * apiextensions . CustomResourceDefinition ) field . ErrorList {
allErrs := genericvalidation . ValidateObjectMetaUpdate ( & obj . ObjectMeta , & oldObj . ObjectMeta , field . NewPath ( "metadata" ) )
allErrs = append ( allErrs , ValidateCustomResourceDefinitionStatus ( & obj . Status , field . NewPath ( "status" ) ) ... )
return allErrs
}
// ValidateCustomResourceDefinitionVersion statically validates.
func ValidateCustomResourceDefinitionVersion ( version * apiextensions . CustomResourceDefinitionVersion , fldPath * field . Path , statusEnabled bool ) field . ErrorList {
allErrs := field . ErrorList { }
allErrs = append ( allErrs , ValidateCustomResourceDefinitionValidation ( version . Schema , statusEnabled , fldPath . Child ( "schema" ) ) ... )
allErrs = append ( allErrs , ValidateCustomResourceDefinitionSubresources ( version . Subresources , fldPath . Child ( "subresources" ) ) ... )
for i := range version . AdditionalPrinterColumns {
allErrs = append ( allErrs , ValidateCustomResourceColumnDefinition ( & version . AdditionalPrinterColumns [ i ] , fldPath . Child ( "additionalPrinterColumns" ) . Index ( i ) ) ... )
}
return allErrs
}
// ValidateCustomResourceDefinitionSpec statically validates
func ValidateCustomResourceDefinitionSpec ( spec * apiextensions . CustomResourceDefinitionSpec , fldPath * field . Path ) field . ErrorList {
2019-04-07 17:07:55 +00:00
return validateCustomResourceDefinitionSpec ( spec , true , fldPath )
}
func validateCustomResourceDefinitionSpec ( spec * apiextensions . CustomResourceDefinitionSpec , requireRecognizedVersion bool , fldPath * field . Path ) field . ErrorList {
2019-01-12 04:58:27 +00:00
allErrs := field . ErrorList { }
if len ( spec . Group ) == 0 {
allErrs = append ( allErrs , field . Required ( fldPath . Child ( "group" ) , "" ) )
} else if errs := validationutil . IsDNS1123Subdomain ( spec . Group ) ; len ( errs ) > 0 {
allErrs = append ( allErrs , field . Invalid ( fldPath . Child ( "group" ) , spec . Group , strings . Join ( errs , "," ) ) )
} else if len ( strings . Split ( spec . Group , "." ) ) < 2 {
allErrs = append ( allErrs , field . Invalid ( fldPath . Child ( "group" ) , spec . Group , "should be a domain with at least one dot" ) )
}
allErrs = append ( allErrs , validateEnumStrings ( fldPath . Child ( "scope" ) , string ( spec . Scope ) , [ ] string { string ( apiextensions . ClusterScoped ) , string ( apiextensions . NamespaceScoped ) } , true ) ... )
storageFlagCount := 0
versionsMap := map [ string ] bool { }
uniqueNames := true
for i , version := range spec . Versions {
if version . Storage {
storageFlagCount ++
}
if versionsMap [ version . Name ] {
uniqueNames = false
} else {
versionsMap [ version . Name ] = true
}
if errs := validationutil . IsDNS1035Label ( version . Name ) ; len ( errs ) > 0 {
allErrs = append ( allErrs , field . Invalid ( fldPath . Child ( "versions" ) . Index ( i ) . Child ( "name" ) , spec . Versions [ i ] . Name , strings . Join ( errs , "," ) ) )
}
subresources := getSubresourcesForVersion ( spec , version . Name )
allErrs = append ( allErrs , ValidateCustomResourceDefinitionVersion ( & version , fldPath . Child ( "versions" ) . Index ( i ) , hasStatusEnabled ( subresources ) ) ... )
}
// The top-level and per-version fields are mutual exclusive
if spec . Validation != nil && hasPerVersionSchema ( spec . Versions ) {
allErrs = append ( allErrs , field . Forbidden ( fldPath . Child ( "validation" ) , "top-level and per-version schemas are mutually exclusive" ) )
}
if spec . Subresources != nil && hasPerVersionSubresources ( spec . Versions ) {
allErrs = append ( allErrs , field . Forbidden ( fldPath . Child ( "subresources" ) , "top-level and per-version subresources are mutually exclusive" ) )
}
if len ( spec . AdditionalPrinterColumns ) > 0 && hasPerVersionColumns ( spec . Versions ) {
allErrs = append ( allErrs , field . Forbidden ( fldPath . Child ( "additionalPrinterColumns" ) , "top-level and per-version additionalPrinterColumns are mutually exclusive" ) )
}
// Per-version fields may not all be set to identical values (top-level field should be used instead)
if hasIdenticalPerVersionSchema ( spec . Versions ) {
allErrs = append ( allErrs , field . Invalid ( fldPath . Child ( "versions" ) , spec . Versions , "per-version schemas may not all be set to identical values (top-level validation should be used instead)" ) )
}
if hasIdenticalPerVersionSubresources ( spec . Versions ) {
allErrs = append ( allErrs , field . Invalid ( fldPath . Child ( "versions" ) , spec . Versions , "per-version subresources may not all be set to identical values (top-level subresources should be used instead)" ) )
}
if hasIdenticalPerVersionColumns ( spec . Versions ) {
allErrs = append ( allErrs , field . Invalid ( fldPath . Child ( "versions" ) , spec . Versions , "per-version additionalPrinterColumns may not all be set to identical values (top-level additionalPrinterColumns should be used instead)" ) )
}
if ! uniqueNames {
allErrs = append ( allErrs , field . Invalid ( fldPath . Child ( "versions" ) , spec . Versions , "must contain unique version names" ) )
}
if storageFlagCount != 1 {
allErrs = append ( allErrs , field . Invalid ( fldPath . Child ( "versions" ) , spec . Versions , "must have exactly one version marked as storage version" ) )
}
if len ( spec . Version ) != 0 {
if errs := validationutil . IsDNS1035Label ( spec . Version ) ; len ( errs ) > 0 {
allErrs = append ( allErrs , field . Invalid ( fldPath . Child ( "version" ) , spec . Version , strings . Join ( errs , "," ) ) )
}
if len ( spec . Versions ) >= 1 && spec . Versions [ 0 ] . Name != spec . Version {
allErrs = append ( allErrs , field . Invalid ( fldPath . Child ( "version" ) , spec . Version , "must match the first version in spec.versions" ) )
}
}
// in addition to the basic name restrictions, some names are required for spec, but not for status
if len ( spec . Names . Plural ) == 0 {
allErrs = append ( allErrs , field . Required ( fldPath . Child ( "names" , "plural" ) , "" ) )
}
if len ( spec . Names . Singular ) == 0 {
allErrs = append ( allErrs , field . Required ( fldPath . Child ( "names" , "singular" ) , "" ) )
}
if len ( spec . Names . Kind ) == 0 {
allErrs = append ( allErrs , field . Required ( fldPath . Child ( "names" , "kind" ) , "" ) )
}
if len ( spec . Names . ListKind ) == 0 {
allErrs = append ( allErrs , field . Required ( fldPath . Child ( "names" , "listKind" ) , "" ) )
}
allErrs = append ( allErrs , ValidateCustomResourceDefinitionNames ( & spec . Names , fldPath . Child ( "names" ) ) ... )
2019-04-07 17:07:55 +00:00
allErrs = append ( allErrs , ValidateCustomResourceDefinitionValidation ( spec . Validation , hasAnyStatusEnabled ( spec ) , fldPath . Child ( "validation" ) ) ... )
allErrs = append ( allErrs , ValidateCustomResourceDefinitionSubresources ( spec . Subresources , fldPath . Child ( "subresources" ) ) ... )
2019-01-12 04:58:27 +00:00
for i := range spec . AdditionalPrinterColumns {
if errs := ValidateCustomResourceColumnDefinition ( & spec . AdditionalPrinterColumns [ i ] , fldPath . Child ( "additionalPrinterColumns" ) . Index ( i ) ) ; len ( errs ) > 0 {
allErrs = append ( allErrs , errs ... )
}
}
2019-04-07 17:07:55 +00:00
allErrs = append ( allErrs , validateCustomResourceConversion ( spec . Conversion , requireRecognizedVersion , fldPath . Child ( "conversion" ) ) ... )
2019-01-12 04:58:27 +00:00
return allErrs
}
func validateEnumStrings ( fldPath * field . Path , value string , accepted [ ] string , required bool ) field . ErrorList {
if value == "" {
if required {
return field . ErrorList { field . Required ( fldPath , "" ) }
}
return field . ErrorList { }
}
for _ , a := range accepted {
if a == value {
return field . ErrorList { }
}
}
return field . ErrorList { field . NotSupported ( fldPath , value , accepted ) }
}
2019-04-07 17:07:55 +00:00
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
}
2019-01-12 04:58:27 +00:00
// ValidateCustomResourceConversion statically validates
func ValidateCustomResourceConversion ( conversion * apiextensions . CustomResourceConversion , fldPath * field . Path ) field . ErrorList {
2019-04-07 17:07:55 +00:00
return validateCustomResourceConversion ( conversion , true , fldPath )
}
func validateCustomResourceConversion ( conversion * apiextensions . CustomResourceConversion , requireRecognizedVersion bool , fldPath * field . Path ) field . ErrorList {
2019-01-12 04:58:27 +00:00
allErrs := field . ErrorList { }
if conversion == nil {
return allErrs
}
allErrs = append ( allErrs , validateEnumStrings ( fldPath . Child ( "strategy" ) , string ( conversion . Strategy ) , [ ] string { string ( apiextensions . NoneConverter ) , string ( apiextensions . WebhookConverter ) } , true ) ... )
if conversion . Strategy == apiextensions . WebhookConverter {
if conversion . WebhookClientConfig == nil {
allErrs = append ( allErrs , field . Required ( fldPath . Child ( "webhookClientConfig" ) , "required when strategy is set to Webhook, but not allowed because the CustomResourceWebhookConversion feature is disabled" ) )
} else {
cc := conversion . WebhookClientConfig
switch {
case ( cc . URL == nil ) == ( cc . Service == nil ) :
allErrs = append ( allErrs , field . Required ( fldPath . Child ( "webhookClientConfig" ) , "exactly one of url or service is required" ) )
case cc . URL != nil :
allErrs = append ( allErrs , webhook . ValidateWebhookURL ( fldPath . Child ( "webhookClientConfig" ) . Child ( "url" ) , * cc . URL , true ) ... )
case cc . Service != nil :
allErrs = append ( allErrs , webhook . ValidateWebhookService ( fldPath . Child ( "webhookClientConfig" ) . Child ( "service" ) , cc . Service . Name , cc . Service . Namespace , cc . Service . Path ) ... )
}
}
2019-04-07 17:07:55 +00:00
allErrs = append ( allErrs , validateConversionReviewVersions ( conversion . ConversionReviewVersions , requireRecognizedVersion , fldPath . Child ( "conversionReviewVersions" ) ) ... )
} 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" ) )
}
2019-01-12 04:58:27 +00:00
}
return allErrs
}
// ValidateCustomResourceDefinitionSpecUpdate statically validates
func ValidateCustomResourceDefinitionSpecUpdate ( spec , oldSpec * apiextensions . CustomResourceDefinitionSpec , established bool , fldPath * field . Path ) field . ErrorList {
2019-04-07 17:07:55 +00:00
requireRecognizedVersion := oldSpec . Conversion == nil || hasValidConversionReviewVersionOrEmpty ( oldSpec . Conversion . ConversionReviewVersions )
allErrs := validateCustomResourceDefinitionSpec ( spec , requireRecognizedVersion , fldPath )
2019-01-12 04:58:27 +00:00
if established {
// these effect the storage and cannot be changed therefore
allErrs = append ( allErrs , genericvalidation . ValidateImmutableField ( spec . Scope , oldSpec . Scope , fldPath . Child ( "scope" ) ) ... )
allErrs = append ( allErrs , genericvalidation . ValidateImmutableField ( spec . Names . Kind , oldSpec . Names . Kind , fldPath . Child ( "names" , "kind" ) ) ... )
}
// these affects the resource name, which is always immutable, so this can't be updated.
allErrs = append ( allErrs , genericvalidation . ValidateImmutableField ( spec . Group , oldSpec . Group , fldPath . Child ( "group" ) ) ... )
allErrs = append ( allErrs , genericvalidation . ValidateImmutableField ( spec . Names . Plural , o ldSpec . Names . Plural , fldPath . Child ( "names" , "plural" ) ) ... )
return allErrs
}
// getSubresourcesForVersion returns the subresources for given version in given CRD spec.
// NOTE That this function assumes version always exist since it's used by the validation process
// that iterates through the existing versions.
func getSubresourcesForVersion ( crd * apiextensions . CustomResourceDefinitionSpec , version string ) * apiextensions . CustomResourceSubresources {
if ! hasPerVersionSubresources ( crd . Versions ) {
return crd . Subresources
}
for _ , v := range crd . Versions {
if version == v . Name {
return v . Subresources
}
}
return nil
}
// hasAnyStatusEnabled returns true if given CRD spec has at least one Status Subresource set
// among the top-level and per-version Subresources.
func hasAnyStatusEnabled ( crd * apiextensions . CustomResourceDefinitionSpec ) bool {
if hasStatusEnabled ( crd . Subresources ) {
return true
}
for _ , v := range crd . Versions {
if hasStatusEnabled ( v . Subresources ) {
return true
}
}
return false
}
// hasStatusEnabled returns true if given CRD Subresources has non-nil Status set.
func hasStatusEnabled ( subresources * apiextensions . CustomResourceSubresources ) bool {
if subresources != nil && subresources . Status != nil {
return true
}
return false
}
// hasPerVersionSchema returns true if a CRD uses per-version schema.
func hasPerVersionSchema ( versions [ ] apiextensions . CustomResourceDefinitionVersion ) bool {
for _ , v := range versions {
if v . Schema != nil {
return true
}
}
return false
}
// hasPerVersionSubresources returns true if a CRD uses per-version subresources.
func hasPerVersionSubresources ( versions [ ] apiextensions . CustomResourceDefinitionVersion ) bool {
for _ , v := range versions {
if v . Subresources != nil {
return true
}
}
return false
}
// hasPerVersionColumns returns true if a CRD uses per-version columns.
func hasPerVersionColumns ( versions [ ] apiextensions . CustomResourceDefinitionVersion ) bool {
for _ , v := range versions {
if len ( v . AdditionalPrinterColumns ) > 0 {
return true
}
}
return false
}
// hasIdenticalPerVersionSchema returns true if a CRD sets identical non-nil values
// to all per-version schemas
func hasIdenticalPerVersionSchema ( versions [ ] apiextensions . CustomResourceDefinitionVersion ) bool {
if len ( versions ) == 0 {
return false
}
value := versions [ 0 ] . Schema
for _ , v := range versions {
if v . Schema == nil || ! apiequality . Semantic . DeepEqual ( v . Schema , value ) {
return false
}
}
return true
}
// hasIdenticalPerVersionSubresources returns true if a CRD sets identical non-nil values
// to all per-version subresources
func hasIdenticalPerVersionSubresources ( versions [ ] apiextensions . CustomResourceDefinitionVersion ) bool {
if len ( versions ) == 0 {
return false
}
value := versions [ 0 ] . Subresources
for _ , v := range versions {
if v . Subresources == nil || ! apiequality . Semantic . DeepEqual ( v . Subresources , value ) {
return false
}
}
return true
}
// hasIdenticalPerVersionColumns returns true if a CRD sets identical non-nil values
// to all per-version columns
func hasIdenticalPerVersionColumns ( versions [ ] apiextensions . CustomResourceDefinitionVersion ) bool {
if len ( versions ) == 0 {
return false
}
value := versions [ 0 ] . AdditionalPrinterColumns
for _ , v := range versions {
if len ( v . AdditionalPrinterColumns ) == 0 || ! apiequality . Semantic . DeepEqual ( v . AdditionalPrinterColumns , value ) {
return false
}
}
return true
}
// ValidateCustomResourceDefinitionStatus statically validates
func ValidateCustomResourceDefinitionStatus ( status * apiextensions . CustomResourceDefinitionStatus , fldPath * field . Path ) field . ErrorList {
allErrs := field . ErrorList { }
allErrs = append ( allErrs , ValidateCustomResourceDefinitionNames ( & status . AcceptedNames , fldPath . Child ( "acceptedNames" ) ) ... )
return allErrs
}
// ValidateCustomResourceDefinitionNames statically validates
func ValidateCustomResourceDefinitionNames ( names * apiextensions . CustomResourceDefinitionNames , fldPath * field . Path ) field . ErrorList {
allErrs := field . ErrorList { }
if errs := validationutil . IsDNS1035Label ( names . Plural ) ; len ( names . Plural ) > 0 && len ( errs ) > 0 {
allErrs = append ( allErrs , field . Invalid ( fldPath . Child ( "plural" ) , names . Plural , strings . Join ( errs , "," ) ) )
}
if errs := validationutil . IsDNS1035Label ( names . Singular ) ; len ( names . Singular ) > 0 && len ( errs ) > 0 {
allErrs = append ( allErrs , field . Invalid ( fldPath . Child ( "singular" ) , names . Singular , strings . Join ( errs , "," ) ) )
}
if errs := validationutil . IsDNS1035Label ( strings . ToLower ( names . Kind ) ) ; len ( names . Kind ) > 0 && len ( errs ) > 0 {
allErrs = append ( allErrs , field . Invalid ( fldPath . Child ( "kind" ) , names . Kind , "may have mixed case, but should otherwise match: " + strings . Join ( errs , "," ) ) )
}
if errs := validationutil . IsDNS1035Label ( strings . ToLower ( names . ListKind ) ) ; len ( names . ListKind ) > 0 && len ( errs ) > 0 {
allErrs = append ( allErrs , field . Invalid ( fldPath . Child ( "listKind" ) , names . ListKind , "may have mixed case, but should otherwise match: " + strings . Join ( errs , "," ) ) )
}
for i , shortName := range names . ShortNames {
if errs := validationutil . IsDNS1035Label ( shortName ) ; len ( errs ) > 0 {
allErrs = append ( allErrs , field . Invalid ( fldPath . Child ( "shortNames" ) . Index ( i ) , shortName , strings . Join ( errs , "," ) ) )
}
}
// kind and listKind may not be the same or parsing become ambiguous
if len ( names . Kind ) > 0 && names . Kind == names . ListKind {
allErrs = append ( allErrs , field . Invalid ( fldPath . Child ( "listKind" ) , names . ListKind , "kind and listKind may not be the same" ) )
}
for i , category := range names . Categories {
if errs := validationutil . IsDNS1035Label ( category ) ; len ( errs ) > 0 {
allErrs = append ( allErrs , field . Invalid ( fldPath . Child ( "categories" ) . Index ( i ) , category , strings . Join ( errs , "," ) ) )
}
}
return allErrs
}
// ValidateCustomResourceColumnDefinition statically validates a printer column.
func ValidateCustomResourceColumnDefinition ( col * apiextensions . CustomResourceColumnDefinition , fldPath * field . Path ) field . ErrorList {
allErrs := field . ErrorList { }
if len ( col . Name ) == 0 {
allErrs = append ( allErrs , field . Required ( fldPath . Child ( "name" ) , "" ) )
}
if len ( col . Type ) == 0 {
allErrs = append ( allErrs , field . Required ( fldPath . Child ( "type" ) , fmt . Sprintf ( "must be one of %s" , strings . Join ( printerColumnDatatypes . List ( ) , "," ) ) ) )
} else if ! printerColumnDatatypes . Has ( col . Type ) {
allErrs = append ( allErrs , field . Invalid ( fldPath . Child ( "type" ) , col . Type , fmt . Sprintf ( "must be one of %s" , strings . Join ( printerColumnDatatypes . List ( ) , "," ) ) ) )
}
if len ( col . Format ) > 0 && ! customResourceColumnDefinitionFormats . Has ( col . Format ) {
allErrs = append ( allErrs , field . Invalid ( fldPath . Child ( "format" ) , col . Format , fmt . Sprintf ( "must be one of %s" , strings . Join ( customResourceColumnDefinitionFormats . List ( ) , "," ) ) ) )
}
if len ( col . JSONPath ) == 0 {
allErrs = append ( allErrs , field . Required ( fldPath . Child ( "JSONPath" ) , "" ) )
} else if errs := validateSimpleJSONPath ( col . JSONPath , fldPath . Child ( "JSONPath" ) ) ; len ( errs ) > 0 {
allErrs = append ( allErrs , errs ... )
}
return allErrs
}
// specStandardValidator applies validations for different OpenAPI specification versions.
type specStandardValidator interface {
validate ( spec * apiextensions . JSONSchemaProps , fldPath * field . Path ) field . ErrorList
}
// ValidateCustomResourceDefinitionValidation statically validates
func ValidateCustomResourceDefinitionValidation ( customResourceValidation * apiextensions . CustomResourceValidation , statusSubresourceEnabled bool , fldPath * field . Path ) field . ErrorList {
allErrs := field . ErrorList { }
if customResourceValidation == nil {
return allErrs
}
if schema := customResourceValidation . OpenAPIV3Schema ; schema != nil {
// if the status subresource is enabled, only certain fields are allowed inside the root schema.
// these fields are chosen such that, if status is extracted as properties["status"], it's validation is not lost.
2019-04-07 17:07:55 +00:00
if statusSubresourceEnabled {
2019-01-12 04:58:27 +00:00
v := reflect . ValueOf ( schema ) . Elem ( )
for i := 0 ; i < v . NumField ( ) ; i ++ {
// skip zero values
if value := v . Field ( i ) . Interface ( ) ; reflect . DeepEqual ( value , reflect . Zero ( reflect . TypeOf ( value ) ) . Interface ( ) ) {
continue
}
fieldName := v . Type ( ) . Field ( i ) . Name
// only "object" type is valid at root of the schema since validation schema for status is extracted as properties["status"]
if fieldName == "Type" {
if schema . Type != "object" {
allErrs = append ( allErrs , field . Invalid ( fldPath . Child ( "openAPIV3Schema.type" ) , schema . Type , fmt . Sprintf ( ` only "object" is allowed as the type at the root of the schema if the status subresource is enabled ` ) ) )
break
}
continue
}
if ! allowedAtRootSchema ( fieldName ) {
allErrs = append ( allErrs , field . Invalid ( fldPath . Child ( "openAPIV3Schema" ) , * schema , fmt . Sprintf ( ` only %v fields are allowed at the root of the schema if the status subresource is enabled ` , allowedFieldsAtRootSchema ) ) )
break
}
}
}
2019-04-07 17:07:55 +00:00
if schema . Nullable {
allErrs = append ( allErrs , field . Forbidden ( fldPath . Child ( "openAPIV3Schema.nullable" ) , fmt . Sprintf ( ` nullable cannot be true at the root ` ) ) )
}
2019-01-12 04:58:27 +00:00
openAPIV3Schema := & specStandardValidatorV3 { }
allErrs = append ( allErrs , ValidateCustomResourceDefinitionOpenAPISchema ( schema , fldPath . Child ( "openAPIV3Schema" ) , openAPIV3Schema ) ... )
}
return allErrs
}
// ValidateCustomResourceDefinitionOpenAPISchema statically validates
func ValidateCustomResourceDefinitionOpenAPISchema ( schema * apiextensions . JSONSchemaProps , fldPath * field . Path , ssv specStandardValidator ) field . ErrorList {
allErrs := field . ErrorList { }
if schema == nil {
return allErrs
}
allErrs = append ( allErrs , ssv . validate ( schema , fldPath ) ... )
if schema . UniqueItems == true {
allErrs = append ( allErrs , field . Forbidden ( fldPath . Child ( "uniqueItems" ) , "uniqueItems cannot be set to true since the runtime complexity becomes quadratic" ) )
}
// additionalProperties and properties are mutual exclusive because otherwise they
// contradict Kubernetes' API convention to ignore unknown fields.
//
// In other words:
// - properties are for structs,
// - additionalProperties are for map[string]interface{}
//
// Note: when patternProperties is added to OpenAPI some day, this will have to be
// restricted like additionalProperties.
if schema . AdditionalProperties != nil {
if len ( schema . Properties ) != 0 {
if schema . AdditionalProperties . Allows == false || schema . AdditionalProperties . Schema != nil {
allErrs = append ( allErrs , field . Forbidden ( fldPath . Child ( "additionalProperties" ) , "additionalProperties and properties are mutual exclusive" ) )
}
}
allErrs = append ( allErrs , ValidateCustomResourceDefinitionOpenAPISchema ( schema . AdditionalProperties . Schema , fldPath . Child ( "additionalProperties" ) , ssv ) ... )
}
if len ( schema . Properties ) != 0 {
for property , jsonSchema := range schema . Properties {
allErrs = append ( allErrs , ValidateCustomResourceDefinitionOpenAPISchema ( & jsonSchema , fldPath . Child ( "properties" ) . Key ( property ) , ssv ) ... )
}
}
if len ( schema . PatternProperties ) != 0 {
for property , jsonSchema := range schema . PatternProperties {
allErrs = append ( allErrs , ValidateCustomResourceDefinitionOpenAPISchema ( & jsonSchema , fldPath . Child ( "patternProperties" ) . Key ( property ) , ssv ) ... )
}
}
if schema . AdditionalItems != nil {
allErrs = append ( allErrs , ValidateCustomResourceDefinitionOpenAPISchema ( schema . AdditionalItems . Schema , fldPath . Child ( "additionalItems" ) , ssv ) ... )
}
allErrs = append ( allErrs , ValidateCustomResourceDefinitionOpenAPISchema ( schema . Not , fldPath . Child ( "not" ) , ssv ) ... )
if len ( schema . AllOf ) != 0 {
for i , jsonSchema := range schema . AllOf {
allErrs = append ( allErrs , ValidateCustomResourceDefinitionOpenAPISchema ( & jsonSchema , fldPath . Child ( "allOf" ) . Index ( i ) , ssv ) ... )
}
}
if len ( schema . OneOf ) != 0 {
for i , jsonSchema := range schema . OneOf {
allErrs = append ( allErrs , ValidateCustomResourceDefinitionOpenAPISchema ( & jsonSchema , fldPath . Child ( "oneOf" ) . Index ( i ) , ssv ) ... )
}
}
if len ( schema . AnyOf ) != 0 {
for i , jsonSchema := range schema . AnyOf {
allErrs = append ( allErrs , ValidateCustomResourceDefinitionOpenAPISchema ( & jsonSchema , fldPath . Child ( "anyOf" ) . Index ( i ) , ssv ) ... )
}
}
if len ( schema . Definitions ) != 0 {
for definition , jsonSchema := range schema . Definitions {
allErrs = append ( allErrs , ValidateCustomResourceDefinitionOpenAPISchema ( & jsonSchema , fldPath . Child ( "definitions" ) . Key ( definition ) , ssv ) ... )
}
}
if schema . Items != nil {
allErrs = append ( allErrs , ValidateCustomResourceDefinitionOpenAPISchema ( schema . Items . Schema , fldPath . Child ( "items" ) , ssv ) ... )
if len ( schema . Items . JSONSchemas ) != 0 {
for i , jsonSchema := range schema . Items . JSONSchemas {
allErrs = append ( allErrs , ValidateCustomResourceDefinitionOpenAPISchema ( & jsonSchema , fldPath . Child ( "items" ) . Index ( i ) , ssv ) ... )
}
}
}
if schema . Dependencies != nil {
for dependency , jsonSchemaPropsOrStringArray := range schema . Dependencies {
allErrs = append ( allErrs , ValidateCustomResourceDefinitionOpenAPISchema ( jsonSchemaPropsOrStringArray . Schema , fldPath . Child ( "dependencies" ) . Key ( dependency ) , ssv ) ... )
}
}
return allErrs
}
type specStandardValidatorV3 struct { }
// validate validates against OpenAPI Schema v3.
func ( v * specStandardValidatorV3 ) validate ( schema * apiextensions . JSONSchemaProps , fldPath * field . Path ) field . ErrorList {
allErrs := field . ErrorList { }
if schema == nil {
return allErrs
}
if schema . Default != nil {
allErrs = append ( allErrs , field . Forbidden ( fldPath . Child ( "default" ) , "default is not supported" ) )
}
if schema . ID != "" {
allErrs = append ( allErrs , field . Forbidden ( fldPath . Child ( "id" ) , "id is not supported" ) )
}
if schema . AdditionalItems != nil {
allErrs = append ( allErrs , field . Forbidden ( fldPath . Child ( "additionalItems" ) , "additionalItems is not supported" ) )
}
if len ( schema . PatternProperties ) != 0 {
allErrs = append ( allErrs , field . Forbidden ( fldPath . Child ( "patternProperties" ) , "patternProperties is not supported" ) )
}
if len ( schema . Definitions ) != 0 {
allErrs = append ( allErrs , field . Forbidden ( fldPath . Child ( "definitions" ) , "definitions is not supported" ) )
}
if schema . Dependencies != nil {
allErrs = append ( allErrs , field . Forbidden ( fldPath . Child ( "dependencies" ) , "dependencies is not supported" ) )
}
if schema . Ref != nil {
allErrs = append ( allErrs , field . Forbidden ( fldPath . Child ( "$ref" ) , "$ref is not supported" ) )
}
if schema . Type == "null" {
2019-04-07 17:07:55 +00:00
allErrs = append ( allErrs , field . Forbidden ( fldPath . Child ( "type" ) , "type cannot be set to null, use nullable as an alternative" ) )
2019-01-12 04:58:27 +00:00
}
if schema . Items != nil && len ( schema . Items . JSONSchemas ) != 0 {
allErrs = append ( allErrs , field . Forbidden ( fldPath . Child ( "items" ) , "items must be a schema object and not an array" ) )
}
return allErrs
}
// ValidateCustomResourceDefinitionSubresources statically validates
func ValidateCustomResourceDefinitionSubresources ( subresources * apiextensions . CustomResourceSubresources , fldPath * field . Path ) field . ErrorList {
allErrs := field . ErrorList { }
if subresources == nil {
return allErrs
}
if subresources . Scale != nil {
if len ( subresources . Scale . SpecReplicasPath ) == 0 {
allErrs = append ( allErrs , field . Required ( fldPath . Child ( "scale.specReplicasPath" ) , "" ) )
} else {
// should be constrained json path under .spec
if errs := validateSimpleJSONPath ( subresources . Scale . SpecReplicasPath , fldPath . Child ( "scale.specReplicasPath" ) ) ; len ( errs ) > 0 {
allErrs = append ( allErrs , errs ... )
} else if ! strings . HasPrefix ( subresources . Scale . SpecReplicasPath , ".spec." ) {
allErrs = append ( allErrs , field . Invalid ( fldPath . Child ( "scale.specReplicasPath" ) , subresources . Scale . SpecReplicasPath , "should be a json path under .spec" ) )
}
}
if len ( subresources . Scale . StatusReplicasPath ) == 0 {
allErrs = append ( allErrs , field . Required ( fldPath . Child ( "scale.statusReplicasPath" ) , "" ) )
} else {
// should be constrained json path under .status
if errs := validateSimpleJSONPath ( subresources . Scale . StatusReplicasPath , fldPath . Child ( "scale.statusReplicasPath" ) ) ; len ( errs ) > 0 {
allErrs = append ( allErrs , errs ... )
} else if ! strings . HasPrefix ( subresources . Scale . StatusReplicasPath , ".status." ) {
allErrs = append ( allErrs , field . Invalid ( fldPath . Child ( "scale.statusReplicasPath" ) , subresources . Scale . StatusReplicasPath , "should be a json path under .status" ) )
}
}
// if labelSelectorPath is present, it should be a constrained json path under .status
if subresources . Scale . LabelSelectorPath != nil && len ( * subresources . Scale . LabelSelectorPath ) > 0 {
if errs := validateSimpleJSONPath ( * subresources . Scale . LabelSelectorPath , fldPath . Child ( "scale.labelSelectorPath" ) ) ; len ( errs ) > 0 {
allErrs = append ( allErrs , errs ... )
} else if ! strings . HasPrefix ( * subresources . Scale . LabelSelectorPath , ".status." ) {
allErrs = append ( allErrs , field . Invalid ( fldPath . Child ( "scale.labelSelectorPath" ) , subresources . Scale . LabelSelectorPath , "should be a json path under .status" ) )
}
}
}
return allErrs
}
func validateSimpleJSONPath ( s string , fldPath * field . Path ) field . ErrorList {
allErrs := field . ErrorList { }
switch {
case len ( s ) == 0 :
allErrs = append ( allErrs , field . Invalid ( fldPath , s , "must not be empty" ) )
case s [ 0 ] != '.' :
allErrs = append ( allErrs , field . Invalid ( fldPath , s , "must be a simple json path starting with ." ) )
case s != "." :
if cs := strings . Split ( s [ 1 : ] , "." ) ; len ( cs ) < 1 {
allErrs = append ( allErrs , field . Invalid ( fldPath , s , "must be a json path in the dot notation" ) )
}
}
return allErrs
}
var allowedFieldsAtRootSchema = [ ] string { "Description" , "Type" , "Format" , "Title" , "Maximum" , "ExclusiveMaximum" , "Minimum" , "ExclusiveMinimum" , "MaxLength" , "MinLength" , "Pattern" , "MaxItems" , "MinItems" , "UniqueItems" , "MultipleOf" , "Required" , "Items" , "Properties" , "ExternalDocs" , "Example" }
func allowedAtRootSchema ( field string ) bool {
for _ , v := range allowedFieldsAtRootSchema {
if field == v {
return true
}
}
return false
}