diff --git a/controllers/cli_download_controller.go b/controllers/cli_download_controller.go index e2323576e6..9ac15793aa 100644 --- a/controllers/cli_download_controller.go +++ b/controllers/cli_download_controller.go @@ -22,12 +22,13 @@ import ( ) 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" + cliServerDeploymentName = "openshift-adp-oadp-cli-server" + cliServerServiceName = "openshift-adp-cli-server" + cliServerServiceAccountName = "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 @@ -74,9 +75,29 @@ func (c *CLIDownloadSetup) Start(ctx context.Context) error { // 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{} + // 1. Create the dedicated ServiceAccount used by the CLI server pod. + serviceAccount := &corev1.ServiceAccount{} err := c.Client.Get(ctx, client.ObjectKey{ + Name: cliServerServiceAccountName, + Namespace: c.Namespace, + }, serviceAccount) + + if errors.IsNotFound(err) { + serviceAccount = buildCLIServerServiceAccount(c.Namespace) + if err := controllerutil.SetOwnerReference(operatorDeployment, serviceAccount, c.Client.Scheme()); err != nil { + return fmt.Errorf("failed to set owner reference on service account: %w", err) + } + if err := c.Client.Create(ctx, serviceAccount); err != nil && !errors.IsAlreadyExists(err) { + return fmt.Errorf("failed to create CLI server service account: %w", err) + } + c.Log.Info("Created CLI server service account") + } else if err != nil { + return fmt.Errorf("failed to get CLI server service account: %w", err) + } + + // 2. Create or update the deployment + deployment := &appsv1.Deployment{} + err = c.Client.Get(ctx, client.ObjectKey{ Name: cliServerDeploymentName, Namespace: c.Namespace, }, deployment) @@ -93,9 +114,29 @@ func (c *CLIDownloadSetup) reconcileCLIResources(ctx context.Context, operatorDe c.Log.Info("Created CLI server deployment", "image", cliServerImage) } else if err != nil { return fmt.Errorf("failed to get CLI server deployment: %w", err) + } else { + desired := buildCLIServerDeployment(c.Namespace, cliServerImage) + needsUpdate := false + if len(deployment.Spec.Template.Spec.Containers) > 0 && + deployment.Spec.Template.Spec.Containers[0].ReadinessProbe == nil { + deployment.Spec.Template.Spec.Containers[0].ReadinessProbe = desired.Spec.Template.Spec.Containers[0].ReadinessProbe + deployment.Spec.Template.Spec.Containers[0].LivenessProbe = desired.Spec.Template.Spec.Containers[0].LivenessProbe + needsUpdate = true + } + if deployment.Spec.Template.Spec.ServiceAccountName != cliServerServiceAccountName { + deployment.Spec.Template.Spec.ServiceAccountName = desired.Spec.Template.Spec.ServiceAccountName + deployment.Spec.Template.Spec.AutomountServiceAccountToken = desired.Spec.Template.Spec.AutomountServiceAccountToken + needsUpdate = true + } + if needsUpdate { + if err := c.Client.Update(ctx, deployment); err != nil { + return fmt.Errorf("failed to update CLI server deployment: %w", err) + } + c.Log.Info("Updated CLI server deployment with readiness/liveness probes and/or service account") + } } - // 2. Create or update the service + // 3. Create or update the service service := &corev1.Service{} err = c.Client.Get(ctx, client.ObjectKey{ Name: cliServerServiceName, @@ -116,7 +157,7 @@ func (c *CLIDownloadSetup) reconcileCLIResources(ctx context.Context, operatorDe return fmt.Errorf("failed to get CLI server service: %w", err) } - // 3. Create or get the route + // 4. Create or get the route route := &routev1.Route{} err = c.Client.Get(ctx, client.ObjectKey{ Name: cliServerRouteName, @@ -181,7 +222,7 @@ func (c *CLIDownloadSetup) reconcileCLIResources(ctx context.Context, operatorDe } } - // 4. Create or update ConsoleCLIDownload (cluster-scoped) + // 5. Create or update ConsoleCLIDownload (cluster-scoped) downloadURL := fmt.Sprintf("https://%s/", hostname) consoleCLIDownload := &consolev1.ConsoleCLIDownload{} @@ -252,6 +293,7 @@ func buildCLIServerDeployment(namespace, image string) *appsv1.Deployment { runAsNonRoot := true allowPrivilegeEscalation := false readOnlyRootFilesystem := true + automountServiceAccountToken := false return &appsv1.Deployment{ ObjectMeta: metav1.ObjectMeta{ @@ -276,6 +318,8 @@ func buildCLIServerDeployment(namespace, image string) *appsv1.Deployment { }, }, Spec: corev1.PodSpec{ + ServiceAccountName: cliServerServiceAccountName, + AutomountServiceAccountToken: &automountServiceAccountToken, SecurityContext: &corev1.PodSecurityContext{ RunAsNonRoot: &runAsNonRoot, }, @@ -290,6 +334,26 @@ func buildCLIServerDeployment(namespace, image string) *appsv1.Deployment { Protocol: corev1.ProtocolTCP, }, }, + ReadinessProbe: &corev1.Probe{ + ProbeHandler: corev1.ProbeHandler{ + HTTPGet: &corev1.HTTPGetAction{ + Path: "/", + Port: intstr.FromString("http"), + }, + }, + InitialDelaySeconds: 5, + PeriodSeconds: 10, + }, + LivenessProbe: &corev1.Probe{ + ProbeHandler: corev1.ProbeHandler{ + HTTPGet: &corev1.HTTPGetAction{ + Path: "/", + Port: intstr.FromString("http"), + }, + }, + InitialDelaySeconds: 15, + PeriodSeconds: 20, + }, Resources: corev1.ResourceRequirements{ Limits: corev1.ResourceList{ corev1.ResourceCPU: resource.MustParse("100m"), @@ -316,6 +380,21 @@ func buildCLIServerDeployment(namespace, image string) *appsv1.Deployment { } } +func buildCLIServerServiceAccount(namespace string) *corev1.ServiceAccount { + automountServiceAccountToken := false + return &corev1.ServiceAccount{ + ObjectMeta: metav1.ObjectMeta{ + Name: cliServerServiceAccountName, + Namespace: namespace, + Labels: map[string]string{ + "app": "oadp-cli", + managedByLabel: operatorName, + }, + }, + AutomountServiceAccountToken: &automountServiceAccountToken, + } +} + func buildCLIServerService(namespace string) *corev1.Service { return &corev1.Service{ ObjectMeta: metav1.ObjectMeta{