Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
144 changes: 125 additions & 19 deletions internal/controller/cli_download_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -74,9 +75,80 @@ 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 or reconcile the dedicated ServiceAccount used by the CLI server pod.
// A non-default ServiceAccount is required (rather than relying on the
// namespace's "default" SA) to satisfy access-control best practices,
// even though this workload needs no Kubernetes API permissions.
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)
}
err = c.Client.Create(ctx, serviceAccount)
if 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)
} else {
// ServiceAccount already exists — reconcile it to ensure desired state.
desired := buildCLIServerServiceAccount(c.Namespace)
needsUpdate := false

// Ensure AutomountServiceAccountToken is explicitly false.
if serviceAccount.AutomountServiceAccountToken == nil ||
*serviceAccount.AutomountServiceAccountToken != *desired.AutomountServiceAccountToken {
serviceAccount.AutomountServiceAccountToken = desired.AutomountServiceAccountToken
needsUpdate = true
}

// Ensure required labels are present.
if serviceAccount.Labels == nil {
serviceAccount.Labels = make(map[string]string)
}
for k, v := range desired.Labels {
if serviceAccount.Labels[k] != v {
serviceAccount.Labels[k] = v
needsUpdate = true
}
}

// Check for the pre-existing owner reference BEFORE mutating.
// SetOwnerReference is idempotent and will add the reference if it's
// missing, so checking the list *after* calling it would always find
// a match and mask the fact that an update was actually needed.
hasOwnerRef := false
for _, ref := range serviceAccount.OwnerReferences {
if ref.UID == operatorDeployment.UID {
hasOwnerRef = true
break
}
}
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 !hasOwnerRef {
needsUpdate = true
}

if needsUpdate {
if err := c.Client.Update(ctx, serviceAccount); err != nil {
return fmt.Errorf("failed to update CLI server service account: %w", err)
}
c.Log.Info("Updated CLI server service account")
}
}

// 2. Create or update the deployment
deployment := &appsv1.Deployment{}
err = c.Client.Get(ctx, client.ObjectKey{
Name: cliServerDeploymentName,
Namespace: c.Namespace,
}, deployment)
Expand All @@ -95,19 +167,35 @@ 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 if len(deployment.Spec.Template.Spec.Containers) > 0 &&
deployment.Spec.Template.Spec.Containers[0].ReadinessProbe == nil {
// Deployment exists from a version before probes were added; backfill them.
} else {
// Deployment exists from a version before probes/service account were added; backfill them.
desired := buildCLIServerDeployment(c.Namespace, cliServerImage)
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
if err := c.Client.Update(ctx, deployment); err != nil {
return fmt.Errorf("failed to update CLI server deployment with probes: %w", err)
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
needsUpdate = true
}
desiredAutomount := desired.Spec.Template.Spec.AutomountServiceAccountToken
currentAutomount := deployment.Spec.Template.Spec.AutomountServiceAccountToken
if desiredAutomount != nil && (currentAutomount == nil || *currentAutomount != *desiredAutomount) {
deployment.Spec.Template.Spec.AutomountServiceAccountToken = desiredAutomount
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")
}
c.Log.Info("Updated CLI server deployment with readiness/liveness probes")
}

// 2. Create or update the service
// 3. Create or update the service
service := &corev1.Service{}
err = c.Client.Get(ctx, client.ObjectKey{
Name: cliServerServiceName,
Expand All @@ -130,7 +218,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,
Expand Down Expand Up @@ -200,7 +288,7 @@ func (c *CLIDownloadSetup) reconcileCLIResources(ctx context.Context, operatorDe
}
}

// 4. Create or update ConsoleCLIDownload (cluster-scoped)
// 5. 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.
Expand Down Expand Up @@ -278,6 +366,7 @@ func buildCLIServerDeployment(namespace, image string) *appsv1.Deployment {
runAsNonRoot := true
allowPrivilegeEscalation := false
readOnlyRootFilesystem := true
automountServiceAccountToken := false

return &appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{
Expand All @@ -302,6 +391,8 @@ func buildCLIServerDeployment(namespace, image string) *appsv1.Deployment {
},
},
Spec: corev1.PodSpec{
ServiceAccountName: cliServerServiceAccountName,
AutomountServiceAccountToken: &automountServiceAccountToken,
SecurityContext: &corev1.PodSecurityContext{
RunAsNonRoot: &runAsNonRoot,
},
Expand Down Expand Up @@ -362,6 +453,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{
Expand Down
Loading