Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
141 changes: 141 additions & 0 deletions cmd/atenet/internal/app/router/agentgateway.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
// 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"
"strings"

corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/util/intstr"
)

type agentgatewayProvider struct {
cfg RouterConfig
}

func (p agentgatewayProvider) Name() string {
return NetworkingModeAgentgateway
}

func (p agentgatewayProvider) RequiresXDS() bool {
return false
}

func (p agentgatewayProvider) ConfigMapData() map[string]string {
return map[string]string{"config.yaml": p.localConfig()}
}

func (p agentgatewayProvider) Container() corev1.Container {
ports := []corev1.ContainerPort{
{Name: "http", ContainerPort: int32(p.cfg.HttpPort)},
{Name: "readiness", ContainerPort: 15021},
{Name: "metrics", ContainerPort: 15020},
}
if p.cfg.HttpsPort > 0 && tlsCertPath(p.cfg) != "" {
ports = append(ports, corev1.ContainerPort{Name: "https", ContainerPort: int32(p.cfg.HttpsPort)})
}

return corev1.Container{
Name: "agentgateway",
Image: p.cfg.AgentgatewayImage,
Args: []string{"-f", "/etc/agentgateway/config.yaml"},
Ports: ports,
VolumeMounts: []corev1.VolumeMount{
{Name: "proxy-config", MountPath: "/etc/agentgateway"},
},
ReadinessProbe: &corev1.Probe{
ProbeHandler: corev1.ProbeHandler{
HTTPGet: &corev1.HTTPGetAction{
Path: "/healthz/ready",
Port: intstr.FromInt32(15021),
},
},
PeriodSeconds: 10,
},
}
}

func (p agentgatewayProvider) ServicePorts() []corev1.ServicePort {
ports := []corev1.ServicePort{
{Name: "http", Port: int32(p.cfg.HttpPort), TargetPort: intstr.FromString("http")},
}
if p.cfg.HttpsPort > 0 && tlsCertPath(p.cfg) != "" {
ports = append(ports, corev1.ServicePort{Name: "https", Port: int32(p.cfg.HttpsPort), TargetPort: intstr.FromString("https")})
}
return ports
}

func (p agentgatewayProvider) CheckReady(ctx context.Context) (bool, string) {
return checkHTTPReady(ctx, "http://127.0.0.1:15021/healthz/ready", "")
}

func (p agentgatewayProvider) localConfig() string {
httpRoute := p.routeBlock("substrate-http")
config := fmt.Sprintf(`# yaml-language-server: $schema=https://agentgateway.dev/schema/config
config:
adminAddr: "127.0.0.1:15000"
readinessAddr: "0.0.0.0:15021"
statsAddr: "0.0.0.0:15020"
binds:
- port: %d
listeners:
- name: http
protocol: HTTP
routes:
%s`, p.cfg.HttpPort, indent(httpRoute, 4))

if p.cfg.HttpsPort > 0 && tlsCertPath(p.cfg) != "" {
cert := tlsCertPath(p.cfg)
key := tlsKeyPath(p.cfg)
config += fmt.Sprintf(`- port: %d
listeners:
- name: https
protocol: HTTPS
tls:
cert: %q
key: %q
routes:
%s`, p.cfg.HttpsPort, cert, key, indent(p.routeBlock("substrate-https"), 4))
}

return config
}

func (p agentgatewayProvider) routeBlock(name string) string {
return fmt.Sprintf(`- name: %s
matches:
- path:
pathPrefix: /
policies:
extProc:
host: %q
failureMode: failClosed
backends:
- dynamic: {}
`, name, fmt.Sprintf("%s:%d", p.cfg.ExtprocAddr, p.cfg.ExtprocPort))
}

func indent(s string, spaces int) string {
prefix := strings.Repeat(" ", spaces)
lines := strings.Split(s, "\n")
for i, line := range lines {
if line != "" {
lines[i] = prefix + line
}
}
return strings.Join(lines, "\n")
}
25 changes: 16 additions & 9 deletions cmd/atenet/internal/app/router/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,10 @@ type Controller struct {
cfg RouterConfig
xdsSrv *XdsServer
extprocSrv *ExtProcServer
provider proxyProvider

atStore atStore
envoyRunner *envoyrunner
proxyRunner *proxyrunner
}

