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
9 changes: 9 additions & 0 deletions charts/substrate-atelet/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,15 @@ This chart reads the same value paths as the substrate chart for `atelet`,
`image`, `imagePullSecrets` and `global`, so the substrate release's values
file can configure both releases.

These values are only in this chart:

| Key | Default | Description |
|---|---|---|
| `atelet.extraVolumes` | `[]` | Volumes added to the atelet pod, such as a host file a credential provider reads |
| `atelet.extraVolumeMounts` | `[]` | Volume mounts added to the atelet container |
| `podMonitors.enabled` | `false` | Render a PodMonitor that scrapes atelet's `/metrics` |
| `podMonitors.apiVersion` | `monitoring.coreos.com/v1` | PodMonitor API version; `azmonitoring.coreos.com/v1` for Azure Managed Prometheus |

Object storage is set under `objectStore` instead of `rustfs` and
`atelet.storageBackend`:

Expand Down
21 changes: 21 additions & 0 deletions charts/substrate-atelet/templates/atelet.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,9 @@ spec:
subPath: config.yaml
{{- end }}
readOnly: true
{{- end }}
{{- with .Values.atelet.extraVolumeMounts }}
{{- toYaml . | nindent 8 }}
{{- end }}
volumes:
- name: objectstore-plugin
Expand Down Expand Up @@ -368,3 +371,21 @@ spec:
matchLabels:
podcert.ate.dev/canarying: live
path: trust-bundle.pem
{{- with .Values.atelet.extraVolumes }}
{{- toYaml . | nindent 6 }}
{{- end }}
{{- if .Values.podMonitors.enabled }}
---
apiVersion: {{ .Values.podMonitors.apiVersion }}
kind: PodMonitor
metadata:
name: {{ include "substrate.fullname" (list "atelet" .) }}
namespace: {{ .Release.Namespace }}
spec:
selector:
matchLabels:
app: atelet
podMetricsEndpoints:
- port: prometheus
path: /metrics
{{- end }}
11 changes: 11 additions & 0 deletions charts/substrate-atelet/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,10 @@ atelet:
nodeSelector: {}
extraArgs: []
extraEnv: []
# Added to the atelet pod's volumes and its container's volumeMounts, such
# as a host file a credential provider reads.
extraVolumes: []
extraVolumeMounts: []
resources:
requests:
cpu: 50m
Expand Down Expand Up @@ -94,6 +98,13 @@ otel:
enabled: true
endpoint: ""

# A PodMonitor that scrapes atelet's /metrics endpoint. It needs the
# Prometheus Operator CRDs, or apiVersion azmonitoring.coreos.com/v1 for Azure
# Managed Prometheus.
podMonitors:
enabled: false
apiVersion: monitoring.coreos.com/v1

