Skip to content

Commit e79bd33

Browse files
ci: Add bundle-sync verification to operator PR workflow (#6751)
* ci: Add bundle-sync verification to operator PR workflow Signed-off-by: ntkathole <nikhilkathole2683@gmail.com> * fix: Regenerate operator bundle to sync RBAC permissions Run `make bundle` to pick up refined clusterrole/clusterrolebinding RBAC rules and drop stale subjectaccessreviews permission. Signed-off-by: ntkathole <nikhilkathole2683@gmail.com> * fix: Retry on AlreadyExists for cluster-scoped RBAC resources retry.RetryOnConflict only handles Conflict (resource version mismatch). When concurrent reconcile loops both GET a NotFound resource and race to Create it, the loser gets AlreadyExists which was not retried. Switch to retry.OnError with a predicate covering both IsConflict and IsAlreadyExists so the retry re-GETs the now-existing resource and proceeds with an update. Signed-off-by: ntkathole <nikhilkathole2683@gmail.com> Co-authored-by: Cursor <cursoragent@cursor.com> * fix: Regenerate operator bundle to sync RBAC and CRD changes Run make bundle to pick up the clusterrole get/list RBAC verbs and updated CRD field descriptions. Signed-off-by: ntkathole <nikhilkathole2683@gmail.com> Co-authored-by: Cursor <cursoragent@cursor.com> * fix: Handle AlreadyExists and Conflict as non-errors for cluster RBAC The controller-runtime cached client can return stale NotFound for cluster-scoped resources when the informer cache has not yet synced. This causes CreateOrUpdate to attempt a Create that fails with AlreadyExists. Retrying does not help because the cache remains stale during the short retry window. Treat AlreadyExists and Conflict as non-errors since the resource exists in the desired state. The next reconcile cycle will update its contents once the cache has synced. This follows the standard Kubernetes operator eventual-consistency pattern. Signed-off-by: ntkathole <nikhilkathole2683@gmail.com> Co-authored-by: Cursor <cursoragent@cursor.com> --------- Signed-off-by: ntkathole <nikhilkathole2683@gmail.com> Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent aade119 commit e79bd33

4 files changed

Lines changed: 72 additions & 31 deletions

File tree

.github/workflows/operator_pr.yml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,16 @@ jobs:
1919
run: make -C infra/feast-operator test
2020
- name: After code formatting, check for uncommitted differences
2121
run: git diff --exit-code infra/feast-operator
22+
- name: Regenerate bundle and verify CSV is in sync
23+
run: make -C infra/feast-operator bundle
24+
- name: Check for uncommitted bundle differences
25+
run: |
26+
# createdAt and operator-sdk builder version change every run;
27+
# ignore them so only real RBAC / structural drift fails the check.
28+
if ! git diff --exit-code \
29+
-I 'createdAt:' \
30+
-I 'operator-sdk-v' \
31+
infra/feast-operator/bundle/ infra/feast-operator/bundle.Dockerfile; then
32+
echo "::error::Bundle manifests are out of sync. Run 'make bundle' in infra/feast-operator/ and commit the result."
33+
exit 1
34+
fi

infra/feast-operator/bundle/manifests/feast-operator.clusterserviceversion.yaml

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,7 @@ metadata:
147147
}
148148
]
149149
capabilities: Basic Install
150-
createdAt: "2026-07-31T18:28:04Z"
150+
createdAt: "2026-08-18T16:15:46Z"
151151
operators.operatorframework.io/builder: operator-sdk-v1.41.0
152152
operators.operatorframework.io/project_layout: go.kubebuilder.io/v4
153153
name: feast-operator.v0.65.0
@@ -342,10 +342,36 @@ spec:
342342
- rbac.authorization.k8s.io
343343
resources:
344344
- clusterrolebindings
345+
verbs:
346+
- create
347+
- delete
348+
- get
349+
- list
350+
- update
351+
- apiGroups:
352+
- rbac.authorization.k8s.io
353+
resources:
354+
- clusterroles
355+
verbs:
356+
- create
357+
- get
358+
- list
359+
- apiGroups:
360+
- rbac.authorization.k8s.io
361+
resourceNames:
362+
- feast-discover-namespaces
363+
- feast-oidc-token-review
364+
- feast-token-review-cluster-role
365+
resources:
345366
- clusterroles
367+
verbs:
368+
- delete
369+
- update
370+
- apiGroups:
371+
- rbac.authorization.k8s.io
372+
resources:
346373
- rolebindings
347374
- roles
348-
- subjectaccessreviews
349375
verbs:
350376
- create
351377
- delete

infra/feast-operator/bundle/manifests/feast.dev_featurestores.yaml

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -59,8 +59,6 @@ spec:
5959
type: object
6060
noAuth:
6161
description: NoAuth explicitly disables authentication and authorization.
62-
When set to true, Feast services run without any auth checks.
63-
Use only for development or testing environments.
6462
type: boolean
6563
oidc:
6664
description: |-
@@ -6458,8 +6456,7 @@ spec:
64586456
type: object
64596457
noAuth:
64606458
description: NoAuth explicitly disables authentication and
6461-
authorization. When set to true, Feast services run without
6462-
any auth checks. Use only for development or testing environments.
6459+
authorization.
64636460
type: boolean
64646461
oidc:
64656462
description: |-
@@ -13065,8 +13062,6 @@ spec:
1306513062
type: object
1306613063
noAuth:
1306713064
description: NoAuth explicitly disables authentication and authorization.
13068-
When set to true, Feast services run without any auth checks.
13069-
Use only for development or testing environments.
1307013065
type: boolean
1307113066
oidc:
1307213067
description: |-
@@ -17581,8 +17576,7 @@ spec:
1758117576
type: object
1758217577
noAuth:
1758317578
description: NoAuth explicitly disables authentication and
17584-
authorization. When set to true, Feast services run without
17585-
any auth checks. Use only for development or testing environments.
17579+
authorization.
1758617580
type: boolean
1758717581
oidc:
1758817582
description: |-

