-
Notifications
You must be signed in to change notification settings - Fork 224
OCPBUGS-9037, OCPBUGS-64565: Ensure canary cert matches the default ingress controller's cert #1334
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
openshift-merge-bot
merged 1 commit into
openshift:master
from
rfredette:ocpbugs-9037-canary-cert-controller
Jan 27, 2026
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or 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
255 changes: 255 additions & 0 deletions
255
pkg/operator/controller/canary-certificate/controller.go
This file contains hidden or 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,255 @@ | ||
| package canarycertificate | ||
|
|
||
| import ( | ||
| "context" | ||
| "fmt" | ||
|
|
||
| "github.com/google/go-cmp/cmp" | ||
| "github.com/google/go-cmp/cmp/cmpopts" | ||
| logf "github.com/openshift/cluster-ingress-operator/pkg/log" | ||
| "github.com/openshift/cluster-ingress-operator/pkg/manifests" | ||
|
|
||
| operatorcontroller "github.com/openshift/cluster-ingress-operator/pkg/operator/controller" | ||
|
|
||
| operatorv1 "github.com/openshift/api/operator/v1" | ||
|
|
||
| appsv1 "k8s.io/api/apps/v1" | ||
| corev1 "k8s.io/api/core/v1" | ||
| "k8s.io/apimachinery/pkg/api/errors" | ||
| metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | ||
| "k8s.io/apimachinery/pkg/types" | ||
| "k8s.io/client-go/tools/record" | ||
|
|
||
| "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 ( | ||
| canaryCertControllerName = "canary_certificate_controller" | ||
| ) | ||
|
|
||
| var ( | ||
| log = logf.Logger.WithName(canaryCertControllerName) | ||
| ) | ||
|
|
||
| // Config holds all the things necessary for the controller to run. | ||
| type Config struct { | ||
| OperatorNamespace string | ||
| OperandNamespace string | ||
| } | ||
|
|
||
| // reconciler handles the actual canary certificate reconciliation logic in | ||
| // response to events. | ||
| type reconciler struct { | ||
| config Config | ||
| client client.Client | ||
| recorder record.EventRecorder | ||
| } | ||
|
|
||
| // New creates the canary certificate controller | ||
| // | ||
| // The canary certificate controller mirrors the default ingress controller's | ||
| // certificate into the canary's namespace. It watches the canary's certificate | ||
| // and the default ingress controller's certificate. | ||
| func New(mgr manager.Manager, config Config) (controller.Controller, error) { | ||
| operatorCache := mgr.GetCache() | ||
| reconciler := &reconciler{ | ||
| config: config, | ||
| client: mgr.GetClient(), | ||
| recorder: mgr.GetEventRecorderFor(canaryCertControllerName), | ||
| } | ||
| c, err := controller.New(canaryCertControllerName, mgr, controller.Options{Reconciler: reconciler}) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
| // Watch for updates on the canary's certificate. | ||
| isCanaryCert := predicate.NewPredicateFuncs(func(o client.Object) bool { | ||
| return o.GetName() == operatorcontroller.CanaryCertificateName().Name && o.GetNamespace() == operatorcontroller.CanaryCertificateName().Namespace | ||
| }) | ||
| // Also watch for updates on the default ingress controller's certificate. | ||
| // Because the default ingress controller's certificate name can be set at | ||
| // runtime, a Get() needs to be done to determine the correct certificate. | ||
| isDefaultIngressCert := predicate.NewPredicateFuncs(func(o client.Object) bool { | ||
| if o.GetNamespace() != config.OperandNamespace { | ||
| return false | ||
| } | ||
|
|
||
| defaultICName := types.NamespacedName{ | ||
| Namespace: config.OperatorNamespace, | ||
| Name: manifests.DefaultIngressControllerName, | ||
| } | ||
| defaultIC := &operatorv1.IngressController{} | ||
| if err := reconciler.client.Get(context.Background(), defaultICName, defaultIC); err != nil { | ||
| log.Error(err, "Failed to get default IngressController") | ||
| return false | ||
| } | ||
|
|
||
| defaultCertName := operatorcontroller.RouterEffectiveDefaultCertificateSecretName(defaultIC, config.OperandNamespace) | ||
|
|
||
| return o.GetName() == defaultCertName.Name | ||
| }) | ||
| // Regardless of which certificate changed, this controller only has one | ||
| // reconcile target: the canary certificate's secret. | ||
| enqueueRequestForCanaryCertificate := handler.EnqueueRequestsFromMapFunc(func(ctx context.Context, a client.Object) []reconcile.Request { | ||
| return []reconcile.Request{{NamespacedName: operatorcontroller.CanaryCertificateName()}} | ||
| }) | ||
| if err := c.Watch(source.Kind[client.Object](operatorCache, &corev1.Secret{}, enqueueRequestForCanaryCertificate, predicate.Or(isCanaryCert, isDefaultIngressCert))); err != nil { | ||
| return nil, err | ||
| } | ||
| return c, nil | ||
| } | ||
|
|
||
| // Reconcile ensures the canary's certificate mirrors the default ingress | ||
| // controller's certificate. | ||
| func (r *reconciler) Reconcile(ctx context.Context, request reconcile.Request) (reconcile.Result, error) { | ||
| log.Info("Reconciling", "request", request) | ||
| result := reconcile.Result{} | ||
|
|
||
| if _, _, err := r.ensureCanaryCertificate(ctx); err != nil { | ||
| return result, fmt.Errorf("failed to ensure canary certificate: %w", err) | ||
| } | ||
|
|
||
| return result, nil | ||
| } | ||
|
|
||
| // ensureCanaryCertificate ensures the canary certificate secret exists, and | ||
| // that it uses the same certificate as the default ingress controller. | ||
| func (r *reconciler) ensureCanaryCertificate(ctx context.Context) (bool, *corev1.Secret, error) { | ||
| defaultIngressControllerName := types.NamespacedName{ | ||
| Namespace: r.config.OperatorNamespace, | ||
| Name: manifests.DefaultIngressControllerName, | ||
| } | ||
| defaultIngressController := &operatorv1.IngressController{} | ||
| if err := r.client.Get(ctx, defaultIngressControllerName, defaultIngressController); err != nil { | ||
| return false, nil, err | ||
| } | ||
| defaultCertName := operatorcontroller.RouterEffectiveDefaultCertificateSecretName(defaultIngressController, r.config.OperandNamespace) | ||
| defaultCert := &corev1.Secret{} | ||
| if err := r.client.Get(ctx, defaultCertName, defaultCert); err != nil { | ||
| return false, nil, err | ||
| } | ||
| canaryDaemonSet := &appsv1.DaemonSet{} | ||
| if err := r.client.Get(ctx, operatorcontroller.CanaryDaemonSetName(), canaryDaemonSet); err != nil { | ||
| return false, nil, err | ||
| } | ||
| haveCert, current, err := r.currentCanaryCertificate(ctx) | ||
| if err != nil { | ||
| return false, nil, err | ||
| } | ||
| trueVar := true | ||
| ownerRef := metav1.OwnerReference{ | ||
| APIVersion: "apps/v1", | ||
| Kind: "DaemonSet", | ||
| Name: canaryDaemonSet.Name, | ||
| UID: canaryDaemonSet.UID, | ||
| Controller: &trueVar, | ||
| } | ||
| desired := desiredCanaryCertificate(ownerRef, defaultCert) | ||
|
|
||
| switch { | ||
| case !haveCert: | ||
| if err := r.createCanaryCertificate(ctx, desired); err != nil { | ||
| return false, nil, err | ||
| } | ||
| return r.currentCanaryCertificate(ctx) | ||
| case haveCert: | ||
| if updated, err := r.updateCanaryCertificate(ctx, current, desired); err != nil { | ||
| return true, current, err | ||
| } else if updated { | ||
| return r.currentCanaryCertificate(ctx) | ||
| } | ||
| } | ||
| return true, current, nil | ||
| } | ||
|
|
||
| // currentCanaryCertificate returns the current canary certificate secret, if it exists. | ||
| func (r *reconciler) currentCanaryCertificate(ctx context.Context) (bool, *corev1.Secret, error) { | ||
| currentCanaryCert := &corev1.Secret{} | ||
| if err := r.client.Get(ctx, operatorcontroller.CanaryCertificateName(), currentCanaryCert); err != nil { | ||
| if errors.IsNotFound(err) { | ||
| return false, nil, nil | ||
| } | ||
| return false, nil, err | ||
| } | ||
| return true, currentCanaryCert, nil | ||
|
|
||
| } | ||
|
|
||
| // desiredCanaryCertificate returns the desired canary certificate secret, based | ||
| // on the default ingress controller's certificate. | ||
| func desiredCanaryCertificate(canaryOwnerRef metav1.OwnerReference, defaultIngressCertificate *corev1.Secret) *corev1.Secret { | ||
| canaryCertName := operatorcontroller.CanaryCertificateName() | ||
| return &corev1.Secret{ | ||
| ObjectMeta: metav1.ObjectMeta{ | ||
| Name: canaryCertName.Name, | ||
| Namespace: canaryCertName.Namespace, | ||
| OwnerReferences: []metav1.OwnerReference{canaryOwnerRef}, | ||
| }, | ||
| // No validation should be done here on Data or Type. The canary should | ||
| // accurately reflect the state of the default ingress controller, so | ||
| // either validation needs to be done at the ingresscontroller level, or | ||
| // invalid certificates will be detected by the canary at runtime. | ||
| Data: defaultIngressCertificate.Data, | ||
| Type: defaultIngressCertificate.Type, | ||
rfredette marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
| } | ||
|
|
||
| // createCanaryCertificate creates the canary certificate, or returns an error | ||
| // if it cannot. | ||
| func (r *reconciler) createCanaryCertificate(ctx context.Context, certificate *corev1.Secret) error { | ||
| if err := r.client.Create(ctx, certificate); err != nil { | ||
| return err | ||
| } | ||
|
|
||
| r.recorder.Event(certificate, "Normal", "CreatedCanaryCertificate", "created canary certificate") | ||
| return nil | ||
| } | ||
|
|
||
| // updateCanaryCertificate updates the canary certificate if the desired version | ||
| // differs from the current one, and returns an error if it cannot. | ||
| func (r *reconciler) updateCanaryCertificate(ctx context.Context, current, desired *corev1.Secret) (bool, error) { | ||
| changed, updated := canaryCertificateChanged(current, desired) | ||
| if !changed { | ||
| return false, nil | ||
| } | ||
| if err := r.client.Update(ctx, updated); err != nil { | ||
| return false, err | ||
| } | ||
| r.recorder.Event(updated, "Normal", "UpdatedCanaryCertificate", "updated canary certificate") | ||
| return true, nil | ||
| } | ||
|
|
||
| // canaryCertificateChanged returns an updated certificate secret if the current | ||
| // secret differs from the desired one. | ||
| func canaryCertificateChanged(current, desired *corev1.Secret) (bool, *corev1.Secret) { | ||
| changed := false | ||
| updated := current.DeepCopy() | ||
|
|
||
| if !cmp.Equal(current.OwnerReferences, desired.OwnerReferences) { | ||
| updated.OwnerReferences = desired.OwnerReferences | ||
| changed = true | ||
| } | ||
| if !cmp.Equal(current.Data, desired.Data) { | ||
| updated.Data = desired.Data | ||
| changed = true | ||
| } | ||
| if current.Type != desired.Type { | ||
| updated.Type = desired.Type | ||
| changed = true | ||
| } | ||
| if !cmp.Equal(current.Annotations, desired.Annotations, cmpopts.EquateEmpty()) { | ||
| updated.Annotations = desired.Annotations | ||
| changed = true | ||
| } | ||
| if !cmp.Equal(current.Labels, desired.Labels, cmpopts.EquateEmpty()) { | ||
| updated.Labels = desired.Labels | ||
| changed = true | ||
| } | ||
|
|
||
| return changed, updated | ||
| } | ||
104 changes: 104 additions & 0 deletions
104
pkg/operator/controller/canary-certificate/controller_test.go
This file contains hidden or 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,104 @@ | ||
| package canarycertificate | ||
|
|
||
| import ( | ||
| "testing" | ||
|
|
||
| corev1 "k8s.io/api/core/v1" | ||
| metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | ||
| ) | ||
|
|
||
| func Test_canaryCertificateChanged(t *testing.T) { | ||
| trueVar := true | ||
| testCases := []struct { | ||
| description string | ||
| mutate func(*corev1.Secret) | ||
| expect bool | ||
| }{ | ||
| { | ||
| description: "no change", | ||
| mutate: func(_ *corev1.Secret) {}, | ||
| expect: false, | ||
| }, | ||
| { | ||
| description: "new owner ref added", | ||
| mutate: func(certSecret *corev1.Secret) { | ||
| newOwnerRef := metav1.OwnerReference{ | ||
| APIVersion: "apps/v1", | ||
| Kind: "Service", | ||
| Name: "random-service", | ||
| UID: "bazquux", | ||
| Controller: &trueVar, | ||
| } | ||
| certSecret.OwnerReferences = append(certSecret.OwnerReferences, newOwnerRef) | ||
| }, | ||
| expect: true, | ||
| }, | ||
| { | ||
| description: "cert data changed", | ||
| mutate: func(certSecret *corev1.Secret) { | ||
| certSecret.Data = map[string][]byte{ | ||
| "foo": []byte("barbaz"), | ||
| } | ||
| }, | ||
| expect: true, | ||
| }, | ||
| { | ||
| description: "cert type changed", | ||
| mutate: func(certSecret *corev1.Secret) { | ||
| certSecret.Type = corev1.SecretTypeBasicAuth | ||
| }, | ||
| expect: true, | ||
| }, | ||
| { | ||
| description: "annotation added", | ||
| mutate: func(certSecret *corev1.Secret) { | ||
| if certSecret.Annotations == nil { | ||
| certSecret.Annotations = map[string]string{} | ||
| } | ||
| certSecret.Annotations["asdf"] = "asdfasdfasdf" | ||
| }, | ||
| expect: true, | ||
| }, | ||
| { | ||
| description: "label added", | ||
| mutate: func(certSecret *corev1.Secret) { | ||
| if certSecret.Labels == nil { | ||
| certSecret.Labels = map[string]string{} | ||
| } | ||
| certSecret.Labels["newlabel"] = "asdfasdf" | ||
| }, | ||
| expect: true, | ||
| }, | ||
| } | ||
| for _, tc := range testCases { | ||
| t.Run(tc.description, func(t *testing.T) { | ||
| defaultICSecret := &corev1.Secret{ | ||
| Data: map[string][]byte{ | ||
| "foo": []byte("bar"), | ||
| "baz": []byte("quux"), | ||
| }, | ||
| Type: corev1.SecretTypeOpaque, | ||
| } | ||
| ownerRef := metav1.OwnerReference{ | ||
| APIVersion: "apps/v1", | ||
| Kind: "DaemonSet", | ||
| Name: "ingress-canary", | ||
| UID: "foobar", | ||
| Controller: &trueVar, | ||
| } | ||
| original := desiredCanaryCertificate(ownerRef, defaultICSecret) | ||
| mutated := original.DeepCopy() | ||
| tc.mutate(mutated) | ||
| if changed, updated := canaryCertificateChanged(original, mutated); changed != tc.expect { | ||
| t.Errorf("expect canaryCertificateChanged to be %t, got %t", tc.expect, changed) | ||
| } else if changed { | ||
| if updatedChanged, _ := canaryCertificateChanged(original, updated); !updatedChanged { | ||
| t.Error("canaryCertificateChanged reported changes but did not make any update") | ||
| } | ||
| if changedAgain, _ := canaryCertificateChanged(mutated, updated); changedAgain { | ||
| t.Error("canaryCertificateChanged does not behave as a fixed point function") | ||
| } | ||
| } | ||
| }) | ||
| } | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.