Skip to content

Commit

Permalink
ingressclass: New controller.
Browse files Browse the repository at this point in the history
Add a controller that creates and manages an ingressclass for each
ingresscontroller.

* manifests/00-cluster-role.yaml: Give the operator access to
ingressclasses.
* pkg/manifests/bindata.go: Regenerate.
* pkg/operator/controller/ingressclass/controller.go: New file.  Define a
controller that manages ingressclasses for ingresscontrollers.
(controllerName): New const for the new controller's name.
(New): New function.  Create and return a controller that watches
ingresscontrollers and ingressclasses and reconciles them, using the new
ingressClassHasIngressController function and
ingressClassToIngressController method to map events for ingressclasses to
reconcile requests for ingresscontrollers.
(ingressClassHasIngressController): New function.  Given an ingressclass,
return a Boolean value indicating whether it references an
ingresscontroller.
(ingressClassToIngressController): New method.  Given a client object,
check if the object is an ingressclass with an associated
ingresscontroller, and return a slice of reconciliation requests with a
request for any ingresscontroller that is associated with the ingressclass.
(Config): New type.  Store the configuration needed to create an
ingressclass controller.
(reconciler): New type.  Store the state of an ingressclass controller.
(Reconcile): New method.  Handle a reconciliation request for an
ingresscontroller by ensuring that it has the expected ingressclass.
* pkg/operator/controller/ingressclass/ingressclass.go: New file.
(ensureIngressClass): New method.  Ensure the expected ingressclass exists
for the given ingresscontroller, using the new desiredIngressClass function
and currentIngressClass and updateIngressClass methods.
(desiredIngressClass): New function.  Given an ingresscontroller's name,
return a desired ingressclass.
(currentIngressClass): New method.  Given an ingresscontroller's name,
return its current ingressclass if it exists.
(updateIngressClass): New method.  Given current and desired
ingressclasses, update the current ingress class if needed, using the new
ingressclassChanged function.
(ingressclassChanged): New function.  Compare current and expected
ingressclasses to determine if they match, and return an updated
ingressclass if they do not.
* pkg/operator/controller/names.go (IngressClassName): New function.  Given
an ingresscontroller's name, return the name of the corresponding
ingressclass.
* pkg/operator/operator.go (New): Initialize the new controller.
* test/e2e/operator_test.go (TestDefaultIngressClass): New test.  Verify
that the ingressclass controller has created an ingressclass for the
default ingresscontroller.
  • Loading branch information
Miciah committed Apr 6, 2021
1 parent f97d943 commit 44c2306
Show file tree
Hide file tree
Showing 7 changed files with 302 additions and 4 deletions.
11 changes: 11 additions & 0 deletions manifests/00-cluster-role.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,17 @@ rules:
verbs:
- update

- apiGroups:
- networking.k8s.io
resources:
- ingressclasses
verbs:
- create
- get
- list
- update
- watch

# Mirrored from assets/router/metrics/cluster-role.yaml
- apiGroups:
- route.openshift.io
Expand Down
8 changes: 4 additions & 4 deletions pkg/manifests/bindata.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

114 changes: 114 additions & 0 deletions pkg/operator/controller/ingressclass/controller.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
package ingressclass

import (
"context"

"github.com/pkg/errors"

logf "github.com/openshift/cluster-ingress-operator/pkg/log"

"k8s.io/client-go/tools/record"

operatorv1 "github.com/openshift/api/operator/v1"
routev1 "github.com/openshift/api/route/v1"

networkingv1 "k8s.io/api/networking/v1"

"k8s.io/apimachinery/pkg/types"

"sigs.k8s.io/controller-runtime/pkg/cache"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller"
"sigs.k8s.io/controller-runtime/pkg/handler"
"sigs.k8s.io/controller-runtime/pkg/manager"
"sigs.k8s.io/controller-runtime/pkg/predicate"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
"sigs.k8s.io/controller-runtime/pkg/source"
)

const (
controllerName = "ingressclass_controller"
)

var (
log = logf.Logger.WithName(controllerName)
)

// New creates and returns a controller that creates and manages IngressClass
// objects for IngressControllers.
func New(mgr manager.Manager, config Config) (controller.Controller, error) {
reconciler := &reconciler{
config: config,
client: mgr.GetClient(),
cache: mgr.GetCache(),
recorder: mgr.GetEventRecorderFor(controllerName),
}
c, err := controller.New(controllerName, mgr, controller.Options{Reconciler: reconciler})
if err != nil {
return nil, err
}
if err := c.Watch(&source.Kind{Type: &operatorv1.IngressController{}}, &handler.EnqueueRequestForObject{}); err != nil {
return nil, err
}
if err := c.Watch(&source.Kind{Type: &networkingv1.IngressClass{}}, handler.EnqueueRequestsFromMapFunc(reconciler.ingressClassToIngressController), predicate.NewPredicateFuncs(ingressClassHasIngressController)); err != nil {
return nil, err
}
return c, nil
}

