| /* |
| 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 lifecycle |
| |
| import ( |
| "fmt" |
| "testing" |
| "time" |
| |
| "k8s.io/api/core/v1" |
| metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" |
| "k8s.io/apimachinery/pkg/runtime" |
| "k8s.io/apimachinery/pkg/runtime/schema" |
| "k8s.io/apimachinery/pkg/util/clock" |
| "k8s.io/apimachinery/pkg/util/sets" |
| "k8s.io/apimachinery/pkg/util/wait" |
| "k8s.io/apiserver/pkg/admission" |
| kubeadmission "k8s.io/apiserver/pkg/admission/initializer" |
| informers "k8s.io/client-go/informers" |
| clientset "k8s.io/client-go/kubernetes" |
| "k8s.io/client-go/kubernetes/fake" |
| core "k8s.io/client-go/testing" |
| ) |
| |
| // newHandlerForTest returns a configured handler for testing. |
| func newHandlerForTest(c clientset.Interface) (*Lifecycle, informers.SharedInformerFactory, error) { |
| return newHandlerForTestWithClock(c, clock.RealClock{}) |
| } |
| |
| // newHandlerForTestWithClock returns a configured handler for testing. |
| func newHandlerForTestWithClock(c clientset.Interface, cacheClock clock.Clock) (*Lifecycle, informers.SharedInformerFactory, error) { |
| f := informers.NewSharedInformerFactory(c, 5*time.Minute) |
| handler, err := newLifecycleWithClock(sets.NewString(metav1.NamespaceDefault, metav1.NamespaceSystem), cacheClock) |
| if err != nil { |
| return nil, f, err |
| } |
| pluginInitializer := kubeadmission.New(c, f, nil, nil) |
| pluginInitializer.Initialize(handler) |
| err = admission.ValidateInitialization(handler) |
| return handler, f, err |
| } |
| |
| // newMockClientForTest creates a mock client that returns a client configured for the specified list of namespaces with the specified phase. |
| func newMockClientForTest(namespaces map[string]v1.NamespacePhase) *fake.Clientset { |
| mockClient := &fake.Clientset{} |
| mockClient.AddReactor("list", "namespaces", func(action core.Action) (bool, runtime.Object, error) { |
| namespaceList := &v1.NamespaceList{ |
| ListMeta: metav1.ListMeta{ |
| ResourceVersion: fmt.Sprintf("%d", len(namespaces)), |
| }, |
| } |
| index := 0 |
| for name, phase := range namespaces { |
| namespaceList.Items = append(namespaceList.Items, v1.Namespace{ |
| ObjectMeta: metav1.ObjectMeta{ |
| Name: name, |
| ResourceVersion: fmt.Sprintf("%d", index), |
| }, |
| Status: v1.NamespaceStatus{ |
| Phase: phase, |
| }, |
| }) |
| index++ |
| } |
| return true, namespaceList, nil |
| }) |
| return mockClient |
| } |
| |
| // newPod returns a new pod for the specified namespace |
| func newPod(namespace string) v1.Pod { |
| return v1.Pod{ |
| ObjectMeta: metav1.ObjectMeta{Name: "123", Namespace: namespace}, |
| Spec: v1.PodSpec{ |
| Volumes: []v1.Volume{{Name: "vol"}}, |
| Containers: []v1.Container{{Name: "ctr", Image: "image"}}, |
| }, |
| } |
| } |
| |
| func TestAccessReviewCheckOnMissingNamespace(t *testing.T) { |
| namespace := "test" |
| mockClient := newMockClientForTest(map[string]v1.NamespacePhase{}) |
| mockClient.AddReactor("get", "namespaces", func(action core.Action) (bool, runtime.Object, error) { |
| return true, nil, fmt.Errorf("nope, out of luck") |
| }) |
| handler, informerFactory, err := newHandlerForTest(mockClient) |
| if err != nil { |
| t.Errorf("unexpected error initializing handler: %v", err) |
| } |
| informerFactory.Start(wait.NeverStop) |
| |
| err = handler.Admit(admission.NewAttributesRecord(nil, nil, schema.GroupVersionKind{Group: "authorization.k8s.io", Version: "v1", Kind: "LocalSubjectAccesReview"}, namespace, "", schema.GroupVersionResource{Group: "authorization.k8s.io", Version: "v1", Resource: "localsubjectaccessreviews"}, "", admission.Create, false, nil)) |
| if err != nil { |
| t.Error(err) |
| } |
| } |
| |
| // TestAdmissionNamespaceDoesNotExist verifies pod is not admitted if namespace does not exist. |
| func TestAdmissionNamespaceDoesNotExist(t *testing.T) { |
| namespace := "test" |
| mockClient := newMockClientForTest(map[string]v1.NamespacePhase{}) |
| mockClient.AddReactor("get", "namespaces", func(action core.Action) (bool, runtime.Object, error) { |
| return true, nil, fmt.Errorf("nope, out of luck") |
| }) |
| handler, informerFactory, err := newHandlerForTest(mockClient) |
| if err != nil { |
| t.Errorf("unexpected error initializing handler: %v", err) |
| } |
| informerFactory.Start(wait.NeverStop) |
| |
| pod := newPod(namespace) |
| err = handler.Admit(admission.NewAttributesRecord(&pod, nil, v1.SchemeGroupVersion.WithKind("Pod").GroupKind().WithVersion("version"), pod.Namespace, pod.Name, v1.Resource("pods").WithVersion("version"), "", admission.Create, false, nil)) |
| if err == nil { |
| actions := "" |
| for _, action := range mockClient.Actions() { |
| actions = actions + action.GetVerb() + ":" + action.GetResource().Resource + ":" + action.GetSubresource() + ", " |
| } |
| t.Errorf("expected error returned from admission handler: %v", actions) |
| } |
| |
| // verify create operations in the namespace cause an error |
| err = handler.Admit(admission.NewAttributesRecord(&pod, nil, v1.SchemeGroupVersion.WithKind("Pod").GroupKind().WithVersion("version"), pod.Namespace, pod.Name, v1.Resource("pods").WithVersion("version"), "", admission.Create, false, nil)) |
| if err == nil { |
| t.Errorf("Expected error rejecting creates in a namespace when it is missing") |
| } |
| |
| // verify update operations in the namespace cause an error |
| err = handler.Admit(admission.NewAttributesRecord(&pod, nil, v1.SchemeGroupVersion.WithKind("Pod").GroupKind().WithVersion("version"), pod.Namespace, pod.Name, v1.Resource("pods").WithVersion("version"), "", admission.Update, false, nil)) |
| if err == nil { |
| t.Errorf("Expected error rejecting updates in a namespace when it is missing") |
| } |
| |
| // verify delete operations in the namespace can proceed |
| err = handler.Admit(admission.NewAttributesRecord(nil, nil, v1.SchemeGroupVersion.WithKind("Pod").GroupKind().WithVersion("version"), pod.Namespace, pod.Name, v1.Resource("pods").WithVersion("version"), "", admission.Delete, false, nil)) |
| if err != nil { |
| t.Errorf("Unexpected error returned from admission handler: %v", err) |
| } |
| } |
| |
| // TestAdmissionNamespaceActive verifies a resource is admitted when the namespace is active. |
| func TestAdmissionNamespaceActive(t *testing.T) { |
| namespace := "test" |
| mockClient := newMockClientForTest(map[string]v1.NamespacePhase{ |
| namespace: v1.NamespaceActive, |
| }) |
| |
| handler, informerFactory, err := newHandlerForTest(mockClient) |
| if err != nil { |
| t.Errorf("unexpected error initializing handler: %v", err) |
| } |
| informerFactory.Start(wait.NeverStop) |
| |
| pod := newPod(namespace) |
| err = handler.Admit(admission.NewAttributesRecord(&pod, nil, v1.SchemeGroupVersion.WithKind("Pod").GroupKind().WithVersion("version"), pod.Namespace, pod.Name, v1.Resource("pods").WithVersion("version"), "", admission.Create, false, nil)) |
| if err != nil { |
| t.Errorf("unexpected error returned from admission handler") |
| } |
| } |
| |
| // TestAdmissionNamespaceTerminating verifies a resource is not created when the namespace is active. |
| func TestAdmissionNamespaceTerminating(t *testing.T) { |
| namespace := "test" |
| mockClient := newMockClientForTest(map[string]v1.NamespacePhase{ |
| namespace: v1.NamespaceTerminating, |
| }) |
| |
| handler, informerFactory, err := newHandlerForTest(mockClient) |
| if err != nil { |
| t.Errorf("unexpected error initializing handler: %v", err) |
| } |
| informerFactory.Start(wait.NeverStop) |
| |
| pod := newPod(namespace) |
| // verify create operations in the namespace cause an error |
| err = handler.Admit(admission.NewAttributesRecord(&pod, nil, v1.SchemeGroupVersion.WithKind("Pod").GroupKind().WithVersion("version"), pod.Namespace, pod.Name, v1.Resource("pods").WithVersion("version"), "", admission.Create, false, nil)) |
| if err == nil { |
| t.Errorf("Expected error rejecting creates in a namespace when it is terminating") |
| } |
| |
| // verify update operations in the namespace can proceed |
| err = handler.Admit(admission.NewAttributesRecord(&pod, nil, v1.SchemeGroupVersion.WithKind("Pod").GroupKind().WithVersion("version"), pod.Namespace, pod.Name, v1.Resource("pods").WithVersion("version"), "", admission.Update, false, nil)) |
| if err != nil { |
| t.Errorf("Unexpected error returned from admission handler: %v", err) |
| } |
| |
| // verify delete operations in the namespace can proceed |
| err = handler.Admit(admission.NewAttributesRecord(nil, nil, v1.SchemeGroupVersion.WithKind("Pod").GroupKind().WithVersion("version"), pod.Namespace, pod.Name, v1.Resource("pods").WithVersion("version"), "", admission.Delete, false, nil)) |
| if err != nil { |
| t.Errorf("Unexpected error returned from admission handler: %v", err) |
| } |
| |
| // verify delete of namespace default can never proceed |
| err = handler.Admit(admission.NewAttributesRecord(nil, nil, v1.SchemeGroupVersion.WithKind("Namespace").GroupKind().WithVersion("version"), "", metav1.NamespaceDefault, v1.Resource("namespaces").WithVersion("version"), "", admission.Delete, false, nil)) |
| if err == nil { |
| t.Errorf("Expected an error that this namespace can never be deleted") |
| } |
| |
| // verify delete of namespace other than default can proceed |
| err = handler.Admit(admission.NewAttributesRecord(nil, nil, v1.SchemeGroupVersion.WithKind("Namespace").GroupKind().WithVersion("version"), "", "other", v1.Resource("namespaces").WithVersion("version"), "", admission.Delete, false, nil)) |
| if err != nil { |
| t.Errorf("Did not expect an error %v", err) |
| } |
| } |
| |
| // TestAdmissionNamespaceForceLiveLookup verifies live lookups are done after deleting a namespace |
| func TestAdmissionNamespaceForceLiveLookup(t *testing.T) { |
| namespace := "test" |
| getCalls := int64(0) |
| phases := map[string]v1.NamespacePhase{namespace: v1.NamespaceActive} |
| mockClient := newMockClientForTest(phases) |
| mockClient.AddReactor("get", "namespaces", func(action core.Action) (bool, runtime.Object, error) { |
| getCalls++ |
| return true, &v1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: namespace}, Status: v1.NamespaceStatus{Phase: phases[namespace]}}, nil |
| }) |
| |
| fakeClock := clock.NewFakeClock(time.Now()) |
| |
| handler, informerFactory, err := newHandlerForTestWithClock(mockClient, fakeClock) |
| if err != nil { |
| t.Errorf("unexpected error initializing handler: %v", err) |
| } |
| informerFactory.Start(wait.NeverStop) |
| |
| pod := newPod(namespace) |
| // verify create operations in the namespace is allowed |
| err = handler.Admit(admission.NewAttributesRecord(&pod, nil, v1.SchemeGroupVersion.WithKind("Pod").GroupKind().WithVersion("version"), pod.Namespace, pod.Name, v1.Resource("pods").WithVersion("version"), "", admission.Create, false, nil)) |
| if err != nil { |
| t.Errorf("Unexpected error rejecting creates in an active namespace") |
| } |
| if getCalls != 0 { |
| t.Errorf("Expected no live lookups of the namespace, got %d", getCalls) |
| } |
| getCalls = 0 |
| |
| // verify delete of namespace can proceed |
| err = handler.Admit(admission.NewAttributesRecord(nil, nil, v1.SchemeGroupVersion.WithKind("Namespace").GroupKind().WithVersion("version"), namespace, namespace, v1.Resource("namespaces").WithVersion("version"), "", admission.Delete, false, nil)) |
| if err != nil { |
| t.Errorf("Expected namespace deletion to be allowed") |
| } |
| if getCalls != 0 { |
| t.Errorf("Expected no live lookups of the namespace, got %d", getCalls) |
| } |
| getCalls = 0 |
| |
| // simulate the phase changing |
| phases[namespace] = v1.NamespaceTerminating |
| |
| // verify create operations in the namespace cause an error |
| err = handler.Admit(admission.NewAttributesRecord(&pod, nil, v1.SchemeGroupVersion.WithKind("Pod").GroupKind().WithVersion("version"), pod.Namespace, pod.Name, v1.Resource("pods").WithVersion("version"), "", admission.Create, false, nil)) |
| if err == nil { |
| t.Errorf("Expected error rejecting creates in a namespace right after deleting it") |
| } |
| if getCalls != 1 { |
| t.Errorf("Expected a live lookup of the namespace at t=0, got %d", getCalls) |
| } |
| getCalls = 0 |
| |
| // Ensure the live lookup is still forced up to forceLiveLookupTTL |
| fakeClock.Step(forceLiveLookupTTL) |
| |
| // verify create operations in the namespace cause an error |
| err = handler.Admit(admission.NewAttributesRecord(&pod, nil, v1.SchemeGroupVersion.WithKind("Pod").GroupKind().WithVersion("version"), pod.Namespace, pod.Name, v1.Resource("pods").WithVersion("version"), "", admission.Create, false, nil)) |
| if err == nil { |
| t.Errorf("Expected error rejecting creates in a namespace right after deleting it") |
| } |
| if getCalls != 1 { |
| t.Errorf("Expected a live lookup of the namespace at t=forceLiveLookupTTL, got %d", getCalls) |
| } |
| getCalls = 0 |
| |
| // Ensure the live lookup expires |
| fakeClock.Step(time.Millisecond) |
| |
| // verify create operations in the namespace don't force a live lookup after the timeout |
| handler.Admit(admission.NewAttributesRecord(&pod, nil, v1.SchemeGroupVersion.WithKind("Pod").GroupKind().WithVersion("version"), pod.Namespace, pod.Name, v1.Resource("pods").WithVersion("version"), "", admission.Create, false, nil)) |
| if getCalls != 0 { |
| t.Errorf("Expected no live lookup of the namespace at t=forceLiveLookupTTL+1ms, got %d", getCalls) |
| } |
| getCalls = 0 |
| } |