-
Notifications
You must be signed in to change notification settings - Fork 0
feat: implement rolebinding primitive #35
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
26 commits
Select commit
Hold shift + click to select a range
d366d3c
Add BindingSubjectsEditor for RoleBinding/ClusterRoleBinding subjects
sourcehawk 7765194
Add RoleBinding primitive package
sourcehawk 8611f3f
Add RoleBinding primitive documentation
sourcehawk a9bff6f
Add RoleBinding primitive example
sourcehawk 802ef5e
Fix DefaultFieldApplicator ResourceVersion check and add tests
sourcehawk de75160
Clarify Feature field docs to match other primitive conventions
sourcehawk 5442539
Add RoleRef validation in Build() and nil pointer guard in BindingSub…
sourcehawk c34b773
Preserve server-managed metadata in default field applicator
sourcehawk e31c6db
add to readme
sourcehawk dcd7f17
Zero trailing slice elements in RemoveSubject to avoid retaining refe…
sourcehawk 60c22e3
Update rolebinding docs to document server-managed field preservation
sourcehawk cafef24
Remove beginFeature from primitive mutator constructors
sourcehawk d6dfad2
Align mutators with exported BeginFeature interface
sourcehawk 601e90d
Merge remote-tracking branch 'origin/main' into feature/rolebinding-p…
sourcehawk 183d2e1
Format rolebinding markdown files with prettier
sourcehawk e105191
Merge remote-tracking branch 'origin/main' into feature/rolebinding-p…
sourcehawk 1098190
Do not initialize an empty plan on rolebinding mutator construction
sourcehawk 174c95f
Add rolebinding primitive to built-in primitives index
sourcehawk 6eb5170
Add rolebinding to run-examples and add Resource-level tests
sourcehawk 8b70d95
Add BindingSubjectsEditor to Mutation Editors table in docs
sourcehawk 6d776bf
Clarify roleRef preservation semantics in Mutate() docstring
sourcehawk 858062a
Fix markdown table alignment in Mutation Editors section
sourcehawk 2dcaf0f
Remove field applicators and flavors in favor of Server-Side Apply
sourcehawk 8e67874
Merge main into feature/rolebinding-primitive, preferring main
sourcehawk 7447dda
Fix misleading comments about SSA annotation persistence and roleRef …
sourcehawk 44c8a56
fix deployment primitive example
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,204 @@ | ||
| # RoleBinding Primitive | ||
|
|
||
| The `rolebinding` primitive is the framework's built-in static abstraction for managing Kubernetes `RoleBinding` | ||
| resources. It integrates with the component lifecycle and provides a structured mutation API for managing subjects and | ||
| object metadata. | ||
|
|
||
sourcehawk marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| ## Capabilities | ||
|
|
||
| | Capability | Detail | | ||
| | --------------------- | -------------------------------------------------------------------------------------------------------- | | ||
| | **Static lifecycle** | No health tracking, grace periods, or suspension — the resource is reconciled to desired state | | ||
| | **Mutation pipeline** | Typed editors for subjects and object metadata, with a raw escape hatch for free-form access | | ||
sourcehawk marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| | **Immutable roleRef** | `roleRef` must be set on the base object and cannot be changed after creation (requires delete/recreate) | | ||
| | **Data extraction** | Reads generated or updated values back from the reconciled RoleBinding after each sync cycle | | ||
|
|
||
| ## Building a RoleBinding Primitive | ||
|
|
||
| ```go | ||
| import "github.com/sourcehawk/operator-component-framework/pkg/primitives/rolebinding" | ||
|
|
||
| base := &rbacv1.RoleBinding{ | ||
| ObjectMeta: metav1.ObjectMeta{ | ||
| Name: "app-rolebinding", | ||
| Namespace: owner.Namespace, | ||
| }, | ||
| RoleRef: rbacv1.RoleRef{ | ||
| APIGroup: "rbac.authorization.k8s.io", | ||
| Kind: "Role", | ||
| Name: "app-role", | ||
| }, | ||
| Subjects: []rbacv1.Subject{ | ||
| {Kind: "ServiceAccount", Name: "app-sa", Namespace: owner.Namespace}, | ||
| }, | ||
| } | ||
|
|
||
| resource, err := rolebinding.NewBuilder(base). | ||
| WithMutation(MySubjectMutation(owner.Spec.Version)). | ||
| Build() | ||
| ``` | ||
|
|
||
| `roleRef` must be set on the base object passed to `NewBuilder`. It is immutable after creation in Kubernetes and is not | ||
| modifiable via the mutation API. | ||
|
|
||
| ## Mutations | ||
|
|
||
| Mutations are the primary mechanism for modifying a `RoleBinding` 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 AddServiceAccountMutation(version, saName, saNamespace string) rolebinding.Mutation { | ||
| return rolebinding.Mutation{ | ||
| Name: "add-service-account", | ||
| Feature: feature.NewResourceFeature(version, nil), // always enabled | ||
| Mutate: func(m *rolebinding.Mutator) error { | ||
| m.EditSubjects(func(e *editors.BindingSubjectsEditor) error { | ||
| e.EnsureSubject(rbacv1.Subject{ | ||
| Kind: "ServiceAccount", | ||
| Name: saName, | ||
| Namespace: saNamespace, | ||
| }) | ||
| return nil | ||
| }) | ||
| return nil | ||
| }, | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| ### Boolean-gated mutations | ||
|
|
||
| ```go | ||
| func MonitoringSubjectMutation(version string, enabled bool) rolebinding.Mutation { | ||
| return rolebinding.Mutation{ | ||
| Name: "monitoring-subject", | ||
| Feature: feature.NewResourceFeature(version, nil).When(enabled), | ||
| Mutate: func(m *rolebinding.Mutator) error { | ||
| m.EditSubjects(func(e *editors.BindingSubjectsEditor) error { | ||
| e.EnsureSubject(rbacv1.Subject{ | ||
| Kind: "ServiceAccount", | ||
| Name: "monitoring-agent", | ||
| Namespace: "monitoring", | ||
| }) | ||
| return nil | ||
| }) | ||
| return nil | ||
| }, | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| ### Version-gated mutations | ||
|
|
||
| ```go | ||
| var legacyConstraint = mustSemverConstraint("< 2.0.0") | ||
|
|
||
| func LegacySubjectMutation(version string) rolebinding.Mutation { | ||
| return rolebinding.Mutation{ | ||
| Name: "legacy-subject", | ||
| Feature: feature.NewResourceFeature( | ||
| version, | ||
| []feature.VersionConstraint{legacyConstraint}, | ||
| ), | ||
| Mutate: func(m *rolebinding.Mutator) error { | ||
| m.EditSubjects(func(e *editors.BindingSubjectsEditor) error { | ||
| e.EnsureSubject(rbacv1.Subject{ | ||
| Kind: "User", | ||
| Name: "legacy-admin", | ||
| }) | ||
| 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 RoleBinding | | ||
| | 2 | Subject edits | `.subjects` entries via BindingSubjectsEditor | | ||
|
|
||
| Within each category, edits are applied in their registration order. Later features observe the RoleBinding as modified | ||
| by all previous features. | ||
|
|
||
| ## Editors | ||
|
|
||
| ### BindingSubjectsEditor | ||
|
|
||
| The primary API for modifying the subjects list. Use `m.EditSubjects` for full control: | ||
|
|
||
| ```go | ||
| m.EditSubjects(func(e *editors.BindingSubjectsEditor) error { | ||
| e.EnsureSubject(rbacv1.Subject{ | ||
| Kind: "ServiceAccount", | ||
| Name: "my-sa", | ||
| Namespace: "default", | ||
| }) | ||
| e.RemoveSubject("ServiceAccount", "old-sa", "default") | ||
| return nil | ||
| }) | ||
| ``` | ||
|
|
||
| #### EnsureSubject | ||
|
|
||
| `EnsureSubject` upserts a subject by the combination of `Kind`, `Name`, and `Namespace`. If a matching subject already | ||
| exists, it is replaced; otherwise the new subject is appended. | ||
|
|
||
| #### RemoveSubject | ||
|
|
||
| `RemoveSubject` removes a subject identified by kind, name, and namespace. It is a no-op if no matching subject exists. | ||
|
|
||
| #### Raw | ||
|
|
||
| `Raw()` returns a pointer to the underlying `[]rbacv1.Subject` slice for free-form access when the structured methods | ||
| are insufficient: | ||
|
|
||
| ```go | ||
| m.EditSubjects(func(e *editors.BindingSubjectsEditor) error { | ||
| raw := e.Raw() | ||
| *raw = append(*raw, rbacv1.Subject{ | ||
| Kind: "Group", | ||
| Name: "developers", | ||
| }) | ||
| 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("operator.example.io/version", version) | ||
| return nil | ||
| }) | ||
| ``` | ||
|
|
||
| ## Guidance | ||
|
|
||
| **Set `roleRef` on the base object, not via mutations.** Kubernetes makes `roleRef` immutable after creation. To change | ||
| a `roleRef`, delete and recreate the RoleBinding. | ||
|
|
||
| **`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. | ||
|
|
||
| **Use `EnsureSubject` for idempotent subject management.** `EnsureSubject` upserts by Kind+Name+Namespace, making it | ||
| safe to call on every reconciliation without creating duplicates. | ||
|
|
||
| **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
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,32 @@ | ||
| # RoleBinding Primitive Example | ||
|
|
||
| This example demonstrates the usage of the `rolebinding` primitive within the operator component framework. It shows how | ||
| to manage a Kubernetes RoleBinding as a component of a larger application, utilising features like: | ||
|
|
||
| - **Base Construction**: Initializing a RoleBinding with an immutable `roleRef` and basic metadata. | ||
| - **Feature Mutations**: Composing subjects from independent, feature-gated mutations using `EditSubjects`. | ||
| - **Metadata Mutations**: Setting version labels on the RoleBinding via `EditObjectMetadata`. | ||
| - **Data Extraction**: Inspecting subjects and roleRef 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`: base subject binding, version labelling, and feature-gated monitoring subject. | ||
| - `resources/`: Contains the central `NewRoleBindingResource` factory that assembles all features using | ||
| `rolebinding.Builder`. | ||
| - `main.go`: A standalone entry point that demonstrates multiple reconciliation cycles with a fake client. | ||
|
|
||
| ## Running the Example | ||
|
|
||
| ```bash | ||
| go run examples/rolebinding-primitive/main.go | ||
| ``` | ||
sourcehawk marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| This will: | ||
|
|
||
| 1. Initialize a fake Kubernetes client. | ||
| 2. Create an `ExampleApp` owner object. | ||
| 3. Reconcile through three spec variations, printing the subjects after each cycle. | ||
| 4. Print the resulting status conditions. | ||
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,54 @@ | ||
| // Package app provides a sample controller using the rolebinding primitive. | ||
| 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. | ||
| type ExampleController struct { | ||
| client.Client | ||
| Scheme *runtime.Scheme | ||
| Recorder record.EventRecorder | ||
| Metrics component.Recorder | ||
|
|
||
| // NewRoleBindingResource is a factory function to create the rolebinding resource. | ||
| // This allows us to inject the resource construction logic. | ||
| NewRoleBindingResource 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 rolebinding resource for this owner. | ||
| rbResource, err := r.NewRoleBindingResource(owner) | ||
| if err != nil { | ||
| return err | ||
| } | ||
|
|
||
| // 2. Build the component that manages the rolebinding. | ||
| comp, err := component.NewComponentBuilder(). | ||
| WithName("example-app"). | ||
| WithConditionType("AppReady"). | ||
| WithResource(rbResource, 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) | ||
| } |
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.