diff --git a/cluster/gce/config-test.sh b/cluster/gce/config-test.sh index 2905476add4..0ffc30e208d 100755 --- a/cluster/gce/config-test.sh +++ b/cluster/gce/config-test.sh @@ -191,7 +191,7 @@ fi ENABLE_RESCHEDULER="${KUBE_ENABLE_RESCHEDULER:-true}" # If we included ResourceQuota, we should keep it at the end of the list to prevent incrementing quota usage prematurely. -ADMISSION_CONTROL="${KUBE_ADMISSION_CONTROL:-NamespaceLifecycle,LimitRanger,ServiceAccount,PersistentVolumeLabel,DefaultStorageClass,ResourceQuota,DefaultTolerationSeconds}" +ADMISSION_CONTROL="${KUBE_ADMISSION_CONTROL:-NamespaceLifecycle,LimitRanger,ServiceAccount,PersistentVolumeLabel,DefaultStorageClass,ResourceQuota,DefaultTolerationSeconds,PodPreset}" # Optional: if set to true kube-up will automatically check for existing resources and clean them up. KUBE_UP_AUTOMATIC_CLEANUP=${KUBE_UP_AUTOMATIC_CLEANUP:-false} diff --git a/cmd/kube-apiserver/app/plugins.go b/cmd/kube-apiserver/app/plugins.go index 4e8057b4cc9..ad85f1e51d0 100644 --- a/cmd/kube-apiserver/app/plugins.go +++ b/cmd/kube-apiserver/app/plugins.go @@ -39,6 +39,7 @@ import ( _ "k8s.io/kubernetes/plugin/pkg/admission/namespace/lifecycle" _ "k8s.io/kubernetes/plugin/pkg/admission/persistentvolume/label" _ "k8s.io/kubernetes/plugin/pkg/admission/podnodeselector" + _ "k8s.io/kubernetes/plugin/pkg/admission/podpreset" _ "k8s.io/kubernetes/plugin/pkg/admission/resourcequota" _ "k8s.io/kubernetes/plugin/pkg/admission/security/podsecuritypolicy" _ "k8s.io/kubernetes/plugin/pkg/admission/securitycontext/scdeny" diff --git a/cmd/libs/go2idl/client-gen/main.go b/cmd/libs/go2idl/client-gen/main.go index 1f3c0b9e0e8..f525554cbd1 100644 --- a/cmd/libs/go2idl/client-gen/main.go +++ b/cmd/libs/go2idl/client-gen/main.go @@ -46,6 +46,7 @@ var ( "storage/", "apps/", "policy/", + "settings/", }, "group/versions that client-gen will generate clients for. At most one version per group is allowed. Specified in the format \"group1/version1,group2/version2...\". Default to \"api/,extensions/,autoscaling/,batch/,rbac/\"") includedTypesOverrides = flag.StringSlice("included-types-overrides", []string{}, "list of group/version/type for which client should be generated. By default, client is generated for all types which have genclient=true in types.go. This overrides that. For each groupVersion in this list, only the types mentioned here will be included. The default check of genclient=true will be used for other group versions.") basePath = flag.String("input-base", "k8s.io/kubernetes/pkg/apis", "base path to look for the api group. Default to \"k8s.io/kubernetes/pkg/apis\"") diff --git a/cmd/libs/go2idl/go-to-protobuf/protobuf/cmd.go b/cmd/libs/go2idl/go-to-protobuf/protobuf/cmd.go index 39cc2c7c655..51414fbdbc2 100644 --- a/cmd/libs/go2idl/go-to-protobuf/protobuf/cmd.go +++ b/cmd/libs/go2idl/go-to-protobuf/protobuf/cmd.go @@ -85,6 +85,7 @@ func New() *Generator { `k8s.io/kubernetes/federation/apis/federation/v1beta1`, `k8s.io/kubernetes/pkg/apis/certificates/v1beta1`, `k8s.io/kubernetes/pkg/apis/imagepolicy/v1alpha1`, + `k8s.io/kubernetes/pkg/apis/settings/v1alpha1`, `k8s.io/kubernetes/pkg/apis/storage/v1beta1`, }, ","), DropEmbeddedFields: "k8s.io/apimachinery/pkg/apis/meta/v1.TypeMeta", diff --git a/hack/.linted_packages b/hack/.linted_packages index a8b492d6918..f50bb8b7274 100644 --- a/hack/.linted_packages +++ b/hack/.linted_packages @@ -79,6 +79,8 @@ pkg/apis/imagepolicy/install pkg/apis/policy/install pkg/apis/rbac/install pkg/apis/rbac/v1alpha1 +pkg/apis/settings/install +pkg/apis/settings/validation pkg/apis/storage/install pkg/apis/storage/validation pkg/bootstrap/api @@ -103,6 +105,8 @@ pkg/client/informers/informers_generated/externalversions/policy/v1beta1 pkg/client/informers/informers_generated/externalversions/rbac pkg/client/informers/informers_generated/externalversions/rbac/v1alpha1 pkg/client/informers/informers_generated/externalversions/rbac/v1beta1 +pkg/client/informers/informers_generated/externalversions/settings +pkg/client/informers/informers_generated/externalversions/settings/v1alpha1 pkg/client/informers/informers_generated/externalversions/storage pkg/client/informers/informers_generated/externalversions/storage/v1beta1 pkg/client/informers/informers_generated/internalversion @@ -122,6 +126,8 @@ pkg/client/informers/informers_generated/internalversion/policy pkg/client/informers/informers_generated/internalversion/policy/internalversion pkg/client/informers/informers_generated/internalversion/rbac pkg/client/informers/informers_generated/internalversion/rbac/internalversion +pkg/client/informers/informers_generated/internalversion/settings +pkg/client/informers/informers_generated/internalversion/settings/internalversion pkg/client/informers/informers_generated/internalversion/storage pkg/client/informers/informers_generated/internalversion/storage/internalversion pkg/client/listers/apps/internalversion @@ -152,6 +158,8 @@ pkg/client/listers/policy/v1beta1 pkg/client/listers/rbac/internalversion pkg/client/listers/rbac/v1alpha1 pkg/client/listers/rbac/v1beta1 +pkg/client/listers/settings/internalversion +pkg/client/listers/settings/v1alpha1 pkg/client/listers/storage/internalversion pkg/client/listers/storage/v1beta1 pkg/client/metrics diff --git a/hack/lib/init.sh b/hack/lib/init.sh index 68af35d2c39..812c8682b03 100644 --- a/hack/lib/init.sh +++ b/hack/lib/init.sh @@ -29,7 +29,7 @@ KUBE_OUTPUT_BINPATH="${KUBE_OUTPUT}/bin" # compression for build container KUBE_RSYNC_COMPRESS="${KUBE_RSYNC_COMPRESS:-0}" -# Set no_proxy for localhost if behind a proxy, otherwise, +# Set no_proxy for localhost if behind a proxy, otherwise, # the connections to localhost in scripts will time out export no_proxy=127.0.0.1,localhost @@ -68,7 +68,8 @@ imagepolicy.k8s.io/v1alpha1 \ policy/v1beta1 \ rbac.authorization.k8s.io/v1beta1 \ rbac.authorization.k8s.io/v1alpha1 \ -storage.k8s.io/v1beta1\ +settings.k8s.io/v1alpha1 \ +storage.k8s.io/v1beta1 \ }" # not all group versions are exposed by the server. This list contains those diff --git a/hack/update-codegen.sh b/hack/update-codegen.sh index 0f407cce5a1..9bf1b35de08 100755 --- a/hack/update-codegen.sh +++ b/hack/update-codegen.sh @@ -43,7 +43,7 @@ GV_DIRS=() for gv in "${GROUP_VERSIONS[@]}"; do # add items, but strip off any leading apis/ you find to match command expectations api_dir=$(kube::util::group-version-to-pkg-path "${gv}") - nopkg_dir=${api_dir#pkg/} + nopkg_dir=${api_dir#pkg/} pkg_dir=${nopkg_dir#apis/} # skip groups that aren't being served, clients for these don't matter diff --git a/pkg/api/testapi/testapi.go b/pkg/api/testapi/testapi.go index c567f09f170..b03b1528c87 100644 --- a/pkg/api/testapi/testapi.go +++ b/pkg/api/testapi/testapi.go @@ -45,6 +45,7 @@ import ( "k8s.io/kubernetes/pkg/apis/imagepolicy" "k8s.io/kubernetes/pkg/apis/policy" "k8s.io/kubernetes/pkg/apis/rbac" + "k8s.io/kubernetes/pkg/apis/settings" "k8s.io/kubernetes/pkg/apis/storage" _ "k8s.io/kubernetes/federation/apis/federation/install" @@ -60,6 +61,7 @@ import ( _ "k8s.io/kubernetes/pkg/apis/imagepolicy/install" _ "k8s.io/kubernetes/pkg/apis/policy/install" _ "k8s.io/kubernetes/pkg/apis/rbac/install" + _ "k8s.io/kubernetes/pkg/apis/settings/install" _ "k8s.io/kubernetes/pkg/apis/storage/install" ) @@ -75,6 +77,7 @@ var ( Federation TestGroup Rbac TestGroup Certificates TestGroup + Settings TestGroup Storage TestGroup ImagePolicy TestGroup @@ -230,6 +233,15 @@ func init() { externalTypes: api.Scheme.KnownTypes(externalGroupVersion), } } + if _, ok := Groups[settings.GroupName]; !ok { + externalGroupVersion := schema.GroupVersion{Group: settings.GroupName, Version: api.Registry.GroupOrDie(settings.GroupName).GroupVersion.Version} + Groups[settings.GroupName] = TestGroup{ + externalGroupVersion: externalGroupVersion, + internalGroupVersion: settings.SchemeGroupVersion, + internalTypes: api.Scheme.KnownTypes(settings.SchemeGroupVersion), + externalTypes: api.Scheme.KnownTypes(externalGroupVersion), + } + } if _, ok := Groups[storage.GroupName]; !ok { externalGroupVersion := schema.GroupVersion{Group: storage.GroupName, Version: api.Registry.GroupOrDie(storage.GroupName).GroupVersion.Version} Groups[storage.GroupName] = TestGroup{ @@ -276,6 +288,7 @@ func init() { Extensions = Groups[extensions.GroupName] Federation = Groups[federation.GroupName] Rbac = Groups[rbac.GroupName] + Settings = Groups[settings.GroupName] Storage = Groups[storage.GroupName] ImagePolicy = Groups[imagepolicy.GroupName] Authorization = Groups[authorization.GroupName] diff --git a/pkg/api/validation/validation.go b/pkg/api/validation/validation.go index 9d7b4b9d96e..98bfe14713f 100644 --- a/pkg/api/validation/validation.go +++ b/pkg/api/validation/validation.go @@ -319,7 +319,7 @@ func ValidateNoNewFinalizers(newFinalizers []string, oldFinalizers []string, fld return genericvalidation.ValidateNoNewFinalizers(newFinalizers, oldFinalizers, fldPath) } -func validateVolumes(volumes []api.Volume, fldPath *field.Path) (sets.String, field.ErrorList) { +func ValidateVolumes(volumes []api.Volume, fldPath *field.Path) (sets.String, field.ErrorList) { allErrs := field.ErrorList{} allNames := sets.String{} @@ -1330,7 +1330,7 @@ func validateContainerPorts(ports []api.ContainerPort, fldPath *field.Path) fiel return allErrs } -func validateEnv(vars []api.EnvVar, fldPath *field.Path) field.ErrorList { +func ValidateEnv(vars []api.EnvVar, fldPath *field.Path) field.ErrorList { allErrs := field.ErrorList{} for i, ev := range vars { @@ -1422,7 +1422,7 @@ func validateContainerResourceFieldSelector(fs *api.ResourceFieldSelector, expre return allErrs } -func validateEnvFrom(vars []api.EnvFromSource, fldPath *field.Path) field.ErrorList { +func ValidateEnvFrom(vars []api.EnvFromSource, fldPath *field.Path) field.ErrorList { allErrs := field.ErrorList{} for i, ev := range vars { idxPath := fldPath.Index(i) @@ -1523,7 +1523,7 @@ func validateSecretKeySelector(s *api.SecretKeySelector, fldPath *field.Path) fi return allErrs } -func validateVolumeMounts(mounts []api.VolumeMount, volumes sets.String, fldPath *field.Path) field.ErrorList { +func ValidateVolumeMounts(mounts []api.VolumeMount, volumes sets.String, fldPath *field.Path) field.ErrorList { allErrs := field.ErrorList{} mountpoints := sets.NewString() @@ -1778,8 +1778,8 @@ func validateContainers(containers []api.Container, volumes sets.String, fldPath allErrs = append(allErrs, validateProbe(ctr.ReadinessProbe, idxPath.Child("readinessProbe"))...) allErrs = append(allErrs, validateContainerPorts(ctr.Ports, idxPath.Child("ports"))...) - allErrs = append(allErrs, validateEnv(ctr.Env, idxPath.Child("env"))...) - allErrs = append(allErrs, validateVolumeMounts(ctr.VolumeMounts, volumes, idxPath.Child("volumeMounts"))...) + allErrs = append(allErrs, ValidateEnv(ctr.Env, idxPath.Child("env"))...) + allErrs = append(allErrs, ValidateVolumeMounts(ctr.VolumeMounts, volumes, idxPath.Child("volumeMounts"))...) allErrs = append(allErrs, validatePullPolicy(ctr.ImagePullPolicy, idxPath.Child("imagePullPolicy"))...) allErrs = append(allErrs, ValidateResourceRequirements(&ctr.Resources, idxPath.Child("resources"))...) allErrs = append(allErrs, ValidateSecurityContext(ctr.SecurityContext, idxPath.Child("securityContext"))...) @@ -1988,7 +1988,7 @@ func ValidatePod(pod *api.Pod) field.ErrorList { func ValidatePodSpec(spec *api.PodSpec, fldPath *field.Path) field.ErrorList { allErrs := field.ErrorList{} - allVolumes, vErrs := validateVolumes(spec.Volumes, fldPath.Child("volumes")) + allVolumes, vErrs := ValidateVolumes(spec.Volumes, fldPath.Child("volumes")) allErrs = append(allErrs, vErrs...) allErrs = append(allErrs, validateContainers(spec.Containers, allVolumes, fldPath.Child("containers"))...) allErrs = append(allErrs, validateInitContainers(spec.InitContainers, spec.Containers, allVolumes, fldPath.Child("initContainers"))...) diff --git a/pkg/api/validation/validation_test.go b/pkg/api/validation/validation_test.go index 60d385a41c1..c96c0349f76 100644 --- a/pkg/api/validation/validation_test.go +++ b/pkg/api/validation/validation_test.go @@ -1931,7 +1931,7 @@ func TestValidateVolumes(t *testing.T) { } for i, tc := range testCases { - names, errs := validateVolumes([]api.Volume{tc.vol}, field.NewPath("field")) + names, errs := ValidateVolumes([]api.Volume{tc.vol}, field.NewPath("field")) if len(errs) > 0 && tc.errtype == "" { t.Errorf("[%d: %q] unexpected error(s): %v", i, tc.name, errs) } else if len(errs) > 1 { @@ -1957,7 +1957,7 @@ func TestValidateVolumes(t *testing.T) { {Name: "abc", VolumeSource: api.VolumeSource{EmptyDir: &api.EmptyDirVolumeSource{}}}, {Name: "abc", VolumeSource: api.VolumeSource{EmptyDir: &api.EmptyDirVolumeSource{}}}, } - _, errs := validateVolumes(dupsCase, field.NewPath("field")) + _, errs := ValidateVolumes(dupsCase, field.NewPath("field")) if len(errs) == 0 { t.Errorf("expected error") } else if len(errs) != 1 { @@ -2121,7 +2121,7 @@ func TestValidateEnv(t *testing.T) { }, }, } - if errs := validateEnv(successCase, field.NewPath("field")); len(errs) != 0 { + if errs := ValidateEnv(successCase, field.NewPath("field")); len(errs) != 0 { t.Errorf("expected success: %v", errs) } @@ -2303,7 +2303,7 @@ func TestValidateEnv(t *testing.T) { }, } for _, tc := range errorCases { - if errs := validateEnv(tc.envs, field.NewPath("field")); len(errs) == 0 { + if errs := ValidateEnv(tc.envs, field.NewPath("field")); len(errs) == 0 { t.Errorf("expected failure for %s", tc.name) } else { for i := range errs { @@ -2341,7 +2341,7 @@ func TestValidateEnvFrom(t *testing.T) { }, }, } - if errs := validateEnvFrom(successCase, field.NewPath("field")); len(errs) != 0 { + if errs := ValidateEnvFrom(successCase, field.NewPath("field")); len(errs) != 0 { t.Errorf("expected success: %v", errs) } @@ -2413,7 +2413,7 @@ func TestValidateEnvFrom(t *testing.T) { }, } for _, tc := range errorCases { - if errs := validateEnvFrom(tc.envs, field.NewPath("field")); len(errs) == 0 { + if errs := ValidateEnvFrom(tc.envs, field.NewPath("field")); len(errs) == 0 { t.Errorf("expected failure for %s", tc.name) } else { for i := range errs { @@ -2439,7 +2439,7 @@ func TestValidateVolumeMounts(t *testing.T) { {Name: "abc-123", MountPath: "/bad", SubPath: "..baz"}, {Name: "abc", MountPath: "c:/foo/bar"}, } - if errs := validateVolumeMounts(successCase, volumes, field.NewPath("field")); len(errs) != 0 { + if errs := ValidateVolumeMounts(successCase, volumes, field.NewPath("field")); len(errs) != 0 { t.Errorf("expected success: %v", errs) } @@ -2454,7 +2454,7 @@ func TestValidateVolumeMounts(t *testing.T) { "subpath ends in ..": {{Name: "abc", MountPath: "/bar", SubPath: "./.."}}, } for k, v := range errorCases { - if errs := validateVolumeMounts(v, volumes, field.NewPath("field")); len(errs) == 0 { + if errs := ValidateVolumeMounts(v, volumes, field.NewPath("field")); len(errs) == 0 { t.Errorf("expected failure for %s", k) } } diff --git a/pkg/apis/settings/doc.go b/pkg/apis/settings/doc.go new file mode 100644 index 00000000000..292cb6b244f --- /dev/null +++ b/pkg/apis/settings/doc.go @@ -0,0 +1,19 @@ +/* +Copyright 2016 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 +// +groupName=settings.k8s.io +package settings // import "k8s.io/kubernetes/pkg/apis/settings" diff --git a/pkg/apis/settings/install/install.go b/pkg/apis/settings/install/install.go new file mode 100644 index 00000000000..105a78d8648 --- /dev/null +++ b/pkg/apis/settings/install/install.go @@ -0,0 +1,49 @@ +/* +Copyright 2016 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 installs the settings API group, making it available as +// an option to all of the API encoding/decoding machinery. +package install + +import ( + "k8s.io/apimachinery/pkg/apimachinery/announced" + "k8s.io/apimachinery/pkg/apimachinery/registered" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/kubernetes/pkg/api" + "k8s.io/kubernetes/pkg/apis/settings" + "k8s.io/kubernetes/pkg/apis/settings/v1alpha1" +) + +func init() { + Install(api.GroupFactoryRegistry, api.Registry, api.Scheme) +} + +// 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: settings.GroupName, + VersionPreferenceOrder: []string{v1alpha1.SchemeGroupVersion.Version}, + ImportPrefix: "k8s.io/kubernetes/pkg/apis/settings", + AddInternalObjectsToScheme: settings.AddToScheme, + }, + announced.VersionToSchemeFunc{ + v1alpha1.SchemeGroupVersion.Version: v1alpha1.AddToScheme, + }, + ).Announce(groupFactoryRegistry).RegisterAndEnable(registry, scheme); err != nil { + panic(err) + } +} diff --git a/pkg/apis/settings/register.go b/pkg/apis/settings/register.go new file mode 100644 index 00000000000..85847012793 --- /dev/null +++ b/pkg/apis/settings/register.go @@ -0,0 +1,52 @@ +/* +Copyright 2016 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 settings + +import ( + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +var ( + SchemeBuilder = runtime.NewSchemeBuilder(addKnownTypes) + AddToScheme = SchemeBuilder.AddToScheme +) + +// GroupName is the group name use in this package +const GroupName = "settings.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 a Group qualified GroupKind +func Kind(kind string) schema.GroupKind { + return SchemeGroupVersion.WithKind(kind).GroupKind() +} + +// Resource takes an unqualified resource and returns a Group qualified GroupResource +func Resource(resource string) schema.GroupResource { + return SchemeGroupVersion.WithResource(resource).GroupResource() +} + +// Adds the list of known types to api.Scheme. +func addKnownTypes(scheme *runtime.Scheme) error { + scheme.AddKnownTypes(SchemeGroupVersion, + &PodPreset{}, + &PodPresetList{}, + ) + return nil +} diff --git a/pkg/apis/settings/types.go b/pkg/apis/settings/types.go new file mode 100644 index 00000000000..910143e2a38 --- /dev/null +++ b/pkg/apis/settings/types.go @@ -0,0 +1,63 @@ +/* +Copyright 2016 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 settings + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/kubernetes/pkg/api" +) + +// +genclient=true + +// PodPreset is a policy resource that defines additional runtime +// requirements for a Pod. +type PodPreset struct { + metav1.TypeMeta + // +optional + metav1.ObjectMeta + + // +optional + Spec PodPresetSpec +} + +// PodPresetSpec is a description of a pod injection policy. +type PodPresetSpec struct { + // Selector is a label query over a set of resources, in this case pods. + // Required. + Selector metav1.LabelSelector + // Env defines the collection of EnvVar to inject into containers. + // +optional + Env []api.EnvVar + // EnvFrom defines the collection of EnvFromSource to inject into containers. + // +optional + EnvFrom []api.EnvFromSource + // Volumes defines the collection of Volume to inject into the pod. + // +optional + Volumes []api.Volume + // VolumeMounts defines the collection of VolumeMount to inject into containers. + // +optional + VolumeMounts []api.VolumeMount +} + +// PodPresetList is a list of PodPreset objects. +type PodPresetList struct { + metav1.TypeMeta + // +optional + metav1.ListMeta + + Items []PodPreset +} diff --git a/pkg/apis/settings/v1alpha1/doc.go b/pkg/apis/settings/v1alpha1/doc.go new file mode 100644 index 00000000000..df04c846b39 --- /dev/null +++ b/pkg/apis/settings/v1alpha1/doc.go @@ -0,0 +1,23 @@ +/* +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/settings +// +k8s:openapi-gen=true +// +k8s:defaulter-gen=TypeMeta + +// +groupName=settings.k8s.io +package v1alpha1 // import "k8s.io/kubernetes/pkg/apis/settings/v1alpha1" diff --git a/pkg/apis/settings/v1alpha1/register.go b/pkg/apis/settings/v1alpha1/register.go new file mode 100644 index 00000000000..45afb50ca9a --- /dev/null +++ b/pkg/apis/settings/v1alpha1/register.go @@ -0,0 +1,49 @@ +/* +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" +) + +// GroupName is the group name use in this package +const GroupName = "settings.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 ( + SchemeBuilder = runtime.NewSchemeBuilder(addKnownTypes) + AddToScheme = SchemeBuilder.AddToScheme +) + +// Adds the list of known types to api.Scheme. +func addKnownTypes(scheme *runtime.Scheme) error { + scheme.AddKnownTypes(SchemeGroupVersion, + &PodPreset{}, + &PodPresetList{}, + ) + metav1.AddToGroupVersion(scheme, SchemeGroupVersion) + return nil +} diff --git a/pkg/apis/settings/v1alpha1/types.go b/pkg/apis/settings/v1alpha1/types.go new file mode 100644 index 00000000000..fbfffd13605 --- /dev/null +++ b/pkg/apis/settings/v1alpha1/types.go @@ -0,0 +1,67 @@ +/* +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/kubernetes/pkg/api/v1" +) + +// +genclient=true + +// PodPreset is a policy resource that defines additional runtime +// requirements for a Pod. +type PodPreset struct { + metav1.TypeMeta `json:",inline"` + // +optional + metav1.ObjectMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"` + + // +optional + Spec PodPresetSpec `json:"spec,omitempty" protobuf:"bytes,2,opt,name=spec"` +} + +// PodPresetSpec is a description of a pod injection policy. +type PodPresetSpec struct { + // Selector is a label query over a set of resources, in this case pods. + // Required. + Selector metav1.LabelSelector `json:"selector,omitempty" protobuf:"bytes,1,opt,name=selector"` + + // Env defines the collection of EnvVar to inject into containers. + // +optional + Env []v1.EnvVar `json:"env,omitempty" protobuf:"bytes,2,rep,name=env"` + // EnvFrom defines the collection of EnvFromSource to inject into containers. + // +optional + EnvFrom []v1.EnvFromSource `json:"envFrom,omitempty" protobuf:"bytes,3,rep,name=envFrom"` + // Volumes defines the collection of Volume to inject into the pod. + // +optional + Volumes []v1.Volume `json:"volumes,omitempty" protobuf:"bytes,4,rep,name=volumes"` + // VolumeMounts defines the collection of VolumeMount to inject into containers. + // +optional + VolumeMounts []v1.VolumeMount `json:"volumeMounts,omitempty" protobuf:"bytes,5,rep,name=volumeMounts"` +} + +// PodPresetList is a list of PodPreset objects. +type PodPresetList struct { + metav1.TypeMeta `json:",inline"` + // Standard list metadata. + // More info: http://releases.k8s.io/HEAD/docs/devel/api-conventions.md#metadata + // +optional + metav1.ListMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"` + + // Items is a list of schema objects. + Items []PodPreset `json:"items" protobuf:"bytes,2,rep,name=items"` +} diff --git a/pkg/apis/settings/validation/validation.go b/pkg/apis/settings/validation/validation.go new file mode 100644 index 00000000000..f38f07e7a81 --- /dev/null +++ b/pkg/apis/settings/validation/validation.go @@ -0,0 +1,68 @@ +/* +Copyright 2016 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 ( + unversionedvalidation "k8s.io/apimachinery/pkg/apis/meta/v1/validation" + "k8s.io/apimachinery/pkg/util/validation/field" + apivalidation "k8s.io/kubernetes/pkg/api/validation" + "k8s.io/kubernetes/pkg/apis/settings" +) + +// ValidatePodPresetName can be used to check whether the given PodPreset name is valid. +// Prefix indicates this name will be used as part of generation, in which case +// trailing dashes are allowed. +func ValidatePodPresetName(name string, prefix bool) []string { + // TODO: Validate that there's name for the suffix inserted by the pods. + // Currently this is just "-index". In the future we may allow a user + // specified list of suffixes and we need to validate the longest one. + return apivalidation.NameIsDNSSubdomain(name, prefix) +} + +// ValidatePodPresetSpec tests if required fields in the PodPreset spec are set. +func ValidatePodPresetSpec(spec *settings.PodPresetSpec, fldPath *field.Path) field.ErrorList { + allErrs := field.ErrorList{} + + allErrs = append(allErrs, unversionedvalidation.ValidateLabelSelector(&spec.Selector, fldPath.Child("selector"))...) + + if spec.Env == nil && spec.EnvFrom == nil && spec.VolumeMounts == nil && spec.Volumes == nil { + allErrs = append(allErrs, field.Required(fldPath.Child("volumes", "env", "envFrom", "volumeMounts"), "must specify at least one")) + } + + volumes, vErrs := apivalidation.ValidateVolumes(spec.Volumes, fldPath.Child("volumes")) + allErrs = append(allErrs, vErrs...) + allErrs = append(allErrs, apivalidation.ValidateEnv(spec.Env, fldPath.Child("env"))...) + allErrs = append(allErrs, apivalidation.ValidateEnvFrom(spec.EnvFrom, fldPath.Child("envFrom"))...) + allErrs = append(allErrs, apivalidation.ValidateVolumeMounts(spec.VolumeMounts, volumes, fldPath.Child("volumeMounts"))...) + + return allErrs +} + +// ValidatePodPreset validates a PodPreset. +func ValidatePodPreset(pip *settings.PodPreset) field.ErrorList { + allErrs := apivalidation.ValidateObjectMeta(&pip.ObjectMeta, true, ValidatePodPresetName, field.NewPath("metadata")) + allErrs = append(allErrs, ValidatePodPresetSpec(&pip.Spec, field.NewPath("spec"))...) + return allErrs +} + +// ValidatePodPresetUpdate tests if required fields in the PodPreset are set. +func ValidatePodPresetUpdate(pip, oldPip *settings.PodPreset) field.ErrorList { + allErrs := apivalidation.ValidateObjectMetaUpdate(&pip.ObjectMeta, &oldPip.ObjectMeta, field.NewPath("metadata")) + allErrs = append(allErrs, ValidatePodPresetSpec(&pip.Spec, field.NewPath("spec"))...) + + return allErrs +} diff --git a/pkg/apis/settings/validation/validation_test.go b/pkg/apis/settings/validation/validation_test.go new file mode 100644 index 00000000000..343f6465c0c --- /dev/null +++ b/pkg/apis/settings/validation/validation_test.go @@ -0,0 +1,190 @@ +/* +Copyright 2016 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" + + "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/kubernetes/pkg/api" + "k8s.io/kubernetes/pkg/apis/settings" +) + +func TestValidateEmptyPodPreset(t *testing.T) { + emptyPodPreset := &settings.PodPreset{ + Spec: settings.PodPresetSpec{}, + } + + errList := ValidatePodPreset(emptyPodPreset) + if errList == nil { + t.Fatal("empty pod preset should return an error") + } +} + +func TestValidateEmptyPodPresetItems(t *testing.T) { + emptyPodPreset := &settings.PodPreset{ + ObjectMeta: v1.ObjectMeta{ + Name: "hello", + Namespace: "sample", + }, + Spec: settings.PodPresetSpec{ + Selector: v1.LabelSelector{ + MatchExpressions: []v1.LabelSelectorRequirement{ + { + Key: "security", + Operator: v1.LabelSelectorOpIn, + Values: []string{"S2"}, + }, + }, + }, + }, + } + + errList := ValidatePodPreset(emptyPodPreset) + if !strings.Contains(errList.ToAggregate().Error(), "must specify at least one") { + t.Fatal("empty pod preset with label selector should return an error") + } +} + +func TestValidatePodPresets(t *testing.T) { + p := &settings.PodPreset{ + ObjectMeta: v1.ObjectMeta{ + Name: "hello", + Namespace: "sample", + }, + Spec: settings.PodPresetSpec{ + Selector: v1.LabelSelector{ + MatchExpressions: []v1.LabelSelectorRequirement{ + { + Key: "security", + Operator: v1.LabelSelectorOpIn, + Values: []string{"S2"}, + }, + }, + }, + Volumes: []api.Volume{{Name: "vol", VolumeSource: api.VolumeSource{EmptyDir: &api.EmptyDirVolumeSource{}}}}, + Env: []api.EnvVar{{Name: "abc", Value: "value"}, {Name: "ABC", Value: "value"}}, + EnvFrom: []api.EnvFromSource{ + { + ConfigMapRef: &api.ConfigMapEnvSource{ + LocalObjectReference: api.LocalObjectReference{Name: "abc"}, + }, + }, + { + Prefix: "pre_", + ConfigMapRef: &api.ConfigMapEnvSource{ + LocalObjectReference: api.LocalObjectReference{Name: "abc"}, + }, + }, + }, + }, + } + + errList := ValidatePodPreset(p) + if errList != nil { + if errList.ToAggregate() != nil { + t.Fatalf("errors: %#v", errList.ToAggregate().Error()) + } + } + + p = &settings.PodPreset{ + ObjectMeta: v1.ObjectMeta{ + Name: "hello", + Namespace: "sample", + }, + Spec: settings.PodPresetSpec{ + Selector: v1.LabelSelector{ + MatchExpressions: []v1.LabelSelectorRequirement{ + { + Key: "security", + Operator: v1.LabelSelectorOpIn, + Values: []string{"S2"}, + }, + }, + }, + Volumes: []api.Volume{{Name: "vol", VolumeSource: api.VolumeSource{EmptyDir: &api.EmptyDirVolumeSource{}}}}, + Env: []api.EnvVar{{Name: "abc", Value: "value"}, {Name: "ABC", Value: "value"}}, + VolumeMounts: []api.VolumeMount{ + {Name: "vol", MountPath: "/foo"}, + }, + EnvFrom: []api.EnvFromSource{ + { + ConfigMapRef: &api.ConfigMapEnvSource{ + LocalObjectReference: api.LocalObjectReference{Name: "abc"}, + }, + }, + { + Prefix: "pre_", + ConfigMapRef: &api.ConfigMapEnvSource{ + LocalObjectReference: api.LocalObjectReference{Name: "abc"}, + }, + }, + }, + }, + } + + errList = ValidatePodPreset(p) + if errList != nil { + if errList.ToAggregate() != nil { + t.Fatalf("errors: %#v", errList.ToAggregate().Error()) + } + } +} + +func TestValidatePodPresetsiVolumeMountError(t *testing.T) { + p := &settings.PodPreset{ + ObjectMeta: v1.ObjectMeta{ + Name: "hello", + Namespace: "sample", + }, + Spec: settings.PodPresetSpec{ + Selector: v1.LabelSelector{ + MatchExpressions: []v1.LabelSelectorRequirement{ + { + Key: "security", + Operator: v1.LabelSelectorOpIn, + Values: []string{"S2"}, + }, + }, + }, + Volumes: []api.Volume{{Name: "vol", VolumeSource: api.VolumeSource{EmptyDir: &api.EmptyDirVolumeSource{}}}}, + VolumeMounts: []api.VolumeMount{ + {Name: "dne", MountPath: "/foo"}, + }, + Env: []api.EnvVar{{Name: "abc", Value: "value"}, {Name: "ABC", Value: "value"}}, + EnvFrom: []api.EnvFromSource{ + { + ConfigMapRef: &api.ConfigMapEnvSource{ + LocalObjectReference: api.LocalObjectReference{Name: "abc"}, + }, + }, + { + Prefix: "pre_", + ConfigMapRef: &api.ConfigMapEnvSource{ + LocalObjectReference: api.LocalObjectReference{Name: "abc"}, + }, + }, + }, + }, + } + + errList := ValidatePodPreset(p) + if !strings.Contains(errList.ToAggregate().Error(), "spec.volumeMounts[0].name: Not found") { + t.Fatal("should have returned error for volume that does not exist") + } +} diff --git a/pkg/client/clientset_generated/clientset/import_known_versions.go b/pkg/client/clientset_generated/clientset/import_known_versions.go index 9cd90c556a5..061c2af6d59 100644 --- a/pkg/client/clientset_generated/clientset/import_known_versions.go +++ b/pkg/client/clientset_generated/clientset/import_known_versions.go @@ -31,6 +31,7 @@ import ( _ "k8s.io/kubernetes/pkg/apis/extensions/install" _ "k8s.io/kubernetes/pkg/apis/policy/install" _ "k8s.io/kubernetes/pkg/apis/rbac/install" + _ "k8s.io/kubernetes/pkg/apis/settings/install" _ "k8s.io/kubernetes/pkg/apis/storage/install" ) diff --git a/pkg/master/import_known_versions.go b/pkg/master/import_known_versions.go index 61e44ab9b79..30a4420b966 100644 --- a/pkg/master/import_known_versions.go +++ b/pkg/master/import_known_versions.go @@ -33,6 +33,7 @@ import ( _ "k8s.io/kubernetes/pkg/apis/imagepolicy/install" _ "k8s.io/kubernetes/pkg/apis/policy/install" _ "k8s.io/kubernetes/pkg/apis/rbac/install" + _ "k8s.io/kubernetes/pkg/apis/settings/install" _ "k8s.io/kubernetes/pkg/apis/storage/install" ) diff --git a/pkg/master/master.go b/pkg/master/master.go index 66c0f5757d9..c50b5224ec5 100644 --- a/pkg/master/master.go +++ b/pkg/master/master.go @@ -45,6 +45,7 @@ import ( policyapiv1beta1 "k8s.io/kubernetes/pkg/apis/policy/v1beta1" rbacapi "k8s.io/kubernetes/pkg/apis/rbac/v1alpha1" rbacv1beta1 "k8s.io/kubernetes/pkg/apis/rbac/v1beta1" + settingsapi "k8s.io/kubernetes/pkg/apis/settings/v1alpha1" storageapiv1beta1 "k8s.io/kubernetes/pkg/apis/storage/v1beta1" corev1client "k8s.io/kubernetes/pkg/client/clientset_generated/clientset/typed/core/v1" coreclient "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset/typed/core/internalversion" @@ -68,6 +69,7 @@ import ( extensionsrest "k8s.io/kubernetes/pkg/registry/extensions/rest" policyrest "k8s.io/kubernetes/pkg/registry/policy/rest" rbacrest "k8s.io/kubernetes/pkg/registry/rbac/rest" + settingsrest "k8s.io/kubernetes/pkg/registry/settings/rest" storagerest "k8s.io/kubernetes/pkg/registry/storage/rest" ) @@ -248,6 +250,7 @@ func (c completedConfig) New() (*Master, error) { policyrest.RESTStorageProvider{}, rbacrest.RESTStorageProvider{Authorizer: c.GenericConfig.Authorizer}, storagerest.RESTStorageProvider{}, + settingsrest.RESTStorageProvider{}, } m.InstallAPIs(c.Config.APIResourceConfigSource, c.Config.GenericConfig.RESTOptionsGetter, restStorageProviders...) @@ -371,6 +374,7 @@ func DefaultAPIResourceConfigSource() *serverstorage.ResourceConfig { policyapiv1beta1.SchemeGroupVersion, rbacv1beta1.SchemeGroupVersion, rbacapi.SchemeGroupVersion, + settingsapi.SchemeGroupVersion, storageapiv1beta1.SchemeGroupVersion, certificatesapiv1beta1.SchemeGroupVersion, authorizationapiv1.SchemeGroupVersion, diff --git a/pkg/registry/settings/podpreset/doc.go b/pkg/registry/settings/podpreset/doc.go new file mode 100644 index 00000000000..30c955a50dc --- /dev/null +++ b/pkg/registry/settings/podpreset/doc.go @@ -0,0 +1,17 @@ +/* +Copyright 2015 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 podpreset // import "k8s.io/kubernetes/pkg/registry/settings/podpreset" diff --git a/pkg/registry/settings/podpreset/registry.go b/pkg/registry/settings/podpreset/registry.go new file mode 100644 index 00000000000..a6225de3b93 --- /dev/null +++ b/pkg/registry/settings/podpreset/registry.go @@ -0,0 +1,84 @@ +/* +Copyright 2016 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 podpreset + +import ( + metainternalversion "k8s.io/apimachinery/pkg/apis/meta/internalversion" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/watch" + genericapirequest "k8s.io/apiserver/pkg/endpoints/request" + "k8s.io/apiserver/pkg/registry/rest" + "k8s.io/kubernetes/pkg/api" + "k8s.io/kubernetes/pkg/apis/settings" +) + +// Registry is an interface for things that know how to store PodPresets. +type Registry interface { + ListPodPresets(ctx genericapirequest.Context, options *metainternalversion.ListOptions) (*settings.PodPresetList, error) + CreatePodPreset(ctx genericapirequest.Context, pp *settings.PodPreset) error + UpdatePodPreset(ctx genericapirequest.Context, pp *settings.PodPreset) error + GetPodPreset(ctx genericapirequest.Context, ppID string, options *metav1.GetOptions) (*settings.PodPreset, error) + DeletePodPreset(ctx genericapirequest.Context, ppID string) error + WatchPodPresets(ctx genericapirequest.Context, options *metainternalversion.ListOptions) (watch.Interface, error) +} + +// storage puts strong typing around storage calls +type storage struct { + rest.StandardStorage +} + +// NewRegistry returns a new Registry interface for the given Storage. Any mismatched +// types will panic. +func NewRegistry(s rest.StandardStorage) Registry { + return &storage{s} +} + +func (s *storage) ListPodPresets(ctx genericapirequest.Context, options *metainternalversion.ListOptions) (*settings.PodPresetList, error) { + obj, err := s.List(ctx, options) + if err != nil { + return nil, err + } + + return obj.(*settings.PodPresetList), nil +} + +func (s *storage) CreatePodPreset(ctx genericapirequest.Context, pp *settings.PodPreset) error { + _, err := s.Create(ctx, pp) + return err +} + +func (s *storage) UpdatePodPreset(ctx genericapirequest.Context, pp *settings.PodPreset) error { + _, _, err := s.Update(ctx, pp.Name, rest.DefaultUpdatedObjectInfo(pp, api.Scheme)) + return err +} + +func (s *storage) WatchPodPresets(ctx genericapirequest.Context, options *metainternalversion.ListOptions) (watch.Interface, error) { + return s.Watch(ctx, options) +} + +func (s *storage) GetPodPreset(ctx genericapirequest.Context, name string, options *metav1.GetOptions) (*settings.PodPreset, error) { + obj, err := s.Get(ctx, name, options) + if err != nil { + return nil, err + } + return obj.(*settings.PodPreset), nil +} + +func (s *storage) DeletePodPreset(ctx genericapirequest.Context, name string) error { + _, _, err := s.Delete(ctx, name, nil) + return err +} diff --git a/pkg/registry/settings/podpreset/storage/storage.go b/pkg/registry/settings/podpreset/storage/storage.go new file mode 100644 index 00000000000..a5216d2882c --- /dev/null +++ b/pkg/registry/settings/podpreset/storage/storage.go @@ -0,0 +1,57 @@ +/* +Copyright 2015 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 storage + +import ( + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apiserver/pkg/registry/generic" + genericregistry "k8s.io/apiserver/pkg/registry/generic/registry" + "k8s.io/kubernetes/pkg/api" + settingsapi "k8s.io/kubernetes/pkg/apis/settings" + "k8s.io/kubernetes/pkg/registry/cachesize" + "k8s.io/kubernetes/pkg/registry/settings/podpreset" +) + +// rest implements a RESTStorage for replication controllers against etcd +type REST struct { + *genericregistry.Store +} + +// NewREST returns a RESTStorage object that will work against replication controllers. +func NewREST(optsGetter generic.RESTOptionsGetter) *REST { + store := &genericregistry.Store{ + Copier: api.Scheme, + NewFunc: func() runtime.Object { return &settingsapi.PodPreset{} }, + NewListFunc: func() runtime.Object { return &settingsapi.PodPresetList{} }, + ObjectNameFunc: func(obj runtime.Object) (string, error) { + return obj.(*settingsapi.PodPreset).GetName(), nil + }, + PredicateFunc: podpreset.Matcher, + QualifiedResource: settingsapi.Resource("podpresets"), + WatchCacheSize: cachesize.GetWatchCacheSizeByResource("podpresets"), + + CreateStrategy: podpreset.Strategy, + UpdateStrategy: podpreset.Strategy, + DeleteStrategy: podpreset.Strategy, + } + options := &generic.StoreOptions{RESTOptions: optsGetter, AttrFunc: podpreset.GetAttrs} + if err := store.CompleteWithOptions(options); err != nil { + panic(err) // TODO: Propagate error up + } + + return &REST{store} +} diff --git a/pkg/registry/settings/podpreset/strategy.go b/pkg/registry/settings/podpreset/strategy.go new file mode 100644 index 00000000000..5bbb4583f68 --- /dev/null +++ b/pkg/registry/settings/podpreset/strategy.go @@ -0,0 +1,112 @@ +/* +Copyright 2014 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 podpreset + +import ( + "fmt" + + "k8s.io/apimachinery/pkg/fields" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/util/validation/field" + genericapirequest "k8s.io/apiserver/pkg/endpoints/request" + "k8s.io/apiserver/pkg/registry/generic" + apistorage "k8s.io/apiserver/pkg/storage" + "k8s.io/apiserver/pkg/storage/names" + "k8s.io/kubernetes/pkg/api" + "k8s.io/kubernetes/pkg/apis/settings" + "k8s.io/kubernetes/pkg/apis/settings/validation" +) + +// podPresetStrategy implements verification logic for Pod Injection Policies. +type podPresetStrategy struct { + runtime.ObjectTyper + names.NameGenerator +} + +// Strategy is the default logic that applies when creating and updating Pod Injection Policy objects. +var Strategy = podPresetStrategy{api.Scheme, names.SimpleNameGenerator} + +// NamespaceScoped returns true because all Pod Injection Policies need to be within a namespace. +func (podPresetStrategy) NamespaceScoped() bool { + return true +} + +// PrepareForCreate clears the status of a Pod Injection Policy before creation. +func (podPresetStrategy) PrepareForCreate(ctx genericapirequest.Context, obj runtime.Object) { + pip := obj.(*settings.PodPreset) + pip.Generation = 1 +} + +// PrepareForUpdate clears fields that are not allowed to be set by end users on update. +func (podPresetStrategy) PrepareForUpdate(ctx genericapirequest.Context, obj, old runtime.Object) { + newPodPreset := obj.(*settings.PodPreset) + oldPodPreset := old.(*settings.PodPreset) + + // Update is not allowed + newPodPreset.Spec = oldPodPreset.Spec +} + +// Validate validates a new PodPreset. +func (podPresetStrategy) Validate(ctx genericapirequest.Context, obj runtime.Object) field.ErrorList { + pip := obj.(*settings.PodPreset) + return validation.ValidatePodPreset(pip) +} + +// Canonicalize normalizes the object after validation. +func (podPresetStrategy) Canonicalize(obj runtime.Object) {} + +// AllowCreateOnUpdate is false for PodPreset; this means POST is needed to create one. +func (podPresetStrategy) AllowCreateOnUpdate() bool { + return false +} + +// ValidateUpdate is the default update validation for an end user. +func (podPresetStrategy) ValidateUpdate(ctx genericapirequest.Context, obj, old runtime.Object) field.ErrorList { + validationErrorList := validation.ValidatePodPreset(obj.(*settings.PodPreset)) + updateErrorList := validation.ValidatePodPresetUpdate(obj.(*settings.PodPreset), old.(*settings.PodPreset)) + return append(validationErrorList, updateErrorList...) +} + +// AllowUnconditionalUpdate is the default update policy for Pod Injection Policy objects. +func (podPresetStrategy) AllowUnconditionalUpdate() bool { + return true +} + +// SelectableFields returns a field set that represents the object. +func SelectableFields(pip *settings.PodPreset) fields.Set { + return generic.ObjectMetaFieldsSet(&pip.ObjectMeta, true) +} + +// GetAttrs returns labels and fields of a given object for filtering purposes. +func GetAttrs(obj runtime.Object) (labels.Set, fields.Set, error) { + pip, ok := obj.(*settings.PodPreset) + if !ok { + return nil, nil, fmt.Errorf("given object is not a PodPreset.") + } + return labels.Set(pip.ObjectMeta.Labels), SelectableFields(pip), nil +} + +// Matcher is the filter used by the generic etcd backend to watch events +// from etcd to clients of the apiserver only interested in specific labels/fields. +func Matcher(label labels.Selector, field fields.Selector) apistorage.SelectionPredicate { + return apistorage.SelectionPredicate{ + Label: label, + Field: field, + GetAttrs: GetAttrs, + } +} diff --git a/pkg/registry/settings/rest/storage_settings.go b/pkg/registry/settings/rest/storage_settings.go new file mode 100644 index 00000000000..8e9a41b4db4 --- /dev/null +++ b/pkg/registry/settings/rest/storage_settings.go @@ -0,0 +1,56 @@ +/* +Copyright 2016 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 rest + +import ( + "k8s.io/apiserver/pkg/registry/generic" + "k8s.io/apiserver/pkg/registry/rest" + genericapiserver "k8s.io/apiserver/pkg/server" + serverstorage "k8s.io/apiserver/pkg/server/storage" + "k8s.io/kubernetes/pkg/api" + "k8s.io/kubernetes/pkg/apis/settings" + settingsapiv1alpha1 "k8s.io/kubernetes/pkg/apis/settings/v1alpha1" + podpresetstore "k8s.io/kubernetes/pkg/registry/settings/podpreset/storage" +) + +type RESTStorageProvider struct{} + +func (p RESTStorageProvider) NewRESTStorage(apiResourceConfigSource serverstorage.APIResourceConfigSource, restOptionsGetter generic.RESTOptionsGetter) (genericapiserver.APIGroupInfo, bool) { + apiGroupInfo := genericapiserver.NewDefaultAPIGroupInfo(settings.GroupName, api.Registry, api.Scheme, api.ParameterCodec, api.Codecs) + + if apiResourceConfigSource.AnyResourcesForVersionEnabled(settingsapiv1alpha1.SchemeGroupVersion) { + apiGroupInfo.VersionedResourcesStorageMap[settingsapiv1alpha1.SchemeGroupVersion.Version] = p.v1alpha1Storage(apiResourceConfigSource, restOptionsGetter) + apiGroupInfo.GroupMeta.GroupVersion = settingsapiv1alpha1.SchemeGroupVersion + } + + return apiGroupInfo, true +} + +func (p RESTStorageProvider) v1alpha1Storage(apiResourceConfigSource serverstorage.APIResourceConfigSource, restOptionsGetter generic.RESTOptionsGetter) map[string]rest.Storage { + version := settingsapiv1alpha1.SchemeGroupVersion + + storage := map[string]rest.Storage{} + if apiResourceConfigSource.ResourceEnabled(version.WithResource("podpresets")) { + podPresetStorage := podpresetstore.NewREST(restOptionsGetter) + storage["podpresets"] = podPresetStorage + } + return storage +} + +func (p RESTStorageProvider) GroupName() string { + return settings.GroupName +} diff --git a/plugin/pkg/admission/podpreset/admission.go b/plugin/pkg/admission/podpreset/admission.go new file mode 100644 index 00000000000..0fd98c7e9b1 --- /dev/null +++ b/plugin/pkg/admission/podpreset/admission.go @@ -0,0 +1,320 @@ +/* +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 admission + +import ( + "fmt" + "io" + "reflect" + + "github.com/golang/glog" + + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apiserver/pkg/admission" + "k8s.io/kubernetes/pkg/api" + "k8s.io/kubernetes/pkg/apis/settings" + "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset" + informers "k8s.io/kubernetes/pkg/client/informers/informers_generated/internalversion" + settingslisters "k8s.io/kubernetes/pkg/client/listers/settings/internalversion" + kubeapiserveradmission "k8s.io/kubernetes/pkg/kubeapiserver/admission" +) + +const ( + annotationPrefix = "podpreset.admission.kubernetes.io" + pluginName = "PodPreset" +) + +func init() { + admission.RegisterPlugin(pluginName, func(config io.Reader) (admission.Interface, error) { + return NewPlugin(), nil + }) +} + +// podPresetPlugin is an implementation of admission.Interface. +type podPresetPlugin struct { + *admission.Handler + client internalclientset.Interface + + lister settingslisters.PodPresetLister +} + +var _ = kubeapiserveradmission.WantsInformerFactory(&podPresetPlugin{}) +var _ = kubeapiserveradmission.WantsInternalClientSet(&podPresetPlugin{}) + +// NewPlugin creates a new pod preset admission plugin. +func NewPlugin() *podPresetPlugin { + return &podPresetPlugin{ + Handler: admission.NewHandler(admission.Create, admission.Update), + } +} + +func (plugin *podPresetPlugin) Validate() error { + if plugin.client == nil { + return fmt.Errorf("%s requires a client", pluginName) + } + if plugin.lister == nil { + return fmt.Errorf("%s requires a lister", pluginName) + } + return nil +} + +func (a *podPresetPlugin) SetInternalClientSet(client internalclientset.Interface) { + a.client = client +} + +func (a *podPresetPlugin) SetInformerFactory(f informers.SharedInformerFactory) { + podPresetInformer := f.Settings().InternalVersion().PodPresets() + a.lister = podPresetInformer.Lister() + a.SetReadyFunc(podPresetInformer.Informer().HasSynced) +} + +// Admit injects a pod with the specific fields for each pod preset it matches. +func (c *podPresetPlugin) Admit(a admission.Attributes) error { + // Ignore all calls to subresources or resources other than pods. + // Ignore all operations other than CREATE. + if len(a.GetSubresource()) != 0 || a.GetResource().GroupResource() != api.Resource("pods") || a.GetOperation() != admission.Create { + return nil + } + + pod, ok := a.GetObject().(*api.Pod) + if !ok { + return errors.NewBadRequest("Resource was marked with kind Pod but was unable to be converted") + } + list, err := c.lister.PodPresets(pod.GetNamespace()).List(labels.Everything()) + if err != nil { + return fmt.Errorf("listing pod presets failed: %v", err) + } + + // get the pod presets and iterate over them + for _, pip := range list { + selector, err := metav1.LabelSelectorAsSelector(&pip.Spec.Selector) + if err != nil { + return fmt.Errorf("listing pod presets for namespace:%s failed: %v", pod.GetNamespace(), err) + } + + // check if the pod labels match the selector + if !selector.Matches(labels.Set(pod.Labels)) { + continue + } + + glog.V(4).Infof("PodPreset %s matches pod %s labels", pip.GetName(), pod.GetName()) + + // merge in policy for Env + if pip.Spec.Env != nil { + for i, ctr := range pod.Spec.Containers { + r, err := mergeEnv(pip, ctr.Env) + if err != nil { + // add event to pod + c.addEvent(pod, pip, err.Error()) + + return nil + } + pod.Spec.Containers[i].Env = r + } + } + + // merge in policy for EnvFrom + if pip.Spec.EnvFrom != nil { + for i, ctr := range pod.Spec.Containers { + r, err := mergeEnvFrom(pip, ctr.EnvFrom) + if err != nil { + // add event to pod + c.addEvent(pod, pip, err.Error()) + + return nil + } + pod.Spec.Containers[i].EnvFrom = r + } + } + + // merge in policy for VolumeMounts + if pip.Spec.VolumeMounts != nil { + for i, ctr := range pod.Spec.Containers { + r, err := mergeVolumeMounts(pip, ctr.VolumeMounts) + if err != nil { + // add event to pod + c.addEvent(pod, pip, err.Error()) + + return nil + } + pod.Spec.Containers[i].VolumeMounts = r + } + } + + // merge in policy for Volumes + if pip.Spec.Volumes != nil { + r, err := mergeVolumes(pip, pod.Spec.Volumes) + if err != nil { + // add event to pod + c.addEvent(pod, pip, err.Error()) + + return nil + } + pod.Spec.Volumes = r + } + + glog.V(4).Infof("PodPreset %s merged with pod %s successfully", pip.GetName(), pod.GetName()) + + // add annotation + if pod.ObjectMeta.Annotations == nil { + pod.ObjectMeta.Annotations = map[string]string{} + } + pod.ObjectMeta.Annotations[fmt.Sprintf("%s/%s", annotationPrefix, pip.GetName())] = pip.GetResourceVersion() + } + + return nil +} + +func mergeEnv(pip *settings.PodPreset, original []api.EnvVar) ([]api.EnvVar, error) { + // if there were no original envvar just return the pip envvar + if original == nil { + return pip.Spec.Env, nil + } + + orig := map[string]interface{}{} + for _, v := range original { + orig[v.Name] = v + } + + // check for conflicts. + for _, v := range pip.Spec.Env { + found, ok := orig[v.Name] + if !ok { + // if we don't already have it append it and continue + original = append(original, v) + continue + } + + // make sure they are identical or throw an error + if !reflect.DeepEqual(found, v) { + return nil, fmt.Errorf("merging env for %s has a conflict on %s: \n%#v\ndoes not match\n%#v\n in container", pip.GetName(), v.Name, v, found) + } + } + + return original, nil +} + +func mergeEnvFrom(pip *settings.PodPreset, original []api.EnvFromSource) ([]api.EnvFromSource, error) { + // if there were no original envfrom just return the pip envfrom + if original == nil { + return pip.Spec.EnvFrom, nil + } + + return append(original, pip.Spec.EnvFrom...), nil +} + +func mergeVolumeMounts(pip *settings.PodPreset, original []api.VolumeMount) ([]api.VolumeMount, error) { + // if there were no original volume mount just return the pip volume mount + if original == nil { + return pip.Spec.VolumeMounts, nil + } + + // first key by name + orig := map[string]interface{}{} + for _, v := range original { + orig[v.Name] = v + } + + // check for conflicts. + for _, v := range pip.Spec.VolumeMounts { + found, ok := orig[v.Name] + if !ok { + // if we don't already have it continue + continue + } + + // make sure they are identical or throw an error + if !reflect.DeepEqual(found, v) { + return nil, fmt.Errorf("merging volume mounts for %s has a conflict on %s: \n%#v\ndoes not match\n%#v\n in container", pip.GetName(), v.Name, v, found) + } + } + + // key by mount path + orig = map[string]interface{}{} + for _, v := range original { + orig[v.MountPath] = v + } + + // check for conflicts. + for _, v := range pip.Spec.VolumeMounts { + found, ok := orig[v.MountPath] + if !ok { + // if we don't already have it append it and continue + original = append(original, v) + continue + } + + // make sure they are identical or throw an error + if !reflect.DeepEqual(found, v) { + return nil, fmt.Errorf("merging volume mounts for %s has a conflict on mount path %s: \n%#v\ndoes not match\n%#v\n in container", pip.GetName(), v.MountPath, v, found) + } + } + + return original, nil +} + +func mergeVolumes(pip *settings.PodPreset, original []api.Volume) ([]api.Volume, error) { + // if there were no original volumes just return the pip volumes + if original == nil { + return pip.Spec.Volumes, nil + } + + orig := map[string]api.Volume{} + for _, v := range original { + orig[v.Name] = v + } + + // check for conflicts. + for _, v := range pip.Spec.Volumes { + found, ok := orig[v.Name] + if !ok { + // if we don't already have it append it and continue + original = append(original, v) + continue + } + + if !reflect.DeepEqual(found, v) { + return nil, fmt.Errorf("merging volumes for %s has a conflict on %s: \n%#v\ndoes not match\n%#v\nin pod spec", pip.GetName(), v.Name, v, found) + } + } + + return original, nil +} + +func (c *podPresetPlugin) addEvent(pod *api.Pod, pip *settings.PodPreset, message string) { + ref, err := api.GetReference(api.Scheme, pod) + if err != nil { + glog.Errorf("pip %s: get reference for pod %s failed: %v", pip.GetName(), pod.GetName(), err) + return + } + + e := &api.Event{ + InvolvedObject: *ref, + Message: message, + Source: api.EventSource{ + Component: fmt.Sprintf("pip %s", pip.GetName()), + }, + Type: "Warning", + } + + if _, err := c.client.Core().Events(pod.GetNamespace()).Create(e); err != nil { + glog.Errorf("pip %s: creating pod event failed: %v", pip.GetName(), err) + return + } +} diff --git a/plugin/pkg/admission/podpreset/admission_test.go b/plugin/pkg/admission/podpreset/admission_test.go new file mode 100644 index 00000000000..43f378cd6f8 --- /dev/null +++ b/plugin/pkg/admission/podpreset/admission_test.go @@ -0,0 +1,582 @@ +/* +Copyright 2016 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 admission + +import ( + "reflect" + "testing" + + "k8s.io/apimachinery/pkg/apis/meta/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + kadmission "k8s.io/apiserver/pkg/admission" + "k8s.io/apiserver/pkg/authentication/user" + "k8s.io/kubernetes/pkg/api" + "k8s.io/kubernetes/pkg/apis/settings" + "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset/fake" + informers "k8s.io/kubernetes/pkg/client/informers/informers_generated/internalversion" + settingslisters "k8s.io/kubernetes/pkg/client/listers/settings/internalversion" + "k8s.io/kubernetes/pkg/controller" +) + +func TestMergeEnv(t *testing.T) { + tests := map[string]struct { + orig []api.EnvVar + mod []api.EnvVar + result []api.EnvVar + shouldFail bool + }{ + "empty original": { + mod: []api.EnvVar{{Name: "abc", Value: "value2"}, {Name: "ABC", Value: "value3"}}, + result: []api.EnvVar{{Name: "abc", Value: "value2"}, {Name: "ABC", Value: "value3"}}, + shouldFail: false, + }, + "good merge": { + orig: []api.EnvVar{{Name: "abcd", Value: "value2"}, {Name: "hello", Value: "value3"}}, + mod: []api.EnvVar{{Name: "abc", Value: "value2"}, {Name: "ABC", Value: "value3"}}, + result: []api.EnvVar{{Name: "abcd", Value: "value2"}, {Name: "hello", Value: "value3"}, {Name: "abc", Value: "value2"}, {Name: "ABC", Value: "value3"}}, + shouldFail: false, + }, + "conflict": { + orig: []api.EnvVar{{Name: "abc", Value: "value3"}}, + mod: []api.EnvVar{{Name: "abc", Value: "value2"}, {Name: "ABC", Value: "value3"}}, + shouldFail: true, + }, + "one is exact same": { + orig: []api.EnvVar{{Name: "abc", Value: "value2"}, {Name: "hello", Value: "value3"}}, + mod: []api.EnvVar{{Name: "abc", Value: "value2"}, {Name: "ABC", Value: "value3"}}, + result: []api.EnvVar{{Name: "abc", Value: "value2"}, {Name: "hello", Value: "value3"}, {Name: "ABC", Value: "value3"}}, + shouldFail: false, + }, + } + + for name, test := range tests { + result, err := mergeEnv( + &settings.PodPreset{Spec: settings.PodPresetSpec{Env: test.mod}}, + test.orig, + ) + if test.shouldFail && err == nil { + t.Fatalf("expected test %q to fail but got nil", name) + } + if !test.shouldFail && err != nil { + t.Fatalf("test %q failed: %v", name, err) + } + if !reflect.DeepEqual(test.result, result) { + t.Fatalf("results were not equal for test %q: got %#v; expected: %#v", name, result, test.result) + } + } +} + +func TestMergeEnvFrom(t *testing.T) { + tests := map[string]struct { + orig []api.EnvFromSource + mod []api.EnvFromSource + result []api.EnvFromSource + shouldFail bool + }{ + "empty original": { + mod: []api.EnvFromSource{ + { + ConfigMapRef: &api.ConfigMapEnvSource{ + LocalObjectReference: api.LocalObjectReference{Name: "abc"}, + }, + }, + { + Prefix: "pre_", + ConfigMapRef: &api.ConfigMapEnvSource{ + LocalObjectReference: api.LocalObjectReference{Name: "abc"}, + }, + }, + }, + result: []api.EnvFromSource{ + { + ConfigMapRef: &api.ConfigMapEnvSource{ + LocalObjectReference: api.LocalObjectReference{Name: "abc"}, + }, + }, + { + Prefix: "pre_", + ConfigMapRef: &api.ConfigMapEnvSource{ + LocalObjectReference: api.LocalObjectReference{Name: "abc"}, + }, + }, + }, + shouldFail: false, + }, + "good merge": { + orig: []api.EnvFromSource{ + { + ConfigMapRef: &api.ConfigMapEnvSource{ + LocalObjectReference: api.LocalObjectReference{Name: "thing"}, + }, + }, + }, + mod: []api.EnvFromSource{ + { + ConfigMapRef: &api.ConfigMapEnvSource{ + LocalObjectReference: api.LocalObjectReference{Name: "abc"}, + }, + }, + { + Prefix: "pre_", + ConfigMapRef: &api.ConfigMapEnvSource{ + LocalObjectReference: api.LocalObjectReference{Name: "abc"}, + }, + }, + }, + result: []api.EnvFromSource{ + { + ConfigMapRef: &api.ConfigMapEnvSource{ + LocalObjectReference: api.LocalObjectReference{Name: "thing"}, + }, + }, + { + ConfigMapRef: &api.ConfigMapEnvSource{ + LocalObjectReference: api.LocalObjectReference{Name: "abc"}, + }, + }, + { + Prefix: "pre_", + ConfigMapRef: &api.ConfigMapEnvSource{ + LocalObjectReference: api.LocalObjectReference{Name: "abc"}, + }, + }, + }, + shouldFail: false, + }, + } + + for name, test := range tests { + result, err := mergeEnvFrom( + &settings.PodPreset{Spec: settings.PodPresetSpec{EnvFrom: test.mod}}, + test.orig, + ) + if test.shouldFail && err == nil { + t.Fatalf("expected test %q to fail but got nil", name) + } + if !test.shouldFail && err != nil { + t.Fatalf("test %q failed: %v", name, err) + } + if !reflect.DeepEqual(test.result, result) { + t.Fatalf("results were not equal for test %q: got %#v; expected: %#v", name, result, test.result) + } + } +} + +func TestMergeVolumeMounts(t *testing.T) { + tests := map[string]struct { + orig []api.VolumeMount + mod []api.VolumeMount + result []api.VolumeMount + shouldFail bool + }{ + "empty original": { + mod: []api.VolumeMount{ + { + Name: "simply-mounted-volume", + MountPath: "/opt/", + }, + }, + result: []api.VolumeMount{ + { + Name: "simply-mounted-volume", + MountPath: "/opt/", + }, + }, + shouldFail: false, + }, + "good merge": { + mod: []api.VolumeMount{ + { + Name: "simply-mounted-volume", + MountPath: "/opt/", + }, + }, + orig: []api.VolumeMount{ + { + Name: "etc-volume", + MountPath: "/etc/", + }, + }, + result: []api.VolumeMount{ + { + Name: "etc-volume", + MountPath: "/etc/", + }, + { + Name: "simply-mounted-volume", + MountPath: "/opt/", + }, + }, + shouldFail: false, + }, + "conflict": { + mod: []api.VolumeMount{ + { + Name: "simply-mounted-volume", + MountPath: "/opt/", + }, + { + Name: "etc-volume", + MountPath: "/things/", + }, + }, + orig: []api.VolumeMount{ + { + Name: "etc-volume", + MountPath: "/etc/", + }, + }, + shouldFail: true, + }, + "conflict on mount path": { + mod: []api.VolumeMount{ + { + Name: "simply-mounted-volume", + MountPath: "/opt/", + }, + { + Name: "things-volume", + MountPath: "/etc/", + }, + }, + orig: []api.VolumeMount{ + { + Name: "etc-volume", + MountPath: "/etc/", + }, + }, + shouldFail: true, + }, + "one is exact same": { + mod: []api.VolumeMount{ + { + Name: "simply-mounted-volume", + MountPath: "/opt/", + }, + { + Name: "etc-volume", + MountPath: "/etc/", + }, + }, + orig: []api.VolumeMount{ + { + Name: "etc-volume", + MountPath: "/etc/", + }, + }, + result: []api.VolumeMount{ + { + Name: "etc-volume", + MountPath: "/etc/", + }, + { + Name: "simply-mounted-volume", + MountPath: "/opt/", + }, + }, + shouldFail: false, + }, + } + + for name, test := range tests { + result, err := mergeVolumeMounts( + &settings.PodPreset{Spec: settings.PodPresetSpec{VolumeMounts: test.mod}}, + test.orig, + ) + if test.shouldFail && err == nil { + t.Fatalf("expected test %q to fail but got nil", name) + } + if !test.shouldFail && err != nil { + t.Fatalf("test %q failed: %v", name, err) + } + if !reflect.DeepEqual(test.result, result) { + t.Fatalf("results were not equal for test %q: got %#v; expected: %#v", name, result, test.result) + } + } +} + +func TestMergeVolumes(t *testing.T) { + tests := map[string]struct { + orig []api.Volume + mod []api.Volume + result []api.Volume + shouldFail bool + }{ + "empty original": { + mod: []api.Volume{ + {Name: "vol", VolumeSource: api.VolumeSource{EmptyDir: &api.EmptyDirVolumeSource{}}}, + {Name: "vol2", VolumeSource: api.VolumeSource{EmptyDir: &api.EmptyDirVolumeSource{}}}, + }, + result: []api.Volume{ + {Name: "vol", VolumeSource: api.VolumeSource{EmptyDir: &api.EmptyDirVolumeSource{}}}, + {Name: "vol2", VolumeSource: api.VolumeSource{EmptyDir: &api.EmptyDirVolumeSource{}}}, + }, + shouldFail: false, + }, + "good merge": { + orig: []api.Volume{ + {Name: "vol3", VolumeSource: api.VolumeSource{EmptyDir: &api.EmptyDirVolumeSource{}}}, + {Name: "vol4", VolumeSource: api.VolumeSource{EmptyDir: &api.EmptyDirVolumeSource{}}}, + }, + mod: []api.Volume{ + {Name: "vol", VolumeSource: api.VolumeSource{EmptyDir: &api.EmptyDirVolumeSource{}}}, + {Name: "vol2", VolumeSource: api.VolumeSource{EmptyDir: &api.EmptyDirVolumeSource{}}}, + }, + result: []api.Volume{ + {Name: "vol3", VolumeSource: api.VolumeSource{EmptyDir: &api.EmptyDirVolumeSource{}}}, + {Name: "vol4", VolumeSource: api.VolumeSource{EmptyDir: &api.EmptyDirVolumeSource{}}}, + {Name: "vol", VolumeSource: api.VolumeSource{EmptyDir: &api.EmptyDirVolumeSource{}}}, + {Name: "vol2", VolumeSource: api.VolumeSource{EmptyDir: &api.EmptyDirVolumeSource{}}}, + }, + shouldFail: false, + }, + "conflict": { + orig: []api.Volume{ + {Name: "vol3", VolumeSource: api.VolumeSource{EmptyDir: &api.EmptyDirVolumeSource{}}}, + {Name: "vol4", VolumeSource: api.VolumeSource{EmptyDir: &api.EmptyDirVolumeSource{}}}, + }, + mod: []api.Volume{ + {Name: "vol3", VolumeSource: api.VolumeSource{HostPath: &api.HostPathVolumeSource{Path: "/etc/apparmor.d"}}}, + {Name: "vol2", VolumeSource: api.VolumeSource{EmptyDir: &api.EmptyDirVolumeSource{}}}, + }, + shouldFail: true, + }, + "one is exact same": { + orig: []api.Volume{ + {Name: "vol3", VolumeSource: api.VolumeSource{EmptyDir: &api.EmptyDirVolumeSource{}}}, + {Name: "vol4", VolumeSource: api.VolumeSource{EmptyDir: &api.EmptyDirVolumeSource{}}}, + }, + mod: []api.Volume{ + {Name: "vol3", VolumeSource: api.VolumeSource{EmptyDir: &api.EmptyDirVolumeSource{}}}, + {Name: "vol2", VolumeSource: api.VolumeSource{EmptyDir: &api.EmptyDirVolumeSource{}}}, + }, + result: []api.Volume{ + {Name: "vol3", VolumeSource: api.VolumeSource{EmptyDir: &api.EmptyDirVolumeSource{}}}, + {Name: "vol4", VolumeSource: api.VolumeSource{EmptyDir: &api.EmptyDirVolumeSource{}}}, + {Name: "vol2", VolumeSource: api.VolumeSource{EmptyDir: &api.EmptyDirVolumeSource{}}}, + }, + shouldFail: false, + }, + } + + for name, test := range tests { + result, err := mergeVolumes( + &settings.PodPreset{Spec: settings.PodPresetSpec{Volumes: test.mod}}, + test.orig, + ) + if test.shouldFail && err == nil { + t.Fatalf("expected test %q to fail but got nil", name) + } + if !test.shouldFail && err != nil { + t.Fatalf("test %q failed: %v", name, err) + } + if !reflect.DeepEqual(test.result, result) { + t.Fatalf("results were not equal for test %q: got %#v; expected: %#v", name, result, test.result) + } + } +} + +// NewTestAdmission provides an admission plugin with test implementations of internal structs. It uses +// an authorizer that always returns true. +func NewTestAdmission(lister settingslisters.PodPresetLister, objects ...runtime.Object) kadmission.Interface { + // Build a test client that the admission plugin can use to look up the service account missing from its cache + client := fake.NewSimpleClientset(objects...) + + return &podPresetPlugin{ + client: client, + Handler: kadmission.NewHandler(kadmission.Create), + lister: lister, + } +} + +func TestAdmitConflictWithDifferentNamespaceShouldDoNothing(t *testing.T) { + containerName := "container" + + pod := &api.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "mypod", + Namespace: "namespace", + Labels: map[string]string{ + "security": "S2", + }, + }, + Spec: api.PodSpec{ + Containers: []api.Container{ + { + Name: containerName, + Env: []api.EnvVar{{Name: "abc", Value: "value2"}, {Name: "ABC", Value: "value3"}}, + }, + }, + }, + } + + pip := &settings.PodPreset{ + ObjectMeta: v1.ObjectMeta{ + Name: "hello", + Namespace: "othernamespace", + }, + Spec: settings.PodPresetSpec{ + Selector: v1.LabelSelector{ + MatchExpressions: []v1.LabelSelectorRequirement{ + { + Key: "security", + Operator: v1.LabelSelectorOpIn, + Values: []string{"S2"}, + }, + }, + }, + Env: []api.EnvVar{{Name: "abc", Value: "value"}, {Name: "ABC", Value: "value"}}, + }, + } + + err := admitPod(pod, pip) + if err != nil { + t.Fatal(err) + } +} + +func TestAdmitConflictWithNonMatchingLabelsShouldNotError(t *testing.T) { + containerName := "container" + + pod := &api.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "mypod", + Namespace: "namespace", + Labels: map[string]string{ + "security": "S2", + }, + }, + Spec: api.PodSpec{ + Containers: []api.Container{ + { + Name: containerName, + Env: []api.EnvVar{{Name: "abc", Value: "value2"}, {Name: "ABC", Value: "value3"}}, + }, + }, + }, + } + + pip := &settings.PodPreset{ + ObjectMeta: v1.ObjectMeta{ + Name: "hello", + Namespace: "namespace", + }, + Spec: settings.PodPresetSpec{ + Selector: v1.LabelSelector{ + MatchExpressions: []v1.LabelSelectorRequirement{ + { + Key: "security", + Operator: v1.LabelSelectorOpIn, + Values: []string{"S3"}, + }, + }, + }, + Env: []api.EnvVar{{Name: "abc", Value: "value"}, {Name: "ABC", Value: "value"}}, + }, + } + + err := admitPod(pod, pip) + if err != nil { + t.Fatal(err) + } +} + +func TestAdmit(t *testing.T) { + containerName := "container" + + pod := &api.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "mypod", + Namespace: "namespace", + Labels: map[string]string{ + "security": "S2", + }, + }, + Spec: api.PodSpec{ + Containers: []api.Container{ + { + Name: containerName, + Env: []api.EnvVar{{Name: "abc", Value: "value2"}, {Name: "ABCD", Value: "value3"}}, + }, + }, + }, + } + + pip := &settings.PodPreset{ + ObjectMeta: v1.ObjectMeta{ + Name: "hello", + Namespace: "namespace", + }, + Spec: settings.PodPresetSpec{ + Selector: v1.LabelSelector{ + MatchExpressions: []v1.LabelSelectorRequirement{ + { + Key: "security", + Operator: v1.LabelSelectorOpIn, + Values: []string{"S2"}, + }, + }, + }, + Volumes: []api.Volume{{Name: "vol", VolumeSource: api.VolumeSource{EmptyDir: &api.EmptyDirVolumeSource{}}}}, + Env: []api.EnvVar{{Name: "abcd", Value: "value"}, {Name: "ABC", Value: "value"}}, + EnvFrom: []api.EnvFromSource{ + { + ConfigMapRef: &api.ConfigMapEnvSource{ + LocalObjectReference: api.LocalObjectReference{Name: "abc"}, + }, + }, + { + Prefix: "pre_", + ConfigMapRef: &api.ConfigMapEnvSource{ + LocalObjectReference: api.LocalObjectReference{Name: "abc"}, + }, + }, + }, + }, + } + + err := admitPod(pod, pip) + if err != nil { + t.Fatal(err) + } +} + +func admitPod(pod *api.Pod, pip *settings.PodPreset) error { + informerFactory := informers.NewSharedInformerFactory(nil, controller.NoResyncPeriodFunc()) + store := informerFactory.Settings().InternalVersion().PodPresets().Informer().GetStore() + store.Add(pip) + plugin := NewTestAdmission(informerFactory.Settings().InternalVersion().PodPresets().Lister()) + attrs := kadmission.NewAttributesRecord( + pod, + nil, + api.Kind("Pod").WithVersion("version"), + "namespace", + "", + api.Resource("pods").WithVersion("version"), + "", + kadmission.Create, + &user.DefaultInfo{}, + ) + + err := plugin.Admit(attrs) + if err != nil { + return err + } + + return nil +} diff --git a/test/e2e/BUILD b/test/e2e/BUILD index e619ccbc816..161db9e76e7 100644 --- a/test/e2e/BUILD +++ b/test/e2e/BUILD @@ -73,6 +73,7 @@ go_library( "persistent_volumes-disruptive.go", "persistent_volumes-vsphere.go", "pod_gc.go", + "podpreset.go", "pods.go", "portforward.go", "pre_stop.go", @@ -118,6 +119,7 @@ go_library( "//pkg/apis/extensions:go_default_library", "//pkg/apis/extensions/v1beta1:go_default_library", "//pkg/apis/rbac/v1beta1:go_default_library", + "//pkg/apis/settings/v1alpha1:go_default_library", "//pkg/apis/storage/util:go_default_library", "//pkg/apis/storage/v1beta1:go_default_library", "//pkg/apis/storage/v1beta1/util:go_default_library", diff --git a/test/e2e/podpreset.go b/test/e2e/podpreset.go new file mode 100644 index 00000000000..5cd75703460 --- /dev/null +++ b/test/e2e/podpreset.go @@ -0,0 +1,274 @@ +/* +Copyright 2015 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 e2e + +import ( + "reflect" + "strconv" + "time" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/watch" + "k8s.io/kubernetes/pkg/api/v1" + settings "k8s.io/kubernetes/pkg/apis/settings/v1alpha1" + "k8s.io/kubernetes/pkg/client/clientset_generated/clientset" + "k8s.io/kubernetes/test/e2e/framework" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = framework.KubeDescribe("PodPreset", func() { + f := framework.NewDefaultFramework("podpreset") + + var podClient *framework.PodClient + BeforeEach(func() { + // only run on gce for the time being til we find an easier way to update + // the admission controllers used on the others + framework.SkipUnlessProviderIs("gce") + podClient = f.PodClient() + }) + + // Simplest case: all pods succeed promptly + It("should create a pod preset", func() { + By("Creating a pod preset") + + pip := &settings.PodPreset{ + ObjectMeta: metav1.ObjectMeta{ + Name: "hello", + Namespace: f.Namespace.Name, + }, + Spec: settings.PodPresetSpec{ + Selector: metav1.LabelSelector{ + MatchExpressions: []metav1.LabelSelectorRequirement{ + { + Key: "security", + Operator: metav1.LabelSelectorOpIn, + Values: []string{"S2"}, + }, + }, + }, + Volumes: []v1.Volume{{Name: "vol", VolumeSource: v1.VolumeSource{EmptyDir: &v1.EmptyDirVolumeSource{}}}}, + VolumeMounts: []v1.VolumeMount{ + {Name: "vol", MountPath: "/foo"}, + }, + Env: []v1.EnvVar{{Name: "abc", Value: "value"}, {Name: "ABC", Value: "value"}}, + }, + } + + _, err := createPodPreset(f.ClientSet, f.Namespace.Name, pip) + Expect(err).NotTo(HaveOccurred()) + + By("creating the pod") + name := "pod-preset-pod" + value := strconv.Itoa(time.Now().Nanosecond()) + pod := &v1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: f.Namespace.Name, + Labels: map[string]string{ + "name": "foo", + "time": value, + "security": "S2", + }, + }, + Spec: v1.PodSpec{ + Containers: []v1.Container{ + { + Name: "nginx", + Image: "gcr.io/google_containers/nginx-slim:0.7", + }, + }, + }, + } + + By("setting up watch") + selector := labels.SelectorFromSet(labels.Set(map[string]string{"time": value})) + options := metav1.ListOptions{LabelSelector: selector.String()} + pods, err := podClient.List(options) + Expect(err).NotTo(HaveOccurred(), "failed to query for pod") + Expect(len(pods.Items)).To(Equal(0)) + options = metav1.ListOptions{ + LabelSelector: selector.String(), + ResourceVersion: pods.ListMeta.ResourceVersion, + } + w, err := podClient.Watch(options) + Expect(err).NotTo(HaveOccurred(), "failed to set up watch") + + By("submitting the pod to kubernetes") + podClient.Create(pod) + + By("verifying the pod is in kubernetes") + selector = labels.SelectorFromSet(labels.Set(map[string]string{"time": value})) + options = metav1.ListOptions{LabelSelector: selector.String()} + pods, err = podClient.List(options) + Expect(err).NotTo(HaveOccurred(), "failed to query for pod") + Expect(len(pods.Items)).To(Equal(1)) + + By("verifying pod creation was observed") + select { + case event, _ := <-w.ResultChan(): + if event.Type != watch.Added { + framework.Failf("Failed to observe pod creation: %v", event) + } + case <-time.After(framework.PodStartTimeout): + Fail("Timeout while waiting for pod creation") + } + + // We need to wait for the pod to be running, otherwise the deletion + // may be carried out immediately rather than gracefully. + framework.ExpectNoError(f.WaitForPodRunning(pod.Name)) + + By("ensuring pod is modified") + // save the running pod + pod, err = podClient.Get(pod.Name, metav1.GetOptions{}) + Expect(err).NotTo(HaveOccurred(), "failed to GET scheduled pod") + + // check the annotation is there + if _, ok := pod.Annotations["podpreset.admission.kubernetes.io/hello"]; !ok { + framework.Failf("Annotation not found in pod annotations: \n%v\n", pod.Annotations) + } + + // verify the env is the same + if !reflect.DeepEqual(pip.Spec.Env, pod.Spec.Containers[0].Env) { + framework.Failf("env of pod container does not match the env of the pip: expected %#v, got: %#v", pip.Spec.Env, pod.Spec.Containers[0].Env) + } + }) + + It("should not modify the pod on conflict", func() { + By("Creating a pod preset") + + pip := &settings.PodPreset{ + ObjectMeta: metav1.ObjectMeta{ + Name: "hello", + Namespace: f.Namespace.Name, + }, + Spec: settings.PodPresetSpec{ + Selector: metav1.LabelSelector{ + MatchExpressions: []metav1.LabelSelectorRequirement{ + { + Key: "security", + Operator: metav1.LabelSelectorOpIn, + Values: []string{"S2"}, + }, + }, + }, + Volumes: []v1.Volume{{Name: "vol", VolumeSource: v1.VolumeSource{EmptyDir: &v1.EmptyDirVolumeSource{}}}}, + VolumeMounts: []v1.VolumeMount{ + {Name: "vol", MountPath: "/foo"}, + }, + Env: []v1.EnvVar{{Name: "abc", Value: "value"}, {Name: "ABC", Value: "value"}}, + }, + } + + _, err := createPodPreset(f.ClientSet, f.Namespace.Name, pip) + Expect(err).NotTo(HaveOccurred()) + + By("creating the pod") + name := "pod-preset-pod" + value := strconv.Itoa(time.Now().Nanosecond()) + originalPod := &v1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: f.Namespace.Name, + Labels: map[string]string{ + "name": "foo", + "time": value, + "security": "S2", + }, + }, + Spec: v1.PodSpec{ + Containers: []v1.Container{ + { + Name: "nginx", + Image: "gcr.io/google_containers/nginx-slim:0.7", + Env: []v1.EnvVar{{Name: "abc", Value: "value2"}, {Name: "ABC", Value: "value"}}, + }, + }, + }, + } + + By("setting up watch") + selector := labels.SelectorFromSet(labels.Set(map[string]string{"time": value})) + options := metav1.ListOptions{LabelSelector: selector.String()} + pods, err := podClient.List(options) + Expect(err).NotTo(HaveOccurred(), "failed to query for pod") + Expect(len(pods.Items)).To(Equal(0)) + options = metav1.ListOptions{ + LabelSelector: selector.String(), + ResourceVersion: pods.ListMeta.ResourceVersion, + } + w, err := podClient.Watch(options) + Expect(err).NotTo(HaveOccurred(), "failed to set up watch") + + By("submitting the pod to kubernetes") + podClient.Create(originalPod) + + By("verifying the pod is in kubernetes") + selector = labels.SelectorFromSet(labels.Set(map[string]string{"time": value})) + options = metav1.ListOptions{LabelSelector: selector.String()} + pods, err = podClient.List(options) + Expect(err).NotTo(HaveOccurred(), "failed to query for pod") + Expect(len(pods.Items)).To(Equal(1)) + + By("verifying pod creation was observed") + select { + case event, _ := <-w.ResultChan(): + if event.Type != watch.Added { + framework.Failf("Failed to observe pod creation: %v", event) + } + case <-time.After(framework.PodStartTimeout): + Fail("Timeout while waiting for pod creation") + } + + // We need to wait for the pod to be running, otherwise the deletion + // may be carried out immediately rather than gracefully. + framework.ExpectNoError(f.WaitForPodRunning(originalPod.Name)) + + By("ensuring pod is modified") + // save the running pod + pod, err := podClient.Get(originalPod.Name, metav1.GetOptions{}) + Expect(err).NotTo(HaveOccurred(), "failed to GET scheduled pod") + + // check the annotation is not there + if _, ok := pod.Annotations["podpreset.admission.kubernetes.io/hello"]; ok { + framework.Failf("Annotation found in pod annotations and should not be: \n%v\n", pod.Annotations) + } + + // verify the env is the same + if !reflect.DeepEqual(originalPod.Spec.Containers[0].Env, pod.Spec.Containers[0].Env) { + framework.Failf("env of pod container does not match the env of the original pod: expected %#v, got: %#v", originalPod.Spec.Containers[0].Env, pod.Spec.Containers[0].Env) + } + }) +}) + +func getPodPreset(c clientset.Interface, ns, name string) (*settings.PodPreset, error) { + return c.Settings().PodPresets(ns).Get(name, metav1.GetOptions{}) +} + +func createPodPreset(c clientset.Interface, ns string, job *settings.PodPreset) (*settings.PodPreset, error) { + return c.Settings().PodPresets(ns).Create(job) +} + +func updatePodPreset(c clientset.Interface, ns string, job *settings.PodPreset) (*settings.PodPreset, error) { + return c.Settings().PodPresets(ns).Update(job) +} + +func deletePodPreset(c clientset.Interface, ns, name string) error { + return c.Settings().PodPresets(ns).Delete(name, nil) +}