func NewController(
Expand All @@ -42,8 +43,11 @@ func NewController(
cfg RouterConfig,
xdsSrv *XdsServer,
extprocSrv *ExtProcServer,
provider proxyProvider,
) *Controller {
xdsSrv.SetConfig(cfg.HttpPort, cfg.ExtprocPort, cfg.ExtprocAddr)
if xdsSrv != nil {
xdsSrv.SetConfig(cfg.HttpPort, cfg.ExtprocPort, cfg.ExtprocAddr)
}

var store atStore
if cfg.TemplatesFile != "" {
Expand All @@ -58,9 +62,10 @@ func NewController(
cfg: cfg,
xdsSrv: xdsSrv,
extprocSrv: extprocSrv,
provider: provider,

atStore: store,
envoyRunner: newEnvoyRunner(k8sClient, cfg),
proxyRunner: newProxyRunner(k8sClient, cfg, provider),
}
}

Expand Down Expand Up @@ -92,16 +97,18 @@ func (c *Controller) reconcile(ctx context.Context) error {
return err
}

if err := c.xdsSrv.UpdateSnapshot(); err != nil {
slog.ErrorContext(ctx, "xDS Configuration generation problem", slog.String("err", err.Error()))
return err
if c.provider.RequiresXDS() {
if err := c.xdsSrv.UpdateSnapshot(); err != nil {
slog.ErrorContext(ctx, "xDS Configuration generation problem", slog.String("err", err.Error()))
return err
}
}

if !c.cfg.Standalone && c.cfg.TemplatesFile == "" {
// Reconcile Envoy router Deployment and Kubernetes cluster entities
err := c.envoyRunner.reconcile(ctx)
// Reconcile router proxy Deployment and Kubernetes cluster entities.
err := c.proxyRunner.reconcile(ctx)
if err != nil {
slog.ErrorContext(ctx, "Error during Envoy router reconciliation", slog.String("err", err.Error()))
slog.ErrorContext(ctx, "Error during router proxy reconciliation", slog.String("err", err.Error()))
return err
}
}
Expand Down
16 changes: 10 additions & 6 deletions cmd/atenet/internal/app/router/dashboard.html
Original file line number Diff line number Diff line change
Expand Up @@ -252,12 +252,16 @@ <h1>atenet Router Status</h1>
<span class="label">Namespace Context</span>
<span class="value">{{ .Namespace }}</span>
</div>
<div class="metadata-item">
<span class="label">Networking Mode</span>
<span class="value">{{ .NetworkingMode }}</span>
</div>
</div>

<div class="card">
<div class="card-title">Component Network Allocation</div>
<div class="metadata-item">
<span class="label">Workload Port (Http Envoy)</span>
<span class="label">Workload HTTP Port</span>
<span class="value">{{ .HttpPort }}</span>
</div>
<div class="metadata-item">
Expand All @@ -278,20 +282,20 @@ <h1>atenet Router Status</h1>
<div class="card-title">System Component Health Checks</div>

<div class="metadata-item">
<span class="label">Envoy Health</span>
{{ if .Health.Envoy.Healthy }}
<span class="label">Proxy Health ({{ .Health.Provider }})</span>
{{ if .Health.Proxy.Healthy }}
<span class="value badge" style="background: rgba(16, 185, 129, 0.1); color: #10b981;">Healthy</span>
{{ else }}
<span class="value badge" style="background: rgba(239, 68, 68, 0.1); color: #ef4444;">Degraded</span>
{{ end }}
</div>
<div class="metadata-item" style="margin-top: -0.25rem; margin-bottom: 0.5rem; font-size: 0.8rem;">
<span class="label">Message / Err:</span>
<span class="value" style="color: var(--text-secondary);">{{ .Health.Envoy.Message }}</span>
<span class="value" style="color: var(--text-secondary);">{{ .Health.Proxy.Message }}</span>
</div>
<div class="metadata-item" style="font-size: 0.75rem; color: var(--text-secondary);">
<span>Ok: {{ .Health.Envoy.SuccessCount }} | Err: {{ .Health.Envoy.FailureCount }}</span>
<span>LKG: {{ if not .Health.Envoy.LastSuccess.IsZero }}{{ .Health.Envoy.LastSuccess.Format "15:04:05" }}{{ else }}N/A{{ end }}</span>
<span>Ok: {{ .Health.Proxy.SuccessCount }} | Err: {{ .Health.Proxy.FailureCount }}</span>
<span>LKG: {{ if not .Health.Proxy.LastSuccess.IsZero }}{{ .Health.Proxy.LastSuccess.Format "15:04:05" }}{{ else }}N/A{{ end }}</span>
</div>

<div style="border-bottom: 1px solid var(--card-border); margin: 0.5rem 0;"></div>
Expand Down
118 changes: 118 additions & 0 deletions cmd/atenet/internal/app/router/envoy.go
Original file line number Diff line number Diff line change
@@ -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")
}
Loading
Loading