Skip to content

Commit ba1e9f7

Browse files
jkhelilcursoragent
andcommitted
fix(namespacesync): export PipelineRoleBinding for e2e test
The e2e test referenced tconfig.PipelineRoleBinding from the tektonconfig package, which no longer exists after the rbac.go cleanup. Export the constant from namespacesync (its new owner) and update the test import alias accordingly. Signed-off-by: Jawed khelil <jkhelil@redhat.com> Assisted-by: Claude Sonnet 4.6 (via Cursor) Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 9b8855b commit ba1e9f7

12 files changed

Lines changed: 417 additions & 25 deletions

File tree

config/base/generated-crds/operator.tekton.dev_tektonconfigs.yaml

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1021,6 +1021,45 @@ spec:
10211021
type: string
10221022
type: object
10231023
type: object
1024+
secretBindings:
1025+
description: SecretBindings declares secrets to be automatically
1026+
bound to the pipeline SA in each namespace. Each entry must
1027+
set exactly one of labelSelector or secretName.
1028+
items:
1029+
description: SecretBinding describes a secret or class of
1030+
secrets to bind to the pipeline SA.
1031+
properties:
1032+
labelSelector:
1033+
description: LabelSelector selects secrets by label.
1034+
All matching secrets in a namespace are bound.
1035+
properties:
1036+
matchExpressions:
1037+
items:
1038+
properties:
1039+
key:
1040+
type: string
1041+
operator:
1042+
type: string
1043+
values:
1044+
items:
1045+
type: string
1046+
type: array
1047+
required:
1048+
- key
1049+
- operator
1050+
type: object
1051+
type: array
1052+
matchLabels:
1053+
additionalProperties:
1054+
type: string
1055+
type: object
1056+
type: object
1057+
secretName:
1058+
description: SecretName binds a specific named secret
1059+
in each namespace to the pipeline SA.
1060+
type: string
1061+
type: object
1062+
type: array
10241063
type: object
10251064
type: object
10261065
type: object

docs/TektonConfig.md

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -575,6 +575,129 @@ In the deployment the environment name will be converted as follows,
575575
- `tekton-hub-api` => `TEKTON_HUB_API`
576576
- `artifact-hub-api` => `ARTIFACT_HUB_API`
577577

