diff --git a/pkg/api/defaulting_test.go b/pkg/api/defaulting_test.go index 8572e8dda57..f850fd8b160 100644 --- a/pkg/api/defaulting_test.go +++ b/pkg/api/defaulting_test.go @@ -103,24 +103,28 @@ func TestDefaulting(t *testing.T) { // This object contains only int fields which currently breaks the defaulting test because // it's pretty stupid. Once we add non integer fields, we should uncomment this. // {Group: "kubeadm.k8s.io", Version: "v1alpha1", Kind: "NodeConfiguration"}: {}, - {Group: "extensions", Version: "v1beta1", Kind: "DaemonSet"}: {}, - {Group: "extensions", Version: "v1beta1", Kind: "DaemonSetList"}: {}, - {Group: "extensions", Version: "v1beta1", Kind: "Deployment"}: {}, - {Group: "extensions", Version: "v1beta1", Kind: "DeploymentList"}: {}, - {Group: "apps", Version: "v1beta1", Kind: "Deployment"}: {}, - {Group: "apps", Version: "v1beta1", Kind: "DeploymentList"}: {}, - {Group: "extensions", Version: "v1beta1", Kind: "ReplicaSet"}: {}, - {Group: "extensions", Version: "v1beta1", Kind: "ReplicaSetList"}: {}, - {Group: "rbac.authorization.k8s.io", Version: "v1alpha1", Kind: "ClusterRoleBinding"}: {}, - {Group: "rbac.authorization.k8s.io", Version: "v1alpha1", Kind: "ClusterRoleBindingList"}: {}, - {Group: "rbac.authorization.k8s.io", Version: "v1alpha1", Kind: "RoleBinding"}: {}, - {Group: "rbac.authorization.k8s.io", Version: "v1alpha1", Kind: "RoleBindingList"}: {}, - {Group: "rbac.authorization.k8s.io", Version: "v1beta1", Kind: "ClusterRoleBinding"}: {}, - {Group: "rbac.authorization.k8s.io", Version: "v1beta1", Kind: "ClusterRoleBindingList"}: {}, - {Group: "rbac.authorization.k8s.io", Version: "v1beta1", Kind: "RoleBinding"}: {}, - {Group: "rbac.authorization.k8s.io", Version: "v1beta1", Kind: "RoleBindingList"}: {}, - {Group: "settings.k8s.io", Version: "v1alpha1", Kind: "PodPreset"}: {}, - {Group: "settings.k8s.io", Version: "v1alpha1", Kind: "PodPresetList"}: {}, + {Group: "extensions", Version: "v1beta1", Kind: "DaemonSet"}: {}, + {Group: "extensions", Version: "v1beta1", Kind: "DaemonSetList"}: {}, + {Group: "extensions", Version: "v1beta1", Kind: "Deployment"}: {}, + {Group: "extensions", Version: "v1beta1", Kind: "DeploymentList"}: {}, + {Group: "apps", Version: "v1beta1", Kind: "Deployment"}: {}, + {Group: "apps", Version: "v1beta1", Kind: "DeploymentList"}: {}, + {Group: "extensions", Version: "v1beta1", Kind: "ReplicaSet"}: {}, + {Group: "extensions", Version: "v1beta1", Kind: "ReplicaSetList"}: {}, + {Group: "rbac.authorization.k8s.io", Version: "v1alpha1", Kind: "ClusterRoleBinding"}: {}, + {Group: "rbac.authorization.k8s.io", Version: "v1alpha1", Kind: "ClusterRoleBindingList"}: {}, + {Group: "rbac.authorization.k8s.io", Version: "v1alpha1", Kind: "RoleBinding"}: {}, + {Group: "rbac.authorization.k8s.io", Version: "v1alpha1", Kind: "RoleBindingList"}: {}, + {Group: "rbac.authorization.k8s.io", Version: "v1beta1", Kind: "ClusterRoleBinding"}: {}, + {Group: "rbac.authorization.k8s.io", Version: "v1beta1", Kind: "ClusterRoleBindingList"}: {}, + {Group: "rbac.authorization.k8s.io", Version: "v1beta1", Kind: "RoleBinding"}: {}, + {Group: "rbac.authorization.k8s.io", Version: "v1beta1", Kind: "RoleBindingList"}: {}, + {Group: "settings.k8s.io", Version: "v1alpha1", Kind: "PodPreset"}: {}, + {Group: "settings.k8s.io", Version: "v1alpha1", Kind: "PodPresetList"}: {}, + {Group: "admissionregistration.k8s.io", Version: "v1alpha1", Kind: "InitializerConfiguration"}: {}, + {Group: "admissionregistration.k8s.io", Version: "v1alpha1", Kind: "InitializerConfigurationList"}: {}, + {Group: "admissionregistration.k8s.io", Version: "v1alpha1", Kind: "ExternalAdmissionHookConfiguration"}: {}, + {Group: "admissionregistration.k8s.io", Version: "v1alpha1", Kind: "ExternalAdmissionHookConfigurationList"}: {}, } f := fuzz.New().NilChance(.5).NumElements(1, 1).RandSource(rand.NewSource(1)) diff --git a/pkg/api/testapi/testapi.go b/pkg/api/testapi/testapi.go index 65a429ab303..c7e7dbc9366 100644 --- a/pkg/api/testapi/testapi.go +++ b/pkg/api/testapi/testapi.go @@ -36,6 +36,7 @@ import ( "k8s.io/apimachinery/pkg/runtime/serializer/recognizer" "k8s.io/kubernetes/federation/apis/federation" "k8s.io/kubernetes/pkg/api" + "k8s.io/kubernetes/pkg/apis/admissionregistration" "k8s.io/kubernetes/pkg/apis/apps" "k8s.io/kubernetes/pkg/apis/authorization" "k8s.io/kubernetes/pkg/apis/autoscaling" @@ -50,6 +51,7 @@ import ( _ "k8s.io/kubernetes/federation/apis/federation/install" _ "k8s.io/kubernetes/pkg/api/install" + _ "k8s.io/kubernetes/pkg/apis/admissionregistration/install" _ "k8s.io/kubernetes/pkg/apis/apps/install" _ "k8s.io/kubernetes/pkg/apis/authentication/install" _ "k8s.io/kubernetes/pkg/apis/authorization/install" @@ -278,7 +280,15 @@ func init() { externalTypes: api.Scheme.KnownTypes(externalGroupVersion), } } - + if _, ok := Groups[admissionregistration.GroupName]; !ok { + externalGroupVersion := schema.GroupVersion{Group: admissionregistration.GroupName, Version: api.Registry.GroupOrDie(admissionregistration.GroupName).GroupVersion.Version} + Groups[admissionregistration.GroupName] = TestGroup{ + externalGroupVersion: externalGroupVersion, + internalGroupVersion: admissionregistration.SchemeGroupVersion, + internalTypes: api.Scheme.KnownTypes(admissionregistration.SchemeGroupVersion), + externalTypes: api.Scheme.KnownTypes(externalGroupVersion), + } + } Default = Groups[api.GroupName] Autoscaling = Groups[autoscaling.GroupName] Batch = Groups[batch.GroupName] diff --git a/pkg/api/testing/fuzzer.go b/pkg/api/testing/fuzzer.go index ee9ca68723e..e3884e42e85 100644 --- a/pkg/api/testing/fuzzer.go +++ b/pkg/api/testing/fuzzer.go @@ -36,6 +36,7 @@ import ( "k8s.io/kubernetes/pkg/api" "k8s.io/kubernetes/pkg/api/testapi" "k8s.io/kubernetes/pkg/api/v1" + "k8s.io/kubernetes/pkg/apis/admissionregistration" "k8s.io/kubernetes/pkg/apis/apps" "k8s.io/kubernetes/pkg/apis/autoscaling" "k8s.io/kubernetes/pkg/apis/batch" @@ -738,6 +739,21 @@ func certificateFuncs(t apitesting.TestingCommon) []interface{} { } } +func admissionregistrationFuncs(t apitesting.TestingCommon) []interface{} { + return []interface{}{ + func(obj *admissionregistration.ExternalAdmissionHook, c fuzz.Continue) { + c.FuzzNoCustom(obj) // fuzz self without calling this function again + p := admissionregistration.FailurePolicyType("Fail") + obj.FailurePolicy = &p + }, + func(obj *admissionregistration.Initializer, c fuzz.Continue) { + c.FuzzNoCustom(obj) // fuzz self without calling this function again + p := admissionregistration.FailurePolicyType("Fail") + obj.FailurePolicy = &p + }, + } +} + func FuzzerFuncs(t apitesting.TestingCommon, codecs runtimeserializer.CodecFactory) []interface{} { return apitesting.MergeFuzzerFuncs(t, apitesting.GenericFuzzerFuncs(t, codecs), @@ -751,6 +767,7 @@ func FuzzerFuncs(t apitesting.TestingCommon, codecs runtimeserializer.CodecFacto kubeadmfuzzer.KubeadmFuzzerFuncs(t), policyFuncs(t), certificateFuncs(t), + admissionregistrationFuncs(t), ) } diff --git a/pkg/apis/admissionregistration/doc.go b/pkg/apis/admissionregistration/doc.go new file mode 100644 index 00000000000..5dfc4e74e37 --- /dev/null +++ b/pkg/apis/admissionregistration/doc.go @@ -0,0 +1,24 @@ +/* +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. +*/ + +// +k8s:deepcopy-gen=package,register + +// Package admissionregistration is the internal version of the API. +// AdmissionConfiguration and AdmissionPluginConfiguration are legacy static admission plugin configuration +// InitializerConfiguration and ExternalAdmissionHookConfiguration is for the +// new dynamic admission controller configuration. +// +groupName=admissionregistration.k8s.io +package admissionregistration // import "k8s.io/kubernetes/pkg/apis/admissionregistration" diff --git a/pkg/apis/admissionregistration/install/install.go b/pkg/apis/admissionregistration/install/install.go new file mode 100644 index 00000000000..113c8c6dec4 --- /dev/null +++ b/pkg/apis/admissionregistration/install/install.go @@ -0,0 +1,44 @@ +/* +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 install + +import ( + "k8s.io/apimachinery/pkg/apimachinery/announced" + "k8s.io/apimachinery/pkg/apimachinery/registered" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/util/sets" + "k8s.io/kubernetes/pkg/apis/admissionregistration" + "k8s.io/kubernetes/pkg/apis/admissionregistration/v1alpha1" +) + +// Install registers the API group and adds types to a scheme +func Install(groupFactoryRegistry announced.APIGroupFactoryRegistry, registry *registered.APIRegistrationManager, scheme *runtime.Scheme) { + if err := announced.NewGroupMetaFactory( + &announced.GroupMetaFactoryArgs{ + GroupName: admissionregistration.GroupName, + RootScopedKinds: sets.NewString("InitializerConfiguration", "ExternalAdmissionHookConfiguration"), + VersionPreferenceOrder: []string{v1alpha1.SchemeGroupVersion.Version}, + ImportPrefix: "k8s.io/kubernetes/pkg/apis/admissionregistration", + AddInternalObjectsToScheme: admissionregistration.AddToScheme, + }, + announced.VersionToSchemeFunc{ + v1alpha1.SchemeGroupVersion.Version: v1alpha1.AddToScheme, + }, + ).Announce(groupFactoryRegistry).RegisterAndEnable(registry, scheme); err != nil { + panic(err) + } +} diff --git a/pkg/apis/admissionregistration/register.go b/pkg/apis/admissionregistration/register.go new file mode 100644 index 00000000000..1fe291a8f23 --- /dev/null +++ b/pkg/apis/admissionregistration/register.go @@ -0,0 +1,53 @@ +/* +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 admissionregistration + +import ( + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +const GroupName = "admissionregistration.k8s.io" + +// SchemeGroupVersion is group version used to register these objects +var SchemeGroupVersion = schema.GroupVersion{Group: GroupName, Version: runtime.APIVersionInternal} + +// Kind takes an unqualified kind and returns back a Group qualified GroupKind +func Kind(kind string) schema.GroupKind { + return SchemeGroupVersion.WithKind(kind).GroupKind() +} + +// Resource takes an unqualified resource and returns back a Group qualified GroupResource +func Resource(resource string) schema.GroupResource { + return SchemeGroupVersion.WithResource(resource).GroupResource() +} + +var ( + SchemeBuilder = runtime.NewSchemeBuilder(addKnownTypes) + AddToScheme = SchemeBuilder.AddToScheme +) + +// Adds the list of known types to scheme. +func addKnownTypes(scheme *runtime.Scheme) error { + scheme.AddKnownTypes(SchemeGroupVersion, + &InitializerConfiguration{}, + &InitializerConfigurationList{}, + &ExternalAdmissionHookConfiguration{}, + &ExternalAdmissionHookConfigurationList{}, + ) + return nil +} diff --git a/pkg/apis/admissionregistration/types.go b/pkg/apis/admissionregistration/types.go new file mode 100644 index 00000000000..f10b51c8ac4 --- /dev/null +++ b/pkg/apis/admissionregistration/types.go @@ -0,0 +1,206 @@ +/* +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 admissionregistration + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// InitializerConfiguration describes the configuration of initializers. +type InitializerConfiguration struct { + metav1.TypeMeta + // Standard object metadata; More info: http://releases.k8s.io/HEAD/docs/devel/api-conventions.md#metadata. + // +optional + metav1.ObjectMeta + + // Initializers is a list of resources and their default initializers + // Order-sensitive. + // When merging multiple InitializerConfigurations, we sort the initializers + // from different InitializerConfigurations by the name of the + // InitializerConfigurations; the order of the initializers from the same + // InitializerConfiguration is preserved. + // +optional + Initializers []Initializer +} + +// InitializerConfigurationList is a list of InitializerConfiguration. +type InitializerConfigurationList struct { + metav1.TypeMeta + // Standard list metadata. + // More info: http://releases.k8s.io/HEAD/docs/devel/api-conventions.md#types-kinds + // +optional + metav1.ListMeta + + // List of InitializerConfiguration. + Items []InitializerConfiguration +} + +// Initializer describes the name and the failure policy of an initializer, and +// what resources it applies to. +type Initializer struct { + // Name is the identifier of the initializer. It will be added to the + // object that needs to be initialized. + // Name should be fully qualified, e.g., alwayspullimages.kubernetes.io, where + // "alwayspullimages" is the name of the webhook, and kubernetes.io is the name + // of the organization. + // Required + Name string + + // Rules describes what resources/subresources the initializer cares about. + // The initializer cares about an operation if it matches _any_ Rule. + Rules []Rule + + // FailurePolicy defines what happens if the responsible initializer controller + // fails to takes action. Allowed values are Ignore, or Fail. If "Ignore" is + // set, initializer is removed from the initializers list of an object if + // the timeout is reached; If "Fail" is set, admissionregistration returns timeout error + // if the timeout is reached. + FailurePolicy *FailurePolicyType +} + +// Rule is a tuple of APIGroups, APIVersion, and Resources.It is recommended +// to make sure that all the tuple expansions are valid. +type Rule struct { + // APIGroups is the API groups the resources belong to. '*' is all groups. + // If '*' is present, the length of the slice must be one. + // Required. + APIGroups []string + + // APIVersions is the API versions the resources belong to. '*' is all versions. + // If '*' is present, the length of the slice must be one. + // Required. + APIVersions []string + + // Resources is a list of resources this rule applies to. + // + // For example: + // 'pods' means pods. + // 'pods/log' means the log subresource of pods. + // '*' means all resources, but not subresources. + // 'pods/*' means all subresources of pods. + // '*/scale' means all scale subresources. + // '*/*' means all resources and their subresources. + // + // If '*' or '*/*' is present, the length of the slice must be one. + // Required. + Resources []string +} + +type FailurePolicyType string + +const ( + // Ignore means the initilizer is removed from the initializers list of an + // object if the initializer is timed out. + Ignore FailurePolicyType = "Ignore" + // For 1.7, only "Ignore" is allowed. "Fail" will be allowed when the + // extensible admission feature is beta. + Fail FailurePolicyType = "Fail" +) + +// ExternalAdmissionHookConfiguration describes the configuration of initializers. +type ExternalAdmissionHookConfiguration struct { + metav1.TypeMeta + // Standard object metadata; More info: http://releases.k8s.io/HEAD/docs/devel/api-conventions.md#metadata. + // +optional + metav1.ObjectMeta + // ExternalAdmissionHooks is a list of external admission webhooks and the + // affected resources and operations. + // +optional + ExternalAdmissionHooks []ExternalAdmissionHook +} + +// ExternalAdmissionHookConfigurationList is a list of ExternalAdmissionHookConfiguration. +type ExternalAdmissionHookConfigurationList struct { + metav1.TypeMeta + // Standard list metadata. + // More info: http://releases.k8s.io/HEAD/docs/devel/api-conventions.md#types-kinds + // +optional + metav1.ListMeta + // List of ExternalAdmissionHookConfiguration. + Items []ExternalAdmissionHookConfiguration +} + +// ExternalAdmissionHook describes an external admission webhook and the +// resources and operations it applies to. +type ExternalAdmissionHook struct { + // The name of the external admission webhook. + // Name should be fully qualified, e.g., imagepolicy.kubernetes.io, where + // "imagepolicy" is the name of the webhook, and kubernetes.io is the name + // of the organization. + // Required. + Name string + + // ClientConfig defines how to communicate with the hook. + // Required + ClientConfig AdmissionHookClientConfig + + // Rules describes what operations on what resources/subresources the webhook cares about. + // The webhook cares about an operation if it matches _any_ Rule. + Rules []RuleWithOperations + + // FailurePolicy defines how unrecognized errors from the admission endpoint are handled - + // allowed values are Ignore or Fail. Defaults to Ignore. + // +optional + FailurePolicy *FailurePolicyType +} + +// RuleWithOperations is a tuple of Operations and Resources. It is recommended to make +// sure that all the tuple expansions are valid. +type RuleWithOperations struct { + // Operations is the operations the admission hook cares about - CREATE, UPDATE, or * + // for all operations. + // If '*' is present, the length of the slice must be one. + // Required. + Operations []OperationType + // Rule is embedded, it describes other criteria of the rule, like + // APIGroups, APIVersions, Resources, etc. + Rule +} + +type OperationType string + +// The constants should be kept in sync with those defined in k8s.io/kubernetes/pkg/admission/interface.go. +const ( + OperationAll OperationType = "*" + Create OperationType = "CREATE" + Update OperationType = "UPDATE" + Delete OperationType = "DELETE" + Connect OperationType = "CONNECT" +) + +// AdmissionHookClientConfig contains the information to make a TLS +// connection with the webhook +type AdmissionHookClientConfig struct { + // Service is a reference to the service for this webhook. If there is only + // one port open for the service, that port will be used. If there are multiple + // ports open, port 443 will be used if it is open, otherwise it is an error. + // Required + Service ServiceReference + // CABundle is a PEM encoded CA bundle which will be used to validate webhook's server certificate. + // Required + CABundle []byte +} + +// ServiceReference holds a reference to Service.legacy.k8s.io +type ServiceReference struct { + // Namespace is the namespace of the service + // Required + Namespace string + // Name is the name of the service + // Required + Name string +} diff --git a/pkg/apis/admissionregistration/v1alpha1/defaults.go b/pkg/apis/admissionregistration/v1alpha1/defaults.go new file mode 100644 index 00000000000..5aed8f1d787 --- /dev/null +++ b/pkg/apis/admissionregistration/v1alpha1/defaults.go @@ -0,0 +1,39 @@ +/* +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 v1alpha1 + +import ( + "k8s.io/apimachinery/pkg/runtime" +) + +func addDefaultingFuncs(scheme *runtime.Scheme) error { + return RegisterDefaults(scheme) +} + +func SetDefaults_Initializer(obj *Initializer) { + if obj.FailurePolicy == nil { + policy := Ignore + obj.FailurePolicy = &policy + } +} + +func SetDefaults_ExternalAdmissionHook(obj *ExternalAdmissionHook) { + if obj.FailurePolicy == nil { + policy := Ignore + obj.FailurePolicy = &policy + } +} diff --git a/pkg/apis/admissionregistration/v1alpha1/doc.go b/pkg/apis/admissionregistration/v1alpha1/doc.go new file mode 100644 index 00000000000..9f75528f9aa --- /dev/null +++ b/pkg/apis/admissionregistration/v1alpha1/doc.go @@ -0,0 +1,27 @@ +/* +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. +*/ + +// +k8s:deepcopy-gen=package,register +// +k8s:conversion-gen=k8s.io/kubernetes/pkg/apis/admissionregistration +// +k8s:openapi-gen=true +// +k8s:defaulter-gen=TypeMeta + +// Package v1alpha1 is the v1alpha1 version of the API. +// AdmissionConfiguration and AdmissionPluginConfiguration are legacy static admission plugin configuration +// InitializerConfiguration and ExternalAdmissionHookConfiguration is for the +// new dynamic admission controller configuration. +// +groupName=admissionregistration.k8s.io +package v1alpha1 // import "k8s.io/kubernetes/pkg/apis/admissionregistration/v1alpha1" diff --git a/pkg/apis/admissionregistration/v1alpha1/register.go b/pkg/apis/admissionregistration/v1alpha1/register.go new file mode 100644 index 00000000000..10fa4a161b5 --- /dev/null +++ b/pkg/apis/admissionregistration/v1alpha1/register.go @@ -0,0 +1,60 @@ +/* +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 v1alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +const GroupName = "admissionregistration.k8s.io" + +// SchemeGroupVersion is group version used to register these objects +var SchemeGroupVersion = schema.GroupVersion{Group: GroupName, Version: "v1alpha1"} + +// Resource takes an unqualified resource and returns a Group qualified GroupResource +func Resource(resource string) schema.GroupResource { + return SchemeGroupVersion.WithResource(resource).GroupResource() +} + +var ( + // TODO: move SchemeBuilder with zz_generated.deepcopy.go to k8s.io/api. + // localSchemeBuilder and AddToScheme will stay in k8s.io/kubernetes. + SchemeBuilder runtime.SchemeBuilder + localSchemeBuilder = &SchemeBuilder + AddToScheme = localSchemeBuilder.AddToScheme +) + +func init() { + // We only register manually written functions here. The registration of the + // generated functions takes place in the generated files. The separation + // makes the code compile even when the generated files are missing. + localSchemeBuilder.Register(addKnownTypes, addDefaultingFuncs) +} + +// Adds the list of known types to scheme. +func addKnownTypes(scheme *runtime.Scheme) error { + scheme.AddKnownTypes(SchemeGroupVersion, + &InitializerConfiguration{}, + &InitializerConfigurationList{}, + &ExternalAdmissionHookConfiguration{}, + &ExternalAdmissionHookConfigurationList{}, + ) + metav1.AddToGroupVersion(scheme, SchemeGroupVersion) + return nil +} diff --git a/pkg/apis/admissionregistration/v1alpha1/types.go b/pkg/apis/admissionregistration/v1alpha1/types.go new file mode 100644 index 00000000000..206038313be --- /dev/null +++ b/pkg/apis/admissionregistration/v1alpha1/types.go @@ -0,0 +1,210 @@ +/* +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 v1alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// InitializerConfiguration describes the configuration of initializers. +type InitializerConfiguration struct { + metav1.TypeMeta `json:",inline"` + // Standard object metadata; More info: http://releases.k8s.io/HEAD/docs/devel/api-conventions.md#metadata. + // +optional + metav1.ObjectMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"` + + // Initializers is a list of resources and their default initializers + // Order-sensitive. + // When merging multiple InitializerConfigurations, we sort the initializers + // from different InitializerConfigurations by the name of the + // InitializerConfigurations; the order of the initializers from the same + // InitializerConfiguration is preserved. + // +patchMergeKey=name + // +patchStrategy=merge + // +optional + Initializers []Initializer `json:"initializers,omitempty" patchStrategy:"merge" patchMergeKey:"name" protobuf:"bytes,2,rep,name=initializers"` +} + +// InitializerConfigurationList is a list of InitializerConfiguration. +type InitializerConfigurationList struct { + metav1.TypeMeta `json:",inline"` + // Standard list metadata. + // More info: http://releases.k8s.io/HEAD/docs/devel/api-conventions.md#types-kinds + // +optional + metav1.ListMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"` + + // List of InitializerConfiguration. + Items []InitializerConfiguration `json:"items" protobuf:"bytes,2,rep,name=items"` +} + +// Initializer describes the name and the failure policy of an initializer, and +// what resources it applies to. +type Initializer struct { + // Name is the identifier of the initializer. It will be added to the + // object that needs to be initialized. + // Name should be fully qualified, e.g., alwayspullimages.kubernetes.io, where + // "alwayspullimages" is the name of the webhook, and kubernetes.io is the name + // of the organization. + // Required + Name string `json:"name" protobuf:"bytes,1,opt,name=name"` + + // Rules describes what resources/subresources the initializer cares about. + // The initializer cares about an operation if it matches _any_ Rule. + Rules []Rule `json:"rules,omitempty" protobuf:"bytes,2,rep,name=rules"` + + // FailurePolicy defines what happens if the responsible initializer controller + // fails to takes action. Allowed values are Ignore, or Fail. If "Ignore" is + // set, initializer is removed from the initializers list of an object if + // the timeout is reached; If "Fail" is set, admissionregistration returns timeout error + // if the timeout is reached. + FailurePolicy *FailurePolicyType `json:"failurePolicy,omitempty" protobuf:"bytes,3,opt,name=failurePolicy,casttype=FailurePolicyType"` +} + +// Rule is a tuple of APIGroups, APIVersion, and Resources.It is recommended +// to make sure that all the tuple expansions are valid. +type Rule struct { + // APIGroups is the API groups the resources belong to. '*' is all groups. + // If '*' is present, the length of the slice must be one. + // Required. + APIGroups []string `json:"apiGroups,omitempty" protobuf:"bytes,1,rep,name=apiGroups"` + + // APIVersions is the API versions the resources belong to. '*' is all versions. + // If '*' is present, the length of the slice must be one. + // Required. + APIVersions []string `json:"apiVersions,omitempty" protobuf:"bytes,2,rep,name=apiVersions"` + + // Resources is a list of resources this rule applies to. + // + // For example: + // 'pods' means pods. + // 'pods/log' means the log subresource of pods. + // '*' means all resources, but not subresources. + // 'pods/*' means all subresources of pods. + // '*/scale' means all scale subresources. + // '*/*' means all resources and their subresources. + // + // If '*' or '*/*' is present, the length of the slice must be one. + // Required. + Resources []string `json:"resources,omitempty" protobuf:"bytes,3,rep,name=resources"` +} + +type FailurePolicyType string + +const ( + // Ignore means the initilizer is removed from the initializers list of an + // object if the initializer is timed out. + Ignore FailurePolicyType = "Ignore" + // For 1.7, only "Ignore" is allowed. "Fail" will be allowed when the + // extensible admission feature is beta. + Fail FailurePolicyType = "Fail" +) + +// ExternalAdmissionHookConfiguration describes the configuration of initializers. +type ExternalAdmissionHookConfiguration struct { + metav1.TypeMeta `json:",inline"` + // Standard object metadata; More info: http://releases.k8s.io/HEAD/docs/devel/api-conventions.md#metadata. + // +optional + metav1.ObjectMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"` + // ExternalAdmissionHooks is a list of external admission webhooks and the + // affected resources and operations. + // +optional + // +patchMergeKey=name + // +patchStrategy=merge + ExternalAdmissionHooks []ExternalAdmissionHook `json:"externalAdmissionHooks,omitempty" patchStrategy:"merge" patchMergeKey:"name" protobuf:"bytes,2,rep,name=externalAdmissionHooks"` +} + +// ExternalAdmissionHookConfigurationList is a list of ExternalAdmissionHookConfiguration. +type ExternalAdmissionHookConfigurationList struct { + metav1.TypeMeta `json:",inline"` + // Standard list metadata. + // More info: http://releases.k8s.io/HEAD/docs/devel/api-conventions.md#types-kinds + // +optional + metav1.ListMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"` + // List of ExternalAdmissionHookConfiguration. + Items []ExternalAdmissionHookConfiguration `json:"items" protobuf:"bytes,2,rep,name=items"` +} + +// ExternalAdmissionHook describes an external admission webhook and the +// resources and operations it applies to. +type ExternalAdmissionHook struct { + // The name of the external admission webhook. + // Name should be fully qualified, e.g., imagepolicy.kubernetes.io, where + // "imagepolicy" is the name of the webhook, and kubernetes.io is the name + // of the organization. + // Required. + Name string `json:"name" protobuf:"bytes,1,opt,name=name"` + + // ClientConfig defines how to communicate with the hook. + // Required + ClientConfig AdmissionHookClientConfig `json:"clientConfig" protobuf:"bytes,2,opt,name=clientConfig"` + + // Rules describes what operations on what resources/subresources the webhook cares about. + // The webhook cares about an operation if it matches _any_ Rule. + Rules []RuleWithOperations `json:"rules,omitempty" protobuf:"bytes,3,rep,name=rules"` + + // FailurePolicy defines how unrecognized errors from the admission endpoint are handled - + // allowed values are Ignore or Fail. Defaults to Ignore. + // +optional + FailurePolicy *FailurePolicyType `json:"failurePolicy,omitempty" protobuf:"bytes,4,opt,name=failurePolicy,casttype=FailurePolicyType"` +} + +// RuleWithOperations is a tuple of Operations and Resources. It is recommended to make +// sure that all the tuple expansions are valid. +type RuleWithOperations struct { + // Operations is the operations the admission hook cares about - CREATE, UPDATE, or * + // for all operations. + // If '*' is present, the length of the slice must be one. + // Required. + Operations []OperationType `json:"operations,omitempty" protobuf:"bytes,1,rep,name=operations,casttype=OperationType"` + // Rule is embedded, it describes other criteria of the rule, like + // APIGroups, APIVersions, Resources, etc. + Rule `json:",inline" protobuf:"bytes,2,opt,name=rule"` +} + +type OperationType string + +// The constants should be kept in sync with those defined in k8s.io/kubernetes/pkg/admission/interface.go. +const ( + OperationAll OperationType = "*" + Create OperationType = "CREATE" + Update OperationType = "UPDATE" + Delete OperationType = "DELETE" + Connect OperationType = "CONNECT" +) + +// AdmissionHookClientConfig contains the information to make a TLS +// connection with the webhook +type AdmissionHookClientConfig struct { + // Service is a reference to the service for this webhook. If there is only + // one port open for the service, that port will be used. If there are multiple + // ports open, port 443 will be used if it is open, otherwise it is an error. + // Required + Service ServiceReference `json:"service" protobuf:"bytes,1,opt,name=service"` + // CABundle is a PEM encoded CA bundle which will be used to validate webhook's server certificate. + // Required + CABundle []byte `json:"caBundle" protobuf:"bytes,2,opt,name=caBundle"` +} + +// ServiceReference holds a reference to Service.legacy.k8s.io +type ServiceReference struct { + // Namespace is the namespace of the service + // Required + Namespace string `json:"namespace" protobuf:"bytes,1,opt,name=namespace"` + // Name is the name of the service + // Required + Name string `json:"name" protobuf:"bytes,2,opt,name=name"` +} diff --git a/pkg/apis/admissionregistration/validation/validation.go b/pkg/apis/admissionregistration/validation/validation.go new file mode 100644 index 00000000000..5425b16bebb --- /dev/null +++ b/pkg/apis/admissionregistration/validation/validation.go @@ -0,0 +1,174 @@ +/* +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 ( + "strings" + + genericvalidation "k8s.io/apimachinery/pkg/api/validation" + "k8s.io/apimachinery/pkg/util/sets" + validationutil "k8s.io/apimachinery/pkg/util/validation" + "k8s.io/apimachinery/pkg/util/validation/field" + "k8s.io/kubernetes/pkg/apis/admissionregistration" +) + +func ValidateInitializerConfiguration(ic *admissionregistration.InitializerConfiguration) field.ErrorList { + allErrors := genericvalidation.ValidateObjectMeta(&ic.ObjectMeta, false, genericvalidation.NameIsDNSSubdomain, field.NewPath("metadata")) + for i, initializer := range ic.Initializers { + allErrors = append(allErrors, validateInitializer(&initializer, field.NewPath("initializers").Index(i))...) + } + return allErrors +} + +func validateInitializer(initializer *admissionregistration.Initializer, fldPath *field.Path) field.ErrorList { + var allErrors field.ErrorList + // initlializer.Name must be fully qualified + if len(initializer.Name) == 0 { + allErrors = append(allErrors, field.Required(fldPath.Child("name"), "")) + } + if errs := validationutil.IsDNS1123Subdomain(initializer.Name); len(errs) > 0 { + allErrors = append(allErrors, field.Invalid(fldPath.Child("name"), initializer.Name, strings.Join(errs, ","))) + } + if len(strings.Split(initializer.Name, ".")) < 3 { + allErrors = append(allErrors, field.Invalid(fldPath.Child("name"), initializer.Name, "should be a domain with at least two dots")) + } + + for i, rule := range initializer.Rules { + allErrors = append(allErrors, validateRule(&rule, fldPath.Child("rules").Index(i))...) + } + // TODO: relax the validation rule when admissionregistration is beta. + if initializer.FailurePolicy != nil && *initializer.FailurePolicy != admissionregistration.Ignore { + allErrors = append(allErrors, field.NotSupported(fldPath.Child("failurePolicy"), *initializer.FailurePolicy, []string{string(admissionregistration.Ignore)})) + } + return allErrors +} + +func hasWildcard(slice []string) bool { + for _, s := range slice { + if s == "*" { + return true + } + } + return false +} + +func validateRule(rule *admissionregistration.Rule, fldPath *field.Path) field.ErrorList { + var allErrors field.ErrorList + if len(rule.APIGroups) == 0 { + allErrors = append(allErrors, field.Required(fldPath.Child("apiGroups"), "")) + } + if len(rule.APIGroups) > 1 && hasWildcard(rule.APIGroups) { + allErrors = append(allErrors, field.Invalid(fldPath.Child("apiGroups"), rule.APIGroups, "if '*' is present, must not specify other API groups")) + } + // Note: group could be empty, e.g., the legacy "v1" API + if len(rule.APIVersions) == 0 { + allErrors = append(allErrors, field.Required(fldPath.Child("apiVersions"), "")) + } + if len(rule.APIVersions) > 1 && hasWildcard(rule.APIVersions) { + allErrors = append(allErrors, field.Invalid(fldPath.Child("apiVersions"), rule.APIVersions, "if '*' is present, must not specify other API versions")) + } + for i, version := range rule.APIVersions { + if version == "" { + allErrors = append(allErrors, field.Required(fldPath.Child("apiVersions").Index(i), "")) + } + } + if len(rule.Resources) == 0 { + allErrors = append(allErrors, field.Required(fldPath.Child("resources"), "")) + } + if len(rule.Resources) > 1 && hasWildcard(rule.Resources) { + allErrors = append(allErrors, field.Invalid(fldPath.Child("Resources"), rule.Resources, "if '*' is present, must not specify other resources")) + } + for i, resource := range rule.Resources { + if resource == "" { + allErrors = append(allErrors, field.Required(fldPath.Child("resources").Index(i), "")) + } + } + return allErrors +} + +func ValidateInitializerConfigurationUpdate(newIC, oldIC *admissionregistration.InitializerConfiguration) field.ErrorList { + return ValidateInitializerConfiguration(newIC) +} + +func ValidateExternalAdmissionHookConfiguration(e *admissionregistration.ExternalAdmissionHookConfiguration) field.ErrorList { + allErrors := genericvalidation.ValidateObjectMeta(&e.ObjectMeta, false, genericvalidation.NameIsDNSSubdomain, field.NewPath("metadata")) + for i, hook := range e.ExternalAdmissionHooks { + allErrors = append(allErrors, validateExternalAdmissionHook(&hook, field.NewPath("externalAdmissionHook").Index(i))...) + } + return allErrors +} + +func validateExternalAdmissionHook(hook *admissionregistration.ExternalAdmissionHook, fldPath *field.Path) field.ErrorList { + var allErrors field.ErrorList + // hook.Name must be fully qualified + if len(hook.Name) == 0 { + allErrors = append(allErrors, field.Required(fldPath.Child("name"), "")) + } + if errs := validationutil.IsDNS1123Subdomain(hook.Name); len(errs) > 0 { + allErrors = append(allErrors, field.Invalid(fldPath.Child("name"), hook.Name, strings.Join(errs, ","))) + } + if len(strings.Split(hook.Name, ".")) < 3 { + allErrors = append(allErrors, field.Invalid(fldPath.Child("name"), hook.Name, "should be a domain with at least two dots")) + } + + for i, rule := range hook.Rules { + allErrors = append(allErrors, validateRuleWithOperations(&rule, fldPath.Child("rules").Index(i))...) + } + // TODO: relax the validation rule when admissionregistration is beta. + if hook.FailurePolicy != nil && *hook.FailurePolicy != admissionregistration.Ignore { + allErrors = append(allErrors, field.NotSupported(fldPath.Child("failurePolicy"), *hook.FailurePolicy, []string{string(admissionregistration.Ignore)})) + } + return allErrors +} + +var supportedOperations = sets.NewString( + string(admissionregistration.OperationAll), + string(admissionregistration.Create), + string(admissionregistration.Update), + string(admissionregistration.Delete), + string(admissionregistration.Connect), +) + +func hasWildcardOperation(operations []admissionregistration.OperationType) bool { + for _, o := range operations { + if o == admissionregistration.OperationAll { + return true + } + } + return false +} + +func validateRuleWithOperations(ruleWithOperations *admissionregistration.RuleWithOperations, fldPath *field.Path) field.ErrorList { + var allErrors field.ErrorList + if len(ruleWithOperations.Operations) == 0 { + allErrors = append(allErrors, field.Required(fldPath.Child("operations"), "")) + } + if len(ruleWithOperations.Operations) > 1 && hasWildcardOperation(ruleWithOperations.Operations) { + allErrors = append(allErrors, field.Invalid(fldPath.Child("operations"), ruleWithOperations.Operations, "if '*' is present, must not specify other operations")) + } + for i, operation := range ruleWithOperations.Operations { + if !supportedOperations.Has(string(operation)) { + allErrors = append(allErrors, field.NotSupported(fldPath.Child("operations").Index(i), operation, supportedOperations.List())) + } + } + allErrors = append(allErrors, validateRule(&ruleWithOperations.Rule, fldPath)...) + return allErrors +} + +func ValidateExternalAdmissionHookConfigurationUpdate(newC, oldC *admissionregistration.ExternalAdmissionHookConfiguration) field.ErrorList { + return ValidateExternalAdmissionHookConfiguration(newC) +} diff --git a/pkg/apis/admissionregistration/validation/validation_test.go b/pkg/apis/admissionregistration/validation/validation_test.go new file mode 100644 index 00000000000..e700d6a3807 --- /dev/null +++ b/pkg/apis/admissionregistration/validation/validation_test.go @@ -0,0 +1,378 @@ +/* +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 ( + "strings" + "testing" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/kubernetes/pkg/apis/admissionregistration" +) + +func getInitializerConfiguration(initializers []admissionregistration.Initializer) *admissionregistration.InitializerConfiguration { + return &admissionregistration.InitializerConfiguration{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config", + }, + Initializers: initializers, + } +} + +func TestValidateInitializerConfiguration(t *testing.T) { + tests := []struct { + name string + config *admissionregistration.InitializerConfiguration + expectedError string + }{ + { + name: "0 rule is valid", + config: getInitializerConfiguration( + []admissionregistration.Initializer{ + { + Name: "initializer.k8s.io", + }, + }), + }, + { + name: "all initializers must have a fully qualified name", + config: getInitializerConfiguration( + []admissionregistration.Initializer{ + { + Name: "initializer.k8s.io", + }, + { + Name: "k8s.io", + }, + { + Name: "", + }, + }), + expectedError: `initializers[1].name: Invalid value: "k8s.io": should be a domain with at least two dots, initializers[2].name: Required value`, + }, + { + name: "APIGroups must not be empty or nil", + config: getInitializerConfiguration( + []admissionregistration.Initializer{ + { + Name: "initializer.k8s.io", + Rules: []admissionregistration.Rule{ + { + APIGroups: []string{}, + APIVersions: []string{"a"}, + Resources: []string{"a"}, + }, + { + APIGroups: nil, + APIVersions: []string{"a"}, + Resources: []string{"a"}, + }, + }, + }, + }), + expectedError: `initializers[0].rules[0].apiGroups: Required value, initializers[0].rules[1].apiGroups: Required value`, + }, + { + name: "APIVersions must not be empty or nil", + config: getInitializerConfiguration( + []admissionregistration.Initializer{ + { + Name: "initializer.k8s.io", + Rules: []admissionregistration.Rule{ + { + APIGroups: []string{"a"}, + APIVersions: []string{}, + Resources: []string{"a"}, + }, + { + APIGroups: []string{"a"}, + APIVersions: nil, + Resources: []string{"a"}, + }, + }, + }, + }), + expectedError: `initializers[0].rules[0].apiVersions: Required value, initializers[0].rules[1].apiVersions: Required value`, + }, + { + name: "Resources must not be empty or nil", + config: getInitializerConfiguration( + []admissionregistration.Initializer{ + { + Name: "initializer.k8s.io", + Rules: []admissionregistration.Rule{ + { + APIGroups: []string{"a"}, + APIVersions: []string{"a"}, + Resources: []string{}, + }, + { + APIGroups: []string{"a"}, + APIVersions: []string{"a"}, + Resources: nil, + }, + }, + }, + }), + expectedError: `initializers[0].rules[0].resources: Required value, initializers[0].rules[1].resources: Required value`, + }, + { + name: "\"\" is a valid APIGroup", + config: getInitializerConfiguration( + []admissionregistration.Initializer{ + { + Name: "initializer.k8s.io", + Rules: []admissionregistration.Rule{ + { + APIGroups: []string{"a", ""}, + APIVersions: []string{"a"}, + Resources: []string{"a"}, + }, + }, + }, + }), + }, + { + name: "\"\" is NOT a valid APIVersion", + config: getInitializerConfiguration( + []admissionregistration.Initializer{ + { + Name: "initializer.k8s.io", + Rules: []admissionregistration.Rule{ + { + APIGroups: []string{"a"}, + APIVersions: []string{"a", ""}, + Resources: []string{"a"}, + }, + }, + }, + }), + expectedError: "apiVersions[1]: Required value", + }, + { + name: "\"\" is NOT a valid Resource", + config: getInitializerConfiguration( + []admissionregistration.Initializer{ + { + Name: "initializer.k8s.io", + Rules: []admissionregistration.Rule{ + { + APIGroups: []string{"a"}, + APIVersions: []string{"a"}, + Resources: []string{"a", ""}, + }, + }, + }, + }), + expectedError: "resources[1]: Required value", + }, + { + name: "wildcard cannot be mixed with other strings", + config: getInitializerConfiguration( + []admissionregistration.Initializer{ + { + Name: "initializer.k8s.io", + Rules: []admissionregistration.Rule{ + { + APIGroups: []string{"a", "*"}, + APIVersions: []string{"a", "*"}, + Resources: []string{"a", "*"}, + }, + }, + }, + }), + expectedError: `initializers[0].rules[0].apiGroups: Invalid value: []string{"a", "*"}: if '*' is present, must not specify other API groups, initializers[0].rules[0].apiVersions: Invalid value: []string{"a", "*"}: if '*' is present, must not specify other API versions, initializers[0].rules[0].Resources: Invalid value: []string{"a", "*"}: if '*' is present, must not specify other resources`, + }, + { + name: "FailurePolicy can only be \"Ignore\"", + config: getInitializerConfiguration( + []admissionregistration.Initializer{ + { + Name: "initializer.k8s.io", + FailurePolicy: func() *admissionregistration.FailurePolicyType { + r := admissionregistration.Fail + return &r + }(), + }, + }), + expectedError: `failurePolicy: Unsupported value: "Fail": supported values: Ignore`, + }, + } + + for _, test := range tests { + errs := ValidateInitializerConfiguration(test.config) + err := errs.ToAggregate() + if err != nil { + if e, a := test.expectedError, err.Error(); !strings.Contains(a, e) || e == "" { + t.Errorf("test case %s, expected to contain %s, got %s", test.name, e, a) + } + } else { + if test.expectedError != "" { + t.Errorf("test case %s, unexpected no error, expected to contain %s", test.name, test.expectedError) + } + } + } +} + +func getExternalAdmissionHookConfiguration(hooks []admissionregistration.ExternalAdmissionHook) *admissionregistration.ExternalAdmissionHookConfiguration { + return &admissionregistration.ExternalAdmissionHookConfiguration{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config", + }, + ExternalAdmissionHooks: hooks, + } +} + +func TestValidateExternalAdmissionHookConfiguration(t *testing.T) { + tests := []struct { + name string + config *admissionregistration.ExternalAdmissionHookConfiguration + expectedError string + }{ + { + name: "all ExternalAdmissionHook must have a fully qualified name", + config: getExternalAdmissionHookConfiguration( + []admissionregistration.ExternalAdmissionHook{ + { + Name: "webhook.k8s.io", + }, + { + Name: "k8s.io", + }, + { + Name: "", + }, + }), + expectedError: `externalAdmissionHook[1].name: Invalid value: "k8s.io": should be a domain with at least two dots, externalAdmissionHook[2].name: Required value`, + }, + { + name: "Operations must not be empty or nil", + config: getExternalAdmissionHookConfiguration( + []admissionregistration.ExternalAdmissionHook{ + { + Name: "webhook.k8s.io", + Rules: []admissionregistration.RuleWithOperations{ + { + Operations: []admissionregistration.OperationType{}, + Rule: admissionregistration.Rule{ + APIGroups: []string{"a"}, + APIVersions: []string{"a"}, + Resources: []string{"a"}, + }, + }, + { + Operations: nil, + Rule: admissionregistration.Rule{ + APIGroups: []string{"a"}, + APIVersions: []string{"a"}, + Resources: []string{"a"}, + }, + }, + }, + }, + }), + expectedError: `externalAdmissionHook[0].rules[0].operations: Required value, externalAdmissionHook[0].rules[1].operations: Required value`, + }, + { + name: "\"\" is NOT a valid operation", + config: getExternalAdmissionHookConfiguration( + []admissionregistration.ExternalAdmissionHook{ + { + Name: "webhook.k8s.io", + Rules: []admissionregistration.RuleWithOperations{ + { + Operations: []admissionregistration.OperationType{"CREATE", ""}, + Rule: admissionregistration.Rule{ + APIGroups: []string{"a"}, + APIVersions: []string{"a"}, + Resources: []string{"a"}, + }, + }, + }, + }, + }), + expectedError: `Unsupported value: ""`, + }, + { + name: "operation must be either create/update/delete/connect", + config: getExternalAdmissionHookConfiguration( + []admissionregistration.ExternalAdmissionHook{ + { + Name: "webhook.k8s.io", + Rules: []admissionregistration.RuleWithOperations{ + { + Operations: []admissionregistration.OperationType{"PATCH"}, + Rule: admissionregistration.Rule{ + APIGroups: []string{"a"}, + APIVersions: []string{"a"}, + Resources: []string{"a"}, + }, + }, + }, + }, + }), + expectedError: `Unsupported value: "PATCH"`, + }, + { + name: "wildcard cannot be mixed with other strings", + config: getExternalAdmissionHookConfiguration( + []admissionregistration.ExternalAdmissionHook{ + { + Name: "webhook.k8s.io", + Rules: []admissionregistration.RuleWithOperations{ + { + Operations: []admissionregistration.OperationType{"CREATE", "*"}, + Rule: admissionregistration.Rule{ + APIGroups: []string{"a"}, + APIVersions: []string{"a"}, + Resources: []string{"a"}, + }, + }, + }, + }, + }), + expectedError: `if '*' is present, must not specify other operations`, + }, + { + name: "FailurePolicy can only be \"Ignore\"", + config: getExternalAdmissionHookConfiguration( + []admissionregistration.ExternalAdmissionHook{ + { + Name: "initializer.k8s.io", + FailurePolicy: func() *admissionregistration.FailurePolicyType { + r := admissionregistration.Fail + return &r + }(), + }, + }), + expectedError: `failurePolicy: Unsupported value: "Fail": supported values: Ignore`, + }, + } + for _, test := range tests { + errs := ValidateExternalAdmissionHookConfiguration(test.config) + err := errs.ToAggregate() + if err != nil { + if e, a := test.expectedError, err.Error(); !strings.Contains(a, e) || e == "" { + t.Errorf("test case %s, expected to contain %s, got %s", test.name, e, a) + } + } else { + if test.expectedError != "" { + t.Errorf("test case %s, unexpected no error, expected to contain %s", test.name, test.expectedError) + } + } + } +}