Skip to content

Commit 337c577

Browse files
committed
Rework fast-path provisioning to per-Gateway data-plane Deployment
Address review feedback on gke-labs#624: - Provision a dedicated data-plane Deployment per Gateway in pkg/provisioning/singlepod selecting its own LoadBalancer Service. - Support data-plane mode in GARI with GatewayFilter and DisableStatusUpdates. - Derive Service ports from Gateway effective listeners (including ListenerSets). - Gate Programmed condition on Deployment availability and Service LoadBalancer address assignment without fallback. - Implement label-based orphan sweeping and deletion cleanup via GatewayDeleteHandler.OnGatewayDeleted. - Drop dynamic listener manager, port allocator, scoped proxy, and Gateway finalizer. Fixes gke-labs#620
1 parent 34188f6 commit 337c577

24 files changed

Lines changed: 734 additions & 1253 deletions

‎.agents/skills/implement-conformance-test/journal/httproute-multiple-gateways.md‎

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,25 +7,27 @@
77

88
## 2. Issue / Failure Analysis
99
- **Observed Behavior**: In single-pod fast-path mode, all Gateways previously shared a single `default/gari-proxy` LoadBalancer Service and shared proxy ports (`:8000`, `:8443`), routing solely by Host header. Gateways could not be isolated when two Gateways had overlapping catch-all paths (e.g., both having `/` on port 80).
10-
- **Root Cause**: Fast-path mode lacked per-Gateway LoadBalancer Service provisioning and distinct pod port allocation for listener isolation.
10+
- **Root Cause**: Fast-path mode lacked per-Gateway data-plane Deployment provisioning and per-Gateway LoadBalancer Services for traffic and listener isolation.
1111

1212
## 3. Implementation / Fix Strategy
1313
- **Approach**:
14-
1. Implemented per-Gateway `type: LoadBalancer` Service provisioning in `pkg/provisioning/singlepod` with deterministic naming (`gari-gw-<hash>`), Gateway identifying labels, and pod port allocation in range `20000-29999`.
15-
2. Maintained stable port allocations across restarts seeded from existing Services, and implemented clean finalizer/GC cleanup upon Gateway deletion.
16-
3. Added `ListenerAddressProvider` dynamic listener management in `pkg/gari` and scoped proxy routing in `pkg/proxy` so connections arriving on a dynamic listener port are evaluated only against that Gateway's listeners, certificates, and routes.
17-
4. Preserved shared-listener fallback when no `ListenerAddressProvider` is configured (for embedders like `snigateway`).
18-
5. Removed the shared `gari-proxy` Service from `k8s/controller.yaml`, updated RBAC for Service CRUD, and updated e2e harness and tests to use Gateway `status.addresses`.
19-
6. Enabled `tests.HTTPRouteMultipleGateways` in `tests/e2e/conformance_test.go`.
14+
1. Implemented per-Gateway Deployment and `type: LoadBalancer` Service provisioning in `pkg/provisioning/singlepod` with deterministic naming (`gari-gw-<hash>`), Gateway identifying labels (`gateway.networking.k8s.io/gateway-name`, `gateway.networking.k8s.io/gateway-namespace`, `app.kubernetes.io/managed-by: gari-singlepod`), and service selector matching its dedicated Deployment.
15+
2. Derived Service ports dynamically from effective listeners (including ListenerSets).
16+
3. Added data-plane mode (`--dataplane-mode`, `--gateway-namespace`, `--gateway-name`, `DisableStatusUpdates: true`) so each per-Gateway pod reconciles and serves only its designated Gateway without colliding on Host headers or writing status back.
17+
4. Gated Gateway `Programmed` condition on the provisioned Deployment being ready/available and the Service having an assigned LoadBalancer address (with no fallback).
18+
5. Implemented label-based orphan sweeping and deletion cleanup via `GatewayDeleteHandler.OnGatewayDeleted` instead of cross-namespace finalizers.
19+
6. Removed the shared `gari-proxy` Service from `k8s/controller.yaml`, granted Deployment and Service CRUD permissions in ClusterRole RBAC, and updated e2e harness and tests to use Gateway `status.addresses`.
20+
7. Enabled `tests.HTTPRouteMultipleGateways` in `tests/e2e/conformance_test.go`.
2021
- **Key Files Modified**:
2122
- `pkg/provisioning/singlepod/address_provider.go`
2223
- `pkg/provisioning/singlepod/address_provider_test.go`
2324
- `pkg/controller/gateway_controller.go`
25+
- `pkg/controller/httproute_controller.go`
26+
- `pkg/controller/listenerset_controller.go`
27+
- `pkg/controller/backendtlspolicy_controller.go`
2428
- `pkg/controller/utils.go`
25-
- `pkg/controller/httproute_controller.go` (and other reconcilers)
26-
- `pkg/proxy/proxy.go`
27-
- `pkg/proxy/proxy_test.go`
28-
- `pkg/gari/dynamic_listeners.go`
29+
- `pkg/state/compiled.go`
30+
- `pkg/state/compiled_test.go`
2931
- `pkg/gari/gari.go`
3032
- `cmd/gateway-api-reference-implementation/main.go`
3133
- `k8s/controller.yaml`
@@ -34,7 +36,7 @@
3436
- `tests/e2e/conformance_test.go`
3537