// ingressClassHasIngressController returns a value indicating whether the
// provided ingressclass references an ingresscontroller.
func ingressClassHasIngressController(o client.Object) bool {
class := o.(*networkingv1.IngressClass)
return class.Spec.Controller == routev1.IngressToRouteIngressClassControllerName &&
class.Spec.Parameters != nil &&
class.Spec.Parameters.APIGroup != nil &&
*class.Spec.Parameters.APIGroup == operatorv1.GroupVersion.String() &&
class.Spec.Parameters.Kind == "IngressController"
}

// ingressClassToIngressController takes an ingressclass and returns a slice of
// reconcile.Request with a request to reconcile the ingresscontroller that is
// associated with the ingressclass.
func (r *reconciler) ingressClassToIngressController(o client.Object) []reconcile.Request {
class := o.(*networkingv1.IngressClass)
return []reconcile.Request{{
NamespacedName: types.NamespacedName{
Namespace: r.config.Namespace,
Name: class.Spec.Parameters.Name,
}},
}
}

// Config holds all the configuration that must be provided when creating the
// controller.
type Config struct {
Namespace string
}

// reconciler handles the actual ingressclass reconciliation logic.
type reconciler struct {
config Config

client client.Client
cache cache.Cache
recorder record.EventRecorder
}

// Reconcile expects request to refer to an IngressController in the operator
// namespace and creates or reconciles an IngressClass object for that
// IngressController.
func (r *reconciler) Reconcile(ctx context.Context, request reconcile.Request) (reconcile.Result, error) {
log.Info("reconciling", "request", request)

classes := &networkingv1.IngressClassList{}
if err := r.cache.List(ctx, classes); err != nil {
return reconcile.Result{}, errors.Wrap(err, "failed to list ingressclasses")
}

if _, _, err := r.ensureIngressClass(request.NamespacedName.Name, classes.Items); err != nil {
return reconcile.Result{}, errors.Wrapf(err, "failed to ensure ingressclass for ingresscontroller %q", request.NamespacedName)
}

return reconcile.Result{}, nil
}
141 changes: 141 additions & 0 deletions pkg/operator/controller/ingressclass/ingressclass.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
package ingressclass

import (
"context"
"fmt"

"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"

operatorv1 "github.com/openshift/api/operator/v1"
routev1 "github.com/openshift/api/route/v1"
"github.com/openshift/cluster-ingress-operator/pkg/operator/controller"

networkingv1 "k8s.io/api/networking/v1"

"k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

// ensureIngressClass ensures an IngressClass exists for the IngressController
// with the given name. Returns a Boolean indicating whether the IngressClass
// exists, the current IngressClass if it does exist, and an error value.
func (r *reconciler) ensureIngressClass(ingresscontrollerName string, ingressclasses []networkingv1.IngressClass) (bool, *networkingv1.IngressClass, error) {
want, desired := desiredIngressClass(ingresscontrollerName, ingressclasses)

have, current, err := r.currentIngressClass(ingresscontrollerName)
if err != nil {
return false, nil, err
}

switch {
case !want && !have:
return false, nil, nil
case !want && have:
if err := r.client.Delete(context.TODO(), current); err != nil {
if !errors.IsNotFound(err) {
return true, current, fmt.Errorf("failed to delete IngressClass: %w", err)
}
} else {
log.Info("deleted IngressClass", "ingressclass", current)
}
return false, nil, nil
case want && !have:
if err := r.client.Create(context.TODO(), desired); err != nil {
return false, nil, fmt.Errorf("failed to create IngressClass: %w", err)
}
log.Info("created IngressClass", "ingressclass", desired)
return r.currentIngressClass(ingresscontrollerName)
case want && have:
if updated, err := r.updateIngressClass(current, desired); err != nil {
return true, current, fmt.Errorf("failed to update IngressClass: %w", err)
} else if updated {
return r.currentIngressClass(ingresscontrollerName)
}
}

return true, current, nil
}

// desiredIngressClass returns a Boolean indicating whether an IngressClass
// is desired, as well as the IngressClass if one is desired.
func desiredIngressClass(ingresscontrollerName string, ingressclasses []networkingv1.IngressClass) (bool, *networkingv1.IngressClass) {
name := controller.IngressClassName(ingresscontrollerName)
class := &networkingv1.IngressClass{
ObjectMeta: metav1.ObjectMeta{
Name: name.Name,
},
Spec: networkingv1.IngressClassSpec{
Controller: routev1.IngressToRouteIngressClassControllerName,
Parameters: &networkingv1.IngressClassParametersReference{
APIGroup: &operatorv1.GroupName,
Kind: "IngressController",
Name: ingresscontrollerName,
},
},
}
// When creating an IngressClass for the "default" IngressController,
// annotate the IngressClass as the default IngressClass if no other
// IngressClass has the annotation.
if ingresscontrollerName == "default" {
const defaultAnnotation = "ingressclass.kubernetes.io/is-default-class"
someIngressClassIsDefault := false
for _, class := range ingressclasses {
if class.Annotations[defaultAnnotation] == "true" {
someIngressClassIsDefault = true
break
}
}
if !someIngressClassIsDefault {
class.ObjectMeta.Annotations = map[string]string{
defaultAnnotation: "true",
}
}
}
return true, class
}

// currentIngressClass returns a Boolean indicating whether an IngressClass
// exists for the IngressController with the given name, as well as the
// IngressClass if it does exist and an error value.
func (r *reconciler) currentIngressClass(ingresscontrollerName string) (bool, *networkingv1.IngressClass, error) {
name := controller.IngressClassName(ingresscontrollerName)
class := &networkingv1.IngressClass{}
if err := r.client.Get(context.TODO(), name, class); err != nil {
if errors.IsNotFound(err) {
return false, nil, nil
}
return false, nil, err
}
return true, class, nil
}

// updateIngressClass updates an IngressClass. Returns a Boolean indicating
// whether the IngressClass was updated, and an error value.
func (r *reconciler) updateIngressClass(current, desired *networkingv1.IngressClass) (bool, error) {
changed, updated := ingressclassChanged(current, desired)
if !changed {
return false, nil
}

// Diff before updating because the client may mutate the object.
diff := cmp.Diff(current, updated, cmpopts.EquateEmpty())
if err := r.client.Update(context.TODO(), updated); err != nil {
log.Info("updated IngressClass", "name", updated.Name, "diff", diff)
return false, err
}
return true, nil
}

// ingressclassChanged checks if the current IngressClass spec matches
// the expected spec and if not returns an updated one.
func ingressclassChanged(current, expected *networkingv1.IngressClass) (bool, *networkingv1.IngressClass) {
if cmp.Equal(current.Spec, expected.Spec, cmpopts.EquateEmpty()) {
return false, nil
}

updated := current.DeepCopy()
updated.Spec = expected.Spec

return true, updated
}
4 changes: 4 additions & 0 deletions pkg/operator/controller/names.go
Original file line number Diff line number Diff line change
Expand Up @@ -207,3 +207,7 @@ func CanaryRouteName() types.NamespacedName {
Name: "canary",
}
}

