mirror of
https://github.com/optim-enterprises-bv/kubernetes.git
synced 2025-11-02 11:18:16 +00:00
Merge pull request #41931 from jessfraz/pip
Automatic merge from submit-queue (batch tested with PRs 41931, 39821, 41841, 42197, 42195) Admission Controller: Add Pod Preset Based off the proposal in https://github.com/kubernetes/community/pull/254 cc @pmorie @pwittrock TODO: - [ ] tests **What this PR does / why we need it**: Implements the Pod Injection Policy admission controller **Which issue this PR fixes** *(optional, in `fixes #<issue number>(, fixes #<issue_number>, ...)` format, will close that issue when PR gets merged)*: fixes # **Special notes for your reviewer**: **Release note**: ```release-note Added new Api `PodPreset` to enable defining cross-cutting injection of Volumes and Environment into Pods. ```
This commit is contained in:
@@ -29,6 +29,7 @@ filegroup(
|
||||
"//plugin/pkg/admission/namespace/lifecycle:all-srcs",
|
||||
"//plugin/pkg/admission/persistentvolume/label:all-srcs",
|
||||
"//plugin/pkg/admission/podnodeselector:all-srcs",
|
||||
"//plugin/pkg/admission/podpreset:all-srcs",
|
||||
"//plugin/pkg/admission/resourcequota:all-srcs",
|
||||
"//plugin/pkg/admission/security:all-srcs",
|
||||
"//plugin/pkg/admission/securitycontext/scdeny:all-srcs",
|
||||
|
||||
60
plugin/pkg/admission/podpreset/BUILD
Normal file
60
plugin/pkg/admission/podpreset/BUILD
Normal file
@@ -0,0 +1,60 @@
|
||||
package(default_visibility = ["//visibility:public"])
|
||||
|
||||
licenses(["notice"])
|
||||
|
||||
load(
|
||||
"@io_bazel_rules_go//go:def.bzl",
|
||||
"go_library",
|
||||
"go_test",
|
||||
)
|
||||
|
||||
go_test(
|
||||
name = "go_default_test",
|
||||
srcs = ["admission_test.go"],
|
||||
library = ":go_default_library",
|
||||
tags = ["automanaged"],
|
||||
deps = [
|
||||
"//pkg/api:go_default_library",
|
||||
"//pkg/apis/settings:go_default_library",
|
||||
"//pkg/client/clientset_generated/internalclientset/fake:go_default_library",
|
||||
"//pkg/client/informers/informers_generated/internalversion:go_default_library",
|
||||
"//pkg/client/listers/settings/internalversion:go_default_library",
|
||||
"//pkg/controller:go_default_library",
|
||||
"//vendor:k8s.io/apimachinery/pkg/apis/meta/v1",
|
||||
"//vendor:k8s.io/apimachinery/pkg/runtime",
|
||||
"//vendor:k8s.io/apiserver/pkg/admission",
|
||||
"//vendor:k8s.io/apiserver/pkg/authentication/user",
|
||||
],
|
||||
)
|
||||
|
||||
go_library(
|
||||
name = "go_default_library",
|
||||
srcs = ["admission.go"],
|
||||
tags = ["automanaged"],
|
||||
deps = [
|
||||
"//pkg/api:go_default_library",
|
||||
"//pkg/apis/settings:go_default_library",
|
||||
"//pkg/client/clientset_generated/internalclientset:go_default_library",
|
||||
"//pkg/client/informers/informers_generated/internalversion:go_default_library",
|
||||
"//pkg/client/listers/settings/internalversion:go_default_library",
|
||||
"//pkg/kubeapiserver/admission:go_default_library",
|
||||
"//vendor:github.com/golang/glog",
|
||||
"//vendor:k8s.io/apimachinery/pkg/api/errors",
|
||||
"//vendor:k8s.io/apimachinery/pkg/apis/meta/v1",
|
||||
"//vendor:k8s.io/apimachinery/pkg/labels",
|
||||
"//vendor:k8s.io/apiserver/pkg/admission",
|
||||
],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "package-srcs",
|
||||
srcs = glob(["**"]),
|
||||
tags = ["automanaged"],
|
||||
visibility = ["//visibility:private"],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "all-srcs",
|
||||
srcs = [":package-srcs"],
|
||||
tags = ["automanaged"],
|
||||
)
|
||||
320
plugin/pkg/admission/podpreset/admission.go
Normal file
320
plugin/pkg/admission/podpreset/admission.go
Normal file
@@ -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
|
||||
}
|
||||
}
|
||||
582
plugin/pkg/admission/podpreset/admission_test.go
Normal file
582
plugin/pkg/admission/podpreset/admission_test.go
Normal file
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user