3638
## 4. Validation & Results
37-
- **Unit Tests**: Added port allocator tests, singlepod address provider tests, and scoped proxy routing tests; verified with `ap test`.
39+
- **Unit Tests**: Added unit tests for per-Gateway Deployment & Service provisioning, effective listener ports, readiness gating, and orphan sweeping; verified with `ap test` and `ap lint`.
3840
- **Conformance Logs**:
3941
```
4042
--- PASS: TestConformance (102.75s)

‎cmd/gateway-api-reference-implementation/main.go‎

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,20 +22,37 @@ import (
2222
"github.com/gke-labs/gateway-api-reference-implementation/pkg/provisioning/singlepod"
2323
"k8s.io/klog/v2/textlogger"
2424
ctrl "sigs.k8s.io/controller-runtime"
25+
gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"
2526
)
2627

2728
var setupLog = ctrl.Log.WithName("setup")
2829

2930
func main() {
3031
opts := gari.DefaultOptions()
3132

32-
var controllerNamespace string
33+
var (
34+
controllerNamespace string
35+
dataplaneMode bool
36+
gatewayNamespace string
37+
gatewayName string
38+
dataplaneImage string
39+
)
40+
3341
defaultNs := os.Getenv("POD_NAMESPACE")
3442
if defaultNs == "" {
3543
defaultNs = singlepod.DefaultControllerNamespace
3644
}
3745

38-
flag.StringVar(&controllerNamespace, "controller-namespace", defaultNs, "The namespace of the controller where per-Gateway Services are provisioned.")
46+
defaultImg := os.Getenv("GARI_IMAGE")
47+
if defaultImg == "" {
48+
defaultImg = singlepod.DefaultDataplaneImage
49+
}
50+
51+
flag.StringVar(&controllerNamespace, "controller-namespace", defaultNs, "The namespace of the controller where per-Gateway Services and Deployments are provisioned.")
52+
flag.BoolVar(&dataplaneMode, "dataplane-mode", false, "Run in data-plane mode serving a single Gateway.")
53+
flag.StringVar(&gatewayNamespace, "gateway-namespace", "", "The namespace of the Gateway to serve in data-plane mode.")
54+
flag.StringVar(&gatewayName, "gateway-name", "", "The name of the Gateway to serve in data-plane mode.")
55+
flag.StringVar(&dataplaneImage, "dataplane-image", defaultImg, "The container image to use for provisioned per-Gateway data-plane Deployments.")
3956
flag.StringVar(&opts.MetricsAddr, "metrics-bind-address", opts.MetricsAddr, "The address the metric endpoint binds to.")
4057
flag.StringVar(&opts.HealthProbeBindAddress, "health-probe-bind-address", opts.HealthProbeBindAddress, "The address the probe endpoint binds to.")
4158
flag.StringVar(&opts.ProxyAddr, "proxy-bind-address", opts.ProxyAddr, "The address the proxy binds to.")
@@ -54,7 +71,27 @@ func main() {
5471

5572
ctrl.SetLogger(textlogger.NewLogger(logConfig))
5673

57-
opts.AddressProvider = singlepod.NewAddressProvider(nil, controllerNamespace)
74+
if dataplaneMode {
75+
opts.DisableStatusUpdates = true
76+
opts.LeaderElection = false
77+
opts.ProxyHTTP3Addr = opts.ProxyHTTPSAddr // enable HTTP/3 on the HTTPS port for dataplane
78+
opts.AddressProvider = nil
79+
if gatewayNamespace != "" && gatewayName != "" {
80+
opts.GatewayFilter = func(gw *gatewayv1.Gateway) bool {
81+
return gw.Namespace == gatewayNamespace && gw.Name == gatewayName
82+
}
83+
}
84+
} else {
85+
opts.ProxyAddr = ""
86+
opts.ProxyHTTPSAddr = ""
87+
opts.ProxyHTTP3Addr = ""
88+
opts.AddressProvider = singlepod.NewAddressProvider(
89+
nil,
90+
controllerNamespace,
91+
singlepod.WithDataplaneImage(dataplaneImage),
92+
singlepod.WithEnableH2C(opts.EnableH2C),
93+
)
94+
}
5895

5996
ctx := ctrl.SetupSignalHandler()
6097
if err := gari.Run(ctx, opts); err != nil {

‎k8s/controller.yaml‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ rules:
1515
- apiGroups: [""]
1616
resources: ["services"]
1717
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
18+
- apiGroups: ["apps"]
19+
resources: ["deployments"]
20+
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
1821
- apiGroups: [""]
1922
resources: ["configmaps", "secrets", "namespaces"]
2023
verbs: ["get", "list", "watch"]

‎pkg/controller/backendtlspolicy_controller.go‎

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -37,12 +37,12 @@ import (
3737

3838
type BackendTLSPolicyReconciler struct {
3939
client.Client
40-
Scheme *runtime.Scheme
41-
State *state.State
42-
Proxy *proxy.Proxy
43-
ControllerName string
44-
OnGatewaysUpdate func([]*gatewayv1.Gateway)
45-
ModelUpdater ModelUpdater
40+
Scheme *runtime.Scheme
41+
State *state.State
42+
Proxy *proxy.Proxy
43+
ControllerName string
44+
OnGatewaysUpdate func([]*gatewayv1.Gateway)
45+
DisableStatusUpdates bool
4646
}
4747

4848
func (r *BackendTLSPolicyReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
@@ -267,7 +267,7 @@ func (r *BackendTLSPolicyReconciler) Reconcile(ctx context.Context, req ctrl.Req
267267
}
268268
}
269269

270-
if updated {
270+
if updated && !r.DisableStatusUpdates {
271271
policy.Status.Ancestors = ancestors
272272
if err := r.Status().Update(ctx, policy); err != nil {
273273
l.Error(err, "unable to update BackendTLSPolicy status")
@@ -285,7 +285,7 @@ func (r *BackendTLSPolicyReconciler) Reconcile(ctx context.Context, req ctrl.Req
285285
}
286286

287287
func (r *BackendTLSPolicyReconciler) updateProxy() {
288-
updateProxy(r.State, r.Proxy, r.ControllerName, r.OnGatewaysUpdate, r.ModelUpdater)
288+
updateProxy(r.State, r.Proxy, r.ControllerName, r.OnGatewaysUpdate)
289289
}
290290

291291
func (r *BackendTLSPolicyReconciler) SetupWithManager(mgr ctrl.Manager) error {

‎pkg/controller/configmap_controller.go‎

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,6 @@ type ConfigMapReconciler struct {
3838
Proxy *proxy.Proxy
3939
ControllerName string
4040
OnGatewaysUpdate func([]*gatewayv1.Gateway)
41-
ModelUpdater ModelUpdater
4241
}
4342

4443
func (r *ConfigMapReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
@@ -62,7 +61,7 @@ func (r *ConfigMapReconciler) Reconcile(ctx context.Context, req ctrl.Request) (
6261
}
6362

6463
func (r *ConfigMapReconciler) updateProxy() {
65-
updateProxy(r.State, r.Proxy, r.ControllerName, r.OnGatewaysUpdate, r.ModelUpdater)
64+
updateProxy(r.State, r.Proxy, r.ControllerName, r.OnGatewaysUpdate)
6665
}
6766

6867
func (r *ConfigMapReconciler) SetupWithManager(mgr ctrl.Manager) error {

‎pkg/controller/controller_test.go‎

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -910,11 +910,16 @@ func findCondition(conditions []metav1.Condition, condType string) *metav1.Condi
910910

911911
type fakeAddressProvider struct {
912912
addresses []gatewayv1.GatewayStatusAddress
913+
ready bool
913914
err error
914915
}
915916

916-
func (f *fakeAddressProvider) GatewayAddresses(ctx context.Context, gw *gatewayv1.Gateway) ([]gatewayv1.GatewayStatusAddress, error) {
917-
return f.addresses, f.err
917+
func (f *fakeAddressProvider) GatewayAddresses(ctx context.Context, gw *gatewayv1.Gateway, effectiveListeners []*state.EffectiveListener) ([]gatewayv1.GatewayStatusAddress, bool, error) {
918+
ready := f.ready
919+
if !ready && f.err == nil && len(f.addresses) > 0 {
920+
ready = true
921+
}
922+
return f.addresses, ready, f.err
918923
}
919924

920925
type fakeAddressWatcherProvider struct {

0 commit comments

Comments
 (0)