func IngressClassName(ingressControllerName string) types.NamespacedName {
return types.NamespacedName{Name: "openshift-" + ingressControllerName}
}
8 changes: 8 additions & 0 deletions pkg/operator/operator.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
configurableroutecontroller "github.com/openshift/cluster-ingress-operator/pkg/operator/controller/configurable-route"
dnscontroller "github.com/openshift/cluster-ingress-operator/pkg/operator/controller/dns"
ingresscontroller "github.com/openshift/cluster-ingress-operator/pkg/operator/controller/ingress"
ingressclasscontroller "github.com/openshift/cluster-ingress-operator/pkg/operator/controller/ingressclass"
statuscontroller "github.com/openshift/cluster-ingress-operator/pkg/operator/controller/status"
"github.com/openshift/library-go/pkg/operator/events"

Expand Down Expand Up @@ -126,6 +127,13 @@ func New(config operatorconfig.Config, kubeConfig *rest.Config) (*Operator, erro
return nil, fmt.Errorf("failed to create dns controller: %v", err)
}

// Set up the ingressclass controller.
if _, err := ingressclasscontroller.New(mgr, ingressclasscontroller.Config{
Namespace: config.Namespace,
}); err != nil {
return nil, fmt.Errorf("failed to create ingressclass controller: %w", err)
}

// Set up the canary controller when the config.CanaryImage is not empty
// Canary can be disabled when running the operator locally.
if len(config.CanaryImage) != 0 {
Expand Down
20 changes: 20 additions & 0 deletions test/e2e/operator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import (

appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
networkingv1 "k8s.io/api/networking/v1"
policyv1beta1 "k8s.io/api/policy/v1beta1"

"k8s.io/apimachinery/pkg/api/errors"
Expand Down Expand Up @@ -167,6 +168,25 @@ func TestDefaultIngressControllerSteadyConditions(t *testing.T) {
}
}

// TestDefaultIngressClass verifies that the ingressclass controller has created
// an ingressclass for the default ingresscontroller.
func TestDefaultIngressClass(t *testing.T) {
name := controller.IngressClassName(manifests.DefaultIngressControllerName)
ingressclass := &networkingv1.IngressClass{}
if err := kclient.Get(context.TODO(), name, ingressclass); err != nil {
t.Errorf("failed to get ingressclass %q: %v", name, err)
}
const (
defaultAnnotation = "ingressclass.kubernetes.io/is-default-class"
expected = "true"
)
if actual, ok := ingressclass.Annotations[defaultAnnotation]; !ok {
t.Fatalf("ingressclass %q has no %q annotation", name, defaultAnnotation)
} else if actual != expected {
t.Fatalf("expected %q annotation to have value %q, found %q", defaultAnnotation, expected, actual)
}
}

func TestUserDefinedIngressController(t *testing.T) {
name := types.NamespacedName{Namespace: operatorNamespace, Name: "test"}
ing := newLoadBalancerController(name, name.Name+"."+dnsConfig.Spec.BaseDomain)
Expand Down

0 comments on commit 44c2306

Please sign in to comment.