diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index 35b9f51e..e5cd3e9d 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -119,6 +119,15 @@ rules: resources: - clusterrolebindings - clusterroles + verbs: + - create + - delete + - get + - list + - update +- apiGroups: + - rbac.authorization.k8s.io + resources: - rolebindings - roles verbs: diff --git a/controller/proposal/rbac.go b/controller/proposal/rbac.go index 3ff7fa63..4ea55f28 100644 --- a/controller/proposal/rbac.go +++ b/controller/proposal/rbac.go @@ -4,24 +4,31 @@ import ( "context" "fmt" "strings" + "sync/atomic" corev1 "k8s.io/api/core/v1" rbacv1 "k8s.io/api/rbac/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "sigs.k8s.io/controller-runtime/pkg/client" + logf "sigs.k8s.io/controller-runtime/pkg/log" agenticv1alpha1 "github.com/openshift/lightspeed-agentic-operator/api/v1alpha1" ) +// +kubebuilder:rbac:groups="",resources=serviceaccounts,verbs=create;delete;get + const ( - rbacNamespacesAnnotation = "agentic.openshift.io/rbac-namespaces" + rbacNamespacesAnnotation = "agentic.openshift.io/rbac-namespaces" + defaultReaderClusterRoleBinding = "lightspeed-agent-cluster-reader" ErrCreateExecutionSA = "create execution SA" ErrCreateRole = "create Role in" ErrCreateRoleBinding = "create RoleBinding in" ErrCreateClusterRole = "create ClusterRole" ErrCreateClusterRoleBinding = "create ClusterRoleBinding" + ErrAddReaderSubject = "add subject to reader ClusterRoleBinding" + ErrRemoveReaderSubject = "remove subject from reader ClusterRoleBinding" ErrDeleteRoleBinding = "delete RoleBinding in" ErrDeleteRole = "delete Role in" ErrDeleteClusterRoleBinding = "delete ClusterRoleBinding" @@ -29,7 +36,44 @@ const ( ErrDeleteExecutionSA = "delete execution SA" ) -// +kubebuilder:rbac:groups="",resources=serviceaccounts,verbs=create;delete;get +var readerRoleBinding atomic.Value + +func init() { + readerRoleBinding.Store(defaultReaderClusterRoleBinding) +} + +// discoverReaderBinding finds the ClusterRoleBinding for the lightspeed-agent SA +// when the default name doesn't exist. Updates readerRoleBinding on success. +func discoverReaderBinding(ctx context.Context, c client.Client, operatorNS string) error { + log := logf.FromContext(ctx) + log.Info("default reader ClusterRoleBinding not found, discovering by SA subject", LogKeyName, defaultReaderClusterRoleBinding) + + crbList := &rbacv1.ClusterRoleBindingList{} + if err := c.List(ctx, crbList); err != nil { + return fmt.Errorf("list ClusterRoleBindings for reader discovery: %w", err) + } + + var matches []string + for i := range crbList.Items { + for _, s := range crbList.Items[i].Subjects { + if s.Kind == rbacv1.ServiceAccountKind && s.Name == defaultSandboxSA && s.Namespace == operatorNS { + matches = append(matches, crbList.Items[i].Name) + break + } + } + } + + if len(matches) == 0 { + return fmt.Errorf("no ClusterRoleBinding found with subject %s/%s — ensure reader RBAC is configured", operatorNS, defaultSandboxSA) + } + if len(matches) > 1 { + log.Info("multiple ClusterRoleBindings found for lightspeed-agent SA", "all", matches, "selected", matches[0]) + } + + readerRoleBinding.Store(matches[0]) + log.Info("resolved reader ClusterRoleBinding", LogKeyName, matches[0]) + return nil +} // executionSAName returns the per-proposal ServiceAccount name for execution RBAC isolation. // Uses the same truncation pattern as executionRoleName. Collision is theoretically possible @@ -39,7 +83,8 @@ func executionSAName(proposal *agenticv1alpha1.Proposal) string { return truncateK8sName(fmt.Sprintf("ls-exec-%s-%s", proposal.Namespace, proposal.Name)) } -// ensureExecutionSA creates a per-proposal ServiceAccount for execution RBAC isolation. +// ensureExecutionSA creates a per-proposal ServiceAccount for execution RBAC isolation +// and adds it as a subject to the shared cluster-reader ClusterRoleBinding for base read access. // No owner reference — cross-namespace owner refs are unsupported by Kubernetes GC. // Cleanup is handled explicitly by cleanupExecutionRBAC (via finalizer on Proposal deletion). func ensureExecutionSA(ctx context.Context, c client.Client, proposal *agenticv1alpha1.Proposal, operatorNS string) (string, error) { @@ -54,9 +99,96 @@ func ensureExecutionSA(ctx context.Context, c client.Client, proposal *agenticv1 if err := c.Create(ctx, sa); err != nil && !apierrors.IsAlreadyExists(err) { return "", fmt.Errorf("%s %s: %w", ErrCreateExecutionSA, saName, err) } + + if err := addReaderSubject(ctx, c, saName, operatorNS); err != nil { + return "", err + } + return saName, nil } +// addReaderSubject adds the SA as a subject to the shared cluster-reader ClusterRoleBinding. +// Idempotent — skips if the subject is already present. Retries on conflict (optimistic locking). +// If the default binding name doesn't exist, discovers the correct name by scanning SA subjects. +func addReaderSubject(ctx context.Context, c client.Client, saName, namespace string) error { + subject := rbacv1.Subject{ + Kind: rbacv1.ServiceAccountKind, + Name: saName, + Namespace: namespace, + } + + for attempts := 0; attempts < 3; attempts++ { + bindingName := readerRoleBinding.Load().(string) + crb := &rbacv1.ClusterRoleBinding{} + if err := c.Get(ctx, client.ObjectKey{Name: bindingName}, crb); err != nil { + if apierrors.IsNotFound(err) { + if discoverErr := discoverReaderBinding(ctx, c, namespace); discoverErr != nil { + return fmt.Errorf("%s: %w", ErrAddReaderSubject, discoverErr) + } + continue + } + return fmt.Errorf("%s: %w", ErrAddReaderSubject, err) + } + + for _, s := range crb.Subjects { + if s.Kind == subject.Kind && s.Name == subject.Name && s.Namespace == subject.Namespace { + return nil + } + } + + crb.Subjects = append(crb.Subjects, subject) + err := c.Update(ctx, crb) + if err == nil { + return nil + } + if !apierrors.IsConflict(err) { + return fmt.Errorf("%s: %w", ErrAddReaderSubject, err) + } + } + return fmt.Errorf("%s: conflict after retries", ErrAddReaderSubject) +} + +// removeReaderSubject removes the SA from the shared cluster-reader ClusterRoleBinding. +// Idempotent — no-op if the subject is not present. Retries on conflict (optimistic locking). +// If the default binding name doesn't exist, discovers the correct name (mirrors addReaderSubject). +func removeReaderSubject(ctx context.Context, c client.Client, saName, namespace string) error { + for attempts := 0; attempts < 3; attempts++ { + bindingName := readerRoleBinding.Load().(string) + crb := &rbacv1.ClusterRoleBinding{} + if err := c.Get(ctx, client.ObjectKey{Name: bindingName}, crb); err != nil { + if apierrors.IsNotFound(err) { + if discoverErr := discoverReaderBinding(ctx, c, namespace); discoverErr != nil { + return nil + } + continue + } + return fmt.Errorf("%s: %w", ErrRemoveReaderSubject, err) + } + + filtered := make([]rbacv1.Subject, 0, len(crb.Subjects)) + for _, s := range crb.Subjects { + if s.Kind == rbacv1.ServiceAccountKind && s.Name == saName && s.Namespace == namespace { + continue + } + filtered = append(filtered, s) + } + + if len(filtered) == len(crb.Subjects) { + return nil + } + + crb.Subjects = filtered + err := c.Update(ctx, crb) + if err == nil { + return nil + } + if !apierrors.IsConflict(err) { + return fmt.Errorf("%s: %w", ErrRemoveReaderSubject, err) + } + } + return fmt.Errorf("%s: conflict after retries", ErrRemoveReaderSubject) +} + // deleteExecutionSA explicitly deletes the per-proposal ServiceAccount after execution completes. func deleteExecutionSA(ctx context.Context, c client.Client, proposal *agenticv1alpha1.Proposal, operatorNS string) error { saName := executionSAName(proposal) @@ -169,6 +301,11 @@ func cleanupExecutionRBAC(ctx context.Context, c client.Client, proposal *agenti return fmt.Errorf("%s %s: %w", ErrDeleteClusterRole, crName, err) } + saName := executionSAName(proposal) + if err := removeReaderSubject(ctx, c, saName, operatorNS); err != nil { + return err + } + if err := deleteExecutionSA(ctx, c, proposal, operatorNS); err != nil { return fmt.Errorf("%s: %w", ErrDeleteExecutionSA, err) } diff --git a/controller/proposal/rbac_test.go b/controller/proposal/rbac_test.go index 7c55daaa..191ee4d1 100644 --- a/controller/proposal/rbac_test.go +++ b/controller/proposal/rbac_test.go @@ -2,25 +2,44 @@ package proposal import ( "context" + "fmt" "strings" "testing" corev1 "k8s.io/api/core/v1" rbacv1 "k8s.io/api/rbac/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client/fake" + "sigs.k8s.io/controller-runtime/pkg/client/interceptor" agenticv1alpha1 "github.com/openshift/lightspeed-agentic-operator/api/v1alpha1" ) +// readerBinding returns the pre-existing cluster-reader ClusterRoleBinding fixture +// that must exist for execution SA setup to succeed. +func readerBinding() *rbacv1.ClusterRoleBinding { + return &rbacv1.ClusterRoleBinding{ + ObjectMeta: metav1.ObjectMeta{Name: defaultReaderClusterRoleBinding}, + RoleRef: rbacv1.RoleRef{APIGroup: rbacv1.GroupName, Kind: "ClusterRole", Name: "cluster-reader"}, + Subjects: []rbacv1.Subject{{ + Kind: rbacv1.ServiceAccountKind, + Name: "lightspeed-agent", + Namespace: "default", + }}, + } +} + // --------------------------------------------------------------------------- // ensureExecutionRBAC // --------------------------------------------------------------------------- func TestEnsureExecutionRBAC_NamespaceScopedOnly(t *testing.T) { ctx := context.Background() - fc := fake.NewClientBuilder().WithScheme(testScheme()).Build() + fc := fake.NewClientBuilder().WithScheme(testScheme()).WithObjects(readerBinding()).Build() proposal := &agenticv1alpha1.Proposal{ ObjectMeta: metav1.ObjectMeta{Name: "fix-oom", Namespace: "default"}, @@ -108,7 +127,7 @@ func TestEnsureExecutionRBAC_NamespaceScopedOnly(t *testing.T) { func TestEnsureExecutionRBAC_ClusterScopedOnly(t *testing.T) { ctx := context.Background() - fc := fake.NewClientBuilder().WithScheme(testScheme()).Build() + fc := fake.NewClientBuilder().WithScheme(testScheme()).WithObjects(readerBinding()).Build() proposal := &agenticv1alpha1.Proposal{ ObjectMeta: metav1.ObjectMeta{Name: "check-nodes", Namespace: "default"}, @@ -152,7 +171,7 @@ func TestEnsureExecutionRBAC_ClusterScopedOnly(t *testing.T) { func TestEnsureExecutionRBAC_BothScopes(t *testing.T) { ctx := context.Background() - fc := fake.NewClientBuilder().WithScheme(testScheme()).Build() + fc := fake.NewClientBuilder().WithScheme(testScheme()).WithObjects(readerBinding()).Build() proposal := &agenticv1alpha1.Proposal{ ObjectMeta: metav1.ObjectMeta{Name: "full-fix", Namespace: "default"}, @@ -188,7 +207,7 @@ func TestEnsureExecutionRBAC_BothScopes(t *testing.T) { func TestEnsureExecutionRBAC_MultipleNamespaces(t *testing.T) { ctx := context.Background() - fc := fake.NewClientBuilder().WithScheme(testScheme()).Build() + fc := fake.NewClientBuilder().WithScheme(testScheme()).WithObjects(readerBinding()).Build() proposal := &agenticv1alpha1.Proposal{ ObjectMeta: metav1.ObjectMeta{Name: "multi-ns", Namespace: "default"}, @@ -226,7 +245,7 @@ func TestEnsureExecutionRBAC_MultipleNamespaces(t *testing.T) { func TestEnsureExecutionRBAC_Idempotent(t *testing.T) { ctx := context.Background() - fc := fake.NewClientBuilder().WithScheme(testScheme()).Build() + fc := fake.NewClientBuilder().WithScheme(testScheme()).WithObjects(readerBinding()).Build() proposal := &agenticv1alpha1.Proposal{ ObjectMeta: metav1.ObjectMeta{Name: "idem", Namespace: "default"}, @@ -253,7 +272,7 @@ func TestEnsureExecutionRBAC_Idempotent(t *testing.T) { func TestEnsureExecutionRBAC_NilResult(t *testing.T) { ctx := context.Background() - fc := fake.NewClientBuilder().WithScheme(testScheme()).Build() + fc := fake.NewClientBuilder().WithScheme(testScheme()).WithObjects(readerBinding()).Build() proposal := &agenticv1alpha1.Proposal{ ObjectMeta: metav1.ObjectMeta{Name: "no-rbac", Namespace: "default"}, @@ -266,7 +285,7 @@ func TestEnsureExecutionRBAC_NilResult(t *testing.T) { func TestEnsureExecutionRBAC_EmptyRules(t *testing.T) { ctx := context.Background() - fc := fake.NewClientBuilder().WithScheme(testScheme()).Build() + fc := fake.NewClientBuilder().WithScheme(testScheme()).WithObjects(readerBinding()).Build() proposal := &agenticv1alpha1.Proposal{ ObjectMeta: metav1.ObjectMeta{Name: "empty-rules", Namespace: "default"}, @@ -287,7 +306,7 @@ func TestEnsureExecutionRBAC_EmptyRules(t *testing.T) { func TestEnsureExecutionRBAC_NamespacesFromRBACRules(t *testing.T) { ctx := context.Background() - fc := fake.NewClientBuilder().WithScheme(testScheme()).Build() + fc := fake.NewClientBuilder().WithScheme(testScheme()).WithObjects(readerBinding()).Build() ns1 := "app-ns" ns2 := "data-ns" @@ -316,7 +335,7 @@ func TestEnsureExecutionRBAC_NamespacesFromRBACRules(t *testing.T) { func TestEnsureExecutionRBAC_ResourceNames(t *testing.T) { ctx := context.Background() - fc := fake.NewClientBuilder().WithScheme(testScheme()).Build() + fc := fake.NewClientBuilder().WithScheme(testScheme()).WithObjects(readerBinding()).Build() proposal := &agenticv1alpha1.Proposal{ ObjectMeta: metav1.ObjectMeta{Name: "with-names", Namespace: "default"}, @@ -351,7 +370,7 @@ func TestEnsureExecutionRBAC_ResourceNames(t *testing.T) { func TestCleanupExecutionRBAC_NamespaceAndCluster(t *testing.T) { ctx := context.Background() - fc := fake.NewClientBuilder().WithScheme(testScheme()).Build() + fc := fake.NewClientBuilder().WithScheme(testScheme()).WithObjects(readerBinding()).Build() proposal := &agenticv1alpha1.Proposal{ ObjectMeta: metav1.ObjectMeta{ @@ -416,7 +435,7 @@ func TestCleanupExecutionRBAC_NamespaceAndCluster(t *testing.T) { func TestCleanupExecutionRBAC_NoAnnotation(t *testing.T) { ctx := context.Background() - fc := fake.NewClientBuilder().WithScheme(testScheme()).Build() + fc := fake.NewClientBuilder().WithScheme(testScheme()).WithObjects(readerBinding()).Build() proposal := &agenticv1alpha1.Proposal{ ObjectMeta: metav1.ObjectMeta{Name: "no-annot", Namespace: "default"}, @@ -447,7 +466,7 @@ func TestCleanupExecutionRBAC_NoAnnotation(t *testing.T) { func TestCleanupExecutionRBAC_MissingResources(t *testing.T) { ctx := context.Background() - fc := fake.NewClientBuilder().WithScheme(testScheme()).Build() + fc := fake.NewClientBuilder().WithScheme(testScheme()).WithObjects(readerBinding()).Build() proposal := &agenticv1alpha1.Proposal{ ObjectMeta: metav1.ObjectMeta{ @@ -792,3 +811,138 @@ func TestRBACLabels_TruncatesLongProposalName(t *testing.T) { t.Errorf("proposal label = %q, want %q", labels[LabelProposal], strings.Repeat("a", 63)) } } + +// --------------------------------------------------------------------------- +// addReaderSubject / removeReaderSubject / discoverReaderBinding +// --------------------------------------------------------------------------- + +func TestAddReaderSubject_Idempotent(t *testing.T) { + ctx := context.Background() + readerRoleBinding.Store(defaultReaderClusterRoleBinding) + fc := fake.NewClientBuilder().WithScheme(testScheme()).WithObjects(readerBinding()).Build() + + if err := addReaderSubject(ctx, fc, "ls-exec-test", "default"); err != nil { + t.Fatalf("first add: %v", err) + } + if err := addReaderSubject(ctx, fc, "ls-exec-test", "default"); err != nil { + t.Fatalf("second add: %v", err) + } + + var crb rbacv1.ClusterRoleBinding + if err := fc.Get(ctx, types.NamespacedName{Name: defaultReaderClusterRoleBinding}, &crb); err != nil { + t.Fatalf("get binding: %v", err) + } + + count := 0 + for _, s := range crb.Subjects { + if s.Name == "ls-exec-test" { + count++ + } + } + if count != 1 { + t.Fatalf("expected 1 subject entry, got %d", count) + } +} + +func TestRemoveReaderSubject_NotPresent(t *testing.T) { + ctx := context.Background() + readerRoleBinding.Store(defaultReaderClusterRoleBinding) + fc := fake.NewClientBuilder().WithScheme(testScheme()).WithObjects(readerBinding()).Build() + + if err := removeReaderSubject(ctx, fc, "ls-exec-nonexistent", "default"); err != nil { + t.Fatalf("remove non-existent subject should no-op, got: %v", err) + } +} + +func TestDiscoverReaderBinding_NoMatches(t *testing.T) { + ctx := context.Background() + readerRoleBinding.Store(defaultReaderClusterRoleBinding) + + unrelatedBinding := &rbacv1.ClusterRoleBinding{ + ObjectMeta: metav1.ObjectMeta{Name: "unrelated-binding"}, + RoleRef: rbacv1.RoleRef{APIGroup: rbacv1.GroupName, Kind: "ClusterRole", Name: "admin"}, + Subjects: []rbacv1.Subject{{ + Kind: rbacv1.ServiceAccountKind, + Name: "other-sa", + Namespace: "other-ns", + }}, + } + fc := fake.NewClientBuilder().WithScheme(testScheme()).WithObjects(unrelatedBinding).Build() + + err := discoverReaderBinding(ctx, fc, "default") + if err == nil { + t.Fatal("expected error when no matching bindings found") + } + if !strings.Contains(err.Error(), "no ClusterRoleBinding found") { + t.Fatalf("unexpected error: %v", err) + } +} + +func TestDiscoverReaderBinding_MultipleMatches(t *testing.T) { + ctx := context.Background() + readerRoleBinding.Store(defaultReaderClusterRoleBinding) + + binding1 := &rbacv1.ClusterRoleBinding{ + ObjectMeta: metav1.ObjectMeta{Name: "custom-reader-1"}, + RoleRef: rbacv1.RoleRef{APIGroup: rbacv1.GroupName, Kind: "ClusterRole", Name: "cluster-reader"}, + Subjects: []rbacv1.Subject{{ + Kind: rbacv1.ServiceAccountKind, + Name: defaultSandboxSA, + Namespace: "default", + }}, + } + binding2 := &rbacv1.ClusterRoleBinding{ + ObjectMeta: metav1.ObjectMeta{Name: "custom-reader-2"}, + RoleRef: rbacv1.RoleRef{APIGroup: rbacv1.GroupName, Kind: "ClusterRole", Name: "cluster-monitoring-view"}, + Subjects: []rbacv1.Subject{{ + Kind: rbacv1.ServiceAccountKind, + Name: defaultSandboxSA, + Namespace: "default", + }}, + } + fc := fake.NewClientBuilder().WithScheme(testScheme()).WithObjects(binding1, binding2).Build() + + err := discoverReaderBinding(ctx, fc, "default") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + resolved := readerRoleBinding.Load().(string) + if resolved != "custom-reader-1" && resolved != "custom-reader-2" { + t.Fatalf("expected one of the custom bindings, got: %s", resolved) + } +} + +func TestAddReaderSubject_ConflictRetryExhaustion(t *testing.T) { + ctx := context.Background() + readerRoleBinding.Store(defaultReaderClusterRoleBinding) + + callCount := 0 + fc := fake.NewClientBuilder(). + WithScheme(testScheme()). + WithObjects(readerBinding()). + WithInterceptorFuncs(interceptor.Funcs{ + Update: func(ctx context.Context, c client.WithWatch, obj client.Object, opts ...client.UpdateOption) error { + if _, ok := obj.(*rbacv1.ClusterRoleBinding); ok { + callCount++ + return apierrors.NewConflict( + schema.GroupResource{Group: "rbac.authorization.k8s.io", Resource: "clusterrolebindings"}, + defaultReaderClusterRoleBinding, + fmt.Errorf("modified concurrently"), + ) + } + return c.Update(ctx, obj, opts...) + }, + }).Build() + + err := addReaderSubject(ctx, fc, "ls-exec-conflict-test", "default") + if err == nil { + t.Fatal("expected error after conflict retries exhausted") + } + if !strings.Contains(err.Error(), "conflict after retries") { + t.Fatalf("unexpected error: %v", err) + } + if callCount != 3 { + t.Fatalf("expected 3 update attempts, got %d", callCount) + } +} diff --git a/controller/proposal/reconciler.go b/controller/proposal/reconciler.go index 4063a1c5..53473a05 100644 --- a/controller/proposal/reconciler.go +++ b/controller/proposal/reconciler.go @@ -45,7 +45,7 @@ type ProposalReconciler struct { // +kubebuilder:rbac:groups=agentic.openshift.io,resources=escalationresults,verbs=get;list;watch;create // +kubebuilder:rbac:groups=agentic.openshift.io,resources=analysisresults/status;executionresults/status;verificationresults/status;escalationresults/status,verbs=get;patch;update // +kubebuilder:rbac:groups=rbac.authorization.k8s.io,resources=roles;rolebindings,verbs=get;create;delete -// +kubebuilder:rbac:groups=rbac.authorization.k8s.io,resources=clusterroles;clusterrolebindings,verbs=get;create;delete +// +kubebuilder:rbac:groups=rbac.authorization.k8s.io,resources=clusterroles;clusterrolebindings,verbs=get;list;create;update;delete // +kubebuilder:rbac:groups=agentic.openshift.io,resources=agenticolsconfigs,verbs=get;list;watch func (r *ProposalReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { diff --git a/controller/proposal/reconciler_test.go b/controller/proposal/reconciler_test.go index 1fd40fc1..405c8935 100644 --- a/controller/proposal/reconciler_test.go +++ b/controller/proposal/reconciler_test.go @@ -150,13 +150,25 @@ func testAutoApprovePolicyWithMaxAttempts(maxAttempts int32) *agenticv1alpha1.Ap // objects needed to resolve a full workflow. func defaultObjects() []client.Object { return []client.Object{ - testDefaultAgent(), testLLM("smart"), testAutoApprovePolicy(), + testDefaultAgent(), testLLM("smart"), testAutoApprovePolicy(), testReaderClusterRoleBinding(), } } func defaultObjectsWithMaxAttempts(maxAttempts int32) []client.Object { return []client.Object{ - testDefaultAgent(), testLLM("smart"), testAutoApprovePolicyWithMaxAttempts(maxAttempts), + testDefaultAgent(), testLLM("smart"), testAutoApprovePolicyWithMaxAttempts(maxAttempts), testReaderClusterRoleBinding(), + } +} + +func testReaderClusterRoleBinding() *rbacv1.ClusterRoleBinding { + return &rbacv1.ClusterRoleBinding{ + ObjectMeta: metav1.ObjectMeta{Name: defaultReaderClusterRoleBinding}, + RoleRef: rbacv1.RoleRef{APIGroup: rbacv1.GroupName, Kind: "ClusterRole", Name: "cluster-reader"}, + Subjects: []rbacv1.Subject{{ + Kind: rbacv1.ServiceAccountKind, + Name: "lightspeed-agent", + Namespace: "default", + }}, } }