infra/feast-operator/internal/controller/authz/authz.go

Lines changed: 29 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,10 @@ import (
88
feastdevv1 "github.com/feast-dev/feast/infra/feast-operator/api/v1"
99
"github.com/feast-dev/feast/infra/feast-operator/internal/controller/services"
1010
rbacv1 "k8s.io/api/rbac/v1"
11+
apierrors "k8s.io/apimachinery/pkg/api/errors"
1112
apimeta "k8s.io/apimachinery/pkg/api/meta"
1213
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
1314
"k8s.io/apimachinery/pkg/labels"
14-
"k8s.io/client-go/util/retry"
1515
"sigs.k8s.io/controller-runtime/pkg/client"
1616
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
1717
"sigs.k8s.io/controller-runtime/pkg/log"
@@ -146,17 +146,21 @@ func (authz *FeastAuthorization) createFeastRole() error {
146146

147147
func (authz *FeastAuthorization) createFeastClusterRole() error {
148148
logger := log.FromContext(authz.Handler.Context)
149-
return retry.RetryOnConflict(retry.DefaultRetry, func() error {
150-
clusterRole := authz.initFeastClusterRole()
151-
if op, err := controllerutil.CreateOrUpdate(authz.Handler.Context, authz.Handler.Client, clusterRole, controllerutil.MutateFn(func() error {
152-
return authz.setFeastClusterRole(clusterRole)
153-
})); err != nil {
154-
return err
155-
} else if op == controllerutil.OperationResultCreated || op == controllerutil.OperationResultUpdated {
156-
logger.Info("Successfully reconciled", "ClusterRole", clusterRole.Name, "operation", op)
157-
}
149+
clusterRole := authz.initFeastClusterRole()
150+
op, err := controllerutil.CreateOrUpdate(authz.Handler.Context, authz.Handler.Client, clusterRole, controllerutil.MutateFn(func() error {
151+
return authz.setFeastClusterRole(clusterRole)
152+
}))
153+
if apierrors.IsAlreadyExists(err) || apierrors.IsConflict(err) {
154+
logger.Info("ClusterRole conflict or already exists, will reconcile on next cycle", "ClusterRole", clusterRole.Name, "error", err)
158155
return nil
159-
})
156+
}
157+
if err != nil {
158+
return err
159+
}
160+
if op == controllerutil.OperationResultCreated || op == controllerutil.OperationResultUpdated {
161+
logger.Info("Successfully reconciled", "ClusterRole", clusterRole.Name, "operation", op)
162+
}
163+
return nil
160164
}
161165

162166
func (authz *FeastAuthorization) initFeastClusterRole() *rbacv1.ClusterRole {
@@ -228,17 +232,21 @@ func (authz *FeastAuthorization) setFeastClusterRoleBinding(clusterRoleBinding *
228232
// Create ClusterRoleBinding
229233
func (authz *FeastAuthorization) createFeastClusterRoleBinding() error {
230234
logger := log.FromContext(authz.Handler.Context)
231-
return retry.RetryOnConflict(retry.DefaultRetry, func() error {
232-
clusterRoleBinding := authz.initFeastClusterRoleBinding()
233-
if op, err := controllerutil.CreateOrUpdate(authz.Handler.Context, authz.Handler.Client, clusterRoleBinding, controllerutil.MutateFn(func() error {
234-
return authz.setFeastClusterRoleBinding(clusterRoleBinding)
235-
})); err != nil {
236-
return err
237-
} else if op == controllerutil.OperationResultCreated || op == controllerutil.OperationResultUpdated {
238-
logger.Info("Successfully reconciled", "ClusterRoleBinding", clusterRoleBinding.Name, "operation", op)
239-
}
235+
clusterRoleBinding := authz.initFeastClusterRoleBinding()
236+
op, err := controllerutil.CreateOrUpdate(authz.Handler.Context, authz.Handler.Client, clusterRoleBinding, controllerutil.MutateFn(func() error {
237+
return authz.setFeastClusterRoleBinding(clusterRoleBinding)
238+
}))
239+
if apierrors.IsAlreadyExists(err) || apierrors.IsConflict(err) {
240+
logger.Info("ClusterRoleBinding conflict or already exists, will reconcile on next cycle", "ClusterRoleBinding", clusterRoleBinding.Name, "error", err)
240241
return nil
241-
})
242+
}
243+
if err != nil {
244+
return err
245+
}
246+
if op == controllerutil.OperationResultCreated || op == controllerutil.OperationResultUpdated {
247+
logger.Info("Successfully reconciled", "ClusterRoleBinding", clusterRoleBinding.Name, "operation", op)
248+
}
249+
return nil
242250
}
243251

244252
func (authz *FeastAuthorization) initFeastRole() *rbacv1.Role {

0 commit comments

Comments
 (0)