-
Notifications
You must be signed in to change notification settings - Fork 191
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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 ingressClassToIngressControllers method to map events for ingressclasses to reconcile requests for ingresscontrollers. (ingressClassToIngressControllers): 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, using the new ingressClassReferencesSomeIngressController function. (ingressClassReferencesSomeIngressController): New function. Given an ingressclass, return a Boolean value indicating whether it references an ingresscontroller. (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
Showing
7 changed files
with
307 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,130 @@ | ||
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/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.ingressClassToIngressControllers)); err != nil { | ||
return nil, err | ||
} | ||
return c, nil | ||
} | ||
|
||
// ingressClassToIngressControllers takes a client object, checks if it is an | ||
// ingressclass with an associated ingresscontroller, and returns a slice of | ||
// reconcile.Request with a request to reconcile any ingresscontroller that is | ||
// associated with the ingressclass. | ||
func (r *reconciler) ingressClassToIngressControllers(o client.Object) []reconcile.Request { | ||
var requests []reconcile.Request | ||
classes := &networkingv1.IngressClassList{} | ||
if err := r.cache.List(context.Background(), classes); err != nil { | ||
log.Error(err, "failed to list ingressclasses") | ||
return requests | ||
} | ||
for _, class := range classes.Items { | ||
if !ingressClassReferencesSomeIngressController(&class) { | ||
continue | ||
} | ||
ingresscontrollerName := class.Spec.Parameters.Name | ||
if class.Spec.Parameters.Name != ingresscontrollerName { | ||
continue | ||
} | ||
log.Info("queueing ingresscontroller", "name", ingresscontrollerName, "related", o.GetSelfLink()) | ||
request := reconcile.Request{ | ||
NamespacedName: types.NamespacedName{ | ||
Namespace: r.config.Namespace, | ||
Name: ingresscontrollerName, | ||
}, | ||
} | ||
requests = append(requests, request) | ||
} | ||
return requests | ||
} | ||
|
||
// ingressClassReferencesSomeIngressController returns a value indicating | ||
// whether the provide IngressClass references an IngressController. | ||
func ingressClassReferencesSomeIngressController(class *networkingv1.IngressClass) bool { | ||
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" | ||
} | ||
|
||
// 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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,139 @@ | ||
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: %v", 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: %v", 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: %v", err) | ||
} else if updated { | ||
log.Info("updated IngressClass", "ingressclass", desired) | ||
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 | ||
} | ||
|
||
if err := r.client.Update(context.TODO(), updated); err != nil { | ||
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters