@@ -278,8 +282,8 @@
atenet Router Status
System Component Health Checks
- Envoy Health
- {{ if .Health.Envoy.Healthy }}
+ Proxy Health ({{ .Health.Provider }})
+ {{ if .Health.Proxy.Healthy }}
Healthy
{{ else }}
Degraded
@@ -287,11 +291,11 @@
atenet Router Status
Message / Err:
- {{ .Health.Envoy.Message }}
+ {{ .Health.Proxy.Message }}
- Ok: {{ .Health.Envoy.SuccessCount }} | Err: {{ .Health.Envoy.FailureCount }}
- LKG: {{ if not .Health.Envoy.LastSuccess.IsZero }}{{ .Health.Envoy.LastSuccess.Format "15:04:05" }}{{ else }}N/A{{ end }}
+ Ok: {{ .Health.Proxy.SuccessCount }} | Err: {{ .Health.Proxy.FailureCount }}
+ LKG: {{ if not .Health.Proxy.LastSuccess.IsZero }}{{ .Health.Proxy.LastSuccess.Format "15:04:05" }}{{ else }}N/A{{ end }}
diff --git a/cmd/atenet/internal/app/router/envoy.go b/cmd/atenet/internal/app/router/envoy.go
new file mode 100644
index 000000000..88ec95365
--- /dev/null
+++ b/cmd/atenet/internal/app/router/envoy.go
@@ -0,0 +1,118 @@
+// Copyright 2026 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package router
+
+import (
+ "context"
+ "fmt"
+
+ corev1 "k8s.io/api/core/v1"
+ "k8s.io/apimachinery/pkg/util/intstr"
+)
+
+type envoyProvider struct {
+ cfg RouterConfig
+}
+
+func (p envoyProvider) Name() string {
+ return NetworkingModeEnvoy
+}
+
+func (p envoyProvider) RequiresXDS() bool {
+ return true
+}
+
+func (p envoyProvider) ConfigMapData() map[string]string {
+ envoyYaml := fmt.Sprintf(`admin:
+ address:
+ socket_address:
+ address: 0.0.0.0
+ port_value: 9901
+
+node:
+ id: %s
+ cluster: substrate-router-cluster
+
+dynamic_resources:
+ lds_config:
+ resource_api_version: V3
+ ads: {}
+ cds_config:
+ resource_api_version: V3
+ ads: {}
+ ads_config:
+ api_type: GRPC
+ transport_api_version: V3
+ grpc_services:
+ - envoy_grpc:
+ cluster_name: xds_cluster
+
+static_resources:
+ clusters:
+ - name: xds_cluster
+ connect_timeout: 0.25s
+ type: STRICT_DNS
+ lb_policy: ROUND_ROBIN
+ typed_extension_protocol_options:
+ envoy.extensions.upstreams.http.v3.HttpProtocolOptions:
+ "@type": type.googleapis.com/envoy.extensions.upstreams.http.v3.HttpProtocolOptions
+ explicit_http_config:
+ http2_protocol_options: {}
+ load_assignment:
+ cluster_name: xds_cluster
+ endpoints:
+ - lb_endpoints:
+ - endpoint:
+ address:
+ socket_address:
+ address: atenet-router
+ port_value: %d
+`, NodeID, p.cfg.XdsPort)
+
+ return map[string]string{"envoy.yaml": envoyYaml}
+}
+
+func (p envoyProvider) Container() corev1.Container {
+ return corev1.Container{
+ Name: "envoy",
+ Image: p.cfg.EnvoyImage,
+ Command: []string{
+ "/usr/local/bin/envoy",
+ "-c",
+ "/etc/envoy/envoy.yaml",
+ "--component-log-level",
+ "upstream:debug,router:debug,ext_proc:debug",
+ },
+ Ports: []corev1.ContainerPort{
+ {Name: "http", ContainerPort: int32(p.cfg.HttpPort)},
+ {Name: "https", ContainerPort: int32(p.cfg.HttpsPort)},
+ {Name: "admin", ContainerPort: 9901},
+ },
+ VolumeMounts: []corev1.VolumeMount{
+ {Name: "proxy-config", MountPath: "/etc/envoy"},
+ },
+ }
+}
+
+func (p envoyProvider) ServicePorts() []corev1.ServicePort {
+ return []corev1.ServicePort{
+ {Name: "http", Port: int32(p.cfg.HttpPort), TargetPort: intstr.FromString("http")},
+ {Name: "https", Port: int32(p.cfg.HttpsPort), TargetPort: intstr.FromString("https")},
+ }
+}
+
+func (p envoyProvider) CheckReady(ctx context.Context) (bool, string) {
+ return checkHTTPReady(ctx, "http://127.0.0.1:9901/ready", "LIVE")
+}
diff --git a/cmd/atenet/internal/app/router/envoyrunner.go b/cmd/atenet/internal/app/router/envoyrunner.go
deleted file mode 100644
index 8d38be29f..000000000
--- a/cmd/atenet/internal/app/router/envoyrunner.go
+++ /dev/null
@@ -1,258 +0,0 @@
-// Copyright 2026 Google LLC
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package router
-
-import (
- "context"
- "fmt"
- "log/slog"
-
- appsv1 "k8s.io/api/apps/v1"
- corev1 "k8s.io/api/core/v1"
- k8serrors "k8s.io/apimachinery/pkg/api/errors"
- metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
- "k8s.io/apimachinery/pkg/util/intstr"
- "sigs.k8s.io/controller-runtime/pkg/client"
-)
-
-const (
- EnvoyDeploymentName = "atenet-router-envoy"
- EnvoyServiceName = "atenet-router-envoy"
- EnvoyConfigMapName = "atenet-router-envoy-config"
-)
-
-// envoyrunner manages the dynamic deployment and lifecycle of the underlying
-// Envoy proxy instance running inside Kubernetes.
-type envoyrunner struct {
- k8sClient client.Client
- cfg RouterConfig
-}
-
-func newEnvoyRunner(k8sClient client.Client, cfg RouterConfig) *envoyrunner {
- return &envoyrunner{
- k8sClient: k8sClient,
- cfg: cfg,
- }
-}
-
-func (r *envoyrunner) reconcile(ctx context.Context) error {
- if err := r.reconcileEnvoyConfigMap(ctx); err != nil {
- return fmt.Errorf("failed configmap reconciliation: %w", err)
- }
-
- if err := r.reconcileEnvoyDeployment(ctx); err != nil {
- return fmt.Errorf("failed deployment reconciliation: %w", err)
- }
-
- if err := r.reconcileEnvoyService(ctx); err != nil {
- return fmt.Errorf("failed service reconciliation: %w", err)
- }
-
- return nil
-}
-
-func (r *envoyrunner) reconcileEnvoyConfigMap(ctx context.Context) error {
- envoyYaml := fmt.Sprintf(`admin:
- address:
- socket_address:
- address: 0.0.0.0
- port_value: 9901
-
-node:
- id: %s
- cluster: substrate-router-cluster
-
-dynamic_resources:
- lds_config:
- resource_api_version: V3
- ads: {}
- cds_config:
- resource_api_version: V3
- ads: {}
- ads_config:
- api_type: GRPC
- transport_api_version: V3
- grpc_services:
- - envoy_grpc:
- cluster_name: xds_cluster
-
-static_resources:
- clusters:
- - name: xds_cluster
- connect_timeout: 0.25s
- type: STRICT_DNS
- lb_policy: ROUND_ROBIN
- typed_extension_protocol_options:
- envoy.extensions.upstreams.http.v3.HttpProtocolOptions:
- "@type": type.googleapis.com/envoy.extensions.upstreams.http.v3.HttpProtocolOptions
- explicit_http_config:
- http2_protocol_options: {}
- load_assignment:
- cluster_name: xds_cluster
- endpoints:
- - lb_endpoints:
- - endpoint:
- address:
- socket_address:
- address: atenet-router
- port_value: %d
-`, NodeID, r.cfg.XdsPort)
-
- cm := &corev1.ConfigMap{
- ObjectMeta: metav1.ObjectMeta{
- Name: EnvoyConfigMapName,
- Namespace: r.cfg.Namespace,
- },
- Data: map[string]string{
- "envoy.yaml": envoyYaml,
- },
- }
-
- var existing corev1.ConfigMap
- err := r.k8sClient.Get(ctx, client.ObjectKey{Namespace: r.cfg.Namespace, Name: EnvoyConfigMapName}, &existing)
- if err != nil {
- if k8serrors.IsNotFound(err) {
- slog.InfoContext(ctx, "Creating Envoy bootstrap ConfigMap",
- slog.String("namespace", r.cfg.Namespace),
- slog.String("name", EnvoyConfigMapName))
- return r.k8sClient.Create(ctx, cm)
- }
- return err
- }
-
- existing.Data = cm.Data
- return r.k8sClient.Update(ctx, &existing)
-}
-
-func (r *envoyrunner) reconcileEnvoyDeployment(ctx context.Context) error {
- replicas := int32(1)
-
- dep := &appsv1.Deployment{
- ObjectMeta: metav1.ObjectMeta{
- Name: EnvoyDeploymentName,
- Namespace: r.cfg.Namespace,
- Labels: map[string]string{
- "app": "atenet-router-envoy",
- },
- },
- Spec: appsv1.DeploymentSpec{
- Replicas: &replicas,
- Selector: &metav1.LabelSelector{
- MatchLabels: map[string]string{
- "app": "atenet-router-envoy",
- },
- },
- Template: corev1.PodTemplateSpec{
- ObjectMeta: metav1.ObjectMeta{
- Labels: map[string]string{
- "app": "atenet-router-envoy",
- },
- },
- Spec: corev1.PodSpec{
- Containers: []corev1.Container{
- {
- Name: "envoy",
- Image: r.cfg.EnvoyImage,
- Command: []string{
- "/usr/local/bin/envoy",
- "-c",
- "/etc/envoy/envoy.yaml",
- "--component-log-level",
- "upstream:debug,router:debug,ext_proc:debug",
- },
- Ports: []corev1.ContainerPort{
- {
- Name: "http",
- ContainerPort: int32(r.cfg.HttpPort),
- },
- {
- Name: "admin",
- ContainerPort: 9901,
- },
- },
- VolumeMounts: []corev1.VolumeMount{
- {
- Name: "envoy-config",
- MountPath: "/etc/envoy",
- },
- },
- },
- },
- Volumes: []corev1.Volume{
- {
- Name: "envoy-config",
- VolumeSource: corev1.VolumeSource{
- ConfigMap: &corev1.ConfigMapVolumeSource{
- LocalObjectReference: corev1.LocalObjectReference{
- Name: EnvoyConfigMapName,
- },
- },
- },
- },
- },
- },
- },
- },
- }
-
- var existing appsv1.Deployment
- err := r.k8sClient.Get(ctx, client.ObjectKey{Namespace: r.cfg.Namespace, Name: EnvoyDeploymentName}, &existing)
- if err != nil {
- if k8serrors.IsNotFound(err) {
- slog.InfoContext(ctx, "Creating managed Envoy router Deployment", slog.String("namespace", r.cfg.Namespace))
- return r.k8sClient.Create(ctx, dep)
- }
- return err
- }
-
- existing.Spec = dep.Spec
- return r.k8sClient.Update(ctx, &existing)
-}
-
-func (r *envoyrunner) reconcileEnvoyService(ctx context.Context) error {
- svc := &corev1.Service{
- ObjectMeta: metav1.ObjectMeta{
- Name: EnvoyServiceName,
- Namespace: r.cfg.Namespace,
- },
- Spec: corev1.ServiceSpec{
- Type: corev1.ServiceTypeClusterIP,
- Selector: map[string]string{
- "app": "atenet-router-envoy",
- },
- Ports: []corev1.ServicePort{
- {
- Name: "http",
- Port: int32(r.cfg.HttpPort),
- TargetPort: intstr.FromString("http"),
- },
- },
- },
- }
-
- var existing corev1.Service
- err := r.k8sClient.Get(ctx, client.ObjectKey{Namespace: r.cfg.Namespace, Name: EnvoyServiceName}, &existing)
- if err != nil {
- if k8serrors.IsNotFound(err) {
- slog.InfoContext(ctx, "Creating managed Envoy router ClusterIP service", slog.String("namespace", r.cfg.Namespace))
- return r.k8sClient.Create(ctx, svc)
- }
- return err
- }
-
- existing.Spec.Ports = svc.Spec.Ports
- existing.Spec.Selector = svc.Spec.Selector
- return r.k8sClient.Update(ctx, &existing)
-}
diff --git a/cmd/atenet/internal/app/router/health.go b/cmd/atenet/internal/app/router/health.go
index 5d8d9449d..e743cd06c 100644
--- a/cmd/atenet/internal/app/router/health.go
+++ b/cmd/atenet/internal/app/router/health.go
@@ -17,10 +17,7 @@ package router
import (
"context"
"fmt"
- "io"
"log/slog"
- "net/http"
- "strings"
"sync"
"time"
@@ -38,9 +35,10 @@ type ComponentHealth struct {
}
type RouterHealthReport struct {
- Envoy ComponentHealth `json:"envoy"`
- K8sAPI ComponentHealth `json:"k8s_api"`
- AteAPI ComponentHealth `json:"ate_api"`
+ Proxy ComponentHealth `json:"proxy"`
+ Provider string `json:"provider"`
+ K8sAPI ComponentHealth `json:"k8s_api"`
+ AteAPI ComponentHealth `json:"ate_api"`
}
// routerHealth periodically checks the dependent services of router to track health
@@ -54,9 +52,10 @@ type routerHealth struct {
clientset kubernetes.Interface
apiClient ateapipb.ControlClient
cfg RouterConfig
+ provider proxyProvider
}
-func newRouterHealth(interval time.Duration, clientset kubernetes.Interface, apiClient ateapipb.ControlClient, cfg RouterConfig) *routerHealth {
+func newRouterHealth(interval time.Duration, clientset kubernetes.Interface, apiClient ateapipb.ControlClient, cfg RouterConfig, provider proxyProvider) *routerHealth {
if interval <= 0 {
interval = time.Second
}
@@ -65,6 +64,7 @@ func newRouterHealth(interval time.Duration, clientset kubernetes.Interface, api
clientset: clientset,
apiClient: apiClient,
cfg: cfg,
+ provider: provider,
}
}
@@ -91,20 +91,25 @@ func (rh *routerHealth) check(ctx context.Context) {
slog.InfoContext(ctx, "Checking health")
- // 1. Check Envoy
+ // 1. Check configured proxy
{
- healthy, msg := rh.checkEnvoy(ctx)
+ providerName := ""
+ if rh.provider != nil {
+ providerName = rh.provider.Name()
+ rh.report.Provider = providerName
+ }
+ healthy, msg := rh.checkProxy(ctx)
if healthy {
- rh.report.Envoy.Healthy = true
- rh.report.Envoy.Message = msg
- rh.report.Envoy.LastSuccess = time.Now()
- rh.report.Envoy.SuccessCount++
+ rh.report.Proxy.Healthy = true
+ rh.report.Proxy.Message = msg
+ rh.report.Proxy.LastSuccess = time.Now()
+ rh.report.Proxy.SuccessCount++
} else {
- rh.report.Envoy.Healthy = false
- rh.report.Envoy.Message = msg
- rh.report.Envoy.LastFailure = time.Now()
- rh.report.Envoy.FailureCount++
- slog.ErrorContext(ctx, "Envoy health check failed", slog.String("msg", msg))
+ rh.report.Proxy.Healthy = false
+ rh.report.Proxy.Message = msg
+ rh.report.Proxy.LastFailure = time.Now()
+ rh.report.Proxy.FailureCount++
+ slog.ErrorContext(ctx, "Proxy health check failed", slog.String("provider", providerName), slog.String("msg", msg))
}
}
@@ -143,36 +148,11 @@ func (rh *routerHealth) check(ctx context.Context) {
}
}
-func (rh *routerHealth) checkEnvoy(ctx context.Context) (bool, string) {
- timeoutCtx, cancel := context.WithTimeout(ctx, 500*time.Millisecond)
- defer cancel()
-
- req, err := http.NewRequestWithContext(timeoutCtx, "GET", "http://127.0.0.1:9901/ready", nil)
- if err != nil {
- return false, err.Error()
- }
-
- resp, err := http.DefaultClient.Do(req)
- if err != nil {
- return false, err.Error()
+func (rh *routerHealth) checkProxy(ctx context.Context) (bool, string) {
+ if rh.provider == nil {
+ return false, "No proxy provider"
}
- defer resp.Body.Close()
-
- if resp.StatusCode != http.StatusOK {
- return false, fmt.Sprintf("unexpected status code %d", resp.StatusCode)
- }
-
- bodyBytes, err := io.ReadAll(resp.Body)
- if err != nil {
- return false, err.Error()
- }
-
- bodyStr := strings.TrimSpace(string(bodyBytes))
- if bodyStr != "LIVE" {
- return false, fmt.Sprintf("expected LIVE but got %q", bodyStr)
- }
-
- return true, "LIVE"
+ return rh.provider.CheckReady(ctx)
}
func (rh *routerHealth) checkK8s() (bool, string) {
diff --git a/cmd/atenet/internal/app/router/provider.go b/cmd/atenet/internal/app/router/provider.go
new file mode 100644
index 000000000..ccd68cf79
--- /dev/null
+++ b/cmd/atenet/internal/app/router/provider.go
@@ -0,0 +1,102 @@
+// Copyright 2026 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package router
+
+import (
+ "context"
+ "fmt"
+ "io"
+ "net/http"
+ "strings"
+ "time"
+
+ corev1 "k8s.io/api/core/v1"
+)
+
+const (
+ NetworkingModeEnvoy = "envoy"
+ NetworkingModeAgentgateway = "agentgateway"
+)
+
+// proxyProvider owns proxy-specific configuration and runtime details. The
+// Substrate router keeps actor lookup and ext_proc behavior shared above this
+// provider boundary.
+type proxyProvider interface {
+ Name() string
+ RequiresXDS() bool
+ ConfigMapData() map[string]string
+ Container() corev1.Container
+ ServicePorts() []corev1.ServicePort
+ CheckReady(ctx context.Context) (bool, string)
+}
+
+func newProxyProvider(cfg RouterConfig) (proxyProvider, error) {
+ switch strings.ToLower(cfg.NetworkingMode) {
+ case "", NetworkingModeEnvoy:
+ return envoyProvider{cfg: cfg}, nil
+ case NetworkingModeAgentgateway:
+ return agentgatewayProvider{cfg: cfg}, nil
+ default:
+ return nil, fmt.Errorf("unsupported networking mode %q", cfg.NetworkingMode)
+ }
+}
+
+func tlsCertPath(cfg RouterConfig) string {
+ return cfg.TLSCertPath
+}
+
+func tlsKeyPath(cfg RouterConfig) string {
+ if cfg.TLSKeyPath != "" {
+ return cfg.TLSKeyPath
+ }
+ if cfg.TLSCertPath != "" {
+ return cfg.TLSCertPath
+ }
+ return ""
+}
+
+func checkHTTPReady(ctx context.Context, url string, expectedBody string) (bool, string) {
+ timeoutCtx, cancel := context.WithTimeout(ctx, 500*time.Millisecond)
+ defer cancel()
+
+ req, err := http.NewRequestWithContext(timeoutCtx, "GET", url, nil)
+ if err != nil {
+ return false, err.Error()
+ }
+
+ resp, err := http.DefaultClient.Do(req)
+ if err != nil {
+ return false, err.Error()
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode != http.StatusOK {
+ return false, fmt.Sprintf("unexpected status code %d", resp.StatusCode)
+ }
+
+ bodyBytes, err := io.ReadAll(resp.Body)
+ if err != nil {
+ return false, err.Error()
+ }
+
+ bodyStr := strings.TrimSpace(string(bodyBytes))
+ if expectedBody != "" && bodyStr != expectedBody {
+ return false, fmt.Sprintf("expected %s but got %q", expectedBody, bodyStr)
+ }
+ if bodyStr == "" {
+ return true, "ready"
+ }
+ return true, bodyStr
+}
diff --git a/cmd/atenet/internal/app/router/proxyrunner.go b/cmd/atenet/internal/app/router/proxyrunner.go
new file mode 100644
index 000000000..d55913f1e
--- /dev/null
+++ b/cmd/atenet/internal/app/router/proxyrunner.go
@@ -0,0 +1,183 @@
+// Copyright 2026 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package router
+
+import (
+ "context"
+ "fmt"
+ "log/slog"
+
+ appsv1 "k8s.io/api/apps/v1"
+ corev1 "k8s.io/api/core/v1"
+ k8serrors "k8s.io/apimachinery/pkg/api/errors"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "sigs.k8s.io/controller-runtime/pkg/client"
+)
+
+const (
+ ProxyDeploymentName = "atenet-router-proxy"
+ ProxyServiceName = "atenet-router-proxy"
+ ProxyConfigMapName = "atenet-router-proxy-config"
+)
+
+// proxyrunner manages the dynamic deployment and lifecycle of the configured
+// networking proxy instance running inside Kubernetes.
+type proxyrunner struct {
+ k8sClient client.Client
+ cfg RouterConfig
+ provider proxyProvider
+}
+
+func newProxyRunner(k8sClient client.Client, cfg RouterConfig, provider proxyProvider) *proxyrunner {
+ return &proxyrunner{
+ k8sClient: k8sClient,
+ cfg: cfg,
+ provider: provider,
+ }
+}
+
+func (r *proxyrunner) reconcile(ctx context.Context) error {
+ if err := r.reconcileProxyConfigMap(ctx); err != nil {
+ return fmt.Errorf("failed configmap reconciliation: %w", err)
+ }
+
+ if err := r.reconcileProxyDeployment(ctx); err != nil {
+ return fmt.Errorf("failed deployment reconciliation: %w", err)
+ }
+
+ if err := r.reconcileProxyService(ctx); err != nil {
+ return fmt.Errorf("failed service reconciliation: %w", err)
+ }
+
+ return nil
+}
+
+func (r *proxyrunner) reconcileProxyConfigMap(ctx context.Context) error {
+ cm := &corev1.ConfigMap{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: ProxyConfigMapName,
+ Namespace: r.cfg.Namespace,
+ },
+ Data: r.provider.ConfigMapData(),
+ }
+
+ var existing corev1.ConfigMap
+ err := r.k8sClient.Get(ctx, client.ObjectKey{Namespace: r.cfg.Namespace, Name: ProxyConfigMapName}, &existing)
+ if err != nil {
+ if k8serrors.IsNotFound(err) {
+ slog.InfoContext(ctx, "Creating proxy ConfigMap",
+ slog.String("namespace", r.cfg.Namespace),
+ slog.String("name", ProxyConfigMapName),
+ slog.String("provider", r.provider.Name()))
+ return r.k8sClient.Create(ctx, cm)
+ }
+ return err
+ }
+
+ existing.Data = cm.Data
+ return r.k8sClient.Update(ctx, &existing)
+}
+
+func (r *proxyrunner) reconcileProxyDeployment(ctx context.Context) error {
+ replicas := int32(1)
+ labels := map[string]string{
+ "app": "atenet-router-proxy",
+ "substrate.ate.dev/proxy": r.provider.Name(),
+ "substrate.ate.dev/router": "atenet-router",
+ }
+
+ dep := &appsv1.Deployment{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: ProxyDeploymentName,
+ Namespace: r.cfg.Namespace,
+ Labels: labels,
+ },
+ Spec: appsv1.DeploymentSpec{
+ Replicas: &replicas,
+ Selector: &metav1.LabelSelector{
+ MatchLabels: labels,
+ },
+ Template: corev1.PodTemplateSpec{
+ ObjectMeta: metav1.ObjectMeta{
+ Labels: labels,
+ },
+ Spec: corev1.PodSpec{
+ Containers: []corev1.Container{r.provider.Container()},
+ Volumes: []corev1.Volume{
+ {
+ Name: "proxy-config",
+ VolumeSource: corev1.VolumeSource{
+ ConfigMap: &corev1.ConfigMapVolumeSource{
+ LocalObjectReference: corev1.LocalObjectReference{
+ Name: ProxyConfigMapName,
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ }
+
+ var existing appsv1.Deployment
+ err := r.k8sClient.Get(ctx, client.ObjectKey{Namespace: r.cfg.Namespace, Name: ProxyDeploymentName}, &existing)
+ if err != nil {
+ if k8serrors.IsNotFound(err) {
+ slog.InfoContext(ctx, "Creating managed router proxy Deployment",
+ slog.String("namespace", r.cfg.Namespace),
+ slog.String("provider", r.provider.Name()))
+ return r.k8sClient.Create(ctx, dep)
+ }
+ return err
+ }
+
+ existing.Spec = dep.Spec
+ return r.k8sClient.Update(ctx, &existing)
+}
+
+func (r *proxyrunner) reconcileProxyService(ctx context.Context) error {
+ svc := &corev1.Service{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: ProxyServiceName,
+ Namespace: r.cfg.Namespace,
+ },
+ Spec: corev1.ServiceSpec{
+ Type: corev1.ServiceTypeClusterIP,
+ Selector: map[string]string{
+ "app": "atenet-router-proxy",
+ "substrate.ate.dev/proxy": r.provider.Name(),
+ "substrate.ate.dev/router": "atenet-router",
+ },
+ Ports: r.provider.ServicePorts(),
+ },
+ }
+
+ var existing corev1.Service
+ err := r.k8sClient.Get(ctx, client.ObjectKey{Namespace: r.cfg.Namespace, Name: ProxyServiceName}, &existing)
+ if err != nil {
+ if k8serrors.IsNotFound(err) {
+ slog.InfoContext(ctx, "Creating managed router proxy ClusterIP service",
+ slog.String("namespace", r.cfg.Namespace),
+ slog.String("provider", r.provider.Name()))
+ return r.k8sClient.Create(ctx, svc)
+ }
+ return err
+ }
+
+ existing.Spec.Ports = svc.Spec.Ports
+ existing.Spec.Selector = svc.Spec.Selector
+ return r.k8sClient.Update(ctx, &existing)
+}
diff --git a/cmd/atenet/internal/app/router/router.go b/cmd/atenet/internal/app/router/router.go
index c85fe4692..1c39e20be 100644
--- a/cmd/atenet/internal/app/router/router.go
+++ b/cmd/atenet/internal/app/router/router.go
@@ -61,21 +61,24 @@ func init() {
// RouterConfig holds deployment setup and endpoint options for the router node instance.
type RouterConfig struct {
- Standalone bool
- Namespace string
- Kubeconfig string
- AteapiAddr string
- HttpPort int
- XdsPort int
- ExtprocPort int
- ExtprocAddr string
- EnvoyImage string
- TemplatesFile string
- StatusPort int
- HealthInterval time.Duration
- HttpsPort int
- EnvoyCertPath string
- LogLevel string
+ Standalone bool
+ Namespace string
+ Kubeconfig string
+ AteapiAddr string
+ HttpPort int
+ XdsPort int
+ ExtprocPort int
+ ExtprocAddr string
+ NetworkingMode string
+ EnvoyImage string
+ AgentgatewayImage string
+ TemplatesFile string
+ StatusPort int
+ HealthInterval time.Duration
+ HttpsPort int
+ TLSCertPath string
+ TLSKeyPath string
+ LogLevel string
}
// RouterServer instantiates and coordinates runtime threads executing system modules.
@@ -140,17 +143,24 @@ func NewCmd() *cobra.Command {
cmd.Flags().IntVar(&cfg.XdsPort, "port-xds", 18000, "TCP port listening for the xDS dynamic Envoy connections")
cmd.Flags().IntVar(&cfg.ExtprocPort, "port-extproc", 50051, "Listen port for the Envoy dynamic External Processing (ext_proc) server")
cmd.Flags().StringVar(&cfg.ExtprocAddr, "extproc-address", "127.0.0.1", "Host IP or address of the Envoy External Processing (ext_proc) server")
+ cmd.Flags().StringVar(&cfg.NetworkingMode, "networking-mode", NetworkingModeEnvoy, "Networking proxy mode: envoy or agentgateway")
cmd.Flags().StringVar(&cfg.EnvoyImage, "envoy-image", "envoyproxy/envoy:v1.30-latest", "Image URI used for dynamically launched router instances")
+ cmd.Flags().StringVar(&cfg.AgentgatewayImage, "agentgateway-image", "cr.agentgateway.dev/agentgateway:v1.3.0-alpha.1", "Image URI used for Agentgateway router instances")
cmd.Flags().StringVar(&cfg.TemplatesFile, "actor-templates-file", "", "Path to offline YAML configuration file listing ActorTemplates")
cmd.Flags().IntVar(&cfg.StatusPort, "status-port", 4040, "Port to serve /statusz on (set <= 0 to disable serving status)")
cmd.Flags().DurationVar(&cfg.HealthInterval, "health-interval", 1*time.Second, "Interval for checking health of dependent services")
cmd.Flags().IntVar(&cfg.HttpsPort, "port-https", 8443, "TCP port for HTTPS workload traffic entering through the Envoy Router")
- cmd.Flags().StringVar(&cfg.EnvoyCertPath, "envoy-cert-path", "", "Path to the Envoy certificate file (if empty, a self-signed cert will be generated for testing)")
+ cmd.Flags().StringVar(&cfg.TLSCertPath, "tls-cert-path", "", "Path to the proxy TLS certificate file")
+ cmd.Flags().StringVar(&cfg.TLSKeyPath, "tls-key-path", "", "Path to the proxy TLS private key file")
return cmd
}
func NewRouterServer(cfg RouterConfig) (*RouterServer, error) {
+ if cfg.NetworkingMode == "" {
+ cfg.NetworkingMode = NetworkingModeEnvoy
+ }
+
var k8sClient client.Client
var clientset kubernetes.Interface
var err error
@@ -211,26 +221,30 @@ func (s *RouterServer) Run(ctx context.Context) error {
g, ctx := errgroup.WithContext(ctx)
+ provider, err := newProxyProvider(s.cfg)
+ if err != nil {
+ return err
+ }
+
xdsSrv := NewXdsServer(s.cfg.XdsPort)
xdsSrv.SetConfig(s.cfg.HttpPort, s.cfg.ExtprocPort, s.cfg.ExtprocAddr)
var certContent, keyContent string
- if s.cfg.EnvoyCertPath == "" {
- slog.InfoContext(ctx, "No Envoy certificate path provided, generating self-signed certificate for testing")
- var err error
+ if provider.RequiresXDS() && tlsCertPath(s.cfg) == "" {
+ slog.InfoContext(ctx, "No proxy TLS certificate path provided, generating self-signed certificate for testing")
certContent, keyContent, err = generateSelfSignedCert()
if err != nil {
return fmt.Errorf("failed to generate self-signed cert: %w", err)
}
}
- xdsSrv.SetTlsConfig(s.cfg.HttpsPort, s.cfg.EnvoyCertPath, certContent, keyContent)
+ xdsSrv.SetTlsConfig(s.cfg.HttpsPort, tlsCertPath(s.cfg), certContent, keyContent)
if s.extprocSrv == nil {
s.extprocSrv = NewExtProcServer(s.cfg.ExtprocPort, s.apiClient)
}
- ctrl := NewController(s.k8sClient, s.clientset, s.cfg, xdsSrv, s.extprocSrv)
+ ctrl := NewController(s.k8sClient, s.clientset, s.cfg, xdsSrv, s.extprocSrv, provider)
- s.health = newRouterHealth(s.cfg.HealthInterval, s.clientset, s.apiClient, s.cfg)
+ s.health = newRouterHealth(s.cfg.HealthInterval, s.clientset, s.apiClient, s.cfg, provider)
// Start Controller / Watcher
g.Go(func() error {
@@ -245,17 +259,19 @@ func (s *RouterServer) Run(ctx context.Context) error {
return nil
})
- // Start xDS Server
- g.Go(func() error {
- slog.InfoContext(ctx, "Starting Envoy xDS Server", slog.Int("port", s.cfg.XdsPort))
- lis, err := net.Listen("tcp", fmt.Sprintf(":%d", s.cfg.XdsPort))
- if err != nil {
- return fmt.Errorf("failed to listen on port %d: %w", s.cfg.XdsPort, err)
- }
- defer lis.Close()
+ if provider.RequiresXDS() {
+ // Start xDS Server
+ g.Go(func() error {
+ slog.InfoContext(ctx, "Starting xDS Server", slog.Int("port", s.cfg.XdsPort), slog.String("provider", provider.Name()))
+ lis, err := net.Listen("tcp", fmt.Sprintf(":%d", s.cfg.XdsPort))
+ if err != nil {
+ return fmt.Errorf("failed to listen on port %d: %w", s.cfg.XdsPort, err)
+ }
+ defer lis.Close()
- return xdsSrv.Serve(ctx, lis)
- })
+ return xdsSrv.Serve(ctx, lis)
+ })
+ }
// Start ExtProc Server
g.Go(func() error {
diff --git a/cmd/atenet/internal/app/router/status.go b/cmd/atenet/internal/app/router/status.go
index c8e8be55c..da642f566 100644
--- a/cmd/atenet/internal/app/router/status.go
+++ b/cmd/atenet/internal/app/router/status.go
@@ -131,6 +131,7 @@ type DashboardContext struct {
BuildTag string `json:"build_tag"`
RouterClusterIP string `json:"router_cluster_ip"`
Namespace string `json:"namespace"`
+ NetworkingMode string `json:"networking_mode"`
HttpPort int `json:"port_http"`
XdsPort int `json:"port_xds"`
ExtprocPort int `json:"port_extproc"`
@@ -238,6 +239,7 @@ func (s *RouterServer) handleStatusz(w http.ResponseWriter, req *http.Request) {
BuildTag: buildInfo,
RouterClusterIP: routerIP,
Namespace: s.cfg.Namespace,
+ NetworkingMode: s.cfg.NetworkingMode,
HttpPort: s.cfg.HttpPort,
XdsPort: s.cfg.XdsPort,
ExtprocPort: s.cfg.ExtprocPort,
diff --git a/hack/install-ate.sh b/hack/install-ate.sh
index 8e043e85f..9f26dd714 100755
--- a/hack/install-ate.sh
+++ b/hack/install-ate.sh
@@ -39,6 +39,8 @@ fi
# ATE_DEMOS is an array that registers the prefix name of the demo functions.
ATE_DEMOS=()
+ATE_INSTALL_ATENET_ROUTER="${ATE_INSTALL_ATENET_ROUTER:-envoy}"
+
# Include demos.
source "${ROOT}"/hack/install-demo-counter.sh
source "${ROOT}"/hack/install-demo-sandbox.sh
@@ -61,6 +63,7 @@ function usage() {
echo "Overall infrastructure (all infrastructure components):"
echo ""
echo " --deploy-ate-system Deploy core system (CRDs, atelet, apiserver)"
+ echo " --router=envoy|agentgateway Select atenet-router implementation (default: envoy)"
echo " --delete-ate-system Delete core system"
echo " --delete-all Delete core system and all registered demos"
echo ""
@@ -108,6 +111,63 @@ run_ko() {
-- ${KUBECTL_CONTEXT:+--context=${KUBECTL_CONTEXT}}
}
+set_atenet_router() {
+ case "$1" in
+ envoy|agentgateway)
+ ATE_INSTALL_ATENET_ROUTER="$1"
+ ;;
+ *)
+ echo "unsupported atenet router mode: $1" >&2
+ exit 1
+ ;;
+ esac
+}
+
+atenet_router_manifest() {
+ case "${ATE_INSTALL_ATENET_ROUTER}" in
+ envoy)
+ echo "manifests/ate-install/atenet-router.yaml"
+ ;;
+ agentgateway)
+ echo "manifests/ate-install/atenet-router-agentgateway.yaml"
+ ;;
+ *)
+ echo "unsupported atenet router mode: ${ATE_INSTALL_ATENET_ROUTER}" >&2
+ exit 1
+ ;;
+ esac
+}
+
+ate_install_kustomize_base_dir() {
+ case "${ATE_INSTALL_ATENET_ROUTER}" in
+ envoy)
+ echo "manifests/ate-install/base"
+ ;;
+ agentgateway)
+ echo "manifests/ate-install/base-agentgateway"
+ ;;
+ *)
+ echo "unsupported atenet router mode: ${ATE_INSTALL_ATENET_ROUTER}" >&2
+ exit 1
+ ;;
+ esac
+}
+
+ate_install_kustomize_dir() {
+ case "${ATE_INSTALL_ATENET_ROUTER}" in
+ envoy)
+ echo "manifests/ate-install/kind"
+ ;;
+ agentgateway)
+ echo "manifests/ate-install/kind-agentgateway"
+ ;;
+ *)
+ echo "unsupported atenet router mode: ${ATE_INSTALL_ATENET_ROUTER}" >&2
+ exit 1
+ ;;
+ esac
+}
+
create_valkey_ca_certs_secret() {
log_step "create_valkey_ca_certs_secret"
local ca_certs=""
@@ -232,10 +292,10 @@ deploy_ate_system() {
local manifests=""
if [[ "${ATE_INSTALL_KIND:-false}" == "true" ]]; then
# Build everything resolved with Kustomize for Kind
- manifests=$(kubectl kustomize manifests/ate-install/kind --load-restrictor LoadRestrictionsNone | run_ko resolve -f -)
+ manifests=$(kubectl kustomize "$(ate_install_kustomize_dir)" --load-restrictor LoadRestrictionsNone | run_ko resolve -f -)
else
# Build everything resolved with base manifests for GKE
- manifests=$(run_ko resolve -f manifests/ate-install)
+ manifests=$(kubectl kustomize "$(ate_install_kustomize_base_dir)" --load-restrictor LoadRestrictionsNone | run_ko resolve -f -)
fi
echo "${manifests}" | run_kubectl apply -f -
@@ -305,7 +365,7 @@ deploy_atenet() {
run_kubectl apply -f manifests/ate-install/ate-system-namespace.yaml \
&& run_kubectl wait --for=jsonpath='{.status.phase}'=Active namespace/ate-system --timeout=60s
- run_ko apply -f manifests/ate-install/atenet-router.yaml
+ run_ko apply -f "$(atenet_router_manifest)"
run_ko apply -f manifests/ate-install/atenet-dns.yaml
run_kubectl rollout status deployment/atenet-router -n ate-system --timeout=120s
run_kubectl rollout status deployment/atenet-dns -n ate-system --timeout=120s
@@ -324,7 +384,7 @@ delete_ate_system() {
delete_atenet() {
log_step "delete_atenet"
- run_kubectl delete --ignore-not-found -f manifests/ate-install/atenet-router.yaml
+ run_kubectl delete --ignore-not-found -f "$(atenet_router_manifest)"
}
delete_all() {
@@ -349,6 +409,9 @@ for arg in "$@"; do
usage
exit 0
;;
+ --router=*)
+ set_atenet_router "${arg#--router=}"
+ ;;
esac
done
@@ -367,6 +430,7 @@ while [[ "$#" -gt 0 ]]; do
case $1 in
--deploy-ate-system) deploy_ate_system ;;
+ --router=*) ;;
--delete-ate-system) delete_ate_system ;;
--delete-all) delete_all ;;
diff --git a/manifests/ate-install/atenet-router-agentgateway.yaml b/manifests/ate-install/atenet-router-agentgateway.yaml
new file mode 100644
index 000000000..d39972906
--- /dev/null
+++ b/manifests/ate-install/atenet-router-agentgateway.yaml
@@ -0,0 +1,197 @@
+# Copyright 2026 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+apiVersion: v1
+kind: ServiceAccount
+metadata:
+ name: atenet-router
+ namespace: ate-system
+ labels:
+ app: atenet-router
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRole
+metadata:
+ name: atenet-router
+rules:
+- apiGroups:
+ - "ate.dev"
+ resources:
+ - actortemplates
+ verbs:
+ - get
+ - watch
+ - list
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRoleBinding
+metadata:
+ name: atenet-router
+subjects:
+- kind: ServiceAccount
+ name: atenet-router
+ namespace: ate-system
+roleRef:
+ kind: ClusterRole
+ name: atenet-router
+ apiGroup: rbac.authorization.k8s.io
+---
+apiVersion: v1
+kind: ConfigMap
+metadata:
+ name: atenet-router-agentgateway-config
+ namespace: ate-system
+data:
+ config.yaml: |
+ binds:
+ - port: 8080
+ listeners:
+ - name: http
+ protocol: HTTP
+ routes:
+ - name: substrate-http
+ matches:
+ - path:
+ pathPrefix: /
+ policies:
+ extProc:
+ host: "127.0.0.1:50051"
+ processingOptions:
+ requestBodyMode: none
+ responseBodyMode: none
+ requestHeaderMode: send
+ responseHeaderMode: skip
+ requestTrailerMode: skip
+ responseTrailerMode: skip
+ backends:
+ - dynamic: {}
+ - port: 8443
+ listeners:
+ - name: https
+ protocol: HTTPS
+ tls:
+ cert: "/run/servicedns.podcert.ate.dev/cert.pem"
+ key: "/run/servicedns.podcert.ate.dev/key.pem"
+ routes:
+ - name: substrate-https
+ matches:
+ - path:
+ pathPrefix: /
+ policies:
+ extProc:
+ host: "127.0.0.1:50051"
+ processingOptions:
+ requestBodyMode: none
+ responseBodyMode: none
+ requestHeaderMode: send
+ responseHeaderMode: skip
+ requestTrailerMode: skip
+ responseTrailerMode: skip
+ backends:
+ - dynamic: {}
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: atenet-router
+ namespace: ate-system
+ labels:
+ app: atenet-router
+spec:
+ replicas: 1
+ selector:
+ matchLabels:
+ app: atenet-router
+ template:
+ metadata:
+ labels:
+ app: atenet-router
+ spec:
+ serviceAccountName: atenet-router
+ containers:
+ - name: atenet-router
+ image: ko://github.com/agent-substrate/substrate/cmd/atenet
+ args:
+ - "router"
+ - "--networking-mode=agentgateway"
+ - "--standalone"
+ - "--namespace=ate-system"
+ - "--port-http=8080"
+ - "--port-extproc=50051"
+ - "--extproc-address=127.0.0.1"
+ - "--ateapi-address=api.ate-system.svc:443"
+ - "--status-port=4040"
+ - "--port-https=8443"
+ - "--tls-cert-path=/run/servicedns.podcert.ate.dev/cert.pem"
+ - "--tls-key-path=/run/servicedns.podcert.ate.dev/key.pem"
+ ports:
+ - name: extproc
+ containerPort: 50051
+ - name: status
+ containerPort: 4040
+ - name: agentgateway
+ image: cr.agentgateway.dev/agentgateway:v1.3.0-alpha.1
+ args:
+ - "-f"
+ - "/etc/agentgateway/config.yaml"
+ ports:
+ - name: http
+ containerPort: 8080
+ - name: https
+ containerPort: 8443
+ - name: readiness
+ containerPort: 15021
+ - name: metrics
+ containerPort: 15020
+ readinessProbe:
+ httpGet:
+ path: /healthz/ready
+ port: 15021
+ periodSeconds: 10
+ volumeMounts:
+ - name: agentgateway-config
+ mountPath: /etc/agentgateway
+ - name: "servicedns"
+ mountPath: "/run/servicedns.podcert.ate.dev"
+ volumes:
+ - name: agentgateway-config
+ configMap:
+ name: atenet-router-agentgateway-config
+ - name: "servicedns"
+ projected:
+ sources:
+ - podCertificate:
+ signerName: servicedns.podcert.ate.dev/identity
+ keyType: ECDSAP256
+ certificateChainPath: cert.pem
+ keyPath: key.pem
+---
+apiVersion: v1
+kind: Service
+metadata:
+ name: atenet-router
+ namespace: ate-system
+spec:
+ type: ClusterIP
+ selector:
+ app: atenet-router
+ ports:
+ - name: http
+ port: 80
+ targetPort: 8080
+ protocol: TCP
+ - name: https
+ port: 443
+ targetPort: 8443
+ protocol: TCP
diff --git a/manifests/ate-install/atenet-router.yaml b/manifests/ate-install/atenet-router.yaml
index d4269a59a..6bc84bb63 100644
--- a/manifests/ate-install/atenet-router.yaml
+++ b/manifests/ate-install/atenet-router.yaml
@@ -122,6 +122,7 @@ spec:
image: ko://github.com/agent-substrate/substrate/cmd/atenet
args:
- "router"
+ - "--networking-mode=envoy"
- "--standalone"
- "--namespace=ate-system"
- "--port-http=8080"
@@ -131,7 +132,8 @@ spec:
- "--ateapi-address=api.ate-system.svc:443"
- "--status-port=4040"
- "--port-https=8443"
- - "--envoy-cert-path=/run/servicedns.podcert.ate.dev/credential-bundle.pem"
+ - "--tls-cert-path=/run/servicedns.podcert.ate.dev/credential-bundle.pem"
+ - "--tls-key-path=/run/servicedns.podcert.ate.dev/credential-bundle.pem"
ports:
- name: xds
containerPort: 18000
diff --git a/manifests/ate-install/base-agentgateway/kustomization.yaml b/manifests/ate-install/base-agentgateway/kustomization.yaml
new file mode 100644
index 000000000..d5883ba79
--- /dev/null
+++ b/manifests/ate-install/base-agentgateway/kustomization.yaml
@@ -0,0 +1,25 @@
+# Copyright 2026 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+apiVersion: kustomize.config.k8s.io/v1beta1
+kind: Kustomization
+
+resources:
+ - ../ate-api-server.yaml
+ - ../ate-controller.yaml
+ - ../atelet.yaml
+ - ../atenet-dns.yaml
+ - ../atenet-router-agentgateway.yaml
+ - ../valkey.yaml
+ - ../pod-certificate-controller.yaml
diff --git a/manifests/ate-install/base/kustomization.yaml b/manifests/ate-install/base/kustomization.yaml
new file mode 100644
index 000000000..61cde5ace
--- /dev/null
+++ b/manifests/ate-install/base/kustomization.yaml
@@ -0,0 +1,25 @@
+# Copyright 2026 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+apiVersion: kustomize.config.k8s.io/v1beta1
+kind: Kustomization
+
+resources:
+ - ../ate-api-server.yaml
+ - ../ate-controller.yaml
+ - ../atelet.yaml
+ - ../atenet-dns.yaml
+ - ../atenet-router.yaml
+ - ../valkey.yaml
+ - ../pod-certificate-controller.yaml
diff --git a/manifests/ate-install/kind-agentgateway/kustomization.yaml b/manifests/ate-install/kind-agentgateway/kustomization.yaml
new file mode 100644
index 000000000..d9c9192b9
--- /dev/null
+++ b/manifests/ate-install/kind-agentgateway/kustomization.yaml
@@ -0,0 +1,43 @@
+# Copyright 2026 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+apiVersion: kustomize.config.k8s.io/v1beta1
+kind: Kustomization
+
+resources:
+ - ../ate-api-server.yaml
+ - ../ate-controller.yaml
+ - ../kind/atelet
+ - ../atenet-dns.yaml
+ - ../atenet-router-agentgateway.yaml
+ - ../valkey.yaml
+ - ../pod-certificate-controller.yaml
+ - ../kind/rustfs.yaml
+ - ../kind/otel-collector.yaml
+
+patches:
+ - patch: |-
+ apiVersion: apps/v1
+ kind: Deployment
+ metadata:
+ name: ate-api-server-deployment
+ namespace: ate-system
+ spec:
+ template:
+ spec:
+ containers:
+ - name: ate-api-server
+ env:
+ - name: OTEL_EXPORTER_OTLP_ENDPOINT
+ value: http://opentelemetry-collector.otel-system.svc:4317