Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
abf9a04
feat(api): add GatewayClassParameters.spec.service ServiceConfig type…
oysteins10 May 25, 2026
cff91b0
docs(api): clarify LoadBalancerSourceRanges CIDR validation timing
oysteins10 May 25, 2026
8a18c79
feat(controller): add resolveServiceConfig with class/gateway overlay
oysteins10 May 25, 2026
81b17dc
test(controller): cover nil-gateway and defensive-copy paths for reso…
oysteins10 May 25, 2026
c9ad53e
feat(controller): add mergeWithManaged sentinel-based annotation/labe…
oysteins10 May 25, 2026
debc33a
test(controller): cover desired-wins and idempotency for mergeWithMan…
oysteins10 May 25, 2026
fa69e68
feat(controller): apply ResolvedServiceConfig in buildService
oysteins10 May 25, 2026
bfda2a1
test(controller): pin selector invariant and tighten sentinel test
oysteins10 May 25, 2026
bce2fb4
feat(controller): extend needsServiceUpdate and update path for Servi…
oysteins10 May 25, 2026
1b56200
fix(controller): re-apply Spec.Selector on Service drift, dedupe ptr …
oysteins10 May 25, 2026
bfbd3da
feat(controller): wire resolveServiceConfig into reconcileResources
oysteins10 May 25, 2026
3551596
test(controller): broaden infra-hash regression test forbidden-field …
oysteins10 May 25, 2026
fa4f7da
test(controller): envtest scenarios for configurable Service shape
oysteins10 May 25, 2026
36ba3d3
test(controller): clean up derived resources in Service-shape envtests
oysteins10 May 25, 2026
ebf2f10
docs: configurable Service shape (#70)
oysteins10 May 25, 2026
b6e6709
fix(controller): treat unset ExternalTrafficPolicy as Cluster to avoi…
oysteins10 May 25, 2026
9847f3b
fix(controller): correct Service sentinel storage and preserve NodePort
perbu May 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
17 changes: 17 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,23 @@ All notable changes to Varnish Gateway are documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Added

- **GatewayClassParameters: configurable data-plane Service shape (#70).**
New `spec.service` field exposes Service `type`, `annotations`, `labels`,
`loadBalancerClass`, `loadBalancerSourceRanges`, and
`externalTrafficPolicy`. Defaults preserve the existing `Type: LoadBalancer`
behavior, so existing deployments need no changes. Per-Gateway overlay
for labels and annotations via the Gateway API v1.1+
`Gateway.spec.infrastructure` field. Cross-field validation via CEL at
admission time. Annotations added by cloud controllers (e.g., AWS LB
Controller, MetalLB) are preserved across reconciles via a managed-keys
sentinel on the Service. Unblocks gateway-behind-gateway, bare-metal
MetalLB, and internal-cache deployment patterns. Service shape changes
do not trigger pod restarts.

## [v0.21.4 - 2026-05-24]

### Security
Expand Down
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,7 @@ The Gateway and HTTPRoute controllers have clearly defined responsibilities:
- Fetch and merge user VCL from GatewayClassParameters
- Create and update ConfigMap with `main.vcl`
- Manage Deployment, Service, RBAC resources
- Resolve Service shape (Type, annotations, labels, LB class, source ranges, traffic policy) from `GatewayClassParameters.spec.service`, overlaid with `Gateway.spec.infrastructure.{labels,annotations}`
- Compute infrastructure hash for pod restart detection
- Watches:
- Gateway resources (primary)
Expand Down
67 changes: 67 additions & 0 deletions api/v1alpha1/gatewayclassparameters_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,12 @@ type GatewayClassParametersSpec struct {
// hash).
// +optional
TopologySpreadConstraints []corev1.TopologySpreadConstraint `json:"topologySpreadConstraints,omitempty"`

// Service configures the data-plane Service. When omitted, the operator
// preserves the historical default (Type: LoadBalancer, no annotations).
// Service shape changes do NOT trigger pod restarts.
// +optional
Service *ServiceConfig `json:"service,omitempty"`
}

// PodDisruptionBudget configures a PodDisruptionBudget for Gateway pods.
Expand Down Expand Up @@ -150,6 +156,67 @@ type ConfigMapReference struct {
Key string `json:"key,omitempty"`
}

// ServiceConfig configures the data-plane Service object.
//
// Fields that only make sense for specific Service types are validated by
// CEL rules at admission time. The Type field defaults to LoadBalancer when
// omitted (handled by the resolver, not by a kubebuilder default, to keep
// stored CRs minimal).
//
// Labels and Annotations are layered: GatewayClassParameters provides the
// defaults, and Gateway.spec.infrastructure.{labels,annotations} can override
// or add to them on a per-Gateway basis. The other fields (Type,
// LoadBalancerClass, LoadBalancerSourceRanges, ExternalTrafficPolicy) are
// class-level only — to use different values per Gateway, create separate
// GatewayClasses.
//
// +kubebuilder:validation:XValidation:rule="!has(self.loadBalancerClass) || !has(self.type) || self.type == 'LoadBalancer'",message="loadBalancerClass is only valid when type is LoadBalancer"
// +kubebuilder:validation:XValidation:rule="!has(self.loadBalancerSourceRanges) || !has(self.type) || self.type == 'LoadBalancer'",message="loadBalancerSourceRanges is only valid when type is LoadBalancer"
// +kubebuilder:validation:XValidation:rule="!has(self.externalTrafficPolicy) || !has(self.type) || self.type == 'LoadBalancer' || self.type == 'NodePort'",message="externalTrafficPolicy is only valid when type is LoadBalancer or NodePort"
type ServiceConfig struct {
// Type selects the Service type. Defaults to LoadBalancer when omitted.
// Headless services (clusterIP: None) and ExternalName are intentionally
// not supported.
// +optional
// +kubebuilder:validation:Enum=ClusterIP;NodePort;LoadBalancer
Type *corev1.ServiceType `json:"type,omitempty"`

// Annotations are added to the Service's ObjectMeta. Keys set here are
// tracked via the varnish.io/managed-annotations sentinel and pruned on
// removal; annotations added by other actors (cloud controllers, etc.)
// are preserved.
// +optional
Annotations map[string]string `json:"annotations,omitempty"`

// Labels are added to the Service's ObjectMeta. The operator's
// controller-managed labels (app.kubernetes.io/managed-by,
// gateway.networking.k8s.io/gateway-name,
// gateway.networking.k8s.io/gateway-namespace) cannot be overridden;
// user-supplied keys colliding with these are dropped with a log warning.
// Other keys are tracked via the varnish.io/managed-labels sentinel.
// +optional
Labels map[string]string `json:"labels,omitempty"`

// LoadBalancerClass selects an implementation of load balancer
// (cluster-specific). Only valid when Type is LoadBalancer.
// +optional
LoadBalancerClass *string `json:"loadBalancerClass,omitempty"`

// LoadBalancerSourceRanges restricts traffic to the LoadBalancer to the
// listed CIDRs. Only valid when Type is LoadBalancer. CIDR syntax is not
// validated at admission time for this CRD; Kubernetes validates it when
// the Service is reconciled.
// +optional
LoadBalancerSourceRanges []string `json:"loadBalancerSourceRanges,omitempty"`

// ExternalTrafficPolicy selects how nodes route external traffic to the
// Service. Local preserves the client source IP at the cost of imbalanced
// load. Only valid when Type is LoadBalancer or NodePort.
// +optional
// +kubebuilder:validation:Enum=Cluster;Local
ExternalTrafficPolicy *corev1.ServiceExternalTrafficPolicy `json:"externalTrafficPolicy,omitempty"`
}

// GatewayClassParametersList contains a list of GatewayClassParameters.
//
// +kubebuilder:object:root=true
Expand Down
54 changes: 54 additions & 0 deletions api/v1alpha1/zz_generated.deepcopy.go
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,55 @@ func (in *ConfigMapReference) DeepCopy() *ConfigMapReference {
return out
}

// DeepCopyInto copies the receiver into out.
func (in *ServiceConfig) DeepCopyInto(out *ServiceConfig) {
*out = *in
if in.Type != nil {
in, out := &in.Type, &out.Type
*out = new(corev1.ServiceType)
**out = **in
}
if in.Annotations != nil {
in, out := &in.Annotations, &out.Annotations
*out = make(map[string]string, len(*in))
for key, val := range *in {
(*out)[key] = val
}
}
if in.Labels != nil {
in, out := &in.Labels, &out.Labels
*out = make(map[string]string, len(*in))
for key, val := range *in {
(*out)[key] = val
}
}
if in.LoadBalancerClass != nil {
in, out := &in.LoadBalancerClass, &out.LoadBalancerClass
*out = new(string)
**out = **in
}
if in.LoadBalancerSourceRanges != nil {
in, out := &in.LoadBalancerSourceRanges, &out.LoadBalancerSourceRanges
*out = make([]string, len(*in))
copy(*out, *in)
}
if in.ExternalTrafficPolicy != nil {
in, out := &in.ExternalTrafficPolicy, &out.ExternalTrafficPolicy
*out = new(corev1.ServiceExternalTrafficPolicy)
**out = **in
}
}

// DeepCopy creates a deep copy of ServiceConfig.
func (in *ServiceConfig) DeepCopy() *ServiceConfig {
if in == nil {
return nil
}
out := new(ServiceConfig)
in.DeepCopyInto(out)
return out
}

// DeepCopyInto copies the receiver into out.
func (in *VarnishLogging) DeepCopyInto(out *VarnishLogging) {
*out = *in
Expand Down Expand Up @@ -255,6 +304,11 @@ func (in *GatewayClassParametersSpec) DeepCopyInto(out *GatewayClassParametersSp
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
if in.Service != nil {
in, out := &in.Service, &out.Service
*out = new(ServiceConfig)
(*in).DeepCopyInto(*out)
}
}

// DeepCopyInto copies the receiver into out.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3677,6 +3677,77 @@ spec:
More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/
type: object
type: object
service:
description: |-
Service configures the data-plane Service. When omitted, the operator
preserves the historical default (Type: LoadBalancer, no annotations).
Service shape changes do NOT trigger pod restarts.
properties:
annotations:
additionalProperties:
type: string
description: |-
Annotations are added to the Service's ObjectMeta. Keys set here are
tracked via the varnish.io/managed-annotations sentinel and pruned on
removal; annotations added by other actors (cloud controllers, etc.)
are preserved.
type: object
externalTrafficPolicy:
description: |-
ExternalTrafficPolicy selects how nodes route external traffic to the
Service. Local preserves the client source IP at the cost of imbalanced
load. Only valid when Type is LoadBalancer or NodePort.
enum:
- Cluster
- Local
type: string
labels:
additionalProperties:
type: string
description: |-
Labels are added to the Service's ObjectMeta. The operator's
controller-managed labels (app.kubernetes.io/managed-by,
gateway.networking.k8s.io/gateway-name,
gateway.networking.k8s.io/gateway-namespace) cannot be overridden;
user-supplied keys colliding with these are dropped with a log warning.
Other keys are tracked via the varnish.io/managed-labels sentinel.
type: object
loadBalancerClass:
description: |-
LoadBalancerClass selects an implementation of load balancer
(cluster-specific). Only valid when Type is LoadBalancer.
type: string
loadBalancerSourceRanges:
description: |-
LoadBalancerSourceRanges restricts traffic to the LoadBalancer to the
listed CIDRs. Only valid when Type is LoadBalancer. CIDR syntax is not
validated at admission time for this CRD; Kubernetes validates it when
the Service is reconciled.
items:
type: string
type: array
type:
description: |-
Type selects the Service type. Defaults to LoadBalancer when omitted.
Headless services (clusterIP: None) and ExternalName are intentionally
not supported.
enum:
- ClusterIP
- NodePort
- LoadBalancer
type: string
type: object
x-kubernetes-validations:
- message: loadBalancerClass is only valid when type is LoadBalancer
rule: '!has(self.loadBalancerClass) || !has(self.type) || self.type
== ''LoadBalancer'''
- message: loadBalancerSourceRanges is only valid when type is LoadBalancer
rule: '!has(self.loadBalancerSourceRanges) || !has(self.type) ||
self.type == ''LoadBalancer'''
- message: externalTrafficPolicy is only valid when type is LoadBalancer
or NodePort
rule: '!has(self.externalTrafficPolicy) || !has(self.type) || self.type
== ''LoadBalancer'' || self.type == ''NodePort'''
topologySpreadConstraints:
description: |-
TopologySpreadConstraints lets you pin Gateway pods across failure
Expand Down
71 changes: 71 additions & 0 deletions deploy/00-prereqs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3686,6 +3686,77 @@ spec:
More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/
type: object
type: object
service:
description: |-
Service configures the data-plane Service. When omitted, the operator
preserves the historical default (Type: LoadBalancer, no annotations).
Service shape changes do NOT trigger pod restarts.
properties:
annotations:
additionalProperties:
type: string
description: |-
Annotations are added to the Service's ObjectMeta. Keys set here are
tracked via the varnish.io/managed-annotations sentinel and pruned on
removal; annotations added by other actors (cloud controllers, etc.)
are preserved.
type: object
externalTrafficPolicy:
description: |-
ExternalTrafficPolicy selects how nodes route external traffic to the
Service. Local preserves the client source IP at the cost of imbalanced
load. Only valid when Type is LoadBalancer or NodePort.
enum:
- Cluster
- Local
type: string
labels:
additionalProperties:
type: string
description: |-
Labels are added to the Service's ObjectMeta. The operator's
controller-managed labels (app.kubernetes.io/managed-by,
gateway.networking.k8s.io/gateway-name,
gateway.networking.k8s.io/gateway-namespace) cannot be overridden;
user-supplied keys colliding with these are dropped with a log warning.
Other keys are tracked via the varnish.io/managed-labels sentinel.
type: object
loadBalancerClass:
description: |-
LoadBalancerClass selects an implementation of load balancer
(cluster-specific). Only valid when Type is LoadBalancer.
type: string
loadBalancerSourceRanges:
description: |-
LoadBalancerSourceRanges restricts traffic to the LoadBalancer to the
listed CIDRs. Only valid when Type is LoadBalancer. CIDR syntax is not
validated at admission time for this CRD; Kubernetes validates it when
the Service is reconciled.
items:
type: string
type: array
type:
description: |-
Type selects the Service type. Defaults to LoadBalancer when omitted.
Headless services (clusterIP: None) and ExternalName are intentionally
not supported.
enum:
- ClusterIP
- NodePort
- LoadBalancer
type: string
type: object
x-kubernetes-validations:
- message: loadBalancerClass is only valid when type is LoadBalancer
rule: '!has(self.loadBalancerClass) || !has(self.type) || self.type
== ''LoadBalancer'''
- message: loadBalancerSourceRanges is only valid when type is LoadBalancer
rule: '!has(self.loadBalancerSourceRanges) || !has(self.type) ||
self.type == ''LoadBalancer'''
- message: externalTrafficPolicy is only valid when type is LoadBalancer
or NodePort
rule: '!has(self.externalTrafficPolicy) || !has(self.type) || self.type
== ''LoadBalancer'' || self.type == ''NodePort'''
topologySpreadConstraints:
description: |-
TopologySpreadConstraints lets you pin Gateway pods across failure
Expand Down
Loading