# Values under `global` are visible to this chart and to every subchart.
global:
# -- Mirror registry that overrides where every image is pulled from. It wins
Expand Down
3 changes: 3 additions & 0 deletions charts/substrate/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ See `values.yaml` for the full set; the important keys:
| `atenetEgress.extraCredentialProviders` | `[]` | Credential providers you deploy, added after the bundled `k8s.io` provider, each `{name, address}`. `name` is the host of the `ate-secret://` URIs it serves; `address` is the `host:port` the egress gateway dials over mTLS. Its serving certificate must chain to the servicedns CA. See [egress credential injection](../../docs/guides/egress-credential-injection.md#the-plugin-api) |
| `atenetEgress.agentgateway.logLevel` | `info` | Egress proxy `RUST_LOG` filter, e.g. `debug` or `info,agentgateway=debug` when troubleshooting |
| `rustfs.enabled` | `true` | Deploy an in-cluster S3-compatible RustFS bucket for snapshots |
| `rustfs.storageClassName` | `""` | StorageClass of the RustFS data volume; empty uses the cluster's default |
| `ateApi.snapshotLocation` | `""` | Cluster-wide snapshot URI; defaults to `gs://<rustfs.bucket>/` with bundled RustFS |
| `atelet.enabled` | `true` | Deprecated. Set to `false` and install atelet with the [substrate-atelet](../substrate-atelet/README.md) chart |
| `atelet.storageBackend` | `s3` | Default snapshot backend, wired to RustFS when `rustfs.enabled=true` |
Expand All @@ -112,6 +113,8 @@ See `values.yaml` for the full set; the important keys:
| `otel.metrics.endpoint` | `""` | OTLP endpoint for metrics, overriding `otel.endpoint` |
| `otel.logs.enabled` | `true` | Enable OTLP actor events from ateapi and the ateworkers, plus the router access log. Actor events go to stdout when OTLP logs are disabled |
| `otel.logs.endpoint` | `""` | OTLP endpoint for logs, overriding `otel.endpoint` |
| `podMonitors.enabled` | `false` | Render PodMonitors that scrape `/metrics` on ate-api-server, ate-controller, atenet-router and atenet-egress; ate-controller's only while `otel.metrics.enabled`, since it serves no `/metrics` otherwise |
| `podMonitors.apiVersion` | `monitoring.coreos.com/v1` | PodMonitor API version; `azmonitoring.coreos.com/v1` for Azure Managed Prometheus |

## Router timeouts

Expand Down
38 changes: 38 additions & 0 deletions charts/substrate/templates/podmonitors.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
{{/*
Copyright 2026 The Agent Substrate Authors

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.
*/}}

{{- if .Values.podMonitors.enabled }}
{{- $targets := list (list "ate-api-server" "prometheus") (list "atenet-router" "gw-metrics") (list "atenet-egress" "stats") }}
{{- /* ate-controller closes its metrics port when OTEL_METRICS_EXPORTER is none. */}}
{{- if .Values.otel.metrics.enabled }}
{{- $targets = append $targets (list "ate-controller" "metrics") }}
{{- end }}
{{- range $targets }}
---
apiVersion: {{ $.Values.podMonitors.apiVersion }}
kind: PodMonitor
metadata:
name: {{ include "substrate.fullname" (list (index . 0) $) }}
namespace: {{ $.Release.Namespace }}
spec:
selector:
matchLabels:
app: {{ index . 0 }}
podMetricsEndpoints:
- port: {{ index . 1 }}
path: /metrics
{{- end }}
{{- end }}
3 changes: 3 additions & 0 deletions charts/substrate/templates/rustfs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ metadata:
spec:
accessModes:
- ReadWriteOnce
{{- with .Values.rustfs.storageClassName }}
storageClassName: {{ . | quote }}
{{- end }}
resources:
requests:
storage: {{ .Values.rustfs.storageSize }}
Expand Down
19 changes: 19 additions & 0 deletions charts/substrate/tests/control_plane_test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,25 @@ tests:
value:
- protocol: TCP
port: api
- it: leaves the RustFS volume on the default StorageClass
template: rustfs.yaml
documentSelector:
path: kind
value: PersistentVolumeClaim
asserts:
- notExists:
path: spec.storageClassName
- it: puts the RustFS volume on rustfs.storageClassName
template: rustfs.yaml
documentSelector:
path: kind
value: PersistentVolumeClaim
set:
rustfs.storageClassName: managed-csi-premium
asserts:
- equal:
path: spec.storageClassName
value: managed-csi-premium

- it: supplies the required API config and bootstraps the release client
template: ate-api-server.yaml
Expand Down
83 changes: 83 additions & 0 deletions charts/substrate/tests/podmonitors_test.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
# Copyright 2026 The Agent Substrate Authors
#
# 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.

suite: PodMonitors
templates:
- podmonitors.yaml
release:
name: custom
namespace: custom-system
tests:
- it: renders no PodMonitors by default
template: podmonitors.yaml
asserts:
- hasDocuments:
count: 0
- it: scrapes each control plane component's metrics port
template: podmonitors.yaml
set:
podMonitors:
enabled: true
apiVersion: azmonitoring.coreos.com/v1
asserts:
- hasDocuments:
count: 4
- isAPIVersion:
of: azmonitoring.coreos.com/v1
- isKind:
of: PodMonitor
- equal:
path: metadata.namespace
value: custom-system
- equal:
path: metadata.name
value: custom-ate-api-server
documentIndex: 0
- equal:
path: spec
value:
selector:
matchLabels:
app: ate-api-server
podMetricsEndpoints:
- port: prometheus
path: /metrics
documentIndex: 0
- equal:
path: spec.podMetricsEndpoints[0].port
value: gw-metrics
documentIndex: 1
- equal:
path: spec.podMetricsEndpoints[0].port
value: stats
documentIndex: 2
- equal:
path: spec.selector.matchLabels.app
value: ate-controller
documentIndex: 3
- equal:
path: spec.podMetricsEndpoints[0].port
value: metrics
documentIndex: 3
- it: skips ate-controller when it serves no metrics
template: podmonitors.yaml
set:
podMonitors.enabled: true
otel.metrics.enabled: false
asserts:
- hasDocuments:
count: 3
- notEqual:
path: spec.selector.matchLabels.app
value: ate-controller
10 changes: 10 additions & 0 deletions charts/substrate/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ postgres:
rustfs:
enabled: true
storageSize: 1Gi
# StorageClass of the data volume. Empty uses the cluster's default.
storageClassName: ""
bucket: ate-snapshots
accessKey: rustfsadmin
secretKey: rustfsadmin
Expand Down Expand Up @@ -229,6 +231,14 @@ otel:
enabled: true
endpoint: ""

# PodMonitors that scrape the control plane's /metrics endpoints. The
# substrate-atelet chart has its own for atelet. They need the Prometheus
# Operator CRDs, or apiVersion azmonitoring.coreos.com/v1 for Azure Managed
# Prometheus.
podMonitors:
enabled: false
apiVersion: monitoring.coreos.com/v1

# Values under `global` are visible to this chart and to every subchart. A parent
# chart or an operator sets one value here instead of one value per chart.
global:
Expand Down
58 changes: 58 additions & 0 deletions cmd/atelet/helm_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,9 @@ import (
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
utilyaml "k8s.io/apimachinery/pkg/util/yaml"
"k8s.io/utils/ptr"
"sigs.k8s.io/yaml"
)

Expand Down Expand Up @@ -145,6 +147,60 @@ providers: []`
}
}

func TestHelmAteletExtraVolumes(t *testing.T) {
if _, err := exec.LookPath("helm"); err != nil {
t.Skip("helm is not installed")
}
objs := helmTemplate(t, "atelet", "../../charts/substrate-atelet",
"--set-json", `atelet.extraVolumes=[{"name": "extra", "hostPath": {"path": "/etc/extra.json", "type": "File"}}]`,
"--set-json", `atelet.extraVolumeMounts=[{"name": "extra", "mountPath": "/etc/extra.json", "readOnly": true}]`)
for _, obj := range objs {
if obj.GetKind() != "DaemonSet" {
continue
}
var ds appsv1.DaemonSet
if err := runtime.DefaultUnstructuredConverter.FromUnstructured(obj.Object, &ds); err != nil {
t.Fatal(err)
}
pod := ds.Spec.Template.Spec
wantVolume := corev1.Volume{Name: "extra", VolumeSource: corev1.VolumeSource{HostPath: &corev1.HostPathVolumeSource{Path: "/etc/extra.json", Type: ptr.To(corev1.HostPathFile)}}}
if !slices.ContainsFunc(pod.Volumes, func(v corev1.Volume) bool { return reflect.DeepEqual(v, wantVolume) }) {
t.Errorf("atelet volumes %v don't include %v", pod.Volumes, wantVolume)
}
wantMount := corev1.VolumeMount{Name: "extra", MountPath: "/etc/extra.json", ReadOnly: true}
if !slices.ContainsFunc(pod.Containers[0].VolumeMounts, func(m corev1.VolumeMount) bool { return reflect.DeepEqual(m, wantMount) }) {
t.Errorf("atelet volume mounts %v don't include %v", pod.Containers[0].VolumeMounts, wantMount)
}
return
}
t.Fatal("no atelet DaemonSet rendered")
}

func TestHelmAteletPodMonitor(t *testing.T) {
if _, err := exec.LookPath("helm"); err != nil {
t.Skip("helm is not installed")
}
objs := helmTemplate(t, "atelet", "../../charts/substrate-atelet",
"--set", "podMonitors.enabled=true", "--set", "podMonitors.apiVersion=azmonitoring.coreos.com/v1")
for _, obj := range objs {
if obj.GetKind() != "PodMonitor" {
continue
}
if got := obj.GetAPIVersion(); got != "azmonitoring.coreos.com/v1" {
t.Errorf("PodMonitor apiVersion = %q, want azmonitoring.coreos.com/v1", got)
}
want := map[string]any{
"selector": map[string]any{"matchLabels": map[string]any{"app": "atelet"}},
"podMetricsEndpoints": []any{map[string]any{"port": "prometheus", "path": "/metrics"}},
}
if diff := cmp.Diff(want, obj.Object["spec"]); diff != "" {
t.Errorf("PodMonitor spec mismatch (-want +got):\n%s", diff)
}
return
}
t.Fatal("no PodMonitor rendered")
}

func TestHelmSandboxAssetStorage(t *testing.T) {
for _, tc := range []struct {
name, backend string
Expand Down Expand Up @@ -285,6 +341,8 @@ func TestHelmAteletChartMatchesSubstrateChart(t *testing.T) {
"--set", "preview[0]=ALL",
"--set", "otel.endpoint=http://collector:4317",
"--set", "atelet.nodeSelector.pool=agents",
"--set", "atelet.imageCredentialProviderConfig=/var/lib/kubelet/credential-provider-config.yaml",
"--set", "atelet.imageCredentialProviderBinDir=/var/lib/kubelet/credential-provider",
"--set", "image.tag=test",
},
substrate: append([]string{"--set-json", `objectStorePlugin.extraEnv=[
Expand Down
Loading