-
Notifications
You must be signed in to change notification settings - Fork 0
feat: implement clusterrole primitive #37
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
Changes from all commits
3fc108f
163c5d4
0b5ce0d
e57a631
455871c
7c43752
a0d4f8d
488a4ca
85d940c
3acf33c
d60ed11
ff378c1
a604af1
69f9bd3
f51c934
717ddca
cf4218a
8d8bdfc
ddc6e74
84f5260
3145f52
9962187
1b5aa4d
a82b772
d502741
2ff185d
48bcb6b
31e8bd2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -114,13 +114,14 @@ This design: | |||||||||||
|
|
||||||||||||
| Editors provide scoped, typed APIs for modifying specific parts of a resource: | ||||||||||||
|
|
||||||||||||
| | Editor | Scope | | ||||||||||||
| | ---------------------- | ----------------------------------------------------------------------- | | ||||||||||||
| | `ContainerEditor` | Environment variables, arguments, resource limits, ports | | ||||||||||||
| | `PodSpecEditor` | Volumes, tolerations, node selectors, service account, security context | | ||||||||||||
| | `DeploymentSpecEditor` | Replicas, update strategy, label selectors | | ||||||||||||
| | `ConfigMapDataEditor` | `.data` entries — set, remove, deep-merge YAML patches, raw access | | ||||||||||||
| | `ObjectMetaEditor` | Labels and annotations on any Kubernetes object | | ||||||||||||
| | Editor | Scope | | ||||||||||||
| | ---------------------- | --------------------------------------------------------------------------------- | | ||||||||||||
| | `ContainerEditor` | Environment variables, arguments, resource limits, ports | | ||||||||||||
| | `PodSpecEditor` | Volumes, tolerations, node selectors, service account, security context | | ||||||||||||
| | `DeploymentSpecEditor` | Replicas, update strategy, label selectors | | ||||||||||||
| | `ConfigMapDataEditor` | `.data` entries — set, remove, deep-merge YAML patches, raw access | | ||||||||||||
| | `PolicyRulesEditor` | `.rules` entries on Role and ClusterRole objects — add, remove, clear, raw access | | ||||||||||||
| | `ObjectMetaEditor` | Labels and annotations on any Kubernetes object | | ||||||||||||
|
|
||||||||||||
| Every editor exposes a `.Raw()` method for cases where the typed API is insufficient, giving direct access to the | ||||||||||||
| underlying Kubernetes struct while keeping the mutation scoped to that editor's target. | ||||||||||||
|
|
@@ -141,10 +142,15 @@ have been applied. This means a single mutation can safely add a container and t | |||||||||||
|
|
||||||||||||
| ## Built-in Primitives | ||||||||||||
|
|
||||||||||||
| | Primitive | Category | Documentation | | ||||||||||||
| | --------------------------- | -------- | ----------------------------------------- | | ||||||||||||
| | `pkg/primitives/deployment` | Workload | [deployment.md](primitives/deployment.md) | | ||||||||||||
| | `pkg/primitives/configmap` | Static | [configmap.md](primitives/configmap.md) | | ||||||||||||
| | Primitive | Category | Documentation | | ||||||||||||
| | ---------------------------- | -------- | ------------------------------------------- | | ||||||||||||
| | `pkg/primitives/deployment` | Workload | [deployment.md](primitives/deployment.md) | | ||||||||||||
| | `pkg/primitives/configmap` | Static | [configmap.md](primitives/configmap.md) | | ||||||||||||
| | `pkg/primitives/clusterrole` | Static | [clusterrole.md](primitives/clusterrole.md) | | ||||||||||||
sourcehawk marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||
|
|
||||||||||||
|
||||||||||||
| The `clusterrole` primitive is exercised by the `examples/clusterrole-primitive` example. Because it requires | |
| cluster-scoped RBAC and may need elevated permissions, it is intentionally not included in the default | |
| `make run-examples` target used for CI/local smoke runs. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,292 @@ | ||
| # ClusterRole Primitive | ||
|
|
||
| The `clusterrole` primitive is the framework's built-in static abstraction for managing Kubernetes `ClusterRole` | ||
| resources. It integrates with the component lifecycle and provides a structured mutation API for managing `.rules`, | ||
| `.aggregationRule`, and object metadata. | ||
|
|
||
| ClusterRole is cluster-scoped: it has no namespace. The builder validates that the Name is set and that Namespace is | ||
| empty — setting a namespace on a cluster-scoped resource is rejected. | ||
|
|
||
sourcehawk marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| > **Ownership limitation:** During reconciliation, the framework attempts to set a controller reference on managed | ||
| > objects, but only when the owner and dependent scopes are compatible. When a namespaced owner manages a cluster-scoped | ||
| > resource such as a `ClusterRole`, the owner reference is skipped (and this is logged) instead of causing the reconcile | ||
| > to fail. In this case, the `ClusterRole` is **not** owned by the custom resource for Kubernetes garbage-collection or | ||
| > ownership semantics, so it will not be automatically deleted when the owner is removed; you must handle its lifecycle | ||
| > explicitly or use a cluster-scoped owner if automatic cleanup is required. | ||
|
|
||
| ## Capabilities | ||
|
|
||
| | Capability | Detail | | ||
| | --------------------- | -------------------------------------------------------------------------------------------------------------------------- | | ||
| | **Static lifecycle** | No health tracking, grace periods, or suspension — the resource is reconciled to desired state | | ||
| | **Mutation pipeline** | Typed editors (`PolicyRulesEditor`) for `.rules` and object metadata, with aggregation rule support and a raw escape hatch | | ||
| | **Cluster-scoped** | No namespace required — identity format is `rbac.authorization.k8s.io/v1/ClusterRole/<name>` | | ||
| | **Data extraction** | Reads generated or updated values back from the reconciled ClusterRole after each sync cycle | | ||
sourcehawk marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| ## Building a ClusterRole Primitive | ||
|
|
||
| ```go | ||
| import "github.com/sourcehawk/operator-component-framework/pkg/primitives/clusterrole" | ||
|
|
||
| base := &rbacv1.ClusterRole{ | ||
| ObjectMeta: metav1.ObjectMeta{ | ||
| Name: "my-operator-role", | ||
| }, | ||
| Rules: []rbacv1.PolicyRule{ | ||
| { | ||
| APIGroups: []string{""}, | ||
| Resources: []string{"pods"}, | ||
| Verbs: []string{"get", "list", "watch"}, | ||
| }, | ||
| }, | ||
| } | ||
|
|
||
| resource, err := clusterrole.NewBuilder(base). | ||
| WithMutation(MyFeatureMutation(owner.Spec.Version)). | ||
| Build() | ||
| ``` | ||
|
|
||
| ## Mutations | ||
|
|
||
| Mutations are the primary mechanism for modifying a `ClusterRole` 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: | ||
|
|
||
| ```go | ||
| func PodReadMutation() clusterrole.Mutation { | ||
| return clusterrole.Mutation{ | ||
| Name: "pod-read", | ||
| Mutate: func(m *clusterrole.Mutator) error { | ||
| m.AddRule(rbacv1.PolicyRule{ | ||
| APIGroups: []string{""}, | ||
| Resources: []string{"pods"}, | ||
| Verbs: []string{"get", "list", "watch"}, | ||
| }) | ||
| return nil | ||
| }, | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| Mutations are applied in the order they are registered with the builder. | ||
|
|
||
| ### Boolean-gated mutations | ||
|
|
||
| ```go | ||
| func SecretAccessMutation(version string, needsSecrets bool) clusterrole.Mutation { | ||
| return clusterrole.Mutation{ | ||
| Name: "secret-access", | ||
| Feature: feature.NewResourceFeature(version, nil).When(needsSecrets), | ||
| Mutate: func(m *clusterrole.Mutator) error { | ||
| m.AddRule(rbacv1.PolicyRule{ | ||
| APIGroups: []string{""}, | ||
| Resources: []string{"secrets"}, | ||
| Verbs: []string{"get", "list"}, | ||
| }) | ||
| return nil | ||
| }, | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| ### Version-gated mutations | ||
|
|
||
| ```go | ||
| var legacyConstraint = mustSemverConstraint("< 2.0.0") | ||
|
|
||
| func LegacyRBACMutation(version string) clusterrole.Mutation { | ||
| return clusterrole.Mutation{ | ||
| Name: "legacy-rbac", | ||
| Feature: feature.NewResourceFeature( | ||
| version, | ||
| []feature.VersionConstraint{legacyConstraint}, | ||
| ), | ||
| Mutate: func(m *clusterrole.Mutator) error { | ||
| m.AddRule(rbacv1.PolicyRule{ | ||
| APIGroups: []string{"extensions"}, | ||
| Resources: []string{"deployments"}, | ||
| Verbs: []string{"get", "list"}, | ||
| }) | ||
| return nil | ||
| }, | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| All version constraints and `When()` conditions must be satisfied for a mutation to apply. | ||
|
|
||
| ## Internal Mutation Ordering | ||
|
|
||
| The Mutator maintains feature boundaries: each feature's mutations are planned together and applied in the order the | ||
| features were registered. Within each feature, edits are applied in a fixed category order: | ||
|
|
||
| | Step | Category | What it affects | | ||
| | ---- | ---------------- | ------------------------------------------- | | ||
| | 1 | Metadata edits | Labels and annotations on the `ClusterRole` | | ||
| | 2 | Rules edits | `.rules` entries — EditRules, AddRule | | ||
| | 3 | Aggregation rule | `.aggregationRule` — SetAggregationRule | | ||
sourcehawk marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| Within each category, edits are applied in their registration order. For aggregation rules, the last | ||
| `SetAggregationRule` call wins within each feature. Later features observe the ClusterRole as modified by all previous | ||
| features. | ||
|
|
||
| ## Editors | ||
|
|
||
| ### PolicyRulesEditor | ||
|
|
||
| The primary API for modifying `.rules` entries. Use `m.EditRules` for full control: | ||
|
|
||
| ```go | ||
| m.EditRules(func(e *editors.PolicyRulesEditor) error { | ||
| e.AddRule(rbacv1.PolicyRule{ | ||
| APIGroups: []string{"apps"}, | ||
| Resources: []string{"deployments"}, | ||
| Verbs: []string{"get", "list", "watch"}, | ||
| }) | ||
| return nil | ||
| }) | ||
| ``` | ||
|
|
||
| #### AddRule | ||
|
|
||
| `AddRule` appends a PolicyRule to the rules slice: | ||
|
|
||
| ```go | ||
| m.EditRules(func(e *editors.PolicyRulesEditor) error { | ||
| e.AddRule(rbacv1.PolicyRule{ | ||
| APIGroups: []string{""}, | ||
| Resources: []string{"configmaps"}, | ||
| Verbs: []string{"get", "list"}, | ||
| }) | ||
| return nil | ||
| }) | ||
| ``` | ||
|
|
||
| #### RemoveRuleByIndex | ||
|
|
||
| `RemoveRuleByIndex` removes the rule at the given index. It is a no-op if the index is out of bounds: | ||
|
|
||
| ```go | ||
| m.EditRules(func(e *editors.PolicyRulesEditor) error { | ||
| e.RemoveRuleByIndex(0) // remove the first rule | ||
| return nil | ||
| }) | ||
| ``` | ||
|
|
||
| #### Clear | ||
|
|
||
| `Clear` removes all rules: | ||
|
|
||
| ```go | ||
| m.EditRules(func(e *editors.PolicyRulesEditor) error { | ||
| e.Clear() | ||
| return nil | ||
| }) | ||
| ``` | ||
|
|
||
| #### Raw Escape Hatch | ||
|
|
||
| `Raw()` returns a pointer to the underlying `[]rbacv1.PolicyRule` for free-form editing: | ||
|
|
||
| ```go | ||
| m.EditRules(func(e *editors.PolicyRulesEditor) error { | ||
| raw := e.Raw() | ||
| *raw = append(*raw, customRules...) | ||
| 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/version", version) | ||
| e.EnsureAnnotation("managed-by", "my-operator") | ||
| return nil | ||
| }) | ||
| ``` | ||
|
|
||
| ## Convenience Methods | ||
|
|
||
| The `Mutator` exposes a convenience wrapper for the most common `.rules` operation: | ||
|
|
||
| | Method | Equivalent to | | ||
| | --------------- | ------------------------------- | | ||
| | `AddRule(rule)` | `EditRules` → `e.AddRule(rule)` | | ||
sourcehawk marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| Use `AddRule` for simple, single-rule mutations. Use `EditRules` when you need multiple operations or raw access in a | ||
| single edit block. | ||
|
|
||
| ## SetAggregationRule | ||
|
|
||
| `SetAggregationRule` sets the ClusterRole's `.aggregationRule` field. An aggregation rule causes the API server to | ||
| combine rules from ClusterRoles whose labels match the provided selectors, instead of using `.rules` directly: | ||
|
|
||
| ```go | ||
| m.SetAggregationRule(&rbacv1.AggregationRule{ | ||
| ClusterRoleSelectors: []metav1.LabelSelector{ | ||
| {MatchLabels: map[string]string{"rbac.example.com/aggregate-to-admin": "true"}}, | ||
| }, | ||
| }) | ||
| ``` | ||
|
|
||
| Setting the aggregation rule to nil clears it. Within a single feature, the last `SetAggregationRule` call wins. | ||
|
|
||
| ## Full Example: Feature-Composed RBAC | ||
|
|
||
| ```go | ||
| func CoreRulesMutation() clusterrole.Mutation { | ||
| return clusterrole.Mutation{ | ||
| Name: "core-rules", | ||
| Mutate: func(m *clusterrole.Mutator) error { | ||
| m.AddRule(rbacv1.PolicyRule{ | ||
| APIGroups: []string{""}, | ||
| Resources: []string{"pods", "services", "configmaps"}, | ||
| Verbs: []string{"get", "list", "watch"}, | ||
| }) | ||
| return nil | ||
| }, | ||
| } | ||
| } | ||
|
|
||
| func CRDAccessMutation(version string, manageCRDs bool) clusterrole.Mutation { | ||
| return clusterrole.Mutation{ | ||
| Name: "crd-access", | ||
| Feature: feature.NewResourceFeature(version, nil).When(manageCRDs), | ||
| Mutate: func(m *clusterrole.Mutator) error { | ||
| m.AddRule(rbacv1.PolicyRule{ | ||
| APIGroups: []string{"apiextensions.k8s.io"}, | ||
| Resources: []string{"customresourcedefinitions"}, | ||
| Verbs: []string{"get", "list", "watch"}, | ||
| }) | ||
| return nil | ||
| }, | ||
| } | ||
| } | ||
|
|
||
| resource, err := clusterrole.NewBuilder(base). | ||
| WithMutation(CoreRulesMutation()). | ||
| WithMutation(CRDAccessMutation(owner.Spec.Version, owner.Spec.ManageCRDs)). | ||
| Build() | ||
| ``` | ||
|
|
||
| When `ManageCRDs` is true, the final rules include both core and CRD access rules. When false, only the core rules are | ||
| written. Neither mutation needs to know about the other. | ||
|
|
||
| ## 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. | ||
|
|
||
| **Use `SetAggregationRule` for composite roles.** When you want the API server to aggregate rules from multiple | ||
| ClusterRoles based on label selectors, use `SetAggregationRule` instead of managing `.rules` directly. The two | ||
| approaches are mutually exclusive in the Kubernetes API — the API server ignores `.rules` when `.aggregationRule` is | ||
| set. | ||
|
|
||
| **Register mutations in dependency order.** If mutation B relies on a rule added by mutation A, register A first. | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| # ClusterRole Primitive Example | ||
|
|
||
| This example demonstrates the usage of the `clusterrole` primitive within the operator component framework. It shows how | ||
| to manage a Kubernetes ClusterRole as a component of a larger application, utilising features like: | ||
|
|
||
| - **Base Construction**: Initializing a cluster-scoped ClusterRole with basic metadata. | ||
| - **Feature Mutations**: Composing RBAC rules from independent, feature-gated mutations using `AddRule`. | ||
| - **Metadata Mutations**: Setting version labels on the ClusterRole via `EditObjectMetadata`. | ||
| - **Data Extraction**: Inspecting ClusterRole rules 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`: core rules, version labelling, and feature-gated secret and deployment access. | ||
| - `resources/`: Contains the central `NewClusterRoleResource` factory that assembles all features using | ||
| `clusterrole.Builder`. | ||
| - `main.go`: A standalone entry point that demonstrates multiple reconciliation cycles with a fake client. | ||
|
|
||
| ## Running the Example | ||
|
|
||
| ```bash | ||
| go run examples/clusterrole-primitive/main.go | ||
| ``` | ||
|
|
||
| This will: | ||
|
|
||
| 1. Initialize a fake Kubernetes client. | ||
| 2. Create an `ExampleApp` owner object. | ||
| 3. Reconcile through four spec variations, printing the composed rules after each cycle. | ||
| 4. Print the resulting status conditions. |
Uh oh!
There was an error while loading. Please reload this page.