-
Notifications
You must be signed in to change notification settings - Fork 0
feat: implement clusterrolebinding primitive #36
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
27 commits
Select commit
Hold shift + click to select a range
b50c0ad
Add BindingSubjectsEditor for RoleBinding/ClusterRoleBinding subjects
sourcehawk 6141584
Add ClusterRoleBinding primitive package
sourcehawk 8828947
Add ClusterRoleBinding primitive documentation
sourcehawk daee41d
Add ClusterRoleBinding primitive example
sourcehawk c20bed0
format
sourcehawk 28ddf45
address copilot comments
sourcehawk c27ce01
Address Copilot review: namespace validation and StaticResource type
sourcehawk eebdf9d
Preserve ResourceVersion in DefaultFieldApplicator on updates
sourcehawk a527376
preserve server-managed metadata in default field applicator
sourcehawk a9ce977
Refactor builder to use generic.NewStaticBuilder and address review c…
sourcehawk 82dd2d8
Update ClusterRoleBinding docs to document server-managed field prese…
sourcehawk 1a69ad8
Fix ClusterRoleBinding mutator to not call beginFeature in constructor
sourcehawk 189eeab
Add ClusterRoleBinding to primitives index and run-examples target
sourcehawk d142266
Export BeginFeature() to match updated FeatureMutator interface
sourcehawk 0d026c2
Merge remote-tracking branch 'origin/main' into feature/clusterrolebi…
sourcehawk 2f6e17b
Format markdown files with prettier
sourcehawk 4cbc0c0
Merge remote-tracking branch 'origin/main' into feature/clusterrolebi…
sourcehawk a015957
Do not initialize an empty plan on ClusterRoleBinding mutator constru…
sourcehawk 660628d
Fix BindingSubjectsEditor docs to match actual API
sourcehawk afcf01a
fix lint
sourcehawk a22b721
Merge remote-tracking branch 'origin/main' into feature/clusterrolebi…
sourcehawk 214d7f9
Remove field applicators and flavors from clusterrolebinding primitive
sourcehawk a1cb596
Fix example to remove references to deleted field applicator API
sourcehawk 9e856e7
Remove stale field flavors reference from clusterrolebinding example …
sourcehawk 80a9c00
Address PR review: safe mutator API, side-effect-free Raw(), test imp…
sourcehawk bb60841
Merge origin/main into feature/clusterrolebinding-primitive
sourcehawk 888e1f3
resolve
sourcehawk File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,197 @@ | ||
| # ClusterRoleBinding Primitive | ||
|
|
||
| The `clusterrolebinding` primitive is the framework's built-in static abstraction for managing Kubernetes | ||
| `ClusterRoleBinding` resources. It integrates with the component lifecycle and provides a structured mutation API for | ||
| managing `.subjects` entries and object metadata. | ||
|
|
||
| ## Capabilities | ||
|
|
||
| | Capability | Detail | | ||
| | --------------------- | ------------------------------------------------------------------------------------------------------------ | | ||
| | **Static lifecycle** | No health tracking, grace periods, or suspension — the resource is reconciled to desired state | | ||
| | **Cluster-scoped** | Cluster-scoped resource — Build() validates Name and requires metadata.namespace to be empty (errors if set) | | ||
| | **Mutation pipeline** | Typed editors for `.subjects` entries and object metadata, with a raw escape hatch for free-form access | | ||
| | **Data extraction** | Reads generated or updated values back from the reconciled ClusterRoleBinding after each sync cycle | | ||
|
|
||
| ## Building a ClusterRoleBinding Primitive | ||
|
|
||
| ```go | ||
| import "github.com/sourcehawk/operator-component-framework/pkg/primitives/clusterrolebinding" | ||
|
|
||
| base := &rbacv1.ClusterRoleBinding{ | ||
| ObjectMeta: metav1.ObjectMeta{ | ||
| Name: "app-cluster-admin", | ||
| }, | ||
| RoleRef: rbacv1.RoleRef{ | ||
| APIGroup: "rbac.authorization.k8s.io", | ||
| Kind: "ClusterRole", | ||
| Name: "cluster-admin", | ||
| }, | ||
| Subjects: []rbacv1.Subject{ | ||
| { | ||
| Kind: "ServiceAccount", | ||
| Name: "app-sa", | ||
| Namespace: "default", | ||
| }, | ||
| }, | ||
| } | ||
|
|
||
| resource, err := clusterrolebinding.NewBuilder(base). | ||
| WithMutation(MySubjectMutation(owner.Spec.Version)). | ||
| Build() | ||
| ``` | ||
|
|
||
| ## Mutations | ||
|
|
||
| Mutations are the primary mechanism for modifying a `ClusterRoleBinding` beyond its baseline. Each mutation is a named | ||
| function that receives a `*Mutator` and records edit intent through typed editors. | ||
|
|
||
| The `Feature` field controls when a mutation applies. Leaving it nil applies the mutation unconditionally. A feature | ||
| with no version constraints and no `When()` conditions is also always enabled: | ||
|
|
||
| ```go | ||
| func MySubjectMutation(version string) clusterrolebinding.Mutation { | ||
| return clusterrolebinding.Mutation{ | ||
| Name: "my-subjects", | ||
| Feature: feature.NewResourceFeature(version, nil), | ||
| Mutate: func(m *clusterrolebinding.Mutator) error { | ||
| m.EditSubjects(func(e *editors.BindingSubjectsEditor) error { | ||
| e.EnsureServiceAccount("my-sa", "default") | ||
| return nil | ||
| }) | ||
| return nil | ||
| }, | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| Mutations are applied in the order they are registered with the builder. If one mutation depends on a change made by | ||
| another, register the dependency first. | ||
|
|
||
| ### Boolean-gated mutations | ||
|
|
||
| ```go | ||
| func ConditionalSubjectMutation(version string, addExtraSubject bool) clusterrolebinding.Mutation { | ||
| return clusterrolebinding.Mutation{ | ||
| Name: "conditional-subject", | ||
| Feature: feature.NewResourceFeature(version, nil).When(addExtraSubject), | ||
| Mutate: func(m *clusterrolebinding.Mutator) error { | ||
| m.EditSubjects(func(e *editors.BindingSubjectsEditor) error { | ||
| e.EnsureServiceAccount("extra-sa", "monitoring") | ||
| return nil | ||
| }) | ||
| return nil | ||
| }, | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| All version constraints and `When()` conditions must be satisfied for a mutation to apply. | ||
|
|
||
| ## Internal Mutation Ordering | ||
|
|
||
| Within a single mutation, edit operations are applied in a fixed category order regardless of the order they are | ||
| recorded: | ||
|
|
||
| | Step | Category | What it affects | | ||
| | ---- | -------------- | ------------------------------------------------------- | | ||
| | 1 | Metadata edits | Labels and annotations on the `ClusterRoleBinding` | | ||
| | 2 | Subject edits | `.subjects` entries — Add, Remove, EnsureServiceAccount | | ||
|
|
||
| Within each category, edits are applied in their registration order. Later features observe the ClusterRoleBinding as | ||
| modified by all previous features. | ||
|
|
||
| ## Editors | ||
|
|
||
| ### BindingSubjectsEditor | ||
|
|
||
| The primary API for modifying `.subjects` entries. Use `m.EditSubjects` for full control: | ||
|
|
||
| ```go | ||
| m.EditSubjects(func(e *editors.BindingSubjectsEditor) error { | ||
| e.EnsureServiceAccount("my-sa", "default") | ||
| e.RemoveSubject("User", "old-user", "") | ||
| return nil | ||
| }) | ||
| ``` | ||
|
|
||
| #### EnsureSubject | ||
|
|
||
| Upserts a subject in the subjects list. A subject is identified by the combination of Kind, Name, and Namespace. If a | ||
| matching subject already exists it is replaced; otherwise the new subject is appended: | ||
|
|
||
| ```go | ||
| m.EditSubjects(func(e *editors.BindingSubjectsEditor) error { | ||
| e.EnsureSubject(rbacv1.Subject{ | ||
| Kind: "Group", | ||
| Name: "developers", | ||
| APIGroup: "rbac.authorization.k8s.io", | ||
| }) | ||
| return nil | ||
| }) | ||
| ``` | ||
|
|
||
| #### EnsureServiceAccount | ||
|
|
||
| Convenience method that ensures a `ServiceAccount` subject with the given name and namespace exists: | ||
|
|
||
| ```go | ||
| m.EditSubjects(func(e *editors.BindingSubjectsEditor) error { | ||
| e.EnsureServiceAccount("app-sa", "production") | ||
| return nil | ||
| }) | ||
| ``` | ||
|
|
||
| #### RemoveSubject and RemoveServiceAccount | ||
|
|
||
| `RemoveSubject` removes a subject matching the given kind, name, and namespace. `RemoveServiceAccount` is a convenience | ||
| wrapper for removing `ServiceAccount` subjects: | ||
|
|
||
| ```go | ||
| m.EditSubjects(func(e *editors.BindingSubjectsEditor) error { | ||
| e.RemoveSubject("User", "old-user", "") | ||
| e.RemoveServiceAccount("deprecated-sa", "default") | ||
| return nil | ||
| }) | ||
| ``` | ||
|
|
||
| #### Raw Escape Hatch | ||
|
|
||
| `Raw()` returns a pointer to the underlying `[]rbacv1.Subject` for free-form editing: | ||
|
|
||
| ```go | ||
| m.EditSubjects(func(e *editors.BindingSubjectsEditor) error { | ||
| raw := e.Raw() | ||
| for i := range *raw { | ||
| if (*raw)[i].Kind == "ServiceAccount" { | ||
| (*raw)[i].Namespace = "updated-namespace" | ||
| } | ||
| } | ||
| return nil | ||
| }) | ||
| ``` | ||
|
|
||
| ### ObjectMetaEditor | ||
|
|
||
| Modifies labels and annotations via `m.EditObjectMetadata`. | ||
|
|
||
| Available methods: `EnsureLabel`, `RemoveLabel`, `EnsureAnnotation`, `RemoveAnnotation`, `Raw`. | ||
|
|
||
| ```go | ||
| m.EditObjectMetadata(func(e *editors.ObjectMetaEditor) error { | ||
| e.EnsureLabel("app.kubernetes.io/managed-by", "my-operator") | ||
| e.EnsureAnnotation("description", "cluster-wide admin binding") | ||
| return nil | ||
| }) | ||
| ``` | ||
|
|
||
| ## Guidance | ||
|
|
||
| **`Feature: nil` applies unconditionally.** Omit `Feature` (leave it nil) for mutations that should always run. Use | ||
| `feature.NewResourceFeature(version, constraints)` when version-based gating is needed, and chain `.When(bool)` for | ||
| boolean conditions. | ||
|
|
||
| **Cluster-scoped resources have no namespace.** Unlike namespaced primitives, ClusterRoleBinding does not require or | ||
| validate a namespace. The identity format is `rbac.authorization.k8s.io/v1/ClusterRoleBinding/<name>`. | ||
|
|
||
| **Register mutations in dependency order.** If mutation B relies on a subject added by mutation A, register A first. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| # ClusterRoleBinding Primitive Example | ||
|
|
||
| This example demonstrates the usage of the `clusterrolebinding` primitive within the operator component framework. It | ||
| shows how to manage a Kubernetes ClusterRoleBinding as a component of a larger application, utilising features like: | ||
|
|
||
| - **Base Construction**: Initializing a ClusterRoleBinding with a roleRef and base subjects. | ||
| - **Feature Mutations**: Adding subjects conditionally via feature-gated mutations using `EditSubjects`. | ||
| - **Metadata Mutations**: Setting version labels on the ClusterRoleBinding via `EditObjectMetadata`. | ||
| - **Data Extraction**: Inspecting ClusterRoleBinding state after each reconcile cycle. | ||
|
|
||
| ## Directory Structure | ||
|
|
||
| - `app/`: Defines the controller that uses the component framework. The `ExampleApp` CRD is shared from | ||
| `examples/shared/app`. | ||
| - `features/`: Contains modular feature definitions: | ||
| - `mutations.go`: version labelling and feature-gated monitoring subject addition. | ||
| - `resources/`: Contains the central `NewClusterRoleBindingResource` factory that assembles all features using | ||
| `clusterrolebinding.Builder`. | ||
| - `main.go`: A standalone entry point that demonstrates building and mutating a ClusterRoleBinding through multiple spec | ||
| variations. | ||
|
|
||
| ## Running the Example | ||
|
|
||
| ```bash | ||
| go run examples/clusterrolebinding-primitive/main.go | ||
| ``` | ||
sourcehawk marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| This will: | ||
|
|
||
| 1. Create an in-memory `ExampleApp` owner object. | ||
| 2. For each of three spec variations, build a fresh resource and apply mutations to a simulated current | ||
| ClusterRoleBinding. | ||
| 3. Print the reconciled ClusterRoleBinding state (labels, roleRef, subjects) after each mutation cycle. | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,65 @@ | ||
| // Package app provides a sample controller using the clusterrolebinding primitive. | ||
| // | ||
| // Note: ClusterRoleBinding is cluster-scoped. When the owner is namespace-scoped, | ||
| // the component framework automatically skips setting a controller owner reference | ||
| // (since Kubernetes does not allow cross-scope owner references) and logs an info | ||
| // message. This means the ClusterRoleBinding will not be garbage-collected when | ||
| // the owner is deleted — operators should implement their own cleanup logic if | ||
| // needed. In production, using a cluster-scoped CRD as the owner avoids this | ||
| // limitation entirely. | ||
| package app | ||
|
|
||
| import ( | ||
| "context" | ||
|
|
||
| sharedapp "github.com/sourcehawk/operator-component-framework/examples/shared/app" | ||
| "github.com/sourcehawk/operator-component-framework/pkg/component" | ||
| "k8s.io/apimachinery/pkg/runtime" | ||
| "k8s.io/client-go/tools/record" | ||
| "sigs.k8s.io/controller-runtime/pkg/client" | ||
| ) | ||
|
|
||
| // ExampleController reconciles an ExampleApp object using the component framework. | ||
| // | ||
| // When the owner is namespace-scoped, the framework skips the controller owner | ||
| // reference for cluster-scoped resources. Use a cluster-scoped owner CRD in | ||
| // production if automatic garbage collection is required. | ||
| type ExampleController struct { | ||
| client.Client | ||
| Scheme *runtime.Scheme | ||
| Recorder record.EventRecorder | ||
| Metrics component.Recorder | ||
|
|
||
| // NewClusterRoleBindingResource is a factory function to create the clusterrolebinding resource. | ||
| NewClusterRoleBindingResource func(*sharedapp.ExampleApp) (component.Resource, error) | ||
| } | ||
|
|
||
| // Reconcile performs the reconciliation for a single ExampleApp. | ||
| func (r *ExampleController) Reconcile(ctx context.Context, owner *sharedapp.ExampleApp) error { | ||
| // 1. Build the clusterrolebinding resource for this owner. | ||
| crbResource, err := r.NewClusterRoleBindingResource(owner) | ||
| if err != nil { | ||
| return err | ||
| } | ||
|
|
||
| // 2. Build the component that manages the clusterrolebinding. | ||
| comp, err := component.NewComponentBuilder(). | ||
| WithName("example-app"). | ||
| WithConditionType("AppReady"). | ||
| WithResource(crbResource, component.ResourceOptions{}). | ||
| Build() | ||
| if err != nil { | ||
| return err | ||
| } | ||
|
|
||
| // 3. Execute the component reconciliation. | ||
| resCtx := component.ReconcileContext{ | ||
| Client: r.Client, | ||
| Scheme: r.Scheme, | ||
| Recorder: r.Recorder, | ||
| Metrics: r.Metrics, | ||
| Owner: owner, | ||
| } | ||
|
|
||
| return comp.Reconcile(ctx, resCtx) | ||
| } |
40 changes: 40 additions & 0 deletions
40
examples/clusterrolebinding-primitive/features/mutations.go
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,40 @@ | ||
| // Package features provides sample mutations for the clusterrolebinding primitive example. | ||
| package features | ||
|
|
||
| import ( | ||
| "github.com/sourcehawk/operator-component-framework/pkg/feature" | ||
| "github.com/sourcehawk/operator-component-framework/pkg/mutation/editors" | ||
| "github.com/sourcehawk/operator-component-framework/pkg/primitives/clusterrolebinding" | ||
| ) | ||
|
|
||
| // VersionLabelMutation sets the app.kubernetes.io/version label on the | ||
| // ClusterRoleBinding. It is always enabled. | ||
| func VersionLabelMutation(version string) clusterrolebinding.Mutation { | ||
| return clusterrolebinding.Mutation{ | ||
| Name: "version-label", | ||
| Feature: feature.NewResourceFeature(version, nil), | ||
| Mutate: func(m *clusterrolebinding.Mutator) error { | ||
| m.EditObjectMetadata(func(e *editors.ObjectMetaEditor) error { | ||
| e.EnsureLabel("app.kubernetes.io/version", version) | ||
| return nil | ||
| }) | ||
| return nil | ||
| }, | ||
| } | ||
| } | ||
|
|
||
| // MonitoringSubjectMutation adds a monitoring service account as a subject | ||
| // when metrics are enabled. | ||
| func MonitoringSubjectMutation(version string, enableMetrics bool) clusterrolebinding.Mutation { | ||
| return clusterrolebinding.Mutation{ | ||
| Name: "monitoring-subject", | ||
| Feature: feature.NewResourceFeature(version, nil).When(enableMetrics), | ||
| Mutate: func(m *clusterrolebinding.Mutator) error { | ||
| m.EditSubjects(func(e *editors.BindingSubjectsEditor) error { | ||
| e.EnsureServiceAccount("monitoring-agent", "monitoring") | ||
| return nil | ||
| }) | ||
| return nil | ||
| }, | ||
| } | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.