Skip to content
2 changes: 1 addition & 1 deletion bindata/assets/deployments/downloads-deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ metadata:
component: downloads
annotations: {}
spec:
serviceAccountName: downloads
selector:
matchLabels:
app: console
Expand All @@ -25,6 +24,7 @@ spec:
target.workload.openshift.io/management: '{"effect": "PreferredDuringScheduling"}'
openshift.io/required-scc: restricted-v2
spec:
serviceAccountName: downloads
nodeSelector:
kubernetes.io/os: linux
node-role.kubernetes.io/master: ""
Expand Down
11 changes: 11 additions & 0 deletions bindata/assets/serviceaccounts/downloads-sa.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
apiVersion: v1
kind: ServiceAccount
metadata:
name: downloads
namespace: openshift-console
annotations:
include.release.openshift.io/hypershift: "true"
include.release.openshift.io/ibm-cloud-managed: "true"
include.release.openshift.io/self-managed-high-availability: "true"
include.release.openshift.io/single-node-developer: "true"
capability.openshift.io/name: Console
11 changes: 11 additions & 0 deletions manifests/03-rbac-role-cluster.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,17 @@ rules:
- list
- watch
- delete
- apiGroups:
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We just need one

  - apiGroups:
      - ""
    resources:
      - serviceaccounts
    verbs:
      - get
      - list
      - watch
      - create
      - update
      - delete
    resourceNames:
      - console
      - downloads

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking here , if we did squash it all together then we may have issues with looking for service accounts. I kept it separate so we can list, watch and get any service accounts. However, there is no reason why we cannot merge the first two rules.

From what I understand, the minimum number of rules we can have is two. We can move list, watch and get to the third rule. However, we cannot restrict create .

You cannot restrict deletecollection or top-level create requests by resource name. For create, this limitation is because the name of the new object may not be known at authorization time.

As for the third rule, if we wanted to make modifications to the service account, we wouldn't want for the controller to be able to delete just any service account, so having resourceNames specified here allows us to restrict where possible.

So, the compromise we can do is keep create without restriction, and we can restrict everything else. I'll go ahead and update to reflect this, and feel free to correct me if I misunderstood anything.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I also need to double check if the get used in the controller looks for these service accounts specifically or not and make adjustments.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @jhadvig - sadly we cannot use it like this. Found this in console-operator logs

026-03-11T13:25:17.260764907Z E0311 13:25:17.260731       1 reflector.go:205] "Failed to watch" err="failed to list *v1.ServiceAccount: serviceaccounts is forbidden: User \"system:serviceaccount:openshift-console-operator:console-operator\" cannot list resource \"serviceaccounts\" in API group \"\" in the namespace \"openshift-console\"" 

This is the current rule formation:

- apiGroups:
      - ""
    resources:
      - serviceaccounts
    verbs:
      - create
  - apiGroups:
      - ""
    resources:
      - serviceaccounts
    verbs:
      - list
      - watch
      - get
      - update
      - delete
    resourceNames:
      - console
      - downloads

I will probably need to move it to this

- apiGroups:
      - ""
    resources:
      - serviceaccounts
    verbs:
      - create
      - list
      - watch
      - get
  - apiGroups:
      - ""
    resources:
      - serviceaccounts
    verbs:
      - update
      - delete
    resourceNames:
      - console
      - downloads

Let me know if this suits. :)

Copy link
Copy Markdown
Member

@jhadvig jhadvig Mar 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit:
its a bit weird that we have the create action but not delete.
if anything I would add the create to the second list, sicne we are only creating these two SAs

