Ingress Primitive¶
The ingress primitive wraps a Kubernetes Ingress and integrates with the component lifecycle as an Integration,
Graceful, and Suspendable resource, providing a structured mutation API for managing rules, TLS configuration, and
metadata.
Capabilities¶
| Capability | Detail |
|---|---|
| Operational | Reports OperationPending until the ingress controller assigns an address, then Operational |
| Graceful | Reports Degraded until a load balancer IP or hostname is assigned, then Healthy |
| Suspendable | No-op by default. Ingress is left in place; backend returns 502/503 when the backing service is down |
| DataExtractable | Reads assigned load balancer addresses after each sync cycle via WithDataExtractor |
| Mutation pipeline | Typed editors for metadata and Ingress spec (rules, TLS, class name, default backend) |
See Lifecycle Interfaces for the full set of status values each interface reports.
Building an Ingress Primitive¶
import "github.com/sourcehawk/operator-component-framework/pkg/primitives/ingress"
base := &networkingv1.Ingress{
ObjectMeta: metav1.ObjectMeta{
Name: "web-ingress",
Namespace: owner.Namespace,
},
Spec: networkingv1.IngressSpec{
IngressClassName: ptr.To("nginx"),
Rules: []networkingv1.IngressRule{
{
Host: "app.example.com",
IngressRuleValue: networkingv1.IngressRuleValue{
HTTP: &networkingv1.HTTPIngressRuleValue{
Paths: []networkingv1.HTTPIngressPath{
{
Path: "/",
PathType: ptr.To(networkingv1.PathTypePrefix),
Backend: networkingv1.IngressBackend{
Service: &networkingv1.IngressServiceBackend{
Name: "web-svc",
Port: networkingv1.ServiceBackendPort{Number: 80},
},
},
},
},
},
},
},
},
},
}
resource, err := ingress.NewBuilder(base).
WithMutation(MyFeatureMutation(owner.Spec.Version)).
Build()
Mutations¶
Register mutations with WithMutation. The mutation system, boolean-gated mutations, and version-gated mutations are
explained in The Mutation System,
Boolean-Gated Mutations, and
Version-Gated Mutations.
A kind-specific example gating a TLS mutation on a boolean condition:
func TLSMutation(version string, enabled bool) ingress.Mutation {
return ingress.Mutation{
Name: "tls",
Feature: feature.NewVersionGate(version, nil).When(enabled),
Mutate: func(m *ingress.Mutator) error {
m.EditIngressSpec(func(e *editors.IngressSpecEditor) error {
e.EnsureTLS(networkingv1.IngressTLS{
Hosts: []string{"app.example.com"},
SecretName: "tls-cert",
})
return nil
})
return nil
},
}
}
Internal Mutation Ordering¶
Within a single mutation, edits are applied in a fixed category order regardless of recording order:
| Step | Category | What it affects |
|---|---|---|
| 1 | Metadata edits | Labels and annotations on the Ingress object |
| 2 | Ingress spec edits | Ingress class, default backend, rules, TLS via editor |
Within each category, edits run in registration order. Later features observe the Ingress as modified by all earlier ones.
Relevant Editors¶
See Mutation Editors for the general editor model.
IngressSpecEditor¶
The primary API for modifying the Ingress spec. Use m.EditIngressSpec for full control:
m.EditIngressSpec(func(e *editors.IngressSpecEditor) error {
e.SetIngressClassName("nginx")
e.EnsureRule(networkingv1.IngressRule{Host: "app.example.com"})
e.EnsureTLS(networkingv1.IngressTLS{
Hosts: []string{"app.example.com"},
SecretName: "tls-cert",
})
return nil
})
SetIngressClassName¶
Sets the spec.ingressClassName field.
SetDefaultBackend¶
Sets the default backend for traffic that does not match any rule.
EnsureRule and RemoveRule¶
EnsureRule upserts a rule by Host. If a rule with the same host already exists, it is replaced. RemoveRule deletes
the rule with the given host; it is a no-op if no matching rule exists.
m.EditIngressSpec(func(e *editors.IngressSpecEditor) error {
e.EnsureRule(networkingv1.IngressRule{
Host: "api.example.com",
IngressRuleValue: networkingv1.IngressRuleValue{
HTTP: &networkingv1.HTTPIngressRuleValue{
Paths: []networkingv1.HTTPIngressPath{
{
Path: "/v1",
PathType: ptr.To(networkingv1.PathTypePrefix),
Backend: networkingv1.IngressBackend{
Service: &networkingv1.IngressServiceBackend{
Name: "api-svc",
Port: networkingv1.ServiceBackendPort{Number: 8080},
},
},
},
},
},
},
})
e.RemoveRule("deprecated.example.com")
return nil
})
EnsureTLS and RemoveTLS¶
EnsureTLS upserts a TLS entry by the first host in the Hosts slice. RemoveTLS removes TLS entries whose first host
matches any of the provided hosts.
m.EditIngressSpec(func(e *editors.IngressSpecEditor) error {
e.EnsureTLS(networkingv1.IngressTLS{
Hosts: []string{"app.example.com", "www.example.com"},
SecretName: "wildcard-tls",
})
e.RemoveTLS("old.example.com")
return nil
})
Raw Escape Hatch¶
Raw() returns the underlying *networkingv1.IngressSpec for direct access:
m.EditIngressSpec(func(e *editors.IngressSpecEditor) error {
spec := e.Raw()
// direct manipulation
return nil
})
ObjectMetaEditor¶
Modifies labels and annotations via m.EditObjectMetadata.
Available methods: EnsureLabel, RemoveLabel, EnsureAnnotation, RemoveAnnotation, Raw.
m.EditObjectMetadata(func(e *editors.ObjectMetaEditor) error {
e.EnsureAnnotation("nginx.ingress.kubernetes.io/rewrite-target", "/")
return nil
})
Operational Status¶
The Ingress primitive implements concepts.Operational. The default handler iterates over Status.LoadBalancer.Ingress
entries and requires at least one with a non-empty IP or Hostname:
| Condition | Status |
|---|---|
Entry with IP != "" or Hostname != "" |
Operational |
| Otherwise | OperationPending |
Override with WithCustomOperationalStatus for more complex health checks, such as verifying specific annotations set
by cloud providers.
Grace Status¶
The default grace status handler inspects Status.LoadBalancer.Ingress after the grace period expires:
| Condition | Status |
|---|---|
At least one entry with a non-empty IP or Hostname |
Healthy |
No entries, or all entries lack both IP and Hostname |
Degraded |
Override with WithCustomGraceStatus:
ingress.NewBuilder(base).
WithCustomGraceStatus(func(ing *networkingv1.Ingress) (concepts.GraceStatusWithReason, error) {
status, err := ingress.DefaultGraceStatusHandler(ing)
if err != nil {
return status, err
}
// Add custom logic
return status, nil
})
Suspension¶
Default Behavior¶
The default suspension strategy is a no-op:
DefaultDeleteOnSuspendHandlerreturnsfalse. The Ingress is not deleted.DefaultSuspendMutationHandlerdoes nothing. The Ingress spec is not modified.DefaultSuspensionStatusHandlerimmediately reportsSuspendedwith reason"Ingress suspended (backend unavailable)".
Rationale: deleting an Ingress causes the ingress controller to reload its configuration, which affects the entire cluster's routing, not just the suspended service. When the backend service is suspended, the Ingress returning 502/503 is the correct observable behavior.
Custom Suspension¶
Override any of the suspension handlers via the builder:
resource, err := ingress.NewBuilder(base).
WithCustomSuspendDeletionDecision(func(_ *networkingv1.Ingress) bool {
return true // delete on suspend
}).
Build()
Full Example¶
func BaseIngressMutation(version string) ingress.Mutation {
return ingress.Mutation{
Name: "base-ingress",
Feature: feature.NewVersionGate(version, nil),
Mutate: func(m *ingress.Mutator) error {
m.EditIngressSpec(func(e *editors.IngressSpecEditor) error {
e.SetIngressClassName("nginx")
e.EnsureRule(networkingv1.IngressRule{
Host: "app.example.com",
IngressRuleValue: networkingv1.IngressRuleValue{
HTTP: &networkingv1.HTTPIngressRuleValue{
Paths: []networkingv1.HTTPIngressPath{
{
Path: "/",
PathType: ptr.To(networkingv1.PathTypePrefix),
Backend: networkingv1.IngressBackend{
Service: &networkingv1.IngressServiceBackend{
Name: "web-svc",
Port: networkingv1.ServiceBackendPort{Number: 80},
},
},
},
},
},
},
})
return nil
})
return nil
},
}
}
func TLSMutation(version string, enabled bool) ingress.Mutation {
return ingress.Mutation{
Name: "tls",
Feature: feature.NewVersionGate(version, nil).When(enabled),
Mutate: func(m *ingress.Mutator) error {
m.EditIngressSpec(func(e *editors.IngressSpecEditor) error {
e.EnsureTLS(networkingv1.IngressTLS{
Hosts: []string{"app.example.com"},
SecretName: "tls-cert",
})
return nil
})
return nil
},
}
}
resource, err := ingress.NewBuilder(base).
WithMutation(BaseIngressMutation(owner.Spec.Version)).
WithMutation(TLSMutation(owner.Spec.Version, owner.Spec.TLSEnabled)).
Build()
When TLSEnabled is true, the Ingress includes a TLS block for the host. When false, only the rule is present.
Guidance¶
Feature: nil applies unconditionally. Omit Feature for mutations that should always run. Use
feature.NewVersionGate(version, constraints) for version-based gating and chain .When(bool) for boolean conditions.
Register mutations in dependency order. If mutation B relies on a rule added by mutation A, register A first.
Prefer no-op suspension. The default no-op suspension is almost always correct for Ingress resources. Only override to delete-on-suspend if your use case specifically requires removing the Ingress from the cluster during suspension.
Use EnsureRule for idempotent rule management. Rules are matched by Host; repeated calls with the same host
replace the existing rule rather than duplicating it.