This repository was archived by the owner on Sep 9, 2026. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 75
MGMT-23908: Public IP feedback controller #200
Merged
openshift-merge-bot
merged 1 commit into
osac-project:main
from
DakCrowder:public-ip-feedback-controller
Apr 29, 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
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,266 @@ | ||
| /* | ||
| Copyright (c) 2026 Red Hat Inc. | ||
|
|
||
| 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 controller | ||
|
|
||
| import ( | ||
| "context" | ||
| "errors" | ||
| "fmt" | ||
|
|
||
| "google.golang.org/grpc" | ||
| "google.golang.org/grpc/codes" | ||
| "google.golang.org/grpc/status" | ||
| ctrl "sigs.k8s.io/controller-runtime" | ||
| "sigs.k8s.io/controller-runtime/pkg/builder" | ||
| clnt "sigs.k8s.io/controller-runtime/pkg/client" | ||
| "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" | ||
| ctrllog "sigs.k8s.io/controller-runtime/pkg/log" | ||
| mcmanager "sigs.k8s.io/multicluster-runtime/pkg/manager" | ||
|
|
||
| "github.com/osac-project/osac-operator/api/v1alpha1" | ||
| privatev1 "github.com/osac-project/osac-operator/internal/api/osac/private/v1" | ||
| ) | ||
|
|
||
| var ErrPublicIPNotFound = errors.New("public IP not found in fulfillment service") | ||
|
|
||
| // PublicIPFeedbackReconciler sends updates to the fulfillment service. | ||
| type PublicIPFeedbackReconciler struct { | ||
| hubClient clnt.Client | ||
| publicIPsClient privatev1.PublicIPsClient | ||
| networkingNamespace string | ||
| } | ||
|
|
||
| type publicIPFeedbackReconcilerTask struct { | ||
| r *PublicIPFeedbackReconciler | ||
| object *v1alpha1.PublicIP | ||
| publicIP *privatev1.PublicIP | ||
| } | ||
|
|
||
| // NewPublicIPFeedbackReconciler creates a reconciler that sends to the fulfillment service updates about public IPs. | ||
| func NewPublicIPFeedbackReconciler(hubClient clnt.Client, grpcConn *grpc.ClientConn, networkingNamespace string) *PublicIPFeedbackReconciler { | ||
| return &PublicIPFeedbackReconciler{ | ||
| hubClient: hubClient, | ||
| publicIPsClient: privatev1.NewPublicIPsClient(grpcConn), | ||
| networkingNamespace: networkingNamespace, | ||
| } | ||
| } | ||
|
|
||
| // SetupWithManager adds the reconciler to the controller manager. | ||
| func (r *PublicIPFeedbackReconciler) SetupWithManager(mgr mcmanager.Manager) error { | ||
| localMgr := mgr.GetLocalManager() | ||
| if localMgr == nil { | ||
| return fmt.Errorf("local manager is nil") | ||
| } | ||
|
|
||
| return ctrl.NewControllerManagedBy(localMgr). | ||
| Named("publicip-feedback"). | ||
| For(&v1alpha1.PublicIP{}, builder.WithPredicates(NetworkingNamespacePredicate(r.networkingNamespace))). | ||
| Complete(r) | ||
| } | ||
|
|
||
| // Reconcile is the implementation of the reconciler interface. | ||
| func (r *PublicIPFeedbackReconciler) Reconcile(ctx context.Context, request ctrl.Request) (ctrl.Result, error) { | ||
| log := ctrllog.FromContext(ctx) | ||
|
|
||
| // Step 1: Fetch the object to reconcile, and do nothing if it no longer exists: | ||
| object := &v1alpha1.PublicIP{} | ||
| if err := r.hubClient.Get(ctx, request.NamespacedName, object); err != nil { | ||
| return ctrl.Result{}, clnt.IgnoreNotFound(err) | ||
| } | ||
|
|
||
| // Step 2: Get the identifier of the public IP from the labels. If this isn't present it means that the object | ||
| // wasn't created by the fulfillment service, so we ignore it. | ||
| publicIPID, ok := object.Labels[osacPublicIPIDLabel] | ||
| if !ok { | ||
| if !object.DeletionTimestamp.IsZero() && controllerutil.ContainsFinalizer(object, osacPublicIPFeedbackFinalizer) { | ||
| log.Info("CR without public IP ID label is being deleted, removing feedback finalizer") | ||
| if controllerutil.RemoveFinalizer(object, osacPublicIPFeedbackFinalizer) { | ||
| return ctrl.Result{}, r.hubClient.Update(ctx, object) | ||
| } | ||
| } | ||
| log.Info( | ||
| "There is no label containing the public IP identifier, will ignore it", | ||
| "label", osacPublicIPIDLabel, | ||
| ) | ||
| return ctrl.Result{}, nil | ||
| } | ||
|
|
||
| // Step 3: Fetch the public IP from the fulfillment service so we can compare before/after. | ||
| publicIP, err := r.fetchPublicIP(ctx, publicIPID) | ||
| if err != nil { | ||
| if !object.DeletionTimestamp.IsZero() && errors.Is(err, ErrPublicIPNotFound) { | ||
| log.Info("PublicIP record not found during deletion, removing feedback finalizer", "publicIPID", publicIPID) | ||
| if controllerutil.RemoveFinalizer(object, osacPublicIPFeedbackFinalizer) { | ||
| return ctrl.Result{}, r.hubClient.Update(ctx, object) | ||
| } | ||
| } | ||
| return ctrl.Result{}, err | ||
| } | ||
|
|
||
| // Create a task to do the rest of the job, but using copies of the objects, so that we can later compare the | ||
| // before and after values and save only the objects that have changed. | ||
| t := &publicIPFeedbackReconcilerTask{ | ||
| r: r, | ||
| object: object, | ||
| publicIP: clone(publicIP), | ||
| } | ||
|
|
||
| // Step 4: Sync CR state to the fulfillment service record. | ||
| if object.DeletionTimestamp.IsZero() { | ||
| if err := t.handleUpdate(ctx); err != nil { | ||
| return ctrl.Result{}, err | ||
| } | ||
| } else { | ||
| t.handleDelete() | ||
| } | ||
|
|
||
| // Step 5: Persist synced state to the fulfillment service. | ||
| if err := r.savePublicIP(ctx, publicIP, t.publicIP); err != nil { | ||
| return ctrl.Result{}, err | ||
| } | ||
|
|
||
| // Step 6: Handle finalizer removal and signal for deletions. | ||
| if !object.DeletionTimestamp.IsZero() && controllerutil.ContainsFinalizer(object, osacPublicIPFeedbackFinalizer) { | ||
| if len(object.GetFinalizers()) == 1 { | ||
| log.Info( | ||
| "Feedback finalizer is last remaining, removing finalizer and signaling", | ||
| "publicIPID", publicIPID, | ||
| ) | ||
| if controllerutil.RemoveFinalizer(object, osacPublicIPFeedbackFinalizer) { | ||
| if err := r.hubClient.Update(ctx, object); err != nil { | ||
| return ctrl.Result{}, err | ||
| } | ||
| } | ||
| _, signalErr := r.publicIPsClient.Signal(ctx, privatev1.PublicIPsSignalRequest_builder{ | ||
| Id: publicIPID, | ||
| }.Build()) | ||
| if signalErr != nil { | ||
| log.Error( | ||
| signalErr, | ||
| "Failed to signal fulfillment service, periodic sync will handle cleanup", | ||
| "publicIPID", publicIPID, | ||
| ) | ||
| } | ||
| } else { | ||
| log.Info( | ||
| "Other finalizers still present, waiting", | ||
| "finalizers", object.GetFinalizers(), | ||
| ) | ||
| } | ||
| } | ||
|
|
||
| return ctrl.Result{}, nil | ||
| } | ||
|
|
||
| func (r *PublicIPFeedbackReconciler) fetchPublicIP(ctx context.Context, id string) (*privatev1.PublicIP, error) { | ||
| response, err := r.publicIPsClient.Get(ctx, privatev1.PublicIPsGetRequest_builder{ | ||
| Id: id, | ||
| }.Build()) | ||
| if err != nil { | ||
| if status.Code(err) == codes.NotFound { | ||
| return nil, fmt.Errorf("%w: %w", ErrPublicIPNotFound, err) | ||
| } | ||
| return nil, err | ||
| } | ||
| publicIP := response.GetObject() | ||
| if publicIP == nil { | ||
| return nil, fmt.Errorf("%w: response contained nil object", ErrPublicIPNotFound) | ||
| } | ||
| if !publicIP.HasSpec() { | ||
| publicIP.SetSpec(&privatev1.PublicIPSpec{}) | ||
| } | ||
| if !publicIP.HasStatus() { | ||
| publicIP.SetStatus(&privatev1.PublicIPStatus{}) | ||
| } | ||
| return publicIP, nil | ||
| } | ||
|
|
||
| func (r *PublicIPFeedbackReconciler) savePublicIP(ctx context.Context, before, after *privatev1.PublicIP) error { | ||
| log := ctrllog.FromContext(ctx) | ||
|
|
||
| if !equal(after, before) { | ||
| log.Info( | ||
| "Updating public IP", | ||
| "before", before, | ||
| "after", after, | ||
| ) | ||
| _, err := r.publicIPsClient.Update(ctx, privatev1.PublicIPsUpdateRequest_builder{ | ||
| Object: after, | ||
| }.Build()) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| } | ||
| return nil | ||
| } | ||
|
|
||
| func (t *publicIPFeedbackReconcilerTask) handleUpdate(ctx context.Context) error { | ||
| if controllerutil.AddFinalizer(t.object, osacPublicIPFeedbackFinalizer) { | ||
| if err := t.r.hubClient.Update(ctx, t.object); err != nil { | ||
| return err | ||
| } | ||
| } | ||
| t.syncPhase(ctx) | ||
| t.syncAddress() | ||
| return nil | ||
| } | ||
|
|
||
| func (t *publicIPFeedbackReconcilerTask) handleDelete() { | ||
| if t.object.Status.Phase == v1alpha1.PublicIPPhaseFailed { | ||
| t.publicIP.GetStatus().SetState(privatev1.PublicIPState_PUBLIC_IP_STATE_FAILED) | ||
| return | ||
| } | ||
| t.publicIP.GetStatus().SetState(privatev1.PublicIPState_PUBLIC_IP_STATE_RELEASING) | ||
| } | ||
|
|
||
| func (t *publicIPFeedbackReconcilerTask) syncPhase(ctx context.Context) { | ||
| switch t.object.Status.Phase { | ||
| case v1alpha1.PublicIPPhaseProgressing: | ||
| t.syncPhaseProgressing() | ||
| case v1alpha1.PublicIPPhaseFailed: | ||
| t.syncPhaseFailed() | ||
| case v1alpha1.PublicIPPhaseReady: | ||
| t.syncPhaseReady() | ||
| case v1alpha1.PublicIPPhaseDeleting: | ||
| t.syncPhaseDeleting() | ||
| default: | ||
| log := ctrllog.FromContext(ctx) | ||
| log.Info( | ||
| "Unknown phase, will ignore it", | ||
| "phase", t.object.Status.Phase, | ||
| ) | ||
| } | ||
| } | ||
|
|
||
| func (t *publicIPFeedbackReconcilerTask) syncPhaseProgressing() { | ||
| t.publicIP.GetStatus().SetState(privatev1.PublicIPState_PUBLIC_IP_STATE_PENDING) | ||
| } | ||
|
|
||
| func (t *publicIPFeedbackReconcilerTask) syncPhaseFailed() { | ||
| t.publicIP.GetStatus().SetState(privatev1.PublicIPState_PUBLIC_IP_STATE_FAILED) | ||
| } | ||
|
|
||
| func (t *publicIPFeedbackReconcilerTask) syncPhaseReady() { | ||
| t.publicIP.GetStatus().SetState(privatev1.PublicIPState_PUBLIC_IP_STATE_ALLOCATED) | ||
| } | ||
|
|
||
| func (t *publicIPFeedbackReconcilerTask) syncPhaseDeleting() { | ||
| t.publicIP.GetStatus().SetState(privatev1.PublicIPState_PUBLIC_IP_STATE_RELEASING) | ||
| } | ||
|
|
||
| func (t *publicIPFeedbackReconcilerTask) syncAddress() { | ||
| if t.object.Status.Address != "" { | ||
| t.publicIP.GetStatus().SetAddress(t.object.Status.Address) | ||
| } | ||
| } |
Oops, something went wrong.
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.