- ""
resources:
- serviceaccounts
verbs:
- get
- list
- delete
- create
- update
- watch
Comment thread
ehearne-redhat marked this conversation as resolved.
---
kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1
Expand Down
12 changes: 0 additions & 12 deletions manifests/06-sa.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -21,16 +21,4 @@ metadata:
include.release.openshift.io/self-managed-high-availability: "true"
include.release.openshift.io/single-node-developer: "true"
capability.openshift.io/name: Console
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: downloads
namespace: openshift-console
annotations:
include.release.openshift.io/hypershift: "true"
include.release.openshift.io/ibm-cloud-managed: "true"
include.release.openshift.io/self-managed-high-availability: "true"
include.release.openshift.io/single-node-developer: "true"
capability.openshift.io/name: Console
---
115 changes: 115 additions & 0 deletions pkg/console/controllers/downloadsserviceaccount/controller.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
package downloadsserviceaccount

import (
"context"
"fmt"
"time"

corev1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
coreinformersv1 "k8s.io/client-go/informers/core/v1"
coreclientv1 "k8s.io/client-go/kubernetes/typed/core/v1"
"k8s.io/klog/v2"

operatorv1 "github.com/openshift/api/operator/v1"
operatorinformerv1 "github.com/openshift/client-go/operator/informers/externalversions/operator/v1"
operatorlistersv1 "github.com/openshift/client-go/operator/listers/operator/v1"
"github.com/openshift/console-operator/pkg/api"
"github.com/openshift/console-operator/pkg/console/controllers/util"
"github.com/openshift/console-operator/pkg/console/status"
serviceaccountsub "github.com/openshift/console-operator/pkg/console/subresource/serviceaccount"
"github.com/openshift/library-go/pkg/controller/factory"
"github.com/openshift/library-go/pkg/operator/events"
"github.com/openshift/library-go/pkg/operator/resource/resourceapply"
"github.com/openshift/library-go/pkg/operator/v1helpers"
)

type DownloadsServiceAccountSyncController struct {
operatorClient v1helpers.OperatorClient
// configs
consoleOperatorLister operatorlistersv1.ConsoleLister
// core kube
serviceAccountClient coreclientv1.ServiceAccountsGetter
}

func NewDownloadsServiceAccountSyncController(
// clients
operatorClient v1helpers.OperatorClient,
// informer
operatorConfigInformer operatorinformerv1.ConsoleInformer,
// core kube
serviceAccountClient coreclientv1.ServiceAccountsGetter,
serviceAccountInformer coreinformersv1.ServiceAccountInformer,
// events
recorder events.Recorder,
) factory.Controller {
ctrl := &DownloadsServiceAccountSyncController{
// configs
operatorClient: operatorClient,
consoleOperatorLister: operatorConfigInformer.Lister(),
// client
serviceAccountClient: serviceAccountClient,
}

configNameFilter := util.IncludeNamesFilter(api.ConfigResourceName)
downloadsNameFilter := util.IncludeNamesFilter(api.DownloadsResourceName)

return factory.New().
WithFilteredEventsInformers( // configs
configNameFilter,
operatorConfigInformer.Informer(),
).WithFilteredEventsInformers( // downloads service account
downloadsNameFilter,
serviceAccountInformer.Informer(),
).ResyncEvery(time.Minute).WithSync(ctrl.Sync).
ToController("ConsoleDownloadsServiceAccountSyncController", recorder.WithComponentSuffix("console-downloads-service-account-controller"))
}

func (c *DownloadsServiceAccountSyncController) Sync(ctx context.Context, controllerContext factory.SyncContext) error {
operatorConfig, err := c.consoleOperatorLister.Get(api.ConfigResourceName)
if err != nil {
return err
}
operatorConfigCopy := operatorConfig.DeepCopy()

switch operatorConfigCopy.Spec.ManagementState {
case operatorv1.Managed:
klog.V(4).Infoln("console is in a managed state: syncing downloads service account")
case operatorv1.Unmanaged:
klog.V(4).Infoln("console is in an unmanaged state: skipping downloads service account sync")
return nil
case operatorv1.Removed:
klog.V(4).Infoln("console is in a removed state: removing downloads service account")
return c.removeDownloadsServiceAccount(ctx)
default:
return fmt.Errorf("unknown state: %v", operatorConfigCopy.Spec.ManagementState)
}
statusHandler := status.NewStatusHandler(c.operatorClient)

_, _, serviceAccountErr := c.SyncDownloadsServiceAccount(ctx, operatorConfigCopy, controllerContext)
statusHandler.AddConditions(status.HandleProgressingOrDegraded("DownloadsServiceAccountSync", "FailedApply", serviceAccountErr))
if serviceAccountErr != nil {
return statusHandler.FlushAndReturn(serviceAccountErr)
}

return statusHandler.FlushAndReturn(nil)
}

