/* Copyright 2020 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 defaultingressclass import ( "context" "fmt" "io" networkingv1beta1 "k8s.io/api/networking/v1beta1" "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/labels" "k8s.io/apiserver/pkg/admission" genericadmissioninitializer "k8s.io/apiserver/pkg/admission/initializer" "k8s.io/client-go/informers" networkingv1beta1listers "k8s.io/client-go/listers/networking/v1beta1" "k8s.io/component-base/featuregate" "k8s.io/klog" "k8s.io/kubernetes/pkg/apis/networking" "k8s.io/kubernetes/pkg/features" ) const ( // PluginName is the name of this admission controller plugin PluginName = "DefaultIngressClass" ) // Register registers a plugin func Register(plugins *admission.Plugins) { plugins.Register(PluginName, func(config io.Reader) (admission.Interface, error) { plugin := newPlugin() return plugin, nil }) } // classDefaulterPlugin holds state for and implements the admission plugin. type classDefaulterPlugin struct { *admission.Handler lister networkingv1beta1listers.IngressClassLister inspectedFeatures bool defaultIngressClassEnabled bool } var _ admission.Interface = &classDefaulterPlugin{} var _ admission.MutationInterface = &classDefaulterPlugin{} var _ = genericadmissioninitializer.WantsExternalKubeInformerFactory(&classDefaulterPlugin{}) // newPlugin creates a new admission plugin. func newPlugin() *classDefaulterPlugin { return &classDefaulterPlugin{ Handler: admission.NewHandler(admission.Create), } } // InspectFeatureGates allows setting bools without taking a dep on a global variable func (a *classDefaulterPlugin) InspectFeatureGates(featureGates featuregate.FeatureGate) { a.defaultIngressClassEnabled = featureGates.Enabled(features.DefaultIngressClass) a.inspectedFeatures = true } // SetExternalKubeInformerFactory sets a lister and readyFunc for this // classDefaulterPlugin using the provided SharedInformerFactory. func (a *classDefaulterPlugin) SetExternalKubeInformerFactory(f informers.SharedInformerFactory) { if !a.defaultIngressClassEnabled { return } informer := f.Networking().V1beta1().IngressClasses() a.lister = informer.Lister() a.SetReadyFunc(informer.Informer().HasSynced) } // ValidateInitialization ensures lister is set. func (a *classDefaulterPlugin) ValidateInitialization() error { if !a.inspectedFeatures { return fmt.Errorf("InspectFeatureGates was not called") } if !a.defaultIngressClassEnabled { return nil } if a.lister == nil { return fmt.Errorf("missing lister") } return nil } // Admit sets the default value of a Ingress's class if the user did not specify // a class. func (a *classDefaulterPlugin) Admit(ctx context.Context, attr admission.Attributes, o admission.ObjectInterfaces) error { if !a.defaultIngressClassEnabled { return nil } if attr.GetResource().GroupResource() != networkingv1beta1.Resource("ingresses") { return nil } if len(attr.GetSubresource()) != 0 { return nil } ingress, ok := attr.GetObject().(*networking.Ingress) // if we can't convert then we don't handle this object so just return if !ok { klog.V(3).Infof("Expected Ingress resource, got: %v", attr.GetKind()) return errors.NewInternalError(fmt.Errorf("Expected Ingress resource, got: %v", attr.GetKind())) } // IngressClassName field has been set, no need to set a default value. if ingress.Spec.IngressClassName != nil { return nil } // Ingress class annotation has been set, no need to set a default value. if _, ok := ingress.Annotations[networkingv1beta1.AnnotationIngressClass]; ok { return nil } klog.V(4).Infof("No class specified on Ingress %s", ingress.Name) defaultClass, err := getDefaultClass(a.lister) if err != nil { return admission.NewForbidden(attr, err) } // No default class specified, no need to set a default value. if defaultClass == nil { return nil } klog.V(4).Infof("Defaulting class for Ingress %s to %s", ingress.Name, defaultClass.Name) ingress.Spec.IngressClassName = &defaultClass.Name return nil } // getDefaultClass returns the default IngressClass from the store, or nil. func getDefaultClass(lister networkingv1beta1listers.IngressClassLister) (*networkingv1beta1.IngressClass, error) { list, err := lister.List(labels.Everything()) if err != nil { return nil, err } defaultClasses := []*networkingv1beta1.IngressClass{} for _, class := range list { if class.Annotations[networkingv1beta1.AnnotationIsDefaultIngressClass] == "true" { defaultClasses = append(defaultClasses, class) } } if len(defaultClasses) == 0 { return nil, nil } if len(defaultClasses) > 1 { klog.V(3).Infof("%d default IngressClasses were found, only 1 allowed", len(defaultClasses)) return nil, errors.NewInternalError(fmt.Errorf("%d default IngressClasses were found, only 1 allowed", len(defaultClasses))) } return defaultClasses[0], nil }