From f8e63bae3fc4807284cc2863ff16978ec673782a Mon Sep 17 00:00:00 2001 From: Joseph Date: Mon, 20 Oct 2025 09:54:09 -0400 Subject: [PATCH 01/10] Add ConsoleCLIDownload for oadp-cli Signed-off-by: Joseph --- bundle/manifests/oadp-operator.oadp-cli.yaml | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 bundle/manifests/oadp-operator.oadp-cli.yaml diff --git a/bundle/manifests/oadp-operator.oadp-cli.yaml b/bundle/manifests/oadp-operator.oadp-cli.yaml new file mode 100644 index 0000000000..85faedfc6e --- /dev/null +++ b/bundle/manifests/oadp-operator.oadp-cli.yaml @@ -0,0 +1,10 @@ +apiVersion: console.openshift.io/v1 +kind: ConsoleCLIDownload +metadata: + name: oadp-cli +spec: + description: oadp-cli is the command line tool for working with the OADP operator + displayName: oadp - OADP operator Command Line Interface (CLI) + links: + - href: https://github.com/migtools/oadp-cli/releases/ + text: Download oadp \ No newline at end of file From 74f07b630e075b3de97db9056dc42b73f3a7bb4b Mon Sep 17 00:00:00 2001 From: Joseph Date: Thu, 23 Oct 2025 23:39:16 -0400 Subject: [PATCH 02/10] Setup oadp-cli binary server on operator startup - First pass, route and ConsoleCLIDownload will be moved into a controller Signed-off-by: Joseph --- .../oadp-operator.clusterserviceversion.yaml | 46 ++++++ bundle/manifests/oadp-operator.oadp-cli.yaml | 10 -- .../openshift-adp-cli-server_v1_service.yaml | 16 ++ cmd/main.go | 140 ++++++++++++++++++ config/default/kustomization.yaml | 1 + config/oadp-cli/binary-deployment.yaml | 37 +++++ config/oadp-cli/kustomization.yaml | 5 + config/oadp-cli/service.yaml | 14 ++ config/rbac/role.yaml | 12 ++ .../dataprotectionapplication_controller.go | 1 + 10 files changed, 272 insertions(+), 10 deletions(-) delete mode 100644 bundle/manifests/oadp-operator.oadp-cli.yaml create mode 100644 bundle/manifests/openshift-adp-cli-server_v1_service.yaml create mode 100644 config/oadp-cli/binary-deployment.yaml create mode 100644 config/oadp-cli/kustomization.yaml create mode 100644 config/oadp-cli/service.yaml diff --git a/bundle/manifests/oadp-operator.clusterserviceversion.yaml b/bundle/manifests/oadp-operator.clusterserviceversion.yaml index cf45ed4573..1711358077 100644 --- a/bundle/manifests/oadp-operator.clusterserviceversion.yaml +++ b/bundle/manifests/oadp-operator.clusterserviceversion.yaml @@ -1045,6 +1045,18 @@ spec: - get - list - watch + - apiGroups: + - console.openshift.io + resources: + - consoleclidownloads + verbs: + - create + - delete + - get + - list + - patch + - update + - watch - apiGroups: - coordination.k8s.io - corev1 @@ -1347,6 +1359,40 @@ spec: path: token - emptyDir: {} name: tmp-dir + - label: + app: oadp-cli + name: openshift-adp-oadp-cli-server + spec: + replicas: 1 + selector: + matchLabels: + app: oadp-cli + strategy: {} + template: + metadata: + labels: + app: oadp-cli + spec: + containers: + - image: quay.io/rh_ee_jvaikath/oadp-cli-binaries + name: oadp-cli-server + resources: + limits: + cpu: 100m + memory: 64Mi + requests: + cpu: 50m + memory: 32Mi + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + readOnlyRootFilesystem: true + securityContext: + runAsNonRoot: true + serviceAccountName: openshift-adp-controller-manager + terminationGracePeriodSeconds: 10 permissions: - rules: - apiGroups: diff --git a/bundle/manifests/oadp-operator.oadp-cli.yaml b/bundle/manifests/oadp-operator.oadp-cli.yaml deleted file mode 100644 index 85faedfc6e..0000000000 --- a/bundle/manifests/oadp-operator.oadp-cli.yaml +++ /dev/null @@ -1,10 +0,0 @@ -apiVersion: console.openshift.io/v1 -kind: ConsoleCLIDownload -metadata: - name: oadp-cli -spec: - description: oadp-cli is the command line tool for working with the OADP operator - displayName: oadp - OADP operator Command Line Interface (CLI) - links: - - href: https://github.com/migtools/oadp-cli/releases/ - text: Download oadp \ No newline at end of file diff --git a/bundle/manifests/openshift-adp-cli-server_v1_service.yaml b/bundle/manifests/openshift-adp-cli-server_v1_service.yaml new file mode 100644 index 0000000000..acf8f20787 --- /dev/null +++ b/bundle/manifests/openshift-adp-cli-server_v1_service.yaml @@ -0,0 +1,16 @@ +apiVersion: v1 +kind: Service +metadata: + creationTimestamp: null + name: openshift-adp-cli-server +spec: + ports: + - name: http + port: 80 + protocol: TCP + targetPort: 8080 + selector: + app: oadp-cli + type: ClusterIP +status: + loadBalancer: {} diff --git a/cmd/main.go b/cmd/main.go index 8965fdb57b..83ec1e14e2 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -23,22 +23,28 @@ import ( "flag" "fmt" "os" + "time" + "github.com/go-logr/logr" snapshotv1api "github.com/kubernetes-csi/external-snapshotter/client/v6/apis/volumesnapshot/v1" configv1 "github.com/openshift/api/config/v1" + consolev1 "github.com/openshift/api/console/v1" routev1 "github.com/openshift/api/route/v1" security "github.com/openshift/api/security/v1" monitor "github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring/v1" velerov1 "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" appsv1 "k8s.io/api/apps/v1" apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/intstr" utilruntime "k8s.io/apimachinery/pkg/util/runtime" "k8s.io/client-go/discovery" "k8s.io/client-go/kubernetes" clientgoscheme "k8s.io/client-go/kubernetes/scheme" + // Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.) // to ensure that exec-entrypoint and run can make use of them. _ "k8s.io/client-go/plugin/pkg/client/auth" @@ -54,6 +60,7 @@ import ( oadpv1alpha1 "github.com/openshift/oadp-operator/api/v1alpha1" "github.com/openshift/oadp-operator/internal/controller" pkgclient "github.com/openshift/oadp-operator/pkg/client" + //+kubebuilder:scaffold:imports "github.com/openshift/oadp-operator/pkg/credentials/stsflow" "github.com/openshift/oadp-operator/pkg/leaderelection" @@ -86,6 +93,32 @@ func init() { //+kubebuilder:scaffold:scheme } +// oadpCLISetupRunnable implements manager.Runnable to set up OADP CLI downloads after cache starts +type oadpCLISetupRunnable struct { + client client.Client + namespace string + log logr.Logger +} + +func (r *oadpCLISetupRunnable) Start(ctx context.Context) error { + // Run setup in a goroutine and keep the runnable alive + go func() { + r.log.Info("setting up OADP CLI download resources") + if err := setupOADPCLIDownload(ctx, r.client, r.namespace); err != nil { + r.log.Error(err, "unable to setup OADP CLI download, continuing anyway") + } + }() + + // Block until context is cancelled (manager shutdown) + <-ctx.Done() + return nil +} + +// NeedLeaderElection returns false so this runs even if not the leader +func (r *oadpCLISetupRunnable) NeedLeaderElection() bool { + return true +} + func main() { var metricsAddr string var enableLeaderElection bool @@ -200,6 +233,11 @@ func main() { os.Exit(1) } + if err := consolev1.AddToScheme(mgr.GetScheme()); err != nil { + setupLog.Error(err, "unable to add OpenShift console API to scheme") + os.Exit(1) + } + if err := velerov1.AddToScheme(mgr.GetScheme()); err != nil { setupLog.Error(err, "unable to add Velero APIs to scheme") os.Exit(1) @@ -275,6 +313,16 @@ func main() { os.Exit(1) } + // Add OADP CLI download setup as a manager runnable + if err := mgr.Add(&oadpCLISetupRunnable{ + client: mgr.GetClient(), + namespace: watchNamespace, + log: setupLog, + }); err != nil { + setupLog.Error(err, "unable to add OADP CLI setup runnable") + os.Exit(1) + } + setupLog.Info("starting manager") if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil { setupLog.Error(err, "problem running manager") @@ -348,3 +396,95 @@ func DoesCRDExist(CRDGroupVersion, CRDName string, kubeconf *rest.Config) (bool, } return discoveryResult, nil } + +func setupOADPCLIDownload(ctx context.Context, c client.Client, namespace string) error { + // Create Route, wait for hostname, create ConsoleCLIDownload + // Implementation goes here + route := &routev1.Route{ + ObjectMeta: metav1.ObjectMeta{ + Name: "openshift-adp-cli-server-route", + Namespace: namespace, + }, + Spec: routev1.RouteSpec{ + To: routev1.RouteTargetReference{ + Kind: "Service", + Name: "openshift-adp-cli-server", + }, + Port: &routev1.RoutePort{ + TargetPort: intstr.FromString("http"), + }, + TLS: &routev1.TLSConfig{ + Termination: routev1.TLSTerminationEdge, + InsecureEdgeTerminationPolicy: routev1.InsecureEdgeTerminationPolicyRedirect, + }, + }, + } + err := c.Create(ctx, route) + if err != nil && !errors.IsAlreadyExists(err) { + return err + } + if errors.IsAlreadyExists(err) { + // Route already exists, just get it + err = c.Get(ctx, client.ObjectKey{ + Name: "openshift-adp-cli-server-route", + Namespace: namespace, + }, route) + if err != nil { + return fmt.Errorf("failed to get existing route: %w", err) + } + } + + hostname := "" + // 2. Get hostname from Status + for i := 0; i < 3; i++ { + err = c.Get(ctx, client.ObjectKey{ + Name: "openshift-adp-cli-server-route", + Namespace: namespace, + }, route) + + if err != nil { + return fmt.Errorf("failed to get route: %w", err) + } + + // Check if hostname is assigned + if len(route.Status.Ingress) > 0 && route.Status.Ingress[0].Host != "" { + hostname = route.Status.Ingress[0].Host + break + } + + // Backoff: wait 2^i seconds (1s, 2s, 4s) + if i < 2 { + setupLog.Info("route hostname not ready, retrying...", "attempt", i+1) + time.Sleep(time.Duration(1< Date: Fri, 24 Oct 2025 16:11:33 -0400 Subject: [PATCH 03/10] Use controller to startup route and ConsoleCLIDownload Better than piggybacking off leader election, if leader changes it will repeatcontroller can better ensure route changes and deployment changes don't affect the end goal of having ConsoleCLIDownload with the right link Signed-off-by: Joseph --- cmd/main.go | 140 +---------------- internal/controller/oadp_cli_controller.go | 172 +++++++++++++++++++++ 2 files changed, 180 insertions(+), 132 deletions(-) create mode 100644 internal/controller/oadp_cli_controller.go diff --git a/cmd/main.go b/cmd/main.go index 83ec1e14e2..397b19eb72 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -23,9 +23,7 @@ import ( "flag" "fmt" "os" - "time" - "github.com/go-logr/logr" snapshotv1api "github.com/kubernetes-csi/external-snapshotter/client/v6/apis/volumesnapshot/v1" configv1 "github.com/openshift/api/config/v1" consolev1 "github.com/openshift/api/console/v1" @@ -35,11 +33,9 @@ import ( velerov1 "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" appsv1 "k8s.io/api/apps/v1" apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" - "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" - "k8s.io/apimachinery/pkg/util/intstr" utilruntime "k8s.io/apimachinery/pkg/util/runtime" "k8s.io/client-go/discovery" "k8s.io/client-go/kubernetes" @@ -93,32 +89,6 @@ func init() { //+kubebuilder:scaffold:scheme } -// oadpCLISetupRunnable implements manager.Runnable to set up OADP CLI downloads after cache starts -type oadpCLISetupRunnable struct { - client client.Client - namespace string - log logr.Logger -} - -func (r *oadpCLISetupRunnable) Start(ctx context.Context) error { - // Run setup in a goroutine and keep the runnable alive - go func() { - r.log.Info("setting up OADP CLI download resources") - if err := setupOADPCLIDownload(ctx, r.client, r.namespace); err != nil { - r.log.Error(err, "unable to setup OADP CLI download, continuing anyway") - } - }() - - // Block until context is cancelled (manager shutdown) - <-ctx.Done() - return nil -} - -// NeedLeaderElection returns false so this runs even if not the leader -func (r *oadpCLISetupRunnable) NeedLeaderElection() bool { - return true -} - func main() { var metricsAddr string var enableLeaderElection bool @@ -302,6 +272,14 @@ func main() { setupLog.Error(err, "unable to create controller", "controller", "DataProtectionTest") os.Exit(1) } + if err = (&controller.OADPCLIReconciler{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + Namespace: watchNamespace, + }).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "OADPCLI") + os.Exit(1) + } //+kubebuilder:scaffold:builder if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil { @@ -313,16 +291,6 @@ func main() { os.Exit(1) } - // Add OADP CLI download setup as a manager runnable - if err := mgr.Add(&oadpCLISetupRunnable{ - client: mgr.GetClient(), - namespace: watchNamespace, - log: setupLog, - }); err != nil { - setupLog.Error(err, "unable to add OADP CLI setup runnable") - os.Exit(1) - } - setupLog.Info("starting manager") if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil { setupLog.Error(err, "problem running manager") @@ -396,95 +364,3 @@ func DoesCRDExist(CRDGroupVersion, CRDName string, kubeconf *rest.Config) (bool, } return discoveryResult, nil } - -func setupOADPCLIDownload(ctx context.Context, c client.Client, namespace string) error { - // Create Route, wait for hostname, create ConsoleCLIDownload - // Implementation goes here - route := &routev1.Route{ - ObjectMeta: metav1.ObjectMeta{ - Name: "openshift-adp-cli-server-route", - Namespace: namespace, - }, - Spec: routev1.RouteSpec{ - To: routev1.RouteTargetReference{ - Kind: "Service", - Name: "openshift-adp-cli-server", - }, - Port: &routev1.RoutePort{ - TargetPort: intstr.FromString("http"), - }, - TLS: &routev1.TLSConfig{ - Termination: routev1.TLSTerminationEdge, - InsecureEdgeTerminationPolicy: routev1.InsecureEdgeTerminationPolicyRedirect, - }, - }, - } - err := c.Create(ctx, route) - if err != nil && !errors.IsAlreadyExists(err) { - return err - } - if errors.IsAlreadyExists(err) { - // Route already exists, just get it - err = c.Get(ctx, client.ObjectKey{ - Name: "openshift-adp-cli-server-route", - Namespace: namespace, - }, route) - if err != nil { - return fmt.Errorf("failed to get existing route: %w", err) - } - } - - hostname := "" - // 2. Get hostname from Status - for i := 0; i < 3; i++ { - err = c.Get(ctx, client.ObjectKey{ - Name: "openshift-adp-cli-server-route", - Namespace: namespace, - }, route) - - if err != nil { - return fmt.Errorf("failed to get route: %w", err) - } - - // Check if hostname is assigned - if len(route.Status.Ingress) > 0 && route.Status.Ingress[0].Host != "" { - hostname = route.Status.Ingress[0].Host - break - } - - // Backoff: wait 2^i seconds (1s, 2s, 4s) - if i < 2 { - setupLog.Info("route hostname not ready, retrying...", "attempt", i+1) - time.Sleep(time.Duration(1< Date: Mon, 27 Oct 2025 06:06:26 -0400 Subject: [PATCH 04/10] Fix download link text Signed-off-by: Joseph --- internal/controller/oadp_cli_controller.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/controller/oadp_cli_controller.go b/internal/controller/oadp_cli_controller.go index e4d9842a3e..d2dafbf8c5 100644 --- a/internal/controller/oadp_cli_controller.go +++ b/internal/controller/oadp_cli_controller.go @@ -114,7 +114,7 @@ func (r *OADPCLIReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct Links: []consolev1.CLIDownloadLink{ { Href: downloadURL, - Text: "Download OADP CLI for Linux x86_64", + Text: "Download OADP CLI", }, }, } From 037ad6dd9903540ba017a8097d9eb61b5386e04d Mon Sep 17 00:00:00 2001 From: Joseph Date: Mon, 27 Oct 2025 06:23:31 -0400 Subject: [PATCH 05/10] Linting Signed-off-by: Joseph --- cmd/main.go | 2 -- internal/controller/oadp_cli_controller.go | 10 +++++----- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/cmd/main.go b/cmd/main.go index 397b19eb72..f53da6e6ac 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -40,7 +40,6 @@ import ( "k8s.io/client-go/discovery" "k8s.io/client-go/kubernetes" clientgoscheme "k8s.io/client-go/kubernetes/scheme" - // Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.) // to ensure that exec-entrypoint and run can make use of them. _ "k8s.io/client-go/plugin/pkg/client/auth" @@ -56,7 +55,6 @@ import ( oadpv1alpha1 "github.com/openshift/oadp-operator/api/v1alpha1" "github.com/openshift/oadp-operator/internal/controller" pkgclient "github.com/openshift/oadp-operator/pkg/client" - //+kubebuilder:scaffold:imports "github.com/openshift/oadp-operator/pkg/credentials/stsflow" "github.com/openshift/oadp-operator/pkg/leaderelection" diff --git a/internal/controller/oadp_cli_controller.go b/internal/controller/oadp_cli_controller.go index d2dafbf8c5..9a4685d3a2 100644 --- a/internal/controller/oadp_cli_controller.go +++ b/internal/controller/oadp_cli_controller.go @@ -36,8 +36,8 @@ const ( ) func (r *OADPCLIReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { - log := log.FromContext(ctx) - log.Info("Reconciling OADP CLI download resources", "triggered_by", req.NamespacedName) + logger := log.FromContext(ctx) + logger.Info("Reconciling OADP CLI download resources", "triggered_by", req.NamespacedName) // 1. Check if CLI server deployment exists deployment := &appsv1.Deployment{} @@ -47,7 +47,7 @@ func (r *OADPCLIReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct }, deployment) if errors.IsNotFound(err) { - log.V(1).Info("CLI server deployment not found, nothing to reconcile") + logger.V(1).Info("CLI server deployment not found, nothing to reconcile") return ctrl.Result{}, nil } if err != nil { @@ -131,7 +131,7 @@ func (r *OADPCLIReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct if err != nil && !errors.IsAlreadyExists(err) { return ctrl.Result{}, fmt.Errorf("failed to create ConsoleCLIDownload: %w", err) } - log.Info("Created ConsoleCLIDownload", "url", downloadURL) + logger.Info("Created ConsoleCLIDownload", "url", downloadURL) } else if err != nil { return ctrl.Result{}, fmt.Errorf("failed to get ConsoleCLIDownload: %w", err) } else { @@ -142,7 +142,7 @@ func (r *OADPCLIReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct if err != nil { return ctrl.Result{}, fmt.Errorf("failed to update ConsoleCLIDownload: %w", err) } - log.Info("Updated ConsoleCLIDownload with new URL", "url", downloadURL) + logger.Info("Updated ConsoleCLIDownload with new URL", "url", downloadURL) } } From a3188cc8792854f584eb84a0c68ef6f41150d2ca Mon Sep 17 00:00:00 2001 From: Joseph Date: Mon, 27 Oct 2025 14:02:17 -0400 Subject: [PATCH 06/10] Use konveyor org Signed-off-by: Joseph --- bundle/manifests/oadp-operator.clusterserviceversion.yaml | 2 +- config/oadp-cli/binary-deployment.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/bundle/manifests/oadp-operator.clusterserviceversion.yaml b/bundle/manifests/oadp-operator.clusterserviceversion.yaml index 1711358077..83f6be3d10 100644 --- a/bundle/manifests/oadp-operator.clusterserviceversion.yaml +++ b/bundle/manifests/oadp-operator.clusterserviceversion.yaml @@ -1374,7 +1374,7 @@ spec: app: oadp-cli spec: containers: - - image: quay.io/rh_ee_jvaikath/oadp-cli-binaries + - image: quay.io/konveyor/oadp-cli-binaries name: oadp-cli-server resources: limits: diff --git a/config/oadp-cli/binary-deployment.yaml b/config/oadp-cli/binary-deployment.yaml index eb04804aa6..47c400e878 100644 --- a/config/oadp-cli/binary-deployment.yaml +++ b/config/oadp-cli/binary-deployment.yaml @@ -20,7 +20,7 @@ spec: runAsNonRoot: true containers: - name: oadp-cli-server - image: quay.io/rh_ee_jvaikath/oadp-cli-binaries + image: quay.io/konveyor/oadp-cli-binaries resources: limits: cpu: 100m From 525d269406facfc0f5256a564dfc72ad09b41052 Mon Sep 17 00:00:00 2001 From: Joseph Date: Thu, 30 Oct 2025 14:02:35 -0400 Subject: [PATCH 07/10] Start oadp-cli on server startup Signed-off-by: Joseph --- .../oadp-operator.clusterserviceversion.yaml | 38 +- .../openshift-adp-cli-server_v1_service.yaml | 16 - cmd/main.go | 17 +- config/default/kustomization.yaml | 1 - config/manager/manager.yaml | 2 + config/oadp-cli/binary-deployment.yaml | 37 -- config/oadp-cli/kustomization.yaml | 5 - config/oadp-cli/service.yaml | 14 - .../controller/cli_download_controller.go | 366 ++++++++++++++++++ internal/controller/oadp_cli_controller.go | 172 -------- 10 files changed, 382 insertions(+), 286 deletions(-) delete mode 100644 bundle/manifests/openshift-adp-cli-server_v1_service.yaml delete mode 100644 config/oadp-cli/binary-deployment.yaml delete mode 100644 config/oadp-cli/kustomization.yaml delete mode 100644 config/oadp-cli/service.yaml create mode 100644 internal/controller/cli_download_controller.go delete mode 100644 internal/controller/oadp_cli_controller.go diff --git a/bundle/manifests/oadp-operator.clusterserviceversion.yaml b/bundle/manifests/oadp-operator.clusterserviceversion.yaml index 83f6be3d10..b43ef47bfc 100644 --- a/bundle/manifests/oadp-operator.clusterserviceversion.yaml +++ b/bundle/manifests/oadp-operator.clusterserviceversion.yaml @@ -1304,6 +1304,8 @@ spec: value: quay.io/konveyor/oadp-vmfr-access-sshd:latest - name: RELATED_IMAGE_VM_FILE_RESTORE_BROWSER value: quay.io/konveyor/oadp-vmfr-access-filebrowser:latest + - name: RELATED_IMAGE_CONSOLE_CLI_DOWNLOAD + value: quay.io/konveyor/console-cli-download:latest image: quay.io/konveyor/oadp-operator:latest imagePullPolicy: Always livenessProbe: @@ -1359,40 +1361,6 @@ spec: path: token - emptyDir: {} name: tmp-dir - - label: - app: oadp-cli - name: openshift-adp-oadp-cli-server - spec: - replicas: 1 - selector: - matchLabels: - app: oadp-cli - strategy: {} - template: - metadata: - labels: - app: oadp-cli - spec: - containers: - - image: quay.io/konveyor/oadp-cli-binaries - name: oadp-cli-server - resources: - limits: - cpu: 100m - memory: 64Mi - requests: - cpu: 50m - memory: 32Mi - securityContext: - allowPrivilegeEscalation: false - capabilities: - drop: - - ALL - readOnlyRootFilesystem: true - securityContext: - runAsNonRoot: true - serviceAccountName: openshift-adp-controller-manager - terminationGracePeriodSeconds: 10 permissions: - rules: - apiGroups: @@ -1508,4 +1476,6 @@ spec: name: vm-file-restore-ssh - image: quay.io/konveyor/oadp-vmfr-access-filebrowser:latest name: vm-file-restore-browser + - image: quay.io/konveyor/console-cli-download:latest + name: console-cli-download version: 99.0.0 diff --git a/bundle/manifests/openshift-adp-cli-server_v1_service.yaml b/bundle/manifests/openshift-adp-cli-server_v1_service.yaml deleted file mode 100644 index acf8f20787..0000000000 --- a/bundle/manifests/openshift-adp-cli-server_v1_service.yaml +++ /dev/null @@ -1,16 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - creationTimestamp: null - name: openshift-adp-cli-server -spec: - ports: - - name: http - port: 80 - protocol: TCP - targetPort: 8080 - selector: - app: oadp-cli - type: ClusterIP -status: - loadBalancer: {} diff --git a/cmd/main.go b/cmd/main.go index f53da6e6ac..078ac653ec 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -270,15 +270,18 @@ func main() { setupLog.Error(err, "unable to create controller", "controller", "DataProtectionTest") os.Exit(1) } - if err = (&controller.OADPCLIReconciler{ - Client: mgr.GetClient(), - Scheme: mgr.GetScheme(), - Namespace: watchNamespace, - }).SetupWithManager(mgr); err != nil { - setupLog.Error(err, "unable to create controller", "controller", "OADPCLI") + //+kubebuilder:scaffold:builder + + // Add CLI download setup runnable + if err := mgr.Add(&controller.CLIDownloadSetup{ + Client: mgr.GetClient(), + Namespace: watchNamespace, + OperatorName: "openshift-adp-controller-manager", + OperatorNamespace: watchNamespace, + }); err != nil { + setupLog.Error(err, "unable to add CLI download setup") os.Exit(1) } - //+kubebuilder:scaffold:builder if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil { setupLog.Error(err, "unable to set up health check") diff --git a/config/default/kustomization.yaml b/config/default/kustomization.yaml index a56c4d753a..997b5592c6 100644 --- a/config/default/kustomization.yaml +++ b/config/default/kustomization.yaml @@ -18,7 +18,6 @@ resources: - ../crd - ../rbac - ../manager -- ../oadp-cli # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in # crd/kustomization.yaml #- ../webhook diff --git a/config/manager/manager.yaml b/config/manager/manager.yaml index 866b4464c0..5cf38eb430 100644 --- a/config/manager/manager.yaml +++ b/config/manager/manager.yaml @@ -96,6 +96,8 @@ spec: value: quay.io/konveyor/oadp-vmfr-access-sshd:latest - name: RELATED_IMAGE_VM_FILE_RESTORE_BROWSER value: quay.io/konveyor/oadp-vmfr-access-filebrowser:latest + - name: RELATED_IMAGE_CONSOLE_CLI_DOWNLOAD + value: quay.io/konveyor/oadp-cli-binaries:latest args: - --leader-elect image: controller:latest diff --git a/config/oadp-cli/binary-deployment.yaml b/config/oadp-cli/binary-deployment.yaml deleted file mode 100644 index 47c400e878..0000000000 --- a/config/oadp-cli/binary-deployment.yaml +++ /dev/null @@ -1,37 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: oadp-cli-server - namespace: system - labels: - app: oadp-cli -spec: - replicas: 1 - selector: - matchLabels: - app: oadp-cli - template: - metadata: - labels: - app: oadp-cli - spec: - serviceAccountName: controller-manager - securityContext: - runAsNonRoot: true - containers: - - name: oadp-cli-server - image: quay.io/konveyor/oadp-cli-binaries - resources: - limits: - cpu: 100m - memory: 64Mi - requests: - cpu: 50m - memory: 32Mi - securityContext: - allowPrivilegeEscalation: false - capabilities: - drop: - - ALL - readOnlyRootFilesystem: true - terminationGracePeriodSeconds: 10 \ No newline at end of file diff --git a/config/oadp-cli/kustomization.yaml b/config/oadp-cli/kustomization.yaml deleted file mode 100644 index 913bb24396..0000000000 --- a/config/oadp-cli/kustomization.yaml +++ /dev/null @@ -1,5 +0,0 @@ -apiVersion: kustomize.config.k8s.io/v1beta1 -kind: Kustomization -resources: -- binary-deployment.yaml -- service.yaml diff --git a/config/oadp-cli/service.yaml b/config/oadp-cli/service.yaml deleted file mode 100644 index c2a36fa83b..0000000000 --- a/config/oadp-cli/service.yaml +++ /dev/null @@ -1,14 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - name: cli-server - namespace: system -spec: - selector: - app: oadp-cli - ports: - - name: http - port: 80 # Standard HTTP port - targetPort: 8080 # But your container still listens on 8080 - protocol: TCP - type: ClusterIP \ No newline at end of file diff --git a/internal/controller/cli_download_controller.go b/internal/controller/cli_download_controller.go new file mode 100644 index 0000000000..08debb6ac9 --- /dev/null +++ b/internal/controller/cli_download_controller.go @@ -0,0 +1,366 @@ +package controller + +import ( + "context" + "fmt" + "os" + "time" + + "github.com/go-logr/logr" + consolev1 "github.com/openshift/api/console/v1" + routev1 "github.com/openshift/api/route/v1" + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/intstr" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" +) + +const ( + cliServerDeploymentName = "openshift-adp-oadp-cli-server" + cliServerServiceName = "openshift-adp-cli-server" + cliServerRouteName = "oadp-cli-server-route" + cliDownloadName = "openshift-adp-oadp-cli" + managedByLabel = "app.kubernetes.io/managed-by" + operatorName = "oadp-operator" +) + +// CLIDownloadSetup is a runnable that sets up CLI download resources when the operator starts +type CLIDownloadSetup struct { + Client client.Client + Namespace string + OperatorName string + OperatorNamespace string + Log logr.Logger +} + +// Start implements the Runnable interface +func (c *CLIDownloadSetup) Start(ctx context.Context) error { + c.Log = ctrl.Log.WithName("cli-download-setup") + c.Log.Info("Starting CLI download setup") + + // Get the CLI server image from environment variable + cliServerImage := os.Getenv("RELATED_IMAGE_CONSOLE_CLI_DOWNLOAD") + if cliServerImage == "" { + cliServerImage = "quay.io/konveyor/oadp-cli-binaries" // fallback default + c.Log.Info("Using default CLI server image", "image", cliServerImage) + } + + // Get the operator deployment to use as owner for namespaced resources + operatorDeployment := &appsv1.Deployment{} + err := c.Client.Get(ctx, types.NamespacedName{ + Name: c.OperatorName, + Namespace: c.OperatorNamespace, + }, operatorDeployment) + if err != nil { + c.Log.Error(err, "Failed to get operator deployment") + return err + } + + // Create CLI resources (idempotent - will reuse existing ConsoleCLIDownload if present) + if err := c.reconcileCLIResources(ctx, operatorDeployment, cliServerImage); err != nil { + return err + } + + c.Log.Info("CLI download setup completed successfully") + return nil +} + +// reconcileCLIResources creates or updates all CLI-related resources +// This is idempotent - it will reuse existing resources if they already exist +func (c *CLIDownloadSetup) reconcileCLIResources(ctx context.Context, operatorDeployment *appsv1.Deployment, cliServerImage string) error { + // 1. Create or update the deployment + deployment := &appsv1.Deployment{} + err := c.Client.Get(ctx, client.ObjectKey{ + Name: cliServerDeploymentName, + Namespace: c.Namespace, + }, deployment) + + if errors.IsNotFound(err) { + deployment = buildCLIServerDeployment(c.Namespace, cliServerImage) + // Set operator deployment as owner for automatic garbage collection + // Use SetOwnerReference instead of SetControllerReference to avoid needing finalizer permissions + if err := controllerutil.SetOwnerReference(operatorDeployment, deployment, c.Client.Scheme()); err != nil { + return fmt.Errorf("failed to set owner reference on deployment: %w", err) + } + err = c.Client.Create(ctx, deployment) + if err != nil && !errors.IsAlreadyExists(err) { + return fmt.Errorf("failed to create CLI server deployment: %w", err) + } + c.Log.Info("Created CLI server deployment", "image", cliServerImage) + } else if err != nil { + return fmt.Errorf("failed to get CLI server deployment: %w", err) + } + + // 2. Create or update the service + service := &corev1.Service{} + err = c.Client.Get(ctx, client.ObjectKey{ + Name: cliServerServiceName, + Namespace: c.Namespace, + }, service) + + if errors.IsNotFound(err) { + service = buildCLIServerService(c.Namespace) + // Set operator deployment as owner for automatic garbage collection + // Use SetOwnerReference instead of SetControllerReference to avoid needing finalizer permissions + if err := controllerutil.SetOwnerReference(operatorDeployment, service, c.Client.Scheme()); err != nil { + return fmt.Errorf("failed to set owner reference on service: %w", err) + } + err = c.Client.Create(ctx, service) + if err != nil && !errors.IsAlreadyExists(err) { + return fmt.Errorf("failed to create CLI server service: %w", err) + } + c.Log.Info("Created CLI server service") + } else if err != nil { + return fmt.Errorf("failed to get CLI server service: %w", err) + } + + // 3. Create or get the route + route := &routev1.Route{} + err = c.Client.Get(ctx, client.ObjectKey{ + Name: cliServerRouteName, + Namespace: c.Namespace, + }, route) + + if errors.IsNotFound(err) { + route = buildCLIServerRoute(c.Namespace) + // Set operator deployment as owner for automatic garbage collection + // Use SetOwnerReference instead of SetControllerReference to avoid needing finalizer permissions + if err := controllerutil.SetOwnerReference(operatorDeployment, route, c.Client.Scheme()); err != nil { + return fmt.Errorf("failed to set owner reference on route: %w", err) + } + err = c.Client.Create(ctx, route) + if err != nil && !errors.IsAlreadyExists(err) { + return fmt.Errorf("failed to create CLI server route: %w", err) + } + c.Log.Info("Created CLI server route, waiting for hostname assignment") + // Wait a bit for hostname to be assigned + time.Sleep(2 * time.Second) + // Re-fetch to get the hostname + if err := c.Client.Get(ctx, client.ObjectKey{ + Name: cliServerRouteName, + Namespace: c.Namespace, + }, route); err != nil { + return fmt.Errorf("failed to get route after creation: %w", err) + } + } else if err != nil { + return fmt.Errorf("failed to get CLI server route: %w", err) + } + + // Check if route has hostname + hostname := route.Spec.Host + if hostname == "" { + c.Log.Info("Route hostname not yet assigned, will retry") + // Schedule a retry in background + go func() { + time.Sleep(5 * time.Second) + if err := c.reconcileCLIResources(ctx, operatorDeployment, cliServerImage); err != nil { + c.Log.Error(err, "Failed to reconcile CLI resources on retry") + } + }() + return nil + } + + // 4. Create or update ConsoleCLIDownload (cluster-scoped) + // Note: This is idempotent. If the ConsoleCLIDownload already exists from a previous + // installation, we'll reuse it and update the URL to point to our route. + // We use a fixed name to prevent duplicates across operator reinstalls. + downloadURL := fmt.Sprintf("https://%s/", hostname) + + consoleCLIDownload := &consolev1.ConsoleCLIDownload{} + err = c.Client.Get(ctx, client.ObjectKey{Name: cliDownloadName}, consoleCLIDownload) + + desiredSpec := consolev1.ConsoleCLIDownloadSpec{ + Description: "OADP operator Command Line Interface (CLI)", + DisplayName: "oadp - OADP operator Command Line Interface (CLI)", + Links: []consolev1.CLIDownloadLink{ + { + Href: downloadURL, + Text: "Download OADP CLI", + }, + }, + } + + if errors.IsNotFound(err) { + // ConsoleCLIDownload doesn't exist, create it + consoleCLIDownload = &consolev1.ConsoleCLIDownload{ + ObjectMeta: metav1.ObjectMeta{ + Name: cliDownloadName, + Labels: map[string]string{ + managedByLabel: operatorName, + "app.kubernetes.io/instance": c.OperatorNamespace, + }, + }, + Spec: desiredSpec, + } + err = c.Client.Create(ctx, consoleCLIDownload) + if err != nil && !errors.IsAlreadyExists(err) { + return fmt.Errorf("failed to create ConsoleCLIDownload: %w", err) + } + c.Log.Info("Created ConsoleCLIDownload", "url", downloadURL) + } else if err != nil { + return fmt.Errorf("failed to get ConsoleCLIDownload: %w", err) + } else { + // ConsoleCLIDownload exists (possibly from previous install), update if needed + needsUpdate := false + + // Update labels if missing + if consoleCLIDownload.Labels == nil { + consoleCLIDownload.Labels = make(map[string]string) + } + if consoleCLIDownload.Labels[managedByLabel] != operatorName { + consoleCLIDownload.Labels[managedByLabel] = operatorName + consoleCLIDownload.Labels["app.kubernetes.io/instance"] = c.OperatorNamespace + needsUpdate = true + } + + // Update spec if URL changed + if len(consoleCLIDownload.Spec.Links) == 0 || consoleCLIDownload.Spec.Links[0].Href != downloadURL { + consoleCLIDownload.Spec = desiredSpec + needsUpdate = true + } + + if needsUpdate { + err = c.Client.Update(ctx, consoleCLIDownload) + if err != nil { + return fmt.Errorf("failed to update ConsoleCLIDownload: %w", err) + } + c.Log.Info("Updated ConsoleCLIDownload", "url", downloadURL) + } else { + c.Log.Info("ConsoleCLIDownload already exists and is up-to-date", "url", downloadURL) + } + } + + return nil +} + +func buildCLIServerDeployment(namespace, image string) *appsv1.Deployment { + replicas := int32(1) + runAsNonRoot := true + allowPrivilegeEscalation := false + readOnlyRootFilesystem := true + + return &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: cliServerDeploymentName, + Namespace: namespace, + Labels: map[string]string{ + "app": "oadp-cli", + managedByLabel: operatorName, + }, + }, + Spec: appsv1.DeploymentSpec{ + Replicas: &replicas, + Selector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "app": "oadp-cli", + }, + }, + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{ + "app": "oadp-cli", + }, + }, + Spec: corev1.PodSpec{ + SecurityContext: &corev1.PodSecurityContext{ + RunAsNonRoot: &runAsNonRoot, + }, + Containers: []corev1.Container{ + { + Name: "oadp-cli-server", + Image: image, + Ports: []corev1.ContainerPort{ + { + Name: "http", + ContainerPort: 8080, + Protocol: corev1.ProtocolTCP, + }, + }, + Resources: corev1.ResourceRequirements{ + Limits: corev1.ResourceList{ + corev1.ResourceCPU: resource.MustParse("100m"), + corev1.ResourceMemory: resource.MustParse("64Mi"), + }, + Requests: corev1.ResourceList{ + corev1.ResourceCPU: resource.MustParse("50m"), + corev1.ResourceMemory: resource.MustParse("32Mi"), + }, + }, + SecurityContext: &corev1.SecurityContext{ + AllowPrivilegeEscalation: &allowPrivilegeEscalation, + Capabilities: &corev1.Capabilities{ + Drop: []corev1.Capability{"ALL"}, + }, + ReadOnlyRootFilesystem: &readOnlyRootFilesystem, + }, + }, + }, + TerminationGracePeriodSeconds: int64Ptr(10), + }, + }, + }, + } +} + +func buildCLIServerService(namespace string) *corev1.Service { + return &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: cliServerServiceName, + Namespace: namespace, + Labels: map[string]string{ + "app": "oadp-cli", + managedByLabel: operatorName, + }, + }, + Spec: corev1.ServiceSpec{ + Selector: map[string]string{ + "app": "oadp-cli", + }, + Ports: []corev1.ServicePort{ + { + Name: "http", + Port: 80, + TargetPort: intstr.FromInt(8080), + Protocol: corev1.ProtocolTCP, + }, + }, + Type: corev1.ServiceTypeClusterIP, + }, + } +} + +func buildCLIServerRoute(namespace string) *routev1.Route { + return &routev1.Route{ + ObjectMeta: metav1.ObjectMeta{ + Name: cliServerRouteName, + Namespace: namespace, + Labels: map[string]string{ + "app": "oadp-cli", + managedByLabel: operatorName, + }, + }, + Spec: routev1.RouteSpec{ + To: routev1.RouteTargetReference{ + Kind: "Service", + Name: cliServerServiceName, + }, + Port: &routev1.RoutePort{ + TargetPort: intstr.FromString("http"), + }, + TLS: &routev1.TLSConfig{ + Termination: routev1.TLSTerminationEdge, + InsecureEdgeTerminationPolicy: routev1.InsecureEdgeTerminationPolicyRedirect, + }, + }, + } +} + +func int64Ptr(i int64) *int64 { + return &i +} diff --git a/internal/controller/oadp_cli_controller.go b/internal/controller/oadp_cli_controller.go deleted file mode 100644 index 9a4685d3a2..0000000000 --- a/internal/controller/oadp_cli_controller.go +++ /dev/null @@ -1,172 +0,0 @@ -package controller - -import ( - "context" - "fmt" - "time" - - consolev1 "github.com/openshift/api/console/v1" - routev1 "github.com/openshift/api/route/v1" - appsv1 "k8s.io/api/apps/v1" - "k8s.io/apimachinery/pkg/api/errors" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/util/intstr" - ctrl "sigs.k8s.io/controller-runtime" - "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/event" - "sigs.k8s.io/controller-runtime/pkg/log" - "sigs.k8s.io/controller-runtime/pkg/predicate" -) - -//+kubebuilder:rbac:groups=apps,resources=deployments,verbs=get;list;watch -//+kubebuilder:rbac:groups=route.openshift.io,resources=routes,verbs=get;list;watch;create;update;patch;delete -//+kubebuilder:rbac:groups=console.openshift.io,resources=consoleclidownloads,verbs=get;list;watch;create;update;patch;delete - -type OADPCLIReconciler struct { - client.Client - Scheme *runtime.Scheme - Namespace string -} - -const ( - cliServerDeploymentName = "openshift-adp-oadp-cli-server" - cliServerRouteName = "oadp-cli-server-route" - cliServerServiceName = "openshift-adp-cli-server" -) - -func (r *OADPCLIReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { - logger := log.FromContext(ctx) - logger.Info("Reconciling OADP CLI download resources", "triggered_by", req.NamespacedName) - - // 1. Check if CLI server deployment exists - deployment := &appsv1.Deployment{} - err := r.Get(ctx, client.ObjectKey{ - Name: cliServerDeploymentName, - Namespace: r.Namespace, - }, deployment) - - if errors.IsNotFound(err) { - logger.V(1).Info("CLI server deployment not found, nothing to reconcile") - return ctrl.Result{}, nil - } - if err != nil { - return ctrl.Result{}, fmt.Errorf("failed to get CLI server deployment: %w", err) - } - - // 2. Check if CLI server route exists - route := &routev1.Route{} - err = r.Get(ctx, client.ObjectKey{ - Name: cliServerRouteName, - Namespace: r.Namespace, - }, route) - - if err != nil && !errors.IsNotFound(err) { - return ctrl.Result{}, fmt.Errorf("failed to get CLI server route: %w", err) - } - - // Route not found, create it - if errors.IsNotFound(err) { - route = &routev1.Route{ - ObjectMeta: metav1.ObjectMeta{ - Name: cliServerRouteName, - Namespace: r.Namespace, - }, - Spec: routev1.RouteSpec{ - To: routev1.RouteTargetReference{ - Kind: "Service", - Name: cliServerServiceName, - }, - Port: &routev1.RoutePort{ - TargetPort: intstr.FromString("http"), - }, - TLS: &routev1.TLSConfig{ - Termination: routev1.TLSTerminationEdge, - InsecureEdgeTerminationPolicy: routev1.InsecureEdgeTerminationPolicyRedirect, - }, - }, - } - err = r.Create(ctx, route) - if err != nil && !errors.IsAlreadyExists(err) { - return ctrl.Result{}, fmt.Errorf("failed to create CLI server route: %w", err) - } - - // If AlreadyExists, just continue - another reconcile loop created it - // Wait for route to get hostname assigned - return ctrl.Result{RequeueAfter: 10 * time.Second}, nil - } - - // At this point, route with hostname exists. Grab it - hostname := route.Spec.Host - if hostname == "" { - return ctrl.Result{}, fmt.Errorf("CLI server route has no hostname") - } - - // 3. Create or update ConsoleCLIDownload - downloadURL := fmt.Sprintf("https://%s/", hostname) - - consoleCLIDownload := &consolev1.ConsoleCLIDownload{} - err = r.Get(ctx, client.ObjectKey{Name: "openshift-adp-oadp-cli"}, consoleCLIDownload) - - desiredSpec := consolev1.ConsoleCLIDownloadSpec{ - Description: "OADP operator Command Line Interface (CLI)", - DisplayName: "oadp - OADP operator Command Line Interface (CLI)", - Links: []consolev1.CLIDownloadLink{ - { - Href: downloadURL, - Text: "Download OADP CLI", - }, - }, - } - - if errors.IsNotFound(err) { - // Create new ConsoleCLIDownload - consoleCLIDownload = &consolev1.ConsoleCLIDownload{ - ObjectMeta: metav1.ObjectMeta{ - Name: "openshift-adp-oadp-cli", - }, - Spec: desiredSpec, - } - err = r.Create(ctx, consoleCLIDownload) - if err != nil && !errors.IsAlreadyExists(err) { - return ctrl.Result{}, fmt.Errorf("failed to create ConsoleCLIDownload: %w", err) - } - logger.Info("Created ConsoleCLIDownload", "url", downloadURL) - } else if err != nil { - return ctrl.Result{}, fmt.Errorf("failed to get ConsoleCLIDownload: %w", err) - } else { - // Update existing ConsoleCLIDownload if URL changed - if len(consoleCLIDownload.Spec.Links) == 0 || consoleCLIDownload.Spec.Links[0].Href != downloadURL { - consoleCLIDownload.Spec = desiredSpec - err = r.Update(ctx, consoleCLIDownload) - if err != nil { - return ctrl.Result{}, fmt.Errorf("failed to update ConsoleCLIDownload: %w", err) - } - logger.Info("Updated ConsoleCLIDownload with new URL", "url", downloadURL) - } - } - - return ctrl.Result{}, nil -} - -// SetupWithManager sets up the controller with the Manager. -func (r *OADPCLIReconciler) SetupWithManager(mgr ctrl.Manager) error { - return ctrl.NewControllerManagedBy(mgr). - For(&appsv1.Deployment{}). - WithEventFilter(cliServerPredicate()). - Complete(r) -} - -func cliServerPredicate() predicate.Predicate { - return predicate.Funcs{ - CreateFunc: func(e event.CreateEvent) bool { - return e.Object.GetName() == cliServerDeploymentName - }, - UpdateFunc: func(e event.UpdateEvent) bool { - return e.ObjectNew.GetName() == cliServerDeploymentName - }, - DeleteFunc: func(e event.DeleteEvent) bool { - return e.Object.GetName() == cliServerDeploymentName - }, - } -} From b46f2aff6cc1c31b3c8149412701153bddd12007 Mon Sep 17 00:00:00 2001 From: Joseph Date: Thu, 30 Oct 2025 14:46:21 -0400 Subject: [PATCH 08/10] Make bundle Signed-off-by: Joseph --- bundle/manifests/oadp-operator.clusterserviceversion.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bundle/manifests/oadp-operator.clusterserviceversion.yaml b/bundle/manifests/oadp-operator.clusterserviceversion.yaml index b43ef47bfc..2f8b776c00 100644 --- a/bundle/manifests/oadp-operator.clusterserviceversion.yaml +++ b/bundle/manifests/oadp-operator.clusterserviceversion.yaml @@ -1305,7 +1305,7 @@ spec: - name: RELATED_IMAGE_VM_FILE_RESTORE_BROWSER value: quay.io/konveyor/oadp-vmfr-access-filebrowser:latest - name: RELATED_IMAGE_CONSOLE_CLI_DOWNLOAD - value: quay.io/konveyor/console-cli-download:latest + value: quay.io/konveyor/oadp-cli-binaries:latest image: quay.io/konveyor/oadp-operator:latest imagePullPolicy: Always livenessProbe: @@ -1476,6 +1476,6 @@ spec: name: vm-file-restore-ssh - image: quay.io/konveyor/oadp-vmfr-access-filebrowser:latest name: vm-file-restore-browser - - image: quay.io/konveyor/console-cli-download:latest + - image: quay.io/konveyor/oadp-cli-binaries:latest name: console-cli-download version: 99.0.0 From ba57d52abcab355d7223655396b769953b6af5a4 Mon Sep 17 00:00:00 2001 From: Joseph Date: Fri, 21 Nov 2025 11:37:12 -0500 Subject: [PATCH 09/10] Do not initialize controller if watchNamespace is empty Signed-off-by: Joseph --- cmd/main.go | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/cmd/main.go b/cmd/main.go index 078ac653ec..6693a6a3fd 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -273,14 +273,19 @@ func main() { //+kubebuilder:scaffold:builder // Add CLI download setup runnable - if err := mgr.Add(&controller.CLIDownloadSetup{ - Client: mgr.GetClient(), - Namespace: watchNamespace, - OperatorName: "openshift-adp-controller-manager", - OperatorNamespace: watchNamespace, - }); err != nil { - setupLog.Error(err, "unable to add CLI download setup") - os.Exit(1) + // Only add if watchNamespace is set (skip for cluster-scoped mode) + if watchNamespace != "" { + if err := mgr.Add(&controller.CLIDownloadSetup{ + Client: mgr.GetClient(), + Namespace: watchNamespace, + OperatorName: "openshift-adp-controller-manager", + OperatorNamespace: watchNamespace, + }); err != nil { + setupLog.Error(err, "unable to add CLI download setup") + os.Exit(1) + } + } else { + setupLog.Info("Skipping CLI download setup - watchNamespace not set") } if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil { From a78c3bb40ec845ac3b384cae864d2bed6bf1a9c2 Mon Sep 17 00:00:00 2001 From: Joseph Date: Fri, 21 Nov 2025 11:41:26 -0500 Subject: [PATCH 10/10] Add bounded retries, exponential backoff and context awareness Signed-off-by: Joseph --- .../controller/cli_download_controller.go | 42 +++++++++++++++---- 1 file changed, 33 insertions(+), 9 deletions(-) diff --git a/internal/controller/cli_download_controller.go b/internal/controller/cli_download_controller.go index 08debb6ac9..0fe463812d 100644 --- a/internal/controller/cli_download_controller.go +++ b/internal/controller/cli_download_controller.go @@ -152,18 +152,42 @@ func (c *CLIDownloadSetup) reconcileCLIResources(ctx context.Context, operatorDe return fmt.Errorf("failed to get CLI server route: %w", err) } - // Check if route has hostname + // Check if route has hostname - retry with backoff if not assigned hostname := route.Spec.Host if hostname == "" { - c.Log.Info("Route hostname not yet assigned, will retry") - // Schedule a retry in background - go func() { - time.Sleep(5 * time.Second) - if err := c.reconcileCLIResources(ctx, operatorDeployment, cliServerImage); err != nil { - c.Log.Error(err, "Failed to reconcile CLI resources on retry") + c.Log.Info("Route hostname not yet assigned, retrying with backoff") + maxRetries := 5 + backoff := 2 * time.Second + + for attempt := 1; attempt <= maxRetries; attempt++ { + select { + case <-ctx.Done(): + return ctx.Err() + case <-time.After(backoff): + // Re-fetch route to check for hostname + if err := c.Client.Get(ctx, client.ObjectKey{ + Name: cliServerRouteName, + Namespace: c.Namespace, + }, route); err != nil { + c.Log.Error(err, "Failed to get route on retry", "attempt", attempt) + continue + } + + if route.Spec.Host != "" { + hostname = route.Spec.Host + c.Log.Info("Route hostname assigned", "hostname", hostname, "attempt", attempt) + break + } + + c.Log.Info("Route hostname still not assigned, will retry", "attempt", attempt, "maxRetries", maxRetries) + backoff *= 2 // Exponential backoff } - }() - return nil + } + + if hostname == "" { + c.Log.Info("Route hostname not assigned after max retries, ConsoleCLIDownload will not be created. It will be created on next reconciliation when hostname becomes available.") + return nil + } } // 4. Create or update ConsoleCLIDownload (cluster-scoped)