func init() { admission.RegisterPlugin("OwnerReferencesPermissionEnforcement", func(config io.Reader) (admission.Interface, error) { return &gcPermissionsEnforcement{ Handler: admission.NewHandler(admission.Create, admission.Update), }, nil }) }
// NewImagePolicyWebhook a new imagePolicyWebhook from the provided config file. // The config file is specified by --admission-controller-config-file and has the // following format for a webhook: // // { // "imagePolicy": { // "kubeConfigFile": "path/to/kubeconfig/for/backend", // "allowTTL": 30, # time in s to cache approval // "denyTTL": 30, # time in s to cache denial // "retryBackoff": 500, # time in ms to wait between retries // "defaultAllow": true # determines behavior if the webhook backend fails // } // } // // The config file may be json or yaml. // // The kubeconfig property refers to another file in the kubeconfig format which // specifies how to connect to the webhook backend. // // The kubeconfig's cluster field is used to refer to the remote service, user refers to the returned authorizer. // // # clusters refers to the remote service. // clusters: // - name: name-of-remote-imagepolicy-service // cluster: // certificate-authority: /path/to/ca.pem # CA for verifying the remote service. // server: https://images.example.com/policy # URL of remote service to query. Must use 'https'. // // # users refers to the API server's webhook configuration. // users: // - name: name-of-api-server // user: // client-certificate: /path/to/cert.pem # cert for the webhook plugin to use // client-key: /path/to/key.pem # key matching the cert // // For additional HTTP configuration, refer to the kubeconfig documentation // http://kubernetes.io/v1.1/docs/user-guide/kubeconfig-file.html. func NewImagePolicyWebhook(configFile io.Reader) (admission.Interface, error) { // TODO: move this to a versioned configuration file format var config AdmissionConfig d := yaml.NewYAMLOrJSONDecoder(configFile, 4096) err := d.Decode(&config) if err != nil { return nil, err } whConfig := config.ImagePolicyWebhook if err := normalizeWebhookConfig(&whConfig); err != nil { return nil, err } gw, err := webhook.NewGenericWebhook(whConfig.KubeConfigFile, groupVersions, whConfig.RetryBackoff) if err != nil { return nil, err } return &imagePolicyWebhook{ Handler: admission.NewHandler(admission.Create, admission.Update), webhook: gw, responseCache: cache.NewLRUExpireCache(1024), allowTTL: whConfig.AllowTTL, denyTTL: whConfig.DenyTTL, defaultAllow: whConfig.DefaultAllow, }, nil }
// TestAdmissionIgnoresSubresources verifies that the admission controller ignores subresources // It verifies that creation of a pod that would have exceeded quota is properly failed // It verifies that create operations to a subresource that would have exceeded quota would succeed func TestAdmissionIgnoresSubresources(t *testing.T) { resourceQuota := &api.ResourceQuota{} resourceQuota.Name = "quota" resourceQuota.Namespace = "test" resourceQuota.Status = api.ResourceQuotaStatus{ Hard: api.ResourceList{}, Used: api.ResourceList{}, } resourceQuota.Status.Hard[api.ResourceMemory] = resource.MustParse("2Gi") resourceQuota.Status.Used[api.ResourceMemory] = resource.MustParse("1Gi") kubeClient := fake.NewSimpleClientset(resourceQuota) indexer := cache.NewIndexer(cache.MetaNamespaceKeyFunc, cache.Indexers{"namespace": cache.MetaNamespaceIndexFunc}) stopCh := make(chan struct{}) defer close(stopCh) quotaAccessor, _ := newQuotaAccessor(kubeClient) quotaAccessor.indexer = indexer go quotaAccessor.Run(stopCh) evaluator := NewQuotaEvaluator(quotaAccessor, install.NewRegistry(nil, nil), nil, 5, stopCh) handler := "aAdmission{ Handler: admission.NewHandler(admission.Create, admission.Update), evaluator: evaluator, } indexer.Add(resourceQuota) newPod := validPod("123", 1, getResourceRequirements(getResourceList("100m", "2Gi"), getResourceList("", ""))) err := handler.Admit(admission.NewAttributesRecord(newPod, nil, api.Kind("Pod").WithVersion("version"), newPod.Namespace, newPod.Name, api.Resource("pods").WithVersion("version"), "", admission.Create, nil)) if err == nil { t.Errorf("Expected an error because the pod exceeded allowed quota") } err = handler.Admit(admission.NewAttributesRecord(newPod, nil, api.Kind("Pod").WithVersion("version"), newPod.Namespace, newPod.Name, api.Resource("pods").WithVersion("version"), "subresource", admission.Create, nil)) if err != nil { t.Errorf("Did not expect an error because the action went to a subresource: %v", err) } }
func newInitialResources(source dataSource, percentile int64, nsOnly bool) admission.Interface { return &initialResources{ Handler: admission.NewHandler(admission.Create), source: source, percentile: percentile, nsOnly: nsOnly, } }
// NewDenyExecOnPrivileged creates a new admission controller that is only checking the privileged // option. This is for legacy support of the DenyExecOnPrivileged admission controller. Most // of the time NewDenyEscalatingExec should be preferred. func NewDenyExecOnPrivileged() admission.Interface { return &denyExec{ Handler: admission.NewHandler(admission.Connect), hostIPC: false, hostPID: false, privileged: true, } }
// NewDenyEscalatingExec creates a new admission controller that denies an exec operation on a pod // using host based configurations. func NewDenyEscalatingExec() admission.Interface { return &denyExec{ Handler: admission.NewHandler(admission.Connect), hostIPC: true, hostPID: true, privileged: true, } }
func newLifecycleWithClock(immortalNamespaces sets.String, clock utilcache.Clock) (admission.Interface, error) { forceLiveLookupCache := utilcache.NewLRUExpireCacheWithClock(100, clock) return &lifecycle{ Handler: admission.NewHandler(admission.Create, admission.Update, admission.Delete), immortalNamespaces: immortalNamespaces, forceLiveLookupCache: forceLiveLookupCache, }, nil }
// NewResourceQuota configures an admission controller that can enforce quota constraints // using the provided registry. The registry must have the capability to handle group/kinds that // are persisted by the server this admission controller is intercepting func NewResourceQuota(registry quota.Registry, numEvaluators int, stopCh <-chan struct{}) (admission.Interface, error) { return "aAdmission{ Handler: admission.NewHandler(admission.Create, admission.Update), stopCh: stopCh, registry: registry, numEvaluators: numEvaluators, }, nil }
// NewPlugin creates a new PSP admission plugin. func NewPlugin(strategyFactory psp.StrategyFactory, pspMatcher PSPMatchFn, failOnNoPolicies bool) *podSecurityPolicyPlugin { return &podSecurityPolicyPlugin{ Handler: admission.NewHandler(admission.Create, admission.Update), strategyFactory: strategyFactory, pspMatcher: pspMatcher, failOnNoPolicies: failOnNoPolicies, } }
// Test to ensure legacy admission controller works as expected. func TestDenyExecOnPrivileged(t *testing.T) { privPod := validPod("privileged") priv := true privPod.Spec.Containers[0].SecurityContext = &api.SecurityContext{ Privileged: &priv, } hostPIDPod := validPod("hostPID") hostPIDPod.Spec.SecurityContext = &api.PodSecurityContext{} hostPIDPod.Spec.SecurityContext.HostPID = true hostIPCPod := validPod("hostIPC") hostIPCPod.Spec.SecurityContext = &api.PodSecurityContext{} hostIPCPod.Spec.SecurityContext.HostIPC = true testCases := map[string]struct { pod *api.Pod shouldAccept bool }{ "priv": { shouldAccept: false, pod: privPod, }, "hostPID": { shouldAccept: true, pod: hostPIDPod, }, "hostIPC": { shouldAccept: true, pod: hostIPCPod, }, "non privileged": { shouldAccept: true, pod: validPod("nonPrivileged"), }, } // use the same code as NewDenyExecOnPrivileged, using the direct object though to allow testAdmission to // inject the client handler := &denyExec{ Handler: admission.NewHandler(admission.Connect), hostIPC: false, hostPID: false, privileged: true, } for _, tc := range testCases { testAdmission(t, tc.pod, handler, tc.shouldAccept) } // test init containers for _, tc := range testCases { tc.pod.Spec.InitContainers = tc.pod.Spec.Containers tc.pod.Spec.Containers = nil testAdmission(t, tc.pod, handler, tc.shouldAccept) } }
// TestAdmissionSetsMissingNamespace verifies that if an object lacks a // namespace, it will be set. func TestAdmissionSetsMissingNamespace(t *testing.T) { namespace := "test" resourceQuota := &api.ResourceQuota{ ObjectMeta: metav1.ObjectMeta{Name: "quota", Namespace: namespace, ResourceVersion: "124"}, Status: api.ResourceQuotaStatus{ Hard: api.ResourceList{ api.ResourcePods: resource.MustParse("3"), }, Used: api.ResourceList{ api.ResourcePods: resource.MustParse("1"), }, }, } kubeClient := fake.NewSimpleClientset(resourceQuota) indexer := cache.NewIndexer(cache.MetaNamespaceKeyFunc, cache.Indexers{"namespace": cache.MetaNamespaceIndexFunc}) // create a dummy evaluator so we can trigger quota podEvaluator := &generic.ObjectCountEvaluator{ AllowCreateOnUpdate: false, InternalGroupKind: api.Kind("Pod"), ResourceName: api.ResourcePods, } registry := &generic.GenericRegistry{ InternalEvaluators: map[schema.GroupKind]quota.Evaluator{ podEvaluator.GroupKind(): podEvaluator, }, } stopCh := make(chan struct{}) defer close(stopCh) quotaAccessor, _ := newQuotaAccessor(kubeClient) quotaAccessor.indexer = indexer go quotaAccessor.Run(stopCh) evaluator := NewQuotaEvaluator(quotaAccessor, install.NewRegistry(nil, nil), nil, 5, stopCh) evaluator.(*quotaEvaluator).registry = registry handler := "aAdmission{ Handler: admission.NewHandler(admission.Create, admission.Update), evaluator: evaluator, } indexer.Add(resourceQuota) newPod := validPod("pod-without-namespace", 1, getResourceRequirements(getResourceList("1", "2Gi"), getResourceList("", ""))) // unset the namespace newPod.ObjectMeta.Namespace = "" err := handler.Admit(admission.NewAttributesRecord(newPod, nil, api.Kind("Pod").WithVersion("version"), namespace, newPod.Name, api.Resource("pods").WithVersion("version"), "", admission.Create, nil)) if err != nil { t.Errorf("Got unexpected error: %v", err) } if newPod.Namespace != namespace { t.Errorf("Got unexpected pod namespace: %q != %q", newPod.Namespace, namespace) } }
// NewServiceAccount returns an admission.Interface implementation which limits admission of Pod CREATE requests based on the pod's ServiceAccount: // 1. If the pod does not specify a ServiceAccount, it sets the pod's ServiceAccount to "default" // 2. It ensures the ServiceAccount referenced by the pod exists // 3. If LimitSecretReferences is true, it rejects the pod if the pod references Secret objects which the pod's ServiceAccount does not reference // 4. If the pod does not contain any ImagePullSecrets, the ImagePullSecrets of the service account are added. // 5. If MountServiceAccountToken is true, it adds a VolumeMount with the pod's ServiceAccount's api token secret to containers func NewServiceAccount() *serviceAccount { return &serviceAccount{ Handler: admission.NewHandler(admission.Create), // TODO: enable this once we've swept secret usage to account for adding secret references to service accounts LimitSecretReferences: false, // Auto mount service account API token secrets MountServiceAccountToken: true, // Reject pod creation until a service account token is available RequireAPIToken: true, } }
// TestAdmitPodInNamespaceWithoutQuota ensures that if a namespace has no quota, that a pod can get in func TestAdmitPodInNamespaceWithoutQuota(t *testing.T) { resourceQuota := &api.ResourceQuota{ ObjectMeta: metav1.ObjectMeta{Name: "quota", Namespace: "other", ResourceVersion: "124"}, Status: api.ResourceQuotaStatus{ Hard: api.ResourceList{ api.ResourceCPU: resource.MustParse("3"), api.ResourceMemory: resource.MustParse("100Gi"), api.ResourceLimitsMemory: resource.MustParse("200Gi"), api.ResourcePods: resource.MustParse("5"), }, Used: api.ResourceList{ api.ResourceCPU: resource.MustParse("1"), api.ResourceMemory: resource.MustParse("50Gi"), api.ResourceLimitsMemory: resource.MustParse("100Gi"), api.ResourcePods: resource.MustParse("3"), }, }, } kubeClient := fake.NewSimpleClientset(resourceQuota) indexer := cache.NewIndexer(cache.MetaNamespaceKeyFunc, cache.Indexers{"namespace": cache.MetaNamespaceIndexFunc}) liveLookupCache, err := lru.New(100) if err != nil { t.Fatal(err) } stopCh := make(chan struct{}) defer close(stopCh) quotaAccessor, _ := newQuotaAccessor(kubeClient) quotaAccessor.indexer = indexer quotaAccessor.liveLookupCache = liveLookupCache go quotaAccessor.Run(stopCh) evaluator := NewQuotaEvaluator(quotaAccessor, install.NewRegistry(nil, nil), nil, 5, stopCh) handler := "aAdmission{ Handler: admission.NewHandler(admission.Create, admission.Update), evaluator: evaluator, } // Add to the index indexer.Add(resourceQuota) newPod := validPod("not-allowed-pod", 1, getResourceRequirements(getResourceList("100m", "2Gi"), getResourceList("200m", ""))) // Add to the lru cache so we do not do a live client lookup liveLookupCache.Add(newPod.Namespace, liveLookupEntry{expiry: time.Now().Add(time.Duration(30 * time.Second)), items: []*api.ResourceQuota{}}) err = handler.Admit(admission.NewAttributesRecord(newPod, nil, api.Kind("Pod").WithVersion("version"), newPod.Namespace, newPod.Name, api.Resource("pods").WithVersion("version"), "", admission.Create, nil)) if err != nil { t.Errorf("Did not expect an error because the pod is in a different namespace than the quota") } }
// NewLimitRanger returns an object that enforces limits based on the supplied limit function func NewLimitRanger(actions LimitRangerActions) (admission.Interface, error) { liveLookupCache, err := lru.New(10000) if err != nil { return nil, err } if actions == nil { actions = &DefaultLimitRangerActions{} } return &limitRanger{ Handler: admission.NewHandler(admission.Create, admission.Update), actions: actions, liveLookupCache: liveLookupCache, liveTTL: time.Duration(30 * time.Second), }, nil }
// TestAdmitEnforceQuotaConstraints verifies that if a quota tracks a particular resource that that resource is // specified on the pod. In this case, we create a quota that tracks cpu request, memory request, and memory limit. // We ensure that a pod that does not specify a memory limit that it fails in admission. func TestAdmitEnforceQuotaConstraints(t *testing.T) { resourceQuota := &api.ResourceQuota{ ObjectMeta: metav1.ObjectMeta{Name: "quota", Namespace: "test", ResourceVersion: "124"}, Status: api.ResourceQuotaStatus{ Hard: api.ResourceList{ api.ResourceCPU: resource.MustParse("3"), api.ResourceMemory: resource.MustParse("100Gi"), api.ResourceLimitsMemory: resource.MustParse("200Gi"), api.ResourcePods: resource.MustParse("5"), }, Used: api.ResourceList{ api.ResourceCPU: resource.MustParse("1"), api.ResourceMemory: resource.MustParse("50Gi"), api.ResourceLimitsMemory: resource.MustParse("100Gi"), api.ResourcePods: resource.MustParse("3"), }, }, } kubeClient := fake.NewSimpleClientset(resourceQuota) indexer := cache.NewIndexer(cache.MetaNamespaceKeyFunc, cache.Indexers{"namespace": cache.MetaNamespaceIndexFunc}) stopCh := make(chan struct{}) defer close(stopCh) quotaAccessor, _ := newQuotaAccessor(kubeClient) quotaAccessor.indexer = indexer go quotaAccessor.Run(stopCh) evaluator := NewQuotaEvaluator(quotaAccessor, install.NewRegistry(nil, nil), nil, 5, stopCh) handler := "aAdmission{ Handler: admission.NewHandler(admission.Create, admission.Update), evaluator: evaluator, } indexer.Add(resourceQuota) // verify all values are specified as required on the quota newPod := validPod("not-allowed-pod", 1, getResourceRequirements(getResourceList("100m", "2Gi"), getResourceList("200m", ""))) err := handler.Admit(admission.NewAttributesRecord(newPod, nil, api.Kind("Pod").WithVersion("version"), newPod.Namespace, newPod.Name, api.Resource("pods").WithVersion("version"), "", admission.Create, nil)) if err == nil { t.Errorf("Expected an error because the pod does not specify a memory limit") } // verify the requests and limits are actually valid (in this case, we fail because the limits < requests) newPod = validPod("not-allowed-pod", 1, getResourceRequirements(getResourceList("200m", "2Gi"), getResourceList("100m", "1Gi"))) err = handler.Admit(admission.NewAttributesRecord(newPod, nil, api.Kind("Pod").WithVersion("version"), newPod.Namespace, newPod.Name, api.Resource("pods").WithVersion("version"), "", admission.Create, nil)) if err == nil { t.Errorf("Expected an error because the pod does not specify a memory limit") } }
// TestAdmitRejectsNegativeUsage verifies that usage for any measured resource cannot be negative. func TestAdmitRejectsNegativeUsage(t *testing.T) { resourceQuota := &api.ResourceQuota{ ObjectMeta: metav1.ObjectMeta{Name: "quota", Namespace: "test", ResourceVersion: "124"}, Status: api.ResourceQuotaStatus{ Hard: api.ResourceList{ api.ResourcePersistentVolumeClaims: resource.MustParse("3"), api.ResourceRequestsStorage: resource.MustParse("100Gi"), }, Used: api.ResourceList{ api.ResourcePersistentVolumeClaims: resource.MustParse("1"), api.ResourceRequestsStorage: resource.MustParse("10Gi"), }, }, } kubeClient := fake.NewSimpleClientset(resourceQuota) indexer := cache.NewIndexer(cache.MetaNamespaceKeyFunc, cache.Indexers{"namespace": cache.MetaNamespaceIndexFunc}) stopCh := make(chan struct{}) defer close(stopCh) quotaAccessor, _ := newQuotaAccessor(kubeClient) quotaAccessor.indexer = indexer go quotaAccessor.Run(stopCh) evaluator := NewQuotaEvaluator(quotaAccessor, install.NewRegistry(nil, nil), nil, 5, stopCh) handler := "aAdmission{ Handler: admission.NewHandler(admission.Create, admission.Update), evaluator: evaluator, } indexer.Add(resourceQuota) // verify quota rejects negative pvc storage requests newPvc := validPersistentVolumeClaim("not-allowed-pvc", getResourceRequirements(api.ResourceList{api.ResourceStorage: resource.MustParse("-1Gi")}, api.ResourceList{})) err := handler.Admit(admission.NewAttributesRecord(newPvc, nil, api.Kind("PersistentVolumeClaim").WithVersion("version"), newPvc.Namespace, newPvc.Name, api.Resource("persistentvolumeclaims").WithVersion("version"), "", admission.Create, nil)) if err == nil { t.Errorf("Expected an error because the pvc has negative storage usage") } // verify quota accepts non-negative pvc storage requests newPvc = validPersistentVolumeClaim("not-allowed-pvc", getResourceRequirements(api.ResourceList{api.ResourceStorage: resource.MustParse("1Gi")}, api.ResourceList{})) err = handler.Admit(admission.NewAttributesRecord(newPvc, nil, api.Kind("PersistentVolumeClaim").WithVersion("version"), newPvc.Namespace, newPvc.Name, api.Resource("persistentvolumeclaims").WithVersion("version"), "", admission.Create, nil)) if err != nil { t.Errorf("Unexpected error: %v", err) } }
// TestAdmitBestEffortQuotaLimitIgnoresBurstable validates that a besteffort quota does not match a resource // guaranteed pod. func TestAdmitBestEffortQuotaLimitIgnoresBurstable(t *testing.T) { resourceQuota := &api.ResourceQuota{ ObjectMeta: metav1.ObjectMeta{Name: "quota-besteffort", Namespace: "test", ResourceVersion: "124"}, Spec: api.ResourceQuotaSpec{ Scopes: []api.ResourceQuotaScope{api.ResourceQuotaScopeBestEffort}, }, Status: api.ResourceQuotaStatus{ Hard: api.ResourceList{ api.ResourcePods: resource.MustParse("5"), }, Used: api.ResourceList{ api.ResourcePods: resource.MustParse("3"), }, }, } kubeClient := fake.NewSimpleClientset(resourceQuota) indexer := cache.NewIndexer(cache.MetaNamespaceKeyFunc, cache.Indexers{"namespace": cache.MetaNamespaceIndexFunc}) stopCh := make(chan struct{}) defer close(stopCh) quotaAccessor, _ := newQuotaAccessor(kubeClient) quotaAccessor.indexer = indexer go quotaAccessor.Run(stopCh) evaluator := NewQuotaEvaluator(quotaAccessor, install.NewRegistry(nil, nil), nil, 5, stopCh) handler := "aAdmission{ Handler: admission.NewHandler(admission.Create, admission.Update), evaluator: evaluator, } indexer.Add(resourceQuota) newPod := validPod("allowed-pod", 1, getResourceRequirements(getResourceList("100m", "1Gi"), getResourceList("", ""))) err := handler.Admit(admission.NewAttributesRecord(newPod, nil, api.Kind("Pod").WithVersion("version"), newPod.Namespace, newPod.Name, api.Resource("pods").WithVersion("version"), "", admission.Create, nil)) if err != nil { t.Errorf("Unexpected error: %v", err) } decimatedActions := removeListWatch(kubeClient.Actions()) if len(decimatedActions) != 0 { t.Errorf("Expected no client actions because the incoming pod did not match best effort quota: %v", kubeClient.Actions()) } }
// TestAdmissionIgnoresDelete verifies that the admission controller ignores delete operations func TestAdmissionIgnoresDelete(t *testing.T) { kubeClient := fake.NewSimpleClientset() indexer := cache.NewIndexer(cache.MetaNamespaceKeyFunc, cache.Indexers{"namespace": cache.MetaNamespaceIndexFunc}) stopCh := make(chan struct{}) defer close(stopCh) quotaAccessor, _ := newQuotaAccessor(kubeClient) quotaAccessor.indexer = indexer go quotaAccessor.Run(stopCh) evaluator := NewQuotaEvaluator(quotaAccessor, install.NewRegistry(nil, nil), nil, 5, stopCh) handler := "aAdmission{ Handler: admission.NewHandler(admission.Create, admission.Update), evaluator: evaluator, } namespace := "default" err := handler.Admit(admission.NewAttributesRecord(nil, nil, api.Kind("Pod").WithVersion("version"), namespace, "name", api.Resource("pods").WithVersion("version"), "", admission.Delete, nil)) if err != nil { t.Errorf("ResourceQuota should admit all deletes: %v", err) } }
// TestAdmitWhenUnrelatedResourceExceedsQuota verifies that if resource X exceeds quota, it does not prohibit resource Y from admission. func TestAdmitWhenUnrelatedResourceExceedsQuota(t *testing.T) { resourceQuota := &api.ResourceQuota{ ObjectMeta: metav1.ObjectMeta{Name: "quota", Namespace: "test", ResourceVersion: "124"}, Status: api.ResourceQuotaStatus{ Hard: api.ResourceList{ api.ResourceServices: resource.MustParse("3"), api.ResourcePods: resource.MustParse("4"), }, Used: api.ResourceList{ api.ResourceServices: resource.MustParse("4"), api.ResourcePods: resource.MustParse("1"), }, }, } kubeClient := fake.NewSimpleClientset(resourceQuota) indexer := cache.NewIndexer(cache.MetaNamespaceKeyFunc, cache.Indexers{"namespace": cache.MetaNamespaceIndexFunc}) stopCh := make(chan struct{}) defer close(stopCh) quotaAccessor, _ := newQuotaAccessor(kubeClient) quotaAccessor.indexer = indexer go quotaAccessor.Run(stopCh) evaluator := NewQuotaEvaluator(quotaAccessor, install.NewRegistry(nil, nil), nil, 5, stopCh) handler := "aAdmission{ Handler: admission.NewHandler(admission.Create, admission.Update), evaluator: evaluator, } indexer.Add(resourceQuota) // create a pod that should pass existing quota newPod := validPod("allowed-pod", 1, getResourceRequirements(getResourceList("", ""), getResourceList("", ""))) err := handler.Admit(admission.NewAttributesRecord(newPod, nil, api.Kind("Pod").WithVersion("version"), newPod.Namespace, newPod.Name, api.Resource("pods").WithVersion("version"), "", admission.Create, nil)) if err != nil { t.Errorf("Unexpected error: %v", err) } }
func TestGCAdmission(t *testing.T) { tests := []struct { name string username string resource schema.GroupVersionResource oldObj runtime.Object newObj runtime.Object expectedAllowed bool }{ { name: "super-user, create, no objectref change", username: "******", resource: api.SchemeGroupVersion.WithResource("pods"), newObj: &api.Pod{}, expectedAllowed: true, }, { name: "super-user, create, objectref change", username: "******", resource: api.SchemeGroupVersion.WithResource("pods"), newObj: &api.Pod{ObjectMeta: metav1.ObjectMeta{OwnerReferences: []metav1.OwnerReference{{Name: "first"}}}}, expectedAllowed: true, }, { name: "non-deleter, create, no objectref change", username: "******", resource: api.SchemeGroupVersion.WithResource("pods"), newObj: &api.Pod{}, expectedAllowed: true, }, { name: "non-deleter, create, objectref change", username: "******", resource: api.SchemeGroupVersion.WithResource("pods"), newObj: &api.Pod{ObjectMeta: metav1.ObjectMeta{OwnerReferences: []metav1.OwnerReference{{Name: "first"}}}}, expectedAllowed: false, }, { name: "non-pod-deleter, create, no objectref change", username: "******", resource: api.SchemeGroupVersion.WithResource("pods"), newObj: &api.Pod{}, expectedAllowed: true, }, { name: "non-pod-deleter, create, objectref change", username: "******", resource: api.SchemeGroupVersion.WithResource("pods"), newObj: &api.Pod{ObjectMeta: metav1.ObjectMeta{OwnerReferences: []metav1.OwnerReference{{Name: "first"}}}}, expectedAllowed: false, }, { name: "non-pod-deleter, create, objectref change, but not a pod", username: "******", resource: api.SchemeGroupVersion.WithResource("not-pods"), newObj: &api.Pod{ObjectMeta: metav1.ObjectMeta{OwnerReferences: []metav1.OwnerReference{{Name: "first"}}}}, expectedAllowed: true, }, { name: "super-user, update, no objectref change", username: "******", resource: api.SchemeGroupVersion.WithResource("pods"), oldObj: &api.Pod{}, newObj: &api.Pod{}, expectedAllowed: true, }, { name: "super-user, update, no objectref change two", username: "******", resource: api.SchemeGroupVersion.WithResource("pods"), oldObj: &api.Pod{ObjectMeta: metav1.ObjectMeta{OwnerReferences: []metav1.OwnerReference{{Name: "first"}}}}, newObj: &api.Pod{ObjectMeta: metav1.ObjectMeta{OwnerReferences: []metav1.OwnerReference{{Name: "first"}}}}, expectedAllowed: true, }, { name: "super-user, update, objectref change", username: "******", resource: api.SchemeGroupVersion.WithResource("pods"), oldObj: &api.Pod{}, newObj: &api.Pod{ObjectMeta: metav1.ObjectMeta{OwnerReferences: []metav1.OwnerReference{{Name: "first"}}}}, expectedAllowed: true, }, { name: "non-deleter, update, no objectref change", username: "******", resource: api.SchemeGroupVersion.WithResource("pods"), oldObj: &api.Pod{}, newObj: &api.Pod{}, expectedAllowed: true, }, { name: "non-deleter, update, no objectref change two", username: "******", resource: api.SchemeGroupVersion.WithResource("pods"), oldObj: &api.Pod{ObjectMeta: metav1.ObjectMeta{OwnerReferences: []metav1.OwnerReference{{Name: "first"}}}}, newObj: &api.Pod{ObjectMeta: metav1.ObjectMeta{OwnerReferences: []metav1.OwnerReference{{Name: "first"}}}}, expectedAllowed: true, }, { name: "non-deleter, update, objectref change", username: "******", resource: api.SchemeGroupVersion.WithResource("pods"), oldObj: &api.Pod{}, newObj: &api.Pod{ObjectMeta: metav1.ObjectMeta{OwnerReferences: []metav1.OwnerReference{{Name: "first"}}}}, expectedAllowed: false, }, { name: "non-deleter, update, objectref change two", username: "******", resource: api.SchemeGroupVersion.WithResource("pods"), oldObj: &api.Pod{ObjectMeta: metav1.ObjectMeta{OwnerReferences: []metav1.OwnerReference{{Name: "first"}}}}, newObj: &api.Pod{ObjectMeta: metav1.ObjectMeta{OwnerReferences: []metav1.OwnerReference{{Name: "first"}, {Name: "second"}}}}, expectedAllowed: false, }, { name: "non-pod-deleter, update, no objectref change", username: "******", resource: api.SchemeGroupVersion.WithResource("pods"), oldObj: &api.Pod{}, newObj: &api.Pod{}, expectedAllowed: true, }, { name: "non-pod-deleter, update, objectref change", username: "******", resource: api.SchemeGroupVersion.WithResource("pods"), oldObj: &api.Pod{}, newObj: &api.Pod{ObjectMeta: metav1.ObjectMeta{OwnerReferences: []metav1.OwnerReference{{Name: "first"}}}}, expectedAllowed: false, }, { name: "non-pod-deleter, update, objectref change, but not a pod", username: "******", resource: api.SchemeGroupVersion.WithResource("not-pods"), oldObj: &api.Pod{}, newObj: &api.Pod{ObjectMeta: metav1.ObjectMeta{OwnerReferences: []metav1.OwnerReference{{Name: "first"}}}}, expectedAllowed: true, }, } gcAdmit := &gcPermissionsEnforcement{ Handler: admission.NewHandler(admission.Create, admission.Update), authorizer: fakeAuthorizer{}, } for _, tc := range tests { operation := admission.Create if tc.oldObj != nil { operation = admission.Update } user := &user.DefaultInfo{Name: tc.username} attributes := admission.NewAttributesRecord(tc.newObj, tc.oldObj, schema.GroupVersionKind{}, api.NamespaceDefault, "foo", tc.resource, "", operation, user) err := gcAdmit.Admit(attributes) switch { case err != nil && !tc.expectedAllowed: case err != nil && tc.expectedAllowed: t.Errorf("%v: unexpected err: %v", tc.name, err) case err == nil && !tc.expectedAllowed: t.Errorf("%v: missing err", tc.name) case err == nil && tc.expectedAllowed: } } }
// newPlugin creates a new admission plugin. func newPlugin() *claimDefaulterPlugin { return &claimDefaulterPlugin{ Handler: admission.NewHandler(admission.Create), } }
// NewSecurityContextDeny creates a new instance of the SecurityContextDeny admission controller func NewSecurityContextDeny() admission.Interface { return &plugin{ Handler: admission.NewHandler(admission.Create, admission.Update), } }
// TestAdmitBelowBestEffortQuotaLimit creates a best effort and non-best effort quota. // It verifies that best effort pods are properly scoped to the best effort quota document. func TestAdmitBelowBestEffortQuotaLimit(t *testing.T) { resourceQuotaBestEffort := &api.ResourceQuota{ ObjectMeta: metav1.ObjectMeta{Name: "quota-besteffort", Namespace: "test", ResourceVersion: "124"}, Spec: api.ResourceQuotaSpec{ Scopes: []api.ResourceQuotaScope{api.ResourceQuotaScopeBestEffort}, }, Status: api.ResourceQuotaStatus{ Hard: api.ResourceList{ api.ResourcePods: resource.MustParse("5"), }, Used: api.ResourceList{ api.ResourcePods: resource.MustParse("3"), }, }, } resourceQuotaNotBestEffort := &api.ResourceQuota{ ObjectMeta: metav1.ObjectMeta{Name: "quota-not-besteffort", Namespace: "test", ResourceVersion: "124"}, Spec: api.ResourceQuotaSpec{ Scopes: []api.ResourceQuotaScope{api.ResourceQuotaScopeNotBestEffort}, }, Status: api.ResourceQuotaStatus{ Hard: api.ResourceList{ api.ResourcePods: resource.MustParse("5"), }, Used: api.ResourceList{ api.ResourcePods: resource.MustParse("3"), }, }, } kubeClient := fake.NewSimpleClientset(resourceQuotaBestEffort, resourceQuotaNotBestEffort) indexer := cache.NewIndexer(cache.MetaNamespaceKeyFunc, cache.Indexers{"namespace": cache.MetaNamespaceIndexFunc}) stopCh := make(chan struct{}) defer close(stopCh) quotaAccessor, _ := newQuotaAccessor(kubeClient) quotaAccessor.indexer = indexer go quotaAccessor.Run(stopCh) evaluator := NewQuotaEvaluator(quotaAccessor, install.NewRegistry(nil, nil), nil, 5, stopCh) handler := "aAdmission{ Handler: admission.NewHandler(admission.Create, admission.Update), evaluator: evaluator, } indexer.Add(resourceQuotaBestEffort) indexer.Add(resourceQuotaNotBestEffort) // create a pod that is best effort because it does not make a request for anything newPod := validPod("allowed-pod", 1, getResourceRequirements(getResourceList("", ""), getResourceList("", ""))) err := handler.Admit(admission.NewAttributesRecord(newPod, nil, api.Kind("Pod").WithVersion("version"), newPod.Namespace, newPod.Name, api.Resource("pods").WithVersion("version"), "", admission.Create, nil)) if err != nil { t.Errorf("Unexpected error: %v", err) } expectedActionSet := sets.NewString( strings.Join([]string{"update", "resourcequotas", "status"}, "-"), ) actionSet := sets.NewString() for _, action := range kubeClient.Actions() { actionSet.Insert(strings.Join([]string{action.GetVerb(), action.GetResource().Resource, action.GetSubresource()}, "-")) } if !actionSet.HasAll(expectedActionSet.List()...) { t.Errorf("Expected actions:\n%v\n but got:\n%v\nDifference:\n%v", expectedActionSet, actionSet, expectedActionSet.Difference(actionSet)) } decimatedActions := removeListWatch(kubeClient.Actions()) lastActionIndex := len(decimatedActions) - 1 usage := decimatedActions[lastActionIndex].(testcore.UpdateAction).GetObject().(*api.ResourceQuota) if usage.Name != resourceQuotaBestEffort.Name { t.Errorf("Incremented the wrong quota, expected %v, actual %v", resourceQuotaBestEffort.Name, usage.Name) } expectedUsage := api.ResourceQuota{ Status: api.ResourceQuotaStatus{ Hard: api.ResourceList{ api.ResourcePods: resource.MustParse("5"), }, Used: api.ResourceList{ api.ResourcePods: resource.MustParse("4"), }, }, } for k, v := range expectedUsage.Status.Used { actual := usage.Status.Used[k] actualValue := actual.String() expectedValue := v.String() if expectedValue != actualValue { t.Errorf("Usage Used: Key: %v, Expected: %v, Actual: %v", k, expectedValue, actualValue) } } }
// NewInterPodAntiAffinity creates a new instance of the LimitPodHardAntiAffinityTopology admission controller func NewInterPodAntiAffinity() admission.Interface { return &plugin{ Handler: admission.NewHandler(admission.Create, admission.Update), } }
// TestAdmitHandlesCreatingUpdates verifies that admit handles updates which behave as creates func TestAdmitHandlesCreatingUpdates(t *testing.T) { // in this scenario, there is an existing service resourceQuota := &api.ResourceQuota{ ObjectMeta: metav1.ObjectMeta{Name: "quota", Namespace: "test", ResourceVersion: "124"}, Status: api.ResourceQuotaStatus{ Hard: api.ResourceList{ api.ResourceServices: resource.MustParse("10"), api.ResourceServicesLoadBalancers: resource.MustParse("10"), api.ResourceServicesNodePorts: resource.MustParse("10"), }, Used: api.ResourceList{ api.ResourceServices: resource.MustParse("1"), api.ResourceServicesLoadBalancers: resource.MustParse("1"), api.ResourceServicesNodePorts: resource.MustParse("0"), }, }, } // start up quota system kubeClient := fake.NewSimpleClientset(resourceQuota) indexer := cache.NewIndexer(cache.MetaNamespaceKeyFunc, cache.Indexers{"namespace": cache.MetaNamespaceIndexFunc}) stopCh := make(chan struct{}) defer close(stopCh) quotaAccessor, _ := newQuotaAccessor(kubeClient) quotaAccessor.indexer = indexer go quotaAccessor.Run(stopCh) evaluator := NewQuotaEvaluator(quotaAccessor, install.NewRegistry(nil, nil), nil, 5, stopCh) handler := "aAdmission{ Handler: admission.NewHandler(admission.Create, admission.Update), evaluator: evaluator, } indexer.Add(resourceQuota) // old service didn't exist, so this update is actually a create oldService := &api.Service{ ObjectMeta: metav1.ObjectMeta{Name: "service", Namespace: "test", ResourceVersion: ""}, Spec: api.ServiceSpec{Type: api.ServiceTypeLoadBalancer}, } newService := &api.Service{ ObjectMeta: metav1.ObjectMeta{Name: "service", Namespace: "test"}, Spec: api.ServiceSpec{ Type: api.ServiceTypeNodePort, Ports: []api.ServicePort{{Port: 1234}}, }, } err := handler.Admit(admission.NewAttributesRecord(newService, oldService, api.Kind("Service").WithVersion("version"), newService.Namespace, newService.Name, api.Resource("services").WithVersion("version"), "", admission.Update, nil)) if err != nil { t.Errorf("Unexpected error: %v", err) } if len(kubeClient.Actions()) == 0 { t.Errorf("Expected a client action") } // the only action should have been to update the quota (since we should not have fetched the previous item) expectedActionSet := sets.NewString( strings.Join([]string{"update", "resourcequotas", "status"}, "-"), ) actionSet := sets.NewString() for _, action := range kubeClient.Actions() { actionSet.Insert(strings.Join([]string{action.GetVerb(), action.GetResource().Resource, action.GetSubresource()}, "-")) } if !actionSet.HasAll(expectedActionSet.List()...) { t.Errorf("Expected actions:\n%v\n but got:\n%v\nDifference:\n%v", expectedActionSet, actionSet, expectedActionSet.Difference(actionSet)) } // verify that the "old" object was ignored for calculating the new usage decimatedActions := removeListWatch(kubeClient.Actions()) lastActionIndex := len(decimatedActions) - 1 usage := decimatedActions[lastActionIndex].(testcore.UpdateAction).GetObject().(*api.ResourceQuota) expectedUsage := api.ResourceQuota{ Status: api.ResourceQuotaStatus{ Hard: api.ResourceList{ api.ResourceServices: resource.MustParse("10"), api.ResourceServicesLoadBalancers: resource.MustParse("10"), api.ResourceServicesNodePorts: resource.MustParse("10"), }, Used: api.ResourceList{ api.ResourceServices: resource.MustParse("2"), api.ResourceServicesLoadBalancers: resource.MustParse("1"), api.ResourceServicesNodePorts: resource.MustParse("1"), }, }, } for k, v := range expectedUsage.Status.Used { actual := usage.Status.Used[k] actualValue := actual.String() expectedValue := v.String() if expectedValue != actualValue { t.Errorf("Usage Used: Key: %v, Expected: %v, Actual: %v", k, expectedValue, actualValue) } } }
// NewProvision creates a new namespace provision admission control handler func NewProvision() admission.Interface { return &provision{ Handler: admission.NewHandler(admission.Create), } }
// NewAlwaysPullImages creates a new always pull images admission control handler func NewAlwaysPullImages() admission.Interface { return &alwaysPullImages{ Handler: admission.NewHandler(admission.Create, admission.Update), } }
// NewExists creates a new namespace exists admission control handler func NewExists() admission.Interface { return &exists{ Handler: admission.NewHandler(admission.Create, admission.Update, admission.Delete), } }
func NewPodNodeSelector(clusterNodeSelectors map[string]string) *podNodeSelector { return &podNodeSelector{ Handler: admission.NewHandler(admission.Create), clusterNodeSelectors: clusterNodeSelectors, } }
// NewPersistentVolumeLabel returns an admission.Interface implementation which adds labels to PersistentVolume CREATE requests, // based on the labels provided by the underlying cloud provider. // // As a side effect, the cloud provider may block invalid or non-existent volumes. func NewPersistentVolumeLabel() *persistentVolumeLabel { return &persistentVolumeLabel{ Handler: admission.NewHandler(admission.Create), } }