Adding IngressClass to networking/v1beta1

Co-authored-by: Christopher M. Luciano <cmluciano@us.ibm.com>
This commit is contained in:
Rob Scott
2020-02-24 21:20:45 -08:00
parent debb1edee1
commit 132d2afca0
76 changed files with 4749 additions and 398 deletions

View File

@@ -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",

View 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"],
)

View 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
}

View 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)
}
})
}
}