k8s.io/apiserver@v0.31.1/pkg/admission/plugin/namespace/lifecycle/admission_test.go (about)

     1  /*
     2  Copyright 2015 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package lifecycle
    18  
    19  import (
    20  	"context"
    21  	"fmt"
    22  	"reflect"
    23  	"testing"
    24  	"time"
    25  
    26  	"github.com/google/go-cmp/cmp"
    27  	v1 "k8s.io/api/core/v1"
    28  	"k8s.io/apimachinery/pkg/api/errors"
    29  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    30  	"k8s.io/apimachinery/pkg/runtime"
    31  	"k8s.io/apimachinery/pkg/runtime/schema"
    32  	"k8s.io/apimachinery/pkg/util/sets"
    33  	"k8s.io/apimachinery/pkg/util/wait"
    34  	"k8s.io/apiserver/pkg/admission"
    35  	kubeadmission "k8s.io/apiserver/pkg/admission/initializer"
    36  	informers "k8s.io/client-go/informers"
    37  	clientset "k8s.io/client-go/kubernetes"
    38  	"k8s.io/client-go/kubernetes/fake"
    39  	core "k8s.io/client-go/testing"
    40  	"k8s.io/utils/clock"
    41  	testingclock "k8s.io/utils/clock/testing"
    42  )
    43  
    44  // newHandlerForTest returns a configured handler for testing.
    45  func newHandlerForTest(c clientset.Interface) (*Lifecycle, informers.SharedInformerFactory, error) {
    46  	return newHandlerForTestWithClock(c, clock.RealClock{})
    47  }
    48  
    49  // newHandlerForTestWithClock returns a configured handler for testing.
    50  func newHandlerForTestWithClock(c clientset.Interface, cacheClock clock.Clock) (*Lifecycle, informers.SharedInformerFactory, error) {
    51  	f := informers.NewSharedInformerFactory(c, 5*time.Minute)
    52  	handler, err := newLifecycleWithClock(sets.NewString(metav1.NamespaceDefault, metav1.NamespaceSystem), cacheClock)
    53  	if err != nil {
    54  		return nil, f, err
    55  	}
    56  	pluginInitializer := kubeadmission.New(c, nil, f, nil, nil, nil, nil)
    57  	pluginInitializer.Initialize(handler)
    58  	err = admission.ValidateInitialization(handler)
    59  	return handler, f, err
    60  }
    61  
    62  // newMockClientForTest creates a mock client that returns a client configured for the specified list of namespaces with the specified phase.
    63  func newMockClientForTest(namespaces map[string]v1.NamespacePhase) *fake.Clientset {
    64  	mockClient := &fake.Clientset{}
    65  	mockClient.AddReactor("list", "namespaces", func(action core.Action) (bool, runtime.Object, error) {
    66  		namespaceList := &v1.NamespaceList{
    67  			ListMeta: metav1.ListMeta{
    68  				ResourceVersion: fmt.Sprintf("%d", len(namespaces)),
    69  			},
    70  		}
    71  		index := 0
    72  		for name, phase := range namespaces {
    73  			namespaceList.Items = append(namespaceList.Items, v1.Namespace{
    74  				ObjectMeta: metav1.ObjectMeta{
    75  					Name:            name,
    76  					ResourceVersion: fmt.Sprintf("%d", index),
    77  				},
    78  				Status: v1.NamespaceStatus{
    79  					Phase: phase,
    80  				},
    81  			})
    82  			index++
    83  		}
    84  		return true, namespaceList, nil
    85  	})
    86  	return mockClient
    87  }
    88  
    89  // newPod returns a new pod for the specified namespace
    90  func newPod(namespace string) v1.Pod {
    91  	return v1.Pod{
    92  		ObjectMeta: metav1.ObjectMeta{Name: "123", Namespace: namespace},
    93  		Spec: v1.PodSpec{
    94  			Volumes:    []v1.Volume{{Name: "vol"}},
    95  			Containers: []v1.Container{{Name: "ctr", Image: "image"}},
    96  		},
    97  	}
    98  }
    99  
   100  func TestAccessReviewCheckOnMissingNamespace(t *testing.T) {
   101  	namespace := "test"
   102  	mockClient := newMockClientForTest(map[string]v1.NamespacePhase{})
   103  	mockClient.AddReactor("get", "namespaces", func(action core.Action) (bool, runtime.Object, error) {
   104  		return true, nil, fmt.Errorf("nope, out of luck")
   105  	})
   106  	handler, informerFactory, err := newHandlerForTest(mockClient)
   107  	if err != nil {
   108  		t.Errorf("unexpected error initializing handler: %v", err)
   109  	}
   110  
   111  	stopCh := make(chan struct{})
   112  	defer close(stopCh)
   113  
   114  	informerFactory.Start(stopCh)
   115  
   116  	err = handler.Admit(context.TODO(), 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, &metav1.CreateOptions{}, false, nil), nil)
   117  	if err != nil {
   118  		t.Error(err)
   119  	}
   120  }
   121  
   122  // TestAdmissionNamespaceDoesNotExist verifies pod is not admitted if namespace does not exist.
   123  func TestAdmissionNamespaceDoesNotExist(t *testing.T) {
   124  	namespace := "test"
   125  	mockClient := newMockClientForTest(map[string]v1.NamespacePhase{})
   126  	mockClient.AddReactor("get", "namespaces", func(action core.Action) (bool, runtime.Object, error) {
   127  		return true, nil, fmt.Errorf("nope, out of luck")
   128  	})
   129  	handler, informerFactory, err := newHandlerForTest(mockClient)
   130  	if err != nil {
   131  		t.Errorf("unexpected error initializing handler: %v", err)
   132  	}
   133  	informerFactory.Start(wait.NeverStop)
   134  
   135  	pod := newPod(namespace)
   136  	err = handler.Admit(context.TODO(), admission.NewAttributesRecord(&pod, nil, v1.SchemeGroupVersion.WithKind("Pod").GroupKind().WithVersion("version"), pod.Namespace, pod.Name, v1.Resource("pods").WithVersion("version"), "", admission.Create, &metav1.CreateOptions{}, false, nil), nil)
   137  	if err == nil {
   138  		actions := ""
   139  		for _, action := range mockClient.Actions() {
   140  			actions = actions + action.GetVerb() + ":" + action.GetResource().Resource + ":" + action.GetSubresource() + ", "
   141  		}
   142  		t.Errorf("expected error returned from admission handler: %v", actions)
   143  	}
   144  
   145  	// verify create operations in the namespace cause an error
   146  	err = handler.Admit(context.TODO(), admission.NewAttributesRecord(&pod, nil, v1.SchemeGroupVersion.WithKind("Pod").GroupKind().WithVersion("version"), pod.Namespace, pod.Name, v1.Resource("pods").WithVersion("version"), "", admission.Create, &metav1.CreateOptions{}, false, nil), nil)
   147  	if err == nil {
   148  		t.Errorf("Expected error rejecting creates in a namespace when it is missing")
   149  	}
   150  
   151  	// verify update operations in the namespace cause an error
   152  	err = handler.Admit(context.TODO(), admission.NewAttributesRecord(&pod, nil, v1.SchemeGroupVersion.WithKind("Pod").GroupKind().WithVersion("version"), pod.Namespace, pod.Name, v1.Resource("pods").WithVersion("version"), "", admission.Update, &metav1.UpdateOptions{}, false, nil), nil)
   153  	if err == nil {
   154  		t.Errorf("Expected error rejecting updates in a namespace when it is missing")
   155  	}
   156  
   157  	// verify delete operations in the namespace can proceed
   158  	err = handler.Admit(context.TODO(), admission.NewAttributesRecord(nil, nil, v1.SchemeGroupVersion.WithKind("Pod").GroupKind().WithVersion("version"), pod.Namespace, pod.Name, v1.Resource("pods").WithVersion("version"), "", admission.Delete, &metav1.DeleteOptions{}, false, nil), nil)
   159  	if err != nil {
   160  		t.Errorf("Unexpected error returned from admission handler: %v", err)
   161  	}
   162  }
   163  
   164  // TestAdmissionNamespaceActive verifies a resource is admitted when the namespace is active.
   165  func TestAdmissionNamespaceActive(t *testing.T) {
   166  	namespace := "test"
   167  	mockClient := newMockClientForTest(map[string]v1.NamespacePhase{
   168  		namespace: v1.NamespaceActive,
   169  	})
   170  
   171  	handler, informerFactory, err := newHandlerForTest(mockClient)
   172  	if err != nil {
   173  		t.Errorf("unexpected error initializing handler: %v", err)
   174  	}
   175  	informerFactory.Start(wait.NeverStop)
   176  
   177  	pod := newPod(namespace)
   178  	err = handler.Admit(context.TODO(), admission.NewAttributesRecord(&pod, nil, v1.SchemeGroupVersion.WithKind("Pod").GroupKind().WithVersion("version"), pod.Namespace, pod.Name, v1.Resource("pods").WithVersion("version"), "", admission.Create, &metav1.CreateOptions{}, false, nil), nil)
   179  	if err != nil {
   180  		t.Errorf("unexpected error returned from admission handler")
   181  	}
   182  }
   183  
   184  // TestAdmissionNamespaceTerminating verifies a resource is not created when the namespace is active.
   185  func TestAdmissionNamespaceTerminating(t *testing.T) {
   186  	namespace := "test"
   187  	mockClient := newMockClientForTest(map[string]v1.NamespacePhase{
   188  		namespace: v1.NamespaceTerminating,
   189  	})
   190  
   191  	handler, informerFactory, err := newHandlerForTest(mockClient)
   192  	if err != nil {
   193  		t.Errorf("unexpected error initializing handler: %v", err)
   194  	}
   195  	informerFactory.Start(wait.NeverStop)
   196  
   197  	pod := newPod(namespace)
   198  	// verify create operations in the namespace cause an error
   199  	err = handler.Admit(context.TODO(), admission.NewAttributesRecord(&pod, nil, v1.SchemeGroupVersion.WithKind("Pod").GroupKind().WithVersion("version"), pod.Namespace, pod.Name, v1.Resource("pods").WithVersion("version"), "", admission.Create, &metav1.CreateOptions{}, false, nil), nil)
   200  	if err == nil {
   201  		t.Errorf("Expected error rejecting creates in a namespace when it is terminating")
   202  	}
   203  	expectedCause := metav1.StatusCause{
   204  		Type:    v1.NamespaceTerminatingCause,
   205  		Message: fmt.Sprintf("namespace %s is being terminated", namespace),
   206  		Field:   "metadata.namespace",
   207  	}
   208  	if cause, ok := errors.StatusCause(err, v1.NamespaceTerminatingCause); !ok || !reflect.DeepEqual(expectedCause, cause) {
   209  		t.Errorf("Expected status cause indicating the namespace is terminating: %t %s", ok, cmp.Diff(expectedCause, cause))
   210  	}
   211  
   212  	// verify update operations in the namespace can proceed
   213  	err = handler.Admit(context.TODO(), admission.NewAttributesRecord(&pod, nil, v1.SchemeGroupVersion.WithKind("Pod").GroupKind().WithVersion("version"), pod.Namespace, pod.Name, v1.Resource("pods").WithVersion("version"), "", admission.Update, &metav1.UpdateOptions{}, false, nil), nil)
   214  	if err != nil {
   215  		t.Errorf("Unexpected error returned from admission handler: %v", err)
   216  	}
   217  
   218  	// verify delete operations in the namespace can proceed
   219  	err = handler.Admit(context.TODO(), admission.NewAttributesRecord(nil, nil, v1.SchemeGroupVersion.WithKind("Pod").GroupKind().WithVersion("version"), pod.Namespace, pod.Name, v1.Resource("pods").WithVersion("version"), "", admission.Delete, &metav1.DeleteOptions{}, false, nil), nil)
   220  	if err != nil {
   221  		t.Errorf("Unexpected error returned from admission handler: %v", err)
   222  	}
   223  
   224  	// verify delete of namespace default can never proceed
   225  	err = handler.Admit(context.TODO(), admission.NewAttributesRecord(nil, nil, v1.SchemeGroupVersion.WithKind("Namespace").GroupKind().WithVersion("version"), "", metav1.NamespaceDefault, v1.Resource("namespaces").WithVersion("version"), "", admission.Delete, &metav1.DeleteOptions{}, false, nil), nil)
   226  	if err == nil {
   227  		t.Errorf("Expected an error that this namespace can never be deleted")
   228  	}
   229  
   230  	// verify delete of namespace other than default can proceed
   231  	err = handler.Admit(context.TODO(), admission.NewAttributesRecord(nil, nil, v1.SchemeGroupVersion.WithKind("Namespace").GroupKind().WithVersion("version"), "", "other", v1.Resource("namespaces").WithVersion("version"), "", admission.Delete, &metav1.DeleteOptions{}, false, nil), nil)
   232  	if err != nil {
   233  		t.Errorf("Did not expect an error %v", err)
   234  	}
   235  }
   236  
   237  // TestAdmissionNamespaceForceLiveLookup verifies live lookups are done after deleting a namespace
   238  func TestAdmissionNamespaceForceLiveLookup(t *testing.T) {
   239  	namespace := "test"
   240  	getCalls := int64(0)
   241  	phases := map[string]v1.NamespacePhase{namespace: v1.NamespaceActive}
   242  	mockClient := newMockClientForTest(phases)
   243  	mockClient.AddReactor("get", "namespaces", func(action core.Action) (bool, runtime.Object, error) {
   244  		getCalls++
   245  		return true, &v1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: namespace}, Status: v1.NamespaceStatus{Phase: phases[namespace]}}, nil
   246  	})
   247  
   248  	fakeClock := testingclock.NewFakeClock(time.Now())
   249  
   250  	handler, informerFactory, err := newHandlerForTestWithClock(mockClient, fakeClock)
   251  	if err != nil {
   252  		t.Errorf("unexpected error initializing handler: %v", err)
   253  	}
   254  	informerFactory.Start(wait.NeverStop)
   255  
   256  	pod := newPod(namespace)
   257  	// verify create operations in the namespace is allowed
   258  	err = handler.Admit(context.TODO(), admission.NewAttributesRecord(&pod, nil, v1.SchemeGroupVersion.WithKind("Pod").GroupKind().WithVersion("version"), pod.Namespace, pod.Name, v1.Resource("pods").WithVersion("version"), "", admission.Create, &metav1.CreateOptions{}, false, nil), nil)
   259  	if err != nil {
   260  		t.Errorf("Unexpected error rejecting creates in an active namespace")
   261  	}
   262  	if getCalls != 0 {
   263  		t.Errorf("Expected no live lookups of the namespace, got %d", getCalls)
   264  	}
   265  	getCalls = 0
   266  
   267  	// verify delete of namespace can proceed
   268  	err = handler.Admit(context.TODO(), admission.NewAttributesRecord(nil, nil, v1.SchemeGroupVersion.WithKind("Namespace").GroupKind().WithVersion("version"), namespace, namespace, v1.Resource("namespaces").WithVersion("version"), "", admission.Delete, &metav1.DeleteOptions{}, false, nil), nil)
   269  	if err != nil {
   270  		t.Errorf("Expected namespace deletion to be allowed")
   271  	}
   272  	if getCalls != 0 {
   273  		t.Errorf("Expected no live lookups of the namespace, got %d", getCalls)
   274  	}
   275  	getCalls = 0
   276  
   277  	// simulate the phase changing
   278  	phases[namespace] = v1.NamespaceTerminating
   279  
   280  	// verify create operations in the namespace cause an error
   281  	err = handler.Admit(context.TODO(), admission.NewAttributesRecord(&pod, nil, v1.SchemeGroupVersion.WithKind("Pod").GroupKind().WithVersion("version"), pod.Namespace, pod.Name, v1.Resource("pods").WithVersion("version"), "", admission.Create, &metav1.CreateOptions{}, false, nil), nil)
   282  	if err == nil {
   283  		t.Errorf("Expected error rejecting creates in a namespace right after deleting it")
   284  	}
   285  	if getCalls != 1 {
   286  		t.Errorf("Expected a live lookup of the namespace at t=0, got %d", getCalls)
   287  	}
   288  	getCalls = 0
   289  
   290  	// Ensure the live lookup is still forced up to forceLiveLookupTTL
   291  	fakeClock.Step(forceLiveLookupTTL)
   292  
   293  	// verify create operations in the namespace cause an error
   294  	err = handler.Admit(context.TODO(), admission.NewAttributesRecord(&pod, nil, v1.SchemeGroupVersion.WithKind("Pod").GroupKind().WithVersion("version"), pod.Namespace, pod.Name, v1.Resource("pods").WithVersion("version"), "", admission.Create, &metav1.CreateOptions{}, false, nil), nil)
   295  	if err == nil {
   296  		t.Errorf("Expected error rejecting creates in a namespace right after deleting it")
   297  	}
   298  	if getCalls != 1 {
   299  		t.Errorf("Expected a live lookup of the namespace at t=forceLiveLookupTTL, got %d", getCalls)
   300  	}
   301  	getCalls = 0
   302  
   303  	// Ensure the live lookup expires
   304  	fakeClock.Step(time.Millisecond)
   305  
   306  	// verify create operations in the namespace don't force a live lookup after the timeout
   307  	handler.Admit(context.TODO(), admission.NewAttributesRecord(&pod, nil, v1.SchemeGroupVersion.WithKind("Pod").GroupKind().WithVersion("version"), pod.Namespace, pod.Name, v1.Resource("pods").WithVersion("version"), "", admission.Create, &metav1.CreateOptions{}, false, nil), nil)
   308  	if getCalls != 0 {
   309  		t.Errorf("Expected no live lookup of the namespace at t=forceLiveLookupTTL+1ms, got %d", getCalls)
   310  	}
   311  	getCalls = 0
   312  }