Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
8a6c262
Add NetworkPolicy primitive with editor, builder, mutator, and flavors
sourcehawk Mar 22, 2026
849a657
Add NetworkPolicy primitive example with feature-gated ingress rules
sourcehawk Mar 22, 2026
6e59ab6
Add NetworkPolicy primitive documentation and example README
sourcehawk Mar 22, 2026
cdab1da
Add missing NetworkPolicySpecEditor source file
sourcehawk Mar 22, 2026
03a4232
Address Copilot review comments on NetworkPolicy primitive
sourcehawk Mar 22, 2026
9751532
Fix inaccurate DefaultFieldApplicator docs for NetworkPolicy
sourcehawk Mar 22, 2026
02c3a38
preserve server-managed metadata in default field applicator
sourcehawk Mar 22, 2026
7940f54
Apply suggestion from @sourcehawk
sourcehawk Mar 22, 2026
b0f0e78
address comments
sourcehawk Mar 23, 2026
235f9cb
fix: align doc comments per Copilot review feedback
sourcehawk Mar 23, 2026
b940579
docs: document server-managed field preservation in NetworkPolicy def…
sourcehawk Mar 23, 2026
ace11f8
fix: avoid duplicate beginFeature call in NetworkPolicy mutator const…
sourcehawk Mar 23, 2026
2dd76e0
docs: improve NetworkPolicy mutator GoDoc and add primitive to docs i…
sourcehawk Mar 23, 2026
3971a74
fix: export BeginFeature to match FeatureMutator interface
sourcehawk Mar 23, 2026
e731269
merge: resolve conflict in primitives.md built-in table
sourcehawk Mar 24, 2026
f1edbd2
style: apply prettier markdown formatting
sourcehawk Mar 24, 2026
816c263
fix: assert Build() errors in networkpolicy resource tests
sourcehawk Mar 24, 2026
3ddb19f
fix: align MetricsEnabled reference with actual field name EnableMetrics
sourcehawk Mar 24, 2026
cda4560
refactor: rename EnsureIngressRule/EnsureEgressRule to AppendIngressR…
sourcehawk Mar 24, 2026
5a8800f
docs: fix inaccurate description of nil Feature behavior
sourcehawk Mar 24, 2026
3a66b99
Merge remote-tracking branch 'origin/main' into feature/networkpolicy…
sourcehawk Mar 24, 2026
c4014f3
fix: do not initialize an empty plan on NetworkPolicy mutator constru…
sourcehawk Mar 24, 2026
0de73a6
Merge remote-tracking branch 'origin/main' into feature/networkpolicy…
sourcehawk Mar 25, 2026
eab4e60
refactor: remove field applicators and flavors from networkpolicy pri…
sourcehawk Mar 25, 2026
73b4abf
fix: remove references to deleted field applicators in example
sourcehawk Mar 25, 2026
0968d4f
docs: update README to remove references to deleted field flavors API
sourcehawk Mar 25, 2026
0886829
fix lint
sourcehawk Mar 25, 2026
b2eccef
fix: guard against nil active plan in mutator edit methods
sourcehawk Mar 25, 2026
305bac3
docs: update NewMutator doc to reflect implicit feature plan behavior
sourcehawk Mar 25, 2026
777a429
fix: use value assertion instead of pointer equality in networkpolicy…
sourcehawk Mar 25, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 13 additions & 11 deletions docs/primitives.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
| `NetworkPolicySpecEditor` | Pod selector, ingress/egress rules, policy types |
| `ObjectMetaEditor` | Labels and annotations on any Kubernetes object |
Comment on lines +117 to +124
Copy link

Copilot AI Mar 25, 2026

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.

Copilot uses AI. Check for mistakes.

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.
Expand All @@ -141,10 +142,11 @@ 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/networkpolicy` | Static | [networkpolicy.md](primitives/networkpolicy.md) |

## Usage Examples

Expand Down
284 changes: 284 additions & 0 deletions docs/primitives/networkpolicy.md
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
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PR description mentions that the networkpolicy primitive includes “flavors”, but this primitive package/docs don’t define any flavor helpers or builder options related to field-application flavors. Please either add the intended flavor support or update the PR description/docs to avoid advertising functionality that isn’t present.

Copilot uses AI. Check for mistakes.
## 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()
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.
35 changes: 35 additions & 0 deletions examples/networkpolicy-primitive/README.md
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.
Loading
Loading