Adding IngressClass to networking/v1beta1
Co-authored-by: Christopher M. Luciano <cmluciano@us.ibm.com>
This commit is contained in:
@@ -15,6 +15,7 @@ filegroup(
|
||||
"//plugin/pkg/admission/alwayspullimages:all-srcs",
|
||||
"//plugin/pkg/admission/antiaffinity:all-srcs",
|
||||
"//plugin/pkg/admission/certificates:all-srcs",
|
||||
"//plugin/pkg/admission/defaultingressclass:all-srcs",
|
||||
"//plugin/pkg/admission/defaulttolerationseconds:all-srcs",
|
||||
"//plugin/pkg/admission/deny:all-srcs",
|
||||
"//plugin/pkg/admission/eventratelimit:all-srcs",
|
||||
|
53
plugin/pkg/admission/defaultingressclass/BUILD
Normal file
53
plugin/pkg/admission/defaultingressclass/BUILD
Normal file
@@ -0,0 +1,53 @@
|
||||
load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
|
||||
|
||||
go_library(
|
||||
name = "go_default_library",
|
||||
srcs = ["admission.go"],
|
||||
importpath = "k8s.io/kubernetes/plugin/pkg/admission/defaultingressclass",
|
||||
visibility = ["//visibility:public"],
|
||||
deps = [
|
||||
"//pkg/apis/networking:go_default_library",
|
||||
"//pkg/features:go_default_library",
|
||||
"//staging/src/k8s.io/api/networking/v1beta1:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/api/errors:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/labels:go_default_library",
|
||||
"//staging/src/k8s.io/apiserver/pkg/admission:go_default_library",
|
||||
"//staging/src/k8s.io/apiserver/pkg/admission/initializer:go_default_library",
|
||||
"//staging/src/k8s.io/client-go/informers:go_default_library",
|
||||
"//staging/src/k8s.io/client-go/listers/networking/v1beta1:go_default_library",
|
||||
"//staging/src/k8s.io/component-base/featuregate:go_default_library",
|
||||
"//vendor/k8s.io/klog:go_default_library",
|
||||
],
|
||||
)
|
||||
|
||||
go_test(
|
||||
name = "go_default_test",
|
||||
srcs = ["admission_test.go"],
|
||||
embed = [":go_default_library"],
|
||||
deps = [
|
||||
"//pkg/apis/core:go_default_library",
|
||||
"//pkg/apis/networking:go_default_library",
|
||||
"//pkg/controller:go_default_library",
|
||||
"//staging/src/k8s.io/api/networking/v1beta1:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/api/errors:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
|
||||
"//staging/src/k8s.io/apiserver/pkg/admission:go_default_library",
|
||||
"//staging/src/k8s.io/apiserver/pkg/admission/testing:go_default_library",
|
||||
"//staging/src/k8s.io/client-go/informers:go_default_library",
|
||||
"//vendor/k8s.io/utils/pointer:go_default_library",
|
||||
],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "package-srcs",
|
||||
srcs = glob(["**"]),
|
||||
tags = ["automanaged"],
|
||||
visibility = ["//visibility:private"],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "all-srcs",
|
||||
srcs = [":package-srcs"],
|
||||
tags = ["automanaged"],
|
||||
visibility = ["//visibility:public"],
|
||||
)
|
173
plugin/pkg/admission/defaultingressclass/admission.go
Normal file
173
plugin/pkg/admission/defaultingressclass/admission.go
Normal file
@@ -0,0 +1,173 @@
|
||||
/*
|
||||
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
|
||||
}
|
198
plugin/pkg/admission/defaultingressclass/admission_test.go
Normal file
198
plugin/pkg/admission/defaultingressclass/admission_test.go
Normal file
@@ -0,0 +1,198 @@
|
||||
/*
|
||||
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"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
networkingv1beta1 "k8s.io/api/networking/v1beta1"
|
||||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apiserver/pkg/admission"
|
||||
admissiontesting "k8s.io/apiserver/pkg/admission/testing"
|
||||
"k8s.io/client-go/informers"
|
||||
api "k8s.io/kubernetes/pkg/apis/core"
|
||||
"k8s.io/kubernetes/pkg/apis/networking"
|
||||
"k8s.io/kubernetes/pkg/controller"
|
||||
utilpointer "k8s.io/utils/pointer"
|
||||
)
|
||||
|
||||
func TestAdmission(t *testing.T) {
|
||||
defaultClass1 := &networkingv1beta1.IngressClass{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: "IngressClass",
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "default1",
|
||||
Annotations: map[string]string{
|
||||
networkingv1beta1.AnnotationIsDefaultIngressClass: "true",
|
||||
},
|
||||
},
|
||||
}
|
||||
defaultClass2 := &networkingv1beta1.IngressClass{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "default2",
|
||||
Annotations: map[string]string{
|
||||
networkingv1beta1.AnnotationIsDefaultIngressClass: "true",
|
||||
},
|
||||
},
|
||||
}
|
||||
// Class that has explicit default = false
|
||||
classWithFalseDefault := &networkingv1beta1.IngressClass{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: "IngressClass",
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "nondefault1",
|
||||
Annotations: map[string]string{
|
||||
networkingv1beta1.AnnotationIsDefaultIngressClass: "false",
|
||||
},
|
||||
},
|
||||
}
|
||||
// Class with missing default annotation (=non-default)
|
||||
classWithNoDefault := &networkingv1beta1.IngressClass{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: "IngressClass",
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "nondefault2",
|
||||
},
|
||||
}
|
||||
// Class with empty default annotation (=non-default)
|
||||
classWithEmptyDefault := &networkingv1beta1.IngressClass{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: "IngressClass",
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "nondefault2",
|
||||
Annotations: map[string]string{
|
||||
networkingv1beta1.AnnotationIsDefaultIngressClass: "",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
classes []*networkingv1beta1.IngressClass
|
||||
classField *string
|
||||
classAnnotation *string
|
||||
expectedClass *string
|
||||
expectedError error
|
||||
}{
|
||||
{
|
||||
name: "no default, no modification of Ingress",
|
||||
classes: []*networkingv1beta1.IngressClass{classWithFalseDefault, classWithNoDefault, classWithEmptyDefault},
|
||||
classField: nil,
|
||||
classAnnotation: nil,
|
||||
expectedClass: nil,
|
||||
expectedError: nil,
|
||||
},
|
||||
{
|
||||
name: "one default, modify Ingress with class=nil",
|
||||
classes: []*networkingv1beta1.IngressClass{defaultClass1, classWithFalseDefault, classWithNoDefault, classWithEmptyDefault},
|
||||
classField: nil,
|
||||
classAnnotation: nil,
|
||||
expectedClass: utilpointer.StringPtr(defaultClass1.Name),
|
||||
expectedError: nil,
|
||||
},
|
||||
{
|
||||
name: "one default, no modification of Ingress with class field=''",
|
||||
classes: []*networkingv1beta1.IngressClass{defaultClass1, classWithFalseDefault, classWithNoDefault, classWithEmptyDefault},
|
||||
classField: utilpointer.StringPtr(""),
|
||||
classAnnotation: nil,
|
||||
expectedClass: utilpointer.StringPtr(""),
|
||||
expectedError: nil,
|
||||
},
|
||||
{
|
||||
name: "one default, no modification of Ingress with class field='foo'",
|
||||
classes: []*networkingv1beta1.IngressClass{defaultClass1, classWithFalseDefault, classWithNoDefault, classWithEmptyDefault},
|
||||
classField: utilpointer.StringPtr("foo"),
|
||||
classAnnotation: nil,
|
||||
expectedClass: utilpointer.StringPtr("foo"),
|
||||
expectedError: nil,
|
||||
},
|
||||
{
|
||||
name: "one default, no modification of Ingress with class annotation='foo'",
|
||||
classes: []*networkingv1beta1.IngressClass{defaultClass1, classWithFalseDefault, classWithNoDefault, classWithEmptyDefault},
|
||||
classField: nil,
|
||||
classAnnotation: utilpointer.StringPtr("foo"),
|
||||
expectedClass: nil,
|
||||
expectedError: nil,
|
||||
},
|
||||
{
|
||||
name: "two defaults, error with Ingress with class field=nil",
|
||||
classes: []*networkingv1beta1.IngressClass{defaultClass1, defaultClass2, classWithFalseDefault, classWithNoDefault, classWithEmptyDefault},
|
||||
classField: nil,
|
||||
classAnnotation: nil,
|
||||
expectedClass: nil,
|
||||
expectedError: errors.NewForbidden(networkingv1beta1.Resource("ingresses"), "testing", errors.NewInternalError(fmt.Errorf("2 default IngressClasses were found, only 1 allowed"))),
|
||||
},
|
||||
{
|
||||
name: "two defaults, no modification with Ingress with class field=''",
|
||||
classes: []*networkingv1beta1.IngressClass{defaultClass1, defaultClass2, classWithFalseDefault, classWithNoDefault, classWithEmptyDefault},
|
||||
classField: utilpointer.StringPtr(""),
|
||||
classAnnotation: nil,
|
||||
expectedClass: utilpointer.StringPtr(""),
|
||||
expectedError: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
ctrl := newPlugin()
|
||||
ctrl.defaultIngressClassEnabled = true
|
||||
informerFactory := informers.NewSharedInformerFactory(nil, controller.NoResyncPeriodFunc())
|
||||
ctrl.SetExternalKubeInformerFactory(informerFactory)
|
||||
for _, c := range testCase.classes {
|
||||
informerFactory.Networking().V1beta1().IngressClasses().Informer().GetStore().Add(c)
|
||||
}
|
||||
|
||||
ingress := &networking.Ingress{ObjectMeta: metav1.ObjectMeta{Name: "testing", Namespace: "testing"}}
|
||||
if testCase.classField != nil {
|
||||
ingress.Spec.IngressClassName = testCase.classField
|
||||
}
|
||||
if testCase.classAnnotation != nil {
|
||||
ingress.Annotations = map[string]string{networkingv1beta1.AnnotationIngressClass: *testCase.classAnnotation}
|
||||
}
|
||||
|
||||
attrs := admission.NewAttributesRecord(
|
||||
ingress, // new object
|
||||
nil, // old object
|
||||
api.Kind("Ingress").WithVersion("version"),
|
||||
ingress.Namespace,
|
||||
ingress.Name,
|
||||
networkingv1beta1.Resource("ingresses").WithVersion("version"),
|
||||
"", // subresource
|
||||
admission.Create,
|
||||
&metav1.CreateOptions{},
|
||||
false, // dryRun
|
||||
nil, // userInfo
|
||||
)
|
||||
|
||||
err := admissiontesting.WithReinvocationTesting(t, ctrl).Admit(context.TODO(), attrs, nil)
|
||||
if !reflect.DeepEqual(err, testCase.expectedError) {
|
||||
t.Errorf("Expected error: %v, got %v", testCase.expectedError, err)
|
||||
}
|
||||
if !reflect.DeepEqual(testCase.expectedClass, ingress.Spec.IngressClassName) {
|
||||
t.Errorf("Expected class name %+v, got %+v", *testCase.expectedClass, ingress.Spec.IngressClassName)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user