-
Notifications
You must be signed in to change notification settings - Fork 0
feat: implement networkpolicy primitive #31
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
base: main
Are you sure you want to change the base?
Changes from all commits
8a6c262
849a657
6e59ab6
cdab1da
03a4232
9751532
02c3a38
7940f54
b0f0e78
235f9cb
b940579
ace11f8
2dd76e0
3971a74
e731269
f1edbd2
816c263
3ddb19f
cda4560
5a8800f
3a66b99
c4014f3
0de73a6
eab4e60
73b4abf
0968d4f
0886829
b2eccef
305bac3
777a429
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 |
|---|---|---|
| @@ -0,0 +1,284 @@ | ||
| # NetworkPolicy Primitive | ||
|
|
||
| The `networkpolicy` primitive is the framework's built-in static abstraction for managing Kubernetes `NetworkPolicy` | ||
| resources. It integrates with the component lifecycle and provides a structured mutation API for managing pod selectors, | ||
| ingress rules, egress rules, and policy types. | ||
|
|
||
|
Comment on lines
+3
to
+6
|
||
| ## Capabilities | ||
|
|
||
| | Capability | Detail | | ||
| | --------------------- | ----------------------------------------------------------------------------------------------------------- | | ||
| | **Static lifecycle** | No health tracking, grace periods, or suspension — the resource is reconciled to desired state | | ||
| | **Mutation pipeline** | Typed editors for NetworkPolicy spec and object metadata, with a raw escape hatch for free-form access | | ||
| | **Append semantics** | Ingress and egress rules have no unique key — `AppendIngressRule`/`AppendEgressRule` append unconditionally | | ||
| | **Data extraction** | Reads generated or updated values back from the reconciled NetworkPolicy after each sync cycle | | ||
|
|
||
| ## Building a NetworkPolicy Primitive | ||
|
|
||
| ```go | ||
| import "github.com/sourcehawk/operator-component-framework/pkg/primitives/networkpolicy" | ||
|
|
||
| base := &networkingv1.NetworkPolicy{ | ||
| ObjectMeta: metav1.ObjectMeta{ | ||
| Name: "app-netpol", | ||
| Namespace: owner.Namespace, | ||
| }, | ||
| Spec: networkingv1.NetworkPolicySpec{ | ||
| PodSelector: metav1.LabelSelector{ | ||
| MatchLabels: map[string]string{"app": owner.Name}, | ||
| }, | ||
| PolicyTypes: []networkingv1.PolicyType{ | ||
| networkingv1.PolicyTypeIngress, | ||
| networkingv1.PolicyTypeEgress, | ||
| }, | ||
| }, | ||
| } | ||
|
|
||
| resource, err := networkpolicy.NewBuilder(base). | ||
| WithMutation(HTTPIngressMutation()). | ||
| Build() | ||
| ``` | ||
|
|
||
| ## Mutations | ||
|
|
||
| Mutations are the primary mechanism for modifying a `NetworkPolicy` 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 — prefer this | ||
| for mutations that should always run and do not need feature-gate evaluation: | ||
|
|
||
| ```go | ||
| func HTTPIngressMutation() networkpolicy.Mutation { | ||
| return networkpolicy.Mutation{ | ||
| Name: "http-ingress", | ||
| // Feature is nil — mutation is applied unconditionally. | ||
| Mutate: func(m *networkpolicy.Mutator) error { | ||
| m.EditNetworkPolicySpec(func(e *editors.NetworkPolicySpecEditor) error { | ||
| port := intstr.FromInt32(8080) | ||
| tcp := corev1.ProtocolTCP | ||
| e.AppendIngressRule(networkingv1.NetworkPolicyIngressRule{ | ||
| Ports: []networkingv1.NetworkPolicyPort{ | ||
| {Protocol: &tcp, Port: &port}, | ||
| }, | ||
| }) | ||
| 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 MetricsIngressMutation(version string, enableMetrics bool) networkpolicy.Mutation { | ||
| return networkpolicy.Mutation{ | ||
| Name: "metrics-ingress", | ||
| Feature: feature.NewResourceFeature(version, nil).When(enableMetrics), | ||
| Mutate: func(m *networkpolicy.Mutator) error { | ||
| m.EditNetworkPolicySpec(func(e *editors.NetworkPolicySpecEditor) error { | ||
| port := intstr.FromInt32(9090) | ||
| tcp := corev1.ProtocolTCP | ||
| e.AppendIngressRule(networkingv1.NetworkPolicyIngressRule{ | ||
| Ports: []networkingv1.NetworkPolicyPort{ | ||
| {Protocol: &tcp, Port: &port}, | ||
| }, | ||
| }) | ||
| return nil | ||
| }) | ||
| return nil | ||
| }, | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| ### Version-gated mutations | ||
|
|
||
| ```go | ||
| var legacyConstraint = mustSemverConstraint("< 2.0.0") | ||
|
|
||
| func LegacyNetworkPolicyMutation(version string) networkpolicy.Mutation { | ||
| return networkpolicy.Mutation{ | ||
| Name: "legacy-policy", | ||
| Feature: feature.NewResourceFeature( | ||
| version, | ||
| []feature.VersionConstraint{legacyConstraint}, | ||
| ), | ||
| Mutate: func(m *networkpolicy.Mutator) error { | ||
| m.EditNetworkPolicySpec(func(e *editors.NetworkPolicySpecEditor) error { | ||
| e.SetPolicyTypes([]networkingv1.PolicyType{ | ||
| networkingv1.PolicyTypeIngress, | ||
| }) | ||
| 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 `NetworkPolicy` | | ||
| | 2 | Spec edits | Pod selector, ingress rules, egress rules, policy types via Raw | | ||
|
|
||
| Within each category, edits are applied in their registration order. Later features observe the NetworkPolicy as | ||
| modified by all previous features. | ||
|
|
||
| ## Editors | ||
|
|
||
| ### NetworkPolicySpecEditor | ||
|
|
||
| The primary API for modifying the NetworkPolicy spec. Use `m.EditNetworkPolicySpec` for full control: | ||
|
|
||
| ```go | ||
| m.EditNetworkPolicySpec(func(e *editors.NetworkPolicySpecEditor) error { | ||
| e.SetPodSelector(metav1.LabelSelector{ | ||
| MatchLabels: map[string]string{"app": "web"}, | ||
| }) | ||
| port := intstr.FromInt32(80) | ||
| tcp := corev1.ProtocolTCP | ||
| e.AppendIngressRule(networkingv1.NetworkPolicyIngressRule{ | ||
| Ports: []networkingv1.NetworkPolicyPort{ | ||
| {Protocol: &tcp, Port: &port}, | ||
| }, | ||
| }) | ||
| return nil | ||
| }) | ||
| ``` | ||
|
|
||
| #### SetPodSelector | ||
|
|
||
| Sets the pod selector that determines which pods the policy applies to within the namespace. An empty `LabelSelector` | ||
| matches all pods. | ||
|
|
||
| #### AppendIngressRule and AppendEgressRule | ||
|
|
||
| Append a rule unconditionally. Ingress and egress rules have no unique key, so these methods always append. To replace | ||
| the full set of rules atomically, call `RemoveIngressRules` or `RemoveEgressRules` first: | ||
|
|
||
| ```go | ||
| m.EditNetworkPolicySpec(func(e *editors.NetworkPolicySpecEditor) error { | ||
| // Replace all ingress rules atomically. | ||
| e.RemoveIngressRules() | ||
| e.AppendIngressRule(newRule1) | ||
| e.AppendIngressRule(newRule2) | ||
| return nil | ||
| }) | ||
| ``` | ||
|
|
||
| #### RemoveIngressRules and RemoveEgressRules | ||
|
|
||
| Clear all ingress or egress rules respectively. Use before `AppendIngressRule`/`AppendEgressRule` to replace the full | ||
| set atomically. | ||
|
|
||
| #### SetPolicyTypes | ||
|
|
||
| Sets the policy types. Valid values are `networkingv1.PolicyTypeIngress` and `networkingv1.PolicyTypeEgress`. When | ||
| `Egress` is included, egress rules must be set explicitly to permit traffic; an empty list denies all egress. | ||
|
|
||
| #### Raw Escape Hatch | ||
|
|
||
| `Raw()` returns the underlying `*networkingv1.NetworkPolicySpec` for free-form editing when none of the structured | ||
| methods are sufficient: | ||
|
|
||
| ```go | ||
| m.EditNetworkPolicySpec(func(e *editors.NetworkPolicySpecEditor) error { | ||
| raw := e.Raw() | ||
sourcehawk marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| if raw.PodSelector.MatchLabels == nil { | ||
| raw.PodSelector.MatchLabels = make(map[string]string) | ||
| } | ||
| raw.PodSelector.MatchLabels["role"] = "db" | ||
| 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("policy/managed-by", "operator") | ||
| return nil | ||
| }) | ||
| ``` | ||
|
|
||
| ## Full Example: Feature-Composed Network Policy | ||
|
|
||
| ```go | ||
| func HTTPIngressMutation() networkpolicy.Mutation { | ||
| return networkpolicy.Mutation{ | ||
| Name: "http-ingress", | ||
| Mutate: func(m *networkpolicy.Mutator) error { | ||
| m.EditNetworkPolicySpec(func(e *editors.NetworkPolicySpecEditor) error { | ||
| port := intstr.FromInt32(8080) | ||
| tcp := corev1.ProtocolTCP | ||
| e.AppendIngressRule(networkingv1.NetworkPolicyIngressRule{ | ||
| Ports: []networkingv1.NetworkPolicyPort{ | ||
| {Protocol: &tcp, Port: &port}, | ||
| }, | ||
| }) | ||
| return nil | ||
| }) | ||
| return nil | ||
| }, | ||
| } | ||
| } | ||
|
|
||
| func MetricsIngressMutation(version string, enabled bool) networkpolicy.Mutation { | ||
| return networkpolicy.Mutation{ | ||
| Name: "metrics-ingress", | ||
| Feature: feature.NewResourceFeature(version, nil).When(enabled), | ||
| Mutate: func(m *networkpolicy.Mutator) error { | ||
| m.EditNetworkPolicySpec(func(e *editors.NetworkPolicySpecEditor) error { | ||
| port := intstr.FromInt32(9090) | ||
| tcp := corev1.ProtocolTCP | ||
| e.AppendIngressRule(networkingv1.NetworkPolicyIngressRule{ | ||
| Ports: []networkingv1.NetworkPolicyPort{ | ||
| {Protocol: &tcp, Port: &port}, | ||
| }, | ||
| }) | ||
| return nil | ||
| }) | ||
| return nil | ||
| }, | ||
| } | ||
| } | ||
|
|
||
| resource, err := networkpolicy.NewBuilder(base). | ||
| WithMutation(HTTPIngressMutation()). | ||
| WithMutation(MetricsIngressMutation(owner.Spec.Version, owner.Spec.EnableMetrics)). | ||
| Build() | ||
| ``` | ||
|
|
||
| When `EnableMetrics` is true, the final NetworkPolicy will have both HTTP and metrics ingress rules. When false, only | ||
| the HTTP rule is present. 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 `RemoveIngressRules`/`RemoveEgressRules` for atomic replacement.** Since rules have no unique key, there is no | ||
| upsert-by-key operation. To replace the full set of rules, call `Remove*Rules` first and then add the desired rules. | ||
| Alternatively, use `Raw()` for fine-grained manipulation. | ||
|
|
||
| **Register mutations in dependency order.** If mutation B relies on a rule added by mutation A, register A first. Since | ||
| `AppendIngressRule`/`AppendEgressRule` append unconditionally, the order of registration determines the order of rules | ||
| in the resulting spec. | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| # NetworkPolicy Primitive Example | ||
|
|
||
| This example demonstrates the usage of the `networkpolicy` primitive within the operator component framework. It shows | ||
| how to manage a Kubernetes NetworkPolicy as a component of a larger application, utilising features like: | ||
|
|
||
| - **Base Construction**: Initializing a NetworkPolicy with pod selector and policy types. | ||
| - **Feature Mutations**: Composing ingress and egress rules from independent, feature-gated mutations. | ||
| - **Boolean-Gated Rules**: Conditionally adding metrics ingress rules based on a spec flag. | ||
| - **Metadata Mutations**: Setting version labels on the NetworkPolicy via metadata editors. | ||
| - **Label Coexistence**: Demonstrating how label updates from this component can coexist with labels managed by other | ||
| controllers. | ||
| - **Data Extraction**: Reading the applied policy configuration 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`: HTTP ingress, boolean-gated metrics ingress, DNS egress, and version labelling. | ||
| - `resources/`: Contains the central `NewNetworkPolicyResource` factory that assembles all features using | ||
| `networkpolicy.Builder`. | ||
| - `main.go`: A standalone entry point that demonstrates multiple reconciliation cycles with a fake client. | ||
|
|
||
| ## Running the Example | ||
|
|
||
| ```bash | ||
| go run examples/networkpolicy-primitive/main.go | ||
| ``` | ||
|
|
||
| This will: | ||
|
|
||
| 1. Initialize a fake Kubernetes client. | ||
| 2. Create an `ExampleApp` owner object. | ||
| 3. Reconcile through three spec variations, printing the applied policy details after each cycle. | ||
| 4. Print the resulting status conditions. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These tables use
||at the start of each row, which GitHub-flavored Markdown renders as an extra empty leading column (because the first cell is blank). Use standard table syntax with a single leading|(or no leading pipe) for correct rendering.