578+
### NamespaceSync (OpenShift only)
579+
580+
The `namespaceSync` block under `spec.platforms.openshift` controls the **NamespaceSyncController**, which watches every user namespace and ensures Tekton-required resources are present and up to date. It replaces the legacy per-namespace batch loop that was part of the RBAC reconciler.
581+
582+
#### Resources managed per namespace
583+
584+
| Resource | Kind | Purpose |
585+
|---|---|---|
586+
| `pipeline` | `ServiceAccount` | Identity for PipelineRun pods |
587+
| `pipelines-scc-rolebinding` | `RoleBinding` → `pipelines-scc-clusterrole` | Grants the pipeline SA permission to use the default SCC |
588+
| `openshift-pipelines-edit` | `RoleBinding` → `ClusterRole/edit` | Gives the pipeline SA edit access within its namespace |
589+
| `config-trusted-cabundle` | `ConfigMap` | CA bundle for custom/internal PKI trust |
590+
| `config-service-cabundle` | `ConfigMap` | OpenShift service CA bundle |
591+
| `openshift-pipelines-clusterinterceptors` | `ClusterRoleBinding` subject | Lets the pipeline SA call ClusterInterceptors |
592+
593+
#### Configuration fields
594+
595+
```yaml
596+
spec:
597+
platforms:
598+
openshift:
599+
namespaceSync:
600+
createPipelineSA: true # create/maintain the pipeline SA
601+
createSCCRoleBinding: true # create/maintain pipelines-scc-rolebinding
602+
createEditRoleBinding: true # create/maintain openshift-pipelines-edit
603+
createCABundles: true # inject CA bundle ConfigMaps
604+
605+
# Optional: restrict which namespaces are synced.
606+
# Omit entirely to sync all non-system namespaces (default).
607+
# Set to {} to opt out all namespaces without changing the flags above.
608+
namespaceSelector:
609+
matchLabels:
610+
pipelines.openshift.io/sync: "true"
611+
612+
# Optional: automatically bind secrets to the pipeline SA.
613+
# Use secretName for an exact name, or labelSelector to match by label.
614+
secretBindings:
615+
- secretName: pipeline-quay-openshift # Quay Bridge robot account secret
616+
- labelSelector:
617+
matchLabels:
618+
quay-integration: my-quay # all secrets with this label
619+
```
620+
621+
All boolean fields default to `true` when the `namespaceSync` block is present.
622+
623+
#### Disabling individual features
624+
625+
Set the flag to `false` to stop managing that resource class. Existing resources
626+
are **not deleted** — the controller simply stops reconciling them:
627+
628+
```yaml
629+
spec:
630+
platforms:
631+
openshift:
632+
namespaceSync:
633+
createEditRoleBinding: false # do not create openshift-pipelines-edit
634+
```
635+
636+
#### Restricting sync to specific namespaces
637+
638+
Use `namespaceSelector` to limit which namespaces the controller acts on.
639+
Label namespaces you want synced, then configure the selector to match:
640+
641+
```bash
642+
# Label a namespace to opt in
643+
oc label namespace my-project pipelines.openshift.io/sync=true
644+
```
645+
646+
```yaml
647+
spec:
648+
platforms:
649+
openshift:
650+
namespaceSync:
651+
namespaceSelector:
652+
matchLabels:
653+
pipelines.openshift.io/sync: "true"
654+
```
655+
656+
To disable sync for **all** namespaces while keeping the feature flags intact,
657+
set an empty selector:
658+
659+
```yaml
660+
namespaceSync:
661+
namespaceSelector: {} # matches nothing → no namespace is synced
662+
```
663+
664+
#### Quay Bridge secret auto-binding
665+
666+
When the [Quay Bridge Operator](https://github.com/quay/quay-bridge-operator) is
667+
installed, it creates a robot-account secret named `pipeline-quay-openshift` in
668+
each namespace. Declare a `secretBinding` to have the NamespaceSyncController
669+
automatically bind that secret to the `pipeline` SA as an image pull secret:
670+
671+
```yaml
672+
spec:
673+
platforms:
674+
openshift:
675+
namespaceSync:
676+
secretBindings:
677+
- secretName: pipeline-quay-openshift
678+
```
679+
680+
Once configured:
681+
- When the secret appears in a namespace it is added to both `imagePullSecrets`
682+
and `secrets` on the `pipeline` SA within seconds.
683+
- When the secret is deleted the reference is removed automatically.
684+
685+
#### Migration from legacy `spec.params`
686+
687+
Older releases controlled this behaviour through `spec.params` entries. The
688+
operator automatically migrates these on the first webhook call after an upgrade:
689+
690+
| Legacy `spec.params` | Typed field |
691+
|---|---|
692+
| `createRbacResource: "false"` | `createPipelineSA`, `createSCCRoleBinding`, `createEditRoleBinding` all set to `false` |
693+
| `createCABundleConfigMaps: "false"` | `createCABundles: false` |
694+
| `legacyPipelineRbac: "false"` | `createEditRoleBinding: false` |
695+
696+
After migration the legacy params are removed from `spec.params` and the typed
697+
fields take effect. There is no need to manually update the TektonConfig CR.
698+
699+
---
700+
578701
### OpenShiftPipelinesAsCode
579702

580703
The PipelinesAsCode section allows you to customize the Pipelines as Code features on both Kubernetes and OpenShift. When you change the TektonConfig CR, the Operator automatically applies the settings to custom resources and configmaps in your installation. On Kubernetes, configure `spec.platforms.kubernetes.pipelinesAsCode` (the managed CR remains `OpenShiftPipelinesAsCode` for API compatibility).

pkg/apis/operator/v1alpha1/openshift_platform.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,14 @@ type NamespaceSyncConfig struct {
8787
// Each entry must set exactly one of labelSelector or secretName.
8888
// +optional
8989
SecretBindings []SecretBinding `json:"secretBindings,omitempty"`
90+
91+
// NamespaceSelector is an optional label selector that restricts which
92+
// namespaces are synced. When absent every non-system namespace is synced
93+
// (the default). Use matchLabels/matchExpressions to limit sync to a subset
94+
// of namespaces, or set it to an empty object ({}) to opt out all namespaces
95+
// without touching the individual feature flags.
96+
// +optional
97+
NamespaceSelector *metav1.LabelSelector `json:"namespaceSelector,omitempty"`
9098
}
9199

92100
// SecretBinding describes a secret (or class of secrets by label) that the

pkg/apis/operator/v1alpha1/tektonconfig_defaults.go

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,21 +29,51 @@ import (
2929
// the equivalent typed fields in spec.platforms.openshift.namespaceSync, then removes
3030
// the migrated params from the slice. Params that are already absent are left at their
3131
// typed-field defaults (true). This runs only on OpenShift.
32+
//
33+
// Original semantics (preserved here):
34+
// - createRbacResource=false → master RBAC switch off; disables SA, SCC RoleBinding,
35+
// AND edit RoleBinding, regardless of legacyPipelineRbac.
36+
// - createCABundleConfigMaps=false → disables CA bundle ConfigMaps only.
37+
// - legacyPipelineRbac=false → disables edit RoleBinding, but only when
38+
// createRbacResource is true (or absent).
3239
func migrateNamespaceSyncParams(tc *TektonConfig) {
3340
ns := tc.Spec.Platforms.OpenShift.NamespaceSync
41+
42+
// First pass: resolve the master RBAC switch so that its precedence over
43+
// legacyPipelineRbac is respected regardless of param ordering in the slice.
44+
masterRBACEnabled := true
45+
for _, p := range tc.Spec.Params {
46+
if p.Name == "createRbacResource" && p.Value == "false" {
47+
masterRBACEnabled = false
48+
break
49+
}
50+
}
51+
3452
remaining := tc.Spec.Params[:0]
3553
for _, p := range tc.Spec.Params {
3654
switch p.Name {
3755
case "createRbacResource":
3856
if ns.CreatePipelineSA == nil {
39-
ns.CreatePipelineSA = ptr.Bool(p.Value != "false")
57+
ns.CreatePipelineSA = ptr.Bool(masterRBACEnabled)
58+
}
59+
if !masterRBACEnabled {
60+
// createRbacResource=false disabled all per-namespace RBAC in the
61+
// old implementation. Carry that forward to the typed fields.
62+
if ns.CreateSCCRoleBinding == nil {
63+
ns.CreateSCCRoleBinding = ptr.Bool(false)
64+
}
65+
if ns.CreateEditRoleBinding == nil {
66+
ns.CreateEditRoleBinding = ptr.Bool(false)
67+
}
4068
}
4169
case "createCABundleConfigMaps":
4270
if ns.CreateCABundles == nil {
4371
ns.CreateCABundles = ptr.Bool(p.Value != "false")
4472
}
4573
case "legacyPipelineRbac":
46-
if ns.CreateEditRoleBinding == nil {
74+
// legacyPipelineRbac only had effect when createRbacResource was
75+
// enabled; the master switch took precedence when it was false.
76+
if masterRBACEnabled && ns.CreateEditRoleBinding == nil {
4777
ns.CreateEditRoleBinding = ptr.Bool(p.Value != "false")
4878
}
4979
default:

pkg/apis/operator/v1alpha1/zz_generated.deepcopy.go

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pkg/reconciler/openshift/namespacesync/controller.go

Lines changed: 66 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -26,14 +26,18 @@ import (
2626
pkgcommon "github.com/tektoncd/operator/pkg/common"
2727

2828
corev1 "k8s.io/api/core/v1"
29+
rbacv1 "k8s.io/api/rbac/v1"
30+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
31+
"k8s.io/apimachinery/pkg/labels"
2932
"k8s.io/apimachinery/pkg/types"
3033
"k8s.io/client-go/tools/cache"
3134
kubeclient "knative.dev/pkg/client/injection/kube/client"
3235
nsinformer "knative.dev/pkg/client/injection/kube/informers/core/v1/namespace"
36+
secretinformer "knative.dev/pkg/client/injection/kube/informers/core/v1/secret"
3337
sainformer "knative.dev/pkg/client/injection/kube/informers/core/v1/serviceaccount"
38+
rbinformer "knative.dev/pkg/client/injection/kube/informers/rbac/v1/rolebinding"
3439
"knative.dev/pkg/configmap"
3540
"knative.dev/pkg/controller"
36-
secretinformer "knative.dev/pkg/injection/clients/namespacedkube/informers/core/v1/secret"
3741
"knative.dev/pkg/logging"
3842
)
3943

@@ -45,6 +49,7 @@ func NewController(ctx context.Context, _ configmap.Watcher) *controller.Impl {
4549
nsInf := nsinformer.Get(ctx)
4650
saInf := sainformer.Get(ctx)
4751
secretInf := secretinformer.Get(ctx)
52+
rbInf := rbinformer.Get(ctx)
4853
tcInf := tektonConfigInformer.Get(ctx)
4954

5055
rec := &Reconciler{
@@ -132,15 +137,20 @@ func NewController(ctx context.Context, _ configmap.Watcher) *controller.Impl {
132137
// - A newly created secret that matches a binding rule is bound immediately.
133138
// - A deleted named secret is unbound from the pipeline SA.
134139
//
135-
// Only trigger re-reconciliation when NamespaceSync has SecretBindings
136-
// configured, to avoid a thundering herd on clusters without secret bindings.
140+
// We use the cluster-wide kube factory here (NOT the namespacedkube one)
141+
// because we need to observe secrets in all user namespaces, not just the
142+
// operator's own namespace.
143+
//
144+
// To avoid a thundering-herd on clusters that don't use secret bindings,
145+
// we only enqueue when NamespaceSync has SecretBindings configured AND the
146+
// secret name/labels match at least one binding rule.
137147
if _, err := secretInf.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
138148
AddFunc: func(obj interface{}) {
139149
secret, ok := obj.(*corev1.Secret)
140150
if !ok {
141151
return
142152
}
143-
if namespaceSyncHasSecretBindings(tcInf.Lister()) {
153+
if secretMatchesBinding(secret, tcInf.Lister()) {
144154
impl.EnqueueKey(types.NamespacedName{Name: secret.Namespace})
145155
}
146156
},
@@ -154,14 +164,41 @@ func NewController(ctx context.Context, _ configmap.Watcher) *controller.Impl {
154164
return
155165
}
156166
}
157-
if namespaceSyncHasSecretBindings(tcInf.Lister()) {
167+
if secretMatchesBinding(secret, tcInf.Lister()) {
158168
impl.EnqueueKey(types.NamespacedName{Name: secret.Namespace})
159169
}
160170
},
161171
}); err != nil {
162172
logger.Panicf("Couldn't register Secret informer event handler: %v", err)
163173
}
164174

175+
// RoleBinding Delete → re-enqueue the namespace for self-healing.
176+
// We only watch the two RoleBindings that NamespaceSyncController owns:
177+
// pipelines-scc-rolebinding and openshift-pipelines-edit. Watching all
178+
// RoleBindings would be too noisy; we filter by name at the handler level.
179+
managedRoleBindings := map[string]struct{}{
180+
pipelinesSCCRoleBinding: {},
181+
PipelineRoleBinding: {},
182+
}
183+
if _, err := rbInf.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
184+
DeleteFunc: func(obj interface{}) {
185+
rb, ok := obj.(*rbacv1.RoleBinding)
186+
if !ok {
187+
if d, ok := obj.(cache.DeletedFinalStateUnknown); ok {
188+
rb, ok = d.Obj.(*rbacv1.RoleBinding)
189+
}
190+
if !ok {
191+
return
192+
}
193+
}
194+
if _, watched := managedRoleBindings[rb.Name]; watched {
195+
impl.EnqueueKey(types.NamespacedName{Name: rb.Namespace})
196+
}
197+
},
198+
}); err != nil {
199+
logger.Panicf("Couldn't register RoleBinding informer event handler: %v", err)
200+
}
201+
165202
// TektonConfig changed → re-enqueue all namespaces only when the NamespaceSync
166203
// config itself changed. Unrelated TektonConfig field changes (e.g. pipeline
167204
// options, pruner settings) must not trigger a full namespace sweep — that
@@ -190,16 +227,35 @@ func NewController(ctx context.Context, _ configmap.Watcher) *controller.Impl {
190227
return impl
191228
}
192229

193-
// namespaceSyncHasSecretBindings returns true when TektonConfig has at least one
194-
// SecretBinding configured. Used to short-circuit Secret event handling when
195-
// no secret binding is needed.
196-
func namespaceSyncHasSecretBindings(lister interface {
230+
// secretMatchesBinding returns true when the given secret matches at least one
231+
// SecretBinding rule in the current TektonConfig. This is used to filter Secret
232+
// events so we only re-enqueue namespaces for secrets we actually care about,
233+
// avoiding unnecessary reconciles for the high volume of dockercfg / token
234+
// secrets that Kubernetes creates automatically.
235+
func secretMatchesBinding(secret *corev1.Secret, lister interface {
197236
Get(string) (*v1alpha1.TektonConfig, error)
198237
}) bool {
199238
tc, err := lister.Get(v1alpha1.ConfigResourceName)
200239
if err != nil {
201240
return false
202241
}
203242
cfg := tc.Spec.Platforms.OpenShift.NamespaceSync
204-
return cfg != nil && len(cfg.SecretBindings) > 0
243+
if cfg == nil || len(cfg.SecretBindings) == 0 {
244+
return false
245+
}
246+
for _, b := range cfg.SecretBindings {
247+
if b.SecretName != "" && b.SecretName == secret.Name {
248+
return true
249+
}
250+
if b.LabelSelector != nil {
251+
sel, err := metav1.LabelSelectorAsSelector(b.LabelSelector)
252+
if err != nil {
253+
continue
254+
}
255+
if sel.Matches(labels.Set(secret.Labels)) {
256+
return true
257+
}
258+
}
259+
}
260+
return false
205261
}

0 commit comments

Comments
 (0)