func (c *DownloadsServiceAccountSyncController) SyncDownloadsServiceAccount(ctx context.Context, operatorConfigCopy *operatorv1.Console, controllerContext factory.SyncContext) (*corev1.ServiceAccount, bool, error) {
requiredDownloadsServiceAccount := serviceaccountsub.DefaultDownloadsServiceAccount(operatorConfigCopy)

return resourceapply.ApplyServiceAccount(ctx,
c.serviceAccountClient,
controllerContext.Recorder(),
requiredDownloadsServiceAccount,
)
}

func (c *DownloadsServiceAccountSyncController) removeDownloadsServiceAccount(ctx context.Context) error {
err := c.serviceAccountClient.ServiceAccounts(api.OpenShiftConsoleNamespace).Delete(ctx, api.DownloadsResourceName, metav1.DeleteOptions{})
if apierrors.IsNotFound(err) {
return nil
}
return err
}
13 changes: 13 additions & 0 deletions pkg/console/starter/starter.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import (
"github.com/openshift/console-operator/pkg/console/controllers/clidownloads"
"github.com/openshift/console-operator/pkg/console/controllers/clioidcclientstatus"
"github.com/openshift/console-operator/pkg/console/controllers/downloadsdeployment"
"github.com/openshift/console-operator/pkg/console/controllers/downloadsserviceaccount"
"github.com/openshift/console-operator/pkg/console/controllers/healthcheck"
"github.com/openshift/console-operator/pkg/console/controllers/migration"
"github.com/openshift/console-operator/pkg/console/controllers/oauthclients"
Expand Down Expand Up @@ -338,6 +339,17 @@ func RunOperator(ctx context.Context, controllerContext *controllercmd.Controlle
recorder,
)

downloadsServiceAccountController := downloadsserviceaccount.NewDownloadsServiceAccountSyncController(
// clients
operatorClient,
// operator
operatorConfigInformers.Operator().V1().Consoles(),
// service accounts
kubeClient.CoreV1(),
kubeInformersNamespaced.Core().V1().ServiceAccounts(),
recorder,
)

cliDownloadsController := clidownloads.NewCLIDownloadsSyncController(
// top level config
configClient.ConfigV1(),
Expand Down Expand Up @@ -639,6 +651,7 @@ func RunOperator(ctx context.Context, controllerContext *controllercmd.Controlle
consoleOperator,
cliDownloadsController,
downloadsDeploymentController,
downloadsServiceAccountController,
consoleRouteHealthCheckController,
consolePDBController,
downloadsPDBController,
Expand Down
18 changes: 18 additions & 0 deletions pkg/console/subresource/serviceaccount/serviceaccount.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package serviceaccount

import (
corev1 "k8s.io/api/core/v1"

operatorv1 "github.com/openshift/api/operator/v1"
"github.com/openshift/console-operator/bindata"
"github.com/openshift/console-operator/pkg/console/subresource/util"
"github.com/openshift/library-go/pkg/operator/resource/resourceread"
)

func DefaultDownloadsServiceAccount(operatorConfig *operatorv1.Console) *corev1.ServiceAccount {
serviceAccount := resourceread.ReadServiceAccountV1OrDie(
bindata.MustAsset("assets/serviceaccounts/downloads-sa.yaml"),
)
util.AddOwnerRef(serviceAccount, util.OwnerRefFrom(operatorConfig))
return serviceAccount
}