k8s.io/apiserver@v0.31.1/pkg/admission/plugin/namespace/lifecycle/admission.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  	"io"
    23  	"time"
    24  
    25  	"k8s.io/klog/v2"
    26  
    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/schema"
    31  	utilcache "k8s.io/apimachinery/pkg/util/cache"
    32  	"k8s.io/apimachinery/pkg/util/sets"
    33  	"k8s.io/apiserver/pkg/admission"
    34  	"k8s.io/apiserver/pkg/admission/initializer"
    35  	"k8s.io/client-go/informers"
    36  	"k8s.io/client-go/kubernetes"
    37  	corelisters "k8s.io/client-go/listers/core/v1"
    38  	"k8s.io/utils/clock"
    39  )
    40  
    41  const (
    42  	// PluginName indicates the name of admission plug-in
    43  	PluginName = "NamespaceLifecycle"
    44  	// how long a namespace stays in the force live lookup cache before expiration.
    45  	forceLiveLookupTTL = 30 * time.Second
    46  	// how long to wait for a missing namespace before re-checking the cache (and then doing a live lookup)
    47  	// this accomplishes two things:
    48  	// 1. It allows a watch-fed cache time to observe a namespace creation event
    49  	// 2. It allows time for a namespace creation to distribute to members of a storage cluster,
    50  	//    so the live lookup has a better chance of succeeding even if it isn't performed against the leader.
    51  	missingNamespaceWait = 50 * time.Millisecond
    52  )
    53  
    54  // Register registers a plugin
    55  func Register(plugins *admission.Plugins) {
    56  	plugins.Register(PluginName, func(config io.Reader) (admission.Interface, error) {
    57  		return NewLifecycle(sets.NewString(metav1.NamespaceDefault, metav1.NamespaceSystem, metav1.NamespacePublic))
    58  	})
    59  }
    60  
    61  // Lifecycle is an implementation of admission.Interface.
    62  // It enforces life-cycle constraints around a Namespace depending on its Phase
    63  type Lifecycle struct {
    64  	*admission.Handler
    65  	client             kubernetes.Interface
    66  	immortalNamespaces sets.String
    67  	namespaceLister    corelisters.NamespaceLister
    68  	// forceLiveLookupCache holds a list of entries for namespaces that we have a strong reason to believe are stale in our local cache.
    69  	// if a namespace is in this cache, then we will ignore our local state and always fetch latest from api server.
    70  	forceLiveLookupCache *utilcache.LRUExpireCache
    71  }
    72  
    73  var _ = initializer.WantsExternalKubeInformerFactory(&Lifecycle{})
    74  var _ = initializer.WantsExternalKubeClientSet(&Lifecycle{})
    75  
    76  // Admit makes an admission decision based on the request attributes
    77  func (l *Lifecycle) Admit(ctx context.Context, a admission.Attributes, o admission.ObjectInterfaces) error {
    78  	// prevent deletion of immortal namespaces
    79  	if a.GetOperation() == admission.Delete && a.GetKind().GroupKind() == v1.SchemeGroupVersion.WithKind("Namespace").GroupKind() && l.immortalNamespaces.Has(a.GetName()) {
    80  		return errors.NewForbidden(a.GetResource().GroupResource(), a.GetName(), fmt.Errorf("this namespace may not be deleted"))
    81  	}
    82  
    83  	// always allow non-namespaced resources
    84  	if len(a.GetNamespace()) == 0 && a.GetKind().GroupKind() != v1.SchemeGroupVersion.WithKind("Namespace").GroupKind() {
    85  		return nil
    86  	}
    87  
    88  	if a.GetKind().GroupKind() == v1.SchemeGroupVersion.WithKind("Namespace").GroupKind() {
    89  		// if a namespace is deleted, we want to prevent all further creates into it
    90  		// while it is undergoing termination.  to reduce incidences where the cache
    91  		// is slow to update, we add the namespace into a force live lookup list to ensure
    92  		// we are not looking at stale state.
    93  		if a.GetOperation() == admission.Delete {
    94  			l.forceLiveLookupCache.Add(a.GetName(), true, forceLiveLookupTTL)
    95  		}
    96  		// allow all operations to namespaces
    97  		return nil
    98  	}
    99  
   100  	// always allow deletion of other resources
   101  	if a.GetOperation() == admission.Delete {
   102  		return nil
   103  	}
   104  
   105  	// always allow access review checks.  Returning status about the namespace would be leaking information
   106  	if isAccessReview(a) {
   107  		return nil
   108  	}
   109  
   110  	// we need to wait for our caches to warm
   111  	if !l.WaitForReady() {
   112  		return admission.NewForbidden(a, fmt.Errorf("not yet ready to handle request"))
   113  	}
   114  
   115  	var (
   116  		exists bool
   117  		err    error
   118  	)
   119  
   120  	namespace, err := l.namespaceLister.Get(a.GetNamespace())
   121  	if err != nil {
   122  		if !errors.IsNotFound(err) {
   123  			return errors.NewInternalError(err)
   124  		}
   125  	} else {
   126  		exists = true
   127  	}
   128  
   129  	if !exists && a.GetOperation() == admission.Create {
   130  		// give the cache time to observe the namespace before rejecting a create.
   131  		// this helps when creating a namespace and immediately creating objects within it.
   132  		time.Sleep(missingNamespaceWait)
   133  		namespace, err = l.namespaceLister.Get(a.GetNamespace())
   134  		switch {
   135  		case errors.IsNotFound(err):
   136  			// no-op
   137  		case err != nil:
   138  			return errors.NewInternalError(err)
   139  		default:
   140  			exists = true
   141  		}
   142  		if exists {
   143  			klog.V(4).InfoS("Namespace existed in cache after waiting", "namespace", klog.KRef("", a.GetNamespace()))
   144  		}
   145  	}
   146  
   147  	// forceLiveLookup if true will skip looking at local cache state and instead always make a live call to server.
   148  	forceLiveLookup := false
   149  	if _, ok := l.forceLiveLookupCache.Get(a.GetNamespace()); ok {
   150  		// we think the namespace was marked for deletion, but our current local cache says otherwise, we will force a live lookup.
   151  		forceLiveLookup = exists && namespace.Status.Phase == v1.NamespaceActive
   152  	}
   153  
   154  	// refuse to operate on non-existent namespaces
   155  	if !exists || forceLiveLookup {
   156  		// as a last resort, make a call directly to storage
   157  		namespace, err = l.client.CoreV1().Namespaces().Get(context.TODO(), a.GetNamespace(), metav1.GetOptions{})
   158  		switch {
   159  		case errors.IsNotFound(err):
   160  			return err
   161  		case err != nil:
   162  			return errors.NewInternalError(err)
   163  		}
   164  
   165  		klog.V(4).InfoS("Found namespace via storage lookup", "namespace", klog.KRef("", a.GetNamespace()))
   166  	}
   167  
   168  	// ensure that we're not trying to create objects in terminating namespaces
   169  	if a.GetOperation() == admission.Create {
   170  		if namespace.Status.Phase != v1.NamespaceTerminating {
   171  			return nil
   172  		}
   173  
   174  		err := admission.NewForbidden(a, fmt.Errorf("unable to create new content in namespace %s because it is being terminated", a.GetNamespace()))
   175  		if apierr, ok := err.(*errors.StatusError); ok {
   176  			apierr.ErrStatus.Details.Causes = append(apierr.ErrStatus.Details.Causes, metav1.StatusCause{
   177  				Type:    v1.NamespaceTerminatingCause,
   178  				Message: fmt.Sprintf("namespace %s is being terminated", a.GetNamespace()),
   179  				Field:   "metadata.namespace",
   180  			})
   181  		}
   182  		return err
   183  	}
   184  
   185  	return nil
   186  }
   187  
   188  // NewLifecycle creates a new namespace Lifecycle admission control handler
   189  func NewLifecycle(immortalNamespaces sets.String) (*Lifecycle, error) {
   190  	return newLifecycleWithClock(immortalNamespaces, clock.RealClock{})
   191  }
   192  
   193  func newLifecycleWithClock(immortalNamespaces sets.String, clock utilcache.Clock) (*Lifecycle, error) {
   194  	forceLiveLookupCache := utilcache.NewLRUExpireCacheWithClock(100, clock)
   195  	return &Lifecycle{
   196  		Handler:              admission.NewHandler(admission.Create, admission.Update, admission.Delete),
   197  		immortalNamespaces:   immortalNamespaces,
   198  		forceLiveLookupCache: forceLiveLookupCache,
   199  	}, nil
   200  }
   201  
   202  // SetExternalKubeInformerFactory implements the WantsExternalKubeInformerFactory interface.
   203  func (l *Lifecycle) SetExternalKubeInformerFactory(f informers.SharedInformerFactory) {
   204  	namespaceInformer := f.Core().V1().Namespaces()
   205  	l.namespaceLister = namespaceInformer.Lister()
   206  	l.SetReadyFunc(namespaceInformer.Informer().HasSynced)
   207  }
   208  
   209  // SetExternalKubeClientSet implements the WantsExternalKubeClientSet interface.
   210  func (l *Lifecycle) SetExternalKubeClientSet(client kubernetes.Interface) {
   211  	l.client = client
   212  }
   213  
   214  // ValidateInitialization implements the InitializationValidator interface.
   215  func (l *Lifecycle) ValidateInitialization() error {
   216  	if l.namespaceLister == nil {
   217  		return fmt.Errorf("missing namespaceLister")
   218  	}
   219  	if l.client == nil {
   220  		return fmt.Errorf("missing client")
   221  	}
   222  	return nil
   223  }
   224  
   225  // accessReviewResources are resources which give a view into permissions in a namespace.  Users must be allowed to create these
   226  // resources because returning "not found" errors allows someone to search for the "people I'm going to fire in 2017" namespace.
   227  var accessReviewResources = map[schema.GroupResource]bool{
   228  	{Group: "authorization.k8s.io", Resource: "localsubjectaccessreviews"}: true,
   229  }
   230  
   231  func isAccessReview(a admission.Attributes) bool {
   232  	return accessReviewResources[a.GetResource().GroupResource()]
   233  }