From 626ce68f1575aa3d0fa9c32de628200c0fd11d9c Mon Sep 17 00:00:00 2001 From: Eric Curtin Date: Tue, 13 Oct 2026 13:40:47 +0000 Subject: [PATCH 01/42] atecontroller: the :8080 metrics listener follows OTEL_METRICS_EXPORTER (#1976) The `:8080` listener follows `OTEL_METRICS_EXPORTER`, per the policy in #2067. Unset keeps push and `:8080`, `otlp` closes `:8080`, `prometheus` serves only, `none` does neither. Fixes #933 - [x] Tests pass - [x] Appropriate changes to documentation are included in the PR --- cmd/atecontroller/main.go | 62 ++++++++++---- cmd/atecontroller/main_test.go | 62 ++++++++++++++ docs/dev/best-practices/metrics.md | 6 +- docs/observability.md | 6 +- internal/serverboot/serverboot.go | 58 +++++++++---- internal/serverboot/serverboot_test.go | 112 +++++++++++++++++++------ 6 files changed, 242 insertions(+), 64 deletions(-) diff --git a/cmd/atecontroller/main.go b/cmd/atecontroller/main.go index f9a4bbd298..87ce83a43a 100644 --- a/cmd/atecontroller/main.go +++ b/cmd/atecontroller/main.go @@ -36,6 +36,7 @@ import ( corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/fields" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" utilruntime "k8s.io/apimachinery/pkg/util/runtime" "k8s.io/client-go/kubernetes" clientgoscheme "k8s.io/client-go/kubernetes/scheme" @@ -44,6 +45,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/healthz" ctrlmetrics "sigs.k8s.io/controller-runtime/pkg/metrics" + metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" // Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.) _ "k8s.io/client-go/plugin/pkg/client/auth" @@ -91,7 +93,14 @@ func init() { utilruntime.Must(clientv1alpha1.AddToScheme(scheme)) // Register our CRD } -const serviceName = "atecontroller" +const ( + serviceName = "atecontroller" + + // metricsPullAddr is the manager's metrics listener, and metricsOffAddr + // turns it off. + metricsPullAddr = ":8080" + metricsOffAddr = "0" +) // logr verbosity V(n) maps to slog level -n, so V(1) stays below Info until // --log-level=debug. logr carries no context, so these records have no trace IDs. @@ -99,6 +108,34 @@ func newControllerRuntimeLogger(h slog.Handler) logr.Logger { return logr.FromSlogHandler(h) } +// metricsBindAddr is the manager's metrics address. servePull is whether +// OTEL_METRICS_EXPORTER leaves the listener on. +func metricsBindAddr(servePull bool) string { + if servePull { + return metricsPullAddr + } + return metricsOffAddr +} + +// managerOptions configures the controller-runtime manager. +func managerOptions(egressMITMCAPool types.NamespacedName, servePull bool) ctrl.Options { + return ctrl.Options{ + Scheme: scheme, + Metrics: metricsserver.Options{BindAddress: metricsBindAddr(servePull)}, + Cache: cache.Options{ + ByObject: map[client.Object]cache.ByObject{ + &corev1.Secret{}: { + Namespaces: map[string]cache.Config{ + egressMITMCAPool.Namespace: { + FieldSelector: fields.OneTermEqualSelector("metadata.name", egressMITMCAPool.Name), + }, + }, + }, + }, + }, + } +} + func main() { pflag.Parse() if *showVersion { @@ -131,13 +168,15 @@ func main() { defer serverboot.ShutdownProvider("TracerProvider", tp.Shutdown) // controller-runtime records reconcile, workqueue, and runtime metrics into its - // own Prometheus registry, which the manager serves. On the OTLP path the - // bridged queue histograms are padded so the Telemetry API accepts idle ones. - mp, err := serverboot.InitMetricsBridged(ctx, serviceName, ctrlmetrics.Registry, padEmptyExponentialHistograms) + // own Prometheus registry. OTEL_METRICS_EXPORTER picks where they go: the + // OTLP push, which pads the bridged queue histograms so the Telemetry API + // accepts idle ones, and the manager's scrape listener. + mp, servePull, err := serverboot.InitMetricsBridged(ctx, serviceName, ctrlmetrics.Registry, padEmptyExponentialHistograms) if err != nil { serverboot.Fatal(ctx, "Failed to initialize metrics", err) } defer serverboot.ShutdownProvider("MeterProvider", mp.Shutdown) + slog.InfoContext(ctx, "Metrics listener", slog.Bool("enabled", servePull), slog.String("addr", metricsBindAddr(servePull))) k8sConfig := ctrl.GetConfigOrDie() k8sClient, err := kubernetes.NewForConfig(k8sConfig) @@ -184,20 +223,7 @@ func main() { // EgressMITMTrustReconciler watches the Secret `egress-mitm-ca-pool`. systemNamespace := installdefaults.NamespaceFromPodEnv() egressMITMCAPool := controllers.EgressMITMCAPoolRef(systemNamespace) - mgr, err := ctrl.NewManager(k8sConfig, ctrl.Options{ - Scheme: scheme, - Cache: cache.Options{ - ByObject: map[client.Object]cache.ByObject{ - &corev1.Secret{}: { - Namespaces: map[string]cache.Config{ - egressMITMCAPool.Namespace: { - FieldSelector: fields.OneTermEqualSelector("metadata.name", egressMITMCAPool.Name), - }, - }, - }, - }, - }, - }) + mgr, err := ctrl.NewManager(k8sConfig, managerOptions(egressMITMCAPool, servePull)) if err != nil { setupLog.Error(err, "unable to start manager") os.Exit(1) diff --git a/cmd/atecontroller/main_test.go b/cmd/atecontroller/main_test.go index 7d95bff394..0068d361b5 100644 --- a/cmd/atecontroller/main_test.go +++ b/cmd/atecontroller/main_test.go @@ -21,7 +21,11 @@ import ( "testing" prombridge "go.opentelemetry.io/contrib/bridges/prometheus" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/cache" ctrlmetrics "sigs.k8s.io/controller-runtime/pkg/metrics" + metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" ) func TestNewControllerRuntimeLoggerVerbosity(t *testing.T) { @@ -78,3 +82,61 @@ func TestBridgedRegistryProducesBeforeManagerStart(t *testing.T) { t.Errorf("no go_* family in %v", names) } } + +// The manager opens its metrics listener only when OTEL_METRICS_EXPORTER leaves +// the scrape endpoint on. +func TestManagerOptionsMetricsListener(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + servePull bool + wantAddr string + wantServed bool + }{ + {"serving", true, metricsPullAddr, true}, + {"disabled", false, metricsOffAddr, false}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + opts := managerOptions(types.NamespacedName{Namespace: "ate-system", Name: "pool"}, tt.servePull) + + if opts.Metrics.BindAddress != tt.wantAddr { + t.Errorf("metrics address = %q, want %q", opts.Metrics.BindAddress, tt.wantAddr) + } + srv, err := metricsserver.NewServer(opts.Metrics, nil, nil) + if err != nil { + t.Fatalf("NewServer: %v", err) + } + if served := srv != nil; served != tt.wantServed { + t.Errorf("serves a metrics listener = %t, want %t", served, tt.wantServed) + } + }) + } +} + +func TestManagerOptionsScopeSecretCache(t *testing.T) { + t.Parallel() + + ref := types.NamespacedName{Namespace: "ate-system", Name: "pool"} + opts := managerOptions(ref, true) + + var byObject cache.ByObject + found := false + for obj, by := range opts.Cache.ByObject { + if _, isSecret := obj.(*corev1.Secret); isSecret { + byObject, found = by, true + } + } + if !found { + t.Fatal("no cache scope for Secrets") + } + cfg, ok := byObject.Namespaces[ref.Namespace] + if !ok || len(byObject.Namespaces) != 1 { + t.Fatalf("Secret cache namespaces = %v, want only %q", byObject.Namespaces, ref.Namespace) + } + if got := cfg.FieldSelector.String(); got != "metadata.name="+ref.Name { + t.Errorf("Secret field selector = %q, want %q", got, "metadata.name="+ref.Name) + } +} diff --git a/docs/dev/best-practices/metrics.md b/docs/dev/best-practices/metrics.md index d39232f572..9990b2a7e0 100644 --- a/docs/dev/best-practices/metrics.md +++ b/docs/dev/best-practices/metrics.md @@ -278,9 +278,9 @@ component name as the scope (`"atelet"`, `"ateapi"`, `"atecontroller"`): The meter provider itself is set up once per binary by `serverboot.InitMetrics` (Prometheus reader plus OTLP push), `serverboot.InitMetricsBridged` (OTLP push with a foreign Prometheus registry -bridged onto it, or that registry alone under `OTEL_METRICS_EXPORTER=none`; -atecontroller), or `serverboot.InitMetricsPushOnlyVia` (OTLP push only, over -the atelet relay when given one; ateom). +bridged onto it, or that registry alone under `OTEL_METRICS_EXPORTER=prometheus`, +and whether to serve it; atecontroller), or `serverboot.InitMetricsPushOnlyVia` +(OTLP push only, over the atelet relay when given one; ateom). A new component calls one of these and defers `ShutdownProvider`; a new package inside an existing component adds nothing there. diff --git a/docs/observability.md b/docs/observability.md index 042b8007ba..5cee8a8350 100644 --- a/docs/observability.md +++ b/docs/observability.md @@ -344,7 +344,7 @@ Weaver permits only `groups` and `imports` at the top level of a registry file, ### Bridged controller-runtime metrics (atecontroller) -atecontroller bridges controller-runtime's private Prometheus registry, which the manager serves on an unscraped `:8080`, onto its OTLP reader. So `controller_runtime_*`, `workqueue_*`, `certwatcher_*`, `rest_client_*`, `leader_election_*`, `go_*`, and `process_*` reach the collector too, keeping their Prometheus names because they are upstream instruments and renaming them would break existing controller-runtime dashboards. +atecontroller bridges controller-runtime's private Prometheus registry, which the manager serves on `:8080` only while `OTEL_METRICS_EXPORTER` leaves it on (see [Scraping instead of pushing](#scraping-instead-of-pushing)), onto its OTLP reader. So `controller_runtime_*`, `workqueue_*`, `certwatcher_*`, `rest_client_*`, `leader_election_*`, `go_*`, and `process_*` reach the collector too, keeping their Prometheus names because they are upstream instruments and renaming them would break existing controller-runtime dashboards. These can be used to answer whether the controller is keeping up, e.g. rising `workqueue_depth` or `workqueue_queue_duration_seconds` means reconciles are falling behind, and `controller_runtime_reconcile_errors_total` says which controller. @@ -356,7 +356,9 @@ A queue that has never processed an item bridges as an exponential histogram wit ### Scraping instead of pushing -ateapi, atelet, atenet-router and the credential provider also serve every instrument on their Prometheus `/metrics` endpoint. A cluster that scrapes those endpoints sets `OTEL_METRICS_EXPORTER=none` on the components, so each series reaches the backend once. With `none` the components install no OTLP metric reader and keep the Prometheus one; traces and logs are unaffected. atecontroller then registers its instruments (`ate.workerpool.*`) on controller-runtime's registry, so the manager's `:8080` serves them next to the controller-runtime families. ateom serves no endpoint of its own, so leave the variable unset on the worker pods, or it exports no metrics at all. The variable takes a comma-separated list, with the same rules as `OTEL_LOGS_EXPORTER`. `otlp` (the default) keeps the push, and `prometheus` or `none` stops it. `/metrics` stays on for each value. ateom serves no `/metrics`, so there `prometheus` is skipped as unknown with a warning, and the push stays on unless the variable is `none`. +ateapi, atelet, atenet-router and the credential provider also serve every instrument on their Prometheus `/metrics` endpoint. A cluster that scrapes those endpoints sets `OTEL_METRICS_EXPORTER=prometheus` on the components, so each series reaches the backend once. With `prometheus` the components install no OTLP metric reader and keep the Prometheus one; traces and logs are unaffected. `none` stops the push too. For now `/metrics` stays on for every value, including `otlp` and `none`. This is temporary until the probes move off `:9090`; `otlp` and `none` will then turn it off ([#2067](https://github.com/agent-substrate/substrate/issues/2067)). The variable takes a comma-separated list, with the same rules as `OTEL_LOGS_EXPORTER`. Unset is `otlp,prometheus`, or `otlp` alone on ateom. `otlp` keeps the push, and `prometheus` or `none` stops it. ateom serves no `/metrics`, so there `prometheus` is skipped as unknown with a warning, and the push stays on unless the variable is `none`. + +atecontroller's `:8080` listener follows the variable. When the variable is unset or `otlp,prometheus`, atecontroller pushes every metric over OTLP, and `:8080` serves only the controller-runtime families ([#2402](https://github.com/agent-substrate/substrate/issues/2402)). With `otlp`, atecontroller pushes every metric and closes `:8080`. With `prometheus`, atecontroller does not push, and `:8080` serves the controller-runtime families and the `ate.workerpool.*` instruments. With `none`, atecontroller does not push and closes `:8080`. ### Local Metrics with Prometheus (Kind Cluster) diff --git a/internal/serverboot/serverboot.go b/internal/serverboot/serverboot.go index f673b6ca7c..43eb34accf 100644 --- a/internal/serverboot/serverboot.go +++ b/internal/serverboot/serverboot.go @@ -208,17 +208,27 @@ const ( tracesExporterEnv = "OTEL_TRACES_EXPORTER" ) -// metricsPushEnabled applies OTEL_METRICS_EXPORTER with the shared exporter -// rules: the OTLP reader is installed while otlp is selected, as it is when the -// variable is unset. prometheus, the specification's pull exporter, is known -// only when pullServed, i.e. the component serves its own /metrics; elsewhere -// it is skipped as unknown, so a push-only component keeps exporting. -func metricsPushEnabled(ctx context.Context, pullServed bool) bool { +// metricsExporters applies OTEL_METRICS_EXPORTER with the shared exporter +// rules. prometheus, the specification's pull exporter, is known only when +// pullServed, i.e. the component serves its own /metrics; elsewhere it is +// skipped as unknown, so a push-only component keeps exporting. Unset selects +// every exporter the component has. +func metricsExporters(ctx context.Context, pullServed bool) Exporters { known := []string{ExporterOTLP} + def := Exporters{ExporterOTLP: true} if pullServed { known = append(known, exporterPrometheus) + def[exporterPrometheus] = true } - return resolveExportersEnv(ctx, metricsExporterEnv, known, Exporters{ExporterOTLP: true}).Has(ExporterOTLP) + exporters := resolveExportersEnv(ctx, metricsExporterEnv, known, def) + slog.InfoContext(ctx, "Metrics exporters resolved", slog.String("exporters", exporters.String())) + return exporters +} + +// metricsPushEnabled reports whether the OTLP reader is installed: while otlp +// is selected, as it is when the variable is unset. +func metricsPushEnabled(ctx context.Context, pullServed bool) bool { + return metricsExporters(ctx, pullServed).Has(ExporterOTLP) } // tracesPushEnabled applies OTEL_TRACES_EXPORTER with the shared exporter @@ -247,31 +257,45 @@ func InitMetrics(ctx context.Context, serviceName string) (*sdkmetric.MeterProvi // it does not own (atecontroller: controller-runtime's), so every metric // reaches the backend exactly once on whichever path is active. With the OTLP // export, reg is bridged onto the push and the OTel instruments stay off it, -// since the bridge would otherwise push them a second time. When -// OTEL_METRICS_EXPORTER does not select otlp, nothing is pushed and the OTel -// instruments register on reg, so its endpoint serves both. +// since the bridge would otherwise push them a second time. With prometheus +// alone, nothing is pushed and the OTel instruments register on reg, so its +// endpoint serves both. +// +// servePull reports whether OTEL_METRICS_EXPORTER leaves the scraped endpoint +// on, which the caller must then serve from reg: unset, prometheus, or a list +// with it. otlp and none turn it off, and with none no reader is installed. // // wrapProducer, when non-nil, wraps the bridge producer on the OTLP path, for // a rewrite the push backend needs but the scraped endpoint does not. func InitMetricsBridged(ctx context.Context, serviceName string, reg interface { promclient.Registerer promclient.Gatherer -}, wrapProducer func(sdkmetric.Producer) sdkmetric.Producer) (*sdkmetric.MeterProvider, error) { +}, wrapProducer func(sdkmetric.Producer) sdkmetric.Producer) (mp *sdkmetric.MeterProvider, servePull bool, err error) { if serviceName == "" { - return nil, fmt.Errorf("serviceName is required") + return nil, false, fmt.Errorf("serviceName is required") } - if metricsPushEnabled(ctx, true) { + exporters := metricsExporters(ctx, true) + servePull = exporters.Has(exporterPrometheus) + switch { + case exporters.Has(ExporterOTLP): producer := prombridge.NewMetricProducer(prombridge.WithGatherer(reg)) if wrapProducer != nil { producer = wrapProducer(producer) } - return newMeterProvider(ctx, serviceName, true, false, nil, []sdkmetric.Producer{producer}) + mp, err = newMeterProvider(ctx, serviceName, true, false, nil, []sdkmetric.Producer{producer}) + case servePull: + var promReader *prometheus.Exporter + if promReader, err = prometheus.New(prometheus.WithRegisterer(reg)); err != nil { + return nil, false, fmt.Errorf("create Prometheus metric exporter: %w", err) + } + mp, err = newMeterProvider(ctx, serviceName, false, false, nil, nil, promReader) + default: + mp, err = newMeterProvider(ctx, serviceName, false, false, nil, nil) } - promReader, err := prometheus.New(prometheus.WithRegisterer(reg)) if err != nil { - return nil, fmt.Errorf("create Prometheus metric exporter: %w", err) + return nil, false, err } - return newMeterProvider(ctx, serviceName, false, false, nil, nil, promReader) + return mp, servePull, nil } // InitMetricsPushOnlyVia is InitMetrics without the Prometheus reader, for a diff --git a/internal/serverboot/serverboot_test.go b/internal/serverboot/serverboot_test.go index b685a434b0..a7b41f2526 100644 --- a/internal/serverboot/serverboot_test.go +++ b/internal/serverboot/serverboot_test.go @@ -439,23 +439,32 @@ func bridgedRegistry(t *testing.T) *prometheus.Registry { return reg } -// With the OTLP export, the bridged family and the OTel instrument are each -// pushed once. The instrument must stay off the bridged registry: there the -// bridge would push it a second time, under its Prometheus name. +// With the OTLP export, whether selected or the default, the bridged family and +// the OTel instrument are each pushed once. The instrument must stay off the +// bridged registry: there the bridge would push it a second time, under its +// Prometheus name. func TestInitMetricsBridgedPushesEachMetricOnce(t *testing.T) { - collector := startMetricsCollector(t, "otlp") - reg := bridgedRegistry(t) - mp, err := InitMetricsBridged(t.Context(), "test-bridged-otlp", reg, nil) - if err != nil { - t.Fatalf("InitMetricsBridged: %v", err) - } - addOne(t, mp, "ate.test.bridged.count") - if gatheredNames(t, reg)["ate_test_bridged_count_total"] { - t.Error("the OTel instrument is registered on the bridged registry") - } - pushed := collector.flush(t, mp) - if pushed["ate.test.bridged.count"] != 1 || pushed["test_bridged_family"] != 1 || pushed["ate_test_bridged_count_total"] != 0 { - t.Errorf("want ate.test.bridged.count and test_bridged_family pushed once each and nothing else for them, got %v", pushed) + for _, tt := range []struct{ name, value string }{ + {"unset", ""}, + {"otlp", "otlp"}, + {"list", "otlp,prometheus"}, + } { + t.Run(tt.name, func(t *testing.T) { + collector := startMetricsCollector(t, tt.value) + reg := bridgedRegistry(t) + mp, _, err := InitMetricsBridged(t.Context(), "test-bridged-otlp", reg, nil) + if err != nil { + t.Fatalf("InitMetricsBridged: %v", err) + } + addOne(t, mp, "ate.test.bridged.count") + if gatheredNames(t, reg)["ate_test_bridged_count_total"] { + t.Error("the OTel instrument is registered on the bridged registry") + } + pushed := collector.flush(t, mp) + if pushed["ate.test.bridged.count"] != 1 || pushed["test_bridged_family"] != 1 || pushed["ate_test_bridged_count_total"] != 0 { + t.Errorf("want ate.test.bridged.count and test_bridged_family pushed once each and nothing else for them, got %v", pushed) + } + }) } } @@ -469,7 +478,7 @@ func TestInitMetricsBridgedWrapsPushedProducer(t *testing.T) { return inner.Produce(ctx) }) } - mp, err := InitMetricsBridged(t.Context(), "test-bridged-wrap", bridgedRegistry(t), wrap) + mp, _, err := InitMetricsBridged(t.Context(), "test-bridged-wrap", bridgedRegistry(t), wrap) if err != nil { t.Fatalf("InitMetricsBridged: %v", err) } @@ -486,27 +495,82 @@ type producerFunc func(context.Context) ([]metricdata.ScopeMetrics, error) func (f producerFunc) Produce(ctx context.Context) ([]metricdata.ScopeMetrics, error) { return f(ctx) } -// With OTEL_METRICS_EXPORTER=none, the OTel instrument is served from the +// With OTEL_METRICS_EXPORTER=prometheus, the OTel instrument is served from the // bridged registry next to its own families, and nothing is pushed. -func TestInitMetricsBridgedExporterNoneServesFromRegistry(t *testing.T) { - collector := startMetricsCollector(t, "none") +func TestInitMetricsBridgedPrometheusServesFromRegistry(t *testing.T) { + collector := startMetricsCollector(t, "prometheus") reg := bridgedRegistry(t) - mp, err := InitMetricsBridged(t.Context(), "test-bridged-none", reg, nil) + mp, servePull, err := InitMetricsBridged(t.Context(), "test-bridged-prometheus", reg, nil) if err != nil { t.Fatalf("InitMetricsBridged: %v", err) } - addOne(t, mp, "ate.test.bridgednone.count") + if !servePull { + t.Error("OTEL_METRICS_EXPORTER=prometheus turned the scrape endpoint off") + } + addOne(t, mp, "ate.test.bridgedprom.count") names := gatheredNames(t, reg) - if !names["ate_test_bridgednone_count_total"] || !names["test_bridged_family"] { + if !names["ate_test_bridgedprom_count_total"] || !names["test_bridged_family"] { t.Errorf("the bridged registry does not serve both the instrument and its own family: %v", names) } + if pushed := collector.flush(t, mp); pushed != nil { + t.Errorf("OTEL_METRICS_EXPORTER=prometheus still pushed %v", pushed) + } +} + +// With OTEL_METRICS_EXPORTER=none there is no push and no endpoint to serve, +// so the instrument is not registered anywhere. +func TestInitMetricsBridgedExporterNoneExportsNothing(t *testing.T) { + collector := startMetricsCollector(t, "none") + reg := bridgedRegistry(t) + mp, servePull, err := InitMetricsBridged(t.Context(), "test-bridged-none", reg, nil) + if err != nil { + t.Fatalf("InitMetricsBridged: %v", err) + } + if servePull { + t.Error("OTEL_METRICS_EXPORTER=none left the scrape endpoint on") + } + addOne(t, mp, "ate.test.bridgednone.count") + if gatheredNames(t, reg)["ate_test_bridgednone_count_total"] { + t.Error("the instrument is registered on the bridged registry") + } if pushed := collector.flush(t, mp); pushed != nil { t.Errorf("OTEL_METRICS_EXPORTER=none still pushed %v", pushed) } } +// servePull follows the policy table: unset and the list keep both paths. +func TestInitMetricsBridgedServePull(t *testing.T) { + tests := []struct { + name string + value string + wantPush bool + wantPull bool + }{ + {"unset", "", true, true}, + {"otlp", "otlp", true, false}, + {"prometheus", "prometheus", false, true}, + {"list", "otlp,prometheus", true, true}, + {"none", "none", false, false}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + collector := startMetricsCollector(t, tt.value) + mp, servePull, err := InitMetricsBridged(t.Context(), "test-bridged-table", bridgedRegistry(t), nil) + if err != nil { + t.Fatalf("InitMetricsBridged: %v", err) + } + if servePull != tt.wantPull { + t.Errorf("servePull = %t, want %t", servePull, tt.wantPull) + } + if pushed := collector.flush(t, mp) != nil; pushed != tt.wantPush { + t.Errorf("pushed = %t, want %t", pushed, tt.wantPush) + } + }) + } +} + func TestInitMetricsBridgedRequiresServiceName(t *testing.T) { - if _, err := InitMetricsBridged(t.Context(), "", prometheus.NewRegistry(), nil); err == nil { + if _, _, err := InitMetricsBridged(t.Context(), "", prometheus.NewRegistry(), nil); err == nil { t.Error("InitMetricsBridged(\"\") must return an error") } } From 68895abba0e4593f3aa91a6966a042224f617178 Mon Sep 17 00:00:00 2001 From: Julian Gutierrez Oschmann Date: Tue, 13 Oct 2026 14:02:43 +0000 Subject: [PATCH 02/42] Use TitleCase for initialisms in API proto names. (#2520) The protobuf style guide treats an abbreviation as a single word in message, enum, service and RPC names (HttpRule, not HTTPRule). Our protos mixed that with Go-style all-caps initialisms, so generated Go code mixed TitleCase field names with all-caps type names. For example: ``` Https: &ateapipb.HTTPSRule{...} ``` Handwritten Go keeps Go's initialism rule (HTTP, TLS, JWT). Only names generated from protos use TitleCase. It's not ideal, but it's likely the least bad option. Fixes #2405 --- cmd/ateapi/internal/apivalidation/actor.go | 4 +- .../internal/apivalidation/actor_template.go | 2 +- .../apivalidation/actor_template_test.go | 20 +- .../internal/apivalidation/actor_test.go | 6 +- .../internal/apivalidation/egress_policy.go | 6 +- .../apivalidation/egress_policy_test.go | 22 +- .../apivalidation/zz_generated.validation.go | 390 +++++++++--------- cmd/ateapi/internal/controlapi/actor.go | 4 +- .../internal/controlapi/egress_policy_test.go | 2 +- .../controlapi/functionaltest/actor_test.go | 6 +- .../functionaltest/egress_policy_test.go | 4 +- .../internal/controlapi/workload_spec.go | 2 +- .../internal/controlapi/workload_spec_test.go | 4 +- cmd/ateapi/internal/defaults/defaults_test.go | 32 +- .../internal/store/storecontract/contract.go | 6 +- cmd/atelet/main.go | 2 +- cmd/atelet/main_test.go | 4 +- .../router/egress/actorjwt/actorjwt.go | 12 +- .../router/egress/actorjwt/actorjwt_test.go | 24 +- .../internal/router/egress/credentials.go | 2 +- .../router/egress/credentials_test.go | 18 +- .../internal/router/egress/egress_test.go | 18 +- .../internal/router/egress/request_test.go | 4 +- .../internal/cmd/actortemplate_test.go | 2 +- .../internal/cmd/egress_policy_test.go | 30 +- .../internal/printer/egress_policy_test.go | 4 +- .../ateinterceptors/ateinterceptors_test.go | 8 +- .../contextlogging/contextlogging_test.go | 30 +- internal/e2e/egresspolicy.go | 12 +- internal/egresspolicy/egresspolicy_test.go | 12 +- internal/proto/ateletpb/atelet.pb.go | 34 +- internal/proto/ateletpb/atelet.proto | 6 +- internal/proto/ateompb/ateom.pb.go | 34 +- internal/proto/ateompb/ateom.proto | 6 +- internal/protoredact/protoredact_test.go | 16 +- internal/serverboot/logging_test.go | 2 +- internal/wakeupprobe/wakeupprobe_test.go | 18 +- pkg/proto/ateapipb/ateapi.pb.go | 252 +++++------ pkg/proto/ateapipb/ateapi.proto | 44 +- pkg/proto/ateapipb/ateapi_grpc.pb.go | 30 +- 40 files changed, 567 insertions(+), 567 deletions(-) diff --git a/cmd/ateapi/internal/apivalidation/actor.go b/cmd/ateapi/internal/apivalidation/actor.go index 77c0d27a5b..a57b113742 100644 --- a/cmd/ateapi/internal/apivalidation/actor.go +++ b/cmd/ateapi/internal/apivalidation/actor.go @@ -83,10 +83,10 @@ func ValidateRevertActorRequest(ctx context.Context, req *ateapipb.RevertActorRe return Validate_RevertActorRequest(ctx, op, nil, req, nil) } -func ValidateMintActorJWTRequest(ctx context.Context, req *ateapipb.MintActorJWTRequest) field.ErrorList { +func ValidateMintActorJWTRequest(ctx context.Context, req *ateapipb.MintActorJwtRequest) field.ErrorList { // Call the generated validation. op := operation.Operation{Type: operation.Create} - return Validate_MintActorJWTRequest(ctx, op, nil, req, nil) + return Validate_MintActorJwtRequest(ctx, op, nil, req, nil) } func ValidateMintActorCertificateRequest(ctx context.Context, req *ateapipb.MintActorCertificateRequest) field.ErrorList { diff --git a/cmd/ateapi/internal/apivalidation/actor_template.go b/cmd/ateapi/internal/apivalidation/actor_template.go index 4df9c84a41..1418f0188f 100644 --- a/cmd/ateapi/internal/apivalidation/actor_template.go +++ b/cmd/ateapi/internal/apivalidation/actor_template.go @@ -100,7 +100,7 @@ func ValidateCustom_CreateActorTemplateRequest_ActorTemplate(_ context.Context, // or fragment. var httpGetPathRE = regexp.MustCompile(`^/([A-Za-z0-9\-._~!$&'()*+,;=:@/]|%[0-9A-Fa-f]{2})*$`) -func ValidateCustom_HTTPGetAction_Path(_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ *string) field.ErrorList { +func ValidateCustom_HttpGetAction_Path(_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ *string) field.ErrorList { if !httpGetPathRE.MatchString(*value) { return field.ErrorList{field.Invalid(fldPath, *value, "must be a URL path starting with '/', using only RFC 3986 path-segment characters, without query or fragment")} } diff --git a/cmd/ateapi/internal/apivalidation/actor_template_test.go b/cmd/ateapi/internal/apivalidation/actor_template_test.go index 12d979784d..7ed4788ee7 100644 --- a/cmd/ateapi/internal/apivalidation/actor_template_test.go +++ b/cmd/ateapi/internal/apivalidation/actor_template_test.go @@ -692,7 +692,7 @@ func TestValidateActorTemplate(t *testing.T) { name: "valid wakeup probe", mutate: func(tmpl *ateapipb.ActorTemplate) { tmpl.Containers[0].WakeupProbe = &ateapipb.ContainerWakeupProbe{ - HttpGet: &ateapipb.HTTPGetAction{Path: "/healthz", Port: 8080}, + HttpGet: &ateapipb.HttpGetAction{Path: "/healthz", Port: 8080}, TimeoutSeconds: 60, } }, @@ -705,20 +705,20 @@ func TestValidateActorTemplate(t *testing.T) { }, { name: "missing wakeup probe timeout_seconds", mutate: func(tmpl *ateapipb.ActorTemplate) { - tmpl.Containers[0].WakeupProbe = &ateapipb.ContainerWakeupProbe{HttpGet: &ateapipb.HTTPGetAction{Path: "/healthz", Port: 8080}} + tmpl.Containers[0].WakeupProbe = &ateapipb.ContainerWakeupProbe{HttpGet: &ateapipb.HttpGetAction{Path: "/healthz", Port: 8080}} }, want: field.ErrorList{field.Required(field.NewPath("containers").Index(0).Child("wakeup_probe", "timeout_seconds"), "")}, }, { name: "missing wakeup probe http_get.path", mutate: func(tmpl *ateapipb.ActorTemplate) { - tmpl.Containers[0].WakeupProbe = &ateapipb.ContainerWakeupProbe{HttpGet: &ateapipb.HTTPGetAction{Port: 8080}, TimeoutSeconds: 60} + tmpl.Containers[0].WakeupProbe = &ateapipb.ContainerWakeupProbe{HttpGet: &ateapipb.HttpGetAction{Port: 8080}, TimeoutSeconds: 60} }, want: field.ErrorList{field.Required(field.NewPath("containers").Index(0).Child("wakeup_probe", "http_get", "path"), "")}, }, { name: "wakeup probe timeout_seconds out of range", mutate: func(tmpl *ateapipb.ActorTemplate) { tmpl.Containers[0].WakeupProbe = &ateapipb.ContainerWakeupProbe{ - HttpGet: &ateapipb.HTTPGetAction{Path: "/healthz", Port: 8080}, + HttpGet: &ateapipb.HttpGetAction{Path: "/healthz", Port: 8080}, TimeoutSeconds: 3601, } }, @@ -727,7 +727,7 @@ func TestValidateActorTemplate(t *testing.T) { name: "negative wakeup probe timeout_seconds", mutate: func(tmpl *ateapipb.ActorTemplate) { tmpl.Containers[0].WakeupProbe = &ateapipb.ContainerWakeupProbe{ - HttpGet: &ateapipb.HTTPGetAction{Path: "/healthz", Port: 8080}, + HttpGet: &ateapipb.HttpGetAction{Path: "/healthz", Port: 8080}, TimeoutSeconds: -1, } }, @@ -736,7 +736,7 @@ func TestValidateActorTemplate(t *testing.T) { name: "wakeup probe missing port", mutate: func(tmpl *ateapipb.ActorTemplate) { tmpl.Containers[0].WakeupProbe = &ateapipb.ContainerWakeupProbe{ - HttpGet: &ateapipb.HTTPGetAction{Path: "/healthz"}, + HttpGet: &ateapipb.HttpGetAction{Path: "/healthz"}, TimeoutSeconds: 60, } }, @@ -745,7 +745,7 @@ func TestValidateActorTemplate(t *testing.T) { name: "negative wakeup probe port", mutate: func(tmpl *ateapipb.ActorTemplate) { tmpl.Containers[0].WakeupProbe = &ateapipb.ContainerWakeupProbe{ - HttpGet: &ateapipb.HTTPGetAction{Path: "/healthz", Port: -1}, + HttpGet: &ateapipb.HttpGetAction{Path: "/healthz", Port: -1}, TimeoutSeconds: 60, } }, @@ -754,7 +754,7 @@ func TestValidateActorTemplate(t *testing.T) { name: "wakeup probe port out of range", mutate: func(tmpl *ateapipb.ActorTemplate) { tmpl.Containers[0].WakeupProbe = &ateapipb.ContainerWakeupProbe{ - HttpGet: &ateapipb.HTTPGetAction{Path: "/healthz", Port: 65536}, + HttpGet: &ateapipb.HttpGetAction{Path: "/healthz", Port: 65536}, TimeoutSeconds: 60, } }, @@ -763,7 +763,7 @@ func TestValidateActorTemplate(t *testing.T) { name: "wakeup probe path with query string", mutate: func(tmpl *ateapipb.ActorTemplate) { tmpl.Containers[0].WakeupProbe = &ateapipb.ContainerWakeupProbe{ - HttpGet: &ateapipb.HTTPGetAction{Path: "/readyz?verbose=1", Port: 8080}, + HttpGet: &ateapipb.HttpGetAction{Path: "/readyz?verbose=1", Port: 8080}, TimeoutSeconds: 60, } }, @@ -772,7 +772,7 @@ func TestValidateActorTemplate(t *testing.T) { name: "wakeup probe path not starting with slash", mutate: func(tmpl *ateapipb.ActorTemplate) { tmpl.Containers[0].WakeupProbe = &ateapipb.ContainerWakeupProbe{ - HttpGet: &ateapipb.HTTPGetAction{Path: "readyz", Port: 8080}, + HttpGet: &ateapipb.HttpGetAction{Path: "readyz", Port: 8080}, TimeoutSeconds: 60, } }, diff --git a/cmd/ateapi/internal/apivalidation/actor_test.go b/cmd/ateapi/internal/apivalidation/actor_test.go index 57d6e36892..63bd6d3175 100644 --- a/cmd/ateapi/internal/apivalidation/actor_test.go +++ b/cmd/ateapi/internal/apivalidation/actor_test.go @@ -1097,8 +1097,8 @@ func withActorWorkerAssignment(mods ...func(*ateapipb.WorkerAssignment)) func(*a } func TestValidateMintActorJWTRequest(t *testing.T) { - withExpiration := func(seconds int64) *ateapipb.MintActorJWTRequest { - return &ateapipb.MintActorJWTRequest{ + withExpiration := func(seconds int64) *ateapipb.MintActorJwtRequest { + return &ateapipb.MintActorJwtRequest{ Actor: &ateapipb.ObjectRef{Atespace: "ns1", Name: "id1"}, Audiences: []string{"https://example.com"}, ExpirationSeconds: seconds, @@ -1107,7 +1107,7 @@ func TestValidateMintActorJWTRequest(t *testing.T) { path := field.NewPath("expiration_seconds") tests := []struct { name string - req *ateapipb.MintActorJWTRequest + req *ateapipb.MintActorJwtRequest want field.ErrorList }{ {name: "minimum expiration", req: withExpiration(300)}, diff --git a/cmd/ateapi/internal/apivalidation/egress_policy.go b/cmd/ateapi/internal/apivalidation/egress_policy.go index 7a6bdabce7..c22f0a7c9e 100644 --- a/cmd/ateapi/internal/apivalidation/egress_policy.go +++ b/cmd/ateapi/internal/apivalidation/egress_policy.go @@ -160,15 +160,15 @@ func (k portKey) String() string { return fmt.Sprintf("port %d", k.number) } -func ValidateCustom_HTTPRule_Hostnames(_ context.Context, _ operation.Operation, p *field.Path, patterns, _ []string) field.ErrorList { +func ValidateCustom_HttpRule_Hostnames(_ context.Context, _ operation.Operation, p *field.Path, patterns, _ []string) field.ErrorList { return validateHostnamePatterns(patterns, p) } -func ValidateCustom_HTTPSRule_Hostnames(_ context.Context, _ operation.Operation, p *field.Path, patterns, _ []string) field.ErrorList { +func ValidateCustom_HttpsRule_Hostnames(_ context.Context, _ operation.Operation, p *field.Path, patterns, _ []string) field.ErrorList { return validateHostnamePatterns(patterns, p) } -func ValidateCustom_TLSPassthroughRule_Hostnames(_ context.Context, _ operation.Operation, p *field.Path, patterns, _ []string) field.ErrorList { +func ValidateCustom_TlsPassthroughRule_Hostnames(_ context.Context, _ operation.Operation, p *field.Path, patterns, _ []string) field.ErrorList { return validateHostnamePatterns(patterns, p) } diff --git a/cmd/ateapi/internal/apivalidation/egress_policy_test.go b/cmd/ateapi/internal/apivalidation/egress_policy_test.go index b45b0a1774..89829a165b 100644 --- a/cmd/ateapi/internal/apivalidation/egress_policy_test.go +++ b/cmd/ateapi/internal/apivalidation/egress_policy_test.go @@ -31,7 +31,7 @@ func validEgressPolicy() *ateapipb.EgressPolicy { return &ateapipb.EgressPolicy{ Metadata: &ateapipb.ResourceMetadata{Atespace: testAtespace, Name: "default"}, Rules: []*ateapipb.EgressRule{{ - Http: &ateapipb.HTTPRule{ + Http: &ateapipb.HttpRule{ Hostnames: []string{"api.example.com"}, Effects: &ateapipb.HttpRuleEffects{ ReplaceHeaders: []*ateapipb.CredentialHeader{{ @@ -51,7 +51,7 @@ func actorJWTHeader(audiences ...string) *ateapipb.CredentialHeader { return &ateapipb.CredentialHeader{ Header: "Authorization", Prefix: "Bearer ", - ActorJwt: &ateapipb.ActorJWTSource{Audiences: audiences, ExpirationSeconds: 900}, + ActorJwt: &ateapipb.ActorJwtSource{Audiences: audiences, ExpirationSeconds: 900}, } } @@ -444,13 +444,13 @@ func TestValidateEgressPolicyRules(t *testing.T) { } withoutEffects := func(p *ateapipb.EgressPolicy) { p.Rules[0].Http.Effects = nil } trivialHTTPRule := func(hostname string) *ateapipb.EgressRule { - return &ateapipb.EgressRule{Http: &ateapipb.HTTPRule{Hostnames: []string{hostname}}} + return &ateapipb.EgressRule{Http: &ateapipb.HttpRule{Hostnames: []string{hostname}}} } httpsRule := func(ports *ateapipb.Ports, patterns ...string) *ateapipb.EgressRule { - return &ateapipb.EgressRule{Https: &ateapipb.HTTPSRule{Hostnames: patterns, Ports: ports}} + return &ateapipb.EgressRule{Https: &ateapipb.HttpsRule{Hostnames: patterns, Ports: ports}} } passthroughRule := func(ports *ateapipb.Ports, patterns ...string) *ateapipb.EgressRule { - return &ateapipb.EgressRule{TlsPassthrough: &ateapipb.TLSPassthroughRule{Hostnames: patterns, Ports: ports}} + return &ateapipb.EgressRule{TlsPassthrough: &ateapipb.TlsPassthroughRule{Hostnames: patterns, Ports: ports}} } portNumbers := func(numbers ...int32) *ateapipb.Ports { return &ateapipb.Ports{Numbers: numbers} } allPorts := func() *ateapipb.Ports { return &ateapipb.Ports{All: &ateapipb.AllPorts{}} } @@ -504,7 +504,7 @@ func TestValidateEgressPolicyRules(t *testing.T) { }, { name: "two protocols", mutate: func(p *ateapipb.EgressPolicy) { - p.Rules[0].Https = &ateapipb.HTTPSRule{Hostnames: []string{"api.example.com"}} + p.Rules[0].Https = &ateapipb.HttpsRule{Hostnames: []string{"api.example.com"}} }, want: field.ErrorList{ field.Invalid(rule, nil, "one of").WithOrigin("union"), @@ -737,7 +737,7 @@ func TestValidateEgressPolicyRules(t *testing.T) { }, { name: "same pattern on different ports", mutate: func(p *ateapipb.EgressPolicy) { - p.Rules = append(p.Rules, &ateapipb.EgressRule{Http: &ateapipb.HTTPRule{Hostnames: []string{"api.example.com"}, Ports: portNumbers(8080)}}) + p.Rules = append(p.Rules, &ateapipb.EgressRule{Http: &ateapipb.HttpRule{Hostnames: []string{"api.example.com"}, Ports: portNumbers(8080)}}) }, }, { name: "same pattern on the default port ties", @@ -765,7 +765,7 @@ func TestValidateEgressPolicyRules(t *testing.T) { name: "http and tls passthrough on every port tie", mutate: func(p *ateapipb.EgressPolicy) { p.Rules = []*ateapipb.EgressRule{ - {Http: &ateapipb.HTTPRule{Hostnames: []string{"*"}, Ports: allPorts()}}, + {Http: &ateapipb.HttpRule{Hostnames: []string{"*"}, Ports: allPorts()}}, passthroughRule(allPorts(), "*"), } }, @@ -878,7 +878,7 @@ func TestValidateEgressPolicyRules(t *testing.T) { }, { name: "actor JWT on an https rule", mutate: func(p *ateapipb.EgressPolicy) { - p.Rules[0] = &ateapipb.EgressRule{Https: &ateapipb.HTTPSRule{ + p.Rules[0] = &ateapipb.EgressRule{Https: &ateapipb.HttpsRule{ Hostnames: []string{"api.example.com"}, Effects: &ateapipb.HttpRuleEffects{ReplaceHeaders: []*ateapipb.CredentialHeader{actorJWTHeader("https://api.example.com")}}, }} @@ -893,7 +893,7 @@ func TestValidateEgressPolicyRules(t *testing.T) { }, { name: "credential URI and actor JWT in one header", mutate: func(p *ateapipb.EgressPolicy) { - p.Rules[0].Http.Effects.ReplaceHeaders[0].ActorJwt = &ateapipb.ActorJWTSource{Audiences: []string{"https://api.example.com"}, ExpirationSeconds: 900} + p.Rules[0].Http.Effects.ReplaceHeaders[0].ActorJwt = &ateapipb.ActorJwtSource{Audiences: []string{"https://api.example.com"}, ExpirationSeconds: 900} }, want: field.ErrorList{ field.Invalid(staticHeader, nil, "one of").WithOrigin("union"), @@ -988,7 +988,7 @@ func TestValidateEgressPolicyRules(t *testing.T) { }, { name: "effects on an https rule", mutate: func(p *ateapipb.EgressPolicy) { - p.Rules[0] = &ateapipb.EgressRule{Https: &ateapipb.HTTPSRule{Hostnames: []string{"api.example.com"}, Effects: &ateapipb.HttpRuleEffects{}}} + p.Rules[0] = &ateapipb.EgressRule{Https: &ateapipb.HttpsRule{Hostnames: []string{"api.example.com"}, Effects: &ateapipb.HttpRuleEffects{}}} }, want: field.ErrorList{ field.Required(rule.Child("https", "effects"), "at least one effect must be specified"), diff --git a/cmd/ateapi/internal/apivalidation/zz_generated.validation.go b/cmd/ateapi/internal/apivalidation/zz_generated.validation.go index a5fc763555..f27f64d8c1 100644 --- a/cmd/ateapi/internal/apivalidation/zz_generated.validation.go +++ b/cmd/ateapi/internal/apivalidation/zz_generated.validation.go @@ -396,13 +396,13 @@ func Validate_ActorCrash( return errs } -// Validate_ActorJWTSource validates an instance of ActorJWTSource according +// Validate_ActorJwtSource validates an instance of ActorJwtSource according // to declarative validation rules in the API schema. -func Validate_ActorJWTSource( +func Validate_ActorJwtSource( ctx context.Context, op operation.Operation, fldPath *field.Path, - obj, oldObj *ateapipb.ActorJWTSource) (errs field.ErrorList) { + obj, oldObj *ateapipb.ActorJwtSource) (errs field.ErrorList) { - { // field ateapipb.ActorJWTSource.Audiences + { // field ateapipb.ActorJwtSource.Audiences fn := func( fldPath *field.Path, obj, oldObj []string, @@ -445,13 +445,13 @@ func Validate_ActorJWTSource( return } oldVal := safe.Field(oldObj, - func(oldObj *ateapipb.ActorJWTSource) []string { + func(oldObj *ateapipb.ActorJwtSource) []string { return oldObj.Audiences }) errs = append(errs, fn(fldPath.Child("audiences"), obj.Audiences, oldVal, oldObj != nil)...) } - { // field ateapipb.ActorJWTSource.ExpirationSeconds + { // field ateapipb.ActorJwtSource.ExpirationSeconds fn := func( fldPath *field.Path, obj, oldObj *int64, @@ -480,7 +480,7 @@ func Validate_ActorJWTSource( return } oldVal := safe.Field(oldObj, - func(oldObj *ateapipb.ActorJWTSource) *int64 { + func(oldObj *ateapipb.ActorJwtSource) *int64 { return &oldObj.ExpirationSeconds }) errs = append(errs, fn(fldPath.Child("expiration_seconds"), &obj.ExpirationSeconds, oldVal, oldObj != nil)...) @@ -1901,7 +1901,7 @@ func Validate_ContainerWakeupProbe( { // field ateapipb.ContainerWakeupProbe.HttpGet fn := func( fldPath *field.Path, - obj, oldObj *ateapipb.HTTPGetAction, + obj, oldObj *ateapipb.HttpGetAction, oldValueCorrelated bool) (errs field.ErrorList) { // don't revalidate unchanged data if oldValueCorrelated && op.Type == operation.Update { @@ -1919,11 +1919,11 @@ func Validate_ContainerWakeupProbe( return // do not proceed } // call the type's validation function - errs = append(errs, Validate_HTTPGetAction(ctx, op, fldPath, obj, oldObj)...) + errs = append(errs, Validate_HttpGetAction(ctx, op, fldPath, obj, oldObj)...) return } oldVal := safe.Field(oldObj, - func(oldObj *ateapipb.ContainerWakeupProbe) *ateapipb.HTTPGetAction { + func(oldObj *ateapipb.ContainerWakeupProbe) *ateapipb.HttpGetAction { return oldObj.HttpGet }) errs = append(errs, fn(fldPath.Child("http_get"), obj.HttpGet, oldVal, oldObj != nil)...) @@ -2522,7 +2522,7 @@ func Validate_CredentialHeader( { // field ateapipb.CredentialHeader.ActorJwt fn := func( fldPath *field.Path, - obj, oldObj *ateapipb.ActorJWTSource, + obj, oldObj *ateapipb.ActorJwtSource, oldValueCorrelated bool) (errs field.ErrorList) { // don't revalidate unchanged data if oldValueCorrelated && op.Type == operation.Update { @@ -2539,11 +2539,11 @@ func Validate_CredentialHeader( return // do not proceed } // call the type's validation function - errs = append(errs, Validate_ActorJWTSource(ctx, op, fldPath, obj, oldObj)...) + errs = append(errs, Validate_ActorJwtSource(ctx, op, fldPath, obj, oldObj)...) return } oldVal := safe.Field(oldObj, - func(oldObj *ateapipb.CredentialHeader) *ateapipb.ActorJWTSource { + func(oldObj *ateapipb.CredentialHeader) *ateapipb.ActorJwtSource { return oldObj.ActorJwt }) errs = append(errs, fn(fldPath.Child("actor_jwt"), obj.ActorJwt, oldVal, oldObj != nil)...) @@ -3529,7 +3529,7 @@ func Validate_EgressRule( { // field ateapipb.EgressRule.Http fn := func( fldPath *field.Path, - obj, oldObj *ateapipb.HTTPRule, + obj, oldObj *ateapipb.HttpRule, oldValueCorrelated bool) (errs field.ErrorList) { // don't revalidate unchanged data if oldValueCorrelated && op.Type == operation.Update { @@ -3546,11 +3546,11 @@ func Validate_EgressRule( return // do not proceed } // call the type's validation function - errs = append(errs, Validate_HTTPRule(ctx, op, fldPath, obj, oldObj)...) + errs = append(errs, Validate_HttpRule(ctx, op, fldPath, obj, oldObj)...) return } oldVal := safe.Field(oldObj, - func(oldObj *ateapipb.EgressRule) *ateapipb.HTTPRule { + func(oldObj *ateapipb.EgressRule) *ateapipb.HttpRule { return oldObj.Http }) errs = append(errs, fn(fldPath.Child("http"), obj.Http, oldVal, oldObj != nil)...) @@ -3559,7 +3559,7 @@ func Validate_EgressRule( { // field ateapipb.EgressRule.Https fn := func( fldPath *field.Path, - obj, oldObj *ateapipb.HTTPSRule, + obj, oldObj *ateapipb.HttpsRule, oldValueCorrelated bool) (errs field.ErrorList) { // don't revalidate unchanged data if oldValueCorrelated && op.Type == operation.Update { @@ -3576,11 +3576,11 @@ func Validate_EgressRule( return // do not proceed } // call the type's validation function - errs = append(errs, Validate_HTTPSRule(ctx, op, fldPath, obj, oldObj)...) + errs = append(errs, Validate_HttpsRule(ctx, op, fldPath, obj, oldObj)...) return } oldVal := safe.Field(oldObj, - func(oldObj *ateapipb.EgressRule) *ateapipb.HTTPSRule { + func(oldObj *ateapipb.EgressRule) *ateapipb.HttpsRule { return oldObj.Https }) errs = append(errs, fn(fldPath.Child("https"), obj.Https, oldVal, oldObj != nil)...) @@ -3589,7 +3589,7 @@ func Validate_EgressRule( { // field ateapipb.EgressRule.TlsPassthrough fn := func( fldPath *field.Path, - obj, oldObj *ateapipb.TLSPassthroughRule, + obj, oldObj *ateapipb.TlsPassthroughRule, oldValueCorrelated bool) (errs field.ErrorList) { // don't revalidate unchanged data if oldValueCorrelated && op.Type == operation.Update { @@ -3606,11 +3606,11 @@ func Validate_EgressRule( return // do not proceed } // call the type's validation function - errs = append(errs, Validate_TLSPassthroughRule(ctx, op, fldPath, obj, oldObj)...) + errs = append(errs, Validate_TlsPassthroughRule(ctx, op, fldPath, obj, oldObj)...) return } oldVal := safe.Field(oldObj, - func(oldObj *ateapipb.EgressRule) *ateapipb.TLSPassthroughRule { + func(oldObj *ateapipb.EgressRule) *ateapipb.TlsPassthroughRule { return oldObj.TlsPassthrough }) errs = append(errs, fn(fldPath.Child("tls_passthrough"), obj.TlsPassthrough, oldVal, oldObj != nil)...) @@ -4648,13 +4648,13 @@ func Validate_GoldenSnapshotStatus( return errs } -// Validate_HTTPGetAction validates an instance of HTTPGetAction according +// Validate_HttpGetAction validates an instance of HttpGetAction according // to declarative validation rules in the API schema. -func Validate_HTTPGetAction( +func Validate_HttpGetAction( ctx context.Context, op operation.Operation, fldPath *field.Path, - obj, oldObj *ateapipb.HTTPGetAction) (errs field.ErrorList) { + obj, oldObj *ateapipb.HttpGetAction) (errs field.ErrorList) { - { // field ateapipb.HTTPGetAction.Path + { // field ateapipb.HttpGetAction.Path fn := func( fldPath *field.Path, obj, oldObj *string, @@ -4675,7 +4675,7 @@ func Validate_HTTPGetAction( return // do not proceed } // custom validation - if e := ValidateCustom_HTTPGetAction_Path(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + if e := ValidateCustom_HttpGetAction_Path(ctx, op, fldPath, obj, oldObj); len(e) != 0 { errs = append(errs, e...) } if e := validate.MaxLength(ctx, op, fldPath, obj, oldObj, 1024); len(e) != 0 { @@ -4684,13 +4684,13 @@ func Validate_HTTPGetAction( return } oldVal := safe.Field(oldObj, - func(oldObj *ateapipb.HTTPGetAction) *string { + func(oldObj *ateapipb.HttpGetAction) *string { return &oldObj.Path }) errs = append(errs, fn(fldPath.Child("path"), &obj.Path, oldVal, oldObj != nil)...) } - { // field ateapipb.HTTPGetAction.Port + { // field ateapipb.HttpGetAction.Port fn := func( fldPath *field.Path, obj, oldObj *int32, @@ -4719,7 +4719,7 @@ func Validate_HTTPGetAction( return } oldVal := safe.Field(oldObj, - func(oldObj *ateapipb.HTTPGetAction) *int32 { + func(oldObj *ateapipb.HttpGetAction) *int32 { return &oldObj.Port }) errs = append(errs, fn(fldPath.Child("port"), &obj.Port, oldVal, oldObj != nil)...) @@ -4728,13 +4728,13 @@ func Validate_HTTPGetAction( return errs } -// Validate_HTTPRule validates an instance of HTTPRule according +// Validate_HttpRule validates an instance of HttpRule according // to declarative validation rules in the API schema. -func Validate_HTTPRule( +func Validate_HttpRule( ctx context.Context, op operation.Operation, fldPath *field.Path, - obj, oldObj *ateapipb.HTTPRule) (errs field.ErrorList) { + obj, oldObj *ateapipb.HttpRule) (errs field.ErrorList) { - { // field ateapipb.HTTPRule.Hostnames + { // field ateapipb.HttpRule.Hostnames fn := func( fldPath *field.Path, obj, oldObj []string, @@ -4759,7 +4759,7 @@ func Validate_HTTPRule( return // do not proceed } // custom validation - if e := ValidateCustom_HTTPRule_Hostnames(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + if e := ValidateCustom_HttpRule_Hostnames(ctx, op, fldPath, obj, oldObj); len(e) != 0 { errs = append(errs, e...) } // lists with set semantics require unique values @@ -4769,13 +4769,13 @@ func Validate_HTTPRule( return } oldVal := safe.Field(oldObj, - func(oldObj *ateapipb.HTTPRule) []string { + func(oldObj *ateapipb.HttpRule) []string { return oldObj.Hostnames }) errs = append(errs, fn(fldPath.Child("hostnames"), obj.Hostnames, oldVal, oldObj != nil)...) } - { // field ateapipb.HTTPRule.Ports + { // field ateapipb.HttpRule.Ports fn := func( fldPath *field.Path, obj, oldObj *ateapipb.Ports, @@ -4799,13 +4799,13 @@ func Validate_HTTPRule( return } oldVal := safe.Field(oldObj, - func(oldObj *ateapipb.HTTPRule) *ateapipb.Ports { + func(oldObj *ateapipb.HttpRule) *ateapipb.Ports { return oldObj.Ports }) errs = append(errs, fn(fldPath.Child("ports"), obj.Ports, oldVal, oldObj != nil)...) } - { // field ateapipb.HTTPRule.Effects + { // field ateapipb.HttpRule.Effects fn := func( fldPath *field.Path, obj, oldObj *ateapipb.HttpRuleEffects, @@ -4829,7 +4829,7 @@ func Validate_HTTPRule( return } oldVal := safe.Field(oldObj, - func(oldObj *ateapipb.HTTPRule) *ateapipb.HttpRuleEffects { + func(oldObj *ateapipb.HttpRule) *ateapipb.HttpRuleEffects { return oldObj.Effects }) errs = append(errs, fn(fldPath.Child("effects"), obj.Effects, oldVal, oldObj != nil)...) @@ -4838,17 +4838,23 @@ func Validate_HTTPRule( return errs } -// Validate_HTTPSRule validates an instance of HTTPSRule according +// Validate_HttpRuleEffects validates an instance of HttpRuleEffects according // to declarative validation rules in the API schema. -func Validate_HTTPSRule( +func Validate_HttpRuleEffects( ctx context.Context, op operation.Operation, fldPath *field.Path, - obj, oldObj *ateapipb.HTTPSRule) (errs field.ErrorList) { + obj, oldObj *ateapipb.HttpRuleEffects) (errs field.ErrorList) { + + // custom validation + if e := ValidateCustom_HttpRuleEffects(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + errs = append(errs, e...) + } - { // field ateapipb.HTTPSRule.Hostnames + { // field ateapipb.HttpRuleEffects.ReplaceHeaders fn := func( fldPath *field.Path, - obj, oldObj []string, + obj, oldObj []*ateapipb.CredentialHeader, oldValueCorrelated bool) (errs field.ErrorList) { + // Uniqueness validation is implemented via custom, handwritten validation // don't revalidate unchanged data if oldValueCorrelated && op.Type == operation.Update { if ateDeepEqual(obj, oldObj) { @@ -4857,38 +4863,51 @@ func Validate_HTTPSRule( } // call field-attached validations earlyReturn := false - if e := validate.MaxItems(ctx, op, fldPath, obj, oldObj, 256).MarkShortCircuit(); len(e) != 0 { + if e := validate.PtrSliceNoNils[ateapipb.CredentialHeader](ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { errs = append(errs, e...) earlyReturn = true } - if e := validate.RequiredSlice(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + if e := validate.MaxItems(ctx, op, fldPath, obj, oldObj, 16).MarkShortCircuit(); len(e) != 0 { errs = append(errs, e...) earlyReturn = true } + if e := validate.OptionalSlice(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + earlyReturn = true + } if earlyReturn { return // do not proceed } // custom validation - if e := ValidateCustom_HTTPSRule_Hostnames(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + if e := ValidateCustom_HttpRuleEffects_ReplaceHeaders(ctx, op, fldPath, obj, oldObj); len(e) != 0 { errs = append(errs, e...) } - // lists with set semantics require unique values - if e := validate.ValSliceUnique(ctx, op, fldPath, obj, oldObj, validate.DirectEqual); len(e) != 0 { + // iterate the list and call the type's validation function + if e := validate.EachPtrSliceVal(ctx, op, fldPath, obj, oldObj, + func(a *ateapipb.CredentialHeader, b *ateapipb.CredentialHeader) bool { return a.Header == b.Header }, ateDeepEqual, Validate_CredentialHeader); len(e) != 0 { errs = append(errs, e...) } return } oldVal := safe.Field(oldObj, - func(oldObj *ateapipb.HTTPSRule) []string { - return oldObj.Hostnames + func(oldObj *ateapipb.HttpRuleEffects) []*ateapipb.CredentialHeader { + return oldObj.ReplaceHeaders }) - errs = append(errs, fn(fldPath.Child("hostnames"), obj.Hostnames, oldVal, oldObj != nil)...) + errs = append(errs, fn(fldPath.Child("replace_headers"), obj.ReplaceHeaders, oldVal, oldObj != nil)...) } - { // field ateapipb.HTTPSRule.Ports + return errs +} + +// Validate_HttpsRule validates an instance of HttpsRule according +// to declarative validation rules in the API schema. +func Validate_HttpsRule( + ctx context.Context, op operation.Operation, fldPath *field.Path, + obj, oldObj *ateapipb.HttpsRule) (errs field.ErrorList) { + + { // field ateapipb.HttpsRule.Hostnames fn := func( fldPath *field.Path, - obj, oldObj *ateapipb.Ports, + obj, oldObj []string, oldValueCorrelated bool) (errs field.ErrorList) { // don't revalidate unchanged data if oldValueCorrelated && op.Type == operation.Update { @@ -4898,27 +4917,38 @@ func Validate_HTTPSRule( } // call field-attached validations earlyReturn := false - if e := validate.OptionalPointer(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + if e := validate.MaxItems(ctx, op, fldPath, obj, oldObj, 256).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if e := validate.RequiredSlice(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) earlyReturn = true } if earlyReturn { return // do not proceed } - // call the type's validation function - errs = append(errs, Validate_Ports(ctx, op, fldPath, obj, oldObj)...) + // custom validation + if e := ValidateCustom_HttpsRule_Hostnames(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + errs = append(errs, e...) + } + // lists with set semantics require unique values + if e := validate.ValSliceUnique(ctx, op, fldPath, obj, oldObj, validate.DirectEqual); len(e) != 0 { + errs = append(errs, e...) + } return } oldVal := safe.Field(oldObj, - func(oldObj *ateapipb.HTTPSRule) *ateapipb.Ports { - return oldObj.Ports + func(oldObj *ateapipb.HttpsRule) []string { + return oldObj.Hostnames }) - errs = append(errs, fn(fldPath.Child("ports"), obj.Ports, oldVal, oldObj != nil)...) + errs = append(errs, fn(fldPath.Child("hostnames"), obj.Hostnames, oldVal, oldObj != nil)...) } - { // field ateapipb.HTTPSRule.Effects + { // field ateapipb.HttpsRule.Ports fn := func( fldPath *field.Path, - obj, oldObj *ateapipb.HttpRuleEffects, + obj, oldObj *ateapipb.Ports, oldValueCorrelated bool) (errs field.ErrorList) { // don't revalidate unchanged data if oldValueCorrelated && op.Type == operation.Update { @@ -4935,36 +4965,21 @@ func Validate_HTTPSRule( return // do not proceed } // call the type's validation function - errs = append(errs, Validate_HttpRuleEffects(ctx, op, fldPath, obj, oldObj)...) + errs = append(errs, Validate_Ports(ctx, op, fldPath, obj, oldObj)...) return } oldVal := safe.Field(oldObj, - func(oldObj *ateapipb.HTTPSRule) *ateapipb.HttpRuleEffects { - return oldObj.Effects + func(oldObj *ateapipb.HttpsRule) *ateapipb.Ports { + return oldObj.Ports }) - errs = append(errs, fn(fldPath.Child("effects"), obj.Effects, oldVal, oldObj != nil)...) - } - - return errs -} - -// Validate_HttpRuleEffects validates an instance of HttpRuleEffects according -// to declarative validation rules in the API schema. -func Validate_HttpRuleEffects( - ctx context.Context, op operation.Operation, fldPath *field.Path, - obj, oldObj *ateapipb.HttpRuleEffects) (errs field.ErrorList) { - - // custom validation - if e := ValidateCustom_HttpRuleEffects(ctx, op, fldPath, obj, oldObj); len(e) != 0 { - errs = append(errs, e...) + errs = append(errs, fn(fldPath.Child("ports"), obj.Ports, oldVal, oldObj != nil)...) } - { // field ateapipb.HttpRuleEffects.ReplaceHeaders + { // field ateapipb.HttpsRule.Effects fn := func( fldPath *field.Path, - obj, oldObj []*ateapipb.CredentialHeader, + obj, oldObj *ateapipb.HttpRuleEffects, oldValueCorrelated bool) (errs field.ErrorList) { - // Uniqueness validation is implemented via custom, handwritten validation // don't revalidate unchanged data if oldValueCorrelated && op.Type == operation.Update { if ateDeepEqual(obj, oldObj) { @@ -4973,36 +4988,21 @@ func Validate_HttpRuleEffects( } // call field-attached validations earlyReturn := false - if e := validate.PtrSliceNoNils[ateapipb.CredentialHeader](ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { - errs = append(errs, e...) - earlyReturn = true - } - if e := validate.MaxItems(ctx, op, fldPath, obj, oldObj, 16).MarkShortCircuit(); len(e) != 0 { - errs = append(errs, e...) - earlyReturn = true - } - if e := validate.OptionalSlice(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + if e := validate.OptionalPointer(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { earlyReturn = true } if earlyReturn { return // do not proceed } - // custom validation - if e := ValidateCustom_HttpRuleEffects_ReplaceHeaders(ctx, op, fldPath, obj, oldObj); len(e) != 0 { - errs = append(errs, e...) - } - // iterate the list and call the type's validation function - if e := validate.EachPtrSliceVal(ctx, op, fldPath, obj, oldObj, - func(a *ateapipb.CredentialHeader, b *ateapipb.CredentialHeader) bool { return a.Header == b.Header }, ateDeepEqual, Validate_CredentialHeader); len(e) != 0 { - errs = append(errs, e...) - } + // call the type's validation function + errs = append(errs, Validate_HttpRuleEffects(ctx, op, fldPath, obj, oldObj)...) return } oldVal := safe.Field(oldObj, - func(oldObj *ateapipb.HttpRuleEffects) []*ateapipb.CredentialHeader { - return oldObj.ReplaceHeaders + func(oldObj *ateapipb.HttpsRule) *ateapipb.HttpRuleEffects { + return oldObj.Effects }) - errs = append(errs, fn(fldPath.Child("replace_headers"), obj.ReplaceHeaders, oldVal, oldObj != nil)...) + errs = append(errs, fn(fldPath.Child("effects"), obj.Effects, oldVal, oldObj != nil)...) } return errs @@ -5804,13 +5804,13 @@ func Validate_MintActorCertificateRequest( return errs } -// Validate_MintActorJWTRequest validates an instance of MintActorJWTRequest according +// Validate_MintActorJwtRequest validates an instance of MintActorJwtRequest according // to declarative validation rules in the API schema. -func Validate_MintActorJWTRequest( +func Validate_MintActorJwtRequest( ctx context.Context, op operation.Operation, fldPath *field.Path, - obj, oldObj *ateapipb.MintActorJWTRequest) (errs field.ErrorList) { + obj, oldObj *ateapipb.MintActorJwtRequest) (errs field.ErrorList) { - { // field ateapipb.MintActorJWTRequest.Actor + { // field ateapipb.MintActorJwtRequest.Actor fn := func( fldPath *field.Path, obj, oldObj *ateapipb.ObjectRef, @@ -5835,13 +5835,13 @@ func Validate_MintActorJWTRequest( return } oldVal := safe.Field(oldObj, - func(oldObj *ateapipb.MintActorJWTRequest) *ateapipb.ObjectRef { + func(oldObj *ateapipb.MintActorJwtRequest) *ateapipb.ObjectRef { return oldObj.Actor }) errs = append(errs, fn(fldPath.Child("actor"), obj.Actor, oldVal, oldObj != nil)...) } - { // field ateapipb.MintActorJWTRequest.Audiences + { // field ateapipb.MintActorJwtRequest.Audiences fn := func( fldPath *field.Path, obj, oldObj []string, @@ -5878,13 +5878,13 @@ func Validate_MintActorJWTRequest( return } oldVal := safe.Field(oldObj, - func(oldObj *ateapipb.MintActorJWTRequest) []string { + func(oldObj *ateapipb.MintActorJwtRequest) []string { return oldObj.Audiences }) errs = append(errs, fn(fldPath.Child("audiences"), obj.Audiences, oldVal, oldObj != nil)...) } - { // field ateapipb.MintActorJWTRequest.ExpirationSeconds + { // field ateapipb.MintActorJwtRequest.ExpirationSeconds fn := func( fldPath *field.Path, obj, oldObj *int64, @@ -5913,7 +5913,7 @@ func Validate_MintActorJWTRequest( return } oldVal := safe.Field(oldObj, - func(oldObj *ateapipb.MintActorJWTRequest) *int64 { + func(oldObj *ateapipb.MintActorJwtRequest) *int64 { return &oldObj.ExpirationSeconds }) errs = append(errs, fn(fldPath.Child("expiration_seconds"), &obj.ExpirationSeconds, oldVal, oldObj != nil)...) @@ -7862,87 +7862,6 @@ func Validate_SystemInfoVolumeSource( return errs } -// Validate_TLSPassthroughRule validates an instance of TLSPassthroughRule according -// to declarative validation rules in the API schema. -func Validate_TLSPassthroughRule( - ctx context.Context, op operation.Operation, fldPath *field.Path, - obj, oldObj *ateapipb.TLSPassthroughRule) (errs field.ErrorList) { - - { // field ateapipb.TLSPassthroughRule.Hostnames - fn := func( - fldPath *field.Path, - obj, oldObj []string, - oldValueCorrelated bool) (errs field.ErrorList) { - // don't revalidate unchanged data - if oldValueCorrelated && op.Type == operation.Update { - if ateDeepEqual(obj, oldObj) { - return nil - } - } - // call field-attached validations - earlyReturn := false - if e := validate.MaxItems(ctx, op, fldPath, obj, oldObj, 256).MarkShortCircuit(); len(e) != 0 { - errs = append(errs, e...) - earlyReturn = true - } - if e := validate.RequiredSlice(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { - errs = append(errs, e...) - earlyReturn = true - } - if earlyReturn { - return // do not proceed - } - // custom validation - if e := ValidateCustom_TLSPassthroughRule_Hostnames(ctx, op, fldPath, obj, oldObj); len(e) != 0 { - errs = append(errs, e...) - } - // lists with set semantics require unique values - if e := validate.ValSliceUnique(ctx, op, fldPath, obj, oldObj, validate.DirectEqual); len(e) != 0 { - errs = append(errs, e...) - } - return - } - oldVal := safe.Field(oldObj, - func(oldObj *ateapipb.TLSPassthroughRule) []string { - return oldObj.Hostnames - }) - errs = append(errs, fn(fldPath.Child("hostnames"), obj.Hostnames, oldVal, oldObj != nil)...) - } - - { // field ateapipb.TLSPassthroughRule.Ports - fn := func( - fldPath *field.Path, - obj, oldObj *ateapipb.Ports, - oldValueCorrelated bool) (errs field.ErrorList) { - // don't revalidate unchanged data - if oldValueCorrelated && op.Type == operation.Update { - if ateDeepEqual(obj, oldObj) { - return nil - } - } - // call field-attached validations - earlyReturn := false - if e := validate.RequiredPointer(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { - errs = append(errs, e...) - earlyReturn = true - } - if earlyReturn { - return // do not proceed - } - // call the type's validation function - errs = append(errs, Validate_Ports(ctx, op, fldPath, obj, oldObj)...) - return - } - oldVal := safe.Field(oldObj, - func(oldObj *ateapipb.TLSPassthroughRule) *ateapipb.Ports { - return oldObj.Ports - }) - errs = append(errs, fn(fldPath.Child("ports"), obj.Ports, oldVal, oldObj != nil)...) - } - - return errs -} - // Validate_Tag validates an instance of Tag according // to declarative validation rules in the API schema. func Validate_Tag( @@ -8203,6 +8122,87 @@ func Validate_TagStatus( return errs } +// Validate_TlsPassthroughRule validates an instance of TlsPassthroughRule according +// to declarative validation rules in the API schema. +func Validate_TlsPassthroughRule( + ctx context.Context, op operation.Operation, fldPath *field.Path, + obj, oldObj *ateapipb.TlsPassthroughRule) (errs field.ErrorList) { + + { // field ateapipb.TlsPassthroughRule.Hostnames + fn := func( + fldPath *field.Path, + obj, oldObj []string, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if ateDeepEqual(obj, oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.MaxItems(ctx, op, fldPath, obj, oldObj, 256).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if e := validate.RequiredSlice(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + // custom validation + if e := ValidateCustom_TlsPassthroughRule_Hostnames(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + errs = append(errs, e...) + } + // lists with set semantics require unique values + if e := validate.ValSliceUnique(ctx, op, fldPath, obj, oldObj, validate.DirectEqual); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateapipb.TlsPassthroughRule) []string { + return oldObj.Hostnames + }) + errs = append(errs, fn(fldPath.Child("hostnames"), obj.Hostnames, oldVal, oldObj != nil)...) + } + + { // field ateapipb.TlsPassthroughRule.Ports + fn := func( + fldPath *field.Path, + obj, oldObj *ateapipb.Ports, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if ateDeepEqual(obj, oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.RequiredPointer(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + // call the type's validation function + errs = append(errs, Validate_Ports(ctx, op, fldPath, obj, oldObj)...) + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateapipb.TlsPassthroughRule) *ateapipb.Ports { + return oldObj.Ports + }) + errs = append(errs, fn(fldPath.Child("ports"), obj.Ports, oldVal, oldObj != nil)...) + } + + return errs +} + // Validate_TrustBundleDataSource validates an instance of TrustBundleDataSource according // to declarative validation rules in the API schema. func Validate_TrustBundleDataSource( diff --git a/cmd/ateapi/internal/controlapi/actor.go b/cmd/ateapi/internal/controlapi/actor.go index 19740224df..36d02f4204 100644 --- a/cmd/ateapi/internal/controlapi/actor.go +++ b/cmd/ateapi/internal/controlapi/actor.go @@ -542,7 +542,7 @@ func (s *RPCService) RevertActor(ctx context.Context, req *ateapipb.RevertActorR return &ateapipb.RevertActorResponse{Actor: actor}, nil } -func (s *RPCService) MintActorJWT(ctx context.Context, req *ateapipb.MintActorJWTRequest) (*ateapipb.MintActorJWTResponse, error) { +func (s *RPCService) MintActorJwt(ctx context.Context, req *ateapipb.MintActorJwtRequest) (*ateapipb.MintActorJwtResponse, error) { if errs := apivalidation.ValidateMintActorJWTRequest(ctx, req); len(errs) > 0 { return nil, apierror.InvalidArgument("%v", errs.ToAggregate()) } @@ -591,7 +591,7 @@ func (s *RPCService) MintActorJWT(ctx context.Context, req *ateapipb.MintActorJW return nil, fmt.Errorf("while signing actor JWT: %w", err) } - return &ateapipb.MintActorJWTResponse{ + return &ateapipb.MintActorJwtResponse{ ActorJwt: actorJWT, ExpiresAt: timestamppb.New(expiresAt), }, nil diff --git a/cmd/ateapi/internal/controlapi/egress_policy_test.go b/cmd/ateapi/internal/controlapi/egress_policy_test.go index 6e782a3337..90f7777ade 100644 --- a/cmd/ateapi/internal/controlapi/egress_policy_test.go +++ b/cmd/ateapi/internal/controlapi/egress_policy_test.go @@ -65,7 +65,7 @@ func TestActorEgressPolicy(t *testing.T) { Uid: "ignored", Version: 99, }, Rules: []*ateapipb.EgressRule{{ - Http: &ateapipb.HTTPRule{ + Http: &ateapipb.HttpRule{ Hostnames: []string{"api.example.com"}, Effects: &ateapipb.HttpRuleEffects{ ReplaceHeaders: []*ateapipb.CredentialHeader{{ diff --git a/cmd/ateapi/internal/controlapi/functionaltest/actor_test.go b/cmd/ateapi/internal/controlapi/functionaltest/actor_test.go index 49f4f64b4d..abefeb01a4 100644 --- a/cmd/ateapi/internal/controlapi/functionaltest/actor_test.go +++ b/cmd/ateapi/internal/controlapi/functionaltest/actor_test.go @@ -5219,7 +5219,7 @@ func TestMintActorJWT_Success(t *testing.T) { if err != nil { t.Fatalf("CreateActor failed: %v", err) } - mintResp, err := tc.client.MintActorJWT(t.Context(), &ateapipb.MintActorJWTRequest{ + mintResp, err := tc.client.MintActorJwt(t.Context(), &ateapipb.MintActorJwtRequest{ Actor: &ateapipb.ObjectRef{ Atespace: createResp.GetMetadata().GetAtespace(), Name: createResp.GetMetadata().GetName(), @@ -5228,7 +5228,7 @@ func TestMintActorJWT_Success(t *testing.T) { ExpirationSeconds: 1800, }) if err != nil { - t.Fatalf("Error while calling MintActorJWT: %v", err) + t.Fatalf("Error while calling MintActorJwt: %v", err) } segments := strings.Split(mintResp.GetActorJwt(), ".") @@ -5258,7 +5258,7 @@ func TestMintActorJWT_Success(t *testing.T) { // assertActorJWTLifetime checks that expires_at matches the exp claim and that // the token is valid for want after it was issued. -func assertActorJWTLifetime(t *testing.T, resp *ateapipb.MintActorJWTResponse, claims actoridjwt.WireClaims, want time.Duration) { +func assertActorJWTLifetime(t *testing.T, resp *ateapipb.MintActorJwtResponse, claims actoridjwt.WireClaims, want time.Duration) { t.Helper() if got, exp := resp.GetExpiresAt().AsTime(), time.Unix(int64(claims.Expiration), 0); !got.Equal(exp) { t.Errorf("expires_at = %v, want the exp claim %v", got, exp) diff --git a/cmd/ateapi/internal/controlapi/functionaltest/egress_policy_test.go b/cmd/ateapi/internal/controlapi/functionaltest/egress_policy_test.go index 0ac4692738..65c5693b9f 100644 --- a/cmd/ateapi/internal/controlapi/functionaltest/egress_policy_test.go +++ b/cmd/ateapi/internal/controlapi/functionaltest/egress_policy_test.go @@ -49,7 +49,7 @@ func functionalEgressPolicy() *ateapipb.EgressPolicy { return &ateapipb.EgressPolicy{ Metadata: &ateapipb.ResourceMetadata{Atespace: testAtespace, Name: "default"}, Rules: []*ateapipb.EgressRule{{ - Http: &ateapipb.HTTPRule{Hostnames: []string{"api.example.com"}}, + Http: &ateapipb.HttpRule{Hostnames: []string{"api.example.com"}}, }}, } } @@ -131,7 +131,7 @@ func TestUpdateActorEgressPolicy(t *testing.T) { tc, actor := setupEgressPolicyActor(t, "ns-update-egress-policy") created := createEgressPolicy(t, tc, actor) toUpdate := proto.Clone(created).(*ateapipb.EgressPolicy) - toUpdate.Rules = []*ateapipb.EgressRule{{TlsPassthrough: &ateapipb.TLSPassthroughRule{Hostnames: []string{"*"}, Ports: &ateapipb.Ports{Numbers: []int32{443}}}}} + toUpdate.Rules = []*ateapipb.EgressRule{{TlsPassthrough: &ateapipb.TlsPassthroughRule{Hostnames: []string{"*"}, Ports: &ateapipb.Ports{Numbers: []int32{443}}}}} updated, err := tc.client.UpdateActorEgressPolicy(context.Background(), &ateapipb.UpdateActorEgressPolicyRequest{ Actor: actor, diff --git a/cmd/ateapi/internal/controlapi/workload_spec.go b/cmd/ateapi/internal/controlapi/workload_spec.go index 2c666994bf..e59021e09e 100644 --- a/cmd/ateapi/internal/controlapi/workload_spec.go +++ b/cmd/ateapi/internal/controlapi/workload_spec.go @@ -238,7 +238,7 @@ func toAteletWakeupProbe(in *ateapipb.ContainerWakeupProbe) *ateletpb.WakeupProb } out := &ateletpb.WakeupProbe{TimeoutSeconds: in.GetTimeoutSeconds()} if in.GetHttpGet() != nil { - out.HttpGet = &ateletpb.HTTPGetAction{ + out.HttpGet = &ateletpb.HttpGetAction{ Path: in.GetHttpGet().GetPath(), Port: in.GetHttpGet().GetPort(), } diff --git a/cmd/ateapi/internal/controlapi/workload_spec_test.go b/cmd/ateapi/internal/controlapi/workload_spec_test.go index b39a90bbfd..7a86971ebc 100644 --- a/cmd/ateapi/internal/controlapi/workload_spec_test.go +++ b/cmd/ateapi/internal/controlapi/workload_spec_test.go @@ -350,7 +350,7 @@ func TestWorkloadSpecFromActorTemplatePropagatesWakeupProbe(t *testing.T) { Name: "with-probe", Image: "main", WakeupProbe: &ateapipb.ContainerWakeupProbe{ - HttpGet: &ateapipb.HTTPGetAction{Path: "/health", Port: 8080}, + HttpGet: &ateapipb.HttpGetAction{Path: "/health", Port: 8080}, TimeoutSeconds: 45, }, }, @@ -370,7 +370,7 @@ func TestWorkloadSpecFromActorTemplatePropagatesWakeupProbe(t *testing.T) { Name: "with-probe", Image: "main", WakeupProbe: &ateletpb.WakeupProbe{ - HttpGet: &ateletpb.HTTPGetAction{Path: "/health", Port: 8080}, + HttpGet: &ateletpb.HttpGetAction{Path: "/health", Port: 8080}, TimeoutSeconds: 45, }, }, diff --git a/cmd/ateapi/internal/defaults/defaults_test.go b/cmd/ateapi/internal/defaults/defaults_test.go index 5a4c90042c..0aa3646204 100644 --- a/cmd/ateapi/internal/defaults/defaults_test.go +++ b/cmd/ateapi/internal/defaults/defaults_test.go @@ -64,11 +64,11 @@ func TestApply(t *testing.T) { }, { name: "wakeup probe gets timeout and path", in: &ateapipb.ActorTemplate{Containers: []*ateapipb.Container{ - {Name: "main", WakeupProbe: &ateapipb.ContainerWakeupProbe{HttpGet: &ateapipb.HTTPGetAction{Port: 8080}}}, + {Name: "main", WakeupProbe: &ateapipb.ContainerWakeupProbe{HttpGet: &ateapipb.HttpGetAction{Port: 8080}}}, }}, want: &ateapipb.ActorTemplate{Containers: []*ateapipb.Container{ {Name: "main", WakeupProbe: &ateapipb.ContainerWakeupProbe{ - HttpGet: &ateapipb.HTTPGetAction{Port: 8080, Path: "/"}, + HttpGet: &ateapipb.HttpGetAction{Port: 8080, Path: "/"}, TimeoutSeconds: 30, }}, }}, @@ -84,27 +84,27 @@ func TestApply(t *testing.T) { name: "set wakeup probe fields are kept", in: &ateapipb.ActorTemplate{Containers: []*ateapipb.Container{ {Name: "main", WakeupProbe: &ateapipb.ContainerWakeupProbe{ - HttpGet: &ateapipb.HTTPGetAction{Port: 8080, Path: "/healthz"}, + HttpGet: &ateapipb.HttpGetAction{Port: 8080, Path: "/healthz"}, TimeoutSeconds: 5, }}, }}, want: &ateapipb.ActorTemplate{Containers: []*ateapipb.Container{ {Name: "main", WakeupProbe: &ateapipb.ContainerWakeupProbe{ - HttpGet: &ateapipb.HTTPGetAction{Port: 8080, Path: "/healthz"}, + HttpGet: &ateapipb.HttpGetAction{Port: 8080, Path: "/healthz"}, TimeoutSeconds: 5, }}, }}, }, { name: "every container is defaulted", in: &ateapipb.ActorTemplate{Containers: []*ateapipb.Container{ - {Name: "a", WakeupProbe: &ateapipb.ContainerWakeupProbe{HttpGet: &ateapipb.HTTPGetAction{Port: 1}}}, + {Name: "a", WakeupProbe: &ateapipb.ContainerWakeupProbe{HttpGet: &ateapipb.HttpGetAction{Port: 1}}}, {Name: "b"}, - {Name: "c", WakeupProbe: &ateapipb.ContainerWakeupProbe{HttpGet: &ateapipb.HTTPGetAction{Port: 3}}}, + {Name: "c", WakeupProbe: &ateapipb.ContainerWakeupProbe{HttpGet: &ateapipb.HttpGetAction{Port: 3}}}, }}, want: &ateapipb.ActorTemplate{Containers: []*ateapipb.Container{ - {Name: "a", WakeupProbe: &ateapipb.ContainerWakeupProbe{HttpGet: &ateapipb.HTTPGetAction{Port: 1, Path: "/"}, TimeoutSeconds: 30}}, + {Name: "a", WakeupProbe: &ateapipb.ContainerWakeupProbe{HttpGet: &ateapipb.HttpGetAction{Port: 1, Path: "/"}, TimeoutSeconds: 30}}, {Name: "b"}, - {Name: "c", WakeupProbe: &ateapipb.ContainerWakeupProbe{HttpGet: &ateapipb.HTTPGetAction{Port: 3, Path: "/"}, TimeoutSeconds: 30}}, + {Name: "c", WakeupProbe: &ateapipb.ContainerWakeupProbe{HttpGet: &ateapipb.HttpGetAction{Port: 3, Path: "/"}, TimeoutSeconds: 30}}, }}, }, { name: "actor has no defaults", @@ -117,17 +117,17 @@ func TestApply(t *testing.T) { }, { name: "egress policy defaults http and https ports only", in: &ateapipb.EgressPolicy{Rules: []*ateapipb.EgressRule{ - {Http: &ateapipb.HTTPRule{Hostnames: []string{"a.example.com"}}}, - {Https: &ateapipb.HTTPSRule{Hostnames: []string{"b.example.com"}}}, - {Http: &ateapipb.HTTPRule{Hostnames: []string{"c.example.com"}, Ports: &ateapipb.Ports{All: &ateapipb.AllPorts{}}}}, - {TlsPassthrough: &ateapipb.TLSPassthroughRule{Hostnames: []string{"d.example.com"}}}, + {Http: &ateapipb.HttpRule{Hostnames: []string{"a.example.com"}}}, + {Https: &ateapipb.HttpsRule{Hostnames: []string{"b.example.com"}}}, + {Http: &ateapipb.HttpRule{Hostnames: []string{"c.example.com"}, Ports: &ateapipb.Ports{All: &ateapipb.AllPorts{}}}}, + {TlsPassthrough: &ateapipb.TlsPassthroughRule{Hostnames: []string{"d.example.com"}}}, {}, }}, want: &ateapipb.EgressPolicy{Rules: []*ateapipb.EgressRule{ - {Http: &ateapipb.HTTPRule{Hostnames: []string{"a.example.com"}, Ports: &ateapipb.Ports{Numbers: []int32{80}}}}, - {Https: &ateapipb.HTTPSRule{Hostnames: []string{"b.example.com"}, Ports: &ateapipb.Ports{Numbers: []int32{443}}}}, - {Http: &ateapipb.HTTPRule{Hostnames: []string{"c.example.com"}, Ports: &ateapipb.Ports{All: &ateapipb.AllPorts{}}}}, - {TlsPassthrough: &ateapipb.TLSPassthroughRule{Hostnames: []string{"d.example.com"}}}, + {Http: &ateapipb.HttpRule{Hostnames: []string{"a.example.com"}, Ports: &ateapipb.Ports{Numbers: []int32{80}}}}, + {Https: &ateapipb.HttpsRule{Hostnames: []string{"b.example.com"}, Ports: &ateapipb.Ports{Numbers: []int32{443}}}}, + {Http: &ateapipb.HttpRule{Hostnames: []string{"c.example.com"}, Ports: &ateapipb.Ports{All: &ateapipb.AllPorts{}}}}, + {TlsPassthrough: &ateapipb.TlsPassthroughRule{Hostnames: []string{"d.example.com"}}}, {}, }}, }, { diff --git a/cmd/ateapi/internal/store/storecontract/contract.go b/cmd/ateapi/internal/store/storecontract/contract.go index f356967998..c573db9eef 100644 --- a/cmd/ateapi/internal/store/storecontract/contract.go +++ b/cmd/ateapi/internal/store/storecontract/contract.go @@ -232,7 +232,7 @@ func runEgressPolicyContractTests(t *testing.T, setup func(t *testing.T) store.I policy := &ateapipb.EgressPolicy{ Metadata: &ateapipb.ResourceMetadata{Atespace: testAtespace, Name: "default"}, Rules: []*ateapipb.EgressRule{{ - Http: &ateapipb.HTTPRule{Hostnames: []string{"api.example.com"}, Ports: &ateapipb.Ports{Numbers: []int32{80}}}, + Http: &ateapipb.HttpRule{Hostnames: []string{"api.example.com"}, Ports: &ateapipb.Ports{Numbers: []int32{80}}}, }}, } @@ -262,7 +262,7 @@ func runEgressPolicyContractTests(t *testing.T, setup func(t *testing.T) store.I updated, err := s.UpdateEgressPolicy(ctx, actorRef, store.PreconditionFrom(created), func(policy *ateapipb.EgressPolicy) error { policy.Metadata.Atespace = "other" policy.Metadata.Name = "other" - policy.Rules = []*ateapipb.EgressRule{{TlsPassthrough: &ateapipb.TLSPassthroughRule{Hostnames: []string{"*"}, Ports: &ateapipb.Ports{Numbers: []int32{443}}}}} + policy.Rules = []*ateapipb.EgressRule{{TlsPassthrough: &ateapipb.TlsPassthroughRule{Hostnames: []string{"*"}, Ports: &ateapipb.Ports{Numbers: []int32{443}}}}} return nil }) if err != nil || updated.GetMetadata().GetAtespace() != testAtespace || updated.GetMetadata().GetName() != "default" || updated.GetMetadata().GetVersion() != 2 || updated.GetMetadata().GetUid() != created.GetMetadata().GetUid() { @@ -3225,7 +3225,7 @@ func runUnknownFieldContractTests(t *testing.T, setup func(t *testing.T) store.I created, err := s.CreateEgressPolicy(ctx, ref, withUnknownField(&ateapipb.EgressPolicy{ Metadata: &ateapipb.ResourceMetadata{Atespace: testAtespace, Name: "default"}, Rules: []*ateapipb.EgressRule{withUnknownField(&ateapipb.EgressRule{ - Http: &ateapipb.HTTPRule{Hostnames: []string{"api.example.com"}, Ports: &ateapipb.Ports{Numbers: []int32{80}}}, + Http: &ateapipb.HttpRule{Hostnames: []string{"api.example.com"}, Ports: &ateapipb.Ports{Numbers: []int32{80}}}, })}, })) if err != nil { diff --git a/cmd/atelet/main.go b/cmd/atelet/main.go index e7a2859a92..c6e63d32b9 100644 --- a/cmd/atelet/main.go +++ b/cmd/atelet/main.go @@ -1648,7 +1648,7 @@ func toAteomWakeupProbe(in *ateletpb.WakeupProbe) *ateompb.WakeupProbe { } out := &ateompb.WakeupProbe{} if hg := in.GetHttpGet(); hg != nil { - out.HttpGet = &ateompb.HTTPGetAction{ + out.HttpGet = &ateompb.HttpGetAction{ Path: hg.GetPath(), Port: hg.GetPort(), } diff --git a/cmd/atelet/main_test.go b/cmd/atelet/main_test.go index 1cbb89367a..2fe768baf1 100644 --- a/cmd/atelet/main_test.go +++ b/cmd/atelet/main_test.go @@ -964,7 +964,7 @@ func TestBuildAteomWorkloadSpecForwardsWakeupProbe(t *testing.T) { Name: "with-probe", Image: "main", WakeupProbe: &ateletpb.WakeupProbe{ - HttpGet: &ateletpb.HTTPGetAction{Path: "/health", Port: 8080}, + HttpGet: &ateletpb.HttpGetAction{Path: "/health", Port: 8080}, TimeoutSeconds: 45, }, }, @@ -978,7 +978,7 @@ func TestBuildAteomWorkloadSpecForwardsWakeupProbe(t *testing.T) { { Name: "with-probe", WakeupProbe: &ateompb.WakeupProbe{ - HttpGet: &ateompb.HTTPGetAction{Path: "/health", Port: 8080}, + HttpGet: &ateompb.HttpGetAction{Path: "/health", Port: 8080}, TimeoutSeconds: 45, }, }, diff --git a/cmd/atenet/internal/router/egress/actorjwt/actorjwt.go b/cmd/atenet/internal/router/egress/actorjwt/actorjwt.go index 3c65c0783e..b3c6022ad5 100644 --- a/cmd/atenet/internal/router/egress/actorjwt/actorjwt.go +++ b/cmd/atenet/internal/router/egress/actorjwt/actorjwt.go @@ -46,9 +46,9 @@ func New(client ateapipb.ControlClient) *Minter { } // Token returns a JWT for the actor ref names, bound to src's audiences and -// lifetime. MintActorJWT errors are returned wrapped. -func (m *Minter) Token(ctx context.Context, ref resources.ActorRef, src *ateapipb.ActorJWTSource) (string, error) { - req := &ateapipb.MintActorJWTRequest{ +// lifetime. MintActorJwt errors are returned wrapped. +func (m *Minter) Token(ctx context.Context, ref resources.ActorRef, src *ateapipb.ActorJwtSource) (string, error) { + req := &ateapipb.MintActorJwtRequest{ Actor: ref.ToObjectRef(), Audiences: slices.Sorted(slices.Values(src.GetAudiences())), ExpirationSeconds: src.GetExpirationSeconds(), @@ -80,11 +80,11 @@ func (m *Minter) Token(ctx context.Context, ref resources.ActorRef, src *ateapip // mint sends req and caches the token under key, unless a flight that just // finished has already stored one. -func (m *Minter) mint(ctx context.Context, key string, ref resources.ActorRef, req *ateapipb.MintActorJWTRequest) (string, error) { +func (m *Minter) mint(ctx context.Context, key string, ref resources.ActorRef, req *ateapipb.MintActorJwtRequest) (string, error) { if jwt, ok := m.cached(key); ok { return jwt, nil } - resp, err := m.client.MintActorJWT(ctx, req) + resp, err := m.client.MintActorJwt(ctx, req) if err != nil { return "", fmt.Errorf("minting an actor JWT for %s: %w", ref, err) } @@ -101,7 +101,7 @@ func (m *Minter) cached(key string) (string, bool) { // tokenKey identifies the token req mints by req's own encoding. Deterministic // encoding is stable within one binary, which is all an in-process cache needs. -func tokenKey(req *ateapipb.MintActorJWTRequest) (string, error) { +func tokenKey(req *ateapipb.MintActorJwtRequest) (string, error) { b, err := proto.MarshalOptions{Deterministic: true}.Marshal(req) if err != nil { return "", fmt.Errorf("encoding the token key: %w", err) diff --git a/cmd/atenet/internal/router/egress/actorjwt/actorjwt_test.go b/cmd/atenet/internal/router/egress/actorjwt/actorjwt_test.go index 71a79dfc73..026c9e0afc 100644 --- a/cmd/atenet/internal/router/egress/actorjwt/actorjwt_test.go +++ b/cmd/atenet/internal/router/egress/actorjwt/actorjwt_test.go @@ -39,18 +39,18 @@ import ( var testActor = resources.ActorRef{Atespace: "default", Name: "my-actor"} -// fakeControl's nth MintActorJWT call returns "jwt-", expiring +// fakeControl's nth MintActorJwt call returns "jwt-", expiring // expiration_seconds from now. err, when set, is returned instead, and gate, // when non-nil, blocks each call until it is closed. type fakeControl struct { ateapipb.UnimplementedControlServer calls atomic.Int32 - last atomic.Pointer[ateapipb.MintActorJWTRequest] + last atomic.Pointer[ateapipb.MintActorJwtRequest] err error gate chan struct{} } -func (f *fakeControl) MintActorJWT(ctx context.Context, req *ateapipb.MintActorJWTRequest) (*ateapipb.MintActorJWTResponse, error) { +func (f *fakeControl) MintActorJwt(ctx context.Context, req *ateapipb.MintActorJwtRequest) (*ateapipb.MintActorJwtResponse, error) { n := f.calls.Add(1) f.last.Store(req) if f.gate != nil { @@ -63,7 +63,7 @@ func (f *fakeControl) MintActorJWT(ctx context.Context, req *ateapipb.MintActorJ if f.err != nil { return nil, f.err } - return &ateapipb.MintActorJWTResponse{ + return &ateapipb.MintActorJwtResponse{ ActorJwt: fmt.Sprintf("jwt-%d", n), ExpiresAt: timestamppb.New(time.Now().Add(time.Duration(req.GetExpirationSeconds()) * time.Second)), }, nil @@ -89,12 +89,12 @@ func newMinter(t *testing.T, ctl *fakeControl) *Minter { return New(ateapipb.NewControlClient(conn)) } -func jwtSource(lifetime int64, audiences ...string) *ateapipb.ActorJWTSource { - return &ateapipb.ActorJWTSource{Audiences: audiences, ExpirationSeconds: lifetime} +func jwtSource(lifetime int64, audiences ...string) *ateapipb.ActorJwtSource { + return &ateapipb.ActorJwtSource{Audiences: audiences, ExpirationSeconds: lifetime} } // wantToken calls m.Token and fails the test unless it returns want. -func wantToken(t *testing.T, m *Minter, ref resources.ActorRef, src *ateapipb.ActorJWTSource, want string) { +func wantToken(t *testing.T, m *Minter, ref resources.ActorRef, src *ateapipb.ActorJwtSource, want string) { t.Helper() got, err := m.Token(context.Background(), ref, src) if err != nil { @@ -112,13 +112,13 @@ func TestReuseTokenUntilAThirdOfItsLifetimeRemains(t *testing.T) { src := jwtSource(900, "https://b.example", "https://a.example") wantToken(t, m, testActor, src, "jwt-1") - want := &ateapipb.MintActorJWTRequest{ + want := &ateapipb.MintActorJwtRequest{ Actor: &ateapipb.ObjectRef{Atespace: "default", Name: "my-actor"}, Audiences: []string{"https://a.example", "https://b.example"}, ExpirationSeconds: 900, } if got := ctl.last.Load(); !proto.Equal(got, want) { - t.Errorf("MintActorJWT request = %v, want %v", got, want) + t.Errorf("MintActorJwt request = %v, want %v", got, want) } time.Sleep(600*time.Second - time.Nanosecond) @@ -146,7 +146,7 @@ func TestDoNotCacheErrors(t *testing.T) { src := jwtSource(900, "a") if _, err := m.Token(context.Background(), testActor, src); status.Code(err) != codes.Unavailable { - t.Fatalf("Token = %v, want the Unavailable MintActorJWT error", err) + t.Fatalf("Token = %v, want the Unavailable MintActorJwt error", err) } ctl.err = nil wantToken(t, m, testActor, src, "jwt-2") @@ -170,7 +170,7 @@ func TestCollapseConcurrentMints(t *testing.T) { close(ctl.gate) wg.Wait() if calls := ctl.calls.Load(); calls != 1 { - t.Errorf("MintActorJWT calls = %d, want 1 for %d concurrent callers", calls, callers) + t.Errorf("MintActorJwt calls = %d, want 1 for %d concurrent callers", calls, callers) } }) } @@ -198,7 +198,7 @@ func TestMintOutlivesCanceledCaller(t *testing.T) { synctest.Wait() wantToken(t, m, testActor, jwtSource(900, "a"), "jwt-1") if calls := ctl.calls.Load(); calls != 1 { - t.Errorf("MintActorJWT calls = %d, want 1", calls) + t.Errorf("MintActorJwt calls = %d, want 1", calls) } }) } diff --git a/cmd/atenet/internal/router/egress/credentials.go b/cmd/atenet/internal/router/egress/credentials.go index 733ba95474..996c361335 100644 --- a/cmd/atenet/internal/router/egress/credentials.go +++ b/cmd/atenet/internal/router/egress/credentials.go @@ -119,7 +119,7 @@ func (h *Handler) applyEffects(ctx context.Context, ref resources.ActorRef, dest // actorJWT mints the JWT src asks for. Every error it returns is already a // client-facing denial. -func (h *Handler) actorJWT(ctx context.Context, ref resources.ActorRef, dest egresspolicy.Destination, src *ateapipb.ActorJWTSource) ([]byte, error) { +func (h *Handler) actorJWT(ctx context.Context, ref resources.ActorRef, dest egresspolicy.Destination, src *ateapipb.ActorJwtSource) ([]byte, error) { jwt, err := h.actorJWTs.Token(ctx, ref, src) if err != nil { slog.ErrorContext(ctx, "egress denied: actor JWT mint failed", diff --git a/cmd/atenet/internal/router/egress/credentials_test.go b/cmd/atenet/internal/router/egress/credentials_test.go index 49630191c9..57fbcb09bd 100644 --- a/cmd/atenet/internal/router/egress/credentials_test.go +++ b/cmd/atenet/internal/router/egress/credentials_test.go @@ -114,11 +114,11 @@ func TestInjectionOnRequestLegs(t *testing.T) { // A request that does not carry the header a rule replaces goes out unchanged: // nothing is fetched, added, or denied, even where injecting would fail. func TestInjectionSkippedWithoutHeader(t *testing.T) { - jwtPolicy := &ateapipb.EgressPolicy{Rules: []*ateapipb.EgressRule{{Https: &ateapipb.HTTPSRule{ + jwtPolicy := &ateapipb.EgressPolicy{Rules: []*ateapipb.EgressRule{{Https: &ateapipb.HttpsRule{ Hostnames: []string{"api.example.com"}, Effects: &ateapipb.HttpRuleEffects{ReplaceHeaders: []*ateapipb.CredentialHeader{{ Header: "authorization", - ActorJwt: &ateapipb.ActorJWTSource{Audiences: []string{"https://api.example.com"}}, + ActorJwt: &ateapipb.ActorJwtSource{Audiences: []string{"https://api.example.com"}}, }}}, }}}} for _, tc := range []struct { @@ -154,7 +154,7 @@ func TestInjectionSkippedWithoutHeader(t *testing.T) { // Of a rule's replacements, only those whose header the request carries are // applied, matching the header name case-insensitively. func TestInjectionReplacesOnlyCarriedHeaders(t *testing.T) { - policy := &ateapipb.EgressPolicy{Rules: []*ateapipb.EgressRule{{Https: &ateapipb.HTTPSRule{ + policy := &ateapipb.EgressPolicy{Rules: []*ateapipb.EgressRule{{Https: &ateapipb.HttpsRule{ Hostnames: []string{"api.example.com"}, Effects: &ateapipb.HttpRuleEffects{ReplaceHeaders: []*ateapipb.CredentialHeader{ {Header: "authorization", Prefix: "Bearer ", CredentialUri: "ate-secret://k8s/default/token"}, @@ -259,7 +259,7 @@ func actorJWTHeader(header string) *ateapipb.CredentialHeader { return &ateapipb.CredentialHeader{ Header: header, Prefix: "Bearer ", - ActorJwt: &ateapipb.ActorJWTSource{Audiences: []string{"https://api.example.com"}, ExpirationSeconds: 900}, + ActorJwt: &ateapipb.ActorJwtSource{Audiences: []string{"https://api.example.com"}, ExpirationSeconds: 900}, } } @@ -270,9 +270,9 @@ func replaceHeadersPolicy(leg string, entries ...*ateapipb.CredentialHeader) *at hosts := []string{"api.example.com"} effects := &ateapipb.HttpRuleEffects{ReplaceHeaders: entries} if leg == extproc.EgressCleartextFilterChainName { - return &ateapipb.EgressPolicy{Rules: []*ateapipb.EgressRule{{Http: &ateapipb.HTTPRule{Hostnames: hosts, Effects: effects}}}} + return &ateapipb.EgressPolicy{Rules: []*ateapipb.EgressRule{{Http: &ateapipb.HttpRule{Hostnames: hosts, Effects: effects}}}} } - return &ateapipb.EgressPolicy{Rules: []*ateapipb.EgressRule{{Https: &ateapipb.HTTPSRule{Hostnames: hosts, Effects: effects}}}} + return &ateapipb.EgressPolicy{Rules: []*ateapipb.EgressRule{{Https: &ateapipb.HttpsRule{Hostnames: hosts, Effects: effects}}}} } var requestLegs = []string{extproc.EgressTLSMITMFilterChainName, extproc.EgressCleartextFilterChainName} @@ -308,13 +308,13 @@ func TestActorJWTInjection(t *testing.T) { if h0.GetAppendAction() != corev3.HeaderValueOption_OVERWRITE_IF_EXISTS_OR_ADD { t.Errorf("append action = %v, want OVERWRITE_IF_EXISTS_OR_ADD", h0.GetAppendAction()) } - want := &ateapipb.MintActorJWTRequest{ + want := &ateapipb.MintActorJwtRequest{ Actor: &ateapipb.ObjectRef{Atespace: testEgressAtespace, Name: testEgressActor}, Audiences: []string{"https://api.example.com"}, ExpirationSeconds: 900, } if got := client.lastMint.Load(); !proto.Equal(got, want) { - t.Errorf("MintActorJWT request = %v, want %v", got, want) + t.Errorf("MintActorJwt request = %v, want %v", got, want) } }) } @@ -332,7 +332,7 @@ func TestActorJWTInjectionSkippedWithoutHeader(t *testing.T) { t.Errorf("got %d injected headers, want 0", len(got)) } if calls := client.mintCalls.Load(); calls != 0 { - t.Errorf("MintActorJWT calls = %d, want 0", calls) + t.Errorf("MintActorJwt calls = %d, want 0", calls) } } diff --git a/cmd/atenet/internal/router/egress/egress_test.go b/cmd/atenet/internal/router/egress/egress_test.go index 1b1febf633..41ffa029c0 100644 --- a/cmd/atenet/internal/router/egress/egress_test.go +++ b/cmd/atenet/internal/router/egress/egress_test.go @@ -185,10 +185,10 @@ type egressMockClient struct { // closed, so a test can hold several callers on one fetch. policyGate chan struct{} - // The nth MintActorJWT call returns "jwt-", expiring + // The nth MintActorJwt call returns "jwt-", expiring // expiration_seconds from now. mintErr, when set, is returned instead. mintCalls atomic.Int32 - lastMint atomic.Pointer[ateapipb.MintActorJWTRequest] + lastMint atomic.Pointer[ateapipb.MintActorJwtRequest] mintErr error } @@ -220,13 +220,13 @@ func (m *egressMockClient) GetActorEgressPolicy(ctx context.Context, req *ateapi return m.policy, nil } -func (m *egressMockClient) MintActorJWT(_ context.Context, req *ateapipb.MintActorJWTRequest, _ ...grpc.CallOption) (*ateapipb.MintActorJWTResponse, error) { +func (m *egressMockClient) MintActorJwt(_ context.Context, req *ateapipb.MintActorJwtRequest, _ ...grpc.CallOption) (*ateapipb.MintActorJwtResponse, error) { n := m.mintCalls.Add(1) m.lastMint.Store(req) if m.mintErr != nil { return nil, m.mintErr } - return &ateapipb.MintActorJWTResponse{ + return &ateapipb.MintActorJwtResponse{ ActorJwt: fmt.Sprintf("jwt-%d", n), ExpiresAt: timestamppb.New(time.Now().Add(time.Duration(req.GetExpirationSeconds()) * time.Second)), }, nil @@ -240,31 +240,31 @@ func allowAllPolicy() *ateapipb.EgressPolicy { func httpPolicy(patterns ...string) *ateapipb.EgressPolicy { return &ateapipb.EgressPolicy{Rules: []*ateapipb.EgressRule{{ - Http: &ateapipb.HTTPRule{Hostnames: patterns}, + Http: &ateapipb.HttpRule{Hostnames: patterns}, }}} } func httpPolicyOnPorts(ports *ateapipb.Ports, patterns ...string) *ateapipb.EgressPolicy { return &ateapipb.EgressPolicy{Rules: []*ateapipb.EgressRule{{ - Http: &ateapipb.HTTPRule{Hostnames: patterns, Ports: ports}, + Http: &ateapipb.HttpRule{Hostnames: patterns, Ports: ports}, }}} } func httpsPolicy(patterns ...string) *ateapipb.EgressPolicy { return &ateapipb.EgressPolicy{Rules: []*ateapipb.EgressRule{{ - Https: &ateapipb.HTTPSRule{Hostnames: patterns}, + Https: &ateapipb.HttpsRule{Hostnames: patterns}, }}} } func httpsPolicyOnPorts(ports *ateapipb.Ports, patterns ...string) *ateapipb.EgressPolicy { return &ateapipb.EgressPolicy{Rules: []*ateapipb.EgressRule{{ - Https: &ateapipb.HTTPSRule{Hostnames: patterns, Ports: ports}, + Https: &ateapipb.HttpsRule{Hostnames: patterns, Ports: ports}, }}} } func passthroughPolicy(ports *ateapipb.Ports, patterns ...string) *ateapipb.EgressPolicy { return &ateapipb.EgressPolicy{Rules: []*ateapipb.EgressRule{{ - TlsPassthrough: &ateapipb.TLSPassthroughRule{Hostnames: patterns, Ports: ports}, + TlsPassthrough: &ateapipb.TlsPassthroughRule{Hostnames: patterns, Ports: ports}, }}} } diff --git a/cmd/atenet/internal/router/egress/request_test.go b/cmd/atenet/internal/router/egress/request_test.go index 43eed50ebd..505472f4c9 100644 --- a/cmd/atenet/internal/router/egress/request_test.go +++ b/cmd/atenet/internal/router/egress/request_test.go @@ -52,14 +52,14 @@ func sampleEffects() *ateapipb.HttpRuleEffects { // the authorization header, which only the MITM leg can honor. func credentialInjectionPolicySample(pattern string) *ateapipb.EgressPolicy { return &ateapipb.EgressPolicy{Rules: []*ateapipb.EgressRule{{ - Https: &ateapipb.HTTPSRule{Hostnames: []string{pattern}, Effects: sampleEffects()}, + Https: &ateapipb.HttpsRule{Hostnames: []string{pattern}, Effects: sampleEffects()}, }}} } // cleartextInjectionPolicy is the same replacement on an http rule. func cleartextInjectionPolicy(pattern string) *ateapipb.EgressPolicy { return &ateapipb.EgressPolicy{Rules: []*ateapipb.EgressRule{{ - Http: &ateapipb.HTTPRule{Hostnames: []string{pattern}, Effects: sampleEffects()}, + Http: &ateapipb.HttpRule{Hostnames: []string{pattern}, Effects: sampleEffects()}, }}} } diff --git a/cmd/kubectl-ate/internal/cmd/actortemplate_test.go b/cmd/kubectl-ate/internal/cmd/actortemplate_test.go index b4ffe73c56..fe48767df8 100644 --- a/cmd/kubectl-ate/internal/cmd/actortemplate_test.go +++ b/cmd/kubectl-ate/internal/cmd/actortemplate_test.go @@ -81,7 +81,7 @@ func TestActorTemplateFromManifest(t *testing.T) { Image: "ko://github.com/agent-substrate/substrate/demos/counter", Command: []string{"/ko-app/counter", "--extra-port=9090"}, WakeupProbe: &ateapipb.ContainerWakeupProbe{ - HttpGet: &ateapipb.HTTPGetAction{Path: "/readyz", Port: 80}, + HttpGet: &ateapipb.HttpGetAction{Path: "/readyz", Port: 80}, }, VolumeMounts: []*ateapipb.VolumeMount{{Name: "data", MountPath: "/home/counter"}}, }}, diff --git a/cmd/kubectl-ate/internal/cmd/egress_policy_test.go b/cmd/kubectl-ate/internal/cmd/egress_policy_test.go index d736701d1d..6871aa919a 100644 --- a/cmd/kubectl-ate/internal/cmd/egress_policy_test.go +++ b/cmd/kubectl-ate/internal/cmd/egress_policy_test.go @@ -39,9 +39,9 @@ func TestEgressPolicyFromManifest(t *testing.T) { t.Parallel() fullMetadata := &ateapipb.ResourceMetadata{Atespace: "team-a", Name: "default"} - httpRules := []*ateapipb.EgressRule{{Http: &ateapipb.HTTPRule{Hostnames: []string{"api.example.com"}}}} - tlsRules := []*ateapipb.EgressRule{{TlsPassthrough: &ateapipb.TLSPassthroughRule{Hostnames: []string{"*.example.com"}, Ports: &ateapipb.Ports{Numbers: []int32{443}}}}} - anyRules := []*ateapipb.EgressRule{{Http: &ateapipb.HTTPRule{Hostnames: []string{"*"}}}} + httpRules := []*ateapipb.EgressRule{{Http: &ateapipb.HttpRule{Hostnames: []string{"api.example.com"}}}} + tlsRules := []*ateapipb.EgressRule{{TlsPassthrough: &ateapipb.TlsPassthroughRule{Hostnames: []string{"*.example.com"}, Ports: &ateapipb.Ports{Numbers: []int32{443}}}}} + anyRules := []*ateapipb.EgressRule{{Http: &ateapipb.HttpRule{Hostnames: []string{"*"}}}} tests := []struct { name string @@ -283,7 +283,7 @@ rules: func TestOverrideEgressPolicyMetadata(t *testing.T) { t.Parallel() - rules := []*ateapipb.EgressRule{{Http: &ateapipb.HTTPRule{Hostnames: []string{"*"}}}} + rules := []*ateapipb.EgressRule{{Http: &ateapipb.HttpRule{Hostnames: []string{"*"}}}} filled := &ateapipb.EgressPolicy{Metadata: &ateapipb.ResourceMetadata{Atespace: "team-a", Name: "default"}, Rules: rules} pinned := &ateapipb.ResourceMetadata{Atespace: "team-a", Name: "default", Uid: "3f2b1c0e-8d5a-4b6e-9c1d-2a7e4f6b8c0d", Version: 2} @@ -365,11 +365,11 @@ func TestEgressPolicyManifest_RoundTrip(t *testing.T) { UpdateTime: timestamppb.New(time.Date(2026, 1, 1, 12, 0, 0, 0, time.UTC)), }, Rules: []*ateapipb.EgressRule{ - {Http: &ateapipb.HTTPRule{Hostnames: []string{"*.example.com"}, Ports: &ateapipb.Ports{Numbers: []int32{80, 8080}}}}, - {Https: &ateapipb.HTTPSRule{Hostnames: []string{"api.example.com"}, Effects: &ateapipb.HttpRuleEffects{ + {Http: &ateapipb.HttpRule{Hostnames: []string{"*.example.com"}, Ports: &ateapipb.Ports{Numbers: []int32{80, 8080}}}}, + {Https: &ateapipb.HttpsRule{Hostnames: []string{"api.example.com"}, Effects: &ateapipb.HttpRuleEffects{ ReplaceHeaders: []*ateapipb.CredentialHeader{{Header: "authorization", Prefix: "Bearer ", CredentialUri: "ate-secret://k8s/default/token"}}, }}}, - {TlsPassthrough: &ateapipb.TLSPassthroughRule{Hostnames: []string{"*"}, Ports: &ateapipb.Ports{All: &ateapipb.AllPorts{}}}}, + {TlsPassthrough: &ateapipb.TlsPassthroughRule{Hostnames: []string{"*"}, Ports: &ateapipb.Ports{All: &ateapipb.AllPorts{}}}}, }, } @@ -496,7 +496,7 @@ func TestGetEgressPolicyRunner_Run(t *testing.T) { Version: 1, CreateTime: timestamppb.New(now.Add(-5 * time.Minute)), // table row prints AGE 5m }, - Rules: []*ateapipb.EgressRule{{Http: &ateapipb.HTTPRule{Hostnames: []string{"api.example.com"}}}}, + Rules: []*ateapipb.EgressRule{{Http: &ateapipb.HttpRule{Hostnames: []string{"api.example.com"}}}}, } tests := []struct { @@ -623,7 +623,7 @@ func TestCreateEgressPolicyRunner_Run(t *testing.T) { pinTime(t, now) actor := &ateapipb.ObjectRef{Atespace: "team-a", Name: "c1"} - rules := []*ateapipb.EgressRule{{Http: &ateapipb.HTTPRule{Hostnames: []string{"api.example.com"}}}} + rules := []*ateapipb.EgressRule{{Http: &ateapipb.HttpRule{Hostnames: []string{"api.example.com"}}}} // A manifest cloned from another actor still carries that actor's // server-managed fields; the CLI sends them as is and the server scrubs them. manifest := &ateapipb.EgressPolicy{ @@ -739,8 +739,8 @@ func TestUpdateEgressPolicyRunner_Run(t *testing.T) { actor := &ateapipb.ObjectRef{Atespace: "team-a", Name: "c1"} const uid = "3f2b1c0e-8d5a-4b6e-9c1d-2a7e4f6b8c0d" rules := []*ateapipb.EgressRule{ - {Http: &ateapipb.HTTPRule{Hostnames: []string{"api.example.com"}}}, - {Https: &ateapipb.HTTPSRule{Hostnames: []string{"www.example.com"}}}, + {Http: &ateapipb.HttpRule{Hostnames: []string{"api.example.com"}}}, + {Https: &ateapipb.HttpsRule{Hostnames: []string{"www.example.com"}}}, } // The manifest is what `get -o yaml` printed, edited: it still carries the // uid, version, and timestamps of the policy being replaced. @@ -967,7 +967,7 @@ func TestDeleteEgressPolicyRunner_Run(t *testing.T) { actor := &ateapipb.ObjectRef{Atespace: "team-a", Name: "c1"} deleted := &ateapipb.EgressPolicy{ Metadata: &ateapipb.ResourceMetadata{Atespace: "team-a", Name: "default", Version: 1}, - Rules: []*ateapipb.EgressRule{{Http: &ateapipb.HTTPRule{Hostnames: []string{"api.example.com"}}}}, + Rules: []*ateapipb.EgressRule{{Http: &ateapipb.HttpRule{Hostnames: []string{"api.example.com"}}}}, } unguarded := &ateapipb.DeleteActorEgressPolicyRequest{Actor: actor, Name: resources.SingletonName} wantActorReq := &ateapipb.GetActorRequest{Actor: actor} @@ -1103,19 +1103,19 @@ func TestEgressPolicyFromManifest_NighthawkEgress(t *testing.T) { }, Rules: []*ateapipb.EgressRule{ { - Http: &ateapipb.HTTPRule{ + Http: &ateapipb.HttpRule{ Hostnames: []string{"*"}, Ports: &ateapipb.Ports{Numbers: []int32{80}}, }, }, { - Https: &ateapipb.HTTPSRule{ + Https: &ateapipb.HttpsRule{ Hostnames: []string{"*"}, Ports: &ateapipb.Ports{Numbers: []int32{443}}, }, }, { - TlsPassthrough: &ateapipb.TLSPassthroughRule{ + TlsPassthrough: &ateapipb.TlsPassthroughRule{ Hostnames: []string{"*"}, Ports: &ateapipb.Ports{Numbers: []int32{8443}}, }, diff --git a/cmd/kubectl-ate/internal/printer/egress_policy_test.go b/cmd/kubectl-ate/internal/printer/egress_policy_test.go index a209bf5cba..58c1c2b97d 100644 --- a/cmd/kubectl-ate/internal/printer/egress_policy_test.go +++ b/cmd/kubectl-ate/internal/printer/egress_policy_test.go @@ -37,8 +37,8 @@ func TestPrintEgressPolicyTo(t *testing.T) { CreateTime: timestamppb.New(now.Add(-5 * time.Minute)), }, Rules: []*ateapipb.EgressRule{ - {Http: &ateapipb.HTTPRule{Hostnames: []string{"api.example.com"}}}, - {Https: &ateapipb.HTTPSRule{Hostnames: []string{"www.example.com"}, Ports: &ateapipb.Ports{Numbers: []int32{8443}}}}, + {Http: &ateapipb.HttpRule{Hostnames: []string{"api.example.com"}}}, + {Https: &ateapipb.HttpsRule{Hostnames: []string{"www.example.com"}, Ports: &ateapipb.Ports{Numbers: []int32{8443}}}}, }, } empty := &ateapipb.EgressPolicy{ diff --git a/internal/ateinterceptors/ateinterceptors_test.go b/internal/ateinterceptors/ateinterceptors_test.go index 5f3b848d91..371f04f782 100644 --- a/internal/ateinterceptors/ateinterceptors_test.go +++ b/internal/ateinterceptors/ateinterceptors_test.go @@ -396,9 +396,9 @@ func TestServerUnaryInterceptorResponseLogMasksActorJWT(t *testing.T) { log := captureDefaultLog(t) const token = "eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJhY3RvciJ9.c2lnbmF0dXJl" - resp := &ateapipb.MintActorJWTResponse{ActorJwt: token} + resp := &ateapipb.MintActorJwtResponse{ActorJwt: token} - got, err := ServerUnaryInterceptor(context.Background(), &ateapipb.MintActorJWTRequest{}, &grpc.UnaryServerInfo{FullMethod: "/ateapi.Control/MintActorJWT"}, func(ctx context.Context, req interface{}) (interface{}, error) { + got, err := ServerUnaryInterceptor(context.Background(), &ateapipb.MintActorJwtRequest{}, &grpc.UnaryServerInfo{FullMethod: "/ateapi.Control/MintActorJwt"}, func(ctx context.Context, req interface{}) (interface{}, error) { return resp, nil }) if err != nil { @@ -412,7 +412,7 @@ func TestServerUnaryInterceptorResponseLogMasksActorJWT(t *testing.T) { if !strings.Contains(gotLog, `"actor_jwt":"`+protoredact.Placeholder+`"`) { t.Fatalf("log does not show the redacted actor_jwt: %s", gotLog) } - if got.(*ateapipb.MintActorJWTResponse).GetActorJwt() != token { + if got.(*ateapipb.MintActorJwtResponse).GetActorJwt() != token { t.Fatalf("interceptor mutated the response returned to the client") } } @@ -445,7 +445,7 @@ func TestInternalServerUnaryInterceptorLogMasksBytesFields(t *testing.T) { func TestServerUnaryInterceptorLogsNilResponseOnHandlerError(t *testing.T) { log := captureDefaultLog(t) - _, err := ServerUnaryInterceptor(context.Background(), &ateapipb.MintActorJWTRequest{}, &grpc.UnaryServerInfo{FullMethod: "/ateapi.Control/MintActorJWT"}, func(ctx context.Context, req interface{}) (interface{}, error) { + _, err := ServerUnaryInterceptor(context.Background(), &ateapipb.MintActorJwtRequest{}, &grpc.UnaryServerInfo{FullMethod: "/ateapi.Control/MintActorJwt"}, func(ctx context.Context, req interface{}) (interface{}, error) { return nil, apierror.PermissionDenied("no") }) if status.Code(err) != codes.PermissionDenied { diff --git a/internal/contextlogging/contextlogging_test.go b/internal/contextlogging/contextlogging_test.go index f1203b4538..29eb97b8d8 100644 --- a/internal/contextlogging/contextlogging_test.go +++ b/internal/contextlogging/contextlogging_test.go @@ -143,14 +143,14 @@ func TestHandleUngroupedKeepsTraceFieldsTopLevel(t *testing.T) { // tokenValuer stands in for a type that implements slog.LogValuer and // resolves to a proto; the handler must resolve it before deciding. type tokenValuer struct { - m *ateapipb.MintActorJWTResponse + m *ateapipb.MintActorJwtResponse } func (v tokenValuer) LogValue() slog.Value { return slog.AnyValue(v.m) } func TestHandleRedactsProtoAttrsAnywhereInTheRecord(t *testing.T) { const token = "eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJhY3RvciJ9.c2lnbmF0dXJl" - resp := &ateapipb.MintActorJWTResponse{ActorJwt: token} + resp := &ateapipb.MintActorJwtResponse{ActorJwt: token} ref := &ateapipb.ObjectRef{Atespace: "ate-demo-sandbox", Name: "agent"} tpl := &ateapipb.ActorTemplate{Containers: []*ateapipb.Container{{ Name: "c", Env: []*ateapipb.EnvVar{{Name: "API_KEY", Value: "sk-secret"}}, @@ -202,7 +202,7 @@ func (stringValuer) LogValue() slog.Value { return slog.StringValue("resolved") // groupValuer resolves to a group that holds a proto, the shape a typed // reference with a nested message would take. type groupValuer struct { - m *ateapipb.MintActorJWTResponse + m *ateapipb.MintActorJwtResponse } func (g groupValuer) LogValue() slog.Value { @@ -215,7 +215,7 @@ type countingValuer struct{ n *int } func (c countingValuer) LogValue() slog.Value { *c.n++; return slog.StringValue("once") } func TestRedactValueReportsWhatItChanges(t *testing.T) { - sensitive := &ateapipb.MintActorJWTResponse{ActorJwt: "token"} + sensitive := &ateapipb.MintActorJwtResponse{ActorJwt: "token"} envless := &ateapipb.ActorTemplate{Containers: []*ateapipb.Container{{Name: "c"}}} for _, tc := range []struct { name string @@ -228,7 +228,7 @@ func TestRedactValueReportsWhatItChanges(t *testing.T) { {"nil any", slog.AnyValue(nil), false}, {"clean type", slog.AnyValue(&ateapipb.ObjectRef{Name: "n"}), false}, {"labeled type, nothing set", slog.AnyValue(envless), false}, - {"typed nil labeled type", slog.AnyValue((*ateapipb.MintActorJWTResponse)(nil)), false}, + {"typed nil labeled type", slog.AnyValue((*ateapipb.MintActorJwtResponse)(nil)), false}, {"group of plain values", slog.GroupValue(slog.String("a", "b"), slog.Any("ref", &ateapipb.ObjectRef{})), false}, {"empty group", slog.GroupValue(), false}, {"sensitive", slog.AnyValue(sensitive), true}, @@ -261,23 +261,23 @@ func TestRedactAttrsReturnsTheInputWhenNothingChanges(t *testing.T) { if len(out) != len(in) || &out[0] != &in[0] { t.Fatal("redactAttrs copied a slice with nothing to change") } - sensitive := &ateapipb.MintActorJWTResponse{ActorJwt: "token"} + sensitive := &ateapipb.MintActorJwtResponse{ActorJwt: "token"} in = append(in, slog.Group("g2", slog.Any("r", sensitive))) out = redactAttrs(in) if &out[0] == &in[0] { t.Fatal("redactAttrs returned the input although an attribute changed") } - if in[3].Value.Group()[0].Value.Any().(*ateapipb.MintActorJWTResponse).GetActorJwt() != "token" { + if in[3].Value.Group()[0].Value.Any().(*ateapipb.MintActorJwtResponse).GetActorJwt() != "token" { t.Fatal("redactAttrs modified the input slice") } - if out[3].Value.Group()[0].Value.Any().(*ateapipb.MintActorJWTResponse).GetActorJwt() != protoredact.Placeholder { + if out[3].Value.Group()[0].Value.Any().(*ateapipb.MintActorJwtResponse).GetActorJwt() != protoredact.Placeholder { t.Fatal("redactAttrs did not mask the group member") } } func TestHandleCoversGroupsBoundAttrsAndNilsTogether(t *testing.T) { const token = "eyJhbGciOiJSUzI1NiJ9.token" - sensitive := &ateapipb.MintActorJWTResponse{ActorJwt: token} + sensitive := &ateapipb.MintActorJwtResponse{ActorJwt: token} var buf bytes.Buffer base := slog.New(NewHandler(slog.NewJSONHandler(&buf, &slog.HandlerOptions{ReplaceAttr: dropTime}))) @@ -288,7 +288,7 @@ func TestHandleCoversGroupsBoundAttrsAndNilsTogether(t *testing.T) { With(slog.Group("bound", slog.Any("resp", sensitive))). WithGroup("inner"). Info("x", - slog.Any("nilresp", (*ateapipb.MintActorJWTResponse)(nil)), + slog.Any("nilresp", (*ateapipb.MintActorJwtResponse)(nil)), slog.Any("valuer", groupValuer{sensitive}), slog.String("plain", "keep"), ) @@ -313,7 +313,7 @@ func TestHandleResolvesLogValuersOnce(t *testing.T) { calls := 0 attrs := []slog.Attr{slog.Any("v", countingValuer{&calls}), slog.String("a", "b")} if withProto { - attrs = append(attrs, slog.Any("resp", &ateapipb.MintActorJWTResponse{ActorJwt: "t"})) + attrs = append(attrs, slog.Any("resp", &ateapipb.MintActorJwtResponse{ActorJwt: "t"})) } slog.New(NewHandler(slog.NewJSONHandler(io.Discard, nil))).LogAttrs(context.Background(), slog.LevelInfo, "x", attrs...) if calls != 1 { @@ -419,12 +419,12 @@ func BenchmarkHandle(b *testing.B) { // with ==: a value receiver and a slice field. Comparing two of them as // interfaces panics at run time. type uncomparableMsg struct { - *ateapipb.MintActorJWTResponse + *ateapipb.MintActorJwtResponse _ []int } func (u uncomparableMsg) ProtoReflect() protoreflect.Message { - return u.MintActorJWTResponse.ProtoReflect() + return u.MintActorJwtResponse.ProtoReflect() } func TestHandleToleratesUncomparableMessageTypes(t *testing.T) { @@ -435,8 +435,8 @@ func TestHandleToleratesUncomparableMessageTypes(t *testing.T) { msg uncomparableMsg want string }{ - {"nothing to mask", uncomparableMsg{MintActorJWTResponse: &ateapipb.MintActorJWTResponse{}}, `"v":{}`}, - {"secret set", uncomparableMsg{MintActorJWTResponse: &ateapipb.MintActorJWTResponse{ActorJwt: "token"}}, protoredact.Placeholder}, + {"nothing to mask", uncomparableMsg{MintActorJwtResponse: &ateapipb.MintActorJwtResponse{}}, `"v":{}`}, + {"secret set", uncomparableMsg{MintActorJwtResponse: &ateapipb.MintActorJwtResponse{ActorJwt: "token"}}, protoredact.Placeholder}, } { t.Run(tc.name, func(t *testing.T) { var buf bytes.Buffer diff --git a/internal/e2e/egresspolicy.go b/internal/e2e/egresspolicy.go index d250d83b41..dae98f054b 100644 --- a/internal/e2e/egresspolicy.go +++ b/internal/e2e/egresspolicy.go @@ -34,8 +34,8 @@ import ( // rule on every port would tie with the http rule, which the API rejects. func EgressAllowAll() []*ateapipb.EgressRule { return []*ateapipb.EgressRule{ - {Http: &ateapipb.HTTPRule{Hostnames: []string{"*"}, Ports: &ateapipb.Ports{All: &ateapipb.AllPorts{}}}}, - {Https: &ateapipb.HTTPSRule{Hostnames: []string{"*"}}}, + {Http: &ateapipb.HttpRule{Hostnames: []string{"*"}, Ports: &ateapipb.Ports{All: &ateapipb.AllPorts{}}}}, + {Https: &ateapipb.HttpsRule{Hostnames: []string{"*"}}}, } } @@ -43,20 +43,20 @@ func EgressAllowAll() []*ateapipb.EgressRule { // hosts matching patterns (exact names, or "*." plus a name for one leftmost // label) on port 80. func EgressAllowHTTP(patterns ...string) *ateapipb.EgressRule { - return &ateapipb.EgressRule{Http: &ateapipb.HTTPRule{Hostnames: patterns}} + return &ateapipb.EgressRule{Http: &ateapipb.HttpRule{Hostnames: patterns}} } // EgressAllowHTTPS is a rule that lets an actor send HTTPS, intercepted by the // gateway, to the hosts matching patterns on port 443. func EgressAllowHTTPS(patterns ...string) *ateapipb.EgressRule { - return &ateapipb.EgressRule{Https: &ateapipb.HTTPSRule{Hostnames: patterns}} + return &ateapipb.EgressRule{Https: &ateapipb.HttpsRule{Hostnames: patterns}} } // EgressAllowPassthrough is a rule that lets an actor send TLS to the hosts // matching patterns on any port, relayed by the gateway without decryption. func EgressAllowPassthrough(patterns ...string) *ateapipb.EgressRule { return &ateapipb.EgressRule{ - TlsPassthrough: &ateapipb.TLSPassthroughRule{ + TlsPassthrough: &ateapipb.TlsPassthroughRule{ Hostnames: patterns, Ports: &ateapipb.Ports{All: &ateapipb.AllPorts{}}, }, @@ -67,7 +67,7 @@ func EgressAllowPassthrough(patterns ...string) *ateapipb.EgressRule { // given ports. func EgressAllowPassthroughOnPorts(ports []int32, patterns ...string) *ateapipb.EgressRule { return &ateapipb.EgressRule{ - TlsPassthrough: &ateapipb.TLSPassthroughRule{ + TlsPassthrough: &ateapipb.TlsPassthroughRule{ Hostnames: patterns, Ports: &ateapipb.Ports{Numbers: ports}, }, diff --git a/internal/egresspolicy/egresspolicy_test.go b/internal/egresspolicy/egresspolicy_test.go index 9158bc72ef..c26994db72 100644 --- a/internal/egresspolicy/egresspolicy_test.go +++ b/internal/egresspolicy/egresspolicy_test.go @@ -23,19 +23,19 @@ import ( ) func httpRule(patterns ...string) *ateapipb.EgressRule { - return &ateapipb.EgressRule{Http: &ateapipb.HTTPRule{Hostnames: patterns}} + return &ateapipb.EgressRule{Http: &ateapipb.HttpRule{Hostnames: patterns}} } func httpRuleOnPorts(ports *ateapipb.Ports, patterns ...string) *ateapipb.EgressRule { - return &ateapipb.EgressRule{Http: &ateapipb.HTTPRule{Hostnames: patterns, Ports: ports}} + return &ateapipb.EgressRule{Http: &ateapipb.HttpRule{Hostnames: patterns, Ports: ports}} } func httpsRule(patterns ...string) *ateapipb.EgressRule { - return &ateapipb.EgressRule{Https: &ateapipb.HTTPSRule{Hostnames: patterns}} + return &ateapipb.EgressRule{Https: &ateapipb.HttpsRule{Hostnames: patterns}} } func passthroughRule(ports *ateapipb.Ports, patterns ...string) *ateapipb.EgressRule { - return &ateapipb.EgressRule{TlsPassthrough: &ateapipb.TLSPassthroughRule{Hostnames: patterns, Ports: ports}} + return &ateapipb.EgressRule{TlsPassthrough: &ateapipb.TlsPassthroughRule{Hostnames: patterns, Ports: ports}} } func ports(numbers ...int32) *ateapipb.Ports { return &ateapipb.Ports{Numbers: numbers} } @@ -214,7 +214,7 @@ func TestEvaluateRequest(t *testing.T) { CredentialUri: "ate-secret://k8s/default/token", }}, } - withEffects := &ateapipb.EgressRule{Https: &ateapipb.HTTPSRule{ + withEffects := &ateapipb.EgressRule{Https: &ateapipb.HttpsRule{ Hostnames: []string{"api.example.com"}, Effects: effects, }} @@ -400,7 +400,7 @@ func TestEvaluateRequest(t *testing.T) { } func httpsRuleOnPorts(ports *ateapipb.Ports, patterns ...string) *ateapipb.EgressRule { - return &ateapipb.EgressRule{Https: &ateapipb.HTTPSRule{Hostnames: patterns, Ports: ports}} + return &ateapipb.EgressRule{Https: &ateapipb.HttpsRule{Hostnames: patterns, Ports: ports}} } func TestSNIRules(t *testing.T) { diff --git a/internal/proto/ateletpb/atelet.pb.go b/internal/proto/ateletpb/atelet.pb.go index fea6efe1c3..af0456c727 100644 --- a/internal/proto/ateletpb/atelet.pb.go +++ b/internal/proto/ateletpb/atelet.pb.go @@ -2388,7 +2388,7 @@ func (x *EnvEntry) GetValue() string { // Only HTTP is supported today. type WakeupProbe struct { state protoimpl.MessageState `protogen:"open.v1"` - HttpGet *HTTPGetAction `protobuf:"bytes,1,opt,name=http_get,json=httpGet,proto3" json:"http_get,omitempty"` + HttpGet *HttpGetAction `protobuf:"bytes,1,opt,name=http_get,json=httpGet,proto3" json:"http_get,omitempty"` // How long to keep polling before giving up and failing the actor start. TimeoutSeconds int32 `protobuf:"varint,2,opt,name=timeout_seconds,json=timeoutSeconds,proto3" json:"timeout_seconds,omitempty"` unknownFields protoimpl.UnknownFields @@ -2425,7 +2425,7 @@ func (*WakeupProbe) Descriptor() ([]byte, []int) { return file_atelet_proto_rawDescGZIP(), []int{35} } -func (x *WakeupProbe) GetHttpGet() *HTTPGetAction { +func (x *WakeupProbe) GetHttpGet() *HttpGetAction { if x != nil { return x.HttpGet } @@ -2439,8 +2439,8 @@ func (x *WakeupProbe) GetTimeoutSeconds() int32 { return 0 } -// HTTPGetAction performs an HTTP GET against the container. -type HTTPGetAction struct { +// HttpGetAction performs an HTTP GET against the container. +type HttpGetAction struct { state protoimpl.MessageState `protogen:"open.v1"` // Path to access on the HTTP server. Path string `protobuf:"bytes,1,opt,name=path,proto3" json:"path,omitempty"` @@ -2450,20 +2450,20 @@ type HTTPGetAction struct { sizeCache protoimpl.SizeCache } -func (x *HTTPGetAction) Reset() { - *x = HTTPGetAction{} +func (x *HttpGetAction) Reset() { + *x = HttpGetAction{} mi := &file_atelet_proto_msgTypes[36] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *HTTPGetAction) String() string { +func (x *HttpGetAction) String() string { return protoimpl.X.MessageStringOf(x) } -func (*HTTPGetAction) ProtoMessage() {} +func (*HttpGetAction) ProtoMessage() {} -func (x *HTTPGetAction) ProtoReflect() protoreflect.Message { +func (x *HttpGetAction) ProtoReflect() protoreflect.Message { mi := &file_atelet_proto_msgTypes[36] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) @@ -2475,19 +2475,19 @@ func (x *HTTPGetAction) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use HTTPGetAction.ProtoReflect.Descriptor instead. -func (*HTTPGetAction) Descriptor() ([]byte, []int) { +// Deprecated: Use HttpGetAction.ProtoReflect.Descriptor instead. +func (*HttpGetAction) Descriptor() ([]byte, []int) { return file_atelet_proto_rawDescGZIP(), []int{36} } -func (x *HTTPGetAction) GetPath() string { +func (x *HttpGetAction) GetPath() string { if x != nil { return x.Path } return "" } -func (x *HTTPGetAction) GetPort() int32 { +func (x *HttpGetAction) GetPort() int32 { if x != nil { return x.Port } @@ -3405,9 +3405,9 @@ const file_atelet_proto_rawDesc = "" + "\x04name\x18\x01 \x01(\tR\x04name\x12\x19\n" + "\x05value\x18\x02 \x01(\tB\x03\x80\x01\x01R\x05value\"h\n" + "\vWakeupProbe\x120\n" + - "\bhttp_get\x18\x01 \x01(\v2\x15.atelet.HTTPGetActionR\ahttpGet\x12'\n" + + "\bhttp_get\x18\x01 \x01(\v2\x15.atelet.HttpGetActionR\ahttpGet\x12'\n" + "\x0ftimeout_seconds\x18\x02 \x01(\x05R\x0etimeoutSeconds\"7\n" + - "\rHTTPGetAction\x12\x12\n" + + "\rHttpGetAction\x12\x12\n" + "\x04path\x18\x01 \x01(\tR\x04path\x12\x12\n" + "\x04port\x18\x02 \x01(\x05R\x04port\"\r\n" + "\vRunResponse\"C\n" + @@ -3545,7 +3545,7 @@ var file_atelet_proto_goTypes = []any{ (*ResourceLimits)(nil), // 36: atelet.ResourceLimits (*EnvEntry)(nil), // 37: atelet.EnvEntry (*WakeupProbe)(nil), // 38: atelet.WakeupProbe - (*HTTPGetAction)(nil), // 39: atelet.HTTPGetAction + (*HttpGetAction)(nil), // 39: atelet.HttpGetAction (*RunResponse)(nil), // 40: atelet.RunResponse (*LocalCheckpointConfiguration)(nil), // 41: atelet.LocalCheckpointConfiguration (*ExternalCheckpointConfiguration)(nil), // 42: atelet.ExternalCheckpointConfiguration @@ -3594,7 +3594,7 @@ var file_atelet_proto_depIdxs = []int32{ 34, // 29: atelet.Container.security_context:type_name -> atelet.SecurityContext 36, // 30: atelet.Container.resources:type_name -> atelet.ResourceLimits 35, // 31: atelet.SecurityContext.capabilities:type_name -> atelet.Capabilities - 39, // 32: atelet.WakeupProbe.http_get:type_name -> atelet.HTTPGetAction + 39, // 32: atelet.WakeupProbe.http_get:type_name -> atelet.HttpGetAction 22, // 33: atelet.CheckpointRequest.spec:type_name -> atelet.WorkloadSpec 1, // 34: atelet.CheckpointRequest.type:type_name -> atelet.CheckpointType 41, // 35: atelet.CheckpointRequest.local_config:type_name -> atelet.LocalCheckpointConfiguration diff --git a/internal/proto/ateletpb/atelet.proto b/internal/proto/ateletpb/atelet.proto index 48c5a0a4a1..3a192351d4 100644 --- a/internal/proto/ateletpb/atelet.proto +++ b/internal/proto/ateletpb/atelet.proto @@ -474,13 +474,13 @@ message EnvEntry { // WakeupProbe describes how to check that a container is ready to serve. // Only HTTP is supported today. message WakeupProbe { - HTTPGetAction http_get = 1; + HttpGetAction http_get = 1; // How long to keep polling before giving up and failing the actor start. int32 timeout_seconds = 2; } -// HTTPGetAction performs an HTTP GET against the container. -message HTTPGetAction { +// HttpGetAction performs an HTTP GET against the container. +message HttpGetAction { // Path to access on the HTTP server. string path = 1; // TCP port to connect to (1..65535). diff --git a/internal/proto/ateompb/ateom.pb.go b/internal/proto/ateompb/ateom.pb.go index 87fde50518..bfadf9f340 100644 --- a/internal/proto/ateompb/ateom.pb.go +++ b/internal/proto/ateompb/ateom.pb.go @@ -1142,7 +1142,7 @@ func (x *ImageVolumeMount) GetMountPath() string { // Only HTTP is supported today. type WakeupProbe struct { state protoimpl.MessageState `protogen:"open.v1"` - HttpGet *HTTPGetAction `protobuf:"bytes,1,opt,name=http_get,json=httpGet,proto3" json:"http_get,omitempty"` + HttpGet *HttpGetAction `protobuf:"bytes,1,opt,name=http_get,json=httpGet,proto3" json:"http_get,omitempty"` // How long to keep polling before giving up and failing the actor start. TimeoutSeconds int32 `protobuf:"varint,2,opt,name=timeout_seconds,json=timeoutSeconds,proto3" json:"timeout_seconds,omitempty"` unknownFields protoimpl.UnknownFields @@ -1179,7 +1179,7 @@ func (*WakeupProbe) Descriptor() ([]byte, []int) { return file_ateom_proto_rawDescGZIP(), []int{13} } -func (x *WakeupProbe) GetHttpGet() *HTTPGetAction { +func (x *WakeupProbe) GetHttpGet() *HttpGetAction { if x != nil { return x.HttpGet } @@ -1193,8 +1193,8 @@ func (x *WakeupProbe) GetTimeoutSeconds() int32 { return 0 } -// HTTPGetAction performs an HTTP GET against the container. -type HTTPGetAction struct { +// HttpGetAction performs an HTTP GET against the container. +type HttpGetAction struct { state protoimpl.MessageState `protogen:"open.v1"` // Path to access on the HTTP server. Path string `protobuf:"bytes,1,opt,name=path,proto3" json:"path,omitempty"` @@ -1204,20 +1204,20 @@ type HTTPGetAction struct { sizeCache protoimpl.SizeCache } -func (x *HTTPGetAction) Reset() { - *x = HTTPGetAction{} +func (x *HttpGetAction) Reset() { + *x = HttpGetAction{} mi := &file_ateom_proto_msgTypes[14] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *HTTPGetAction) String() string { +func (x *HttpGetAction) String() string { return protoimpl.X.MessageStringOf(x) } -func (*HTTPGetAction) ProtoMessage() {} +func (*HttpGetAction) ProtoMessage() {} -func (x *HTTPGetAction) ProtoReflect() protoreflect.Message { +func (x *HttpGetAction) ProtoReflect() protoreflect.Message { mi := &file_ateom_proto_msgTypes[14] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) @@ -1229,19 +1229,19 @@ func (x *HTTPGetAction) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use HTTPGetAction.ProtoReflect.Descriptor instead. -func (*HTTPGetAction) Descriptor() ([]byte, []int) { +// Deprecated: Use HttpGetAction.ProtoReflect.Descriptor instead. +func (*HttpGetAction) Descriptor() ([]byte, []int) { return file_ateom_proto_rawDescGZIP(), []int{14} } -func (x *HTTPGetAction) GetPath() string { +func (x *HttpGetAction) GetPath() string { if x != nil { return x.Path } return "" } -func (x *HTTPGetAction) GetPort() int32 { +func (x *HttpGetAction) GetPort() int32 { if x != nil { return x.Port } @@ -2127,9 +2127,9 @@ const file_ateom_proto_rawDesc = "" + "\n" + "mount_path\x18\x02 \x01(\tR\tmountPath\"g\n" + "\vWakeupProbe\x12/\n" + - "\bhttp_get\x18\x01 \x01(\v2\x14.ateom.HTTPGetActionR\ahttpGet\x12'\n" + + "\bhttp_get\x18\x01 \x01(\v2\x14.ateom.HttpGetActionR\ahttpGet\x12'\n" + "\x0ftimeout_seconds\x18\x02 \x01(\x05R\x0etimeoutSeconds\"7\n" + - "\rHTTPGetAction\x12\x12\n" + + "\rHttpGetAction\x12\x12\n" + "\x04path\x18\x01 \x01(\tR\x04path\x12\x12\n" + "\x04port\x18\x02 \x01(\x05R\x04port\"\x15\n" + "\x13RunWorkloadResponse\"\xdb\x04\n" + @@ -2256,7 +2256,7 @@ var file_ateom_proto_goTypes = []any{ (*SystemInfoVolumeMount)(nil), // 14: ateom.SystemInfoVolumeMount (*ImageVolumeMount)(nil), // 15: ateom.ImageVolumeMount (*WakeupProbe)(nil), // 16: ateom.WakeupProbe - (*HTTPGetAction)(nil), // 17: ateom.HTTPGetAction + (*HttpGetAction)(nil), // 17: ateom.HttpGetAction (*RunWorkloadResponse)(nil), // 18: ateom.RunWorkloadResponse (*CheckpointWorkloadRequest)(nil), // 19: ateom.CheckpointWorkloadRequest (*CheckpointWorkloadResponse)(nil), // 20: ateom.CheckpointWorkloadResponse @@ -2286,7 +2286,7 @@ var file_ateom_proto_depIdxs = []int32{ 15, // 11: ateom.Container.image_volume_mounts:type_name -> ateom.ImageVolumeMount 10, // 12: ateom.Container.container_spec:type_name -> ateom.ContainerSpec 11, // 13: ateom.ContainerSpec.resources:type_name -> ateom.ResourceLimits - 17, // 14: ateom.WakeupProbe.http_get:type_name -> ateom.HTTPGetAction + 17, // 14: ateom.WakeupProbe.http_get:type_name -> ateom.HttpGetAction 8, // 15: ateom.CheckpointWorkloadRequest.spec:type_name -> ateom.WorkloadSpec 29, // 16: ateom.CheckpointWorkloadRequest.runtime_asset_paths:type_name -> ateom.CheckpointWorkloadRequest.RuntimeAssetPathsEntry 0, // 17: ateom.CheckpointWorkloadRequest.fidelity:type_name -> ateom.SnapshotFidelity diff --git a/internal/proto/ateompb/ateom.proto b/internal/proto/ateompb/ateom.proto index 557cfa803b..22002d1df0 100644 --- a/internal/proto/ateompb/ateom.proto +++ b/internal/proto/ateompb/ateom.proto @@ -269,13 +269,13 @@ message ImageVolumeMount { // WakeupProbe describes how to check that a container is ready to serve. // Only HTTP is supported today. message WakeupProbe { - HTTPGetAction http_get = 1; + HttpGetAction http_get = 1; // How long to keep polling before giving up and failing the actor start. int32 timeout_seconds = 2; } -// HTTPGetAction performs an HTTP GET against the container. -message HTTPGetAction { +// HttpGetAction performs an HTTP GET against the container. +message HttpGetAction { // Path to access on the HTTP server. string path = 1; // TCP port to connect to (1..65535). diff --git a/internal/protoredact/protoredact_test.go b/internal/protoredact/protoredact_test.go index bd6bc5126b..e8a4f59816 100644 --- a/internal/protoredact/protoredact_test.go +++ b/internal/protoredact/protoredact_test.go @@ -278,10 +278,10 @@ func TestRedactedHandlesTypedNilWithoutPanicking(t *testing.T) { // A typed nil proto pointer, as a handler returns alongside an error and // the interceptor then logs, must come back as a nil message of the same // type, for a redactable type and for a clean one alike. - var jwt *ateapipb.MintActorJWTResponse + var jwt *ateapipb.MintActorJwtResponse if r, changed := protoredact.Redacted(jwt); changed { t.Errorf("typed nil redactable: reported changed, got %#v", r) - } else if m, ok := r.(*ateapipb.MintActorJWTResponse); !ok || m != nil { + } else if m, ok := r.(*ateapipb.MintActorJwtResponse); !ok || m != nil { t.Errorf("typed nil redactable: got %#v", r) } var ref *ateapipb.ObjectRef @@ -302,7 +302,7 @@ func TestRedactedReturnsMessagesWithNothingToMaskWithoutCopying(t *testing.T) { &ateapipb.Worker{}, &ateapipb.ActorTemplate{Containers: []*ateapipb.Container{{Name: "c", Image: "img"}}}, &ateapipb.ListActorTemplatesResponse{ActorTemplates: []*ateapipb.ActorTemplate{{Containers: []*ateapipb.Container{{Name: "c"}}}}}, - &ateapipb.MintActorJWTResponse{}, + &ateapipb.MintActorJwtResponse{}, &ateletpb.RunRequest{Spec: &ateletpb.WorkloadSpec{Containers: []*ateletpb.Container{{Env: []*ateletpb.EnvEntry{{Name: "K"}}}}}}, } { if protoredact.NeedsRedaction(m) { @@ -314,7 +314,7 @@ func TestRedactedReturnsMessagesWithNothingToMaskWithoutCopying(t *testing.T) { } // A populated labeled field, however deep, forces a masked copy. for _, m := range []proto.Message{ - &ateapipb.MintActorJWTResponse{ActorJwt: "t"}, + &ateapipb.MintActorJwtResponse{ActorJwt: "t"}, &ateapipb.ActorTemplate{Containers: []*ateapipb.Container{{Env: []*ateapipb.EnvVar{{Name: "K", Value: "v"}}}}}, &ateapipb.ListActorTemplatesResponse{ActorTemplates: []*ateapipb.ActorTemplate{{}, {Containers: []*ateapipb.Container{{}, {Env: []*ateapipb.EnvVar{{Name: "K", Value: "v"}}}}}}}, &ateletpb.RunRequest{Spec: &ateletpb.WorkloadSpec{Containers: []*ateletpb.Container{{Env: []*ateletpb.EnvEntry{{Name: "K", Value: "v"}}}}}}, @@ -335,7 +335,7 @@ func TestNeedsRedactionToleratesTypedNilOfAnyImplementation(t *testing.T) { // A typed nil generated message answers ProtoReflect safely; a typed nil // dynamicpb.Message returns the nil receiver, which faults on use. Both // must be reported clean and returned unchanged. - var gen *ateapipb.MintActorJWTResponse + var gen *ateapipb.MintActorJwtResponse var dyn *dynamicpb.Message for _, m := range []proto.Message{gen, dyn} { if protoredact.NeedsRedaction(m) { @@ -351,7 +351,7 @@ func TestNeedsRedactionIsSafeUnderConcurrentFirstUse(t *testing.T) { // The per-type answer is memoized on first sight; many goroutines // meeting several types at once must agree and not race. msgs := []proto.Message{ - &ateapipb.MintActorJWTResponse{ActorJwt: "t"}, + &ateapipb.MintActorJwtResponse{ActorJwt: "t"}, &ateapipb.ListActorsResponse{}, &ateapipb.ActorTemplate{Containers: []*ateapipb.Container{{Env: []*ateapipb.EnvVar{{Name: "K", Value: "v"}}}}}, &ateapipb.Worker{}, @@ -416,7 +416,7 @@ func isDebugRedact(fd protoreflect.FieldDescriptor) bool { func TestDebugRedactFieldsArePinned(t *testing.T) { want := map[string]bool{ "ateapi.EnvVar.value": true, - "ateapi.MintActorJWTResponse.actor_jwt": true, + "ateapi.MintActorJwtResponse.actor_jwt": true, "atelet.AssetFile.url": true, "atelet.EnvEntry.value": true, "ateom.ContainerSpec.env": true, @@ -455,7 +455,7 @@ func TestSecretLikeFieldsAreLabeled(t *testing.T) { "page_token": "opaque list cursor", "next_page_token": "opaque list cursor", "ateapi.CredentialHeader.credential_uri": "ate-secret:// reference to a credential, not the value", - "ateapi.CredentialHeader.actor_jwt": "ActorJWTSource: audiences and lifetime for minting, not a token", + "ateapi.CredentialHeader.actor_jwt": "ActorJwtSource: audiences and lifetime for minting, not a token", } forEachField(func(fd protoreflect.FieldDescriptor) { if !secretLikeName.MatchString(string(fd.Name())) || isDebugRedact(fd) { diff --git a/internal/serverboot/logging_test.go b/internal/serverboot/logging_test.go index 1f25dfcd74..ed17344eab 100644 --- a/internal/serverboot/logging_test.go +++ b/internal/serverboot/logging_test.go @@ -284,7 +284,7 @@ func TestInitLoggerInstallsRedactingHandler(t *testing.T) { } const token = "eyJhbGciOiJSUzI1NiJ9.secret-token" - slog.Info("Handle RPC", slog.Any("resp", &ateapipb.MintActorJWTResponse{ActorJwt: token})) + slog.Info("Handle RPC", slog.Any("resp", &ateapipb.MintActorJwtResponse{ActorJwt: token})) got := buf.String() if strings.Contains(got, token) { t.Fatalf("default logger wrote a debug_redact field: %s", got) diff --git a/internal/wakeupprobe/wakeupprobe_test.go b/internal/wakeupprobe/wakeupprobe_test.go index e211019406..481a05d9f5 100644 --- a/internal/wakeupprobe/wakeupprobe_test.go +++ b/internal/wakeupprobe/wakeupprobe_test.go @@ -39,19 +39,19 @@ func TestURL(t *testing.T) { }{ { name: "missing path", - probe: &ateompb.WakeupProbe{HttpGet: &ateompb.HTTPGetAction{Port: 8080}}, + probe: &ateompb.WakeupProbe{HttpGet: &ateompb.HttpGetAction{Port: 8080}}, actorIP: "169.254.17.2", wantErr: true, }, { name: "explicit path", - probe: &ateompb.WakeupProbe{HttpGet: &ateompb.HTTPGetAction{Path: "/health", Port: 9000}}, + probe: &ateompb.WakeupProbe{HttpGet: &ateompb.HttpGetAction{Path: "/health", Port: 9000}}, actorIP: "169.254.17.2", want: "http://169.254.17.2:9000/health", }, { name: "path without leading slash is normalized", - probe: &ateompb.WakeupProbe{HttpGet: &ateompb.HTTPGetAction{Path: "ready", Port: 80}}, + probe: &ateompb.WakeupProbe{HttpGet: &ateompb.HttpGetAction{Path: "ready", Port: 80}}, actorIP: "10.0.0.1", want: "http://10.0.0.1:80/ready", }, @@ -63,13 +63,13 @@ func TestURL(t *testing.T) { }, { name: "port zero", - probe: &ateompb.WakeupProbe{HttpGet: &ateompb.HTTPGetAction{Port: 0}}, + probe: &ateompb.WakeupProbe{HttpGet: &ateompb.HttpGetAction{Port: 0}}, actorIP: "1.2.3.4", wantErr: true, }, { name: "port too large", - probe: &ateompb.WakeupProbe{HttpGet: &ateompb.HTTPGetAction{Port: 70000}}, + probe: &ateompb.WakeupProbe{HttpGet: &ateompb.HttpGetAction{Port: 70000}}, actorIP: "1.2.3.4", wantErr: true, }, @@ -98,7 +98,7 @@ func TestWait_ReturnsOnFirst200(t *testing.T) { defer srv.Close() ip, port := splitHostPort(t, srv.URL) - probe := &ateompb.WakeupProbe{HttpGet: &ateompb.HTTPGetAction{Path: "/readyz", Port: int32(port)}, TimeoutSeconds: 1} + probe := &ateompb.WakeupProbe{HttpGet: &ateompb.HttpGetAction{Path: "/readyz", Port: int32(port)}, TimeoutSeconds: 1} ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) defer cancel() @@ -119,7 +119,7 @@ func TestWait_WaitsForServerToBecomeReady(t *testing.T) { defer srv.Close() ip, port := splitHostPort(t, srv.URL) - probe := &ateompb.WakeupProbe{HttpGet: &ateompb.HTTPGetAction{Path: "/readyz", Port: int32(port)}, TimeoutSeconds: 1} + probe := &ateompb.WakeupProbe{HttpGet: &ateompb.HttpGetAction{Path: "/readyz", Port: int32(port)}, TimeoutSeconds: 1} flipAt := time.Now().Add(50 * time.Millisecond) go func() { @@ -146,7 +146,7 @@ func TestWait_ContextCancellation(t *testing.T) { // Bind a port and immediately close to ensure connect-refused, so the // poll loop is exercised but no server ever returns 200. port := pickFreePort(t) - probe := &ateompb.WakeupProbe{HttpGet: &ateompb.HTTPGetAction{Path: "/readyz", Port: int32(port)}, TimeoutSeconds: 1} + probe := &ateompb.WakeupProbe{HttpGet: &ateompb.HttpGetAction{Path: "/readyz", Port: int32(port)}, TimeoutSeconds: 1} ctx, cancel := context.WithCancel(context.Background()) go func() { @@ -201,7 +201,7 @@ func TestWait_GivesUpAtProbeTimeout(t *testing.T) { // probe's own deadline rather than the package default. port := pickFreePort(t) probe := &ateompb.WakeupProbe{ - HttpGet: &ateompb.HTTPGetAction{Path: "/readyz", Port: int32(port)}, + HttpGet: &ateompb.HttpGetAction{Path: "/readyz", Port: int32(port)}, TimeoutSeconds: 1, } diff --git a/pkg/proto/ateapipb/ateapi.pb.go b/pkg/proto/ateapipb/ateapi.pb.go index 8d60d0f399..2bcbb20599 100644 --- a/pkg/proto/ateapipb/ateapi.pb.go +++ b/pkg/proto/ateapipb/ateapi.pb.go @@ -1323,19 +1323,19 @@ type EgressRule struct { // // +k8s:optional // +k8s:unionMember - Http *HTTPRule `protobuf:"bytes,1,opt,name=http,proto3" json:"http,omitempty"` + Http *HttpRule `protobuf:"bytes,1,opt,name=http,proto3" json:"http,omitempty"` // HTTPS to the named hosts, intercepted (man-in-the-middle) so the gateway // can authorize and modify each request. HTTP/1.1 and HTTP/2 inside the // TLS connection; HTTP/3 is not supported. // // +k8s:optional // +k8s:unionMember - Https *HTTPSRule `protobuf:"bytes,2,opt,name=https,proto3" json:"https,omitempty"` + Https *HttpsRule `protobuf:"bytes,2,opt,name=https,proto3" json:"https,omitempty"` // TLS to the named servers, forwarded without decryption. // // +k8s:optional // +k8s:unionMember - TlsPassthrough *TLSPassthroughRule `protobuf:"bytes,3,opt,name=tls_passthrough,json=tlsPassthrough,proto3" json:"tls_passthrough,omitempty"` + TlsPassthrough *TlsPassthroughRule `protobuf:"bytes,3,opt,name=tls_passthrough,json=tlsPassthrough,proto3" json:"tls_passthrough,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -1370,31 +1370,31 @@ func (*EgressRule) Descriptor() ([]byte, []int) { return file_ateapi_proto_rawDescGZIP(), []int{8} } -func (x *EgressRule) GetHttp() *HTTPRule { +func (x *EgressRule) GetHttp() *HttpRule { if x != nil { return x.Http } return nil } -func (x *EgressRule) GetHttps() *HTTPSRule { +func (x *EgressRule) GetHttps() *HttpsRule { if x != nil { return x.Https } return nil } -func (x *EgressRule) GetTlsPassthrough() *TLSPassthroughRule { +func (x *EgressRule) GetTlsPassthrough() *TlsPassthroughRule { if x != nil { return x.TlsPassthrough } return nil } -// HTTPRule allows cleartext HTTP and is evaluated per request on the request +// HttpRule allows cleartext HTTP and is evaluated per request on the request // authority (Host or :authority) and port. The gateway resolves the name and // dials it. -type HTTPRule struct { +type HttpRule struct { state protoimpl.MessageState `protogen:"open.v1"` // Hostname patterns: a DNS name, or a DNS name with "*" as its leftmost // label. The rule matches when any pattern matches. @@ -1433,20 +1433,20 @@ type HTTPRule struct { sizeCache protoimpl.SizeCache } -func (x *HTTPRule) Reset() { - *x = HTTPRule{} +func (x *HttpRule) Reset() { + *x = HttpRule{} mi := &file_ateapi_proto_msgTypes[9] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *HTTPRule) String() string { +func (x *HttpRule) String() string { return protoimpl.X.MessageStringOf(x) } -func (*HTTPRule) ProtoMessage() {} +func (*HttpRule) ProtoMessage() {} -func (x *HTTPRule) ProtoReflect() protoreflect.Message { +func (x *HttpRule) ProtoReflect() protoreflect.Message { mi := &file_ateapi_proto_msgTypes[9] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) @@ -1458,33 +1458,33 @@ func (x *HTTPRule) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use HTTPRule.ProtoReflect.Descriptor instead. -func (*HTTPRule) Descriptor() ([]byte, []int) { +// Deprecated: Use HttpRule.ProtoReflect.Descriptor instead. +func (*HttpRule) Descriptor() ([]byte, []int) { return file_ateapi_proto_rawDescGZIP(), []int{9} } -func (x *HTTPRule) GetHostnames() []string { +func (x *HttpRule) GetHostnames() []string { if x != nil { return x.Hostnames } return nil } -func (x *HTTPRule) GetPorts() *Ports { +func (x *HttpRule) GetPorts() *Ports { if x != nil { return x.Ports } return nil } -func (x *HTTPRule) GetEffects() *HttpRuleEffects { +func (x *HttpRule) GetEffects() *HttpRuleEffects { if x != nil { return x.Effects } return nil } -// HTTPSRule allows HTTPS that the gateway intercepts (MITM). Every Actor behind such +// HttpsRule allows HTTPS that the gateway intercepts (MITM). Every Actor behind such // a rule must trust the gateway CA. Without it, TLS to these names fails in the // Actor. // @@ -1495,10 +1495,10 @@ func (x *HTTPRule) GetEffects() *HttpRuleEffects { // // Interception is for HTTP. A connection that sends no SNI does not match, // and a non-HTTP protocol inside an intercepted connection fails. -type HTTPSRule struct { +type HttpsRule struct { state protoimpl.MessageState `protogen:"open.v1"` // Server names matched against the SNI, in the pattern syntax of - // HTTPRule.hostnames. + // HttpRule.hostnames. // // +k8s:required // +k8s:maxItems=256 @@ -1518,20 +1518,20 @@ type HTTPSRule struct { sizeCache protoimpl.SizeCache } -func (x *HTTPSRule) Reset() { - *x = HTTPSRule{} +func (x *HttpsRule) Reset() { + *x = HttpsRule{} mi := &file_ateapi_proto_msgTypes[10] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *HTTPSRule) String() string { +func (x *HttpsRule) String() string { return protoimpl.X.MessageStringOf(x) } -func (*HTTPSRule) ProtoMessage() {} +func (*HttpsRule) ProtoMessage() {} -func (x *HTTPSRule) ProtoReflect() protoreflect.Message { +func (x *HttpsRule) ProtoReflect() protoreflect.Message { mi := &file_ateapi_proto_msgTypes[10] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) @@ -1543,43 +1543,43 @@ func (x *HTTPSRule) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use HTTPSRule.ProtoReflect.Descriptor instead. -func (*HTTPSRule) Descriptor() ([]byte, []int) { +// Deprecated: Use HttpsRule.ProtoReflect.Descriptor instead. +func (*HttpsRule) Descriptor() ([]byte, []int) { return file_ateapi_proto_rawDescGZIP(), []int{10} } -func (x *HTTPSRule) GetHostnames() []string { +func (x *HttpsRule) GetHostnames() []string { if x != nil { return x.Hostnames } return nil } -func (x *HTTPSRule) GetPorts() *Ports { +func (x *HttpsRule) GetPorts() *Ports { if x != nil { return x.Ports } return nil } -func (x *HTTPSRule) GetEffects() *HttpRuleEffects { +func (x *HttpsRule) GetEffects() *HttpRuleEffects { if x != nil { return x.Effects } return nil } -// TLSPassthroughRule allows TLS that the gateway forwards without decryption. +// TlsPassthroughRule allows TLS that the gateway forwards without decryption. // It is evaluated once per connection at the ClientHello, on the SNI and // port, for any protocol carried over TLS. A connection that sends no SNI // does not match. Protocols that upgrade to TLS after a cleartext exchange // (STARTTLS) do not match. Because the gateway reads nothing after the // ClientHello, a passthrough rule has no effects, and a policy change reaches // an open connection only when it ends. -type TLSPassthroughRule struct { +type TlsPassthroughRule struct { state protoimpl.MessageState `protogen:"open.v1"` // Server names matched against the SNI, in the pattern syntax of - // HTTPRule.hostnames. + // HttpRule.hostnames. // // +k8s:required // +k8s:maxItems=256 @@ -1594,20 +1594,20 @@ type TLSPassthroughRule struct { sizeCache protoimpl.SizeCache } -func (x *TLSPassthroughRule) Reset() { - *x = TLSPassthroughRule{} +func (x *TlsPassthroughRule) Reset() { + *x = TlsPassthroughRule{} mi := &file_ateapi_proto_msgTypes[11] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *TLSPassthroughRule) String() string { +func (x *TlsPassthroughRule) String() string { return protoimpl.X.MessageStringOf(x) } -func (*TLSPassthroughRule) ProtoMessage() {} +func (*TlsPassthroughRule) ProtoMessage() {} -func (x *TLSPassthroughRule) ProtoReflect() protoreflect.Message { +func (x *TlsPassthroughRule) ProtoReflect() protoreflect.Message { mi := &file_ateapi_proto_msgTypes[11] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) @@ -1619,19 +1619,19 @@ func (x *TLSPassthroughRule) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use TLSPassthroughRule.ProtoReflect.Descriptor instead. -func (*TLSPassthroughRule) Descriptor() ([]byte, []int) { +// Deprecated: Use TlsPassthroughRule.ProtoReflect.Descriptor instead. +func (*TlsPassthroughRule) Descriptor() ([]byte, []int) { return file_ateapi_proto_rawDescGZIP(), []int{11} } -func (x *TLSPassthroughRule) GetHostnames() []string { +func (x *TlsPassthroughRule) GetHostnames() []string { if x != nil { return x.Hostnames } return nil } -func (x *TLSPassthroughRule) GetPorts() *Ports { +func (x *TlsPassthroughRule) GetPorts() *Ports { if x != nil { return x.Ports } @@ -1840,7 +1840,7 @@ type CredentialHeader struct { // // +k8s:optional // +k8s:unionMember - ActorJwt *ActorJWTSource `protobuf:"bytes,4,opt,name=actor_jwt,json=actorJwt,proto3" json:"actor_jwt,omitempty"` + ActorJwt *ActorJwtSource `protobuf:"bytes,4,opt,name=actor_jwt,json=actorJwt,proto3" json:"actor_jwt,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -1896,18 +1896,18 @@ func (x *CredentialHeader) GetCredentialUri() string { return "" } -func (x *CredentialHeader) GetActorJwt() *ActorJWTSource { +func (x *CredentialHeader) GetActorJwt() *ActorJwtSource { if x != nil { return x.ActorJwt } return nil } -// ActorJWTSource has the gateway mint a JWT for the Actor that sent the +// ActorJwtSource has the gateway mint a JWT for the Actor that sent the // request. Relying parties verify it with OpenID Connect Discovery against its // issuer. The gateway reuses a JWT across requests while enough of its // lifetime remains, so its jti does not identify a single request. -type ActorJWTSource struct { +type ActorJwtSource struct { state protoimpl.MessageState `protogen:"open.v1"` // The audiences the JWT is bound to, which become its aud claim. // @@ -1927,20 +1927,20 @@ type ActorJWTSource struct { sizeCache protoimpl.SizeCache } -func (x *ActorJWTSource) Reset() { - *x = ActorJWTSource{} +func (x *ActorJwtSource) Reset() { + *x = ActorJwtSource{} mi := &file_ateapi_proto_msgTypes[16] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *ActorJWTSource) String() string { +func (x *ActorJwtSource) String() string { return protoimpl.X.MessageStringOf(x) } -func (*ActorJWTSource) ProtoMessage() {} +func (*ActorJwtSource) ProtoMessage() {} -func (x *ActorJWTSource) ProtoReflect() protoreflect.Message { +func (x *ActorJwtSource) ProtoReflect() protoreflect.Message { mi := &file_ateapi_proto_msgTypes[16] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) @@ -1952,19 +1952,19 @@ func (x *ActorJWTSource) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use ActorJWTSource.ProtoReflect.Descriptor instead. -func (*ActorJWTSource) Descriptor() ([]byte, []int) { +// Deprecated: Use ActorJwtSource.ProtoReflect.Descriptor instead. +func (*ActorJwtSource) Descriptor() ([]byte, []int) { return file_ateapi_proto_rawDescGZIP(), []int{16} } -func (x *ActorJWTSource) GetAudiences() []string { +func (x *ActorJwtSource) GetAudiences() []string { if x != nil { return x.Audiences } return nil } -func (x *ActorJWTSource) GetExpirationSeconds() int64 { +func (x *ActorJwtSource) GetExpirationSeconds() int64 { if x != nil { return x.ExpirationSeconds } @@ -3495,7 +3495,7 @@ type ContainerWakeupProbe struct { // http_get is the request to poll. // // +k8s:required - HttpGet *HTTPGetAction `protobuf:"bytes,1,opt,name=http_get,json=httpGet,proto3" json:"http_get,omitempty"` + HttpGet *HttpGetAction `protobuf:"bytes,1,opt,name=http_get,json=httpGet,proto3" json:"http_get,omitempty"` // timeout_seconds is how long to keep polling before giving up. // Defaults to 30 when unset. // @@ -3537,7 +3537,7 @@ func (*ContainerWakeupProbe) Descriptor() ([]byte, []int) { return file_ateapi_proto_rawDescGZIP(), []int{36} } -func (x *ContainerWakeupProbe) GetHttpGet() *HTTPGetAction { +func (x *ContainerWakeupProbe) GetHttpGet() *HttpGetAction { if x != nil { return x.HttpGet } @@ -3551,8 +3551,8 @@ func (x *ContainerWakeupProbe) GetTimeoutSeconds() int32 { return 0 } -// HTTPGetAction describes the HTTP GET the probe polls. -type HTTPGetAction struct { +// HttpGetAction describes the HTTP GET the probe polls. +type HttpGetAction struct { state protoimpl.MessageState `protogen:"open.v1"` // path must be a URL path starting with "/", using only RFC 3986 // path-segment characters, without query or fragment. @@ -3572,20 +3572,20 @@ type HTTPGetAction struct { sizeCache protoimpl.SizeCache } -func (x *HTTPGetAction) Reset() { - *x = HTTPGetAction{} +func (x *HttpGetAction) Reset() { + *x = HttpGetAction{} mi := &file_ateapi_proto_msgTypes[37] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *HTTPGetAction) String() string { +func (x *HttpGetAction) String() string { return protoimpl.X.MessageStringOf(x) } -func (*HTTPGetAction) ProtoMessage() {} +func (*HttpGetAction) ProtoMessage() {} -func (x *HTTPGetAction) ProtoReflect() protoreflect.Message { +func (x *HttpGetAction) ProtoReflect() protoreflect.Message { mi := &file_ateapi_proto_msgTypes[37] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) @@ -3597,19 +3597,19 @@ func (x *HTTPGetAction) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use HTTPGetAction.ProtoReflect.Descriptor instead. -func (*HTTPGetAction) Descriptor() ([]byte, []int) { +// Deprecated: Use HttpGetAction.ProtoReflect.Descriptor instead. +func (*HttpGetAction) Descriptor() ([]byte, []int) { return file_ateapi_proto_rawDescGZIP(), []int{37} } -func (x *HTTPGetAction) GetPath() string { +func (x *HttpGetAction) GetPath() string { if x != nil { return x.Path } return "" } -func (x *HTTPGetAction) GetPort() int32 { +func (x *HttpGetAction) GetPort() int32 { if x != nil { return x.Port } @@ -5688,8 +5688,8 @@ func (x *GetTagRequest) GetTag() *ObjectRef { return nil } -// Request message for MintActorJWT. -type MintActorJWTRequest struct { +// Request message for MintActorJwt. +type MintActorJwtRequest struct { state protoimpl.MessageState `protogen:"open.v1"` // The actor for which the JWT should be issued. // @@ -5715,20 +5715,20 @@ type MintActorJWTRequest struct { sizeCache protoimpl.SizeCache } -func (x *MintActorJWTRequest) Reset() { - *x = MintActorJWTRequest{} +func (x *MintActorJwtRequest) Reset() { + *x = MintActorJwtRequest{} mi := &file_ateapi_proto_msgTypes[75] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *MintActorJWTRequest) String() string { +func (x *MintActorJwtRequest) String() string { return protoimpl.X.MessageStringOf(x) } -func (*MintActorJWTRequest) ProtoMessage() {} +func (*MintActorJwtRequest) ProtoMessage() {} -func (x *MintActorJWTRequest) ProtoReflect() protoreflect.Message { +func (x *MintActorJwtRequest) ProtoReflect() protoreflect.Message { mi := &file_ateapi_proto_msgTypes[75] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) @@ -5740,34 +5740,34 @@ func (x *MintActorJWTRequest) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use MintActorJWTRequest.ProtoReflect.Descriptor instead. -func (*MintActorJWTRequest) Descriptor() ([]byte, []int) { +// Deprecated: Use MintActorJwtRequest.ProtoReflect.Descriptor instead. +func (*MintActorJwtRequest) Descriptor() ([]byte, []int) { return file_ateapi_proto_rawDescGZIP(), []int{75} } -func (x *MintActorJWTRequest) GetActor() *ObjectRef { +func (x *MintActorJwtRequest) GetActor() *ObjectRef { if x != nil { return x.Actor } return nil } -func (x *MintActorJWTRequest) GetAudiences() []string { +func (x *MintActorJwtRequest) GetAudiences() []string { if x != nil { return x.Audiences } return nil } -func (x *MintActorJWTRequest) GetExpirationSeconds() int64 { +func (x *MintActorJwtRequest) GetExpirationSeconds() int64 { if x != nil { return x.ExpirationSeconds } return 0 } -// Response message for MintActorJWT. -type MintActorJWTResponse struct { +// Response message for MintActorJwt. +type MintActorJwtResponse struct { state protoimpl.MessageState `protogen:"open.v1"` // The signed actor JWT. Relying parties verify it with OpenID Connect // Discovery against its issuer. @@ -5790,20 +5790,20 @@ type MintActorJWTResponse struct { sizeCache protoimpl.SizeCache } -func (x *MintActorJWTResponse) Reset() { - *x = MintActorJWTResponse{} +func (x *MintActorJwtResponse) Reset() { + *x = MintActorJwtResponse{} mi := &file_ateapi_proto_msgTypes[76] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *MintActorJWTResponse) String() string { +func (x *MintActorJwtResponse) String() string { return protoimpl.X.MessageStringOf(x) } -func (*MintActorJWTResponse) ProtoMessage() {} +func (*MintActorJwtResponse) ProtoMessage() {} -func (x *MintActorJWTResponse) ProtoReflect() protoreflect.Message { +func (x *MintActorJwtResponse) ProtoReflect() protoreflect.Message { mi := &file_ateapi_proto_msgTypes[76] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) @@ -5815,19 +5815,19 @@ func (x *MintActorJWTResponse) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use MintActorJWTResponse.ProtoReflect.Descriptor instead. -func (*MintActorJWTResponse) Descriptor() ([]byte, []int) { +// Deprecated: Use MintActorJwtResponse.ProtoReflect.Descriptor instead. +func (*MintActorJwtResponse) Descriptor() ([]byte, []int) { return file_ateapi_proto_rawDescGZIP(), []int{76} } -func (x *MintActorJWTResponse) GetActorJwt() string { +func (x *MintActorJwtResponse) GetActorJwt() string { if x != nil { return x.ActorJwt } return "" } -func (x *MintActorJWTResponse) GetExpiresAt() *timestamppb.Timestamp { +func (x *MintActorJwtResponse) GetExpiresAt() *timestamppb.Timestamp { if x != nil { return x.ExpiresAt } @@ -8669,18 +8669,18 @@ const file_ateapi_proto_rawDesc = "" + "\x05rules\x18\x02 \x03(\v2\x12.ateapi.EgressRuleR\x05rules\"\xa0\x01\n" + "\n" + "EgressRule\x12$\n" + - "\x04http\x18\x01 \x01(\v2\x10.ateapi.HTTPRuleR\x04http\x12'\n" + - "\x05https\x18\x02 \x01(\v2\x11.ateapi.HTTPSRuleR\x05https\x12C\n" + - "\x0ftls_passthrough\x18\x03 \x01(\v2\x1a.ateapi.TLSPassthroughRuleR\x0etlsPassthrough\"\x80\x01\n" + - "\bHTTPRule\x12\x1c\n" + + "\x04http\x18\x01 \x01(\v2\x10.ateapi.HttpRuleR\x04http\x12'\n" + + "\x05https\x18\x02 \x01(\v2\x11.ateapi.HttpsRuleR\x05https\x12C\n" + + "\x0ftls_passthrough\x18\x03 \x01(\v2\x1a.ateapi.TlsPassthroughRuleR\x0etlsPassthrough\"\x80\x01\n" + + "\bHttpRule\x12\x1c\n" + "\thostnames\x18\x01 \x03(\tR\thostnames\x12#\n" + "\x05ports\x18\x02 \x01(\v2\r.ateapi.PortsR\x05ports\x121\n" + "\aeffects\x18\x03 \x01(\v2\x17.ateapi.HttpRuleEffectsR\aeffects\"\x81\x01\n" + - "\tHTTPSRule\x12\x1c\n" + + "\tHttpsRule\x12\x1c\n" + "\thostnames\x18\x01 \x03(\tR\thostnames\x12#\n" + "\x05ports\x18\x02 \x01(\v2\r.ateapi.PortsR\x05ports\x121\n" + "\aeffects\x18\x03 \x01(\v2\x17.ateapi.HttpRuleEffectsR\aeffects\"W\n" + - "\x12TLSPassthroughRule\x12\x1c\n" + + "\x12TlsPassthroughRule\x12\x1c\n" + "\thostnames\x18\x01 \x03(\tR\thostnames\x12#\n" + "\x05ports\x18\x02 \x01(\v2\r.ateapi.PortsR\x05ports\"E\n" + "\x05Ports\x12\"\n" + @@ -8694,8 +8694,8 @@ const file_ateapi_proto_rawDesc = "" + "\x06header\x18\x01 \x01(\tR\x06header\x12\x16\n" + "\x06prefix\x18\x02 \x01(\tR\x06prefix\x12%\n" + "\x0ecredential_uri\x18\x03 \x01(\tR\rcredentialUri\x123\n" + - "\tactor_jwt\x18\x04 \x01(\v2\x16.ateapi.ActorJWTSourceR\bactorJwt\"]\n" + - "\x0eActorJWTSource\x12\x1c\n" + + "\tactor_jwt\x18\x04 \x01(\v2\x16.ateapi.ActorJwtSourceR\bactorJwt\"]\n" + + "\x0eActorJwtSource\x12\x1c\n" + "\taudiences\x18\x01 \x03(\tR\taudiences\x12-\n" + "\x12expiration_seconds\x18\x02 \x01(\x03R\x11expirationSeconds\"\xfa\x02\n" + "\vActorStatus\x12(\n" + @@ -8788,9 +8788,9 @@ const file_ateapi_proto_rawDesc = "" + "\x04name\x18\x01 \x01(\tR\x04name\x12\x19\n" + "\x05value\x18\x02 \x01(\tB\x03\x80\x01\x01R\x05value\"q\n" + "\x14ContainerWakeupProbe\x120\n" + - "\bhttp_get\x18\x01 \x01(\v2\x15.ateapi.HTTPGetActionR\ahttpGet\x12'\n" + + "\bhttp_get\x18\x01 \x01(\v2\x15.ateapi.HttpGetActionR\ahttpGet\x12'\n" + "\x0ftimeout_seconds\x18\x02 \x01(\x05R\x0etimeoutSeconds\"7\n" + - "\rHTTPGetAction\x12\x12\n" + + "\rHttpGetAction\x12\x12\n" + "\x04path\x18\x01 \x01(\tR\x04path\x12\x12\n" + "\x04port\x18\x02 \x01(\x05R\x04port\"\xa9\x02\n" + "\x06Volume\x12\x12\n" + @@ -8895,11 +8895,11 @@ const file_ateapi_proto_rawDesc = "" + "\aoptions\x18\x03 \x01(\v2\x15.ateapi.DeleteOptionsR\aoptions\"4\n" + "\rGetTagRequest\x12#\n" + "\x03tag\x18\x01 \x01(\v2\x11.ateapi.ObjectRefR\x03tag\"\x8b\x01\n" + - "\x13MintActorJWTRequest\x12'\n" + + "\x13MintActorJwtRequest\x12'\n" + "\x05actor\x18\x05 \x01(\v2\x11.ateapi.ObjectRefR\x05actor\x12\x1c\n" + "\taudiences\x18\x01 \x03(\tR\taudiences\x12-\n" + "\x12expiration_seconds\x18\b \x01(\x03R\x11expirationSeconds\"s\n" + - "\x14MintActorJWTResponse\x12 \n" + + "\x14MintActorJwtResponse\x12 \n" + "\tactor_jwt\x18\x01 \x01(\tB\x03\x80\x01\x01R\bactorJwt\x129\n" + "\n" + "expires_at\x18\x02 \x01(\v2\x1a.google.protobuf.TimestampR\texpiresAt\"\xa3\x01\n" + @@ -9111,7 +9111,7 @@ const file_ateapi_proto_rawDesc = "" + "\x17CreateActorEgressPolicy\x12&.ateapi.CreateActorEgressPolicyRequest\x1a\x14.ateapi.EgressPolicy\"\x00\x12Y\n" + "\x17UpdateActorEgressPolicy\x12&.ateapi.UpdateActorEgressPolicyRequest\x1a\x14.ateapi.EgressPolicy\"\x00\x12Y\n" + "\x17DeleteActorEgressPolicy\x12&.ateapi.DeleteActorEgressPolicyRequest\x1a\x14.ateapi.EgressPolicy\"\x00\x12K\n" + - "\fMintActorJWT\x12\x1b.ateapi.MintActorJWTRequest\x1a\x1c.ateapi.MintActorJWTResponse\"\x00\x12c\n" + + "\fMintActorJwt\x12\x1b.ateapi.MintActorJwtRequest\x1a\x1c.ateapi.MintActorJwtResponse\"\x00\x12c\n" + "\x14MintActorCertificate\x12#.ateapi.MintActorCertificateRequest\x1a$.ateapi.MintActorCertificateResponse\"\x00\x124\n" + "\tCreateTag\x12\x18.ateapi.CreateTagRequest\x1a\v.ateapi.Tag\"\x00\x12.\n" + "\x06GetTag\x12\x15.ateapi.GetTagRequest\x1a\v.ateapi.Tag\"\x00\x12?\n" + @@ -9181,14 +9181,14 @@ var file_ateapi_proto_goTypes = []any{ (*Actor)(nil), // 16: ateapi.Actor (*EgressPolicy)(nil), // 17: ateapi.EgressPolicy (*EgressRule)(nil), // 18: ateapi.EgressRule - (*HTTPRule)(nil), // 19: ateapi.HTTPRule - (*HTTPSRule)(nil), // 20: ateapi.HTTPSRule - (*TLSPassthroughRule)(nil), // 21: ateapi.TLSPassthroughRule + (*HttpRule)(nil), // 19: ateapi.HttpRule + (*HttpsRule)(nil), // 20: ateapi.HttpsRule + (*TlsPassthroughRule)(nil), // 21: ateapi.TlsPassthroughRule (*Ports)(nil), // 22: ateapi.Ports (*AllPorts)(nil), // 23: ateapi.AllPorts (*HttpRuleEffects)(nil), // 24: ateapi.HttpRuleEffects (*CredentialHeader)(nil), // 25: ateapi.CredentialHeader - (*ActorJWTSource)(nil), // 26: ateapi.ActorJWTSource + (*ActorJwtSource)(nil), // 26: ateapi.ActorJwtSource (*ActorStatus)(nil), // 27: ateapi.ActorStatus (*ActorCrash)(nil), // 28: ateapi.ActorCrash (*WorkerAssignment)(nil), // 29: ateapi.WorkerAssignment @@ -9209,7 +9209,7 @@ var file_ateapi_proto_goTypes = []any{ (*Capabilities)(nil), // 44: ateapi.Capabilities (*EnvVar)(nil), // 45: ateapi.EnvVar (*ContainerWakeupProbe)(nil), // 46: ateapi.ContainerWakeupProbe - (*HTTPGetAction)(nil), // 47: ateapi.HTTPGetAction + (*HttpGetAction)(nil), // 47: ateapi.HttpGetAction (*Volume)(nil), // 48: ateapi.Volume (*ImageVolumeSource)(nil), // 49: ateapi.ImageVolumeSource (*DurableDirVolumeSource)(nil), // 50: ateapi.DurableDirVolumeSource @@ -9247,8 +9247,8 @@ var file_ateapi_proto_goTypes = []any{ (*UpdateActorEgressPolicyRequest)(nil), // 82: ateapi.UpdateActorEgressPolicyRequest (*DeleteActorEgressPolicyRequest)(nil), // 83: ateapi.DeleteActorEgressPolicyRequest (*GetTagRequest)(nil), // 84: ateapi.GetTagRequest - (*MintActorJWTRequest)(nil), // 85: ateapi.MintActorJWTRequest - (*MintActorJWTResponse)(nil), // 86: ateapi.MintActorJWTResponse + (*MintActorJwtRequest)(nil), // 85: ateapi.MintActorJwtRequest + (*MintActorJwtResponse)(nil), // 86: ateapi.MintActorJwtResponse (*MintActorCertificateRequest)(nil), // 87: ateapi.MintActorCertificateRequest (*MintActorCertificateResponse)(nil), // 88: ateapi.MintActorCertificateResponse (*ListTagsRequest)(nil), // 89: ateapi.ListTagsRequest @@ -9314,17 +9314,17 @@ var file_ateapi_proto_depIdxs = []int32{ 27, // 14: ateapi.Actor.status:type_name -> ateapi.ActorStatus 14, // 15: ateapi.EgressPolicy.metadata:type_name -> ateapi.ResourceMetadata 18, // 16: ateapi.EgressPolicy.rules:type_name -> ateapi.EgressRule - 19, // 17: ateapi.EgressRule.http:type_name -> ateapi.HTTPRule - 20, // 18: ateapi.EgressRule.https:type_name -> ateapi.HTTPSRule - 21, // 19: ateapi.EgressRule.tls_passthrough:type_name -> ateapi.TLSPassthroughRule - 22, // 20: ateapi.HTTPRule.ports:type_name -> ateapi.Ports - 24, // 21: ateapi.HTTPRule.effects:type_name -> ateapi.HttpRuleEffects - 22, // 22: ateapi.HTTPSRule.ports:type_name -> ateapi.Ports - 24, // 23: ateapi.HTTPSRule.effects:type_name -> ateapi.HttpRuleEffects - 22, // 24: ateapi.TLSPassthroughRule.ports:type_name -> ateapi.Ports + 19, // 17: ateapi.EgressRule.http:type_name -> ateapi.HttpRule + 20, // 18: ateapi.EgressRule.https:type_name -> ateapi.HttpsRule + 21, // 19: ateapi.EgressRule.tls_passthrough:type_name -> ateapi.TlsPassthroughRule + 22, // 20: ateapi.HttpRule.ports:type_name -> ateapi.Ports + 24, // 21: ateapi.HttpRule.effects:type_name -> ateapi.HttpRuleEffects + 22, // 22: ateapi.HttpsRule.ports:type_name -> ateapi.Ports + 24, // 23: ateapi.HttpsRule.effects:type_name -> ateapi.HttpRuleEffects + 22, // 24: ateapi.TlsPassthroughRule.ports:type_name -> ateapi.Ports 23, // 25: ateapi.Ports.all:type_name -> ateapi.AllPorts 25, // 26: ateapi.HttpRuleEffects.replace_headers:type_name -> ateapi.CredentialHeader - 26, // 27: ateapi.CredentialHeader.actor_jwt:type_name -> ateapi.ActorJWTSource + 26, // 27: ateapi.CredentialHeader.actor_jwt:type_name -> ateapi.ActorJwtSource 4, // 28: ateapi.ActorStatus.state:type_name -> ateapi.ActorState 29, // 29: ateapi.ActorStatus.worker_assignment:type_name -> ateapi.WorkerAssignment 15, // 30: ateapi.ActorStatus.external_volumes:type_name -> ateapi.ExternalVolume @@ -9360,7 +9360,7 @@ var file_ateapi_proto_depIdxs = []int32{ 43, // 60: ateapi.Container.security_context:type_name -> ateapi.SecurityContext 35, // 61: ateapi.Container.resources:type_name -> ateapi.Resources 44, // 62: ateapi.SecurityContext.capabilities:type_name -> ateapi.Capabilities - 47, // 63: ateapi.ContainerWakeupProbe.http_get:type_name -> ateapi.HTTPGetAction + 47, // 63: ateapi.ContainerWakeupProbe.http_get:type_name -> ateapi.HttpGetAction 50, // 64: ateapi.Volume.durable_dir:type_name -> ateapi.DurableDirVolumeSource 51, // 65: ateapi.Volume.external_volume_template:type_name -> ateapi.ExternalVolumeTemplate 52, // 66: ateapi.Volume.system_info:type_name -> ateapi.SystemInfoVolumeSource @@ -9401,8 +9401,8 @@ var file_ateapi_proto_depIdxs = []int32{ 33, // 101: ateapi.DeleteActorEgressPolicyRequest.actor:type_name -> ateapi.ObjectRef 94, // 102: ateapi.DeleteActorEgressPolicyRequest.options:type_name -> ateapi.DeleteOptions 33, // 103: ateapi.GetTagRequest.tag:type_name -> ateapi.ObjectRef - 33, // 104: ateapi.MintActorJWTRequest.actor:type_name -> ateapi.ObjectRef - 132, // 105: ateapi.MintActorJWTResponse.expires_at:type_name -> google.protobuf.Timestamp + 33, // 104: ateapi.MintActorJwtRequest.actor:type_name -> ateapi.ObjectRef + 132, // 105: ateapi.MintActorJwtResponse.expires_at:type_name -> google.protobuf.Timestamp 33, // 106: ateapi.MintActorCertificateRequest.actor:type_name -> ateapi.ObjectRef 31, // 107: ateapi.ListTagsResponse.tags:type_name -> ateapi.Tag 31, // 108: ateapi.CreateTagRequest.tag:type_name -> ateapi.Tag @@ -9466,7 +9466,7 @@ var file_ateapi_proto_depIdxs = []int32{ 81, // 166: ateapi.Control.CreateActorEgressPolicy:input_type -> ateapi.CreateActorEgressPolicyRequest 82, // 167: ateapi.Control.UpdateActorEgressPolicy:input_type -> ateapi.UpdateActorEgressPolicyRequest 83, // 168: ateapi.Control.DeleteActorEgressPolicy:input_type -> ateapi.DeleteActorEgressPolicyRequest - 85, // 169: ateapi.Control.MintActorJWT:input_type -> ateapi.MintActorJWTRequest + 85, // 169: ateapi.Control.MintActorJwt:input_type -> ateapi.MintActorJwtRequest 87, // 170: ateapi.Control.MintActorCertificate:input_type -> ateapi.MintActorCertificateRequest 91, // 171: ateapi.Control.CreateTag:input_type -> ateapi.CreateTagRequest 84, // 172: ateapi.Control.GetTag:input_type -> ateapi.GetTagRequest @@ -9511,7 +9511,7 @@ var file_ateapi_proto_depIdxs = []int32{ 17, // 211: ateapi.Control.CreateActorEgressPolicy:output_type -> ateapi.EgressPolicy 17, // 212: ateapi.Control.UpdateActorEgressPolicy:output_type -> ateapi.EgressPolicy 17, // 213: ateapi.Control.DeleteActorEgressPolicy:output_type -> ateapi.EgressPolicy - 86, // 214: ateapi.Control.MintActorJWT:output_type -> ateapi.MintActorJWTResponse + 86, // 214: ateapi.Control.MintActorJwt:output_type -> ateapi.MintActorJwtResponse 88, // 215: ateapi.Control.MintActorCertificate:output_type -> ateapi.MintActorCertificateResponse 31, // 216: ateapi.Control.CreateTag:output_type -> ateapi.Tag 31, // 217: ateapi.Control.GetTag:output_type -> ateapi.Tag diff --git a/pkg/proto/ateapipb/ateapi.proto b/pkg/proto/ateapipb/ateapi.proto index bd8ca8d230..3762751f22 100644 --- a/pkg/proto/ateapipb/ateapi.proto +++ b/pkg/proto/ateapipb/ateapi.proto @@ -65,7 +65,7 @@ service Control { // Create a Substrate-issued JWT asserting the actor identity. // // * Called by the egress gateway when actor JWT injection is configured for outbound requests. - rpc MintActorJWT(MintActorJWTRequest) returns (MintActorJWTResponse) {} + rpc MintActorJwt(MintActorJwtRequest) returns (MintActorJwtResponse) {} // Create a Substrate-issued SPIFFE certificate asserting the actor identity. // @@ -522,7 +522,7 @@ message EgressRule { // // +k8s:optional // +k8s:unionMember - HTTPRule http = 1; + HttpRule http = 1; // HTTPS to the named hosts, intercepted (man-in-the-middle) so the gateway // can authorize and modify each request. HTTP/1.1 and HTTP/2 inside the @@ -530,19 +530,19 @@ message EgressRule { // // +k8s:optional // +k8s:unionMember - HTTPSRule https = 2; + HttpsRule https = 2; // TLS to the named servers, forwarded without decryption. // // +k8s:optional // +k8s:unionMember - TLSPassthroughRule tls_passthrough = 3; + TlsPassthroughRule tls_passthrough = 3; } -// HTTPRule allows cleartext HTTP and is evaluated per request on the request +// HttpRule allows cleartext HTTP and is evaluated per request on the request // authority (Host or :authority) and port. The gateway resolves the name and // dials it. -message HTTPRule { +message HttpRule { // Hostname patterns: a DNS name, or a DNS name with "*" as its leftmost // label. The rule matches when any pattern matches. // @@ -580,7 +580,7 @@ message HTTPRule { HttpRuleEffects effects = 3; } -// HTTPSRule allows HTTPS that the gateway intercepts (MITM). Every Actor behind such +// HttpsRule allows HTTPS that the gateway intercepts (MITM). Every Actor behind such // a rule must trust the gateway CA. Without it, TLS to these names fails in the // Actor. // @@ -591,9 +591,9 @@ message HTTPRule { // // Interception is for HTTP. A connection that sends no SNI does not match, // and a non-HTTP protocol inside an intercepted connection fails. -message HTTPSRule { +message HttpsRule { // Server names matched against the SNI, in the pattern syntax of - // HTTPRule.hostnames. + // HttpRule.hostnames. // // +k8s:required // +k8s:maxItems=256 @@ -613,16 +613,16 @@ message HTTPSRule { HttpRuleEffects effects = 3; } -// TLSPassthroughRule allows TLS that the gateway forwards without decryption. +// TlsPassthroughRule allows TLS that the gateway forwards without decryption. // It is evaluated once per connection at the ClientHello, on the SNI and // port, for any protocol carried over TLS. A connection that sends no SNI // does not match. Protocols that upgrade to TLS after a cleartext exchange // (STARTTLS) do not match. Because the gateway reads nothing after the // ClientHello, a passthrough rule has no effects, and a policy change reaches // an open connection only when it ends. -message TLSPassthroughRule { +message TlsPassthroughRule { // Server names matched against the SNI, in the pattern syntax of - // HTTPRule.hostnames. + // HttpRule.hostnames. // // +k8s:required // +k8s:maxItems=256 @@ -720,14 +720,14 @@ message CredentialHeader { // // +k8s:optional // +k8s:unionMember - ActorJWTSource actor_jwt = 4; + ActorJwtSource actor_jwt = 4; } -// ActorJWTSource has the gateway mint a JWT for the Actor that sent the +// ActorJwtSource has the gateway mint a JWT for the Actor that sent the // request. Relying parties verify it with OpenID Connect Discovery against its // issuer. The gateway reuses a JWT across requests while enough of its // lifetime remains, so its jti does not identify a single request. -message ActorJWTSource { +message ActorJwtSource { // The audiences the JWT is bound to, which become its aud claim. // // +k8s:required @@ -1286,7 +1286,7 @@ message ContainerWakeupProbe { // http_get is the request to poll. // // +k8s:required - HTTPGetAction http_get = 1; + HttpGetAction http_get = 1; // timeout_seconds is how long to keep polling before giving up. // Defaults to 30 when unset. @@ -1297,8 +1297,8 @@ message ContainerWakeupProbe { int32 timeout_seconds = 2; } -// HTTPGetAction describes the HTTP GET the probe polls. -message HTTPGetAction { +// HttpGetAction describes the HTTP GET the probe polls. +message HttpGetAction { // path must be a URL path starting with "/", using only RFC 3986 // path-segment characters, without query or fragment. // Defaults to "/" when unset. @@ -1782,8 +1782,8 @@ message GetTagRequest { ObjectRef tag = 1; } -// Request message for MintActorJWT. -message MintActorJWTRequest { +// Request message for MintActorJwt. +message MintActorJwtRequest { // The actor for which the JWT should be issued. // // Must be a valid actor that currently exists according to the actor store. @@ -1808,8 +1808,8 @@ message MintActorJWTRequest { int64 expiration_seconds = 8; } -// Response message for MintActorJWT. -message MintActorJWTResponse { +// Response message for MintActorJwt. +message MintActorJwtResponse { // The signed actor JWT. Relying parties verify it with OpenID Connect // Discovery against its issuer. // diff --git a/pkg/proto/ateapipb/ateapi_grpc.pb.go b/pkg/proto/ateapipb/ateapi_grpc.pb.go index 6242d56047..b43217ac5d 100644 --- a/pkg/proto/ateapipb/ateapi_grpc.pb.go +++ b/pkg/proto/ateapipb/ateapi_grpc.pb.go @@ -45,7 +45,7 @@ const ( Control_CreateActorEgressPolicy_FullMethodName = "/ateapi.Control/CreateActorEgressPolicy" Control_UpdateActorEgressPolicy_FullMethodName = "/ateapi.Control/UpdateActorEgressPolicy" Control_DeleteActorEgressPolicy_FullMethodName = "/ateapi.Control/DeleteActorEgressPolicy" - Control_MintActorJWT_FullMethodName = "/ateapi.Control/MintActorJWT" + Control_MintActorJwt_FullMethodName = "/ateapi.Control/MintActorJwt" Control_MintActorCertificate_FullMethodName = "/ateapi.Control/MintActorCertificate" Control_CreateTag_FullMethodName = "/ateapi.Control/CreateTag" Control_GetTag_FullMethodName = "/ateapi.Control/GetTag" @@ -114,7 +114,7 @@ type ControlClient interface { // Create a Substrate-issued JWT asserting the actor identity. // // * Called by the egress gateway when actor JWT injection is configured for outbound requests. - MintActorJWT(ctx context.Context, in *MintActorJWTRequest, opts ...grpc.CallOption) (*MintActorJWTResponse, error) + MintActorJwt(ctx context.Context, in *MintActorJwtRequest, opts ...grpc.CallOption) (*MintActorJwtResponse, error) // Create a Substrate-issued SPIFFE certificate asserting the actor identity. // // SPIFFE URI: spiffe://${trustdomain}/actor/${atespace}/${actor} @@ -319,10 +319,10 @@ func (c *controlClient) DeleteActorEgressPolicy(ctx context.Context, in *DeleteA return out, nil } -func (c *controlClient) MintActorJWT(ctx context.Context, in *MintActorJWTRequest, opts ...grpc.CallOption) (*MintActorJWTResponse, error) { +func (c *controlClient) MintActorJwt(ctx context.Context, in *MintActorJwtRequest, opts ...grpc.CallOption) (*MintActorJwtResponse, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) - out := new(MintActorJWTResponse) - err := c.cc.Invoke(ctx, Control_MintActorJWT_FullMethodName, in, out, cOpts...) + out := new(MintActorJwtResponse) + err := c.cc.Invoke(ctx, Control_MintActorJwt_FullMethodName, in, out, cOpts...) if err != nil { return nil, err } @@ -656,7 +656,7 @@ type ControlServer interface { // Create a Substrate-issued JWT asserting the actor identity. // // * Called by the egress gateway when actor JWT injection is configured for outbound requests. - MintActorJWT(context.Context, *MintActorJWTRequest) (*MintActorJWTResponse, error) + MintActorJwt(context.Context, *MintActorJwtRequest) (*MintActorJwtResponse, error) // Create a Substrate-issued SPIFFE certificate asserting the actor identity. // // SPIFFE URI: spiffe://${trustdomain}/actor/${atespace}/${actor} @@ -777,8 +777,8 @@ func (UnimplementedControlServer) UpdateActorEgressPolicy(context.Context, *Upda func (UnimplementedControlServer) DeleteActorEgressPolicy(context.Context, *DeleteActorEgressPolicyRequest) (*EgressPolicy, error) { return nil, status.Error(codes.Unimplemented, "method DeleteActorEgressPolicy not implemented") } -func (UnimplementedControlServer) MintActorJWT(context.Context, *MintActorJWTRequest) (*MintActorJWTResponse, error) { - return nil, status.Error(codes.Unimplemented, "method MintActorJWT not implemented") +func (UnimplementedControlServer) MintActorJwt(context.Context, *MintActorJwtRequest) (*MintActorJwtResponse, error) { + return nil, status.Error(codes.Unimplemented, "method MintActorJwt not implemented") } func (UnimplementedControlServer) MintActorCertificate(context.Context, *MintActorCertificateRequest) (*MintActorCertificateResponse, error) { return nil, status.Error(codes.Unimplemented, "method MintActorCertificate not implemented") @@ -1104,20 +1104,20 @@ func _Control_DeleteActorEgressPolicy_Handler(srv interface{}, ctx context.Conte return interceptor(ctx, in, info, handler) } -func _Control_MintActorJWT_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(MintActorJWTRequest) +func _Control_MintActorJwt_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(MintActorJwtRequest) if err := dec(in); err != nil { return nil, err } if interceptor == nil { - return srv.(ControlServer).MintActorJWT(ctx, in) + return srv.(ControlServer).MintActorJwt(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: Control_MintActorJWT_FullMethodName, + FullMethod: Control_MintActorJwt_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(ControlServer).MintActorJWT(ctx, req.(*MintActorJWTRequest)) + return srv.(ControlServer).MintActorJwt(ctx, req.(*MintActorJwtRequest)) } return interceptor(ctx, in, info, handler) } @@ -1700,8 +1700,8 @@ var Control_ServiceDesc = grpc.ServiceDesc{ Handler: _Control_DeleteActorEgressPolicy_Handler, }, { - MethodName: "MintActorJWT", - Handler: _Control_MintActorJWT_Handler, + MethodName: "MintActorJwt", + Handler: _Control_MintActorJwt_Handler, }, { MethodName: "MintActorCertificate", From d6bcb91a365f1b4a5fcec3ea034fd714b8c74989 Mon Sep 17 00:00:00 2001 From: Jeff Luo Date: Tue, 13 Oct 2026 14:56:23 +0000 Subject: [PATCH 03/42] Health probes move to their own server on :9091, and :9090 serves only /metrics (#2424) ateapi, atelet, atenet-router, atenet-egress, and the kubernetes-secrets credential provider served `/readyz` and `/healthz` on the Prometheus `/metrics` server on `:9090`. A later change lets `OTEL_METRICS_EXPORTER` turn off `/metrics`, and that change must not stop the kubelet probes. This PR moves the probes to a separate health server. ## Breaking change The health probes move from `:9090` to `:9091` (`/readyz`, `/healthz`). `:9090` now serves only `/metrics`. If you use your own manifests, change your probes to the new `health` port. Apply the new images and manifests together. atelet no longer has `hostPort: 9090`. ## Testing - `go test -race` passes for `internal/serverboot`, `cmd/ate-setup/internal/steps`, `cmd/ateapi`, `cmd/atelet`, `cmd/atenet/internal/router`, and `cmd/credential-provider/kubernetes-secrets`. - All the changed binaries build. Part of #2067 > It's a good idea to open an issue first for discussion. - [x] Tests pass - [x] Appropriate changes to documentation are included in the PR --- cmd/ate-setup/internal/steps/cloudsql.go | 3 +- cmd/ateapi/main.go | 10 +- cmd/atelet/main.go | 8 +- cmd/atenet/internal/router/cmd.go | 3 +- cmd/atenet/internal/router/config.go | 1 + cmd/atenet/internal/router/router.go | 9 +- cmd/atenet/internal/router/status_test.go | 1 + cmd/ateom-gvisor/main.go | 4 +- cmd/ateom-microvm/main.go | 4 +- cmd/benchmarking/glutton/main.go | 5 +- .../kubernetes-secrets/main.go | 15 +-- hack/verify-atenet-drain.sh | 12 +-- internal/serverboot/serverboot.go | 93 ++++++++++--------- internal/serverboot/serverboot_test.go | 60 ++++++------ manifests/ate-install/ate-api-server.yaml | 10 +- manifests/ate-install/atelet.yaml | 8 +- manifests/ate-install/atenet-egress.yaml | 8 +- manifests/ate-install/atenet-router.yaml | 6 +- .../cloudsql/proxy-sidecar-patch.yaml | 3 +- .../k8s-credential-provider.yaml | 5 +- 20 files changed, 143 insertions(+), 125 deletions(-) diff --git a/cmd/ate-setup/internal/steps/cloudsql.go b/cmd/ate-setup/internal/steps/cloudsql.go index 651858633a..d2b0c9892d 100644 --- a/cmd/ate-setup/internal/steps/cloudsql.go +++ b/cmd/ate-setup/internal/steps/cloudsql.go @@ -180,7 +180,8 @@ func cloudSQLDSN(s cloudSQLSettings) (string, error) { // sidecar's configuration, and nothing else. It is empty without Cloud SQL, // which prunes the keys a previous Cloud SQL install left behind. // -// The health checks listen on 9801 because ateapi's metrics own 9090. +// The health checks listen on 9801 because ateapi's metrics and health servers +// own 9090 and 9091. func cloudSQLEnvVars(s cloudSQLSettings) map[string]string { if s.Instance == "" { return map[string]string{} diff --git a/cmd/ateapi/main.go b/cmd/ateapi/main.go index d1896c2b3b..ab3580bbcc 100644 --- a/cmd/ateapi/main.go +++ b/cmd/ateapi/main.go @@ -69,6 +69,7 @@ const minResyncInterval = 250 * time.Millisecond var ( listenAddr = pflag.String("grpc-listen-addr", ":443", "Address and port the gRPC server should listen on.") metricsListenAddr = pflag.String("metrics-listen-addr", ":9090", "Address and port the prometheus metrics server should listen on.") + healthListenAddr = pflag.String("health-listen-addr", ":9091", "Address and port the /readyz and /healthz probe server should listen on.") grpcServerCredBundle = pflag.String("grpc-server-cred-bundle", "", "File with the server TLS credential bundle.") authenticationConfigFile = pflag.String("authentication-config", "", "YAML file configuring trusted JWT providers.") @@ -149,6 +150,9 @@ func main() { serverboot.Fatal(ctx, "Failed to initialize metrics", err) } defer serverboot.ShutdownProvider("MeterProvider", mp.Shutdown) + // The probes have their own server, so /metrics starts here and serves + // during the store connect and the schema migrations. + go serverboot.StartMetricsServer(ctx, *metricsListenAddr) lp, err := serverboot.InitLogging(ctx, serverboot.LoggingOptions{ ServiceName: "ateapi", @@ -348,11 +352,7 @@ func main() { ateapipb.RegisterWorkerServiceServer(mux, workerservice.New(persistence, controlSrv, ateletSPIFFEID, actorIDCAPool)) readiness := &serverboot.Readiness{} - go serverboot.StartMetricsServer(ctx, serverboot.MetricsServerOptions{ - Addr: *metricsListenAddr, - Readiness: readiness, - EnableHealthz: true, - }) + go serverboot.StartHealthServer(ctx, *healthListenAddr, readiness) drainDone := drainOnShutdown(shutdownCtx, mux, readiness) diff --git a/cmd/atelet/main.go b/cmd/atelet/main.go index c6e63d32b9..6338b03f65 100644 --- a/cmd/atelet/main.go +++ b/cmd/atelet/main.go @@ -87,6 +87,7 @@ import ( var ( port = pflag.Int("port", atelet.DefaultPort, "The port to listen on") metricsListenAddr = pflag.String("metrics-listen-addr", ":9090", "Address and port the prometheus metrics server should listen on.") + healthListenAddr = pflag.String("health-listen-addr", ":9091", "Address and port the /readyz and /healthz probe server should listen on.") grpcServerCredBundle = pflag.String("grpc-server-cred-bundle", "/run/podidentity.podcert.ate.dev/credential-bundle.pem", "Credential bundle atelet presents as its gRPC serving certificate.") clientCACerts = pflag.String("client-ca-certs", "/run/podidentity.podcert.ate.dev/trust-bundle.pem", "CA bundle used to verify gRPC client certificates.") @@ -168,11 +169,8 @@ func main() { // readiness flips to not-ready on SIGTERM so /readyz reports 503 while the // pod drains, while /healthz stays 200 for liveness. readiness := &serverboot.Readiness{} - go serverboot.StartMetricsServer(ctx, serverboot.MetricsServerOptions{ - Addr: *metricsListenAddr, - Readiness: readiness, - EnableHealthz: true, - }) + go serverboot.StartMetricsServer(ctx, *metricsListenAddr) + go serverboot.StartHealthServer(ctx, *healthListenAddr, readiness) // The OTLP relay lets the ateom pods on this node export telemetry over a // unix socket instead of their own network (see internal/otlprelay). Started diff --git a/cmd/atenet/internal/router/cmd.go b/cmd/atenet/internal/router/cmd.go index b10ec9ad2d..d97dd3dbcb 100644 --- a/cmd/atenet/internal/router/cmd.go +++ b/cmd/atenet/internal/router/cmd.go @@ -54,6 +54,7 @@ func bindRouterFlags(fs *pflag.FlagSet, cfg *routerConfig) { fs.StringVar(&cfg.LogLevel, "log-level", "info", "Log level: debug, info, warn, error") fs.StringVar(&cfg.MetricsAddr, "metrics-listen-addr", ":9090", "Address and port the prometheus metrics server should listen on.") + fs.StringVar(&cfg.HealthAddr, "health-listen-addr", ":9091", "Address and port the /readyz and /healthz probe server should listen on.") fs.StringVar(&cfg.AtenetRouter, "atenet-dataplane", string(atenetRouterEnvoy), "Atenet ingress and egress dataplane: envoy or agentgateway") fs.StringVar(&cfg.Namespace, "namespace", "default", "Target operations namespace") fs.StringVar(&cfg.RouterServiceName, "router-service-name", installdefaults.RouterServiceName, "Service name of this atenet-router in the operations namespace. Override when the deployment renames the Service.") @@ -64,7 +65,7 @@ func bindRouterFlags(fs *pflag.FlagSet, cfg *routerConfig) { fs.IntVar(&cfg.ConnectTLSPort, "port-connect-tls", 8444, "TCP port for CONNECT-tunneled traffic entering through the router dataplane over TLS. --port-https also defaults to 8443, and both listeners are commonly enabled at once, so --port-connect-tls defaults to a different port (8444) rather than colliding with it") fs.IntVar(&cfg.XdsPort, "port-xds", 18000, "TCP port listening for the xDS dynamic Envoy connections") fs.IntVar(&cfg.ExtprocPort, "port-extproc", 50051, "Listen port for the External Processing (ext_proc) server the dataplane calls") - fs.StringVar(&cfg.ExtprocAddr, "extproc-address", "127.0.0.1", "Address of the External Processing (ext_proc) server: both the address it binds and the address the co-located dataplane is told to dial. Defaults to loopback, which keeps it unreachable from other pods; readiness is probed via /readyz on the metrics port, not this one. Empty binds every interface") + fs.StringVar(&cfg.ExtprocAddr, "extproc-address", "127.0.0.1", "Address of the External Processing (ext_proc) server: both the address it binds and the address the co-located dataplane is told to dial. Defaults to loopback, which keeps it unreachable from other pods; readiness is probed via /readyz on the health port, not this one. Empty binds every interface") fs.IntVar(&cfg.StatusPort, "status-port", 4040, "Port to serve /statusz on (set <= 0 to disable serving status)") fs.DurationVar(&cfg.HealthInterval, "health-interval", 1*time.Second, "Interval for checking health of dependent services") fs.IntVar(&cfg.HttpsPort, "port-https", 8443, "TCP port for HTTPS workload traffic entering through the router dataplane") diff --git a/cmd/atenet/internal/router/config.go b/cmd/atenet/internal/router/config.go index dc3aa927d0..81dd399d0e 100644 --- a/cmd/atenet/internal/router/config.go +++ b/cmd/atenet/internal/router/config.go @@ -195,6 +195,7 @@ type routerConfig struct { LogLevel string MetricsAddr string + HealthAddr string // OtlpCollectorAddress is the OTLP gRPC collector that Envoy reports // tracing spans to, as host:port or an http:// URL. It defaults to // OTEL_EXPORTER_OTLP_ENDPOINT — Envoy gets its whole configuration over diff --git a/cmd/atenet/internal/router/router.go b/cmd/atenet/internal/router/router.go index 36ac759664..d2c7477f69 100644 --- a/cmd/atenet/internal/router/router.go +++ b/cmd/atenet/internal/router/router.go @@ -158,11 +158,8 @@ func (s *RouterServer) Run(ctx context.Context) error { // pod drains — dropping it from the Service endpoints — while /healthz // stays 200 for liveness. readiness := &serverboot.Readiness{} - go serverboot.StartMetricsServer(ctx, serverboot.MetricsServerOptions{ - Addr: s.cfg.MetricsAddr, - Readiness: readiness, - EnableHealthz: true, - }) + go serverboot.StartMetricsServer(ctx, s.cfg.MetricsAddr) + go serverboot.StartHealthServer(ctx, s.cfg.HealthAddr, readiness) dialOpts, err := ateapiauth.DialOptions(ateapiauth.ClientConfig{ K8sClient: s.clientset, @@ -285,7 +282,7 @@ func (s *RouterServer) Run(ctx context.Context) error { // co-located in the same pod and always dials over loopback, so binding // loopback keeps the ext_proc server unreachable from other pods on the // flat pod network, where nothing authenticates the caller. Readiness is - // probed via /readyz on the metrics port, not this one, so a loopback bind + // probed via /readyz on the health port, not this one, so a loopback bind // does not break it. An empty address binds every interface. extprocListenAddr := net.JoinHostPort(s.cfg.ExtprocAddr, strconv.Itoa(s.cfg.ExtprocPort)) slog.InfoContext(ctx, "Starting ExtProc Server", slog.String("address", extprocListenAddr)) diff --git a/cmd/atenet/internal/router/status_test.go b/cmd/atenet/internal/router/status_test.go index 6fccee90d6..8e372a6b26 100644 --- a/cmd/atenet/internal/router/status_test.go +++ b/cmd/atenet/internal/router/status_test.go @@ -67,6 +67,7 @@ func TestStatuszEndpoint(t *testing.T) { ExtprocPort: 50051, ExtProcMaxRequests: defaultExtProcMaxRequests, MetricsAddr: "127.0.0.1:0", + HealthAddr: "127.0.0.1:0", ParkedRequest: ingress.DefaultParkedRequestConfig(), Auth: authConfig{ AteapiCAFile: caPath, diff --git a/cmd/ateom-gvisor/main.go b/cmd/ateom-gvisor/main.go index c7215fbeb3..b5b3229815 100644 --- a/cmd/ateom-gvisor/main.go +++ b/cmd/ateom-gvisor/main.go @@ -68,7 +68,7 @@ var ( tunnelConfig = ateomtunnel.RegisterFlags(pflag.CommandLine) - readinessListenAddress = pflag.String("readiness-listen-address", "0.0.0.0:8080", "Address for HTTP readiness checks") + readinessListenAddress = pflag.String("readiness-listen-address", "0.0.0.0:8080", "Address for the HTTP /readyz and /healthz probes") maxActors = pflag.Int("max-actors", 1000, "How many actors this worker will host at once") usageSampleInterval = pflag.Duration("usage-sample-interval", time.Minute, "How often the ateom samples the resource usage of its actors. Each sample writes an ate.actor.usage_sampled record per actor, and GetActiveWorkloadStats serves the latest.") @@ -272,7 +272,7 @@ func do(ctx context.Context) error { } }() - go serverboot.StartReadinessServer(ctx, *readinessListenAddress, readiness) + go serverboot.StartHealthServer(ctx, *readinessListenAddress, readiness) if err := svr.Serve(lis); err != nil { return fmt.Errorf("while serving: %w", err) diff --git a/cmd/ateom-microvm/main.go b/cmd/ateom-microvm/main.go index 36bde56802..9a85d85ac2 100644 --- a/cmd/ateom-microvm/main.go +++ b/cmd/ateom-microvm/main.go @@ -74,7 +74,7 @@ var ( tunnelConfig = ateomtunnel.RegisterFlags(pflag.CommandLine) - readinessListenAddress = pflag.String("readiness-listen-address", "0.0.0.0:8080", "Address for HTTP readiness checks") + readinessListenAddress = pflag.String("readiness-listen-address", "0.0.0.0:8080", "Address for the HTTP /readyz and /healthz probes") maxActors = pflag.Int("max-actors", 1000, "How many actors this worker will host at once") usageSampleInterval = pflag.Duration("usage-sample-interval", time.Minute, "How often the ateom samples the resource usage of its actors. Each sample writes an ate.actor.usage_sampled record per actor, and GetActiveWorkloadStats serves the latest.") ) @@ -290,7 +290,7 @@ func do(ctx context.Context) error { } }() - go serverboot.StartReadinessServer(ctx, *readinessListenAddress, readiness) + go serverboot.StartHealthServer(ctx, *readinessListenAddress, readiness) slog.InfoContext(ctx, "ateom-microvm serving", slog.String("socket", sockPath)) if err := svr.Serve(lis); err != nil { diff --git a/cmd/benchmarking/glutton/main.go b/cmd/benchmarking/glutton/main.go index 901d175789..8f5d8fd09c 100644 --- a/cmd/benchmarking/glutton/main.go +++ b/cmd/benchmarking/glutton/main.go @@ -93,10 +93,7 @@ func main() { serverboot.Fatal(ctx, "Failed to start listener", fmt.Errorf("%s: %w", *listenAddr, err)) } - go serverboot.StartMetricsServer(ctx, serverboot.MetricsServerOptions{ - Addr: *metricsListenAddr, - Readiness: &serverboot.Readiness{}, - }) + go serverboot.StartMetricsServer(ctx, *metricsListenAddr) slog.InfoContext(ctx, "glutton starting", slog.String("listen-addr", *listenAddr), diff --git a/cmd/credential-provider/kubernetes-secrets/main.go b/cmd/credential-provider/kubernetes-secrets/main.go index 5960919614..b88463474c 100644 --- a/cmd/credential-provider/kubernetes-secrets/main.go +++ b/cmd/credential-provider/kubernetes-secrets/main.go @@ -50,7 +50,8 @@ const serviceName = "credprovider" var ( listenAddr = pflag.String("listen-address", ":50051", "gRPC listen address") - metricsAddr = pflag.String("metrics-address", ":9090", "Prometheus/health HTTP listen address") + metricsAddr = pflag.String("metrics-address", ":9090", "Prometheus /metrics HTTP listen address") + healthAddr = pflag.String("health-address", ":9091", "/readyz and /healthz HTTP listen address") statusAddr = pflag.String("status-address", ":4040", "/statusz HTTP listen address; empty disables the page") serverBundle = pflag.String("server-cred-bundle", "", "credential bundle (PEM key+chain) presented for serving TLS (required)") clientCAFile = pflag.String("client-ca-file", "", "CA bundle that caller (injector) client certificates must chain to (required)") @@ -88,12 +89,7 @@ func run(ctx context.Context) error { } defer serverboot.ShutdownProvider("MeterProvider", mp.Shutdown) - readiness := &serverboot.Readiness{} - go serverboot.StartMetricsServer(ctx, serverboot.MetricsServerOptions{ - Addr: *metricsAddr, - Readiness: readiness, - EnableHealthz: true, - }) + go serverboot.StartMetricsServer(ctx, *metricsAddr) client, err := newKubeClient() if err != nil { @@ -129,6 +125,11 @@ func run(ctx context.Context) error { return fmt.Errorf("listen on %s: %w", *listenAddr, err) } + // The health server starts after the gRPC listener opens, so /readyz does + // not report 200 before the pod can accept calls. + readiness := &serverboot.Readiness{} + go serverboot.StartHealthServer(ctx, *healthAddr, readiness) + shutdownCtx, stop := signal.NotifyContext(ctx, syscall.SIGINT, syscall.SIGTERM) defer stop() diff --git a/hack/verify-atenet-drain.sh b/hack/verify-atenet-drain.sh index 0f9368de6e..f7dabd00bd 100755 --- a/hack/verify-atenet-drain.sh +++ b/hack/verify-atenet-drain.sh @@ -55,7 +55,7 @@ SUFFIX="$(date +%s)" ACTOR_BUSY="busy-${SUFFIX}" # occupies the only worker ACTOR_PARKED="parked-${SUFFIX}" # its request parks, then survives the drain LOCAL_HTTP_PORT="${LOCAL_HTTP_PORT:-18080}" -LOCAL_METRICS_PORT="${LOCAL_METRICS_PORT:-19090}" +LOCAL_HEALTH_PORT="${LOCAL_HEALTH_PORT:-19091}" # Xs must be the trailing characters: BSD (macOS) mktemp rejects suffixes. LOG_FILE="$(mktemp /tmp/atenet-drain-check-log.XXXXXX)" CURL_OUT="$(mktemp /tmp/atenet-drain-check-out.XXXXXX)" @@ -101,10 +101,10 @@ run_kubectl_ate get actor-template "${DEMO_POOL}" -a "${ATESPACE}" -o json 2>/de if run_kubectl_ate get workers 2>/dev/null | awk 'NR>1 && $4=="ASSIGNED"' | grep -q .; then fail "workers on the ${DEMO_POOL} pool are ASSIGNED; the check scales the pool to 1 and would crash running actors — suspend them first" fi -for port in "${LOCAL_HTTP_PORT}" "${LOCAL_METRICS_PORT}"; do +for port in "${LOCAL_HTTP_PORT}" "${LOCAL_HEALTH_PORT}"; do if (exec 3<>"/dev/tcp/127.0.0.1/${port}") 2>/dev/null; then exec 3>&- 3<&- - fail "local port ${port} is already in use (a stale port-forward?); free it or set LOCAL_HTTP_PORT/LOCAL_METRICS_PORT" + fail "local port ${port} is already in use (a stale port-forward?); free it or set LOCAL_HTTP_PORT/LOCAL_HEALTH_PORT" fi done @@ -137,12 +137,12 @@ run_kubectl logs -n "${ROUTER_NS}" "${POD}" -c atenet-router -f > "${LOG_FILE}" BG_PIDS+=($!) run_kubectl port-forward -n "${ROUTER_NS}" svc/atenet-router "${LOCAL_HTTP_PORT}:80" >/dev/null 2>&1 & BG_PIDS+=($!) -run_kubectl port-forward -n "${ROUTER_NS}" "${POD}" "${LOCAL_METRICS_PORT}:9090" >/dev/null 2>&1 & +run_kubectl port-forward -n "${ROUTER_NS}" "${POD}" "${LOCAL_HEALTH_PORT}:9091" >/dev/null 2>&1 & BG_PIDS+=($!) sleep 3 -readyz() { curl -s -o /dev/null -w '%{http_code}' "localhost:${LOCAL_METRICS_PORT}/readyz"; } -healthz() { curl -s -o /dev/null -w '%{http_code}' "localhost:${LOCAL_METRICS_PORT}/healthz"; } +readyz() { curl -s -o /dev/null -w '%{http_code}' "localhost:${LOCAL_HEALTH_PORT}/readyz"; } +healthz() { curl -s -o /dev/null -w '%{http_code}' "localhost:${LOCAL_HEALTH_PORT}/healthz"; } # The port-forwards need a moment to come up; retry before judging. for i in $(seq 1 20); do diff --git a/internal/serverboot/serverboot.go b/internal/serverboot/serverboot.go index 43eb34accf..652a44ce62 100644 --- a/internal/serverboot/serverboot.go +++ b/internal/serverboot/serverboot.go @@ -14,8 +14,9 @@ // Package serverboot collects the startup boilerplate shared by the // long-running substrate server binaries (ateapi, atelet, ateom-gvisor, -// ateom-microvm): slog wiring, OTel tracer + meter providers, a Prometheus + -// /readyz HTTP surface, and a couple of small helpers for startup fail-fast. +// ateom-microvm): slog wiring, OTel tracer + meter providers, a Prometheus +// /metrics server, a /readyz and /healthz server for the kubelet probes, and a +// couple of small helpers for startup fail-fast. package serverboot import ( @@ -27,6 +28,7 @@ import ( "net/http" "os" "sync/atomic" + "time" "github.com/agent-substrate/substrate/internal/ateattr" "github.com/agent-substrate/substrate/internal/contextlogging" @@ -386,64 +388,69 @@ func (r *Readiness) MarkNotReady() { r.notReady.Store(true) } // Ready reports whether /readyz returns 200. func (r *Readiness) Ready() bool { return !r.notReady.Load() } -// MetricsServerOptions configures StartMetricsServer. -type MetricsServerOptions struct { - // Addr is the TCP listen address (e.g. ":9090"). - Addr string - // Readiness, if non-nil, enables a /readyz handler: 200 while - // Ready, 503 after MarkNotReady. A zero-value Readiness never - // flips, giving a static 200 for binaries with no drain sequence. - // Nil serves no /readyz at all. - Readiness *Readiness - // EnableHealthz adds an always-200 /healthz for liveness probes, - // which must keep succeeding while a draining server fails /readyz. - EnableHealthz bool +// httpServerReadHeaderTimeout bounds how long a client can take to send the +// request headers, so a slow client cannot hold a connection open forever. +const httpServerReadHeaderTimeout = 10 * time.Second + +// StartMetricsServer runs an HTTP server exposing only /metrics +// (Prometheus). Blocks until the server stops; designed to be `go`-launched. +// The kubelet probes use StartHealthServer, so a change to the metrics +// endpoint does not change the health checks. A serve failure exits the +// process: the probes no longer report a metrics server that cannot bind, so +// without the exit the only sign of the failure is one log line. +func StartMetricsServer(ctx context.Context, addr string) { + slog.InfoContext(ctx, "Starting metrics HTTP server", slog.String("addr", addr)) + if err := listenAndServe(addr, metricsMux()); err != nil { + slog.Error("Metrics HTTP server failed", slog.Any("err", err)) + os.Exit(1) + } } -// StartMetricsServer runs an HTTP server exposing /metrics (Prometheus) -// and optionally /readyz and /healthz. Blocks until http.ListenAndServe -// returns; designed to be `go`-launched. -func StartMetricsServer(ctx context.Context, opts MetricsServerOptions) { - slog.InfoContext(ctx, "Starting metrics HTTP server", slog.String("addr", opts.Addr)) - if err := http.ListenAndServe(opts.Addr, metricsMux(opts)); err != nil { - slog.Error("Failed to start prometheus metrics server", slog.Any("err", err)) +// StartHealthServer runs an HTTP server exposing /readyz and /healthz for the +// kubelet probes. /readyz returns 200 while readiness is Ready and 503 after +// MarkNotReady. /healthz always returns 200, so liveness keeps succeeding +// while a draining server fails readiness. Blocks until the server stops; +// designed to be `go`-launched. A serve failure exits the process: a pod whose +// probes can never succeed never turns Ready, so dying loudly lets the kubelet +// restart it instead of leaving a pod that looks alive but never gets work. +func StartHealthServer(ctx context.Context, addr string, readiness *Readiness) { + slog.InfoContext(ctx, "Starting health HTTP server", slog.String("addr", addr)) + if err := listenAndServe(addr, healthMux(readiness)); err != nil { + slog.Error("Health HTTP server failed", slog.Any("err", err)) + os.Exit(1) } } -// StartReadinessServer runs an HTTP server exposing only /readyz. Blocks until -// http.ListenAndServe returns; designed to be `go`-launched. A serve failure -// exits the process: a worker whose readiness endpoint cannot come up never -// turns Ready and never registers, so dying loudly lets the kubelet restart it -// instead of leaving a pod that looks alive but can never receive work. -func StartReadinessServer(ctx context.Context, addr string, readiness *Readiness) { - slog.InfoContext(ctx, "Starting readiness HTTP server", slog.String("addr", addr)) - if err := http.ListenAndServe(addr, readinessMux(readiness)); err != nil { - slog.Error("Readiness HTTP server failed", slog.Any("err", err)) - os.Exit(1) +func listenAndServe(addr string, handler http.Handler) error { + return newHTTPServer(addr, handler).ListenAndServe() +} + +// newHTTPServer builds the server for StartMetricsServer and +// StartHealthServer; split out so tests can check its timeouts. +func newHTTPServer(addr string, handler http.Handler) *http.Server { + return &http.Server{ + Addr: addr, + Handler: handler, + ReadHeaderTimeout: httpServerReadHeaderTimeout, } } // metricsMux builds the handler for StartMetricsServer; split out so // tests can exercise the endpoints without binding a port. -func metricsMux(opts MetricsServerOptions) *http.ServeMux { +func metricsMux() *http.ServeMux { mux := http.NewServeMux() mux.Handle("/metrics", promhttp.Handler()) - if opts.Readiness != nil { - mux.Handle("/readyz", readinessHandler(opts.Readiness)) - } - if opts.EnableHealthz { - mux.HandleFunc("/healthz", func(w http.ResponseWriter, _ *http.Request) { - w.WriteHeader(http.StatusOK) - _, _ = w.Write([]byte("ok")) - }) - } return mux } -// readinessMux builds the handler for StartReadinessServer. -func readinessMux(readiness *Readiness) *http.ServeMux { +// healthMux builds the handler for StartHealthServer. +func healthMux(readiness *Readiness) *http.ServeMux { mux := http.NewServeMux() mux.Handle("/readyz", readinessHandler(readiness)) + mux.HandleFunc("/healthz", func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte("ok")) + }) return mux } diff --git a/internal/serverboot/serverboot_test.go b/internal/serverboot/serverboot_test.go index a7b41f2526..1ee6ab44c5 100644 --- a/internal/serverboot/serverboot_test.go +++ b/internal/serverboot/serverboot_test.go @@ -179,10 +179,7 @@ func TestMeterProviderRelayAttribute(t *testing.T) { func TestReadyzDrainsWhileHealthzStaysUp(t *testing.T) { readiness := &Readiness{} - mux := metricsMux(MetricsServerOptions{ - Readiness: readiness, - EnableHealthz: true, - }) + mux := healthMux(readiness) if got := getCode(t, mux, "/readyz"); got != http.StatusOK { t.Errorf("/readyz before drain = %d, want %d", got, http.StatusOK) @@ -202,40 +199,45 @@ func TestReadyzDrainsWhileHealthzStaysUp(t *testing.T) { } func TestReadyzStaticWithZeroValueReadiness(t *testing.T) { - mux := metricsMux(MetricsServerOptions{Readiness: &Readiness{}}) + mux := healthMux(&Readiness{}) if got := getCode(t, mux, "/readyz"); got != http.StatusOK { t.Errorf("/readyz with zero-value Readiness = %d, want %d", got, http.StatusOK) } } -func TestReadinessMux(t *testing.T) { - readiness := &Readiness{} - mux := readinessMux(readiness) - - if got := getCode(t, mux, "/readyz"); got != http.StatusOK { - t.Errorf("/readyz before drain = %d, want %d", got, http.StatusOK) - } - if got := getCode(t, mux, "/metrics"); got != http.StatusNotFound { - t.Errorf("/metrics = %d, want %d", got, http.StatusNotFound) - } - - readiness.MarkNotReady() - if got := getCode(t, mux, "/readyz"); got != http.StatusServiceUnavailable { - t.Errorf("/readyz during drain = %d, want %d", got, http.StatusServiceUnavailable) +// The probes and /metrics live on separate servers, so turning one off can +// never take the other with it. +func TestMetricsAndHealthServeSeparateEndpoints(t *testing.T) { + servers := map[string]*http.ServeMux{ + "metrics": metricsMux(), + "health": healthMux(&Readiness{}), } -} - -func TestReadyzAbsentWithoutReadiness(t *testing.T) { - mux := metricsMux(MetricsServerOptions{}) - if got := getCode(t, mux, "/readyz"); got != http.StatusNotFound { - t.Errorf("/readyz with nil Readiness = %d, want %d", got, http.StatusNotFound) + for _, tc := range []struct { + server string + path string + want int + }{ + {server: "metrics", path: "/metrics", want: http.StatusOK}, + {server: "metrics", path: "/readyz", want: http.StatusNotFound}, + {server: "metrics", path: "/healthz", want: http.StatusNotFound}, + {server: "health", path: "/metrics", want: http.StatusNotFound}, + {server: "health", path: "/readyz", want: http.StatusOK}, + {server: "health", path: "/healthz", want: http.StatusOK}, + } { + t.Run(tc.server+tc.path, func(t *testing.T) { + if got := getCode(t, servers[tc.server], tc.path); got != tc.want { + t.Errorf("%s server GET %s = %d, want %d", tc.server, tc.path, got, tc.want) + } + }) } } -func TestHealthzAbsentUnlessEnabled(t *testing.T) { - mux := metricsMux(MetricsServerOptions{Readiness: &Readiness{}}) - if got := getCode(t, mux, "/healthz"); got != http.StatusNotFound { - t.Errorf("/healthz without EnableHealthz = %d, want %d", got, http.StatusNotFound) +// A server with no ReadHeaderTimeout lets a slow client hold a connection +// open forever. +func TestHTTPServerBoundsHeaderRead(t *testing.T) { + srv := newHTTPServer(":0", http.NewServeMux()) + if srv.ReadHeaderTimeout <= 0 { + t.Errorf("ReadHeaderTimeout = %v, want > 0", srv.ReadHeaderTimeout) } } diff --git a/manifests/ate-install/ate-api-server.yaml b/manifests/ate-install/ate-api-server.yaml index 168fd63596..77e3a4843d 100644 --- a/manifests/ate-install/ate-api-server.yaml +++ b/manifests/ate-install/ate-api-server.yaml @@ -220,14 +220,16 @@ spec: - containerPort: 443 - name: prometheus containerPort: 9090 + - name: health + containerPort: 9091 readinessProbe: httpGet: path: /readyz - port: 9090 + port: health initialDelaySeconds: 5 periodSeconds: 2 failureThreshold: 3 - # :9090 is bound near the end of boot, after the store connect and + # :9091 is bound near the end of boot, after the store connect and # the schema migrations, so nothing answers here until both finish. # The store connect alone budgets 60s (30 attempts, 2s apart) and the # migrations serialize across replicas on an advisory lock. Without a @@ -239,7 +241,7 @@ spec: startupProbe: httpGet: path: /healthz - port: 9090 + port: health periodSeconds: 5 failureThreshold: 24 # /healthz stays 200 while a terminating pod drains; /readyz @@ -248,7 +250,7 @@ spec: livenessProbe: httpGet: path: /healthz - port: 9090 + port: health initialDelaySeconds: 10 periodSeconds: 10 volumes: diff --git a/manifests/ate-install/atelet.yaml b/manifests/ate-install/atelet.yaml index 02c4cc4ce9..8121cd75de 100644 --- a/manifests/ate-install/atelet.yaml +++ b/manifests/ate-install/atelet.yaml @@ -281,12 +281,14 @@ spec: hostPort: 8085 - name: prometheus containerPort: 9090 - hostPort: 9090 + protocol: TCP + - name: health + containerPort: 9091 protocol: TCP readinessProbe: httpGet: path: /readyz - port: 9090 + port: health initialDelaySeconds: 5 periodSeconds: 2 failureThreshold: 3 @@ -295,7 +297,7 @@ spec: livenessProbe: httpGet: path: /healthz - port: 9090 + port: health initialDelaySeconds: 10 periodSeconds: 10 volumeMounts: diff --git a/manifests/ate-install/atenet-egress.yaml b/manifests/ate-install/atenet-egress.yaml index 805026ec3e..6d8a880f70 100644 --- a/manifests/ate-install/atenet-egress.yaml +++ b/manifests/ate-install/atenet-egress.yaml @@ -1437,14 +1437,16 @@ spec: containerPort: 50051 - name: metrics containerPort: 9090 - # Probed on the metrics port, not the ext_proc port: the ext_proc server + - name: health + containerPort: 9091 + # Probed on the health port, not the ext_proc port: the ext_proc server # binds loopback (--extproc-address 127.0.0.1) so it is unreachable from # other pods, which a pod-IP tcpSocket probe on 50051 could not reach. The - # metrics server binds all interfaces and /readyz reflects the process. + # health server binds all interfaces and /readyz reflects the process. readinessProbe: httpGet: path: /readyz - port: 9090 + port: health initialDelaySeconds: 5 periodSeconds: 2 failureThreshold: 3 diff --git a/manifests/ate-install/atenet-router.yaml b/manifests/ate-install/atenet-router.yaml index e6c3b603d1..cf7308d082 100644 --- a/manifests/ate-install/atenet-router.yaml +++ b/manifests/ate-install/atenet-router.yaml @@ -292,10 +292,12 @@ spec: containerPort: 4040 - name: metrics containerPort: 9090 + - name: health + containerPort: 9091 readinessProbe: httpGet: path: /readyz - port: 9090 + port: health initialDelaySeconds: 5 periodSeconds: 2 failureThreshold: 3 @@ -304,7 +306,7 @@ spec: livenessProbe: httpGet: path: /healthz - port: 9090 + port: health initialDelaySeconds: 10 periodSeconds: 10 volumeMounts: diff --git a/manifests/ate-install/cloudsql/proxy-sidecar-patch.yaml b/manifests/ate-install/cloudsql/proxy-sidecar-patch.yaml index c42ae2015b..2e41661bc0 100644 --- a/manifests/ate-install/cloudsql/proxy-sidecar-patch.yaml +++ b/manifests/ate-install/cloudsql/proxy-sidecar-patch.yaml @@ -51,7 +51,8 @@ spec: memory: 128Mi # /startup returns 200 once the proxy can accept connections; gating # on it means ateapi never races the tunnel. Health checks listen on - # CSQL_PROXY_HTTP_PORT (9801; ateapi's metrics own 9090). + # CSQL_PROXY_HTTP_PORT (9801; ateapi's metrics and health servers own + # 9090 and 9091). startupProbe: httpGet: path: /startup diff --git a/manifests/egress-credential-injection/k8s-credential-provider.yaml b/manifests/egress-credential-injection/k8s-credential-provider.yaml index 94192d671c..089983539a 100644 --- a/manifests/egress-credential-injection/k8s-credential-provider.yaml +++ b/manifests/egress-credential-injection/k8s-credential-provider.yaml @@ -75,6 +75,7 @@ spec: args: - "--listen-address=:50051" - "--metrics-address=:9090" + - "--health-address=:9091" - "--status-address=:4040" # Serve with the pod's servicedns identity (SAN k8s-credential-provider.ate-system.svc) # and require the injector to present a podidentity client cert whose chain @@ -92,12 +93,14 @@ spec: containerPort: 50051 - name: metrics containerPort: 9090 + - name: health + containerPort: 9091 - name: statusz containerPort: 4040 readinessProbe: httpGet: path: /readyz - port: metrics + port: health periodSeconds: 10 securityContext: allowPrivilegeEscalation: false From 8a5b3a472972b01b9f0cd025f93c5207907ab5bf Mon Sep 17 00:00:00 2001 From: Jeff Luo Date: Tue, 13 Oct 2026 14:57:06 +0000 Subject: [PATCH 04/42] Lifecycle metrics omit empty template labels, and a crash during a delete reports the delete operation (#2356) ## Behavior changes - `ate.actor.crashes`: when ateapi loses the worker of an actor in DELETING (possible with `any_state`), the crash now has `ate.actor.operation.name=delete`. Before this change it had `unknown`. - The crash message in the actor status now starts with `delete failed: ` in that case. Before this change it had no prefix. - `ate.actor.lifecycle.operation.duration`: when resume, suspend, pause, or revert fails before ateapi loads the actor, the series now omits `ate.template.name` and `ate.template.atespace`. Before this change it had them as the empty string. - `rpc.server.call.duration` and `rpc.client.call.duration` now list `rpc.system.name` in the registry, and the client histogram also lists `server.address` and `server.port`. This is only a change to the docs. otelgrpc already sent these keys. --- cmd/ateapi/internal/controlapi/actor.go | 4 +- cmd/ateapi/internal/controlapi/metrics.go | 17 ++-- .../internal/controlapi/metrics_test.go | 10 ++ .../controlapi/workflow_worker_delete.go | 2 + .../controlapi/workflow_worker_delete_test.go | 1 + docs/metrics/registry/metrics.yaml | 93 +++++++++++++++---- docs/metrics/substrate.yaml | 16 +++- docs/observability.md | 4 +- 8 files changed, 114 insertions(+), 33 deletions(-) diff --git a/cmd/ateapi/internal/controlapi/actor.go b/cmd/ateapi/internal/controlapi/actor.go index 36d02f4204..98e0004b37 100644 --- a/cmd/ateapi/internal/controlapi/actor.go +++ b/cmd/ateapi/internal/controlapi/actor.go @@ -429,8 +429,8 @@ func (s *RPCService) DeleteActor(ctx context.Context, req *ateapipb.DeleteActorR } start := time.Now() // Template dims only once the record resolved: the request names only the - // actor, so failures before the load carry none. No pool pair: delete only - // runs from SUSPENDED or CRASHED, which already released the worker. + // actor, so failures before the load carry none. No pool pair, even when + // any_state deletes an actor that still holds a worker. defer func() { var attrs []attribute.KeyValue if deleted != nil { diff --git a/cmd/ateapi/internal/controlapi/metrics.go b/cmd/ateapi/internal/controlapi/metrics.go index 8ace3106ea..e0c1a1405e 100644 --- a/cmd/ateapi/internal/controlapi/metrics.go +++ b/cmd/ateapi/internal/controlapi/metrics.go @@ -201,17 +201,20 @@ func (i *Instruments) recordLifecycleOp(ctx context.Context, op string, start ti i.lifecycleOpDuration.Record(ctx, time.Since(start).Seconds(), metric.WithAttributes(attrs...)) } -// lifecycleOpAttrs builds the resume/suspend/pause dimensions from workflow -// state. Nil-safe, and omits the pool, snapshot-kind and snapshot-fidelity labels -// until they are known so a failure before the assign/restore steps never emits -// an empty-string series. snapshotKind is empty for suspend/pause, which do not +// lifecycleOpAttrs builds the resume/suspend/pause/revert dimensions from +// workflow state. Nil-safe, and omits the template, pool, snapshot-kind and +// snapshot-fidelity labels until they are known so a failure before the +// load/assign/restore steps never emits an empty-string series. snapshotKind is empty for suspend/pause, which do not // restore; snapshotScope applies to all three and separates a full restore // from a data-only one of the same kind. // The pool keys are set together or not at all; see ateattr.WorkerPoolAttributes. func lifecycleOpAttrs(actor *ateapipb.Actor, template *ateapipb.ActorTemplate, snapshotKind, snapshotScope string) []attribute.KeyValue { - attrs := []attribute.KeyValue{ - ateattr.TemplateNameKey.String(actor.GetActorTemplate().GetName()), - ateattr.TemplateAtespaceKey.String(actor.GetActorTemplate().GetAtespace()), + var attrs []attribute.KeyValue + if actor != nil { + attrs = append(attrs, + ateattr.TemplateNameKey.String(actor.GetActorTemplate().GetName()), + ateattr.TemplateAtespaceKey.String(actor.GetActorTemplate().GetAtespace()), + ) } ass := actor.GetStatus().GetWorkerAssignment() attrs = append(attrs, ateattr.WorkerPoolAttributes(ass.GetWorkerNamespace(), ass.GetWorkerPool())...) diff --git a/cmd/ateapi/internal/controlapi/metrics_test.go b/cmd/ateapi/internal/controlapi/metrics_test.go index 8f4b1f6a72..c0b191e427 100644 --- a/cmd/ateapi/internal/controlapi/metrics_test.go +++ b/cmd/ateapi/internal/controlapi/metrics_test.go @@ -294,6 +294,16 @@ func TestLifecycleOpAttrsOmitsUnknownScope(t *testing.T) { } } +// TestLifecycleOpAttrsOmitsTemplateBeforeLoad guards a failure before or in the +// load step, such as lease contention or a missing ActorTemplate: the workflow +// holds no actor, so the template keys are omitted, as on a failed delete, +// rather than recorded as the empty string. +func TestLifecycleOpAttrsOmitsTemplateBeforeLoad(t *testing.T) { + if got := lifecycleOpAttrs(nil, nil, "", ""); len(got) != 0 { + t.Errorf("lifecycleOpAttrs(nil actor) = %v, want no attributes", got) + } +} + // TestRecordLifecycleOp_OutcomeClassification asserts success omits error.type and // each gRPC failure maps onto its status-code string; the absence of error.type is // the success signal, so there is no separate failure counter. diff --git a/cmd/ateapi/internal/controlapi/workflow_worker_delete.go b/cmd/ateapi/internal/controlapi/workflow_worker_delete.go index 742f7179ce..f90d9c0cb7 100644 --- a/cmd/ateapi/internal/controlapi/workflow_worker_delete.go +++ b/cmd/ateapi/internal/controlapi/workflow_worker_delete.go @@ -211,6 +211,8 @@ func (w *WorkerWorkflow) crashBoundActor(ctx context.Context, worker *ateapipb.W opName = ateattr.OperationPause case ateapipb.ActorState_ACTOR_STATE_REVERTING: opName = ateattr.OperationRevert + case ateapipb.ActorState_ACTOR_STATE_DELETING: + opName = ateattr.OperationDelete } wasAlreadyCrashed := actor.GetStatus().GetState() == ateapipb.ActorState_ACTOR_STATE_CRASHED diff --git a/cmd/ateapi/internal/controlapi/workflow_worker_delete_test.go b/cmd/ateapi/internal/controlapi/workflow_worker_delete_test.go index 7f77931523..235bdd71cb 100644 --- a/cmd/ateapi/internal/controlapi/workflow_worker_delete_test.go +++ b/cmd/ateapi/internal/controlapi/workflow_worker_delete_test.go @@ -170,6 +170,7 @@ func TestDeleteWorkerWorkflow_ReleasedActorStateTransitions(t *testing.T) { {name: "suspending becomes crashed", start: ateapipb.ActorState_ACTOR_STATE_SUSPENDING, wantState: ateapipb.ActorState_ACTOR_STATE_CRASHED, wantOp: ateattr.OperationSuspend, wantMetric: true}, {name: "pausing becomes crashed", start: ateapipb.ActorState_ACTOR_STATE_PAUSING, wantState: ateapipb.ActorState_ACTOR_STATE_CRASHED, wantOp: ateattr.OperationPause, wantMetric: true}, {name: "reverting becomes crashed", start: ateapipb.ActorState_ACTOR_STATE_REVERTING, wantState: ateapipb.ActorState_ACTOR_STATE_CRASHED, wantOp: ateattr.OperationRevert, wantMetric: true}, + {name: "deleting becomes crashed", start: ateapipb.ActorState_ACTOR_STATE_DELETING, wantState: ateapipb.ActorState_ACTOR_STATE_CRASHED, wantOp: ateattr.OperationDelete, wantMetric: true}, {name: "suspended stays suspended", start: ateapipb.ActorState_ACTOR_STATE_SUSPENDED, wantState: ateapipb.ActorState_ACTOR_STATE_SUSPENDED, wantMetric: false}, {name: "crashed is not counted twice", start: ateapipb.ActorState_ACTOR_STATE_CRASHED, wantState: ateapipb.ActorState_ACTOR_STATE_CRASHED, wantMetric: false}, } diff --git a/docs/metrics/registry/metrics.yaml b/docs/metrics/registry/metrics.yaml index b74d03a2c5..1dd1da1669 100644 --- a/docs/metrics/registry/metrics.yaml +++ b/docs/metrics/registry/metrics.yaml @@ -124,6 +124,10 @@ groups: stability: development value: deleting brief: ateapi is removing the actor. + - id: reverting + stability: development + value: reverting + brief: ateapi is discarding the execution of the actor and returning it to the suspended state. - id: deleted stability: development value: deleted @@ -162,11 +166,14 @@ groups: - id: revert stability: development value: revert - brief: ateapi discarded the actor's execution and returned it to its existing snapshot. + brief: ateapi discarded the execution of the actor and returned it to the suspended state. - id: unknown stability: development value: unknown - brief: The operation is not in this list. This is the default value. + brief: > + No lifecycle operation is running, such as when a running or + paused actor crashes because its worker is lost, or the + operation is not in this list. This is the default value. - id: registry.ate.template type: attribute_group @@ -589,7 +596,13 @@ groups: - id: final stability: development value: final - brief: The last sample of an activation, written when a checkpoint or a terminate tears it down. It is read just before the teardown; when that read fails or times out, it repeats the newest sample. + brief: > + The last sample of an activation, written when a checkpoint, a + terminate, a graceful shutdown, or a re-host on ateom-microvm + ends it. When the sandbox is still running, ateom reads the + stats just before the teardown; when no read runs, or the read + fails or times out, it repeats the newest measured sample, or + writes a pending record when there is none. - id: ate.stats.memory.usage stability: development type: int @@ -689,7 +702,28 @@ groups: stability: stable type: string brief: The gRPC status that the server returned. - examples: ["OK", "ResourceExhausted"] + examples: ["OK", "RESOURCE_EXHAUSTED"] + - id: rpc.system.name + stability: stable + type: string + brief: The RPC system. otelgrpc always sets the value grpc. + examples: ["grpc"] + - id: server.address + stability: stable + type: string + brief: > + The address of the server that a gRPC client called. otelgrpc reads it + from the remote address of the connection, so it holds an IP address + and not the name in the dial target. For a unix socket it holds the + path of the socket. atelet calls each ateom on a socket path that holds + the UID of the worker pod, so on atelet this key has one value for + each worker pod. + examples: ["10.8.2.14", "/var/lib/ate/ateoms//ateom.sock"] + - id: server.port + stability: stable + type: int + brief: The TCP port of the server that a gRPC client called. + examples: [8085] # --------------------------------------------------------------------------- # Metrics. ateapi @@ -781,8 +815,8 @@ groups: note: > ateapi does not record a resume of an actor that is already in operation. Thus this histogram counts the true activations and not the traffic of the - router. The set of labels changes with the operation. A resume has the - most labels. A delete has only the operation and error.type. + router. The set of labels changes with the operation. Each attribute + below gives the condition for its key. annotations: substrate: emitted_by: [ateapi] @@ -794,24 +828,37 @@ groups: - ref: ate.actor.operation.name requirement_level: required - ref: ate.template.atespace - requirement_level: required + requirement_level: + conditionally_required: > + The operation is a create, or ateapi loaded the actor. Thus this key + is absent when a delete fails or when the load step fails. For a + resume, a suspend and a pause, the load step also reads the + ActorTemplate. - ref: ate.template.name - requirement_level: required + requirement_level: + conditionally_required: > + The operation is a create, or ateapi loaded the actor. Thus this key + is absent when a delete fails or when the load step fails. For a + resume, a suspend and a pause, the load step also reads the + ActorTemplate. - ref: ate.workerpool.namespace requirement_level: - conditionally_required: The actor has a worker. + conditionally_required: The operation is not a create or a delete, and the actor has a worker. - ref: ate.workerpool.name requirement_level: - conditionally_required: The actor has a worker. + conditionally_required: The operation is not a create or a delete, and the actor has a worker. - ref: ate.sandbox.class requirement_level: - conditionally_required: ateapi found the ActorTemplate. + conditionally_required: > + The operation is resume, suspend, pause or revert, and ateapi found + the ActorTemplate. A revert continues when the ActorTemplate no + longer exists, and then omits this key. - ref: ate.snapshot.kind requirement_level: - conditionally_required: The operation is a resume. A suspend and a pause do not restore, thus this key is absent. + conditionally_required: The operation is a resume and the snapshot source is known. Other operations do not restore, thus this key is absent. - ref: ate.snapshot.fidelity requirement_level: - conditionally_required: The fidelity is known. + conditionally_required: The operation is resume, suspend or pause, and the fidelity is known. - ref: error.type requirement_level: conditionally_required: The operation failed. If this key is absent, the operation was correct. @@ -1247,17 +1294,21 @@ groups: brief: The time, the rate and the errors of each gRPC method on the server. note: > The otelgrpc interceptor sends this instrument. The code of Substrate does - not make it. This instrument is the only instrumentation of ateom. ateom - has it on the gRPC interface that atelet calls. + not make it. This instrument is the only metric of ateom. ateom has it on + the gRPC interface that atelet calls. credprovider is the + kubernetes-secrets credential provider in + cmd/credential-provider/kubernetes-secrets. annotations: substrate: - emitted_by: [ateapi, atelet, atenet-router, ateom-gvisor, ateom-microvm] + emitted_by: [ateapi, atelet, atenet-router, ateom-gvisor, ateom-microvm, credprovider] golden_signals: [latency, traffic, errors] source: external instrumentation: go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc code_anchor: cmd/atenet/internal/router/xds.go cuj: Which RPC is slow or fails, and on which component? attributes: + - ref: rpc.system.name + requirement_level: required - ref: rpc.method requirement_level: required - ref: rpc.response.status_code @@ -1281,13 +1332,21 @@ groups: golden_signals: [latency, traffic, errors] source: external instrumentation: go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc - code_anchor: internal/ateclient/builder.go + code_anchor: cmd/ateapi/internal/controlapi/dialer.go cuj: A component waits on another component. Which call, and for how long? attributes: + - ref: rpc.system.name + requirement_level: required - ref: rpc.method requirement_level: required - ref: rpc.response.status_code requirement_level: required + - ref: server.address + requirement_level: + conditionally_required: The client sent the request headers on a connection. + - ref: server.port + requirement_level: + conditionally_required: The client called the server over TCP. A call on a unix socket has no port. # --------------------------------------------------------------------------- # The metrics that Substrate exports but does not define # diff --git a/docs/metrics/substrate.yaml b/docs/metrics/substrate.yaml index ffef2b775c..3313e3782f 100644 --- a/docs/metrics/substrate.yaml +++ b/docs/metrics/substrate.yaml @@ -26,8 +26,9 @@ registry_version: 1 # The version of the upstream semantic conventions that Substrate borrows from. -# Substrate takes error.type, file.name and the rpc.* attributes from it. This -# is the only version that the code imports under cmd/, internal/ and pkg/. +# Substrate takes error.type, file.name, the rpc.* attributes, server.address +# and server.port from it. This is the only version that the code imports under +# cmd/, internal/ and pkg/. # It is not the version of this registry: manifest.yaml holds that. upstream_semconv_version: 1.40.0 @@ -124,9 +125,9 @@ bridged_metric_families: # ----------------------------------------------------------------------------- # The rules that keep the number of series small # -# No rule is enforced at this time. This PR adds the registry only. Each rule -# says what could enforce it, so that a reader knows the difference between a -# rule that a machine holds and a rule that a person holds. +# No rule is enforced at this time. Each rule says what could enforce it, so +# that a reader knows the difference between a rule that a machine holds and a +# rule that a person holds. # ----------------------------------------------------------------------------- cardinality_rules: - id: no-actor-identity @@ -228,6 +229,11 @@ blind_spots: - area: podcertcontroller code: cmd/podcertcontroller brief: This component has no metrics. + - area: GCP Secret Manager credential provider + code: internal/plugins/gcp-secret-manager + brief: > + This plugin has no metrics. It does not install the otelgrpc server + handler. Thus rpc.server.call.duration has no credprovider series for it. - area: golden snapshot pipeline code: cmd/ateapi/internal/controlapi/template_reconciler.go brief: The template reconciler in ate-api has no instruments. Thus nothing measures a build of a golden image. diff --git a/docs/observability.md b/docs/observability.md index 5cee8a8350..6d5ea25873 100644 --- a/docs/observability.md +++ b/docs/observability.md @@ -264,7 +264,7 @@ Agent Substrate emits foundational OpenTelemetry system and server metrics to mo | Metric | Emitted by | Type | Measures | |--------|------------|------|----------| -| `rpc.server.call.duration` | ateapi & atelet (gRPC servers, via `otelgrpc`) | histogram | per-method gRPC latency, request rate, and errors (labels `rpc.method`, `rpc.response.status_code`) | +| `rpc.server.call.duration` | ateapi, atelet, atenet-router, ateom-gvisor, ateom-microvm & credprovider (gRPC servers, via `otelgrpc`) | histogram | per-method gRPC latency, request rate, and errors (labels `rpc.method`, `rpc.response.status_code`) | | `ate.actor.crashes` | ateapi | counter | Number of times actors transitioned to `ACTOR_STATE_CRASHED` (labels `ate.actor.operation.name`, `ate.template.atespace`, `ate.template.name`, `ate.workerpool.namespace`, `ate.workerpool.name`, `ate.sandbox.class`) | | `atenet.router.route.duration` | atenet-router | histogram | Substrate E2E — Envoy receiving a request to Envoy forwarding it to the resolved worker, excluding actor compute and the response (labels `ate.template.atespace`, `ate.template.name`, `ate.router.outcome`, `ate.router.resume`) | | `atelet.snapshot.size` | atelet | histogram | uncompressed allocated size in bytes of each snapshot image written during checkpoint (`st_blocks * 512`, excluding sparse holes in micro-VM `memory-ranges`) (labels `file.name`, `ate.template.atespace`, `ate.template.name`) | @@ -273,7 +273,7 @@ Agent Substrate emits foundational OpenTelemetry system and server metrics to mo | `ate.workerpool.ready_workers` | atecontroller | up/down counter | number of worker pods currently ready for a WorkerPool, from `status.readyReplicas` (labels `ate.workerpool.namespace`, `ate.workerpool.name`) | | `ate.workerpool.workers` | ateapi | up/down counter | live worker count per pool, split by state (`idle`/`partial`/`at_capacity`/`unschedulable`) and sandbox class to provide fleet capacity and saturation at a glance | -| `ate.actor.lifecycle.operation.duration` | ateapi | histogram | how long each actor operation (create/resume/suspend/pause/delete/revert) takes and whether it failed (`error.type` present = failure, absent = success); labeled by operation, template, pool (`ate.workerpool.namespace` + `ate.workerpool.name`), sandbox class, and snapshot kind and scope on resume; already-running resume no-ops are not recorded so the histogram tracks actual activations, not router traffic | +| `ate.actor.lifecycle.operation.duration` | ateapi | histogram | how long each actor operation (create/resume/suspend/pause/delete/revert) takes and whether it failed (`error.type` present = failure, absent = success); labeled by operation, template, pool (`ate.workerpool.namespace` + `ate.workerpool.name`), sandbox class, snapshot kind on resume, and snapshot fidelity on resume, suspend, and pause; already-running resume no-ops are not recorded so the histogram tracks actual activations, not router traffic | | `ate.scheduler.assignment.duration` | ateapi | histogram | time of one attempt to assign a worker to an actor, with one record for each resume (ateapi retries an attempt that loses a race for a worker and records only the last attempt), with the outcome (`assigned` / `no_capacity` / `error`), the assigned pool (`ate.workerpool.namespace` + `ate.workerpool.name`) and sandbox class to catch scheduling latency and capacity starvation problems | | `ate.actor.restore.duration` | atelet | histogram | how long each phase of a restore takes on the worker node, which is where cold-start latency actually goes once ateapi hands off (labels `ate.snapshot.phase`, `ate.snapshot.kind`, `ate.snapshot.fidelity`, `ate.template.atespace`, `ate.template.name`, `ate.sandbox.class`) | | `ate.actor.checkpoint.duration` | atelet | histogram | the same phase breakdown for writing a snapshot, so a slow suspend can be attributed to ateom or to the upload (same labels as the restore histogram) | From c1ab95dea49bacfdeefcb00dd67b66d27372021c Mon Sep 17 00:00:00 2001 From: shrutiyam-glitch Date: Tue, 13 Oct 2026 16:00:40 +0000 Subject: [PATCH 05/42] atelet: introduce declarative validation for the AteomHerder RPCs (#1997) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes part of #1709 This adds declarative validation to atelet's AteomHerder RPCs and to the WorkloadSpec they carry. Malformed requests are now rejected with `InvalidArgument` before atelet builds any host path or touches an ateom. The hand-written validators they replace are removed. With this, every atelet RPC request is validated, so the generator now selects them by the `Request` suffix, as controlapi does. | RPC | Rules | |---|---| | Run | target ateom UID, atespace and actor name are required short names; actor UID is a required UUID; template identity, when set, is short names; spec is required and validated in full; `cpu_milli` is 0..999999 and `memory_bytes` ≥ 0; egress gateway address, when set, is a host:port; sandbox assets are required | | Terminate | same identity rules; spec is optional (delete of template-less actors) and validated in full when set | | Checkpoint | same identity rules; spec required; type is LOCAL or EXTERNAL and the matching config must be set; local snapshot name is a short name; external snapshot URI is parseable; scope is FULL or DATA | | Restore | same as Checkpoint, plus: scope may also be DATA_ON_GOLDEN, and `base_config` is required exactly then; same size, egress and sandbox asset rules as Run | | UploadPausedCheckpoint | same identity rules, but the golden atespace is rejected; local snapshot name is a short name; destination URI is parseable; desired scope is FULL or DATA | **WorkloadSpec** - Containers: names are unique short names, and `pause` is reserved. Images are pinned by digest, the same check ateapi applies when a template is created. Args, env, working dir, probes and mounts are bounded. - Volumes: each volume has exactly one source; external and image sources are checked; system-info file paths are unique; at most one `actor_metadata` data source is allowed (the same rule is added on the ateapi side). **Sandbox assets:** the sandbox class is gvisor or microvm; the pause image is pinned by digest (stricter than the SandboxConfig CRD's `@` check); asset URLs are bounded; hashes are SHA-256. **Wire format:** the `Volume`, `SystemInfoDataSource`, Checkpoint `config` and Restore `config` oneofs become plain fields with the same field numbers, so the wire format doesn't change. They're validated as unions, because validation tags can't be placed on oneof members. - [x] Tests pass - [x] Appropriate changes to documentation are included in the PR --- cmd/ateapi/internal/apivalidation/actor.go | 35 +- .../internal/apivalidation/actor_template.go | 146 +- .../apivalidation/actor_template_test.go | 133 - .../internal/apivalidation/validation_test.go | 119 +- .../apivalidation/zz_generated.validation.go | 16 +- .../internal/controlapi/workflow_delete.go | 10 +- .../internal/controlapi/workflow_pause.go | 6 +- .../internal/controlapi/workflow_resume.go | 10 +- .../internal/controlapi/workflow_suspend.go | 6 +- .../internal/controlapi/workload_spec.go | 42 +- .../internal/controlapi/workload_spec_test.go | 64 +- cmd/ateapi/main.go | 7 + cmd/atelet/imagevolume_test.go | 21 +- cmd/atelet/internal/apivalidation/validate.go | 107 + .../internal/apivalidation/validation_test.go | 1423 +++++- .../internal/apivalidation/workloadspec.go | 153 + .../apivalidation/zz_generated.validation.go | 4303 ++++++++++++++++- cmd/atelet/lifecycle_test.go | 100 +- cmd/atelet/main.go | 204 +- cmd/atelet/main_test.go | 301 +- cmd/atelet/systeminfovolume.go | 12 +- cmd/atelet/systeminfovolume_test.go | 22 +- cmd/atelet/volumes_test.go | 72 +- cmd/ateom-gvisor/stats_test.go | 9 +- .../isolate/fake-atelet/herder_test.go | 4 +- internal/proto/ateletpb/atelet.pb.go | 781 +-- internal/proto/ateletpb/atelet.proto | 439 +- internal/resources/validate.go | 264 +- internal/resources/validate_test.go | 233 +- pkg/proto/ateapipb/ateapi.pb.go | 8 +- pkg/proto/ateapipb/ateapi.proto | 8 +- 31 files changed, 7388 insertions(+), 1670 deletions(-) create mode 100644 cmd/atelet/internal/apivalidation/workloadspec.go diff --git a/cmd/ateapi/internal/apivalidation/actor.go b/cmd/ateapi/internal/apivalidation/actor.go index a57b113742..fc1b6895a7 100644 --- a/cmd/ateapi/internal/apivalidation/actor.go +++ b/cmd/ateapi/internal/apivalidation/actor.go @@ -16,12 +16,11 @@ package apivalidation import ( "context" - "strings" + "github.com/agent-substrate/substrate/internal/resources" "github.com/agent-substrate/substrate/pkg/proto/ateapipb" "k8s.io/apimachinery/pkg/api/operation" "k8s.io/apimachinery/pkg/api/validate" - "k8s.io/apimachinery/pkg/util/validation" "k8s.io/apimachinery/pkg/util/validation/field" ) @@ -134,34 +133,14 @@ func ValidateCustom_WorkerAssignment_WorkerPodIps(_ context.Context, _ operation return validateDualStackIPs(fldPath, value) } -// ValidateCustom_ExternalVolume_VolumeType checks that a volume type string is well-formed. -// It allows an optional "substrate.io/" prefix, followed by a valid DNS-1123 subdomain. func ValidateCustom_ExternalVolume_VolumeType(_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ *string) field.ErrorList { - if value == nil || *value == "" { - return nil - } - var errs field.ErrorList - valToValidate := strings.TrimPrefix(*value, "substrate.io/") - for _, msg := range validation.IsDNS1123Subdomain(valToValidate) { - errs = append(errs, field.Invalid(fldPath, *value, msg)) - } - return errs + return resources.ValidateVolumeType(fldPath, *value) } -// ValidateCustom_ExternalVolume_StorageVolumeId checks that an external volume's storage ID does not -// contain control characters (U+0000-U+0008, U+000B, U+000C, U+000E-U+001F, U+007F-U+009F). func ValidateCustom_ExternalVolume_StorageVolumeId(_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ *string) field.ErrorList { - if value == nil || *value == "" { - return nil - } - for _, r := range *value { - if (r >= 0x0000 && r <= 0x0008) || - r == 0x000B || - r == 0x000C || - (r >= 0x000E && r <= 0x001F) || - (r >= 0x007F && r <= 0x009F) { - return field.ErrorList{field.Invalid(fldPath, *value, "must not contain control characters (U+0000-U+0008, U+000B, U+000C, U+000E-U+001F, U+007F-U+009F)")} - } - } - return nil + return resources.ValidateStorageVolumeID(fldPath, *value) +} + +func ValidateCustom_ExternalVolume_VolumeContext(_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ map[string]string) field.ErrorList { + return resources.ValidateCSIMap(fldPath, value) } diff --git a/cmd/ateapi/internal/apivalidation/actor_template.go b/cmd/ateapi/internal/apivalidation/actor_template.go index 1418f0188f..483861fe91 100644 --- a/cmd/ateapi/internal/apivalidation/actor_template.go +++ b/cmd/ateapi/internal/apivalidation/actor_template.go @@ -17,13 +17,8 @@ package apivalidation import ( "context" "fmt" - "regexp" - "strings" - - "github.com/distribution/reference" "github.com/agent-substrate/substrate/internal/resources" - "github.com/agent-substrate/substrate/internal/volumepath" "github.com/agent-substrate/substrate/pkg/proto/ateapipb" "k8s.io/apimachinery/pkg/api/operation" "k8s.io/apimachinery/pkg/api/resource" @@ -95,91 +90,40 @@ func ValidateCustom_CreateActorTemplateRequest_ActorTemplate(_ context.Context, return errs } -// httpGetPathRE constrains wakeup probe paths to RFC 3986 path-segment -// characters only, with well-formed percent-escapes, and no query string -// or fragment. -var httpGetPathRE = regexp.MustCompile(`^/([A-Za-z0-9\-._~!$&'()*+,;=:@/]|%[0-9A-Fa-f]{2})*$`) - func ValidateCustom_HttpGetAction_Path(_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ *string) field.ErrorList { - if !httpGetPathRE.MatchString(*value) { - return field.ErrorList{field.Invalid(fldPath, *value, "must be a URL path starting with '/', using only RFC 3986 path-segment characters, without query or fragment")} - } - return nil + return resources.ValidateHTTPGetPath(fldPath, *value) } -// mountPathBadSegmentRE matches '.' or '..' path segments. -var mountPathBadSegmentRE = regexp.MustCompile(`(^|/)[.][.]?(/|$)`) - -// ValidateCustom_VolumeMount_MountPath requires a clean absolute Unix path -// that starts with '/', is not '/', and contains no ':', '.' or '..' -// segments, '//', trailing '/', or control characters. func ValidateCustom_VolumeMount_MountPath(_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ *string) field.ErrorList { - p := *value - bad := !strings.HasPrefix(p, "/") || len(p) == 1 || - strings.HasSuffix(p, "/") || strings.Contains(p, "//") || - strings.Contains(p, ":") || mountPathBadSegmentRE.MatchString(p) - if !bad { - for _, r := range p { - if r < 0x20 || r == 0x7f { - bad = true - break - } - } - } - if bad { - return field.ErrorList{field.Invalid(fldPath, p, "must be a clean absolute Unix path: must start with '/', not be '/', and contain no ':', '..', '.', '//', trailing '/', or control characters")} - } - return nil + return resources.ValidateMountPath(fldPath, *value) } // ValidateCustom_Container_VolumeMounts rejects mounts that nest under one -// another. +// another. Mount-path uniqueness is enforced by the list key. func ValidateCustom_Container_VolumeMounts(_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ []*ateapipb.VolumeMount) field.ErrorList { - var errs field.ErrorList + paths := make([]string, len(value)) for i, m := range value { - path := m.GetMountPath() - if path == "" { - continue // required is enforced by tags - } - // Nested mounts are unsupported (volumes cannot mount onto - // other volumes). - for j := 0; j < i; j++ { - prior := value[j].GetMountPath() - if prior == "" || prior == path { - continue - } - if strings.HasPrefix(path, prior+"/") || strings.HasPrefix(prior, path+"/") { - errs = append(errs, field.Invalid(fldPath.Index(i).Child("mount_path"), path, - fmt.Sprintf("must not nest under or over another mount (%q)", prior))) - } - } - } - return errs -} - -// validateProjectedPath applies the projected-path rule shared with atelet, -// which re-checks it before writing to the host. -func validateProjectedPath(fldPath *field.Path, p string) field.ErrorList { - if err := volumepath.ValidateProjected(p); err != nil { - return field.ErrorList{field.Invalid(fldPath, p, err.Error())} + paths[i] = m.GetMountPath() } - return nil + return resources.ValidateNestedMountPaths(fldPath, paths) } func ValidateCustom_ActorMetadataItem_Path(_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ *string) field.ErrorList { - return validateProjectedPath(fldPath, *value) + return resources.ValidateProjectedPath(fldPath, *value) } func ValidateCustom_TrustBundleDataSource_Path(_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ *string) field.ErrorList { - return validateProjectedPath(fldPath, *value) + return resources.ValidateProjectedPath(fldPath, *value) } -// ValidateCustom_SystemInfoVolumeSource_DataSources requires every projected -// file path to be unique across all data sources: atelet writes them in -// order into one tree, so a repeated path silently clobbers the earlier file. +// ValidateCustom_SystemInfoVolumeSource_DataSources allows at most one +// actor_metadata entry and requires every projected file path to be unique +// across all data sources: atelet writes them in order into one tree, so a +// repeated path silently clobbers the earlier file. func ValidateCustom_SystemInfoVolumeSource_DataSources(_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ []*ateapipb.SystemInfoDataSource) field.ErrorList { var errs field.ErrorList seen := sets.New[string]() + sawMetadata := false for i, ds := range value { switch { case ds == nil: @@ -189,6 +133,10 @@ func ValidateCustom_SystemInfoVolumeSource_DataSources(_ context.Context, _ oper } seen.Insert(ds.TrustBundle.Path) case ds.ActorMetadata != nil: + if sawMetadata { + errs = append(errs, field.Forbidden(fldPath.Index(i).Child("actor_metadata"), "at most one actor_metadata entry may appear")) + } + sawMetadata = true for j, item := range ds.ActorMetadata.Items { if item == nil { continue @@ -203,31 +151,12 @@ func ValidateCustom_SystemInfoVolumeSource_DataSources(_ context.Context, _ oper return errs } -// validatePinnedImage requires a well-formed OCI image reference pinned by -// digest (e.g. "name@sha256:..."): changing the image content under a fixed -// reference invalidates snapshots. It parses with the same grammar the -// container runtimes use, so a malformed digest is rejected rather than -// treated as pinned. -func validatePinnedImage(fldPath *field.Path, value string) field.ErrorList { - if value == "" { - return nil // required is enforced by tags - } - ref, err := reference.ParseNormalizedNamed(value) - if err != nil { - return field.ErrorList{field.Invalid(fldPath, value, fmt.Sprintf("must be a well-formed image reference: %v", err))} - } - if _, ok := ref.(reference.Digested); !ok { - return field.ErrorList{field.Invalid(fldPath, value, "must be pinned by digest (changing the image invalidates snapshots)")} - } - return nil -} - func ValidateCustom_ImageVolumeSource_Reference(_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ *string) field.ErrorList { - return validatePinnedImage(fldPath, *value) + return resources.ValidatePinnedImage(fldPath, *value) } func ValidateCustom_Container_Image(_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ *string) field.ErrorList { - return validatePinnedImage(fldPath, *value) + return resources.ValidatePinnedImage(fldPath, *value) } func ValidateCustom_ExternalVolumeTemplate_Capacity(_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ *string) field.ErrorList { @@ -247,45 +176,14 @@ func ValidateCustom_SnapshotConfig_StorageLocation(_ context.Context, _ operatio return nil } -// envVarNameRE constrains env var names to any printable ASCII character -// except '='. -var envVarNameRE = regexp.MustCompile(`^[ -<>-~]+$`) - func ValidateCustom_EnvVar_Name(_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ *string) field.ErrorList { - if !envVarNameRE.MatchString(*value) { - return field.ErrorList{field.Invalid(fldPath, *value, "may contain any printable ASCII character except '='")} - } - return nil -} - -// capabilityRE constrains Linux capability names: uppercase, without the -// "CAP_" prefix (which is added when the OCI spec is written; the prefixed -// spelling would silently grant nothing). -var capabilityRE = regexp.MustCompile(`^[A-Z][A-Z0-9_]*$`) - -func validateCapabilities(fldPath *field.Path, caps []string, allowAll bool) field.ErrorList { - var errs field.ErrorList - for i, c := range caps { - p := fldPath.Index(i) - switch { - case c == "ALL" && !allowAll: - errs = append(errs, field.Invalid(p, c, "add does not accept 'ALL'; name the individual capabilities the container needs")) - case c == "ALL": - case len(c) > 63: - errs = append(errs, field.TooLong(p, nil, 63)) - case strings.HasPrefix(c, "CAP_"): - errs = append(errs, field.Invalid(p, c, "must be named without the 'CAP_' prefix (e.g. 'NET_BIND_SERVICE')")) - case !capabilityRE.MatchString(c): - errs = append(errs, field.Invalid(p, c, "must be an uppercase capability name like 'NET_BIND_SERVICE'")) - } - } - return errs + return resources.ValidateEnvVarName(fldPath, *value) } func ValidateCustom_Capabilities_Add(_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ []string) field.ErrorList { - return validateCapabilities(fldPath, value, false) + return resources.ValidateCapabilities(fldPath, value, false) } func ValidateCustom_Capabilities_Drop(_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ []string) field.ErrorList { - return validateCapabilities(fldPath, value, true) + return resources.ValidateCapabilities(fldPath, value, true) } diff --git a/cmd/ateapi/internal/apivalidation/actor_template_test.go b/cmd/ateapi/internal/apivalidation/actor_template_test.go index 7ed4788ee7..fe044102bf 100644 --- a/cmd/ateapi/internal/apivalidation/actor_template_test.go +++ b/cmd/ateapi/internal/apivalidation/actor_template_test.go @@ -447,40 +447,6 @@ func TestValidateActorTemplate(t *testing.T) { } }, want: field.ErrorList{field.Invalid(field.NewPath("containers").Index(0).Child("volume_mounts").Index(1).Child("mount_path"), nil, "")}, - }, { - name: "deeply nested mount path is rejected", - mutate: func(tmpl *ateapipb.ActorTemplate) { - tmpl.Containers[0].VolumeMounts = []*ateapipb.VolumeMount{ - {Name: "data", MountPath: "/data"}, - {Name: "deep", MountPath: "/data/a/b/c"}, - } - }, - want: field.ErrorList{field.Invalid(field.NewPath("containers").Index(0).Child("volume_mounts").Index(1).Child("mount_path"), nil, "")}, - }, { - name: "nesting is rejected regardless of listing order", - mutate: func(tmpl *ateapipb.ActorTemplate) { - tmpl.Containers[0].VolumeMounts = []*ateapipb.VolumeMount{ - {Name: "deep", MountPath: "/data/a/b/c"}, - {Name: "data", MountPath: "/data"}, - } - }, - want: field.ErrorList{field.Invalid(field.NewPath("containers").Index(0).Child("volume_mounts").Index(1).Child("mount_path"), nil, "")}, - }, { - name: "sibling subpaths under an unmounted ancestor are allowed", - mutate: func(tmpl *ateapipb.ActorTemplate) { - tmpl.Containers[0].VolumeMounts = []*ateapipb.VolumeMount{ - {Name: "data", MountPath: "/data/a"}, - {Name: "other", MountPath: "/data/b"}, - } - }, - }, { - name: "shared segment prefix below the root is allowed", - mutate: func(tmpl *ateapipb.ActorTemplate) { - tmpl.Containers[0].VolumeMounts = []*ateapipb.VolumeMount{ - {Name: "data", MountPath: "/data/a"}, - {Name: "other", MountPath: "/data/ab"}, - } - }, }, { name: "the same path in different containers is allowed", mutate: func(tmpl *ateapipb.ActorTemplate) { @@ -488,22 +454,6 @@ func TestValidateActorTemplate(t *testing.T) { tmpl.Containers[0].VolumeMounts = []*ateapipb.VolumeMount{{Name: "data", MountPath: "/var/data"}} tmpl.Containers[1].VolumeMounts = []*ateapipb.VolumeMount{{Name: "data", MountPath: "/var/data"}} }, - }, { - name: "shared path prefix without nesting is allowed", - mutate: func(tmpl *ateapipb.ActorTemplate) { - tmpl.Containers[0].VolumeMounts = []*ateapipb.VolumeMount{ - {Name: "data", MountPath: "/data"}, - {Name: "other", MountPath: "/database"}, - } - }, - }, { - name: "two volumes at distinct paths are allowed", - mutate: func(tmpl *ateapipb.ActorTemplate) { - tmpl.Containers[0].VolumeMounts = []*ateapipb.VolumeMount{ - {Name: "data", MountPath: "/var/data"}, - {Name: "other", MountPath: "/mnt/other"}, - } - }, }, { name: "duplicate volume name", mutate: func(tmpl *ateapipb.ActorTemplate) { @@ -571,35 +521,11 @@ func TestValidateActorTemplate(t *testing.T) { field.Invalid(field.NewPath("containers").Index(0).Child("image"), nil, ""), field.TooLong(field.NewPath("containers").Index(0).Child("image"), nil, 512).WithOrigin("maxLength"), }, - }, { - name: "invalid image: bare repository without digest", - mutate: func(tmpl *ateapipb.ActorTemplate) { - tmpl.Containers[0].Image = "ubuntu" - }, - want: field.ErrorList{field.Invalid(field.NewPath("containers").Index(0).Child("image"), nil, "")}, }, { name: "valid image: pinned by digest", mutate: func(tmpl *ateapipb.ActorTemplate) { tmpl.Containers[0].Image = "example.com/app@sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" }, - }, { - name: "invalid image: uppercase repository", - mutate: func(tmpl *ateapipb.ActorTemplate) { - tmpl.Containers[0].Image = "example.com/App:v1" - }, - want: field.ErrorList{field.Invalid(field.NewPath("containers").Index(0).Child("image"), nil, "")}, - }, { - name: "invalid image: malformed digest", - mutate: func(tmpl *ateapipb.ActorTemplate) { - tmpl.Containers[0].Image = "example.com/app@sha256:abc" - }, - want: field.ErrorList{field.Invalid(field.NewPath("containers").Index(0).Child("image"), nil, "")}, - }, { - name: "invalid image: empty tag", - mutate: func(tmpl *ateapipb.ActorTemplate) { - tmpl.Containers[0].Image = "example.com/app:" - }, - want: field.ErrorList{field.Invalid(field.NewPath("containers").Index(0).Child("image"), nil, "")}, }, { name: "container image missing digest", mutate: func(tmpl *ateapipb.ActorTemplate) { @@ -623,23 +549,12 @@ func TestValidateActorTemplate(t *testing.T) { mutate: func(tmpl *ateapipb.ActorTemplate) { tmpl.Containers[0].Env = []*ateapipb.EnvVar{{Name: "PORT", Value: "8080"}, {Name: "DEBUG"}} }, - }, { - name: "env name with unusual printable characters is allowed", - mutate: func(tmpl *ateapipb.ActorTemplate) { - tmpl.Containers[0].Env = []*ateapipb.EnvVar{{Name: "my.var-2 (test)!", Value: "v"}} - }, }, { name: "env name with an equals sign", mutate: func(tmpl *ateapipb.ActorTemplate) { tmpl.Containers[0].Env = []*ateapipb.EnvVar{{Name: "FOO=BAR"}} }, want: field.ErrorList{field.Invalid(field.NewPath("containers").Index(0).Child("env").Index(0).Child("name"), nil, "")}, - }, { - name: "env name with a control character", - mutate: func(tmpl *ateapipb.ActorTemplate) { - tmpl.Containers[0].Env = []*ateapipb.EnvVar{{Name: "FOO BAR"}} - }, - want: field.ErrorList{field.Invalid(field.NewPath("containers").Index(0).Child("env").Index(0).Child("name"), nil, "")}, }, { name: "env missing name", mutate: func(tmpl *ateapipb.ActorTemplate) { @@ -660,12 +575,6 @@ func TestValidateActorTemplate(t *testing.T) { tmpl.Containers[0].SecurityContext = &ateapipb.SecurityContext{Capabilities: &ateapipb.Capabilities{Add: []string{"ALL"}}} }, want: field.ErrorList{field.Invalid(field.NewPath("containers").Index(0).Child("security_context", "capabilities", "add").Index(0), nil, "")}, - }, { - name: "capability with CAP_ prefix", - mutate: func(tmpl *ateapipb.ActorTemplate) { - tmpl.Containers[0].SecurityContext = &ateapipb.SecurityContext{Capabilities: &ateapipb.Capabilities{Add: []string{"CAP_NET_BIND_SERVICE"}}} - }, - want: field.ErrorList{field.Invalid(field.NewPath("containers").Index(0).Child("security_context", "capabilities", "add").Index(0), nil, "")}, }, { name: "lowercase capability", mutate: func(tmpl *ateapipb.ActorTemplate) { @@ -806,24 +715,6 @@ func TestValidateActorTemplate(t *testing.T) { tmpl.Containers[0].VolumeMounts = []*ateapipb.VolumeMount{{Name: "data", MountPath: "var/data"}} }, want: field.ErrorList{field.Invalid(field.NewPath("containers").Index(0).Child("volume_mounts").Index(0).Child("mount_path"), nil, "")}, - }, { - name: "root mount_path", - mutate: func(tmpl *ateapipb.ActorTemplate) { - tmpl.Containers[0].VolumeMounts = []*ateapipb.VolumeMount{{Name: "data", MountPath: "/"}} - }, - want: field.ErrorList{field.Invalid(field.NewPath("containers").Index(0).Child("volume_mounts").Index(0).Child("mount_path"), nil, "")}, - }, { - name: "mount_path with dot-dot segment", - mutate: func(tmpl *ateapipb.ActorTemplate) { - tmpl.Containers[0].VolumeMounts = []*ateapipb.VolumeMount{{Name: "data", MountPath: "/var/../etc"}} - }, - want: field.ErrorList{field.Invalid(field.NewPath("containers").Index(0).Child("volume_mounts").Index(0).Child("mount_path"), nil, "")}, - }, { - name: "mount_path with trailing slash", - mutate: func(tmpl *ateapipb.ActorTemplate) { - tmpl.Containers[0].VolumeMounts = []*ateapipb.VolumeMount{{Name: "data", MountPath: "/var/data/"}} - }, - want: field.ErrorList{field.Invalid(field.NewPath("containers").Index(0).Child("volume_mounts").Index(0).Child("mount_path"), nil, "")}, }, { name: "valid durable_dir volume", mutate: func(tmpl *ateapipb.ActorTemplate) { @@ -882,18 +773,6 @@ func TestValidateActorTemplate(t *testing.T) { tmpl.Volumes = []*ateapipb.Volume{{Name: "tools", Image: &ateapipb.ImageVolumeSource{Reference: "example.com/app:v1"}}} }, want: field.ErrorList{field.Invalid(field.NewPath("volumes").Index(0).Child("image", "reference"), nil, "")}, - }, { - name: "image volume reference with malformed digest", - mutate: func(tmpl *ateapipb.ActorTemplate) { - tmpl.Volumes = []*ateapipb.Volume{{Name: "tools", Image: &ateapipb.ImageVolumeSource{Reference: "example.com/app@sha256:abc"}}} - }, - want: field.ErrorList{field.Invalid(field.NewPath("volumes").Index(0).Child("image", "reference"), nil, "")}, - }, { - name: "image volume reference not a reference at all", - mutate: func(tmpl *ateapipb.ActorTemplate) { - tmpl.Volumes = []*ateapipb.Volume{{Name: "tools", Image: &ateapipb.ImageVolumeSource{Reference: "@"}}} - }, - want: field.ErrorList{field.Invalid(field.NewPath("volumes").Index(0).Child("image", "reference"), nil, "")}, }, { name: "valid external volume template", mutate: func(tmpl *ateapipb.ActorTemplate) { @@ -976,18 +855,6 @@ func TestValidateActorTemplate(t *testing.T) { tmpl.Containers[0].Resources = &ateapipb.Resources{Limits: []*ateapipb.Limits{{Name: "memory", Quantity: "two gigs"}}} }, want: field.ErrorList{field.Invalid(field.NewPath("containers").Index(0).Child("resources", "limits").Index(0).Child("quantity"), nil, "")}, - }, { - name: "zero limit quantity", - mutate: func(tmpl *ateapipb.ActorTemplate) { - tmpl.Containers[0].Resources = &ateapipb.Resources{Limits: []*ateapipb.Limits{{Name: "memory", Quantity: "0"}}} - }, - want: field.ErrorList{field.Invalid(field.NewPath("containers").Index(0).Child("resources", "limits").Index(0).Child("quantity"), nil, "")}, - }, { - name: "cpu limit of 1000 cores", - mutate: func(tmpl *ateapipb.ActorTemplate) { - tmpl.Containers[0].Resources = &ateapipb.Resources{Limits: []*ateapipb.Limits{{Name: "cpu", Quantity: "1k"}}} - }, - want: field.ErrorList{field.Invalid(field.NewPath("containers").Index(0).Child("resources", "limits").Index(0).Child("quantity"), nil, "")}, }, { name: "too many limits", mutate: func(tmpl *ateapipb.ActorTemplate) { diff --git a/cmd/ateapi/internal/apivalidation/validation_test.go b/cmd/ateapi/internal/apivalidation/validation_test.go index a8dd874f67..9a90cf92c7 100644 --- a/cmd/ateapi/internal/apivalidation/validation_test.go +++ b/cmd/ateapi/internal/apivalidation/validation_test.go @@ -760,58 +760,12 @@ func TestValidateSystemInfoVolumeSource(t *testing.T) { s.DataSources[0].ActorMetadata.Items[0].Path = strings.Repeat("p", 256) }), want: field.ErrorList{field.TooLong(itemsPath.Index(0).Child("path"), nil, 255).WithOrigin("maxLength")}, - }, { - name: "valid: nested item path", - obj: valid(func(s *ateapipb.SystemInfoVolumeSource) { - s.DataSources[0].ActorMetadata.Items[0].Path = "meta/actor-name" - }), }, { name: "item path traversal", obj: valid(func(s *ateapipb.SystemInfoVolumeSource) { s.DataSources[0].ActorMetadata.Items[0].Path = "../../traversal-escape" }), want: field.ErrorList{field.Invalid(itemsPath.Index(0).Child("path"), nil, "")}, - }, { - name: "absolute item path", - obj: valid(func(s *ateapipb.SystemInfoVolumeSource) { - s.DataSources[0].ActorMetadata.Items[0].Path = "/escaped" - }), - want: field.ErrorList{field.Invalid(itemsPath.Index(0).Child("path"), nil, "")}, - }, { - name: "item path with dot segment", - obj: valid(func(s *ateapipb.SystemInfoVolumeSource) { - s.DataSources[0].ActorMetadata.Items[0].Path = "./actor-name" - }), - want: field.ErrorList{field.Invalid(itemsPath.Index(0).Child("path"), nil, "")}, - }, { - name: "item path with trailing slash", - obj: valid(func(s *ateapipb.SystemInfoVolumeSource) { - s.DataSources[0].ActorMetadata.Items[0].Path = "actor-name/" - }), - want: field.ErrorList{field.Invalid(itemsPath.Index(0).Child("path"), nil, "")}, - }, { - name: "item path with empty segment", - obj: valid(func(s *ateapipb.SystemInfoVolumeSource) { - s.DataSources[0].ActorMetadata.Items[0].Path = "meta//actor-name" - }), - want: field.ErrorList{field.Invalid(itemsPath.Index(0).Child("path"), nil, "")}, - }, { - name: "item path with NUL byte", - obj: valid(func(s *ateapipb.SystemInfoVolumeSource) { - s.DataSources[0].ActorMetadata.Items[0].Path = "actor\x00name" - }), - want: field.ErrorList{field.Invalid(itemsPath.Index(0).Child("path"), nil, "")}, - }, { - name: "valid: unicode item path", - obj: valid(func(s *ateapipb.SystemInfoVolumeSource) { - s.DataSources[0].ActorMetadata.Items[0].Path = "méta/имя" - }), - }, { - name: "item path with too many segments", - obj: valid(func(s *ateapipb.SystemInfoVolumeSource) { - s.DataSources[0].ActorMetadata.Items[0].Path = strings.Repeat("d/", 16) + "actor-name" - }), - want: field.ErrorList{field.Invalid(itemsPath.Index(0).Child("path"), nil, "")}, }, { name: "trust bundle path traversal", obj: valid(func(s *ateapipb.SystemInfoVolumeSource) { @@ -830,6 +784,19 @@ func TestValidateSystemInfoVolumeSource(t *testing.T) { s.DataSources[1].TrustBundle.Path = "actor-uid" }), want: field.ErrorList{field.Duplicate(dsPath.Index(1).Child("trust_bundle", "path"), nil)}, + }, { + name: "second actor_metadata entry", + obj: valid(func(s *ateapipb.SystemInfoVolumeSource) { + s.DataSources = append(s.DataSources, &ateapipb.SystemInfoDataSource{ + ActorMetadata: &ateapipb.ActorMetadataDataSource{ + Items: []*ateapipb.ActorMetadataItem{{ + Field: ateapipb.ActorMetadataField_ACTOR_METADATA_FIELD_NAME, + Path: "actor-name-2", + }}, + }, + }) + }), + want: field.ErrorList{field.Forbidden(dsPath.Index(2).Child("actor_metadata"), "")}, }} for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -890,17 +857,10 @@ func TestValidateTrustBundleDataSource(t *testing.T) { name: "path too long", obj: valid(func(tb *ateapipb.TrustBundleDataSource) { tb.Path = strings.Repeat("p", 256) }), want: field.ErrorList{field.TooLong(field.NewPath("path"), nil, 255).WithOrigin("maxLength")}, - }, { - name: "valid: nested path", - obj: valid(func(tb *ateapipb.TrustBundleDataSource) { tb.Path = "certs/ca.pem" }), }, { name: "path traversal", obj: valid(func(tb *ateapipb.TrustBundleDataSource) { tb.Path = "../escape" }), want: field.ErrorList{field.Invalid(field.NewPath("path"), nil, "")}, - }, { - name: "absolute path", - obj: valid(func(tb *ateapipb.TrustBundleDataSource) { tb.Path = "/escaped" }), - want: field.ErrorList{field.Invalid(field.NewPath("path"), nil, "")}, }} for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -949,29 +909,10 @@ func TestValidateExternalVolume(t *testing.T) { name: "invalid storage volume id with null U+0000", obj: valid(func(v *ateapipb.ExternalVolume) { v.StorageVolumeId = "vol\x00id" }), want: field.ErrorList{field.Invalid(field.NewPath("storage_volume_id"), nil, "")}, - }, { - name: "invalid storage volume id with unit separator U+001F", - obj: valid(func(v *ateapipb.ExternalVolume) { v.StorageVolumeId = "vol\x1fid" }), - want: field.ErrorList{field.Invalid(field.NewPath("storage_volume_id"), nil, "")}, - }, { - name: "invalid storage volume id with DEL U+007F", - obj: valid(func(v *ateapipb.ExternalVolume) { v.StorageVolumeId = "vol\x7fid" }), - want: field.ErrorList{field.Invalid(field.NewPath("storage_volume_id"), nil, "")}, - }, { - name: "invalid storage volume id with C1 control U+0080", - obj: valid(func(v *ateapipb.ExternalVolume) { v.StorageVolumeId = "vol\u0080id" }), - want: field.ErrorList{field.Invalid(field.NewPath("storage_volume_id"), nil, "")}, - }, { - name: "invalid storage volume id with C1 control U+009F", - obj: valid(func(v *ateapipb.ExternalVolume) { v.StorageVolumeId = "vol\u009fid" }), - want: field.ErrorList{field.Invalid(field.NewPath("storage_volume_id"), nil, "")}, }, { name: "storage volume id too long", obj: valid(func(v *ateapipb.ExternalVolume) { v.StorageVolumeId = strings.Repeat("x", 257) }), want: field.ErrorList{field.TooLong(field.NewPath("storage_volume_id"), nil, 256).WithOrigin("maxLength")}, - }, { - name: "valid csi volume type", - obj: valid(func(v *ateapipb.ExternalVolume) { v.VolumeType = "pd.csi.storage.gke.io" }), }, { name: "missing volume type", obj: valid(func(v *ateapipb.ExternalVolume) { v.VolumeType = "" }), @@ -993,18 +934,6 @@ func TestValidateExternalVolume(t *testing.T) { }, { name: "valid volume with substrate.io prefixed volume type", obj: valid(func(v *ateapipb.ExternalVolume) { v.VolumeType = "substrate.io/mock" }), - }, { - name: "invalid volume type with empty plugin after substrate.io prefix", - obj: valid(func(v *ateapipb.ExternalVolume) { v.VolumeType = "substrate.io/" }), - want: field.ErrorList{field.Invalid(field.NewPath("volume_type"), nil, "")}, - }, { - name: "invalid volume type with invalid plugin name after substrate.io prefix", - obj: valid(func(v *ateapipb.ExternalVolume) { v.VolumeType = "substrate.io/Mock_Plugin" }), - want: field.ErrorList{field.Invalid(field.NewPath("volume_type"), nil, "")}, - }, { - name: "invalid volume type with non-substrate prefix", - obj: valid(func(v *ateapipb.ExternalVolume) { v.VolumeType = "other.io/mock" }), - want: field.ErrorList{field.Invalid(field.NewPath("volume_type"), nil, "")}, }, { name: "negative status", obj: valid(func(v *ateapipb.ExternalVolume) { v.Status = ateapipb.ExternalVolume_Status(-1) }), @@ -1017,25 +946,25 @@ func TestValidateExternalVolume(t *testing.T) { name: "storage volume id at the bound", obj: valid(func(v *ateapipb.ExternalVolume) { v.StorageVolumeId = strings.Repeat("x", 256) }), }, { - name: "too many volume_context entries", + name: "volume_context with many entries, as a CSI driver may return", obj: valid(func(v *ateapipb.ExternalVolume) { - ctxMap := make(map[string]string, 33) - for i := 0; i < 33; i++ { - ctxMap[fmt.Sprintf("key-%d", i)] = "v" + ctxMap := make(map[string]string, 100) + for i := range 100 { + ctxMap[fmt.Sprintf("key-%03d", i)] = "v" } v.VolumeContext = ctxMap }), - want: field.ErrorList{field.TooMany(field.NewPath("volume_context"), 33, 32).WithOrigin("maxProperties")}, }, { - name: "volume_context key too long", - obj: valid(func(v *ateapipb.ExternalVolume) { v.VolumeContext = map[string]string{strings.Repeat("k", 129): "v"} }), - want: field.ErrorList{field.TooLong(field.NewPath("volume_context"), nil, 128).WithOrigin("maxLength")}, + name: "volume_context with one long value, as a CSI driver may return", + obj: valid(func(v *ateapipb.ExternalVolume) { + v.VolumeContext = map[string]string{"attachment": strings.Repeat("v", 4000)} + }), }, { - name: "volume_context value too long", + name: "volume_context over the size bound", obj: valid(func(v *ateapipb.ExternalVolume) { - v.VolumeContext = map[string]string{"attachment": strings.Repeat("v", 257)} + v.VolumeContext = map[string]string{"attachment": strings.Repeat("v", 40*1024)} }), - want: field.ErrorList{field.TooLong(field.NewPath("volume_context").Key("attachment"), nil, 256).WithOrigin("maxLength")}, + want: field.ErrorList{field.TooLong(field.NewPath("volume_context"), nil, 40*1024)}, }} for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/cmd/ateapi/internal/apivalidation/zz_generated.validation.go b/cmd/ateapi/internal/apivalidation/zz_generated.validation.go index f27f64d8c1..6cace304e7 100644 --- a/cmd/ateapi/internal/apivalidation/zz_generated.validation.go +++ b/cmd/ateapi/internal/apivalidation/zz_generated.validation.go @@ -3866,26 +3866,14 @@ func Validate_ExternalVolume( } // call field-attached validations earlyReturn := false - if e := validate.MaxProperties(ctx, op, fldPath, obj, oldObj, 32).MarkShortCircuit(); len(e) != 0 { - errs = append(errs, e...) - earlyReturn = true - } if e := validate.OptionalMap(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { earlyReturn = true } if earlyReturn { return // do not proceed } - if e := validate.EachMapKey(ctx, op, fldPath, obj, oldObj, - func(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *string) field.ErrorList { - return validate.MaxLength(ctx, op, fldPath, obj, oldObj, 128) - }); len(e) != 0 { - errs = append(errs, e...) - } - if e := validate.EachMapVal(ctx, op, fldPath, obj, oldObj, validate.DirectEqual, - func(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *string) field.ErrorList { - return validate.MaxLength(ctx, op, fldPath, obj, oldObj, 256) - }); len(e) != 0 { + // custom validation + if e := ValidateCustom_ExternalVolume_VolumeContext(ctx, op, fldPath, obj, oldObj); len(e) != 0 { errs = append(errs, e...) } return diff --git a/cmd/ateapi/internal/controlapi/workflow_delete.go b/cmd/ateapi/internal/controlapi/workflow_delete.go index 90ce7d507b..e97af85ba5 100644 --- a/cmd/ateapi/internal/controlapi/workflow_delete.go +++ b/cmd/ateapi/internal/controlapi/workflow_delete.go @@ -179,12 +179,10 @@ func (w *ActorWorkflow) ensureAteletTerminated(ctx context.Context, actorRef res if vol.GetStorageVolumeId() != "" { workloadSpec.Volumes = append(workloadSpec.Volumes, &ateletpb.Volume{ Name: vol.GetName(), - Source: &ateletpb.Volume_External{ - External: &ateletpb.ExternalVolumeSource{ - StorageVolumeId: vol.GetStorageVolumeId(), - VolumeType: vol.GetVolumeType(), - VolumeContext: vol.GetVolumeContext(), - }, + External: &ateletpb.ExternalVolumeSource{ + StorageVolumeId: vol.GetStorageVolumeId(), + VolumeType: vol.GetVolumeType(), + VolumeContext: vol.GetVolumeContext(), }, }) } diff --git a/cmd/ateapi/internal/controlapi/workflow_pause.go b/cmd/ateapi/internal/controlapi/workflow_pause.go index f48e68106b..d2893eb272 100644 --- a/cmd/ateapi/internal/controlapi/workflow_pause.go +++ b/cmd/ateapi/internal/controlapi/workflow_pause.go @@ -195,10 +195,8 @@ func (w *ActorWorkflow) ensureAteletPaused(ctx context.Context, actorRef resourc ActorTemplateName: actor.GetActorTemplate().GetName(), Spec: workloadSpec, Type: ateletpb.CheckpointType_CHECKPOINT_TYPE_LOCAL, - Config: &ateletpb.CheckpointRequest_LocalConfig{ - LocalConfig: &ateletpb.LocalCheckpointConfiguration{ - SnapshotName: inProgressSnap.GetUuid(), - }, + LocalConfig: &ateletpb.LocalCheckpointConfiguration{ + SnapshotName: inProgressSnap.GetUuid(), }, Fidelity: fidelityToAtelet(actorTemplate.GetSnapshotConfig().GetPreferredFidelity()), ActorUid: actor.GetMetadata().Uid, diff --git a/cmd/ateapi/internal/controlapi/workflow_resume.go b/cmd/ateapi/internal/controlapi/workflow_resume.go index 3ee70ac6c3..3e472702f9 100644 --- a/cmd/ateapi/internal/controlapi/workflow_resume.go +++ b/cmd/ateapi/internal/controlapi/workflow_resume.go @@ -713,9 +713,7 @@ func (w *ActorWorkflow) ensureAteletRestored(ctx context.Context, actorRef resou MemoryBytes: memBytes, } req.Type = ateletpb.CheckpointType_CHECKPOINT_TYPE_LOCAL - req.Config = &ateletpb.RestoreRequest_LocalConfig{ - LocalConfig: &ateletpb.LocalCheckpointConfiguration{SnapshotName: localSnap.GetUuid()}, - } + req.LocalConfig = &ateletpb.LocalCheckpointConfiguration{SnapshotName: localSnap.GetUuid()} req.Fidelity = fidelityToAtelet(localSnap.GetFidelity()) tele.WireFidelity = ateattr.SnapshotFidelityValue(req.Fidelity) @@ -739,10 +737,8 @@ func (w *ActorWorkflow) ensureAteletRestored(ctx context.Context, actorRef resou ActorTemplateName: actor.GetActorTemplate().GetName(), Spec: workloadSpec, Type: ateletpb.CheckpointType_CHECKPOINT_TYPE_EXTERNAL, - Config: &ateletpb.RestoreRequest_ExternalConfig{ - ExternalConfig: &ateletpb.ExternalRestoreConfiguration{ - SnapshotUri: src.SnapshotURI.String(), - }, + ExternalConfig: &ateletpb.ExternalRestoreConfiguration{ + SnapshotUri: src.SnapshotURI.String(), }, Fidelity: scope, SandboxAssets: sandboxAssets, diff --git a/cmd/ateapi/internal/controlapi/workflow_suspend.go b/cmd/ateapi/internal/controlapi/workflow_suspend.go index 99c89fea5c..ec2612202b 100644 --- a/cmd/ateapi/internal/controlapi/workflow_suspend.go +++ b/cmd/ateapi/internal/controlapi/workflow_suspend.go @@ -261,10 +261,8 @@ func (w *ActorWorkflow) ensureAteletSuspended(ctx context.Context, actorRef reso ActorTemplateName: actor.GetActorTemplate().GetName(), Spec: workloadSpec, Type: ateletpb.CheckpointType_CHECKPOINT_TYPE_EXTERNAL, - Config: &ateletpb.CheckpointRequest_ExternalConfig{ - ExternalConfig: &ateletpb.ExternalCheckpointConfiguration{ - SnapshotUri: inProgressSnap.GetDurableSnapshot().GetObject().GetSnapshotUri(), - }, + ExternalConfig: &ateletpb.ExternalCheckpointConfiguration{ + SnapshotUri: inProgressSnap.GetDurableSnapshot().GetObject().GetSnapshotUri(), }, Fidelity: fidelityToAtelet(preferredFidelity(actor.GetMetadata().GetAtespace(), actorTemplate)), ActorUid: actor.GetMetadata().Uid, diff --git a/cmd/ateapi/internal/controlapi/workload_spec.go b/cmd/ateapi/internal/controlapi/workload_spec.go index e59021e09e..bd1df172ce 100644 --- a/cmd/ateapi/internal/controlapi/workload_spec.go +++ b/cmd/ateapi/internal/controlapi/workload_spec.go @@ -57,10 +57,8 @@ func workloadSpecFromActorTemplate(actorTemplate *ateapipb.ActorTemplate, actor switch { case vol.GetDurableDir() != nil: workloadSpec.Volumes = append(workloadSpec.Volumes, &ateletpb.Volume{ - Name: vol.GetName(), - Source: &ateletpb.Volume_DurableDir{ - DurableDir: &ateletpb.DurableDirVolume{}, - }, + Name: vol.GetName(), + DurableDir: &ateletpb.DurableDirVolume{}, }) case vol.GetSystemInfo() != nil: @@ -76,19 +74,15 @@ func workloadSpecFromActorTemplate(actorTemplate *ateapipb.ActorTemplate, actor }) } ateletSystemInfo.DataSources = append(ateletSystemInfo.DataSources, &ateletpb.SystemInfoDataSource{ - DataSource: &ateletpb.SystemInfoDataSource_ActorMetadata{ - ActorMetadata: actorMetadata, - }, + ActorMetadata: actorMetadata, }) case dataSource.GetTrustBundle() != nil: // atelet resolves named trustBundles against its allowlist // and ClusterTrustBundle informer at write time ateletSystemInfo.DataSources = append(ateletSystemInfo.DataSources, &ateletpb.SystemInfoDataSource{ - DataSource: &ateletpb.SystemInfoDataSource_TrustBundle{ - TrustBundle: &ateletpb.TrustBundleDataSource{ - Names: dataSource.GetTrustBundle().GetNames(), - Path: dataSource.GetTrustBundle().GetPath(), - }, + TrustBundle: &ateletpb.TrustBundleDataSource{ + Names: dataSource.GetTrustBundle().GetNames(), + Path: dataSource.GetTrustBundle().GetPath(), }, }) default: @@ -96,19 +90,15 @@ func workloadSpecFromActorTemplate(actorTemplate *ateapipb.ActorTemplate, actor } } workloadSpec.Volumes = append(workloadSpec.Volumes, &ateletpb.Volume{ - Name: vol.GetName(), - Source: &ateletpb.Volume_SystemInfo{ - SystemInfo: ateletSystemInfo, - }, + Name: vol.GetName(), + SystemInfo: ateletSystemInfo, }) case vol.GetImage() != nil: workloadSpec.Volumes = append(workloadSpec.Volumes, &ateletpb.Volume{ Name: vol.GetName(), - Source: &ateletpb.Volume_Image{ - Image: &ateletpb.ImageVolumeSource{ - Reference: vol.GetImage().GetReference(), - }, + Image: &ateletpb.ImageVolumeSource{ + Reference: vol.GetImage().GetReference(), }, }) @@ -188,13 +178,11 @@ func appendExternalVolumes(workloadSpec *ateletpb.WorkloadSpec, template *ateapi } workloadSpec.Volumes = append(workloadSpec.Volumes, &ateletpb.Volume{ Name: vol.GetName(), - Source: &ateletpb.Volume_External{ - External: &ateletpb.ExternalVolumeSource{ - StorageVolumeId: storageVolID, - VolumeType: volType, - VolumeContext: volCtx, - PublishContext: volumePublishContexts[vol.GetName()], - }, + External: &ateletpb.ExternalVolumeSource{ + StorageVolumeId: storageVolID, + VolumeType: volType, + VolumeContext: volCtx, + PublishContext: volumePublishContexts[vol.GetName()], }, }) } diff --git a/cmd/ateapi/internal/controlapi/workload_spec_test.go b/cmd/ateapi/internal/controlapi/workload_spec_test.go index 7a86971ebc..68d569b5b6 100644 --- a/cmd/ateapi/internal/controlapi/workload_spec_test.go +++ b/cmd/ateapi/internal/controlapi/workload_spec_test.go @@ -50,8 +50,8 @@ func TestWorkloadSpecFromActorTemplate(t *testing.T) { want: &ateletpb.WorkloadSpec{ Volumes: []*ateletpb.Volume{ { - Name: "home", - Source: &ateletpb.Volume_DurableDir{DurableDir: &ateletpb.DurableDirVolume{}}, + Name: "home", + DurableDir: &ateletpb.DurableDirVolume{}, }, }, Containers: []*ateletpb.Container{ @@ -100,19 +100,15 @@ func TestWorkloadSpecFromActorTemplate(t *testing.T) { Volumes: []*ateletpb.Volume{ { Name: "system-info", - Source: &ateletpb.Volume_SystemInfo{ - SystemInfo: &ateletpb.SystemInfoVolume{ - DataSources: []*ateletpb.SystemInfoDataSource{ - {DataSource: &ateletpb.SystemInfoDataSource_ActorMetadata{ - ActorMetadata: &ateletpb.ActorMetadataDataSource{ - Items: []*ateletpb.ActorMetadataItem{ - {Field: ateletpb.ActorMetadataField_ACTOR_METADATA_FIELD_NAME, Path: "actor-name"}, - {Field: ateletpb.ActorMetadataField_ACTOR_METADATA_FIELD_ATESPACE, Path: "atespace"}, - {Field: ateletpb.ActorMetadataField_ACTOR_METADATA_FIELD_UID, Path: "identity/actor-uid"}, - }, - }, - }}, - }, + SystemInfo: &ateletpb.SystemInfoVolume{ + DataSources: []*ateletpb.SystemInfoDataSource{ + {ActorMetadata: &ateletpb.ActorMetadataDataSource{ + Items: []*ateletpb.ActorMetadataItem{ + {Field: ateletpb.ActorMetadataField_ACTOR_METADATA_FIELD_NAME, Path: "actor-name"}, + {Field: ateletpb.ActorMetadataField_ACTOR_METADATA_FIELD_ATESPACE, Path: "atespace"}, + {Field: ateletpb.ActorMetadataField_ACTOR_METADATA_FIELD_UID, Path: "identity/actor-uid"}, + }, + }}, }, }, }, @@ -150,12 +146,12 @@ func TestWorkloadSpecFromActorTemplate(t *testing.T) { want: &ateletpb.WorkloadSpec{ Volumes: []*ateletpb.Volume{ { - Name: "home", - Source: &ateletpb.Volume_DurableDir{DurableDir: &ateletpb.DurableDirVolume{}}, + Name: "home", + DurableDir: &ateletpb.DurableDirVolume{}, }, { - Name: "agent", - Source: &ateletpb.Volume_Image{Image: &ateletpb.ImageVolumeSource{Reference: "example.com/agent@sha256:abc"}}, + Name: "agent", + Image: &ateletpb.ImageVolumeSource{Reference: "example.com/agent@sha256:abc"}, }, }, Containers: []*ateletpb.Container{ @@ -200,13 +196,9 @@ func TestWorkloadSpecFromActorTemplate(t *testing.T) { Volumes: []*ateletpb.Volume{ { Name: "system-info", - Source: &ateletpb.Volume_SystemInfo{ - SystemInfo: &ateletpb.SystemInfoVolume{ - DataSources: []*ateletpb.SystemInfoDataSource{ - {DataSource: &ateletpb.SystemInfoDataSource_TrustBundle{ - TrustBundle: &ateletpb.TrustBundleDataSource{Names: []string{"egress-trust"}, Path: "trust/ca.pem"}, - }}, - }, + SystemInfo: &ateletpb.SystemInfoVolume{ + DataSources: []*ateletpb.SystemInfoDataSource{ + {TrustBundle: &ateletpb.TrustBundleDataSource{Names: []string{"egress-trust"}, Path: "trust/ca.pem"}}, }, }, }, @@ -243,8 +235,8 @@ func TestWorkloadSpecFromActorTemplate(t *testing.T) { want: &ateletpb.WorkloadSpec{ Volumes: []*ateletpb.Volume{ { - Name: "home", - Source: &ateletpb.Volume_DurableDir{DurableDir: &ateletpb.DurableDirVolume{}}, + Name: "home", + DurableDir: &ateletpb.DurableDirVolume{}, }, }, Containers: []*ateletpb.Container{ @@ -272,8 +264,8 @@ func TestWorkloadSpecFromActorTemplate(t *testing.T) { want: &ateletpb.WorkloadSpec{ Volumes: []*ateletpb.Volume{ { - Name: "home", - Source: &ateletpb.Volume_DurableDir{DurableDir: &ateletpb.DurableDirVolume{}}, + Name: "home", + DurableDir: &ateletpb.DurableDirVolume{}, }, }, Containers: []*ateletpb.Container{{Name: "main", Image: "main"}}, @@ -456,13 +448,11 @@ func TestAppendExternalVolumes(t *testing.T) { Volumes: []*ateletpb.Volume{ { Name: "vol-1", - Source: &ateletpb.Volume_External{ - External: &ateletpb.ExternalVolumeSource{ - StorageVolumeId: "vol-gce-pd-123", - VolumeType: "pd-standard", - VolumeContext: map[string]string{"foo": "bar"}, - PublishContext: tt.wantPublishCtx, - }, + External: &ateletpb.ExternalVolumeSource{ + StorageVolumeId: "vol-gce-pd-123", + VolumeType: "pd-standard", + VolumeContext: map[string]string{"foo": "bar"}, + PublishContext: tt.wantPublishCtx, }, }, }, diff --git a/cmd/ateapi/main.go b/cmd/ateapi/main.go index ab3580bbcc..9c5c5f541d 100644 --- a/cmd/ateapi/main.go +++ b/cmd/ateapi/main.go @@ -41,6 +41,7 @@ import ( "github.com/agent-substrate/substrate/internal/oidcdiscovery" "github.com/agent-substrate/substrate/internal/oidcjwt" "github.com/agent-substrate/substrate/internal/pluginsocket" + "github.com/agent-substrate/substrate/internal/resources" "github.com/agent-substrate/substrate/internal/serverboot" "github.com/agent-substrate/substrate/internal/version" "github.com/agent-substrate/substrate/internal/volume" @@ -56,6 +57,7 @@ import ( "google.golang.org/grpc/credentials" "google.golang.org/grpc/keepalive" "google.golang.org/grpc/reflection" + "k8s.io/apimachinery/pkg/util/validation/field" "k8s.io/client-go/informers" "k8s.io/client-go/kubernetes" "k8s.io/client-go/rest" @@ -129,6 +131,11 @@ func main() { if err := validateActorJWTIssuer(*actorJWTIssuer); err != nil { serverboot.Fatal(ctx, "Invalid --actor-jwt-issuer", err) } + // atelet applies the same rule to EgressGateway.address on every Run and + // Restore; a value it would refuse must fail the rollout, not the actors. + if errs := resources.ValidateHostPort(field.NewPath("default-egress-gateway-address"), *defaultEgressGatewayAddress); len(errs) > 0 { + serverboot.Fatal(ctx, "Invalid --default-egress-gateway-address", errs.ToAggregate()) + } // Kept separate from ctx so that in-progress work (clients, informers) is // not cancelled the moment SIGTERM arrives. The drainOnShutdown diff --git a/cmd/atelet/imagevolume_test.go b/cmd/atelet/imagevolume_test.go index a4df056f00..e22c60cb86 100644 --- a/cmd/atelet/imagevolume_test.go +++ b/cmd/atelet/imagevolume_test.go @@ -72,7 +72,9 @@ func singleFileLayer(t *testing.T, path, body string) v1.Layer { return l } -func pushTestImage(t *testing.T, ref string, layers ...v1.Layer) { +// pushTestImage pushes the image and returns its digest-pinned reference, +// which is what validated workload specs carry. +func pushTestImage(t *testing.T, ref string, layers ...v1.Layer) string { t.Helper() img, err := mutate.AppendLayers(empty.Image, layers...) if err != nil { @@ -85,6 +87,11 @@ func pushTestImage(t *testing.T, ref string, layers ...v1.Layer) { if err := remote.Write(tag, img); err != nil { t.Fatalf("remote.Write(%q): %v", ref, err) } + digest, err := img.Digest() + if err != nil { + t.Fatalf("computing digest of %q: %v", ref, err) + } + return tag.Context().Name() + "@" + digest.String() } func newImageVolumeStore(t *testing.T) *imagecache.Store { @@ -104,8 +111,8 @@ func TestResolveImageVolumes_RecordsLayersAndDigest(t *testing.T) { pushTestImage(t, ref, singleFileLayer(t, "payload-binary", "binary")) volumes := []*ateletpb.Volume{{ - Name: "agent", - Source: &ateletpb.Volume_Image{Image: &ateletpb.ImageVolumeSource{Reference: ref}}, + Name: "agent", + Image: &ateletpb.ImageVolumeSource{Reference: ref}, }} mounts := []*ateletpb.VolumeMount{{Name: "agent", MountPath: "/ate"}} @@ -138,8 +145,8 @@ func TestResolveImageVolumes_MultiLayer(t *testing.T) { ) volumes := []*ateletpb.Volume{{ - Name: "agent", - Source: &ateletpb.Volume_Image{Image: &ateletpb.ImageVolumeSource{Reference: ref}}, + Name: "agent", + Image: &ateletpb.ImageVolumeSource{Reference: ref}, }} mounts := []*ateletpb.VolumeMount{{Name: "agent", MountPath: "/ate"}} @@ -161,8 +168,8 @@ func TestResolveImageVolumes_MultiLayer(t *testing.T) { // unused volume cannot fail the actor. func TestResolveImageVolumes_UnmountedVolumeNotPulled(t *testing.T) { volumes := []*ateletpb.Volume{{ - Name: "agent", - Source: &ateletpb.Volume_Image{Image: &ateletpb.ImageVolumeSource{Reference: "127.0.0.1:1/nope@sha256:abc"}}, + Name: "agent", + Image: &ateletpb.ImageVolumeSource{Reference: "127.0.0.1:1/nope@sha256:abc"}, }} got, err := resolveImageVolumes(t.Context(), newImageVolumeStore(t), volumes, nil) diff --git a/cmd/atelet/internal/apivalidation/validate.go b/cmd/atelet/internal/apivalidation/validate.go index cb69930019..651efe361c 100644 --- a/cmd/atelet/internal/apivalidation/validate.go +++ b/cmd/atelet/internal/apivalidation/validate.go @@ -16,9 +16,11 @@ package apivalidation import ( "context" + "fmt" "github.com/agent-substrate/substrate/internal/proto/ateletpb" "github.com/agent-substrate/substrate/internal/resources" + atev1alpha1 "github.com/agent-substrate/substrate/pkg/api/v1alpha1" "k8s.io/apimachinery/pkg/api/operation" "k8s.io/apimachinery/pkg/util/validation/field" ) @@ -33,6 +35,31 @@ func ValidateMintActorCertificateRequest(ctx context.Context, req *ateletpb.Mint return Validate_MintActorCertificateRequest(ctx, operation.Operation{Type: operation.Create}, nil, req, nil) } +// ValidateRunRequest runs the generated validation for req. +func ValidateRunRequest(ctx context.Context, req *ateletpb.RunRequest) field.ErrorList { + return Validate_RunRequest(ctx, operation.Operation{Type: operation.Create}, nil, req, nil) +} + +// ValidateTerminateRequest runs the generated validation for req. +func ValidateTerminateRequest(ctx context.Context, req *ateletpb.TerminateRequest) field.ErrorList { + return Validate_TerminateRequest(ctx, operation.Operation{Type: operation.Create}, nil, req, nil) +} + +// ValidateCheckpointRequest runs the generated validation for req. +func ValidateCheckpointRequest(ctx context.Context, req *ateletpb.CheckpointRequest) field.ErrorList { + return Validate_CheckpointRequest(ctx, operation.Operation{Type: operation.Create}, nil, req, nil) +} + +// ValidateRestoreRequest runs the generated validation for req. +func ValidateRestoreRequest(ctx context.Context, req *ateletpb.RestoreRequest) field.ErrorList { + return Validate_RestoreRequest(ctx, operation.Operation{Type: operation.Create}, nil, req, nil) +} + +// ValidateUploadPausedCheckpointRequest runs the generated validation for req. +func ValidateUploadPausedCheckpointRequest(ctx context.Context, req *ateletpb.UploadPausedCheckpointRequest) field.ErrorList { + return Validate_UploadPausedCheckpointRequest(ctx, operation.Operation{Type: operation.Create}, nil, req, nil) +} + // ValidateRegisterWorkerRequest runs the generated validation for req. func ValidateRegisterWorkerRequest(ctx context.Context, req *ateletpb.RegisterWorkerRequest) field.ErrorList { return Validate_RegisterWorkerRequest(ctx, operation.Operation{Type: operation.Create}, nil, req, nil) @@ -45,6 +72,86 @@ func ValidateCustom_Limits(_ context.Context, _ operation.Operation, fldPath *fi return resources.ValidateLimit(fldPath, value.GetName(), value.GetQuantity()) } +func ValidateCustom_EgressGateway_Address(_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ *string) field.ErrorList { + return resources.ValidateHostPort(fldPath, *value) +} + +// ValidateCustom_SandboxAssets_SandboxClass allows the sandbox classes of the +// SandboxConfig CRD. +func ValidateCustom_SandboxAssets_SandboxClass(_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ *string) field.ErrorList { + switch atev1alpha1.SandboxClass(*value) { + case atev1alpha1.SandboxClassGvisor, atev1alpha1.SandboxClassMicroVM: + return nil + } + return field.ErrorList{field.NotSupported(fldPath, *value, []string{ + string(atev1alpha1.SandboxClassGvisor), string(atev1alpha1.SandboxClassMicroVM), + })} +} + +func ValidateCustom_SandboxAssets_PauseImage(_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ *string) field.ErrorList { + return resources.ValidatePinnedImage(fldPath, *value) +} + +func ValidateCustom_AssetFile_Sha256(_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ *string) field.ErrorList { + if err := resources.ValidateRunscHash(*value); err != nil { + return field.ErrorList{field.Invalid(fldPath, *value, err.Error())} + } + return nil +} + +// ValidateCustom_UploadPausedCheckpointRequest_Atespace rejects the golden +// atespace: golden actors are never paused, so a paused checkpoint is never +// promoted to a golden snapshot. +func ValidateCustom_UploadPausedCheckpointRequest_Atespace(_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ *string) field.ErrorList { + if *value == resources.GoldenActorAtespace { + return field.ErrorList{field.Forbidden(fldPath, fmt.Sprintf("atespace %q holds golden actors, which are never paused", *value))} + } + return nil +} + +// ValidateCustom_CheckpointRequest requires the config that matches type. +func ValidateCustom_CheckpointRequest(_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ *ateletpb.CheckpointRequest) field.ErrorList { + return validateConfigMatchesType(fldPath, value.GetType(), value.GetLocalConfig() != nil, value.GetExternalConfig() != nil) +} + +// ValidateCustom_RestoreRequest requires the config that matches type. +func ValidateCustom_RestoreRequest(_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ *ateletpb.RestoreRequest) field.ErrorList { + return validateConfigMatchesType(fldPath, value.GetType(), value.GetLocalConfig() != nil, value.GetExternalConfig() != nil) +} + +// validateConfigMatchesType requires the config that matches a checkpoint +// type. The union tags already require exactly one config. +func validateConfigMatchesType(fldPath *field.Path, typ ateletpb.CheckpointType, hasLocal, hasExternal bool) field.ErrorList { + switch { + case typ == ateletpb.CheckpointType_CHECKPOINT_TYPE_LOCAL && !hasLocal: + return field.ErrorList{field.Required(fldPath.Child("local_config"), "required when type is CHECKPOINT_TYPE_LOCAL")} + case typ == ateletpb.CheckpointType_CHECKPOINT_TYPE_EXTERNAL && !hasExternal: + return field.ErrorList{field.Required(fldPath.Child("external_config"), "required when type is CHECKPOINT_TYPE_EXTERNAL")} + } + return nil +} + +func ValidateCustom_ExternalCheckpointConfiguration_SnapshotUri(_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ *string) field.ErrorList { + return validateSnapshotURI(fldPath, *value) +} + +func ValidateCustom_ExternalRestoreConfiguration_SnapshotUri(_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ *string) field.ErrorList { + return validateSnapshotURI(fldPath, *value) +} + +func ValidateCustom_UploadPausedCheckpointRequest_DestinationSnapshotUri(_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ *string) field.ErrorList { + return validateSnapshotURI(fldPath, *value) +} + +// validateSnapshotURI requires a snapshot URI that resources.ParseSnapshotURI +// accepts. +func validateSnapshotURI(fldPath *field.Path, uri string) field.ErrorList { + if _, err := resources.ParseSnapshotURI(uri); err != nil { + return field.ErrorList{field.Invalid(fldPath, uri, err.Error())} + } + return nil +} + // ateDeepEqual is the deep-equal function declarative validation's generated // code calls by name; it delegates to resources.DeepEqual. func ateDeepEqual[T any](a, b T) bool { diff --git a/cmd/atelet/internal/apivalidation/validation_test.go b/cmd/atelet/internal/apivalidation/validation_test.go index 938a5b3e28..50f50d3c84 100644 --- a/cmd/atelet/internal/apivalidation/validation_test.go +++ b/cmd/atelet/internal/apivalidation/validation_test.go @@ -21,9 +21,33 @@ import ( "testing" "github.com/agent-substrate/substrate/internal/proto/ateletpb" + "github.com/agent-substrate/substrate/internal/resources" + "k8s.io/apimachinery/pkg/api/operation" "k8s.io/apimachinery/pkg/util/validation/field" ) +const testDigestImage = "example.com/app@sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + +const testSHA256 = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + +func validSandboxAssets(mutate ...func(*ateletpb.SandboxAssets)) *ateletpb.SandboxAssets { + a := &ateletpb.SandboxAssets{ + SandboxClass: "gvisor", + PauseImage: testDigestImage, + Assets: map[string]*ateletpb.ArchAssets{ + "amd64": {Files: map[string]*ateletpb.AssetFile{"gvisor": {Url: "gs://bucket/gvisor.tar.zstd", Sha256: testSHA256}}}, + "arm64": {Files: map[string]*ateletpb.AssetFile{"gvisor": {Url: "gs://bucket/gvisor.tar.zstd", Sha256: testSHA256}}}, + }, + } + for _, m := range mutate { + m(a) + } + return a +} + +// createOp is for tests of nested messages, which have no request wrapper. +var createOp = operation.Operation{Type: operation.Create} + func assertValidateErr(t *testing.T, got field.ErrorList, want field.ErrorList) { t.Helper() field.ErrorMatcher{}.ByType().ByField().ByOrigin().Test(t, want, got) @@ -171,6 +195,814 @@ func TestValidateMintActorCertificateRequest(t *testing.T) { } } +func TestValidateRunRequest(t *testing.T) { + valid := func(mutate ...func(*ateletpb.RunRequest)) *ateletpb.RunRequest { + r := &ateletpb.RunRequest{ + WorkerPodUid: "0f9a3b1c-2d4e-5f60-7182-93a4b5c6d7e8", + Atespace: "team-a", + ActorName: "actor-1", + ActorUid: "01234567-89ab-cdef-0123-456789abcdef", + ActorTemplateAtespace: "team-a", + ActorTemplateName: "tmpl-1", + Spec: &ateletpb.WorkloadSpec{Containers: []*ateletpb.Container{{Name: "worker", Image: testDigestImage}}}, + SandboxAssets: validSandboxAssets(), + CpuMilli: 500, + MemoryBytes: 1 << 30, + } + for _, m := range mutate { + m(r) + } + return r + } + containerName := field.NewPath("spec", "containers").Index(0).Child("name") + + tests := []struct { + name string + obj *ateletpb.RunRequest + want field.ErrorList + }{{ + name: "valid", + obj: valid(), + }, { + name: "missing worker_pod_uid", + obj: valid(func(r *ateletpb.RunRequest) { r.WorkerPodUid = "" }), + want: field.ErrorList{field.Required(field.NewPath("worker_pod_uid"), "")}, + }, { + name: "invalid worker_pod_uid: path escape", + obj: valid(func(r *ateletpb.RunRequest) { r.WorkerPodUid = "../escape" }), + want: field.ErrorList{field.Invalid(field.NewPath("worker_pod_uid"), nil, "").WithOrigin("format=k8s-uuid")}, + }, { + name: "invalid worker_pod_uid: not a pod UID", + obj: valid(func(r *ateletpb.RunRequest) { r.WorkerPodUid = "ateom-1" }), + want: field.ErrorList{field.Invalid(field.NewPath("worker_pod_uid"), nil, "").WithOrigin("format=k8s-uuid")}, + }, { + name: "missing atespace", + obj: valid(func(r *ateletpb.RunRequest) { r.Atespace = "" }), + want: field.ErrorList{field.Required(field.NewPath("atespace"), "")}, + }, { + name: "invalid atespace: path escape", + obj: valid(func(r *ateletpb.RunRequest) { r.Atespace = "../escape" }), + want: field.ErrorList{field.Invalid(field.NewPath("atespace"), nil, "").WithOrigin("format=k8s-short-name")}, + }, { + name: "missing actor_name", + obj: valid(func(r *ateletpb.RunRequest) { r.ActorName = "" }), + want: field.ErrorList{field.Required(field.NewPath("actor_name"), "")}, + }, { + name: "invalid actor_name: path escape", + obj: valid(func(r *ateletpb.RunRequest) { r.ActorName = "../escape" }), + want: field.ErrorList{field.Invalid(field.NewPath("actor_name"), nil, "").WithOrigin("format=k8s-short-name")}, + }, { + name: "missing actor_uid", + obj: valid(func(r *ateletpb.RunRequest) { r.ActorUid = "" }), + want: field.ErrorList{field.Required(field.NewPath("actor_uid"), "")}, + }, { + name: "invalid actor_uid: path escape", + obj: valid(func(r *ateletpb.RunRequest) { r.ActorUid = "../escape" }), + want: field.ErrorList{field.Invalid(field.NewPath("actor_uid"), nil, "").WithOrigin("format=k8s-uuid")}, + }, { + name: "invalid actor_template_atespace: uppercase", + obj: valid(func(r *ateletpb.RunRequest) { r.ActorTemplateAtespace = "Team-A" }), + want: field.ErrorList{field.Invalid(field.NewPath("actor_template_atespace"), nil, "").WithOrigin("format=k8s-short-name")}, + }, { + name: "invalid actor_template_name: trailing dash", + obj: valid(func(r *ateletpb.RunRequest) { r.ActorTemplateName = "tmpl-" }), + want: field.ErrorList{field.Invalid(field.NewPath("actor_template_name"), nil, "").WithOrigin("format=k8s-short-name")}, + }, { + name: "unset template identity is allowed", + obj: valid(func(r *ateletpb.RunRequest) { + r.ActorTemplateAtespace = "" + r.ActorTemplateName = "" + }), + }, { + name: "missing spec", + obj: valid(func(r *ateletpb.RunRequest) { r.Spec = nil }), + want: field.ErrorList{field.Required(field.NewPath("spec"), "")}, + }, { + name: "missing sandbox_assets", + obj: valid(func(r *ateletpb.RunRequest) { r.SandboxAssets = nil }), + want: field.ErrorList{field.Required(field.NewPath("sandbox_assets"), "")}, + }, { + name: "sandbox asset errors surface at the sandbox_assets path", + obj: valid(func(r *ateletpb.RunRequest) { r.SandboxAssets.PauseImage = "registry.k8s.io/pause:3.10.2" }), + want: field.ErrorList{field.Invalid(field.NewPath("sandbox_assets", "pause_image"), nil, "")}, + }, { + name: "invalid container name: path escape", + obj: valid(func(r *ateletpb.RunRequest) { + r.Spec.Containers[0].Name = "../escape" + }), + want: field.ErrorList{field.Invalid(containerName, nil, "").WithOrigin("format=k8s-short-name")}, + }, { + name: "duplicate container name", + obj: valid(func(r *ateletpb.RunRequest) { + r.Spec.Containers = append(r.Spec.Containers, &ateletpb.Container{Name: "worker", Image: testDigestImage}) + }), + want: field.ErrorList{field.Duplicate(field.NewPath("spec", "containers").Index(1), nil)}, + }, { + name: "egress gateway with a DNS name", + obj: valid(func(r *ateletpb.RunRequest) { + r.EgressGateway = &ateletpb.EgressGateway{Address: "atenet-egress.ate-system.svc:443"} + }), + }, { + name: "egress gateway with an IPv6 address", + obj: valid(func(r *ateletpb.RunRequest) { + r.EgressGateway = &ateletpb.EgressGateway{Address: "[fd00::1]:443"} + }), + }, { + name: "egress gateway missing address", + obj: valid(func(r *ateletpb.RunRequest) { + r.EgressGateway = &ateletpb.EgressGateway{} + }), + want: field.ErrorList{field.Required(field.NewPath("egress_gateway", "address"), "")}, + }, { + name: "egress gateway address without a port", + obj: valid(func(r *ateletpb.RunRequest) { + r.EgressGateway = &ateletpb.EgressGateway{Address: "atenet-egress.ate-system.svc"} + }), + want: field.ErrorList{field.Invalid(field.NewPath("egress_gateway", "address"), nil, "")}, + }, { + name: "egress gateway address too long", + obj: valid(func(r *ateletpb.RunRequest) { + r.EgressGateway = &ateletpb.EgressGateway{Address: strings.Repeat("a", 258) + ":443"} + }), + want: field.ErrorList{ + field.TooLong(field.NewPath("egress_gateway", "address"), nil, 261).WithOrigin("maxLength"), + field.Invalid(field.NewPath("egress_gateway", "address"), nil, ""), + }, + }, { + name: "negative cpu_milli", + obj: valid(func(r *ateletpb.RunRequest) { r.CpuMilli = -1 }), + want: field.ErrorList{field.Invalid(field.NewPath("cpu_milli"), nil, "").WithOrigin("minimum")}, + }, { + name: "cpu_milli at the bound", + obj: valid(func(r *ateletpb.RunRequest) { r.CpuMilli = 999999 }), + }, { + name: "cpu_milli over the bound", + obj: valid(func(r *ateletpb.RunRequest) { r.CpuMilli = 1000000 }), + want: field.ErrorList{field.Invalid(field.NewPath("cpu_milli"), nil, "").WithOrigin("maximum")}, + }, { + name: "negative memory_bytes", + obj: valid(func(r *ateletpb.RunRequest) { r.MemoryBytes = -1 }), + want: field.ErrorList{field.Invalid(field.NewPath("memory_bytes"), nil, "").WithOrigin("minimum")}, + }, { + name: "unset sizes are allowed", + obj: valid(func(r *ateletpb.RunRequest) { + r.CpuMilli = 0 + r.MemoryBytes = 0 + }), + }} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assertValidateErr(t, ValidateRunRequest(context.Background(), tt.obj), tt.want) + }) + } +} + +// TestValidateSandboxAssets covers SandboxAssets and the asset files it holds. +func TestValidateSandboxAssets(t *testing.T) { + file := func(url, sha string) *ateletpb.ArchAssets { + return &ateletpb.ArchAssets{Files: map[string]*ateletpb.AssetFile{"gvisor": {Url: url, Sha256: sha}}} + } + amd64 := field.NewPath("assets").Key("amd64") + amd64File := amd64.Child("files").Key("gvisor") + + tests := []struct { + name string + obj *ateletpb.SandboxAssets + want field.ErrorList + }{{ + name: "valid gvisor", + obj: validSandboxAssets(), + }, { + name: "valid microvm", + obj: validSandboxAssets(func(a *ateletpb.SandboxAssets) { a.SandboxClass = "microvm" }), + }, { + name: "missing sandbox_class", + obj: validSandboxAssets(func(a *ateletpb.SandboxAssets) { a.SandboxClass = "" }), + want: field.ErrorList{field.Required(field.NewPath("sandbox_class"), "")}, + }, { + name: "unknown sandbox_class", + obj: validSandboxAssets(func(a *ateletpb.SandboxAssets) { a.SandboxClass = "kvm" }), + want: field.ErrorList{field.NotSupported[string](field.NewPath("sandbox_class"), nil, nil)}, + }, { + name: "sandbox_class is case-sensitive", + obj: validSandboxAssets(func(a *ateletpb.SandboxAssets) { a.SandboxClass = "GVISOR" }), + want: field.ErrorList{field.NotSupported[string](field.NewPath("sandbox_class"), nil, nil)}, + }, { + name: "sandbox_class too long", + obj: validSandboxAssets(func(a *ateletpb.SandboxAssets) { a.SandboxClass = strings.Repeat("g", 64) }), + want: field.ErrorList{ + field.TooLong(field.NewPath("sandbox_class"), nil, 63).WithOrigin("maxLength"), + field.NotSupported[string](field.NewPath("sandbox_class"), nil, nil), + }, + }, { + name: "valid: no pause_image (microvm has no pause container)", + obj: validSandboxAssets(func(a *ateletpb.SandboxAssets) { a.PauseImage = "" }), + }, { + name: "pause_image not pinned by digest", + obj: validSandboxAssets(func(a *ateletpb.SandboxAssets) { a.PauseImage = "registry.k8s.io/pause:3.10.2" }), + want: field.ErrorList{field.Invalid(field.NewPath("pause_image"), nil, "")}, + }, { + name: "missing assets", + obj: validSandboxAssets(func(a *ateletpb.SandboxAssets) { a.Assets = nil }), + want: field.ErrorList{field.Required(field.NewPath("assets"), "")}, + }, { + name: "too many architectures", + obj: validSandboxAssets(func(a *ateletpb.SandboxAssets) { + for _, arch := range []string{"386", "arm", "loong64", "mips64", "ppc64le", "riscv64", "s390x"} { + a.Assets[arch] = file("gs://bucket/gvisor.tar.zstd", testSHA256) + } + }), + want: field.ErrorList{field.TooMany(field.NewPath("assets"), 9, 8).WithOrigin("maxProperties")}, + }, { + name: "architecture with no files", + obj: validSandboxAssets(func(a *ateletpb.SandboxAssets) { a.Assets["amd64"] = &ateletpb.ArchAssets{} }), + want: field.ErrorList{field.Required(amd64.Child("files"), "")}, + }, { + name: "missing url", + obj: validSandboxAssets(func(a *ateletpb.SandboxAssets) { a.Assets["amd64"] = file("", testSHA256) }), + want: field.ErrorList{field.Required(amd64File.Child("url"), "")}, + }, { + name: "missing sha256", + obj: validSandboxAssets(func(a *ateletpb.SandboxAssets) { a.Assets["amd64"] = file("gs://bucket/gvisor.tar.zstd", "") }), + want: field.ErrorList{field.Required(amd64File.Child("sha256"), "")}, + }, { + name: "sha256 too short", + obj: validSandboxAssets(func(a *ateletpb.SandboxAssets) { a.Assets["amd64"] = file("gs://bucket/gvisor.tar.zstd", "deadbeef") }), + want: field.ErrorList{field.Invalid(amd64File.Child("sha256"), nil, "")}, + }, { + name: "sha256 too long", + obj: validSandboxAssets(func(a *ateletpb.SandboxAssets) { + a.Assets["amd64"] = file("gs://bucket/gvisor.tar.zstd", testSHA256+"0") + }), + want: field.ErrorList{ + field.TooLong(amd64File.Child("sha256"), nil, 64).WithOrigin("maxLength"), + field.Invalid(amd64File.Child("sha256"), nil, ""), + }, + }, { + name: "sha256 with a path escape", + obj: validSandboxAssets(func(a *ateletpb.SandboxAssets) { + a.Assets["amd64"] = file("gs://bucket/gvisor.tar.zstd", "../"+testSHA256[3:]) + }), + want: field.ErrorList{field.Invalid(amd64File.Child("sha256"), nil, "")}, + }} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assertValidateErr(t, Validate_SandboxAssets(context.Background(), createOp, nil, tt.obj, nil), tt.want) + }) + } +} + +func TestValidateCheckpointRequest(t *testing.T) { + const snapshotURI = "gs://bucket/root/atespaces/team-a/actors/01234567-89ab-cdef-0123-456789abcdef/snapshots/snap-1" + valid := func(mutate ...func(*ateletpb.CheckpointRequest)) *ateletpb.CheckpointRequest { + r := &ateletpb.CheckpointRequest{ + WorkerPodUid: "0f9a3b1c-2d4e-5f60-7182-93a4b5c6d7e8", + Atespace: "team-a", + ActorName: "actor-1", + ActorUid: "01234567-89ab-cdef-0123-456789abcdef", + ActorTemplateAtespace: "team-a", + ActorTemplateName: "tmpl-1", + Spec: &ateletpb.WorkloadSpec{}, + Type: ateletpb.CheckpointType_CHECKPOINT_TYPE_EXTERNAL, + ExternalConfig: &ateletpb.ExternalCheckpointConfiguration{SnapshotUri: snapshotURI}, + Fidelity: ateletpb.SnapshotFidelity_SNAPSHOT_FIDELITY_MEMORY, + } + for _, m := range mutate { + m(r) + } + return r + } + local := func(name string) func(*ateletpb.CheckpointRequest) { + return func(r *ateletpb.CheckpointRequest) { + r.Type = ateletpb.CheckpointType_CHECKPOINT_TYPE_LOCAL + r.ExternalConfig = nil + r.LocalConfig = &ateletpb.LocalCheckpointConfiguration{SnapshotName: name} + } + } + + tests := []struct { + name string + obj *ateletpb.CheckpointRequest + want field.ErrorList + }{{ + name: "valid external", + obj: valid(), + }, { + name: "valid local", + obj: valid(local("pause-snap-1")), + }, { + name: "valid volumes fidelity", + obj: valid(func(r *ateletpb.CheckpointRequest) { r.Fidelity = ateletpb.SnapshotFidelity_SNAPSHOT_FIDELITY_VOLUMES }), + }, { + name: "missing worker_pod_uid", + obj: valid(func(r *ateletpb.CheckpointRequest) { r.WorkerPodUid = "" }), + want: field.ErrorList{field.Required(field.NewPath("worker_pod_uid"), "")}, + }, { + name: "invalid worker_pod_uid: path escape", + obj: valid(func(r *ateletpb.CheckpointRequest) { r.WorkerPodUid = "../escape" }), + want: field.ErrorList{field.Invalid(field.NewPath("worker_pod_uid"), nil, "").WithOrigin("format=k8s-uuid")}, + }, { + name: "invalid worker_pod_uid: not a pod UID", + obj: valid(func(r *ateletpb.CheckpointRequest) { r.WorkerPodUid = "ateom-1" }), + want: field.ErrorList{field.Invalid(field.NewPath("worker_pod_uid"), nil, "").WithOrigin("format=k8s-uuid")}, + }, { + name: "invalid atespace: path escape", + obj: valid(func(r *ateletpb.CheckpointRequest) { r.Atespace = "../escape" }), + want: field.ErrorList{field.Invalid(field.NewPath("atespace"), nil, "").WithOrigin("format=k8s-short-name")}, + }, { + name: "invalid actor_name: uppercase", + obj: valid(func(r *ateletpb.CheckpointRequest) { r.ActorName = "UPPER" }), + want: field.ErrorList{field.Invalid(field.NewPath("actor_name"), nil, "").WithOrigin("format=k8s-short-name")}, + }, { + name: "invalid actor_uid: not a UUID", + obj: valid(func(r *ateletpb.CheckpointRequest) { r.ActorUid = "../escape" }), + want: field.ErrorList{field.Invalid(field.NewPath("actor_uid"), nil, "").WithOrigin("format=k8s-uuid")}, + }, { + name: "invalid actor_template_name: trailing dash", + obj: valid(func(r *ateletpb.CheckpointRequest) { r.ActorTemplateName = "tmpl-" }), + want: field.ErrorList{field.Invalid(field.NewPath("actor_template_name"), nil, "").WithOrigin("format=k8s-short-name")}, + }, { + name: "unset template identity is allowed", + obj: valid(func(r *ateletpb.CheckpointRequest) { + r.ActorTemplateAtespace = "" + r.ActorTemplateName = "" + }), + }, { + name: "missing spec", + obj: valid(func(r *ateletpb.CheckpointRequest) { r.Spec = nil }), + want: field.ErrorList{field.Required(field.NewPath("spec"), "")}, + }, { + name: "spec errors surface at the spec's path", + obj: valid(func(r *ateletpb.CheckpointRequest) { + r.Spec = &ateletpb.WorkloadSpec{Containers: []*ateletpb.Container{{Name: "_pause", Image: testDigestImage}}} + }), + want: field.ErrorList{field.Invalid(field.NewPath("spec", "containers").Index(0).Child("name"), nil, "").WithOrigin("format=k8s-short-name")}, + }, { + name: "unspecified type", + obj: valid(func(r *ateletpb.CheckpointRequest) { + r.Type = ateletpb.CheckpointType_CHECKPOINT_TYPE_UNSPECIFIED + }), + want: field.ErrorList{field.Required(field.NewPath("type"), "")}, + }, { + name: "unknown type", + obj: valid(func(r *ateletpb.CheckpointRequest) { r.Type = ateletpb.CheckpointType(3) }), + want: field.ErrorList{field.Invalid(field.NewPath("type"), nil, "").WithOrigin("maximum")}, + }, { + name: "no config", + obj: valid(func(r *ateletpb.CheckpointRequest) { r.ExternalConfig = nil }), + want: field.ErrorList{ + field.Invalid(nil, nil, "").WithOrigin("union"), + field.Required(field.NewPath("external_config"), ""), + }, + }, { + name: "both configs", + obj: valid(func(r *ateletpb.CheckpointRequest) { + r.LocalConfig = &ateletpb.LocalCheckpointConfiguration{SnapshotName: "pause-snap-1"} + }), + want: field.ErrorList{field.Invalid(nil, nil, "").WithOrigin("union")}, + }, { + name: "local type with external config", + obj: valid(func(r *ateletpb.CheckpointRequest) { + r.Type = ateletpb.CheckpointType_CHECKPOINT_TYPE_LOCAL + }), + want: field.ErrorList{field.Required(field.NewPath("local_config"), "")}, + }, { + name: "external type with local config", + obj: valid(local("pause-snap-1"), func(r *ateletpb.CheckpointRequest) { + r.Type = ateletpb.CheckpointType_CHECKPOINT_TYPE_EXTERNAL + }), + want: field.ErrorList{field.Required(field.NewPath("external_config"), "")}, + }, { + name: "missing local snapshot_name", + obj: valid(local("")), + want: field.ErrorList{field.Required(field.NewPath("local_config", "snapshot_name"), "")}, + }, { + name: "invalid local snapshot_name: path escape", + obj: valid(local("../escape")), + want: field.ErrorList{field.Invalid(field.NewPath("local_config", "snapshot_name"), nil, "").WithOrigin("format=k8s-short-name")}, + }, { + name: "missing external snapshot_uri", + obj: valid(func(r *ateletpb.CheckpointRequest) { r.ExternalConfig.SnapshotUri = "" }), + want: field.ErrorList{field.Required(field.NewPath("external_config", "snapshot_uri"), "")}, + }, { + name: "invalid external snapshot_uri: no bucket", + obj: valid(func(r *ateletpb.CheckpointRequest) { r.ExternalConfig.SnapshotUri = "relative/path" }), + want: field.ErrorList{field.Invalid(field.NewPath("external_config", "snapshot_uri"), nil, "")}, + }, { + name: "external snapshot_uri too long", + obj: valid(func(r *ateletpb.CheckpointRequest) { + r.ExternalConfig.SnapshotUri = "gs://bucket/" + strings.Repeat("p", 2048) + }), + want: field.ErrorList{ + field.TooLong(field.NewPath("external_config", "snapshot_uri"), nil, 2048).WithOrigin("maxLength"), + field.Invalid(field.NewPath("external_config", "snapshot_uri"), nil, ""), + }, + }, { + name: "unspecified fidelity", + obj: valid(func(r *ateletpb.CheckpointRequest) { + r.Fidelity = ateletpb.SnapshotFidelity_SNAPSHOT_FIDELITY_UNSPECIFIED + }), + want: field.ErrorList{field.Required(field.NewPath("fidelity"), "")}, + }, { + name: "unknown fidelity", + obj: valid(func(r *ateletpb.CheckpointRequest) { r.Fidelity = 4 }), + want: field.ErrorList{field.Invalid(field.NewPath("fidelity"), nil, "").WithOrigin("maximum")}, + }, { + name: "rootfs fidelity is forwarded for the runtime to judge", + obj: valid(func(r *ateletpb.CheckpointRequest) { r.Fidelity = ateletpb.SnapshotFidelity_SNAPSHOT_FIDELITY_ROOTFS }), + }} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assertValidateErr(t, ValidateCheckpointRequest(context.Background(), tt.obj), tt.want) + }) + } +} + +func TestValidateRestoreRequest(t *testing.T) { + const snapshotURI = "gs://bucket/root/atespaces/team-a/actors/01234567-89ab-cdef-0123-456789abcdef/snapshots/snap-1" + valid := func(mutate ...func(*ateletpb.RestoreRequest)) *ateletpb.RestoreRequest { + r := &ateletpb.RestoreRequest{ + WorkerPodUid: "0f9a3b1c-2d4e-5f60-7182-93a4b5c6d7e8", + Atespace: "team-a", + ActorName: "actor-1", + ActorUid: "01234567-89ab-cdef-0123-456789abcdef", + ActorTemplateAtespace: "team-a", + ActorTemplateName: "tmpl-1", + Spec: &ateletpb.WorkloadSpec{}, + Type: ateletpb.CheckpointType_CHECKPOINT_TYPE_EXTERNAL, + ExternalConfig: &ateletpb.ExternalRestoreConfiguration{SnapshotUri: snapshotURI}, + Fidelity: ateletpb.SnapshotFidelity_SNAPSHOT_FIDELITY_MEMORY, + SandboxAssets: validSandboxAssets(), + } + for _, m := range mutate { + m(r) + } + return r + } + local := func(name string) func(*ateletpb.RestoreRequest) { + return func(r *ateletpb.RestoreRequest) { + r.Type = ateletpb.CheckpointType_CHECKPOINT_TYPE_LOCAL + r.ExternalConfig = nil + r.LocalConfig = &ateletpb.LocalCheckpointConfiguration{SnapshotName: name} + } + } + + tests := []struct { + name string + obj *ateletpb.RestoreRequest + want field.ErrorList + }{{ + name: "valid external", + obj: valid(), + }, { + name: "valid local", + obj: valid(local("pause-snap-1")), + }, { + name: "valid volumes fidelity", + obj: valid(func(r *ateletpb.RestoreRequest) { r.Fidelity = ateletpb.SnapshotFidelity_SNAPSHOT_FIDELITY_VOLUMES }), + }, { + name: "valid size and egress", + obj: valid(func(r *ateletpb.RestoreRequest) { + r.CpuMilli = 999999 + r.MemoryBytes = 1 << 30 + r.EgressGateway = &ateletpb.EgressGateway{Address: "10.0.0.1:15001"} + }), + }, { + name: "missing worker_pod_uid", + obj: valid(func(r *ateletpb.RestoreRequest) { r.WorkerPodUid = "" }), + want: field.ErrorList{field.Required(field.NewPath("worker_pod_uid"), "")}, + }, { + name: "invalid worker_pod_uid: path escape", + obj: valid(func(r *ateletpb.RestoreRequest) { r.WorkerPodUid = "../escape" }), + want: field.ErrorList{field.Invalid(field.NewPath("worker_pod_uid"), nil, "").WithOrigin("format=k8s-uuid")}, + }, { + name: "invalid worker_pod_uid: not a pod UID", + obj: valid(func(r *ateletpb.RestoreRequest) { r.WorkerPodUid = "ateom-1" }), + want: field.ErrorList{field.Invalid(field.NewPath("worker_pod_uid"), nil, "").WithOrigin("format=k8s-uuid")}, + }, { + name: "invalid atespace: path escape", + obj: valid(func(r *ateletpb.RestoreRequest) { r.Atespace = "../escape" }), + want: field.ErrorList{field.Invalid(field.NewPath("atespace"), nil, "").WithOrigin("format=k8s-short-name")}, + }, { + name: "invalid actor_name: uppercase", + obj: valid(func(r *ateletpb.RestoreRequest) { r.ActorName = "UPPER" }), + want: field.ErrorList{field.Invalid(field.NewPath("actor_name"), nil, "").WithOrigin("format=k8s-short-name")}, + }, { + name: "invalid actor_uid: not a UUID", + obj: valid(func(r *ateletpb.RestoreRequest) { r.ActorUid = "../escape" }), + want: field.ErrorList{field.Invalid(field.NewPath("actor_uid"), nil, "").WithOrigin("format=k8s-uuid")}, + }, { + name: "invalid actor_template_atespace: slash", + obj: valid(func(r *ateletpb.RestoreRequest) { r.ActorTemplateAtespace = "no/slashes" }), + want: field.ErrorList{field.Invalid(field.NewPath("actor_template_atespace"), nil, "").WithOrigin("format=k8s-short-name")}, + }, { + name: "unset template identity is allowed", + obj: valid(func(r *ateletpb.RestoreRequest) { + r.ActorTemplateAtespace = "" + r.ActorTemplateName = "" + }), + }, { + name: "missing spec", + obj: valid(func(r *ateletpb.RestoreRequest) { r.Spec = nil }), + want: field.ErrorList{field.Required(field.NewPath("spec"), "")}, + }, { + name: "spec errors surface at the spec's path", + obj: valid(func(r *ateletpb.RestoreRequest) { + r.Spec = &ateletpb.WorkloadSpec{Containers: []*ateletpb.Container{{Name: "_pause", Image: testDigestImage}}} + }), + want: field.ErrorList{field.Invalid(field.NewPath("spec", "containers").Index(0).Child("name"), nil, "").WithOrigin("format=k8s-short-name")}, + }, { + name: "unspecified type", + obj: valid(func(r *ateletpb.RestoreRequest) { r.Type = ateletpb.CheckpointType_CHECKPOINT_TYPE_UNSPECIFIED }), + want: field.ErrorList{field.Required(field.NewPath("type"), "")}, + }, { + name: "unknown type", + obj: valid(func(r *ateletpb.RestoreRequest) { r.Type = ateletpb.CheckpointType(3) }), + want: field.ErrorList{field.Invalid(field.NewPath("type"), nil, "").WithOrigin("maximum")}, + }, { + name: "no config", + obj: valid(func(r *ateletpb.RestoreRequest) { r.ExternalConfig = nil }), + want: field.ErrorList{ + field.Invalid(nil, nil, "").WithOrigin("union"), + field.Required(field.NewPath("external_config"), ""), + }, + }, { + name: "both configs", + obj: valid(func(r *ateletpb.RestoreRequest) { + r.LocalConfig = &ateletpb.LocalCheckpointConfiguration{SnapshotName: "pause-snap-1"} + }), + want: field.ErrorList{field.Invalid(nil, nil, "").WithOrigin("union")}, + }, { + name: "local type with external config", + obj: valid(func(r *ateletpb.RestoreRequest) { r.Type = ateletpb.CheckpointType_CHECKPOINT_TYPE_LOCAL }), + want: field.ErrorList{field.Required(field.NewPath("local_config"), "")}, + }, { + name: "external type with local config", + obj: valid(local("pause-snap-1"), func(r *ateletpb.RestoreRequest) { + r.Type = ateletpb.CheckpointType_CHECKPOINT_TYPE_EXTERNAL + }), + want: field.ErrorList{field.Required(field.NewPath("external_config"), "")}, + }, { + name: "invalid local snapshot_name: path escape", + obj: valid(local("../escape")), + want: field.ErrorList{field.Invalid(field.NewPath("local_config", "snapshot_name"), nil, "").WithOrigin("format=k8s-short-name")}, + }, { + name: "missing external snapshot_uri", + obj: valid(func(r *ateletpb.RestoreRequest) { r.ExternalConfig.SnapshotUri = "" }), + want: field.ErrorList{field.Required(field.NewPath("external_config", "snapshot_uri"), "")}, + }, { + name: "invalid external snapshot_uri: no bucket", + obj: valid(func(r *ateletpb.RestoreRequest) { r.ExternalConfig.SnapshotUri = "relative/path" }), + want: field.ErrorList{field.Invalid(field.NewPath("external_config", "snapshot_uri"), nil, "")}, + }, { + name: "unspecified fidelity", + obj: valid(func(r *ateletpb.RestoreRequest) { r.Fidelity = ateletpb.SnapshotFidelity_SNAPSHOT_FIDELITY_UNSPECIFIED }), + want: field.ErrorList{field.Required(field.NewPath("fidelity"), "")}, + }, { + name: "unknown fidelity", + obj: valid(func(r *ateletpb.RestoreRequest) { r.Fidelity = ateletpb.SnapshotFidelity(4) }), + want: field.ErrorList{field.Invalid(field.NewPath("fidelity"), nil, "").WithOrigin("maximum")}, + }, { + name: "rootfs fidelity is forwarded for the runtime to judge", + obj: valid(func(r *ateletpb.RestoreRequest) { r.Fidelity = ateletpb.SnapshotFidelity_SNAPSHOT_FIDELITY_ROOTFS }), + }, { + name: "negative cpu_milli", + obj: valid(func(r *ateletpb.RestoreRequest) { r.CpuMilli = -1 }), + want: field.ErrorList{field.Invalid(field.NewPath("cpu_milli"), nil, "").WithOrigin("minimum")}, + }, { + name: "cpu_milli too large", + obj: valid(func(r *ateletpb.RestoreRequest) { r.CpuMilli = 1000000 }), + want: field.ErrorList{field.Invalid(field.NewPath("cpu_milli"), nil, "").WithOrigin("maximum")}, + }, { + name: "negative memory_bytes", + obj: valid(func(r *ateletpb.RestoreRequest) { r.MemoryBytes = -1 }), + want: field.ErrorList{field.Invalid(field.NewPath("memory_bytes"), nil, "").WithOrigin("minimum")}, + }, { + name: "empty egress gateway address", + obj: valid(func(r *ateletpb.RestoreRequest) { r.EgressGateway = &ateletpb.EgressGateway{} }), + want: field.ErrorList{field.Required(field.NewPath("egress_gateway", "address"), "")}, + }, { + name: "missing sandbox_assets", + obj: valid(func(r *ateletpb.RestoreRequest) { r.SandboxAssets = nil }), + want: field.ErrorList{field.Required(field.NewPath("sandbox_assets"), "")}, + }, { + name: "sandbox_assets errors surface at their path", + obj: valid(func(r *ateletpb.RestoreRequest) { + r.SandboxAssets = validSandboxAssets(func(a *ateletpb.SandboxAssets) { a.SandboxClass = "kata" }) + }), + want: field.ErrorList{field.NotSupported[string](field.NewPath("sandbox_assets", "sandbox_class"), nil, nil)}, + }} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assertValidateErr(t, ValidateRestoreRequest(context.Background(), tt.obj), tt.want) + }) + } +} + +func TestValidateUploadPausedCheckpointRequest(t *testing.T) { + valid := func(mutate ...func(*ateletpb.UploadPausedCheckpointRequest)) *ateletpb.UploadPausedCheckpointRequest { + r := &ateletpb.UploadPausedCheckpointRequest{ + Atespace: "team-a", + ActorName: "actor-1", + ActorUid: "01234567-89ab-cdef-0123-456789abcdef", + ActorTemplateAtespace: "team-a", + ActorTemplateName: "tmpl-1", + LocalSnapshotName: "pause-snap-1", + DestinationSnapshotUri: "gs://bucket/root/atespaces/team-a/actors/01234567-89ab-cdef-0123-456789abcdef/snapshots/snap-1", + DesiredFidelity: ateletpb.SnapshotFidelity_SNAPSHOT_FIDELITY_MEMORY, + } + for _, m := range mutate { + m(r) + } + return r + } + + tests := []struct { + name string + obj *ateletpb.UploadPausedCheckpointRequest + want field.ErrorList + }{{ + name: "valid", + obj: valid(), + }, { + name: "valid volumes fidelity", + obj: valid(func(r *ateletpb.UploadPausedCheckpointRequest) { + r.DesiredFidelity = ateletpb.SnapshotFidelity_SNAPSHOT_FIDELITY_VOLUMES + }), + }, { + name: "missing atespace", + obj: valid(func(r *ateletpb.UploadPausedCheckpointRequest) { r.Atespace = "" }), + want: field.ErrorList{field.Required(field.NewPath("atespace"), "")}, + }, { + name: "invalid atespace: path escape", + obj: valid(func(r *ateletpb.UploadPausedCheckpointRequest) { r.Atespace = "../escape" }), + want: field.ErrorList{field.Invalid(field.NewPath("atespace"), nil, "").WithOrigin("format=k8s-short-name")}, + }, { + name: "golden atespace", + obj: valid(func(r *ateletpb.UploadPausedCheckpointRequest) { r.Atespace = resources.GoldenActorAtespace }), + want: field.ErrorList{field.Forbidden(field.NewPath("atespace"), "")}, + }, { + name: "invalid actor_name: uppercase", + obj: valid(func(r *ateletpb.UploadPausedCheckpointRequest) { r.ActorName = "UPPER" }), + want: field.ErrorList{field.Invalid(field.NewPath("actor_name"), nil, "").WithOrigin("format=k8s-short-name")}, + }, { + name: "missing actor_uid", + obj: valid(func(r *ateletpb.UploadPausedCheckpointRequest) { r.ActorUid = "" }), + want: field.ErrorList{field.Required(field.NewPath("actor_uid"), "")}, + }, { + name: "invalid actor_uid: not a UUID", + obj: valid(func(r *ateletpb.UploadPausedCheckpointRequest) { r.ActorUid = "actor-uid-1" }), + want: field.ErrorList{field.Invalid(field.NewPath("actor_uid"), nil, "").WithOrigin("format=k8s-uuid")}, + }, { + name: "invalid actor_template_atespace: slash", + obj: valid(func(r *ateletpb.UploadPausedCheckpointRequest) { r.ActorTemplateAtespace = "no/slashes" }), + want: field.ErrorList{field.Invalid(field.NewPath("actor_template_atespace"), nil, "").WithOrigin("format=k8s-short-name")}, + }, { + name: "unset template identity is allowed", + obj: valid(func(r *ateletpb.UploadPausedCheckpointRequest) { + r.ActorTemplateAtespace = "" + r.ActorTemplateName = "" + }), + }, { + name: "missing local_snapshot_name", + obj: valid(func(r *ateletpb.UploadPausedCheckpointRequest) { r.LocalSnapshotName = "" }), + want: field.ErrorList{field.Required(field.NewPath("local_snapshot_name"), "")}, + }, { + name: "invalid local_snapshot_name: path escape", + obj: valid(func(r *ateletpb.UploadPausedCheckpointRequest) { r.LocalSnapshotName = "../escape" }), + want: field.ErrorList{field.Invalid(field.NewPath("local_snapshot_name"), nil, "").WithOrigin("format=k8s-short-name")}, + }, { + name: "missing destination_snapshot_uri", + obj: valid(func(r *ateletpb.UploadPausedCheckpointRequest) { r.DestinationSnapshotUri = "" }), + want: field.ErrorList{field.Required(field.NewPath("destination_snapshot_uri"), "")}, + }, { + name: "invalid destination_snapshot_uri", + obj: valid(func(r *ateletpb.UploadPausedCheckpointRequest) { r.DestinationSnapshotUri = "not-a-uri" }), + want: field.ErrorList{field.Invalid(field.NewPath("destination_snapshot_uri"), nil, "")}, + }, { + name: "destination_snapshot_uri too long", + obj: valid(func(r *ateletpb.UploadPausedCheckpointRequest) { + r.DestinationSnapshotUri = "gs://bucket/" + strings.Repeat("p", 2048) + }), + want: field.ErrorList{ + field.TooLong(field.NewPath("destination_snapshot_uri"), nil, 2048).WithOrigin("maxLength"), + field.Invalid(field.NewPath("destination_snapshot_uri"), nil, ""), + }, + }, { + name: "unspecified desired_fidelity", + obj: valid(func(r *ateletpb.UploadPausedCheckpointRequest) { + r.DesiredFidelity = ateletpb.SnapshotFidelity_SNAPSHOT_FIDELITY_UNSPECIFIED + }), + want: field.ErrorList{field.Required(field.NewPath("desired_fidelity"), "")}, + }, { + name: "unknown desired_fidelity", + obj: valid(func(r *ateletpb.UploadPausedCheckpointRequest) { + r.DesiredFidelity = 4 + }), + want: field.ErrorList{field.Invalid(field.NewPath("desired_fidelity"), nil, "").WithOrigin("maximum")}, + }, { + name: "rootfs desired_fidelity is forwarded for the runtime to judge", + obj: valid(func(r *ateletpb.UploadPausedCheckpointRequest) { + r.DesiredFidelity = ateletpb.SnapshotFidelity_SNAPSHOT_FIDELITY_ROOTFS + }), + }} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assertValidateErr(t, ValidateUploadPausedCheckpointRequest(context.Background(), tt.obj), tt.want) + }) + } +} + +func TestValidateTerminateRequest(t *testing.T) { + valid := func(mutate ...func(*ateletpb.TerminateRequest)) *ateletpb.TerminateRequest { + r := &ateletpb.TerminateRequest{ + WorkerPodUid: "0f9a3b1c-2d4e-5f60-7182-93a4b5c6d7e8", + Atespace: "team-a", + ActorName: "actor-1", + ActorUid: "01234567-89ab-cdef-0123-456789abcdef", + ActorTemplateAtespace: "team-a", + ActorTemplateName: "tmpl-1", + Spec: &ateletpb.WorkloadSpec{}, + } + for _, m := range mutate { + m(r) + } + return r + } + + tests := []struct { + name string + obj *ateletpb.TerminateRequest + want field.ErrorList + }{{ + name: "valid", + obj: valid(), + }, { + name: "unset worker_pod_uid is allowed: node-only cleanup", + obj: valid(func(r *ateletpb.TerminateRequest) { r.WorkerPodUid = "" }), + }, { + name: "invalid worker_pod_uid: path escape", + obj: valid(func(r *ateletpb.TerminateRequest) { r.WorkerPodUid = "../escape" }), + want: field.ErrorList{field.Invalid(field.NewPath("worker_pod_uid"), nil, "").WithOrigin("format=k8s-uuid")}, + }, { + name: "invalid worker_pod_uid: not a pod UID", + obj: valid(func(r *ateletpb.TerminateRequest) { r.WorkerPodUid = "ateom-1" }), + want: field.ErrorList{field.Invalid(field.NewPath("worker_pod_uid"), nil, "").WithOrigin("format=k8s-uuid")}, + }, { + name: "missing atespace", + obj: valid(func(r *ateletpb.TerminateRequest) { r.Atespace = "" }), + want: field.ErrorList{field.Required(field.NewPath("atespace"), "")}, + }, { + name: "invalid atespace: uppercase", + obj: valid(func(r *ateletpb.TerminateRequest) { r.Atespace = "Team-A" }), + want: field.ErrorList{field.Invalid(field.NewPath("atespace"), nil, "").WithOrigin("format=k8s-short-name")}, + }, { + name: "missing actor_name", + obj: valid(func(r *ateletpb.TerminateRequest) { r.ActorName = "" }), + want: field.ErrorList{field.Required(field.NewPath("actor_name"), "")}, + }, { + name: "invalid actor_name: path escape", + obj: valid(func(r *ateletpb.TerminateRequest) { r.ActorName = "../escape" }), + want: field.ErrorList{field.Invalid(field.NewPath("actor_name"), nil, "").WithOrigin("format=k8s-short-name")}, + }, { + name: "missing actor_uid", + obj: valid(func(r *ateletpb.TerminateRequest) { r.ActorUid = "" }), + want: field.ErrorList{field.Required(field.NewPath("actor_uid"), "")}, + }, { + name: "invalid actor_uid: not a uuid", + obj: valid(func(r *ateletpb.TerminateRequest) { r.ActorUid = "not-a-uuid" }), + want: field.ErrorList{field.Invalid(field.NewPath("actor_uid"), nil, "").WithOrigin("format=k8s-uuid")}, + }, { + name: "invalid actor_template_atespace: uppercase", + obj: valid(func(r *ateletpb.TerminateRequest) { r.ActorTemplateAtespace = "Team-A" }), + want: field.ErrorList{field.Invalid(field.NewPath("actor_template_atespace"), nil, "").WithOrigin("format=k8s-short-name")}, + }, { + name: "invalid actor_template_name: trailing dash", + obj: valid(func(r *ateletpb.TerminateRequest) { r.ActorTemplateName = "tmpl-" }), + want: field.ErrorList{field.Invalid(field.NewPath("actor_template_name"), nil, "").WithOrigin("format=k8s-short-name")}, + }, { + name: "spec errors surface at the spec's path", + obj: valid(func(r *ateletpb.TerminateRequest) { + r.Spec = &ateletpb.WorkloadSpec{Containers: []*ateletpb.Container{{Name: "_pause", Image: testDigestImage}}} + }), + want: field.ErrorList{field.Invalid(field.NewPath("spec", "containers").Index(0).Child("name"), nil, "").WithOrigin("format=k8s-short-name")}, + }, { + name: "unset template identity is allowed", + obj: valid(func(r *ateletpb.TerminateRequest) { + r.ActorTemplateAtespace = "" + r.ActorTemplateName = "" + }), + }, { + name: "unset spec is allowed", + obj: valid(func(r *ateletpb.TerminateRequest) { r.Spec = nil }), + }} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assertValidateErr(t, ValidateTerminateRequest(context.Background(), tt.obj), tt.want) + }) + } +} + func TestValidateRegisterWorkerRequest(t *testing.T) { testRuntime := func() *ateletpb.SandboxRuntime { return &ateletpb.SandboxRuntime{ @@ -420,7 +1252,7 @@ func TestValidateRegisterWorkerRequest(t *testing.T) { }, }, { name: "quantity too long", - obj: withLimits(&ateletpb.Limits{Name: "memory", Quantity: strings.Repeat("1", 33)}), + obj: withLimits(&ateletpb.Limits{Name: "memory", Quantity: "1." + strings.Repeat("0", 31)}), want: field.ErrorList{field.TooLong(limitsPath.Index(0).Child("quantity"), nil, 32).WithOrigin("maxLength")}, }, { name: "duplicate resource name", @@ -434,21 +1266,6 @@ func TestValidateRegisterWorkerRequest(t *testing.T) { name: "malformed quantity", obj: withLimits(&ateletpb.Limits{Name: "cpu", Quantity: "not-a-quantity"}), want: field.ErrorList{field.Invalid(limitsPath.Index(0).Child("quantity"), nil, "")}, - }, { - name: "negative quantity", - obj: withLimits(&ateletpb.Limits{Name: "memory", Quantity: "-1Gi"}), - want: field.ErrorList{field.Invalid(limitsPath.Index(0).Child("quantity"), nil, "")}, - }, { - name: "zero quantity", - obj: withLimits(&ateletpb.Limits{Name: "memory", Quantity: "0"}), - want: field.ErrorList{field.Invalid(limitsPath.Index(0).Child("quantity"), nil, "")}, - }, { - name: "cpu below the bound", - obj: withLimits(&ateletpb.Limits{Name: "cpu", Quantity: "999"}), - }, { - name: "cpu at the bound", - obj: withLimits(&ateletpb.Limits{Name: "cpu", Quantity: "1000"}), - want: field.ErrorList{field.Invalid(limitsPath.Index(0).Child("quantity"), nil, "")}, }, { name: "too many limits", obj: withLimits( @@ -468,3 +1285,577 @@ func TestValidateRegisterWorkerRequest(t *testing.T) { }) } } + +// TestValidateWorkloadSpec covers the rules WorkloadSpec owns: the volumes and +// containers lists. One nested case per list proves the element validators +// run; their own rules are covered by TestValidateVolume and +// TestValidateContainer. +func TestValidateWorkloadSpec(t *testing.T) { + ctr := func(name string) *ateletpb.Container { + return &ateletpb.Container{Name: name, Image: testDigestImage} + } + valid := func(mutate ...func(*ateletpb.WorkloadSpec)) *ateletpb.WorkloadSpec { + s := &ateletpb.WorkloadSpec{ + Volumes: []*ateletpb.Volume{{Name: "data", DurableDir: &ateletpb.DurableDirVolume{}}}, + Containers: []*ateletpb.Container{ctr("main"), ctr("sidecar")}, + } + for _, m := range mutate { + m(s) + } + return s + } + + tests := []struct { + name string + obj *ateletpb.WorkloadSpec + want field.ErrorList + }{{ + name: "valid", + obj: valid(), + }, { + name: "empty spec", + obj: &ateletpb.WorkloadSpec{}, + }, { + name: "no volumes: the delete flow may send containers only", + obj: valid(func(s *ateletpb.WorkloadSpec) { s.Volumes = nil }), + }, { + name: "duplicate volume names", + obj: valid(func(s *ateletpb.WorkloadSpec) { + s.Volumes = append(s.Volumes, &ateletpb.Volume{Name: "data", Image: &ateletpb.ImageVolumeSource{Reference: testDigestImage}}) + }), + want: field.ErrorList{field.Duplicate(field.NewPath("volumes").Index(1), nil)}, + }, { + name: "too many volumes", + obj: valid(func(s *ateletpb.WorkloadSpec) { + s.Volumes = nil + for i := range 33 { + s.Volumes = append(s.Volumes, &ateletpb.Volume{Name: fmt.Sprintf("v%d", i), DurableDir: &ateletpb.DurableDirVolume{}}) + } + }), + want: field.ErrorList{field.TooMany(field.NewPath("volumes"), 33, 32).WithOrigin("maxItems")}, + }, { + name: "volume errors surface at the volume's path", + obj: valid(func(s *ateletpb.WorkloadSpec) { s.Volumes[0].DurableDir = nil }), + want: field.ErrorList{field.Invalid(field.NewPath("volumes").Index(0), nil, "").WithOrigin("union")}, + }, { + name: "duplicate container names", + obj: valid(func(s *ateletpb.WorkloadSpec) { + s.Containers = []*ateletpb.Container{ctr("main"), ctr("main")} + }), + want: field.ErrorList{field.Duplicate(field.NewPath("containers").Index(1), nil)}, + }, { + name: "too many containers", + obj: valid(func(s *ateletpb.WorkloadSpec) { + s.Containers = nil + for _, n := range []string{"a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k"} { + s.Containers = append(s.Containers, ctr(n)) + } + }), + want: field.ErrorList{field.TooMany(field.NewPath("containers"), 11, 10).WithOrigin("maxItems")}, + }, { + name: "container errors surface at the container's path", + obj: valid(func(s *ateletpb.WorkloadSpec) { + s.Containers[0].Resources = &ateletpb.ResourceLimits{CpuMillis: -1} + }), + want: field.ErrorList{field.Invalid(field.NewPath("containers").Index(0).Child("resources", "cpu_millis"), nil, "").WithOrigin("minimum")}, + }, { + name: "mount references a declared volume", + obj: valid(func(s *ateletpb.WorkloadSpec) { + s.Containers[0].VolumeMounts = []*ateletpb.VolumeMount{{Name: "data", MountPath: "/data"}} + }), + }, { + name: "mount references an undeclared volume", + obj: valid(func(s *ateletpb.WorkloadSpec) { + s.Containers[1].VolumeMounts = []*ateletpb.VolumeMount{{Name: "data", MountPath: "/data"}, {Name: "ghost", MountPath: "/ghost"}} + }), + want: field.ErrorList{field.Invalid(field.NewPath("containers").Index(1).Child("volume_mounts").Index(1).Child("name"), nil, "")}, + }} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assertValidateErr(t, Validate_WorkloadSpec(context.Background(), createOp, nil, tt.obj, nil), tt.want) + }) + } +} + +// TestValidateVolume covers Volume and every source it can hold, with errors +// asserted at their paths under the volume. +func TestValidateVolume(t *testing.T) { + durable := func(mutate ...func(*ateletpb.Volume)) *ateletpb.Volume { + v := &ateletpb.Volume{Name: "data", DurableDir: &ateletpb.DurableDirVolume{}} + for _, m := range mutate { + m(v) + } + return v + } + external := func(mutate ...func(*ateletpb.ExternalVolumeSource)) *ateletpb.Volume { + e := &ateletpb.ExternalVolumeSource{ + StorageVolumeId: "projects/p/zones/z/disks/vol-1", + VolumeType: "substrate.io/mock", + VolumeContext: map[string]string{"fsType": "ext4"}, + } + for _, m := range mutate { + m(e) + } + return &ateletpb.Volume{Name: "data", External: e} + } + image := func(ref string) *ateletpb.Volume { + return &ateletpb.Volume{Name: "data", Image: &ateletpb.ImageVolumeSource{Reference: ref}} + } + systemInfo := func(ds ...*ateletpb.SystemInfoDataSource) *ateletpb.Volume { + return &ateletpb.Volume{Name: "data", SystemInfo: &ateletpb.SystemInfoVolume{DataSources: ds}} + } + bundle := func(name, path string) *ateletpb.SystemInfoDataSource { + return &ateletpb.SystemInfoDataSource{TrustBundle: &ateletpb.TrustBundleDataSource{Names: []string{name}, Path: path}} + } + item := func(f ateletpb.ActorMetadataField, path string) *ateletpb.ActorMetadataItem { + return &ateletpb.ActorMetadataItem{Field: f, Path: path} + } + metadata := func(items ...*ateletpb.ActorMetadataItem) *ateletpb.SystemInfoDataSource { + return &ateletpb.SystemInfoDataSource{ActorMetadata: &ateletpb.ActorMetadataDataSource{Items: items}} + } + fieldName := ateletpb.ActorMetadataField_ACTOR_METADATA_FIELD_NAME + + extPath := field.NewPath("external") + dsPath := field.NewPath("system_info", "data_sources") + bundlePath := dsPath.Index(0).Child("trust_bundle") + itemsPath := dsPath.Index(0).Child("actor_metadata", "items") + + tests := []struct { + name string + obj *ateletpb.Volume + want field.ErrorList + }{ + // Volume. + { + name: "valid durable dir", + obj: durable(), + }, { + name: "missing name", + obj: durable(func(v *ateletpb.Volume) { v.Name = "" }), + want: field.ErrorList{field.Required(field.NewPath("name"), "")}, + }, { + name: "invalid name: uppercase", + obj: durable(func(v *ateletpb.Volume) { v.Name = "Data" }), + want: field.ErrorList{field.Invalid(field.NewPath("name"), nil, "").WithOrigin("format=k8s-short-name")}, + }, { + name: "no source set", + obj: durable(func(v *ateletpb.Volume) { v.DurableDir = nil }), + want: field.ErrorList{field.Invalid(nil, nil, "").WithOrigin("union")}, + }, { + name: "two sources set", + obj: durable(func(v *ateletpb.Volume) { + v.External = &ateletpb.ExternalVolumeSource{StorageVolumeId: "vol-1"} + }), + want: field.ErrorList{field.Invalid(nil, nil, "").WithOrigin("union")}, + }, + + // ExternalVolumeSource. + { + name: "valid external", + obj: external(), + }, { + name: "external: missing storage_volume_id", + obj: external(func(e *ateletpb.ExternalVolumeSource) { e.StorageVolumeId = "" }), + want: field.ErrorList{field.Required(extPath.Child("storage_volume_id"), "")}, + }, { + name: "external: storage_volume_id with a control character", + obj: external(func(e *ateletpb.ExternalVolumeSource) { e.StorageVolumeId = "vol\x01" }), + want: field.ErrorList{field.Invalid(extPath.Child("storage_volume_id"), nil, "")}, + }, { + name: "external: storage_volume_id too long", + obj: external(func(e *ateletpb.ExternalVolumeSource) { e.StorageVolumeId = strings.Repeat("x", 257) }), + want: field.ErrorList{field.TooLong(extPath.Child("storage_volume_id"), nil, 256).WithOrigin("maxLength")}, + }, { + name: "external: unset volume_type is allowed", + obj: external(func(e *ateletpb.ExternalVolumeSource) { e.VolumeType = "" }), + }, { + name: "external: invalid volume_type: uppercase", + obj: external(func(e *ateletpb.ExternalVolumeSource) { e.VolumeType = "substrate.io/Mock" }), + want: field.ErrorList{field.Invalid(extPath.Child("volume_type"), nil, "")}, + }, { + name: "external: volume_context with one long value, as a CSI driver may return", + obj: external(func(e *ateletpb.ExternalVolumeSource) { + e.VolumeContext = map[string]string{"k": strings.Repeat("v", 4000)} + }), + }, { + name: "external: volume_context over the size bound", + obj: external(func(e *ateletpb.ExternalVolumeSource) { + e.VolumeContext = map[string]string{"k": strings.Repeat("v", 40*1024)} + }), + want: field.ErrorList{field.TooLong(extPath.Child("volume_context"), nil, 40*1024)}, + }, { + name: "external: publish_context with many entries, as a CSI driver may return", + obj: external(func(e *ateletpb.ExternalVolumeSource) { + e.PublishContext = make(map[string]string) + for i := range 100 { + e.PublishContext[fmt.Sprintf("key-%03d", i)] = "v" + } + }), + }, { + name: "external: publish_context over the size bound", + obj: external(func(e *ateletpb.ExternalVolumeSource) { + e.PublishContext = map[string]string{"a": strings.Repeat("v", 20*1024), "b": strings.Repeat("v", 20*1024)} + }), + want: field.ErrorList{field.TooLong(extPath.Child("publish_context"), nil, 40*1024)}, + }, + + // ImageVolumeSource. + { + name: "valid image", + obj: image(testDigestImage), + }, { + name: "image: missing reference", + obj: image(""), + want: field.ErrorList{field.Required(field.NewPath("image", "reference"), "")}, + }, { + name: "image: reference not pinned by digest", + obj: image("example.com/app:v1"), + want: field.ErrorList{field.Invalid(field.NewPath("image", "reference"), nil, "")}, + }, + + // SystemInfoVolume and SystemInfoDataSource. + { + name: "valid system info", + obj: systemInfo(bundle("podcert", "a.pem"), bundle("podcert", "b.pem"), metadata(item(fieldName, "name"))), + }, { + name: "system info: data source with neither set", + obj: systemInfo(&ateletpb.SystemInfoDataSource{}), + want: field.ErrorList{field.Invalid(dsPath.Index(0), nil, "").WithOrigin("union")}, + }, { + name: "system info: data source with both set", + obj: systemInfo(&ateletpb.SystemInfoDataSource{ + ActorMetadata: &ateletpb.ActorMetadataDataSource{Items: []*ateletpb.ActorMetadataItem{item(fieldName, "name")}}, + TrustBundle: &ateletpb.TrustBundleDataSource{Names: []string{"podcert"}, Path: "p"}, + }), + want: field.ErrorList{field.Invalid(dsPath.Index(0), nil, "").WithOrigin("union")}, + }, { + name: "system info: duplicate path across trust bundles", + obj: systemInfo(bundle("podcert", "a.pem"), bundle("podcert", "a.pem")), + want: field.ErrorList{field.Duplicate(dsPath.Index(1).Child("trust_bundle", "path"), nil)}, + }, { + name: "system info: duplicate path between bundle and metadata item", + obj: systemInfo(bundle("podcert", "name"), metadata(item(fieldName, "name"))), + want: field.ErrorList{field.Duplicate(dsPath.Index(1).Child("actor_metadata", "items").Index(0).Child("path"), nil)}, + }, { + name: "system info: second actor_metadata entry", + obj: systemInfo(metadata(item(fieldName, "name")), metadata(item(fieldName, "name-2"))), + want: field.ErrorList{field.Forbidden(dsPath.Index(1).Child("actor_metadata"), "")}, + }, { + name: "system info: too many data sources", + obj: systemInfo( + bundle("podcert", "a"), bundle("podcert", "b"), bundle("podcert", "c"), + bundle("podcert", "d"), bundle("podcert", "e"), bundle("podcert", "f"), + bundle("podcert", "g"), bundle("podcert", "h"), bundle("podcert", "i"), + ), + want: field.ErrorList{field.TooMany(dsPath, 9, 8).WithOrigin("maxItems")}, + }, + + // TrustBundleDataSource. + { + name: "trust bundle: missing path", + obj: systemInfo(bundle("podcert", "")), + want: field.ErrorList{field.Required(bundlePath.Child("path"), "")}, + }, { + name: "trust bundle: absolute path", + obj: systemInfo(bundle("podcert", "/trust/bundle.pem")), + want: field.ErrorList{field.Invalid(bundlePath.Child("path"), nil, "")}, + }, { + name: "trust bundle: path too long", + obj: systemInfo(bundle("podcert", strings.Repeat("p", 256))), + want: field.ErrorList{field.TooLong(bundlePath.Child("path"), nil, 255).WithOrigin("maxLength")}, + }, { + name: "trust bundle: no names", + obj: systemInfo(&ateletpb.SystemInfoDataSource{TrustBundle: &ateletpb.TrustBundleDataSource{Path: "trust/bundle.pem"}}), + want: field.ErrorList{field.Required(bundlePath.Child("names"), "")}, + }, { + name: "trust bundle: several names", + obj: systemInfo(&ateletpb.SystemInfoDataSource{TrustBundle: &ateletpb.TrustBundleDataSource{Names: []string{"egress-mitm.ate.dev", "system-roots.ate.dev"}, Path: "trust/bundle.pem"}}), + }, { + name: "trust bundle: too many names", + obj: systemInfo(&ateletpb.SystemInfoDataSource{TrustBundle: &ateletpb.TrustBundleDataSource{Names: []string{"a", "b", "c", "d", "e", "f", "g", "h", "i"}, Path: "trust/bundle.pem"}}), + want: field.ErrorList{field.TooMany(bundlePath.Child("names"), 9, 8).WithOrigin("maxItems")}, + }, { + name: "trust bundle: duplicate names", + obj: systemInfo(&ateletpb.SystemInfoDataSource{TrustBundle: &ateletpb.TrustBundleDataSource{Names: []string{"a", "a"}, Path: "trust/bundle.pem"}}), + want: field.ErrorList{field.Duplicate(bundlePath.Child("names").Index(1), nil)}, + }, { + name: "trust bundle: empty name", + obj: systemInfo(bundle("", "trust/bundle.pem")), + want: field.ErrorList{field.TooShort(bundlePath.Child("names").Index(0), "", 1).WithOrigin("minLength")}, + }, { + name: "trust bundle: name too long", + obj: systemInfo(bundle(strings.Repeat("n", 254), "trust/bundle.pem")), + want: field.ErrorList{field.TooLong(bundlePath.Child("names").Index(0), nil, 253).WithOrigin("maxLength")}, + }, + + // ActorMetadataDataSource. + { + name: "actor metadata: several fields", + obj: systemInfo(metadata( + item(fieldName, "name"), + item(ateletpb.ActorMetadataField_ACTOR_METADATA_FIELD_UID, "ids/uid"), + )), + }, { + name: "actor metadata: empty items", + obj: systemInfo(metadata()), + want: field.ErrorList{field.Required(itemsPath, "")}, + }, { + name: "actor metadata: same field projected twice", + obj: systemInfo(metadata(item(fieldName, "a"), item(fieldName, "b"))), + want: field.ErrorList{field.Duplicate(itemsPath.Index(1), nil)}, + }, { + name: "actor metadata: unspecified field", + obj: systemInfo(metadata(item(ateletpb.ActorMetadataField_ACTOR_METADATA_FIELD_UNSPECIFIED, "a"))), + want: field.ErrorList{field.Required(itemsPath.Index(0).Child("field"), "")}, + }, { + name: "actor metadata: field outside the enum", + obj: systemInfo(metadata(item(ateletpb.ActorMetadataField(4), "a"))), + want: field.ErrorList{field.Invalid(itemsPath.Index(0).Child("field"), nil, "").WithOrigin("maximum")}, + }, { + name: "actor metadata: escaping item path", + obj: systemInfo(metadata(item(fieldName, "../name"))), + want: field.ErrorList{field.Invalid(itemsPath.Index(0).Child("path"), nil, "")}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assertValidateErr(t, Validate_Volume(context.Background(), createOp, nil, tt.obj, nil), tt.want) + }) + } +} + +// TestValidateContainer covers Container and every message it holds, with +// errors asserted at their paths under the container. +func TestValidateContainer(t *testing.T) { + valid := func(mutate ...func(*ateletpb.Container)) *ateletpb.Container { + c := &ateletpb.Container{ + Name: "main", + Image: testDigestImage, + Command: []string{"/bin/app"}, + Args: []string{"--serve"}, + Env: []*ateletpb.EnvEntry{{Name: "PORT", Value: "8080"}}, + VolumeMounts: []*ateletpb.VolumeMount{ + {Name: "data", MountPath: "/data"}, + {Name: "data", MountPath: "/mnt/data"}, + }, + } + for _, m := range mutate { + m(c) + } + return c + } + env := func(mutate func(*ateletpb.EnvEntry)) *ateletpb.Container { + return valid(func(c *ateletpb.Container) { mutate(c.Env[0]) }) + } + // mount replaces the mounts with a single one, so the nesting check + // between mounts stays out of the way. + mount := func(name, path string) *ateletpb.Container { + return valid(func(c *ateletpb.Container) { + c.VolumeMounts = []*ateletpb.VolumeMount{{Name: name, MountPath: path}} + }) + } + probe := func(mutate ...func(*ateletpb.WakeupProbe)) *ateletpb.Container { + p := &ateletpb.WakeupProbe{ + HttpGet: &ateletpb.HttpGetAction{Path: "/healthz", Port: 8080}, + TimeoutSeconds: 30, + } + for _, m := range mutate { + m(p) + } + return valid(func(c *ateletpb.Container) { c.WakeupProbe = p }) + } + caps := func(add, drop []string) *ateletpb.Container { + return valid(func(c *ateletpb.Container) { + c.SecurityContext = &ateletpb.SecurityContext{Capabilities: &ateletpb.Capabilities{Add: add, Drop: drop}} + }) + } + limits := func(r *ateletpb.ResourceLimits) *ateletpb.Container { + return valid(func(c *ateletpb.Container) { c.Resources = r }) + } + + envPath := field.NewPath("env").Index(0) + mountPath := field.NewPath("volume_mounts").Index(0) + httpGetPath := field.NewPath("wakeup_probe", "http_get") + capsPath := field.NewPath("security_context", "capabilities") + + tests := []struct { + name string + obj *ateletpb.Container + want field.ErrorList + }{ + // Container name, image, command, and args. + { + name: "valid: the same volume mounted at two paths", + obj: valid(), + }, { + name: "missing name", + obj: valid(func(c *ateletpb.Container) { c.Name = "" }), + want: field.ErrorList{field.Required(field.NewPath("name"), "")}, + }, { + name: "invalid name: uppercase", + obj: valid(func(c *ateletpb.Container) { c.Name = "Main" }), + want: field.ErrorList{field.Invalid(field.NewPath("name"), nil, "").WithOrigin("format=k8s-short-name")}, + }, { + name: "missing image", + obj: valid(func(c *ateletpb.Container) { c.Image = "" }), + want: field.ErrorList{field.Required(field.NewPath("image"), "")}, + }, { + name: "image not pinned by digest", + obj: valid(func(c *ateletpb.Container) { c.Image = "example.com/app:v1" }), + want: field.ErrorList{field.Invalid(field.NewPath("image"), nil, "")}, + }, { + name: "empty command and args are allowed", + obj: valid(func(c *ateletpb.Container) { + c.Command = nil + c.Args = nil + }), + }, { + name: "too many command items", + obj: valid(func(c *ateletpb.Container) { c.Command = make([]string, 65) }), + want: field.ErrorList{field.TooMany(field.NewPath("command"), 65, 64).WithOrigin("maxItems")}, + }, { + name: "arg over the length guardrail", + obj: valid(func(c *ateletpb.Container) { c.Args = []string{strings.Repeat("a", 4097)} }), + want: field.ErrorList{field.TooLong(field.NewPath("args").Index(0), nil, 4096).WithOrigin("maxLength")}, + }, + + // EnvEntry. + { + name: "duplicate env names", + obj: valid(func(c *ateletpb.Container) { + c.Env = append(c.Env, &ateletpb.EnvEntry{Name: "PORT", Value: "9"}) + }), + want: field.ErrorList{field.Duplicate(field.NewPath("env").Index(1), nil)}, + }, { + name: "env: missing name", + obj: env(func(e *ateletpb.EnvEntry) { e.Name = "" }), + want: field.ErrorList{field.Required(envPath.Child("name"), "")}, + }, { + name: "env: name with equals sign", + obj: env(func(e *ateletpb.EnvEntry) { e.Name = "A=B" }), + want: field.ErrorList{field.Invalid(envPath.Child("name"), nil, "")}, + }, { + name: "env: name too long", + obj: env(func(e *ateletpb.EnvEntry) { e.Name = strings.Repeat("N", 257) }), + want: field.ErrorList{field.TooLong(envPath.Child("name"), nil, 256).WithOrigin("maxLength")}, + }, { + name: "env: empty value is allowed", + obj: env(func(e *ateletpb.EnvEntry) { e.Value = "" }), + }, { + name: "env: value too long", + obj: env(func(e *ateletpb.EnvEntry) { e.Value = strings.Repeat("v", 32769) }), + want: field.ErrorList{field.TooLong(envPath.Child("value"), nil, 32768).WithOrigin("maxLength")}, + }, + + // VolumeMount. + { + name: "duplicate mount paths", + obj: valid(func(c *ateletpb.Container) { c.VolumeMounts[1].MountPath = "/data" }), + want: field.ErrorList{field.Duplicate(field.NewPath("volume_mounts").Index(1), nil)}, + }, { + name: "nested mount paths", + obj: valid(func(c *ateletpb.Container) { c.VolumeMounts[1].MountPath = "/data/nested" }), + want: field.ErrorList{field.Invalid(field.NewPath("volume_mounts").Index(1).Child("mount_path"), nil, "")}, + }, { + name: "mount: missing name", + obj: mount("", "/var/data"), + want: field.ErrorList{field.Required(mountPath.Child("name"), "")}, + }, { + name: "mount: invalid name: uppercase", + obj: mount("Data", "/var/data"), + want: field.ErrorList{field.Invalid(mountPath.Child("name"), nil, "").WithOrigin("format=k8s-short-name")}, + }, { + name: "mount: missing mount_path", + obj: mount("data", ""), + want: field.ErrorList{field.Required(mountPath.Child("mount_path"), "")}, + }, { + name: "mount: relative mount_path", + obj: mount("data", "var/data"), + want: field.ErrorList{field.Invalid(mountPath.Child("mount_path"), nil, "")}, + }, + + // WakeupProbe and HttpGetAction. + { + name: "valid probe", + obj: probe(), + }, { + name: "probe: missing http_get", + obj: probe(func(p *ateletpb.WakeupProbe) { p.HttpGet = nil }), + want: field.ErrorList{field.Required(httpGetPath, "")}, + }, { + name: "probe: missing timeout", + obj: probe(func(p *ateletpb.WakeupProbe) { p.TimeoutSeconds = 0 }), + want: field.ErrorList{field.Required(field.NewPath("wakeup_probe", "timeout_seconds"), "")}, + }, { + name: "probe: timeout above the bound", + obj: probe(func(p *ateletpb.WakeupProbe) { p.TimeoutSeconds = 3601 }), + want: field.ErrorList{field.Invalid(field.NewPath("wakeup_probe", "timeout_seconds"), nil, "").WithOrigin("maximum")}, + }, { + name: "probe: missing path", + obj: probe(func(p *ateletpb.WakeupProbe) { p.HttpGet.Path = "" }), + want: field.ErrorList{field.Required(httpGetPath.Child("path"), "")}, + }, { + name: "probe: path without a leading slash", + obj: probe(func(p *ateletpb.WakeupProbe) { p.HttpGet.Path = "healthz" }), + want: field.ErrorList{field.Invalid(httpGetPath.Child("path"), nil, "")}, + }, { + name: "probe: missing port", + obj: probe(func(p *ateletpb.WakeupProbe) { p.HttpGet.Port = 0 }), + want: field.ErrorList{field.Required(httpGetPath.Child("port"), "")}, + }, { + name: "probe: port above the range", + obj: probe(func(p *ateletpb.WakeupProbe) { p.HttpGet.Port = 65536 }), + want: field.ErrorList{field.Invalid(httpGetPath.Child("port"), nil, "").WithOrigin("maximum")}, + }, + + // SecurityContext. + { + name: "valid capabilities", + obj: caps([]string{"NET_BIND_SERVICE"}, []string{"ALL"}), + }, { + name: "empty security context", + obj: valid(func(c *ateletpb.Container) { c.SecurityContext = &ateletpb.SecurityContext{} }), + }, { + name: "capabilities: add does not accept ALL", + obj: caps([]string{"ALL"}, nil), + want: field.ErrorList{field.Invalid(capsPath.Child("add").Index(0), nil, "")}, + }, { + name: "capabilities: lowercase rejected", + obj: caps(nil, []string{"net_bind_service"}), + want: field.ErrorList{field.Invalid(capsPath.Child("drop").Index(0), nil, "")}, + }, { + name: "capabilities: duplicate rejected by the set", + obj: caps([]string{"SYS_TIME", "SYS_TIME"}, nil), + want: field.ErrorList{field.Duplicate(capsPath.Child("add").Index(1), nil)}, + }, + + // ResourceLimits. + { + name: "valid limits", + obj: limits(&ateletpb.ResourceLimits{MemoryBytes: 1 << 30, CpuMillis: 500}), + }, { + name: "limits: zero means unset", + obj: limits(&ateletpb.ResourceLimits{}), + }, { + name: "limits: negative memory_bytes", + obj: limits(&ateletpb.ResourceLimits{MemoryBytes: -1}), + want: field.ErrorList{field.Invalid(field.NewPath("resources", "memory_bytes"), nil, "").WithOrigin("minimum")}, + }, { + name: "limits: negative cpu_millis", + obj: limits(&ateletpb.ResourceLimits{CpuMillis: -1}), + want: field.ErrorList{field.Invalid(field.NewPath("resources", "cpu_millis"), nil, "").WithOrigin("minimum")}, + }, { + name: "limits: cpu_millis at the cap", + obj: limits(&ateletpb.ResourceLimits{CpuMillis: 999999}), + }, { + name: "limits: cpu_millis above the cap", + obj: limits(&ateletpb.ResourceLimits{CpuMillis: 1000000}), + want: field.ErrorList{field.Invalid(field.NewPath("resources", "cpu_millis"), nil, "").WithOrigin("maximum")}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assertValidateErr(t, Validate_Container(context.Background(), createOp, nil, tt.obj, nil), tt.want) + }) + } +} diff --git a/cmd/atelet/internal/apivalidation/workloadspec.go b/cmd/atelet/internal/apivalidation/workloadspec.go new file mode 100644 index 0000000000..c0e3f94693 --- /dev/null +++ b/cmd/atelet/internal/apivalidation/workloadspec.go @@ -0,0 +1,153 @@ +// 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. + +// Custom validations for the WorkloadSpec tree. The field rules are shared +// with the control plane through internal/resources: values arriving here +// already passed them at template creation, so a failure is an internal +// inconsistency, not a user error. + +package apivalidation + +import ( + "context" + + "github.com/agent-substrate/substrate/internal/proto/ateletpb" + "github.com/agent-substrate/substrate/internal/resources" + "k8s.io/apimachinery/pkg/api/operation" + "k8s.io/apimachinery/pkg/util/sets" + "k8s.io/apimachinery/pkg/util/validation/field" +) + +// ValidateCustom_WorkloadSpec rejects container volume mounts that name a +// volume the spec does not declare, mirroring the template-side rule, so a +// dangling mount is refused at the RPC edge rather than after atelet has +// already reset the actor's directories and mounted its external volumes. +func ValidateCustom_WorkloadSpec(_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ *ateletpb.WorkloadSpec) field.ErrorList { + declared := sets.New[string]() + for _, vol := range value.GetVolumes() { + declared.Insert(vol.GetName()) + } + var errs field.ErrorList + for i, ctr := range value.GetContainers() { + for j, mount := range ctr.GetVolumeMounts() { + name := mount.GetName() + if name == "" { + continue // required is enforced by tags + } + if !declared.Has(name) { + errs = append(errs, field.Invalid( + fldPath.Child("containers").Index(i).Child("volume_mounts").Index(j).Child("name"), + name, "must reference a volume declared in the spec")) + } + } + } + return errs +} + +func ValidateCustom_Container_Image(_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ *string) field.ErrorList { + return resources.ValidatePinnedImage(fldPath, *value) +} + +func ValidateCustom_EnvEntry_Name(_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ *string) field.ErrorList { + return resources.ValidateEnvVarName(fldPath, *value) +} + +func ValidateCustom_VolumeMount_MountPath(_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ *string) field.ErrorList { + return resources.ValidateMountPath(fldPath, *value) +} + +// ValidateCustom_Container_VolumeMounts rejects nested mounts. Mount-path +// uniqueness is enforced by the list key. +func ValidateCustom_Container_VolumeMounts(_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ []*ateletpb.VolumeMount) field.ErrorList { + paths := make([]string, len(value)) + for i, m := range value { + paths[i] = m.GetMountPath() + } + return resources.ValidateNestedMountPaths(fldPath, paths) +} + +func ValidateCustom_Capabilities_Add(_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ []string) field.ErrorList { + return resources.ValidateCapabilities(fldPath, value, false) +} + +func ValidateCustom_Capabilities_Drop(_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ []string) field.ErrorList { + return resources.ValidateCapabilities(fldPath, value, true) +} + +func ValidateCustom_HttpGetAction_Path(_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ *string) field.ErrorList { + return resources.ValidateHTTPGetPath(fldPath, *value) +} + +func ValidateCustom_ExternalVolumeSource_StorageVolumeId(_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ *string) field.ErrorList { + return resources.ValidateStorageVolumeID(fldPath, *value) +} + +func ValidateCustom_ExternalVolumeSource_VolumeType(_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ *string) field.ErrorList { + return resources.ValidateVolumeType(fldPath, *value) +} + +func ValidateCustom_ExternalVolumeSource_VolumeContext(_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ map[string]string) field.ErrorList { + return resources.ValidateCSIMap(fldPath, value) +} + +func ValidateCustom_ExternalVolumeSource_PublishContext(_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ map[string]string) field.ErrorList { + return resources.ValidateCSIMap(fldPath, value) +} + +func ValidateCustom_ImageVolumeSource_Reference(_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ *string) field.ErrorList { + return resources.ValidatePinnedImage(fldPath, *value) +} + +func ValidateCustom_ActorMetadataItem_Path(_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ *string) field.ErrorList { + return resources.ValidateProjectedPath(fldPath, *value) +} + +func ValidateCustom_TrustBundleDataSource_Path(_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ *string) field.ErrorList { + return resources.ValidateProjectedPath(fldPath, *value) +} + +// ValidateCustom_SystemInfoVolume_DataSources allows at most one +// actor_metadata entry and requires every projected file path to be unique +// across all data sources: atelet writes them in order into one tree, so a +// repeated path silently clobbers the earlier file. +func ValidateCustom_SystemInfoVolume_DataSources(_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ []*ateletpb.SystemInfoDataSource) field.ErrorList { + var errs field.ErrorList + seen := sets.New[string]() + sawMetadata := false + for i, ds := range value { + switch { + case ds == nil: + case ds.TrustBundle != nil: + if seen.Has(ds.TrustBundle.Path) { + errs = append(errs, field.Duplicate(fldPath.Index(i).Child("trust_bundle", "path"), ds.TrustBundle.Path)) + } + seen.Insert(ds.TrustBundle.Path) + case ds.ActorMetadata != nil: + if sawMetadata { + errs = append(errs, field.Forbidden(fldPath.Index(i).Child("actor_metadata"), "at most one actor_metadata entry may appear")) + } + sawMetadata = true + for j, item := range ds.ActorMetadata.Items { + if item == nil { + continue + } + if seen.Has(item.Path) { + errs = append(errs, field.Duplicate(fldPath.Index(i).Child("actor_metadata", "items").Index(j).Child("path"), item.Path)) + } + seen.Insert(item.Path) + } + } + } + return errs +} diff --git a/cmd/atelet/internal/apivalidation/zz_generated.validation.go b/cmd/atelet/internal/apivalidation/zz_generated.validation.go index c77dd28826..5974aab64d 100644 --- a/cmd/atelet/internal/apivalidation/zz_generated.validation.go +++ b/cmd/atelet/internal/apivalidation/zz_generated.validation.go @@ -29,48 +29,75 @@ import ( field "k8s.io/apimachinery/pkg/util/validation/field" ) -// Validate_AttributeEntry validates an instance of AttributeEntry according +// Validate_ActorMetadataDataSource validates an instance of ActorMetadataDataSource according // to declarative validation rules in the API schema. -func Validate_AttributeEntry( +func Validate_ActorMetadataDataSource( ctx context.Context, op operation.Operation, fldPath *field.Path, - obj, oldObj *ateletpb.AttributeEntry) (errs field.ErrorList) { + obj, oldObj *ateletpb.ActorMetadataDataSource) (errs field.ErrorList) { - { // field ateletpb.AttributeEntry.Key + { // field ateletpb.ActorMetadataDataSource.Items fn := func( fldPath *field.Path, - obj, oldObj *string, + obj, oldObj []*ateletpb.ActorMetadataItem, oldValueCorrelated bool) (errs field.ErrorList) { // don't revalidate unchanged data if oldValueCorrelated && op.Type == operation.Update { - if obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj) { + if ateDeepEqual(obj, oldObj) { return nil } } // call field-attached validations earlyReturn := false - if e := validate.RequiredValue(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + if e := validate.PtrSliceNoNils[ateletpb.ActorMetadataItem](ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if e := validate.MaxItems(ctx, op, fldPath, obj, oldObj, 8).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if e := validate.RequiredSlice(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { errs = append(errs, e...) earlyReturn = true } if earlyReturn { return // do not proceed } - if e := validate.MaxLength(ctx, op, fldPath, obj, oldObj, 128); len(e) != 0 { + if e := validate.MinItems(ctx, op, fldPath, obj, oldObj, 1); len(e) != 0 { + errs = append(errs, e...) + } + // lists with map semantics require unique keys + if e := validate.PtrSliceUnique(ctx, op, fldPath, obj, oldObj, + func(a *ateletpb.ActorMetadataItem, b *ateletpb.ActorMetadataItem) bool { return a.Field == b.Field }); len(e) != 0 { + errs = append(errs, e...) + } + // iterate the list and call the type's validation function + if e := validate.EachPtrSliceVal(ctx, op, fldPath, obj, oldObj, + func(a *ateletpb.ActorMetadataItem, b *ateletpb.ActorMetadataItem) bool { return a.Field == b.Field }, ateDeepEqual, Validate_ActorMetadataItem); len(e) != 0 { errs = append(errs, e...) } return } oldVal := safe.Field(oldObj, - func(oldObj *ateletpb.AttributeEntry) *string { - return &oldObj.Key + func(oldObj *ateletpb.ActorMetadataDataSource) []*ateletpb.ActorMetadataItem { + return oldObj.Items }) - errs = append(errs, fn(fldPath.Child("key"), &obj.Key, oldVal, oldObj != nil)...) + errs = append(errs, fn(fldPath.Child("items"), obj.Items, oldVal, oldObj != nil)...) } - { // field ateletpb.AttributeEntry.Value + return errs +} + +// Validate_ActorMetadataItem validates an instance of ActorMetadataItem according +// to declarative validation rules in the API schema. +func Validate_ActorMetadataItem( + ctx context.Context, op operation.Operation, fldPath *field.Path, + obj, oldObj *ateletpb.ActorMetadataItem) (errs field.ErrorList) { + + { // field ateletpb.ActorMetadataItem.Field fn := func( fldPath *field.Path, - obj, oldObj *string, + obj, oldObj *ateletpb.ActorMetadataField, oldValueCorrelated bool) (errs field.ErrorList) { // don't revalidate unchanged data if oldValueCorrelated && op.Type == operation.Update { @@ -87,33 +114,22 @@ func Validate_AttributeEntry( if earlyReturn { return // do not proceed } - if e := validate.MaxLength(ctx, op, fldPath, obj, oldObj, 256); len(e) != 0 { + if e := validate.Maximum(ctx, op, fldPath, obj, oldObj, 3); len(e) != 0 { + errs = append(errs, e...) + } + if e := validate.Minimum(ctx, op, fldPath, obj, oldObj, 1); len(e) != 0 { errs = append(errs, e...) } return } oldVal := safe.Field(oldObj, - func(oldObj *ateletpb.AttributeEntry) *string { - return &oldObj.Value + func(oldObj *ateletpb.ActorMetadataItem) *ateletpb.ActorMetadataField { + return &oldObj.Field }) - errs = append(errs, fn(fldPath.Child("value"), &obj.Value, oldVal, oldObj != nil)...) - } - - return errs -} - -// Validate_Limits validates an instance of Limits according -// to declarative validation rules in the API schema. -func Validate_Limits( - ctx context.Context, op operation.Operation, fldPath *field.Path, - obj, oldObj *ateletpb.Limits) (errs field.ErrorList) { - - // custom validation - if e := ValidateCustom_Limits(ctx, op, fldPath, obj, oldObj); len(e) != 0 { - errs = append(errs, e...) + errs = append(errs, fn(fldPath.Child("field"), &obj.Field, oldVal, oldObj != nil)...) } - { // field ateletpb.Limits.Name + { // field ateletpb.ActorMetadataItem.Path fn := func( fldPath *field.Path, obj, oldObj *string, @@ -133,60 +149,91 @@ func Validate_Limits( if earlyReturn { return // do not proceed } - if e := validate.MaxLength(ctx, op, fldPath, obj, oldObj, 16); len(e) != 0 { + // custom validation + if e := ValidateCustom_ActorMetadataItem_Path(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + errs = append(errs, e...) + } + if e := validate.MaxLength(ctx, op, fldPath, obj, oldObj, 255); len(e) != 0 { + errs = append(errs, e...) + } + if e := validate.MinLength(ctx, op, fldPath, obj, oldObj, 1); len(e) != 0 { errs = append(errs, e...) } return } oldVal := safe.Field(oldObj, - func(oldObj *ateletpb.Limits) *string { - return &oldObj.Name + func(oldObj *ateletpb.ActorMetadataItem) *string { + return &oldObj.Path }) - errs = append(errs, fn(fldPath.Child("name"), &obj.Name, oldVal, oldObj != nil)...) + errs = append(errs, fn(fldPath.Child("path"), &obj.Path, oldVal, oldObj != nil)...) } - { // field ateletpb.Limits.Quantity + return errs +} + +// Validate_ArchAssets validates an instance of ArchAssets according +// to declarative validation rules in the API schema. +func Validate_ArchAssets( + ctx context.Context, op operation.Operation, fldPath *field.Path, + obj, oldObj *ateletpb.ArchAssets) (errs field.ErrorList) { + + { // field ateletpb.ArchAssets.Files fn := func( fldPath *field.Path, - obj, oldObj *string, + obj, oldObj map[string]*ateletpb.AssetFile, oldValueCorrelated bool) (errs field.ErrorList) { // don't revalidate unchanged data if oldValueCorrelated && op.Type == operation.Update { - if obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj) { + if ateDeepEqual(obj, oldObj) { return nil } } // call field-attached validations earlyReturn := false - if e := validate.RequiredValue(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + if e := validate.PtrMapNoNils[string, ateletpb.AssetFile](ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if e := validate.MaxProperties(ctx, op, fldPath, obj, oldObj, 16).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if e := validate.RequiredMap(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { errs = append(errs, e...) earlyReturn = true } if earlyReturn { return // do not proceed } - if e := validate.MaxLength(ctx, op, fldPath, obj, oldObj, 32); len(e) != 0 { + if e := validate.EachMapKey(ctx, op, fldPath, obj, oldObj, + func(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *string) field.ErrorList { + return validate.MaxLength(ctx, op, fldPath, obj, oldObj, 64) + }); len(e) != 0 { + errs = append(errs, e...) + } + // iterate the map and call the value type's validation function + if e := validate.EachPtrMapVal(ctx, op, fldPath, obj, oldObj, ateDeepEqual, Validate_AssetFile); len(e) != 0 { errs = append(errs, e...) } return } oldVal := safe.Field(oldObj, - func(oldObj *ateletpb.Limits) *string { - return &oldObj.Quantity + func(oldObj *ateletpb.ArchAssets) map[string]*ateletpb.AssetFile { + return oldObj.Files }) - errs = append(errs, fn(fldPath.Child("quantity"), &obj.Quantity, oldVal, oldObj != nil)...) + errs = append(errs, fn(fldPath.Child("files"), obj.Files, oldVal, oldObj != nil)...) } return errs } -// Validate_MintActorCertificateRequest validates an instance of MintActorCertificateRequest according +// Validate_AssetFile validates an instance of AssetFile according // to declarative validation rules in the API schema. -func Validate_MintActorCertificateRequest( +func Validate_AssetFile( ctx context.Context, op operation.Operation, fldPath *field.Path, - obj, oldObj *ateletpb.MintActorCertificateRequest) (errs field.ErrorList) { + obj, oldObj *ateletpb.AssetFile) (errs field.ErrorList) { - { // field ateletpb.MintActorCertificateRequest.ActorAtespace + { // field ateletpb.AssetFile.Url fn := func( fldPath *field.Path, obj, oldObj *string, @@ -206,19 +253,19 @@ func Validate_MintActorCertificateRequest( if earlyReturn { return // do not proceed } - if e := validate.ShortName(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + if e := validate.MaxLength(ctx, op, fldPath, obj, oldObj, 2048); len(e) != 0 { errs = append(errs, e...) } return } oldVal := safe.Field(oldObj, - func(oldObj *ateletpb.MintActorCertificateRequest) *string { - return &oldObj.ActorAtespace + func(oldObj *ateletpb.AssetFile) *string { + return &oldObj.Url }) - errs = append(errs, fn(fldPath.Child("actor_atespace"), &obj.ActorAtespace, oldVal, oldObj != nil)...) + errs = append(errs, fn(fldPath.Child("url"), &obj.Url, oldVal, oldObj != nil)...) } - { // field ateletpb.MintActorCertificateRequest.ActorName + { // field ateletpb.AssetFile.Sha256 fn := func( fldPath *field.Path, obj, oldObj *string, @@ -238,19 +285,32 @@ func Validate_MintActorCertificateRequest( if earlyReturn { return // do not proceed } - if e := validate.ShortName(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + // custom validation + if e := ValidateCustom_AssetFile_Sha256(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + errs = append(errs, e...) + } + if e := validate.MaxLength(ctx, op, fldPath, obj, oldObj, 64); len(e) != 0 { errs = append(errs, e...) } return } oldVal := safe.Field(oldObj, - func(oldObj *ateletpb.MintActorCertificateRequest) *string { - return &oldObj.ActorName + func(oldObj *ateletpb.AssetFile) *string { + return &oldObj.Sha256 }) - errs = append(errs, fn(fldPath.Child("actor_name"), &obj.ActorName, oldVal, oldObj != nil)...) + errs = append(errs, fn(fldPath.Child("sha256"), &obj.Sha256, oldVal, oldObj != nil)...) } - { // field ateletpb.MintActorCertificateRequest.ActorUid + return errs +} + +// Validate_AttributeEntry validates an instance of AttributeEntry according +// to declarative validation rules in the API schema. +func Validate_AttributeEntry( + ctx context.Context, op operation.Operation, fldPath *field.Path, + obj, oldObj *ateletpb.AttributeEntry) (errs field.ErrorList) { + + { // field ateletpb.AttributeEntry.Key fn := func( fldPath *field.Path, obj, oldObj *string, @@ -270,63 +330,63 @@ func Validate_MintActorCertificateRequest( if earlyReturn { return // do not proceed } - if e := validate.UUID(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + if e := validate.MaxLength(ctx, op, fldPath, obj, oldObj, 128); len(e) != 0 { errs = append(errs, e...) } return } oldVal := safe.Field(oldObj, - func(oldObj *ateletpb.MintActorCertificateRequest) *string { - return &oldObj.ActorUid + func(oldObj *ateletpb.AttributeEntry) *string { + return &oldObj.Key }) - errs = append(errs, fn(fldPath.Child("actor_uid"), &obj.ActorUid, oldVal, oldObj != nil)...) + errs = append(errs, fn(fldPath.Child("key"), &obj.Key, oldVal, oldObj != nil)...) } - { // field ateletpb.MintActorCertificateRequest.CertificateSigningRequest + { // field ateletpb.AttributeEntry.Value fn := func( fldPath *field.Path, - obj, oldObj []byte, + obj, oldObj *string, oldValueCorrelated bool) (errs field.ErrorList) { // don't revalidate unchanged data if oldValueCorrelated && op.Type == operation.Update { - if ateDeepEqual(obj, oldObj) { + if obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj) { return nil } } // call field-attached validations earlyReturn := false - if e := validate.RequiredSlice(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + if e := validate.RequiredValue(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { errs = append(errs, e...) earlyReturn = true } if earlyReturn { return // do not proceed } - if e := validate.MaxBytesSlice(ctx, op, fldPath, obj, oldObj, 16384); len(e) != 0 { + if e := validate.MaxLength(ctx, op, fldPath, obj, oldObj, 256); len(e) != 0 { errs = append(errs, e...) } return } oldVal := safe.Field(oldObj, - func(oldObj *ateletpb.MintActorCertificateRequest) []byte { - return oldObj.CertificateSigningRequest + func(oldObj *ateletpb.AttributeEntry) *string { + return &oldObj.Value }) - errs = append(errs, fn(fldPath.Child("certificate_signing_request"), obj.CertificateSigningRequest, oldVal, oldObj != nil)...) + errs = append(errs, fn(fldPath.Child("value"), &obj.Value, oldVal, oldObj != nil)...) } return errs } -// Validate_RegisterWorkerRequest validates an instance of RegisterWorkerRequest according +// Validate_Capabilities validates an instance of Capabilities according // to declarative validation rules in the API schema. -func Validate_RegisterWorkerRequest( +func Validate_Capabilities( ctx context.Context, op operation.Operation, fldPath *field.Path, - obj, oldObj *ateletpb.RegisterWorkerRequest) (errs field.ErrorList) { + obj, oldObj *ateletpb.Capabilities) (errs field.ErrorList) { - { // field ateletpb.RegisterWorkerRequest.Capacity + { // field ateletpb.Capabilities.Add fn := func( fldPath *field.Path, - obj, oldObj *ateletpb.WorkerResources, + obj, oldObj []string, oldValueCorrelated bool) (errs field.ErrorList) { // don't revalidate unchanged data if oldValueCorrelated && op.Type == operation.Update { @@ -336,28 +396,37 @@ func Validate_RegisterWorkerRequest( } // call field-attached validations earlyReturn := false - if e := validate.RequiredPointer(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + if e := validate.MaxItems(ctx, op, fldPath, obj, oldObj, 64).MarkShortCircuit(); len(e) != 0 { errs = append(errs, e...) earlyReturn = true } + if e := validate.OptionalSlice(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + earlyReturn = true + } if earlyReturn { return // do not proceed } - // call the type's validation function - errs = append(errs, Validate_WorkerResources(ctx, op, fldPath, obj, oldObj)...) + // custom validation + if e := ValidateCustom_Capabilities_Add(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + errs = append(errs, e...) + } + // lists with set semantics require unique values + if e := validate.ValSliceUnique(ctx, op, fldPath, obj, oldObj, validate.DirectEqual); len(e) != 0 { + errs = append(errs, e...) + } return } oldVal := safe.Field(oldObj, - func(oldObj *ateletpb.RegisterWorkerRequest) *ateletpb.WorkerResources { - return oldObj.Capacity + func(oldObj *ateletpb.Capabilities) []string { + return oldObj.Add }) - errs = append(errs, fn(fldPath.Child("capacity"), obj.Capacity, oldVal, oldObj != nil)...) + errs = append(errs, fn(fldPath.Child("add"), obj.Add, oldVal, oldObj != nil)...) } - { // field ateletpb.RegisterWorkerRequest.DefaultRuntime + { // field ateletpb.Capabilities.Drop fn := func( fldPath *field.Path, - obj, oldObj *ateletpb.SandboxRuntime, + obj, oldObj []string, oldValueCorrelated bool) (errs field.ErrorList) { // don't revalidate unchanged data if oldValueCorrelated && op.Type == operation.Update { @@ -367,74 +436,97 @@ func Validate_RegisterWorkerRequest( } // call field-attached validations earlyReturn := false - if e := validate.RequiredPointer(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + if e := validate.MaxItems(ctx, op, fldPath, obj, oldObj, 64).MarkShortCircuit(); len(e) != 0 { errs = append(errs, e...) earlyReturn = true } + if e := validate.OptionalSlice(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + earlyReturn = true + } if earlyReturn { return // do not proceed } - // call the type's validation function - errs = append(errs, Validate_SandboxRuntime(ctx, op, fldPath, obj, oldObj)...) + // custom validation + if e := ValidateCustom_Capabilities_Drop(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + errs = append(errs, e...) + } + // lists with set semantics require unique values + if e := validate.ValSliceUnique(ctx, op, fldPath, obj, oldObj, validate.DirectEqual); len(e) != 0 { + errs = append(errs, e...) + } return } oldVal := safe.Field(oldObj, - func(oldObj *ateletpb.RegisterWorkerRequest) *ateletpb.SandboxRuntime { - return oldObj.DefaultRuntime + func(oldObj *ateletpb.Capabilities) []string { + return oldObj.Drop }) - errs = append(errs, fn(fldPath.Child("default_runtime"), obj.DefaultRuntime, oldVal, oldObj != nil)...) + errs = append(errs, fn(fldPath.Child("drop"), obj.Drop, oldVal, oldObj != nil)...) } - { // field ateletpb.RegisterWorkerRequest.RestorableRuntimes + return errs +} + +var unionMembershipFor_github_com_agent_substrate_substrate_internal_proto_ateletpb_CheckpointRequest_ = validate.NewUnionMembership(validate.NewUnionMember("local_config"), validate.NewUnionMember("external_config")) + +// Validate_CheckpointRequest validates an instance of CheckpointRequest according +// to declarative validation rules in the API schema. +func Validate_CheckpointRequest( + ctx context.Context, op operation.Operation, fldPath *field.Path, + obj, oldObj *ateletpb.CheckpointRequest) (errs field.ErrorList) { + + if e := validate.Union(ctx, op, fldPath, obj, oldObj, unionMembershipFor_github_com_agent_substrate_substrate_internal_proto_ateletpb_CheckpointRequest_, + func(obj *ateletpb.CheckpointRequest) bool { + if obj == nil { + return false + } + return obj.LocalConfig != nil + }, + func(obj *ateletpb.CheckpointRequest) bool { + if obj == nil { + return false + } + return obj.ExternalConfig != nil + }); len(e) != 0 { + errs = append(errs, e...) + } + // custom validation + if e := ValidateCustom_CheckpointRequest(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + errs = append(errs, e...) + } + + { // field ateletpb.CheckpointRequest.WorkerPodUid fn := func( fldPath *field.Path, - obj, oldObj []*ateletpb.SandboxRuntime, + obj, oldObj *string, oldValueCorrelated bool) (errs field.ErrorList) { // don't revalidate unchanged data if oldValueCorrelated && op.Type == operation.Update { - if ateDeepEqual(obj, oldObj) { + if obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj) { return nil } } // call field-attached validations earlyReturn := false - if e := validate.PtrSliceNoNils[ateletpb.SandboxRuntime](ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { - errs = append(errs, e...) - earlyReturn = true - } - if e := validate.MaxItems(ctx, op, fldPath, obj, oldObj, 32).MarkShortCircuit(); len(e) != 0 { + if e := validate.RequiredValue(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { errs = append(errs, e...) earlyReturn = true } - if e := validate.OptionalSlice(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { - earlyReturn = true - } if earlyReturn { return // do not proceed } - // iterate the list and call the type's validation function - if e := validate.EachPtrSliceVal(ctx, op, fldPath, obj, oldObj, nil, nil, Validate_SandboxRuntime); len(e) != 0 { + if e := validate.UUID(ctx, op, fldPath, obj, oldObj); len(e) != 0 { errs = append(errs, e...) } return } oldVal := safe.Field(oldObj, - func(oldObj *ateletpb.RegisterWorkerRequest) []*ateletpb.SandboxRuntime { - return oldObj.RestorableRuntimes + func(oldObj *ateletpb.CheckpointRequest) *string { + return &oldObj.WorkerPodUid }) - errs = append(errs, fn(fldPath.Child("restorable_runtimes"), obj.RestorableRuntimes, oldVal, oldObj != nil)...) + errs = append(errs, fn(fldPath.Child("worker_pod_uid"), &obj.WorkerPodUid, oldVal, oldObj != nil)...) } - return errs -} - -// Validate_RequestActorSuspendRequest validates an instance of RequestActorSuspendRequest according -// to declarative validation rules in the API schema. -func Validate_RequestActorSuspendRequest( - ctx context.Context, op operation.Operation, fldPath *field.Path, - obj, oldObj *ateletpb.RequestActorSuspendRequest) (errs field.ErrorList) { - - { // field ateletpb.RequestActorSuspendRequest.ActorAtespace + { // field ateletpb.CheckpointRequest.Atespace fn := func( fldPath *field.Path, obj, oldObj *string, @@ -460,13 +552,13 @@ func Validate_RequestActorSuspendRequest( return } oldVal := safe.Field(oldObj, - func(oldObj *ateletpb.RequestActorSuspendRequest) *string { - return &oldObj.ActorAtespace + func(oldObj *ateletpb.CheckpointRequest) *string { + return &oldObj.Atespace }) - errs = append(errs, fn(fldPath.Child("actor_atespace"), &obj.ActorAtespace, oldVal, oldObj != nil)...) + errs = append(errs, fn(fldPath.Child("atespace"), &obj.Atespace, oldVal, oldObj != nil)...) } - { // field ateletpb.RequestActorSuspendRequest.ActorName + { // field ateletpb.CheckpointRequest.ActorName fn := func( fldPath *field.Path, obj, oldObj *string, @@ -492,13 +584,13 @@ func Validate_RequestActorSuspendRequest( return } oldVal := safe.Field(oldObj, - func(oldObj *ateletpb.RequestActorSuspendRequest) *string { + func(oldObj *ateletpb.CheckpointRequest) *string { return &oldObj.ActorName }) errs = append(errs, fn(fldPath.Child("actor_name"), &obj.ActorName, oldVal, oldObj != nil)...) } - { // field ateletpb.RequestActorSuspendRequest.ActorUid + { // field ateletpb.CheckpointRequest.ActorUid fn := func( fldPath *field.Path, obj, oldObj *string, @@ -524,112 +616,3750 @@ func Validate_RequestActorSuspendRequest( return } oldVal := safe.Field(oldObj, - func(oldObj *ateletpb.RequestActorSuspendRequest) *string { + func(oldObj *ateletpb.CheckpointRequest) *string { return &oldObj.ActorUid }) errs = append(errs, fn(fldPath.Child("actor_uid"), &obj.ActorUid, oldVal, oldObj != nil)...) } - return errs -} - -// Validate_Resources validates an instance of Resources according -// to declarative validation rules in the API schema. -func Validate_Resources( - ctx context.Context, op operation.Operation, fldPath *field.Path, - obj, oldObj *ateletpb.Resources) (errs field.ErrorList) { - - { // field ateletpb.Resources.Limits + { // field ateletpb.CheckpointRequest.ActorTemplateAtespace fn := func( fldPath *field.Path, - obj, oldObj []*ateletpb.Limits, + obj, oldObj *string, oldValueCorrelated bool) (errs field.ErrorList) { // don't revalidate unchanged data if oldValueCorrelated && op.Type == operation.Update { - if ateDeepEqual(obj, oldObj) { + if obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj) { return nil } } // call field-attached validations earlyReturn := false - if e := validate.PtrSliceNoNils[ateletpb.Limits](ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { - errs = append(errs, e...) - earlyReturn = true - } - if e := validate.MaxItems(ctx, op, fldPath, obj, oldObj, 2).MarkShortCircuit(); len(e) != 0 { - errs = append(errs, e...) - earlyReturn = true - } - if e := validate.OptionalSlice(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + if e := validate.OptionalValue(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { earlyReturn = true } if earlyReturn { return // do not proceed } - // lists with map semantics require unique keys - if e := validate.PtrSliceUnique(ctx, op, fldPath, obj, oldObj, - func(a *ateletpb.Limits, b *ateletpb.Limits) bool { return a.Name == b.Name }); len(e) != 0 { + if e := validate.ShortName(ctx, op, fldPath, obj, oldObj); len(e) != 0 { errs = append(errs, e...) } - // iterate the list and call the type's validation function - if e := validate.EachPtrSliceVal(ctx, op, fldPath, obj, oldObj, - func(a *ateletpb.Limits, b *ateletpb.Limits) bool { return a.Name == b.Name }, ateDeepEqual, Validate_Limits); len(e) != 0 { - errs = append(errs, e...) + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.CheckpointRequest) *string { + return &oldObj.ActorTemplateAtespace + }) + errs = append(errs, fn(fldPath.Child("actor_template_atespace"), &obj.ActorTemplateAtespace, oldVal, oldObj != nil)...) + } + + { // field ateletpb.CheckpointRequest.ActorTemplateName + fn := func( + fldPath *field.Path, + obj, oldObj *string, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.OptionalValue(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + if e := validate.ShortName(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.CheckpointRequest) *string { + return &oldObj.ActorTemplateName + }) + errs = append(errs, fn(fldPath.Child("actor_template_name"), &obj.ActorTemplateName, oldVal, oldObj != nil)...) + } + + { // field ateletpb.CheckpointRequest.Spec + fn := func( + fldPath *field.Path, + obj, oldObj *ateletpb.WorkloadSpec, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if ateDeepEqual(obj, oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.RequiredPointer(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + // call the type's validation function + errs = append(errs, Validate_WorkloadSpec(ctx, op, fldPath, obj, oldObj)...) + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.CheckpointRequest) *ateletpb.WorkloadSpec { + return oldObj.Spec + }) + errs = append(errs, fn(fldPath.Child("spec"), obj.Spec, oldVal, oldObj != nil)...) + } + + { // field ateletpb.CheckpointRequest.Type + fn := func( + fldPath *field.Path, + obj, oldObj *ateletpb.CheckpointType, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.RequiredValue(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + if e := validate.Maximum(ctx, op, fldPath, obj, oldObj, 2); len(e) != 0 { + errs = append(errs, e...) + } + if e := validate.Minimum(ctx, op, fldPath, obj, oldObj, 1); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.CheckpointRequest) *ateletpb.CheckpointType { + return &oldObj.Type + }) + errs = append(errs, fn(fldPath.Child("type"), &obj.Type, oldVal, oldObj != nil)...) + } + + { // field ateletpb.CheckpointRequest.LocalConfig + fn := func( + fldPath *field.Path, + obj, oldObj *ateletpb.LocalCheckpointConfiguration, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if ateDeepEqual(obj, oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.OptionalPointer(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + // call the type's validation function + errs = append(errs, Validate_LocalCheckpointConfiguration(ctx, op, fldPath, obj, oldObj)...) + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.CheckpointRequest) *ateletpb.LocalCheckpointConfiguration { + return oldObj.LocalConfig + }) + errs = append(errs, fn(fldPath.Child("local_config"), obj.LocalConfig, oldVal, oldObj != nil)...) + } + + { // field ateletpb.CheckpointRequest.ExternalConfig + fn := func( + fldPath *field.Path, + obj, oldObj *ateletpb.ExternalCheckpointConfiguration, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if ateDeepEqual(obj, oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.OptionalPointer(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + // call the type's validation function + errs = append(errs, Validate_ExternalCheckpointConfiguration(ctx, op, fldPath, obj, oldObj)...) + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.CheckpointRequest) *ateletpb.ExternalCheckpointConfiguration { + return oldObj.ExternalConfig + }) + errs = append(errs, fn(fldPath.Child("external_config"), obj.ExternalConfig, oldVal, oldObj != nil)...) + } + + { // field ateletpb.CheckpointRequest.Fidelity + fn := func( + fldPath *field.Path, + obj, oldObj *ateletpb.SnapshotFidelity, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.RequiredValue(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + if e := validate.Maximum(ctx, op, fldPath, obj, oldObj, 3); len(e) != 0 { + errs = append(errs, e...) + } + if e := validate.Minimum(ctx, op, fldPath, obj, oldObj, 1); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.CheckpointRequest) *ateletpb.SnapshotFidelity { + return &oldObj.Fidelity + }) + errs = append(errs, fn(fldPath.Child("fidelity"), &obj.Fidelity, oldVal, oldObj != nil)...) + } + + return errs +} + +// Validate_Container validates an instance of Container according +// to declarative validation rules in the API schema. +func Validate_Container( + ctx context.Context, op operation.Operation, fldPath *field.Path, + obj, oldObj *ateletpb.Container) (errs field.ErrorList) { + + { // field ateletpb.Container.Name + fn := func( + fldPath *field.Path, + obj, oldObj *string, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.RequiredValue(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + if e := validate.ShortName(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.Container) *string { + return &oldObj.Name + }) + errs = append(errs, fn(fldPath.Child("name"), &obj.Name, oldVal, oldObj != nil)...) + } + + { // field ateletpb.Container.Image + fn := func( + fldPath *field.Path, + obj, oldObj *string, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.RequiredValue(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + // custom validation + if e := ValidateCustom_Container_Image(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + errs = append(errs, e...) + } + if e := validate.MaxLength(ctx, op, fldPath, obj, oldObj, 512); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.Container) *string { + return &oldObj.Image + }) + errs = append(errs, fn(fldPath.Child("image"), &obj.Image, oldVal, oldObj != nil)...) + } + + { // field ateletpb.Container.Command + fn := func( + fldPath *field.Path, + obj, oldObj []string, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if ateDeepEqual(obj, oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.MaxItems(ctx, op, fldPath, obj, oldObj, 64).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if e := validate.OptionalSlice(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + if e := validate.EachValSliceVal(ctx, op, fldPath, obj, oldObj, nil, nil, + func(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *string) field.ErrorList { + return validate.MaxLength(ctx, op, fldPath, obj, oldObj, 4096) + }); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.Container) []string { + return oldObj.Command + }) + errs = append(errs, fn(fldPath.Child("command"), obj.Command, oldVal, oldObj != nil)...) + } + + { // field ateletpb.Container.Args + fn := func( + fldPath *field.Path, + obj, oldObj []string, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if ateDeepEqual(obj, oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.MaxItems(ctx, op, fldPath, obj, oldObj, 64).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if e := validate.OptionalSlice(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + if e := validate.EachValSliceVal(ctx, op, fldPath, obj, oldObj, nil, nil, + func(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *string) field.ErrorList { + return validate.MaxLength(ctx, op, fldPath, obj, oldObj, 4096) + }); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.Container) []string { + return oldObj.Args + }) + errs = append(errs, fn(fldPath.Child("args"), obj.Args, oldVal, oldObj != nil)...) + } + + { // field ateletpb.Container.Env + fn := func( + fldPath *field.Path, + obj, oldObj []*ateletpb.EnvEntry, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if ateDeepEqual(obj, oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.PtrSliceNoNils[ateletpb.EnvEntry](ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if e := validate.MaxItems(ctx, op, fldPath, obj, oldObj, 32).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if e := validate.OptionalSlice(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + // lists with map semantics require unique keys + if e := validate.PtrSliceUnique(ctx, op, fldPath, obj, oldObj, + func(a *ateletpb.EnvEntry, b *ateletpb.EnvEntry) bool { return a.Name == b.Name }); len(e) != 0 { + errs = append(errs, e...) + } + // iterate the list and call the type's validation function + if e := validate.EachPtrSliceVal(ctx, op, fldPath, obj, oldObj, + func(a *ateletpb.EnvEntry, b *ateletpb.EnvEntry) bool { return a.Name == b.Name }, ateDeepEqual, Validate_EnvEntry); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.Container) []*ateletpb.EnvEntry { + return oldObj.Env + }) + errs = append(errs, fn(fldPath.Child("env"), obj.Env, oldVal, oldObj != nil)...) + } + + { // field ateletpb.Container.WakeupProbe + fn := func( + fldPath *field.Path, + obj, oldObj *ateletpb.WakeupProbe, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if ateDeepEqual(obj, oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.OptionalPointer(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + // call the type's validation function + errs = append(errs, Validate_WakeupProbe(ctx, op, fldPath, obj, oldObj)...) + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.Container) *ateletpb.WakeupProbe { + return oldObj.WakeupProbe + }) + errs = append(errs, fn(fldPath.Child("wakeup_probe"), obj.WakeupProbe, oldVal, oldObj != nil)...) + } + + { // field ateletpb.Container.VolumeMounts + fn := func( + fldPath *field.Path, + obj, oldObj []*ateletpb.VolumeMount, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if ateDeepEqual(obj, oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.PtrSliceNoNils[ateletpb.VolumeMount](ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if e := validate.MaxItems(ctx, op, fldPath, obj, oldObj, 32).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if e := validate.OptionalSlice(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + // custom validation + if e := ValidateCustom_Container_VolumeMounts(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + errs = append(errs, e...) + } + // lists with map semantics require unique keys + if e := validate.PtrSliceUnique(ctx, op, fldPath, obj, oldObj, + func(a *ateletpb.VolumeMount, b *ateletpb.VolumeMount) bool { return a.MountPath == b.MountPath }); len(e) != 0 { + errs = append(errs, e...) + } + // iterate the list and call the type's validation function + if e := validate.EachPtrSliceVal(ctx, op, fldPath, obj, oldObj, + func(a *ateletpb.VolumeMount, b *ateletpb.VolumeMount) bool { return a.MountPath == b.MountPath }, ateDeepEqual, Validate_VolumeMount); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.Container) []*ateletpb.VolumeMount { + return oldObj.VolumeMounts + }) + errs = append(errs, fn(fldPath.Child("volume_mounts"), obj.VolumeMounts, oldVal, oldObj != nil)...) + } + + { // field ateletpb.Container.SecurityContext + fn := func( + fldPath *field.Path, + obj, oldObj *ateletpb.SecurityContext, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if ateDeepEqual(obj, oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.OptionalPointer(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + // call the type's validation function + errs = append(errs, Validate_SecurityContext(ctx, op, fldPath, obj, oldObj)...) + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.Container) *ateletpb.SecurityContext { + return oldObj.SecurityContext + }) + errs = append(errs, fn(fldPath.Child("security_context"), obj.SecurityContext, oldVal, oldObj != nil)...) + } + + { // field ateletpb.Container.Resources + fn := func( + fldPath *field.Path, + obj, oldObj *ateletpb.ResourceLimits, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if ateDeepEqual(obj, oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.OptionalPointer(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + // call the type's validation function + errs = append(errs, Validate_ResourceLimits(ctx, op, fldPath, obj, oldObj)...) + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.Container) *ateletpb.ResourceLimits { + return oldObj.Resources + }) + errs = append(errs, fn(fldPath.Child("resources"), obj.Resources, oldVal, oldObj != nil)...) + } + + return errs +} + +// Validate_EgressGateway validates an instance of EgressGateway according +// to declarative validation rules in the API schema. +func Validate_EgressGateway( + ctx context.Context, op operation.Operation, fldPath *field.Path, + obj, oldObj *ateletpb.EgressGateway) (errs field.ErrorList) { + + { // field ateletpb.EgressGateway.Address + fn := func( + fldPath *field.Path, + obj, oldObj *string, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.RequiredValue(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + // custom validation + if e := ValidateCustom_EgressGateway_Address(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + errs = append(errs, e...) + } + if e := validate.MaxLength(ctx, op, fldPath, obj, oldObj, 261); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.EgressGateway) *string { + return &oldObj.Address + }) + errs = append(errs, fn(fldPath.Child("address"), &obj.Address, oldVal, oldObj != nil)...) + } + + return errs +} + +// Validate_EnvEntry validates an instance of EnvEntry according +// to declarative validation rules in the API schema. +func Validate_EnvEntry( + ctx context.Context, op operation.Operation, fldPath *field.Path, + obj, oldObj *ateletpb.EnvEntry) (errs field.ErrorList) { + + { // field ateletpb.EnvEntry.Name + fn := func( + fldPath *field.Path, + obj, oldObj *string, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.RequiredValue(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + // custom validation + if e := ValidateCustom_EnvEntry_Name(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + errs = append(errs, e...) + } + if e := validate.MaxLength(ctx, op, fldPath, obj, oldObj, 256); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.EnvEntry) *string { + return &oldObj.Name + }) + errs = append(errs, fn(fldPath.Child("name"), &obj.Name, oldVal, oldObj != nil)...) + } + + { // field ateletpb.EnvEntry.Value + fn := func( + fldPath *field.Path, + obj, oldObj *string, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.OptionalValue(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + if e := validate.MaxLength(ctx, op, fldPath, obj, oldObj, 32768); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.EnvEntry) *string { + return &oldObj.Value + }) + errs = append(errs, fn(fldPath.Child("value"), &obj.Value, oldVal, oldObj != nil)...) + } + + return errs +} + +// Validate_ExternalCheckpointConfiguration validates an instance of ExternalCheckpointConfiguration according +// to declarative validation rules in the API schema. +func Validate_ExternalCheckpointConfiguration( + ctx context.Context, op operation.Operation, fldPath *field.Path, + obj, oldObj *ateletpb.ExternalCheckpointConfiguration) (errs field.ErrorList) { + + { // field ateletpb.ExternalCheckpointConfiguration.SnapshotUri + fn := func( + fldPath *field.Path, + obj, oldObj *string, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.RequiredValue(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + // custom validation + if e := ValidateCustom_ExternalCheckpointConfiguration_SnapshotUri(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + errs = append(errs, e...) + } + if e := validate.MaxLength(ctx, op, fldPath, obj, oldObj, 2048); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.ExternalCheckpointConfiguration) *string { + return &oldObj.SnapshotUri + }) + errs = append(errs, fn(fldPath.Child("snapshot_uri"), &obj.SnapshotUri, oldVal, oldObj != nil)...) + } + + return errs +} + +// Validate_ExternalRestoreConfiguration validates an instance of ExternalRestoreConfiguration according +// to declarative validation rules in the API schema. +func Validate_ExternalRestoreConfiguration( + ctx context.Context, op operation.Operation, fldPath *field.Path, + obj, oldObj *ateletpb.ExternalRestoreConfiguration) (errs field.ErrorList) { + + { // field ateletpb.ExternalRestoreConfiguration.SnapshotUri + fn := func( + fldPath *field.Path, + obj, oldObj *string, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.RequiredValue(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + // custom validation + if e := ValidateCustom_ExternalRestoreConfiguration_SnapshotUri(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + errs = append(errs, e...) + } + if e := validate.MaxLength(ctx, op, fldPath, obj, oldObj, 2048); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.ExternalRestoreConfiguration) *string { + return &oldObj.SnapshotUri + }) + errs = append(errs, fn(fldPath.Child("snapshot_uri"), &obj.SnapshotUri, oldVal, oldObj != nil)...) + } + + return errs +} + +// Validate_ExternalVolumeSource validates an instance of ExternalVolumeSource according +// to declarative validation rules in the API schema. +func Validate_ExternalVolumeSource( + ctx context.Context, op operation.Operation, fldPath *field.Path, + obj, oldObj *ateletpb.ExternalVolumeSource) (errs field.ErrorList) { + + { // field ateletpb.ExternalVolumeSource.StorageVolumeId + fn := func( + fldPath *field.Path, + obj, oldObj *string, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.RequiredValue(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + // custom validation + if e := ValidateCustom_ExternalVolumeSource_StorageVolumeId(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + errs = append(errs, e...) + } + if e := validate.MaxLength(ctx, op, fldPath, obj, oldObj, 256); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.ExternalVolumeSource) *string { + return &oldObj.StorageVolumeId + }) + errs = append(errs, fn(fldPath.Child("storage_volume_id"), &obj.StorageVolumeId, oldVal, oldObj != nil)...) + } + + { // field ateletpb.ExternalVolumeSource.VolumeType + fn := func( + fldPath *field.Path, + obj, oldObj *string, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.OptionalValue(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + // custom validation + if e := ValidateCustom_ExternalVolumeSource_VolumeType(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + errs = append(errs, e...) + } + if e := validate.MaxLength(ctx, op, fldPath, obj, oldObj, 253); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.ExternalVolumeSource) *string { + return &oldObj.VolumeType + }) + errs = append(errs, fn(fldPath.Child("volume_type"), &obj.VolumeType, oldVal, oldObj != nil)...) + } + + { // field ateletpb.ExternalVolumeSource.VolumeContext + fn := func( + fldPath *field.Path, + obj, oldObj map[string]string, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if ateDeepEqual(obj, oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.OptionalMap(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + // custom validation + if e := ValidateCustom_ExternalVolumeSource_VolumeContext(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.ExternalVolumeSource) map[string]string { + return oldObj.VolumeContext + }) + errs = append(errs, fn(fldPath.Child("volume_context"), obj.VolumeContext, oldVal, oldObj != nil)...) + } + + { // field ateletpb.ExternalVolumeSource.PublishContext + fn := func( + fldPath *field.Path, + obj, oldObj map[string]string, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if ateDeepEqual(obj, oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.OptionalMap(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + // custom validation + if e := ValidateCustom_ExternalVolumeSource_PublishContext(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.ExternalVolumeSource) map[string]string { + return oldObj.PublishContext + }) + errs = append(errs, fn(fldPath.Child("publish_context"), obj.PublishContext, oldVal, oldObj != nil)...) + } + + return errs +} + +// Validate_HttpGetAction validates an instance of HttpGetAction according +// to declarative validation rules in the API schema. +func Validate_HttpGetAction( + ctx context.Context, op operation.Operation, fldPath *field.Path, + obj, oldObj *ateletpb.HttpGetAction) (errs field.ErrorList) { + + { // field ateletpb.HttpGetAction.Path + fn := func( + fldPath *field.Path, + obj, oldObj *string, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.RequiredValue(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + // custom validation + if e := ValidateCustom_HttpGetAction_Path(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + errs = append(errs, e...) + } + if e := validate.MaxLength(ctx, op, fldPath, obj, oldObj, 1024); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.HttpGetAction) *string { + return &oldObj.Path + }) + errs = append(errs, fn(fldPath.Child("path"), &obj.Path, oldVal, oldObj != nil)...) + } + + { // field ateletpb.HttpGetAction.Port + fn := func( + fldPath *field.Path, + obj, oldObj *int32, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.RequiredValue(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + if e := validate.Maximum(ctx, op, fldPath, obj, oldObj, 65535); len(e) != 0 { + errs = append(errs, e...) + } + if e := validate.Minimum(ctx, op, fldPath, obj, oldObj, 1); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.HttpGetAction) *int32 { + return &oldObj.Port + }) + errs = append(errs, fn(fldPath.Child("port"), &obj.Port, oldVal, oldObj != nil)...) + } + + return errs +} + +// Validate_ImageVolumeSource validates an instance of ImageVolumeSource according +// to declarative validation rules in the API schema. +func Validate_ImageVolumeSource( + ctx context.Context, op operation.Operation, fldPath *field.Path, + obj, oldObj *ateletpb.ImageVolumeSource) (errs field.ErrorList) { + + { // field ateletpb.ImageVolumeSource.Reference + fn := func( + fldPath *field.Path, + obj, oldObj *string, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.RequiredValue(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + // custom validation + if e := ValidateCustom_ImageVolumeSource_Reference(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + errs = append(errs, e...) + } + if e := validate.MaxLength(ctx, op, fldPath, obj, oldObj, 512); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.ImageVolumeSource) *string { + return &oldObj.Reference + }) + errs = append(errs, fn(fldPath.Child("reference"), &obj.Reference, oldVal, oldObj != nil)...) + } + + return errs +} + +// Validate_Limits validates an instance of Limits according +// to declarative validation rules in the API schema. +func Validate_Limits( + ctx context.Context, op operation.Operation, fldPath *field.Path, + obj, oldObj *ateletpb.Limits) (errs field.ErrorList) { + + // custom validation + if e := ValidateCustom_Limits(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + errs = append(errs, e...) + } + + { // field ateletpb.Limits.Name + fn := func( + fldPath *field.Path, + obj, oldObj *string, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.RequiredValue(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + if e := validate.MaxLength(ctx, op, fldPath, obj, oldObj, 16); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.Limits) *string { + return &oldObj.Name + }) + errs = append(errs, fn(fldPath.Child("name"), &obj.Name, oldVal, oldObj != nil)...) + } + + { // field ateletpb.Limits.Quantity + fn := func( + fldPath *field.Path, + obj, oldObj *string, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.RequiredValue(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + if e := validate.MaxLength(ctx, op, fldPath, obj, oldObj, 32); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.Limits) *string { + return &oldObj.Quantity + }) + errs = append(errs, fn(fldPath.Child("quantity"), &obj.Quantity, oldVal, oldObj != nil)...) + } + + return errs +} + +// Validate_LocalCheckpointConfiguration validates an instance of LocalCheckpointConfiguration according +// to declarative validation rules in the API schema. +func Validate_LocalCheckpointConfiguration( + ctx context.Context, op operation.Operation, fldPath *field.Path, + obj, oldObj *ateletpb.LocalCheckpointConfiguration) (errs field.ErrorList) { + + { // field ateletpb.LocalCheckpointConfiguration.SnapshotName + fn := func( + fldPath *field.Path, + obj, oldObj *string, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.RequiredValue(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + if e := validate.ShortName(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.LocalCheckpointConfiguration) *string { + return &oldObj.SnapshotName + }) + errs = append(errs, fn(fldPath.Child("snapshot_name"), &obj.SnapshotName, oldVal, oldObj != nil)...) + } + + return errs +} + +// Validate_MintActorCertificateRequest validates an instance of MintActorCertificateRequest according +// to declarative validation rules in the API schema. +func Validate_MintActorCertificateRequest( + ctx context.Context, op operation.Operation, fldPath *field.Path, + obj, oldObj *ateletpb.MintActorCertificateRequest) (errs field.ErrorList) { + + { // field ateletpb.MintActorCertificateRequest.ActorAtespace + fn := func( + fldPath *field.Path, + obj, oldObj *string, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.RequiredValue(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + if e := validate.ShortName(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.MintActorCertificateRequest) *string { + return &oldObj.ActorAtespace + }) + errs = append(errs, fn(fldPath.Child("actor_atespace"), &obj.ActorAtespace, oldVal, oldObj != nil)...) + } + + { // field ateletpb.MintActorCertificateRequest.ActorName + fn := func( + fldPath *field.Path, + obj, oldObj *string, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.RequiredValue(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + if e := validate.ShortName(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.MintActorCertificateRequest) *string { + return &oldObj.ActorName + }) + errs = append(errs, fn(fldPath.Child("actor_name"), &obj.ActorName, oldVal, oldObj != nil)...) + } + + { // field ateletpb.MintActorCertificateRequest.ActorUid + fn := func( + fldPath *field.Path, + obj, oldObj *string, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.RequiredValue(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + if e := validate.UUID(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.MintActorCertificateRequest) *string { + return &oldObj.ActorUid + }) + errs = append(errs, fn(fldPath.Child("actor_uid"), &obj.ActorUid, oldVal, oldObj != nil)...) + } + + { // field ateletpb.MintActorCertificateRequest.CertificateSigningRequest + fn := func( + fldPath *field.Path, + obj, oldObj []byte, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if ateDeepEqual(obj, oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.RequiredSlice(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + if e := validate.MaxBytesSlice(ctx, op, fldPath, obj, oldObj, 16384); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.MintActorCertificateRequest) []byte { + return oldObj.CertificateSigningRequest + }) + errs = append(errs, fn(fldPath.Child("certificate_signing_request"), obj.CertificateSigningRequest, oldVal, oldObj != nil)...) + } + + return errs +} + +// Validate_RegisterWorkerRequest validates an instance of RegisterWorkerRequest according +// to declarative validation rules in the API schema. +func Validate_RegisterWorkerRequest( + ctx context.Context, op operation.Operation, fldPath *field.Path, + obj, oldObj *ateletpb.RegisterWorkerRequest) (errs field.ErrorList) { + + { // field ateletpb.RegisterWorkerRequest.Capacity + fn := func( + fldPath *field.Path, + obj, oldObj *ateletpb.WorkerResources, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if ateDeepEqual(obj, oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.RequiredPointer(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + // call the type's validation function + errs = append(errs, Validate_WorkerResources(ctx, op, fldPath, obj, oldObj)...) + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.RegisterWorkerRequest) *ateletpb.WorkerResources { + return oldObj.Capacity + }) + errs = append(errs, fn(fldPath.Child("capacity"), obj.Capacity, oldVal, oldObj != nil)...) + } + + { // field ateletpb.RegisterWorkerRequest.DefaultRuntime + fn := func( + fldPath *field.Path, + obj, oldObj *ateletpb.SandboxRuntime, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if ateDeepEqual(obj, oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.RequiredPointer(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + // call the type's validation function + errs = append(errs, Validate_SandboxRuntime(ctx, op, fldPath, obj, oldObj)...) + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.RegisterWorkerRequest) *ateletpb.SandboxRuntime { + return oldObj.DefaultRuntime + }) + errs = append(errs, fn(fldPath.Child("default_runtime"), obj.DefaultRuntime, oldVal, oldObj != nil)...) + } + + { // field ateletpb.RegisterWorkerRequest.RestorableRuntimes + fn := func( + fldPath *field.Path, + obj, oldObj []*ateletpb.SandboxRuntime, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if ateDeepEqual(obj, oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.PtrSliceNoNils[ateletpb.SandboxRuntime](ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if e := validate.MaxItems(ctx, op, fldPath, obj, oldObj, 32).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if e := validate.OptionalSlice(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + // iterate the list and call the type's validation function + if e := validate.EachPtrSliceVal(ctx, op, fldPath, obj, oldObj, nil, nil, Validate_SandboxRuntime); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.RegisterWorkerRequest) []*ateletpb.SandboxRuntime { + return oldObj.RestorableRuntimes + }) + errs = append(errs, fn(fldPath.Child("restorable_runtimes"), obj.RestorableRuntimes, oldVal, oldObj != nil)...) + } + + return errs +} + +// Validate_RequestActorSuspendRequest validates an instance of RequestActorSuspendRequest according +// to declarative validation rules in the API schema. +func Validate_RequestActorSuspendRequest( + ctx context.Context, op operation.Operation, fldPath *field.Path, + obj, oldObj *ateletpb.RequestActorSuspendRequest) (errs field.ErrorList) { + + { // field ateletpb.RequestActorSuspendRequest.ActorAtespace + fn := func( + fldPath *field.Path, + obj, oldObj *string, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.RequiredValue(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + if e := validate.ShortName(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.RequestActorSuspendRequest) *string { + return &oldObj.ActorAtespace + }) + errs = append(errs, fn(fldPath.Child("actor_atespace"), &obj.ActorAtespace, oldVal, oldObj != nil)...) + } + + { // field ateletpb.RequestActorSuspendRequest.ActorName + fn := func( + fldPath *field.Path, + obj, oldObj *string, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.RequiredValue(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + if e := validate.ShortName(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.RequestActorSuspendRequest) *string { + return &oldObj.ActorName + }) + errs = append(errs, fn(fldPath.Child("actor_name"), &obj.ActorName, oldVal, oldObj != nil)...) + } + + { // field ateletpb.RequestActorSuspendRequest.ActorUid + fn := func( + fldPath *field.Path, + obj, oldObj *string, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.RequiredValue(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + if e := validate.UUID(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.RequestActorSuspendRequest) *string { + return &oldObj.ActorUid + }) + errs = append(errs, fn(fldPath.Child("actor_uid"), &obj.ActorUid, oldVal, oldObj != nil)...) + } + + return errs +} + +// Validate_ResourceLimits validates an instance of ResourceLimits according +// to declarative validation rules in the API schema. +func Validate_ResourceLimits( + ctx context.Context, op operation.Operation, fldPath *field.Path, + obj, oldObj *ateletpb.ResourceLimits) (errs field.ErrorList) { + + { // field ateletpb.ResourceLimits.MemoryBytes + fn := func( + fldPath *field.Path, + obj, oldObj *int64, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.OptionalValue(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + if e := validate.Minimum(ctx, op, fldPath, obj, oldObj, 0); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.ResourceLimits) *int64 { + return &oldObj.MemoryBytes + }) + errs = append(errs, fn(fldPath.Child("memory_bytes"), &obj.MemoryBytes, oldVal, oldObj != nil)...) + } + + { // field ateletpb.ResourceLimits.CpuMillis + fn := func( + fldPath *field.Path, + obj, oldObj *int64, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.OptionalValue(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + if e := validate.Maximum(ctx, op, fldPath, obj, oldObj, 999999); len(e) != 0 { + errs = append(errs, e...) + } + if e := validate.Minimum(ctx, op, fldPath, obj, oldObj, 0); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.ResourceLimits) *int64 { + return &oldObj.CpuMillis + }) + errs = append(errs, fn(fldPath.Child("cpu_millis"), &obj.CpuMillis, oldVal, oldObj != nil)...) + } + + return errs +} + +// Validate_Resources validates an instance of Resources according +// to declarative validation rules in the API schema. +func Validate_Resources( + ctx context.Context, op operation.Operation, fldPath *field.Path, + obj, oldObj *ateletpb.Resources) (errs field.ErrorList) { + + { // field ateletpb.Resources.Limits + fn := func( + fldPath *field.Path, + obj, oldObj []*ateletpb.Limits, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if ateDeepEqual(obj, oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.PtrSliceNoNils[ateletpb.Limits](ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if e := validate.MaxItems(ctx, op, fldPath, obj, oldObj, 2).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if e := validate.OptionalSlice(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + // lists with map semantics require unique keys + if e := validate.PtrSliceUnique(ctx, op, fldPath, obj, oldObj, + func(a *ateletpb.Limits, b *ateletpb.Limits) bool { return a.Name == b.Name }); len(e) != 0 { + errs = append(errs, e...) + } + // iterate the list and call the type's validation function + if e := validate.EachPtrSliceVal(ctx, op, fldPath, obj, oldObj, + func(a *ateletpb.Limits, b *ateletpb.Limits) bool { return a.Name == b.Name }, ateDeepEqual, Validate_Limits); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.Resources) []*ateletpb.Limits { + return oldObj.Limits + }) + errs = append(errs, fn(fldPath.Child("limits"), obj.Limits, oldVal, oldObj != nil)...) + } + + return errs +} + +var unionMembershipFor_github_com_agent_substrate_substrate_internal_proto_ateletpb_RestoreRequest_ = validate.NewUnionMembership(validate.NewUnionMember("local_config"), validate.NewUnionMember("external_config")) + +// Validate_RestoreRequest validates an instance of RestoreRequest according +// to declarative validation rules in the API schema. +func Validate_RestoreRequest( + ctx context.Context, op operation.Operation, fldPath *field.Path, + obj, oldObj *ateletpb.RestoreRequest) (errs field.ErrorList) { + + if e := validate.Union(ctx, op, fldPath, obj, oldObj, unionMembershipFor_github_com_agent_substrate_substrate_internal_proto_ateletpb_RestoreRequest_, + func(obj *ateletpb.RestoreRequest) bool { + if obj == nil { + return false + } + return obj.LocalConfig != nil + }, + func(obj *ateletpb.RestoreRequest) bool { + if obj == nil { + return false + } + return obj.ExternalConfig != nil + }); len(e) != 0 { + errs = append(errs, e...) + } + // custom validation + if e := ValidateCustom_RestoreRequest(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + errs = append(errs, e...) + } + + { // field ateletpb.RestoreRequest.WorkerPodUid + fn := func( + fldPath *field.Path, + obj, oldObj *string, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.RequiredValue(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + if e := validate.UUID(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.RestoreRequest) *string { + return &oldObj.WorkerPodUid + }) + errs = append(errs, fn(fldPath.Child("worker_pod_uid"), &obj.WorkerPodUid, oldVal, oldObj != nil)...) + } + + { // field ateletpb.RestoreRequest.Atespace + fn := func( + fldPath *field.Path, + obj, oldObj *string, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.RequiredValue(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + if e := validate.ShortName(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.RestoreRequest) *string { + return &oldObj.Atespace + }) + errs = append(errs, fn(fldPath.Child("atespace"), &obj.Atespace, oldVal, oldObj != nil)...) + } + + { // field ateletpb.RestoreRequest.ActorName + fn := func( + fldPath *field.Path, + obj, oldObj *string, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.RequiredValue(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + if e := validate.ShortName(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.RestoreRequest) *string { + return &oldObj.ActorName + }) + errs = append(errs, fn(fldPath.Child("actor_name"), &obj.ActorName, oldVal, oldObj != nil)...) + } + + { // field ateletpb.RestoreRequest.ActorUid + fn := func( + fldPath *field.Path, + obj, oldObj *string, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.RequiredValue(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + if e := validate.UUID(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.RestoreRequest) *string { + return &oldObj.ActorUid + }) + errs = append(errs, fn(fldPath.Child("actor_uid"), &obj.ActorUid, oldVal, oldObj != nil)...) + } + + { // field ateletpb.RestoreRequest.ActorTemplateAtespace + fn := func( + fldPath *field.Path, + obj, oldObj *string, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.OptionalValue(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + if e := validate.ShortName(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.RestoreRequest) *string { + return &oldObj.ActorTemplateAtespace + }) + errs = append(errs, fn(fldPath.Child("actor_template_atespace"), &obj.ActorTemplateAtespace, oldVal, oldObj != nil)...) + } + + { // field ateletpb.RestoreRequest.ActorTemplateName + fn := func( + fldPath *field.Path, + obj, oldObj *string, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.OptionalValue(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + if e := validate.ShortName(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.RestoreRequest) *string { + return &oldObj.ActorTemplateName + }) + errs = append(errs, fn(fldPath.Child("actor_template_name"), &obj.ActorTemplateName, oldVal, oldObj != nil)...) + } + + { // field ateletpb.RestoreRequest.Spec + fn := func( + fldPath *field.Path, + obj, oldObj *ateletpb.WorkloadSpec, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if ateDeepEqual(obj, oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.RequiredPointer(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + // call the type's validation function + errs = append(errs, Validate_WorkloadSpec(ctx, op, fldPath, obj, oldObj)...) + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.RestoreRequest) *ateletpb.WorkloadSpec { + return oldObj.Spec + }) + errs = append(errs, fn(fldPath.Child("spec"), obj.Spec, oldVal, oldObj != nil)...) + } + + { // field ateletpb.RestoreRequest.Type + fn := func( + fldPath *field.Path, + obj, oldObj *ateletpb.CheckpointType, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.RequiredValue(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + if e := validate.Maximum(ctx, op, fldPath, obj, oldObj, 2); len(e) != 0 { + errs = append(errs, e...) + } + if e := validate.Minimum(ctx, op, fldPath, obj, oldObj, 1); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.RestoreRequest) *ateletpb.CheckpointType { + return &oldObj.Type + }) + errs = append(errs, fn(fldPath.Child("type"), &obj.Type, oldVal, oldObj != nil)...) + } + + { // field ateletpb.RestoreRequest.LocalConfig + fn := func( + fldPath *field.Path, + obj, oldObj *ateletpb.LocalCheckpointConfiguration, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if ateDeepEqual(obj, oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.OptionalPointer(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + // call the type's validation function + errs = append(errs, Validate_LocalCheckpointConfiguration(ctx, op, fldPath, obj, oldObj)...) + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.RestoreRequest) *ateletpb.LocalCheckpointConfiguration { + return oldObj.LocalConfig + }) + errs = append(errs, fn(fldPath.Child("local_config"), obj.LocalConfig, oldVal, oldObj != nil)...) + } + + { // field ateletpb.RestoreRequest.ExternalConfig + fn := func( + fldPath *field.Path, + obj, oldObj *ateletpb.ExternalRestoreConfiguration, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if ateDeepEqual(obj, oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.OptionalPointer(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + // call the type's validation function + errs = append(errs, Validate_ExternalRestoreConfiguration(ctx, op, fldPath, obj, oldObj)...) + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.RestoreRequest) *ateletpb.ExternalRestoreConfiguration { + return oldObj.ExternalConfig + }) + errs = append(errs, fn(fldPath.Child("external_config"), obj.ExternalConfig, oldVal, oldObj != nil)...) + } + + { // field ateletpb.RestoreRequest.Fidelity + fn := func( + fldPath *field.Path, + obj, oldObj *ateletpb.SnapshotFidelity, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.RequiredValue(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + if e := validate.Maximum(ctx, op, fldPath, obj, oldObj, 3); len(e) != 0 { + errs = append(errs, e...) + } + if e := validate.Minimum(ctx, op, fldPath, obj, oldObj, 1); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.RestoreRequest) *ateletpb.SnapshotFidelity { + return &oldObj.Fidelity + }) + errs = append(errs, fn(fldPath.Child("fidelity"), &obj.Fidelity, oldVal, oldObj != nil)...) + } + + { // field ateletpb.RestoreRequest.EgressGateway + fn := func( + fldPath *field.Path, + obj, oldObj *ateletpb.EgressGateway, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if ateDeepEqual(obj, oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.OptionalPointer(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + // call the type's validation function + errs = append(errs, Validate_EgressGateway(ctx, op, fldPath, obj, oldObj)...) + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.RestoreRequest) *ateletpb.EgressGateway { + return oldObj.EgressGateway + }) + errs = append(errs, fn(fldPath.Child("egress_gateway"), obj.EgressGateway, oldVal, oldObj != nil)...) + } + + { // field ateletpb.RestoreRequest.CpuMilli + fn := func( + fldPath *field.Path, + obj, oldObj *int64, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.OptionalValue(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + if e := validate.Maximum(ctx, op, fldPath, obj, oldObj, 999999); len(e) != 0 { + errs = append(errs, e...) + } + if e := validate.Minimum(ctx, op, fldPath, obj, oldObj, 0); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.RestoreRequest) *int64 { + return &oldObj.CpuMilli + }) + errs = append(errs, fn(fldPath.Child("cpu_milli"), &obj.CpuMilli, oldVal, oldObj != nil)...) + } + + { // field ateletpb.RestoreRequest.MemoryBytes + fn := func( + fldPath *field.Path, + obj, oldObj *int64, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.OptionalValue(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + if e := validate.Minimum(ctx, op, fldPath, obj, oldObj, 0); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.RestoreRequest) *int64 { + return &oldObj.MemoryBytes + }) + errs = append(errs, fn(fldPath.Child("memory_bytes"), &obj.MemoryBytes, oldVal, oldObj != nil)...) + } + + { // field ateletpb.RestoreRequest.SandboxAssets + fn := func( + fldPath *field.Path, + obj, oldObj *ateletpb.SandboxAssets, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if ateDeepEqual(obj, oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.RequiredPointer(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + // call the type's validation function + errs = append(errs, Validate_SandboxAssets(ctx, op, fldPath, obj, oldObj)...) + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.RestoreRequest) *ateletpb.SandboxAssets { + return oldObj.SandboxAssets + }) + errs = append(errs, fn(fldPath.Child("sandbox_assets"), obj.SandboxAssets, oldVal, oldObj != nil)...) + } + + return errs +} + +// Validate_RunRequest validates an instance of RunRequest according +// to declarative validation rules in the API schema. +func Validate_RunRequest( + ctx context.Context, op operation.Operation, fldPath *field.Path, + obj, oldObj *ateletpb.RunRequest) (errs field.ErrorList) { + + { // field ateletpb.RunRequest.WorkerPodUid + fn := func( + fldPath *field.Path, + obj, oldObj *string, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.RequiredValue(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + if e := validate.UUID(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.RunRequest) *string { + return &oldObj.WorkerPodUid + }) + errs = append(errs, fn(fldPath.Child("worker_pod_uid"), &obj.WorkerPodUid, oldVal, oldObj != nil)...) + } + + { // field ateletpb.RunRequest.Atespace + fn := func( + fldPath *field.Path, + obj, oldObj *string, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.RequiredValue(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + if e := validate.ShortName(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.RunRequest) *string { + return &oldObj.Atespace + }) + errs = append(errs, fn(fldPath.Child("atespace"), &obj.Atespace, oldVal, oldObj != nil)...) + } + + { // field ateletpb.RunRequest.ActorName + fn := func( + fldPath *field.Path, + obj, oldObj *string, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.RequiredValue(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + if e := validate.ShortName(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.RunRequest) *string { + return &oldObj.ActorName + }) + errs = append(errs, fn(fldPath.Child("actor_name"), &obj.ActorName, oldVal, oldObj != nil)...) + } + + { // field ateletpb.RunRequest.ActorUid + fn := func( + fldPath *field.Path, + obj, oldObj *string, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.RequiredValue(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + if e := validate.UUID(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.RunRequest) *string { + return &oldObj.ActorUid + }) + errs = append(errs, fn(fldPath.Child("actor_uid"), &obj.ActorUid, oldVal, oldObj != nil)...) + } + + { // field ateletpb.RunRequest.ActorTemplateAtespace + fn := func( + fldPath *field.Path, + obj, oldObj *string, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.OptionalValue(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + if e := validate.ShortName(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.RunRequest) *string { + return &oldObj.ActorTemplateAtespace + }) + errs = append(errs, fn(fldPath.Child("actor_template_atespace"), &obj.ActorTemplateAtespace, oldVal, oldObj != nil)...) + } + + { // field ateletpb.RunRequest.ActorTemplateName + fn := func( + fldPath *field.Path, + obj, oldObj *string, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.OptionalValue(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + if e := validate.ShortName(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.RunRequest) *string { + return &oldObj.ActorTemplateName + }) + errs = append(errs, fn(fldPath.Child("actor_template_name"), &obj.ActorTemplateName, oldVal, oldObj != nil)...) + } + + { // field ateletpb.RunRequest.Spec + fn := func( + fldPath *field.Path, + obj, oldObj *ateletpb.WorkloadSpec, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if ateDeepEqual(obj, oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.RequiredPointer(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + // call the type's validation function + errs = append(errs, Validate_WorkloadSpec(ctx, op, fldPath, obj, oldObj)...) + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.RunRequest) *ateletpb.WorkloadSpec { + return oldObj.Spec + }) + errs = append(errs, fn(fldPath.Child("spec"), obj.Spec, oldVal, oldObj != nil)...) + } + + { // field ateletpb.RunRequest.SandboxAssets + fn := func( + fldPath *field.Path, + obj, oldObj *ateletpb.SandboxAssets, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if ateDeepEqual(obj, oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.RequiredPointer(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + // call the type's validation function + errs = append(errs, Validate_SandboxAssets(ctx, op, fldPath, obj, oldObj)...) + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.RunRequest) *ateletpb.SandboxAssets { + return oldObj.SandboxAssets + }) + errs = append(errs, fn(fldPath.Child("sandbox_assets"), obj.SandboxAssets, oldVal, oldObj != nil)...) + } + + { // field ateletpb.RunRequest.EgressGateway + fn := func( + fldPath *field.Path, + obj, oldObj *ateletpb.EgressGateway, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if ateDeepEqual(obj, oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.OptionalPointer(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + // call the type's validation function + errs = append(errs, Validate_EgressGateway(ctx, op, fldPath, obj, oldObj)...) + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.RunRequest) *ateletpb.EgressGateway { + return oldObj.EgressGateway + }) + errs = append(errs, fn(fldPath.Child("egress_gateway"), obj.EgressGateway, oldVal, oldObj != nil)...) + } + + { // field ateletpb.RunRequest.CpuMilli + fn := func( + fldPath *field.Path, + obj, oldObj *int64, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.OptionalValue(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + if e := validate.Maximum(ctx, op, fldPath, obj, oldObj, 999999); len(e) != 0 { + errs = append(errs, e...) + } + if e := validate.Minimum(ctx, op, fldPath, obj, oldObj, 0); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.RunRequest) *int64 { + return &oldObj.CpuMilli + }) + errs = append(errs, fn(fldPath.Child("cpu_milli"), &obj.CpuMilli, oldVal, oldObj != nil)...) + } + + { // field ateletpb.RunRequest.MemoryBytes + fn := func( + fldPath *field.Path, + obj, oldObj *int64, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.OptionalValue(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + if e := validate.Minimum(ctx, op, fldPath, obj, oldObj, 0); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.RunRequest) *int64 { + return &oldObj.MemoryBytes + }) + errs = append(errs, fn(fldPath.Child("memory_bytes"), &obj.MemoryBytes, oldVal, oldObj != nil)...) + } + + return errs +} + +// Validate_SandboxAssets validates an instance of SandboxAssets according +// to declarative validation rules in the API schema. +func Validate_SandboxAssets( + ctx context.Context, op operation.Operation, fldPath *field.Path, + obj, oldObj *ateletpb.SandboxAssets) (errs field.ErrorList) { + + { // field ateletpb.SandboxAssets.SandboxClass + fn := func( + fldPath *field.Path, + obj, oldObj *string, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.RequiredValue(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + // custom validation + if e := ValidateCustom_SandboxAssets_SandboxClass(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + errs = append(errs, e...) + } + if e := validate.MaxLength(ctx, op, fldPath, obj, oldObj, 63); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.SandboxAssets) *string { + return &oldObj.SandboxClass + }) + errs = append(errs, fn(fldPath.Child("sandbox_class"), &obj.SandboxClass, oldVal, oldObj != nil)...) + } + + { // field ateletpb.SandboxAssets.Assets + fn := func( + fldPath *field.Path, + obj, oldObj map[string]*ateletpb.ArchAssets, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if ateDeepEqual(obj, oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.PtrMapNoNils[string, ateletpb.ArchAssets](ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if e := validate.MaxProperties(ctx, op, fldPath, obj, oldObj, 8).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if e := validate.RequiredMap(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + if e := validate.EachMapKey(ctx, op, fldPath, obj, oldObj, + func(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *string) field.ErrorList { + return validate.MaxLength(ctx, op, fldPath, obj, oldObj, 16) + }); len(e) != 0 { + errs = append(errs, e...) + } + // iterate the map and call the value type's validation function + if e := validate.EachPtrMapVal(ctx, op, fldPath, obj, oldObj, ateDeepEqual, Validate_ArchAssets); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.SandboxAssets) map[string]*ateletpb.ArchAssets { + return oldObj.Assets + }) + errs = append(errs, fn(fldPath.Child("assets"), obj.Assets, oldVal, oldObj != nil)...) + } + + { // field ateletpb.SandboxAssets.PauseImage + fn := func( + fldPath *field.Path, + obj, oldObj *string, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.OptionalValue(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + // custom validation + if e := ValidateCustom_SandboxAssets_PauseImage(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + errs = append(errs, e...) + } + if e := validate.MaxLength(ctx, op, fldPath, obj, oldObj, 512); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.SandboxAssets) *string { + return &oldObj.PauseImage + }) + errs = append(errs, fn(fldPath.Child("pause_image"), &obj.PauseImage, oldVal, oldObj != nil)...) + } + + return errs +} + +// Validate_SandboxRuntime validates an instance of SandboxRuntime according +// to declarative validation rules in the API schema. +func Validate_SandboxRuntime( + ctx context.Context, op operation.Operation, fldPath *field.Path, + obj, oldObj *ateletpb.SandboxRuntime) (errs field.ErrorList) { + + { // field ateletpb.SandboxRuntime.SandboxClass + fn := func( + fldPath *field.Path, + obj, oldObj *string, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.RequiredValue(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + if e := validate.MaxLength(ctx, op, fldPath, obj, oldObj, 63); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.SandboxRuntime) *string { + return &oldObj.SandboxClass + }) + errs = append(errs, fn(fldPath.Child("sandbox_class"), &obj.SandboxClass, oldVal, oldObj != nil)...) + } + + { // field ateletpb.SandboxRuntime.Version + fn := func( + fldPath *field.Path, + obj, oldObj *ateletpb.VersionedSandboxCompat, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if ateDeepEqual(obj, oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.RequiredPointer(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + // call the type's validation function + errs = append(errs, Validate_VersionedSandboxCompat(ctx, op, fldPath, obj, oldObj)...) + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.SandboxRuntime) *ateletpb.VersionedSandboxCompat { + return oldObj.Version + }) + errs = append(errs, fn(fldPath.Child("version"), obj.Version, oldVal, oldObj != nil)...) + } + + return errs +} + +// Validate_SecurityContext validates an instance of SecurityContext according +// to declarative validation rules in the API schema. +func Validate_SecurityContext( + ctx context.Context, op operation.Operation, fldPath *field.Path, + obj, oldObj *ateletpb.SecurityContext) (errs field.ErrorList) { + + { // field ateletpb.SecurityContext.Capabilities + fn := func( + fldPath *field.Path, + obj, oldObj *ateletpb.Capabilities, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if ateDeepEqual(obj, oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.OptionalPointer(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + // call the type's validation function + errs = append(errs, Validate_Capabilities(ctx, op, fldPath, obj, oldObj)...) + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.SecurityContext) *ateletpb.Capabilities { + return oldObj.Capabilities + }) + errs = append(errs, fn(fldPath.Child("capabilities"), obj.Capabilities, oldVal, oldObj != nil)...) + } + + return errs +} + +var unionMembershipFor_github_com_agent_substrate_substrate_internal_proto_ateletpb_SystemInfoDataSource_ = validate.NewUnionMembership(validate.NewUnionMember("actor_metadata"), validate.NewUnionMember("trust_bundle")) + +// Validate_SystemInfoDataSource validates an instance of SystemInfoDataSource according +// to declarative validation rules in the API schema. +func Validate_SystemInfoDataSource( + ctx context.Context, op operation.Operation, fldPath *field.Path, + obj, oldObj *ateletpb.SystemInfoDataSource) (errs field.ErrorList) { + + if e := validate.Union(ctx, op, fldPath, obj, oldObj, unionMembershipFor_github_com_agent_substrate_substrate_internal_proto_ateletpb_SystemInfoDataSource_, + func(obj *ateletpb.SystemInfoDataSource) bool { + if obj == nil { + return false + } + return obj.ActorMetadata != nil + }, + func(obj *ateletpb.SystemInfoDataSource) bool { + if obj == nil { + return false + } + return obj.TrustBundle != nil + }); len(e) != 0 { + errs = append(errs, e...) + } + + { // field ateletpb.SystemInfoDataSource.ActorMetadata + fn := func( + fldPath *field.Path, + obj, oldObj *ateletpb.ActorMetadataDataSource, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if ateDeepEqual(obj, oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.OptionalPointer(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + // call the type's validation function + errs = append(errs, Validate_ActorMetadataDataSource(ctx, op, fldPath, obj, oldObj)...) + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.SystemInfoDataSource) *ateletpb.ActorMetadataDataSource { + return oldObj.ActorMetadata + }) + errs = append(errs, fn(fldPath.Child("actor_metadata"), obj.ActorMetadata, oldVal, oldObj != nil)...) + } + + { // field ateletpb.SystemInfoDataSource.TrustBundle + fn := func( + fldPath *field.Path, + obj, oldObj *ateletpb.TrustBundleDataSource, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if ateDeepEqual(obj, oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.OptionalPointer(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + // call the type's validation function + errs = append(errs, Validate_TrustBundleDataSource(ctx, op, fldPath, obj, oldObj)...) + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.SystemInfoDataSource) *ateletpb.TrustBundleDataSource { + return oldObj.TrustBundle + }) + errs = append(errs, fn(fldPath.Child("trust_bundle"), obj.TrustBundle, oldVal, oldObj != nil)...) + } + + return errs +} + +// Validate_SystemInfoVolume validates an instance of SystemInfoVolume according +// to declarative validation rules in the API schema. +func Validate_SystemInfoVolume( + ctx context.Context, op operation.Operation, fldPath *field.Path, + obj, oldObj *ateletpb.SystemInfoVolume) (errs field.ErrorList) { + + { // field ateletpb.SystemInfoVolume.DataSources + fn := func( + fldPath *field.Path, + obj, oldObj []*ateletpb.SystemInfoDataSource, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if ateDeepEqual(obj, oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.PtrSliceNoNils[ateletpb.SystemInfoDataSource](ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if e := validate.MaxItems(ctx, op, fldPath, obj, oldObj, 8).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if e := validate.OptionalSlice(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + // custom validation + if e := ValidateCustom_SystemInfoVolume_DataSources(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + errs = append(errs, e...) + } + // iterate the list and call the type's validation function + if e := validate.EachPtrSliceVal(ctx, op, fldPath, obj, oldObj, nil, nil, Validate_SystemInfoDataSource); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.SystemInfoVolume) []*ateletpb.SystemInfoDataSource { + return oldObj.DataSources + }) + errs = append(errs, fn(fldPath.Child("data_sources"), obj.DataSources, oldVal, oldObj != nil)...) + } + + return errs +} + +// Validate_TerminateRequest validates an instance of TerminateRequest according +// to declarative validation rules in the API schema. +func Validate_TerminateRequest( + ctx context.Context, op operation.Operation, fldPath *field.Path, + obj, oldObj *ateletpb.TerminateRequest) (errs field.ErrorList) { + + { // field ateletpb.TerminateRequest.WorkerPodUid + fn := func( + fldPath *field.Path, + obj, oldObj *string, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.OptionalValue(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + if e := validate.UUID(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.TerminateRequest) *string { + return &oldObj.WorkerPodUid + }) + errs = append(errs, fn(fldPath.Child("worker_pod_uid"), &obj.WorkerPodUid, oldVal, oldObj != nil)...) + } + + { // field ateletpb.TerminateRequest.Atespace + fn := func( + fldPath *field.Path, + obj, oldObj *string, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.RequiredValue(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + if e := validate.ShortName(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.TerminateRequest) *string { + return &oldObj.Atespace + }) + errs = append(errs, fn(fldPath.Child("atespace"), &obj.Atespace, oldVal, oldObj != nil)...) + } + + { // field ateletpb.TerminateRequest.ActorName + fn := func( + fldPath *field.Path, + obj, oldObj *string, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.RequiredValue(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + if e := validate.ShortName(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.TerminateRequest) *string { + return &oldObj.ActorName + }) + errs = append(errs, fn(fldPath.Child("actor_name"), &obj.ActorName, oldVal, oldObj != nil)...) + } + + { // field ateletpb.TerminateRequest.ActorUid + fn := func( + fldPath *field.Path, + obj, oldObj *string, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.RequiredValue(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + if e := validate.UUID(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.TerminateRequest) *string { + return &oldObj.ActorUid + }) + errs = append(errs, fn(fldPath.Child("actor_uid"), &obj.ActorUid, oldVal, oldObj != nil)...) + } + + { // field ateletpb.TerminateRequest.ActorTemplateAtespace + fn := func( + fldPath *field.Path, + obj, oldObj *string, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.OptionalValue(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + if e := validate.ShortName(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.TerminateRequest) *string { + return &oldObj.ActorTemplateAtespace + }) + errs = append(errs, fn(fldPath.Child("actor_template_atespace"), &obj.ActorTemplateAtespace, oldVal, oldObj != nil)...) + } + + { // field ateletpb.TerminateRequest.ActorTemplateName + fn := func( + fldPath *field.Path, + obj, oldObj *string, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.OptionalValue(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + if e := validate.ShortName(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.TerminateRequest) *string { + return &oldObj.ActorTemplateName + }) + errs = append(errs, fn(fldPath.Child("actor_template_name"), &obj.ActorTemplateName, oldVal, oldObj != nil)...) + } + + { // field ateletpb.TerminateRequest.Spec + fn := func( + fldPath *field.Path, + obj, oldObj *ateletpb.WorkloadSpec, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if ateDeepEqual(obj, oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.OptionalPointer(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + // call the type's validation function + errs = append(errs, Validate_WorkloadSpec(ctx, op, fldPath, obj, oldObj)...) + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.TerminateRequest) *ateletpb.WorkloadSpec { + return oldObj.Spec + }) + errs = append(errs, fn(fldPath.Child("spec"), obj.Spec, oldVal, oldObj != nil)...) + } + + return errs +} + +// Validate_TrustBundleDataSource validates an instance of TrustBundleDataSource according +// to declarative validation rules in the API schema. +func Validate_TrustBundleDataSource( + ctx context.Context, op operation.Operation, fldPath *field.Path, + obj, oldObj *ateletpb.TrustBundleDataSource) (errs field.ErrorList) { + + { // field ateletpb.TrustBundleDataSource.Path + fn := func( + fldPath *field.Path, + obj, oldObj *string, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.RequiredValue(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + // custom validation + if e := ValidateCustom_TrustBundleDataSource_Path(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + errs = append(errs, e...) + } + if e := validate.MaxLength(ctx, op, fldPath, obj, oldObj, 255); len(e) != 0 { + errs = append(errs, e...) + } + if e := validate.MinLength(ctx, op, fldPath, obj, oldObj, 1); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.TrustBundleDataSource) *string { + return &oldObj.Path + }) + errs = append(errs, fn(fldPath.Child("path"), &obj.Path, oldVal, oldObj != nil)...) + } + + { // field ateletpb.TrustBundleDataSource.Names + fn := func( + fldPath *field.Path, + obj, oldObj []string, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if ateDeepEqual(obj, oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.MaxItems(ctx, op, fldPath, obj, oldObj, 8).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if e := validate.RequiredSlice(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + if e := validate.MinItems(ctx, op, fldPath, obj, oldObj, 1); len(e) != 0 { + errs = append(errs, e...) + } + if e := validate.EachValSliceVal(ctx, op, fldPath, obj, oldObj, validate.DirectEqual, nil, + func(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *string) field.ErrorList { + return validate.MinLength(ctx, op, fldPath, obj, oldObj, 1) + }); len(e) != 0 { + errs = append(errs, e...) + } + if e := validate.EachValSliceVal(ctx, op, fldPath, obj, oldObj, validate.DirectEqual, nil, + func(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *string) field.ErrorList { + return validate.MaxLength(ctx, op, fldPath, obj, oldObj, 253) + }); len(e) != 0 { + errs = append(errs, e...) + } + // lists with set semantics require unique values + if e := validate.ValSliceUnique(ctx, op, fldPath, obj, oldObj, validate.DirectEqual); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.TrustBundleDataSource) []string { + return oldObj.Names + }) + errs = append(errs, fn(fldPath.Child("names"), obj.Names, oldVal, oldObj != nil)...) + } + + return errs +} + +// Validate_UploadPausedCheckpointRequest validates an instance of UploadPausedCheckpointRequest according +// to declarative validation rules in the API schema. +func Validate_UploadPausedCheckpointRequest( + ctx context.Context, op operation.Operation, fldPath *field.Path, + obj, oldObj *ateletpb.UploadPausedCheckpointRequest) (errs field.ErrorList) { + + { // field ateletpb.UploadPausedCheckpointRequest.Atespace + fn := func( + fldPath *field.Path, + obj, oldObj *string, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.RequiredValue(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + // custom validation + if e := ValidateCustom_UploadPausedCheckpointRequest_Atespace(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + errs = append(errs, e...) + } + if e := validate.ShortName(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.UploadPausedCheckpointRequest) *string { + return &oldObj.Atespace + }) + errs = append(errs, fn(fldPath.Child("atespace"), &obj.Atespace, oldVal, oldObj != nil)...) + } + + { // field ateletpb.UploadPausedCheckpointRequest.ActorName + fn := func( + fldPath *field.Path, + obj, oldObj *string, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.RequiredValue(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + if e := validate.ShortName(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.UploadPausedCheckpointRequest) *string { + return &oldObj.ActorName + }) + errs = append(errs, fn(fldPath.Child("actor_name"), &obj.ActorName, oldVal, oldObj != nil)...) + } + + { // field ateletpb.UploadPausedCheckpointRequest.ActorUid + fn := func( + fldPath *field.Path, + obj, oldObj *string, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.RequiredValue(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + if e := validate.UUID(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.UploadPausedCheckpointRequest) *string { + return &oldObj.ActorUid + }) + errs = append(errs, fn(fldPath.Child("actor_uid"), &obj.ActorUid, oldVal, oldObj != nil)...) + } + + { // field ateletpb.UploadPausedCheckpointRequest.ActorTemplateAtespace + fn := func( + fldPath *field.Path, + obj, oldObj *string, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.OptionalValue(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + if e := validate.ShortName(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.UploadPausedCheckpointRequest) *string { + return &oldObj.ActorTemplateAtespace + }) + errs = append(errs, fn(fldPath.Child("actor_template_atespace"), &obj.ActorTemplateAtespace, oldVal, oldObj != nil)...) + } + + { // field ateletpb.UploadPausedCheckpointRequest.ActorTemplateName + fn := func( + fldPath *field.Path, + obj, oldObj *string, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.OptionalValue(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + if e := validate.ShortName(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.UploadPausedCheckpointRequest) *string { + return &oldObj.ActorTemplateName + }) + errs = append(errs, fn(fldPath.Child("actor_template_name"), &obj.ActorTemplateName, oldVal, oldObj != nil)...) + } + + { // field ateletpb.UploadPausedCheckpointRequest.LocalSnapshotName + fn := func( + fldPath *field.Path, + obj, oldObj *string, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.RequiredValue(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + if e := validate.ShortName(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.UploadPausedCheckpointRequest) *string { + return &oldObj.LocalSnapshotName + }) + errs = append(errs, fn(fldPath.Child("local_snapshot_name"), &obj.LocalSnapshotName, oldVal, oldObj != nil)...) + } + + { // field ateletpb.UploadPausedCheckpointRequest.DestinationSnapshotUri + fn := func( + fldPath *field.Path, + obj, oldObj *string, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.RequiredValue(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + // custom validation + if e := ValidateCustom_UploadPausedCheckpointRequest_DestinationSnapshotUri(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + errs = append(errs, e...) + } + if e := validate.MaxLength(ctx, op, fldPath, obj, oldObj, 2048); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.UploadPausedCheckpointRequest) *string { + return &oldObj.DestinationSnapshotUri + }) + errs = append(errs, fn(fldPath.Child("destination_snapshot_uri"), &obj.DestinationSnapshotUri, oldVal, oldObj != nil)...) + } + + { // field ateletpb.UploadPausedCheckpointRequest.DesiredFidelity + fn := func( + fldPath *field.Path, + obj, oldObj *ateletpb.SnapshotFidelity, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.RequiredValue(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + if e := validate.Maximum(ctx, op, fldPath, obj, oldObj, 3); len(e) != 0 { + errs = append(errs, e...) + } + if e := validate.Minimum(ctx, op, fldPath, obj, oldObj, 1); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.UploadPausedCheckpointRequest) *ateletpb.SnapshotFidelity { + return &oldObj.DesiredFidelity + }) + errs = append(errs, fn(fldPath.Child("desired_fidelity"), &obj.DesiredFidelity, oldVal, oldObj != nil)...) + } + + return errs +} + +// Validate_VersionedSandboxCompat validates an instance of VersionedSandboxCompat according +// to declarative validation rules in the API schema. +func Validate_VersionedSandboxCompat( + ctx context.Context, op operation.Operation, fldPath *field.Path, + obj, oldObj *ateletpb.VersionedSandboxCompat) (errs field.ErrorList) { + + { // field ateletpb.VersionedSandboxCompat.SchemaVersion + fn := func( + fldPath *field.Path, + obj, oldObj *string, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.RequiredValue(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + if e := validate.MaxLength(ctx, op, fldPath, obj, oldObj, 64); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.VersionedSandboxCompat) *string { + return &oldObj.SchemaVersion + }) + errs = append(errs, fn(fldPath.Child("schema_version"), &obj.SchemaVersion, oldVal, oldObj != nil)...) + } + + { // field ateletpb.VersionedSandboxCompat.Attributes + fn := func( + fldPath *field.Path, + obj, oldObj []*ateletpb.AttributeEntry, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if ateDeepEqual(obj, oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.PtrSliceNoNils[ateletpb.AttributeEntry](ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if e := validate.MaxItems(ctx, op, fldPath, obj, oldObj, 32).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if e := validate.RequiredSlice(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + // lists with map semantics require unique keys + if e := validate.PtrSliceUnique(ctx, op, fldPath, obj, oldObj, + func(a *ateletpb.AttributeEntry, b *ateletpb.AttributeEntry) bool { return a.Key == b.Key }); len(e) != 0 { + errs = append(errs, e...) + } + // iterate the list and call the type's validation function + if e := validate.EachPtrSliceVal(ctx, op, fldPath, obj, oldObj, + func(a *ateletpb.AttributeEntry, b *ateletpb.AttributeEntry) bool { return a.Key == b.Key }, ateDeepEqual, Validate_AttributeEntry); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.VersionedSandboxCompat) []*ateletpb.AttributeEntry { + return oldObj.Attributes + }) + errs = append(errs, fn(fldPath.Child("attributes"), obj.Attributes, oldVal, oldObj != nil)...) + } + + return errs +} + +var unionMembershipFor_github_com_agent_substrate_substrate_internal_proto_ateletpb_Volume_ = validate.NewUnionMembership(validate.NewUnionMember("durable_dir"), validate.NewUnionMember("external"), validate.NewUnionMember("system_info"), validate.NewUnionMember("image")) + +// Validate_Volume validates an instance of Volume according +// to declarative validation rules in the API schema. +func Validate_Volume( + ctx context.Context, op operation.Operation, fldPath *field.Path, + obj, oldObj *ateletpb.Volume) (errs field.ErrorList) { + + if e := validate.Union(ctx, op, fldPath, obj, oldObj, unionMembershipFor_github_com_agent_substrate_substrate_internal_proto_ateletpb_Volume_, + func(obj *ateletpb.Volume) bool { + if obj == nil { + return false + } + return obj.DurableDir != nil + }, + func(obj *ateletpb.Volume) bool { + if obj == nil { + return false + } + return obj.External != nil + }, + func(obj *ateletpb.Volume) bool { + if obj == nil { + return false + } + return obj.SystemInfo != nil + }, + func(obj *ateletpb.Volume) bool { + if obj == nil { + return false + } + return obj.Image != nil + }); len(e) != 0 { + errs = append(errs, e...) + } + + { // field ateletpb.Volume.Name + fn := func( + fldPath *field.Path, + obj, oldObj *string, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.RequiredValue(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + if e := validate.ShortName(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + errs = append(errs, e...) } return } oldVal := safe.Field(oldObj, - func(oldObj *ateletpb.Resources) []*ateletpb.Limits { - return oldObj.Limits + func(oldObj *ateletpb.Volume) *string { + return &oldObj.Name }) - errs = append(errs, fn(fldPath.Child("limits"), obj.Limits, oldVal, oldObj != nil)...) + errs = append(errs, fn(fldPath.Child("name"), &obj.Name, oldVal, oldObj != nil)...) } - return errs -} - -// Validate_SandboxRuntime validates an instance of SandboxRuntime according -// to declarative validation rules in the API schema. -func Validate_SandboxRuntime( - ctx context.Context, op operation.Operation, fldPath *field.Path, - obj, oldObj *ateletpb.SandboxRuntime) (errs field.ErrorList) { + { // field ateletpb.Volume.DurableDir + fn := func( + fldPath *field.Path, + obj, oldObj *ateletpb.DurableDirVolume, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if ateDeepEqual(obj, oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.OptionalPointer(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.Volume) *ateletpb.DurableDirVolume { + return oldObj.DurableDir + }) + errs = append(errs, fn(fldPath.Child("durable_dir"), obj.DurableDir, oldVal, oldObj != nil)...) + } - { // field ateletpb.SandboxRuntime.SandboxClass + { // field ateletpb.Volume.External fn := func( fldPath *field.Path, - obj, oldObj *string, + obj, oldObj *ateletpb.ExternalVolumeSource, oldValueCorrelated bool) (errs field.ErrorList) { // don't revalidate unchanged data if oldValueCorrelated && op.Type == operation.Update { - if obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj) { + if ateDeepEqual(obj, oldObj) { return nil } } // call field-attached validations earlyReturn := false - if e := validate.RequiredValue(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { - errs = append(errs, e...) + if e := validate.OptionalPointer(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { earlyReturn = true } if earlyReturn { return // do not proceed } - if e := validate.MaxLength(ctx, op, fldPath, obj, oldObj, 63); len(e) != 0 { - errs = append(errs, e...) + // call the type's validation function + errs = append(errs, Validate_ExternalVolumeSource(ctx, op, fldPath, obj, oldObj)...) + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.Volume) *ateletpb.ExternalVolumeSource { + return oldObj.External + }) + errs = append(errs, fn(fldPath.Child("external"), obj.External, oldVal, oldObj != nil)...) + } + + { // field ateletpb.Volume.SystemInfo + fn := func( + fldPath *field.Path, + obj, oldObj *ateletpb.SystemInfoVolume, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if ateDeepEqual(obj, oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.OptionalPointer(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + earlyReturn = true + } + if earlyReturn { + return // do not proceed } + // call the type's validation function + errs = append(errs, Validate_SystemInfoVolume(ctx, op, fldPath, obj, oldObj)...) return } oldVal := safe.Field(oldObj, - func(oldObj *ateletpb.SandboxRuntime) *string { - return &oldObj.SandboxClass + func(oldObj *ateletpb.Volume) *ateletpb.SystemInfoVolume { + return oldObj.SystemInfo }) - errs = append(errs, fn(fldPath.Child("sandbox_class"), &obj.SandboxClass, oldVal, oldObj != nil)...) + errs = append(errs, fn(fldPath.Child("system_info"), obj.SystemInfo, oldVal, oldObj != nil)...) } - { // field ateletpb.SandboxRuntime.Version + { // field ateletpb.Volume.Image fn := func( fldPath *field.Path, - obj, oldObj *ateletpb.VersionedSandboxCompat, + obj, oldObj *ateletpb.ImageVolumeSource, oldValueCorrelated bool) (errs field.ErrorList) { // don't revalidate unchanged data if oldValueCorrelated && op.Type == operation.Update { @@ -639,34 +4369,33 @@ func Validate_SandboxRuntime( } // call field-attached validations earlyReturn := false - if e := validate.RequiredPointer(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { - errs = append(errs, e...) + if e := validate.OptionalPointer(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { earlyReturn = true } if earlyReturn { return // do not proceed } // call the type's validation function - errs = append(errs, Validate_VersionedSandboxCompat(ctx, op, fldPath, obj, oldObj)...) + errs = append(errs, Validate_ImageVolumeSource(ctx, op, fldPath, obj, oldObj)...) return } oldVal := safe.Field(oldObj, - func(oldObj *ateletpb.SandboxRuntime) *ateletpb.VersionedSandboxCompat { - return oldObj.Version + func(oldObj *ateletpb.Volume) *ateletpb.ImageVolumeSource { + return oldObj.Image }) - errs = append(errs, fn(fldPath.Child("version"), obj.Version, oldVal, oldObj != nil)...) + errs = append(errs, fn(fldPath.Child("image"), obj.Image, oldVal, oldObj != nil)...) } return errs } -// Validate_VersionedSandboxCompat validates an instance of VersionedSandboxCompat according +// Validate_VolumeMount validates an instance of VolumeMount according // to declarative validation rules in the API schema. -func Validate_VersionedSandboxCompat( +func Validate_VolumeMount( ctx context.Context, op operation.Operation, fldPath *field.Path, - obj, oldObj *ateletpb.VersionedSandboxCompat) (errs field.ErrorList) { + obj, oldObj *ateletpb.VolumeMount) (errs field.ErrorList) { - { // field ateletpb.VersionedSandboxCompat.SchemaVersion + { // field ateletpb.VolumeMount.Name fn := func( fldPath *field.Path, obj, oldObj *string, @@ -686,63 +4415,127 @@ func Validate_VersionedSandboxCompat( if earlyReturn { return // do not proceed } - if e := validate.MaxLength(ctx, op, fldPath, obj, oldObj, 64); len(e) != 0 { + if e := validate.ShortName(ctx, op, fldPath, obj, oldObj); len(e) != 0 { errs = append(errs, e...) } return } oldVal := safe.Field(oldObj, - func(oldObj *ateletpb.VersionedSandboxCompat) *string { - return &oldObj.SchemaVersion + func(oldObj *ateletpb.VolumeMount) *string { + return &oldObj.Name }) - errs = append(errs, fn(fldPath.Child("schema_version"), &obj.SchemaVersion, oldVal, oldObj != nil)...) + errs = append(errs, fn(fldPath.Child("name"), &obj.Name, oldVal, oldObj != nil)...) } - { // field ateletpb.VersionedSandboxCompat.Attributes + { // field ateletpb.VolumeMount.MountPath fn := func( fldPath *field.Path, - obj, oldObj []*ateletpb.AttributeEntry, + obj, oldObj *string, oldValueCorrelated bool) (errs field.ErrorList) { // don't revalidate unchanged data if oldValueCorrelated && op.Type == operation.Update { - if ateDeepEqual(obj, oldObj) { + if obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj) { return nil } } // call field-attached validations earlyReturn := false - if e := validate.PtrSliceNoNils[ateletpb.AttributeEntry](ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + if e := validate.RequiredValue(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { errs = append(errs, e...) earlyReturn = true } - if e := validate.MaxItems(ctx, op, fldPath, obj, oldObj, 32).MarkShortCircuit(); len(e) != 0 { + if earlyReturn { + return // do not proceed + } + // custom validation + if e := ValidateCustom_VolumeMount_MountPath(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + errs = append(errs, e...) + } + if e := validate.MaxLength(ctx, op, fldPath, obj, oldObj, 4096); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.VolumeMount) *string { + return &oldObj.MountPath + }) + errs = append(errs, fn(fldPath.Child("mount_path"), &obj.MountPath, oldVal, oldObj != nil)...) + } + + return errs +} + +// Validate_WakeupProbe validates an instance of WakeupProbe according +// to declarative validation rules in the API schema. +func Validate_WakeupProbe( + ctx context.Context, op operation.Operation, fldPath *field.Path, + obj, oldObj *ateletpb.WakeupProbe) (errs field.ErrorList) { + + { // field ateletpb.WakeupProbe.HttpGet + fn := func( + fldPath *field.Path, + obj, oldObj *ateletpb.HttpGetAction, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if ateDeepEqual(obj, oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.RequiredPointer(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { errs = append(errs, e...) earlyReturn = true } - if e := validate.RequiredSlice(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + if earlyReturn { + return // do not proceed + } + // call the type's validation function + errs = append(errs, Validate_HttpGetAction(ctx, op, fldPath, obj, oldObj)...) + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.WakeupProbe) *ateletpb.HttpGetAction { + return oldObj.HttpGet + }) + errs = append(errs, fn(fldPath.Child("http_get"), obj.HttpGet, oldVal, oldObj != nil)...) + } + + { // field ateletpb.WakeupProbe.TimeoutSeconds + fn := func( + fldPath *field.Path, + obj, oldObj *int32, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.RequiredValue(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { errs = append(errs, e...) earlyReturn = true } if earlyReturn { return // do not proceed } - // lists with map semantics require unique keys - if e := validate.PtrSliceUnique(ctx, op, fldPath, obj, oldObj, - func(a *ateletpb.AttributeEntry, b *ateletpb.AttributeEntry) bool { return a.Key == b.Key }); len(e) != 0 { + if e := validate.Maximum(ctx, op, fldPath, obj, oldObj, 3600); len(e) != 0 { errs = append(errs, e...) } - // iterate the list and call the type's validation function - if e := validate.EachPtrSliceVal(ctx, op, fldPath, obj, oldObj, - func(a *ateletpb.AttributeEntry, b *ateletpb.AttributeEntry) bool { return a.Key == b.Key }, ateDeepEqual, Validate_AttributeEntry); len(e) != 0 { + if e := validate.Minimum(ctx, op, fldPath, obj, oldObj, 1); len(e) != 0 { errs = append(errs, e...) } return } oldVal := safe.Field(oldObj, - func(oldObj *ateletpb.VersionedSandboxCompat) []*ateletpb.AttributeEntry { - return oldObj.Attributes + func(oldObj *ateletpb.WakeupProbe) *int32 { + return &oldObj.TimeoutSeconds }) - errs = append(errs, fn(fldPath.Child("attributes"), obj.Attributes, oldVal, oldObj != nil)...) + errs = append(errs, fn(fldPath.Child("timeout_seconds"), &obj.TimeoutSeconds, oldVal, oldObj != nil)...) } return errs @@ -817,3 +4610,109 @@ func Validate_WorkerResources( return errs } + +// Validate_WorkloadSpec validates an instance of WorkloadSpec according +// to declarative validation rules in the API schema. +func Validate_WorkloadSpec( + ctx context.Context, op operation.Operation, fldPath *field.Path, + obj, oldObj *ateletpb.WorkloadSpec) (errs field.ErrorList) { + + // custom validation + if e := ValidateCustom_WorkloadSpec(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + errs = append(errs, e...) + } + + { // field ateletpb.WorkloadSpec.Containers + fn := func( + fldPath *field.Path, + obj, oldObj []*ateletpb.Container, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if ateDeepEqual(obj, oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.PtrSliceNoNils[ateletpb.Container](ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if e := validate.MaxItems(ctx, op, fldPath, obj, oldObj, 10).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if e := validate.OptionalSlice(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + // lists with map semantics require unique keys + if e := validate.PtrSliceUnique(ctx, op, fldPath, obj, oldObj, + func(a *ateletpb.Container, b *ateletpb.Container) bool { return a.Name == b.Name }); len(e) != 0 { + errs = append(errs, e...) + } + // iterate the list and call the type's validation function + if e := validate.EachPtrSliceVal(ctx, op, fldPath, obj, oldObj, + func(a *ateletpb.Container, b *ateletpb.Container) bool { return a.Name == b.Name }, ateDeepEqual, Validate_Container); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.WorkloadSpec) []*ateletpb.Container { + return oldObj.Containers + }) + errs = append(errs, fn(fldPath.Child("containers"), obj.Containers, oldVal, oldObj != nil)...) + } + + { // field ateletpb.WorkloadSpec.Volumes + fn := func( + fldPath *field.Path, + obj, oldObj []*ateletpb.Volume, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if ateDeepEqual(obj, oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.PtrSliceNoNils[ateletpb.Volume](ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if e := validate.MaxItems(ctx, op, fldPath, obj, oldObj, 32).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if e := validate.OptionalSlice(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + // lists with map semantics require unique keys + if e := validate.PtrSliceUnique(ctx, op, fldPath, obj, oldObj, + func(a *ateletpb.Volume, b *ateletpb.Volume) bool { return a.Name == b.Name }); len(e) != 0 { + errs = append(errs, e...) + } + // iterate the list and call the type's validation function + if e := validate.EachPtrSliceVal(ctx, op, fldPath, obj, oldObj, + func(a *ateletpb.Volume, b *ateletpb.Volume) bool { return a.Name == b.Name }, ateDeepEqual, Validate_Volume); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateletpb.WorkloadSpec) []*ateletpb.Volume { + return oldObj.Volumes + }) + errs = append(errs, fn(fldPath.Child("volumes"), obj.Volumes, oldVal, oldObj != nil)...) + } + + return errs +} diff --git a/cmd/atelet/lifecycle_test.go b/cmd/atelet/lifecycle_test.go index c592bfb734..06fdb87a9a 100644 --- a/cmd/atelet/lifecycle_test.go +++ b/cmd/atelet/lifecycle_test.go @@ -172,8 +172,8 @@ func runLocalSnapshotGC(t *testing.T, fidelity ateletpb.SnapshotFidelity) { const ( atespace = "ate-demo" actorName = "counter" - actorUID = "actor-uid-1" - workerPodUID = "worker-pod-uid-1" + actorUID = "01234567-89ab-cdef-0123-456789abcdef" + workerPodUID = "5f1c0d2e-3a4b-4c5d-8e9f-0a1b2c3d4e5f" snapshotName = "pause-snap-1" ) @@ -182,7 +182,7 @@ func runLocalSnapshotGC(t *testing.T, fidelity ateletpb.SnapshotFidelity) { host := imageVolumeTestRegistry(t) image := host + "/actor:v1" - pushTestImage(t, image, singleFileLayer(t, "bin/app", "app")) + pinnedImage := pushTestImage(t, image, singleFileLayer(t, "bin/app", "app")) // A single "runsc" asset served from a fake bucket: enough to exercise the // content-addressed asset fetch without a gVisor release tarball. @@ -195,7 +195,7 @@ func runLocalSnapshotGC(t *testing.T, fidelity ateletpb.SnapshotFidelity) { } sandboxAssets := &ateletpb.SandboxAssets{ SandboxClass: "gvisor", - PauseImage: image, + PauseImage: pinnedImage, Assets: map[string]*ateletpb.ArchAssets{ runtime.GOARCH: {Files: map[string]*ateletpb.AssetFile{ runscAssetName: { @@ -206,7 +206,7 @@ func runLocalSnapshotGC(t *testing.T, fidelity ateletpb.SnapshotFidelity) { }, } spec := &ateletpb.WorkloadSpec{ - Containers: []*ateletpb.Container{{Name: "app", Image: image, Command: []string{"/bin/app"}}}, + Containers: []*ateletpb.Container{{Name: "app", Image: pinnedImage, Command: []string{"/bin/app"}}}, } if _, err := s.Run(ctx, &ateletpb.RunRequest{ @@ -233,9 +233,7 @@ func runLocalSnapshotGC(t *testing.T, fidelity ateletpb.SnapshotFidelity) { Spec: spec, Fidelity: fidelity, Type: ateletpb.CheckpointType_CHECKPOINT_TYPE_LOCAL, - Config: &ateletpb.CheckpointRequest_LocalConfig{ - LocalConfig: &ateletpb.LocalCheckpointConfiguration{SnapshotName: snapshotName}, - }, + LocalConfig: &ateletpb.LocalCheckpointConfiguration{SnapshotName: snapshotName}, }); err != nil { t.Fatalf("Checkpoint: %v", err) } @@ -256,9 +254,7 @@ func runLocalSnapshotGC(t *testing.T, fidelity ateletpb.SnapshotFidelity) { Spec: spec, Fidelity: fidelity, Type: ateletpb.CheckpointType_CHECKPOINT_TYPE_LOCAL, - Config: &ateletpb.RestoreRequest_LocalConfig{ - LocalConfig: &ateletpb.LocalCheckpointConfiguration{SnapshotName: snapshotName}, - }, + LocalConfig: &ateletpb.LocalCheckpointConfiguration{SnapshotName: snapshotName}, }); err != nil { t.Fatalf("Restore: %v", err) } @@ -333,8 +329,8 @@ func TestRestoreUsesRequestSandboxAssets(t *testing.T) { const ( atespace = "ate-demo" actorName = "counter" - actorUID = "actor-uid-1" - workerPodUID = "worker-pod-uid-1" + actorUID = "01234567-89ab-cdef-0123-456789abcdef" + workerPodUID = "5f1c0d2e-3a4b-4c5d-8e9f-0a1b2c3d4e5f" snapshotName = "pause-snap-1" ) @@ -343,11 +339,9 @@ func TestRestoreUsesRequestSandboxAssets(t *testing.T) { host := imageVolumeTestRegistry(t) image := host + "/actor:v1" - pushTestImage(t, image, singleFileLayer(t, "bin/app", "app")) - checkpointPause := host + "/pause:v1" - pushTestImage(t, checkpointPause, singleFileLayer(t, "pause", "pause-v1")) - restorePause := host + "/pause:v2" - pushTestImage(t, restorePause, singleFileLayer(t, "pause", "pause-v2")) + pinnedImage := pushTestImage(t, image, singleFileLayer(t, "bin/app", "app")) + checkpointPause := pushTestImage(t, host+"/pause:v1", singleFileLayer(t, "pause", "pause-v1")) + restorePause := pushTestImage(t, host+"/pause:v2", singleFileLayer(t, "pause", "pause-v2")) runsc := []byte("runsc binary") s := &AteomHerder{ @@ -371,7 +365,7 @@ func TestRestoreUsesRequestSandboxAssets(t *testing.T) { } } spec := &ateletpb.WorkloadSpec{ - Containers: []*ateletpb.Container{{Name: "app", Image: image, Command: []string{"/bin/app"}}}, + Containers: []*ateletpb.Container{{Name: "app", Image: pinnedImage, Command: []string{"/bin/app"}}}, } if _, err := s.Run(ctx, &ateletpb.RunRequest{ @@ -397,9 +391,7 @@ func TestRestoreUsesRequestSandboxAssets(t *testing.T) { Spec: spec, Fidelity: ateletpb.SnapshotFidelity_SNAPSHOT_FIDELITY_MEMORY, Type: ateletpb.CheckpointType_CHECKPOINT_TYPE_LOCAL, - Config: &ateletpb.CheckpointRequest_LocalConfig{ - LocalConfig: &ateletpb.LocalCheckpointConfiguration{SnapshotName: snapshotName}, - }, + LocalConfig: &ateletpb.LocalCheckpointConfiguration{SnapshotName: snapshotName}, }); err != nil { t.Fatalf("Checkpoint: %v", err) } @@ -427,9 +419,7 @@ func TestRestoreUsesRequestSandboxAssets(t *testing.T) { Spec: spec, Fidelity: ateletpb.SnapshotFidelity_SNAPSHOT_FIDELITY_MEMORY, Type: ateletpb.CheckpointType_CHECKPOINT_TYPE_LOCAL, - Config: &ateletpb.RestoreRequest_LocalConfig{ - LocalConfig: &ateletpb.LocalCheckpointConfiguration{SnapshotName: snapshotName}, - }, + LocalConfig: &ateletpb.LocalCheckpointConfiguration{SnapshotName: snapshotName}, }); err != nil { t.Fatalf("Restore: %v", err) } @@ -463,14 +453,14 @@ func TestActivationFailureBeforeRegistration(t *testing.T) { } { t.Run(tc.name, func(t *testing.T) { useTempNodeDirs(t) - const actorUID, snapshotName = "actor-uid-1", "pause-snap-1" + const actorUID, snapshotName = "0f6c2b0e-1b7e-4b3f-9a6a-1d2c3e4f5a61", "pause-snap-1" ctx := t.Context() store := newCTBStore(t) certA := string(testCertPEM(t)) store.set(t, certA) refresher := newSystemInfoVolumeRefresher(trustbundle.NewSource(store.lister.Get, nil), nil) spec := &ateletpb.WorkloadSpec{Volumes: []*ateletpb.Volume{{ - Name: "trust", Source: &ateletpb.Volume_SystemInfo{SystemInfo: trustVolumeSpec("ca.pem")}, + Name: "trust", SystemInfo: trustVolumeSpec("ca.pem"), }}} registered, err := refresher.Register(actorUID, resources.ActorRef{Atespace: "team-a", Name: "actor-1"}, systemInfoVolumesFor(actorUID, spec)) if err != nil { @@ -519,14 +509,14 @@ func TestActivationFailureBeforeRegistration(t *testing.T) { } if tc.restore { _, err = s.Restore(ctx, &ateletpb.RestoreRequest{ - Atespace: "team-a", ActorName: "actor-1", ActorUid: actorUID, WorkerPodUid: "worker-pod-uid-1", + Atespace: "team-a", ActorName: "actor-1", ActorUid: actorUID, WorkerPodUid: "5f1c0d2e-3a4b-4c5d-8e9f-0a1b2c3d4e5f", SandboxAssets: assets, Spec: spec, Fidelity: ateletpb.SnapshotFidelity_SNAPSHOT_FIDELITY_MEMORY, Type: ateletpb.CheckpointType_CHECKPOINT_TYPE_LOCAL, - Config: &ateletpb.RestoreRequest_LocalConfig{LocalConfig: &ateletpb.LocalCheckpointConfiguration{SnapshotName: snapshotName}}, + LocalConfig: &ateletpb.LocalCheckpointConfiguration{SnapshotName: snapshotName}, }) } else { _, err = s.Run(ctx, &ateletpb.RunRequest{ - Atespace: "team-a", ActorName: "actor-1", ActorUid: actorUID, WorkerPodUid: "worker-pod-uid-1", + Atespace: "team-a", ActorName: "actor-1", ActorUid: actorUID, WorkerPodUid: "5f1c0d2e-3a4b-4c5d-8e9f-0a1b2c3d4e5f", SandboxAssets: assets, Spec: spec, }) } @@ -577,6 +567,7 @@ func TestRunFailureAfterRegistrationRemovesOwnRegistration(t *testing.T) { refresher := newSystemInfoVolumeRefresher(trustbundle.NewSource(store.lister.Get, nil), nil) content := []byte("runsc binary") assetHash := fmt.Sprintf("%x", sha256.Sum256(content)) + const actorUID = "0f6c2b0e-1b7e-4b3f-9a6a-1d2c3e4f5a62" s := &AteomHerder{ anonGCSClient: fakeObjectStorage{data: content}, imageCache: newImageVolumeStore(t), @@ -584,18 +575,18 @@ func TestRunFailureAfterRegistrationRemovesOwnRegistration(t *testing.T) { } spec := &ateletpb.WorkloadSpec{ Volumes: []*ateletpb.Volume{{ - Name: "trust", - Source: &ateletpb.Volume_SystemInfo{SystemInfo: trustVolumeSpec("ca.pem")}, + Name: "trust", + SystemInfo: trustVolumeSpec("ca.pem"), }}, } _, err := s.Run(t.Context(), &ateletpb.RunRequest{ Atespace: "team-a", ActorName: "actor-run", - ActorUid: "actor-uid-run", - WorkerPodUid: "worker-pod-uid-1", + ActorUid: actorUID, + WorkerPodUid: "5f1c0d2e-3a4b-4c5d-8e9f-0a1b2c3d4e5f", SandboxAssets: &ateletpb.SandboxAssets{ SandboxClass: "gvisor", - PauseImage: "://invalid-image", + PauseImage: missingPauseImage(t), Assets: map[string]*ateletpb.ArchAssets{runtime.GOARCH: {Files: map[string]*ateletpb.AssetFile{ runscAssetName: {Url: "gs://test-bucket/runsc", Sha256: assetHash}, }}}, @@ -605,20 +596,29 @@ func TestRunFailureAfterRegistrationRemovesOwnRegistration(t *testing.T) { if err == nil || !strings.Contains(err.Error(), "while creating pause OCI bundle") { t.Fatalf("Run error = %v, want the post-registration OCI preparation failure", err) } - if _, err := os.ReadFile(filepath.Join(ateletpath.SystemInfoVolumeRoot("actor-uid-run", "trust"), "ca.pem")); err != nil { + if _, err := os.ReadFile(filepath.Join(ateletpath.SystemInfoVolumeRoot(actorUID, "trust"), "ca.pem")); err != nil { t.Fatalf("Register did not write its projection before OCI preparation failed: %v", err) } - if got := refresher.actors["actor-uid-run"]; got != nil { + if got := refresher.actors[actorUID]; got != nil { t.Fatalf("failed Run left its registration live: %p", got) } } +// missingPauseImage returns a digest-pinned reference that passes request +// validation but names a manifest the local test registry does not serve, so +// the pull fails only once OCI bundle preparation starts. +func missingPauseImage(t *testing.T) string { + t.Helper() + return imageVolumeTestRegistry(t) + "/pause@sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" +} + func TestRestoreFailureAfterRegistrationRemovesOwnRegistration(t *testing.T) { useTempNodeDirs(t) const ( - actorUID = "actor-uid-restore-after" + actorUID = "0f6c2b0e-1b7e-4b3f-9a6a-1d2c3e4f5a63" snapshotName = "pause-snap-after" ) + pauseImage := missingPauseImage(t) store := newCTBStore(t) store.set(t, string(testCertPEM(t))) refresher := newSystemInfoVolumeRefresher(trustbundle.NewSource(store.lister.Get, nil), nil) @@ -626,14 +626,14 @@ func TestRestoreFailureAfterRegistrationRemovesOwnRegistration(t *testing.T) { assetHash := fmt.Sprintf("%x", sha256.Sum256(content)) writeLocalSnapshot(t, ateletpath.LocalSnapshotDir(actorUID, snapshotName), sandboxAssetsRecord{ SandboxClass: "gvisor", - PauseImage: "://invalid-image", + PauseImage: pauseImage, Assets: map[string]assetEntry{"runsc": {URL: "gs://test-bucket/runsc", SHA256: assetHash}}, SnapshotFiles: []string{"checkpoint.img"}, }, map[string]string{"checkpoint.img": "guest-memory"}) spec := &ateletpb.WorkloadSpec{Volumes: []*ateletpb.Volume{{ - Name: "trust", - Source: &ateletpb.Volume_SystemInfo{SystemInfo: trustVolumeSpec("ca.pem")}, + Name: "trust", + SystemInfo: trustVolumeSpec("ca.pem"), }}} s := &AteomHerder{ anonGCSClient: fakeObjectStorage{data: content}, @@ -644,20 +644,18 @@ func TestRestoreFailureAfterRegistrationRemovesOwnRegistration(t *testing.T) { Atespace: "team-a", ActorName: "actor-restore-after", ActorUid: actorUID, - WorkerPodUid: "worker-pod-uid-1", + WorkerPodUid: "5f1c0d2e-3a4b-4c5d-8e9f-0a1b2c3d4e5f", SandboxAssets: &ateletpb.SandboxAssets{ SandboxClass: "gvisor", - PauseImage: "://invalid-image", + PauseImage: pauseImage, Assets: map[string]*ateletpb.ArchAssets{runtime.GOARCH: {Files: map[string]*ateletpb.AssetFile{ runscAssetName: {Url: "gs://test-bucket/runsc", Sha256: assetHash}, }}}, }, - Spec: spec, - Fidelity: ateletpb.SnapshotFidelity_SNAPSHOT_FIDELITY_MEMORY, - Type: ateletpb.CheckpointType_CHECKPOINT_TYPE_LOCAL, - Config: &ateletpb.RestoreRequest_LocalConfig{ - LocalConfig: &ateletpb.LocalCheckpointConfiguration{SnapshotName: snapshotName}, - }, + Spec: spec, + Fidelity: ateletpb.SnapshotFidelity_SNAPSHOT_FIDELITY_MEMORY, + Type: ateletpb.CheckpointType_CHECKPOINT_TYPE_LOCAL, + LocalConfig: &ateletpb.LocalCheckpointConfiguration{SnapshotName: snapshotName}, }) if err == nil || !strings.Contains(err.Error(), "while creating pause OCI bundle") { t.Fatalf("Restore error = %v, want the post-registration OCI preparation failure", err) @@ -681,7 +679,7 @@ func TestTerminateWithoutWorkerPodUID(t *testing.T) { const ( atespace = "ate-demo" actorName = "counter" - actorUID = "actor-uid-1" + actorUID = "0f6c2b0e-1b7e-4b3f-9a6a-1d2c3e4f5a64" snapshotName = "pause-snap-1" ) @@ -705,7 +703,7 @@ func TestTerminateWithoutWorkerPodUID(t *testing.T) { systemInfoVolumes: newSystemInfoVolumeRefresher(nil, nil), } spec := &ateletpb.WorkloadSpec{ - Containers: []*ateletpb.Container{{Name: "app", Image: "example.com/app:v1"}}, + Containers: []*ateletpb.Container{{Name: "app", Image: "example.com/app@sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"}}, } if _, err := s.Terminate(ctx, &ateletpb.TerminateRequest{ diff --git a/cmd/atelet/main.go b/cmd/atelet/main.go index 6338b03f65..e4a5dab2ea 100644 --- a/cmd/atelet/main.go +++ b/cmd/atelet/main.go @@ -30,6 +30,7 @@ import ( "syscall" "time" + "github.com/agent-substrate/substrate/cmd/atelet/internal/apivalidation" "github.com/agent-substrate/substrate/cmd/atelet/internal/ateletpath" "github.com/agent-substrate/substrate/cmd/atelet/internal/credentialprovider" "github.com/agent-substrate/substrate/cmd/atelet/internal/sparsefile" @@ -77,7 +78,6 @@ import ( "google.golang.org/grpc/status" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/fields" - "k8s.io/apimachinery/pkg/util/validation/field" "k8s.io/client-go/kubernetes" "k8s.io/client-go/rest" "k8s.io/client-go/tools/cache" @@ -523,8 +523,8 @@ func NewService( } func (s *AteomHerder) Run(ctx context.Context, req *ateletpb.RunRequest) (resp *ateletpb.RunResponse, err error) { - if err := validateRunRequest(req); err != nil { - return nil, apierror.InvalidArgument("%v", err) + if errs := apivalidation.ValidateRunRequest(ctx, req); len(errs) > 0 { + return nil, resources.ToAPIError(errs) } actorUID := req.GetActorUid() @@ -644,8 +644,8 @@ func allocatedBytes(info os.FileInfo) int64 { } func (s *AteomHerder) Checkpoint(ctx context.Context, req *ateletpb.CheckpointRequest) (_ *ateletpb.CheckpointResponse, err error) { - if err := validateCheckpointRequest(req); err != nil { - return nil, apierror.InvalidArgument("%v", err) + if errs := apivalidation.ValidateCheckpointRequest(ctx, req); len(errs) > 0 { + return nil, resources.ToAPIError(errs) } actorUID := req.GetActorUid() @@ -891,7 +891,7 @@ func shouldHaveSnapshots(req *ateletpb.CheckpointRequest) bool { } for _, vol := range req.GetSpec().GetVolumes() { - if _, ok := vol.GetSource().(*ateletpb.Volume_DurableDir); ok { + if vol.GetDurableDir() != nil { return true } } @@ -950,8 +950,8 @@ func (s *AteomHerder) uploadSnapshot(ctx context.Context, uri resources.Snapshot // files and their self-describing manifest already sit under the actor's // local-checkpoints directory, written by an earlier local Checkpoint (pause). func (s *AteomHerder) UploadPausedCheckpoint(ctx context.Context, req *ateletpb.UploadPausedCheckpointRequest) (_ *ateletpb.UploadPausedCheckpointResponse, err error) { - if err := validateUploadPausedCheckpointRequest(req); err != nil { - return nil, apierror.InvalidArgument("%v", err) + if errs := apivalidation.ValidateUploadPausedCheckpointRequest(ctx, req); len(errs) > 0 { + return nil, resources.ToAPIError(errs) } tStart := time.Now() @@ -1052,8 +1052,8 @@ func readSnapshotManifest(dir string) ([]byte, error) { } func (s *AteomHerder) Restore(ctx context.Context, req *ateletpb.RestoreRequest) (resp *ateletpb.RestoreResponse, err error) { - if err := validateRestoreRequest(req); err != nil { - return nil, apierror.InvalidArgument("%v", err) + if errs := apivalidation.ValidateRestoreRequest(ctx, req); len(errs) > 0 { + return nil, resources.ToAPIError(errs) } actorUID := req.GetActorUid() @@ -1290,8 +1290,8 @@ func (s *AteomHerder) Restore(ctx context.Context, req *ateletpb.RestoreRequest) // Terminate terminates any running workload on ateom, unmounts external volumes, // and resets actor directories on the node. func (s *AteomHerder) Terminate(ctx context.Context, req *ateletpb.TerminateRequest) (*ateletpb.TerminateResponse, error) { - if err := validateTerminateRequest(req); err != nil { - return nil, apierror.InvalidArgument("%v", err) + if errs := apivalidation.ValidateTerminateRequest(ctx, req); len(errs) > 0 { + return nil, resources.ToAPIError(errs) } actorRef := resources.ActorRef{Atespace: req.GetAtespace(), Name: req.GetActorName()} @@ -1493,8 +1493,7 @@ func (s *AteomHerder) prepareOCIBundles( ) ([]*ateompb.ContainerSpec, error) { // Prepare host folders for volume types that need them. for _, vol := range spec.GetVolumes() { - switch vol.GetSource().(type) { - case *ateletpb.Volume_DurableDir: + if vol.GetDurableDir() != nil { volPath := ateletpath.DurableDirVolumeMountPoint(actorUID, vol.GetName()) if err := os.MkdirAll(volPath, 0o700); err != nil { return nil, fmt.Errorf("while creating %q: %w", volPath, err) @@ -1582,29 +1581,29 @@ func buildAteomWorkloadSpec(spec *ateletpb.WorkloadSpec, containerSpecs []*ateom return nil, fmt.Errorf("container %q mounts volume %q which is not defined in workload volumes", ctr.GetName(), volName) } - switch vol.GetSource().(type) { - case *ateletpb.Volume_DurableDir: + switch { + case vol.GetDurableDir() != nil: ddMounts = append(ddMounts, &ateompb.DurableDirVolumeMount{ VolumeName: volName, MountPath: vm.GetMountPath(), }) - case *ateletpb.Volume_External: + case vol.GetExternal() != nil: csiMounts = append(csiMounts, &ateompb.VolumeMount{ VolumeName: volName, MountPath: vm.GetMountPath(), }) - case *ateletpb.Volume_SystemInfo: + case vol.GetSystemInfo() != nil: siMounts = append(siMounts, &ateompb.SystemInfoVolumeMount{ VolumeName: volName, MountPath: vm.GetMountPath(), }) - case *ateletpb.Volume_Image: + case vol.GetImage() != nil: imgMounts = append(imgMounts, &ateompb.ImageVolumeMount{ VolumeName: volName, MountPath: vm.GetMountPath(), }) default: - return nil, fmt.Errorf("container %q mounts volume %q with unsupported source %T", ctr.GetName(), volName, vol.GetSource()) + return nil, fmt.Errorf("container %q mounts volume %q with no source set", ctr.GetName(), volName) } } container := &ateompb.Container{ @@ -1706,171 +1705,6 @@ func (d *AteomDialer) DialAteomPod(ctx context.Context, podUID string) (*grpc.Cl return conn, nil } -// validateRunRequest, validateCheckpointRequest, and validateRestoreRequest -// validate everything in their request that atelet turns into host filesystem -// paths, plus the request-specific fields. atelet listens on an insecure -// hostPort, so any reachable caller could otherwise smuggle a path separator -// or ".." through these fields and make atelet read/RemoveAll/write outside -// the intended directory tree, or collide bundles. Each RPC validates at its -// boundary, before any path is built. The field rules live in -// internal/resources so other components can apply them at their boundaries. -func validateRunRequest(req *ateletpb.RunRequest) error { - var errs field.ErrorList - errs = append(errs, resources.ValidateResourceName(req.GetAtespace(), field.NewPath("atespace"))...) - errs = append(errs, resources.ValidateResourceName(req.GetActorName(), field.NewPath("actor_name"))...) - errs = append(errs, resources.ValidateResourceName(req.GetActorUid(), field.NewPath("actor_uid"))...) - if len(errs) > 0 { - return errs.ToAggregate() - } - // TODO: Migrate all validations below to the validation framework. - if err := resources.ValidateWorkerPodUID(req.GetWorkerPodUid()); err != nil { - return err - } - names := make([]string, 0, len(req.GetSpec().GetContainers())) - for _, ctr := range req.GetSpec().GetContainers() { - names = append(names, ctr.GetName()) - } - return resources.ValidateContainerNames(names) -} - -func validateCheckpointRequest(req *ateletpb.CheckpointRequest) error { - var errs field.ErrorList - errs = append(errs, resources.ValidateResourceName(req.GetAtespace(), field.NewPath("atespace"))...) - errs = append(errs, resources.ValidateResourceName(req.GetActorName(), field.NewPath("actor_name"))...) - errs = append(errs, resources.ValidateResourceName(req.GetActorUid(), field.NewPath("actor_uid"))...) - if len(errs) > 0 { - return errs.ToAggregate() - } - // TODO: Migrate all validations below to the validation framework. - if err := resources.ValidateWorkerPodUID(req.GetWorkerPodUid()); err != nil { - return err - } - names := make([]string, 0, len(req.GetSpec().GetContainers())) - for _, ctr := range req.GetSpec().GetContainers() { - names = append(names, ctr.GetName()) - } - if err := resources.ValidateContainerNames(names); err != nil { - return err - } - - if err := validateFidelity(req.GetFidelity()); err != nil { - return err - } - - switch req.GetType() { - case ateletpb.CheckpointType_CHECKPOINT_TYPE_EXTERNAL: - if _, err := resources.ParseSnapshotURI(req.GetExternalConfig().GetSnapshotUri()); err != nil { - return err - } - case ateletpb.CheckpointType_CHECKPOINT_TYPE_LOCAL: - if !resources.IsValidResourceName(req.GetLocalConfig().GetSnapshotName()) { - return fmt.Errorf("invalid local snapshot name %q", req.GetLocalConfig().GetSnapshotName()) - } - default: - return fmt.Errorf("invalid checkpoint type: %v", req.GetType()) - } - return nil -} - -func validateRestoreRequest(req *ateletpb.RestoreRequest) error { - var errs field.ErrorList - errs = append(errs, resources.ValidateResourceName(req.GetAtespace(), field.NewPath("atespace"))...) - errs = append(errs, resources.ValidateResourceName(req.GetActorName(), field.NewPath("actor_name"))...) - errs = append(errs, resources.ValidateResourceName(req.GetActorUid(), field.NewPath("actor_uid"))...) - if len(errs) > 0 { - return errs.ToAggregate() - } - // TODO: Migrate all validations below to the validation framework. - if err := resources.ValidateWorkerPodUID(req.GetWorkerPodUid()); err != nil { - return err - } - names := make([]string, 0, len(req.GetSpec().GetContainers())) - for _, ctr := range req.GetSpec().GetContainers() { - names = append(names, ctr.GetName()) - } - if err := resources.ValidateContainerNames(names); err != nil { - return err - } - - if err := validateFidelity(req.GetFidelity()); err != nil { - return err - } - - if req.GetSandboxAssets() == nil { - return fmt.Errorf("missing sandbox_assets") - } - - switch req.GetType() { - case ateletpb.CheckpointType_CHECKPOINT_TYPE_EXTERNAL: - if _, err := resources.ParseSnapshotURI(req.GetExternalConfig().GetSnapshotUri()); err != nil { - return err - } - case ateletpb.CheckpointType_CHECKPOINT_TYPE_LOCAL: - if !resources.IsValidResourceName(req.GetLocalConfig().GetSnapshotName()) { - return fmt.Errorf("invalid local snapshot name %q", req.GetLocalConfig().GetSnapshotName()) - } - default: - return fmt.Errorf("invalid checkpoint type: %v", req.GetType()) - } - return nil -} - -func validateTerminateRequest(req *ateletpb.TerminateRequest) error { - var errs field.ErrorList - errs = append(errs, resources.ValidateResourceName(req.GetAtespace(), field.NewPath("atespace"))...) - errs = append(errs, resources.ValidateResourceName(req.GetActorName(), field.NewPath("actor_name"))...) - errs = append(errs, resources.ValidateResourceName(req.GetActorUid(), field.NewPath("actor_uid"))...) - if len(errs) > 0 { - return errs.ToAggregate() - } - if req.GetWorkerPodUid() != "" { - if err := resources.ValidateWorkerPodUID(req.GetWorkerPodUid()); err != nil { - return err - } - } - names := make([]string, 0, len(req.GetSpec().GetContainers())) - for _, ctr := range req.GetSpec().GetContainers() { - names = append(names, ctr.GetName()) - } - return resources.ValidateContainerNames(names) -} - -// validateFidelity accepts every level of the ladder. Which levels a sandbox -// runtime can actually serve is the runtime's call: atelet forwards the -// fidelity and ateom rejects what it cannot capture. -func validateFidelity(fidelity ateletpb.SnapshotFidelity) error { - switch fidelity { - case ateletpb.SnapshotFidelity_SNAPSHOT_FIDELITY_VOLUMES, - ateletpb.SnapshotFidelity_SNAPSHOT_FIDELITY_ROOTFS, - ateletpb.SnapshotFidelity_SNAPSHOT_FIDELITY_MEMORY: - return nil - case ateletpb.SnapshotFidelity_SNAPSHOT_FIDELITY_UNSPECIFIED: - return fmt.Errorf("snapshot fidelity must be non-zero") - default: - return fmt.Errorf("invalid snapshot fidelity: %v", fidelity) - } -} - -func validateUploadPausedCheckpointRequest(req *ateletpb.UploadPausedCheckpointRequest) error { - var errs field.ErrorList - errs = append(errs, resources.ValidateResourceName(req.GetAtespace(), field.NewPath("atespace"))...) - errs = append(errs, resources.ValidateResourceName(req.GetActorName(), field.NewPath("actor_name"))...) - errs = append(errs, resources.ValidateResourceName(req.GetActorUid(), field.NewPath("actor_uid"))...) - errs = append(errs, resources.ValidateResourceName(req.GetLocalSnapshotName(), field.NewPath("local_snapshot_name"))...) - // Golden actors are never paused (the golden flow commits a running - // actor), so never promote a paused checkpoint to a golden snapshot. - if req.GetAtespace() == resources.GoldenActorAtespace { - errs = append(errs, field.Forbidden(field.NewPath("atespace"), fmt.Sprintf("atespace %q holds golden actors, which are never paused", req.GetAtespace()))) - } - if _, err := resources.ParseSnapshotURI(req.GetDestinationSnapshotUri()); err != nil { - errs = append(errs, field.Invalid(field.NewPath("destination_snapshot_uri"), req.GetDestinationSnapshotUri(), err.Error())) - } - if err := validateFidelity(req.GetDesiredFidelity()); err != nil { - errs = append(errs, field.Invalid(field.NewPath("desired_fidelity"), req.GetDesiredFidelity().String(), err.Error())) - } - return errs.ToAggregate() -} - // writeFileAtomic writes data to path by writing a temp file in the same // directory and renaming it over the target so readers never observe a // truncated or partially written value. diff --git a/cmd/atelet/main_test.go b/cmd/atelet/main_test.go index 2fe768baf1..f17d6d08ea 100644 --- a/cmd/atelet/main_test.go +++ b/cmd/atelet/main_test.go @@ -538,216 +538,6 @@ func TestWriteFileAtomic(t *testing.T) { }) } -// validRunRequest, validCheckpointRequest, and validRestoreRequest build -// requests whose every field passes validation; the per-request tests below -// break one field per case. -func validRunRequest() *ateletpb.RunRequest { - return &ateletpb.RunRequest{ - Atespace: "ate-demo", - ActorName: "counter-1", - ActorTemplateAtespace: "ate-demo", - ActorTemplateName: "counter", - WorkerPodUid: "422938ba-8860-4983-a25d-d6bcb0a69d4e", - ActorUid: "123e4567-e89b-12d3-a456-426614174000", - Spec: &ateletpb.WorkloadSpec{Containers: []*ateletpb.Container{{Name: "worker"}}}, - } -} - -func validCheckpointRequest() *ateletpb.CheckpointRequest { - return &ateletpb.CheckpointRequest{ - Atespace: "ate-demo", - ActorName: "counter-1", - ActorTemplateAtespace: "ate-demo", - ActorTemplateName: "counter", - WorkerPodUid: "422938ba-8860-4983-a25d-d6bcb0a69d4e", - ActorUid: "123e4567-e89b-12d3-a456-426614174000", - Spec: &ateletpb.WorkloadSpec{Containers: []*ateletpb.Container{{Name: "worker"}}}, - Type: ateletpb.CheckpointType_CHECKPOINT_TYPE_EXTERNAL, - Config: &ateletpb.CheckpointRequest_ExternalConfig{ - ExternalConfig: &ateletpb.ExternalCheckpointConfiguration{ - SnapshotUri: testSnapshotURI, - }, - }, - Fidelity: ateletpb.SnapshotFidelity_SNAPSHOT_FIDELITY_MEMORY, - } -} - -func validRestoreRequest() *ateletpb.RestoreRequest { - return &ateletpb.RestoreRequest{ - Atespace: "ate-demo", - ActorName: "counter-1", - ActorTemplateAtespace: "ate-demo", - ActorTemplateName: "counter", - WorkerPodUid: "422938ba-8860-4983-a25d-d6bcb0a69d4e", - ActorUid: "123e4567-e89b-12d3-a456-426614174000", - Spec: &ateletpb.WorkloadSpec{Containers: []*ateletpb.Container{{Name: "worker"}}}, - Type: ateletpb.CheckpointType_CHECKPOINT_TYPE_EXTERNAL, - Config: &ateletpb.RestoreRequest_ExternalConfig{ - ExternalConfig: &ateletpb.ExternalRestoreConfiguration{ - SnapshotUri: testSnapshotURI, - }, - }, - Fidelity: ateletpb.SnapshotFidelity_SNAPSHOT_FIDELITY_MEMORY, - SandboxAssets: &ateletpb.SandboxAssets{ - SandboxClass: "gvisor", - PauseImage: testPauseImage, - }, - } -} - -func TestValidateRunRequest(t *testing.T) { - tests := []struct { - name string - mutate func(*ateletpb.RunRequest) - wantErr bool - }{ - {"valid", func(*ateletpb.RunRequest) {}, false}, - {"invalid worker pod uid", func(r *ateletpb.RunRequest) { r.WorkerPodUid = "../escape" }, true}, - {"invalid atespace", func(r *ateletpb.RunRequest) { r.Atespace = "../escape" }, true}, - {"invalid actor name", func(r *ateletpb.RunRequest) { r.ActorName = "../escape" }, true}, - {"invalid actor uid", func(r *ateletpb.RunRequest) { r.ActorUid = "../escape" }, true}, - {"any actor template identity accepted", func(r *ateletpb.RunRequest) { r.ActorTemplateAtespace, r.ActorTemplateName = "Not_Valid", "Not_Valid" }, false}, - {"empty actor template identity accepted", func(r *ateletpb.RunRequest) { r.ActorTemplateAtespace, r.ActorTemplateName = "", "" }, false}, - {"invalid container name", func(r *ateletpb.RunRequest) { - r.Spec.Containers = []*ateletpb.Container{{Name: "../escape"}} - }, true}, - } - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - req := validRunRequest() - tc.mutate(req) - if err := validateRunRequest(req); (err != nil) != tc.wantErr { - t.Errorf("validateRunRequest err = %v, wantErr %v", err, tc.wantErr) - } - }) - } -} - -// Checkpoint and Restore must reject a bad snapshot URI even when -// every common field is valid. -func TestValidateCheckpointRequest(t *testing.T) { - makeReq := func(opts ...func(*ateletpb.CheckpointRequest)) *ateletpb.CheckpointRequest { - r := validCheckpointRequest() - for _, opt := range opts { - opt(r) - } - return r - } - - tests := []struct { - name string - req *ateletpb.CheckpointRequest - wantErr bool - }{ - {"valid", makeReq(), false}, - {"empty snapshot uri", makeReq(func(r *ateletpb.CheckpointRequest) { r.GetExternalConfig().SnapshotUri = "" }), true}, - {"bucketless snapshot uri", makeReq(func(r *ateletpb.CheckpointRequest) { r.GetExternalConfig().SnapshotUri = "relative/path" }), true}, - {"invalid worker pod uid", makeReq(func(r *ateletpb.CheckpointRequest) { r.WorkerPodUid = "../escape" }), true}, - {"invalid atespace", makeReq(func(r *ateletpb.CheckpointRequest) { r.Atespace = "../escape" }), true}, - {"invalid actor name", makeReq(func(r *ateletpb.CheckpointRequest) { r.ActorName = "../escape" }), true}, - {"invalid actor uid", makeReq(func(r *ateletpb.CheckpointRequest) { r.ActorUid = "../escape" }), true}, - {"any actor template identity accepted", makeReq(func(r *ateletpb.CheckpointRequest) { - r.ActorTemplateAtespace, r.ActorTemplateName = "Not_Valid", "Not_Valid" - }), false}, - {"empty actor template identity accepted", makeReq(func(r *ateletpb.CheckpointRequest) { r.ActorTemplateAtespace, r.ActorTemplateName = "", "" }), false}, - {"invalid container name", makeReq(func(r *ateletpb.CheckpointRequest) { - r.Spec.Containers = []*ateletpb.Container{{Name: "../escape"}} - }), true}, - {"invalid local snapshot prefix", makeReq(func(r *ateletpb.CheckpointRequest) { - r.Type = ateletpb.CheckpointType_CHECKPOINT_TYPE_LOCAL - r.Config = &ateletpb.CheckpointRequest_LocalConfig{LocalConfig: &ateletpb.LocalCheckpointConfiguration{SnapshotName: ""}} - }), true}, - {"local snapshot name escapes its directory", makeReq(func(r *ateletpb.CheckpointRequest) { - r.Type = ateletpb.CheckpointType_CHECKPOINT_TYPE_LOCAL - r.Config = &ateletpb.CheckpointRequest_LocalConfig{LocalConfig: &ateletpb.LocalCheckpointConfiguration{SnapshotName: "../escape"}} - }), true}, - {"nested local snapshot prefix", makeReq(func(r *ateletpb.CheckpointRequest) { - r.Type = ateletpb.CheckpointType_CHECKPOINT_TYPE_LOCAL - r.Config = &ateletpb.CheckpointRequest_LocalConfig{LocalConfig: &ateletpb.LocalCheckpointConfiguration{SnapshotName: "pause/2"}} - }), true}, - {"traversal local snapshot prefix", makeReq(func(r *ateletpb.CheckpointRequest) { - r.Type = ateletpb.CheckpointType_CHECKPOINT_TYPE_LOCAL - r.Config = &ateletpb.CheckpointRequest_LocalConfig{LocalConfig: &ateletpb.LocalCheckpointConfiguration{SnapshotName: ".."}} - }), true}, - {"unspecified snapshot type", makeReq(func(r *ateletpb.CheckpointRequest) { r.Type = ateletpb.CheckpointType_CHECKPOINT_TYPE_UNSPECIFIED }), true}, - {"unspecified snapshot fidelity", makeReq(func(r *ateletpb.CheckpointRequest) { - r.Fidelity = ateletpb.SnapshotFidelity_SNAPSHOT_FIDELITY_UNSPECIFIED - }), true}, - {"invalid snapshot fidelity", makeReq(func(r *ateletpb.CheckpointRequest) { r.Fidelity = ateletpb.SnapshotFidelity(23) }), true}, - {"rootfs fidelity", makeReq(func(r *ateletpb.CheckpointRequest) { - r.Fidelity = ateletpb.SnapshotFidelity_SNAPSHOT_FIDELITY_ROOTFS - }), false}, - } - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - if err := validateCheckpointRequest(tc.req); (err != nil) != tc.wantErr { - t.Errorf("validateCheckpointRequest err = %v, wantErr %v", err, tc.wantErr) - } - }) - } -} - -func TestValidateRestoreRequest(t *testing.T) { - makeReq := func(opts ...func(*ateletpb.RestoreRequest)) *ateletpb.RestoreRequest { - r := validRestoreRequest() - for _, opt := range opts { - opt(r) - } - return r - } - - tests := []struct { - name string - req *ateletpb.RestoreRequest - wantErr bool - }{ - {"valid", makeReq(), false}, - {"missing sandbox assets", makeReq(func(r *ateletpb.RestoreRequest) { r.SandboxAssets = nil }), true}, - {"empty snapshot uri", makeReq(func(r *ateletpb.RestoreRequest) { r.GetExternalConfig().SnapshotUri = "" }), true}, - {"bucketless snapshot uri", makeReq(func(r *ateletpb.RestoreRequest) { r.GetExternalConfig().SnapshotUri = "relative/path" }), true}, - {"invalid worker pod uid", makeReq(func(r *ateletpb.RestoreRequest) { r.WorkerPodUid = "../escape" }), true}, - {"invalid atespace", makeReq(func(r *ateletpb.RestoreRequest) { r.Atespace = "../escape" }), true}, - {"invalid actor name", makeReq(func(r *ateletpb.RestoreRequest) { r.ActorName = "../escape" }), true}, - {"invalid actor uid", makeReq(func(r *ateletpb.RestoreRequest) { r.ActorUid = "../escape" }), true}, - {"any actor template identity accepted", makeReq(func(r *ateletpb.RestoreRequest) { - r.ActorTemplateAtespace, r.ActorTemplateName = "Not_Valid", "Not_Valid" - }), false}, - {"empty actor template identity accepted", makeReq(func(r *ateletpb.RestoreRequest) { r.ActorTemplateAtespace, r.ActorTemplateName = "", "" }), false}, - {"invalid container name", makeReq(func(r *ateletpb.RestoreRequest) { - r.Spec.Containers = []*ateletpb.Container{{Name: "../escape"}} - }), true}, - {"invalid local snapshot prefix", makeReq(func(r *ateletpb.RestoreRequest) { - r.Type = ateletpb.CheckpointType_CHECKPOINT_TYPE_LOCAL - r.Config = &ateletpb.RestoreRequest_LocalConfig{LocalConfig: &ateletpb.LocalCheckpointConfiguration{SnapshotName: ""}} - }), true}, - {"local snapshot name escapes its directory", makeReq(func(r *ateletpb.RestoreRequest) { - r.Type = ateletpb.CheckpointType_CHECKPOINT_TYPE_LOCAL - r.Config = &ateletpb.RestoreRequest_LocalConfig{LocalConfig: &ateletpb.LocalCheckpointConfiguration{SnapshotName: "../escape"}} - }), true}, - {"nested local snapshot prefix", makeReq(func(r *ateletpb.RestoreRequest) { - r.Type = ateletpb.CheckpointType_CHECKPOINT_TYPE_LOCAL - r.Config = &ateletpb.RestoreRequest_LocalConfig{LocalConfig: &ateletpb.LocalCheckpointConfiguration{SnapshotName: "pause/2"}} - }), true}, - {"traversal local snapshot prefix", makeReq(func(r *ateletpb.RestoreRequest) { - r.Type = ateletpb.CheckpointType_CHECKPOINT_TYPE_LOCAL - r.Config = &ateletpb.RestoreRequest_LocalConfig{LocalConfig: &ateletpb.LocalCheckpointConfiguration{SnapshotName: ".."}} - }), true}, - {"unspecified snapshot type", makeReq(func(r *ateletpb.RestoreRequest) { r.Type = ateletpb.CheckpointType_CHECKPOINT_TYPE_UNSPECIFIED }), true}, - {"unspecified snapshot fidelity", makeReq(func(r *ateletpb.RestoreRequest) { r.Fidelity = ateletpb.SnapshotFidelity_SNAPSHOT_FIDELITY_UNSPECIFIED }), true}, - {"invalid snapshot fidelity", makeReq(func(r *ateletpb.RestoreRequest) { r.Fidelity = ateletpb.SnapshotFidelity(23) }), true}, - {"rootfs fidelity", makeReq(func(r *ateletpb.RestoreRequest) { - r.Fidelity = ateletpb.SnapshotFidelity_SNAPSHOT_FIDELITY_ROOTFS - }), false}, - } - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - if err := validateRestoreRequest(tc.req); (err != nil) != tc.wantErr { - t.Errorf("validateRestoreRequest err = %v, wantErr %v", err, tc.wantErr) - } - }) - } -} - // Every valid atelet scope must map to its ateom counterpart. func TestToAteomFidelity(t *testing.T) { tests := []struct { @@ -913,7 +703,10 @@ func TestRPCBoundariesReject(t *testing.T) { ctx := context.Background() badUID := "../escape" // valid actor ref, invalid worker pod UID const okAtespace, okID, okActorUID = "ate-demo", "counter-1", "123e4567-e89b-12d3-a456-426614174000" - okSpec := &ateletpb.WorkloadSpec{Containers: []*ateletpb.Container{{Name: "worker"}}} + okSpec := &ateletpb.WorkloadSpec{Containers: []*ateletpb.Container{{ + Name: "worker", + Image: "example.com/app@sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + }}} wantInvalidArgument := func(t *testing.T, rpc string, err error) { t.Helper() @@ -1020,10 +813,10 @@ func TestBuildAteomWorkloadSpecForwardsContainerSpecs(t *testing.T) { func TestBuildAteomWorkloadSpecForwardsDurableDirMounts(t *testing.T) { in := &ateletpb.WorkloadSpec{ Volumes: []*ateletpb.Volume{ - {Name: "data", Source: &ateletpb.Volume_DurableDir{DurableDir: &ateletpb.DurableDirVolume{}}}, - {Name: "cache", Source: &ateletpb.Volume_DurableDir{DurableDir: &ateletpb.DurableDirVolume{}}}, - {Name: "scratch", Source: &ateletpb.Volume_External{External: &ateletpb.ExternalVolumeSource{}}}, - {Name: "system-info", Source: &ateletpb.Volume_SystemInfo{SystemInfo: &ateletpb.SystemInfoVolume{}}}, + {Name: "data", DurableDir: &ateletpb.DurableDirVolume{}}, + {Name: "cache", DurableDir: &ateletpb.DurableDirVolume{}}, + {Name: "scratch", External: &ateletpb.ExternalVolumeSource{}}, + {Name: "system-info", SystemInfo: &ateletpb.SystemInfoVolume{}}, }, Containers: []*ateletpb.Container{ { @@ -1089,7 +882,7 @@ func TestBuildAteomWorkloadSpecValidation(t *testing.T) { name: "missing volume definition", in: &ateletpb.WorkloadSpec{ Volumes: []*ateletpb.Volume{ - {Name: "data", Source: &ateletpb.Volume_DurableDir{DurableDir: &ateletpb.DurableDirVolume{}}}, + {Name: "data", DurableDir: &ateletpb.DurableDirVolume{}}, }, Containers: []*ateletpb.Container{ { @@ -1117,14 +910,14 @@ func TestBuildAteomWorkloadSpecValidation(t *testing.T) { }, }, }, - wantErr: `container "ctr" mounts volume "data" with unsupported source `, + wantErr: `container "ctr" mounts volume "data" with no source set`, }, { name: "duplicate volume names", in: &ateletpb.WorkloadSpec{ Volumes: []*ateletpb.Volume{ - {Name: "data", Source: &ateletpb.Volume_DurableDir{DurableDir: &ateletpb.DurableDirVolume{}}}, - {Name: "data", Source: &ateletpb.Volume_External{External: &ateletpb.ExternalVolumeSource{}}}, + {Name: "data", DurableDir: &ateletpb.DurableDirVolume{}}, + {Name: "data", External: &ateletpb.ExternalVolumeSource{}}, }, Containers: []*ateletpb.Container{ { @@ -1407,9 +1200,9 @@ func TestDrainOnShutdownForceStopsAfterTimeout(t *testing.T) { func TestBuildAteomWorkloadSpec_ImageVolumeMounts(t *testing.T) { spec := &ateletpb.WorkloadSpec{ Volumes: []*ateletpb.Volume{ - {Name: "agent", Source: &ateletpb.Volume_Image{Image: &ateletpb.ImageVolumeSource{}}}, - {Name: "data", Source: &ateletpb.Volume_DurableDir{DurableDir: &ateletpb.DurableDirVolume{}}}, - {Name: "ext", Source: &ateletpb.Volume_External{External: &ateletpb.ExternalVolumeSource{}}}, + {Name: "agent", Image: &ateletpb.ImageVolumeSource{}}, + {Name: "data", DurableDir: &ateletpb.DurableDirVolume{}}, + {Name: "ext", External: &ateletpb.ExternalVolumeSource{}}, }, Containers: []*ateletpb.Container{{ Name: "app", @@ -1508,6 +1301,21 @@ func writeLocalSnapshot(t *testing.T, dir string, rec sandboxAssetsRecord, conte } } +func validCheckpointRequest() *ateletpb.CheckpointRequest { + return &ateletpb.CheckpointRequest{ + Atespace: "ate-demo", + ActorName: "counter-1", + ActorTemplateAtespace: "ate-demo", + ActorTemplateName: "counter", + WorkerPodUid: "422938ba-8860-4983-a25d-d6bcb0a69d4e", + ActorUid: "123e4567-e89b-12d3-a456-426614174000", + Spec: &ateletpb.WorkloadSpec{Containers: []*ateletpb.Container{{Name: "worker"}}}, + Type: ateletpb.CheckpointType_CHECKPOINT_TYPE_EXTERNAL, + ExternalConfig: &ateletpb.ExternalCheckpointConfiguration{SnapshotUri: testSnapshotURI}, + Fidelity: ateletpb.SnapshotFidelity_SNAPSHOT_FIDELITY_MEMORY, + } +} + func validUploadPausedCheckpointRequest() *ateletpb.UploadPausedCheckpointRequest { return &ateletpb.UploadPausedCheckpointRequest{ Atespace: "ate-demo", @@ -1697,47 +1505,6 @@ func TestUploadLocalCheckpointDir(t *testing.T) { }) } -func TestValidateUploadPausedCheckpointRequest(t *testing.T) { - tests := []struct { - name string - mutate func(*ateletpb.UploadPausedCheckpointRequest) - wantErr bool - }{ - {"valid", func(*ateletpb.UploadPausedCheckpointRequest) {}, false}, - {"valid volumes fidelity", func(r *ateletpb.UploadPausedCheckpointRequest) { - r.DesiredFidelity = ateletpb.SnapshotFidelity_SNAPSHOT_FIDELITY_VOLUMES - }, false}, - {"valid rootfs fidelity", func(r *ateletpb.UploadPausedCheckpointRequest) { - r.DesiredFidelity = ateletpb.SnapshotFidelity_SNAPSHOT_FIDELITY_ROOTFS - }, false}, - {"invalid atespace", func(r *ateletpb.UploadPausedCheckpointRequest) { r.Atespace = "../escape" }, true}, - {"golden atespace rejected", func(r *ateletpb.UploadPausedCheckpointRequest) { r.Atespace = resources.GoldenActorAtespace }, true}, - {"invalid actor name", func(r *ateletpb.UploadPausedCheckpointRequest) { r.ActorName = "UPPER" }, true}, - {"invalid actor uid", func(r *ateletpb.UploadPausedCheckpointRequest) { r.ActorUid = "" }, true}, - {"any actor template identity accepted", func(r *ateletpb.UploadPausedCheckpointRequest) { r.ActorTemplateAtespace = "no/slashes" }, false}, - {"empty actor template identity accepted", func(r *ateletpb.UploadPausedCheckpointRequest) { - r.ActorTemplateAtespace, r.ActorTemplateName = "", "" - }, false}, - {"invalid snapshot name", func(r *ateletpb.UploadPausedCheckpointRequest) { r.LocalSnapshotName = "../escape" }, true}, - {"invalid snapshot uri", func(r *ateletpb.UploadPausedCheckpointRequest) { r.DestinationSnapshotUri = "not-a-uri" }, true}, - {"unspecified fidelity", func(r *ateletpb.UploadPausedCheckpointRequest) { - r.DesiredFidelity = ateletpb.SnapshotFidelity_SNAPSHOT_FIDELITY_UNSPECIFIED - }, true}, - {"invalid fidelity", func(r *ateletpb.UploadPausedCheckpointRequest) { - r.DesiredFidelity = ateletpb.SnapshotFidelity(23) - }, true}, - } - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - req := validUploadPausedCheckpointRequest() - tc.mutate(req) - if err := validateUploadPausedCheckpointRequest(req); (err != nil) != tc.wantErr { - t.Errorf("validateUploadPausedCheckpointRequest err = %v, wantErr %v", err, tc.wantErr) - } - }) - } -} - func TestShouldHaveSnapshots(t *testing.T) { tests := []struct { name string @@ -1764,7 +1531,7 @@ func TestShouldHaveSnapshots(t *testing.T) { Fidelity: ateletpb.SnapshotFidelity_SNAPSHOT_FIDELITY_VOLUMES, Spec: &ateletpb.WorkloadSpec{ Volumes: []*ateletpb.Volume{ - {Name: "durable", Source: &ateletpb.Volume_DurableDir{DurableDir: &ateletpb.DurableDirVolume{}}}, + {Name: "durable", DurableDir: &ateletpb.DurableDirVolume{}}, }, }, }, @@ -1776,7 +1543,7 @@ func TestShouldHaveSnapshots(t *testing.T) { Fidelity: ateletpb.SnapshotFidelity_SNAPSHOT_FIDELITY_VOLUMES, Spec: &ateletpb.WorkloadSpec{ Volumes: []*ateletpb.Volume{ - {Name: "csi", Source: &ateletpb.Volume_External{External: &ateletpb.ExternalVolumeSource{}}}, + {Name: "csi", External: &ateletpb.ExternalVolumeSource{}}, }, }, }, @@ -1788,8 +1555,8 @@ func TestShouldHaveSnapshots(t *testing.T) { Fidelity: ateletpb.SnapshotFidelity_SNAPSHOT_FIDELITY_VOLUMES, Spec: &ateletpb.WorkloadSpec{ Volumes: []*ateletpb.Volume{ - {Name: "durable", Source: &ateletpb.Volume_DurableDir{DurableDir: &ateletpb.DurableDirVolume{}}}, - {Name: "csi", Source: &ateletpb.Volume_External{External: &ateletpb.ExternalVolumeSource{}}}, + {Name: "durable", DurableDir: &ateletpb.DurableDirVolume{}}, + {Name: "csi", External: &ateletpb.ExternalVolumeSource{}}, }, }, }, diff --git a/cmd/atelet/systeminfovolume.go b/cmd/atelet/systeminfovolume.go index ebded77046..05f1c444e3 100644 --- a/cmd/atelet/systeminfovolume.go +++ b/cmd/atelet/systeminfovolume.go @@ -168,17 +168,17 @@ func (r *systemInfoVolumeRefresher) DeregisterOwned(owner *registeredActor) { // collectData builds the volume's contents keyed by volume-relative path. func (r *systemInfoVolumeRefresher) collectData(ref resources.ActorRef, actorUID string, si *ateletpb.SystemInfoVolume) (payload map[string][]byte, err error) { payload = map[string][]byte{} - for _, dataSourceAny := range si.GetDataSources() { - switch dataSource := dataSourceAny.GetDataSource().(type) { - case *ateletpb.SystemInfoDataSource_TrustBundle: - tb := dataSource.TrustBundle + for _, dataSource := range si.GetDataSources() { + switch { + case dataSource.GetTrustBundle() != nil: + tb := dataSource.GetTrustBundle() pemBundle, err := r.bundles.Combined(tb.GetNames()) if err != nil { return nil, fmt.Errorf("system-info projection %q: %w", tb.GetPath(), err) } payload[tb.GetPath()] = pemBundle - case *ateletpb.SystemInfoDataSource_ActorMetadata: - for _, item := range dataSource.ActorMetadata.GetItems() { + case dataSource.GetActorMetadata() != nil: + for _, item := range dataSource.GetActorMetadata().GetItems() { var value string switch item.GetField() { case ateletpb.ActorMetadataField_ACTOR_METADATA_FIELD_NAME: diff --git a/cmd/atelet/systeminfovolume_test.go b/cmd/atelet/systeminfovolume_test.go index d08928d77c..c5286c3b33 100644 --- a/cmd/atelet/systeminfovolume_test.go +++ b/cmd/atelet/systeminfovolume_test.go @@ -111,9 +111,7 @@ func (s *ctbStore) remove(t *testing.T) { func trustVolumeSpec(relPath string) *ateletpb.SystemInfoVolume { return &ateletpb.SystemInfoVolume{ DataSources: []*ateletpb.SystemInfoDataSource{ - {DataSource: &ateletpb.SystemInfoDataSource_TrustBundle{ - TrustBundle: &ateletpb.TrustBundleDataSource{Names: []string{trustbundle.EgressName}, Path: relPath}, - }}, + {TrustBundle: &ateletpb.TrustBundleDataSource{Names: []string{trustbundle.EgressName}, Path: relPath}}, }, } } @@ -121,13 +119,11 @@ func trustVolumeSpec(relPath string) *ateletpb.SystemInfoVolume { func metadataVolumeSpec() *ateletpb.SystemInfoVolume { return &ateletpb.SystemInfoVolume{ DataSources: []*ateletpb.SystemInfoDataSource{ - {DataSource: &ateletpb.SystemInfoDataSource_ActorMetadata{ - ActorMetadata: &ateletpb.ActorMetadataDataSource{ - Items: []*ateletpb.ActorMetadataItem{ - {Field: ateletpb.ActorMetadataField_ACTOR_METADATA_FIELD_NAME, Path: "actor-name"}, - {Field: ateletpb.ActorMetadataField_ACTOR_METADATA_FIELD_ATESPACE, Path: "atespace"}, - {Field: ateletpb.ActorMetadataField_ACTOR_METADATA_FIELD_UID, Path: "identity/actor-uid"}, - }, + {ActorMetadata: &ateletpb.ActorMetadataDataSource{ + Items: []*ateletpb.ActorMetadataItem{ + {Field: ateletpb.ActorMetadataField_ACTOR_METADATA_FIELD_NAME, Path: "actor-name"}, + {Field: ateletpb.ActorMetadataField_ACTOR_METADATA_FIELD_ATESPACE, Path: "atespace"}, + {Field: ateletpb.ActorMetadataField_ACTOR_METADATA_FIELD_UID, Path: "identity/actor-uid"}, }, }}, }, @@ -687,9 +683,9 @@ func TestSystemInfoVolumeRefresher_FailedReplacementDropsRegistration(t *testing func TestSystemInfoVolumesFor(t *testing.T) { spec := &ateletpb.WorkloadSpec{Volumes: []*ateletpb.Volume{ - {Name: "data", Source: &ateletpb.Volume_DurableDir{DurableDir: &ateletpb.DurableDirVolume{}}}, - {Name: "trust", Source: &ateletpb.Volume_SystemInfo{SystemInfo: trustVolumeSpec("ca.pem")}}, - {Name: "meta", Source: &ateletpb.Volume_SystemInfo{SystemInfo: metadataVolumeSpec()}}, + {Name: "data", DurableDir: &ateletpb.DurableDirVolume{}}, + {Name: "trust", SystemInfo: trustVolumeSpec("ca.pem")}, + {Name: "meta", SystemInfo: metadataVolumeSpec()}, }} got := systemInfoVolumesFor("uid-1", spec) if len(got) != 2 || got[0].Name != "trust" || got[1].Name != "meta" { diff --git a/cmd/atelet/volumes_test.go b/cmd/atelet/volumes_test.go index e3b13ac9b3..cab35adcb2 100644 --- a/cmd/atelet/volumes_test.go +++ b/cmd/atelet/volumes_test.go @@ -91,27 +91,21 @@ func TestUnmountExternalVolumes(t *testing.T) { extVol1 := &ateletpb.Volume{ Name: "vol-1", - Source: &ateletpb.Volume_External{ - External: &ateletpb.ExternalVolumeSource{ - StorageVolumeId: "mock-vol-1", - VolumeType: "mock-driver", - }, + External: &ateletpb.ExternalVolumeSource{ + StorageVolumeId: "mock-vol-1", + VolumeType: "mock-driver", }, } extVol2 := &ateletpb.Volume{ Name: "vol-2", - Source: &ateletpb.Volume_External{ - External: &ateletpb.ExternalVolumeSource{ - StorageVolumeId: "mock-vol-2", - VolumeType: "mock-driver", - }, + External: &ateletpb.ExternalVolumeSource{ + StorageVolumeId: "mock-vol-2", + VolumeType: "mock-driver", }, } durableVol := &ateletpb.Volume{ - Name: "durable-1", - Source: &ateletpb.Volume_DurableDir{ - DurableDir: &ateletpb.DurableDirVolume{}, - }, + Name: "durable-1", + DurableDir: &ateletpb.DurableDirVolume{}, } t.Run("success", func(t *testing.T) { @@ -177,29 +171,23 @@ func TestMountExternalVolumes(t *testing.T) { extVol1 := &ateletpb.Volume{ Name: "vol-1", - Source: &ateletpb.Volume_External{ - External: &ateletpb.ExternalVolumeSource{ - StorageVolumeId: "mock-vol-1", - VolumeType: "mock-driver", - VolumeContext: map[string]string{"key": "val1"}, - }, + External: &ateletpb.ExternalVolumeSource{ + StorageVolumeId: "mock-vol-1", + VolumeType: "mock-driver", + VolumeContext: map[string]string{"key": "val1"}, }, } extVol2 := &ateletpb.Volume{ Name: "vol-2", - Source: &ateletpb.Volume_External{ - External: &ateletpb.ExternalVolumeSource{ - StorageVolumeId: "mock-vol-2", - VolumeType: "mock-driver", - VolumeContext: map[string]string{"key": "val2"}, - }, + External: &ateletpb.ExternalVolumeSource{ + StorageVolumeId: "mock-vol-2", + VolumeType: "mock-driver", + VolumeContext: map[string]string{"key": "val2"}, }, } durableVol := &ateletpb.Volume{ - Name: "durable-1", - Source: &ateletpb.Volume_DurableDir{ - DurableDir: &ateletpb.DurableDirVolume{}, - }, + Name: "durable-1", + DurableDir: &ateletpb.DurableDirVolume{}, } t.Run("success mounts external volumes and creates mount directory", func(t *testing.T) { @@ -275,11 +263,9 @@ func TestMountExternalVolumes(t *testing.T) { unknownVol := &ateletpb.Volume{ Name: "vol-unknown", - Source: &ateletpb.Volume_External{ - External: &ateletpb.ExternalVolumeSource{ - StorageVolumeId: "mock-vol-unknown", - VolumeType: "unknown-driver", - }, + External: &ateletpb.ExternalVolumeSource{ + StorageVolumeId: "mock-vol-unknown", + VolumeType: "unknown-driver", }, } @@ -394,20 +380,16 @@ func TestVolumeHostDirectoryCleanup(t *testing.T) { extVol1 := &ateletpb.Volume{ Name: "vol-1", - Source: &ateletpb.Volume_External{ - External: &ateletpb.ExternalVolumeSource{ - StorageVolumeId: "mock-vol-1", - VolumeType: "mock-driver", - }, + External: &ateletpb.ExternalVolumeSource{ + StorageVolumeId: "mock-vol-1", + VolumeType: "mock-driver", }, } extVol2 := &ateletpb.Volume{ Name: "vol-2", - Source: &ateletpb.Volume_External{ - External: &ateletpb.ExternalVolumeSource{ - StorageVolumeId: "mock-vol-2", - VolumeType: "mock-driver", - }, + External: &ateletpb.ExternalVolumeSource{ + StorageVolumeId: "mock-vol-2", + VolumeType: "mock-driver", }, } diff --git a/cmd/ateom-gvisor/stats_test.go b/cmd/ateom-gvisor/stats_test.go index 42f80e92da..c1f086a1fe 100644 --- a/cmd/ateom-gvisor/stats_test.go +++ b/cmd/ateom-gvisor/stats_test.go @@ -661,11 +661,12 @@ func TestDeadSandboxCheckDuringDrain(t *testing.T) { } // TestDrainNeverKillsPauseContainer: gracefulShutdown kills the names -// containerNames takes from the spec. atelet rejects a spec that names the -// pause container, so the drain cannot stop the sandbox itself. +// containerNames takes from the spec. atelet requires container names to be +// DNS-1123 labels (k8s-short-name) and the pause container's name is not one, +// so no spec can name it and the drain cannot stop the sandbox itself. func TestDrainNeverKillsPauseContainer(t *testing.T) { - if err := resources.ValidateContainerNames([]string{ocispec.PauseContainer}); err == nil { - t.Fatalf("ValidateContainerNames accepted %q; a spec could then put it on the drain's kill list", ocispec.PauseContainer) + if resources.IsValidResourceName(ocispec.PauseContainer) { + t.Fatalf("%q is a valid container name; a spec could then put it on the drain's kill list", ocispec.PauseContainer) } spec := []*ateompb.Container{{Name: "app"}, {Name: "sidecar"}} for _, name := range containerNames(spec) { diff --git a/cmd/benchmarking/isolate/fake-atelet/herder_test.go b/cmd/benchmarking/isolate/fake-atelet/herder_test.go index 4c90f158f3..33234f4899 100644 --- a/cmd/benchmarking/isolate/fake-atelet/herder_test.go +++ b/cmd/benchmarking/isolate/fake-atelet/herder_test.go @@ -98,8 +98,8 @@ func herderCalls(h *herder) map[string]func(context.Context) error { func externalCheckpoint(uri string) *ateletpb.CheckpointRequest { return &ateletpb.CheckpointRequest{ - Type: ateletpb.CheckpointType_CHECKPOINT_TYPE_EXTERNAL, - Config: &ateletpb.CheckpointRequest_ExternalConfig{ExternalConfig: &ateletpb.ExternalCheckpointConfiguration{SnapshotUri: uri}}, + Type: ateletpb.CheckpointType_CHECKPOINT_TYPE_EXTERNAL, + ExternalConfig: &ateletpb.ExternalCheckpointConfiguration{SnapshotUri: uri}, } } diff --git a/internal/proto/ateletpb/atelet.pb.go b/internal/proto/ateletpb/atelet.pb.go index af0456c727..2181e83c54 100644 --- a/internal/proto/ateletpb/atelet.pb.go +++ b/internal/proto/ateletpb/atelet.pb.go @@ -42,7 +42,7 @@ const ( ActorMetadataField_ACTOR_METADATA_FIELD_UNSPECIFIED ActorMetadataField = 0 ActorMetadataField_ACTOR_METADATA_FIELD_NAME ActorMetadataField = 1 ActorMetadataField_ACTOR_METADATA_FIELD_ATESPACE ActorMetadataField = 2 - ActorMetadataField_ACTOR_METADATA_FIELD_UID ActorMetadataField = 3 + ActorMetadataField_ACTOR_METADATA_FIELD_UID ActorMetadataField = 3 // Keep this in sync with the maximums on fields of this type. ) // Enum value maps for ActorMetadataField. @@ -96,7 +96,7 @@ const ( // Save snapshot only in local filesystem CheckpointType_CHECKPOINT_TYPE_LOCAL CheckpointType = 1 // Save snapshot to object storage - CheckpointType_CHECKPOINT_TYPE_EXTERNAL CheckpointType = 2 + CheckpointType_CHECKPOINT_TYPE_EXTERNAL CheckpointType = 2 // Keep this in sync with the maximums on fields of this type. ) // Enum value maps for CheckpointType. @@ -156,7 +156,7 @@ const ( SnapshotFidelity_SNAPSHOT_FIDELITY_ROOTFS SnapshotFidelity = 2 // Capture process memory plus the full filesystem delta on top of the OCI // image (including any attached DurableDir volumes). - SnapshotFidelity_SNAPSHOT_FIDELITY_MEMORY SnapshotFidelity = 3 + SnapshotFidelity_SNAPSHOT_FIDELITY_MEMORY SnapshotFidelity = 3 // Keep this in sync with the maximums on fields of this type. ) // Enum value maps for SnapshotFidelity. @@ -927,16 +927,36 @@ func (x *MintActorCertificateResponse) GetActorCertificates() [][]byte { type TerminateRequest struct { state protoimpl.MessageState `protogen:"open.v1"` - // If unset, the atelet will just cleanup node resources for the actor. - WorkerPodUid string `protobuf:"bytes,1,opt,name=worker_pod_uid,json=workerPodUid,proto3" json:"worker_pod_uid,omitempty"` - Atespace string `protobuf:"bytes,2,opt,name=atespace,proto3" json:"atespace,omitempty"` - ActorName string `protobuf:"bytes,3,opt,name=actor_name,json=actorName,proto3" json:"actor_name,omitempty"` - ActorUid string `protobuf:"bytes,4,opt,name=actor_uid,json=actorUid,proto3" json:"actor_uid,omitempty"` - ActorTemplateAtespace string `protobuf:"bytes,5,opt,name=actor_template_atespace,json=actorTemplateAtespace,proto3" json:"actor_template_atespace,omitempty"` - ActorTemplateName string `protobuf:"bytes,6,opt,name=actor_template_name,json=actorTemplateName,proto3" json:"actor_template_name,omitempty"` - Spec *WorkloadSpec `protobuf:"bytes,7,opt,name=spec,proto3" json:"spec,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // The UID of the worker pod whose ateom hosts the actor. If unset, atelet + // only cleans up the actor's node resources; it does not reach an ateom. + // + // +k8s:optional + // +k8s:format=k8s-uuid + WorkerPodUid string `protobuf:"bytes,1,opt,name=worker_pod_uid,json=workerPodUid,proto3" json:"worker_pod_uid,omitempty"` + // +k8s:required + // +k8s:format=k8s-short-name + Atespace string `protobuf:"bytes,2,opt,name=atespace,proto3" json:"atespace,omitempty"` + // +k8s:required + // +k8s:format=k8s-short-name + ActorName string `protobuf:"bytes,3,opt,name=actor_name,json=actorName,proto3" json:"actor_name,omitempty"` + // +k8s:required + // +k8s:format=k8s-uuid + ActorUid string `protobuf:"bytes,4,opt,name=actor_uid,json=actorUid,proto3" json:"actor_uid,omitempty"` + // The template identity is carried for metrics attribution. + // + // +k8s:optional + // +k8s:format=k8s-short-name + ActorTemplateAtespace string `protobuf:"bytes,5,opt,name=actor_template_atespace,json=actorTemplateAtespace,proto3" json:"actor_template_atespace,omitempty"` + // +k8s:optional + // +k8s:format=k8s-short-name + ActorTemplateName string `protobuf:"bytes,6,opt,name=actor_template_name,json=actorTemplateName,proto3" json:"actor_template_name,omitempty"` + // A nil spec is tolerated: the delete flow terminates template-less + // actors with a fallback spec. + // + // +k8s:optional + Spec *WorkloadSpec `protobuf:"bytes,7,opt,name=spec,proto3" json:"spec,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *TerminateRequest) Reset() { @@ -1055,24 +1075,51 @@ func (*TerminateResponse) Descriptor() ([]byte, []int) { } type RunRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - WorkerPodUid string `protobuf:"bytes,1,opt,name=worker_pod_uid,json=workerPodUid,proto3" json:"worker_pod_uid,omitempty"` - Atespace string `protobuf:"bytes,2,opt,name=atespace,proto3" json:"atespace,omitempty"` - ActorName string `protobuf:"bytes,3,opt,name=actor_name,json=actorName,proto3" json:"actor_name,omitempty"` - ActorUid string `protobuf:"bytes,4,opt,name=actor_uid,json=actorUid,proto3" json:"actor_uid,omitempty"` - ActorTemplateAtespace string `protobuf:"bytes,5,opt,name=actor_template_atespace,json=actorTemplateAtespace,proto3" json:"actor_template_atespace,omitempty"` - ActorTemplateName string `protobuf:"bytes,6,opt,name=actor_template_name,json=actorTemplateName,proto3" json:"actor_template_name,omitempty"` - Spec *WorkloadSpec `protobuf:"bytes,7,opt,name=spec,proto3" json:"spec,omitempty"` + state protoimpl.MessageState `protogen:"open.v1"` + // The UID of the worker pod whose ateom hosts the actor. + // + // +k8s:required + // +k8s:format=k8s-uuid + WorkerPodUid string `protobuf:"bytes,1,opt,name=worker_pod_uid,json=workerPodUid,proto3" json:"worker_pod_uid,omitempty"` + // +k8s:required + // +k8s:format=k8s-short-name + Atespace string `protobuf:"bytes,2,opt,name=atespace,proto3" json:"atespace,omitempty"` + // +k8s:required + // +k8s:format=k8s-short-name + ActorName string `protobuf:"bytes,3,opt,name=actor_name,json=actorName,proto3" json:"actor_name,omitempty"` + // +k8s:required + // +k8s:format=k8s-uuid + ActorUid string `protobuf:"bytes,4,opt,name=actor_uid,json=actorUid,proto3" json:"actor_uid,omitempty"` + // The template identity is carried for metrics attribution. + // + // +k8s:optional + // +k8s:format=k8s-short-name + ActorTemplateAtespace string `protobuf:"bytes,5,opt,name=actor_template_atespace,json=actorTemplateAtespace,proto3" json:"actor_template_atespace,omitempty"` + // +k8s:optional + // +k8s:format=k8s-short-name + ActorTemplateName string `protobuf:"bytes,6,opt,name=actor_template_name,json=actorTemplateName,proto3" json:"actor_template_name,omitempty"` + // +k8s:required + Spec *WorkloadSpec `protobuf:"bytes,7,opt,name=spec,proto3" json:"spec,omitempty"` // The sandbox binaries to use for booting this actor from scratch. atelet // fetches the relevant assets and records them with the actor's on-node state // so a later Checkpoint can pin the same version into the snapshot manifest. + // + // +k8s:required SandboxAssets *SandboxAssets `protobuf:"bytes,8,opt,name=sandbox_assets,json=sandboxAssets,proto3" json:"sandbox_assets,omitempty"` // When absent the actor has no egress: its TCP is captured and refused. + // + // +k8s:optional EgressGateway *EgressGateway `protobuf:"bytes,9,opt,name=egress_gateway,json=egressGateway,proto3,oneof" json:"egress_gateway,omitempty"` // The actor's declared size, from the ActorTemplate's resource limits. atelet // passes these through to the sandbox so it is sized to the actor (not the // whole host or worker pod). Zero means "unset": keep the runtime default. - CpuMilli int64 `protobuf:"varint,10,opt,name=cpu_milli,json=cpuMilli,proto3" json:"cpu_milli,omitempty"` // CPU limit in millicores (1000 = one core). + // + // +k8s:optional + // +k8s:minimum=0 + // +k8s:maximum=999999 # the control plane caps cpu limits strictly below 1000 cores + CpuMilli int64 `protobuf:"varint,10,opt,name=cpu_milli,json=cpuMilli,proto3" json:"cpu_milli,omitempty"` // CPU limit in millicores (1000 = one core). + // +k8s:optional + // +k8s:minimum=0 MemoryBytes int64 `protobuf:"varint,11,opt,name=memory_bytes,json=memoryBytes,proto3" json:"memory_bytes,omitempty"` // Memory limit in bytes. unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache @@ -1190,6 +1237,10 @@ type EgressGateway struct { state protoimpl.MessageState `protogen:"open.v1"` // address is required and identifies the remote gateway as an IP address or // DNS name followed by its port. + // + // +k8s:required + // +k8s:maxLength=261 # a bracketed or 253-character host, ':' and a 5-digit port + // +k8s:customValidation # host:port shape Address string `protobuf:"bytes,1,opt,name=address,proto3" json:"address,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache @@ -1241,8 +1292,15 @@ type AssetFile struct { // the plugin cannot serve it. // Masked in logged requests, since a signed URL carries its credential in // the query. + // + // +k8s:required + // +k8s:maxLength=2048 # matches the SandboxConfig asset url's bound Url string `protobuf:"bytes,1,opt,name=url,proto3" json:"url,omitempty"` // Lower-case hex SHA256; names the cached file and verifies its integrity. + // + // +k8s:required + // +k8s:maxLength=64 + // +k8s:customValidation # 64 hex characters Sha256 string `protobuf:"bytes,2,opt,name=sha256,proto3" json:"sha256,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache @@ -1295,8 +1353,11 @@ func (x *AssetFile) GetSha256() string { // ArchAssets is the set of assets for a single architecture, keyed by asset // name (a wrapper message because proto map values cannot themselves be maps). type ArchAssets struct { - state protoimpl.MessageState `protogen:"open.v1"` - Files map[string]*AssetFile `protobuf:"bytes,1,rep,name=files,proto3" json:"files,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` // asset name -> file + state protoimpl.MessageState `protogen:"open.v1"` + // +k8s:required + // +k8s:maxProperties=16 + // +k8s:eachKey=+k8s:maxLength=64 + Files map[string]*AssetFile `protobuf:"bytes,1,rep,name=files,proto3" json:"files,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` // asset name -> file unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -1344,14 +1405,27 @@ func (x *ArchAssets) GetFiles() map[string]*AssetFile { // atelet's backend code interprets the asset names (gVisor expects "gvisor", // the release tarball; legacy "runsc", a bare binary, is still accepted). type SandboxAssets struct { - state protoimpl.MessageState `protogen:"open.v1"` - SandboxClass string `protobuf:"bytes,1,opt,name=sandbox_class,json=sandboxClass,proto3" json:"sandbox_class,omitempty"` // e.g. "gvisor" - Assets map[string]*ArchAssets `protobuf:"bytes,2,rep,name=assets,proto3" json:"assets,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` // arch -> {name -> file} + state protoimpl.MessageState `protogen:"open.v1"` + // TODO: send this as an enum, like ateapi's and ateom's SandboxClass, so + // the tags can bound it and the custom validation goes away. + // + // +k8s:required + // +k8s:maxLength=63 # guardrail; the class names are a few characters + // +k8s:customValidation # gvisor or microvm + SandboxClass string `protobuf:"bytes,1,opt,name=sandbox_class,json=sandboxClass,proto3" json:"sandbox_class,omitempty"` // e.g. "gvisor" + // +k8s:required + // +k8s:maxProperties=8 # one entry per architecture + // +k8s:eachKey=+k8s:maxLength=16 + Assets map[string]*ArchAssets `protobuf:"bytes,2,rep,name=assets,proto3" json:"assets,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` // arch -> {name -> file} // pause_image is the image for the sandbox's root container. Like the // binaries above it is sandbox configuration, not workload configuration, // and atelet pins it into the snapshot manifest so a restore rebuilds the // sandbox from the same image. Empty for sandboxes without a pause // container (microvm). + // + // +k8s:optional + // +k8s:maxLength=512 + // +k8s:customValidation # must be a well-formed image reference, pinned by digest PauseImage string `protobuf:"bytes,3,opt,name=pause_image,json=pauseImage,proto3" json:"pause_image,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache @@ -1409,10 +1483,20 @@ func (x *SandboxAssets) GetPauseImage() string { } // WorkloadSpec parallels Pod, but with far fewer configurable fields. +// +// +k8s:customValidation # every volume mount names a declared volume type WorkloadSpec struct { - state protoimpl.MessageState `protogen:"open.v1"` - Containers []*Container `protobuf:"bytes,1,rep,name=containers,proto3" json:"containers,omitempty"` - Volumes []*Volume `protobuf:"bytes,2,rep,name=volumes,proto3" json:"volumes,omitempty"` + state protoimpl.MessageState `protogen:"open.v1"` + // +k8s:optional + // +k8s:maxItems=10 # matches the template's containers bound + // +k8s:listType=map + // +k8s:listMapKey=name + Containers []*Container `protobuf:"bytes,1,rep,name=containers,proto3" json:"containers,omitempty"` + // +k8s:optional + // +k8s:maxItems=32 # matches the template's volumes bound + // +k8s:listType=map + // +k8s:listMapKey=name + Volumes []*Volume `protobuf:"bytes,2,rep,name=volumes,proto3" json:"volumes,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -1498,12 +1582,29 @@ func (*DurableDirVolume) Descriptor() ([]byte, []int) { } type ExternalVolumeSource struct { - state protoimpl.MessageState `protogen:"open.v1"` - StorageVolumeId string `protobuf:"bytes,1,opt,name=storage_volume_id,json=storageVolumeId,proto3" json:"storage_volume_id,omitempty"` - VolumeType string `protobuf:"bytes,2,opt,name=volume_type,json=volumeType,proto3" json:"volume_type,omitempty"` - VolumeContext map[string]string `protobuf:"bytes,3,rep,name=volume_context,json=volumeContext,proto3" json:"volume_context,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + state protoimpl.MessageState `protogen:"open.v1"` + // Required, unlike the actor status counterpart: the control plane only + // sends volumes that finished provisioning. + // + // +k8s:required + // +k8s:maxLength=256 # matches ExternalVolume.storage_volume_id's bound + // +k8s:customValidation # no control characters + StorageVolumeId string `protobuf:"bytes,1,opt,name=storage_volume_id,json=storageVolumeId,proto3" json:"storage_volume_id,omitempty"` + // +k8s:optional + // +k8s:maxLength=253 # matches ExternalVolume.volume_type's bound + // +k8s:customValidation # optional "substrate.io/" prefix + DNS subdomain + VolumeType string `protobuf:"bytes,2,opt,name=volume_type,json=volumeType,proto3" json:"volume_type,omitempty"` + // volume_context is the driver's provisioning metadata. Its entries are the + // driver's own, so the map is only bounded, not validated. + // + // +k8s:optional + // +k8s:customValidation # at most 40 KiB in total, ten times the CSI limit for map fields + VolumeContext map[string]string `protobuf:"bytes,3,rep,name=volume_context,json=volumeContext,proto3" json:"volume_context,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` // publish_context is the driver's attachment metadata for this node, which - // the node plugin needs to complete the mount. + // the node plugin needs to complete the mount. Bounded like volume_context. + // + // +k8s:optional + // +k8s:customValidation # at most 40 KiB in total, ten times the CSI limit for map fields PublishContext map[string]string `protobuf:"bytes,4,rep,name=publish_context,json=publishContext,proto3" json:"publish_context,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache @@ -1568,8 +1669,13 @@ func (x *ExternalVolumeSource) GetPublishContext() map[string]string { } type ImageVolumeSource struct { - state protoimpl.MessageState `protogen:"open.v1"` - Reference string `protobuf:"bytes,1,opt,name=reference,proto3" json:"reference,omitempty"` + state protoimpl.MessageState `protogen:"open.v1"` + // reference is the OCI image reference, pinned by digest. + // + // +k8s:required + // +k8s:maxLength=512 # matches the template ImageVolumeSource.reference's bound + // +k8s:customValidation # must be a well-formed image reference, pinned by digest + Reference string `protobuf:"bytes,1,opt,name=reference,proto3" json:"reference,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -1614,9 +1720,19 @@ func (x *ImageVolumeSource) GetReference() string { // ActorMetadataItem projects one actor identity field to one file at the // given path, relative to the root of the enclosing system-info volume. type ActorMetadataItem struct { - state protoimpl.MessageState `protogen:"open.v1"` - Field ActorMetadataField `protobuf:"varint,1,opt,name=field,proto3,enum=atelet.ActorMetadataField" json:"field,omitempty"` - Path string `protobuf:"bytes,2,opt,name=path,proto3" json:"path,omitempty"` + state protoimpl.MessageState `protogen:"open.v1"` + // +k8s:required + // +k8s:minimum=1 + // +k8s:maximum=3 # keep this in sync with the ActorMetadataField enum + Field ActorMetadataField `protobuf:"varint,1,opt,name=field,proto3,enum=atelet.ActorMetadataField" json:"field,omitempty"` + // path must be a clean relative Unix path; the rule is shared with the + // control plane through internal/resources. + // + // +k8s:required + // +k8s:minLength=1 + // +k8s:maxLength=255 + // +k8s:customValidation # clean relative path + Path string `protobuf:"bytes,2,opt,name=path,proto3" json:"path,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -1668,8 +1784,16 @@ func (x *ActorMetadataItem) GetPath() string { // ActorMetadataDataSource projects the actor's identity fields to files, one // per item. Values are written raw with no trailing newline. type ActorMetadataDataSource struct { - state protoimpl.MessageState `protogen:"open.v1"` - Items []*ActorMetadataItem `protobuf:"bytes,1,rep,name=items,proto3" json:"items,omitempty"` + state protoimpl.MessageState `protogen:"open.v1"` + // items must not project the same field twice. + // + // +k8s:required + // +k8s:minItems=1 + // +k8s:maxItems=8 # matches the template's items bound + // +k8s:listType=atomic + // +k8s:unique=map + // +k8s:listMapKey=field + Items []*ActorMetadataItem `protobuf:"bytes,1,rep,name=items,proto3" json:"items,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -1713,9 +1837,22 @@ func (x *ActorMetadataDataSource) GetItems() []*ActorMetadataItem { // TrustBundleDataSource corresponds to TrustBundleDataSource in ateapi.proto type TrustBundleDataSource struct { - state protoimpl.MessageState `protogen:"open.v1"` - Path string `protobuf:"bytes,1,opt,name=path,proto3" json:"path,omitempty"` - Names []string `protobuf:"bytes,3,rep,name=names,proto3" json:"names,omitempty"` + state protoimpl.MessageState `protogen:"open.v1"` + // path must be a clean relative Unix path; the rule is shared with the + // control plane through internal/resources. + // + // +k8s:required + // +k8s:minLength=1 + // +k8s:maxLength=255 + // +k8s:customValidation # clean relative path + Path string `protobuf:"bytes,1,opt,name=path,proto3" json:"path,omitempty"` + // +k8s:required + // +k8s:minItems=1 + // +k8s:maxItems=8 # matches the template's names bound + // +k8s:listType=set + // +k8s:eachVal=+k8s:minLength=1 + // +k8s:eachVal=+k8s:maxLength=253 # matches the template's bundle-name bound + Names []string `protobuf:"bytes,3,rep,name=names,proto3" json:"names,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -1764,13 +1901,18 @@ func (x *TrustBundleDataSource) GetNames() []string { return nil } +// SystemInfoDataSource selects exactly one projection to place in the +// volume. type SystemInfoDataSource struct { state protoimpl.MessageState `protogen:"open.v1"` - // Types that are valid to be assigned to DataSource: + // Exactly one of actor_metadata / trust_bundle must be set. // - // *SystemInfoDataSource_ActorMetadata - // *SystemInfoDataSource_TrustBundle - DataSource isSystemInfoDataSource_DataSource `protobuf_oneof:"data_source"` + // +k8s:optional + // +k8s:unionMember + ActorMetadata *ActorMetadataDataSource `protobuf:"bytes,1,opt,name=actor_metadata,json=actorMetadata,proto3" json:"actor_metadata,omitempty"` + // +k8s:optional + // +k8s:unionMember + TrustBundle *TrustBundleDataSource `protobuf:"bytes,2,opt,name=trust_bundle,json=trustBundle,proto3" json:"trust_bundle,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -1805,52 +1947,29 @@ func (*SystemInfoDataSource) Descriptor() ([]byte, []int) { return file_atelet_proto_rawDescGZIP(), []int{26} } -func (x *SystemInfoDataSource) GetDataSource() isSystemInfoDataSource_DataSource { - if x != nil { - return x.DataSource - } - return nil -} - func (x *SystemInfoDataSource) GetActorMetadata() *ActorMetadataDataSource { if x != nil { - if x, ok := x.DataSource.(*SystemInfoDataSource_ActorMetadata); ok { - return x.ActorMetadata - } + return x.ActorMetadata } return nil } func (x *SystemInfoDataSource) GetTrustBundle() *TrustBundleDataSource { if x != nil { - if x, ok := x.DataSource.(*SystemInfoDataSource_TrustBundle); ok { - return x.TrustBundle - } + return x.TrustBundle } return nil } -type isSystemInfoDataSource_DataSource interface { - isSystemInfoDataSource_DataSource() -} - -type SystemInfoDataSource_ActorMetadata struct { - ActorMetadata *ActorMetadataDataSource `protobuf:"bytes,1,opt,name=actor_metadata,json=actorMetadata,proto3,oneof"` -} - -type SystemInfoDataSource_TrustBundle struct { - TrustBundle *TrustBundleDataSource `protobuf:"bytes,2,opt,name=trust_bundle,json=trustBundle,proto3,oneof"` -} - -func (*SystemInfoDataSource_ActorMetadata) isSystemInfoDataSource_DataSource() {} - -func (*SystemInfoDataSource_TrustBundle) isSystemInfoDataSource_DataSource() {} - // SystemInfoVolume is a read-only volume whose files are generated by atelet // on every Run/Restore, so they carry the values of the actor actually being // started, whatever checkpointed state it boots from. type SystemInfoVolume struct { - state protoimpl.MessageState `protogen:"open.v1"` + state protoimpl.MessageState `protogen:"open.v1"` + // +k8s:optional + // +k8s:maxItems=8 # matches the template's data_sources bound + // +k8s:listType=atomic + // +k8s:customValidation # at most one actor_metadata; paths unique across entries DataSources []*SystemInfoDataSource `protobuf:"bytes,1,rep,name=data_sources,json=dataSources,proto3" json:"data_sources,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache @@ -1893,16 +2012,26 @@ func (x *SystemInfoVolume) GetDataSources() []*SystemInfoDataSource { return nil } +// Volume names one volume and selects exactly one source for it. type Volume struct { state protoimpl.MessageState `protogen:"open.v1"` - Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` - // Types that are valid to be assigned to Source: - // - // *Volume_DurableDir - // *Volume_External - // *Volume_SystemInfo - // *Volume_Image - Source isVolume_Source `protobuf_oneof:"source"` + // +k8s:required + // +k8s:format=k8s-short-name + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + // Exactly one of durable_dir / external / system_info / image must be set. + // + // +k8s:optional + // +k8s:unionMember + DurableDir *DurableDirVolume `protobuf:"bytes,2,opt,name=durable_dir,json=durableDir,proto3" json:"durable_dir,omitempty"` + // +k8s:optional + // +k8s:unionMember + External *ExternalVolumeSource `protobuf:"bytes,3,opt,name=external,proto3" json:"external,omitempty"` + // +k8s:optional + // +k8s:unionMember + SystemInfo *SystemInfoVolume `protobuf:"bytes,4,opt,name=system_info,json=systemInfo,proto3" json:"system_info,omitempty"` + // +k8s:optional + // +k8s:unionMember + Image *ImageVolumeSource `protobuf:"bytes,5,opt,name=image,proto3" json:"image,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -1944,81 +2073,47 @@ func (x *Volume) GetName() string { return "" } -func (x *Volume) GetSource() isVolume_Source { - if x != nil { - return x.Source - } - return nil -} - func (x *Volume) GetDurableDir() *DurableDirVolume { if x != nil { - if x, ok := x.Source.(*Volume_DurableDir); ok { - return x.DurableDir - } + return x.DurableDir } return nil } func (x *Volume) GetExternal() *ExternalVolumeSource { if x != nil { - if x, ok := x.Source.(*Volume_External); ok { - return x.External - } + return x.External } return nil } func (x *Volume) GetSystemInfo() *SystemInfoVolume { if x != nil { - if x, ok := x.Source.(*Volume_SystemInfo); ok { - return x.SystemInfo - } + return x.SystemInfo } return nil } func (x *Volume) GetImage() *ImageVolumeSource { if x != nil { - if x, ok := x.Source.(*Volume_Image); ok { - return x.Image - } + return x.Image } return nil } -type isVolume_Source interface { - isVolume_Source() -} - -type Volume_DurableDir struct { - DurableDir *DurableDirVolume `protobuf:"bytes,2,opt,name=durable_dir,json=durableDir,proto3,oneof"` -} - -type Volume_External struct { - External *ExternalVolumeSource `protobuf:"bytes,3,opt,name=external,proto3,oneof"` -} - -type Volume_SystemInfo struct { - SystemInfo *SystemInfoVolume `protobuf:"bytes,4,opt,name=system_info,json=systemInfo,proto3,oneof"` -} - -type Volume_Image struct { - Image *ImageVolumeSource `protobuf:"bytes,5,opt,name=image,proto3,oneof"` -} - -func (*Volume_DurableDir) isVolume_Source() {} - -func (*Volume_External) isVolume_Source() {} - -func (*Volume_SystemInfo) isVolume_Source() {} - -func (*Volume_Image) isVolume_Source() {} - type VolumeMount struct { - state protoimpl.MessageState `protogen:"open.v1"` - Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` - MountPath string `protobuf:"bytes,2,opt,name=mount_path,json=mountPath,proto3" json:"mount_path,omitempty"` + state protoimpl.MessageState `protogen:"open.v1"` + // name must match the name of a Volume. + // + // +k8s:required + // +k8s:format=k8s-short-name + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + // mount_path within the container. Must be a clean absolute Unix path. + // + // +k8s:required + // +k8s:maxLength=4096 + // +k8s:customValidation # clean-absolute-path shape; no regex/pattern tag exists + MountPath string `protobuf:"bytes,2,opt,name=mount_path,json=mountPath,proto3" json:"mount_path,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -2068,17 +2163,46 @@ func (x *VolumeMount) GetMountPath() string { } type Container struct { - state protoimpl.MessageState `protogen:"open.v1"` - Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` - Image string `protobuf:"bytes,2,opt,name=image,proto3" json:"image,omitempty"` - Command []string `protobuf:"bytes,3,rep,name=command,proto3" json:"command,omitempty"` - Args []string `protobuf:"bytes,7,rep,name=args,proto3" json:"args,omitempty"` - Env []*EnvEntry `protobuf:"bytes,4,rep,name=env,proto3" json:"env,omitempty"` - WakeupProbe *WakeupProbe `protobuf:"bytes,5,opt,name=wakeup_probe,json=wakeupProbe,proto3" json:"wakeup_probe,omitempty"` - VolumeMounts []*VolumeMount `protobuf:"bytes,6,rep,name=volume_mounts,json=volumeMounts,proto3" json:"volume_mounts,omitempty"` - SecurityContext *SecurityContext `protobuf:"bytes,8,opt,name=security_context,json=securityContext,proto3" json:"security_context,omitempty"` + state protoimpl.MessageState `protogen:"open.v1"` + // +k8s:required + // +k8s:format=k8s-short-name + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + // +k8s:required + // +k8s:maxLength=512 # matches the template Container.image's bound + // +k8s:customValidation # must be a well-formed image reference, pinned by digest + Image string `protobuf:"bytes,2,opt,name=image,proto3" json:"image,omitempty"` + // +k8s:optional + // +k8s:maxItems=64 + // +k8s:listType=atomic + // +k8s:eachVal=+k8s:maxLength=4096 # argv strings; guardrail, not a contract + Command []string `protobuf:"bytes,3,rep,name=command,proto3" json:"command,omitempty"` + // +k8s:optional + // +k8s:maxItems=64 + // +k8s:listType=atomic + // +k8s:eachVal=+k8s:maxLength=4096 # argv strings; guardrail, not a contract + Args []string `protobuf:"bytes,7,rep,name=args,proto3" json:"args,omitempty"` + // +k8s:optional + // +k8s:maxItems=32 # matches the template's env bound + // +k8s:listType=map # each variable is set at most once + // +k8s:listMapKey=name + Env []*EnvEntry `protobuf:"bytes,4,rep,name=env,proto3" json:"env,omitempty"` + // +k8s:optional + WakeupProbe *WakeupProbe `protobuf:"bytes,5,opt,name=wakeup_probe,json=wakeupProbe,proto3" json:"wakeup_probe,omitempty"` + // Keyed by mount_path: each path hosts exactly one mount, while a volume + // may be mounted at multiple paths. + // + // +k8s:optional + // +k8s:maxItems=32 # matches the template's volume_mounts bound + // +k8s:listType=map + // +k8s:listMapKey=mount_path + // +k8s:customValidation # mounts must not nest + VolumeMounts []*VolumeMount `protobuf:"bytes,6,rep,name=volume_mounts,json=volumeMounts,proto3" json:"volume_mounts,omitempty"` + // +k8s:optional + SecurityContext *SecurityContext `protobuf:"bytes,8,opt,name=security_context,json=securityContext,proto3" json:"security_context,omitempty"` // resources are the cgroup limits for this container, resolved by // ate-api-server from the ActorTemplate. Unset means no limits. + // + // +k8s:optional Resources *ResourceLimits `protobuf:"bytes,9,opt,name=resources,proto3" json:"resources,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache @@ -2179,8 +2303,9 @@ func (x *Container) GetResources() *ResourceLimits { // SecurityContext holds security settings for a container's process. type SecurityContext struct { - state protoimpl.MessageState `protogen:"open.v1"` - Capabilities *Capabilities `protobuf:"bytes,1,opt,name=capabilities,proto3" json:"capabilities,omitempty"` + state protoimpl.MessageState `protogen:"open.v1"` + // +k8s:optional + Capabilities *Capabilities `protobuf:"bytes,1,opt,name=capabilities,proto3" json:"capabilities,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -2225,9 +2350,17 @@ func (x *SecurityContext) GetCapabilities() *Capabilities { // Capabilities adjusts a container's Linux capabilities relative to the default // set. Names carry no "CAP_" prefix; drop applies before add. type Capabilities struct { - state protoimpl.MessageState `protogen:"open.v1"` - Add []string `protobuf:"bytes,1,rep,name=add,proto3" json:"add,omitempty"` - Drop []string `protobuf:"bytes,2,rep,name=drop,proto3" json:"drop,omitempty"` + state protoimpl.MessageState `protogen:"open.v1"` + // +k8s:optional + // +k8s:maxItems=64 + // +k8s:listType=set + // +k8s:customValidation # capability grammar; "ALL" not accepted + Add []string `protobuf:"bytes,1,rep,name=add,proto3" json:"add,omitempty"` + // +k8s:optional + // +k8s:maxItems=64 + // +k8s:listType=set + // +k8s:customValidation # capability grammar + Drop []string `protobuf:"bytes,2,rep,name=drop,proto3" json:"drop,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -2281,8 +2414,15 @@ func (x *Capabilities) GetDrop() []string { type ResourceLimits struct { state protoimpl.MessageState `protogen:"open.v1"` // memory_bytes is the memory limit in bytes. 0 means unset. + // + // +k8s:optional + // +k8s:minimum=0 MemoryBytes int64 `protobuf:"varint,1,opt,name=memory_bytes,json=memoryBytes,proto3" json:"memory_bytes,omitempty"` // cpu_millis is the CPU limit in milli-cores (1000 = one core). 0 means unset. + // + // +k8s:optional + // +k8s:minimum=0 + // +k8s:maximum=999999 # the control plane caps cpu limits strictly below 1000 cores CpuMillis int64 `protobuf:"varint,2,opt,name=cpu_millis,json=cpuMillis,proto3" json:"cpu_millis,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache @@ -2333,9 +2473,16 @@ func (x *ResourceLimits) GetCpuMillis() int64 { } type EnvEntry struct { - state protoimpl.MessageState `protogen:"open.v1"` - Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` - Value string `protobuf:"bytes,2,opt,name=value,proto3" json:"value,omitempty"` + state protoimpl.MessageState `protogen:"open.v1"` + // name may be any printable ASCII character except '='. + // + // +k8s:required + // +k8s:maxLength=256 # guardrail + // +k8s:customValidation # printable ASCII except '=' + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + // +k8s:optional + // +k8s:maxLength=32768 # guardrail + Value string `protobuf:"bytes,2,opt,name=value,proto3" json:"value,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -2387,9 +2534,14 @@ func (x *EnvEntry) GetValue() string { // WakeupProbe describes how to check that a container is ready to serve. // Only HTTP is supported today. type WakeupProbe struct { - state protoimpl.MessageState `protogen:"open.v1"` - HttpGet *HttpGetAction `protobuf:"bytes,1,opt,name=http_get,json=httpGet,proto3" json:"http_get,omitempty"` + state protoimpl.MessageState `protogen:"open.v1"` + // +k8s:required + HttpGet *HttpGetAction `protobuf:"bytes,1,opt,name=http_get,json=httpGet,proto3" json:"http_get,omitempty"` // How long to keep polling before giving up and failing the actor start. + // + // +k8s:required + // +k8s:minimum=1 + // +k8s:maximum=3600 # matches the template wakeup probe's bound TimeoutSeconds int32 `protobuf:"varint,2,opt,name=timeout_seconds,json=timeoutSeconds,proto3" json:"timeout_seconds,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache @@ -2443,8 +2595,16 @@ func (x *WakeupProbe) GetTimeoutSeconds() int32 { type HttpGetAction struct { state protoimpl.MessageState `protogen:"open.v1"` // Path to access on the HTTP server. + // + // +k8s:required + // +k8s:maxLength=1024 # matches the template probe path's bound + // +k8s:customValidation # RFC 3986 path shape; no regex/pattern tag exists Path string `protobuf:"bytes,1,opt,name=path,proto3" json:"path,omitempty"` // TCP port to connect to (1..65535). + // + // +k8s:required + // +k8s:minimum=1 + // +k8s:maximum=65535 Port int32 `protobuf:"varint,2,opt,name=port,proto3" json:"port,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache @@ -2536,6 +2696,9 @@ type LocalCheckpointConfiguration struct { // data will be stored. atelet decides where that directory lives, from the // actor's UID, so this is a bare name and must not contain a path separator. // The structure of the checkpoint should generally be treated as opaque. + // + // +k8s:required + // +k8s:format=k8s-short-name SnapshotName string `protobuf:"bytes,1,opt,name=snapshot_name,json=snapshotName,proto3" json:"snapshot_name,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache @@ -2582,6 +2745,10 @@ type ExternalCheckpointConfiguration struct { state protoimpl.MessageState `protogen:"open.v1"` // The object storage URI of the snapshot to write. Object names are appended // to it, so it addresses the snapshot as a whole rather than any one object. + // + // +k8s:required + // +k8s:maxLength=2048 + // +k8s:customValidation # a parseable snapshot URI SnapshotUri string `protobuf:"bytes,1,opt,name=snapshot_uri,json=snapshotUri,proto3" json:"snapshot_uri,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache @@ -2632,6 +2799,10 @@ type ExternalRestoreConfiguration struct { // The object storage URI of the snapshot to read. Object names are // appended to it, so it addresses the snapshot as a whole rather than any // one object. + // + // +k8s:required + // +k8s:maxLength=2048 + // +k8s:customValidation # a parseable snapshot URI SnapshotUri string `protobuf:"bytes,1,opt,name=snapshot_uri,json=snapshotUri,proto3" json:"snapshot_uri,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache @@ -2674,27 +2845,56 @@ func (x *ExternalRestoreConfiguration) GetSnapshotUri() string { return "" } +// +k8s:customValidation # the set config matches type type CheckpointRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - WorkerPodUid string `protobuf:"bytes,1,opt,name=worker_pod_uid,json=workerPodUid,proto3" json:"worker_pod_uid,omitempty"` - Atespace string `protobuf:"bytes,2,opt,name=atespace,proto3" json:"atespace,omitempty"` - ActorName string `protobuf:"bytes,3,opt,name=actor_name,json=actorName,proto3" json:"actor_name,omitempty"` - ActorUid string `protobuf:"bytes,4,opt,name=actor_uid,json=actorUid,proto3" json:"actor_uid,omitempty"` - ActorTemplateAtespace string `protobuf:"bytes,5,opt,name=actor_template_atespace,json=actorTemplateAtespace,proto3" json:"actor_template_atespace,omitempty"` - ActorTemplateName string `protobuf:"bytes,6,opt,name=actor_template_name,json=actorTemplateName,proto3" json:"actor_template_name,omitempty"` + state protoimpl.MessageState `protogen:"open.v1"` + // The UID of the worker pod whose ateom hosts the actor. + // + // +k8s:required + // +k8s:format=k8s-uuid + WorkerPodUid string `protobuf:"bytes,1,opt,name=worker_pod_uid,json=workerPodUid,proto3" json:"worker_pod_uid,omitempty"` + // +k8s:required + // +k8s:format=k8s-short-name + Atespace string `protobuf:"bytes,2,opt,name=atespace,proto3" json:"atespace,omitempty"` + // +k8s:required + // +k8s:format=k8s-short-name + ActorName string `protobuf:"bytes,3,opt,name=actor_name,json=actorName,proto3" json:"actor_name,omitempty"` + // +k8s:required + // +k8s:format=k8s-uuid + ActorUid string `protobuf:"bytes,4,opt,name=actor_uid,json=actorUid,proto3" json:"actor_uid,omitempty"` + // The template identity is carried for metrics attribution. + // + // +k8s:optional + // +k8s:format=k8s-short-name + ActorTemplateAtespace string `protobuf:"bytes,5,opt,name=actor_template_atespace,json=actorTemplateAtespace,proto3" json:"actor_template_atespace,omitempty"` + // +k8s:optional + // +k8s:format=k8s-short-name + ActorTemplateName string `protobuf:"bytes,6,opt,name=actor_template_name,json=actorTemplateName,proto3" json:"actor_template_name,omitempty"` // Sandbox binary config is not sent on checkpoint: atelet uses the version the // actor is currently running (recorded with the actor's on-node state at // Run/Restore) and records it into the snapshot manifest. - Spec *WorkloadSpec `protobuf:"bytes,7,opt,name=spec,proto3" json:"spec,omitempty"` + // + // +k8s:required + Spec *WorkloadSpec `protobuf:"bytes,7,opt,name=spec,proto3" json:"spec,omitempty"` + // +k8s:required + // +k8s:minimum=1 + // +k8s:maximum=2 # keep this in sync with the CheckpointType enum Type CheckpointType `protobuf:"varint,8,opt,name=type,proto3,enum=atelet.CheckpointType" json:"type,omitempty"` - // The checkpoint configuration, depending on the type. + // The checkpoint configuration: local_config when type is LOCAL, + // external_config when it is EXTERNAL. Exactly one is set. // - // Types that are valid to be assigned to Config: + // +k8s:optional + // +k8s:unionMember + LocalConfig *LocalCheckpointConfiguration `protobuf:"bytes,9,opt,name=local_config,json=localConfig,proto3" json:"local_config,omitempty"` + // +k8s:optional + // +k8s:unionMember + ExternalConfig *ExternalCheckpointConfiguration `protobuf:"bytes,10,opt,name=external_config,json=externalConfig,proto3" json:"external_config,omitempty"` + // Fidelity the checkpoint captures. Which levels a runtime can serve is the + // runtime's call: atelet forwards it and ateom rejects what it cannot capture. // - // *CheckpointRequest_LocalConfig - // *CheckpointRequest_ExternalConfig - Config isCheckpointRequest_Config `protobuf_oneof:"config"` - // Fidelity the checkpoint captures. + // +k8s:required + // +k8s:minimum=1 + // +k8s:maximum=3 # keep this in sync with the SnapshotFidelity enum Fidelity SnapshotFidelity `protobuf:"varint,11,opt,name=fidelity,proto3,enum=atelet.SnapshotFidelity" json:"fidelity,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache @@ -2786,27 +2986,16 @@ func (x *CheckpointRequest) GetType() CheckpointType { return CheckpointType_CHECKPOINT_TYPE_UNSPECIFIED } -func (x *CheckpointRequest) GetConfig() isCheckpointRequest_Config { - if x != nil { - return x.Config - } - return nil -} - func (x *CheckpointRequest) GetLocalConfig() *LocalCheckpointConfiguration { if x != nil { - if x, ok := x.Config.(*CheckpointRequest_LocalConfig); ok { - return x.LocalConfig - } + return x.LocalConfig } return nil } func (x *CheckpointRequest) GetExternalConfig() *ExternalCheckpointConfiguration { if x != nil { - if x, ok := x.Config.(*CheckpointRequest_ExternalConfig); ok { - return x.ExternalConfig - } + return x.ExternalConfig } return nil } @@ -2818,22 +3007,6 @@ func (x *CheckpointRequest) GetFidelity() SnapshotFidelity { return SnapshotFidelity_SNAPSHOT_FIDELITY_UNSPECIFIED } -type isCheckpointRequest_Config interface { - isCheckpointRequest_Config() -} - -type CheckpointRequest_LocalConfig struct { - LocalConfig *LocalCheckpointConfiguration `protobuf:"bytes,9,opt,name=local_config,json=localConfig,proto3,oneof"` -} - -type CheckpointRequest_ExternalConfig struct { - ExternalConfig *ExternalCheckpointConfiguration `protobuf:"bytes,10,opt,name=external_config,json=externalConfig,proto3,oneof"` -} - -func (*CheckpointRequest_LocalConfig) isCheckpointRequest_Config() {} - -func (*CheckpointRequest_ExternalConfig) isCheckpointRequest_Config() {} - type CheckpointResponse struct { state protoimpl.MessageState `protogen:"open.v1"` unknownFields protoimpl.UnknownFields @@ -2871,22 +3044,47 @@ func (*CheckpointResponse) Descriptor() ([]byte, []int) { } type UploadPausedCheckpointRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - Atespace string `protobuf:"bytes,1,opt,name=atespace,proto3" json:"atespace,omitempty"` - ActorName string `protobuf:"bytes,2,opt,name=actor_name,json=actorName,proto3" json:"actor_name,omitempty"` - ActorUid string `protobuf:"bytes,3,opt,name=actor_uid,json=actorUid,proto3" json:"actor_uid,omitempty"` + state protoimpl.MessageState `protogen:"open.v1"` + // Golden actors are never paused, so the golden atespace is rejected. + // + // +k8s:required + // +k8s:format=k8s-short-name + // +k8s:customValidation # not the golden atespace + Atespace string `protobuf:"bytes,1,opt,name=atespace,proto3" json:"atespace,omitempty"` + // +k8s:required + // +k8s:format=k8s-short-name + ActorName string `protobuf:"bytes,2,opt,name=actor_name,json=actorName,proto3" json:"actor_name,omitempty"` + // +k8s:required + // +k8s:format=k8s-uuid + ActorUid string `protobuf:"bytes,3,opt,name=actor_uid,json=actorUid,proto3" json:"actor_uid,omitempty"` // For metrics attribution, like on CheckpointRequest. + // + // +k8s:optional + // +k8s:format=k8s-short-name ActorTemplateAtespace string `protobuf:"bytes,4,opt,name=actor_template_atespace,json=actorTemplateAtespace,proto3" json:"actor_template_atespace,omitempty"` - ActorTemplateName string `protobuf:"bytes,5,opt,name=actor_template_name,json=actorTemplateName,proto3" json:"actor_template_name,omitempty"` + // +k8s:optional + // +k8s:format=k8s-short-name + ActorTemplateName string `protobuf:"bytes,5,opt,name=actor_template_name,json=actorTemplateName,proto3" json:"actor_template_name,omitempty"` // The local checkpoint to upload: the uuid of the actor's pause snapshot - // (ateapi Snapshot.uuid). + // (ateapi Snapshot.uuid, validated as a short name there too). + // + // +k8s:required + // +k8s:format=k8s-short-name LocalSnapshotName string `protobuf:"bytes,6,opt,name=local_snapshot_name,json=localSnapshotName,proto3" json:"local_snapshot_name,omitempty"` // Destination object-storage URI (the actor's in-progress snapshot URI). + // + // +k8s:required + // +k8s:maxLength=2048 + // +k8s:customValidation # a parseable snapshot URI DestinationSnapshotUri string `protobuf:"bytes,7,opt,name=destination_snapshot_uri,json=destinationSnapshotUri,proto3" json:"destination_snapshot_uri,omitempty"` // Fidelity the uploaded snapshot must have. The fidelity the pause // checkpoint captured is not sent: atelet reads it from the local // snapshot's own manifest, which is authoritative, and rejects the upload // if the two differ. + // + // +k8s:required + // +k8s:minimum=1 + // +k8s:maximum=3 # keep this in sync with the SnapshotFidelity enum DesiredFidelity SnapshotFidelity `protobuf:"varint,8,opt,name=desired_fidelity,json=desiredFidelity,proto3,enum=atelet.SnapshotFidelity" json:"desired_fidelity,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache @@ -3014,35 +3212,72 @@ func (*UploadPausedCheckpointResponse) Descriptor() ([]byte, []int) { return file_atelet_proto_rawDescGZIP(), []int{44} } +// +k8s:customValidation # the set config matches type type RestoreRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - WorkerPodUid string `protobuf:"bytes,1,opt,name=worker_pod_uid,json=workerPodUid,proto3" json:"worker_pod_uid,omitempty"` - Atespace string `protobuf:"bytes,2,opt,name=atespace,proto3" json:"atespace,omitempty"` - ActorName string `protobuf:"bytes,3,opt,name=actor_name,json=actorName,proto3" json:"actor_name,omitempty"` - ActorUid string `protobuf:"bytes,4,opt,name=actor_uid,json=actorUid,proto3" json:"actor_uid,omitempty"` - ActorTemplateAtespace string `protobuf:"bytes,5,opt,name=actor_template_atespace,json=actorTemplateAtespace,proto3" json:"actor_template_atespace,omitempty"` - ActorTemplateName string `protobuf:"bytes,6,opt,name=actor_template_name,json=actorTemplateName,proto3" json:"actor_template_name,omitempty"` - Spec *WorkloadSpec `protobuf:"bytes,7,opt,name=spec,proto3" json:"spec,omitempty"` - Type CheckpointType `protobuf:"varint,8,opt,name=type,proto3,enum=atelet.CheckpointType" json:"type,omitempty"` - // The checkpoint configuration, depending on the type. - // - // Types that are valid to be assigned to Config: - // - // *RestoreRequest_LocalConfig - // *RestoreRequest_ExternalConfig - Config isRestoreRequest_Config `protobuf_oneof:"config"` + state protoimpl.MessageState `protogen:"open.v1"` + // The UID of the worker pod whose ateom hosts the actor. + // + // +k8s:required + // +k8s:format=k8s-uuid + WorkerPodUid string `protobuf:"bytes,1,opt,name=worker_pod_uid,json=workerPodUid,proto3" json:"worker_pod_uid,omitempty"` + // +k8s:required + // +k8s:format=k8s-short-name + Atespace string `protobuf:"bytes,2,opt,name=atespace,proto3" json:"atespace,omitempty"` + // +k8s:required + // +k8s:format=k8s-short-name + ActorName string `protobuf:"bytes,3,opt,name=actor_name,json=actorName,proto3" json:"actor_name,omitempty"` + // +k8s:required + // +k8s:format=k8s-uuid + ActorUid string `protobuf:"bytes,4,opt,name=actor_uid,json=actorUid,proto3" json:"actor_uid,omitempty"` + // The template identity is carried for metrics attribution. + // + // +k8s:optional + // +k8s:format=k8s-short-name + ActorTemplateAtespace string `protobuf:"bytes,5,opt,name=actor_template_atespace,json=actorTemplateAtespace,proto3" json:"actor_template_atespace,omitempty"` + // +k8s:optional + // +k8s:format=k8s-short-name + ActorTemplateName string `protobuf:"bytes,6,opt,name=actor_template_name,json=actorTemplateName,proto3" json:"actor_template_name,omitempty"` + // +k8s:required + Spec *WorkloadSpec `protobuf:"bytes,7,opt,name=spec,proto3" json:"spec,omitempty"` + // +k8s:required + // +k8s:minimum=1 + // +k8s:maximum=2 # keep this in sync with the CheckpointType enum + Type CheckpointType `protobuf:"varint,8,opt,name=type,proto3,enum=atelet.CheckpointType" json:"type,omitempty"` + // The checkpoint configuration: local_config when type is LOCAL, + // external_config when it is EXTERNAL. Exactly one is set. + // + // +k8s:optional + // +k8s:unionMember + LocalConfig *LocalCheckpointConfiguration `protobuf:"bytes,9,opt,name=local_config,json=localConfig,proto3" json:"local_config,omitempty"` + // +k8s:optional + // +k8s:unionMember + ExternalConfig *ExternalRestoreConfiguration `protobuf:"bytes,10,opt,name=external_config,json=externalConfig,proto3" json:"external_config,omitempty"` // Fidelity to restore from the checkpoint. + // + // +k8s:required + // +k8s:minimum=1 + // +k8s:maximum=3 # keep this in sync with the SnapshotFidelity enum Fidelity SnapshotFidelity `protobuf:"varint,11,opt,name=fidelity,proto3,enum=atelet.SnapshotFidelity" json:"fidelity,omitempty"` // When absent the actor has no egress: its TCP is captured and refused. + // + // +k8s:optional EgressGateway *EgressGateway `protobuf:"bytes,13,opt,name=egress_gateway,json=egressGateway,proto3,oneof" json:"egress_gateway,omitempty"` // The actor's declared size, from the ActorTemplate's resource limits. For // gVisor and micro-VM VOLUMES restores the sandbox is (re)sized to these; // for a MEMORY micro-VM restore the size baked into the snapshot wins. Zero // means "unset": keep the runtime default. - CpuMilli int64 `protobuf:"varint,14,opt,name=cpu_milli,json=cpuMilli,proto3" json:"cpu_milli,omitempty"` // CPU limit in millicores (1000 = one core). + // + // +k8s:optional + // +k8s:minimum=0 + // +k8s:maximum=999999 # the control plane caps cpu limits strictly below 1000 cores + CpuMilli int64 `protobuf:"varint,14,opt,name=cpu_milli,json=cpuMilli,proto3" json:"cpu_milli,omitempty"` // CPU limit in millicores (1000 = one core). + // +k8s:optional + // +k8s:minimum=0 MemoryBytes int64 `protobuf:"varint,15,opt,name=memory_bytes,json=memoryBytes,proto3" json:"memory_bytes,omitempty"` // Memory limit in bytes. // The sandbox binaries and pause image to restore with, resolved from the - // ActorTemplate's SandboxConfig. Required. + // ActorTemplate's SandboxConfig. + // + // +k8s:required SandboxAssets *SandboxAssets `protobuf:"bytes,16,opt,name=sandbox_assets,json=sandboxAssets,proto3" json:"sandbox_assets,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache @@ -3134,27 +3369,16 @@ func (x *RestoreRequest) GetType() CheckpointType { return CheckpointType_CHECKPOINT_TYPE_UNSPECIFIED } -func (x *RestoreRequest) GetConfig() isRestoreRequest_Config { - if x != nil { - return x.Config - } - return nil -} - func (x *RestoreRequest) GetLocalConfig() *LocalCheckpointConfiguration { if x != nil { - if x, ok := x.Config.(*RestoreRequest_LocalConfig); ok { - return x.LocalConfig - } + return x.LocalConfig } return nil } func (x *RestoreRequest) GetExternalConfig() *ExternalRestoreConfiguration { if x != nil { - if x, ok := x.Config.(*RestoreRequest_ExternalConfig); ok { - return x.ExternalConfig - } + return x.ExternalConfig } return nil } @@ -3194,22 +3418,6 @@ func (x *RestoreRequest) GetSandboxAssets() *SandboxAssets { return nil } -type isRestoreRequest_Config interface { - isRestoreRequest_Config() -} - -type RestoreRequest_LocalConfig struct { - LocalConfig *LocalCheckpointConfiguration `protobuf:"bytes,9,opt,name=local_config,json=localConfig,proto3,oneof"` -} - -type RestoreRequest_ExternalConfig struct { - ExternalConfig *ExternalRestoreConfiguration `protobuf:"bytes,10,opt,name=external_config,json=externalConfig,proto3,oneof"` -} - -func (*RestoreRequest_LocalConfig) isRestoreRequest_Config() {} - -func (*RestoreRequest_ExternalConfig) isRestoreRequest_Config() {} - type RestoreResponse struct { state protoimpl.MessageState `protogen:"open.v1"` unknownFields protoimpl.UnknownFields @@ -3362,22 +3570,20 @@ const file_atelet_proto_rawDesc = "" + "\x05items\x18\x01 \x03(\v2\x19.atelet.ActorMetadataItemR\x05items\"M\n" + "\x15TrustBundleDataSource\x12\x12\n" + "\x04path\x18\x01 \x01(\tR\x04path\x12\x14\n" + - "\x05names\x18\x03 \x03(\tR\x05namesJ\x04\b\x02\x10\x03R\x04name\"\xb3\x01\n" + - "\x14SystemInfoDataSource\x12H\n" + - "\x0eactor_metadata\x18\x01 \x01(\v2\x1f.atelet.ActorMetadataDataSourceH\x00R\ractorMetadata\x12B\n" + - "\ftrust_bundle\x18\x02 \x01(\v2\x1d.atelet.TrustBundleDataSourceH\x00R\vtrustBundleB\r\n" + - "\vdata_source\"S\n" + + "\x05names\x18\x03 \x03(\tR\x05namesJ\x04\b\x02\x10\x03R\x04name\"\xa0\x01\n" + + "\x14SystemInfoDataSource\x12F\n" + + "\x0eactor_metadata\x18\x01 \x01(\v2\x1f.atelet.ActorMetadataDataSourceR\ractorMetadata\x12@\n" + + "\ftrust_bundle\x18\x02 \x01(\v2\x1d.atelet.TrustBundleDataSourceR\vtrustBundle\"S\n" + "\x10SystemInfoVolume\x12?\n" + - "\fdata_sources\x18\x01 \x03(\v2\x1c.atelet.SystemInfoDataSourceR\vdataSources\"\x8f\x02\n" + + "\fdata_sources\x18\x01 \x03(\v2\x1c.atelet.SystemInfoDataSourceR\vdataSources\"\xfd\x01\n" + "\x06Volume\x12\x12\n" + - "\x04name\x18\x01 \x01(\tR\x04name\x12;\n" + - "\vdurable_dir\x18\x02 \x01(\v2\x18.atelet.DurableDirVolumeH\x00R\n" + - "durableDir\x12:\n" + - "\bexternal\x18\x03 \x01(\v2\x1c.atelet.ExternalVolumeSourceH\x00R\bexternal\x12;\n" + - "\vsystem_info\x18\x04 \x01(\v2\x18.atelet.SystemInfoVolumeH\x00R\n" + - "systemInfo\x121\n" + - "\x05image\x18\x05 \x01(\v2\x19.atelet.ImageVolumeSourceH\x00R\x05imageB\b\n" + - "\x06source\"@\n" + + "\x04name\x18\x01 \x01(\tR\x04name\x129\n" + + "\vdurable_dir\x18\x02 \x01(\v2\x18.atelet.DurableDirVolumeR\n" + + "durableDir\x128\n" + + "\bexternal\x18\x03 \x01(\v2\x1c.atelet.ExternalVolumeSourceR\bexternal\x129\n" + + "\vsystem_info\x18\x04 \x01(\v2\x18.atelet.SystemInfoVolumeR\n" + + "systemInfo\x12/\n" + + "\x05image\x18\x05 \x01(\v2\x19.atelet.ImageVolumeSourceR\x05image\"@\n" + "\vVolumeMount\x12\x12\n" + "\x04name\x18\x01 \x01(\tR\x04name\x12\x1d\n" + "\n" + @@ -3416,7 +3622,7 @@ const file_atelet_proto_rawDesc = "" + "\x1fExternalCheckpointConfiguration\x12!\n" + "\fsnapshot_uri\x18\x01 \x01(\tR\vsnapshotUri\"A\n" + "\x1cExternalRestoreConfiguration\x12!\n" + - "\fsnapshot_uri\x18\x01 \x01(\tR\vsnapshotUri\"\xae\x04\n" + + "\fsnapshot_uri\x18\x01 \x01(\tR\vsnapshotUri\"\xa0\x04\n" + "\x11CheckpointRequest\x12$\n" + "\x0eworker_pod_uid\x18\x01 \x01(\tR\fworkerPodUid\x12\x1a\n" + "\batespace\x18\x02 \x01(\tR\batespace\x12\x1d\n" + @@ -3426,12 +3632,11 @@ const file_atelet_proto_rawDesc = "" + "\x17actor_template_atespace\x18\x05 \x01(\tR\x15actorTemplateAtespace\x12.\n" + "\x13actor_template_name\x18\x06 \x01(\tR\x11actorTemplateName\x12(\n" + "\x04spec\x18\a \x01(\v2\x14.atelet.WorkloadSpecR\x04spec\x12*\n" + - "\x04type\x18\b \x01(\x0e2\x16.atelet.CheckpointTypeR\x04type\x12I\n" + - "\flocal_config\x18\t \x01(\v2$.atelet.LocalCheckpointConfigurationH\x00R\vlocalConfig\x12R\n" + + "\x04type\x18\b \x01(\x0e2\x16.atelet.CheckpointTypeR\x04type\x12G\n" + + "\flocal_config\x18\t \x01(\v2$.atelet.LocalCheckpointConfigurationR\vlocalConfig\x12P\n" + "\x0fexternal_config\x18\n" + - " \x01(\v2'.atelet.ExternalCheckpointConfigurationH\x00R\x0eexternalConfig\x124\n" + - "\bfidelity\x18\v \x01(\x0e2\x18.atelet.SnapshotFidelityR\bfidelityB\b\n" + - "\x06config\"\x14\n" + + " \x01(\v2'.atelet.ExternalCheckpointConfigurationR\x0eexternalConfig\x124\n" + + "\bfidelity\x18\v \x01(\x0e2\x18.atelet.SnapshotFidelityR\bfidelity\"\x14\n" + "\x12CheckpointResponse\"\x8e\x03\n" + "\x1dUploadPausedCheckpointRequest\x12\x1a\n" + "\batespace\x18\x01 \x01(\tR\batespace\x12\x1d\n" + @@ -3443,7 +3648,7 @@ const file_atelet_proto_rawDesc = "" + "\x13local_snapshot_name\x18\x06 \x01(\tR\x11localSnapshotName\x128\n" + "\x18destination_snapshot_uri\x18\a \x01(\tR\x16destinationSnapshotUri\x12C\n" + "\x10desired_fidelity\x18\b \x01(\x0e2\x18.atelet.SnapshotFidelityR\x0fdesiredFidelity\" \n" + - "\x1eUploadPausedCheckpointResponse\"\xfc\x05\n" + + "\x1eUploadPausedCheckpointResponse\"\xee\x05\n" + "\x0eRestoreRequest\x12$\n" + "\x0eworker_pod_uid\x18\x01 \x01(\tR\fworkerPodUid\x12\x1a\n" + "\batespace\x18\x02 \x01(\tR\batespace\x12\x1d\n" + @@ -3453,16 +3658,15 @@ const file_atelet_proto_rawDesc = "" + "\x17actor_template_atespace\x18\x05 \x01(\tR\x15actorTemplateAtespace\x12.\n" + "\x13actor_template_name\x18\x06 \x01(\tR\x11actorTemplateName\x12(\n" + "\x04spec\x18\a \x01(\v2\x14.atelet.WorkloadSpecR\x04spec\x12*\n" + - "\x04type\x18\b \x01(\x0e2\x16.atelet.CheckpointTypeR\x04type\x12I\n" + - "\flocal_config\x18\t \x01(\v2$.atelet.LocalCheckpointConfigurationH\x00R\vlocalConfig\x12O\n" + + "\x04type\x18\b \x01(\x0e2\x16.atelet.CheckpointTypeR\x04type\x12G\n" + + "\flocal_config\x18\t \x01(\v2$.atelet.LocalCheckpointConfigurationR\vlocalConfig\x12M\n" + "\x0fexternal_config\x18\n" + - " \x01(\v2$.atelet.ExternalRestoreConfigurationH\x00R\x0eexternalConfig\x124\n" + + " \x01(\v2$.atelet.ExternalRestoreConfigurationR\x0eexternalConfig\x124\n" + "\bfidelity\x18\v \x01(\x0e2\x18.atelet.SnapshotFidelityR\bfidelity\x12A\n" + - "\x0eegress_gateway\x18\r \x01(\v2\x15.atelet.EgressGatewayH\x01R\regressGateway\x88\x01\x01\x12\x1b\n" + + "\x0eegress_gateway\x18\r \x01(\v2\x15.atelet.EgressGatewayH\x00R\regressGateway\x88\x01\x01\x12\x1b\n" + "\tcpu_milli\x18\x0e \x01(\x03R\bcpuMilli\x12!\n" + "\fmemory_bytes\x18\x0f \x01(\x03R\vmemoryBytes\x12<\n" + - "\x0esandbox_assets\x18\x10 \x01(\v2\x15.atelet.SandboxAssetsR\rsandboxAssetsB\b\n" + - "\x06configB\x11\n" + + "\x0esandbox_assets\x18\x10 \x01(\v2\x15.atelet.SandboxAssetsR\rsandboxAssetsB\x11\n" + "\x0f_egress_gateway\"\x11\n" + "\x0fRestoreResponse*\x9a\x01\n" + "\x12ActorMetadataField\x12$\n" + @@ -3639,24 +3843,7 @@ func file_atelet_proto_init() { return } file_atelet_proto_msgTypes[14].OneofWrappers = []any{} - file_atelet_proto_msgTypes[26].OneofWrappers = []any{ - (*SystemInfoDataSource_ActorMetadata)(nil), - (*SystemInfoDataSource_TrustBundle)(nil), - } - file_atelet_proto_msgTypes[28].OneofWrappers = []any{ - (*Volume_DurableDir)(nil), - (*Volume_External)(nil), - (*Volume_SystemInfo)(nil), - (*Volume_Image)(nil), - } - file_atelet_proto_msgTypes[41].OneofWrappers = []any{ - (*CheckpointRequest_LocalConfig)(nil), - (*CheckpointRequest_ExternalConfig)(nil), - } - file_atelet_proto_msgTypes[45].OneofWrappers = []any{ - (*RestoreRequest_LocalConfig)(nil), - (*RestoreRequest_ExternalConfig)(nil), - } + file_atelet_proto_msgTypes[45].OneofWrappers = []any{} type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ diff --git a/internal/proto/ateletpb/atelet.proto b/internal/proto/ateletpb/atelet.proto index 3a192351d4..6c13cb4e31 100644 --- a/internal/proto/ateletpb/atelet.proto +++ b/internal/proto/ateletpb/atelet.proto @@ -262,16 +262,39 @@ service AteomHerder { } message TerminateRequest { - // If unset, the atelet will just cleanup node resources for the actor. + // The UID of the worker pod whose ateom hosts the actor. If unset, atelet + // only cleans up the actor's node resources; it does not reach an ateom. + // + // +k8s:optional + // +k8s:format=k8s-uuid string worker_pod_uid = 1; + // +k8s:required + // +k8s:format=k8s-short-name string atespace = 2; + + // +k8s:required + // +k8s:format=k8s-short-name string actor_name = 3; + + // +k8s:required + // +k8s:format=k8s-uuid string actor_uid = 4; + // The template identity is carried for metrics attribution. + // + // +k8s:optional + // +k8s:format=k8s-short-name string actor_template_atespace = 5; + + // +k8s:optional + // +k8s:format=k8s-short-name string actor_template_name = 6; + // A nil spec is tolerated: the delete flow terminates template-less + // actors with a fallback spec. + // + // +k8s:optional WorkloadSpec spec = 7; } @@ -279,29 +302,60 @@ message TerminateResponse { } message RunRequest { + // The UID of the worker pod whose ateom hosts the actor. + // + // +k8s:required + // +k8s:format=k8s-uuid string worker_pod_uid = 1; + // +k8s:required + // +k8s:format=k8s-short-name string atespace = 2; + + // +k8s:required + // +k8s:format=k8s-short-name string actor_name = 3; + + // +k8s:required + // +k8s:format=k8s-uuid string actor_uid = 4; + // The template identity is carried for metrics attribution. + // + // +k8s:optional + // +k8s:format=k8s-short-name string actor_template_atespace = 5; + + // +k8s:optional + // +k8s:format=k8s-short-name string actor_template_name = 6; + // +k8s:required WorkloadSpec spec = 7; // The sandbox binaries to use for booting this actor from scratch. atelet // fetches the relevant assets and records them with the actor's on-node state // so a later Checkpoint can pin the same version into the snapshot manifest. + // + // +k8s:required SandboxAssets sandbox_assets = 8; // When absent the actor has no egress: its TCP is captured and refused. + // + // +k8s:optional optional EgressGateway egress_gateway = 9; // The actor's declared size, from the ActorTemplate's resource limits. atelet // passes these through to the sandbox so it is sized to the actor (not the // whole host or worker pod). Zero means "unset": keep the runtime default. - int64 cpu_milli = 10; // CPU limit in millicores (1000 = one core). + // + // +k8s:optional + // +k8s:minimum=0 + // +k8s:maximum=999999 # the control plane caps cpu limits strictly below 1000 cores + int64 cpu_milli = 10; // CPU limit in millicores (1000 = one core). + + // +k8s:optional + // +k8s:minimum=0 int64 memory_bytes = 11; // Memory limit in bytes. } @@ -309,6 +363,10 @@ message RunRequest { message EgressGateway { // address is required and identifies the remote gateway as an IP address or // DNS name followed by its port. + // + // +k8s:required + // +k8s:maxLength=261 # a bracketed or 253-character host, ':' and a 5-digit port + // +k8s:customValidation # host:port shape string address = 1; } @@ -320,14 +378,25 @@ message AssetFile { // the plugin cannot serve it. // Masked in logged requests, since a signed URL carries its credential in // the query. + // + // +k8s:required + // +k8s:maxLength=2048 # matches the SandboxConfig asset url's bound string url = 1 [ debug_redact = true ]; + // Lower-case hex SHA256; names the cached file and verifies its integrity. + // + // +k8s:required + // +k8s:maxLength=64 + // +k8s:customValidation # 64 hex characters string sha256 = 2; } // ArchAssets is the set of assets for a single architecture, keyed by asset // name (a wrapper message because proto map values cannot themselves be maps). message ArchAssets { + // +k8s:required + // +k8s:maxProperties=16 + // +k8s:eachKey=+k8s:maxLength=64 map files = 1; // asset name -> file } @@ -337,7 +406,17 @@ message ArchAssets { // atelet's backend code interprets the asset names (gVisor expects "gvisor", // the release tarball; legacy "runsc", a bare binary, is still accepted). message SandboxAssets { - string sandbox_class = 1; // e.g. "gvisor" + // TODO: send this as an enum, like ateapi's and ateom's SandboxClass, so + // the tags can bound it and the custom validation goes away. + // + // +k8s:required + // +k8s:maxLength=63 # guardrail; the class names are a few characters + // +k8s:customValidation # gvisor or microvm + string sandbox_class = 1; // e.g. "gvisor" + + // +k8s:required + // +k8s:maxProperties=8 # one entry per architecture + // +k8s:eachKey=+k8s:maxLength=16 map assets = 2; // arch -> {name -> file} // pause_image is the image for the sandbox's root container. Like the @@ -345,12 +424,27 @@ message SandboxAssets { // and atelet pins it into the snapshot manifest so a restore rebuilds the // sandbox from the same image. Empty for sandboxes without a pause // container (microvm). + // + // +k8s:optional + // +k8s:maxLength=512 + // +k8s:customValidation # must be a well-formed image reference, pinned by digest string pause_image = 3; } // WorkloadSpec parallels Pod, but with far fewer configurable fields. +// +// +k8s:customValidation # every volume mount names a declared volume message WorkloadSpec { + // +k8s:optional + // +k8s:maxItems=10 # matches the template's containers bound + // +k8s:listType=map + // +k8s:listMapKey=name repeated Container containers = 1; + + // +k8s:optional + // +k8s:maxItems=32 # matches the template's volumes bound + // +k8s:listType=map + // +k8s:listMapKey=name repeated Volume volumes = 2; } @@ -358,16 +452,40 @@ message DurableDirVolume { } message ExternalVolumeSource { + // Required, unlike the actor status counterpart: the control plane only + // sends volumes that finished provisioning. + // + // +k8s:required + // +k8s:maxLength=256 # matches ExternalVolume.storage_volume_id's bound + // +k8s:customValidation # no control characters string storage_volume_id = 1; + + // +k8s:optional + // +k8s:maxLength=253 # matches ExternalVolume.volume_type's bound + // +k8s:customValidation # optional "substrate.io/" prefix + DNS subdomain string volume_type = 2; + + // volume_context is the driver's provisioning metadata. Its entries are the + // driver's own, so the map is only bounded, not validated. + // + // +k8s:optional + // +k8s:customValidation # at most 40 KiB in total, ten times the CSI limit for map fields map volume_context = 3; // publish_context is the driver's attachment metadata for this node, which - // the node plugin needs to complete the mount. + // the node plugin needs to complete the mount. Bounded like volume_context. + // + // +k8s:optional + // +k8s:customValidation # at most 40 KiB in total, ten times the CSI limit for map fields map publish_context = 4; } message ImageVolumeSource { + // reference is the OCI image reference, pinned by digest. + // + // +k8s:required + // +k8s:maxLength=512 # matches the template ImageVolumeSource.reference's bound + // +k8s:customValidation # must be a well-formed image reference, pinned by digest string reference = 1; } @@ -377,83 +495,197 @@ enum ActorMetadataField { ACTOR_METADATA_FIELD_NAME = 1; ACTOR_METADATA_FIELD_ATESPACE = 2; ACTOR_METADATA_FIELD_UID = 3; + // Keep this in sync with the maximums on fields of this type. } // ActorMetadataItem projects one actor identity field to one file at the // given path, relative to the root of the enclosing system-info volume. message ActorMetadataItem { + // +k8s:required + // +k8s:minimum=1 + // +k8s:maximum=3 # keep this in sync with the ActorMetadataField enum ActorMetadataField field = 1; + + // path must be a clean relative Unix path; the rule is shared with the + // control plane through internal/resources. + // + // +k8s:required + // +k8s:minLength=1 + // +k8s:maxLength=255 + // +k8s:customValidation # clean relative path string path = 2; } // ActorMetadataDataSource projects the actor's identity fields to files, one // per item. Values are written raw with no trailing newline. message ActorMetadataDataSource { + // items must not project the same field twice. + // + // +k8s:required + // +k8s:minItems=1 + // +k8s:maxItems=8 # matches the template's items bound + // +k8s:listType=atomic + // +k8s:unique=map + // +k8s:listMapKey=field repeated ActorMetadataItem items = 1; } // TrustBundleDataSource corresponds to TrustBundleDataSource in ateapi.proto message TrustBundleDataSource { + // path must be a clean relative Unix path; the rule is shared with the + // control plane through internal/resources. + // + // +k8s:required + // +k8s:minLength=1 + // +k8s:maxLength=255 + // +k8s:customValidation # clean relative path string path = 1; + // +k8s:required + // +k8s:minItems=1 + // +k8s:maxItems=8 # matches the template's names bound + // +k8s:listType=set + // +k8s:eachVal=+k8s:minLength=1 + // +k8s:eachVal=+k8s:maxLength=253 # matches the template's bundle-name bound repeated string names = 3; reserved 2; reserved "name"; } +// SystemInfoDataSource selects exactly one projection to place in the +// volume. message SystemInfoDataSource { - oneof data_source { - ActorMetadataDataSource actor_metadata = 1; - TrustBundleDataSource trust_bundle = 2; - } + // Exactly one of actor_metadata / trust_bundle must be set. + // + // +k8s:optional + // +k8s:unionMember + ActorMetadataDataSource actor_metadata = 1; + + // +k8s:optional + // +k8s:unionMember + TrustBundleDataSource trust_bundle = 2; } // SystemInfoVolume is a read-only volume whose files are generated by atelet // on every Run/Restore, so they carry the values of the actor actually being // started, whatever checkpointed state it boots from. message SystemInfoVolume { + // +k8s:optional + // +k8s:maxItems=8 # matches the template's data_sources bound + // +k8s:listType=atomic + // +k8s:customValidation # at most one actor_metadata; paths unique across entries repeated SystemInfoDataSource data_sources = 1; } +// Volume names one volume and selects exactly one source for it. message Volume { + // +k8s:required + // +k8s:format=k8s-short-name string name = 1; - oneof source { - DurableDirVolume durable_dir = 2; - ExternalVolumeSource external = 3; - SystemInfoVolume system_info = 4; - ImageVolumeSource image = 5; - } + // Exactly one of durable_dir / external / system_info / image must be set. + // + // +k8s:optional + // +k8s:unionMember + DurableDirVolume durable_dir = 2; + + // +k8s:optional + // +k8s:unionMember + ExternalVolumeSource external = 3; + + // +k8s:optional + // +k8s:unionMember + SystemInfoVolume system_info = 4; + + // +k8s:optional + // +k8s:unionMember + ImageVolumeSource image = 5; } message VolumeMount { + // name must match the name of a Volume. + // + // +k8s:required + // +k8s:format=k8s-short-name string name = 1; + + // mount_path within the container. Must be a clean absolute Unix path. + // + // +k8s:required + // +k8s:maxLength=4096 + // +k8s:customValidation # clean-absolute-path shape; no regex/pattern tag exists string mount_path = 2; } message Container { + // +k8s:required + // +k8s:format=k8s-short-name string name = 1; + + // +k8s:required + // +k8s:maxLength=512 # matches the template Container.image's bound + // +k8s:customValidation # must be a well-formed image reference, pinned by digest string image = 2; + + // +k8s:optional + // +k8s:maxItems=64 + // +k8s:listType=atomic + // +k8s:eachVal=+k8s:maxLength=4096 # argv strings; guardrail, not a contract repeated string command = 3; + + // +k8s:optional + // +k8s:maxItems=64 + // +k8s:listType=atomic + // +k8s:eachVal=+k8s:maxLength=4096 # argv strings; guardrail, not a contract repeated string args = 7; + + // +k8s:optional + // +k8s:maxItems=32 # matches the template's env bound + // +k8s:listType=map # each variable is set at most once + // +k8s:listMapKey=name repeated EnvEntry env = 4; + + // +k8s:optional WakeupProbe wakeup_probe = 5; + + // Keyed by mount_path: each path hosts exactly one mount, while a volume + // may be mounted at multiple paths. + // + // +k8s:optional + // +k8s:maxItems=32 # matches the template's volume_mounts bound + // +k8s:listType=map + // +k8s:listMapKey=mount_path + // +k8s:customValidation # mounts must not nest repeated VolumeMount volume_mounts = 6; + + // +k8s:optional SecurityContext security_context = 8; // resources are the cgroup limits for this container, resolved by // ate-api-server from the ActorTemplate. Unset means no limits. + // + // +k8s:optional ResourceLimits resources = 9; } // SecurityContext holds security settings for a container's process. message SecurityContext { + // +k8s:optional Capabilities capabilities = 1; } // Capabilities adjusts a container's Linux capabilities relative to the default // set. Names carry no "CAP_" prefix; drop applies before add. message Capabilities { + // +k8s:optional + // +k8s:maxItems=64 + // +k8s:listType=set + // +k8s:customValidation # capability grammar; "ALL" not accepted repeated string add = 1; + + // +k8s:optional + // +k8s:maxItems=64 + // +k8s:listType=set + // +k8s:customValidation # capability grammar repeated string drop = 2; } @@ -461,29 +693,60 @@ message Capabilities { // map so each resource.Quantity is parsed exactly once, in ate-api-server. message ResourceLimits { // memory_bytes is the memory limit in bytes. 0 means unset. + // + // +k8s:optional + // +k8s:minimum=0 int64 memory_bytes = 1; + // cpu_millis is the CPU limit in milli-cores (1000 = one core). 0 means unset. + // + // +k8s:optional + // +k8s:minimum=0 + // +k8s:maximum=999999 # the control plane caps cpu limits strictly below 1000 cores int64 cpu_millis = 2; } message EnvEntry { + // name may be any printable ASCII character except '='. + // + // +k8s:required + // +k8s:maxLength=256 # guardrail + // +k8s:customValidation # printable ASCII except '=' string name = 1; + + // +k8s:optional + // +k8s:maxLength=32768 # guardrail string value = 2 [ debug_redact = true ]; } // WakeupProbe describes how to check that a container is ready to serve. // Only HTTP is supported today. message WakeupProbe { + // +k8s:required HttpGetAction http_get = 1; + // How long to keep polling before giving up and failing the actor start. + // + // +k8s:required + // +k8s:minimum=1 + // +k8s:maximum=3600 # matches the template wakeup probe's bound int32 timeout_seconds = 2; } // HttpGetAction performs an HTTP GET against the container. message HttpGetAction { // Path to access on the HTTP server. + // + // +k8s:required + // +k8s:maxLength=1024 # matches the template probe path's bound + // +k8s:customValidation # RFC 3986 path shape; no regex/pattern tag exists string path = 1; + // TCP port to connect to (1..65535). + // + // +k8s:required + // +k8s:minimum=1 + // +k8s:maximum=65535 int32 port = 2; } @@ -495,12 +758,19 @@ message LocalCheckpointConfiguration { // data will be stored. atelet decides where that directory lives, from the // actor's UID, so this is a bare name and must not contain a path separator. // The structure of the checkpoint should generally be treated as opaque. + // + // +k8s:required + // +k8s:format=k8s-short-name string snapshot_name = 1; } message ExternalCheckpointConfiguration { // The object storage URI of the snapshot to write. Object names are appended // to it, so it addresses the snapshot as a whole rather than any one object. + // + // +k8s:required + // +k8s:maxLength=2048 + // +k8s:customValidation # a parseable snapshot URI string snapshot_uri = 1; } @@ -511,6 +781,7 @@ enum CheckpointType { CHECKPOINT_TYPE_LOCAL = 1; // Save snapshot to object storage CHECKPOINT_TYPE_EXTERNAL = 2; + // Keep this in sync with the maximums on fields of this type. } // SnapshotFidelity mirrors the public ateapi enum: the state layers a @@ -528,6 +799,7 @@ enum SnapshotFidelity { // Capture process memory plus the full filesystem delta on top of the OCI // image (including any attached DurableDir volumes). SNAPSHOT_FIDELITY_MEMORY = 3; + // Keep this in sync with the maximums on fields of this type. } // ExternalRestoreConfiguration is an external snapshot a restore reads. @@ -537,33 +809,72 @@ message ExternalRestoreConfiguration { // The object storage URI of the snapshot to read. Object names are // appended to it, so it addresses the snapshot as a whole rather than any // one object. + // + // +k8s:required + // +k8s:maxLength=2048 + // +k8s:customValidation # a parseable snapshot URI string snapshot_uri = 1; } +// +k8s:customValidation # the set config matches type message CheckpointRequest { + // The UID of the worker pod whose ateom hosts the actor. + // + // +k8s:required + // +k8s:format=k8s-uuid string worker_pod_uid = 1; + // +k8s:required + // +k8s:format=k8s-short-name string atespace = 2; + + // +k8s:required + // +k8s:format=k8s-short-name string actor_name = 3; + + // +k8s:required + // +k8s:format=k8s-uuid string actor_uid = 4; + // The template identity is carried for metrics attribution. + // + // +k8s:optional + // +k8s:format=k8s-short-name string actor_template_atespace = 5; + + // +k8s:optional + // +k8s:format=k8s-short-name string actor_template_name = 6; // Sandbox binary config is not sent on checkpoint: atelet uses the version the // actor is currently running (recorded with the actor's on-node state at // Run/Restore) and records it into the snapshot manifest. + // + // +k8s:required WorkloadSpec spec = 7; + // +k8s:required + // +k8s:minimum=1 + // +k8s:maximum=2 # keep this in sync with the CheckpointType enum CheckpointType type = 8; - // The checkpoint configuration, depending on the type. - oneof config { - LocalCheckpointConfiguration local_config = 9; - ExternalCheckpointConfiguration external_config = 10; - } + // The checkpoint configuration: local_config when type is LOCAL, + // external_config when it is EXTERNAL. Exactly one is set. + // + // +k8s:optional + // +k8s:unionMember + LocalCheckpointConfiguration local_config = 9; + + // +k8s:optional + // +k8s:unionMember + ExternalCheckpointConfiguration external_config = 10; - // Fidelity the checkpoint captures. + // Fidelity the checkpoint captures. Which levels a runtime can serve is the + // runtime's call: atelet forwards it and ateom rejects what it cannot capture. + // + // +k8s:required + // +k8s:minimum=1 + // +k8s:maximum=3 # keep this in sync with the SnapshotFidelity enum SnapshotFidelity fidelity = 11; } @@ -571,66 +882,138 @@ message CheckpointResponse { } message UploadPausedCheckpointRequest { + // Golden actors are never paused, so the golden atespace is rejected. + // + // +k8s:required + // +k8s:format=k8s-short-name + // +k8s:customValidation # not the golden atespace string atespace = 1; + + // +k8s:required + // +k8s:format=k8s-short-name string actor_name = 2; + + // +k8s:required + // +k8s:format=k8s-uuid string actor_uid = 3; // For metrics attribution, like on CheckpointRequest. + // + // +k8s:optional + // +k8s:format=k8s-short-name string actor_template_atespace = 4; + + // +k8s:optional + // +k8s:format=k8s-short-name string actor_template_name = 5; // The local checkpoint to upload: the uuid of the actor's pause snapshot - // (ateapi Snapshot.uuid). + // (ateapi Snapshot.uuid, validated as a short name there too). + // + // +k8s:required + // +k8s:format=k8s-short-name string local_snapshot_name = 6; // Destination object-storage URI (the actor's in-progress snapshot URI). + // + // +k8s:required + // +k8s:maxLength=2048 + // +k8s:customValidation # a parseable snapshot URI string destination_snapshot_uri = 7; // Fidelity the uploaded snapshot must have. The fidelity the pause // checkpoint captured is not sent: atelet reads it from the local // snapshot's own manifest, which is authoritative, and rejects the upload // if the two differ. + // + // +k8s:required + // +k8s:minimum=1 + // +k8s:maximum=3 # keep this in sync with the SnapshotFidelity enum SnapshotFidelity desired_fidelity = 8; } message UploadPausedCheckpointResponse { } +// +k8s:customValidation # the set config matches type message RestoreRequest { + // The UID of the worker pod whose ateom hosts the actor. + // + // +k8s:required + // +k8s:format=k8s-uuid string worker_pod_uid = 1; + // +k8s:required + // +k8s:format=k8s-short-name string atespace = 2; + + // +k8s:required + // +k8s:format=k8s-short-name string actor_name = 3; + + // +k8s:required + // +k8s:format=k8s-uuid string actor_uid = 4; + // The template identity is carried for metrics attribution. + // + // +k8s:optional + // +k8s:format=k8s-short-name string actor_template_atespace = 5; + + // +k8s:optional + // +k8s:format=k8s-short-name string actor_template_name = 6; + // +k8s:required WorkloadSpec spec = 7; + // +k8s:required + // +k8s:minimum=1 + // +k8s:maximum=2 # keep this in sync with the CheckpointType enum CheckpointType type = 8; - // The checkpoint configuration, depending on the type. - oneof config { - LocalCheckpointConfiguration local_config = 9; - ExternalRestoreConfiguration external_config = 10; - } + // The checkpoint configuration: local_config when type is LOCAL, + // external_config when it is EXTERNAL. Exactly one is set. + // + // +k8s:optional + // +k8s:unionMember + LocalCheckpointConfiguration local_config = 9; + + // +k8s:optional + // +k8s:unionMember + ExternalRestoreConfiguration external_config = 10; // Fidelity to restore from the checkpoint. + // + // +k8s:required + // +k8s:minimum=1 + // +k8s:maximum=3 # keep this in sync with the SnapshotFidelity enum SnapshotFidelity fidelity = 11; // When absent the actor has no egress: its TCP is captured and refused. + // + // +k8s:optional optional EgressGateway egress_gateway = 13; // The actor's declared size, from the ActorTemplate's resource limits. For // gVisor and micro-VM VOLUMES restores the sandbox is (re)sized to these; // for a MEMORY micro-VM restore the size baked into the snapshot wins. Zero // means "unset": keep the runtime default. - int64 cpu_milli = 14; // CPU limit in millicores (1000 = one core). + // + // +k8s:optional + // +k8s:minimum=0 + // +k8s:maximum=999999 # the control plane caps cpu limits strictly below 1000 cores + int64 cpu_milli = 14; // CPU limit in millicores (1000 = one core). + + // +k8s:optional + // +k8s:minimum=0 int64 memory_bytes = 15; // Memory limit in bytes. // The sandbox binaries and pause image to restore with, resolved from the - // ActorTemplate's SandboxConfig. Required. + // ActorTemplate's SandboxConfig. + // + // +k8s:required SandboxAssets sandbox_assets = 16; } diff --git a/internal/resources/validate.go b/internal/resources/validate.go index ff3d411a65..2c8f749a21 100644 --- a/internal/resources/validate.go +++ b/internal/resources/validate.go @@ -17,20 +17,26 @@ package resources import ( "encoding/hex" "fmt" + "net" "net/netip" "net/url" "os" "path/filepath" "reflect" + "regexp" + "strconv" "strings" "github.com/agent-substrate/substrate/internal/apierror" "github.com/agent-substrate/substrate/internal/proto/ateompb" + "github.com/agent-substrate/substrate/internal/volumepath" + "github.com/distribution/reference" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" "google.golang.org/protobuf/proto" "k8s.io/apimachinery/pkg/api/resource" "k8s.io/apimachinery/pkg/api/validate/content" + "k8s.io/apimachinery/pkg/util/validation" "k8s.io/apimachinery/pkg/util/validation/field" ) @@ -173,29 +179,6 @@ func ValidateRuntimeAssetPath(root, p string, fldPath *field.Path) field.ErrorLi return nil } -// ValidateContainerNames ensures every application container name is safe to -// use as an OCI bundle path component. Each must be a DNS-1123 label (no -// separator or ".."), must not be the reserved "pause" name (which would -// collide with the sandbox-infra bundle and race its concurrent writer), and -// must be unique (duplicates map to the same bundle path and corrupt each -// other). -func ValidateContainerNames(names []string) error { - seen := make(map[string]struct{}) - for _, name := range names { - if errs := content.IsDNS1123Label(name); len(errs) > 0 { - return fmt.Errorf("invalid container name %q: %s", name, strings.Join(errs, "; ")) - } - if name == "pause" { - return fmt.Errorf("invalid container name %q: reserved for sandbox infrastructure", name) - } - if _, dup := seen[name]; dup { - return fmt.Errorf("duplicate container name %q", name) - } - seen[name] = struct{}{} - } - return nil -} - // ValidateRunscHash ensures the runsc SHA-256 hash is exactly 64 hex // characters before it is used to build the on-disk binary path // (static-files/runsc-) and, on a cache hit, returned for ateom to @@ -302,13 +285,21 @@ func ValidateUUID(uuid string, fldPath *field.Path) field.ErrorList { return nil } -// cpuLimitMax bounds cpu limits: they must be less than 1000 cores. -var cpuLimitMax = resource.MustParse("1k") +// cpuLimitMax bounds cpu limits. The wire carries ceil(cores × 1000) +// millicores and caps it at 999999 (strictly below 1000 cores), so a limit +// above 999.999 cores rounds up out of range. +var cpuLimitMax = resource.MustParse("999.999") + +// memoryLimitMax bounds memory limits at what the wire's int64 byte count +// holds; resource.Quantity.Value wraps silently past it. +var memoryLimitMax = resource.MustParse("9223372036854775807") // ValidateLimit validates one resource limit entry at fldPath: only cpu and -// memory are supported, the quantity must be greater than zero, and the cpu -// limit must be less than 1000 cores. An empty quantity is left to the -// required tag. +// memory are supported, the quantity must be greater than zero, and it must +// fit the scalars the control plane converts it to (cpuLimitMax and +// memoryLimitMax). The bounds compare Quantities rather than the converted +// integers, which wrap for very large values. An empty quantity is left to +// the required tag. func ValidateLimit(fldPath *field.Path, name, quantity string) field.ErrorList { if name != ResourceCPU && name != ResourceMemory { return field.ErrorList{field.NotSupported(fldPath.Child("name"), name, []string{ResourceCPU, ResourceMemory})} @@ -324,8 +315,221 @@ func ValidateLimit(fldPath *field.Path, name, quantity string) field.ErrorList { if q.Sign() <= 0 { errs = append(errs, field.Invalid(fldPath.Child("quantity"), quantity, "must be greater than zero")) } - if name == ResourceCPU && q.Cmp(cpuLimitMax) >= 0 { - errs = append(errs, field.Invalid(fldPath.Child("quantity"), quantity, "cpu limit must be less than 1000 cores")) + switch { + case name == ResourceCPU && q.Cmp(cpuLimitMax) > 0: + errs = append(errs, field.Invalid(fldPath.Child("quantity"), quantity, "cpu limit must be at most 999.999 cores")) + case name == ResourceMemory && q.Cmp(memoryLimitMax) > 0: + errs = append(errs, field.Invalid(fldPath.Child("quantity"), quantity, "memory limit must be at most 9223372036854775807 bytes")) + } + return errs +} + +// ValidatePinnedImage requires a well-formed OCI image reference pinned by +// digest (e.g. "name@sha256:..."): changing the image content under a fixed +// reference invalidates snapshots. It parses with the same grammar the +// container runtimes use, so a malformed digest is rejected rather than +// treated as pinned. +func ValidatePinnedImage(fldPath *field.Path, value string) field.ErrorList { + if value == "" { + return nil + } + ref, err := reference.ParseNormalizedNamed(value) + if err != nil { + return field.ErrorList{field.Invalid(fldPath, value, fmt.Sprintf("must be a well-formed image reference: %v", err))} + } + if _, ok := ref.(reference.Digested); !ok { + return field.ErrorList{field.Invalid(fldPath, value, "must be pinned by digest (changing the image invalidates snapshots)")} + } + return nil +} + +// capabilityRE constrains Linux capability names: uppercase, without the +// "CAP_" prefix (which is added when the OCI spec is written; the prefixed +// spelling would silently grant nothing). +var capabilityRE = regexp.MustCompile(`^[A-Z][A-Z0-9_]*$`) + +// ValidateCapabilities checks each capability name. "ALL" is accepted only +// when allowAll is set, which is the case for drop but not add. +func ValidateCapabilities(fldPath *field.Path, caps []string, allowAll bool) field.ErrorList { + var errs field.ErrorList + for i, c := range caps { + p := fldPath.Index(i) + switch { + case c == "ALL" && !allowAll: + errs = append(errs, field.Invalid(p, c, "add does not accept 'ALL'; name the individual capabilities the container needs")) + case c == "ALL": + case len(c) > 63: + errs = append(errs, field.TooLong(p, nil, 63)) + case strings.HasPrefix(c, "CAP_"): + errs = append(errs, field.Invalid(p, c, "must be named without the 'CAP_' prefix (e.g. 'NET_BIND_SERVICE')")) + case !capabilityRE.MatchString(c): + errs = append(errs, field.Invalid(p, c, "must be an uppercase capability name like 'NET_BIND_SERVICE'")) + } + } + return errs +} + +// mountPathBadSegmentRE matches '.' or '..' path segments. +var mountPathBadSegmentRE = regexp.MustCompile(`(^|/)[.][.]?(/|$)`) + +// ValidateMountPath requires a clean absolute Unix path that starts with '/', +// is not '/', and contains no ':', '.' or '..' segments, '//', trailing '/', +// or control characters. +func ValidateMountPath(fldPath *field.Path, p string) field.ErrorList { + if p == "" { + return nil + } + bad := !strings.HasPrefix(p, "/") || len(p) == 1 || + strings.HasSuffix(p, "/") || strings.Contains(p, "//") || + strings.Contains(p, ":") || mountPathBadSegmentRE.MatchString(p) + if !bad { + for _, r := range p { + if r < 0x20 || r == 0x7f { + bad = true + break + } + } + } + if bad { + return field.ErrorList{field.Invalid(fldPath, p, "must be a clean absolute Unix path: must start with '/', not be '/', and contain no ':', '..', '.', '//', trailing '/', or control characters")} + } + return nil +} + +// ValidateNestedMountPaths rejects mount paths that nest under or over one +// another: volumes cannot mount onto other volumes. paths[i] is reported at +// fldPath[i].mount_path. Identical paths are left to the list-key uniqueness +// check. +func ValidateNestedMountPaths(fldPath *field.Path, paths []string) field.ErrorList { + var errs field.ErrorList + for i, path := range paths { + if path == "" { + continue + } + for _, prior := range paths[:i] { + if prior == "" || prior == path { + continue + } + if strings.HasPrefix(path, prior+"/") || strings.HasPrefix(prior, path+"/") { + errs = append(errs, field.Invalid(fldPath.Index(i).Child("mount_path"), path, + fmt.Sprintf("must not nest under or over another mount (%q)", prior))) + } + } + } + return errs +} + +// httpGetPathRE constrains wakeup probe paths to RFC 3986 path-segment +// characters only, with well-formed percent-escapes, and no query string +// or fragment. +var httpGetPathRE = regexp.MustCompile(`^/([A-Za-z0-9\-._~!$&'()*+,;=:@/]|%[0-9A-Fa-f]{2})*$`) + +// ValidateHTTPGetPath checks a wakeup probe's HTTP path. +func ValidateHTTPGetPath(fldPath *field.Path, p string) field.ErrorList { + if p == "" { + return nil + } + if !httpGetPathRE.MatchString(p) { + return field.ErrorList{field.Invalid(fldPath, p, "must be a URL path starting with '/', using only RFC 3986 path-segment characters, without query or fragment")} + } + return nil +} + +// envVarNameRE constrains env var names to any printable ASCII character +// except '='. +var envVarNameRE = regexp.MustCompile(`^[ -<>-~]+$`) + +// ValidateEnvVarName checks an environment variable name. +func ValidateEnvVarName(fldPath *field.Path, name string) field.ErrorList { + if name == "" { + return nil + } + if !envVarNameRE.MatchString(name) { + return field.ErrorList{field.Invalid(fldPath, name, "may contain any printable ASCII character except '='")} + } + return nil +} + +// ValidateProjectedPath applies volumepath.ValidateProjected to a file path +// projected into a system-info volume. atelet re-checks it before writing to +// the host. +func ValidateProjectedPath(fldPath *field.Path, p string) field.ErrorList { + if p == "" { + return nil + } + if err := volumepath.ValidateProjected(p); err != nil { + return field.ErrorList{field.Invalid(fldPath, p, err.Error())} + } + return nil +} + +// ValidateStorageVolumeID rejects control characters (U+0000-U+0008, U+000B, +// U+000C, U+000E-U+001F, U+007F-U+009F) in an external volume's storage ID. +func ValidateStorageVolumeID(fldPath *field.Path, id string) field.ErrorList { + for _, r := range id { + if (r >= 0x0000 && r <= 0x0008) || + r == 0x000B || + r == 0x000C || + (r >= 0x000E && r <= 0x001F) || + (r >= 0x007F && r <= 0x009F) { + return field.ErrorList{field.Invalid(fldPath, id, "must not contain control characters (U+0000-U+0008, U+000B, U+000C, U+000E-U+001F, U+007F-U+009F)")} + } + } + return nil +} + +// ValidateVolumeType allows an optional "substrate.io/" prefix, followed by a +// valid DNS-1123 subdomain. +func ValidateVolumeType(fldPath *field.Path, volumeType string) field.ErrorList { + if volumeType == "" { + return nil + } + var errs field.ErrorList + for _, msg := range validation.IsDNS1123Subdomain(strings.TrimPrefix(volumeType, "substrate.io/")) { + errs = append(errs, field.Invalid(fldPath, volumeType, msg)) + } + return errs +} + +// csiMapMaxBytes bounds a map a CSI driver produced. The CSI spec caps +// map fields at 4 KiB; this allows ten times that so a future +// revision of the limit does not turn compliant drivers away. It is a +// guardrail against unbounded input, not a contract drivers are held to. +const csiMapMaxBytes = 40 * 1024 + +// ValidateCSIMap bounds a map a CSI driver produced (a volume or publish +// context) at csiMapMaxBytes, counting keys and values. The entries are the +// driver's own and are not validated further. +func ValidateCSIMap(fldPath *field.Path, m map[string]string) field.ErrorList { + size := 0 + for k, v := range m { + size += len(k) + len(v) + } + if size > csiMapMaxBytes { + return field.ErrorList{field.TooLong(fldPath, nil, csiMapMaxBytes)} + } + return nil +} + +// ValidateHostPort requires "host:port", where host is an IP address or a DNS +// subdomain name (a trailing dot, marking it fully qualified, is allowed) and +// port is a number in 1..65535. +func ValidateHostPort(fldPath *field.Path, value string) field.ErrorList { + if value == "" { + return nil + } + host, port, err := net.SplitHostPort(value) + if err != nil { + return field.ErrorList{field.Invalid(fldPath, value, "must be host:port")} + } + var errs field.ErrorList + if _, err := netip.ParseAddr(host); err != nil { + for _, msg := range content.IsDNS1123Subdomain(strings.TrimSuffix(host, ".")) { + errs = append(errs, field.Invalid(fldPath, value, "host: "+msg)) + } + } + if n, err := strconv.Atoi(port); err != nil || n < 1 || n > 65535 { + errs = append(errs, field.Invalid(fldPath, value, "port must be a number between 1 and 65535")) } return errs } diff --git a/internal/resources/validate_test.go b/internal/resources/validate_test.go index 7db4c4997a..e2d3e905f6 100644 --- a/internal/resources/validate_test.go +++ b/internal/resources/validate_test.go @@ -15,6 +15,7 @@ package resources import ( + "fmt" "os" "path/filepath" "regexp" @@ -123,32 +124,6 @@ func TestValidateWorkerPodUID(t *testing.T) { } } -func TestValidateContainerNames(t *testing.T) { - tests := []struct { - name string - names []string - wantErr bool - }{ - {"no containers", nil, false}, - {"single valid", []string{"worker"}, false}, - {"multiple valid", []string{"worker", "sidecar"}, false}, - {"separator", []string{"a/b"}, true}, - {"traversal", []string{".."}, true}, - {"empty name", []string{""}, true}, - {"uppercase", []string{"Worker"}, true}, - {"reserved pause", []string{"pause"}, true}, - {"reserved pause among valid", []string{"worker", "pause"}, true}, - {"duplicate", []string{"worker", "worker"}, true}, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if err := ValidateContainerNames(tt.names); (err != nil) != tt.wantErr { - t.Errorf("ValidateContainerNames(%v) err = %v, wantErr %v", tt.names, err, tt.wantErr) - } - }) - } -} - func TestValidateRunscHash(t *testing.T) { const valid = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" @@ -296,14 +271,20 @@ func TestValidateLimit(t *testing.T) { want field.ErrorList }{ {name: "cpu below the bound", limit: "cpu", quantity: "999"}, + {name: "cpu at the millicore bound", limit: "cpu", quantity: "999.999"}, {name: "memory", limit: "memory", quantity: "1Gi"}, - {name: "memory has no upper bound", limit: "memory", quantity: "1000"}, + {name: "memory at the int64 bound", limit: "memory", quantity: "9223372036854775807"}, {name: "missing quantity left to tags", limit: "cpu"}, {name: "unsupported name", limit: "gpu", quantity: "1", want: field.ErrorList{field.NotSupported[string](path.Child("name"), nil, nil)}}, {name: "malformed quantity", limit: "cpu", quantity: "x", want: field.ErrorList{field.Invalid(quantityPath, nil, "")}}, {name: "zero quantity", limit: "memory", quantity: "0", want: field.ErrorList{field.Invalid(quantityPath, nil, "")}}, {name: "negative quantity", limit: "memory", quantity: "-1", want: field.ErrorList{field.Invalid(quantityPath, nil, "")}}, - {name: "cpu at the bound", limit: "cpu", quantity: "1000", want: field.ErrorList{field.Invalid(quantityPath, nil, "")}}, + {name: "cpu at 1000 cores", limit: "cpu", quantity: "1000", want: field.ErrorList{field.Invalid(quantityPath, nil, "")}}, + {name: "cpu rounds up to 1000 cores in millicores", limit: "cpu", quantity: "999.9995", want: field.ErrorList{field.Invalid(quantityPath, nil, "")}}, + {name: "cpu far beyond int64 millicores", limit: "cpu", quantity: "1e30", want: field.ErrorList{field.Invalid(quantityPath, nil, "")}}, + {name: "memory past the int64 bound", limit: "memory", quantity: "9223372036854775808", want: field.ErrorList{field.Invalid(quantityPath, nil, "")}}, + {name: "memory rounds up past the int64 bound", limit: "memory", quantity: "9223372036854775807.5", want: field.ErrorList{field.Invalid(quantityPath, nil, "")}}, + {name: "memory far beyond int64 bytes", limit: "memory", quantity: "1e30", want: field.ErrorList{field.Invalid(quantityPath, nil, "")}}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -395,6 +376,27 @@ func TestValidateActorDirs(t *testing.T) { } } +const testPinnedImage = "example.com/app@sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + +// stringRuleCase is one input to a single-string rule and whether it fails. +type stringRuleCase struct { + name string + value string + wantErr bool +} + +func runStringRule(t *testing.T, rule func(*field.Path, string) field.ErrorList, tests []stringRuleCase) { + t.Helper() + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + errs := rule(field.NewPath("f"), tt.value) + if (len(errs) > 0) != tt.wantErr { + t.Errorf("value %q: errs = %v, wantErr %v", tt.value, errs, tt.wantErr) + } + }) + } +} + func TestValidateRuntimeAssetPath(t *testing.T) { // on macOS the temp dir is behind the /var symlink root, err := filepath.EvalSymlinks(t.TempDir()) @@ -452,3 +454,178 @@ func TestValidateRuntimeAssetPath(t *testing.T) { }) } } + +func TestValidatePinnedImage(t *testing.T) { + runStringRule(t, ValidatePinnedImage, []stringRuleCase{ + {"empty is left to tags", "", false}, + {"pinned", testPinnedImage, false}, + {"tag only", "example.com/app:v1", true}, + {"malformed digest", "example.com/app@sha256:abc", true}, + {"malformed reference", "Example.com/App", true}, + }) +} + +func TestValidateMountPath(t *testing.T) { + runStringRule(t, ValidateMountPath, []stringRuleCase{ + {"empty is left to tags", "", false}, + {"clean absolute", "/data/cache", false}, + {"relative", "data", true}, + {"root", "/", true}, + {"trailing slash", "/data/", true}, + {"double slash", "/data//x", true}, + {"colon", "/data:x", true}, + {"dot segment", "/data/./x", true}, + {"dot-dot segment", "/data/../x", true}, + {"control character", "/data\x01", true}, + }) +} + +func TestValidateHTTPGetPath(t *testing.T) { + runStringRule(t, ValidateHTTPGetPath, []stringRuleCase{ + {"empty is left to tags", "", false}, + {"simple", "/healthz", false}, + {"percent escape", "/a%20b", false}, + {"no leading slash", "healthz", true}, + {"query", "/healthz?x=1", true}, + {"fragment", "/healthz#x", true}, + {"bad percent escape", "/a%2", true}, + }) +} + +func TestValidateEnvVarName(t *testing.T) { + runStringRule(t, ValidateEnvVarName, []stringRuleCase{ + {"empty is left to tags", "", false}, + {"conventional", "HOME_DIR", false}, + {"printable punctuation", "a.b-c", false}, + {"equals sign", "A=B", true}, + {"control character", "A\tB", true}, + {"non-ASCII", "é", true}, + }) +} + +func TestValidateProjectedPath(t *testing.T) { + runStringRule(t, ValidateProjectedPath, []stringRuleCase{ + {"empty is left to tags", "", false}, + {"relative file", "identity/name", false}, + {"absolute", "/etc/name", true}, + {"escape", "../name", true}, + }) +} + +func TestValidateStorageVolumeID(t *testing.T) { + runStringRule(t, ValidateStorageVolumeID, []stringRuleCase{ + {"empty", "", false}, + {"plain", "projects/p/disks/d-1", false}, + {"newline is allowed", "a\nb", false}, + {"NUL", "a\x00b", true}, + {"DEL", "a\x7fb", true}, + {"C1 control", "a\u0085b", true}, + }) +} + +func TestValidateVolumeType(t *testing.T) { + runStringRule(t, ValidateVolumeType, []stringRuleCase{ + {"empty", "", false}, + {"dns subdomain", "pd.csi.storage.gke.io", false}, + {"substrate prefix", "substrate.io/gcs", false}, + {"uppercase", "PD", true}, + {"other prefix", "example.io/gcs", true}, + }) +} + +func TestValidateCapabilities(t *testing.T) { + tests := []struct { + name string + caps []string + allowAll bool + wantErrs int + }{ + {"valid names", []string{"NET_BIND_SERVICE", "SYS_PTRACE"}, false, 0}, + {"ALL rejected for add", []string{"ALL"}, false, 1}, + {"ALL accepted for drop", []string{"ALL"}, true, 0}, + {"CAP_ prefix", []string{"CAP_NET_ADMIN"}, false, 1}, + {"lowercase", []string{"net_admin"}, false, 1}, + {"too long", []string{strings.Repeat("A", 64)}, false, 1}, + {"each bad entry reported", []string{"CAP_X", "ok", "NET_ADMIN"}, false, 2}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if errs := ValidateCapabilities(field.NewPath("add"), tt.caps, tt.allowAll); len(errs) != tt.wantErrs { + t.Errorf("errs = %v, want %d", errs, tt.wantErrs) + } + }) + } +} + +func TestValidateNestedMountPaths(t *testing.T) { + path := field.NewPath("volume_mounts") + tests := []struct { + name string + paths []string + want field.ErrorList + }{ + {name: "siblings", paths: []string{"/a", "/b", "/ab"}}, + {name: "identical paths are left to the list key", paths: []string{"/a", "/a"}}, + {name: "empty paths are skipped", paths: []string{"", "/a"}}, + { + name: "nested under an earlier mount", + paths: []string{"/a", "/a/b"}, + want: field.ErrorList{field.Invalid(path.Index(1).Child("mount_path"), nil, "")}, + }, + { + name: "nested over an earlier mount", + paths: []string{"/a/b", "/a"}, + want: field.ErrorList{field.Invalid(path.Index(1).Child("mount_path"), nil, "")}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + field.ErrorMatcher{}.ByType().ByField().Test(t, tt.want, ValidateNestedMountPaths(path, tt.paths)) + }) + } +} + +func TestValidateCSIMap(t *testing.T) { + path := field.NewPath("publish_context") + many := make(map[string]string, 100) + for i := range 100 { + many[fmt.Sprintf("key-%03d", i)] = "v" + } + tests := []struct { + name string + m map[string]string + want field.ErrorList + }{ + {name: "nil"}, + {name: "one long value", m: map[string]string{"token": strings.Repeat("x", 4000)}}, + {name: "many small entries", m: many}, + {name: "over the CSI spec limit, within the headroom", m: map[string]string{"k": strings.Repeat("x", 8*1024)}}, + {name: "exactly the bound", m: map[string]string{"k": strings.Repeat("x", 40*1024-1)}}, + {name: "one byte over", m: map[string]string{"k": strings.Repeat("x", 40*1024)}, want: field.ErrorList{field.TooLong(path, nil, 40*1024)}}, + {name: "over across entries", m: map[string]string{"a": strings.Repeat("x", 20*1024), "b": strings.Repeat("y", 20*1024)}, want: field.ErrorList{field.TooLong(path, nil, 40*1024)}}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + field.ErrorMatcher{}.ByType().ByField().Test(t, tt.want, ValidateCSIMap(path, tt.m)) + }) + } +} + +func TestValidateHostPort(t *testing.T) { + runStringRule(t, ValidateHostPort, []stringRuleCase{ + {name: "empty is left to presence tags", value: ""}, + {name: "dns name", value: "atenet-egress.ate-system.svc:443"}, + {name: "fully qualified dns name", value: "atenet-egress.ate-system.svc.cluster.local.:443"}, + {name: "ipv4", value: "10.0.0.1:8080"}, + {name: "ipv6", value: "[fd00::1]:443"}, + {name: "missing port", value: "atenet-egress.ate-system.svc", wantErr: true}, + {name: "empty host", value: ":443", wantErr: true}, + {name: "bare dot host", value: ".:443", wantErr: true}, + {name: "uppercase host", value: "Egress.Example:443", wantErr: true}, + {name: "host with a path", value: "egress/../x:443", wantErr: true}, + {name: "port zero", value: "egress.example:0", wantErr: true}, + {name: "port too large", value: "egress.example:65536", wantErr: true}, + {name: "named port", value: "egress.example:https", wantErr: true}, + {name: "unbracketed ipv6", value: "fd00::1:443", wantErr: true}, + }) +} diff --git a/pkg/proto/ateapipb/ateapi.pb.go b/pkg/proto/ateapipb/ateapi.pb.go index 2bcbb20599..fde3450ce7 100644 --- a/pkg/proto/ateapipb/ateapi.pb.go +++ b/pkg/proto/ateapipb/ateapi.pb.go @@ -1049,13 +1049,11 @@ type ExternalVolume struct { Status ExternalVolume_Status `protobuf:"varint,4,opt,name=status,proto3,enum=ateapi.ExternalVolume_Status" json:"status,omitempty"` // volume_context contains metadata returned by the CSI driver during volume // provisioning, needed by the node plugin for mounting (e.g. attachment - // info). Keys and values are the driver's own, so they are only bounded, + // info). Keys and values are the driver's own, so the map is only bounded, // not validated. // // +k8s:optional - // +k8s:maxProperties=32 - // +k8s:eachKey=+k8s:maxLength=128 - // +k8s:eachVal=+k8s:maxLength=256 + // +k8s:customValidation # at most 40 KiB in total, ten times the CSI limit for map fields VolumeContext map[string]string `protobuf:"bytes,5,rep,name=volume_context,json=volumeContext,proto3" json:"volume_context,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache @@ -3877,7 +3875,7 @@ type SystemInfoVolumeSource struct { // +k8s:optional // +k8s:maxItems=8 // +k8s:listType=atomic - // +k8s:customValidation # paths unique across entries + // +k8s:customValidation # at most one actor_metadata; paths unique across entries DataSources []*SystemInfoDataSource `protobuf:"bytes,1,rep,name=data_sources,json=dataSources,proto3" json:"data_sources,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache diff --git a/pkg/proto/ateapipb/ateapi.proto b/pkg/proto/ateapipb/ateapi.proto index 3762751f22..b53d1541e7 100644 --- a/pkg/proto/ateapipb/ateapi.proto +++ b/pkg/proto/ateapipb/ateapi.proto @@ -425,13 +425,11 @@ message ExternalVolume { // volume_context contains metadata returned by the CSI driver during volume // provisioning, needed by the node plugin for mounting (e.g. attachment - // info). Keys and values are the driver's own, so they are only bounded, + // info). Keys and values are the driver's own, so the map is only bounded, // not validated. // // +k8s:optional - // +k8s:maxProperties=32 - // +k8s:eachKey=+k8s:maxLength=128 - // +k8s:eachVal=+k8s:maxLength=256 + // +k8s:customValidation # at most 40 KiB in total, ten times the CSI limit for map fields map volume_context = 5; } @@ -1392,7 +1390,7 @@ message SystemInfoVolumeSource { // +k8s:optional // +k8s:maxItems=8 // +k8s:listType=atomic - // +k8s:customValidation # paths unique across entries + // +k8s:customValidation # at most one actor_metadata; paths unique across entries repeated SystemInfoDataSource data_sources = 1; } From f0df43bd20edc7d1dcdfff851d88a971675a08dd Mon Sep 17 00:00:00 2001 From: Davanum Srinivas Date: Tue, 13 Oct 2026 16:38:31 +0000 Subject: [PATCH 06/42] hack: warn when the host's neighbor table is at the kernel default (#2497) `net.ipv4.neigh.default.gc_thresh3` is global across network namespaces; at its default of 1024 a host with about a thousand sandboxes overflows the neighbor table and connections to sandboxes time out. - `hack/create-kind-cluster.sh` warns on Linux hosts at or below 1024 and prints the values to set; it changes nothing. - One sentence in the README. `shellcheck` clean. --- README.md | 1 + hack/create-kind-cluster.sh | 10 ++++++++++ 2 files changed, 11 insertions(+) diff --git a/README.md b/README.md index 251188fed9..aa85a02f00 100644 --- a/README.md +++ b/README.md @@ -105,6 +105,7 @@ goals in the near term. To quickly set up the complete environment: 1. Make sure you have [Go](https://go.dev/doc/install), [`kubectl`](https://kubernetes.io/docs/tasks/tools/), and [`docker`](https://www.docker.com/) installed and configured on your dev machine. We will automatically manage other dependencies via Go, including [`kind`](https://kind.sigs.k8s.io/). + A Linux host that will run more than about a thousand sandboxes also needs a larger kernel neighbor table (`net.ipv4.neigh.default.gc_thresh1/2/3` = 4096/16384/32768); `hack/create-kind-cluster.sh` warns when the host is at the default of 1024. 2. Run the following steps: ```shell diff --git a/hack/create-kind-cluster.sh b/hack/create-kind-cluster.sh index 280d9d135d..9d5a0e42e5 100755 --- a/hack/create-kind-cluster.sh +++ b/hack/create-kind-cluster.sh @@ -171,6 +171,16 @@ if [[ "${IP_FAMILY}" == "ipv6" ]]; then fi fi +# 2.4 Every sandbox gets a link-local address, and the kernel's neighbor table +# ceiling is global across network namespaces: at its default of 1024 entries a +# host with about a thousand sandboxes overflows it ("neighbour: arp_cache: +# neighbor table overflow!" in dmesg) and connections to sandboxes time out while +# the host sits idle. Only Linux hosts expose the sysctl; warn, do not change it. +if thresh3="$(sysctl -n net.ipv4.neigh.default.gc_thresh3 2>/dev/null)" && [ "${thresh3}" -le 1024 ]; then + echo "warning: net.ipv4.neigh.default.gc_thresh3=${thresh3}; above about 1000 sandboxes on this host set" >&2 + echo " net.ipv4.neigh.default.gc_thresh1=4096 gc_thresh2=16384 gc_thresh3=32768 (sysctl -w, as root)" >&2 +fi + # 2.5 Enable Proxy ARP/NDP on kind nodes for gVisor loopback pod-to-pod networking echo "Enabling Proxy ARP/NDP on kind nodes..." for node in $("${ROOT}"/hack/kind.sh get nodes --name "${KIND_CLUSTER_NAME}"); do From c427a6ae808ca4e5513604de549c8f6f88988151 Mon Sep 17 00:00:00 2001 From: Eric Curtin Date: Tue, 13 Oct 2026 16:58:45 +0000 Subject: [PATCH 07/42] Credential provider tests no longer fail with text file busy (#2549) Fixes #2528 Fake plugins were exec'd while a concurrent fork still held their write fd (ETXTBSY). Writes now hold `syscall.ForkLock`, in one helper. Under CPU load with `-race`: 13 failures in 600 runs before, 0 after. - [x] Tests pass - [x] Appropriate changes to documentation are included in the PR --- .../credentialprovider_test.go | 35 ++++++++++--------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/cmd/atelet/internal/credentialprovider/credentialprovider_test.go b/cmd/atelet/internal/credentialprovider/credentialprovider_test.go index 53b11328a1..8f64b7b2a7 100644 --- a/cmd/atelet/internal/credentialprovider/credentialprovider_test.go +++ b/cmd/atelet/internal/credentialprovider/credentialprovider_test.go @@ -25,6 +25,7 @@ import ( "slices" "sort" "strings" + "syscall" "testing" "testing/synctest" "time" @@ -33,6 +34,19 @@ import ( "github.com/google/go-containerregistry/pkg/name" ) +// writeExecutable writes script into dir/name as an executable. Forks are held +// off until the file is closed: a child forked while it is open keeps the +// write descriptor until it execs, and exec-ing the script meanwhile fails +// with "text file busy" (ETXTBSY). +func writeExecutable(t *testing.T, dir, name, script string) { + t.Helper() + syscall.ForkLock.RLock() + defer syscall.ForkLock.RUnlock() + if err := os.WriteFile(filepath.Join(dir, name), []byte(script), 0o700); err != nil { + t.Fatalf("Failed to write fake plugin %q: %v", name, err) + } +} + // fakePlugin writes an executable named name into dir that records each // request it is handed (one JSON object per line in /.requests) and // replies with response. It returns the requests file's path. @@ -46,9 +60,7 @@ cat <<'RESPONSE' %s RESPONSE `, requests, requests, response) - if err := os.WriteFile(filepath.Join(dir, pluginName), []byte(script), 0o700); err != nil { - t.Fatalf("Failed to write fake plugin: %v", err) - } + writeExecutable(t, dir, pluginName, script) return requests } @@ -56,10 +68,7 @@ RESPONSE // non-zero. func failingPlugin(t *testing.T, dir, pluginName, message string) { t.Helper() - script := fmt.Sprintf("#!/bin/sh\necho %q >&2\nexit 7\n", message) - if err := os.WriteFile(filepath.Join(dir, pluginName), []byte(script), 0o700); err != nil { - t.Fatalf("Failed to write failing plugin: %v", err) - } + writeExecutable(t, dir, pluginName, fmt.Sprintf("#!/bin/sh\necho %q >&2\nexit 7\n", message)) } // writeConfig writes a CredentialProviderConfig into dir and returns its path. @@ -659,9 +668,7 @@ cat < Date: Tue, 13 Oct 2026 17:56:25 +0000 Subject: [PATCH 08/42] add support for connect from atenet (envoy) to atunnel (#2531) - Adds --upstream-connect (default off). The Envoy router sends actor traffic through mTLS CONNECT tunnels to atunnel's 8443 listener. The :443 (non connect) path is unchanged. - Tunnels are per worker, actor and port, and reused across requests. gRPC goes over HTTP/2, everything else over HTTP/1.1. - atunnel now closes a tunnel when the actor closes its connection, so a reused tunnel can't swallow the next request. - With the flag on, atunnel's 421 and 502 reach clients as 503. - A follow-up PR will make CONNECT the default. Fixes #1309 > It's a good idea to open an issue first for discussion. - [x] Tests pass - [x] Appropriate changes to documentation are included in the PR --- cmd/atenet/internal/router/cmd.go | 1 + cmd/atenet/internal/router/config.go | 5 + cmd/atenet/internal/router/config_test.go | 9 + cmd/atenet/internal/router/dataplane.go | 1 + .../internal/router/extproc/attributes.go | 5 +- cmd/atenet/internal/router/xds.go | 486 +++-- .../router/xds_upstream_connect_test.go | 324 ++++ docs/architecture.md | 3 +- internal/atunnel/ingress.go | 68 +- internal/atunnel/ingress_test.go | 99 +- .../e2e/suites/multiactor/multiactor_test.go | 13 +- .../e2e/suites/networking/grpcingress_test.go | 5 +- .../v3/set_filter_state.pb.go | 156 ++ .../v3/set_filter_state.pb.validate.go | 237 +++ .../v3/set_filter_state_vtproto.pb.go | 171 ++ .../network/tcp_proxy/v3/tcp_proxy.pb.go | 1144 ++++++++++++ .../tcp_proxy/v3/tcp_proxy.pb.validate.go | 1571 +++++++++++++++++ .../tcp_proxy/v3/tcp_proxy_vtproto.pb.go | 1128 ++++++++++++ vendor/modules.txt | 2 + 19 files changed, 5273 insertions(+), 155 deletions(-) create mode 100644 cmd/atenet/internal/router/xds_upstream_connect_test.go create mode 100644 vendor/github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/set_filter_state/v3/set_filter_state.pb.go create mode 100644 vendor/github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/set_filter_state/v3/set_filter_state.pb.validate.go create mode 100644 vendor/github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/set_filter_state/v3/set_filter_state_vtproto.pb.go create mode 100644 vendor/github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/tcp_proxy/v3/tcp_proxy.pb.go create mode 100644 vendor/github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/tcp_proxy/v3/tcp_proxy.pb.validate.go create mode 100644 vendor/github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/tcp_proxy/v3/tcp_proxy_vtproto.pb.go diff --git a/cmd/atenet/internal/router/cmd.go b/cmd/atenet/internal/router/cmd.go index d97dd3dbcb..e07cfdcff2 100644 --- a/cmd/atenet/internal/router/cmd.go +++ b/cmd/atenet/internal/router/cmd.go @@ -84,6 +84,7 @@ func bindRouterFlags(fs *pflag.FlagSet, cfg *routerConfig) { fs.StringVar(&cfg.UpstreamCredentialBundlePath, "upstream-credential-bundle", "/run/podidentity.podcert.ate.dev/credential-bundle.pem", "PEM credential bundle (cert+key) the router presents as the client cert when dialing the actor's atunnel ingress server over mTLS. Empty disables upstream mTLS (legacy plaintext pod-IP:80).") fs.StringVar(&cfg.UpstreamTrustBundlePath, "upstream-trust-bundle", "/run/podidentity.podcert.ate.dev/trust-bundle.pem", "PEM trust bundle used to validate the actor's atunnel ingress server certificate.") fs.StringVar(&cfg.UpstreamSpiffePrefix, "upstream-spiffe-prefix", "spiffe://cluster.local/", "SPIFFE URI SAN prefix (trust domain) the actor's atunnel server cert must match. Empty falls back to default SAN check against the dialed pod IP (which SPIFFE-only certs never match).") + fs.BoolVar(&cfg.UpstreamConnect, "upstream-connect", false, "Reach atunnel through mTLS CONNECT tunnels on port 8443 instead of HTTPS on 443 (Envoy dataplane only). Requires --upstream-credential-bundle") fs.StringVar(&cfg.ActorIdentityCAFile, "actor-identity-ca-file", "", "PEM trust bundle for the actor-identity CA, used to verify the actor client certificates presented on egress CONNECTs. Required by the egress gateway's ext_proc sidecar; empty (the default) leaves egress authentication unconfigured and every egress CONNECT is denied.") diff --git a/cmd/atenet/internal/router/config.go b/cmd/atenet/internal/router/config.go index 81dd399d0e..443183855a 100644 --- a/cmd/atenet/internal/router/config.go +++ b/cmd/atenet/internal/router/config.go @@ -173,6 +173,8 @@ type routerConfig struct { // UpstreamSpiffePrefix validates the actor's atunnel server cert by its // SPIFFE URI SAN prefix (trust domain) instead of the dialed pod IP. UpstreamSpiffePrefix string + // UpstreamConnect tunnels actor traffic to atunnel's CONNECT listener. + UpstreamConnect bool // ActorIdentityCAFile is the PEM trust bundle for the actor-identity CA, // used by the egress gateway's ext_proc sidecar to verify the actor client @@ -320,6 +322,9 @@ func (c routerConfig) validate() error { return fmt.Errorf("--extproc-max-requests (%d) must be >= --parked-request-max (%d): a circuit breaker below the parking lot silently truncates it with Envoy-generated 503s", c.ExtProcMaxRequests, c.ParkedRequest.Max) } + if c.UpstreamConnect && c.UpstreamCredentialBundlePath == "" { + return fmt.Errorf("--upstream-connect requires --upstream-credential-bundle: atunnel's CONNECT listener accepts only mTLS") + } if c.EgressPolicyCacheTTL < 0 { return fmt.Errorf("--egress-policy-cache-ttl must not be negative, got %s (0 disables the cache)", c.EgressPolicyCacheTTL) } diff --git a/cmd/atenet/internal/router/config_test.go b/cmd/atenet/internal/router/config_test.go index 679e9be8f3..de46db31ea 100644 --- a/cmd/atenet/internal/router/config_test.go +++ b/cmd/atenet/internal/router/config_test.go @@ -107,6 +107,15 @@ func TestRouterConfigValidate(t *testing.T) { cfg: routerConfig{Mode: ModeIngress, ParkedRequest: ingress.ParkedRequestConfig{Max: ingress.DefaultParkedRequestMax}, DrainTimeout: -time.Second}, wantErr: "--drain-timeout must not be negative", }, + { + name: "upstream-connect with the upstream credential bundle accepted", + cfg: routerConfig{Mode: ModeIngress, UpstreamConnect: true, UpstreamCredentialBundlePath: "/run/bundle.pem"}, + }, + { + name: "upstream-connect without the upstream credential bundle rejected", + cfg: routerConfig{Mode: ModeIngress, UpstreamConnect: true}, + wantErr: "--upstream-connect requires --upstream-credential-bundle", + }, { name: "negative drain-delay rejected", cfg: routerConfig{Mode: ModeIngress, ParkedRequest: ingress.ParkedRequestConfig{Max: ingress.DefaultParkedRequestMax}, DrainDelay: -time.Second}, diff --git a/cmd/atenet/internal/router/dataplane.go b/cmd/atenet/internal/router/dataplane.go index f2bc3e8c71..c3c4f0d9ab 100644 --- a/cmd/atenet/internal/router/dataplane.go +++ b/cmd/atenet/internal/router/dataplane.go @@ -100,6 +100,7 @@ func (s *RouterServer) startEnvoyDataplane(ctx context.Context, g *errgroup.Grou slog.WarnContext(ctx, "Ingress client authentication is optional: any client that presents no certificate, or uses a plaintext listener, can reach any actor. Set --ingress-auth-mode=static-mtls to require mTLS") } xdsSrv.SetUpstreamTls(s.cfg.UpstreamCredentialBundlePath, s.cfg.UpstreamTrustBundlePath, s.cfg.UpstreamSpiffePrefix) + xdsSrv.SetUpstreamConnect(s.cfg.UpstreamConnect) // The snapshot is a pure function of the configuration applied above, so it // is built exactly once; an Envoy that connects later is served from the diff --git a/cmd/atenet/internal/router/extproc/attributes.go b/cmd/atenet/internal/router/extproc/attributes.go index f347e58d43..e5721d2a5a 100644 --- a/cmd/atenet/internal/router/extproc/attributes.go +++ b/cmd/atenet/internal/router/extproc/attributes.go @@ -143,8 +143,9 @@ const UpstreamDynamicHostFilterStateKey = "envoy.upstream.dynamic_host" const EgressPolicyMetadataFormat = "%DYNAMIC_METADATA(" + EgressPolicyMetadataNamespace + ")%" // OriginalDstFilterStateKey is Envoy's filter-state key for the address an -// ORIGINAL_DST cluster dials. The gateway never writes it and dials only by -// name; the request legs read it as an attribute. +// ORIGINAL_DST cluster dials. The ingress router sets it for actor tunnels. +// The egress gateway never writes it and dials only by name; its request legs +// read it as an attribute. const OriginalDstFilterStateKey = "envoy.network.transport_socket.original_dst_address" // OriginalDstIPAttribute and OriginalDstPortAttribute are the CEL expressions diff --git a/cmd/atenet/internal/router/xds.go b/cmd/atenet/internal/router/xds.go index b50dbefdab..a3b3089426 100644 --- a/cmd/atenet/internal/router/xds.go +++ b/cmd/atenet/internal/router/xds.go @@ -51,6 +51,8 @@ import ( setfilterstatev3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/set_filter_state/v3" originaldstv3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/listener/original_dst/v3" hcmv3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/http_connection_manager/v3" + setfilterstatenetworkv3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/set_filter_state/v3" + tcpproxyv3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/tcp_proxy/v3" internalupstreamv3 "github.com/envoyproxy/go-control-plane/envoy/extensions/transport_sockets/internal_upstream/v3" rawbufferv3 "github.com/envoyproxy/go-control-plane/envoy/extensions/transport_sockets/raw_buffer/v3" tlsv3 "github.com/envoyproxy/go-control-plane/envoy/extensions/transport_sockets/tls/v3" @@ -109,6 +111,12 @@ const ( // request headers for atunnel to authorize. OriginalDstClusterName = "actor_original_dst" + // With upstream CONNECT, requests go through an actor_tunnel cluster into + // the actor_connect listener, which tunnels them to atunnel. + ActorTunnelHTTP1ClusterName = "actor_tunnel_http1" + ActorTunnelHTTP2ClusterName = "actor_tunnel_http2" + ActorConnectName = "actor_connect" + WildcardIP = "0.0.0.0" ConnectUpgradeType = "CONNECT" MainInternalName = "main_internal" @@ -183,6 +191,24 @@ const envoyDefaultStreamIdleTimeout = 5 * time.Minute // one is wide enough to read as deliberate in a config dump. const routeIdleTimeoutMargin = 30 * time.Second +// Filter state that carries the routing decision into actor_connect. +// tunnelKeyFilterStateKey is the only hashable value and Envoy pools tunnels by +// it, so a tunnel only carries one actor and port. A second hashable value +// would split the pool, since Envoy hashes them in map order. +const ( + tunnelKeyFilterStateKey = "dev.ate.tunnel.key" + tunnelWorkerFilterStateKey = "dev.ate.tunnel.worker" + tunnelActorFilterStateKey = "dev.ate.tunnel.actor" + tunnelPortFilterStateKey = "dev.ate.tunnel.port" +) + +// tunnelCircuitBreakerLimit lifts Envoy's default of 1024: there is a tunnel +// per active actor and port, and idle ones count too. +const tunnelCircuitBreakerLimit = 65536 + +// tunnelIdleTimeout closes unused tunnels, which hold actor connections. +const tunnelIdleTimeout = 60 * time.Second + // XdsServer implements an aggregated discovery service server for dynamic Envoy router nodes. type XdsServer struct { xdsPort int @@ -213,6 +239,8 @@ type XdsServer struct { // cert carries only a spiffe:// URI SAN, so without this Envoy's default // SAN check against the dialed IP fails ("verify SAN list"). upstreamSpiffePrefix string + // upstreamConnect: see SetUpstreamConnect. + upstreamConnect bool // Downstream (client-facing) mTLS. When downstreamClientCAPath is set, // every TLS ingress listener requests a client certificate and rejects @@ -380,6 +408,15 @@ func (x *XdsServer) SetUpstreamTls(credentialBundlePath, trustBundlePath, spiffe x.upstreamSpiffePrefix = spiffePrefix } +// SetUpstreamConnect makes actor traffic reach atunnel through CONNECT tunnels +// on atunnel.DefaultConnectPort, one per actor and port. It needs +// SetUpstreamTls, since that listener only accepts mTLS. +func (x *XdsServer) SetUpstreamConnect(enabled bool) { + x.mu.Lock() + defer x.mu.Unlock() + x.upstreamConnect = enabled +} + // SetOtlpCollector enables Envoy-side tracing pointed at the OTLP gRPC // collector. addr empty disables tracing. See normalizeOtlpCollector for the // accepted forms. @@ -486,7 +523,15 @@ func (x *XdsServer) UpdateSnapshot() error { // Clusters clusters := []types.Resource{ x.buildCluster(), - x.buildOriginalDstCluster(), + } + if x.upstreamConnect { + clusters = append(clusters, + buildActorTunnelCluster(ActorTunnelHTTP1ClusterName, false), + buildActorTunnelCluster(ActorTunnelHTTP2ClusterName, true), + x.buildActorConnectCluster(), + ) + } else { + clusters = append(clusters, x.buildOriginalDstCluster()) } if connectEnabled { clusters = append(clusters, x.buildMainInternalCluster()) @@ -505,6 +550,9 @@ func (x *XdsServer) UpdateSnapshot() error { if x.ingressPort > 0 { listeners = append(listeners, x.buildListener()) } + if x.upstreamConnect { + listeners = append(listeners, buildActorConnectListener()) + } if connectEnabled { listeners = append(listeners, x.buildMainInternalListener()) } @@ -764,28 +812,21 @@ func rawBuffer() *corev3.TransportSocket { } } -// MainInternalCluster is a simple cluster that sends traffic to the main_internal -// internal listener (used only for CONNECT requests). -func (x *XdsServer) buildMainInternalCluster() *clusterv3.Cluster { - return &clusterv3.Cluster{ - Name: MainInternalName, - ClusterDiscoveryType: &clusterv3.Cluster_Type{ - Type: clusterv3.Cluster_STATIC, - }, - LoadAssignment: &endpointv3.ClusterLoadAssignment{ - ClusterName: MainInternalName, - Endpoints: []*endpointv3.LocalityLbEndpoints{ - { - LbEndpoints: []*endpointv3.LbEndpoint{ - { - HostIdentifier: &endpointv3.LbEndpoint_Endpoint{ - Endpoint: &endpointv3.Endpoint{ - Address: &corev3.Address{ - Address: &corev3.Address_EnvoyInternalAddress{ - EnvoyInternalAddress: &corev3.EnvoyInternalAddress{ - AddressNameSpecifier: &corev3.EnvoyInternalAddress_ServerListenerName{ - ServerListenerName: MainInternalName, - }, +// internalListenerAssignment points a cluster at an internal listener. +func internalListenerAssignment(clusterName, listenerName string) *endpointv3.ClusterLoadAssignment { + return &endpointv3.ClusterLoadAssignment{ + ClusterName: clusterName, + Endpoints: []*endpointv3.LocalityLbEndpoints{ + { + LbEndpoints: []*endpointv3.LbEndpoint{ + { + HostIdentifier: &endpointv3.LbEndpoint_Endpoint{ + Endpoint: &endpointv3.Endpoint{ + Address: &corev3.Address{ + Address: &corev3.Address_EnvoyInternalAddress{ + EnvoyInternalAddress: &corev3.EnvoyInternalAddress{ + AddressNameSpecifier: &corev3.EnvoyInternalAddress_ServerListenerName{ + ServerListenerName: listenerName, }, }, }, @@ -796,6 +837,18 @@ func (x *XdsServer) buildMainInternalCluster() *clusterv3.Cluster { }, }, }, + } +} + +// MainInternalCluster is a simple cluster that sends traffic to the main_internal +// internal listener (used only for CONNECT requests). +func (x *XdsServer) buildMainInternalCluster() *clusterv3.Cluster { + return &clusterv3.Cluster{ + Name: MainInternalName, + ClusterDiscoveryType: &clusterv3.Cluster_Type{ + Type: clusterv3.Cluster_STATIC, + }, + LoadAssignment: internalListenerAssignment(MainInternalName, MainInternalName), TransportSocket: &corev3.TransportSocket{ Name: "internal_upstream", ConfigType: &corev3.TransportSocket_TypedConfig{ @@ -866,51 +919,191 @@ func (x *XdsServer) buildOriginalDstCluster() *clusterv3.Cluster { return cluster } +// buildActorTunnelCluster feeds the actor_connect listener, one tunnel per +// connection. The actor receives this protocol as is, so it is fixed per +// cluster: HTTP/2 for gRPC, HTTP/1.1 otherwise. +func buildActorTunnelCluster(name string, http2 bool) *clusterv3.Cluster { + explicit := &httpv3.HttpProtocolOptions_ExplicitHttpConfig{ + ProtocolConfig: &httpv3.HttpProtocolOptions_ExplicitHttpConfig_HttpProtocolOptions{ + HttpProtocolOptions: &corev3.Http1ProtocolOptions{}, + }, + } + if http2 { + explicit.ProtocolConfig = &httpv3.HttpProtocolOptions_ExplicitHttpConfig_Http2ProtocolOptions{ + Http2ProtocolOptions: &corev3.Http2ProtocolOptions{}, + } + } + return &clusterv3.Cluster{ + Name: name, + ClusterDiscoveryType: &clusterv3.Cluster_Type{ + Type: clusterv3.Cluster_STATIC, + }, + LoadAssignment: internalListenerAssignment(name, ActorConnectName), + TransportSocket: &corev3.TransportSocket{ + Name: "internal_upstream", + ConfigType: &corev3.TransportSocket_TypedConfig{ + TypedConfig: newAny(&internalupstreamv3.InternalUpstreamTransport{ + TransportSocket: rawBuffer(), + }), + }, + }, + CircuitBreakers: tunnelCircuitBreakers(), + TypedExtensionProtocolOptions: map[string]*anypb.Any{ + httpProtocolOptionsName: newAny(&httpv3.HttpProtocolOptions{ + CommonHttpProtocolOptions: &corev3.HttpProtocolOptions{ + IdleTimeout: durationpb.New(tunnelIdleTimeout), + }, + UpstreamProtocolOptions: &httpv3.HttpProtocolOptions_ExplicitHttpConfig_{ + ExplicitHttpConfig: explicit, + }, + }), + }, + } +} + +// buildActorConnectCluster dials atunnel's CONNECT port on the worker that +// actor_connect puts in filter state. HTTP/2 lets a worker's tunnels share a +// connection. +func (x *XdsServer) buildActorConnectCluster() *clusterv3.Cluster { + return &clusterv3.Cluster{ + Name: ActorConnectName, + ConnectTimeout: durationpb.New(5 * time.Second), + ClusterDiscoveryType: &clusterv3.Cluster_Type{ + Type: clusterv3.Cluster_ORIGINAL_DST, + }, + LbPolicy: clusterv3.Cluster_CLUSTER_PROVIDED, + LbConfig: &clusterv3.Cluster_OriginalDstLbConfig_{ + OriginalDstLbConfig: &clusterv3.Cluster_OriginalDstLbConfig{ + UpstreamPortOverride: wrapperspb.UInt32(atunnel.DefaultConnectPort), + }, + }, + TransportSocket: x.buildUpstreamTransportSocket(), + CircuitBreakers: tunnelCircuitBreakers(), + TypedExtensionProtocolOptions: map[string]*anypb.Any{ + httpProtocolOptionsName: newAny(&httpv3.HttpProtocolOptions{ + UpstreamProtocolOptions: &httpv3.HttpProtocolOptions_ExplicitHttpConfig_{ + ExplicitHttpConfig: &httpv3.HttpProtocolOptions_ExplicitHttpConfig{ + ProtocolConfig: &httpv3.HttpProtocolOptions_ExplicitHttpConfig_Http2ProtocolOptions{ + Http2ProtocolOptions: &corev3.Http2ProtocolOptions{}, + }, + }, + }, + }), + }, + } +} + +func tunnelCircuitBreakers() *clusterv3.CircuitBreakers { + return &clusterv3.CircuitBreakers{ + Thresholds: []*clusterv3.CircuitBreakers_Thresholds{{ + Priority: corev3.RoutingPriority_DEFAULT, + MaxConnections: wrapperspb.UInt32(tunnelCircuitBreakerLimit), + MaxPendingRequests: wrapperspb.UInt32(tunnelCircuitBreakerLimit), + MaxRequests: wrapperspb.UInt32(tunnelCircuitBreakerLimit), + }}, + } +} + func (x *XdsServer) buildRoutes() *routev3.RouteConfiguration { + var routes []*routev3.Route + if x.upstreamConnect { + routes = x.buildActorTunnelRoutes() + } else { + routes = []*routev3.Route{ + { + Match: &routev3.RouteMatch{ + PathSpecifier: &routev3.RouteMatch_Prefix{ + Prefix: "/", + }, + }, + Action: &routev3.Route_Route{ + Route: x.workloadRouteAction(OriginalDstClusterName), + }, + RequestHeadersToAdd: []*corev3.HeaderValueOption{ + { + Header: &corev3.HeaderValue{ + Key: atunnel.TargetPortHeader, + Value: fmt.Sprintf("%%DYNAMIC_METADATA(%s:%s)%%", + ingress.OriginalDstMetadataKey, ingress.OriginalDstPortKey), + }, + AppendAction: corev3.HeaderValueOption_OVERWRITE_IF_EXISTS_OR_ADD, + }, + }, + }, + } + } return &routev3.RouteConfiguration{ Name: RouteName, VirtualHosts: []*routev3.VirtualHost{ { Name: "local_service", Domains: []string{"*"}, - Routes: []*routev3.Route{ - { - Match: &routev3.RouteMatch{ - PathSpecifier: &routev3.RouteMatch_Prefix{ - Prefix: "/", - }, - }, - Action: &routev3.Route_Route{ - Route: &routev3.RouteAction{ - ClusterSpecifier: &routev3.RouteAction_Cluster{ - Cluster: OriginalDstClusterName, - }, - // Two separate limits: Timeout bounds the upstream response, - // IdleTimeout bounds a stream with no activity on it, and - // routeIdleTimeout keeps the second from firing before the - // first. This route also serves CONNECT-tunneled traffic - // re-injected via main_internal, where Envoy applies Timeout to - // the whole tunnel lifetime, so a long-lived tunnel needs - // --route-timeout raised like a long LLM turn does. - Timeout: durationpb.New(x.routeTimeout), - IdleTimeout: durationpb.New(x.routeIdleTimeout()), - }, - }, - RequestHeadersToAdd: []*corev3.HeaderValueOption{ - { - Header: &corev3.HeaderValue{ - Key: atunnel.TargetPortHeader, - Value: fmt.Sprintf("%%DYNAMIC_METADATA(%s:%s)%%", - ingress.OriginalDstMetadataKey, ingress.OriginalDstPortKey), - }, - AppendAction: corev3.HeaderValueOption_OVERWRITE_IF_EXISTS_OR_ADD, - }, - }, + Routes: routes, + }, + }, + } +} + +func (x *XdsServer) workloadRouteAction(cluster string) *routev3.RouteAction { + return &routev3.RouteAction{ + ClusterSpecifier: &routev3.RouteAction_Cluster{ + Cluster: cluster, + }, + // Two separate limits: Timeout bounds the upstream response, + // IdleTimeout bounds a stream with no activity on it, and + // routeIdleTimeout keeps the second from firing before the + // first. This route also serves CONNECT-tunneled traffic + // re-injected via main_internal, where Envoy applies Timeout to + // the whole tunnel lifetime, so a long-lived tunnel needs + // --route-timeout raised like a long LLM turn does. + Timeout: durationpb.New(x.routeTimeout), + IdleTimeout: durationpb.New(x.routeIdleTimeout()), + } +} + +// buildActorTunnelRoutes sends gRPC over HTTP/2 and everything else over +// HTTP/1.1, so HTTP/1.1-only actors keep working. A route can't see the +// client's protocol, so gRPC-shaped HTTP/1.1 requests go out as HTTP/2 too. +func (x *XdsServer) buildActorTunnelRoutes() []*routev3.Route { + route := func(cluster string, headers ...*routev3.HeaderMatcher) *routev3.Route { + return &routev3.Route{ + Match: &routev3.RouteMatch{ + PathSpecifier: &routev3.RouteMatch_Prefix{ + Prefix: "/", + }, + Headers: headers, + }, + Action: &routev3.Route_Route{ + Route: x.workloadRouteAction(cluster), + }, + // Nothing past this point strips it before the actor. + RequestHeadersToRemove: []string{atenet.TargetActorHeader}, + } + } + grpcHeaders := []*routev3.HeaderMatcher{ + { + Name: ":method", + HeaderMatchSpecifier: &routev3.HeaderMatcher_StringMatch{ + StringMatch: &matcherv3.StringMatcher{ + MatchPattern: &matcherv3.StringMatcher_Exact{Exact: "POST"}, + }, + }, + }, + { + Name: "content-type", + HeaderMatchSpecifier: &routev3.HeaderMatcher_StringMatch{ + StringMatch: &matcherv3.StringMatcher{ + MatchPattern: &matcherv3.StringMatcher_SafeRegex{ + SafeRegex: &matcherv3.RegexMatcher{Regex: `(?i)^application/grpc([+;].*)?$`}, }, }, }, }, } + return []*routev3.Route{ + route(ActorTunnelHTTP2ClusterName, grpcHeaders...), + route(ActorTunnelHTTP1ClusterName), + } } func (x *XdsServer) buildMainInternalListener() *listenerv3.Listener { @@ -947,38 +1140,30 @@ func (x *XdsServer) buildMainInternalListener() *listenerv3.Listener { } } -// actorRoutingFilterStateFilter captures the actor routing header so -// main_internal can read it across the CONNECT internal-listener hop. -func actorRoutingFilterStateFilter(captureAuthority bool) *hcmv3.HttpFilter { - values := make([]*setfilterstatecommonv3.FilterStateValue, 0, 2) - routingFields := []struct{ key, format string }{ - {extproc.TargetActorFilterStateKey, "%CEL(request.headers['" + atenet.TargetActorHeader + "'])%"}, - } - if captureAuthority { - routingFields = append(routingFields, struct{ key, format string }{ - extproc.ConnectAuthorityFilterStateKey, "%REQ(" + extproc.AuthorityHeader + ")%", - }) - } - for _, routingField := range routingFields { - values = append(values, &setfilterstatecommonv3.FilterStateValue{ - Key: &setfilterstatecommonv3.FilterStateValue_ObjectKey{ - ObjectKey: routingField.key, - }, - FactoryKey: "envoy.string", - Value: &setfilterstatecommonv3.FilterStateValue_FormatString{ - FormatString: &corev3.SubstitutionFormatString{ - Format: &corev3.SubstitutionFormatString_TextFormatSource{ - TextFormatSource: &corev3.DataSource{ - Specifier: &corev3.DataSource_InlineString{ - InlineString: routingField.format, - }, +// filterStateValue sets key from format. An empty factory means the one +// registered under key. +func filterStateValue(key, factory, format string, shared setfilterstatecommonv3.FilterStateValue_SharedWithUpstream) *setfilterstatecommonv3.FilterStateValue { + return &setfilterstatecommonv3.FilterStateValue{ + Key: &setfilterstatecommonv3.FilterStateValue_ObjectKey{ + ObjectKey: key, + }, + FactoryKey: factory, + Value: &setfilterstatecommonv3.FilterStateValue_FormatString{ + FormatString: &corev3.SubstitutionFormatString{ + Format: &corev3.SubstitutionFormatString_TextFormatSource{ + TextFormatSource: &corev3.DataSource{ + Specifier: &corev3.DataSource_InlineString{ + InlineString: format, }, }, }, }, - SharedWithUpstream: setfilterstatecommonv3.FilterStateValue_ONCE, - }) + }, + SharedWithUpstream: shared, } +} + +func setFilterStateHTTPFilter(values ...*setfilterstatecommonv3.FilterStateValue) *hcmv3.HttpFilter { return &hcmv3.HttpFilter{ Name: "envoy.filters.http.set_filter_state", ConfigType: &hcmv3.HttpFilter_TypedConfig{ @@ -989,6 +1174,98 @@ func actorRoutingFilterStateFilter(captureAuthority bool) *hcmv3.HttpFilter { } } +// actorRoutingFilterStateFilter captures the actor routing header so +// main_internal can read it across the CONNECT internal-listener hop. +func actorRoutingFilterStateFilter(captureAuthority bool) *hcmv3.HttpFilter { + once := setfilterstatecommonv3.FilterStateValue_ONCE + values := []*setfilterstatecommonv3.FilterStateValue{ + filterStateValue(extproc.TargetActorFilterStateKey, "envoy.string", "%CEL(request.headers['"+atenet.TargetActorHeader+"'])%", once), + } + if captureAuthority { + values = append(values, + filterStateValue(extproc.ConnectAuthorityFilterStateKey, "envoy.string", "%REQ("+extproc.AuthorityHeader+")%", once)) + } + return setFilterStateHTTPFilter(values...) +} + +// actorTunnelFilterStateFilter copies ext_proc's routing decision into filter +// state. It must run after ext_proc, which overwrites the actor header with the +// checked value. +func actorTunnelFilterStateFilter() *hcmv3.HttpFilter { + worker := "%DYNAMIC_METADATA(" + ingress.OriginalDstMetadataKey + ":" + ingress.OriginalDstAddressKey + ")%" + actor := "%REQ(" + atenet.TargetActorHeader + ")%" + port := "%DYNAMIC_METADATA(" + ingress.OriginalDstMetadataKey + ":" + ingress.OriginalDstPortKey + ")%" + once := setfilterstatecommonv3.FilterStateValue_ONCE + return setFilterStateHTTPFilter( + filterStateValue(tunnelKeyFilterStateKey, "envoy.hashable_string", worker+"|"+actor+"|"+port, once), + filterStateValue(tunnelWorkerFilterStateKey, "envoy.string", worker, once), + filterStateValue(tunnelActorFilterStateKey, "envoy.string", actor, once), + filterStateValue(tunnelPortFilterStateKey, "envoy.string", port, once), + ) +} + +// buildActorConnectListener turns each connection from an actor_tunnel cluster +// into a CONNECT to atunnel, which takes the port from the authority and +// ignores the host. +func buildActorConnectListener() *listenerv3.Listener { + return &listenerv3.Listener{ + Name: ActorConnectName, + ListenerSpecifier: &listenerv3.Listener_InternalListener{ + InternalListener: &listenerv3.Listener_InternalListenerConfig{}, + }, + FilterChains: []*listenerv3.FilterChain{ + { + Filters: []*listenerv3.Filter{ + { + // Set here so the address stays out of the tunnel + // pool's key. + Name: "envoy.filters.network.set_filter_state", + ConfigType: &listenerv3.Filter_TypedConfig{ + TypedConfig: newAny(&setfilterstatenetworkv3.Config{ + OnNewConnection: []*setfilterstatecommonv3.FilterStateValue{ + filterStateValue(extproc.OriginalDstFilterStateKey, "", + "%FILTER_STATE("+tunnelWorkerFilterStateKey+":PLAIN)%", + setfilterstatecommonv3.FilterStateValue_NONE), + }, + }), + }, + }, + { + Name: "envoy.filters.network.tcp_proxy", + ConfigType: &listenerv3.Filter_TypedConfig{ + TypedConfig: newAny(&tcpproxyv3.TcpProxy{ + StatPrefix: ActorConnectName, + ClusterSpecifier: &tcpproxyv3.TcpProxy_Cluster{ + Cluster: ActorConnectName, + }, + TunnelingConfig: &tcpproxyv3.TcpProxy_TunnelingConfig{ + Hostname: "actor:%FILTER_STATE(" + tunnelPortFilterStateKey + ":PLAIN)%", + HeadersToAdd: []*corev3.HeaderValueOption{ + { + Header: &corev3.HeaderValue{ + Key: atenet.TargetActorHeader, + Value: "%FILTER_STATE(" + tunnelActorFilterStateKey + ":PLAIN)%", + }, + AppendAction: corev3.HeaderValueOption_OVERWRITE_IF_EXISTS_OR_ADD, + }, + }, + // Logs why atunnel refused a CONNECT; the + // client only sees a 503. + PropagateResponseHeaders: true, + }, + // Off, so tunnelIdleTimeout and the route + // timeouts always fire first. + IdleTimeout: durationpb.New(0), + AccessLog: stdoutAccessLog(tunnelAccessLogFormat), + }), + }, + }, + }, + }, + }, + } +} + func (x *XdsServer) buildConnectTerminateHCM(statPrefix string) *anypb.Any { routerAny := newAny(&routerv3.Router{}) hcm := newAny(&hcmv3.HttpConnectionManager{ @@ -997,7 +1274,7 @@ func (x *XdsServer) buildConnectTerminateHCM(statPrefix string) *anypb.Any { Tracing: x.buildTracing(), // TODO: this format is not very useful for CONNECT requests. // Need to customize it to surface useful information - AccessLog: stdoutAccessLog(), + AccessLog: stdoutAccessLog(accessLogFormat), RouteSpecifier: &hcmv3.HttpConnectionManager_RouteConfig{ RouteConfig: buildConnectRoutes(), }, @@ -1113,20 +1390,21 @@ func (x *XdsServer) buildHcm(statPrefix string, captureActorRouting bool) *anypb if captureActorRouting { httpFilters = append(httpFilters, actorRoutingFilterStateFilter(false)) } - httpFilters = append(httpFilters, - &hcmv3.HttpFilter{ - Name: httpExtProcFilterName, - ConfigType: &hcmv3.HttpFilter_TypedConfig{ - TypedConfig: extProcConfig, - }, + httpFilters = append(httpFilters, &hcmv3.HttpFilter{ + Name: httpExtProcFilterName, + ConfigType: &hcmv3.HttpFilter_TypedConfig{ + TypedConfig: extProcConfig, }, - &hcmv3.HttpFilter{ - Name: "envoy.filters.http.router", - ConfigType: &hcmv3.HttpFilter_TypedConfig{ - TypedConfig: routerAny, - }, + }) + if x.upstreamConnect { + httpFilters = append(httpFilters, actorTunnelFilterStateFilter()) + } + httpFilters = append(httpFilters, &hcmv3.HttpFilter{ + Name: "envoy.filters.http.router", + ConfigType: &hcmv3.HttpFilter_TypedConfig{ + TypedConfig: routerAny, }, - ) + }) hcm := newAny(&hcmv3.HttpConnectionManager{ StatPrefix: statPrefix, @@ -1135,7 +1413,7 @@ func (x *XdsServer) buildHcm(statPrefix string, captureActorRouting bool) *anypb {UpgradeType: "websocket"}, }, Tracing: x.buildTracing(), - AccessLog: stdoutAccessLog(), + AccessLog: stdoutAccessLog(accessLogFormat), HttpFilters: httpFilters, RouteSpecifier: &hcmv3.HttpConnectionManager_Rds{ Rds: &hcmv3.Rds{ @@ -1498,9 +1776,15 @@ const accessLogFormat = `[%START_TIME%] "%REQ(:METHOD)% %PATH(NQ:ORIG_OR_PATH)% `%RESP(X-ENVOY-UPSTREAM-SERVICE-TIME)% "%REQ(X-FORWARDED-FOR)%" "%REQ(USER-AGENT)%" ` + `"%REQ(X-REQUEST-ID)%" "%REQ(:AUTHORITY)%" "%UPSTREAM_HOST%"` + "\n" -// stdoutAccessLog returns the access log configuration shared by the router's -// HTTP connection managers. -func stdoutAccessLog() []*accesslogv3.AccessLog { +// tunnelAccessLogFormat logs each tunnel when it closes, with atunnel's CONNECT +// response. +const tunnelAccessLogFormat = `[%START_TIME%] tunnel actor=%FILTER_STATE(` + tunnelActorFilterStateKey + `:PLAIN)% ` + + `port=%FILTER_STATE(` + tunnelPortFilterStateKey + `:PLAIN)% "%UPSTREAM_HOST%" %RESPONSE_FLAGS% ` + + `connect_response=%FILTER_STATE(envoy.tcp_proxy.propagate_response_headers:TYPED)% ` + + `%BYTES_RECEIVED% %BYTES_SENT% %DURATION%` + "\n" + +// stdoutAccessLog returns a stdout access log that writes format. +func stdoutAccessLog(format string) []*accesslogv3.AccessLog { return []*accesslogv3.AccessLog{ { Name: "envoy.access_loggers.stdout", @@ -1510,7 +1794,7 @@ func stdoutAccessLog() []*accesslogv3.AccessLog { LogFormat: &corev3.SubstitutionFormatString{ Format: &corev3.SubstitutionFormatString_TextFormatSource{ TextFormatSource: &corev3.DataSource{ - Specifier: &corev3.DataSource_InlineString{InlineString: accessLogFormat}, + Specifier: &corev3.DataSource_InlineString{InlineString: format}, }, }, }, diff --git a/cmd/atenet/internal/router/xds_upstream_connect_test.go b/cmd/atenet/internal/router/xds_upstream_connect_test.go new file mode 100644 index 0000000000..7bea556e82 --- /dev/null +++ b/cmd/atenet/internal/router/xds_upstream_connect_test.go @@ -0,0 +1,324 @@ +// 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 ( + "regexp" + "testing" + + clusterv3 "github.com/envoyproxy/go-control-plane/envoy/config/cluster/v3" + listenerv3 "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3" + setfilterstatecommonv3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/common/set_filter_state/v3" + setfilterstatev3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/set_filter_state/v3" + hcmv3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/http_connection_manager/v3" + setfilterstatenetworkv3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/set_filter_state/v3" + tcpproxyv3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/tcp_proxy/v3" + tlsv3 "github.com/envoyproxy/go-control-plane/envoy/extensions/transport_sockets/tls/v3" + httpv3 "github.com/envoyproxy/go-control-plane/envoy/extensions/upstreams/http/v3" + cachev3 "github.com/envoyproxy/go-control-plane/pkg/cache/v3" + resourcev3 "github.com/envoyproxy/go-control-plane/pkg/resource/v3" + + "github.com/agent-substrate/substrate/cmd/atenet/internal/router/extproc" + "github.com/agent-substrate/substrate/internal/atenet" + "github.com/agent-substrate/substrate/internal/atunnel" +) + +// upstreamConnectSnapshot also enables the CONNECT listener, so main_internal +// is covered. +func upstreamConnectSnapshot(t *testing.T) *cachev3.Snapshot { + t.Helper() + server := NewXdsServer(18000) + server.SetConfig(8085, 50053, "127.0.0.1") + server.SetConnectPorts(8081, 0) + server.SetUpstreamTls("/run/bundle.pem", "/run/trust.pem", "spiffe://cluster.local/") + server.SetUpstreamConnect(true) + if err := server.UpdateSnapshot(); err != nil { + t.Fatalf("UpdateSnapshot failed: %v", err) + } + res, err := server.snapshot.GetSnapshot(NodeID) + if err != nil { + t.Fatalf("Failed to get snapshot: %v", err) + } + snap := res.(*cachev3.Snapshot) + if err := snap.Consistent(); err != nil { + t.Fatalf("Integrity check failed on snapshot: %v", err) + } + return snap +} + +func TestXdsServer_UpstreamConnectResources(t *testing.T) { + snap := upstreamConnectSnapshot(t) + + clusters := snap.GetResources(resourcev3.ClusterType) + for _, name := range []string{ClusterName, MainInternalName, ActorTunnelHTTP1ClusterName, ActorTunnelHTTP2ClusterName, ActorConnectName} { + if _, ok := clusters[name]; !ok { + t.Errorf("cluster %q missing", name) + } + } + if _, ok := clusters[OriginalDstClusterName]; ok { + t.Errorf("cluster %q built although no route sends to it", OriginalDstClusterName) + } + if len(clusters) != 5 { + t.Errorf("got %d clusters, want 5", len(clusters)) + } + + listeners := snap.GetResources(resourcev3.ListenerType) + l, ok := listeners[ActorConnectName] + if !ok { + t.Fatalf("listener %q missing", ActorConnectName) + } + if l.(*listenerv3.Listener).GetInternalListener() == nil { + t.Errorf("listener %q is not an internal listener", ActorConnectName) + } + + // The tunnel filter copies ext_proc's decision, and the router picks a + // tunnel by it. + for _, name := range []string{IngressHTTPListener, MainInternalName} { + hcm := &hcmv3.HttpConnectionManager{} + if err := listeners[name].(*listenerv3.Listener).GetFilterChains()[0].GetFilters()[0].GetTypedConfig().UnmarshalTo(hcm); err != nil { + t.Fatalf("listener %s: unmarshal HCM: %v", name, err) + } + filters := hcm.GetHttpFilters() + var names []string + for _, f := range filters { + names = append(names, f.GetName()) + } + n := len(filters) + if n < 3 || names[n-3] != httpExtProcFilterName || names[n-2] != "envoy.filters.http.set_filter_state" || names[n-1] != "envoy.filters.http.router" { + t.Fatalf("listener %s HTTP filters = %v, want them to end with ext_proc, set_filter_state, router", name, names) + } + config := &setfilterstatev3.Config{} + if err := filters[n-2].GetTypedConfig().UnmarshalTo(config); err != nil { + t.Fatalf("listener %s: unmarshal set_filter_state: %v", name, err) + } + if got := config.GetOnRequestHeaders()[0].GetObjectKey(); got != tunnelKeyFilterStateKey { + t.Errorf("listener %s: filter after ext_proc sets %q first, want the tunnel filter", name, got) + } + } +} + +// Envoy pools tunnels by the hash of shared hashable values. A non-hashable +// key lets one actor's tunnel serve another, and a second hashable value +// splits the pool, since Envoy hashes them in map order. +func TestActorTunnelFilterStateFilter(t *testing.T) { + config := &setfilterstatev3.Config{} + if err := actorTunnelFilterStateFilter().GetTypedConfig().UnmarshalTo(config); err != nil { + t.Fatalf("unmarshal set_filter_state config: %v", err) + } + + const ( + worker = "%DYNAMIC_METADATA(envoy.filters.listener.original_dst:local)%" + actor = "%REQ(ate-target-actor)%" + port = "%DYNAMIC_METADATA(envoy.filters.listener.original_dst:port)%" + ) + want := map[string]struct{ factory, format string }{ + tunnelKeyFilterStateKey: {"envoy.hashable_string", worker + "|" + actor + "|" + port}, + tunnelWorkerFilterStateKey: {"envoy.string", worker}, + tunnelActorFilterStateKey: {"envoy.string", actor}, + tunnelPortFilterStateKey: {"envoy.string", port}, + } + values := config.GetOnRequestHeaders() + if len(values) != len(want) { + t.Fatalf("filter sets %d values, want %d", len(values), len(want)) + } + for _, v := range values { + w, ok := want[v.GetObjectKey()] + if !ok { + t.Errorf("unexpected value %q", v.GetObjectKey()) + continue + } + if v.GetFactoryKey() != w.factory { + t.Errorf("%s factory = %q, want %q", v.GetObjectKey(), v.GetFactoryKey(), w.factory) + } + if got := v.GetFormatString().GetTextFormatSource().GetInlineString(); got != w.format { + t.Errorf("%s format = %q, want %q", v.GetObjectKey(), got, w.format) + } + if v.GetSharedWithUpstream() != setfilterstatecommonv3.FilterStateValue_ONCE { + t.Errorf("%s shared with upstream = %v, want ONCE", v.GetObjectKey(), v.GetSharedWithUpstream()) + } + } +} + +func TestXdsServer_ActorTunnelRoutes(t *testing.T) { + x := NewXdsServer(0) + x.SetUpstreamConnect(true) + routes := x.buildRoutes().GetVirtualHosts()[0].GetRoutes() + if len(routes) != 2 { + t.Fatalf("got %d routes, want 2", len(routes)) + } + grpc, rest := routes[0], routes[1] + if got := grpc.GetRoute().GetCluster(); got != ActorTunnelHTTP2ClusterName { + t.Errorf("gRPC route sends to %q, want %q", got, ActorTunnelHTTP2ClusterName) + } + if got := rest.GetRoute().GetCluster(); got != ActorTunnelHTTP1ClusterName { + t.Errorf("fallback route sends to %q, want %q", got, ActorTunnelHTTP1ClusterName) + } + if got := rest.GetMatch().GetHeaders(); len(got) != 0 { + t.Errorf("fallback route matches headers %v, want none", got) + } + + // Matches atunnel's isGRPC, minus the protocol check a route can't do. + var method, contentType string + for _, h := range grpc.GetMatch().GetHeaders() { + switch h.GetName() { + case ":method": + method = h.GetStringMatch().GetExact() + case "content-type": + contentType = h.GetStringMatch().GetSafeRegex().GetRegex() + } + } + if method != "POST" { + t.Errorf("gRPC route matches method %q, want POST", method) + } + re, err := regexp.Compile(contentType) + if err != nil { + t.Fatalf("gRPC route content-type regex %q: %v", contentType, err) + } + for ct, isGRPC := range map[string]bool{ + "application/grpc": true, + "application/grpc+proto": true, + "application/grpc;charset=utf-8": true, + "Application/GRPC": true, + "application/grpc-web": false, + "application/json": false, + } { + if re.MatchString(ct) != isGRPC { + t.Errorf("content-type %q matches the gRPC route = %v, want %v", ct, !isGRPC, isGRPC) + } + } + + for _, r := range routes { + if got := r.GetRequestHeadersToRemove(); len(got) != 1 || got[0] != atenet.TargetActorHeader { + t.Errorf("route to %s removes %v, want [%s]: nothing else strips it before the actor", r.GetRoute().GetCluster(), got, atenet.TargetActorHeader) + } + if got := r.GetRequestHeadersToAdd(); len(got) != 0 { + t.Errorf("route to %s adds headers %v, want none: the CONNECT authority carries the port", r.GetRoute().GetCluster(), got) + } + if got := r.GetRoute().GetTimeout().AsDuration(); got != defaultRouteTimeout { + t.Errorf("route to %s timeout = %v, want %v", r.GetRoute().GetCluster(), got, defaultRouteTimeout) + } + if got := r.GetRoute().GetIdleTimeout().AsDuration(); got != x.routeIdleTimeout() { + t.Errorf("route to %s idle timeout = %v, want %v", r.GetRoute().GetCluster(), got, x.routeIdleTimeout()) + } + } +} + +func TestBuildActorConnectListener(t *testing.T) { + filters := buildActorConnectListener().GetFilterChains()[0].GetFilters() + if len(filters) != 2 { + t.Fatalf("got %d network filters, want set_filter_state and tcp_proxy", len(filters)) + } + + setWorker := &setfilterstatenetworkv3.Config{} + if err := filters[0].GetTypedConfig().UnmarshalTo(setWorker); err != nil { + t.Fatalf("unmarshal set_filter_state: %v", err) + } + values := setWorker.GetOnNewConnection() + if len(values) != 1 { + t.Fatalf("set_filter_state sets %d values, want 1", len(values)) + } + if got := values[0].GetObjectKey(); got != extproc.OriginalDstFilterStateKey { + t.Errorf("set_filter_state sets %q, want %q for the ORIGINAL_DST cluster", got, extproc.OriginalDstFilterStateKey) + } + if got, want := values[0].GetFormatString().GetTextFormatSource().GetInlineString(), "%FILTER_STATE(dev.ate.tunnel.worker:PLAIN)%"; got != want { + t.Errorf("worker address format = %q, want %q", got, want) + } + + proxy := &tcpproxyv3.TcpProxy{} + if err := filters[1].GetTypedConfig().UnmarshalTo(proxy); err != nil { + t.Fatalf("unmarshal tcp_proxy: %v", err) + } + if got := proxy.GetCluster(); got != ActorConnectName { + t.Errorf("tcp_proxy cluster = %q, want %q", got, ActorConnectName) + } + tunnel := proxy.GetTunnelingConfig() + if got, want := tunnel.GetHostname(), "actor:%FILTER_STATE(dev.ate.tunnel.port:PLAIN)%"; got != want { + t.Errorf("CONNECT authority = %q, want %q", got, want) + } + headers := tunnel.GetHeadersToAdd() + if len(headers) != 1 || headers[0].GetHeader().GetKey() != atenet.TargetActorHeader || + headers[0].GetHeader().GetValue() != "%FILTER_STATE(dev.ate.tunnel.actor:PLAIN)%" { + t.Errorf("CONNECT headers = %v, want only %s from dev.ate.tunnel.actor", headers, atenet.TargetActorHeader) + } + if idle := proxy.GetIdleTimeout(); idle == nil || idle.AsDuration() != 0 { + t.Errorf("tcp_proxy idle timeout = %v, want 0 (off) so the HTTP side's timeouts fire first", idle) + } +} + +func TestXdsServer_ActorTunnelClusters(t *testing.T) { + protocolOptions := func(t *testing.T, c *clusterv3.Cluster) *httpv3.HttpProtocolOptions { + t.Helper() + opts := &httpv3.HttpProtocolOptions{} + if err := c.GetTypedExtensionProtocolOptions()[httpProtocolOptionsName].UnmarshalTo(opts); err != nil { + t.Fatalf("cluster %s: unmarshal protocol options: %v", c.GetName(), err) + } + return opts + } + checkCircuitBreakers := func(t *testing.T, c *clusterv3.Cluster) { + t.Helper() + limits := c.GetCircuitBreakers().GetThresholds()[0] + for name, got := range map[string]uint32{ + "max_connections": limits.GetMaxConnections().GetValue(), + "max_pending_requests": limits.GetMaxPendingRequests().GetValue(), + "max_requests": limits.GetMaxRequests().GetValue(), + } { + if got != tunnelCircuitBreakerLimit { + t.Errorf("cluster %s %s = %d, want %d", c.GetName(), name, got, tunnelCircuitBreakerLimit) + } + } + } + + for _, http2 := range []bool{false, true} { + c := buildActorTunnelCluster("tunnel", http2) + target := c.GetLoadAssignment().GetEndpoints()[0].GetLbEndpoints()[0].GetEndpoint().GetAddress().GetEnvoyInternalAddress() + if got := target.GetServerListenerName(); got != ActorConnectName { + t.Errorf("http2=%v: tunnel cluster sends to %q, want internal listener %q", http2, got, ActorConnectName) + } + opts := protocolOptions(t, c) + explicit := opts.GetExplicitHttpConfig() + if http2 && explicit.GetHttp2ProtocolOptions() == nil || !http2 && explicit.GetHttpProtocolOptions() == nil { + t.Errorf("http2=%v: tunnel cluster protocol = %v", http2, explicit) + } + if got := opts.GetCommonHttpProtocolOptions().GetIdleTimeout().AsDuration(); got != tunnelIdleTimeout { + t.Errorf("http2=%v: tunnel idle timeout = %v, want %v", http2, got, tunnelIdleTimeout) + } + checkCircuitBreakers(t, c) + } + + x := NewXdsServer(0) + x.SetUpstreamTls("/run/bundle.pem", "/run/trust.pem", "spiffe://cluster.local/") + c := x.buildActorConnectCluster() + if c.GetType() != clusterv3.Cluster_ORIGINAL_DST { + t.Errorf("actor_connect type = %v, want ORIGINAL_DST", c.GetType()) + } + lb := c.GetOriginalDstLbConfig() + if lb.GetMetadataKey() != nil { + t.Error("actor_connect reads a metadata key; the worker address comes from filter state") + } + if got := lb.GetUpstreamPortOverride().GetValue(); got != atunnel.DefaultConnectPort { + t.Errorf("actor_connect port = %d, want atunnel's CONNECT port %d", got, atunnel.DefaultConnectPort) + } + upstreamTLS := &tlsv3.UpstreamTlsContext{} + if err := c.GetTransportSocket().GetTypedConfig().UnmarshalTo(upstreamTLS); err != nil { + t.Fatalf("actor_connect: unmarshal UpstreamTlsContext: %v", err) + } + if len(upstreamTLS.GetCommonTlsContext().GetTlsCertificateSdsSecretConfigs()) == 0 { + t.Error("actor_connect presents no client certificate; atunnel requires mTLS") + } + if got := protocolOptions(t, c).GetExplicitHttpConfig().GetHttp2ProtocolOptions(); got == nil { + t.Error("actor_connect is not HTTP/2; tunnels to one worker would each need a connection") + } + checkCircuitBreakers(t, c) +} diff --git a/docs/architecture.md b/docs/architecture.md index 83eeaa90f3..b4165deb44 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -352,7 +352,8 @@ Handles actor-aware routing and automatic re-animation. authenticated TLS tunnel to the worker's `atunnel` listener on port 443. `atunnel`, hosted by `ateom`, forwards the request to the active Actor over its private veth interface. Worker pod port 80 is not a direct Actor ingress - path. + path. With `--upstream-connect`, the router instead reaches `atunnel` + through mTLS `CONNECT` tunnels on port 8443. * **Arbitrary-Port Ingress**: A client reaches a port on the Actor other than its default (80) with an HTTP `CONNECT` request naming the port in the diff --git a/internal/atunnel/ingress.go b/internal/atunnel/ingress.go index 6bcbe20d51..358a348a76 100644 --- a/internal/atunnel/ingress.go +++ b/internal/atunnel/ingress.go @@ -327,6 +327,7 @@ func (s *Server) ServeConnectHTTP(w http.ResponseWriter, r *http.Request) { } active, ctx, release, ok := s.authorize(r) if !ok { + slog.DebugContext(r.Context(), "atunnel CONNECT rejected", slog.Any("actor_header", r.Header.Values(atenet.TargetActorHeader))) s.reject(w) return } @@ -334,10 +335,12 @@ func (s *Server) ServeConnectHTTP(w http.ResponseWriter, r *http.Request) { _, port, err := net.SplitHostPort(r.Host) if err != nil { + slog.DebugContext(r.Context(), "atunnel CONNECT rejected: no port", slog.Any("actor", active.ref), slog.String("authority", r.Host)) http.Error(w, "CONNECT authority must include a port", http.StatusBadRequest) return } if _, ok := ParsePort(port); !ok { + slog.DebugContext(r.Context(), "atunnel CONNECT rejected: invalid port", slog.Any("actor", active.ref), slog.String("authority", r.Host)) http.Error(w, "invalid CONNECT port", http.StatusBadRequest) return } @@ -346,30 +349,32 @@ func (s *Server) ServeConnectHTTP(w http.ResponseWriter, r *http.Request) { defer cancelDial() upstream, err := active.dial(dialCtx, "tcp", net.JoinHostPort(s.upstream.Hostname(), port)) if err != nil { - slog.WarnContext(r.Context(), "atunnel CONNECT upstream failed", slog.Any("actor", active.ref), slog.Any("err", err)) + slog.WarnContext(r.Context(), "atunnel CONNECT upstream failed", slog.Any("actor", active.ref), slog.String("port", port), slog.Any("err", err)) http.Error(w, "bad gateway", http.StatusBadGateway) return } defer upstream.Close() + log := slog.With(slog.Any("actor", active.ref), slog.String("port", port), slog.String("proto", r.Proto)) + log.DebugContext(ctx, "atunnel CONNECT opened") if r.ProtoMajor == 2 { - s.serveH2Connect(w, r, upstream, ctx) + s.serveH2Connect(w, r, upstream, ctx, log) return } - s.serveH1Connect(w, upstream, ctx) + s.serveH1Connect(w, upstream, ctx, log) } -func (s *Server) serveH1Connect(w http.ResponseWriter, upstream net.Conn, ctx context.Context) { +func (s *Server) serveH1Connect(w http.ResponseWriter, upstream net.Conn, ctx context.Context, log *slog.Logger) { hj, ok := w.(http.Hijacker) if !ok { http.Error(w, "CONNECT hijacking unsupported", http.StatusInternalServerError) return } - client, rw, err := hj.Hijack() + downstream, rw, err := hj.Hijack() if err != nil { return } - defer client.Close() + defer downstream.Close() if _, err := rw.WriteString("HTTP/1.1 200 Connection Established\r\n\r\n"); err != nil { return } @@ -377,10 +382,10 @@ func (s *Server) serveH1Connect(w http.ResponseWriter, upstream net.Conn, ctx co return } - relayIngressWithHalfClose(ctx, upstream, rw, client, client) + relayIngressWithHalfClose(ctx, log, upstream, rw, downstream, downstream) } -func (s *Server) serveH2Connect(w http.ResponseWriter, r *http.Request, upstream net.Conn, ctx context.Context) { +func (s *Server) serveH2Connect(w http.ResponseWriter, r *http.Request, upstream net.Conn, ctx context.Context, log *slog.Logger) { // A HTTP/2 CONNECT tunnel is a pair of streams, not a hijackable TCP // socket. Send the response headers before copying so the peer can start // sending DATA frames, then flush each upstream write promptly. @@ -389,32 +394,33 @@ func (s *Server) serveH2Connect(w http.ResponseWriter, r *http.Request, upstream return } - relayIngressWithHalfClose(ctx, upstream, r.Body, flushingWriter{ResponseWriter: w}, r.Body) + relayIngressWithHalfClose(ctx, log, upstream, r.Body, flushingWriter{ResponseWriter: w}, r.Body) } -// relayIngressWithHalfClose copies a CONNECT stream. When the client request -// stream ends, it half-closes the actor connection and continues forwarding -// actor output until that stream ends too. -func relayIngressWithHalfClose(ctx context.Context, upstream net.Conn, clientReader io.Reader, clientWriter io.Writer, clientCloser io.Closer) { - done := make(chan struct{}, 2) +// relayIngressWithHalfClose copies a CONNECT stream between the router +// (downstream) and the actor (upstream). It half-closes the actor connection +// when the router stops sending, and closes the downstream when the actor +// does, so a router that reuses tunnels doesn't send into a dead one. +func relayIngressWithHalfClose(ctx context.Context, log *slog.Logger, upstream net.Conn, downstreamReader io.Reader, downstreamWriter io.Writer, downstreamCloser io.Closer) { + start := time.Now() go func() { - _, _ = io.Copy(upstream, clientReader) + n, err := io.Copy(upstream, downstreamReader) + log.DebugContext(ctx, "atunnel CONNECT router stopped sending", slog.Int64("bytes", n), slog.Any("err", err), slog.Duration("after", time.Since(start))) closeWrite(upstream) - done <- struct{}{} }() + actorDone := make(chan struct{}) go func() { - _, _ = io.Copy(clientWriter, upstream) - done <- struct{}{} + n, err := io.Copy(downstreamWriter, upstream) + log.DebugContext(ctx, "atunnel CONNECT actor stopped sending", slog.Int64("bytes", n), slog.Any("err", err), slog.Duration("after", time.Since(start))) + close(actorDone) }() - for range 2 { - select { - case <-ctx.Done(): - _ = upstream.Close() - _ = clientCloser.Close() - return - case <-done: - } + select { + case <-ctx.Done(): + log.DebugContext(ctx, "atunnel CONNECT canceled", slog.Any("cause", context.Cause(ctx)), slog.Duration("after", time.Since(start))) + _ = upstream.Close() + case <-actorDone: } + _ = downstreamCloser.Close() } type flushingWriter struct { @@ -555,16 +561,20 @@ func (s *Server) authorize(r *http.Request) (*activation, context.Context, func( } active.wg.Add(1) s.mu.Unlock() - requestCtx, cancel := context.WithCancel(r.Context()) - stop := context.AfterFunc(active.ctx, cancel) + requestCtx, cancel := context.WithCancelCause(r.Context()) + stop := context.AfterFunc(active.ctx, func() { cancel(errActorDeactivated) }) release := func() { active.wg.Done() stop() - cancel() + cancel(nil) } return active, requestCtx, release, true } +// errActorDeactivated is the cancel cause of a request whose actor was +// deactivated, which tells a suspend apart from the router hanging up. +var errActorDeactivated = errors.New("atunnel: actor deactivated") + func (s *Server) reject(w http.ResponseWriter) { w.Header().Set(StaleAssignmentHeader, "true") http.Error(w, "misdirected request", http.StatusMisdirectedRequest) diff --git a/internal/atunnel/ingress_test.go b/internal/atunnel/ingress_test.go index 72664cce87..ca25e299e3 100644 --- a/internal/atunnel/ingress_test.go +++ b/internal/atunnel/ingress_test.go @@ -26,6 +26,7 @@ import ( "encoding/pem" "errors" "io" + "log/slog" "math/big" "net" "net/http" @@ -33,6 +34,8 @@ import ( "net/url" "os" "path/filepath" + "strings" + "sync" "sync/atomic" "testing" "time" @@ -150,19 +153,19 @@ func TestRelayIngressWithHalfClose(t *testing.T) { upstream := receiveWithin(t, accepted, "accepted connection") defer upstream.Close() - clientReader, clientInput := io.Pipe() - defer clientReader.Close() - var clientOutput bytes.Buffer + downstreamReader, downstreamInput := io.Pipe() + defer downstreamReader.Close() + var downstreamOutput bytes.Buffer done := make(chan struct{}) go func() { - relayIngressWithHalfClose(context.Background(), upstream, clientReader, &clientOutput, clientReader) + relayIngressWithHalfClose(context.Background(), slog.New(slog.DiscardHandler), upstream, downstreamReader, &downstreamOutput, downstreamReader) close(done) }() - if _, err := io.WriteString(clientInput, "request"); err != nil { + if _, err := io.WriteString(downstreamInput, "request"); err != nil { t.Fatal(err) } - if err := clientInput.Close(); err != nil { + if err := downstreamInput.Close(); err != nil { t.Fatal(err) } if err := actor.SetReadDeadline(time.Now().Add(time.Second)); err != nil { @@ -187,24 +190,64 @@ func TestRelayIngressWithHalfClose(t *testing.T) { case <-time.After(time.Second): t.Fatal("relay did not finish after the actor response ended") } - if got := clientOutput.String(); got != "response" { - t.Errorf("client received %q, want response", got) + if got := downstreamOutput.String(); got != "response" { + t.Errorf("downstream received %q, want response", got) + } +} + +// A router reuses idle tunnels, so an actor closing its connection must end +// the tunnel, or the router's next request hangs until its timeout. +func TestRelayIngressClosesDownstreamWhenActorCloses(t *testing.T) { + upstream, actor := net.Pipe() + defer upstream.Close() + downstreamReader, downstreamInput := io.Pipe() + defer downstreamInput.Close() + var downstreamOutput bytes.Buffer + var logs syncBuffer + log := slog.New(slog.NewTextHandler(&logs, &slog.HandlerOptions{Level: slog.LevelDebug})) + done := make(chan struct{}) + go func() { + relayIngressWithHalfClose(context.Background(), log, upstream, downstreamReader, &downstreamOutput, downstreamReader) + close(done) + }() + + if _, err := io.WriteString(actor, "response"); err != nil { + t.Fatal(err) + } + if err := actor.Close(); err != nil { + t.Fatal(err) + } + select { + case <-done: + case <-time.After(time.Second): + t.Fatal("relay kept the tunnel open after the actor closed its connection") + } + if got := downstreamOutput.String(); got != "response" { + t.Errorf("downstream received %q, want response", got) + } + if _, err := io.WriteString(downstreamInput, "next request"); err == nil { + t.Fatal("downstream remained open after the actor closed its connection") + } + if want := `msg="atunnel CONNECT actor stopped sending" bytes=8 err=`; !strings.Contains(logs.String(), want) { + t.Errorf("relay logs = %q, want a line containing %q", logs.String(), want) } } func TestRelayIngressCancellationClosesBothSides(t *testing.T) { upstream, actor := net.Pipe() defer actor.Close() - clientReader, clientInput := io.Pipe() - defer clientInput.Close() - ctx, cancel := context.WithCancel(context.Background()) + downstreamReader, downstreamInput := io.Pipe() + defer downstreamInput.Close() + ctx, cancel := context.WithCancelCause(context.Background()) + var logs syncBuffer + log := slog.New(slog.NewTextHandler(&logs, &slog.HandlerOptions{Level: slog.LevelDebug})) done := make(chan struct{}) go func() { - relayIngressWithHalfClose(ctx, upstream, clientReader, io.Discard, clientReader) + relayIngressWithHalfClose(ctx, log, upstream, downstreamReader, io.Discard, downstreamReader) close(done) }() - cancel() + cancel(errActorDeactivated) select { case <-done: case <-time.After(time.Second): @@ -215,11 +258,32 @@ func TestRelayIngressCancellationClosesBothSides(t *testing.T) { if _, err := actor.Read(make([]byte, 1)); err == nil { t.Fatal("actor connection remained open after relay cancellation") } - if _, err := io.WriteString(clientInput, "request"); err == nil { - t.Fatal("client stream remained open after relay cancellation") + if _, err := io.WriteString(downstreamInput, "request"); err == nil { + t.Fatal("downstream remained open after relay cancellation") + } + if want := `msg="atunnel CONNECT canceled" cause="atunnel: actor deactivated"`; !strings.Contains(logs.String(), want) { + t.Errorf("relay logs = %q, want a line containing %q", logs.String(), want) } } +// syncBuffer collects logs from the relay's goroutines. +type syncBuffer struct { + mu sync.Mutex + buf bytes.Buffer +} + +func (b *syncBuffer) Write(p []byte) (int, error) { + b.mu.Lock() + defer b.mu.Unlock() + return b.buf.Write(p) +} + +func (b *syncBuffer) String() string { + b.mu.Lock() + defer b.mu.Unlock() + return b.buf.String() +} + func TestServeHTTP(t *testing.T) { var upstreamHosts []string upstreamURL, err := url.Parse("http://actor.internal:80") @@ -756,9 +820,11 @@ func TestDeactivateCancelsInflightRequest(t *testing.T) { t.Fatal(err) } started := make(chan struct{}) + causes := make(chan error, 1) setActorTransport(t, s, "team-a", "actor-1", roundTripFunc(func(r *http.Request) (*http.Response, error) { close(started) <-r.Context().Done() + causes <- context.Cause(r.Context()) return nil, r.Context().Err() })) @@ -775,6 +841,9 @@ func TestDeactivateCancelsInflightRequest(t *testing.T) { t.Fatal(err) } receiveWithin(t, done, "canceled in-flight request") + if cause := receiveWithin(t, causes, "cancel cause"); !errors.Is(cause, errActorDeactivated) { + t.Errorf("in-flight request canceled with %v, want %v", cause, errActorDeactivated) + } } // testDial is the dialer a test actor is reached through; the sandbox it would diff --git a/internal/e2e/suites/multiactor/multiactor_test.go b/internal/e2e/suites/multiactor/multiactor_test.go index 58b4b9a1b4..33e16a3f8e 100644 --- a/internal/e2e/suites/multiactor/multiactor_test.go +++ b/internal/e2e/suites/multiactor/multiactor_test.go @@ -73,11 +73,14 @@ func TestTwoActorsShareOneWorker(t *testing.T) { } defer rc.Close() - for _, name := range names { - got := whoami(t, ctx, rc, name) - if got.Atespace != multiactorNamespace || got.File != name { - t.Errorf("request for %s was served by atespace=%q actor=%q; each actor must be reached in its own sandbox", - name, got.Atespace, got.File) + // Alternate actors to catch one actor's tunnel serving the other. + for range 3 { + for _, name := range names { + got := whoami(t, ctx, rc, name) + if got.Atespace != multiactorNamespace || got.File != name { + t.Errorf("request for %s was served by atespace=%q actor=%q; each actor must be reached in its own sandbox", + name, got.Atespace, got.File) + } } } diff --git a/internal/e2e/suites/networking/grpcingress_test.go b/internal/e2e/suites/networking/grpcingress_test.go index a395bf3f23..48fdfb367c 100644 --- a/internal/e2e/suites/networking/grpcingress_test.go +++ b/internal/e2e/suites/networking/grpcingress_test.go @@ -109,8 +109,9 @@ func TestIngressProtocolDowngrade(t *testing.T) { } body, _ := io.ReadAll(resp.Body) resp.Body.Close() - // The counter cannot speak h2c. Envoy returns HTTP 502; agentgateway - // reports the upstream failure with gRPC's Unavailable status. + // The counter cannot speak h2c. Through atunnel's HTTPS listener Envoy + // returns HTTP 502; through a CONNECT tunnel the failure is gRPC's + // Unavailable status. grpcStatus := resp.Header.Get("Grpc-Status") if grpcStatus == "" { grpcStatus = resp.Trailer.Get("Grpc-Status") diff --git a/vendor/github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/set_filter_state/v3/set_filter_state.pb.go b/vendor/github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/set_filter_state/v3/set_filter_state.pb.go new file mode 100644 index 0000000000..a11c9a832b --- /dev/null +++ b/vendor/github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/set_filter_state/v3/set_filter_state.pb.go @@ -0,0 +1,156 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.10 +// protoc v7.35.1 +// source: envoy/extensions/filters/network/set_filter_state/v3/set_filter_state.proto + +package set_filter_statev3 + +import ( + _ "github.com/cncf/xds/go/udpa/annotations" + v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/common/set_filter_state/v3" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type Config struct { + state protoimpl.MessageState `protogen:"open.v1"` + // A sequence of the filter state values to apply in the specified order + // when a new connection is received. + OnNewConnection []*v3.FilterStateValue `protobuf:"bytes,1,rep,name=on_new_connection,json=onNewConnection,proto3" json:"on_new_connection,omitempty"` + // A sequence of the filter state values to apply in the specified order + // when the downstream TLS handshake is complete. + // + // For non-TLS downstream connections (where there is no TLS handshake), this + // list is applied when a new connection is received. + OnDownstreamTlsHandshake []*v3.FilterStateValue `protobuf:"bytes,2,rep,name=on_downstream_tls_handshake,json=onDownstreamTlsHandshake,proto3" json:"on_downstream_tls_handshake,omitempty"` + // A sequence of the filter state values to apply in the specified order + // when data is first received from the downstream connection. + OnDownstreamData []*v3.FilterStateValue `protobuf:"bytes,3,rep,name=on_downstream_data,json=onDownstreamData,proto3" json:"on_downstream_data,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Config) Reset() { + *x = Config{} + mi := &file_envoy_extensions_filters_network_set_filter_state_v3_set_filter_state_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Config) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Config) ProtoMessage() {} + +func (x *Config) ProtoReflect() protoreflect.Message { + mi := &file_envoy_extensions_filters_network_set_filter_state_v3_set_filter_state_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Config.ProtoReflect.Descriptor instead. +func (*Config) Descriptor() ([]byte, []int) { + return file_envoy_extensions_filters_network_set_filter_state_v3_set_filter_state_proto_rawDescGZIP(), []int{0} +} + +func (x *Config) GetOnNewConnection() []*v3.FilterStateValue { + if x != nil { + return x.OnNewConnection + } + return nil +} + +func (x *Config) GetOnDownstreamTlsHandshake() []*v3.FilterStateValue { + if x != nil { + return x.OnDownstreamTlsHandshake + } + return nil +} + +func (x *Config) GetOnDownstreamData() []*v3.FilterStateValue { + if x != nil { + return x.OnDownstreamData + } + return nil +} + +var File_envoy_extensions_filters_network_set_filter_state_v3_set_filter_state_proto protoreflect.FileDescriptor + +const file_envoy_extensions_filters_network_set_filter_state_v3_set_filter_state_proto_rawDesc = "" + + "\n" + + "Kenvoy/extensions/filters/network/set_filter_state/v3/set_filter_state.proto\x124envoy.extensions.filters.network.set_filter_state.v3\x1a?envoy/extensions/filters/common/set_filter_state/v3/value.proto\x1a\x1dudpa/annotations/status.proto\"\xf7\x02\n" + + "\x06Config\x12q\n" + + "\x11on_new_connection\x18\x01 \x03(\v2E.envoy.extensions.filters.common.set_filter_state.v3.FilterStateValueR\x0fonNewConnection\x12\x84\x01\n" + + "\x1bon_downstream_tls_handshake\x18\x02 \x03(\v2E.envoy.extensions.filters.common.set_filter_state.v3.FilterStateValueR\x18onDownstreamTlsHandshake\x12s\n" + + "\x12on_downstream_data\x18\x03 \x03(\v2E.envoy.extensions.filters.common.set_filter_state.v3.FilterStateValueR\x10onDownstreamDataB\xd3\x01\xba\x80\xc8\xd1\x06\x02\x10\x02\n" + + "Bio.envoyproxy.envoy.extensions.filters.network.set_filter_state.v3B\x13SetFilterStateProtoP\x01Zngithub.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/set_filter_state/v3;set_filter_statev3b\x06proto3" + +var ( + file_envoy_extensions_filters_network_set_filter_state_v3_set_filter_state_proto_rawDescOnce sync.Once + file_envoy_extensions_filters_network_set_filter_state_v3_set_filter_state_proto_rawDescData []byte +) + +func file_envoy_extensions_filters_network_set_filter_state_v3_set_filter_state_proto_rawDescGZIP() []byte { + file_envoy_extensions_filters_network_set_filter_state_v3_set_filter_state_proto_rawDescOnce.Do(func() { + file_envoy_extensions_filters_network_set_filter_state_v3_set_filter_state_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_envoy_extensions_filters_network_set_filter_state_v3_set_filter_state_proto_rawDesc), len(file_envoy_extensions_filters_network_set_filter_state_v3_set_filter_state_proto_rawDesc))) + }) + return file_envoy_extensions_filters_network_set_filter_state_v3_set_filter_state_proto_rawDescData +} + +var file_envoy_extensions_filters_network_set_filter_state_v3_set_filter_state_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_envoy_extensions_filters_network_set_filter_state_v3_set_filter_state_proto_goTypes = []any{ + (*Config)(nil), // 0: envoy.extensions.filters.network.set_filter_state.v3.Config + (*v3.FilterStateValue)(nil), // 1: envoy.extensions.filters.common.set_filter_state.v3.FilterStateValue +} +var file_envoy_extensions_filters_network_set_filter_state_v3_set_filter_state_proto_depIdxs = []int32{ + 1, // 0: envoy.extensions.filters.network.set_filter_state.v3.Config.on_new_connection:type_name -> envoy.extensions.filters.common.set_filter_state.v3.FilterStateValue + 1, // 1: envoy.extensions.filters.network.set_filter_state.v3.Config.on_downstream_tls_handshake:type_name -> envoy.extensions.filters.common.set_filter_state.v3.FilterStateValue + 1, // 2: envoy.extensions.filters.network.set_filter_state.v3.Config.on_downstream_data:type_name -> envoy.extensions.filters.common.set_filter_state.v3.FilterStateValue + 3, // [3:3] is the sub-list for method output_type + 3, // [3:3] is the sub-list for method input_type + 3, // [3:3] is the sub-list for extension type_name + 3, // [3:3] is the sub-list for extension extendee + 0, // [0:3] is the sub-list for field type_name +} + +func init() { file_envoy_extensions_filters_network_set_filter_state_v3_set_filter_state_proto_init() } +func file_envoy_extensions_filters_network_set_filter_state_v3_set_filter_state_proto_init() { + if File_envoy_extensions_filters_network_set_filter_state_v3_set_filter_state_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_envoy_extensions_filters_network_set_filter_state_v3_set_filter_state_proto_rawDesc), len(file_envoy_extensions_filters_network_set_filter_state_v3_set_filter_state_proto_rawDesc)), + NumEnums: 0, + NumMessages: 1, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_envoy_extensions_filters_network_set_filter_state_v3_set_filter_state_proto_goTypes, + DependencyIndexes: file_envoy_extensions_filters_network_set_filter_state_v3_set_filter_state_proto_depIdxs, + MessageInfos: file_envoy_extensions_filters_network_set_filter_state_v3_set_filter_state_proto_msgTypes, + }.Build() + File_envoy_extensions_filters_network_set_filter_state_v3_set_filter_state_proto = out.File + file_envoy_extensions_filters_network_set_filter_state_v3_set_filter_state_proto_goTypes = nil + file_envoy_extensions_filters_network_set_filter_state_v3_set_filter_state_proto_depIdxs = nil +} diff --git a/vendor/github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/set_filter_state/v3/set_filter_state.pb.validate.go b/vendor/github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/set_filter_state/v3/set_filter_state.pb.validate.go new file mode 100644 index 0000000000..72c9596f7a --- /dev/null +++ b/vendor/github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/set_filter_state/v3/set_filter_state.pb.validate.go @@ -0,0 +1,237 @@ +//go:build !disable_pgv +// Code generated by protoc-gen-validate. DO NOT EDIT. +// source: envoy/extensions/filters/network/set_filter_state/v3/set_filter_state.proto + +package set_filter_statev3 + +import ( + "bytes" + "errors" + "fmt" + "net" + "net/mail" + "net/url" + "regexp" + "sort" + "strings" + "time" + "unicode/utf8" + + "google.golang.org/protobuf/types/known/anypb" +) + +// ensure the imports are used +var ( + _ = bytes.MinRead + _ = errors.New("") + _ = fmt.Print + _ = utf8.UTFMax + _ = (*regexp.Regexp)(nil) + _ = (*strings.Reader)(nil) + _ = net.IPv4len + _ = time.Duration(0) + _ = (*url.URL)(nil) + _ = (*mail.Address)(nil) + _ = anypb.Any{} + _ = sort.Sort +) + +// Validate checks the field values on Config with the rules defined in the +// proto definition for this message. If any rules are violated, the first +// error encountered is returned, or nil if there are no violations. +func (m *Config) Validate() error { + return m.validate(false) +} + +// ValidateAll checks the field values on Config with the rules defined in the +// proto definition for this message. If any rules are violated, the result is +// a list of violation errors wrapped in ConfigMultiError, or nil if none found. +func (m *Config) ValidateAll() error { + return m.validate(true) +} + +func (m *Config) validate(all bool) error { + if m == nil { + return nil + } + + var errors []error + + for idx, item := range m.GetOnNewConnection() { + _, _ = idx, item + + if all { + switch v := interface{}(item).(type) { + case interface{ ValidateAll() error }: + if err := v.ValidateAll(); err != nil { + errors = append(errors, ConfigValidationError{ + field: fmt.Sprintf("OnNewConnection[%v]", idx), + reason: "embedded message failed validation", + cause: err, + }) + } + case interface{ Validate() error }: + if err := v.Validate(); err != nil { + errors = append(errors, ConfigValidationError{ + field: fmt.Sprintf("OnNewConnection[%v]", idx), + reason: "embedded message failed validation", + cause: err, + }) + } + } + } else if v, ok := interface{}(item).(interface{ Validate() error }); ok { + if err := v.Validate(); err != nil { + return ConfigValidationError{ + field: fmt.Sprintf("OnNewConnection[%v]", idx), + reason: "embedded message failed validation", + cause: err, + } + } + } + + } + + for idx, item := range m.GetOnDownstreamTlsHandshake() { + _, _ = idx, item + + if all { + switch v := interface{}(item).(type) { + case interface{ ValidateAll() error }: + if err := v.ValidateAll(); err != nil { + errors = append(errors, ConfigValidationError{ + field: fmt.Sprintf("OnDownstreamTlsHandshake[%v]", idx), + reason: "embedded message failed validation", + cause: err, + }) + } + case interface{ Validate() error }: + if err := v.Validate(); err != nil { + errors = append(errors, ConfigValidationError{ + field: fmt.Sprintf("OnDownstreamTlsHandshake[%v]", idx), + reason: "embedded message failed validation", + cause: err, + }) + } + } + } else if v, ok := interface{}(item).(interface{ Validate() error }); ok { + if err := v.Validate(); err != nil { + return ConfigValidationError{ + field: fmt.Sprintf("OnDownstreamTlsHandshake[%v]", idx), + reason: "embedded message failed validation", + cause: err, + } + } + } + + } + + for idx, item := range m.GetOnDownstreamData() { + _, _ = idx, item + + if all { + switch v := interface{}(item).(type) { + case interface{ ValidateAll() error }: + if err := v.ValidateAll(); err != nil { + errors = append(errors, ConfigValidationError{ + field: fmt.Sprintf("OnDownstreamData[%v]", idx), + reason: "embedded message failed validation", + cause: err, + }) + } + case interface{ Validate() error }: + if err := v.Validate(); err != nil { + errors = append(errors, ConfigValidationError{ + field: fmt.Sprintf("OnDownstreamData[%v]", idx), + reason: "embedded message failed validation", + cause: err, + }) + } + } + } else if v, ok := interface{}(item).(interface{ Validate() error }); ok { + if err := v.Validate(); err != nil { + return ConfigValidationError{ + field: fmt.Sprintf("OnDownstreamData[%v]", idx), + reason: "embedded message failed validation", + cause: err, + } + } + } + + } + + if len(errors) > 0 { + return ConfigMultiError(errors) + } + + return nil +} + +// ConfigMultiError is an error wrapping multiple validation errors returned by +// Config.ValidateAll() if the designated constraints aren't met. +type ConfigMultiError []error + +// Error returns a concatenation of all the error messages it wraps. +func (m ConfigMultiError) Error() string { + msgs := make([]string, 0, len(m)) + for _, err := range m { + msgs = append(msgs, err.Error()) + } + return strings.Join(msgs, "; ") +} + +// AllErrors returns a list of validation violation errors. +func (m ConfigMultiError) AllErrors() []error { return m } + +// ConfigValidationError is the validation error returned by Config.Validate if +// the designated constraints aren't met. +type ConfigValidationError struct { + field string + reason string + cause error + key bool +} + +// Field function returns field value. +func (e ConfigValidationError) Field() string { return e.field } + +// Reason function returns reason value. +func (e ConfigValidationError) Reason() string { return e.reason } + +// Cause function returns cause value. +func (e ConfigValidationError) Cause() error { return e.cause } + +// Key function returns key value. +func (e ConfigValidationError) Key() bool { return e.key } + +// ErrorName returns error name. +func (e ConfigValidationError) ErrorName() string { return "ConfigValidationError" } + +// Error satisfies the builtin error interface +func (e ConfigValidationError) Error() string { + cause := "" + if e.cause != nil { + cause = fmt.Sprintf(" | caused by: %v", e.cause) + } + + key := "" + if e.key { + key = "key for " + } + + return fmt.Sprintf( + "invalid %sConfig.%s: %s%s", + key, + e.field, + e.reason, + cause) +} + +var _ error = ConfigValidationError{} + +var _ interface { + Field() string + Reason() string + Key() bool + Cause() error + ErrorName() string +} = ConfigValidationError{} diff --git a/vendor/github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/set_filter_state/v3/set_filter_state_vtproto.pb.go b/vendor/github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/set_filter_state/v3/set_filter_state_vtproto.pb.go new file mode 100644 index 0000000000..0eea9ee58c --- /dev/null +++ b/vendor/github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/set_filter_state/v3/set_filter_state_vtproto.pb.go @@ -0,0 +1,171 @@ +//go:build vtprotobuf +// +build vtprotobuf + +// Code generated by protoc-gen-go-vtproto. DO NOT EDIT. +// source: envoy/extensions/filters/network/set_filter_state/v3/set_filter_state.proto + +package set_filter_statev3 + +import ( + protohelpers "github.com/planetscale/vtprotobuf/protohelpers" + proto "google.golang.org/protobuf/proto" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +func (m *Config) MarshalVTStrict() (dAtA []byte, err error) { + if m == nil { + return nil, nil + } + size := m.SizeVT() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBufferVTStrict(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *Config) MarshalToVTStrict(dAtA []byte) (int, error) { + size := m.SizeVT() + return m.MarshalToSizedBufferVTStrict(dAtA[:size]) +} + +func (m *Config) MarshalToSizedBufferVTStrict(dAtA []byte) (int, error) { + if m == nil { + return 0, nil + } + i := len(dAtA) + _ = i + var l int + _ = l + if m.unknownFields != nil { + i -= len(m.unknownFields) + copy(dAtA[i:], m.unknownFields) + } + if len(m.OnDownstreamData) > 0 { + for iNdEx := len(m.OnDownstreamData) - 1; iNdEx >= 0; iNdEx-- { + if vtmsg, ok := interface{}(m.OnDownstreamData[iNdEx]).(interface { + MarshalToSizedBufferVTStrict([]byte) (int, error) + }); ok { + size, err := vtmsg.MarshalToSizedBufferVTStrict(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) + } else { + encoded, err := proto.Marshal(m.OnDownstreamData[iNdEx]) + if err != nil { + return 0, err + } + i -= len(encoded) + copy(dAtA[i:], encoded) + i = protohelpers.EncodeVarint(dAtA, i, uint64(len(encoded))) + } + i-- + dAtA[i] = 0x1a + } + } + if len(m.OnDownstreamTlsHandshake) > 0 { + for iNdEx := len(m.OnDownstreamTlsHandshake) - 1; iNdEx >= 0; iNdEx-- { + if vtmsg, ok := interface{}(m.OnDownstreamTlsHandshake[iNdEx]).(interface { + MarshalToSizedBufferVTStrict([]byte) (int, error) + }); ok { + size, err := vtmsg.MarshalToSizedBufferVTStrict(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) + } else { + encoded, err := proto.Marshal(m.OnDownstreamTlsHandshake[iNdEx]) + if err != nil { + return 0, err + } + i -= len(encoded) + copy(dAtA[i:], encoded) + i = protohelpers.EncodeVarint(dAtA, i, uint64(len(encoded))) + } + i-- + dAtA[i] = 0x12 + } + } + if len(m.OnNewConnection) > 0 { + for iNdEx := len(m.OnNewConnection) - 1; iNdEx >= 0; iNdEx-- { + if vtmsg, ok := interface{}(m.OnNewConnection[iNdEx]).(interface { + MarshalToSizedBufferVTStrict([]byte) (int, error) + }); ok { + size, err := vtmsg.MarshalToSizedBufferVTStrict(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) + } else { + encoded, err := proto.Marshal(m.OnNewConnection[iNdEx]) + if err != nil { + return 0, err + } + i -= len(encoded) + copy(dAtA[i:], encoded) + i = protohelpers.EncodeVarint(dAtA, i, uint64(len(encoded))) + } + i-- + dAtA[i] = 0xa + } + } + return len(dAtA) - i, nil +} + +func (m *Config) SizeVT() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + if len(m.OnNewConnection) > 0 { + for _, e := range m.OnNewConnection { + if size, ok := interface{}(e).(interface { + SizeVT() int + }); ok { + l = size.SizeVT() + } else { + l = proto.Size(e) + } + n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } + } + if len(m.OnDownstreamTlsHandshake) > 0 { + for _, e := range m.OnDownstreamTlsHandshake { + if size, ok := interface{}(e).(interface { + SizeVT() int + }); ok { + l = size.SizeVT() + } else { + l = proto.Size(e) + } + n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } + } + if len(m.OnDownstreamData) > 0 { + for _, e := range m.OnDownstreamData { + if size, ok := interface{}(e).(interface { + SizeVT() int + }); ok { + l = size.SizeVT() + } else { + l = proto.Size(e) + } + n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } + } + n += len(m.unknownFields) + return n +} diff --git a/vendor/github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/tcp_proxy/v3/tcp_proxy.pb.go b/vendor/github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/tcp_proxy/v3/tcp_proxy.pb.go new file mode 100644 index 0000000000..732799fa66 --- /dev/null +++ b/vendor/github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/tcp_proxy/v3/tcp_proxy.pb.go @@ -0,0 +1,1144 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.10 +// protoc v7.35.1 +// source: envoy/extensions/filters/network/tcp_proxy/v3/tcp_proxy.proto + +package tcp_proxyv3 + +import ( + _ "github.com/cncf/xds/go/udpa/annotations" + _ "github.com/envoyproxy/go-control-plane/envoy/annotations" + v31 "github.com/envoyproxy/go-control-plane/envoy/config/accesslog/v3" + v3 "github.com/envoyproxy/go-control-plane/envoy/config/core/v3" + v33 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/http_connection_manager/v3" + v32 "github.com/envoyproxy/go-control-plane/envoy/type/v3" + _ "github.com/envoyproxy/protoc-gen-validate/validate" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + durationpb "google.golang.org/protobuf/types/known/durationpb" + wrapperspb "google.golang.org/protobuf/types/known/wrapperspb" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +// Specifies when the TCP proxy establishes the upstream connection. +type UpstreamConnectMode int32 + +const ( + // Establish the upstream connection immediately when the downstream connection is accepted. + // This is the default behavior and provides the lowest latency. + UpstreamConnectMode_IMMEDIATE UpstreamConnectMode = 0 + // Wait for initial data from the downstream connection before establishing the upstream connection. + // This allows preceding filters to inspect the initial data (e.g., extracting SNI from TLS ClientHello) + // before the upstream connection is established. + // + // This mode requires “max_early_data_bytes“ to be set. + // + // .. warning:: + // + // This mode is not suitable for server-first protocols (e.g., SMTP, MySQL, POP3) where the + // server sends the initial greeting. For such protocols, use ``IMMEDIATE`` mode. + UpstreamConnectMode_ON_DOWNSTREAM_DATA UpstreamConnectMode = 1 + // Wait for the downstream TLS handshake to complete before establishing the upstream connection. + // This allows access to the full TLS connection information, including client certificates + // and negotiated parameters, which can be used for routing decisions or passed as metadata + // to the upstream. + // + // This mode requires “max_early_data_bytes“ to be set (can be zero to disable buffering). + // + // .. note:: + // + // This mode is only effective when the downstream connection uses TLS. For non-TLS + // connections, it behaves the same as ``IMMEDIATE``. + UpstreamConnectMode_ON_DOWNSTREAM_TLS_HANDSHAKE UpstreamConnectMode = 2 +) + +// Enum value maps for UpstreamConnectMode. +var ( + UpstreamConnectMode_name = map[int32]string{ + 0: "IMMEDIATE", + 1: "ON_DOWNSTREAM_DATA", + 2: "ON_DOWNSTREAM_TLS_HANDSHAKE", + } + UpstreamConnectMode_value = map[string]int32{ + "IMMEDIATE": 0, + "ON_DOWNSTREAM_DATA": 1, + "ON_DOWNSTREAM_TLS_HANDSHAKE": 2, + } +) + +func (x UpstreamConnectMode) Enum() *UpstreamConnectMode { + p := new(UpstreamConnectMode) + *p = x + return p +} + +func (x UpstreamConnectMode) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (UpstreamConnectMode) Descriptor() protoreflect.EnumDescriptor { + return file_envoy_extensions_filters_network_tcp_proxy_v3_tcp_proxy_proto_enumTypes[0].Descriptor() +} + +func (UpstreamConnectMode) Type() protoreflect.EnumType { + return &file_envoy_extensions_filters_network_tcp_proxy_v3_tcp_proxy_proto_enumTypes[0] +} + +func (x UpstreamConnectMode) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use UpstreamConnectMode.Descriptor instead. +func (UpstreamConnectMode) EnumDescriptor() ([]byte, []int) { + return file_envoy_extensions_filters_network_tcp_proxy_v3_tcp_proxy_proto_rawDescGZIP(), []int{0} +} + +// Specifies how TLVs in “proxy_protocol_tlvs“ are merged with existing PROXY protocol state +// (e.g., downstream TLVs parsed by the proxy_protocol listener filter). +type ProxyProtocolTlvMergePolicy int32 + +const ( + // Add configured TLVs only if no PROXY protocol state exists (e.g., no downstream TLVs). + // If state exists, ignore configured TLVs and use only the existing TLVs. + // This is the default for backward compatibility. + ProxyProtocolTlvMergePolicy_ADD_IF_ABSENT ProxyProtocolTlvMergePolicy = 0 + // Overwrite existing TLVs (e.g., downstream TLVs) by type with configured TLVs. + // Non-conflicting TLVs from both sources are preserved. + // If no state exists, add all configured TLVs. + // Source/destination addresses from existing state are preserved. + ProxyProtocolTlvMergePolicy_OVERWRITE_BY_TYPE_IF_EXISTS_OR_ADD ProxyProtocolTlvMergePolicy = 1 + // Append configured TLVs to existing TLVs (e.g., downstream TLVs), preserving all TLVs + // from both sources (PROXY protocol v2 allows duplicate types). + // If no state exists, add all configured TLVs. + // Source/destination addresses from existing state are preserved. + ProxyProtocolTlvMergePolicy_APPEND_IF_EXISTS_OR_ADD ProxyProtocolTlvMergePolicy = 2 +) + +// Enum value maps for ProxyProtocolTlvMergePolicy. +var ( + ProxyProtocolTlvMergePolicy_name = map[int32]string{ + 0: "ADD_IF_ABSENT", + 1: "OVERWRITE_BY_TYPE_IF_EXISTS_OR_ADD", + 2: "APPEND_IF_EXISTS_OR_ADD", + } + ProxyProtocolTlvMergePolicy_value = map[string]int32{ + "ADD_IF_ABSENT": 0, + "OVERWRITE_BY_TYPE_IF_EXISTS_OR_ADD": 1, + "APPEND_IF_EXISTS_OR_ADD": 2, + } +) + +func (x ProxyProtocolTlvMergePolicy) Enum() *ProxyProtocolTlvMergePolicy { + p := new(ProxyProtocolTlvMergePolicy) + *p = x + return p +} + +func (x ProxyProtocolTlvMergePolicy) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (ProxyProtocolTlvMergePolicy) Descriptor() protoreflect.EnumDescriptor { + return file_envoy_extensions_filters_network_tcp_proxy_v3_tcp_proxy_proto_enumTypes[1].Descriptor() +} + +func (ProxyProtocolTlvMergePolicy) Type() protoreflect.EnumType { + return &file_envoy_extensions_filters_network_tcp_proxy_v3_tcp_proxy_proto_enumTypes[1] +} + +func (x ProxyProtocolTlvMergePolicy) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use ProxyProtocolTlvMergePolicy.Descriptor instead. +func (ProxyProtocolTlvMergePolicy) EnumDescriptor() ([]byte, []int) { + return file_envoy_extensions_filters_network_tcp_proxy_v3_tcp_proxy_proto_rawDescGZIP(), []int{1} +} + +// [#next-free-field: 25] +type TcpProxy struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The prefix to use when emitting :ref:`statistics + // `. + StatPrefix string `protobuf:"bytes,1,opt,name=stat_prefix,json=statPrefix,proto3" json:"stat_prefix,omitempty"` + // Types that are valid to be assigned to ClusterSpecifier: + // + // *TcpProxy_Cluster + // *TcpProxy_WeightedClusters + ClusterSpecifier isTcpProxy_ClusterSpecifier `protobuf_oneof:"cluster_specifier"` + // The on demand policy for the upstream cluster. + // It applies to both + // :ref:`TcpProxy.cluster ` + // and + // :ref:`TcpProxy.weighted_clusters `. + OnDemand *TcpProxy_OnDemand `protobuf:"bytes,14,opt,name=on_demand,json=onDemand,proto3" json:"on_demand,omitempty"` + // Optional endpoint metadata match criteria used by the subset load balancer. Only endpoints + // in the upstream cluster with metadata matching what is set in this field will be considered + // for load balancing. The filter name should be specified as “envoy.lb“. + MetadataMatch *v3.Metadata `protobuf:"bytes,9,opt,name=metadata_match,json=metadataMatch,proto3" json:"metadata_match,omitempty"` + // The idle timeout for connections managed by the TCP proxy filter. The idle timeout is defined as the + // period in which there are no bytes sent or received on either the upstream or downstream connection. + // If not set, the default idle timeout is 1 hour. If set to “0s“, the timeout is disabled. + // It is possible to dynamically override this configuration by setting a per-connection filter state + // object for the key “envoy.tcp_proxy.per_connection_idle_timeout_ms“. + // + // .. warning:: + // + // Disabling this timeout is likely to yield connection leaks due to lost TCP FIN packets, etc. + IdleTimeout *durationpb.Duration `protobuf:"bytes,8,opt,name=idle_timeout,json=idleTimeout,proto3" json:"idle_timeout,omitempty"` + // [#not-implemented-hide:] The idle timeout for connections managed by the TCP proxy + // filter. The idle timeout is defined as the period in which there is no + // active traffic. If not set, there is no idle timeout. When the idle timeout + // is reached the connection will be closed. The distinction between + // downstream_idle_timeout/upstream_idle_timeout provides a means to set + // timeout based on the last byte sent on the downstream/upstream connection. + DownstreamIdleTimeout *durationpb.Duration `protobuf:"bytes,3,opt,name=downstream_idle_timeout,json=downstreamIdleTimeout,proto3" json:"downstream_idle_timeout,omitempty"` + // [#not-implemented-hide:] + UpstreamIdleTimeout *durationpb.Duration `protobuf:"bytes,4,opt,name=upstream_idle_timeout,json=upstreamIdleTimeout,proto3" json:"upstream_idle_timeout,omitempty"` + // Configuration for :ref:`access logs ` emitted by this TCP proxy. + AccessLog []*v31.AccessLog `protobuf:"bytes,5,rep,name=access_log,json=accessLog,proto3" json:"access_log,omitempty"` + // The maximum number of unsuccessful connection attempts that will be made before + // giving up. If the parameter is not specified, 1 connection attempt will be made. + MaxConnectAttempts *wrapperspb.UInt32Value `protobuf:"bytes,7,opt,name=max_connect_attempts,json=maxConnectAttempts,proto3" json:"max_connect_attempts,omitempty"` + // Sets the backoff strategy. If not set, the retries are performed without backoff. + BackoffOptions *v3.BackoffStrategy `protobuf:"bytes,18,opt,name=backoff_options,json=backoffOptions,proto3" json:"backoff_options,omitempty"` + // Optional configuration for TCP proxy hash policy. If hash_policy is not set, the hash-based + // load balancing algorithms will select a host randomly. Currently the number of hash policies is + // limited to 1. + HashPolicy []*v32.HashPolicy `protobuf:"bytes,11,rep,name=hash_policy,json=hashPolicy,proto3" json:"hash_policy,omitempty"` + // If set, this configures tunneling, for example configuration options to tunnel TCP payload over + // HTTP CONNECT. If this message is absent, the payload is proxied upstream as usual. + // It is possible to dynamically override this configuration and disable tunneling per connection by + // setting a per-connection filter state object for the key “envoy.tcp_proxy.disable_tunneling“. + TunnelingConfig *TcpProxy_TunnelingConfig `protobuf:"bytes,12,opt,name=tunneling_config,json=tunnelingConfig,proto3" json:"tunneling_config,omitempty"` + // The maximum duration of a connection. The duration is defined as the period since a connection was + // established. If not set, there is no maximum duration. When “max_downstream_connection_duration“ is + // reached, the connection is closed. The duration must be at least “1ms“. + MaxDownstreamConnectionDuration *durationpb.Duration `protobuf:"bytes,13,opt,name=max_downstream_connection_duration,json=maxDownstreamConnectionDuration,proto3" json:"max_downstream_connection_duration,omitempty"` + // Percentage-based jitter for “max_downstream_connection_duration“. The jitter increases the + // “max_downstream_connection_duration“ by a random duration up to the provided percentage. + // This field is ignored if “max_downstream_connection_duration“ is not set. If not set, no jitter + // is added. + MaxDownstreamConnectionDurationJitterPercentage *v32.Percent `protobuf:"bytes,20,opt,name=max_downstream_connection_duration_jitter_percentage,json=maxDownstreamConnectionDurationJitterPercentage,proto3" json:"max_downstream_connection_duration_jitter_percentage,omitempty"` + // If both this field and :ref:`access_log_flush_interval + // ` + // are specified, the former (deprecated field) is ignored. + // + // .. attention:: + // + // This field is deprecated in favor of + // :ref:`access_log_flush_interval + // `. + // + // Deprecated: Marked as deprecated in envoy/extensions/filters/network/tcp_proxy/v3/tcp_proxy.proto. + AccessLogFlushInterval *durationpb.Duration `protobuf:"bytes,15,opt,name=access_log_flush_interval,json=accessLogFlushInterval,proto3" json:"access_log_flush_interval,omitempty"` + // If both this field and :ref:`flush_access_log_on_connected + // ` + // are specified, the former (deprecated field) is ignored. + // + // .. attention:: + // + // This field is deprecated in favor of + // :ref:`flush_access_log_on_connected + // `. + // + // Deprecated: Marked as deprecated in envoy/extensions/filters/network/tcp_proxy/v3/tcp_proxy.proto. + FlushAccessLogOnConnected bool `protobuf:"varint,16,opt,name=flush_access_log_on_connected,json=flushAccessLogOnConnected,proto3" json:"flush_access_log_on_connected,omitempty"` + // Additional access log options for the TCP proxy. + AccessLogOptions *TcpProxy_TcpAccessLogOptions `protobuf:"bytes,17,opt,name=access_log_options,json=accessLogOptions,proto3" json:"access_log_options,omitempty"` + // TLVs to add to the PROXY protocol header sent upstream. Behavior when PROXY protocol + // state already exists (e.g., downstream TLVs from proxy_protocol listener filter) is + // controlled by “proxy_protocol_tlv_merge_policy“. + // + // .. note:: + // + // To ensure the TLVs are allowed upstream, configure passthrough TLVs on the upstream + // proxy protocol transport. See :ref:`core.v3.ProxyProtocolConfig.pass_through_tlvs + // ` for details. + ProxyProtocolTlvs []*v3.TlvEntry `protobuf:"bytes,19,rep,name=proxy_protocol_tlvs,json=proxyProtocolTlvs,proto3" json:"proxy_protocol_tlvs,omitempty"` + // Specifies how TLVs in “proxy_protocol_tlvs“ are merged with existing PROXY protocol state + // (e.g., downstream TLVs from the proxy_protocol listener filter). See + // :ref:`ProxyProtocolTlvMergePolicy + // `. + ProxyProtocolTlvMergePolicy ProxyProtocolTlvMergePolicy `protobuf:"varint,23,opt,name=proxy_protocol_tlv_merge_policy,json=proxyProtocolTlvMergePolicy,proto3,enum=envoy.extensions.filters.network.tcp_proxy.v3.ProxyProtocolTlvMergePolicy" json:"proxy_protocol_tlv_merge_policy,omitempty"` + // Specifies when to establish the upstream connection. + // + // When not specified, defaults to “IMMEDIATE“ for backward compatibility. + // + // .. attention:: + // + // Server-first protocols (e.g., SMTP, MySQL, POP3) require ``IMMEDIATE`` mode. + UpstreamConnectMode UpstreamConnectMode `protobuf:"varint,21,opt,name=upstream_connect_mode,json=upstreamConnectMode,proto3,enum=envoy.extensions.filters.network.tcp_proxy.v3.UpstreamConnectMode" json:"upstream_connect_mode,omitempty"` + // Maximum bytes of early data to buffer from the downstream connection before + // the upstream connection is established. + // + // If not set, the TCP proxy will read-disable the downstream connection until the + // upstream connection is established (legacy behavior). + // + // If set, enables “receive_before_connect“ mode where the filter allows the filter + // chain to read downstream data before the upstream connection exists. The data is + // buffered and forwarded once the upstream connection is ready. When the buffer exceeds + // this limit, the downstream connection is read-disabled to prevent excessive memory usage. + // + // This field is required when “upstream_connect_mode“ is not “IMMEDIATE“. + // + // .. note:: + // + // Use this carefully with server-first protocols. The upstream may send data before + // receiving anything from downstream, which could fill the early data buffer. + MaxEarlyDataBytes *wrapperspb.UInt32Value `protobuf:"bytes,22,opt,name=max_early_data_bytes,json=maxEarlyDataBytes,proto3" json:"max_early_data_bytes,omitempty"` + // If set to “true“, the TCP proxy checks if the downstream connection was marked as drained + // after each read or write. When drain close is requested for the listener's traffic direction, + // the downstream connection is closed with “FlushWrite“. + // + // This is disabled by default for backward compatibility. + CheckDrainClose *wrapperspb.BoolValue `protobuf:"bytes,24,opt,name=check_drain_close,json=checkDrainClose,proto3" json:"check_drain_close,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *TcpProxy) Reset() { + *x = TcpProxy{} + mi := &file_envoy_extensions_filters_network_tcp_proxy_v3_tcp_proxy_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *TcpProxy) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TcpProxy) ProtoMessage() {} + +func (x *TcpProxy) ProtoReflect() protoreflect.Message { + mi := &file_envoy_extensions_filters_network_tcp_proxy_v3_tcp_proxy_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use TcpProxy.ProtoReflect.Descriptor instead. +func (*TcpProxy) Descriptor() ([]byte, []int) { + return file_envoy_extensions_filters_network_tcp_proxy_v3_tcp_proxy_proto_rawDescGZIP(), []int{0} +} + +func (x *TcpProxy) GetStatPrefix() string { + if x != nil { + return x.StatPrefix + } + return "" +} + +func (x *TcpProxy) GetClusterSpecifier() isTcpProxy_ClusterSpecifier { + if x != nil { + return x.ClusterSpecifier + } + return nil +} + +func (x *TcpProxy) GetCluster() string { + if x != nil { + if x, ok := x.ClusterSpecifier.(*TcpProxy_Cluster); ok { + return x.Cluster + } + } + return "" +} + +func (x *TcpProxy) GetWeightedClusters() *TcpProxy_WeightedCluster { + if x != nil { + if x, ok := x.ClusterSpecifier.(*TcpProxy_WeightedClusters); ok { + return x.WeightedClusters + } + } + return nil +} + +func (x *TcpProxy) GetOnDemand() *TcpProxy_OnDemand { + if x != nil { + return x.OnDemand + } + return nil +} + +func (x *TcpProxy) GetMetadataMatch() *v3.Metadata { + if x != nil { + return x.MetadataMatch + } + return nil +} + +func (x *TcpProxy) GetIdleTimeout() *durationpb.Duration { + if x != nil { + return x.IdleTimeout + } + return nil +} + +func (x *TcpProxy) GetDownstreamIdleTimeout() *durationpb.Duration { + if x != nil { + return x.DownstreamIdleTimeout + } + return nil +} + +func (x *TcpProxy) GetUpstreamIdleTimeout() *durationpb.Duration { + if x != nil { + return x.UpstreamIdleTimeout + } + return nil +} + +func (x *TcpProxy) GetAccessLog() []*v31.AccessLog { + if x != nil { + return x.AccessLog + } + return nil +} + +func (x *TcpProxy) GetMaxConnectAttempts() *wrapperspb.UInt32Value { + if x != nil { + return x.MaxConnectAttempts + } + return nil +} + +func (x *TcpProxy) GetBackoffOptions() *v3.BackoffStrategy { + if x != nil { + return x.BackoffOptions + } + return nil +} + +func (x *TcpProxy) GetHashPolicy() []*v32.HashPolicy { + if x != nil { + return x.HashPolicy + } + return nil +} + +func (x *TcpProxy) GetTunnelingConfig() *TcpProxy_TunnelingConfig { + if x != nil { + return x.TunnelingConfig + } + return nil +} + +func (x *TcpProxy) GetMaxDownstreamConnectionDuration() *durationpb.Duration { + if x != nil { + return x.MaxDownstreamConnectionDuration + } + return nil +} + +func (x *TcpProxy) GetMaxDownstreamConnectionDurationJitterPercentage() *v32.Percent { + if x != nil { + return x.MaxDownstreamConnectionDurationJitterPercentage + } + return nil +} + +// Deprecated: Marked as deprecated in envoy/extensions/filters/network/tcp_proxy/v3/tcp_proxy.proto. +func (x *TcpProxy) GetAccessLogFlushInterval() *durationpb.Duration { + if x != nil { + return x.AccessLogFlushInterval + } + return nil +} + +// Deprecated: Marked as deprecated in envoy/extensions/filters/network/tcp_proxy/v3/tcp_proxy.proto. +func (x *TcpProxy) GetFlushAccessLogOnConnected() bool { + if x != nil { + return x.FlushAccessLogOnConnected + } + return false +} + +func (x *TcpProxy) GetAccessLogOptions() *TcpProxy_TcpAccessLogOptions { + if x != nil { + return x.AccessLogOptions + } + return nil +} + +func (x *TcpProxy) GetProxyProtocolTlvs() []*v3.TlvEntry { + if x != nil { + return x.ProxyProtocolTlvs + } + return nil +} + +func (x *TcpProxy) GetProxyProtocolTlvMergePolicy() ProxyProtocolTlvMergePolicy { + if x != nil { + return x.ProxyProtocolTlvMergePolicy + } + return ProxyProtocolTlvMergePolicy_ADD_IF_ABSENT +} + +func (x *TcpProxy) GetUpstreamConnectMode() UpstreamConnectMode { + if x != nil { + return x.UpstreamConnectMode + } + return UpstreamConnectMode_IMMEDIATE +} + +func (x *TcpProxy) GetMaxEarlyDataBytes() *wrapperspb.UInt32Value { + if x != nil { + return x.MaxEarlyDataBytes + } + return nil +} + +func (x *TcpProxy) GetCheckDrainClose() *wrapperspb.BoolValue { + if x != nil { + return x.CheckDrainClose + } + return nil +} + +type isTcpProxy_ClusterSpecifier interface { + isTcpProxy_ClusterSpecifier() +} + +type TcpProxy_Cluster struct { + // The upstream cluster to connect to. + Cluster string `protobuf:"bytes,2,opt,name=cluster,proto3,oneof"` +} + +type TcpProxy_WeightedClusters struct { + // Multiple upstream clusters can be specified. The request is routed to one of the upstream clusters + // based on the weights assigned to each cluster. + WeightedClusters *TcpProxy_WeightedCluster `protobuf:"bytes,10,opt,name=weighted_clusters,json=weightedClusters,proto3,oneof"` +} + +func (*TcpProxy_Cluster) isTcpProxy_ClusterSpecifier() {} + +func (*TcpProxy_WeightedClusters) isTcpProxy_ClusterSpecifier() {} + +// Allows specification of multiple upstream clusters along with weights indicating the percentage of +// traffic forwarded to each cluster. The cluster selection is based on these weights. +type TcpProxy_WeightedCluster struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Specifies the upstream clusters associated with this configuration. + Clusters []*TcpProxy_WeightedCluster_ClusterWeight `protobuf:"bytes,1,rep,name=clusters,proto3" json:"clusters,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *TcpProxy_WeightedCluster) Reset() { + *x = TcpProxy_WeightedCluster{} + mi := &file_envoy_extensions_filters_network_tcp_proxy_v3_tcp_proxy_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *TcpProxy_WeightedCluster) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TcpProxy_WeightedCluster) ProtoMessage() {} + +func (x *TcpProxy_WeightedCluster) ProtoReflect() protoreflect.Message { + mi := &file_envoy_extensions_filters_network_tcp_proxy_v3_tcp_proxy_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use TcpProxy_WeightedCluster.ProtoReflect.Descriptor instead. +func (*TcpProxy_WeightedCluster) Descriptor() ([]byte, []int) { + return file_envoy_extensions_filters_network_tcp_proxy_v3_tcp_proxy_proto_rawDescGZIP(), []int{0, 0} +} + +func (x *TcpProxy_WeightedCluster) GetClusters() []*TcpProxy_WeightedCluster_ClusterWeight { + if x != nil { + return x.Clusters + } + return nil +} + +// Configuration for tunneling TCP over other transports or application layers. +// Tunneling is supported over HTTP/1.1 and HTTP/2. The upstream protocol is +// determined by the cluster configuration. +// [#next-free-field: 11] +type TcpProxy_TunnelingConfig struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The hostname to send in the synthesized CONNECT headers to the upstream proxy. + // This field evaluates command operators if present; otherwise, the value is used as-is. + // + // For example, dynamically set the hostname using downstream SNI: + // + // .. code-block:: yaml + // + // tunneling_config: + // hostname: "%REQUESTED_SERVER_NAME%:443" + // + // For example, dynamically set the hostname using dynamic metadata: + // + // .. code-block:: yaml + // + // tunneling_config: + // hostname: "%DYNAMIC_METADATA(tunnel:address)%" + Hostname string `protobuf:"bytes,1,opt,name=hostname,proto3" json:"hostname,omitempty"` + // Use the “POST“ method instead of the “CONNECT“ method to tunnel the TCP stream. + // The “protocol: bytestream“ header is not set for HTTP/2 to comply with the specification. + // + // The upstream proxy is expected to interpret the POST payload as raw TCP. + UsePost bool `protobuf:"varint,2,opt,name=use_post,json=usePost,proto3" json:"use_post,omitempty"` + // Additional request headers to send to the upstream proxy. This is mainly used to + // trigger the upstream to convert POST requests back to CONNECT requests. + // + // Neither “:“-prefixed pseudo-headers like “:path“ nor the “host“ header can be overridden. + HeadersToAdd []*v3.HeaderValueOption `protobuf:"bytes,3,rep,name=headers_to_add,json=headersToAdd,proto3" json:"headers_to_add,omitempty"` + // Save response headers to the downstream connection's filter state for consumption + // by network filters. The filter state key is “envoy.tcp_proxy.propagate_response_headers“. + PropagateResponseHeaders bool `protobuf:"varint,4,opt,name=propagate_response_headers,json=propagateResponseHeaders,proto3" json:"propagate_response_headers,omitempty"` + // The path used with the POST method. The default path is “/“. If this field is specified and + // :ref:`use_post field ` + // is not set to “true“, the configuration will be rejected. + PostPath string `protobuf:"bytes,5,opt,name=post_path,json=postPath,proto3" json:"post_path,omitempty"` + // Save response trailers to the downstream connection's filter state for consumption + // by network filters. The filter state key is “envoy.tcp_proxy.propagate_response_trailers“. + PropagateResponseTrailers bool `protobuf:"varint,6,opt,name=propagate_response_trailers,json=propagateResponseTrailers,proto3" json:"propagate_response_trailers,omitempty"` + // The configuration of the request ID extension used for generation, validation, and + // associated tracing operations when tunneling. + // + // If this field is set, a request ID is generated using the specified extension. If + // this field is not set, no request ID is generated. + // + // When a request ID is generated, it is also stored in the downstream connection's + // dynamic metadata under the namespace “envoy.filters.network.tcp_proxy“ with the key + // “tunnel_request_id“ to allow emission from TCP proxy access logs via the + // “%DYNAMIC_METADATA(envoy.filters.network.tcp_proxy:tunnel_request_id)%“ formatter. + // [#extension-category: envoy.request_id] + RequestIdExtension *v33.RequestIDExtension `protobuf:"bytes,7,opt,name=request_id_extension,json=requestIdExtension,proto3" json:"request_id_extension,omitempty"` + // The request header name to use for emitting the generated request ID on the tunneling + // HTTP request. + // + // If not specified or set to an empty string, the default header name “x-request-id“ is + // used. + // + // .. note:: + // + // This setting does not alter the internal request ID handling elsewhere in Envoy and + // only controls the header emitted on the tunneling request. + RequestIdHeader string `protobuf:"bytes,8,opt,name=request_id_header,json=requestIdHeader,proto3" json:"request_id_header,omitempty"` + // The dynamic metadata key to use when storing the generated request ID. The metadata is + // stored under the namespace “envoy.filters.network.tcp_proxy“. + // + // If not specified or set to an empty string, the default key “tunnel_request_id“ is used. + // This enables customizing the key used by access log formatters such as + // “%DYNAMIC_METADATA(envoy.filters.network.tcp_proxy:)%“. + RequestIdMetadataKey string `protobuf:"bytes,9,opt,name=request_id_metadata_key,json=requestIdMetadataKey,proto3" json:"request_id_metadata_key,omitempty"` + // Specifies a collection of Formatter plugins that can be used in substitution formatters + // in “headers_to_add“. + // See the formatters extensions documentation for details. + // [#extension-category: envoy.formatter] + Formatters []*v3.TypedExtensionConfig `protobuf:"bytes,10,rep,name=formatters,proto3" json:"formatters,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *TcpProxy_TunnelingConfig) Reset() { + *x = TcpProxy_TunnelingConfig{} + mi := &file_envoy_extensions_filters_network_tcp_proxy_v3_tcp_proxy_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *TcpProxy_TunnelingConfig) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TcpProxy_TunnelingConfig) ProtoMessage() {} + +func (x *TcpProxy_TunnelingConfig) ProtoReflect() protoreflect.Message { + mi := &file_envoy_extensions_filters_network_tcp_proxy_v3_tcp_proxy_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use TcpProxy_TunnelingConfig.ProtoReflect.Descriptor instead. +func (*TcpProxy_TunnelingConfig) Descriptor() ([]byte, []int) { + return file_envoy_extensions_filters_network_tcp_proxy_v3_tcp_proxy_proto_rawDescGZIP(), []int{0, 1} +} + +func (x *TcpProxy_TunnelingConfig) GetHostname() string { + if x != nil { + return x.Hostname + } + return "" +} + +func (x *TcpProxy_TunnelingConfig) GetUsePost() bool { + if x != nil { + return x.UsePost + } + return false +} + +func (x *TcpProxy_TunnelingConfig) GetHeadersToAdd() []*v3.HeaderValueOption { + if x != nil { + return x.HeadersToAdd + } + return nil +} + +func (x *TcpProxy_TunnelingConfig) GetPropagateResponseHeaders() bool { + if x != nil { + return x.PropagateResponseHeaders + } + return false +} + +func (x *TcpProxy_TunnelingConfig) GetPostPath() string { + if x != nil { + return x.PostPath + } + return "" +} + +func (x *TcpProxy_TunnelingConfig) GetPropagateResponseTrailers() bool { + if x != nil { + return x.PropagateResponseTrailers + } + return false +} + +func (x *TcpProxy_TunnelingConfig) GetRequestIdExtension() *v33.RequestIDExtension { + if x != nil { + return x.RequestIdExtension + } + return nil +} + +func (x *TcpProxy_TunnelingConfig) GetRequestIdHeader() string { + if x != nil { + return x.RequestIdHeader + } + return "" +} + +func (x *TcpProxy_TunnelingConfig) GetRequestIdMetadataKey() string { + if x != nil { + return x.RequestIdMetadataKey + } + return "" +} + +func (x *TcpProxy_TunnelingConfig) GetFormatters() []*v3.TypedExtensionConfig { + if x != nil { + return x.Formatters + } + return nil +} + +type TcpProxy_OnDemand struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Optional configuration for the on-demand cluster discovery service. + // If not specified, on-demand cluster discovery is disabled. When specified, the filter pauses a request + // to an unknown cluster and begins a cluster discovery process. When discovery completes (successfully + // or not), the request is resumed. + OdcdsConfig *v3.ConfigSource `protobuf:"bytes,1,opt,name=odcds_config,json=odcdsConfig,proto3" json:"odcds_config,omitempty"` + // xdstp:// resource locator for on-demand cluster collection. + // [#not-implemented-hide:] + ResourcesLocator string `protobuf:"bytes,2,opt,name=resources_locator,json=resourcesLocator,proto3" json:"resources_locator,omitempty"` + // The timeout for on-demand cluster lookup. If the CDS cannot return the required cluster, + // the downstream request will be closed with the error code detail NO_CLUSTER_FOUND. + // [#not-implemented-hide:] + Timeout *durationpb.Duration `protobuf:"bytes,3,opt,name=timeout,proto3" json:"timeout,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *TcpProxy_OnDemand) Reset() { + *x = TcpProxy_OnDemand{} + mi := &file_envoy_extensions_filters_network_tcp_proxy_v3_tcp_proxy_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *TcpProxy_OnDemand) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TcpProxy_OnDemand) ProtoMessage() {} + +func (x *TcpProxy_OnDemand) ProtoReflect() protoreflect.Message { + mi := &file_envoy_extensions_filters_network_tcp_proxy_v3_tcp_proxy_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use TcpProxy_OnDemand.ProtoReflect.Descriptor instead. +func (*TcpProxy_OnDemand) Descriptor() ([]byte, []int) { + return file_envoy_extensions_filters_network_tcp_proxy_v3_tcp_proxy_proto_rawDescGZIP(), []int{0, 2} +} + +func (x *TcpProxy_OnDemand) GetOdcdsConfig() *v3.ConfigSource { + if x != nil { + return x.OdcdsConfig + } + return nil +} + +func (x *TcpProxy_OnDemand) GetResourcesLocator() string { + if x != nil { + return x.ResourcesLocator + } + return "" +} + +func (x *TcpProxy_OnDemand) GetTimeout() *durationpb.Duration { + if x != nil { + return x.Timeout + } + return nil +} + +type TcpProxy_TcpAccessLogOptions struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The interval for flushing access logs. By default, the TCP proxy flushes a single access log when the + // connection is closed. If this field is set, the TCP proxy flushes access logs periodically at the + // specified interval. + // The interval must be at least 1ms. + AccessLogFlushInterval *durationpb.Duration `protobuf:"bytes,1,opt,name=access_log_flush_interval,json=accessLogFlushInterval,proto3" json:"access_log_flush_interval,omitempty"` + // If set to “true“, the access log is flushed when the TCP proxy successfully establishes a + // connection with the upstream. If the connection fails, the access log is not flushed. + FlushAccessLogOnConnected bool `protobuf:"varint,2,opt,name=flush_access_log_on_connected,json=flushAccessLogOnConnected,proto3" json:"flush_access_log_on_connected,omitempty"` + // If set to “true“, the access log is flushed when the TCP proxy accepts a connection. + FlushAccessLogOnStart bool `protobuf:"varint,3,opt,name=flush_access_log_on_start,json=flushAccessLogOnStart,proto3" json:"flush_access_log_on_start,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *TcpProxy_TcpAccessLogOptions) Reset() { + *x = TcpProxy_TcpAccessLogOptions{} + mi := &file_envoy_extensions_filters_network_tcp_proxy_v3_tcp_proxy_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *TcpProxy_TcpAccessLogOptions) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TcpProxy_TcpAccessLogOptions) ProtoMessage() {} + +func (x *TcpProxy_TcpAccessLogOptions) ProtoReflect() protoreflect.Message { + mi := &file_envoy_extensions_filters_network_tcp_proxy_v3_tcp_proxy_proto_msgTypes[4] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use TcpProxy_TcpAccessLogOptions.ProtoReflect.Descriptor instead. +func (*TcpProxy_TcpAccessLogOptions) Descriptor() ([]byte, []int) { + return file_envoy_extensions_filters_network_tcp_proxy_v3_tcp_proxy_proto_rawDescGZIP(), []int{0, 3} +} + +func (x *TcpProxy_TcpAccessLogOptions) GetAccessLogFlushInterval() *durationpb.Duration { + if x != nil { + return x.AccessLogFlushInterval + } + return nil +} + +func (x *TcpProxy_TcpAccessLogOptions) GetFlushAccessLogOnConnected() bool { + if x != nil { + return x.FlushAccessLogOnConnected + } + return false +} + +func (x *TcpProxy_TcpAccessLogOptions) GetFlushAccessLogOnStart() bool { + if x != nil { + return x.FlushAccessLogOnStart + } + return false +} + +type TcpProxy_WeightedCluster_ClusterWeight struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Name of the upstream cluster. + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + // When a request matches the route, the choice of an upstream cluster is + // determined by its weight. The sum of weights across all entries in the + // clusters array determines the total weight. + Weight uint32 `protobuf:"varint,2,opt,name=weight,proto3" json:"weight,omitempty"` + // Optional endpoint metadata match criteria used by the subset load balancer. Only endpoints + // in the upstream cluster with metadata matching what is set in this field will be considered + // for load balancing. Note that this will be merged with what's provided in + // :ref:`TcpProxy.metadata_match + // `, with values + // here taking precedence. The filter name should be specified as “envoy.lb“. + MetadataMatch *v3.Metadata `protobuf:"bytes,3,opt,name=metadata_match,json=metadataMatch,proto3" json:"metadata_match,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *TcpProxy_WeightedCluster_ClusterWeight) Reset() { + *x = TcpProxy_WeightedCluster_ClusterWeight{} + mi := &file_envoy_extensions_filters_network_tcp_proxy_v3_tcp_proxy_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *TcpProxy_WeightedCluster_ClusterWeight) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TcpProxy_WeightedCluster_ClusterWeight) ProtoMessage() {} + +func (x *TcpProxy_WeightedCluster_ClusterWeight) ProtoReflect() protoreflect.Message { + mi := &file_envoy_extensions_filters_network_tcp_proxy_v3_tcp_proxy_proto_msgTypes[5] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use TcpProxy_WeightedCluster_ClusterWeight.ProtoReflect.Descriptor instead. +func (*TcpProxy_WeightedCluster_ClusterWeight) Descriptor() ([]byte, []int) { + return file_envoy_extensions_filters_network_tcp_proxy_v3_tcp_proxy_proto_rawDescGZIP(), []int{0, 0, 0} +} + +func (x *TcpProxy_WeightedCluster_ClusterWeight) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *TcpProxy_WeightedCluster_ClusterWeight) GetWeight() uint32 { + if x != nil { + return x.Weight + } + return 0 +} + +func (x *TcpProxy_WeightedCluster_ClusterWeight) GetMetadataMatch() *v3.Metadata { + if x != nil { + return x.MetadataMatch + } + return nil +} + +var File_envoy_extensions_filters_network_tcp_proxy_v3_tcp_proxy_proto protoreflect.FileDescriptor + +const file_envoy_extensions_filters_network_tcp_proxy_v3_tcp_proxy_proto_rawDesc = "" + + "\n" + + "=envoy/extensions/filters/network/tcp_proxy/v3/tcp_proxy.proto\x12-envoy.extensions.filters.network.tcp_proxy.v3\x1a)envoy/config/accesslog/v3/accesslog.proto\x1a\"envoy/config/core/v3/backoff.proto\x1a\x1fenvoy/config/core/v3/base.proto\x1a(envoy/config/core/v3/config_source.proto\x1a$envoy/config/core/v3/extension.proto\x1a)envoy/config/core/v3/proxy_protocol.proto\x1aYenvoy/extensions/filters/network/http_connection_manager/v3/http_connection_manager.proto\x1a\x1fenvoy/type/v3/hash_policy.proto\x1a\x1benvoy/type/v3/percent.proto\x1a\x1egoogle/protobuf/duration.proto\x1a\x1egoogle/protobuf/wrappers.proto\x1a#envoy/annotations/deprecation.proto\x1a\x1dudpa/annotations/status.proto\x1a!udpa/annotations/versioning.proto\x1a\x17validate/validate.proto\"\xe6\x1d\n" + + "\bTcpProxy\x12(\n" + + "\vstat_prefix\x18\x01 \x01(\tB\a\xfaB\x04r\x02\x10\x01R\n" + + "statPrefix\x12\x1a\n" + + "\acluster\x18\x02 \x01(\tH\x00R\acluster\x12v\n" + + "\x11weighted_clusters\x18\n" + + " \x01(\v2G.envoy.extensions.filters.network.tcp_proxy.v3.TcpProxy.WeightedClusterH\x00R\x10weightedClusters\x12]\n" + + "\ton_demand\x18\x0e \x01(\v2@.envoy.extensions.filters.network.tcp_proxy.v3.TcpProxy.OnDemandR\bonDemand\x12E\n" + + "\x0emetadata_match\x18\t \x01(\v2\x1e.envoy.config.core.v3.MetadataR\rmetadataMatch\x12<\n" + + "\fidle_timeout\x18\b \x01(\v2\x19.google.protobuf.DurationR\vidleTimeout\x12Q\n" + + "\x17downstream_idle_timeout\x18\x03 \x01(\v2\x19.google.protobuf.DurationR\x15downstreamIdleTimeout\x12M\n" + + "\x15upstream_idle_timeout\x18\x04 \x01(\v2\x19.google.protobuf.DurationR\x13upstreamIdleTimeout\x12C\n" + + "\n" + + "access_log\x18\x05 \x03(\v2$.envoy.config.accesslog.v3.AccessLogR\taccessLog\x12W\n" + + "\x14max_connect_attempts\x18\a \x01(\v2\x1c.google.protobuf.UInt32ValueB\a\xfaB\x04*\x02(\x01R\x12maxConnectAttempts\x12N\n" + + "\x0fbackoff_options\x18\x12 \x01(\v2%.envoy.config.core.v3.BackoffStrategyR\x0ebackoffOptions\x12D\n" + + "\vhash_policy\x18\v \x03(\v2\x19.envoy.type.v3.HashPolicyB\b\xfaB\x05\x92\x01\x02\x10\x01R\n" + + "hashPolicy\x12r\n" + + "\x10tunneling_config\x18\f \x01(\v2G.envoy.extensions.filters.network.tcp_proxy.v3.TcpProxy.TunnelingConfigR\x0ftunnelingConfig\x12t\n" + + "\"max_downstream_connection_duration\x18\r \x01(\v2\x19.google.protobuf.DurationB\f\xfaB\t\xaa\x01\x062\x04\x10\xc0\x84=R\x1fmaxDownstreamConnectionDuration\x12\x85\x01\n" + + "4max_downstream_connection_duration_jitter_percentage\x18\x14 \x01(\v2\x16.envoy.type.v3.PercentR/maxDownstreamConnectionDurationJitterPercentage\x12m\n" + + "\x19access_log_flush_interval\x18\x0f \x01(\v2\x19.google.protobuf.DurationB\x17\xfaB\t\xaa\x01\x062\x04\x10\xc0\x84=\x92dž\xd8\x04\x033.0\x18\x01R\x16accessLogFlushInterval\x12M\n" + + "\x1dflush_access_log_on_connected\x18\x10 \x01(\bB\v\x92dž\xd8\x04\x033.0\x18\x01R\x19flushAccessLogOnConnected\x12y\n" + + "\x12access_log_options\x18\x11 \x01(\v2K.envoy.extensions.filters.network.tcp_proxy.v3.TcpProxy.TcpAccessLogOptionsR\x10accessLogOptions\x12N\n" + + "\x13proxy_protocol_tlvs\x18\x13 \x03(\v2\x1e.envoy.config.core.v3.TlvEntryR\x11proxyProtocolTlvs\x12\x9a\x01\n" + + "\x1fproxy_protocol_tlv_merge_policy\x18\x17 \x01(\x0e2J.envoy.extensions.filters.network.tcp_proxy.v3.ProxyProtocolTlvMergePolicyB\b\xfaB\x05\x82\x01\x02\x10\x01R\x1bproxyProtocolTlvMergePolicy\x12\x80\x01\n" + + "\x15upstream_connect_mode\x18\x15 \x01(\x0e2B.envoy.extensions.filters.network.tcp_proxy.v3.UpstreamConnectModeB\b\xfaB\x05\x82\x01\x02\x10\x01R\x13upstreamConnectMode\x12X\n" + + "\x14max_early_data_bytes\x18\x16 \x01(\v2\x1c.google.protobuf.UInt32ValueB\t\xfaB\x06*\x04\x18\x80\x80@R\x11maxEarlyDataBytes\x12F\n" + + "\x11check_drain_close\x18\x18 \x01(\v2\x1a.google.protobuf.BoolValueR\x0fcheckDrainClose\x1a\xc7\x03\n" + + "\x0fWeightedCluster\x12{\n" + + "\bclusters\x18\x01 \x03(\v2U.envoy.extensions.filters.network.tcp_proxy.v3.TcpProxy.WeightedCluster.ClusterWeightB\b\xfaB\x05\x92\x01\x02\b\x01R\bclusters\x1a\xec\x01\n" + + "\rClusterWeight\x12\x1b\n" + + "\x04name\x18\x01 \x01(\tB\a\xfaB\x04r\x02\x10\x01R\x04name\x12\x1f\n" + + "\x06weight\x18\x02 \x01(\rB\a\xfaB\x04*\x02(\x01R\x06weight\x12E\n" + + "\x0emetadata_match\x18\x03 \x01(\v2\x1e.envoy.config.core.v3.MetadataR\rmetadataMatch:V\x9aň\x1eQ\n" + + "Oenvoy.config.filter.network.tcp_proxy.v2.TcpProxy.WeightedCluster.ClusterWeight:H\x9aň\x1eC\n" + + "Aenvoy.config.filter.network.tcp_proxy.v2.TcpProxy.WeightedCluster\x1a\xc3\x05\n" + + "\x0fTunnelingConfig\x12#\n" + + "\bhostname\x18\x01 \x01(\tB\a\xfaB\x04r\x02\x10\x01R\bhostname\x12\x19\n" + + "\buse_post\x18\x02 \x01(\bR\ausePost\x12X\n" + + "\x0eheaders_to_add\x18\x03 \x03(\v2'.envoy.config.core.v3.HeaderValueOptionB\t\xfaB\x06\x92\x01\x03\x10\xe8\aR\fheadersToAdd\x12<\n" + + "\x1apropagate_response_headers\x18\x04 \x01(\bR\x18propagateResponseHeaders\x12\x1b\n" + + "\tpost_path\x18\x05 \x01(\tR\bpostPath\x12>\n" + + "\x1bpropagate_response_trailers\x18\x06 \x01(\bR\x19propagateResponseTrailers\x12\x81\x01\n" + + "\x14request_id_extension\x18\a \x01(\v2O.envoy.extensions.filters.network.http_connection_manager.v3.RequestIDExtensionR\x12requestIdExtension\x12*\n" + + "\x11request_id_header\x18\b \x01(\tR\x0frequestIdHeader\x125\n" + + "\x17request_id_metadata_key\x18\t \x01(\tR\x14requestIdMetadataKey\x12J\n" + + "\n" + + "formatters\x18\n" + + " \x03(\v2*.envoy.config.core.v3.TypedExtensionConfigR\n" + + "formatters:H\x9aň\x1eC\n" + + "Aenvoy.config.filter.network.tcp_proxy.v2.TcpProxy.TunnelingConfig\x1a\xb3\x01\n" + + "\bOnDemand\x12E\n" + + "\fodcds_config\x18\x01 \x01(\v2\".envoy.config.core.v3.ConfigSourceR\vodcdsConfig\x12+\n" + + "\x11resources_locator\x18\x02 \x01(\tR\x10resourcesLocator\x123\n" + + "\atimeout\x18\x03 \x01(\v2\x19.google.protobuf.DurationR\atimeout\x1a\xf5\x01\n" + + "\x13TcpAccessLogOptions\x12b\n" + + "\x19access_log_flush_interval\x18\x01 \x01(\v2\x19.google.protobuf.DurationB\f\xfaB\t\xaa\x01\x062\x04\x10\xc0\x84=R\x16accessLogFlushInterval\x12@\n" + + "\x1dflush_access_log_on_connected\x18\x02 \x01(\bR\x19flushAccessLogOnConnected\x128\n" + + "\x19flush_access_log_on_start\x18\x03 \x01(\bR\x15flushAccessLogOnStart:8\x9aň\x1e3\n" + + "1envoy.config.filter.network.tcp_proxy.v2.TcpProxyB\x18\n" + + "\x11cluster_specifier\x12\x03\xf8B\x01J\x04\b\x06\x10\aR\rdeprecated_v1*]\n" + + "\x13UpstreamConnectMode\x12\r\n" + + "\tIMMEDIATE\x10\x00\x12\x16\n" + + "\x12ON_DOWNSTREAM_DATA\x10\x01\x12\x1f\n" + + "\x1bON_DOWNSTREAM_TLS_HANDSHAKE\x10\x02*u\n" + + "\x1bProxyProtocolTlvMergePolicy\x12\x11\n" + + "\rADD_IF_ABSENT\x10\x00\x12&\n" + + "\"OVERWRITE_BY_TYPE_IF_EXISTS_OR_ADD\x10\x01\x12\x1b\n" + + "\x17APPEND_IF_EXISTS_OR_ADD\x10\x02B\xb8\x01\xba\x80\xc8\xd1\x06\x02\x10\x02\n" + + ";io.envoyproxy.envoy.extensions.filters.network.tcp_proxy.v3B\rTcpProxyProtoP\x01Z`github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/tcp_proxy/v3;tcp_proxyv3b\x06proto3" + +var ( + file_envoy_extensions_filters_network_tcp_proxy_v3_tcp_proxy_proto_rawDescOnce sync.Once + file_envoy_extensions_filters_network_tcp_proxy_v3_tcp_proxy_proto_rawDescData []byte +) + +func file_envoy_extensions_filters_network_tcp_proxy_v3_tcp_proxy_proto_rawDescGZIP() []byte { + file_envoy_extensions_filters_network_tcp_proxy_v3_tcp_proxy_proto_rawDescOnce.Do(func() { + file_envoy_extensions_filters_network_tcp_proxy_v3_tcp_proxy_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_envoy_extensions_filters_network_tcp_proxy_v3_tcp_proxy_proto_rawDesc), len(file_envoy_extensions_filters_network_tcp_proxy_v3_tcp_proxy_proto_rawDesc))) + }) + return file_envoy_extensions_filters_network_tcp_proxy_v3_tcp_proxy_proto_rawDescData +} + +var file_envoy_extensions_filters_network_tcp_proxy_v3_tcp_proxy_proto_enumTypes = make([]protoimpl.EnumInfo, 2) +var file_envoy_extensions_filters_network_tcp_proxy_v3_tcp_proxy_proto_msgTypes = make([]protoimpl.MessageInfo, 6) +var file_envoy_extensions_filters_network_tcp_proxy_v3_tcp_proxy_proto_goTypes = []any{ + (UpstreamConnectMode)(0), // 0: envoy.extensions.filters.network.tcp_proxy.v3.UpstreamConnectMode + (ProxyProtocolTlvMergePolicy)(0), // 1: envoy.extensions.filters.network.tcp_proxy.v3.ProxyProtocolTlvMergePolicy + (*TcpProxy)(nil), // 2: envoy.extensions.filters.network.tcp_proxy.v3.TcpProxy + (*TcpProxy_WeightedCluster)(nil), // 3: envoy.extensions.filters.network.tcp_proxy.v3.TcpProxy.WeightedCluster + (*TcpProxy_TunnelingConfig)(nil), // 4: envoy.extensions.filters.network.tcp_proxy.v3.TcpProxy.TunnelingConfig + (*TcpProxy_OnDemand)(nil), // 5: envoy.extensions.filters.network.tcp_proxy.v3.TcpProxy.OnDemand + (*TcpProxy_TcpAccessLogOptions)(nil), // 6: envoy.extensions.filters.network.tcp_proxy.v3.TcpProxy.TcpAccessLogOptions + (*TcpProxy_WeightedCluster_ClusterWeight)(nil), // 7: envoy.extensions.filters.network.tcp_proxy.v3.TcpProxy.WeightedCluster.ClusterWeight + (*v3.Metadata)(nil), // 8: envoy.config.core.v3.Metadata + (*durationpb.Duration)(nil), // 9: google.protobuf.Duration + (*v31.AccessLog)(nil), // 10: envoy.config.accesslog.v3.AccessLog + (*wrapperspb.UInt32Value)(nil), // 11: google.protobuf.UInt32Value + (*v3.BackoffStrategy)(nil), // 12: envoy.config.core.v3.BackoffStrategy + (*v32.HashPolicy)(nil), // 13: envoy.type.v3.HashPolicy + (*v32.Percent)(nil), // 14: envoy.type.v3.Percent + (*v3.TlvEntry)(nil), // 15: envoy.config.core.v3.TlvEntry + (*wrapperspb.BoolValue)(nil), // 16: google.protobuf.BoolValue + (*v3.HeaderValueOption)(nil), // 17: envoy.config.core.v3.HeaderValueOption + (*v33.RequestIDExtension)(nil), // 18: envoy.extensions.filters.network.http_connection_manager.v3.RequestIDExtension + (*v3.TypedExtensionConfig)(nil), // 19: envoy.config.core.v3.TypedExtensionConfig + (*v3.ConfigSource)(nil), // 20: envoy.config.core.v3.ConfigSource +} +var file_envoy_extensions_filters_network_tcp_proxy_v3_tcp_proxy_proto_depIdxs = []int32{ + 3, // 0: envoy.extensions.filters.network.tcp_proxy.v3.TcpProxy.weighted_clusters:type_name -> envoy.extensions.filters.network.tcp_proxy.v3.TcpProxy.WeightedCluster + 5, // 1: envoy.extensions.filters.network.tcp_proxy.v3.TcpProxy.on_demand:type_name -> envoy.extensions.filters.network.tcp_proxy.v3.TcpProxy.OnDemand + 8, // 2: envoy.extensions.filters.network.tcp_proxy.v3.TcpProxy.metadata_match:type_name -> envoy.config.core.v3.Metadata + 9, // 3: envoy.extensions.filters.network.tcp_proxy.v3.TcpProxy.idle_timeout:type_name -> google.protobuf.Duration + 9, // 4: envoy.extensions.filters.network.tcp_proxy.v3.TcpProxy.downstream_idle_timeout:type_name -> google.protobuf.Duration + 9, // 5: envoy.extensions.filters.network.tcp_proxy.v3.TcpProxy.upstream_idle_timeout:type_name -> google.protobuf.Duration + 10, // 6: envoy.extensions.filters.network.tcp_proxy.v3.TcpProxy.access_log:type_name -> envoy.config.accesslog.v3.AccessLog + 11, // 7: envoy.extensions.filters.network.tcp_proxy.v3.TcpProxy.max_connect_attempts:type_name -> google.protobuf.UInt32Value + 12, // 8: envoy.extensions.filters.network.tcp_proxy.v3.TcpProxy.backoff_options:type_name -> envoy.config.core.v3.BackoffStrategy + 13, // 9: envoy.extensions.filters.network.tcp_proxy.v3.TcpProxy.hash_policy:type_name -> envoy.type.v3.HashPolicy + 4, // 10: envoy.extensions.filters.network.tcp_proxy.v3.TcpProxy.tunneling_config:type_name -> envoy.extensions.filters.network.tcp_proxy.v3.TcpProxy.TunnelingConfig + 9, // 11: envoy.extensions.filters.network.tcp_proxy.v3.TcpProxy.max_downstream_connection_duration:type_name -> google.protobuf.Duration + 14, // 12: envoy.extensions.filters.network.tcp_proxy.v3.TcpProxy.max_downstream_connection_duration_jitter_percentage:type_name -> envoy.type.v3.Percent + 9, // 13: envoy.extensions.filters.network.tcp_proxy.v3.TcpProxy.access_log_flush_interval:type_name -> google.protobuf.Duration + 6, // 14: envoy.extensions.filters.network.tcp_proxy.v3.TcpProxy.access_log_options:type_name -> envoy.extensions.filters.network.tcp_proxy.v3.TcpProxy.TcpAccessLogOptions + 15, // 15: envoy.extensions.filters.network.tcp_proxy.v3.TcpProxy.proxy_protocol_tlvs:type_name -> envoy.config.core.v3.TlvEntry + 1, // 16: envoy.extensions.filters.network.tcp_proxy.v3.TcpProxy.proxy_protocol_tlv_merge_policy:type_name -> envoy.extensions.filters.network.tcp_proxy.v3.ProxyProtocolTlvMergePolicy + 0, // 17: envoy.extensions.filters.network.tcp_proxy.v3.TcpProxy.upstream_connect_mode:type_name -> envoy.extensions.filters.network.tcp_proxy.v3.UpstreamConnectMode + 11, // 18: envoy.extensions.filters.network.tcp_proxy.v3.TcpProxy.max_early_data_bytes:type_name -> google.protobuf.UInt32Value + 16, // 19: envoy.extensions.filters.network.tcp_proxy.v3.TcpProxy.check_drain_close:type_name -> google.protobuf.BoolValue + 7, // 20: envoy.extensions.filters.network.tcp_proxy.v3.TcpProxy.WeightedCluster.clusters:type_name -> envoy.extensions.filters.network.tcp_proxy.v3.TcpProxy.WeightedCluster.ClusterWeight + 17, // 21: envoy.extensions.filters.network.tcp_proxy.v3.TcpProxy.TunnelingConfig.headers_to_add:type_name -> envoy.config.core.v3.HeaderValueOption + 18, // 22: envoy.extensions.filters.network.tcp_proxy.v3.TcpProxy.TunnelingConfig.request_id_extension:type_name -> envoy.extensions.filters.network.http_connection_manager.v3.RequestIDExtension + 19, // 23: envoy.extensions.filters.network.tcp_proxy.v3.TcpProxy.TunnelingConfig.formatters:type_name -> envoy.config.core.v3.TypedExtensionConfig + 20, // 24: envoy.extensions.filters.network.tcp_proxy.v3.TcpProxy.OnDemand.odcds_config:type_name -> envoy.config.core.v3.ConfigSource + 9, // 25: envoy.extensions.filters.network.tcp_proxy.v3.TcpProxy.OnDemand.timeout:type_name -> google.protobuf.Duration + 9, // 26: envoy.extensions.filters.network.tcp_proxy.v3.TcpProxy.TcpAccessLogOptions.access_log_flush_interval:type_name -> google.protobuf.Duration + 8, // 27: envoy.extensions.filters.network.tcp_proxy.v3.TcpProxy.WeightedCluster.ClusterWeight.metadata_match:type_name -> envoy.config.core.v3.Metadata + 28, // [28:28] is the sub-list for method output_type + 28, // [28:28] is the sub-list for method input_type + 28, // [28:28] is the sub-list for extension type_name + 28, // [28:28] is the sub-list for extension extendee + 0, // [0:28] is the sub-list for field type_name +} + +func init() { file_envoy_extensions_filters_network_tcp_proxy_v3_tcp_proxy_proto_init() } +func file_envoy_extensions_filters_network_tcp_proxy_v3_tcp_proxy_proto_init() { + if File_envoy_extensions_filters_network_tcp_proxy_v3_tcp_proxy_proto != nil { + return + } + file_envoy_extensions_filters_network_tcp_proxy_v3_tcp_proxy_proto_msgTypes[0].OneofWrappers = []any{ + (*TcpProxy_Cluster)(nil), + (*TcpProxy_WeightedClusters)(nil), + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_envoy_extensions_filters_network_tcp_proxy_v3_tcp_proxy_proto_rawDesc), len(file_envoy_extensions_filters_network_tcp_proxy_v3_tcp_proxy_proto_rawDesc)), + NumEnums: 2, + NumMessages: 6, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_envoy_extensions_filters_network_tcp_proxy_v3_tcp_proxy_proto_goTypes, + DependencyIndexes: file_envoy_extensions_filters_network_tcp_proxy_v3_tcp_proxy_proto_depIdxs, + EnumInfos: file_envoy_extensions_filters_network_tcp_proxy_v3_tcp_proxy_proto_enumTypes, + MessageInfos: file_envoy_extensions_filters_network_tcp_proxy_v3_tcp_proxy_proto_msgTypes, + }.Build() + File_envoy_extensions_filters_network_tcp_proxy_v3_tcp_proxy_proto = out.File + file_envoy_extensions_filters_network_tcp_proxy_v3_tcp_proxy_proto_goTypes = nil + file_envoy_extensions_filters_network_tcp_proxy_v3_tcp_proxy_proto_depIdxs = nil +} diff --git a/vendor/github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/tcp_proxy/v3/tcp_proxy.pb.validate.go b/vendor/github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/tcp_proxy/v3/tcp_proxy.pb.validate.go new file mode 100644 index 0000000000..7f0bb8e8da --- /dev/null +++ b/vendor/github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/tcp_proxy/v3/tcp_proxy.pb.validate.go @@ -0,0 +1,1571 @@ +//go:build !disable_pgv +// Code generated by protoc-gen-validate. DO NOT EDIT. +// source: envoy/extensions/filters/network/tcp_proxy/v3/tcp_proxy.proto + +package tcp_proxyv3 + +import ( + "bytes" + "errors" + "fmt" + "net" + "net/mail" + "net/url" + "regexp" + "sort" + "strings" + "time" + "unicode/utf8" + + "google.golang.org/protobuf/types/known/anypb" +) + +// ensure the imports are used +var ( + _ = bytes.MinRead + _ = errors.New("") + _ = fmt.Print + _ = utf8.UTFMax + _ = (*regexp.Regexp)(nil) + _ = (*strings.Reader)(nil) + _ = net.IPv4len + _ = time.Duration(0) + _ = (*url.URL)(nil) + _ = (*mail.Address)(nil) + _ = anypb.Any{} + _ = sort.Sort +) + +// Validate checks the field values on TcpProxy with the rules defined in the +// proto definition for this message. If any rules are violated, the first +// error encountered is returned, or nil if there are no violations. +func (m *TcpProxy) Validate() error { + return m.validate(false) +} + +// ValidateAll checks the field values on TcpProxy with the rules defined in +// the proto definition for this message. If any rules are violated, the +// result is a list of violation errors wrapped in TcpProxyMultiError, or nil +// if none found. +func (m *TcpProxy) ValidateAll() error { + return m.validate(true) +} + +func (m *TcpProxy) validate(all bool) error { + if m == nil { + return nil + } + + var errors []error + + if utf8.RuneCountInString(m.GetStatPrefix()) < 1 { + err := TcpProxyValidationError{ + field: "StatPrefix", + reason: "value length must be at least 1 runes", + } + if !all { + return err + } + errors = append(errors, err) + } + + if all { + switch v := interface{}(m.GetOnDemand()).(type) { + case interface{ ValidateAll() error }: + if err := v.ValidateAll(); err != nil { + errors = append(errors, TcpProxyValidationError{ + field: "OnDemand", + reason: "embedded message failed validation", + cause: err, + }) + } + case interface{ Validate() error }: + if err := v.Validate(); err != nil { + errors = append(errors, TcpProxyValidationError{ + field: "OnDemand", + reason: "embedded message failed validation", + cause: err, + }) + } + } + } else if v, ok := interface{}(m.GetOnDemand()).(interface{ Validate() error }); ok { + if err := v.Validate(); err != nil { + return TcpProxyValidationError{ + field: "OnDemand", + reason: "embedded message failed validation", + cause: err, + } + } + } + + if all { + switch v := interface{}(m.GetMetadataMatch()).(type) { + case interface{ ValidateAll() error }: + if err := v.ValidateAll(); err != nil { + errors = append(errors, TcpProxyValidationError{ + field: "MetadataMatch", + reason: "embedded message failed validation", + cause: err, + }) + } + case interface{ Validate() error }: + if err := v.Validate(); err != nil { + errors = append(errors, TcpProxyValidationError{ + field: "MetadataMatch", + reason: "embedded message failed validation", + cause: err, + }) + } + } + } else if v, ok := interface{}(m.GetMetadataMatch()).(interface{ Validate() error }); ok { + if err := v.Validate(); err != nil { + return TcpProxyValidationError{ + field: "MetadataMatch", + reason: "embedded message failed validation", + cause: err, + } + } + } + + if all { + switch v := interface{}(m.GetIdleTimeout()).(type) { + case interface{ ValidateAll() error }: + if err := v.ValidateAll(); err != nil { + errors = append(errors, TcpProxyValidationError{ + field: "IdleTimeout", + reason: "embedded message failed validation", + cause: err, + }) + } + case interface{ Validate() error }: + if err := v.Validate(); err != nil { + errors = append(errors, TcpProxyValidationError{ + field: "IdleTimeout", + reason: "embedded message failed validation", + cause: err, + }) + } + } + } else if v, ok := interface{}(m.GetIdleTimeout()).(interface{ Validate() error }); ok { + if err := v.Validate(); err != nil { + return TcpProxyValidationError{ + field: "IdleTimeout", + reason: "embedded message failed validation", + cause: err, + } + } + } + + if all { + switch v := interface{}(m.GetDownstreamIdleTimeout()).(type) { + case interface{ ValidateAll() error }: + if err := v.ValidateAll(); err != nil { + errors = append(errors, TcpProxyValidationError{ + field: "DownstreamIdleTimeout", + reason: "embedded message failed validation", + cause: err, + }) + } + case interface{ Validate() error }: + if err := v.Validate(); err != nil { + errors = append(errors, TcpProxyValidationError{ + field: "DownstreamIdleTimeout", + reason: "embedded message failed validation", + cause: err, + }) + } + } + } else if v, ok := interface{}(m.GetDownstreamIdleTimeout()).(interface{ Validate() error }); ok { + if err := v.Validate(); err != nil { + return TcpProxyValidationError{ + field: "DownstreamIdleTimeout", + reason: "embedded message failed validation", + cause: err, + } + } + } + + if all { + switch v := interface{}(m.GetUpstreamIdleTimeout()).(type) { + case interface{ ValidateAll() error }: + if err := v.ValidateAll(); err != nil { + errors = append(errors, TcpProxyValidationError{ + field: "UpstreamIdleTimeout", + reason: "embedded message failed validation", + cause: err, + }) + } + case interface{ Validate() error }: + if err := v.Validate(); err != nil { + errors = append(errors, TcpProxyValidationError{ + field: "UpstreamIdleTimeout", + reason: "embedded message failed validation", + cause: err, + }) + } + } + } else if v, ok := interface{}(m.GetUpstreamIdleTimeout()).(interface{ Validate() error }); ok { + if err := v.Validate(); err != nil { + return TcpProxyValidationError{ + field: "UpstreamIdleTimeout", + reason: "embedded message failed validation", + cause: err, + } + } + } + + for idx, item := range m.GetAccessLog() { + _, _ = idx, item + + if all { + switch v := interface{}(item).(type) { + case interface{ ValidateAll() error }: + if err := v.ValidateAll(); err != nil { + errors = append(errors, TcpProxyValidationError{ + field: fmt.Sprintf("AccessLog[%v]", idx), + reason: "embedded message failed validation", + cause: err, + }) + } + case interface{ Validate() error }: + if err := v.Validate(); err != nil { + errors = append(errors, TcpProxyValidationError{ + field: fmt.Sprintf("AccessLog[%v]", idx), + reason: "embedded message failed validation", + cause: err, + }) + } + } + } else if v, ok := interface{}(item).(interface{ Validate() error }); ok { + if err := v.Validate(); err != nil { + return TcpProxyValidationError{ + field: fmt.Sprintf("AccessLog[%v]", idx), + reason: "embedded message failed validation", + cause: err, + } + } + } + + } + + if wrapper := m.GetMaxConnectAttempts(); wrapper != nil { + + if wrapper.GetValue() < 1 { + err := TcpProxyValidationError{ + field: "MaxConnectAttempts", + reason: "value must be greater than or equal to 1", + } + if !all { + return err + } + errors = append(errors, err) + } + + } + + if all { + switch v := interface{}(m.GetBackoffOptions()).(type) { + case interface{ ValidateAll() error }: + if err := v.ValidateAll(); err != nil { + errors = append(errors, TcpProxyValidationError{ + field: "BackoffOptions", + reason: "embedded message failed validation", + cause: err, + }) + } + case interface{ Validate() error }: + if err := v.Validate(); err != nil { + errors = append(errors, TcpProxyValidationError{ + field: "BackoffOptions", + reason: "embedded message failed validation", + cause: err, + }) + } + } + } else if v, ok := interface{}(m.GetBackoffOptions()).(interface{ Validate() error }); ok { + if err := v.Validate(); err != nil { + return TcpProxyValidationError{ + field: "BackoffOptions", + reason: "embedded message failed validation", + cause: err, + } + } + } + + if len(m.GetHashPolicy()) > 1 { + err := TcpProxyValidationError{ + field: "HashPolicy", + reason: "value must contain no more than 1 item(s)", + } + if !all { + return err + } + errors = append(errors, err) + } + + for idx, item := range m.GetHashPolicy() { + _, _ = idx, item + + if all { + switch v := interface{}(item).(type) { + case interface{ ValidateAll() error }: + if err := v.ValidateAll(); err != nil { + errors = append(errors, TcpProxyValidationError{ + field: fmt.Sprintf("HashPolicy[%v]", idx), + reason: "embedded message failed validation", + cause: err, + }) + } + case interface{ Validate() error }: + if err := v.Validate(); err != nil { + errors = append(errors, TcpProxyValidationError{ + field: fmt.Sprintf("HashPolicy[%v]", idx), + reason: "embedded message failed validation", + cause: err, + }) + } + } + } else if v, ok := interface{}(item).(interface{ Validate() error }); ok { + if err := v.Validate(); err != nil { + return TcpProxyValidationError{ + field: fmt.Sprintf("HashPolicy[%v]", idx), + reason: "embedded message failed validation", + cause: err, + } + } + } + + } + + if all { + switch v := interface{}(m.GetTunnelingConfig()).(type) { + case interface{ ValidateAll() error }: + if err := v.ValidateAll(); err != nil { + errors = append(errors, TcpProxyValidationError{ + field: "TunnelingConfig", + reason: "embedded message failed validation", + cause: err, + }) + } + case interface{ Validate() error }: + if err := v.Validate(); err != nil { + errors = append(errors, TcpProxyValidationError{ + field: "TunnelingConfig", + reason: "embedded message failed validation", + cause: err, + }) + } + } + } else if v, ok := interface{}(m.GetTunnelingConfig()).(interface{ Validate() error }); ok { + if err := v.Validate(); err != nil { + return TcpProxyValidationError{ + field: "TunnelingConfig", + reason: "embedded message failed validation", + cause: err, + } + } + } + + if d := m.GetMaxDownstreamConnectionDuration(); d != nil { + dur, err := d.AsDuration(), d.CheckValid() + if err != nil { + err = TcpProxyValidationError{ + field: "MaxDownstreamConnectionDuration", + reason: "value is not a valid duration", + cause: err, + } + if !all { + return err + } + errors = append(errors, err) + } else { + + gte := time.Duration(0*time.Second + 1000000*time.Nanosecond) + + if dur < gte { + err := TcpProxyValidationError{ + field: "MaxDownstreamConnectionDuration", + reason: "value must be greater than or equal to 1ms", + } + if !all { + return err + } + errors = append(errors, err) + } + + } + } + + if all { + switch v := interface{}(m.GetMaxDownstreamConnectionDurationJitterPercentage()).(type) { + case interface{ ValidateAll() error }: + if err := v.ValidateAll(); err != nil { + errors = append(errors, TcpProxyValidationError{ + field: "MaxDownstreamConnectionDurationJitterPercentage", + reason: "embedded message failed validation", + cause: err, + }) + } + case interface{ Validate() error }: + if err := v.Validate(); err != nil { + errors = append(errors, TcpProxyValidationError{ + field: "MaxDownstreamConnectionDurationJitterPercentage", + reason: "embedded message failed validation", + cause: err, + }) + } + } + } else if v, ok := interface{}(m.GetMaxDownstreamConnectionDurationJitterPercentage()).(interface{ Validate() error }); ok { + if err := v.Validate(); err != nil { + return TcpProxyValidationError{ + field: "MaxDownstreamConnectionDurationJitterPercentage", + reason: "embedded message failed validation", + cause: err, + } + } + } + + if d := m.GetAccessLogFlushInterval(); d != nil { + dur, err := d.AsDuration(), d.CheckValid() + if err != nil { + err = TcpProxyValidationError{ + field: "AccessLogFlushInterval", + reason: "value is not a valid duration", + cause: err, + } + if !all { + return err + } + errors = append(errors, err) + } else { + + gte := time.Duration(0*time.Second + 1000000*time.Nanosecond) + + if dur < gte { + err := TcpProxyValidationError{ + field: "AccessLogFlushInterval", + reason: "value must be greater than or equal to 1ms", + } + if !all { + return err + } + errors = append(errors, err) + } + + } + } + + // no validation rules for FlushAccessLogOnConnected + + if all { + switch v := interface{}(m.GetAccessLogOptions()).(type) { + case interface{ ValidateAll() error }: + if err := v.ValidateAll(); err != nil { + errors = append(errors, TcpProxyValidationError{ + field: "AccessLogOptions", + reason: "embedded message failed validation", + cause: err, + }) + } + case interface{ Validate() error }: + if err := v.Validate(); err != nil { + errors = append(errors, TcpProxyValidationError{ + field: "AccessLogOptions", + reason: "embedded message failed validation", + cause: err, + }) + } + } + } else if v, ok := interface{}(m.GetAccessLogOptions()).(interface{ Validate() error }); ok { + if err := v.Validate(); err != nil { + return TcpProxyValidationError{ + field: "AccessLogOptions", + reason: "embedded message failed validation", + cause: err, + } + } + } + + for idx, item := range m.GetProxyProtocolTlvs() { + _, _ = idx, item + + if all { + switch v := interface{}(item).(type) { + case interface{ ValidateAll() error }: + if err := v.ValidateAll(); err != nil { + errors = append(errors, TcpProxyValidationError{ + field: fmt.Sprintf("ProxyProtocolTlvs[%v]", idx), + reason: "embedded message failed validation", + cause: err, + }) + } + case interface{ Validate() error }: + if err := v.Validate(); err != nil { + errors = append(errors, TcpProxyValidationError{ + field: fmt.Sprintf("ProxyProtocolTlvs[%v]", idx), + reason: "embedded message failed validation", + cause: err, + }) + } + } + } else if v, ok := interface{}(item).(interface{ Validate() error }); ok { + if err := v.Validate(); err != nil { + return TcpProxyValidationError{ + field: fmt.Sprintf("ProxyProtocolTlvs[%v]", idx), + reason: "embedded message failed validation", + cause: err, + } + } + } + + } + + if _, ok := ProxyProtocolTlvMergePolicy_name[int32(m.GetProxyProtocolTlvMergePolicy())]; !ok { + err := TcpProxyValidationError{ + field: "ProxyProtocolTlvMergePolicy", + reason: "value must be one of the defined enum values", + } + if !all { + return err + } + errors = append(errors, err) + } + + if _, ok := UpstreamConnectMode_name[int32(m.GetUpstreamConnectMode())]; !ok { + err := TcpProxyValidationError{ + field: "UpstreamConnectMode", + reason: "value must be one of the defined enum values", + } + if !all { + return err + } + errors = append(errors, err) + } + + if wrapper := m.GetMaxEarlyDataBytes(); wrapper != nil { + + if wrapper.GetValue() > 1048576 { + err := TcpProxyValidationError{ + field: "MaxEarlyDataBytes", + reason: "value must be less than or equal to 1048576", + } + if !all { + return err + } + errors = append(errors, err) + } + + } + + if all { + switch v := interface{}(m.GetCheckDrainClose()).(type) { + case interface{ ValidateAll() error }: + if err := v.ValidateAll(); err != nil { + errors = append(errors, TcpProxyValidationError{ + field: "CheckDrainClose", + reason: "embedded message failed validation", + cause: err, + }) + } + case interface{ Validate() error }: + if err := v.Validate(); err != nil { + errors = append(errors, TcpProxyValidationError{ + field: "CheckDrainClose", + reason: "embedded message failed validation", + cause: err, + }) + } + } + } else if v, ok := interface{}(m.GetCheckDrainClose()).(interface{ Validate() error }); ok { + if err := v.Validate(); err != nil { + return TcpProxyValidationError{ + field: "CheckDrainClose", + reason: "embedded message failed validation", + cause: err, + } + } + } + + oneofClusterSpecifierPresent := false + switch v := m.ClusterSpecifier.(type) { + case *TcpProxy_Cluster: + if v == nil { + err := TcpProxyValidationError{ + field: "ClusterSpecifier", + reason: "oneof value cannot be a typed-nil", + } + if !all { + return err + } + errors = append(errors, err) + } + oneofClusterSpecifierPresent = true + // no validation rules for Cluster + case *TcpProxy_WeightedClusters: + if v == nil { + err := TcpProxyValidationError{ + field: "ClusterSpecifier", + reason: "oneof value cannot be a typed-nil", + } + if !all { + return err + } + errors = append(errors, err) + } + oneofClusterSpecifierPresent = true + + if all { + switch v := interface{}(m.GetWeightedClusters()).(type) { + case interface{ ValidateAll() error }: + if err := v.ValidateAll(); err != nil { + errors = append(errors, TcpProxyValidationError{ + field: "WeightedClusters", + reason: "embedded message failed validation", + cause: err, + }) + } + case interface{ Validate() error }: + if err := v.Validate(); err != nil { + errors = append(errors, TcpProxyValidationError{ + field: "WeightedClusters", + reason: "embedded message failed validation", + cause: err, + }) + } + } + } else if v, ok := interface{}(m.GetWeightedClusters()).(interface{ Validate() error }); ok { + if err := v.Validate(); err != nil { + return TcpProxyValidationError{ + field: "WeightedClusters", + reason: "embedded message failed validation", + cause: err, + } + } + } + + default: + _ = v // ensures v is used + } + if !oneofClusterSpecifierPresent { + err := TcpProxyValidationError{ + field: "ClusterSpecifier", + reason: "value is required", + } + if !all { + return err + } + errors = append(errors, err) + } + + if len(errors) > 0 { + return TcpProxyMultiError(errors) + } + + return nil +} + +// TcpProxyMultiError is an error wrapping multiple validation errors returned +// by TcpProxy.ValidateAll() if the designated constraints aren't met. +type TcpProxyMultiError []error + +// Error returns a concatenation of all the error messages it wraps. +func (m TcpProxyMultiError) Error() string { + msgs := make([]string, 0, len(m)) + for _, err := range m { + msgs = append(msgs, err.Error()) + } + return strings.Join(msgs, "; ") +} + +// AllErrors returns a list of validation violation errors. +func (m TcpProxyMultiError) AllErrors() []error { return m } + +// TcpProxyValidationError is the validation error returned by +// TcpProxy.Validate if the designated constraints aren't met. +type TcpProxyValidationError struct { + field string + reason string + cause error + key bool +} + +// Field function returns field value. +func (e TcpProxyValidationError) Field() string { return e.field } + +// Reason function returns reason value. +func (e TcpProxyValidationError) Reason() string { return e.reason } + +// Cause function returns cause value. +func (e TcpProxyValidationError) Cause() error { return e.cause } + +// Key function returns key value. +func (e TcpProxyValidationError) Key() bool { return e.key } + +// ErrorName returns error name. +func (e TcpProxyValidationError) ErrorName() string { return "TcpProxyValidationError" } + +// Error satisfies the builtin error interface +func (e TcpProxyValidationError) Error() string { + cause := "" + if e.cause != nil { + cause = fmt.Sprintf(" | caused by: %v", e.cause) + } + + key := "" + if e.key { + key = "key for " + } + + return fmt.Sprintf( + "invalid %sTcpProxy.%s: %s%s", + key, + e.field, + e.reason, + cause) +} + +var _ error = TcpProxyValidationError{} + +var _ interface { + Field() string + Reason() string + Key() bool + Cause() error + ErrorName() string +} = TcpProxyValidationError{} + +// Validate checks the field values on TcpProxy_WeightedCluster with the rules +// defined in the proto definition for this message. If any rules are +// violated, the first error encountered is returned, or nil if there are no violations. +func (m *TcpProxy_WeightedCluster) Validate() error { + return m.validate(false) +} + +// ValidateAll checks the field values on TcpProxy_WeightedCluster with the +// rules defined in the proto definition for this message. If any rules are +// violated, the result is a list of violation errors wrapped in +// TcpProxy_WeightedClusterMultiError, or nil if none found. +func (m *TcpProxy_WeightedCluster) ValidateAll() error { + return m.validate(true) +} + +func (m *TcpProxy_WeightedCluster) validate(all bool) error { + if m == nil { + return nil + } + + var errors []error + + if len(m.GetClusters()) < 1 { + err := TcpProxy_WeightedClusterValidationError{ + field: "Clusters", + reason: "value must contain at least 1 item(s)", + } + if !all { + return err + } + errors = append(errors, err) + } + + for idx, item := range m.GetClusters() { + _, _ = idx, item + + if all { + switch v := interface{}(item).(type) { + case interface{ ValidateAll() error }: + if err := v.ValidateAll(); err != nil { + errors = append(errors, TcpProxy_WeightedClusterValidationError{ + field: fmt.Sprintf("Clusters[%v]", idx), + reason: "embedded message failed validation", + cause: err, + }) + } + case interface{ Validate() error }: + if err := v.Validate(); err != nil { + errors = append(errors, TcpProxy_WeightedClusterValidationError{ + field: fmt.Sprintf("Clusters[%v]", idx), + reason: "embedded message failed validation", + cause: err, + }) + } + } + } else if v, ok := interface{}(item).(interface{ Validate() error }); ok { + if err := v.Validate(); err != nil { + return TcpProxy_WeightedClusterValidationError{ + field: fmt.Sprintf("Clusters[%v]", idx), + reason: "embedded message failed validation", + cause: err, + } + } + } + + } + + if len(errors) > 0 { + return TcpProxy_WeightedClusterMultiError(errors) + } + + return nil +} + +// TcpProxy_WeightedClusterMultiError is an error wrapping multiple validation +// errors returned by TcpProxy_WeightedCluster.ValidateAll() if the designated +// constraints aren't met. +type TcpProxy_WeightedClusterMultiError []error + +// Error returns a concatenation of all the error messages it wraps. +func (m TcpProxy_WeightedClusterMultiError) Error() string { + msgs := make([]string, 0, len(m)) + for _, err := range m { + msgs = append(msgs, err.Error()) + } + return strings.Join(msgs, "; ") +} + +// AllErrors returns a list of validation violation errors. +func (m TcpProxy_WeightedClusterMultiError) AllErrors() []error { return m } + +// TcpProxy_WeightedClusterValidationError is the validation error returned by +// TcpProxy_WeightedCluster.Validate if the designated constraints aren't met. +type TcpProxy_WeightedClusterValidationError struct { + field string + reason string + cause error + key bool +} + +// Field function returns field value. +func (e TcpProxy_WeightedClusterValidationError) Field() string { return e.field } + +// Reason function returns reason value. +func (e TcpProxy_WeightedClusterValidationError) Reason() string { return e.reason } + +// Cause function returns cause value. +func (e TcpProxy_WeightedClusterValidationError) Cause() error { return e.cause } + +// Key function returns key value. +func (e TcpProxy_WeightedClusterValidationError) Key() bool { return e.key } + +// ErrorName returns error name. +func (e TcpProxy_WeightedClusterValidationError) ErrorName() string { + return "TcpProxy_WeightedClusterValidationError" +} + +// Error satisfies the builtin error interface +func (e TcpProxy_WeightedClusterValidationError) Error() string { + cause := "" + if e.cause != nil { + cause = fmt.Sprintf(" | caused by: %v", e.cause) + } + + key := "" + if e.key { + key = "key for " + } + + return fmt.Sprintf( + "invalid %sTcpProxy_WeightedCluster.%s: %s%s", + key, + e.field, + e.reason, + cause) +} + +var _ error = TcpProxy_WeightedClusterValidationError{} + +var _ interface { + Field() string + Reason() string + Key() bool + Cause() error + ErrorName() string +} = TcpProxy_WeightedClusterValidationError{} + +// Validate checks the field values on TcpProxy_TunnelingConfig with the rules +// defined in the proto definition for this message. If any rules are +// violated, the first error encountered is returned, or nil if there are no violations. +func (m *TcpProxy_TunnelingConfig) Validate() error { + return m.validate(false) +} + +// ValidateAll checks the field values on TcpProxy_TunnelingConfig with the +// rules defined in the proto definition for this message. If any rules are +// violated, the result is a list of violation errors wrapped in +// TcpProxy_TunnelingConfigMultiError, or nil if none found. +func (m *TcpProxy_TunnelingConfig) ValidateAll() error { + return m.validate(true) +} + +func (m *TcpProxy_TunnelingConfig) validate(all bool) error { + if m == nil { + return nil + } + + var errors []error + + if utf8.RuneCountInString(m.GetHostname()) < 1 { + err := TcpProxy_TunnelingConfigValidationError{ + field: "Hostname", + reason: "value length must be at least 1 runes", + } + if !all { + return err + } + errors = append(errors, err) + } + + // no validation rules for UsePost + + if len(m.GetHeadersToAdd()) > 1000 { + err := TcpProxy_TunnelingConfigValidationError{ + field: "HeadersToAdd", + reason: "value must contain no more than 1000 item(s)", + } + if !all { + return err + } + errors = append(errors, err) + } + + for idx, item := range m.GetHeadersToAdd() { + _, _ = idx, item + + if all { + switch v := interface{}(item).(type) { + case interface{ ValidateAll() error }: + if err := v.ValidateAll(); err != nil { + errors = append(errors, TcpProxy_TunnelingConfigValidationError{ + field: fmt.Sprintf("HeadersToAdd[%v]", idx), + reason: "embedded message failed validation", + cause: err, + }) + } + case interface{ Validate() error }: + if err := v.Validate(); err != nil { + errors = append(errors, TcpProxy_TunnelingConfigValidationError{ + field: fmt.Sprintf("HeadersToAdd[%v]", idx), + reason: "embedded message failed validation", + cause: err, + }) + } + } + } else if v, ok := interface{}(item).(interface{ Validate() error }); ok { + if err := v.Validate(); err != nil { + return TcpProxy_TunnelingConfigValidationError{ + field: fmt.Sprintf("HeadersToAdd[%v]", idx), + reason: "embedded message failed validation", + cause: err, + } + } + } + + } + + // no validation rules for PropagateResponseHeaders + + // no validation rules for PostPath + + // no validation rules for PropagateResponseTrailers + + if all { + switch v := interface{}(m.GetRequestIdExtension()).(type) { + case interface{ ValidateAll() error }: + if err := v.ValidateAll(); err != nil { + errors = append(errors, TcpProxy_TunnelingConfigValidationError{ + field: "RequestIdExtension", + reason: "embedded message failed validation", + cause: err, + }) + } + case interface{ Validate() error }: + if err := v.Validate(); err != nil { + errors = append(errors, TcpProxy_TunnelingConfigValidationError{ + field: "RequestIdExtension", + reason: "embedded message failed validation", + cause: err, + }) + } + } + } else if v, ok := interface{}(m.GetRequestIdExtension()).(interface{ Validate() error }); ok { + if err := v.Validate(); err != nil { + return TcpProxy_TunnelingConfigValidationError{ + field: "RequestIdExtension", + reason: "embedded message failed validation", + cause: err, + } + } + } + + // no validation rules for RequestIdHeader + + // no validation rules for RequestIdMetadataKey + + for idx, item := range m.GetFormatters() { + _, _ = idx, item + + if all { + switch v := interface{}(item).(type) { + case interface{ ValidateAll() error }: + if err := v.ValidateAll(); err != nil { + errors = append(errors, TcpProxy_TunnelingConfigValidationError{ + field: fmt.Sprintf("Formatters[%v]", idx), + reason: "embedded message failed validation", + cause: err, + }) + } + case interface{ Validate() error }: + if err := v.Validate(); err != nil { + errors = append(errors, TcpProxy_TunnelingConfigValidationError{ + field: fmt.Sprintf("Formatters[%v]", idx), + reason: "embedded message failed validation", + cause: err, + }) + } + } + } else if v, ok := interface{}(item).(interface{ Validate() error }); ok { + if err := v.Validate(); err != nil { + return TcpProxy_TunnelingConfigValidationError{ + field: fmt.Sprintf("Formatters[%v]", idx), + reason: "embedded message failed validation", + cause: err, + } + } + } + + } + + if len(errors) > 0 { + return TcpProxy_TunnelingConfigMultiError(errors) + } + + return nil +} + +// TcpProxy_TunnelingConfigMultiError is an error wrapping multiple validation +// errors returned by TcpProxy_TunnelingConfig.ValidateAll() if the designated +// constraints aren't met. +type TcpProxy_TunnelingConfigMultiError []error + +// Error returns a concatenation of all the error messages it wraps. +func (m TcpProxy_TunnelingConfigMultiError) Error() string { + msgs := make([]string, 0, len(m)) + for _, err := range m { + msgs = append(msgs, err.Error()) + } + return strings.Join(msgs, "; ") +} + +// AllErrors returns a list of validation violation errors. +func (m TcpProxy_TunnelingConfigMultiError) AllErrors() []error { return m } + +// TcpProxy_TunnelingConfigValidationError is the validation error returned by +// TcpProxy_TunnelingConfig.Validate if the designated constraints aren't met. +type TcpProxy_TunnelingConfigValidationError struct { + field string + reason string + cause error + key bool +} + +// Field function returns field value. +func (e TcpProxy_TunnelingConfigValidationError) Field() string { return e.field } + +// Reason function returns reason value. +func (e TcpProxy_TunnelingConfigValidationError) Reason() string { return e.reason } + +// Cause function returns cause value. +func (e TcpProxy_TunnelingConfigValidationError) Cause() error { return e.cause } + +// Key function returns key value. +func (e TcpProxy_TunnelingConfigValidationError) Key() bool { return e.key } + +// ErrorName returns error name. +func (e TcpProxy_TunnelingConfigValidationError) ErrorName() string { + return "TcpProxy_TunnelingConfigValidationError" +} + +// Error satisfies the builtin error interface +func (e TcpProxy_TunnelingConfigValidationError) Error() string { + cause := "" + if e.cause != nil { + cause = fmt.Sprintf(" | caused by: %v", e.cause) + } + + key := "" + if e.key { + key = "key for " + } + + return fmt.Sprintf( + "invalid %sTcpProxy_TunnelingConfig.%s: %s%s", + key, + e.field, + e.reason, + cause) +} + +var _ error = TcpProxy_TunnelingConfigValidationError{} + +var _ interface { + Field() string + Reason() string + Key() bool + Cause() error + ErrorName() string +} = TcpProxy_TunnelingConfigValidationError{} + +// Validate checks the field values on TcpProxy_OnDemand with the rules defined +// in the proto definition for this message. If any rules are violated, the +// first error encountered is returned, or nil if there are no violations. +func (m *TcpProxy_OnDemand) Validate() error { + return m.validate(false) +} + +// ValidateAll checks the field values on TcpProxy_OnDemand with the rules +// defined in the proto definition for this message. If any rules are +// violated, the result is a list of violation errors wrapped in +// TcpProxy_OnDemandMultiError, or nil if none found. +func (m *TcpProxy_OnDemand) ValidateAll() error { + return m.validate(true) +} + +func (m *TcpProxy_OnDemand) validate(all bool) error { + if m == nil { + return nil + } + + var errors []error + + if all { + switch v := interface{}(m.GetOdcdsConfig()).(type) { + case interface{ ValidateAll() error }: + if err := v.ValidateAll(); err != nil { + errors = append(errors, TcpProxy_OnDemandValidationError{ + field: "OdcdsConfig", + reason: "embedded message failed validation", + cause: err, + }) + } + case interface{ Validate() error }: + if err := v.Validate(); err != nil { + errors = append(errors, TcpProxy_OnDemandValidationError{ + field: "OdcdsConfig", + reason: "embedded message failed validation", + cause: err, + }) + } + } + } else if v, ok := interface{}(m.GetOdcdsConfig()).(interface{ Validate() error }); ok { + if err := v.Validate(); err != nil { + return TcpProxy_OnDemandValidationError{ + field: "OdcdsConfig", + reason: "embedded message failed validation", + cause: err, + } + } + } + + // no validation rules for ResourcesLocator + + if all { + switch v := interface{}(m.GetTimeout()).(type) { + case interface{ ValidateAll() error }: + if err := v.ValidateAll(); err != nil { + errors = append(errors, TcpProxy_OnDemandValidationError{ + field: "Timeout", + reason: "embedded message failed validation", + cause: err, + }) + } + case interface{ Validate() error }: + if err := v.Validate(); err != nil { + errors = append(errors, TcpProxy_OnDemandValidationError{ + field: "Timeout", + reason: "embedded message failed validation", + cause: err, + }) + } + } + } else if v, ok := interface{}(m.GetTimeout()).(interface{ Validate() error }); ok { + if err := v.Validate(); err != nil { + return TcpProxy_OnDemandValidationError{ + field: "Timeout", + reason: "embedded message failed validation", + cause: err, + } + } + } + + if len(errors) > 0 { + return TcpProxy_OnDemandMultiError(errors) + } + + return nil +} + +// TcpProxy_OnDemandMultiError is an error wrapping multiple validation errors +// returned by TcpProxy_OnDemand.ValidateAll() if the designated constraints +// aren't met. +type TcpProxy_OnDemandMultiError []error + +// Error returns a concatenation of all the error messages it wraps. +func (m TcpProxy_OnDemandMultiError) Error() string { + msgs := make([]string, 0, len(m)) + for _, err := range m { + msgs = append(msgs, err.Error()) + } + return strings.Join(msgs, "; ") +} + +// AllErrors returns a list of validation violation errors. +func (m TcpProxy_OnDemandMultiError) AllErrors() []error { return m } + +// TcpProxy_OnDemandValidationError is the validation error returned by +// TcpProxy_OnDemand.Validate if the designated constraints aren't met. +type TcpProxy_OnDemandValidationError struct { + field string + reason string + cause error + key bool +} + +// Field function returns field value. +func (e TcpProxy_OnDemandValidationError) Field() string { return e.field } + +// Reason function returns reason value. +func (e TcpProxy_OnDemandValidationError) Reason() string { return e.reason } + +// Cause function returns cause value. +func (e TcpProxy_OnDemandValidationError) Cause() error { return e.cause } + +// Key function returns key value. +func (e TcpProxy_OnDemandValidationError) Key() bool { return e.key } + +// ErrorName returns error name. +func (e TcpProxy_OnDemandValidationError) ErrorName() string { + return "TcpProxy_OnDemandValidationError" +} + +// Error satisfies the builtin error interface +func (e TcpProxy_OnDemandValidationError) Error() string { + cause := "" + if e.cause != nil { + cause = fmt.Sprintf(" | caused by: %v", e.cause) + } + + key := "" + if e.key { + key = "key for " + } + + return fmt.Sprintf( + "invalid %sTcpProxy_OnDemand.%s: %s%s", + key, + e.field, + e.reason, + cause) +} + +var _ error = TcpProxy_OnDemandValidationError{} + +var _ interface { + Field() string + Reason() string + Key() bool + Cause() error + ErrorName() string +} = TcpProxy_OnDemandValidationError{} + +// Validate checks the field values on TcpProxy_TcpAccessLogOptions with the +// rules defined in the proto definition for this message. If any rules are +// violated, the first error encountered is returned, or nil if there are no violations. +func (m *TcpProxy_TcpAccessLogOptions) Validate() error { + return m.validate(false) +} + +// ValidateAll checks the field values on TcpProxy_TcpAccessLogOptions with the +// rules defined in the proto definition for this message. If any rules are +// violated, the result is a list of violation errors wrapped in +// TcpProxy_TcpAccessLogOptionsMultiError, or nil if none found. +func (m *TcpProxy_TcpAccessLogOptions) ValidateAll() error { + return m.validate(true) +} + +func (m *TcpProxy_TcpAccessLogOptions) validate(all bool) error { + if m == nil { + return nil + } + + var errors []error + + if d := m.GetAccessLogFlushInterval(); d != nil { + dur, err := d.AsDuration(), d.CheckValid() + if err != nil { + err = TcpProxy_TcpAccessLogOptionsValidationError{ + field: "AccessLogFlushInterval", + reason: "value is not a valid duration", + cause: err, + } + if !all { + return err + } + errors = append(errors, err) + } else { + + gte := time.Duration(0*time.Second + 1000000*time.Nanosecond) + + if dur < gte { + err := TcpProxy_TcpAccessLogOptionsValidationError{ + field: "AccessLogFlushInterval", + reason: "value must be greater than or equal to 1ms", + } + if !all { + return err + } + errors = append(errors, err) + } + + } + } + + // no validation rules for FlushAccessLogOnConnected + + // no validation rules for FlushAccessLogOnStart + + if len(errors) > 0 { + return TcpProxy_TcpAccessLogOptionsMultiError(errors) + } + + return nil +} + +// TcpProxy_TcpAccessLogOptionsMultiError is an error wrapping multiple +// validation errors returned by TcpProxy_TcpAccessLogOptions.ValidateAll() if +// the designated constraints aren't met. +type TcpProxy_TcpAccessLogOptionsMultiError []error + +// Error returns a concatenation of all the error messages it wraps. +func (m TcpProxy_TcpAccessLogOptionsMultiError) Error() string { + msgs := make([]string, 0, len(m)) + for _, err := range m { + msgs = append(msgs, err.Error()) + } + return strings.Join(msgs, "; ") +} + +// AllErrors returns a list of validation violation errors. +func (m TcpProxy_TcpAccessLogOptionsMultiError) AllErrors() []error { return m } + +// TcpProxy_TcpAccessLogOptionsValidationError is the validation error returned +// by TcpProxy_TcpAccessLogOptions.Validate if the designated constraints +// aren't met. +type TcpProxy_TcpAccessLogOptionsValidationError struct { + field string + reason string + cause error + key bool +} + +// Field function returns field value. +func (e TcpProxy_TcpAccessLogOptionsValidationError) Field() string { return e.field } + +// Reason function returns reason value. +func (e TcpProxy_TcpAccessLogOptionsValidationError) Reason() string { return e.reason } + +// Cause function returns cause value. +func (e TcpProxy_TcpAccessLogOptionsValidationError) Cause() error { return e.cause } + +// Key function returns key value. +func (e TcpProxy_TcpAccessLogOptionsValidationError) Key() bool { return e.key } + +// ErrorName returns error name. +func (e TcpProxy_TcpAccessLogOptionsValidationError) ErrorName() string { + return "TcpProxy_TcpAccessLogOptionsValidationError" +} + +// Error satisfies the builtin error interface +func (e TcpProxy_TcpAccessLogOptionsValidationError) Error() string { + cause := "" + if e.cause != nil { + cause = fmt.Sprintf(" | caused by: %v", e.cause) + } + + key := "" + if e.key { + key = "key for " + } + + return fmt.Sprintf( + "invalid %sTcpProxy_TcpAccessLogOptions.%s: %s%s", + key, + e.field, + e.reason, + cause) +} + +var _ error = TcpProxy_TcpAccessLogOptionsValidationError{} + +var _ interface { + Field() string + Reason() string + Key() bool + Cause() error + ErrorName() string +} = TcpProxy_TcpAccessLogOptionsValidationError{} + +// Validate checks the field values on TcpProxy_WeightedCluster_ClusterWeight +// with the rules defined in the proto definition for this message. If any +// rules are violated, the first error encountered is returned, or nil if +// there are no violations. +func (m *TcpProxy_WeightedCluster_ClusterWeight) Validate() error { + return m.validate(false) +} + +// ValidateAll checks the field values on +// TcpProxy_WeightedCluster_ClusterWeight with the rules defined in the proto +// definition for this message. If any rules are violated, the result is a +// list of violation errors wrapped in +// TcpProxy_WeightedCluster_ClusterWeightMultiError, or nil if none found. +func (m *TcpProxy_WeightedCluster_ClusterWeight) ValidateAll() error { + return m.validate(true) +} + +func (m *TcpProxy_WeightedCluster_ClusterWeight) validate(all bool) error { + if m == nil { + return nil + } + + var errors []error + + if utf8.RuneCountInString(m.GetName()) < 1 { + err := TcpProxy_WeightedCluster_ClusterWeightValidationError{ + field: "Name", + reason: "value length must be at least 1 runes", + } + if !all { + return err + } + errors = append(errors, err) + } + + if m.GetWeight() < 1 { + err := TcpProxy_WeightedCluster_ClusterWeightValidationError{ + field: "Weight", + reason: "value must be greater than or equal to 1", + } + if !all { + return err + } + errors = append(errors, err) + } + + if all { + switch v := interface{}(m.GetMetadataMatch()).(type) { + case interface{ ValidateAll() error }: + if err := v.ValidateAll(); err != nil { + errors = append(errors, TcpProxy_WeightedCluster_ClusterWeightValidationError{ + field: "MetadataMatch", + reason: "embedded message failed validation", + cause: err, + }) + } + case interface{ Validate() error }: + if err := v.Validate(); err != nil { + errors = append(errors, TcpProxy_WeightedCluster_ClusterWeightValidationError{ + field: "MetadataMatch", + reason: "embedded message failed validation", + cause: err, + }) + } + } + } else if v, ok := interface{}(m.GetMetadataMatch()).(interface{ Validate() error }); ok { + if err := v.Validate(); err != nil { + return TcpProxy_WeightedCluster_ClusterWeightValidationError{ + field: "MetadataMatch", + reason: "embedded message failed validation", + cause: err, + } + } + } + + if len(errors) > 0 { + return TcpProxy_WeightedCluster_ClusterWeightMultiError(errors) + } + + return nil +} + +// TcpProxy_WeightedCluster_ClusterWeightMultiError is an error wrapping +// multiple validation errors returned by +// TcpProxy_WeightedCluster_ClusterWeight.ValidateAll() if the designated +// constraints aren't met. +type TcpProxy_WeightedCluster_ClusterWeightMultiError []error + +// Error returns a concatenation of all the error messages it wraps. +func (m TcpProxy_WeightedCluster_ClusterWeightMultiError) Error() string { + msgs := make([]string, 0, len(m)) + for _, err := range m { + msgs = append(msgs, err.Error()) + } + return strings.Join(msgs, "; ") +} + +// AllErrors returns a list of validation violation errors. +func (m TcpProxy_WeightedCluster_ClusterWeightMultiError) AllErrors() []error { return m } + +// TcpProxy_WeightedCluster_ClusterWeightValidationError is the validation +// error returned by TcpProxy_WeightedCluster_ClusterWeight.Validate if the +// designated constraints aren't met. +type TcpProxy_WeightedCluster_ClusterWeightValidationError struct { + field string + reason string + cause error + key bool +} + +// Field function returns field value. +func (e TcpProxy_WeightedCluster_ClusterWeightValidationError) Field() string { return e.field } + +// Reason function returns reason value. +func (e TcpProxy_WeightedCluster_ClusterWeightValidationError) Reason() string { return e.reason } + +// Cause function returns cause value. +func (e TcpProxy_WeightedCluster_ClusterWeightValidationError) Cause() error { return e.cause } + +// Key function returns key value. +func (e TcpProxy_WeightedCluster_ClusterWeightValidationError) Key() bool { return e.key } + +// ErrorName returns error name. +func (e TcpProxy_WeightedCluster_ClusterWeightValidationError) ErrorName() string { + return "TcpProxy_WeightedCluster_ClusterWeightValidationError" +} + +// Error satisfies the builtin error interface +func (e TcpProxy_WeightedCluster_ClusterWeightValidationError) Error() string { + cause := "" + if e.cause != nil { + cause = fmt.Sprintf(" | caused by: %v", e.cause) + } + + key := "" + if e.key { + key = "key for " + } + + return fmt.Sprintf( + "invalid %sTcpProxy_WeightedCluster_ClusterWeight.%s: %s%s", + key, + e.field, + e.reason, + cause) +} + +var _ error = TcpProxy_WeightedCluster_ClusterWeightValidationError{} + +var _ interface { + Field() string + Reason() string + Key() bool + Cause() error + ErrorName() string +} = TcpProxy_WeightedCluster_ClusterWeightValidationError{} diff --git a/vendor/github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/tcp_proxy/v3/tcp_proxy_vtproto.pb.go b/vendor/github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/tcp_proxy/v3/tcp_proxy_vtproto.pb.go new file mode 100644 index 0000000000..25505563c1 --- /dev/null +++ b/vendor/github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/tcp_proxy/v3/tcp_proxy_vtproto.pb.go @@ -0,0 +1,1128 @@ +//go:build vtprotobuf +// +build vtprotobuf + +// Code generated by protoc-gen-go-vtproto. DO NOT EDIT. +// source: envoy/extensions/filters/network/tcp_proxy/v3/tcp_proxy.proto + +package tcp_proxyv3 + +import ( + protohelpers "github.com/planetscale/vtprotobuf/protohelpers" + durationpb "github.com/planetscale/vtprotobuf/types/known/durationpb" + wrapperspb "github.com/planetscale/vtprotobuf/types/known/wrapperspb" + proto "google.golang.org/protobuf/proto" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +func (m *TcpProxy_WeightedCluster_ClusterWeight) MarshalVTStrict() (dAtA []byte, err error) { + if m == nil { + return nil, nil + } + size := m.SizeVT() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBufferVTStrict(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *TcpProxy_WeightedCluster_ClusterWeight) MarshalToVTStrict(dAtA []byte) (int, error) { + size := m.SizeVT() + return m.MarshalToSizedBufferVTStrict(dAtA[:size]) +} + +func (m *TcpProxy_WeightedCluster_ClusterWeight) MarshalToSizedBufferVTStrict(dAtA []byte) (int, error) { + if m == nil { + return 0, nil + } + i := len(dAtA) + _ = i + var l int + _ = l + if m.unknownFields != nil { + i -= len(m.unknownFields) + copy(dAtA[i:], m.unknownFields) + } + if m.MetadataMatch != nil { + if vtmsg, ok := interface{}(m.MetadataMatch).(interface { + MarshalToSizedBufferVTStrict([]byte) (int, error) + }); ok { + size, err := vtmsg.MarshalToSizedBufferVTStrict(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) + } else { + encoded, err := proto.Marshal(m.MetadataMatch) + if err != nil { + return 0, err + } + i -= len(encoded) + copy(dAtA[i:], encoded) + i = protohelpers.EncodeVarint(dAtA, i, uint64(len(encoded))) + } + i-- + dAtA[i] = 0x1a + } + if m.Weight != 0 { + i = protohelpers.EncodeVarint(dAtA, i, uint64(m.Weight)) + i-- + dAtA[i] = 0x10 + } + if len(m.Name) > 0 { + i -= len(m.Name) + copy(dAtA[i:], m.Name) + i = protohelpers.EncodeVarint(dAtA, i, uint64(len(m.Name))) + i-- + dAtA[i] = 0xa + } + return len(dAtA) - i, nil +} + +func (m *TcpProxy_WeightedCluster) MarshalVTStrict() (dAtA []byte, err error) { + if m == nil { + return nil, nil + } + size := m.SizeVT() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBufferVTStrict(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *TcpProxy_WeightedCluster) MarshalToVTStrict(dAtA []byte) (int, error) { + size := m.SizeVT() + return m.MarshalToSizedBufferVTStrict(dAtA[:size]) +} + +func (m *TcpProxy_WeightedCluster) MarshalToSizedBufferVTStrict(dAtA []byte) (int, error) { + if m == nil { + return 0, nil + } + i := len(dAtA) + _ = i + var l int + _ = l + if m.unknownFields != nil { + i -= len(m.unknownFields) + copy(dAtA[i:], m.unknownFields) + } + if len(m.Clusters) > 0 { + for iNdEx := len(m.Clusters) - 1; iNdEx >= 0; iNdEx-- { + size, err := m.Clusters[iNdEx].MarshalToSizedBufferVTStrict(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) + i-- + dAtA[i] = 0xa + } + } + return len(dAtA) - i, nil +} + +func (m *TcpProxy_TunnelingConfig) MarshalVTStrict() (dAtA []byte, err error) { + if m == nil { + return nil, nil + } + size := m.SizeVT() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBufferVTStrict(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *TcpProxy_TunnelingConfig) MarshalToVTStrict(dAtA []byte) (int, error) { + size := m.SizeVT() + return m.MarshalToSizedBufferVTStrict(dAtA[:size]) +} + +func (m *TcpProxy_TunnelingConfig) MarshalToSizedBufferVTStrict(dAtA []byte) (int, error) { + if m == nil { + return 0, nil + } + i := len(dAtA) + _ = i + var l int + _ = l + if m.unknownFields != nil { + i -= len(m.unknownFields) + copy(dAtA[i:], m.unknownFields) + } + if len(m.Formatters) > 0 { + for iNdEx := len(m.Formatters) - 1; iNdEx >= 0; iNdEx-- { + if vtmsg, ok := interface{}(m.Formatters[iNdEx]).(interface { + MarshalToSizedBufferVTStrict([]byte) (int, error) + }); ok { + size, err := vtmsg.MarshalToSizedBufferVTStrict(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) + } else { + encoded, err := proto.Marshal(m.Formatters[iNdEx]) + if err != nil { + return 0, err + } + i -= len(encoded) + copy(dAtA[i:], encoded) + i = protohelpers.EncodeVarint(dAtA, i, uint64(len(encoded))) + } + i-- + dAtA[i] = 0x52 + } + } + if len(m.RequestIdMetadataKey) > 0 { + i -= len(m.RequestIdMetadataKey) + copy(dAtA[i:], m.RequestIdMetadataKey) + i = protohelpers.EncodeVarint(dAtA, i, uint64(len(m.RequestIdMetadataKey))) + i-- + dAtA[i] = 0x4a + } + if len(m.RequestIdHeader) > 0 { + i -= len(m.RequestIdHeader) + copy(dAtA[i:], m.RequestIdHeader) + i = protohelpers.EncodeVarint(dAtA, i, uint64(len(m.RequestIdHeader))) + i-- + dAtA[i] = 0x42 + } + if m.RequestIdExtension != nil { + if vtmsg, ok := interface{}(m.RequestIdExtension).(interface { + MarshalToSizedBufferVTStrict([]byte) (int, error) + }); ok { + size, err := vtmsg.MarshalToSizedBufferVTStrict(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) + } else { + encoded, err := proto.Marshal(m.RequestIdExtension) + if err != nil { + return 0, err + } + i -= len(encoded) + copy(dAtA[i:], encoded) + i = protohelpers.EncodeVarint(dAtA, i, uint64(len(encoded))) + } + i-- + dAtA[i] = 0x3a + } + if m.PropagateResponseTrailers { + i-- + if m.PropagateResponseTrailers { + dAtA[i] = 1 + } else { + dAtA[i] = 0 + } + i-- + dAtA[i] = 0x30 + } + if len(m.PostPath) > 0 { + i -= len(m.PostPath) + copy(dAtA[i:], m.PostPath) + i = protohelpers.EncodeVarint(dAtA, i, uint64(len(m.PostPath))) + i-- + dAtA[i] = 0x2a + } + if m.PropagateResponseHeaders { + i-- + if m.PropagateResponseHeaders { + dAtA[i] = 1 + } else { + dAtA[i] = 0 + } + i-- + dAtA[i] = 0x20 + } + if len(m.HeadersToAdd) > 0 { + for iNdEx := len(m.HeadersToAdd) - 1; iNdEx >= 0; iNdEx-- { + if vtmsg, ok := interface{}(m.HeadersToAdd[iNdEx]).(interface { + MarshalToSizedBufferVTStrict([]byte) (int, error) + }); ok { + size, err := vtmsg.MarshalToSizedBufferVTStrict(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) + } else { + encoded, err := proto.Marshal(m.HeadersToAdd[iNdEx]) + if err != nil { + return 0, err + } + i -= len(encoded) + copy(dAtA[i:], encoded) + i = protohelpers.EncodeVarint(dAtA, i, uint64(len(encoded))) + } + i-- + dAtA[i] = 0x1a + } + } + if m.UsePost { + i-- + if m.UsePost { + dAtA[i] = 1 + } else { + dAtA[i] = 0 + } + i-- + dAtA[i] = 0x10 + } + if len(m.Hostname) > 0 { + i -= len(m.Hostname) + copy(dAtA[i:], m.Hostname) + i = protohelpers.EncodeVarint(dAtA, i, uint64(len(m.Hostname))) + i-- + dAtA[i] = 0xa + } + return len(dAtA) - i, nil +} + +func (m *TcpProxy_OnDemand) MarshalVTStrict() (dAtA []byte, err error) { + if m == nil { + return nil, nil + } + size := m.SizeVT() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBufferVTStrict(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *TcpProxy_OnDemand) MarshalToVTStrict(dAtA []byte) (int, error) { + size := m.SizeVT() + return m.MarshalToSizedBufferVTStrict(dAtA[:size]) +} + +func (m *TcpProxy_OnDemand) MarshalToSizedBufferVTStrict(dAtA []byte) (int, error) { + if m == nil { + return 0, nil + } + i := len(dAtA) + _ = i + var l int + _ = l + if m.unknownFields != nil { + i -= len(m.unknownFields) + copy(dAtA[i:], m.unknownFields) + } + if m.Timeout != nil { + size, err := (*durationpb.Duration)(m.Timeout).MarshalToSizedBufferVTStrict(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) + i-- + dAtA[i] = 0x1a + } + if len(m.ResourcesLocator) > 0 { + i -= len(m.ResourcesLocator) + copy(dAtA[i:], m.ResourcesLocator) + i = protohelpers.EncodeVarint(dAtA, i, uint64(len(m.ResourcesLocator))) + i-- + dAtA[i] = 0x12 + } + if m.OdcdsConfig != nil { + if vtmsg, ok := interface{}(m.OdcdsConfig).(interface { + MarshalToSizedBufferVTStrict([]byte) (int, error) + }); ok { + size, err := vtmsg.MarshalToSizedBufferVTStrict(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) + } else { + encoded, err := proto.Marshal(m.OdcdsConfig) + if err != nil { + return 0, err + } + i -= len(encoded) + copy(dAtA[i:], encoded) + i = protohelpers.EncodeVarint(dAtA, i, uint64(len(encoded))) + } + i-- + dAtA[i] = 0xa + } + return len(dAtA) - i, nil +} + +func (m *TcpProxy_TcpAccessLogOptions) MarshalVTStrict() (dAtA []byte, err error) { + if m == nil { + return nil, nil + } + size := m.SizeVT() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBufferVTStrict(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *TcpProxy_TcpAccessLogOptions) MarshalToVTStrict(dAtA []byte) (int, error) { + size := m.SizeVT() + return m.MarshalToSizedBufferVTStrict(dAtA[:size]) +} + +func (m *TcpProxy_TcpAccessLogOptions) MarshalToSizedBufferVTStrict(dAtA []byte) (int, error) { + if m == nil { + return 0, nil + } + i := len(dAtA) + _ = i + var l int + _ = l + if m.unknownFields != nil { + i -= len(m.unknownFields) + copy(dAtA[i:], m.unknownFields) + } + if m.FlushAccessLogOnStart { + i-- + if m.FlushAccessLogOnStart { + dAtA[i] = 1 + } else { + dAtA[i] = 0 + } + i-- + dAtA[i] = 0x18 + } + if m.FlushAccessLogOnConnected { + i-- + if m.FlushAccessLogOnConnected { + dAtA[i] = 1 + } else { + dAtA[i] = 0 + } + i-- + dAtA[i] = 0x10 + } + if m.AccessLogFlushInterval != nil { + size, err := (*durationpb.Duration)(m.AccessLogFlushInterval).MarshalToSizedBufferVTStrict(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) + i-- + dAtA[i] = 0xa + } + return len(dAtA) - i, nil +} + +func (m *TcpProxy) MarshalVTStrict() (dAtA []byte, err error) { + if m == nil { + return nil, nil + } + size := m.SizeVT() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBufferVTStrict(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *TcpProxy) MarshalToVTStrict(dAtA []byte) (int, error) { + size := m.SizeVT() + return m.MarshalToSizedBufferVTStrict(dAtA[:size]) +} + +func (m *TcpProxy) MarshalToSizedBufferVTStrict(dAtA []byte) (int, error) { + if m == nil { + return 0, nil + } + i := len(dAtA) + _ = i + var l int + _ = l + if m.unknownFields != nil { + i -= len(m.unknownFields) + copy(dAtA[i:], m.unknownFields) + } + if m.CheckDrainClose != nil { + size, err := (*wrapperspb.BoolValue)(m.CheckDrainClose).MarshalToSizedBufferVTStrict(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) + i-- + dAtA[i] = 0x1 + i-- + dAtA[i] = 0xc2 + } + if m.ProxyProtocolTlvMergePolicy != 0 { + i = protohelpers.EncodeVarint(dAtA, i, uint64(m.ProxyProtocolTlvMergePolicy)) + i-- + dAtA[i] = 0x1 + i-- + dAtA[i] = 0xb8 + } + if m.MaxEarlyDataBytes != nil { + size, err := (*wrapperspb.UInt32Value)(m.MaxEarlyDataBytes).MarshalToSizedBufferVTStrict(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) + i-- + dAtA[i] = 0x1 + i-- + dAtA[i] = 0xb2 + } + if m.UpstreamConnectMode != 0 { + i = protohelpers.EncodeVarint(dAtA, i, uint64(m.UpstreamConnectMode)) + i-- + dAtA[i] = 0x1 + i-- + dAtA[i] = 0xa8 + } + if m.MaxDownstreamConnectionDurationJitterPercentage != nil { + if vtmsg, ok := interface{}(m.MaxDownstreamConnectionDurationJitterPercentage).(interface { + MarshalToSizedBufferVTStrict([]byte) (int, error) + }); ok { + size, err := vtmsg.MarshalToSizedBufferVTStrict(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) + } else { + encoded, err := proto.Marshal(m.MaxDownstreamConnectionDurationJitterPercentage) + if err != nil { + return 0, err + } + i -= len(encoded) + copy(dAtA[i:], encoded) + i = protohelpers.EncodeVarint(dAtA, i, uint64(len(encoded))) + } + i-- + dAtA[i] = 0x1 + i-- + dAtA[i] = 0xa2 + } + if len(m.ProxyProtocolTlvs) > 0 { + for iNdEx := len(m.ProxyProtocolTlvs) - 1; iNdEx >= 0; iNdEx-- { + if vtmsg, ok := interface{}(m.ProxyProtocolTlvs[iNdEx]).(interface { + MarshalToSizedBufferVTStrict([]byte) (int, error) + }); ok { + size, err := vtmsg.MarshalToSizedBufferVTStrict(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) + } else { + encoded, err := proto.Marshal(m.ProxyProtocolTlvs[iNdEx]) + if err != nil { + return 0, err + } + i -= len(encoded) + copy(dAtA[i:], encoded) + i = protohelpers.EncodeVarint(dAtA, i, uint64(len(encoded))) + } + i-- + dAtA[i] = 0x1 + i-- + dAtA[i] = 0x9a + } + } + if m.BackoffOptions != nil { + if vtmsg, ok := interface{}(m.BackoffOptions).(interface { + MarshalToSizedBufferVTStrict([]byte) (int, error) + }); ok { + size, err := vtmsg.MarshalToSizedBufferVTStrict(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) + } else { + encoded, err := proto.Marshal(m.BackoffOptions) + if err != nil { + return 0, err + } + i -= len(encoded) + copy(dAtA[i:], encoded) + i = protohelpers.EncodeVarint(dAtA, i, uint64(len(encoded))) + } + i-- + dAtA[i] = 0x1 + i-- + dAtA[i] = 0x92 + } + if m.AccessLogOptions != nil { + size, err := m.AccessLogOptions.MarshalToSizedBufferVTStrict(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) + i-- + dAtA[i] = 0x1 + i-- + dAtA[i] = 0x8a + } + if m.FlushAccessLogOnConnected { + i-- + if m.FlushAccessLogOnConnected { + dAtA[i] = 1 + } else { + dAtA[i] = 0 + } + i-- + dAtA[i] = 0x1 + i-- + dAtA[i] = 0x80 + } + if m.AccessLogFlushInterval != nil { + size, err := (*durationpb.Duration)(m.AccessLogFlushInterval).MarshalToSizedBufferVTStrict(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) + i-- + dAtA[i] = 0x7a + } + if m.OnDemand != nil { + size, err := m.OnDemand.MarshalToSizedBufferVTStrict(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) + i-- + dAtA[i] = 0x72 + } + if m.MaxDownstreamConnectionDuration != nil { + size, err := (*durationpb.Duration)(m.MaxDownstreamConnectionDuration).MarshalToSizedBufferVTStrict(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) + i-- + dAtA[i] = 0x6a + } + if m.TunnelingConfig != nil { + size, err := m.TunnelingConfig.MarshalToSizedBufferVTStrict(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) + i-- + dAtA[i] = 0x62 + } + if len(m.HashPolicy) > 0 { + for iNdEx := len(m.HashPolicy) - 1; iNdEx >= 0; iNdEx-- { + if vtmsg, ok := interface{}(m.HashPolicy[iNdEx]).(interface { + MarshalToSizedBufferVTStrict([]byte) (int, error) + }); ok { + size, err := vtmsg.MarshalToSizedBufferVTStrict(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) + } else { + encoded, err := proto.Marshal(m.HashPolicy[iNdEx]) + if err != nil { + return 0, err + } + i -= len(encoded) + copy(dAtA[i:], encoded) + i = protohelpers.EncodeVarint(dAtA, i, uint64(len(encoded))) + } + i-- + dAtA[i] = 0x5a + } + } + if msg, ok := m.ClusterSpecifier.(*TcpProxy_WeightedClusters); ok { + size, err := msg.MarshalToSizedBufferVTStrict(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + } + if m.MetadataMatch != nil { + if vtmsg, ok := interface{}(m.MetadataMatch).(interface { + MarshalToSizedBufferVTStrict([]byte) (int, error) + }); ok { + size, err := vtmsg.MarshalToSizedBufferVTStrict(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) + } else { + encoded, err := proto.Marshal(m.MetadataMatch) + if err != nil { + return 0, err + } + i -= len(encoded) + copy(dAtA[i:], encoded) + i = protohelpers.EncodeVarint(dAtA, i, uint64(len(encoded))) + } + i-- + dAtA[i] = 0x4a + } + if m.IdleTimeout != nil { + size, err := (*durationpb.Duration)(m.IdleTimeout).MarshalToSizedBufferVTStrict(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) + i-- + dAtA[i] = 0x42 + } + if m.MaxConnectAttempts != nil { + size, err := (*wrapperspb.UInt32Value)(m.MaxConnectAttempts).MarshalToSizedBufferVTStrict(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) + i-- + dAtA[i] = 0x3a + } + if len(m.AccessLog) > 0 { + for iNdEx := len(m.AccessLog) - 1; iNdEx >= 0; iNdEx-- { + if vtmsg, ok := interface{}(m.AccessLog[iNdEx]).(interface { + MarshalToSizedBufferVTStrict([]byte) (int, error) + }); ok { + size, err := vtmsg.MarshalToSizedBufferVTStrict(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) + } else { + encoded, err := proto.Marshal(m.AccessLog[iNdEx]) + if err != nil { + return 0, err + } + i -= len(encoded) + copy(dAtA[i:], encoded) + i = protohelpers.EncodeVarint(dAtA, i, uint64(len(encoded))) + } + i-- + dAtA[i] = 0x2a + } + } + if m.UpstreamIdleTimeout != nil { + size, err := (*durationpb.Duration)(m.UpstreamIdleTimeout).MarshalToSizedBufferVTStrict(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) + i-- + dAtA[i] = 0x22 + } + if m.DownstreamIdleTimeout != nil { + size, err := (*durationpb.Duration)(m.DownstreamIdleTimeout).MarshalToSizedBufferVTStrict(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) + i-- + dAtA[i] = 0x1a + } + if msg, ok := m.ClusterSpecifier.(*TcpProxy_Cluster); ok { + size, err := msg.MarshalToSizedBufferVTStrict(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + } + if len(m.StatPrefix) > 0 { + i -= len(m.StatPrefix) + copy(dAtA[i:], m.StatPrefix) + i = protohelpers.EncodeVarint(dAtA, i, uint64(len(m.StatPrefix))) + i-- + dAtA[i] = 0xa + } + return len(dAtA) - i, nil +} + +func (m *TcpProxy_Cluster) MarshalToVTStrict(dAtA []byte) (int, error) { + size := m.SizeVT() + return m.MarshalToSizedBufferVTStrict(dAtA[:size]) +} + +func (m *TcpProxy_Cluster) MarshalToSizedBufferVTStrict(dAtA []byte) (int, error) { + i := len(dAtA) + i -= len(m.Cluster) + copy(dAtA[i:], m.Cluster) + i = protohelpers.EncodeVarint(dAtA, i, uint64(len(m.Cluster))) + i-- + dAtA[i] = 0x12 + return len(dAtA) - i, nil +} +func (m *TcpProxy_WeightedClusters) MarshalToVTStrict(dAtA []byte) (int, error) { + size := m.SizeVT() + return m.MarshalToSizedBufferVTStrict(dAtA[:size]) +} + +func (m *TcpProxy_WeightedClusters) MarshalToSizedBufferVTStrict(dAtA []byte) (int, error) { + i := len(dAtA) + if m.WeightedClusters != nil { + size, err := m.WeightedClusters.MarshalToSizedBufferVTStrict(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) + i-- + dAtA[i] = 0x52 + } else { + i = protohelpers.EncodeVarint(dAtA, i, 0) + i-- + dAtA[i] = 0x52 + } + return len(dAtA) - i, nil +} +func (m *TcpProxy_WeightedCluster_ClusterWeight) SizeVT() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + l = len(m.Name) + if l > 0 { + n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } + if m.Weight != 0 { + n += 1 + protohelpers.SizeOfVarint(uint64(m.Weight)) + } + if m.MetadataMatch != nil { + if size, ok := interface{}(m.MetadataMatch).(interface { + SizeVT() int + }); ok { + l = size.SizeVT() + } else { + l = proto.Size(m.MetadataMatch) + } + n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } + n += len(m.unknownFields) + return n +} + +func (m *TcpProxy_WeightedCluster) SizeVT() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + if len(m.Clusters) > 0 { + for _, e := range m.Clusters { + l = e.SizeVT() + n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } + } + n += len(m.unknownFields) + return n +} + +func (m *TcpProxy_TunnelingConfig) SizeVT() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + l = len(m.Hostname) + if l > 0 { + n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } + if m.UsePost { + n += 2 + } + if len(m.HeadersToAdd) > 0 { + for _, e := range m.HeadersToAdd { + if size, ok := interface{}(e).(interface { + SizeVT() int + }); ok { + l = size.SizeVT() + } else { + l = proto.Size(e) + } + n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } + } + if m.PropagateResponseHeaders { + n += 2 + } + l = len(m.PostPath) + if l > 0 { + n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } + if m.PropagateResponseTrailers { + n += 2 + } + if m.RequestIdExtension != nil { + if size, ok := interface{}(m.RequestIdExtension).(interface { + SizeVT() int + }); ok { + l = size.SizeVT() + } else { + l = proto.Size(m.RequestIdExtension) + } + n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } + l = len(m.RequestIdHeader) + if l > 0 { + n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } + l = len(m.RequestIdMetadataKey) + if l > 0 { + n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } + if len(m.Formatters) > 0 { + for _, e := range m.Formatters { + if size, ok := interface{}(e).(interface { + SizeVT() int + }); ok { + l = size.SizeVT() + } else { + l = proto.Size(e) + } + n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } + } + n += len(m.unknownFields) + return n +} + +func (m *TcpProxy_OnDemand) SizeVT() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + if m.OdcdsConfig != nil { + if size, ok := interface{}(m.OdcdsConfig).(interface { + SizeVT() int + }); ok { + l = size.SizeVT() + } else { + l = proto.Size(m.OdcdsConfig) + } + n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } + l = len(m.ResourcesLocator) + if l > 0 { + n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } + if m.Timeout != nil { + l = (*durationpb.Duration)(m.Timeout).SizeVT() + n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } + n += len(m.unknownFields) + return n +} + +func (m *TcpProxy_TcpAccessLogOptions) SizeVT() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + if m.AccessLogFlushInterval != nil { + l = (*durationpb.Duration)(m.AccessLogFlushInterval).SizeVT() + n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } + if m.FlushAccessLogOnConnected { + n += 2 + } + if m.FlushAccessLogOnStart { + n += 2 + } + n += len(m.unknownFields) + return n +} + +func (m *TcpProxy) SizeVT() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + l = len(m.StatPrefix) + if l > 0 { + n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } + if vtmsg, ok := m.ClusterSpecifier.(interface{ SizeVT() int }); ok { + n += vtmsg.SizeVT() + } + if m.DownstreamIdleTimeout != nil { + l = (*durationpb.Duration)(m.DownstreamIdleTimeout).SizeVT() + n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } + if m.UpstreamIdleTimeout != nil { + l = (*durationpb.Duration)(m.UpstreamIdleTimeout).SizeVT() + n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } + if len(m.AccessLog) > 0 { + for _, e := range m.AccessLog { + if size, ok := interface{}(e).(interface { + SizeVT() int + }); ok { + l = size.SizeVT() + } else { + l = proto.Size(e) + } + n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } + } + if m.MaxConnectAttempts != nil { + l = (*wrapperspb.UInt32Value)(m.MaxConnectAttempts).SizeVT() + n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } + if m.IdleTimeout != nil { + l = (*durationpb.Duration)(m.IdleTimeout).SizeVT() + n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } + if m.MetadataMatch != nil { + if size, ok := interface{}(m.MetadataMatch).(interface { + SizeVT() int + }); ok { + l = size.SizeVT() + } else { + l = proto.Size(m.MetadataMatch) + } + n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } + if len(m.HashPolicy) > 0 { + for _, e := range m.HashPolicy { + if size, ok := interface{}(e).(interface { + SizeVT() int + }); ok { + l = size.SizeVT() + } else { + l = proto.Size(e) + } + n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } + } + if m.TunnelingConfig != nil { + l = m.TunnelingConfig.SizeVT() + n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } + if m.MaxDownstreamConnectionDuration != nil { + l = (*durationpb.Duration)(m.MaxDownstreamConnectionDuration).SizeVT() + n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } + if m.OnDemand != nil { + l = m.OnDemand.SizeVT() + n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } + if m.AccessLogFlushInterval != nil { + l = (*durationpb.Duration)(m.AccessLogFlushInterval).SizeVT() + n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } + if m.FlushAccessLogOnConnected { + n += 3 + } + if m.AccessLogOptions != nil { + l = m.AccessLogOptions.SizeVT() + n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) + } + if m.BackoffOptions != nil { + if size, ok := interface{}(m.BackoffOptions).(interface { + SizeVT() int + }); ok { + l = size.SizeVT() + } else { + l = proto.Size(m.BackoffOptions) + } + n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) + } + if len(m.ProxyProtocolTlvs) > 0 { + for _, e := range m.ProxyProtocolTlvs { + if size, ok := interface{}(e).(interface { + SizeVT() int + }); ok { + l = size.SizeVT() + } else { + l = proto.Size(e) + } + n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) + } + } + if m.MaxDownstreamConnectionDurationJitterPercentage != nil { + if size, ok := interface{}(m.MaxDownstreamConnectionDurationJitterPercentage).(interface { + SizeVT() int + }); ok { + l = size.SizeVT() + } else { + l = proto.Size(m.MaxDownstreamConnectionDurationJitterPercentage) + } + n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) + } + if m.UpstreamConnectMode != 0 { + n += 2 + protohelpers.SizeOfVarint(uint64(m.UpstreamConnectMode)) + } + if m.MaxEarlyDataBytes != nil { + l = (*wrapperspb.UInt32Value)(m.MaxEarlyDataBytes).SizeVT() + n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) + } + if m.ProxyProtocolTlvMergePolicy != 0 { + n += 2 + protohelpers.SizeOfVarint(uint64(m.ProxyProtocolTlvMergePolicy)) + } + if m.CheckDrainClose != nil { + l = (*wrapperspb.BoolValue)(m.CheckDrainClose).SizeVT() + n += 2 + l + protohelpers.SizeOfVarint(uint64(l)) + } + n += len(m.unknownFields) + return n +} + +func (m *TcpProxy_Cluster) SizeVT() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + l = len(m.Cluster) + n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + return n +} +func (m *TcpProxy_WeightedClusters) SizeVT() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + if m.WeightedClusters != nil { + l = m.WeightedClusters.SizeVT() + n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } else { + n += 2 + } + return n +} diff --git a/vendor/modules.txt b/vendor/modules.txt index c8a5f93e16..d2ef0e53b9 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -391,6 +391,8 @@ github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/router/v3 github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/set_filter_state/v3 github.com/envoyproxy/go-control-plane/envoy/extensions/filters/listener/original_dst/v3 github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/http_connection_manager/v3 +github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/set_filter_state/v3 +github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/tcp_proxy/v3 github.com/envoyproxy/go-control-plane/envoy/extensions/load_balancing_policies/client_side_weighted_round_robin/v3 github.com/envoyproxy/go-control-plane/envoy/extensions/load_balancing_policies/common/v3 github.com/envoyproxy/go-control-plane/envoy/extensions/load_balancing_policies/least_request/v3 From 7ec5a134612bd375498bdf7c4cb1fa23a4f08c8a Mon Sep 17 00:00:00 2001 From: Lucky Abolorunke <62015433+Oneimu@users.noreply.github.com> Date: Tue, 13 Oct 2026 18:14:50 +0000 Subject: [PATCH 09/42] benchmarking: append the suspend/resume phase percentiles to stats.jsonl from the runner (#2148) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## benchmarking: append the suspend/resume phase percentiles to stats.jsonl from the runner Stacked on #2009 (base branch `phase-logs-analysis` until it merges). ### Why #2009 adds the tool that turns the `Checkpoint/Restore timing breakdown` records into percentiles, but nothing in the automated runs calls it: the orchestrator deletes the worker and ate-system pods as soon as a test's runner exits, so the records never reach `stats.jsonl` or anything that reads it. The runner is the one process that holds the run's identity, owns `stats.jsonl`, and is still alive while the pods are — so it does the collection itself, the way it already collects cluster facts. ### What - **`locust/phase_breakdown.py`** (new): after locust finishes, lists the `app=atelet` pods in `ate-system` and the `ate.dev/worker-pool` pods in `benchmark-workloads` through the Kubernetes client, reads their logs since the run started (a restarted container's previous log too), parses the records with `phase_report`, and appends to `stats.jsonl`: - one row per layer / op / sandbox class / kind / scope / phase — `metric: phase______`, `measurements`: those dimensions as fields plus `count`, `p50_ms`, `p90_ms`, `p95_ms`, `max_ms`, all strings like the runner's other rows; - one `phase_breakdown_summary` row: pods read and failed, record count, and the atelet checkpoint/restore record counts next to locust's `SuspendActor`/`ResumeActor` request counts. Fewer records than requests means a node log rotated during the run; the runner's log says so. Additive like the cluster facts: a failure here never costs the locust rows. - **`locust/runner.py`**: `--phase-breakdown` (default on, `--no-phase-breakdown` to skip), called after the cluster-facts block. - **`automation/manifests/runner-job.yaml.tmpl`**: `benchmark-runner` gains `get`/`list` on `pods` and `get` on `pods/log` in both namespaces. - **`locust/Dockerfile`**: copies `phase_breakdown.py` and `analysis/phase_report.py` into the image. - **`analysis/phase_report.py`**: `parse_lines()` and `stats_rows()` for the above (the CLI is unchanged). - Docs: `analysis/README.md` "Automated runs", the flag in `benchmarking/README.md`, a pointer in the runbook. ### Testing - `python3 -m unittest discover -s benchmarking/locust/unit_tests` — 18 tests (4 new, mocked `CoreV1Api`: rows appended in shape, rotation warning, unreadable pod skipped, restarted container read twice, empty run still writes the summary, failed requests don't trigger the warning). - `python3 -m unittest discover -s benchmarking/analysis` — 22 tests. - Live: against a GKE cluster with 3 workers, after a 10-cycle glutton run, `append_phase_breakdown` read the six pods and wrote 46 phase rows plus the summary (12 atelet checkpoints vs 10 locust suspends + 2 shutdown suspends). The template's RBAC was applied and a log read impersonating `benchmark-runner` succeeded in both namespaces. --- benchmarking/README.md | 12 +- benchmarking/analysis/README.md | 42 +++- benchmarking/analysis/phase_report.py | 138 ++++++++---- benchmarking/analysis/test_phase_report.py | 92 ++++++-- .../automation/manifests/runner-job.yaml.tmpl | 35 ++- benchmarking/locust/Dockerfile | 2 + benchmarking/locust/phase_breakdown.py | 203 ++++++++++++++++++ benchmarking/locust/runner.py | 32 +++ .../locust/unit_tests/test_phase_breakdown.py | 169 +++++++++++++++ docs/dev/suspend-resume-phase-breakdown.md | 19 +- 10 files changed, 670 insertions(+), 74 deletions(-) create mode 100644 benchmarking/locust/phase_breakdown.py create mode 100644 benchmarking/locust/unit_tests/test_phase_breakdown.py diff --git a/benchmarking/README.md b/benchmarking/README.md index 015964bc58..904a43bd40 100644 --- a/benchmarking/README.md +++ b/benchmarking/README.md @@ -88,7 +88,7 @@ not a local entry point. See [automation/README.md](automation/README.md). python3 runner.py -f tests/.py -t 1m -u 1 --name --dest /tmp/bench ``` -Three flags control the optional post-run measurements described in +Four flags control the optional post-run measurements described in [Benchmark output files](#benchmark-output-files): * `--cluster-facts` / `--no-cluster-facts`: read node capacity and worker pod @@ -99,6 +99,11 @@ Three flags control the optional post-run measurements described in [Optional: Prometheus + Grafana](#optional-prometheus--grafana). * `--atelet-lag-s`: how long to wait after the run before reading the atelet's snapshot metrics. Defaults to 70. +* `--phase-breakdown` / `--no-phase-breakdown`: read the atelet and worker pod + logs once the run ends and add the suspend/resume phase percentiles to + `stats.jsonl`. On by default; needs `pods/log` access in `ate-system` and + `benchmark-workloads` (granted in `automation/manifests/runner-job.yaml.tmpl`). + See [analysis/README.md](analysis/README.md#automated-runs). Test-specific flags are appended to the same command; see the sections below. @@ -382,7 +387,10 @@ them are checked into the repository. * `logs.txt`, `traces.txt`: the runner log, and the trace IDs seen during the run. * `stats.jsonl`: one JSON object per line, one per metric. Every row carries the same five keys: `timestamp`, `tag`, `test_name`, `metric`, and a flat - `measurements` map holding that metric's numbers. + `measurements` map holding that metric's numbers. The `phase_*` rows and + the `phase_breakdown_summary` row are the suspend/resume phase percentiles + read from the node logs (see + [analysis/README.md](analysis/README.md#automated-runs)). * `server_summary.json`: server-side telemetry harvested from Prometheus, including the per-sample bin-packing timeseries. diff --git a/benchmarking/analysis/README.md b/benchmarking/analysis/README.md index 957b0f9894..e5c0ca043c 100644 --- a/benchmarking/analysis/README.md +++ b/benchmarking/analysis/README.md @@ -12,8 +12,8 @@ Two joinable JSON log records feed it, each written by both layers: | record (`msg`) | emitter | keys | |---|---|---| -| `Restore timing breakdown` | atelet and ateom-microvm | `ate.actor.restore.duration.` / `ateom.actor.restore.duration.` | -| `Checkpoint timing breakdown` | atelet and ateom-microvm | `ate.actor.checkpoint.duration.` / `ateom.actor.checkpoint.duration.` | +| `Restore timing breakdown` | atelet, ateom-microvm and ateom-gvisor | `ate.actor.restore.duration.` / `ateom.actor.restore.duration.` | +| `Checkpoint timing breakdown` | atelet, ateom-microvm and ateom-gvisor | `ate.actor.checkpoint.duration.` / `ateom.actor.checkpoint.duration.` | Every record carries the full actor identity (`ate.actor.uid`, name, atespace, template) and the snapshot scope, which the histograms are barred @@ -74,13 +74,18 @@ A failed atelet operation still writes its record, marked with `error.type` **Phase percentiles.** Per layer, operation, sandbox class, snapshot kind, scope and phase: count, p50, p90, p95 and max. A `golden` restore downloads the golden image and a `latest` one the actor's own, so they are separate -rows, as are gVisor and micro-VM checkpoints. The atelet rows split a checkpoint between -`sandbox_assets`, `ateom_checkpoint` and `persist`, and a restore between -`volume_mount`, `manifest_fetch`, `sandbox_assets`, `download`, `oci_unpack` -and `ateom_restore`. The ateom rows split the `ateom_*` phase further: +rows, as are gVisor and micro-VM operations. The atelet rows split a +checkpoint between `sandbox_assets`, `ateom_checkpoint` and `persist`, and a +restore between `volume_mount`, `manifest_fetch`, `sandbox_assets`, +`download`, `oci_unpack` and `ateom_restore`. The ateom rows split the +`ateom_*` phase further with the runtime's own phases: for micro-VM `prep` / `pause` / `snapshot` / `durable_dir` / `rootfs_upper` / `teardown` -for a checkpoint, `prep` / `bundles` / `upper_join` / `lowers` / `tap` / -`vmm_launch` / `vm_restore` / `resume` / `wakeup_probe` for a restore. +on a checkpoint and `prep` / … / `vm_restore` / `wakeup_probe` on a restore; +for gVisor `pause` / `checkpoint` / `durable_dir` / `resume` / `teardown` and +`prep` / `net_setup` / … / `pause_restore` / `app_restore` / `wakeup_probe` / +`activate` (the full lists are in `cmd/ateom-*/phaselog.go`). The ateom +records carry no sandbox class or kind of their own; a paired one takes both +from its atelet record, so both layers split into the same rows. Concurrency matters when reading them: the atelet restore phases overlap (the download runs alongside the asset fetch and OCI unpack), the three @@ -107,6 +112,27 @@ every time are systematic; different phases each time are environmental. The parser is deliberately tolerant: it scans any line for a JSON object and matches on `msg`, so raw `kubectl logs` dumps (even with prefixes) work. +## Automated runs + +The locust runner (`benchmarking/locust/runner.py`) does the collection +itself at the end of a headless run: `phase_breakdown.py` reads the atelet +and worker pod logs through the Kubernetes API while the pods still exist +(the orchestrator deletes them as soon as the runner exits) and appends the +percentiles to the run's `stats.jsonl`, one row per layer / operation / +class / kind / scope / phase (`metric: phase______`; +the measurements map carries those dimensions as fields next to `count`, +`p50_ms`, `p90_ms`, `p95_ms`, `max_ms`, all as strings like the runner's +other rows). A `phase_breakdown_summary` row is always written: pods read +and failed, the record count, and the atelet record counts next to locust's +`SuspendActor` / `ResumeActor` request and failure counts. More records than +requests is normal (boomer suspends its actors on shutdown); fewer records +than successful requests means records were lost, to pods that could not be +read or to a node log rotated during the run, and the runner's log says +which. `--no-phase-breakdown` +skips it. The runner's service account needs `list` on `pods` and `get` on +`pods/log` in `ate-system` and `benchmark-workloads` +(`automation/manifests/runner-job.yaml.tmpl`). + ## Tests ```bash diff --git a/benchmarking/analysis/phase_report.py b/benchmarking/analysis/phase_report.py index 962046f793..3d0e4d2f92 100755 --- a/benchmarking/analysis/phase_report.py +++ b/benchmarking/analysis/phase_report.py @@ -50,6 +50,7 @@ import statistics import sys from collections import defaultdict +from collections.abc import Iterable from dataclasses import dataclass, field from datetime import datetime from pathlib import Path @@ -73,9 +74,11 @@ TEMPLATE_KEY = "ate.template.name" ERROR_TYPE_KEY = "error.type" -# Only ateom-microvm writes the ateom records, and they carry no sandbox -# class of their own; the atelet record of the same operation does. -ATEOM_SANDBOX_CLASS = "microvm" +# The ateom records carry no sandbox class or snapshot kind of their own; +# both ateoms write the same two messages, and the atelet record of the same +# operation says which runtime it was. join_layers copies them across. +MICROVM = "microvm" +GVISOR = "gvisor" # The phase every record reports, and the one the report derives: the part of # the total no logged phase accounts for. @@ -88,11 +91,14 @@ # atelet restore "volume_mount", "manifest_fetch", "sandbox_assets", "download", "oci_unpack", "ateom_restore", - # ateom restore - "prep", "bundles", "upper_join", "lowers", "tap", "vmm_launch", - "vm_restore", "resume", "wakeup_probe", + # ateom restore: micro-VM (cmd/ateom-microvm/phaselog.go) and gVisor + # (cmd/ateom-gvisor/phaselog.go), each in its own sequential order + "prep", "egress_prepare", "net_setup", "bundles", "upper_join", "lowers", + "pause_rootfs", "pause_create", "pause_restore", + "app_rootfs", "app_create", "app_restore", + "tap", "vmm_launch", "vm_restore", "resume", "wakeup_probe", "activate", # atelet + ateom checkpoint - "pause", "snapshot", "durable_dir", "rootfs_upper", + "pause", "checkpoint", "snapshot", "durable_dir", "rootfs_upper", "ateom_checkpoint", "teardown", "persist", UNATTRIBUTED, TOTAL, ] @@ -101,8 +107,9 @@ # Which atelet phase wraps the ateom record. INNER_PHASE = {"restore": "ateom_restore", "checkpoint": "ateom_checkpoint"} -# The ateom checkpoint captures that run concurrently on the paused guest: the -# paused window costs their max, not their sum. +# The micro-VM checkpoint captures that run concurrently on the paused guest: +# the paused window costs their max, not their sum. gVisor's checkpoint phases +# are sequential. CONCURRENT_CAPTURES = ("snapshot", "durable_dir", "rootfs_upper") # Slack on the window in which the ateom record of an operation must land: @@ -155,24 +162,28 @@ def parse_time(s: str) -> float | None: return None -def unattributed(source: str, op: str, phases: dict[str, float]) -> float | None: +def unattributed(source: str, op: str, sandbox_class: str, phases: dict[str, float]) -> float | None: """The total minus what the logged phases account for, where that is - well-defined: the atelet checkpoint phases are sequential; the ateom - checkpoint is prep, pause, the concurrent captures (max), then teardown. - The atelet restore phases overlap by design (download runs alongside the - asset fetch and OCI unpack) and the ateom restore phases partition their - total by construction, so neither gets a residual. The phases are - sub-intervals of the total, so a negative result is a bug in the emitter - or in this formula; it is not clamped, so that it shows.""" + well-defined: the atelet checkpoint phases are sequential; the micro-VM + ateom checkpoint is prep, pause, the concurrent captures (max), then + teardown; the gVisor ateom checkpoint phases are all sequential. The + atelet restore phases overlap by design (download runs alongside the + asset fetch and OCI unpack) and both ateoms' restore phases partition + their total by construction, so neither gets a residual; nor does an + ateom checkpoint whose runtime is unknown (its atelet record is missing). + The phases are sub-intervals of the total, so a negative result is a bug + in the emitter or in this formula; it is not clamped, so that it shows.""" total = phases.get(TOTAL) if total is None: return None if (source, op) == ("atelet", "checkpoint"): spent = sum(phases.get(p, 0.0) for p in ("sandbox_assets", "ateom_checkpoint", "persist")) - elif (source, op) == ("ateom", "checkpoint"): + elif (source, op, sandbox_class) == ("ateom", "checkpoint", MICROVM): spent = (phases.get("prep", 0.0) + phases.get("pause", 0.0) + max(phases.get(p, 0.0) for p in CONCURRENT_CAPTURES) + phases.get("teardown", 0.0)) + elif (source, op, sandbox_class) == ("ateom", "checkpoint", GVISOR): + spent = sum(v for k, v in phases.items() if k != TOTAL) else: return None return total - spent @@ -200,9 +211,6 @@ def parse_line(obj: dict, out: Parsed) -> None: if not phases: continue time_s = obj.get("time", "") - residual = unattributed(source, op, phases) - if residual is not None: - phases[UNATTRIBUTED] = residual out.breakdowns.append(Breakdown( source=source, op=op, @@ -210,8 +218,7 @@ def parse_line(obj: dict, out: Parsed) -> None: ts=parse_time(time_s), actor_uid=obj.get(ACTOR_UID_KEY, ""), template=obj.get(TEMPLATE_KEY, ""), - sandbox_class=obj.get(SANDBOX_CLASS_KEY, "") - or (ATEOM_SANDBOX_CLASS if source == "ateom" else ""), + sandbox_class=obj.get(SANDBOX_CLASS_KEY, ""), scope=obj.get(SCOPE_KEY, ""), kind=obj.get(KIND_KEY, ""), phases=phases, @@ -221,6 +228,27 @@ def parse_line(obj: dict, out: Parsed) -> None: return +def parse_lines(lines: Iterable[str], out: Parsed | None = None) -> Parsed: + """Parse one pod log, line by line, into out (a new Parsed by default). + This is also the entry point for a caller that already holds the log + text, such as the locust runner reading pods through the Kubernetes API.""" + out = Parsed() if out is None else out + for line in lines: + # kubectl log dumps may prefix each line (pod name, timestamp); + # recover the JSON object from the first brace. + brace = line.find("{") + if brace < 0: + continue + out.lines_seen += 1 + try: + obj = json.loads(line[brace:]) + except json.JSONDecodeError: + continue + if isinstance(obj, dict): + parse_line(obj, out) + return out + + def parse_files(paths: list[str]) -> Parsed: out = Parsed() for path in paths: @@ -245,19 +273,7 @@ def parse_files(paths: list[str]) -> Parsed: out.lines_seen += 1 parse_line(obj, out) continue - for line in text.splitlines(): - # kubectl log dumps may prefix each line (pod name, timestamp); - # recover the JSON object from the first brace. - brace = line.find("{") - if brace < 0: - continue - out.lines_seen += 1 - try: - obj = json.loads(line[brace:]) - except json.JSONDecodeError: - continue - if isinstance(obj, dict): - parse_line(obj, out) + parse_lines(text.splitlines(), out) return out @@ -341,7 +357,8 @@ def pair_records(breakdowns: list[Breakdown]) -> dict[int, Breakdown]: operation without a match prints without one. Matching in time order with a consumed set keeps rapid cycles of one actor from sharing or swapping records. A paired ateom record also inherits the snapshot kind - its layer does not log, so the two layers' percentiles split alike.""" + and sandbox class its layer does not log, so the two layers' percentiles + split alike and the residual knows which runtime's rule applies.""" by_actor: dict[tuple, list[Breakdown]] = defaultdict(list) for b in breakdowns: if b.source == "ateom" and b.ts is not None: @@ -367,6 +384,20 @@ def pair_records(breakdowns: list[Breakdown]) -> dict[int, Breakdown]: pairs[id(op)] = inner if not inner.kind: inner.kind = op.kind + if not inner.sandbox_class: + inner.sandbox_class = op.sandbox_class + return pairs + + +def join_layers(breakdowns: list[Breakdown]) -> dict[int, Breakdown]: + """Pair the layers' records, then derive each record's unattributed + residual, which for an ateom checkpoint depends on the runtime the pairing + just identified. Returns the pairs, as pair_records does.""" + pairs = pair_records(breakdowns) + for b in breakdowns: + residual = unattributed(b.source, b.op, b.sandbox_class, b.phases) + if residual is not None: + b.phases[UNATTRIBUTED] = residual return pairs @@ -376,7 +407,7 @@ def report_waterfalls(breakdowns: list[Breakdown], writer, slowest: int, ateom_* phase, and the gap between that phase and the ateom total (RPC and queueing between the layers).""" if pairs is None: - pairs = pair_records(breakdowns) + pairs = join_layers(breakdowns) atelet = [b for b in breakdowns if b.source == "atelet" and not b.failed] atelet.sort(key=lambda b: b.phases.get(TOTAL, 0), reverse=True) @@ -402,6 +433,30 @@ def report_waterfalls(breakdowns: list[Breakdown], writer, slowest: int, writer(f" (gap) {'rpc/queueing':13} {fmt_s(gap)}") +def stats_rows(rows: list[dict], timestamp: str, tag: str, test_name: str) -> list[dict]: + """report_phases() rows in the shape of the benchmark runner's stats.jsonl + (benchmarking/locust/runner.py): one object per metric with the run's + timestamp, tag and test name, and a flat, string-valued measurements map, + as the runner's other writers emit. The metric name carries the row's + dimensions so a dashboard can select a phase the way it selects a locust + request type; the dimensions are also fields of the map, since phase + names contain underscores and the name cannot be split back apart.""" + out = [] + for r in rows: + dims = {k: (r[k] if r[k] and r[k] != "-" else "none") + for k in ("layer", "op", "class", "kind", "scope", "phase")} + metric = "_".join(("phase", *dims.values())) + measurements = {**dims, **{k: r[k] for k in ("count", "p50_ms", "p90_ms", "p95_ms", "max_ms")}} + out.append({ + "timestamp": timestamp, + "tag": tag, + "test_name": test_name, + "metric": re.sub(r"[^a-z0-9_]+", "_", metric.lower()), + "measurements": {k: str(v) for k, v in measurements.items()}, + }) + return out + + def write_csv(dest: Path, name: str, rows: list[dict]) -> None: if not rows: return @@ -434,9 +489,10 @@ def main() -> int: print("no matching records; are these the atelet and worker pod logs?", file=sys.stderr) return 1 - # Pair before the percentiles: a paired ateom record takes its kind from - # the atelet record, so both layers' rows split the same way. - pairs = pair_records(parsed.breakdowns) + # Pair before the percentiles: a paired ateom record takes its kind and + # sandbox class from the atelet record, so both layers' rows split the + # same way and each checkpoint's residual uses its runtime's rule. + pairs = join_layers(parsed.breakdowns) print() phase_rows = report_phases(parsed.breakdowns, print) report_waterfalls(parsed.breakdowns, print, args.slowest, pairs) diff --git a/benchmarking/analysis/test_phase_report.py b/benchmarking/analysis/test_phase_report.py index 45e61169d6..0ad518f77b 100644 --- a/benchmarking/analysis/test_phase_report.py +++ b/benchmarking/analysis/test_phase_report.py @@ -162,11 +162,19 @@ def test_cloud_logging_export_file_is_a_json_array(self): out = phase_report.parse_files([path]) self.assertEqual((out.lines_seen, len(out.breakdowns)), (2, 2)) - def test_ateom_records_default_to_the_microvm_class(self): - out = parse(ATEOM_RESTORE, ATELET_RESTORE) - self.assertEqual(out.breakdowns[0].sandbox_class, "microvm") - self.assertEqual(out.breakdowns[1].sandbox_class, "") # not on this fixture - + def test_ateom_records_take_class_and_kind_from_the_paired_atelet_record(self): + atelet = dict(ATELET_RESTORE, **{"ate.sandbox.class": "gvisor"}) + out = parse(ATEOM_RESTORE, atelet) + self.assertEqual(out.breakdowns[0].sandbox_class, "") # not on the ateom record + phase_report.join_layers(out.breakdowns) + self.assertEqual((out.breakdowns[0].sandbox_class, out.breakdowns[0].kind), ("gvisor", "latest")) + + def test_an_unpaired_ateom_record_keeps_no_class(self): + out = parse(ATEOM_RESTORE) + phase_report.join_layers(out.breakdowns) + self.assertEqual(out.breakdowns[0].sandbox_class, "") + rows = phase_report.report_phases(out.breakdowns, lambda _: None) + self.assertEqual({r["class"] for r in rows}, {"-"}) def test_non_numeric_duration_is_skipped_not_fatal(self): rec = dict(ATELET_RESTORE, **{"ate.actor.restore.duration.download": None, "ate.actor.restore.duration.oci_unpack": "fast"}) @@ -201,6 +209,12 @@ def test_bracket_prefixed_dump_is_read_line_by_line(self): self.assertEqual(len(out.breakdowns), 2) self.assertIn("reading it line by line", err.getvalue()) + def test_parse_lines_feeds_an_existing_parse(self): + out = phase_report.parse_lines([json.dumps(ATELET_RESTORE), "not json", ""]) + self.assertEqual((out.lines_seen, len(out.breakdowns)), (1, 1)) + phase_report.parse_lines(["pod/x " + json.dumps(ATEOM_RESTORE)], out) + self.assertEqual(len(out.breakdowns), 2) + def test_prefixed_kubectl_lines_still_parse(self): with tempfile.TemporaryDirectory() as d: path = os.path.join(d, "pod.log") @@ -213,25 +227,58 @@ def test_prefixed_kubectl_lines_still_parse(self): class UnattributedTest(unittest.TestCase): def test_atelet_checkpoint_residual_is_total_minus_sequential_phases(self): - b = parse(ATELET_CHECKPOINT).breakdowns[0] - self.assertAlmostEqual(b.phases["unattributed"], 6.0 - (0.01 + 1.14 + 4.48)) - - def test_ateom_checkpoint_counts_the_concurrent_captures_once(self): - b = parse(ATEOM_CHECKPOINT).breakdowns[0] + out = parse(ATELET_CHECKPOINT) + phase_report.join_layers(out.breakdowns) + self.assertAlmostEqual(out.breakdowns[0].phases["unattributed"], 6.0 - (0.01 + 1.14 + 4.48)) + def test_microvm_ateom_checkpoint_counts_the_concurrent_captures_once(self): # prep + pause + max(snapshot, rootfs_upper) + teardown; the two # captures overlapped, so only the slower one is spent wall time. - self.assertAlmostEqual(b.phases["unattributed"], 1.2 - (0.04 + 0.01 + 0.9 + 0.2)) - + phases = {k[len("ateom.actor.checkpoint.duration."):]: v + for k, v in ATEOM_CHECKPOINT.items() if k.startswith("ateom.actor.checkpoint.duration.")} + got = phase_report.unattributed("ateom", "checkpoint", "microvm", phases) + self.assertAlmostEqual(got, 1.2 - (0.04 + 0.01 + 0.9 + 0.2)) + + def test_gvisor_ateom_checkpoint_phases_are_sequential(self): + phases = {"prep": 0.01, "pause": 0.02, "checkpoint": 0.15, "durable_dir": 0.03, + "resume": 0.02, "teardown": 0.05, "total": 0.30} + got = phase_report.unattributed("ateom", "checkpoint", "gvisor", phases) + self.assertAlmostEqual(got, 0.30 - (0.01 + 0.02 + 0.15 + 0.03 + 0.02 + 0.05)) + + def test_ateom_checkpoint_of_unknown_runtime_gets_no_residual(self): + out = parse(ATEOM_CHECKPOINT) # no atelet partner, so no class + phase_report.join_layers(out.breakdowns) + self.assertNotIn("unattributed", out.breakdowns[0].phases) + + def test_gvisor_pair_splits_both_layers_by_class(self): + atelet = dict(ATELET_CHECKPOINT, **{"ate.sandbox.class": "gvisor"}) + ateom = { + "time": "2026-09-23T10:00:55.000000000Z", "msg": "Checkpoint timing breakdown", + "ate.actor.uid": "uid-1", "ate.template.name": "swebench-astropy-7336", "ate.snapshot.scope": "full", + "ateom.actor.checkpoint.duration.pause": 0.02, + "ateom.actor.checkpoint.duration.checkpoint": 0.15, + "ateom.actor.checkpoint.duration.teardown": 0.05, + "ateom.actor.checkpoint.duration.total": 0.25, + } + out = parse(ateom, atelet) + phase_report.join_layers(out.breakdowns) + rows = phase_report.report_phases(out.breakdowns, lambda _: None) + ateom_rows = {r["phase"]: r for r in rows if r["layer"] == "ateom"} + self.assertEqual({r["class"] for r in ateom_rows.values()}, {"gvisor"}) + self.assertEqual({r["kind"] for r in ateom_rows.values()}, {"latest"}) + self.assertAlmostEqual(ateom_rows["unattributed"]["p50_ms"], (0.25 - 0.22) * 1000) + # gVisor's phases sort in their own order, before the shared tail. + order = [r["phase"] for r in rows if r["layer"] == "ateom"] + self.assertEqual(order, ["pause", "checkpoint", "teardown", "unattributed", "total"]) def test_restore_layers_get_no_residual(self): out = parse(ATELET_RESTORE, ATEOM_RESTORE) + phase_report.join_layers(out.breakdowns) for b in out.breakdowns: self.assertNotIn("unattributed", b.phases) - def test_negative_residual_is_not_clamped(self): # Cannot happen on a well-formed record; if it does, it must show. - rec = dict(ATELET_CHECKPOINT, **{"ate.actor.checkpoint.duration.total": 1.0}) - self.assertAlmostEqual(parse(rec).breakdowns[0].phases["unattributed"], 1.0 - (0.01 + 1.14 + 4.48)) - + phases = {"sandbox_assets": 0.01, "ateom_checkpoint": 1.14, "persist": 4.48, "total": 1.0} + self.assertAlmostEqual(phase_report.unattributed("atelet", "checkpoint", "microvm", phases), + 1.0 - (0.01 + 1.14 + 4.48)) class ReportTest(unittest.TestCase): def test_failed_records_are_flagged_and_excluded_from_percentiles(self): @@ -285,6 +332,19 @@ def test_waterfall_ignores_an_ateom_record_from_another_cycle(self): self.assertNotIn("ateom vm_restore", text) self.assertNotIn("(gap)", text) + def test_stats_rows_take_the_runner_jsonl_shape(self): + rows = phase_report.report_phases(parse(ATELET_RESTORE).breakdowns, lambda _: None) + entries = phase_report.stats_rows(rows, "2026-09-23T10:00:00Z", "abc123", "glutton_mem_1gi") + download = next(e for e in entries if e["metric"].endswith("_download")) + self.assertEqual(download["metric"], "phase_atelet_restore_none_latest_full_download") + self.assertEqual((download["timestamp"], download["tag"], download["test_name"]), + ("2026-09-23T10:00:00Z", "abc123", "glutton_mem_1gi")) + # String values and the dimensions as fields, like the runner's rows. + self.assertEqual(download["measurements"], { + "layer": "atelet", "op": "restore", "class": "none", "kind": "latest", + "scope": "full", "phase": "download", + "count": "1", "p50_ms": "2400.0", "p90_ms": "2400.0", "p95_ms": "2400.0", "max_ms": "2400.0"}) + def test_waterfall_nests_ateom_and_gap_under_atelet(self): out = parse(ATEOM_RESTORE, ATELET_RESTORE) text = render(phase_report.report_waterfalls, out.breakdowns, slowest=1) diff --git a/benchmarking/automation/manifests/runner-job.yaml.tmpl b/benchmarking/automation/manifests/runner-job.yaml.tmpl index 4ec02c7bb0..9267de1219 100644 --- a/benchmarking/automation/manifests/runner-job.yaml.tmpl +++ b/benchmarking/automation/manifests/runner-job.yaml.tmpl @@ -72,7 +72,40 @@ metadata: rules: - apiGroups: [""] resources: ["pods"] - verbs: ["list"] + # get as well as list: the runner reads the worker pod logs after a test + # for the suspend/resume phase breakdown (phase_breakdown.py). + verbs: ["get", "list"] +- apiGroups: [""] + resources: ["pods/log"] + verbs: ["get"] +--- +# The atelet pod logs hold the other half of the phase breakdown. +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: benchmark-runner-atelet-logs + namespace: ate-system +rules: +- apiGroups: [""] + resources: ["pods"] + verbs: ["get", "list"] +- apiGroups: [""] + resources: ["pods/log"] + verbs: ["get"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: benchmark-runner-atelet-logs + namespace: ate-system +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: benchmark-runner-atelet-logs +subjects: +- kind: ServiceAccount + name: benchmark-runner + namespace: benchmarking --- apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding diff --git a/benchmarking/locust/Dockerfile b/benchmarking/locust/Dockerfile index 2fadab5f80..67791e75fc 100644 --- a/benchmarking/locust/Dockerfile +++ b/benchmarking/locust/Dockerfile @@ -58,6 +58,8 @@ COPY benchmarking/locust/tests/ /app/tests/ COPY benchmarking/locust/runner.py /app/runner.py COPY benchmarking/locust/cluster_facts.py /app/cluster_facts.py COPY benchmarking/locust/server_telemetry.py /app/server_telemetry.py +COPY benchmarking/locust/phase_breakdown.py /app/phase_breakdown.py +COPY benchmarking/analysis/phase_report.py /app/phase_report.py ENV PYTHONPATH=/app:/app/deps ENV PYTHONUNBUFFERED=1 diff --git a/benchmarking/locust/phase_breakdown.py b/benchmarking/locust/phase_breakdown.py new file mode 100644 index 0000000000..ea2953e67c --- /dev/null +++ b/benchmarking/locust/phase_breakdown.py @@ -0,0 +1,203 @@ +# 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. + +"""Suspend/resume phase percentiles for stats.jsonl, read from the node logs. + +atelet and ateom-microvm write one `Checkpoint timing breakdown` / `Restore +timing breakdown` record per operation (see benchmarking/analysis/README.md). +The orchestrator deletes the worker and ate-system pods right after a test, +so the runner reads those pod logs itself, through the Kubernetes API it +already uses for cluster facts, while the pods still exist, and appends the +percentiles to stats.jsonl next to the SuspendActor / ResumeActor rows. + +Like the cluster facts this is additive: a failure here must never cost the +locust measurements. Pass --no-phase-breakdown to runner.py to skip it. +""" + +import csv +import json +import sys +from pathlib import Path +from typing import Any, TextIO + +from kubernetes import client + +from cluster_facts import ( + API_TIMEOUT_SECONDS, + WORKER_POOL_LABEL, + WORKER_POOL_NAMESPACE, + _load_kube_config, + _log, +) + +# In the repository phase_report.py lives in benchmarking/analysis/; the +# locust image copies it next to this file (benchmarking/locust/Dockerfile). +_ANALYSIS_DIR = Path(__file__).resolve().parent.parent / "analysis" +if _ANALYSIS_DIR.is_dir(): + sys.path.insert(0, str(_ANALYSIS_DIR)) +import phase_report # noqa: E402 + +ATELET_NAMESPACE = "ate-system" +ATELET_LABEL = "app=atelet" + +# A log body is a download, not a cached list; give it longer than the +# API_TIMEOUT_SECONDS the list calls use. +LOG_READ_TIMEOUT_SECONDS = 60 + +# The locust request names whose counts the atelet records must reach. More +# records than requests is normal: boomer suspends the actors it created when +# it shuts down, and those checkpoints are measured like any other. +LOCUST_OP_NAMES = {"checkpoint": "SuspendActor", "restore": "ResumeActor"} + + +class PodLogs: + """The lines read from one namespace's pods, and how the reads went.""" + + def __init__(self) -> None: + self.lines: list[str] = [] + self.pods_read = 0 + self.pods_failed = 0 + + +def _read_log(v1: client.CoreV1Api, name: str, namespace: str, + since_seconds: int, previous: bool = False) -> str: + # The raw response: with preloading, the client renders a text/plain body + # as the str() of bytes (one line of b'...'). + resp = v1.read_namespaced_pod_log( + name, namespace, since_seconds=since_seconds, previous=previous, + _request_timeout=LOG_READ_TIMEOUT_SECONDS, _preload_content=False, + ) + return resp.data.decode("utf-8", errors="replace") + + +def read_pod_logs(v1: client.CoreV1Api, namespace: str, label: str, + since_seconds: int, logs: TextIO | None = None) -> PodLogs: + """The log lines of every pod matching label in namespace, from + since_seconds ago. A container that restarted is read twice, its previous + log too, so a mid-run restart does not drop the records before it. A pod + that cannot be read is reported, counted and skipped.""" + out = PodLogs() + try: + pods = v1.list_namespaced_pod( + namespace=namespace, label_selector=label, + resource_version="0", _request_timeout=API_TIMEOUT_SECONDS, + ).items + except Exception as e: + _log(logs, f"Notice: could not list pods in {namespace} ({label}): " + f"{getattr(e, 'reason', e)}") + out.pods_failed += 1 + return out + for pod in pods: + name = pod.metadata.name + statuses = pod.status.container_statuses or [] + restarted = any(getattr(c, "restart_count", 0) for c in statuses) + try: + text = _read_log(v1, name, namespace, since_seconds) + except Exception as e: + _log(logs, f"Notice: could not read logs of {namespace}/{name}: " + f"{getattr(e, 'reason', e)}") + out.pods_failed += 1 + continue + if restarted: + # kubelet may have discarded the previous container's log already; + # that loses the records before the restart, not the current log. + try: + text += "\n" + _read_log(v1, name, namespace, since_seconds, previous=True) + except Exception as e: + _log(logs, f"Notice: no previous log for {namespace}/{name}: " + f"{getattr(e, 'reason', e)}") + out.pods_read += 1 + out.lines.extend(text.splitlines()) + return out + + +def locust_request_counts(stats_csv: Path) -> dict[str, tuple[int, int]]: + """SuspendActor / ResumeActor (requests, failures) from locust's stats.csv.""" + counts: dict[str, tuple[int, int]] = {} + with open(stats_csv) as f: + for row in csv.DictReader(f): + if row.get("Name") in LOCUST_OP_NAMES.values(): + counts[row["Name"]] = (int(row.get("Request Count") or 0), + int(row.get("Failure Count") or 0)) + return counts + + +def collect_phase_breakdown(v1: client.CoreV1Api, since_seconds: int, + logs: TextIO | None = None) -> tuple[list[dict], Any, PodLogs]: + """Reads the atelet and worker pod logs and aggregates the records. + Returns the percentile rows, the parse result and the read tallies.""" + parsed = phase_report.Parsed() + reads = PodLogs() + for namespace, label in ((ATELET_NAMESPACE, ATELET_LABEL), + (WORKER_POOL_NAMESPACE, WORKER_POOL_LABEL)): + got = read_pod_logs(v1, namespace, label, since_seconds, logs) + reads.pods_read += got.pods_read + reads.pods_failed += got.pods_failed + phase_report.parse_lines(got.lines, parsed) + phase_report.join_layers(parsed.breakdowns) + rows = phase_report.report_phases(parsed.breakdowns, lambda _: None) + return rows, parsed, reads + + +def append_phase_breakdown(jsonl_path: Path, stats_csv: Path, since_seconds: int, + timestamp: str, tag: str, test_name: str, + logs: TextIO | None = None) -> int: + """Appends the phase percentile rows to jsonl_path and returns how many. + + A phase_breakdown_summary row is always written once the API was + reachable, so a run with no records is distinguishable from a run where + the reads failed: it carries the pods read and failed, and the atelet + record counts next to locust's SuspendActor / ResumeActor request and + failure counts. A successful request must have reached atelet, so fewer + atelet records than successful requests means records were lost: to pods + that could not be read if there were any, else to a node log rotated + during the run. The log says which. + """ + if not _load_kube_config(logs): + return 0 + v1 = client.CoreV1Api() + rows, parsed, reads = collect_phase_breakdown(v1, since_seconds, logs) + + expected = locust_request_counts(stats_csv) if stats_csv.exists() else {} + summary: dict[str, Any] = {"records": parsed.lines_matched, + "pods_read": reads.pods_read, "pods_failed": reads.pods_failed} + for op, request_name in LOCUST_OP_NAMES.items(): + got = sum(1 for b in parsed.breakdowns if b.source == "atelet" and b.op == op) + summary[f"atelet_{op}_records"] = got + if request_name in expected: + requests, failures = expected[request_name] + summary[f"locust_{request_name}"] = requests + summary[f"locust_{request_name}_failures"] = failures + # A request that failed before reaching atelet leaves no record, + # so only the successful ones are owed one. + if got < requests - failures: + cause = (f"{reads.pods_failed} pod(s) could not be read" if reads.pods_failed + else "a node log was rotated during the run") + _log(logs, f"Warning: locust made {requests - failures} successful {request_name} " + f"requests but the atelet logs hold {got} {op} records; {cause}") + if not rows: + _log(logs, f"Warning: no timing breakdown records in the node logs " + f"({reads.pods_read} pods read, {reads.pods_failed} failed); " + f"check the runner's pods/log access if the run made requests") + + entries = phase_report.stats_rows(rows, timestamp, tag, test_name) + entries.append({"timestamp": timestamp, "tag": tag, "test_name": test_name, + "metric": "phase_breakdown_summary", + "measurements": {k: str(v) for k, v in summary.items()}}) + with open(jsonl_path, "a") as out: + for entry in entries: + out.write(json.dumps(entry) + "\n") + _log(logs, f"Appended {len(entries)} phase breakdown rows from " + f"{parsed.lines_matched} records") + return len(entries) diff --git a/benchmarking/locust/runner.py b/benchmarking/locust/runner.py index e43870ad07..9c722b5b1c 100644 --- a/benchmarking/locust/runner.py +++ b/benchmarking/locust/runner.py @@ -50,6 +50,7 @@ ) from common.boomer_config import build_config_json from server_telemetry import extract_and_record_server_telemetry +from phase_breakdown import append_phase_breakdown # Path inside the locust image to the boomer-worker binary baked in by # benchmarking/locust/Dockerfile. @@ -166,6 +167,17 @@ def parse_args() -> argparse.Namespace: "the default fits the OTel SDK's 60s default export and a 10s scrape" ), ) + p.add_argument( + "--phase-breakdown", + action=argparse.BooleanOptionalAction, + default=True, + help=( + "Read the atelet and worker pod logs after the run and append the " + "suspend/resume phase percentiles to stats.jsonl (see " + "benchmarking/analysis/README.md). Pass --no-phase-breakdown to " + "skip reading pod logs" + ), + ) args, extra = p.parse_known_args() args.locust_extra = extra return args @@ -583,6 +595,26 @@ def main() -> None: except Exception as e: tee(logs, f"Warning: Failed to harvest server telemetry: {e}") + # The phase breakdown is additive too. It reads the node logs now, + # while the pods still exist: the orchestrator deletes them as soon + # as this process exits. The window reaches back to this run's start + # with a margin for the clocks involved. + if stats_generated and args.phase_breakdown: + try: + append_phase_breakdown( + jsonl_path, + stats_csv, + int(time.time() - now.timestamp()) + 60, + data_ts, + args.tag, + args.name, + logs, + ) + except Exception as e: + tee(logs, f"Warning: Failed to record phase breakdown: {e}") + elif stats_generated: + tee(logs, "Skipping phase breakdown (--no-phase-breakdown)") + status_path.write_text( json.dumps( {"locust_exit_code": exit_code, "stats_generated": stats_generated} diff --git a/benchmarking/locust/unit_tests/test_phase_breakdown.py b/benchmarking/locust/unit_tests/test_phase_breakdown.py new file mode 100644 index 0000000000..52063ac0d2 --- /dev/null +++ b/benchmarking/locust/unit_tests/test_phase_breakdown.py @@ -0,0 +1,169 @@ +# 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. + +"""Unit tests for phase_breakdown.py. + +Run via: python3 benchmarking/locust/unit_tests/test_phase_breakdown.py +Never contacts a cluster: pod logs come from a mocked CoreV1Api. Needs the +kubernetes client: pip install -r benchmarking/locust/requirements.txt +""" + +import contextlib +import io +import json +import sys +import tempfile +import unittest +from pathlib import Path +from types import SimpleNamespace +from unittest import mock + +from kubernetes.client.rest import ApiException + +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) + +import phase_breakdown + +ATELET = json.dumps({ + "time": "2026-09-23T10:00:05.500000000Z", "msg": "Restore timing breakdown", + "ate.actor.uid": "uid-1", "ate.template.name": "glutton", "ate.sandbox.class": "microvm", + "ate.snapshot.scope": "full", "ate.snapshot.kind": "latest", + "ate.actor.restore.duration.download": 2.4, + "ate.actor.restore.duration.ateom_restore": 1.1, + "ate.actor.restore.duration.total": 3.9, +}) +ATEOM = json.dumps({ + "time": "2026-09-23T10:00:05.400000000Z", "msg": "Restore timing breakdown", + "ate.actor.uid": "uid-1", "ate.template.name": "glutton", "ate.snapshot.scope": "full", + "ateom.actor.restore.duration.vm_restore": 0.8, + "ateom.actor.restore.duration.total": 1.05, +}) +STATS_CSV = ("Type,Name,Request Count,Failure Count\n" + "grpc,SuspendActor,0,0\ngrpc,ResumeActor,1,0\n,Aggregated,1,0\n") + + +def fake_api(logs_by_namespace, broken_pod=None, restarted=False, previous_logs=None, + previous_missing=False): + """Stand-in CoreV1Api: one pod per namespace, its log from the mapping.""" + api = mock.Mock() + api.list_namespaced_pod.side_effect = lambda namespace, **_: SimpleNamespace( + items=[SimpleNamespace( + metadata=SimpleNamespace(name=f"{namespace}-pod"), + status=SimpleNamespace(container_statuses=[SimpleNamespace(restart_count=1 if restarted else 0)]))]) + + def read_log(name, namespace, previous=False, **_): + if name == broken_pod: + raise ApiException(status=400, reason="container is being created") + if previous and previous_missing: + raise ApiException(status=400, reason="previous terminated container not found") + source = (previous_logs or {}) if previous else logs_by_namespace + # The raw (un-preloaded) response the code asks for: bytes in .data. + return SimpleNamespace(data=source.get(namespace, "").encode()) + api.read_namespaced_pod_log.side_effect = read_log + return api + + +def append(api, stats=STATS_CSV): + with tempfile.TemporaryDirectory() as d: + jsonl, stats_csv = Path(d) / "stats.jsonl", Path(d) / "stats.csv" + jsonl.write_text('{"metric": "grpc_ResumeActor"}\n') + stats_csv.write_text(stats) + out = io.StringIO() + with mock.patch.object(phase_breakdown, "_load_kube_config", return_value=True), \ + mock.patch.object(phase_breakdown.client, "CoreV1Api", return_value=api), \ + contextlib.redirect_stdout(out): + n = phase_breakdown.append_phase_breakdown( + jsonl, stats_csv, 600, "2026-09-23T10:01:00Z", "abc123", "unit-run") + return n, [json.loads(line) for line in jsonl.read_text().splitlines()], out.getvalue() + + +class PhaseBreakdownTest(unittest.TestCase): + def test_rows_are_appended_after_the_locust_rows(self): + api = fake_api({"ate-system": ATELET, "benchmark-workloads": ATEOM}) + n, entries, _ = append(api) + self.assertEqual(entries[0]["metric"], "grpc_ResumeActor") # untouched + metrics = {e["metric"] for e in entries[1:]} + self.assertIn("phase_atelet_restore_microvm_latest_full_download", metrics) + # The ateom row took its kind from the paired atelet record. + self.assertIn("phase_ateom_restore_microvm_latest_full_vm_restore", metrics) + self.assertEqual(n, len(entries) - 1) + for e in entries[1:]: + self.assertEqual((e["timestamp"], e["tag"], e["test_name"]), + ("2026-09-23T10:01:00Z", "abc123", "unit-run")) + summary = next(e for e in entries if e["metric"] == "phase_breakdown_summary") + self.assertEqual(summary["measurements"]["atelet_restore_records"], "1") + self.assertEqual(summary["measurements"]["locust_ResumeActor"], "1") + self.assertEqual((summary["measurements"]["pods_read"], summary["measurements"]["pods_failed"]), ("2", "0")) + + def test_fewer_records_than_successful_requests_is_warned(self): + api = fake_api({"ate-system": ATELET, "benchmark-workloads": ATEOM}) + _, entries, output = append(api, stats=STATS_CSV.replace("ResumeActor,1,0", "ResumeActor,5,0")) + self.assertIn("Warning: locust made 5 successful ResumeActor requests but the atelet logs hold 1", output) + self.assertIn("a node log was rotated", output) + summary = next(e for e in entries if e["metric"] == "phase_breakdown_summary") + self.assertEqual(summary["measurements"]["locust_ResumeActor_failures"], "0") + + def test_requests_that_never_reached_atelet_are_not_owed_a_record(self): + # 5 requests, 4 failed client-side: only 1 successful one reached atelet. + api = fake_api({"ate-system": ATELET, "benchmark-workloads": ATEOM}) + _, _, output = append(api, stats=STATS_CSV.replace("ResumeActor,1,0", "ResumeActor,5,4")) + self.assertNotIn("Warning: locust made", output) + + def test_missing_records_blame_unreadable_pods_when_there_are_any(self): + api = fake_api({"ate-system": ATELET, "benchmark-workloads": ATEOM}, broken_pod="ate-system-pod") + _, _, output = append(api) + self.assertIn("1 pod(s) could not be read", output) + self.assertNotIn("rotated", output) + + def test_a_missing_previous_log_keeps_the_current_one(self): + api = fake_api({"ate-system": ATELET, "benchmark-workloads": ATEOM}, restarted=True, previous_missing=True) + _, entries, output = append(api) + self.assertIn("no previous log for", output) + self.assertTrue(any(e["metric"].startswith("phase_atelet_restore") for e in entries)) + summary = next(e for e in entries if e["metric"] == "phase_breakdown_summary") + self.assertEqual((summary["measurements"]["pods_read"], summary["measurements"]["pods_failed"]), ("2", "0")) + + def test_an_unreadable_pod_is_skipped_not_fatal(self): + api = fake_api({"ate-system": ATELET, "benchmark-workloads": ATEOM}, + broken_pod="benchmark-workloads-pod") + n, entries, output = append(api) + self.assertIn("could not read logs of benchmark-workloads/benchmark-workloads-pod", output) + self.assertTrue(any(e["metric"].startswith("phase_atelet_") for e in entries)) + self.assertFalse(any(e["metric"].startswith("phase_ateom_") for e in entries)) + + def test_no_records_still_writes_the_summary_row(self): + # A failed configuration must not look like an idle run. + n, entries, output = append(fake_api({})) + self.assertEqual((n, len(entries)), (1, 2)) + self.assertEqual(entries[1]["metric"], "phase_breakdown_summary") + self.assertEqual(entries[1]["measurements"]["records"], "0") + self.assertIn("Warning: no timing breakdown records", output) + + def test_a_restarted_container_is_read_twice(self): + api = fake_api({"ate-system": "", "benchmark-workloads": ATEOM}, restarted=True, + previous_logs={"ate-system": ATELET}) + _, entries, _ = append(api) + self.assertTrue(any(e["metric"].startswith("phase_atelet_restore") for e in entries)) + kwargs = [c.kwargs for c in api.read_namespaced_pod_log.call_args_list] + self.assertEqual(sum(1 for k in kwargs if k.get("previous")), 2) + + def test_failed_requests_do_not_trigger_the_rotation_warning(self): + failed = ATELET.replace('"ate.snapshot.kind": "latest",', '"ate.snapshot.kind": "latest", "error.type": "DeadlineExceeded",') + api = fake_api({"ate-system": failed, "benchmark-workloads": ATEOM}) + _, _, output = append(api) + self.assertNotIn("Warning: locust made", output) + + +if __name__ == "__main__": + unittest.main() diff --git a/docs/dev/suspend-resume-phase-breakdown.md b/docs/dev/suspend-resume-phase-breakdown.md index 8cc72e3b6a..c4c3247c4e 100644 --- a/docs/dev/suspend-resume-phase-breakdown.md +++ b/docs/dev/suspend-resume-phase-breakdown.md @@ -24,9 +24,10 @@ the report. or the [GKE Quickstart](../../README.md#gke-quickstart-development). For GKE the scripts below read `.ate-dev-env.sh` from the repository root (`collect_logs.sh` sources it itself; set `NO_DEV_ENV` to opt out). -* The **microVM** sandbox class, if you want the inner (ateom) phases: see - [microvm-local.md](microvm-local.md). ateom-gvisor writes no breakdown - record, so on a gVisor pool the report shows the atelet layer only. +* A **micro-VM** or **gVisor** pool: both ateoms write the inner records, + so the report shows both layers for either class (a worker image that + predates the gVisor records shows the atelet layer only on a gVisor pool). + For micro-VM see [microvm-local.md](microvm-local.md). * `kubectl`, `kubectl-ate` (`go install ./cmd/kubectl-ate`), and `python3` (standard library only). @@ -92,6 +93,12 @@ atespace produce one record pair each. ## 3. Collect the node logs +(A headless `runner.py` run does this step and the next one itself, and +writes the percentiles into its `stats.jsonl`; see +[benchmarking/analysis/README.md](../../benchmarking/analysis/README.md#automated-runs). +The rest of this section is the by-hand path for a run driven from the web +UI, as above.) + Collect **while the pods still exist**. `benchmarking/automation/ orchestrator.py` deletes the workload and ate-system pods right after each test and does not run this step yet, so on a cluster you manage yourself this @@ -153,9 +160,9 @@ phases scale with dirty memory. ## Troubleshooting -* **Only atelet rows, no ateom rows.** The pool is gVisor (ateom-gvisor emits - no record), or the worker image predates the records. Check - `kubectl get workerpools -A` and step 1's grep. +* **Only atelet rows, no ateom rows.** The worker image predates the ateom + records (check step 1's grep against a worker pod), or the ateom records + fell outside the collection window. * **Counts differ between layers.** The collection window cut through a cycle, or a worker pod restarted and took its log with it (the script dumps a restarted container's previous log as `.previous.log`). On GKE the From 1a3ab489dbcd518bd5ccc1718fcca747149bf759 Mon Sep 17 00:00:00 2001 From: Ganeshkumar Ashokavardhanan <35557827+ganeshkumarashok@users.noreply.github.com> Date: Tue, 13 Oct 2026 18:28:27 +0000 Subject: [PATCH 10/42] Align timestamp fields with AIP-142 naming conventions (#2438) ## Summary Align the two remaining nonconforming public Timestamp field names with the API's AIP naming conventions: - `MintActorJwtResponse.expires_at` becomes `expire_time`. - `GoldenSnapshotStatus.take_golden_snapshot_at` becomes `snapshot_time`. Update in-repository callers and existing tests, regenerate protobuf and declarative validation code, and document the `_time` naming rule. Retain the existing validation, lifecycle, and JWT functional coverage; the additional protobuf serialization test file has been removed. Fixes #2415. ## Breaking change Regenerate clients and update references to these fields: | Before | After | | --- | --- | | `expires_at` / `expiresAt` | `expire_time` / `expireTime` | | `take_golden_snapshot_at` / `takeGoldenSnapshotAt` | `snapshot_time` / `snapshotTime` | | Go `ExpiresAt` / `GetExpiresAt()` | `ExpireTime` / `GetExpireTime()` | | Go `TakeGoldenSnapshotAt` / `GetTakeGoldenSnapshotAt()` | `SnapshotTime` / `GetSnapshotTime()` | Timestamp semantics, types, and protobuf field numbers are unchanged. Old-name aliases are not retained, following the project's pre-1.0 compatibility policy. Please apply the `breaking-change` label. ## Verification - [x] Race-enabled tests for the retained validation, lifecycle, and egress JWT packages; the public protobuf package builds. - [x] `TestMintActorJWT` through the functional API harness. - [x] Repository Linux-aware Go lint. - [x] Validation coverage check. - [x] `hack/verify-all.sh`: all 15 current repository verifiers passed on the latest conflict refresh. - [ ] Full Linux/root/infrastructure test matrix (not run locally). ## CI follow-up The October 12 E2E failure was two timeouts to public `httpbin.org` in the credential-injection suite. Current `main` includes #2535, which replaces that external origin with an in-cluster echo server and restores the test after the earlier upstream quarantine. This PR adds no test skip of its own. ## Contribution note This is a first contribution, opened as a draft for coordination and feedback. GitHub Copilot CLI was used to help prepare the code, tests, and PR description. --- .../internal/apivalidation/validation_test.go | 14 ++++---- .../apivalidation/zz_generated.validation.go | 6 ++-- cmd/ateapi/internal/controlapi/actor.go | 6 ++-- .../controlapi/functionaltest/actor_test.go | 6 ++-- .../controlapi/template_reconciler.go | 6 ++-- .../controlapi/template_reconciler_test.go | 24 ++++++------- .../router/egress/actorjwt/actorjwt.go | 2 +- .../router/egress/actorjwt/actorjwt_test.go | 4 +-- .../internal/router/egress/egress_test.go | 4 +-- docs/api-style-guide.md | 1 + pkg/proto/ateapipb/ateapi.pb.go | 34 +++++++++---------- pkg/proto/ateapipb/ateapi.proto | 8 ++--- 12 files changed, 58 insertions(+), 57 deletions(-) diff --git a/cmd/ateapi/internal/apivalidation/validation_test.go b/cmd/ateapi/internal/apivalidation/validation_test.go index 9a90cf92c7..43d5068c83 100644 --- a/cmd/ateapi/internal/apivalidation/validation_test.go +++ b/cmd/ateapi/internal/apivalidation/validation_test.go @@ -1727,8 +1727,8 @@ func TestValidateTagRequestPayloads(t *testing.T) { func validGoldenSnapshotStatus(mutate ...func(*ateapipb.GoldenSnapshotStatus)) *ateapipb.GoldenSnapshotStatus { s := &ateapipb.GoldenSnapshotStatus{ - GoldenTag: &ateapipb.ObjectRef{Atespace: "ate-golden", Name: "01234567-89ab-cdef-0123-456789abcdef"}, - TakeGoldenSnapshotAt: ×tamppb.Timestamp{Seconds: 867}, + GoldenTag: &ateapipb.ObjectRef{Atespace: "ate-golden", Name: "01234567-89ab-cdef-0123-456789abcdef"}, + SnapshotTime: ×tamppb.Timestamp{Seconds: 867}, } for _, m := range mutate { m(s) @@ -1756,8 +1756,8 @@ func TestValidateGoldenSnapshotStatus(t *testing.T) { { name: "valid failed", obj: &ateapipb.GoldenSnapshotStatus{ - TakeGoldenSnapshotAt: ×tamppb.Timestamp{Seconds: 867}, - ErrorMessage: "GoldenActorCrashed: golden actor crashed before its snapshot was taken", + SnapshotTime: ×tamppb.Timestamp{Seconds: 867}, + ErrorMessage: "GoldenActorCrashed: golden actor crashed before its snapshot was taken", }, }, { @@ -1806,14 +1806,14 @@ func TestValidateGoldenSnapshotStatusUpdate(t *testing.T) { { // The reconciler restarts the warmup clock on every resume, so the // deadline must stay mutable. - name: "take_golden_snapshot_at changed", + name: "snapshot_time changed", oldObj: valid(), newObj: valid(func(s *ateapipb.GoldenSnapshotStatus) { - s.TakeGoldenSnapshotAt = ×tamppb.Timestamp{Seconds: 5309} + s.SnapshotTime = ×tamppb.Timestamp{Seconds: 5309} }), }, { - name: "take_golden_snapshot_at set", + name: "snapshot_time set", oldObj: &ateapipb.GoldenSnapshotStatus{}, newObj: valid(), }, diff --git a/cmd/ateapi/internal/apivalidation/zz_generated.validation.go b/cmd/ateapi/internal/apivalidation/zz_generated.validation.go index 6cace304e7..904e5fd626 100644 --- a/cmd/ateapi/internal/apivalidation/zz_generated.validation.go +++ b/cmd/ateapi/internal/apivalidation/zz_generated.validation.go @@ -4574,7 +4574,7 @@ func Validate_GoldenSnapshotStatus( errs = append(errs, fn(fldPath.Child("golden_tag"), obj.GoldenTag, oldVal, oldObj != nil)...) } - { // field ateapipb.GoldenSnapshotStatus.TakeGoldenSnapshotAt + { // field ateapipb.GoldenSnapshotStatus.SnapshotTime fn := func( fldPath *field.Path, obj, oldObj *timestamppb.Timestamp, @@ -4597,9 +4597,9 @@ func Validate_GoldenSnapshotStatus( } oldVal := safe.Field(oldObj, func(oldObj *ateapipb.GoldenSnapshotStatus) *timestamppb.Timestamp { - return oldObj.TakeGoldenSnapshotAt + return oldObj.SnapshotTime }) - errs = append(errs, fn(fldPath.Child("take_golden_snapshot_at"), obj.TakeGoldenSnapshotAt, oldVal, oldObj != nil)...) + errs = append(errs, fn(fldPath.Child("snapshot_time"), obj.SnapshotTime, oldVal, oldObj != nil)...) } { // field ateapipb.GoldenSnapshotStatus.ErrorMessage diff --git a/cmd/ateapi/internal/controlapi/actor.go b/cmd/ateapi/internal/controlapi/actor.go index 98e0004b37..b83b96e1dd 100644 --- a/cmd/ateapi/internal/controlapi/actor.go +++ b/cmd/ateapi/internal/controlapi/actor.go @@ -566,7 +566,7 @@ func (s *RPCService) MintActorJwt(ctx context.Context, req *ateapipb.MintActorJw return nil, fmt.Errorf("at least one audience must be requested") } - // JWT timestamps have one-second resolution; truncating keeps expires_at + // JWT timestamps have one-second resolution; truncating keeps expire_time // equal to the exp claim. now := time.Now().Truncate(time.Second) expiresAt := now.Add(time.Duration(req.GetExpirationSeconds()) * time.Second) @@ -592,8 +592,8 @@ func (s *RPCService) MintActorJwt(ctx context.Context, req *ateapipb.MintActorJw } return &ateapipb.MintActorJwtResponse{ - ActorJwt: actorJWT, - ExpiresAt: timestamppb.New(expiresAt), + ActorJwt: actorJWT, + ExpireTime: timestamppb.New(expiresAt), }, nil } diff --git a/cmd/ateapi/internal/controlapi/functionaltest/actor_test.go b/cmd/ateapi/internal/controlapi/functionaltest/actor_test.go index abefeb01a4..e4ceafd6a4 100644 --- a/cmd/ateapi/internal/controlapi/functionaltest/actor_test.go +++ b/cmd/ateapi/internal/controlapi/functionaltest/actor_test.go @@ -5256,12 +5256,12 @@ func TestMintActorJWT_Success(t *testing.T) { assertActorJWTLifetime(t, mintResp, claims, 30*time.Minute) } -// assertActorJWTLifetime checks that expires_at matches the exp claim and that +// assertActorJWTLifetime checks that expire_time matches the exp claim and that // the token is valid for want after it was issued. func assertActorJWTLifetime(t *testing.T, resp *ateapipb.MintActorJwtResponse, claims actoridjwt.WireClaims, want time.Duration) { t.Helper() - if got, exp := resp.GetExpiresAt().AsTime(), time.Unix(int64(claims.Expiration), 0); !got.Equal(exp) { - t.Errorf("expires_at = %v, want the exp claim %v", got, exp) + if got, exp := resp.GetExpireTime().AsTime(), time.Unix(int64(claims.Expiration), 0); !got.Equal(exp) { + t.Errorf("expire_time = %v, want the exp claim %v", got, exp) } if got := time.Duration(claims.Expiration-claims.IssuedAt) * time.Second; got != want { t.Errorf("exp - iat = %v, want %v", got, want) diff --git a/cmd/ateapi/internal/controlapi/template_reconciler.go b/cmd/ateapi/internal/controlapi/template_reconciler.go index a1857d0f4d..463dac830d 100644 --- a/cmd/ateapi/internal/controlapi/template_reconciler.go +++ b/cmd/ateapi/internal/controlapi/template_reconciler.go @@ -254,7 +254,7 @@ func (r *ActorTemplateReconciler) reconcileOne(ctx context.Context, ref resource return 0, r.fail(ctx, tmpl, reasonGoldenActorCrashed, "golden actor crashed before its snapshot was taken") case ateapipb.ActorState_ACTOR_STATE_RUNNING: - takeAt := goldenSnapshotStatus.GetTakeGoldenSnapshotAt() + takeAt := goldenSnapshotStatus.GetSnapshotTime() if takeAt == nil { // Resumed, but the deadline write was lost (a replica died // after ResumeActor). The elapsed warmup is unknowable, so @@ -262,7 +262,7 @@ func (r *ActorTemplateReconciler) reconcileOne(ctx context.Context, ref resource slog.WarnContext(ctx, "Golden actor running without a snapshot deadline; restarting warmup", slog.String("ActorTemplate", ref.String())) deadline := time.Now().Add(goldenSnapshotWarmupFor(tmpl.GetContainers())) if tmpl, err = r.checkpoint(ctx, tmpl, func(snapshotStatus *ateapipb.GoldenSnapshotStatus) { - snapshotStatus.TakeGoldenSnapshotAt = timestamppb.New(deadline) + snapshotStatus.SnapshotTime = timestamppb.New(deadline) }); err != nil { return 0, err } @@ -303,7 +303,7 @@ func (r *ActorTemplateReconciler) reconcileOne(ctx context.Context, ref resource } deadline := time.Now().Add(goldenSnapshotWarmupFor(tmpl.GetContainers())) if tmpl, err = r.checkpoint(ctx, tmpl, func(snapshotStatus *ateapipb.GoldenSnapshotStatus) { - snapshotStatus.TakeGoldenSnapshotAt = timestamppb.New(deadline) + snapshotStatus.SnapshotTime = timestamppb.New(deadline) }); err != nil { return 0, err } diff --git a/cmd/ateapi/internal/controlapi/template_reconciler_test.go b/cmd/ateapi/internal/controlapi/template_reconciler_test.go index 531248b099..972ced7075 100644 --- a/cmd/ateapi/internal/controlapi/template_reconciler_test.go +++ b/cmd/ateapi/internal/controlapi/template_reconciler_test.go @@ -358,7 +358,7 @@ func seededGoldenStatus(tmpl *ateapipb.ActorTemplate) *ateapipb.GoldenSnapshotSt func withSnapshotDeadline(at time.Time) func(*ateapipb.ActorTemplate) { return func(tmpl *ateapipb.ActorTemplate) { - seededGoldenStatus(tmpl).TakeGoldenSnapshotAt = timestamppb.New(at) + seededGoldenStatus(tmpl).SnapshotTime = timestamppb.New(at) } } @@ -418,7 +418,7 @@ func TestReconcileOne(t *testing.T) { wantMessage string // wantTag indicates that the golden tag should be recorded. wantTag bool - // wantDeadline asserts whether take_golden_snapshot_at is set. + // wantDeadline asserts whether snapshot_time is set. wantDeadline bool wantCreates int wantResumes int @@ -632,8 +632,8 @@ func TestReconcileOne(t *testing.T) { if got := snapshotStatus.GetGoldenTag(); (got != nil) != tt.wantTag { t.Errorf("stored golden tag = %v, want tag %v", got, tt.wantTag) } - if tt.wantDeadline && snapshotStatus.GetTakeGoldenSnapshotAt() == nil { - t.Error("stored take_golden_snapshot_at is nil, want set") + if tt.wantDeadline && snapshotStatus.GetSnapshotTime() == nil { + t.Error("stored snapshot_time is nil, want set") } }) } @@ -675,8 +675,8 @@ func TestReconcileOne_GoldenActorRequests(t *testing.T) { if got := control.suspendReqs[0].GetActor().GetName(); got != testTemplateUID { t.Errorf("suspended actor = %q, want %q", got, testTemplateUID) } - if st.storedStatus(t, testTemplateRef).GetGoldenSnapshotStatus().GetTakeGoldenSnapshotAt() == nil { - t.Error("stored take_golden_snapshot_at is nil, want set") + if st.storedStatus(t, testTemplateRef).GetGoldenSnapshotStatus().GetSnapshotTime() == nil { + t.Error("stored snapshot_time is nil, want set") } } @@ -697,13 +697,13 @@ func TestCheckpoint_TerminalStateErrors(t *testing.T) { // Checkpoint against a template a concurrent writer already // drove to a terminal state. _, err := r.checkpoint(ctx, testTemplate(seed.opt), func(snapshotStatus *ateapipb.GoldenSnapshotStatus) { - snapshotStatus.TakeGoldenSnapshotAt = timestamppb.New(time.Now()) + snapshotStatus.SnapshotTime = timestamppb.New(time.Now()) }) if err == nil { t.Fatal("checkpoint succeeded, want error for terminal template") } - if st.storedStatus(t, testTemplateRef).GetGoldenSnapshotStatus().GetTakeGoldenSnapshotAt() != nil { - t.Error("take_golden_snapshot_at set, want store unchanged") + if st.storedStatus(t, testTemplateRef).GetGoldenSnapshotStatus().GetSnapshotTime() != nil { + t.Error("snapshot_time set, want store unchanged") } }) } @@ -806,13 +806,13 @@ func TestCheckpoint_StaleObservationConflicts(t *testing.T) { } _, err := r.checkpoint(ctx, testTemplate(), func(snapshotStatus *ateapipb.GoldenSnapshotStatus) { - snapshotStatus.TakeGoldenSnapshotAt = timestamppb.New(time.Now()) + snapshotStatus.SnapshotTime = timestamppb.New(time.Now()) }) if !errors.Is(err, store.ErrVersionConflict) { t.Fatalf("checkpoint error = %v, want ErrVersionConflict", err) } - if st.storedStatus(t, testTemplateRef).GetGoldenSnapshotStatus().GetTakeGoldenSnapshotAt() != nil { - t.Error("take_golden_snapshot_at set, want store unchanged") + if st.storedStatus(t, testTemplateRef).GetGoldenSnapshotStatus().GetSnapshotTime() != nil { + t.Error("snapshot_time set, want store unchanged") } } diff --git a/cmd/atenet/internal/router/egress/actorjwt/actorjwt.go b/cmd/atenet/internal/router/egress/actorjwt/actorjwt.go index b3c6022ad5..4f79ca14a1 100644 --- a/cmd/atenet/internal/router/egress/actorjwt/actorjwt.go +++ b/cmd/atenet/internal/router/egress/actorjwt/actorjwt.go @@ -88,7 +88,7 @@ func (m *Minter) mint(ctx context.Context, key string, ref resources.ActorRef, r if err != nil { return "", fmt.Errorf("minting an actor JWT for %s: %w", ref, err) } - reuseFor := time.Until(resp.GetExpiresAt().AsTime()) - time.Duration(req.GetExpirationSeconds())*time.Second/3 + reuseFor := time.Until(resp.GetExpireTime().AsTime()) - time.Duration(req.GetExpirationSeconds())*time.Second/3 m.tokens.Set(key, resp.GetActorJwt(), reuseFor) return resp.GetActorJwt(), nil } diff --git a/cmd/atenet/internal/router/egress/actorjwt/actorjwt_test.go b/cmd/atenet/internal/router/egress/actorjwt/actorjwt_test.go index 026c9e0afc..f7194887a4 100644 --- a/cmd/atenet/internal/router/egress/actorjwt/actorjwt_test.go +++ b/cmd/atenet/internal/router/egress/actorjwt/actorjwt_test.go @@ -64,8 +64,8 @@ func (f *fakeControl) MintActorJwt(ctx context.Context, req *ateapipb.MintActorJ return nil, f.err } return &ateapipb.MintActorJwtResponse{ - ActorJwt: fmt.Sprintf("jwt-%d", n), - ExpiresAt: timestamppb.New(time.Now().Add(time.Duration(req.GetExpirationSeconds()) * time.Second)), + ActorJwt: fmt.Sprintf("jwt-%d", n), + ExpireTime: timestamppb.New(time.Now().Add(time.Duration(req.GetExpirationSeconds()) * time.Second)), }, nil } diff --git a/cmd/atenet/internal/router/egress/egress_test.go b/cmd/atenet/internal/router/egress/egress_test.go index 41ffa029c0..c14ea93bb7 100644 --- a/cmd/atenet/internal/router/egress/egress_test.go +++ b/cmd/atenet/internal/router/egress/egress_test.go @@ -227,8 +227,8 @@ func (m *egressMockClient) MintActorJwt(_ context.Context, req *ateapipb.MintAct return nil, m.mintErr } return &ateapipb.MintActorJwtResponse{ - ActorJwt: fmt.Sprintf("jwt-%d", n), - ExpiresAt: timestamppb.New(time.Now().Add(time.Duration(req.GetExpirationSeconds()) * time.Second)), + ActorJwt: fmt.Sprintf("jwt-%d", n), + ExpireTime: timestamppb.New(time.Now().Add(time.Duration(req.GetExpirationSeconds()) * time.Second)), }, nil } diff --git a/docs/api-style-guide.md b/docs/api-style-guide.md index 6809490e11..fc1a07cec7 100644 --- a/docs/api-style-guide.md +++ b/docs/api-style-guide.md @@ -357,6 +357,7 @@ Rules: - Use standard abbreviations where well-established: `config`, `spec`, `id`, `info`, `stats`. - Adjectives come before the noun: `suspended_actors`, not `actors_suspended`. - Avoid prepositions in field names: `error_reason`, not `reason_for_error`. +- `google.protobuf.Timestamp` fields **must** end in `_time`, following [AIP-142](https://google.aip.dev/142): use `create_time`, `expire_time`, or `crash_time`, not `created_at` or `expires_at`. ### 5.1 Enum naming diff --git a/pkg/proto/ateapipb/ateapi.pb.go b/pkg/proto/ateapipb/ateapi.pb.go index fde3450ce7..be65d3853e 100644 --- a/pkg/proto/ateapipb/ateapi.pb.go +++ b/pkg/proto/ateapipb/ateapi.pb.go @@ -2831,11 +2831,11 @@ type GoldenSnapshotStatus struct { // +k8s:optional // +k8s:subfield(atespace)=+k8s:required GoldenTag *ObjectRef `protobuf:"bytes,1,opt,name=golden_tag,json=goldenTag,proto3" json:"golden_tag,omitempty"` - // take_golden_snapshot_at is when the golden-actor warmup ends and the + // snapshot_time is when the golden-actor warmup ends and the // golden snapshot may be taken. // // +k8s:optional - TakeGoldenSnapshotAt *timestamppb.Timestamp `protobuf:"bytes,2,opt,name=take_golden_snapshot_at,json=takeGoldenSnapshotAt,proto3" json:"take_golden_snapshot_at,omitempty"` + SnapshotTime *timestamppb.Timestamp `protobuf:"bytes,2,opt,name=snapshot_time,json=snapshotTime,proto3" json:"snapshot_time,omitempty"` // error_message is set when the golden snapshot build fails and will not be // retried, formatted as ": ". A nonempty value is terminal. // @@ -2883,9 +2883,9 @@ func (x *GoldenSnapshotStatus) GetGoldenTag() *ObjectRef { return nil } -func (x *GoldenSnapshotStatus) GetTakeGoldenSnapshotAt() *timestamppb.Timestamp { +func (x *GoldenSnapshotStatus) GetSnapshotTime() *timestamppb.Timestamp { if x != nil { - return x.TakeGoldenSnapshotAt + return x.SnapshotTime } return nil } @@ -5782,8 +5782,8 @@ type MintActorJwtResponse struct { // * `ate.dev`: A JSON object with the actor's `atespace`, `actorName`, and // `actorUID`. ActorJwt string `protobuf:"bytes,1,opt,name=actor_jwt,json=actorJwt,proto3" json:"actor_jwt,omitempty"` - // When actor_jwt expires. Equal to its exp claim. - ExpiresAt *timestamppb.Timestamp `protobuf:"bytes,2,opt,name=expires_at,json=expiresAt,proto3" json:"expires_at,omitempty"` + // expire_time is when actor_jwt expires. Equal to its exp claim. + ExpireTime *timestamppb.Timestamp `protobuf:"bytes,2,opt,name=expire_time,json=expireTime,proto3" json:"expire_time,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -5825,9 +5825,9 @@ func (x *MintActorJwtResponse) GetActorJwt() string { return "" } -func (x *MintActorJwtResponse) GetExpiresAt() *timestamppb.Timestamp { +func (x *MintActorJwtResponse) GetExpireTime() *timestamppb.Timestamp { if x != nil { - return x.ExpiresAt + return x.ExpireTime } return nil } @@ -8749,11 +8749,11 @@ const file_ateapi_proto_rawDesc = "" + "\x06limits\x18\x01 \x03(\v2\x0e.ateapi.LimitsR\x06limits\"8\n" + "\x06Limits\x12\x12\n" + "\x04name\x18\x01 \x01(\tR\x04name\x12\x1a\n" + - "\bquantity\x18\x02 \x01(\tR\bquantity\"\xc0\x01\n" + + "\bquantity\x18\x02 \x01(\tR\bquantity\"\xae\x01\n" + "\x14GoldenSnapshotStatus\x120\n" + "\n" + - "golden_tag\x18\x01 \x01(\v2\x11.ateapi.ObjectRefR\tgoldenTag\x12Q\n" + - "\x17take_golden_snapshot_at\x18\x02 \x01(\v2\x1a.google.protobuf.TimestampR\x14takeGoldenSnapshotAt\x12#\n" + + "golden_tag\x18\x01 \x01(\v2\x11.ateapi.ObjectRefR\tgoldenTag\x12?\n" + + "\rsnapshot_time\x18\x02 \x01(\v2\x1a.google.protobuf.TimestampR\fsnapshotTime\x12#\n" + "\rerror_message\x18\x03 \x01(\tR\ferrorMessage\"i\n" + "\x13ActorTemplateStatus\x12R\n" + "\x16golden_snapshot_status\x18\x01 \x01(\v2\x1c.ateapi.GoldenSnapshotStatusR\x14goldenSnapshotStatus\"k\n" + @@ -8896,11 +8896,11 @@ const file_ateapi_proto_rawDesc = "" + "\x13MintActorJwtRequest\x12'\n" + "\x05actor\x18\x05 \x01(\v2\x11.ateapi.ObjectRefR\x05actor\x12\x1c\n" + "\taudiences\x18\x01 \x03(\tR\taudiences\x12-\n" + - "\x12expiration_seconds\x18\b \x01(\x03R\x11expirationSeconds\"s\n" + + "\x12expiration_seconds\x18\b \x01(\x03R\x11expirationSeconds\"u\n" + "\x14MintActorJwtResponse\x12 \n" + - "\tactor_jwt\x18\x01 \x01(\tB\x03\x80\x01\x01R\bactorJwt\x129\n" + - "\n" + - "expires_at\x18\x02 \x01(\v2\x1a.google.protobuf.TimestampR\texpiresAt\"\xa3\x01\n" + + "\tactor_jwt\x18\x01 \x01(\tB\x03\x80\x01\x01R\bactorJwt\x12;\n" + + "\vexpire_time\x18\x02 \x01(\v2\x1a.google.protobuf.TimestampR\n" + + "expireTime\"\xa3\x01\n" + "\x1bMintActorCertificateRequest\x12'\n" + "\x05actor\x18\x06 \x01(\v2\x11.ateapi.ObjectRefR\x05actor\x12\x1b\n" + "\tactor_uid\x18\a \x01(\tR\bactorUid\x12>\n" + @@ -9346,7 +9346,7 @@ var file_ateapi_proto_depIdxs = []int32{ 38, // 48: ateapi.ActorTemplate.status:type_name -> ateapi.ActorTemplateStatus 36, // 49: ateapi.Resources.limits:type_name -> ateapi.Limits 33, // 50: ateapi.GoldenSnapshotStatus.golden_tag:type_name -> ateapi.ObjectRef - 132, // 51: ateapi.GoldenSnapshotStatus.take_golden_snapshot_at:type_name -> google.protobuf.Timestamp + 132, // 51: ateapi.GoldenSnapshotStatus.snapshot_time:type_name -> google.protobuf.Timestamp 37, // 52: ateapi.ActorTemplateStatus.golden_snapshot_status:type_name -> ateapi.GoldenSnapshotStatus 5, // 53: ateapi.SandboxConfig.sandbox_class:type_name -> ateapi.SandboxClass 2, // 54: ateapi.SnapshotConfig.preferred_fidelity:type_name -> ateapi.SnapshotFidelity @@ -9400,7 +9400,7 @@ var file_ateapi_proto_depIdxs = []int32{ 94, // 102: ateapi.DeleteActorEgressPolicyRequest.options:type_name -> ateapi.DeleteOptions 33, // 103: ateapi.GetTagRequest.tag:type_name -> ateapi.ObjectRef 33, // 104: ateapi.MintActorJwtRequest.actor:type_name -> ateapi.ObjectRef - 132, // 105: ateapi.MintActorJwtResponse.expires_at:type_name -> google.protobuf.Timestamp + 132, // 105: ateapi.MintActorJwtResponse.expire_time:type_name -> google.protobuf.Timestamp 33, // 106: ateapi.MintActorCertificateRequest.actor:type_name -> ateapi.ObjectRef 31, // 107: ateapi.ListTagsResponse.tags:type_name -> ateapi.Tag 31, // 108: ateapi.CreateTagRequest.tag:type_name -> ateapi.Tag diff --git a/pkg/proto/ateapipb/ateapi.proto b/pkg/proto/ateapipb/ateapi.proto index b53d1541e7..77e1b967ab 100644 --- a/pkg/proto/ateapipb/ateapi.proto +++ b/pkg/proto/ateapipb/ateapi.proto @@ -1066,11 +1066,11 @@ message GoldenSnapshotStatus { // +k8s:subfield(atespace)=+k8s:required ObjectRef golden_tag = 1; - // take_golden_snapshot_at is when the golden-actor warmup ends and the + // snapshot_time is when the golden-actor warmup ends and the // golden snapshot may be taken. // // +k8s:optional - google.protobuf.Timestamp take_golden_snapshot_at = 2; + google.protobuf.Timestamp snapshot_time = 2; // error_message is set when the golden snapshot build fails and will not be // retried, formatted as ": ". A nonempty value is terminal. @@ -1824,8 +1824,8 @@ message MintActorJwtResponse { // `actorUID`. string actor_jwt = 1 [ debug_redact = true ]; - // When actor_jwt expires. Equal to its exp claim. - google.protobuf.Timestamp expires_at = 2; + // expire_time is when actor_jwt expires. Equal to its exp claim. + google.protobuf.Timestamp expire_time = 2; } // Request message for MintActorCertificate. From e1847fb51a37fd82f6ac014b8cc0e3e4cd45a11e Mon Sep 17 00:00:00 2001 From: haiyanmeng Date: Tue, 13 Oct 2026 18:45:41 +0000 Subject: [PATCH 11/42] benchmarking: pace boomer to a target rate and add the resumepaused user class (#2350) This PR lets boomer hold a fixed request rate for each ladder step, and adds a user class, `resumepaused`, that drives ate-api-server's cold resume path at that rate. It is needed for ate-api-server capacity runs: a closed-loop user can't hold a rate, because the offered load drops whenever the server slows down. Benchmark-only, with no production code changes. ## Commits 1. **Pace boomer to a target request rate per ladder step.** A new dynconfig field, `target_rps`, is set with `--target-rps` or per ladder step as `users:duration@rps` (e.g. `200:3m@4000`). Boomer picks up a change within 10 s. Only user classes that pace themselves honor it, which today is resumepaused. 2. **Add the resumepaused user class.** Each user owns one actor. On its first iteration the user creates the actor and makes it cold with a resume and a pause; every later iteration resumes it (`ResumeActorCold`, paced to `target_rps`) and pauses it again (`PauseActor`). The warm-up is unpaced and recorded under its own names (`CreateActor`, `ResumeActorWarmup`, `PauseActorWarmup`) in the step that added the user. Every actor is deleted on exit. It sends no traffic to the actors, so it runs on the fake data plane (#2300). ## Notes for reviewers - A capacity resumepaused finds is one of resume and pause cycles, not of resumes alone. - Keep `users` at or above the rate times the p99 cycle time, or a step falls short of its rate. - `target_rps` paces one boomer worker. - A step that adds many users carries their warm-up load while they join at the spawn rate, so trim the start of each step. When a step lowers the user count, the stopped users' actors stay paused until boomer exits. - The `tests.yaml` capacity tests come in a follow-up PR. Details are in the ResumePaused section of `benchmarking/README.md`. ## Testing - `go test -race ./internal/benchmarking/boomer/...` - `python3 benchmarking/locust/unit_tests/test_ladder_shape.py` (needs locust) - `python3 benchmarking/automation/test_orchestrator.py` Addresses #2261 - [x] Tests pass - [x] Appropriate changes to documentation are included in the PR --- benchmarking/README.md | 51 ++ benchmarking/automation/tests.yaml | 7 + benchmarking/locust/common/boomer_config.py | 3 + benchmarking/locust/common/rate_config.py | 36 + benchmarking/locust/shapes/ladder_shape.py | 65 +- benchmarking/locust/tests/resumepaused.py | 49 ++ .../locust/unit_tests/test_ladder_shape.py | 93 +++ cmd/benchmarking/boomer-worker/main.go | 3 +- go.mod | 2 +- .../boomer/dynconfig/dynconfig.go | 12 + .../boomer/dynconfig/dynconfig_test.go | 10 +- internal/benchmarking/boomer/metrics/prom.go | 27 +- .../boomer/resumepaused/resumepaused.go | 446 +++++++++++ .../boomer/resumepaused/resumepaused_test.go | 749 ++++++++++++++++++ 14 files changed, 1524 insertions(+), 29 deletions(-) create mode 100644 benchmarking/locust/common/rate_config.py create mode 100644 benchmarking/locust/tests/resumepaused.py create mode 100644 benchmarking/locust/unit_tests/test_ladder_shape.py create mode 100644 internal/benchmarking/boomer/resumepaused/resumepaused.go create mode 100644 internal/benchmarking/boomer/resumepaused/resumepaused_test.go diff --git a/benchmarking/README.md b/benchmarking/README.md index 904a43bd40..0737cd9b18 100644 --- a/benchmarking/README.md +++ b/benchmarking/README.md @@ -369,6 +369,57 @@ The web UI shows the same fields; `0` keeps the value boomer-worker started with The `actors_per_*` ratios in `trial_summary` are wrong for this test: they count users × `--actors-per-user`, not `--total-actors`. +### ResumePaused Benchmark + +The ResumePaused benchmark drives ate-api-server's cold resume path at a set +rate. A cold resume here is a ResumeActor of a paused actor: ateapi takes the +actor lease, binds a worker, and restores the actor from its snapshot. A +ResumeActor of a running actor does none of that. Each user owns one actor +from the `glutton` template. On its first iteration the user creates the actor +and makes it cold with a resume and a pause; every later iteration resumes the +actor and pauses it again, so every measured resume is cold. + +The warm-up runs in the step that added the user, unpaced, and is recorded +under its own names. + +It sends no requests to the actors themselves, so it runs on the fake data +plane, where it measures what a cold resume costs ate-api-server and Postgres +without the router or the restore in the way. The pauses run at the resume +rate, so a capacity it finds is one of resume and pause cycles, not of resumes +alone. + +Actors are named `rp-` and deleted when boomer exits. If the atespace +cannot be ensured, boomer exits at startup. An actor that fails to warm is +deleted, and its user creates another on its next iteration. When a step lowers +the user count, the actors of the stopped users stay paused, unused, until +boomer exits. + +#### ResumePaused Configuration Knobs + +* `--target-rps`: Cold resumes per second the whole boomer process paces to + (default `0`, unpaced). A `--ladder` step ending in `@target_rps`, as in + `200:3m@1000`, changes it as the step starts. Keep the users at or above the + rate times the p99 of one resume plus one pause in seconds, or the step falls + short of its rate. + +The web UI shows the same field. + +Trim the start of each ladder step. A rate change reaches boomer at its next +poll of the master, within 10 s, and a step that adds users carries their +warm-up until they have all joined at the spawn rate. + +#### ResumePaused Reported Metrics + +* `ResumeActorCold`: Latency of each measured resume. A resume that finds its + actor already running fails, as no cold resume happened. +* `PauseActor`: Latency of the pause that makes the actor cold again. +* `PauseActorRecovery`: After a failed cycle, the pause that makes the actor + cold for the next one. It is load on the server, so it is recorded, but + apart from `PauseActor`. A recovery pause that finds the actor already + paused counts as a success. +* `CreateActor`, `ResumeActorWarmup`, `PauseActorWarmup`: A user's warm-up of + its actor, in the step that added the user. + ### Viewing Traces You must have enabled otel tracing for your cluster to view traces. diff --git a/benchmarking/automation/tests.yaml b/benchmarking/automation/tests.yaml index 325d273cae..c911e966d6 100644 --- a/benchmarking/automation/tests.yaml +++ b/benchmarking/automation/tests.yaml @@ -85,6 +85,13 @@ # `--trace-probability` in `flags` is the rate before the first such step. # Thus a ladder with no probability in any step keeps the rate of the flag. # +# A step can end in `@target_rps` (e.g. `200:3m@4000`), which caps the +# requests per second of the whole boomer worker from the step's start; 0 +# removes the cap. Boomer users that pace themselves (resumepaused) honor it. +# Keep `users` at or above the rate times the p99 of one user iteration in +# seconds, or the step falls short of its rate. A resumepaused iteration is a +# resume plus a pause, so size it by the p99 of both together. +# # ---- type: nighthawk-ingress -------------------------------------------------------- # # Runs the Nighthawk adaptive router-capacity benchmark. `duration` is the diff --git a/benchmarking/locust/common/boomer_config.py b/benchmarking/locust/common/boomer_config.py index 17902d78c9..e78c30ea60 100644 --- a/benchmarking/locust/common/boomer_config.py +++ b/benchmarking/locust/common/boomer_config.py @@ -24,6 +24,7 @@ * --mem-target / --mem-churn / --mem-read → common.memload_config.add_memload_arguments * --cpu-cores / --cpu-duty-cycle → common.cpuload_config.add_cpuload_arguments * --max-pings-per-wake → common.ping_config.add_ping_arguments + * --target-rps → common.rate_config.add_rate_arguments This module ties them together so boomer-Go workers can pick up the values the operator set in the web UI form: @@ -76,6 +77,7 @@ "--total-actors": int, "--spawn-concurrency": int, "--actor-deadline": float, + "--target-rps": float, } @@ -157,6 +159,7 @@ def init_boomer_config() -> None: from common.lifecycle_mode import add_lifecycle_mode_arguments from common.memload_config import add_memload_arguments from common.ping_config import add_ping_arguments + from common.rate_config import add_rate_arguments from common.resume_mode import add_resume_mode_arguments from common.spawn_config import add_spawn_arguments from common.sweperf_config import add_sweperf_arguments diff --git a/benchmarking/locust/common/rate_config.py b/benchmarking/locust/common/rate_config.py new file mode 100644 index 0000000000..47e28984dd --- /dev/null +++ b/benchmarking/locust/common/rate_config.py @@ -0,0 +1,36 @@ +# 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. + +"""Request-rate runtime flag, for boomer user classes that pace +(resumepaused).""" + +from locust import events +from locust.argument_parser import LocustArgumentParser + + +@events.init_command_line_parser.add_listener +def add_rate_arguments(parser: LocustArgumentParser) -> None: + parser.add_argument( + "--target-rps", + type=float, + default=0.0, + env_var="LOCUST_TARGET_RPS", + help=( + "Requests per second the whole boomer worker paces to, for user " + "classes that pace (resumepaused). 0 removes the cap. " + "A --ladder step with @target_rps changes it while the run " + "continues" + ), + include_in_web_ui=True, + ) diff --git a/benchmarking/locust/shapes/ladder_shape.py b/benchmarking/locust/shapes/ladder_shape.py index 4e13014c9b..a3e33561a4 100644 --- a/benchmarking/locust/shapes/ladder_shape.py +++ b/benchmarking/locust/shapes/ladder_shape.py @@ -20,11 +20,12 @@ a set of cold caches, and a new baseline for the reader to align. `--ladder` makes the steps one run. Give the steps as a comma-separated list -of `users:duration[:trace_probability]`: +of `users:duration[:trace_probability][@target_rps]`: --ladder 5:3m,15:3m,30:3m the user sweep, S1 --ladder 15:3m:0,15:3m:0.1,15:3m:1 the sample-rate sweep, S2 --ladder 0:5m the idle floor, S0 + --ladder 200:3m@1000,200:3m@2000 a rate sweep at 200 users Name this file in the `file` of the test, after the test itself: @@ -45,6 +46,16 @@ the step change makes. boomer fetches that endpoint on each spawn message, and a step change is a spawn message. +A step that names a target rate works the same way: the rate applies as the +step starts and stays until another step changes it, and `--target-rps` gives +the rate before the first step that names one. Only a user class that paces +reads it (resumepaused), and it caps the requests per second of the whole +worker. The users must be enough to reach it, as each waits for its whole +iteration to return: for resumepaused, a resume plus a pause. +A step that holds the number of users makes no spawn message, thus boomer +reads the new rate at its next poll of /boomer-config, within 10 s; trim the +start of each step. `0` removes the cap. + Use a boomer user class for a ladder. Python and gRPC hold each other at a high number of users, and the latency of that condition is the latency of the load generator and not of substrate. @@ -58,17 +69,19 @@ logger = logging.getLogger(__name__) -# users:duration[:trace_probability] +# users:duration[:trace_probability][@target_rps] _STEP_RE = re.compile( r"^(?P\d+):(?P\d+)(?P[smh]?)" - r"(?::(?P[0-9.]+))?$" + r"(?::(?P[0-9.]+))?" + r"(?:@(?P[0-9.]+))?$" ) _UNIT_SECONDS = {"s": 1, "m": 60, "h": 3600} # A step of the ladder holds one number of users, for one period, at one -# sample rate. `probability` is None when the step does not change the rate. -Step = tuple[int, int, float | None] +# sample rate and one target request rate. `probability` and `rps` are None +# when the step does not change them. +Step = tuple[int, int, float | None, float | None] def parse_ladder(spec: str) -> list[Step]: @@ -84,27 +97,33 @@ def parse_ladder(spec: str) -> list[Step]: if not m: raise ValueError( f"unrecognized ladder step {item!r}; " - f"the form is users:duration[:trace_probability], " - f"as in 15:3m or 15:3m:0.1" + f"the form is users:duration[:trace_probability][@target_rps], " + f"as in 15:3m, 15:3m:0.1, or 200:3m@1000" ) seconds = int(m.group("duration")) * _UNIT_SECONDS[m.group("unit") or "s"] if seconds <= 0: raise ValueError(f"ladder step {item!r} has no duration") - probability = m.group("probability") - if probability is None: - steps.append((int(m.group("users")), seconds, None)) - continue - value = float(probability) - if not 0.0 <= value <= 1.0: + probability = _parse_float(item, m.group("probability"), "trace probability") + if probability is not None and not 0.0 <= probability <= 1.0: raise ValueError( f"ladder step {item!r} has a trace probability outside 0.0 to 1.0" ) - steps.append((int(m.group("users")), seconds, value)) + rps = _parse_float(item, m.group("rps"), "target rate") + steps.append((int(m.group("users")), seconds, probability, rps)) if not steps: raise ValueError("--ladder has no steps") return steps +def _parse_float(item: str, value: str | None, what: str) -> float | None: + if value is None: + return None + try: + return float(value) + except ValueError: + raise ValueError(f"ladder step {item!r} has a {what} that is not a number") + + @events.init_command_line_parser.add_listener def _(parser: LocustArgumentParser) -> None: parser.add_argument( @@ -114,8 +133,8 @@ def _(parser: LocustArgumentParser) -> None: env_var="LOCUST_LADDER", help=( "Run a ladder of steps in one test, as " - "users:duration[:trace_probability],... For example " - "5:3m,15:3m,30:3m" + "users:duration[:trace_probability][@target_rps],... For " + "example 5:3m,15:3m,30:3m" ), include_in_web_ui=True, ) @@ -142,6 +161,11 @@ def _set_trace_probability(environment, probability: float) -> None: set_trace_probability(probability) +def _set_target_rps(environment, rps: float) -> None: + """Apply `rps` to the parsed options, which /boomer-config serves.""" + environment.parsed_options.target_rps = rps + + class LadderShape(LoadTestShape): """Holds each step of `--ladder` in turn, then stops the run. @@ -165,7 +189,7 @@ def _load(self) -> bool: self._steps = parse_ladder(spec) if spec else [] elapsed = 0.0 self._boundaries = [] - for _, seconds, _ in self._steps: + for _, seconds, _, _ in self._steps: elapsed += seconds self._boundaries.append(elapsed) if self._steps: @@ -190,19 +214,22 @@ def tick(self) -> tuple[int, float] | None: logger.info("Ladder complete after %.0fs", run_time) return None - users, _, probability = self._steps[index] + users, _, probability, rps = self._steps[index] spawn_rate = self.runner.environment.parsed_options.ladder_spawn_rate if index != self._current: self._current = index if probability is not None: _set_trace_probability(self.runner.environment, probability) + if rps is not None: + _set_target_rps(self.runner.environment, rps) logger.info( - "Ladder step %d/%d: users=%d trace_probability=%s", + "Ladder step %d/%d: users=%d trace_probability=%s target_rps=%s", index + 1, len(self._steps), users, "unchanged" if probability is None else probability, + "unchanged" if rps is None else rps, ) return (users, spawn_rate) diff --git a/benchmarking/locust/tests/resumepaused.py b/benchmarking/locust/tests/resumepaused.py new file mode 100644 index 0000000000..431ced1e83 --- /dev/null +++ b/benchmarking/locust/tests/resumepaused.py @@ -0,0 +1,49 @@ +# 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. + +"""Stub ResumePausedUser declaration. + +The real load implementation lives in the boomer-Go worker at +internal/benchmarking/boomer/resumepaused/ (registered by the boomer-worker +binary in cmd/benchmarking/boomer-worker/); this Python class is declared +only so the master recognizes the name and attributes boomer's stats rows +to it. A Python worker container sets LOCUST_NO_RESUMEPAUSED_USER=1 to skip +loading this file, leaving boomer as the sole owner of ResumePausedUser load. + +Each user creates one actor and makes it cold on its first iteration, then +resumes and pauses it again on every later one, with no wait. Pace the cold +resumes with --target-rps, or with @target_rps steps of a --ladder. +""" + +import os + +if os.environ.get("LOCUST_NO_RESUMEPAUSED_USER") != "1": + from locust import User, task + from common.boomer_config import init_boomer_config + + # Master serves /boomer-config so the boomer-worker workers can fetch + # runtime flag values (target rate, trace probability). No-op on + # workers without a web UI. + init_boomer_config() + + class ResumePausedUser(User): + host = "api.ate-system.svc.cluster.local:443" + + @task + def noop(self) -> None: + # Unreached under normal operation: the Python worker container + # does not load this file (LOCUST_NO_RESUMEPAUSED_USER=1). Body is + # required because locust validates that every User has at least + # one @task method. + pass diff --git a/benchmarking/locust/unit_tests/test_ladder_shape.py b/benchmarking/locust/unit_tests/test_ladder_shape.py new file mode 100644 index 0000000000..25034b1cd0 --- /dev/null +++ b/benchmarking/locust/unit_tests/test_ladder_shape.py @@ -0,0 +1,93 @@ +# 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. + +"""Unit tests for shapes/ladder_shape.py. + +Run via: python3 benchmarking/locust/unit_tests/test_ladder_shape.py +Needs locust: pip install -r benchmarking/locust/requirements.txt +""" + +import sys +import unittest +from pathlib import Path +from types import SimpleNamespace + +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) + +from shapes.ladder_shape import LadderShape, parse_ladder + + +class ParseLadderTest(unittest.TestCase): + def test_users_and_duration(self): + self.assertEqual( + parse_ladder("5:3m,15:90s,30:1h"), + [(5, 180, None, None), (15, 90, None, None), (30, 3600, None, None)], + ) + + def test_trace_probability(self): + self.assertEqual(parse_ladder("15:3m:0.1"), [(15, 180, 0.1, None)]) + + def test_target_rps(self): + self.assertEqual( + parse_ladder("200:3m@1000, 200:3m@2500.5"), + [(200, 180, None, 1000.0), (200, 180, None, 2500.5)], + ) + + def test_probability_and_target_rps(self): + self.assertEqual(parse_ladder("200:3m:0@0"), [(200, 180, 0.0, 0.0)]) + + def test_invalid(self): + for bad in ("", "200", "200:3m@", "200:3m@x", "200:0m@100", + "200:3m@1.2.3", "200:3m:2@100", "200:3m@100:0.1"): + with self.assertRaises(ValueError, msg=repr(bad)): + parse_ladder(bad) + + +class TickTest(unittest.TestCase): + def shape(self, ladder, run_time): + options = SimpleNamespace( + ladder=ladder, ladder_spawn_rate=5.0, target_rps=0.0 + ) + shape = LadderShape() + shape.runner = SimpleNamespace( + environment=SimpleNamespace(parsed_options=options) + ) + shape.get_run_time = lambda: run_time[0] + return shape, options + + def test_step_sets_target_rps_and_keeps_it(self): + run_time = [0.0] + shape, options = self.shape("200:60s@1000,100:60s,200:60s@0", run_time) + + self.assertEqual(shape.tick(), (200, 5.0)) + self.assertEqual(options.target_rps, 1000.0) + + run_time[0] = 61.0 + self.assertEqual(shape.tick(), (100, 5.0)) + self.assertEqual(options.target_rps, 1000.0) + + run_time[0] = 121.0 + self.assertEqual(shape.tick(), (200, 5.0)) + self.assertEqual(options.target_rps, 0.0) + + run_time[0] = 181.0 + self.assertIsNone(shape.tick()) + + def test_no_ladder_is_inert(self): + shape, _ = self.shape("", [0.0]) + self.assertIsNone(shape.tick()) + + +if __name__ == "__main__": + unittest.main() diff --git a/cmd/benchmarking/boomer-worker/main.go b/cmd/benchmarking/boomer-worker/main.go index b7e48a7ec0..d4e1acbbf4 100644 --- a/cmd/benchmarking/boomer-worker/main.go +++ b/cmd/benchmarking/boomer-worker/main.go @@ -38,6 +38,7 @@ import ( // Register user classes via init(): "github.com/agent-substrate/substrate/internal/benchmarking/boomer/agentsession" _ "github.com/agent-substrate/substrate/internal/benchmarking/boomer/glutton" + _ "github.com/agent-substrate/substrate/internal/benchmarking/boomer/resumepaused" _ "github.com/agent-substrate/substrate/internal/benchmarking/boomer/sweperf" ) @@ -47,7 +48,7 @@ func main() { routerURL = flag.String("router-url", "http://atenet-router.ate-system.svc.cluster.local", "atenet HTTP router base URL (no trailing slash).") atespace = flag.String("atespace", "benchmark", "Atespace every actor this worker creates lives in. Ensured (CreateAtespace, AlreadyExists is ok) at startup.") promAddr = flag.String("prometheus-addr", ":8001", "Address for the Prometheus /metrics endpoint.") - configJSON = flag.String("config-json", "", "Initial dynconfig as a JSON object (keys: trace_probability, min_wait_time, max_wait_time, min_live_time, max_live_time in seconds, durdir_file_size_bytes, resume_mode, lifecycle_mode, durdir_read_mode, durdir_template, mem_target, mem_churn, mem_read, cpu_cores, cpu_duty_cycle, max_pings_per_wake). Unset fields keep their built-in defaults.") + configJSON = flag.String("config-json", "", "Initial dynconfig as a JSON object (keys: trace_probability, min_wait_time, max_wait_time, min_live_time, max_live_time in seconds, durdir_file_size_bytes, resume_mode, lifecycle_mode, durdir_read_mode, durdir_template, mem_target, mem_churn, mem_read, cpu_cores, cpu_duty_cycle, max_pings_per_wake, target_rps, total_actors). Unset fields keep their built-in defaults.") masterWebPort = flag.Int("master-web-port", 0, "If non-zero, fetch dynconfig from http://{master-host}:{master-web-port}/boomer-config on each spawn message. Exits if the first fetch fails; later failures keep the last fetched values. {master-host} comes from boomer's existing --master-host flag.") configPollInterval = flag.Duration("config-poll-interval", 10*time.Second, "With --master-web-port, also fetch dynconfig on this interval. A spawn message comes only when the number of users or the spawn rate changes, thus a load shape that changes the sample rate alone needs this. Zero stops the polling.") userClass = flag.String("user-class", "glutton", fmt.Sprintf("Locust user class to run, lowercase; one of %s.", strings.Join(userclass.Names(), "|"))) diff --git a/go.mod b/go.mod index d1b750a752..0594da0cf5 100644 --- a/go.mod +++ b/go.mod @@ -64,6 +64,7 @@ require ( golang.org/x/net v0.60.0 golang.org/x/sync v0.23.0 golang.org/x/sys v0.48.0 + golang.org/x/time v0.15.0 google.golang.org/api v0.274.0 google.golang.org/genproto/googleapis/rpc v0.0.0-20260803160001-6ac0973c030d google.golang.org/grpc v1.83.2 @@ -245,7 +246,6 @@ require ( golang.org/x/oauth2 v0.36.0 // indirect golang.org/x/term v0.46.0 // indirect golang.org/x/text v0.42.0 // indirect - golang.org/x/time v0.15.0 // indirect gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect gonum.org/v1/gonum v0.17.0 // indirect google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7 // indirect diff --git a/internal/benchmarking/boomer/dynconfig/dynconfig.go b/internal/benchmarking/boomer/dynconfig/dynconfig.go index 6280dddd9d..8abf1fadee 100644 --- a/internal/benchmarking/boomer/dynconfig/dynconfig.go +++ b/internal/benchmarking/boomer/dynconfig/dynconfig.go @@ -85,6 +85,8 @@ type Config struct { TotalActors int // spawn batch size; 0 keeps --total-actors SpawnConcurrency int // actors the spawn batch creates concurrently; 0 keeps --spawn-concurrency ActorDeadline time.Duration // per-actor timeout in the spawn batch; 0 keeps --actor-deadline + + TargetRPS float64 // requests per second the whole worker paces to, for user classes that pace; 0 means unpaced } // Holder lets readers Load() the current Config and writers Store() a new @@ -143,6 +145,8 @@ type payload struct { TotalActors *float64 `json:"total_actors"` SpawnConcurrency *float64 `json:"spawn_concurrency"` ActorDeadline *float64 `json:"actor_deadline"` + + TargetRPS *float64 `json:"target_rps"` } // Parse decodes a JSON blob (typically from a CLI flag) and merges its @@ -254,6 +258,9 @@ func (c Config) Validate() error { if c.ActorDeadline < 0 { return fmt.Errorf("actor_deadline cannot be negative: %v", c.ActorDeadline) } + if c.TargetRPS < 0 { + return fmt.Errorf("target_rps cannot be negative: %v", c.TargetRPS) + } // MaxPingsPerWake < 1 is treated as 1 at read time (see iterate() in // glutton/lifecycle.go), so Config's zero value stays usable — no // validate rejection here. @@ -347,6 +354,9 @@ func (p payload) merge(current Config) Config { if p.ActorDeadline != nil { out.ActorDeadline = time.Duration(*p.ActorDeadline * float64(time.Second)) } + if p.TargetRPS != nil { + out.TargetRPS = *p.TargetRPS + } return out } @@ -431,6 +441,7 @@ func StartPoll( slog.Int("total_actors", next.TotalActors), slog.Int("spawn_concurrency", next.SpawnConcurrency), slog.Duration("actor_deadline", next.ActorDeadline), + slog.Float64("target_rps", next.TargetRPS), ) } } @@ -486,6 +497,7 @@ func SubscribeSpawn(url string, holder *Holder, sampler ProbabilityUpdater, fetc slog.Int("total_actors", next.TotalActors), slog.Int("spawn_concurrency", next.SpawnConcurrency), slog.Duration("actor_deadline", next.ActorDeadline), + slog.Float64("target_rps", next.TargetRPS), ) }) } diff --git a/internal/benchmarking/boomer/dynconfig/dynconfig_test.go b/internal/benchmarking/boomer/dynconfig/dynconfig_test.go index 2f1390d888..8479594179 100644 --- a/internal/benchmarking/boomer/dynconfig/dynconfig_test.go +++ b/internal/benchmarking/boomer/dynconfig/dynconfig_test.go @@ -49,7 +49,8 @@ func TestParseValid(t *testing.T) { "agentsession_script_file": "/etc/agentsession/script.yaml", "total_actors": 50, "spawn_concurrency": 5, - "actor_deadline": 60.0 + "actor_deadline": 60.0, + "target_rps": 2500.5 }`) cfg, err := Parse(jsonBlob, Config{}) @@ -120,6 +121,9 @@ func TestParseValid(t *testing.T) { if cfg.ActorDeadline != 60*time.Second { t.Errorf("ActorDeadline: got %v, want 60s", cfg.ActorDeadline) } + if cfg.TargetRPS != 2500.5 { + t.Errorf("TargetRPS: got %v, want 2500.5", cfg.TargetRPS) + } } func TestParseInvalidValues(t *testing.T) { @@ -199,6 +203,10 @@ func TestParseInvalidValues(t *testing.T) { name: "negative sweperf num cycles", json: `{"sweperf_num_cycles": -1}`, }, + { + name: "negative target rps", + json: `{"target_rps": -1}`, + }, { name: "negative sweperf poll interval", json: `{"sweperf_poll_interval_ms": -1}`, diff --git a/internal/benchmarking/boomer/metrics/prom.go b/internal/benchmarking/boomer/metrics/prom.go index c4f7b099fb..34b811a8f2 100644 --- a/internal/benchmarking/boomer/metrics/prom.go +++ b/internal/benchmarking/boomer/metrics/prom.go @@ -86,18 +86,31 @@ func Serve(ctx context.Context, addr string) error { // RecordSuccess reports a successful request to both boomer (→ locust master // via ZMQ) and the local Prometheus surface. func RecordSuccess(method, name, userClass string, latency time.Duration, responseBytes int64) { - ms := latency.Milliseconds() - boomer.RecordSuccess(method, name, ms, responseBytes) - requestsTotal.WithLabelValues(method, name, "success", userClass).Inc() - requestDuration.WithLabelValues(method, name, "success", userClass).Observe(float64(ms)) + boomer.RecordSuccess(method, name, latency.Milliseconds(), responseBytes) + RecordPromSuccess(method, name, userClass, latency) } // RecordFailure reports a failed request to both boomer and Prometheus. func RecordFailure(method, name, userClass string, latency time.Duration, errMsg string) { - ms := latency.Milliseconds() - boomer.RecordFailure(method, name, ms, errMsg) + boomer.RecordFailure(method, name, latency.Milliseconds(), errMsg) + RecordPromFailure(method, name, userClass, latency) +} + +// RecordPromSuccess reports a successful request to Prometheus only. Use it +// once boomer.Run has returned: boomer may have stopped draining its stats +// channel by then, and a send to it blocks once the channel's buffer fills. +func RecordPromSuccess(method, name, userClass string, latency time.Duration) { + ms := float64(latency.Milliseconds()) + requestsTotal.WithLabelValues(method, name, "success", userClass).Inc() + requestDuration.WithLabelValues(method, name, "success", userClass).Observe(ms) +} + +// RecordPromFailure reports a failed request to Prometheus only, as +// RecordPromSuccess does a successful one. +func RecordPromFailure(method, name, userClass string, latency time.Duration) { + ms := float64(latency.Milliseconds()) requestsTotal.WithLabelValues(method, name, "failure", userClass).Inc() - requestDuration.WithLabelValues(method, name, "failure", userClass).Observe(float64(ms)) + requestDuration.WithLabelValues(method, name, "failure", userClass).Observe(ms) } // UpdateUsers shifts the active-users gauge for a class by delta (positive on diff --git a/internal/benchmarking/boomer/resumepaused/resumepaused.go b/internal/benchmarking/boomer/resumepaused/resumepaused.go new file mode 100644 index 0000000000..4967d63544 --- /dev/null +++ b/internal/benchmarking/boomer/resumepaused/resumepaused.go @@ -0,0 +1,446 @@ +// 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 resumepaused drives ate-api-server's cold resume path: every +// ResumeActor targets a paused actor, so ateapi takes the actor lease, binds +// a worker, and waits on an atelet restore. Each resume is followed by a +// pause that makes the actor cold again. Run on the fake data plane +// (benchmarking/workloads/deploy.sh --fake-data-plane), it measures what a +// cold resume costs ateapi and Postgres without the router or the restore in +// the way. The pauses run at the resume rate, so a capacity it finds is one +// of resume and pause cycles, not of resumes alone. It sends the actors +// themselves no requests, since none exist there. +package resumepaused + +import ( + "context" + "errors" + "fmt" + "log/slog" + "maps" + "os" + "slices" + "sync" + "sync/atomic" + "time" + + "github.com/agent-substrate/substrate/internal/benchmarking/boomer/boomerutil" + bmetrics "github.com/agent-substrate/substrate/internal/benchmarking/boomer/metrics" + "github.com/agent-substrate/substrate/internal/benchmarking/boomer/userclass" + "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "github.com/google/uuid" + "go.opentelemetry.io/otel" + "golang.org/x/time/rate" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +const userClass = "ResumePausedUser" + +func init() { + userclass.Add(userclass.Entry{ + Name: "resumepaused", + LocustFile: "resumepaused.py", + UserClass: userClass, + Init: initResumePaused, + }) +} + +// initResumePaused ensures the atespace and returns the task. Each user +// creates and warms its own actor on its first iteration. +func initResumePaused(cfg *userclass.Config) (taskFn func(), shutdown func(context.Context)) { + if cfg.Tracer == nil { + cfg.Tracer = otel.Tracer("substrate-boomer/resumepaused") + } + rt := newRuntime(cfg) + if err := rt.start(context.Background()); err != nil { + slog.Error("fatal: resumepaused", slog.String("err", err.Error())) + os.Exit(1) + } + return rt.iterate, rt.shutdown +} + +type runtime struct { + cfg *userclass.Config + // users maps a boomer goroutine to the actor it cycles. Boomer gives a + // task no user identity, so the goroutine stands in for the user. Go + // never reuses a goroutine ID, so the entry of a user boomer stopped is + // never looked up again; its actor stays paused until shutdown. + users sync.Map // goroutineID -> actor name + // actorsMu guards actors, every actor that may exist, for shutdown. + actorsMu sync.Mutex + actors map[string]struct{} + // limiter paces the resumes of every user. Its burst of 1 spaces them + // evenly: Wait hands each caller the next slot rather than releasing a + // second's budget at once. + limiter *rate.Limiter + // retryBackoff spaces the retries of a delete. + retryBackoff time.Duration + // setupRetryWait is how long a user waits after its actor failed to + // warm, so a failing setup does not loop on boomer's zero-delay + // re-entry. + setupRetryWait time.Duration + // cycleMu guards closing and every cycles.Add, so no cycle or setup + // starts once shutdown has begun waiting for the running ones. + cycleMu sync.Mutex + closing bool + cycles sync.WaitGroup + // cycleCtx carries every cycle's and setup's RPCs; shutdown cancels it + // when the running ones outlast their share of its budget. + cycleCtx context.Context + cancelCycles context.CancelFunc + // stopped is set when shutdown begins, after boomer.Run has returned. + // Boomer may have stopped draining its stats channel by then, so a + // sample sent to it could block for good once the buffer fills: from + // then on, samples go to Prometheus only. + stopped atomic.Bool + // recordSuccess and recordFailure send a sample to both locust and + // Prometheus while boomer runs. + recordSuccess func(method, name, userClass string, latency time.Duration, responseBytes int64) + recordFailure func(method, name, userClass string, latency time.Duration, errMsg string) +} + +func newRuntime(cfg *userclass.Config) *runtime { + cycleCtx, cancelCycles := context.WithCancel(context.Background()) + return &runtime{ + cfg: cfg, + actors: map[string]struct{}{}, + limiter: rate.NewLimiter(rate.Inf, 1), + retryBackoff: time.Second, + setupRetryWait: time.Second, + cycleCtx: cycleCtx, + cancelCycles: cancelCycles, + recordSuccess: bmetrics.RecordSuccess, + recordFailure: bmetrics.RecordFailure, + } +} + +// start ensures the atespace every actor is created in. +func (r *runtime) start(ctx context.Context) error { + if err := r.ensureAtespace(ctx); err != nil { + return fmt.Errorf("while ensuring atespace %s: %w", r.cfg.Atespace, err) + } + return nil +} + +// iterate is the boomer task. A user's first iteration creates its actor and +// makes it cold; every later one runs a measured cycle on that actor. +func (r *runtime) iterate() { + gid := boomerutil.GoroutineID() + if v, ok := r.users.Load(gid); ok { + r.cycle(v.(string)) + return + } + name, ok := r.setUp() + if !ok { + time.Sleep(r.setupRetryWait) + return + } + r.users.Store(gid, name) +} + +// setUp creates one actor and makes it cold. It is not paced, so its calls +// land in the step that added the user, recorded under their own names. An +// actor that fails to warm is deleted, and the user tries again on its next +// iteration. +func (r *runtime) setUp() (string, bool) { + if !r.startCycle() { + return "", false + } + defer r.cycles.Done() + ctx := r.cycleCtx + name := "rp-" + uuid.NewString() + if err := r.create(ctx, name); err != nil { + return "", false + } + r.track(name) + if err := r.warm(ctx, name); err != nil { + slog.Warn("resumepaused: actor did not warm; the user will retry", + slog.String("actor", name), slog.String("err", err.Error())) + if r.delete(ctx, name) == nil { + r.untrack(name) + } + return "", false + } + return name, true +} + +func (r *runtime) track(name string) { + r.actorsMu.Lock() + defer r.actorsMu.Unlock() + r.actors[name] = struct{}{} +} + +func (r *runtime) untrack(name string) { + r.actorsMu.Lock() + defer r.actorsMu.Unlock() + delete(r.actors, name) +} + +// The template every actor is created from. +const ( + templateName = "glutton" + templateAtespace = "benchmark-workloads" +) + +// create creates one actor from the template. +func (r *runtime) create(ctx context.Context, name string) error { + return r.call(ctx, "CreateActor", func(ctx context.Context) error { + _, err := r.cfg.APIStub.CreateActor(ctx, &ateapipb.CreateActorRequest{ + Actor: &ateapipb.Actor{ + Metadata: &ateapipb.ResourceMetadata{Atespace: r.cfg.Atespace, Name: name}, + ActorTemplate: &ateapipb.ObjectRef{Atespace: templateAtespace, Name: templateName}, + }, + }) + return err + }) +} + +// warm makes a new actor cold: a first resume, then a pause. +func (r *runtime) warm(ctx context.Context, name string) error { + if err := r.resume(ctx, "ResumeActorWarmup", name); err != nil { + return err + } + return r.pause(ctx, "Warmup", name) +} + +// cycle is one paced cold resume of the user's actor, then the pause that +// makes it cold again. Only the resume is paced, so the target rate is cold +// resumes per second. +func (r *runtime) cycle(name string) { + ctx := r.cycleCtx + r.pace(ctx) + if !r.startCycle() { + return + } + defer r.cycles.Done() + err := r.resume(ctx, "ResumeActorCold", name) + if err == nil { + err = r.pause(ctx, "", name) + } + if err != nil { + // The actor may have been left running. Pausing it again makes it + // cold for the next cycle. + r.recover(ctx, name) + } +} + +// recover pauses an actor after a failed cycle, so it is cold for the next +// one. It is load on the server like any other call, so it is recorded, as +// PauseActorRecovery to keep it apart from the measured pauses. An actor +// already paused fails it with FailedPrecondition; the actor is cold, which +// is all recovery wants, so that counts as a success. +func (r *runtime) recover(ctx context.Context, name string) { + _ = r.call(ctx, "PauseActorRecovery", func(ctx context.Context) error { + _, err := r.cfg.APIStub.PauseActor(ctx, &ateapipb.PauseActorRequest{ + Actor: r.ref(name), + }) + if status.Code(err) == codes.FailedPrecondition { + return nil + } + return err + }) +} + +// startCycle registers a cycle or setup with shutdown, or reports false once +// shutdown has begun. +func (r *runtime) startCycle() bool { + r.cycleMu.Lock() + defer r.cycleMu.Unlock() + if r.closing { + return false + } + r.cycles.Add(1) + return true +} + +// pace blocks until the limiter grants the next resume slot, applying the +// current TargetRPS first. Zero means unpaced. +func (r *runtime) pace(ctx context.Context) { + want := rate.Inf + if target := r.cfg.Dyn.Load().TargetRPS; target > 0 { + want = rate.Limit(target) + } + if r.limiter.Limit() != want { + r.limiter.SetLimit(want) + } + _ = r.limiter.Wait(ctx) +} + +// errAlreadyRunning marks a ResumeActor that found its actor running, so no +// cold resume happened and the sample must not count as one. +var errAlreadyRunning = errors.New("actor was already running: not a cold resume") + +// resume calls ResumeActor. A response that reports no resume happened fails +// the sample with errAlreadyRunning. +func (r *runtime) resume(ctx context.Context, metricName, name string) error { + return r.call(ctx, metricName, func(ctx context.Context) error { + resp, err := r.cfg.APIStub.ResumeActor(ctx, &ateapipb.ResumeActorRequest{ + Actor: r.ref(name), + }) + if err == nil && !resp.GetResumed() { + return errAlreadyRunning + } + return err + }) +} + +// pause calls PauseActor. suffix keeps warm-up calls apart from the measured +// ones. +func (r *runtime) pause(ctx context.Context, suffix, name string) error { + return r.call(ctx, "PauseActor"+suffix, func(ctx context.Context) error { + _, err := r.cfg.APIStub.PauseActor(ctx, &ateapipb.PauseActorRequest{ + Actor: r.ref(name), + }) + return err + }) +} + +// shutdown waits for the running cycles and setups, then deletes every +// actor. Boomer stops scheduling tasks before shutdown but does not wait for +// the ones in flight, and a delete that races one of them loses the actor +// lease and fails with Aborted. The running ones get half of ctx's remaining +// time; any still running then are canceled, so a hung RPC cannot spend the +// time the deletes need. DeleteActor with AnyState removes an actor in any +// state, so a canceled cycle or setup still leaves nothing behind. +func (r *runtime) shutdown(ctx context.Context) { + r.stopped.Store(true) + r.cycleMu.Lock() + r.closing = true + r.cycleMu.Unlock() + drained := make(chan struct{}) + go func() { + r.cycles.Wait() + close(drained) + }() + drainCtx := ctx + if deadline, ok := ctx.Deadline(); ok { + var cancel context.CancelFunc + drainCtx, cancel = context.WithTimeout(ctx, time.Until(deadline)/2) + defer cancel() + } + select { + case <-drained: + case <-drainCtx.Done(): + r.cancelCycles() + select { + case <-drained: + case <-ctx.Done(): + } + } + defer r.cancelCycles() + + r.actorsMu.Lock() + names := slices.Collect(maps.Keys(r.actors)) + r.actors = map[string]struct{}{} + r.actorsMu.Unlock() + + const parallelism = 16 + sem := make(chan struct{}, parallelism) + var wg sync.WaitGroup + var leftMu sync.Mutex + left := 0 + for _, name := range names { + wg.Add(1) + sem <- struct{}{} + go func() { + defer wg.Done() + defer func() { <-sem }() + if r.delete(ctx, name) != nil { + leftMu.Lock() + left++ + leftMu.Unlock() + } + }() + } + wg.Wait() + if left > 0 { + slog.Warn("resumepaused: actors left behind after shutdown", + slog.Int("count", left), slog.String("atespace", r.cfg.Atespace)) + } +} + +// delete deletes an actor, retrying while another operation holds its lease. +func (r *runtime) delete(ctx context.Context, name string) error { + const attempts = 5 + var err error + for attempt := range attempts { + if attempt > 0 { + select { + case <-time.After(r.retryBackoff): + case <-ctx.Done(): + return ctx.Err() + } + } + err = r.call(ctx, "DeleteActor", func(ctx context.Context) error { + _, err := r.cfg.APIStub.DeleteActor(ctx, &ateapipb.DeleteActorRequest{ + Actor: r.ref(name), + AnyState: true, + }) + return err + }) + if c := status.Code(err); c != codes.Aborted && c != codes.Unavailable { + return err + } + } + return err +} + +func (r *runtime) ref(name string) *ateapipb.ObjectRef { + return &ateapipb.ObjectRef{Atespace: r.cfg.Atespace, Name: name} +} + +func (r *runtime) ensureAtespace(ctx context.Context) error { + return r.call(ctx, "CreateAtespace", func(ctx context.Context) error { + _, err := r.cfg.APIStub.CreateAtespace(ctx, &ateapipb.CreateAtespaceRequest{ + Atespace: &ateapipb.Atespace{ + Metadata: &ateapipb.ResourceMetadata{Name: r.cfg.Atespace}, + }, + }) + if status.Code(err) == codes.AlreadyExists { + return nil + } + return err + }) +} + +// call times one unary RPC and reports its latency to locust and Prometheus. +func (r *runtime) call(ctx context.Context, name string, do func(context.Context) error) error { + ctx, span := r.cfg.Tracer.Start(ctx, name) + defer span.End() + + start := time.Now() + err := do(ctx) + latency := time.Since(start) + + boomerutil.LogSampledTrace(span, name, latency, boomerutil.SourceClient, err) + r.record(name, latency, err) + return err +} + +// record reports one sample: to locust and Prometheus while boomer runs, to +// Prometheus only once shutdown has begun. +func (r *runtime) record(name string, latency time.Duration, err error) { + stopped := r.stopped.Load() + switch { + case err != nil && stopped: + bmetrics.RecordPromFailure("grpc", name, userClass, latency) + case err != nil: + r.recordFailure("grpc", name, userClass, latency, err.Error()) + case stopped: + bmetrics.RecordPromSuccess("grpc", name, userClass, latency) + default: + r.recordSuccess("grpc", name, userClass, latency, 0) + } +} diff --git a/internal/benchmarking/boomer/resumepaused/resumepaused_test.go b/internal/benchmarking/boomer/resumepaused/resumepaused_test.go new file mode 100644 index 0000000000..c054681e51 --- /dev/null +++ b/internal/benchmarking/boomer/resumepaused/resumepaused_test.go @@ -0,0 +1,749 @@ +// 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 resumepaused + +import ( + "context" + "maps" + "net/http" + "net/http/httptest" + "slices" + "strconv" + "strings" + "sync" + "sync/atomic" + "testing" + "time" + + "github.com/agent-substrate/substrate/internal/benchmarking/boomer/dynconfig" + "github.com/agent-substrate/substrate/internal/benchmarking/boomer/userclass" + "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "github.com/prometheus/client_golang/prometheus/promhttp" + "go.opentelemetry.io/otel" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +// fakeControlClient keeps each actor's state, so a resume of a running actor +// reports Resumed false the way ateapi does. +type fakeControlClient struct { + ateapipb.ControlClient + mu sync.Mutex + calls []string // " " + states map[string]ateapipb.ActorState + // resumeErr, when set, fails every ResumeActor without changing state. + resumeErr error + // resumeFailures is how many ResumeActor calls fail with Unavailable, + // without changing state, before resumes succeed. + resumeFailures int + // pauseErr, when set, fails every PauseActor without changing state. + pauseErr error + // atespaceErr, when set, fails CreateAtespace. + atespaceErr error + // createErr, when set, fails every CreateActor without creating it. + createErr error + // deleteAborts is how many DeleteActor calls fail with Aborted, as when + // another operation holds the actor lease, before deletes succeed. + deleteAborts int + // resumeEntered and resumeGate, when set, hold every ResumeActor: it + // signals resumeEntered and then waits for resumeGate to close or its + // context to end. + resumeEntered chan struct{} + resumeGate chan struct{} +} + +func newFake() *fakeControlClient { + return &fakeControlClient{states: map[string]ateapipb.ActorState{}} +} + +func (f *fakeControlClient) record(rpc, actor string) { + f.calls = append(f.calls, rpc+" "+actor) +} + +func (f *fakeControlClient) setState(name string, s ateapipb.ActorState) { + f.mu.Lock() + defer f.mu.Unlock() + f.states[name] = s +} + +func (f *fakeControlClient) state(name string) ateapipb.ActorState { + f.mu.Lock() + defer f.mu.Unlock() + return f.states[name] +} + +func (f *fakeControlClient) CreateAtespace(ctx context.Context, in *ateapipb.CreateAtespaceRequest, opts ...grpc.CallOption) (*ateapipb.Atespace, error) { + f.mu.Lock() + defer f.mu.Unlock() + f.record("CreateAtespace", in.GetAtespace().GetMetadata().GetName()) + if f.atespaceErr != nil { + return nil, f.atespaceErr + } + return &ateapipb.Atespace{}, nil +} + +func (f *fakeControlClient) CreateActor(ctx context.Context, in *ateapipb.CreateActorRequest, opts ...grpc.CallOption) (*ateapipb.Actor, error) { + f.mu.Lock() + defer f.mu.Unlock() + name := in.GetActor().GetMetadata().GetName() + f.record("CreateActor", name) + if f.createErr != nil { + return nil, f.createErr + } + f.states[name] = ateapipb.ActorState_ACTOR_STATE_SUSPENDED + return &ateapipb.Actor{}, nil +} + +func (f *fakeControlClient) GetActor(ctx context.Context, in *ateapipb.GetActorRequest, opts ...grpc.CallOption) (*ateapipb.Actor, error) { + f.mu.Lock() + defer f.mu.Unlock() + name := in.GetActor().GetName() + f.record("GetActor", name) + s, ok := f.states[name] + if !ok { + return nil, status.Error(codes.NotFound, "no such actor") + } + return &ateapipb.Actor{Status: &ateapipb.ActorStatus{State: s}}, nil +} + +func (f *fakeControlClient) ResumeActor(ctx context.Context, in *ateapipb.ResumeActorRequest, opts ...grpc.CallOption) (*ateapipb.ResumeActorResponse, error) { + if f.resumeGate != nil { + f.resumeEntered <- struct{}{} + select { + case <-f.resumeGate: + case <-ctx.Done(): + return nil, status.FromContextError(ctx.Err()).Err() + } + } + f.mu.Lock() + defer f.mu.Unlock() + name := in.GetActor().GetName() + f.record("ResumeActor", name) + if f.resumeErr != nil { + return nil, f.resumeErr + } + if f.resumeFailures > 0 { + f.resumeFailures-- + return nil, status.Error(codes.Unavailable, "down") + } + switch f.states[name] { + case ateapipb.ActorState_ACTOR_STATE_RUNNING: + return &ateapipb.ResumeActorResponse{Resumed: false}, nil + case ateapipb.ActorState_ACTOR_STATE_PAUSED, ateapipb.ActorState_ACTOR_STATE_SUSPENDED: + f.states[name] = ateapipb.ActorState_ACTOR_STATE_RUNNING + return &ateapipb.ResumeActorResponse{Resumed: true}, nil + } + return nil, status.Error(codes.Aborted, "actor crashed") +} + +func (f *fakeControlClient) PauseActor(ctx context.Context, in *ateapipb.PauseActorRequest, opts ...grpc.CallOption) (*ateapipb.PauseActorResponse, error) { + f.mu.Lock() + defer f.mu.Unlock() + name := in.GetActor().GetName() + f.record("PauseActor", name) + if f.pauseErr != nil { + return nil, f.pauseErr + } + if f.states[name] != ateapipb.ActorState_ACTOR_STATE_RUNNING { + return nil, status.Error(codes.FailedPrecondition, "not running") + } + f.states[name] = ateapipb.ActorState_ACTOR_STATE_PAUSED + return &ateapipb.PauseActorResponse{}, nil +} + +func (f *fakeControlClient) DeleteActor(ctx context.Context, in *ateapipb.DeleteActorRequest, opts ...grpc.CallOption) (*ateapipb.Actor, error) { + f.mu.Lock() + defer f.mu.Unlock() + name := in.GetActor().GetName() + f.record("DeleteActor", name) + if f.deleteAborts > 0 { + f.deleteAborts-- + return nil, status.Error(codes.Aborted, "lease held") + } + delete(f.states, name) + return &ateapipb.Actor{}, nil +} + +// callsTo returns the actors of every recorded call to rpc, in order. +func (f *fakeControlClient) callsTo(rpc string) []string { + f.mu.Lock() + defer f.mu.Unlock() + var out []string + for _, c := range f.calls { + if r, actor, _ := strings.Cut(c, " "); r == rpc { + out = append(out, actor) + } + } + return out +} + +func (f *fakeControlClient) resetCalls() { + f.mu.Lock() + defer f.mu.Unlock() + f.calls = nil +} + +func newTestRuntime(fake *fakeControlClient) *runtime { + r := newRuntime(&userclass.Config{ + APIStub: fake, + Atespace: "bench", + Dyn: dynconfig.NewHolder(dynconfig.Config{}), + Tracer: otel.Tracer("test"), + }) + r.retryBackoff = time.Millisecond + r.setupRetryWait = 0 + return r +} + +// setUpUsers runs the first iteration of n users, each on its own goroutine +// as boomer runs them, and returns their actors sorted. +func setUpUsers(t *testing.T, r *runtime, n int) []string { + t.Helper() + var wg sync.WaitGroup + for range n { + wg.Go(r.iterate) + } + wg.Wait() + got := userActors(r) + if len(got) != n { + t.Fatalf("set up %d users, got actors %v", n, got) + } + return got +} + +// userActors returns the actor of every user, sorted. +func userActors(r *runtime) []string { + var out []string + r.users.Range(func(_, v any) bool { + out = append(out, v.(string)) + return true + }) + slices.Sort(out) + return out +} + +// trackedActors returns every actor shutdown would delete, sorted. +func trackedActors(r *runtime) []string { + r.actorsMu.Lock() + defer r.actorsMu.Unlock() + return slices.Sorted(maps.Keys(r.actors)) +} + +func TestRegistered(t *testing.T) { + e, ok := userclass.Lookup("resumepaused") + if !ok { + t.Fatal("resumepaused is not registered") + } + if e.LocustFile != "resumepaused.py" || e.UserClass != userClass { + t.Errorf("got entry %+v", e) + } +} + +func TestStartEnsuresTheAtespace(t *testing.T) { + fake := newFake() + r := newTestRuntime(fake) + if err := r.start(context.Background()); err != nil { + t.Fatalf("start: %v", err) + } + if got := fake.callsTo("CreateAtespace"); !slices.Equal(got, []string{"bench"}) { + t.Errorf("CreateAtespace calls: got %v", got) + } + if got := fake.callsTo("CreateActor"); len(got) != 0 { + t.Errorf("start created actors %v, want none before a user runs", got) + } +} + +func TestStartFailsWhenTheAtespaceCannotBeEnsured(t *testing.T) { + fake := newFake() + fake.atespaceErr = status.Error(codes.PermissionDenied, "denied") + r := newTestRuntime(fake) + if err := r.start(context.Background()); err == nil { + t.Fatal("start succeeded without an atespace") + } +} + +func TestFirstIterationMakesTheUsersActorCold(t *testing.T) { + fake := newFake() + r := newTestRuntime(fake) + cold := requestCount(t, "ResumeActorCold", "success") + + r.iterate() + actors := userActors(r) + if len(actors) != 1 { + t.Fatalf("got user actors %v, want 1", actors) + } + name := actors[0] + want := []string{"CreateActor " + name, "ResumeActor " + name, "PauseActor " + name} + if !slices.Equal(fake.calls, want) { + t.Errorf("calls: got %v, want %v", fake.calls, want) + } + if got := fake.state(name); got != ateapipb.ActorState_ACTOR_STATE_PAUSED { + t.Errorf("actor is %v, want paused", got) + } + if got := trackedActors(r); !slices.Equal(got, actors) { + t.Errorf("tracked %v, want %v", got, actors) + } + if got := requestCount(t, "ResumeActorCold", "success") - cold; got != 0 { + t.Errorf("ResumeActorCold successes grew by %v during setup, want 0", got) + } +} + +func TestIterateResumesThenPausesTheUsersActor(t *testing.T) { + fake := newFake() + r := newTestRuntime(fake) + r.iterate() + name := userActors(r)[0] + fake.resetCalls() + before := requestCount(t, "ResumeActorCold", "success") + + for range 3 { + r.iterate() + } + want := []string{"ResumeActor " + name, "PauseActor " + name} + want = slices.Concat(want, want, want) + if !slices.Equal(fake.calls, want) { + t.Errorf("calls: got %v, want %v", fake.calls, want) + } + if got := requestCount(t, "ResumeActorCold", "success") - before; got != 3 { + t.Errorf("ResumeActorCold successes grew by %v, want 3", got) + } +} + +func TestEachUserCyclesItsOwnActor(t *testing.T) { + fake := newFake() + r := newTestRuntime(fake) + var wg sync.WaitGroup + for range 3 { + wg.Go(func() { + for range 4 { + r.iterate() + } + }) + } + wg.Wait() + + actors := userActors(r) + if len(actors) != 3 { + t.Fatalf("got user actors %v, want 3", actors) + } + if got := fake.callsTo("CreateActor"); len(got) != 3 { + t.Errorf("CreateActor calls = %v, want one per user", got) + } + // Each user warms its actor once, then cycles it three times. + resumes := map[string]int{} + for _, name := range fake.callsTo("ResumeActor") { + resumes[name]++ + } + for _, name := range actors { + if resumes[name] != 4 { + t.Errorf("actor %s resumed %d times, want 4", name, resumes[name]) + } + } +} + +func TestSetupIsNotPaced(t *testing.T) { + fake := newFake() + r := newTestRuntime(fake) + r.cfg.Dyn.Store(dynconfig.Config{TargetRPS: 1}) + start := time.Now() + setUpUsers(t, r, 5) + if elapsed := time.Since(start); elapsed > 500*time.Millisecond { + t.Errorf("setting up 5 users at 1 resume per second took %v, want well under 1s", elapsed) + } +} + +func TestSetupDeletesAnActorThatDoesNotWarmAndRetries(t *testing.T) { + fake := newFake() + fake.resumeFailures = 1 + r := newTestRuntime(fake) + + r.iterate() + if got := userActors(r); len(got) != 0 { + t.Fatalf("user has actor %v after a failed warm-up", got) + } + created, deleted := fake.callsTo("CreateActor"), fake.callsTo("DeleteActor") + if len(created) != 1 || !slices.Equal(deleted, created) { + t.Errorf("created %v, deleted %v, want the one actor deleted", created, deleted) + } + if got := trackedActors(r); len(got) != 0 { + t.Errorf("tracked %v, want none", got) + } + + r.iterate() + actors := userActors(r) + if len(actors) != 1 || actors[0] == created[0] { + t.Fatalf("got user actors %v, want a new one", actors) + } + if got := fake.state(actors[0]); got != ateapipb.ActorState_ACTOR_STATE_PAUSED { + t.Errorf("actor is %v, want paused", got) + } +} + +func TestSetupKeepsTrackOfAnActorItCouldNotDelete(t *testing.T) { + fake := newFake() + fake.resumeErr = status.Error(codes.Unavailable, "down") + fake.deleteAborts = 5 + r := newTestRuntime(fake) + + r.iterate() + created := fake.callsTo("CreateActor") + if got := trackedActors(r); !slices.Equal(got, created) { + t.Errorf("tracked %v, want %v for shutdown to delete", got, created) + } +} + +func TestSetupRetriesAFailedCreate(t *testing.T) { + fake := newFake() + fake.createErr = status.Error(codes.Unavailable, "down") + r := newTestRuntime(fake) + + r.iterate() + if got := fake.callsTo("DeleteActor"); len(got) != 0 { + t.Errorf("deleted %v, want nothing after a failed create", got) + } + if got := trackedActors(r); len(got) != 0 { + t.Errorf("tracked %v, want none", got) + } + + fake.createErr = nil + r.iterate() + if got := userActors(r); len(got) != 1 { + t.Errorf("got user actors %v, want 1", got) + } +} + +func TestIterateFailsAResumeOfARunningActor(t *testing.T) { + fake := newFake() + r := newTestRuntime(fake) + r.iterate() + name := userActors(r)[0] + fake.setState(name, ateapipb.ActorState_ACTOR_STATE_RUNNING) + fake.resetCalls() + before := requestCount(t, "ResumeActorCold", "failure") + recoveries := requestCount(t, "PauseActorRecovery", "success") + + r.iterate() + if got := requestCount(t, "ResumeActorCold", "failure") - before; got != 1 { + t.Errorf("ResumeActorCold failures grew by %v, want 1", got) + } + if got := requestCount(t, "PauseActorRecovery", "success") - recoveries; got != 1 { + t.Errorf("PauseActorRecovery successes grew by %v, want 1", got) + } + // Recovery pauses the running actor. + want := []string{"ResumeActor " + name, "PauseActor " + name} + if !slices.Equal(fake.calls, want) { + t.Errorf("calls: got %v, want %v", fake.calls, want) + } + if got := fake.state(name); got != ateapipb.ActorState_ACTOR_STATE_PAUSED { + t.Errorf("actor is %v, want paused", got) + } +} + +func TestIterateKeepsTheActorAfterAFailedResume(t *testing.T) { + fake := newFake() + r := newTestRuntime(fake) + r.iterate() + name := userActors(r)[0] + fake.resumeErr = status.Error(codes.ResourceExhausted, "no free workers available") + + r.iterate() + if got := userActors(r); !slices.Equal(got, []string{name}) { + t.Errorf("user actors: got %v, want [%s]", got, name) + } + if got := fake.state(name); got != ateapipb.ActorState_ACTOR_STATE_PAUSED { + t.Errorf("actor is %v, want paused", got) + } +} + +func TestIteratePacesResumes(t *testing.T) { + fake := newFake() + r := newTestRuntime(fake) + r.iterate() + + r.cfg.Dyn.Store(dynconfig.Config{TargetRPS: 200}) + start := time.Now() + for range 21 { + r.iterate() + } + // The first resume goes at once; the other 20 take 5 ms each. + if elapsed := time.Since(start); elapsed < 95*time.Millisecond { + t.Errorf("21 resumes at 200 per second took %v, want >= 100ms", elapsed) + } +} + +func TestIterateRecordsTheRecoveryPauseOfAPausedActorAsASuccess(t *testing.T) { + fake := newFake() + r := newTestRuntime(fake) + r.iterate() + name := userActors(r)[0] + fake.resetCalls() + fake.resumeErr = status.Error(codes.Aborted, "lease held") + resumeFailures := requestCount(t, "ResumeActorCold", "failure") + pauseFailures := requestCount(t, "PauseActor", "failure") + recoverySuccesses := requestCount(t, "PauseActorRecovery", "success") + recoveryFailures := requestCount(t, "PauseActorRecovery", "failure") + + // The resume fails with the actor still paused, so the recovery pause + // finds it paused: a success, as the actor is cold. + r.iterate() + if got := fake.callsTo("PauseActor"); !slices.Equal(got, []string{name}) { + t.Errorf("PauseActor calls = %v, want the recovery pause of %s", got, name) + } + if got := requestCount(t, "ResumeActorCold", "failure") - resumeFailures; got != 1 { + t.Errorf("ResumeActorCold failures grew by %v, want 1", got) + } + if got := requestCount(t, "PauseActor", "failure") - pauseFailures; got != 0 { + t.Errorf("PauseActor failures grew by %v, want 0", got) + } + if got := requestCount(t, "PauseActorRecovery", "success") - recoverySuccesses; got != 1 { + t.Errorf("PauseActorRecovery successes grew by %v, want 1", got) + } + if got := requestCount(t, "PauseActorRecovery", "failure") - recoveryFailures; got != 0 { + t.Errorf("PauseActorRecovery failures grew by %v, want 0", got) + } +} + +func TestIterateRecordsAFailedRecoveryPause(t *testing.T) { + fake := newFake() + r := newTestRuntime(fake) + r.iterate() + fake.pauseErr = status.Error(codes.Unavailable, "down") + pauseFailures := requestCount(t, "PauseActor", "failure") + recoveryFailures := requestCount(t, "PauseActorRecovery", "failure") + + // The resume succeeds, the measured pause fails, and so does the + // recovery pause of the running actor. + r.iterate() + if got := requestCount(t, "PauseActor", "failure") - pauseFailures; got != 1 { + t.Errorf("PauseActor failures grew by %v, want 1", got) + } + if got := requestCount(t, "PauseActorRecovery", "failure") - recoveryFailures; got != 1 { + t.Errorf("PauseActorRecovery failures grew by %v, want 1", got) + } +} + +func TestShutdownDeletesEveryActor(t *testing.T) { + fake := newFake() + r := newTestRuntime(fake) + want := setUpUsers(t, r, 3) + r.shutdown(context.Background()) + + got := fake.callsTo("DeleteActor") + slices.Sort(got) + if !slices.Equal(got, want) { + t.Errorf("deleted %v, want %v", got, want) + } +} + +func TestShutdownRetriesAnAbortedDelete(t *testing.T) { + fake := newFake() + r := newTestRuntime(fake) + name := setUpUsers(t, r, 1)[0] + fake.deleteAborts = 2 + r.shutdown(context.Background()) + + if got := fake.callsTo("DeleteActor"); len(got) != 3 { + t.Errorf("DeleteActor calls = %v, want 3 calls", got) + } + if _, err := fake.GetActor(context.Background(), &ateapipb.GetActorRequest{Actor: r.ref(name)}); status.Code(err) != codes.NotFound { + t.Errorf("actor still exists after shutdown: err = %v", err) + } +} + +func TestShutdownWaitsForARunningCycle(t *testing.T) { + fake := newFake() + r := newTestRuntime(fake) + r.iterate() + name := userActors(r)[0] + fake.resumeEntered = make(chan struct{}) + fake.resumeGate = make(chan struct{}) + cycleDone := make(chan struct{}) + go func() { + r.cycle(name) + close(cycleDone) + }() + <-fake.resumeEntered + + shutdownDone := make(chan struct{}) + go func() { + r.shutdown(context.Background()) + close(shutdownDone) + }() + select { + case <-shutdownDone: + t.Fatal("shutdown returned while a cycle was running") + case <-time.After(20 * time.Millisecond): + } + if got := fake.callsTo("DeleteActor"); len(got) != 0 { + t.Errorf("deleted %v while a cycle was running", got) + } + + close(fake.resumeGate) + <-cycleDone + <-shutdownDone + if got := fake.callsTo("DeleteActor"); !slices.Equal(got, []string{name}) { + t.Errorf("deleted %v, want [%s]", got, name) + } + r.iterate() + if got := fake.callsTo("ResumeActor"); len(got) != 2 { + t.Errorf("ResumeActor calls = %v, want the warm-up and the one cycle only", got) + } +} + +func TestShutdownWaitsForARunningSetupAndDeletesItsActor(t *testing.T) { + fake := newFake() + r := newTestRuntime(fake) + fake.resumeEntered = make(chan struct{}) + fake.resumeGate = make(chan struct{}) + setupDone := make(chan struct{}) + go func() { + r.iterate() + close(setupDone) + }() + <-fake.resumeEntered + + shutdownDone := make(chan struct{}) + go func() { + r.shutdown(context.Background()) + close(shutdownDone) + }() + select { + case <-shutdownDone: + t.Fatal("shutdown returned while a setup was running") + case <-time.After(20 * time.Millisecond): + } + + close(fake.resumeGate) + <-setupDone + <-shutdownDone + created := fake.callsTo("CreateActor") + if got := fake.callsTo("DeleteActor"); len(created) != 1 || !slices.Equal(got, created) { + t.Errorf("created %v, deleted %v, want the setup's actor deleted", created, got) + } + + // No setup starts once shutdown has begun. + r.iterate() + if got := fake.callsTo("CreateActor"); len(got) != 1 { + t.Errorf("CreateActor calls = %v after shutdown, want the one before", got) + } +} + +func TestShutdownRecordsOnlyToPrometheus(t *testing.T) { + fake := newFake() + r := newTestRuntime(fake) + names := setUpUsers(t, r, 3) + var toLocust atomic.Int32 + r.recordSuccess = func(string, string, string, time.Duration, int64) { toLocust.Add(1) } + r.recordFailure = func(string, string, string, time.Duration, string) { toLocust.Add(1) } + + r.cycle(names[0]) + if got := toLocust.Load(); got != 2 { + t.Fatalf("a cycle sent %d samples to locust, want the resume and the pause", got) + } + + // A cycle that finishes once shutdown has begun, and the deletes, record + // to Prometheus only: boomer may no longer drain its stats channel. + fake.resumeEntered = make(chan struct{}) + fake.resumeGate = make(chan struct{}) + go r.cycle(names[0]) + <-fake.resumeEntered + resumes := requestCount(t, "ResumeActorCold", "success") + deletes := requestCount(t, "DeleteActor", "success") + toLocust.Store(0) + shutdownDone := make(chan struct{}) + go func() { + r.shutdown(context.Background()) + close(shutdownDone) + }() + for !closing(r) { + time.Sleep(time.Millisecond) + } + close(fake.resumeGate) + <-shutdownDone + + if got := toLocust.Load(); got != 0 { + t.Errorf("shutdown sent %d samples to locust, want 0", got) + } + if got := requestCount(t, "ResumeActorCold", "success") - resumes; got != 1 { + t.Errorf("ResumeActorCold successes grew by %v, want the in-flight resume", got) + } + if got := requestCount(t, "DeleteActor", "success") - deletes; got != 3 { + t.Errorf("DeleteActor successes grew by %v, want 3", got) + } +} + +func TestShutdownCancelsACycleThatOutlastsItsShare(t *testing.T) { + fake := newFake() + r := newTestRuntime(fake) + r.iterate() + name := userActors(r)[0] + fake.resumeEntered = make(chan struct{}) + fake.resumeGate = make(chan struct{}) // never closed: the resume hangs + cycleDone := make(chan struct{}) + go func() { + r.cycle(name) + close(cycleDone) + }() + <-fake.resumeEntered + + ctx, cancel := context.WithTimeout(context.Background(), 200*time.Millisecond) + defer cancel() + r.shutdown(ctx) + if ctx.Err() != nil { + t.Fatal("shutdown spent its whole budget") + } + select { + case <-cycleDone: + case <-time.After(time.Second): + t.Fatal("the hung cycle was not canceled") + } + if got := fake.callsTo("DeleteActor"); !slices.Equal(got, []string{name}) { + t.Errorf("deleted %v, want [%s]", got, name) + } + if _, err := fake.GetActor(context.Background(), &ateapipb.GetActorRequest{Actor: r.ref(name)}); status.Code(err) != codes.NotFound { + t.Errorf("actor still exists after shutdown: err = %v", err) + } +} + +// closing reports whether shutdown has begun. +func closing(r *runtime) bool { + r.cycleMu.Lock() + defer r.cycleMu.Unlock() + return r.closing +} + +// requestCount scrapes the default registry for the locust_requests_total +// of one request name and status, which is global to the test binary. +func requestCount(t *testing.T, name, result string) float64 { + t.Helper() + return scrape(t, `locust_requests_total{method="grpc",name="`+name+`",status="`+result+`",user_class="ResumePausedUser"} `) +} + +func scrape(t *testing.T, prefix string) float64 { + t.Helper() + rec := httptest.NewRecorder() + promhttp.Handler().ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/metrics", nil)) + for _, line := range strings.Split(rec.Body.String(), "\n") { + if v, ok := strings.CutPrefix(line, prefix); ok { + f, err := strconv.ParseFloat(v, 64) + if err != nil { + t.Fatalf("parse %q: %v", line, err) + } + return f + } + } + return 0 +} From ed40e868e2164e5f5d56973f02669f0b01b1c771 Mon Sep 17 00:00:00 2001 From: Davanum Srinivas Date: Tue, 13 Oct 2026 19:01:57 +0000 Subject: [PATCH 12/42] e2e: fetch in-cluster echo origins in the egress MITM suite (#2561) `TestActorEgressMITMTrust` fetched `https://example.com/` through the gateway's MITM leg and required a 200. When that public origin answered 503 or slowly, the e2e lanes failed on pull requests that did not touch egress. The credential injection suite had the same exposure to `httpbin.org` and already moved to the in-cluster echo server; this does the same for the MITM suite. The intercepted fetches (the trust proof, the system-roots negative, and the Host-port cases) now reach `echo-https` and `echo-http`, testserver origins deployed in the cluster. Their certificate comes from the service-DNS signer, which the gateway trusts after `hack/setup-e2e-egress-tls-kind.sh`; CI already runs that before both lanes. TLS passthrough keeps its public origin, since the proof that the relay did not intercept is the origin's own certificate validating against the system roots. Those fetches now retry transport errors and 5xx for a bounded window and then fail. They never skip, per `docs/dev/best-practices/ci-fail-closed.md`. Verified with `gofmt`, `go vet ./internal/e2e/...`, `go test -c` on the suite, `hack/verify/boilerplate.sh`, and `golangci-lint` on `./internal/e2e/...`. The suite itself needs a cluster and did not run locally; the e2e lanes in CI run it in both sandbox classes. --- .../e2e/suites/egressmitm/egressmitm_test.go | 161 +++++++++++++----- 1 file changed, 121 insertions(+), 40 deletions(-) diff --git a/internal/e2e/suites/egressmitm/egressmitm_test.go b/internal/e2e/suites/egressmitm/egressmitm_test.go index 5b0876fb15..30544ffc35 100644 --- a/internal/e2e/suites/egressmitm/egressmitm_test.go +++ b/internal/e2e/suites/egressmitm/egressmitm_test.go @@ -25,6 +25,7 @@ import ( "io" "net/http" "net/url" + "path" "strings" "testing" "time" @@ -32,29 +33,75 @@ import ( "github.com/agent-substrate/substrate/internal/e2e" "github.com/agent-substrate/substrate/internal/resources" "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + corev1 "k8s.io/api/core/v1" ) const probeTemplate = "probe" var probeNamespace string +// servingBundle is the echo-https pod's certificate chain and key, in one file. +const servingBundle = "/run/servicedns.podcert.ate.dev/credential-bundle.pem" + +// The origins the intercepted fetches reach, one per scheme. They run in the +// cluster so the lane does not depend on a public origin answering. +var ( + // echoHTTPSServer serves the certificate kubelet requests for it from the + // cluster's service-DNS signer, which names the Services selecting the pod + // as ..svc. The egress gateway trusts that signer once + // hack/setup-e2e-egress-tls-kind.sh has run. + echoHTTPSServer = e2e.ServerPod{ + Name: "echo-https", + ImportPath: "github.com/agent-substrate/substrate/internal/e2e/fixtures/testserver", + Args: []string{"http", "--tls-cert=" + servingBundle, "--tls-key=" + servingBundle}, + Port: 443, + TargetPort: 8443, + HealthScheme: corev1.URISchemeHTTPS, + Volumes: []corev1.Volume{{ + Name: "servicedns", + VolumeSource: corev1.VolumeSource{Projected: &corev1.ProjectedVolumeSource{ + Sources: []corev1.VolumeProjection{{PodCertificate: &corev1.PodCertificateProjection{ + SignerName: "servicedns.podcert.ate.dev/identity", + KeyType: "ECDSAP256", + CredentialBundlePath: path.Base(servingBundle), + }}}, + }}, + }}, + VolumeMounts: []corev1.VolumeMount{{Name: "servicedns", MountPath: path.Dir(servingBundle), ReadOnly: true}}, + } + echoHTTPServer = e2e.ServerPod{ + Name: "echo-http", + ImportPath: "github.com/agent-substrate/substrate/internal/e2e/fixtures/testserver", + Args: []string{"http"}, + Port: 80, + TargetPort: 8080, + } +) + // TestActorEgressMITMTrust proves an actor can do TLS through the MITM // egress gateway using only the projected trust bundle: // -// - positive: /fetch with roots=bundle succeeds — the gateway's per-SNI -// minted leaf (signed from the egress-mitm-ca-pool) validates against -// the anchors atelet projected from the reconciler-published bundle. -// The bundle holds no public CAs, so a pass certifies interception is -// on. +// - positive: /fetch of the in-cluster echo origin with roots=bundle +// succeeds — the gateway's per-SNI minted leaf (signed from the +// egress-mitm-ca-pool) validates against the anchors atelet projected +// from the reconciler-published bundle. The bundle holds neither a public +// CA nor the signer of the origin's own certificate, so a pass certifies +// interception is on. // - negative: /fetch with roots=system fails certificate verification — -// the minted leaf chains to no public CA, proving the traffic really is -// intercepted rather than relayed (were it relayed this fetch would -// succeed, and the positive case would be meaningless). +// the minted leaf chains to no public CA, so the positive case trusted +// the projected bundle and nothing else. +// +// Only TLS passthrough reaches a public origin: the proof that the relay did +// not intercept is the origin's own certificate validating against the system +// roots, which no in-cluster certificate does. The echo origins' certificates +// come from the service-DNS signer, which the gateway trusts once +// hack/setup-e2e-egress-tls-kind.sh has run after the install. // // CI runs this in both lanes (see pr-workflow.yaml) — once per sandbox class, // since trust delivery differs per class (gVisor RO bind vs the micro-VM // unified virtio-fs share). Locally: // +// hack/setup-e2e-egress-tls-kind.sh // hack/run-e2e-kind.sh ./internal/e2e/suites/egressmitm -v -args --no-color // E2E_SANDBOX_CLASS=microvm hack/run-e2e-kind.sh ./internal/e2e/suites/egressmitm -v -args --no-color // @@ -68,6 +115,14 @@ func TestActorEgressMITMTrust(t *testing.T) { ctx := context.Background() clients := e2e.GetClients() + echoHTTPS := e2e.DeployServerPod(t, ctx, echoHTTPSServer) + httpSpec := echoHTTPServer + httpSpec.Namespace = echoHTTPS.Namespace + echoHTTP := e2e.DeployServerPod(t, ctx, httpSpec) + // The names the service-DNS signer certifies. + httpsHost := echoHTTPSServer.Name + "." + echoHTTPS.Namespace + ".svc" + httpHost := echoHTTPServer.Name + "." + echoHTTP.Namespace + ".svc" + // Ensure, never replace: the gateway signs with the pool mounted into its // pod, and kubelet propagates Secret updates into that mount on // its own schedule — replacing the pool here would race the propagation @@ -81,7 +136,7 @@ func TestActorEgressMITMTrust(t *testing.T) { probeNamespace, _ = e2e.DeployProbe(t, env["BUCKET_NAME"], "egressmitm", e2e.WithTrustBundle()) const id = "probe-mitm" - createAndResumeActor(t, ctx, clients, id) + createAndResumeActor(t, ctx, clients, id, httpsHost, httpHost) waitForActorState(t, ctx, clients, id, ateapipb.ActorState_ACTOR_STATE_RUNNING) rc, err := e2e.NewRouterClient(ctx) @@ -90,29 +145,14 @@ func TestActorEgressMITMTrust(t *testing.T) { } defer rc.Close() - const origin = "https://" + egressOriginHost + "/" + origin := "https://" + httpsHost + "/healthz" - // The gateway signs with the pool mounted into its pod, and kubelet - // propagates Secret contents into that mount on its own schedule (up to - // ~1 minute). In CI the pool predates the gateway pod, but a LOCAL rerun - // can recreate the pool moments before this fetch (a prior run's cleanup - // deleted it), leaving the gateway briefly signing with the old CA — so - // certificate failures retry for one propagation window before counting. - deadline := time.Now().Add(2 * time.Minute) - var pos fetchResponse - for { - pos = probeFetch(t, ctx, rc, id, origin, "bundle") - isCertErr := strings.Contains(pos.Error, "certificate") || strings.Contains(pos.Error, "x509") - if pos.Error == "" || !isCertErr || time.Now().After(deadline) { - break - } - time.Sleep(5 * time.Second) - } + pos := fetchUntilReached(t, ctx, rc, id, origin, "bundle") if pos.Error != "" { t.Fatalf("TLS through the MITM egress gateway with the projected trust bundle failed: %s — the projected anchors did not validate the gateway's minted leaf (or interception/minting is broken)", pos.Error) } if pos.Status != "200" { - t.Fatalf("fetch %s via projected bundle: status %s, want 200", origin, pos.Status) + t.Fatalf("fetch %s via projected bundle: status %s, want 200 (a 5xx is the gateway failing to reach or trust the echo origin; has hack/setup-e2e-egress-tls-kind.sh run?) body %q", origin, pos.Status, pos.Body) } neg := probeFetch(t, ctx, rc, id, origin, "system") @@ -126,8 +166,8 @@ func TestActorEgressMITMTrust(t *testing.T) { // dials the Host's name on the port the actor dialed, and nothing answers // on port 1. for _, tc := range []struct{ origin, dial string }{ - {origin: "https://" + egressOriginHost + ":1/", dial: egressOriginHost + ":443"}, - {origin: "http://" + egressOriginHost + ":1/", dial: egressOriginHost + ":80"}, + {origin: "https://" + httpsHost + ":1/healthz", dial: echoHTTPS.Address()}, + {origin: "http://" + httpHost + ":1/healthz", dial: echoHTTP.Address()}, } { res := probeFetch(t, ctx, rc, id, tc.origin, "bundle", "dial="+url.QueryEscape(tc.dial)) switch { @@ -142,16 +182,16 @@ func TestActorEgressMITMTrust(t *testing.T) { // must validate with the system roots. That is also the proof it was not // intercepted. const passthroughOrigin = "https://" + egressOriginPassthroughHost + "/" - pt := probeFetch(t, ctx, rc, id, passthroughOrigin, "system") + pt := fetchUntilReached(t, ctx, rc, id, passthroughOrigin, "system") if pt.Error != "" { t.Fatalf("TLS passthrough to %s failed: %s — the origin's certificate did not validate against the system roots, so the connection was intercepted, misrouted, or closed", passthroughOrigin, pt.Error) } if pt.Status != "200" { - t.Fatalf("passthrough fetch %s: status %s, want 200", passthroughOrigin, pt.Status) + t.Fatalf("passthrough fetch %s: status %s, want 200; body %q", passthroughOrigin, pt.Status, pt.Body) } // The gateway dials the name the SNI claims. - bySNI := probeFetch(t, ctx, rc, id, passthroughOrigin, "system", "dial="+url.QueryEscape(unreachableAddress)) + bySNI := fetchUntilReached(t, ctx, rc, id, passthroughOrigin, "system", "dial="+url.QueryEscape(unreachableAddress)) switch { case bySNI.Error != "": t.Errorf("TLS passthrough to %s dialed at %s failed: %s — the gateway dialed the actor's address instead of resolving the SNI", passthroughOrigin, unreachableAddress, bySNI.Error) @@ -182,9 +222,10 @@ func TestActorEgressMITMTrust(t *testing.T) { } } -// egressOriginHost is the one host the probe actor's EgressPolicy allows. +// The public hosts of the passthrough cases. const ( - egressOriginHost = "example.com" + // egressOriginPassthroughHost is relayed unread, so the certificate the + // probe sees is the origin's own and validates against the system roots. egressOriginPassthroughHost = "example.edu" // egressOriginPassthroughWrongPortHost is allowed by passthrough on 8443 // only, while the probe dials 443. @@ -197,6 +238,46 @@ const ( type fetchResponse struct { Status string `json:"status"` Error string `json:"error"` + Body string `json:"body"` +} + +// reachWindow bounds the retries of a fetch that must return 200. +const reachWindow = 2 * time.Minute + +// fetchUntilReached is probeFetch for a fetch that must return 200. It retries +// transient failures for up to reachWindow, then returns the last result for +// the caller to fail on. A denial or any other final status returns at once. +// The failures it rides out: +// +// - a certificate error: the gateway signs with the pool mounted into its +// pod, and kubelet propagates Secret contents into that mount on its own +// schedule (up to ~1 minute). In CI the pool predates the gateway pod, +// but a LOCAL rerun can recreate the pool moments before this fetch (a +// prior run's cleanup deleted it), leaving the gateway briefly signing +// with the old CA; +// - any other transport error, and a 5xx: the actor's route is still +// propagating, or the public passthrough origin reset, timed out or +// answered 5xx. +// +// It never skips. The passthrough origin's reachability is part of what the +// lane proves, and a skip would report that as a pass +// (docs/dev/best-practices/ci-fail-closed.md). +func fetchUntilReached(t *testing.T, ctx context.Context, rc *e2e.RouterClient, id, origin, roots string, extraParams ...string) fetchResponse { + t.Helper() + deadline := time.Now().Add(reachWindow) + for { + resp := probeFetch(t, ctx, rc, id, origin, roots, extraParams...) + if !transientFailure(resp) || time.Now().After(deadline) { + return resp + } + t.Logf("fetch of %s: transient failure (status %q, error %q), retrying", origin, resp.Status, resp.Error) + time.Sleep(5 * time.Second) + } +} + +// transientFailure reports whether fetchUntilReached should retry resp. +func transientFailure(resp fetchResponse) bool { + return resp.Error != "" || strings.HasPrefix(resp.Status, "5") } // probeFetch asks the probe to fetch origin with the given roots mode; @@ -239,9 +320,9 @@ func probeFetch(t *testing.T, ctx context.Context, rc *e2e.RouterClient, id, ori // createAndResumeActor mirrors the identity suite's self-healing actor // lifecycle (actor records outlive the fixture namespace); DeployProbe has -// already waited for the template's golden snapshot. - -func createAndResumeActor(t *testing.T, ctx context.Context, clients *e2e.Clients, id string) { +// already waited for the template's golden snapshot. httpsHost and httpHost +// are the echo origins' names, the hosts the actor's policy intercepts. +func createAndResumeActor(t *testing.T, ctx context.Context, clients *e2e.Clients, id, httpsHost, httpHost string) { t.Helper() ref := &ateapipb.ObjectRef{Atespace: probeNamespace, Name: id} _, _ = clients.SubstrateAPI.SuspendActor(ctx, &ateapipb.SuspendActorRequest{Actor: ref}) @@ -253,10 +334,10 @@ func createAndResumeActor(t *testing.T, ctx context.Context, clients *e2e.Client t.Fatalf("CreateActor %q: %v", id, err) } // The gateway refuses every tunnel for an actor without a policy. Naming - // only the origin also lets the same actor show a denial. + // only the origins also lets the same actor show a denial. e2e.EnsureEgressPolicy(t, ctx, clients, ref, - e2e.EgressAllowHTTP(egressOriginHost), - e2e.EgressAllowHTTPS(egressOriginHost), + e2e.EgressAllowHTTP(httpHost), + e2e.EgressAllowHTTPS(httpsHost), e2e.EgressAllowPassthrough(egressOriginPassthroughHost), e2e.EgressAllowPassthroughOnPorts([]int32{8443}, egressOriginPassthroughWrongPortHost), ) From 3de23a46b32731e171736c61b7bc0eefd29674fe Mon Sep 17 00:00:00 2001 From: Davanum Srinivas Date: Tue, 13 Oct 2026 19:55:02 +0000 Subject: [PATCH 13/42] cmd/atelet: Join the pause-image pull in the prewarm download test (#2564) CI (`go test -race`, Linux) fails `cmd/atelet` with `TestSandboxAssetPrewarmDownloads ... testing.go: TempDir RemoveAll cleanup: unlinkat /tmp/TestSandboxAssetPrewarmDownloads.../002/layers/sha256: directory not empty`. The test returns as soon as the runsc asset lands and joins the prewarm worker, but the pause-image pull the worker started is not joined. `imagecache.Store.EnsureImage` runs pulls detached from the caller's context, so a canceled caller does not fail the other waiters, and it returns to the worker on cancellation while the pull keeps unpacking layers into the store. The TempDir cleanup then races that unpack. The test now looks the pause image up by digest after the asset lands. The lookup joins the in-flight pull through the store's singleflight, so no write outlives the test. Under CPU pressure with the race detector on Linux, 1 of 100 and 1 of 500 runs failed before the change and 0 of 1500 after. --------- Co-authored-by: Lior Lieberman --- cmd/atelet/sandbox_prewarm.go | 7 +++++ cmd/atelet/sandbox_prewarm_test.go | 41 ++++++++++++++++++++---------- 2 files changed, 35 insertions(+), 13 deletions(-) diff --git a/cmd/atelet/sandbox_prewarm.go b/cmd/atelet/sandbox_prewarm.go index 1395643cea..64f6b290ff 100644 --- a/cmd/atelet/sandbox_prewarm.go +++ b/cmd/atelet/sandbox_prewarm.go @@ -57,6 +57,12 @@ var prewarmTimeout = 5 * time.Minute // permanently broken config forever. const prewarmMaxRetries = 8 +// prewarmFinished observes the result of each prewarm attempt, by config +// name, once the worker's call has returned. A no-op in production; a var so +// a test can wait for the worker's own prewarm call to return before it tears +// down what that call writes into. +var prewarmFinished = func(string, error) {} + // sandboxAssetFetcher is the one slice of AteomHerder the prewarmer needs. // Prewarming through the same method as the Run/Restore path keeps the two // fetches from ever diverging on cache layout or validation. @@ -226,6 +232,7 @@ func (p *sandboxPrewarmer) process(ctx context.Context, name string) { } if err == nil { err = p.prewarm(ctx, cfg) + prewarmFinished(name, err) } if err == nil { p.queue.Forget(name) diff --git a/cmd/atelet/sandbox_prewarm_test.go b/cmd/atelet/sandbox_prewarm_test.go index 004fda2e00..e1b561f0c5 100644 --- a/cmd/atelet/sandbox_prewarm_test.go +++ b/cmd/atelet/sandbox_prewarm_test.go @@ -402,10 +402,26 @@ func TestPrewarmDefaultVersionOnly(t *testing.T) { // fake clientset flows through the informer into the prewarm worker, which // lands the asset in the static-files cache without any Run/Restore request. func TestSandboxAssetPrewarmDownloads(t *testing.T) { - origDir, origJitter := nodepath.StaticFilesDir, prewarmMaxJitter + origDir, origJitter, origFinished := nodepath.StaticFilesDir, prewarmMaxJitter, prewarmFinished nodepath.StaticFilesDir = t.TempDir() prewarmMaxJitter = 0 - t.Cleanup(func() { nodepath.StaticFilesDir, prewarmMaxJitter = origDir, origJitter }) + // The worker's own prewarm call returns only when the asset has landed + // and the pause-image pull has finished: it waits for the pull on a live + // context, while the pull runs detached from that context (see + // imagecache.Store.EnsureImage). Waiting for the call keeps the pull's + // last writes from racing the TempDir cleanup. + prewarmed := make(chan error, 1) + prewarmFinished = func(_ string, err error) { + // Non-blocking: a failed attempt is retried, and the worker must not + // block on a result nobody reads. + select { + case prewarmed <- err: + default: + } + } + t.Cleanup(func() { + nodepath.StaticFilesDir, prewarmMaxJitter, prewarmFinished = origDir, origJitter, origFinished + }) host := imageVolumeTestRegistry(t) pauseRef := host + "/pause:3.10" @@ -443,17 +459,16 @@ func TestSandboxAssetPrewarmDownloads(t *testing.T) { defer close(stopCh) factory.Start(stopCh) - wantPath := ateletpath.RunSCBinaryPath(sha) - deadline := time.Now().Add(10 * time.Second) - for { - if _, err := os.Stat(wantPath); err == nil { - return - } else if !errors.Is(err, os.ErrNotExist) { - t.Fatalf("stat %s: %v", wantPath, err) - } - if time.Now().After(deadline) { - t.Fatalf("asset never prewarmed to %s", wantPath) + select { + case err := <-prewarmed: + if err != nil { + t.Fatalf("prewarm: %v", err) } - time.Sleep(10 * time.Millisecond) + case <-time.After(30 * time.Second): + t.Fatal("prewarm never finished") + } + wantPath := ateletpath.RunSCBinaryPath(sha) + if _, err := os.Stat(wantPath); err != nil { + t.Fatalf("asset not prewarmed to %s: %v", wantPath, err) } } From b937c31f70953b6123b43ba8213706619e5ee452 Mon Sep 17 00:00:00 2001 From: Benjamin Elder Date: Tue, 13 Oct 2026 19:44:01 +0000 Subject: [PATCH 14/42] Fix ETXTBSY test flake in credentialprovider (#2365) Use pre-existing plugin scripts in testdata/ instead of writing executable files during parallel tests, avoiding ETXTBSY races when concurrent subtests fork and exec plugins (https://go.dev/issue/22315). --- .../credentialprovider_test.go | 72 ++++++++----------- .../testdata/args-provider.sh | 22 ++++++ .../testdata/env-provider.sh | 22 ++++++ .../testdata/failing-provider.sh | 20 ++++++ .../testdata/fake-provider.sh | 21 ++++++ .../testdata/sleep-provider.sh | 19 +++++ 6 files changed, 132 insertions(+), 44 deletions(-) create mode 100755 cmd/atelet/internal/credentialprovider/testdata/args-provider.sh create mode 100755 cmd/atelet/internal/credentialprovider/testdata/env-provider.sh create mode 100755 cmd/atelet/internal/credentialprovider/testdata/failing-provider.sh create mode 100755 cmd/atelet/internal/credentialprovider/testdata/fake-provider.sh create mode 100755 cmd/atelet/internal/credentialprovider/testdata/sleep-provider.sh diff --git a/cmd/atelet/internal/credentialprovider/credentialprovider_test.go b/cmd/atelet/internal/credentialprovider/credentialprovider_test.go index 8f64b7b2a7..0bab7d1a64 100644 --- a/cmd/atelet/internal/credentialprovider/credentialprovider_test.go +++ b/cmd/atelet/internal/credentialprovider/credentialprovider_test.go @@ -25,7 +25,6 @@ import ( "slices" "sort" "strings" - "syscall" "testing" "testing/synctest" "time" @@ -34,41 +33,38 @@ import ( "github.com/google/go-containerregistry/pkg/name" ) -// writeExecutable writes script into dir/name as an executable. Forks are held -// off until the file is closed: a child forked while it is open keeps the -// write descriptor until it execs, and exec-ing the script meanwhile fails -// with "text file busy" (ETXTBSY). -func writeExecutable(t *testing.T, dir, name, script string) { +// linkPlugin symlinks / to a pre-existing executable script +// in testdata/. Pre-existing scripts are used instead of writing executable +// files during the test to avoid ETXTBSY races when parallel tests fork+exec +// plugins concurrently (https://go.dev/issue/22315). +func linkPlugin(t *testing.T, dir, pluginName, testdataScript string) { t.Helper() - syscall.ForkLock.RLock() - defer syscall.ForkLock.RUnlock() - if err := os.WriteFile(filepath.Join(dir, name), []byte(script), 0o700); err != nil { - t.Fatalf("Failed to write fake plugin %q: %v", name, err) + target, err := filepath.Abs(filepath.Join("testdata", testdataScript)) + if err != nil { + t.Fatalf("Failed to resolve testdata script %q: %v", testdataScript, err) + } + if err := os.Symlink(target, filepath.Join(dir, pluginName)); err != nil { + t.Fatalf("Failed to symlink plugin %q: %v", pluginName, err) } } -// fakePlugin writes an executable named name into dir that records each -// request it is handed (one JSON object per line in /.requests) and -// replies with response. It returns the requests file's path. +// fakePlugin links an executable named pluginName into dir that records each +// request it is handed (one JSON object per line in /.requests) +// and replies with response. It returns the requests file's path. func fakePlugin(t *testing.T, dir, pluginName, response string) string { t.Helper() - requests := filepath.Join(dir, pluginName+".requests") - script := fmt.Sprintf(`#!/bin/sh -cat >> %q -echo >> %q -cat <<'RESPONSE' -%s -RESPONSE -`, requests, requests, response) - writeExecutable(t, dir, pluginName, script) - return requests + if err := os.WriteFile(filepath.Join(dir, pluginName+".response"), []byte(response), 0o600); err != nil { + t.Fatalf("Failed to write plugin response: %v", err) + } + linkPlugin(t, dir, pluginName, "fake-provider.sh") + return filepath.Join(dir, pluginName+".requests") } -// failingPlugin writes an executable that prints message to stderr and exits +// failingPlugin links an executable that prints an error to stderr and exits // non-zero. -func failingPlugin(t *testing.T, dir, pluginName, message string) { +func failingPlugin(t *testing.T, dir, pluginName string) { t.Helper() - writeExecutable(t, dir, pluginName, fmt.Sprintf("#!/bin/sh\necho %q >&2\nexit 7\n", message)) + linkPlugin(t, dir, pluginName, "failing-provider.sh") } // writeConfig writes a CredentialProviderConfig into dir and returns its path. @@ -168,7 +164,7 @@ func TestKeychainSkipsUnmatchedImage(t *testing.T) { dir := t.TempDir() // An image no provider claims must not exec anything, so point the config // at a plugin that would fail loudly if it ever ran. - failingPlugin(t, dir, "fake-provider", "plugin must not be invoked") + failingPlugin(t, dir, "fake-provider") kc, err := New(writeConfig(t, dir, gcpLikeConfig), dir) if err != nil { @@ -227,7 +223,7 @@ func TestKeychainSkipsFailingPlugin(t *testing.T) { { name: "plugin exits non-zero", plugin: func(t *testing.T, dir string) { - failingPlugin(t, dir, "fake-provider", "metadata server unreachable") + failingPlugin(t, dir, "fake-provider") }, wantLog: "metadata server unreachable", }, @@ -403,7 +399,7 @@ func TestKeychainCandidatesSkipInvalidAuthKey(t *testing.T) { func TestKeychainCandidatesSkipFailingProvider(t *testing.T) { t.Parallel() dir := t.TempDir() - failingPlugin(t, dir, "first-provider", "metadata server unreachable") + failingPlugin(t, dir, "first-provider") fakePlugin(t, dir, "second-provider", `{ "kind": "CredentialProviderResponse", "apiVersion": "credentialprovider.kubelet.k8s.io/v1", @@ -662,13 +658,7 @@ func TestKeychainPassesConfiguredEnv(t *testing.T) { dir := t.TempDir() // The plugin echoes an env var back as the username, so the assertion // covers both the configured env and the inherited process env. - script := `#!/bin/sh -cat > /dev/null -cat < /dev/null -cat < /dev/null +cat < /dev/null +cat <&2 +exit 7 diff --git a/cmd/atelet/internal/credentialprovider/testdata/fake-provider.sh b/cmd/atelet/internal/credentialprovider/testdata/fake-provider.sh new file mode 100755 index 0000000000..38ba6f4bec --- /dev/null +++ b/cmd/atelet/internal/credentialprovider/testdata/fake-provider.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env bash + +# 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. + +set -o errexit -o nounset -o pipefail + +cat >> "$0.requests" +echo >> "$0.requests" +cat "$0.response" diff --git a/cmd/atelet/internal/credentialprovider/testdata/sleep-provider.sh b/cmd/atelet/internal/credentialprovider/testdata/sleep-provider.sh new file mode 100755 index 0000000000..a0828483fe --- /dev/null +++ b/cmd/atelet/internal/credentialprovider/testdata/sleep-provider.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash + +# 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. + +set -o errexit -o nounset -o pipefail + +sleep 60 From 792c30c6396533b68ae87db0e3a68418cead1cd7 Mon Sep 17 00:00:00 2001 From: Greg Castle Date: Tue, 13 Oct 2026 19:49:46 +0000 Subject: [PATCH 15/42] ate-system now denies ingress by default; in-cluster API clients must label their namespace ate.dev/api-access=true (#2522) Add default-deny ingress NetworkPolicies for the ate-system control plane, with allow rules for the flows it needs: - ate-api-server :443 from ate-controller, atelet, atenet-router, atenet-egress, and namespaces labeled ate.dev/api-access=true - postgres :5432 and atelet gRPC :8085 from ate-api-server only - atenet-egress https from worker pods (ate.dev/worker-pool) in any namespace - atenet-router data ports (http, https, connect, connect-tls) from any source - metrics and health ports from any source (probes, scrapers) - kind: rustfs S3 API from ate-api-server, atelet and the bucket-init Job The router's /statusz port (4040), the rustfs console, and every other port are reachable only through kubectl port-forward. Breaking change: in-cluster clients of the Substrate API outside ate-system must label their namespace ate.dev/api-access=true. GKE Gateway health checks pointed at the router's status port (4040, /statusz) must move to the metrics port (9090, /readyz). The benchmarking namespace is labeled. I want to get this in for M3 because all exposed APIs become customer dependencies unless we restrict them. Fixes #2278 - [X] Tests pass - [X] Appropriate changes to documentation are included in the PR --- benchmarking/locust/deploy.sh | 3 + benchmarking/monitoring.yaml | 4 + benchmarking/telemetry/meter.yaml | 4 + .../steps/systemnetworkpolicy_test.go | 465 ++++++++++++++++++ demos/claude-code-multiplex/README.md | 2 +- docs/security.md | 2 +- docs/upgrade.md | 12 + .../fixtures/testserver/egressprobe.yaml.tmpl | 1 + .../ate-system-networkpolicies.yaml | 220 +++++++++ manifests/ate-install/base/kustomization.yaml | 1 + manifests/ate-install/kind/kustomization.yaml | 1 + manifests/ate-install/kind/rustfs.yaml | 31 ++ 12 files changed, 744 insertions(+), 2 deletions(-) create mode 100644 cmd/ate-setup/internal/steps/systemnetworkpolicy_test.go create mode 100644 manifests/ate-install/ate-system-networkpolicies.yaml diff --git a/benchmarking/locust/deploy.sh b/benchmarking/locust/deploy.sh index 09b2f3fc96..393ec76d97 100755 --- a/benchmarking/locust/deploy.sh +++ b/benchmarking/locust/deploy.sh @@ -72,6 +72,9 @@ deploy() { # benchmarking/monitoring.yaml is otherwise optional. echo "Ensuring benchmarking namespace exists..." kubectl create namespace benchmarking --dry-run=client -o yaml | kubectl apply -f - + # The boomer workers call the Substrate API, which ate-system's + # NetworkPolicies admit only from namespaces carrying this label. + kubectl label namespace benchmarking ate.dev/api-access=true --overwrite if [[ -n "${AGENTSESSION_SCRIPT}" ]]; then echo "Validating agent-session script ${AGENTSESSION_SCRIPT}..." (cd "${SCRIPT_DIR}/../.." && go run ./cmd/benchmarking/boomer-worker --check-agentsession-script "${AGENTSESSION_SCRIPT}") diff --git a/benchmarking/monitoring.yaml b/benchmarking/monitoring.yaml index f815c7b22a..66c13fdf22 100644 --- a/benchmarking/monitoring.yaml +++ b/benchmarking/monitoring.yaml @@ -16,6 +16,10 @@ apiVersion: v1 kind: Namespace metadata: name: benchmarking + labels: + # Admits the benchmark clients to the Substrate API through + # manifests/ate-install/ate-system-networkpolicies.yaml. + ate.dev/api-access: "true" --- apiVersion: v1 kind: ServiceAccount diff --git a/benchmarking/telemetry/meter.yaml b/benchmarking/telemetry/meter.yaml index 27adc08243..cc9f140658 100644 --- a/benchmarking/telemetry/meter.yaml +++ b/benchmarking/telemetry/meter.yaml @@ -32,6 +32,10 @@ apiVersion: v1 kind: Namespace metadata: name: benchmarking + labels: + # Admits the benchmark clients to the Substrate API through + # manifests/ate-install/ate-system-networkpolicies.yaml. + ate.dev/api-access: "true" --- apiVersion: v1 kind: ConfigMap diff --git a/cmd/ate-setup/internal/steps/systemnetworkpolicy_test.go b/cmd/ate-setup/internal/steps/systemnetworkpolicy_test.go new file mode 100644 index 0000000000..dcac55434e --- /dev/null +++ b/cmd/ate-setup/internal/steps/systemnetworkpolicy_test.go @@ -0,0 +1,465 @@ +// 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 steps + +import ( + "maps" + "path/filepath" + "slices" + "testing" + + corev1 "k8s.io/api/core/v1" + networkingv1 "k8s.io/api/networking/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/runtime" + + "github.com/agent-substrate/substrate/cmd/ate-setup/internal/config" + "github.com/agent-substrate/substrate/cmd/ate-setup/internal/kube" + "github.com/agent-substrate/substrate/cmd/ate-setup/internal/kustomize" +) + +// systemPod is one ate-system workload's pod template, as the NetworkPolicies +// see it. +type systemPod struct { + workload string // kind/name, for messages + labels map[string]string + ports []corev1.ContainerPort +} + +// systemInstall is everything an install of one configuration puts in +// ate-system: the system overlay plus the manifests the installer applies on +// their own (postgres, the egress gateway, the bundled credential provider). +type systemInstall struct { + pods []systemPod + policies []networkingv1.NetworkPolicy +} + +// unadmittedPorts are the container ports no NetworkPolicy may admit, keyed +// by the pod's app label and the port's name. Every other container port of an +// ate-system pod must be admitted by some policy, so a new port fails +// TestSystemNetworkPoliciesCoverEveryPort until someone decides which list it +// belongs on, rather than being silently dropped by the default deny. +var unadmittedPorts = map[string][]string{ + // /statusz exposes cross-tenant request history (#2275); reach it with a + // port-forward. + "atenet-router": {"status", "extproc"}, + // ext_proc is served to the envoy container on loopback. + "atenet-egress": {"extproc"}, + // The provider ships its own policy admitting only the egress gateway's + // gRPC, and docs/egress-credential-injection.md promises nothing else + // gets in; its probes come from the node. + "k8s-credential-provider": {"statusz", "metrics", "health"}, + // kind only: the rustfs web console. + "rustfs": {"console"}, +} + +// installConfigs are the configurations whose ate-system the policies must +// cover: both dataplanes, on GKE and on kind. +func installConfigs() []*config.Config { + var out []*config.Config + for _, kind := range []bool{false, true} { + for _, router := range []string{config.RouterEnvoy, config.RouterAgentgateway} { + out = append(out, &config.Config{Kind: kind, Router: router}) + } + } + return out +} + +func installName(cfg *config.Config) string { + env := "gke" + if cfg.Kind { + env = "kind" + } + return env + "-" + cfg.Router +} + +func loadSystemInstall(t *testing.T, cfg *config.Config) systemInstall { + t.Helper() + root := repoRoot(t) + cfg.Root = root + e := &Env{Cfg: cfg} + + var objs []*unstructured.Unstructured + add := func(what string, rendered []byte, err error) { + t.Helper() + if err != nil { + t.Fatalf("rendering %s: %v", what, err) + } + decoded, err := kube.DecodeManifestBytes(rendered) + if err != nil { + t.Fatalf("decoding %s: %v", what, err) + } + objs = append(objs, decoded...) + } + system := filepath.Join(root, SystemOverlay(cfg)) + rendered, err := kustomize.Build(system) + add(system, rendered, err) + rendered, err = e.render(e.postgresManifestPath()) + add(e.postgresManifestPath(), rendered, err) + if cfg.Router == config.RouterAgentgateway { + egress := filepath.Join(root, installDir, "agentgateway-egress") + rendered, err = kustomize.Build(egress) + add(egress, rendered, err) + } else { + loaded, err := kube.LoadPath(e.atenetEgressManifestPath()) + if err != nil { + t.Fatalf("loading %s: %v", e.atenetEgressManifestPath(), err) + } + objs = append(objs, loaded...) + } + provider, err := kube.LoadPath(e.k8sCredentialProviderPath(k8sCredentialProviderManifest)) + if err != nil { + t.Fatalf("loading the credential provider manifest: %v", err) + } + objs = append(objs, provider...) + + var install systemInstall + for _, obj := range objs { + if obj.GetNamespace() != NamespaceAteSystem { + continue + } + switch obj.GetKind() { + case "NetworkPolicy": + var np networkingv1.NetworkPolicy + if err := runtime.DefaultUnstructuredConverter.FromUnstructured(obj.Object, &np); err != nil { + t.Fatalf("converting networkpolicy/%s: %v", obj.GetName(), err) + } + install.policies = append(install.policies, np) + case "Deployment", "DaemonSet", "StatefulSet", "Job": + raw, ok, _ := unstructured.NestedMap(obj.Object, "spec", "template") + if !ok { + t.Fatalf("%s/%s has no pod template", obj.GetKind(), obj.GetName()) + } + var tmpl corev1.PodTemplateSpec + if err := runtime.DefaultUnstructuredConverter.FromUnstructured(raw, &tmpl); err != nil { + t.Fatalf("converting the pod template of %s/%s: %v", obj.GetKind(), obj.GetName(), err) + } + pod := systemPod{workload: obj.GetKind() + "/" + obj.GetName(), labels: tmpl.Labels} + if obj.GetKind() == "Job" { + // The Job controller adds this label to its pods. + pod.labels = map[string]string{"batch.kubernetes.io/job-name": obj.GetName()} + for k, v := range tmpl.Labels { + pod.labels[k] = v + } + } + for _, c := range slices.Concat(tmpl.Spec.InitContainers, tmpl.Spec.Containers) { + pod.ports = append(pod.ports, c.Ports...) + } + install.pods = append(install.pods, pod) + } + } + if len(install.pods) == 0 { + t.Fatal("found no ate-system workloads; the manifests changed shape and this test is checking nothing") + } + return install +} + +func mustSelector(t *testing.T, sel *metav1.LabelSelector) labels.Selector { + t.Helper() + s, err := metav1.LabelSelectorAsSelector(sel) + if err != nil { + t.Fatalf("invalid label selector %v: %v", sel, err) + } + return s +} + +// policiesFor returns the policies that select pod for ingress. +func (in systemInstall) policiesFor(t *testing.T, pod systemPod) []networkingv1.NetworkPolicy { + t.Helper() + var out []networkingv1.NetworkPolicy + for _, np := range in.policies { + if !slices.Contains(np.Spec.PolicyTypes, networkingv1.PolicyTypeIngress) { + continue + } + if mustSelector(t, &np.Spec.PodSelector).Matches(labels.Set(pod.labels)) { + out = append(out, np) + } + } + return out +} + +// portMatches reports whether a policy port admits the container port. +func portMatches(pp networkingv1.NetworkPolicyPort, cp corev1.ContainerPort) bool { + proto := corev1.ProtocolTCP + if pp.Protocol != nil { + proto = *pp.Protocol + } + cproto := cp.Protocol + if cproto == "" { + cproto = corev1.ProtocolTCP + } + if proto != cproto { + return false + } + if pp.Port == nil { + return true + } + if pp.Port.StrVal != "" { + return cp.Name != "" && pp.Port.StrVal == cp.Name + } + return pp.Port.IntVal == cp.ContainerPort +} + +// admittingRules returns the ingress rules, across every policy selecting pod, +// that admit the container port. +func (in systemInstall) admittingRules(t *testing.T, pod systemPod, cp corev1.ContainerPort) []networkingv1.NetworkPolicyIngressRule { + t.Helper() + var out []networkingv1.NetworkPolicyIngressRule + for _, np := range in.policiesFor(t, pod) { + for _, rule := range np.Spec.Ingress { + if len(rule.Ports) == 0 { + out = append(out, rule) + continue + } + for _, pp := range rule.Ports { + if portMatches(pp, cp) { + out = append(out, rule) + break + } + } + } + } + return out +} + +func (in systemInstall) pod(t *testing.T, app string) systemPod { + t.Helper() + for _, p := range in.pods { + if p.labels["app"] == app { + return p + } + } + t.Fatalf("no ate-system workload with app=%s", app) + return systemPod{} +} + +func namedPort(t *testing.T, pod systemPod, name string) corev1.ContainerPort { + t.Helper() + for _, p := range pod.ports { + if p.Name == name { + return p + } + } + t.Fatalf("%s has no container port named %q", pod.workload, name) + return corev1.ContainerPort{} +} + +func numberedPort(t *testing.T, pod systemPod, n int32) corev1.ContainerPort { + t.Helper() + for _, p := range pod.ports { + if p.ContainerPort == n { + return p + } + } + t.Fatalf("%s has no container port %d", pod.workload, n) + return corev1.ContainerPort{} +} + +// requireOnlyFromApps fails unless every rule admitting the port names only +// ate-system pods whose app label is one of apps. +func requireOnlyFromApps(t *testing.T, in systemInstall, pod systemPod, cp corev1.ContainerPort, apps ...string) { + t.Helper() + rules := in.admittingRules(t, pod, cp) + if len(rules) == 0 { + t.Errorf("%s port %d (%s) is not admitted from anywhere, want from app in %v", pod.workload, cp.ContainerPort, cp.Name, apps) + } + for _, rule := range rules { + if len(rule.From) == 0 { + t.Errorf("%s port %d (%s) is admitted from any source, want only app in %v", pod.workload, cp.ContainerPort, cp.Name, apps) + continue + } + for _, peer := range rule.From { + if peer.NamespaceSelector != nil || peer.IPBlock != nil || peer.PodSelector == nil { + t.Errorf("%s port %d (%s) admits peer %+v, want only ate-system pods with app in %v", pod.workload, cp.ContainerPort, cp.Name, peer, apps) + continue + } + sel := mustSelector(t, peer.PodSelector) + if sel.Empty() { + t.Errorf("%s port %d (%s) admits every ate-system pod, want only app in %v", pod.workload, cp.ContainerPort, cp.Name, apps) + continue + } + // The peer must not select a pod outside apps. + for _, other := range in.pods { + if sel.Matches(labels.Set(other.labels)) && !slices.Contains(apps, other.labels["app"]) { + t.Errorf("%s port %d (%s) admits %s, want only app in %v", pod.workload, cp.ContainerPort, cp.Name, other.workload, apps) + } + } + } + } +} + +// TestSystemNetworkPoliciesDefaultDeny requires every install to carry a +// policy that selects every ate-system pod for ingress and admits nothing. +func TestSystemNetworkPoliciesDefaultDeny(t *testing.T) { + for _, cfg := range installConfigs() { + t.Run(installName(cfg), func(t *testing.T) { + in := loadSystemInstall(t, cfg) + for _, np := range in.policies { + if len(np.Spec.PodSelector.MatchLabels) == 0 && len(np.Spec.PodSelector.MatchExpressions) == 0 && + slices.Equal(np.Spec.PolicyTypes, []networkingv1.PolicyType{networkingv1.PolicyTypeIngress}) && + len(np.Spec.Ingress) == 0 { + return + } + } + t.Error("no NetworkPolicy denies ingress to every ate-system pod by default") + }) + } +} + +// TestSystemNetworkPoliciesCoverEveryPort requires each container port of an +// ate-system pod to be either admitted by a policy or listed in +// unadmittedPorts, and the listed ones to be admitted by none. +func TestSystemNetworkPoliciesCoverEveryPort(t *testing.T) { + for _, cfg := range installConfigs() { + t.Run(installName(cfg), func(t *testing.T) { + in := loadSystemInstall(t, cfg) + for _, pod := range in.pods { + for _, cp := range pod.ports { + denied := slices.Contains(unadmittedPorts[pod.labels["app"]], cp.Name) + admitted := len(in.admittingRules(t, pod, cp)) > 0 + switch { + case denied && admitted: + t.Errorf("%s port %d (%s) must not be admitted by any NetworkPolicy", pod.workload, cp.ContainerPort, cp.Name) + case !denied && !admitted: + t.Errorf("%s port %d (%s) is admitted by no NetworkPolicy, so the default deny drops it; admit it in manifests/ate-install/ate-system-networkpolicies.yaml or add it to unadmittedPorts", pod.workload, cp.ContainerPort, cp.Name) + } + } + } + }) + } +} + +// TestSystemNetworkPoliciesNamedPortsResolve catches a policy port name that +// no selected pod declares, which Kubernetes accepts and silently matches +// nothing. Port names differ between the dataplanes (agentgateway has "stats", +// envoy "envoy-metrics"), so a name only has to resolve in one install. +func TestSystemNetworkPoliciesNamedPortsResolve(t *testing.T) { + type policyPort struct{ policy, port string } + referenced := map[policyPort]bool{} + resolved := map[policyPort]bool{} + for _, cfg := range installConfigs() { + in := loadSystemInstall(t, cfg) + for _, np := range in.policies { + sel := mustSelector(t, &np.Spec.PodSelector) + for _, rule := range np.Spec.Ingress { + for _, pp := range rule.Ports { + if pp.Port == nil || pp.Port.StrVal == "" { + continue + } + key := policyPort{np.Name, pp.Port.StrVal} + referenced[key] = true + for _, pod := range in.pods { + if !sel.Matches(labels.Set(pod.labels)) { + continue + } + for _, cp := range pod.ports { + if cp.Name == pp.Port.StrVal { + resolved[key] = true + } + } + } + } + } + } + } + if len(referenced) == 0 { + t.Fatal("no NetworkPolicy names a port; the manifests changed shape and this test is checking nothing") + } + for key := range referenced { + if !resolved[key] { + t.Errorf("networkpolicy/%s names port %q, which no pod it selects declares in any install", key.policy, key.port) + } + } +} + +// TestSystemNetworkPoliciesLeastPrivilege pins who may reach the sensitive +// ports. +func TestSystemNetworkPoliciesLeastPrivilege(t *testing.T) { + for _, cfg := range installConfigs() { + t.Run(installName(cfg), func(t *testing.T) { + in := loadSystemInstall(t, cfg) + + postgres := in.pod(t, "postgres") + requireOnlyFromApps(t, in, postgres, namedPort(t, postgres, "postgres"), "ate-api-server") + + atelet := in.pod(t, "atelet") + requireOnlyFromApps(t, in, atelet, namedPort(t, atelet, "grpc"), "ate-api-server") + + // The API admits the control plane and opted-in namespaces only. + api := in.pod(t, "ate-api-server") + for _, rule := range in.admittingRules(t, api, numberedPort(t, api, 443)) { + if len(rule.From) == 0 { + t.Error("ate-api-server port 443 is admitted from any source") + } + for _, peer := range rule.From { + if peer.IPBlock != nil { + t.Errorf("ate-api-server port 443 admits ipBlock %v", peer.IPBlock) + } + if peer.NamespaceSelector != nil { + if want := map[string]string{"ate.dev/api-access": "true"}; !maps.Equal(peer.NamespaceSelector.MatchLabels, want) || len(peer.NamespaceSelector.MatchExpressions) != 0 { + t.Errorf("ate-api-server port 443 admits namespaces %v, want only those labeled %v", peer.NamespaceSelector, want) + } + // An opted-in namespace may host a WorkerPool; its worker + // pods never call the API and must stay out. + if peer.PodSelector == nil { + t.Errorf("ate-api-server port 443 admits every pod of an opted-in namespace, including worker pods") + continue + } + sel := mustSelector(t, peer.PodSelector) + if sel.Matches(labels.Set{workerPoolLabelForTest: "any"}) { + t.Errorf("ate-api-server port 443 admits worker pods (%s) of opted-in namespaces", workerPoolLabelForTest) + } + if !sel.Matches(labels.Set{"app": "dashboard"}) { + t.Errorf("ate-api-server port 443 namespace peer %v does not admit ordinary pods of opted-in namespaces", peer.PodSelector) + } + continue + } + if peer.PodSelector == nil || mustSelector(t, peer.PodSelector).Empty() { + t.Errorf("ate-api-server port 443 admits every ate-system pod (%+v)", peer) + } + } + } + + // The egress gateway admits worker pods only. + egress := in.pod(t, "atenet-egress") + rules := in.admittingRules(t, egress, namedPort(t, egress, "https")) + if len(rules) == 0 { + t.Error("atenet-egress https is admitted from nowhere; worker pods cannot reach the egress gateway") + } + for _, rule := range rules { + if len(rule.From) == 0 { + t.Error("atenet-egress https is admitted from any source, want worker pods only") + } + for _, peer := range rule.From { + // Workers live in their WorkerPool's namespace, so the peer + // has to reach across every namespace, not just ate-system. + if peer.NamespaceSelector == nil || !mustSelector(t, peer.NamespaceSelector).Empty() { + t.Errorf("atenet-egress https admits peer %+v, want worker pods from every namespace (namespaceSelector: {})", peer) + } + if peer.PodSelector == nil || !mustSelector(t, peer.PodSelector).Matches(labels.Set{workerPoolLabelForTest: "any"}) || + mustSelector(t, peer.PodSelector).Matches(labels.Set{"app": "probe"}) { + t.Errorf("atenet-egress https admits peer %+v, want pods labeled %s in any namespace", peer, workerPoolLabelForTest) + } + } + } + }) + } +} + +// workerPoolLabelForTest is the label the WorkerPool controller puts on +// worker pods (cmd/atecontroller/internal/controllers/workerpool_apply.go). +const workerPoolLabelForTest = "ate.dev/worker-pool" diff --git a/demos/claude-code-multiplex/README.md b/demos/claude-code-multiplex/README.md index 7bef61dbb3..9edbf68027 100644 --- a/demos/claude-code-multiplex/README.md +++ b/demos/claude-code-multiplex/README.md @@ -28,7 +28,7 @@ This guide assumes you know Kubernetes and the general shape of agent runtimes ( - A Kubernetes cluster with **Agent Substrate** installed (`./hack/install-ate.sh` from this repo's root). - `kubectl` configured against that cluster (the dashboard uses the operator's kubeconfig via [`client-go`](https://github.com/kubernetes/client-go) for pod-log reads). -- Network reach to the substrate **api** gRPC service (`api.ate-system:443`). When running the dashboard from outside the cluster, port-forward it in a separate terminal (same convention as [`demos/sandbox/README.md`](../sandbox/README.md#3-port-forward-services)) and keep it running for the lifetime of the demo: +- Network reach to the substrate **api** gRPC service (`api.ate-system:443`). If you run the dashboard as a pod in the cluster, label its namespace `ate.dev/api-access=true`, because ate-system's NetworkPolicies admit API clients only from labeled namespaces (see [`ate-system-networkpolicies.yaml`](../../manifests/ate-install/ate-system-networkpolicies.yaml)). When running the dashboard from outside the cluster, port-forward it in a separate terminal (same convention as [`demos/sandbox/README.md`](../sandbox/README.md#3-port-forward-services)) and keep it running for the lifetime of the demo: ```bash # Terminal 1: api port-forward kubectl port-forward svc/api 8080:443 -n ate-system diff --git a/docs/security.md b/docs/security.md index 168bf42535..0221bd35d5 100644 --- a/docs/security.md +++ b/docs/security.md @@ -72,7 +72,7 @@ Other forms of tenancy: | Principle | Description | Status | | :--- | :--- | :--- | -| **No Access by Default** | Sandboxed actors and worker nodes launch with default-deny networking and have zero network or API access to control plane services (`ate-api-server`, Kubernetes API servers, PostgreSQL state store). DNS is currently allowed outbound by default, work on that is in progress. | Met (Loopback, private netns)
In-Progress (DNS filtering, see [#2075](https://github.com/agent-substrate/substrate/issues/2075)) | +| **No Access by Default** | Sandboxed actors and worker nodes launch with default-deny networking and have zero network or API access to control plane services (`ate-api-server`, Kubernetes API servers, PostgreSQL state store). DNS is currently allowed outbound by default, work on that is in progress. Every control plane pod in `ate-system` also denies [ingress by default](../manifests/ate-install/ate-system-networkpolicies.yaml), admitting only the flows the control plane needs. | Met (Loopback, private netns)
In-Progress (DNS filtering, see [#2075](https://github.com/agent-substrate/substrate/issues/2075)) | | **Authenticated and Policy-Enforced Ingress** | Inbound invocations to actors terminate at authenticated edge routers (`atenet-router`), which authenticate callers and verify invocation policies before triggering on-demand actor cold-start or resume. | Met (atenet-router)
In-Progress (authentication) | | **Fail-Closed Policy-Enforced Egress Gateway** | Outbound traffic is funneled through the multi-tenant egress proxy (`atenet-egress`), which validates actor identity, enforces destination host allowlists, executes TLS inspection or prompt filtering, and dynamically injects target credentials. If an egress policy enforcement point (PEP) is unconfigured/fails egress is dropped. Unsupported protocols are dropped, see [Egress Policy Table](https://docs.google.com/document/d/1XLCo9SMROddQzwsyEhdBNWuLYFtWWZWfYvGlIO1BcUI/edit?resourcekey=0-st2SqKp-O68nv5LeIUfA-g&tab=t.0#bookmark=id.pjnob1gppauk) for specifics. | Met (atenet-egress)
In-Progress (fail closed, UDP currently passthrough) | diff --git a/docs/upgrade.md b/docs/upgrade.md index 6fc20f5fad..10bcb912bd 100644 --- a/docs/upgrade.md +++ b/docs/upgrade.md @@ -63,6 +63,18 @@ Every item has to hold. Nothing in the roll stops you if one does not. kubectl get ds -n ate-system -l app=atelet ``` +- [ ] If you are upgrading across the release that added the ate-system + NetworkPolicies, every namespace whose pods call the Substrate API + directly (not through `kubectl ate`) is labeled + `ate.dev/api-access=true`. The new manifests deny the API to every + other namespace as soon as they are applied. Worker pods are never + admitted, labeled or not. See + [`ate-system-networkpolicies.yaml`](../manifests/ate-install/ate-system-networkpolicies.yaml). + + ```bash + kubectl label namespace ate.dev/api-access=true + ``` + - [ ] Every serving pool is pinned to `$OLD_VERSION`. An unpinned pool cannot take part in the roll. diff --git a/internal/e2e/fixtures/testserver/egressprobe.yaml.tmpl b/internal/e2e/fixtures/testserver/egressprobe.yaml.tmpl index 52134b2489..7dc0b2f676 100644 --- a/internal/e2e/fixtures/testserver/egressprobe.yaml.tmpl +++ b/internal/e2e/fixtures/testserver/egressprobe.yaml.tmpl @@ -25,6 +25,7 @@ metadata: namespace: ${NAMESPACE} labels: app: egressprobe + ate.dev/worker-pool: egressprobe spec: restartPolicy: Never containers: diff --git a/manifests/ate-install/ate-system-networkpolicies.yaml b/manifests/ate-install/ate-system-networkpolicies.yaml new file mode 100644 index 0000000000..eb10e2480c --- /dev/null +++ b/manifests/ate-install/ate-system-networkpolicies.yaml @@ -0,0 +1,220 @@ +# 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. + +# Ingress NetworkPolicies for the ate-system control plane. +# +# Every pod in ate-system is denied ingress by default; the policies below +# admit only the flows the control plane needs. NetworkPolicies are additive, +# so a port is reachable exactly when some policy here (or another one that +# selects the pod, such as k8s-credential-provider's) admits it. Ports that no +# policy admits -- the router's /statusz port (4040), the bundled postgres from +# anything but ate-api-server, atelet's AteomHerder from anything but +# ate-api-server -- are reachable only through `kubectl port-forward`, which +# NetworkPolicy does not filter. +# +# Policies only take effect on a cluster whose network plugin enforces them +# (GKE Dataplane V2, Calico, Cilium, kindnet, ...). +# +# Several workloads are applied by the installer outside this bundle (postgres, +# atenet-egress, k8s-credential-provider); their policies live here anyway so +# the default deny never lands without the allow rules that go with it. A +# policy whose pods do not exist selects nothing. +# +# Ports are named rather than numbered where the workloads name them, so the +# same policy covers both atenet dataplanes (envoy serves the egress gateway on +# 443, agentgateway on 8443, both as "https"). +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: default-deny-ingress + namespace: ate-system +spec: + podSelector: {} + policyTypes: + - Ingress +--- +# The Substrate API. Admits control plane components and pods in namespaces +# labeled ate.dev/api-access=true (e.g. benchmarks, dashboards). Worker pods +# (ate.dev/worker-pool) are excluded. Requests are still authenticated and +# authorized by ate-api-server; this restricts network-level ingress. +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: ate-api-server + namespace: ate-system +spec: + podSelector: + matchLabels: + app: ate-api-server + policyTypes: + - Ingress + ingress: + - from: + - podSelector: + matchExpressions: + - key: app + operator: In + values: + - ate-controller + - atelet + - atenet-egress + - atenet-router + - namespaceSelector: + matchLabels: + ate.dev/api-access: "true" + podSelector: + matchExpressions: + - key: ate.dev/worker-pool + operator: DoesNotExist + ports: + - protocol: TCP + port: 443 +--- +# The bundled postgres is reached only by ate-api-server. The installer runs +# psql through `kubectl exec`, and the probes are exec probes, so neither needs +# network access. +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: postgres + namespace: ate-system +spec: + podSelector: + matchLabels: + app: postgres + policyTypes: + - Ingress + ingress: + - from: + - podSelector: + matchLabels: + app: ate-api-server + ports: + - protocol: TCP + port: postgres +--- +# atelet's gRPC port is called only by ate-api-server, which dials the atelet +# pod IP. Worker pods talk to atelet over its node-local Unix socket, not this +# port. The hostPort mapping lands on the pod, so this policy covers it too. +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: atelet + namespace: ate-system +spec: + podSelector: + matchLabels: + app: atelet + policyTypes: + - Ingress + ingress: + - from: + - podSelector: + matchLabels: + app: ate-api-server + ports: + - protocol: TCP + port: grpc +--- +# The router's data ports carry every request to an actor, from pods in any +# namespace and from load balancers and Gateways outside the cluster, so they +# admit any source. What can actually reach them is decided by the Service +# type (ClusterIP by default), any Gateway or LoadBalancer in front of the +# router, and the cluster's firewall rules; the router itself checks the target +# actor of every request. +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: atenet-router + namespace: ate-system +spec: + podSelector: + matchLabels: + app: atenet-router + policyTypes: + - Ingress + ingress: + - ports: + - protocol: TCP + port: http + - protocol: TCP + port: https + - protocol: TCP + port: connect + - protocol: TCP + port: connect-tls +--- +# The egress gateway is reached by atunnel in worker pods, which the +# WorkerPool controller labels ate.dev/worker-pool, in whatever namespace the +# WorkerPool lives in. +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: atenet-egress + namespace: ate-system +spec: + podSelector: + matchLabels: + app: atenet-egress + policyTypes: + - Ingress + ingress: + - from: + - namespaceSelector: {} + podSelector: + matchExpressions: + - key: ate.dev/worker-pool + operator: Exists + ports: + - protocol: TCP + port: https +--- +# Metrics and health endpoints. Kubelet probes come from the node and scrapers +# run in whatever namespace the monitoring stack uses (GKE Managed Prometheus, +# otel-system, a user's Prometheus), so these admit any source. They serve +# metrics and readiness only; the router's /statusz is not among them. +# +# k8s-credential-provider is left out: its own policy admits only the egress +# gateway, and it stays that way (docs/egress-credential-injection.md). Its +# kubelet probes come from the node, which NetworkPolicy does not block. +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: ate-system-observability + namespace: ate-system +spec: + podSelector: + matchExpressions: + - key: app + operator: NotIn + values: + - k8s-credential-provider + policyTypes: + - Ingress + ingress: + - ports: + - protocol: TCP + port: metrics + - protocol: TCP + port: prometheus + - protocol: TCP + port: envoy-metrics + - protocol: TCP + port: health + - protocol: TCP + port: healthz + - protocol: TCP + port: stats + - protocol: TCP + port: readiness diff --git a/manifests/ate-install/base/kustomization.yaml b/manifests/ate-install/base/kustomization.yaml index 4538b613ad..caf8b58b21 100644 --- a/manifests/ate-install/base/kustomization.yaml +++ b/manifests/ate-install/base/kustomization.yaml @@ -34,3 +34,4 @@ resources: - ../atenet-router.yaml - ../atenet-router-monitoring.yaml - ../ate-otel-config.yaml + - ../ate-system-networkpolicies.yaml diff --git a/manifests/ate-install/kind/kustomization.yaml b/manifests/ate-install/kind/kustomization.yaml index f97ad60d2c..6562870924 100644 --- a/manifests/ate-install/kind/kustomization.yaml +++ b/manifests/ate-install/kind/kustomization.yaml @@ -31,6 +31,7 @@ resources: - ../ate-controller.yaml - ./atelet - ../atenet-router.yaml + - ../ate-system-networkpolicies.yaml - ate-otel-config.yaml - rustfs.yaml - ./otel-collector.yaml diff --git a/manifests/ate-install/kind/rustfs.yaml b/manifests/ate-install/kind/rustfs.yaml index 3185390d00..1cbe063255 100644 --- a/manifests/ate-install/kind/rustfs.yaml +++ b/manifests/ate-install/kind/rustfs.yaml @@ -131,3 +131,34 @@ spec: done echo "timed out waiting for rustfs" exit 1 +--- +# ../ate-system-networkpolicies.yaml denies ingress to every ate-system pod by +# default. rustfs's S3 API is reached by the snapshot-plugin sidecars of +# ate-api-server and atelet and by the bucket-init Job; its console is not +# admitted (use a port-forward). +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: rustfs + namespace: ate-system +spec: + podSelector: + matchLabels: + app: rustfs + policyTypes: + - Ingress + ingress: + - from: + - podSelector: + matchExpressions: + - key: app + operator: In + values: + - ate-api-server + - atelet + - podSelector: + matchLabels: + batch.kubernetes.io/job-name: rustfs-bucket-init + ports: + - protocol: TCP + port: api From 46a602dfe3f573fdcce1282f708cebf4ae360c03 Mon Sep 17 00:00:00 2001 From: Lior Lieberman Date: Tue, 13 Oct 2026 19:51:14 +0000 Subject: [PATCH 16/42] Router status private (#2573) This picks up #2297 from @aiqubits, rebased on main, with the e2e failure fixed. The router's unauthenticated /statusz dashboard shows recent cross-tenant requests, worker destinations, and process configuration to any pod that can reach it. This binds it to 127.0.0.1 by default through a new --status-address flag and removes its port from the router Service. Admins can still get to it with kubectl -n ate-system port-forward deployment/atenet-router 4040:4040. On top of the original change, the e2e parking suite now port-forwards to the status port on a router pod rather than the Service, since the Service no longer publishes it. #### Breaking change GKE HealthCheckPolicy configs using port 4040 and /statusz should move to port 9091 and /readyz. The router Deployment no longer declares the status container port either. Fixes #2275 - [x] Tests pass - [x] Appropriate changes to documentation are included in the PR --------- Co-authored-by: aiqubits --- cmd/atenet/internal/router/README.md | 19 ++++- cmd/atenet/internal/router/cmd.go | 1 + cmd/atenet/internal/router/config.go | 1 + cmd/atenet/internal/router/config_test.go | 72 +++++++------------ .../router/envoyadmin_manifest_test.go | 29 ++++++-- cmd/atenet/internal/router/router.go | 20 ++++-- cmd/atenet/internal/router/status.go | 13 ++++ cmd/atenet/internal/router/status_test.go | 38 ++++++++++ internal/e2e/statusz.go | 7 +- internal/portforward/portforward.go | 11 +++ manifests/ate-install/atenet-router.yaml | 9 +-- 11 files changed, 150 insertions(+), 70 deletions(-) diff --git a/cmd/atenet/internal/router/README.md b/cmd/atenet/internal/router/README.md index 9714653ea9..717499df69 100644 --- a/cmd/atenet/internal/router/README.md +++ b/cmd/atenet/internal/router/README.md @@ -214,7 +214,17 @@ list). ## status page -Serve a `/statusz` page on port 8080. +The unauthenticated `/statusz` dashboard binds to `127.0.0.1:4040` by default +(`--status-address` and `--status-port`). It is not published by the router +Service. Access it through a pod port-forward: + +```sh +kubectl -n ate-system port-forward deployment/atenet-router 4040:4040 +``` + +Open `http://127.0.0.1:4040/statusz` (or add `?format=json`). Binding +`--status-address` to a non-loopback address exposes the diagnostic data to +other pods. Contents: @@ -222,3 +232,10 @@ Contents: * Command line args * Last 100 queries served * Build tag + +Health checks use `/healthz` (liveness) and `/readyz` (readiness) on the health +listener, configured by `--health-listen-addr` (default `:9091`). For a GKE +Gateway, migrate any `HealthCheckPolicy` using port 4040 and `/statusz` to port +9091 and `/readyz`, so draining pods also stop receiving traffic. +[GKE probes Pod IPs directly](https://docs.cloud.google.com/kubernetes-engine/docs/how-to/configure-gateway-resources#configure_health_checks), +so port 9091 does not need to be published by the router Service. diff --git a/cmd/atenet/internal/router/cmd.go b/cmd/atenet/internal/router/cmd.go index e07cfdcff2..966bd73597 100644 --- a/cmd/atenet/internal/router/cmd.go +++ b/cmd/atenet/internal/router/cmd.go @@ -67,6 +67,7 @@ func bindRouterFlags(fs *pflag.FlagSet, cfg *routerConfig) { fs.IntVar(&cfg.ExtprocPort, "port-extproc", 50051, "Listen port for the External Processing (ext_proc) server the dataplane calls") fs.StringVar(&cfg.ExtprocAddr, "extproc-address", "127.0.0.1", "Address of the External Processing (ext_proc) server: both the address it binds and the address the co-located dataplane is told to dial. Defaults to loopback, which keeps it unreachable from other pods; readiness is probed via /readyz on the health port, not this one. Empty binds every interface") fs.IntVar(&cfg.StatusPort, "status-port", 4040, "Port to serve /statusz on (set <= 0 to disable serving status)") + fs.StringVar(&cfg.StatusAddr, "status-address", "127.0.0.1", "Address to bind the unauthenticated /statusz dashboard to. Defaults to loopback; use /healthz and /readyz on the health port for health checks. Empty binds every interface") fs.DurationVar(&cfg.HealthInterval, "health-interval", 1*time.Second, "Interval for checking health of dependent services") fs.IntVar(&cfg.HttpsPort, "port-https", 8443, "TCP port for HTTPS workload traffic entering through the router dataplane") fs.StringVar(&cfg.EnvoyCertPath, "envoy-cert-path", "", "Path to the Envoy certificate file.") diff --git a/cmd/atenet/internal/router/config.go b/cmd/atenet/internal/router/config.go index 443183855a..6d4e60c4b8 100644 --- a/cmd/atenet/internal/router/config.go +++ b/cmd/atenet/internal/router/config.go @@ -155,6 +155,7 @@ type routerConfig struct { ExtprocPort int ExtprocAddr string StatusPort int + StatusAddr string HealthInterval time.Duration HttpsPort int // ConnectPlainTextPort and ConnectTLSPort are the plaintext and TLS diff --git a/cmd/atenet/internal/router/config_test.go b/cmd/atenet/internal/router/config_test.go index de46db31ea..99a738c2a4 100644 --- a/cmd/atenet/internal/router/config_test.go +++ b/cmd/atenet/internal/router/config_test.go @@ -15,13 +15,11 @@ package router import ( - "os" "strings" "testing" "time" - appsv1 "k8s.io/api/apps/v1" - "sigs.k8s.io/yaml" + "github.com/spf13/pflag" "github.com/agent-substrate/substrate/cmd/atenet/internal/router/ingress" ) @@ -428,58 +426,40 @@ const routerManifestPath = "../../../../manifests/ate-install/atenet-router.yaml // manifest rather than in Go: --drain-delay on the router container, which is // how long it keeps serving after SIGTERM, and the pod's // terminationGracePeriodSeconds, which is the whole budget. -// -// --drain-delay is read back through the router command's own flag set, so a -// renamed or retyped flag fails here rather than being parsed by a private -// copy of the syntax. func routerShutdownBudget(t *testing.T) (drainDelay, grace time.Duration) { t.Helper() - raw, err := os.ReadFile(routerManifestPath) + pod := findDeployment(t, routerManifestPath, "atenet-router").Spec.Template.Spec + if pod.TerminationGracePeriodSeconds == nil { + t.Fatalf("%s sets no terminationGracePeriodSeconds on the atenet-router pod", routerManifestPath) + } + d, err := routerManifestFlags(t).GetDuration("drain-delay") if err != nil { - t.Fatalf("reading %s: %v", routerManifestPath, err) + t.Fatalf("reading --drain-delay from the manifest args: %v", err) } - for _, doc := range strings.Split(string(raw), "\n---\n") { - var head struct { - Kind string `json:"kind"` - Metadata struct { - Name string `json:"name"` - } `json:"metadata"` - } - if err := yaml.Unmarshal([]byte(doc), &head); err != nil { - t.Fatalf("parsing a document of %s: %v", routerManifestPath, err) - } - if head.Kind != "Deployment" || head.Metadata.Name != "atenet-router" { + return d, time.Duration(*pod.TerminationGracePeriodSeconds) * time.Second +} + +// routerManifestFlags returns the router command's flags parsed from the +// atenet-router container's args in the manifest. Reading them back through +// the command's own flag set means a renamed or retyped flag fails here rather +// than being parsed by a private copy of the syntax. +func routerManifestFlags(t *testing.T) *pflag.FlagSet { + t.Helper() + for _, c := range findDeployment(t, routerManifestPath, "atenet-router").Spec.Template.Spec.Containers { + if c.Name != "atenet-router" { continue } - var deployment appsv1.Deployment - if err := yaml.Unmarshal([]byte(doc), &deployment); err != nil { - t.Fatalf("decoding the atenet-router Deployment from %s: %v", routerManifestPath, err) - } - pod := deployment.Spec.Template.Spec - if pod.TerminationGracePeriodSeconds == nil { - t.Fatalf("%s sets no terminationGracePeriodSeconds on the atenet-router pod", routerManifestPath) + cmd := NewRouterCmd() + if len(c.Args) == 0 || c.Args[0] != cmd.Name() { + t.Fatalf("the atenet-router container's args are %v; they should invoke the %q subcommand", c.Args, cmd.Name()) } - for _, c := range pod.Containers { - if c.Name != "atenet-router" { - continue - } - cmd := NewRouterCmd() - if len(c.Args) == 0 || c.Args[0] != cmd.Name() { - t.Fatalf("the atenet-router container's args are %v; they should invoke the %q subcommand", c.Args, cmd.Name()) - } - if err := cmd.ParseFlags(c.Args[1:]); err != nil { - t.Fatalf("the atenet-router container's flags are not ones the binary accepts: %v", err) - } - d, err := cmd.Flags().GetDuration("drain-delay") - if err != nil { - t.Fatalf("reading --drain-delay from the manifest args: %v", err) - } - return d, time.Duration(*pod.TerminationGracePeriodSeconds) * time.Second + if err := cmd.ParseFlags(c.Args[1:]); err != nil { + t.Fatalf("the atenet-router container's flags are not ones the binary accepts: %v", err) } - t.Fatalf("the atenet-router Deployment in %s has no container named atenet-router", routerManifestPath) + return cmd.Flags() } - t.Fatalf("%s has no Deployment named atenet-router", routerManifestPath) - return 0, 0 + t.Fatalf("the atenet-router Deployment in %s has no container named atenet-router", routerManifestPath) + return nil } func TestRouterConfigDrainTimeout(t *testing.T) { diff --git a/cmd/atenet/internal/router/envoyadmin_manifest_test.go b/cmd/atenet/internal/router/envoyadmin_manifest_test.go index 7ccd302648..93e684eb33 100644 --- a/cmd/atenet/internal/router/envoyadmin_manifest_test.go +++ b/cmd/atenet/internal/router/envoyadmin_manifest_test.go @@ -356,20 +356,37 @@ func parseEnvoyAdminBootstrap(t *testing.T, raw string) envoyAdminBootstrap { } func findDeployment(t *testing.T, path, name string) appsv1.Deployment { + t.Helper() + var d appsv1.Deployment + manifestObject(t, path, "Deployment", name, &d) + return d +} + +// manifestObject decodes the object with the given kind and name in the +// manifest at path into into. +func manifestObject(t *testing.T, path, kind, name string, into any) { t.Helper() raw, err := os.ReadFile(path) if err != nil { t.Fatalf("reading %s: %v", path, err) } for _, doc := range strings.Split(string(raw), "\n---\n") { - var d appsv1.Deployment - if err := yaml.Unmarshal([]byte(doc), &d); err != nil { + var head struct { + Kind string `json:"kind"` + Metadata struct { + Name string `json:"name"` + } `json:"metadata"` + } + if err := yaml.Unmarshal([]byte(doc), &head); err != nil { t.Fatalf("parsing a document of %s: %v", path, err) } - if d.Kind == "Deployment" && d.Name == name { - return d + if head.Kind != kind || head.Metadata.Name != name { + continue + } + if err := yaml.Unmarshal([]byte(doc), into); err != nil { + t.Fatalf("decoding the %s %s from %s: %v", name, kind, path, err) } + return } - t.Fatalf("%s has no Deployment named %s", path, name) - return appsv1.Deployment{} + t.Fatalf("%s has no %s named %s", path, kind, name) } diff --git a/cmd/atenet/internal/router/router.go b/cmd/atenet/internal/router/router.go index d2c7477f69..7afad07a3d 100644 --- a/cmd/atenet/internal/router/router.go +++ b/cmd/atenet/internal/router/router.go @@ -257,6 +257,17 @@ func (s *RouterServer) Run(ctx context.Context) error { s.health = newRouterHealth(s.cfg.HealthInterval, s.clientset, s.apiClient, s.cfg) + // Bound before the background servers below start, so a bad + // --status-address fails startup without leaving them running. + var statusListener net.Listener + if s.cfg.StatusPort > 0 { + statusListener, err = s.cfg.listenStatus() + if err != nil { + return err + } + defer statusListener.Close() + } + // The ingress control plane — the xDS server — configures the *ingress* // dataplane. The egress gateway is statically configured, so an egress-only // instance does not run it. @@ -297,12 +308,7 @@ func (s *RouterServer) Run(ctx context.Context) error { }) // Start HTTP status endpoint - if s.cfg.StatusPort > 0 { - listener, err := net.Listen("tcp", fmt.Sprintf(":%d", s.cfg.StatusPort)) - if err != nil { - return fmt.Errorf("failed binding Router HTTP status server port: %w", err) - } - + if statusListener != nil { mux := http.NewServeMux() mux.HandleFunc("/statusz", s.handleStatusz) @@ -312,7 +318,7 @@ func (s *RouterServer) Run(ctx context.Context) error { g.Go(func() error { go func() { - if err := httpServer.Serve(listener); err != nil && !errors.Is(err, http.ErrServerClosed) { + if err := httpServer.Serve(statusListener); err != nil && !errors.Is(err, http.ErrServerClosed) { slog.ErrorContext(ctx, "status HTTP server exited unexpectedly", slog.Any("err", err)) } }() diff --git a/cmd/atenet/internal/router/status.go b/cmd/atenet/internal/router/status.go index 9fe196908f..2afae673a1 100644 --- a/cmd/atenet/internal/router/status.go +++ b/cmd/atenet/internal/router/status.go @@ -20,9 +20,11 @@ import ( "encoding/json" "fmt" "html/template" + "net" "net/http" "os" "runtime/debug" + "strconv" "strings" "time" @@ -63,6 +65,17 @@ type FormattedQuery struct { Duration string `json:"duration"` } +// listenStatus binds the /statusz listener on --status-address and +// --status-port. +func (c routerConfig) listenStatus() (net.Listener, error) { + addr := net.JoinHostPort(c.StatusAddr, strconv.Itoa(c.StatusPort)) + lis, err := net.Listen("tcp", addr) + if err != nil { + return nil, fmt.Errorf("failed binding Router HTTP status server address %q: %w", addr, err) + } + return lis, nil +} + func (s *RouterServer) getRouterIP(ctx context.Context) string { if s.clientset == nil { return "Offline Mode (No Cluster IP)" diff --git a/cmd/atenet/internal/router/status_test.go b/cmd/atenet/internal/router/status_test.go index 8e372a6b26..02f3055a5c 100644 --- a/cmd/atenet/internal/router/status_test.go +++ b/cmd/atenet/internal/router/status_test.go @@ -34,6 +34,8 @@ import ( "testing" "time" + corev1 "k8s.io/api/core/v1" + "github.com/agent-substrate/substrate/cmd/atenet/internal/router/extproc" "github.com/agent-substrate/substrate/cmd/atenet/internal/router/ingress" ) @@ -58,10 +60,15 @@ func TestStatuszEndpoint(t *testing.T) { // Run() dials ateapi in mtls mode, which requires real TLS material; generate it. caPath, clientCertPath := writeTestTLSMaterial(t) + statusAddr, err := NewRouterCmd().Flags().GetString("status-address") + if err != nil { + t.Fatal(err) + } cfg := routerConfig{ Mode: ModeIngress, Namespace: "default", StatusPort: httpPort, + StatusAddr: statusAddr, HttpPort: 8080, XdsPort: 18000, ExtprocPort: 50051, @@ -170,6 +177,37 @@ func TestStatuszEndpoint(t *testing.T) { } } +// The manifest's router args, defaults included, keep /statusz off the pod IP. +func TestRouterManifestBindsStatusToLoopback(t *testing.T) { + addr, err := routerManifestFlags(t).GetString("status-address") + if err != nil { + t.Fatalf("reading --status-address from the manifest args: %v", err) + } + // Port 0 binds any free port; only the address is under test. + lis, err := routerConfig{StatusAddr: addr}.listenStatus() + if err != nil { + t.Fatal(err) + } + defer lis.Close() + if ip := lis.Addr().(*net.TCPAddr).IP; !ip.IsLoopback() { + t.Errorf("--status-address %q in %s binds %s, which other pods can reach", addr, routerManifestPath, ip) + } +} + +func TestRouterServiceDoesNotExposeStatus(t *testing.T) { + statusPort, err := routerManifestFlags(t).GetInt("status-port") + if err != nil { + t.Fatalf("reading --status-port from the manifest args: %v", err) + } + var service corev1.Service + manifestObject(t, routerManifestPath, "Service", "atenet-router", &service) + for _, port := range service.Spec.Ports { + if port.Name == "status" || int(port.Port) == statusPort || port.TargetPort.IntValue() == statusPort || port.TargetPort.StrVal == "status" { + t.Errorf("router Service exposes the status server: %+v", port) + } + } +} + // writeTestKubeconfig writes a kubeconfig pointing at an unreachable local // server and returns its path. It satisfies client construction; any request // made through the resulting clientset fails with a connection error. diff --git a/internal/e2e/statusz.go b/internal/e2e/statusz.go index a837a82874..cae64166fd 100644 --- a/internal/e2e/statusz.go +++ b/internal/e2e/statusz.go @@ -26,8 +26,9 @@ import ( "k8s.io/client-go/kubernetes" ) -// routerStatusPort is the atenet-router Service port for the router's -// --status-port (the "status" container port). +// routerStatusPort is the router's --status-port. The router binds it to +// loopback and the Service does not publish it, so the client forwards to a +// router pod directly. const routerStatusPort = 4040 // StatuszClient reads the atenet router's /statusz page over a @@ -51,7 +52,7 @@ func NewStatuszClient(ctx context.Context) (*StatuszClient, error) { return nil, fmt.Errorf("creating k8s client: %w", err) } - localPort, stop, err := portforward.ServicePortForward(ctx, config, clientset, SystemNamespace(), ResourceName("atenet-router"), routerStatusPort) + localPort, stop, err := portforward.ServicePodPortForward(ctx, config, clientset, SystemNamespace(), ResourceName("atenet-router"), routerStatusPort) if err != nil { return nil, err } diff --git a/internal/portforward/portforward.go b/internal/portforward/portforward.go index 7fa750b251..79d1868e0d 100644 --- a/internal/portforward/portforward.go +++ b/internal/portforward/portforward.go @@ -51,6 +51,17 @@ func ServicePortForward(ctx context.Context, config *rest.Config, clientset kube return podPortForward(ctx, config, clientset, namespace, pod.Name, targetPort) } +// ServicePodPortForward forwards a random local port straight to +// containerPort on a ready pod behind the named Service, for ports the +// Service does not publish. Otherwise it behaves like ServicePortForward. +func ServicePodPortForward(ctx context.Context, config *rest.Config, clientset kubernetes.Interface, namespace, service string, containerPort int32) (int, func(), error) { + pod, _, err := firstReadyPodForService(ctx, clientset, namespace, service) + if err != nil { + return 0, nil, err + } + return podPortForward(ctx, config, clientset, namespace, pod.Name, containerPort) +} + func firstReadyPodForService(ctx context.Context, clientset kubernetes.Interface, namespace, service string) (*corev1.Pod, *corev1.Service, error) { svc, err := clientset.CoreV1().Services(namespace).Get(ctx, service, metav1.GetOptions{}) if err != nil { diff --git a/manifests/ate-install/atenet-router.yaml b/manifests/ate-install/atenet-router.yaml index cf7308d082..cb077f9685 100644 --- a/manifests/ate-install/atenet-router.yaml +++ b/manifests/ate-install/atenet-router.yaml @@ -284,12 +284,11 @@ spec: - configMapRef: name: ate-otel-config # The xDS server (--port-xds) binds 127.0.0.1 and serves only the Envoy - # in this pod, so it is deliberately not declared as a container port. + # in this pod, and the /statusz page (--status-port) binds 127.0.0.1 for + # port-forward access, so neither is declared as a container port. ports: - name: extproc containerPort: 50051 - - name: status - containerPort: 4040 - name: metrics containerPort: 9090 - name: health @@ -438,7 +437,3 @@ spec: port: 8444 targetPort: 8444 protocol: TCP - - name: status - port: 4040 - targetPort: status - protocol: TCP From 890716eafd0e9df36833f675081727f75ec740f5 Mon Sep 17 00:00:00 2001 From: Anna Pendleton Date: Tue, 13 Oct 2026 20:05:14 +0000 Subject: [PATCH 17/42] Envoy, Alpine and dev-tool dependency bumps clear pre-GA CVEs (#2568) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes #2303 - `envoyproxy/envoy` v1.39.1 → v1.39.4 (libssl3 `…1.29` → `…1.30`), everywhere it was pinned. - `alpine` 3.24.1 → 3.24.2 for `demos/sandbox` (OpenSSL 3.5.7 → 3.5.8). - `hack/tools`: `golang.org/x/*` to latest. - [x] Tests pass - [x] Appropriate changes to documentation are included in the PR --------- Co-authored-by: Anna Pendleton <13793293+annapendleton@users.noreply.github.com> --- .ko.yaml | 2 +- .../traffic-driver-invoker.yaml | 2 +- cmd/dataplane/envoy/Dockerfile | 2 +- cmd/dataplane/envoy/README.md | 2 +- hack/setup-e2e-egress-tls-kind.sh | 2 +- hack/tools/code-generator/go.mod | 29 ++----- hack/tools/code-generator/go.sum | 86 ++++--------------- hack/tools/controller-gen/go.mod | 12 +-- hack/tools/controller-gen/go.sum | 28 +++--- hack/tools/go-licenses/go.mod | 12 +-- hack/tools/go-licenses/go.sum | 24 +++--- hack/tools/golangci-lint/go.mod | 10 +-- hack/tools/golangci-lint/go.sum | 24 +++--- hack/tools/gotestsum/go.mod | 12 +-- hack/tools/gotestsum/go.sum | 24 +++--- hack/tools/kind/go.mod | 2 +- hack/tools/kind/go.sum | 3 +- hack/tools/ko/go.mod | 18 ++-- hack/tools/ko/go.sum | 36 ++++---- hack/tools/kube-api-linter/go.mod | 10 +-- hack/tools/kube-api-linter/go.sum | 24 +++--- hack/tools/setup-envtest/go.mod | 2 +- hack/tools/setup-envtest/go.sum | 16 ++-- manifests/ate-install/atenet-router.yaml | 2 +- 24 files changed, 156 insertions(+), 228 deletions(-) diff --git a/.ko.yaml b/.ko.yaml index e49180e73f..7dd90c96e7 100644 --- a/.ko.yaml +++ b/.ko.yaml @@ -19,7 +19,7 @@ defaultPlatforms: - linux/arm64 baseImageOverrides: - github.com/agent-substrate/substrate/demos/sandbox: alpine:3.24@sha256:28bd5fe8b56d1bd048e5babf5b10710ebe0bae67db86916198a6eec434943f8b + github.com/agent-substrate/substrate/demos/sandbox: alpine:3.24.2@sha256:294b683cb724975bec92580e1e685676bd4b50bda910ddb8c51d4cabeaec77e6 # atelet needs glibc to run the node's kubelet credential provider, which # some platforms ship dynamically linked (e.g. AKS, GKE Ubuntu). github.com/agent-substrate/substrate/cmd/atelet: gcr.io/distroless/base-debian13:latest@sha256:389cad21f73e4c37b94ffe5b13736d5a92bd5bd3c6c6b38c2be1c881e14ba2bd diff --git a/benchmarking/nighthawk-egress/traffic-driver-invoker.yaml b/benchmarking/nighthawk-egress/traffic-driver-invoker.yaml index 799ad103bc..4e0f223d6d 100644 --- a/benchmarking/nighthawk-egress/traffic-driver-invoker.yaml +++ b/benchmarking/nighthawk-egress/traffic-driver-invoker.yaml @@ -110,7 +110,7 @@ spec: containers: - name: envoy # Same envoyproxy/envoy digest as cmd/dataplane/envoy/Dockerfile. - image: envoyproxy/envoy:v1.39.1@sha256:57e14a549d7bd43c8d3f6d03e8cfa653e037d4b38e133acd9b54f38c524401b4 + image: envoyproxy/envoy:v1.39.4@sha256:74bd4eb5629708d3fadca1a5e6b85d208f338d7bb54e06b69565905c5294cc01 command: - /usr/local/bin/envoy args: diff --git a/cmd/dataplane/envoy/Dockerfile b/cmd/dataplane/envoy/Dockerfile index 9de411563c..f133873e9e 100644 --- a/cmd/dataplane/envoy/Dockerfile +++ b/cmd/dataplane/envoy/Dockerfile @@ -34,7 +34,7 @@ RUN cargo build --release --locked # Pin the multi-platform image index, not one platform's manifest, so that the # --platform the installer passes (the host architecture on kind, linux/amd64 # otherwise) selects a matching Envoy binary. -FROM envoyproxy/envoy:v1.39.1@sha256:57e14a549d7bd43c8d3f6d03e8cfa653e037d4b38e133acd9b54f38c524401b4 +FROM envoyproxy/envoy:v1.39.4@sha256:74bd4eb5629708d3fadca1a5e6b85d208f338d7bb54e06b69565905c5294cc01 # Copy the built dynamic module shared library from the builder stage COPY --from=builder /usr/src/dynamic-modules/egress-policy/target/release/libenvoy_substrate_egress_policy.so /usr/local/lib/libenvoy_substrate_egress_policy.so diff --git a/cmd/dataplane/envoy/README.md b/cmd/dataplane/envoy/README.md index 23727118fc..a3ffb8602e 100644 --- a/cmd/dataplane/envoy/README.md +++ b/cmd/dataplane/envoy/README.md @@ -13,7 +13,7 @@ cmd/dataplane/envoy/ - **`Dockerfile`**: Multi-stage container build that: 1. Compiles the Rust dynamic module (`envoy-substrate-egress-policy`) into a shared library (`libenvoy_substrate_egress_policy.so`) in a `rust:bookworm` builder stage. - 2. Packages the compiled `.so` into the `envoyproxy/envoy:v1.39-latest` runtime image under `/usr/local/lib/libenvoy_substrate_egress_policy.so` and sets `ENVOY_DYNAMIC_MODULES_SEARCH_PATH=/usr/local/lib`. + 2. Packages the compiled `.so` into the `envoyproxy/envoy:v1.39.4` runtime image under `/usr/local/lib/libenvoy_substrate_egress_policy.so` and sets `ENVOY_DYNAMIC_MODULES_SEARCH_PATH=/usr/local/lib`. - **`dynamic-modules/egress-policy/`**: A Rust crate using the Envoy Dynamic Modules SDK (`envoy-proxy-dynamic-modules-rust-sdk`) that implements a custom Envoy listener filter for Substrate egress policy evaluation. See [`dynamic-modules/egress-policy/README.md`](dynamic-modules/egress-policy/README.md) for module-specific build, test, and Envoy configuration details. ## Building and Deployment diff --git a/hack/setup-e2e-egress-tls-kind.sh b/hack/setup-e2e-egress-tls-kind.sh index 47c36a160d..ebfc6906d4 100755 --- a/hack/setup-e2e-egress-tls-kind.sh +++ b/hack/setup-e2e-egress-tls-kind.sh @@ -42,7 +42,7 @@ fi # this E2E test setup; production keeps the gateway's normal root behavior. root_image="${image}" if [[ "${gateway}" == agentgateway ]]; then - root_image="envoyproxy/envoy:v1.39-latest@sha256:57e14a549d7bd43c8d3f6d03e8cfa653e037d4b38e133acd9b54f38c524401b4" + root_image="envoyproxy/envoy:v1.39.4@sha256:74bd4eb5629708d3fadca1a5e6b85d208f338d7bb54e06b69565905c5294cc01" fi # Use the helper image's public roots on every Pod start. The separate init diff --git a/hack/tools/code-generator/go.mod b/hack/tools/code-generator/go.mod index 2ffce8047e..a6fa2a4ae5 100644 --- a/hack/tools/code-generator/go.mod +++ b/hack/tools/code-generator/go.mod @@ -3,44 +3,25 @@ module github.com/agent-substrate/substrate/hack/tools/code-generator go 1.27.0 require ( - github.com/emicklei/go-restful/v3 v3.13.0 // indirect github.com/fxamacker/cbor/v2 v2.9.2 // indirect github.com/go-logr/logr v1.4.4 // indirect - github.com/go-openapi/jsonpointer v1.0.0 // indirect - github.com/go-openapi/jsonreference v1.0.0 // indirect - github.com/go-openapi/swag v0.28.0 // indirect - github.com/go-openapi/swag/cmdutils v0.28.0 // indirect - github.com/go-openapi/swag/conv v0.28.0 // indirect - github.com/go-openapi/swag/fileutils v0.28.0 // indirect - github.com/go-openapi/swag/jsonutils v0.28.0 // indirect - github.com/go-openapi/swag/loading v0.28.0 // indirect - github.com/go-openapi/swag/mangling v0.28.0 // indirect - github.com/go-openapi/swag/netutils v0.28.0 // indirect - github.com/go-openapi/swag/pools v0.28.0 // indirect - github.com/go-openapi/swag/stringutils v0.28.0 // indirect - github.com/go-openapi/swag/typeutils v0.28.0 // indirect - github.com/go-openapi/swag/yamlutils v0.28.0 // indirect - github.com/google/gnostic-models v0.7.1 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect github.com/spf13/pflag v1.0.10 // indirect github.com/x448/float16 v0.8.4 // indirect go.yaml.in/yaml/v2 v2.4.4 // indirect - go.yaml.in/yaml/v3 v3.0.5 // indirect - golang.org/x/mod v0.40.0 // indirect - golang.org/x/net v0.58.0 // indirect - golang.org/x/sync v0.22.0 // indirect - golang.org/x/text v0.41.0 // indirect - golang.org/x/tools v0.49.0 // indirect - google.golang.org/protobuf v1.36.12 // indirect + golang.org/x/mod v0.42.0 // indirect + golang.org/x/net v0.61.0 // indirect + golang.org/x/sync v0.24.0 // indirect + golang.org/x/text v0.43.0 // indirect + golang.org/x/tools v0.52.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect k8s.io/apimachinery v0.36.0-alpha.2.0.20260904054801-5a60cf1c6b81 // indirect k8s.io/code-generator v0.30.0-alpha.3.0.20260904060942-0cddac4383e4 // indirect k8s.io/gengo/v2 v2.0.0-20260408192533-25e2208e0dc3 // indirect k8s.io/klog/v2 v2.140.0 // indirect k8s.io/kube-openapi v0.0.0-20260721132016-d427ff9ee9ad // indirect - k8s.io/streaming v0.37.0 // indirect k8s.io/utils v0.0.0-20260707023825-cf1189d6abe3 // indirect sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect sigs.k8s.io/randfill v1.0.0 // indirect diff --git a/hack/tools/code-generator/go.sum b/hack/tools/code-generator/go.sum index e2b65bfe38..0821f0432b 100644 --- a/hack/tools/code-generator/go.sum +++ b/hack/tools/code-generator/go.sum @@ -1,43 +1,11 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/emicklei/go-restful/v3 v3.13.0 h1:C4Bl2xDndpU6nJ4bc1jXd+uTmYPVUwkD6bFY/oTyCes= -github.com/emicklei/go-restful/v3 v3.13.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/fxamacker/cbor/v2 v2.9.2 h1:X4Ksno9+x3cz0TZv69ec1hxP/+tymuR8PXQJyDwfh78= github.com/fxamacker/cbor/v2 v2.9.2/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= -github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= -github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/logr v1.4.4 h1:tG4xh9yMsRCAiodLVTxyrkzSZ9+o0L1Kg/+cPVcbP/8= github.com/go-logr/logr v1.4.4/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= -github.com/go-openapi/jsonpointer v1.0.0 h1:kR9tHqY0CtZaOPVFm622dPVNhrvYpwr4uCxgL3h1H8s= -github.com/go-openapi/jsonpointer v1.0.0/go.mod h1:Z3rw7dWu1p9IgitXCFamSlA5lmDiklEB6vkaxcNZW5Y= -github.com/go-openapi/jsonreference v1.0.0 h1:jlmTr6torcd1YgDQvSfNmRtKzYDO4FGBkrAdlAVWnpY= -github.com/go-openapi/jsonreference v1.0.0/go.mod h1:jtwdyGbJk0Xhe5Y+rwtglQP6Sb1WZST4rT32LWB+sv0= -github.com/go-openapi/swag v0.28.0 h1:xkgbOSKj6DZziNpyqRRAOt3GJGtgjgsd2RoyT30VWuw= -github.com/go-openapi/swag v0.28.0/go.mod h1:4qYnT3Cqr1p1VknOdPo70evN4rgQnAg6jwApHyxSGIg= -github.com/go-openapi/swag/cmdutils v0.28.0 h1:7TOeNtkYru1SG8Y34tDh9WBbLsMqGnptuxWiHREPZ4Q= -github.com/go-openapi/swag/cmdutils v0.28.0/go.mod h1:Sm1MVFMkF6guJJ+pQqHnQA3N0j9qALV3NxzDSv6bETM= -github.com/go-openapi/swag/conv v0.28.0 h1:GtqqbyFe7vR5Y7ehxG9W6/OvrSFdf1OLeTGp40TqxH8= -github.com/go-openapi/swag/conv v0.28.0/go.mod h1:mbUE+mzctnhxi864m0Q07SpN8OowD9JhxmxuYvZZD/k= -github.com/go-openapi/swag/fileutils v0.28.0 h1:Z04XWQD7R8Eq+7GnOrjovBxPPmZzsS4gt2H2GPGIViU= -github.com/go-openapi/swag/fileutils v0.28.0/go.mod h1:VvJFZLTZS0AI854gEQz5tk7dBESdLjiNUMSZ/th2ry8= -github.com/go-openapi/swag/jsonutils v0.28.0 h1:YIch6FwO7RXzeAnbO8Tu7dWBZeUEH+4nA0HXltVTnv4= -github.com/go-openapi/swag/jsonutils v0.28.0/go.mod h1:CYM3WlTUcagR2ZoHdz54di/cbBqt82tuxuXgAjxw+mg= -github.com/go-openapi/swag/loading v0.28.0 h1:td8QZdZC9MIYGGSnSPKShKiK22I2tU5UQvuUhIBPRLU= -github.com/go-openapi/swag/loading v0.28.0/go.mod h1:rXB0QiQX5mMveXEA7ouM4KiiM9jVJe4K6BVbwhD1M4k= -github.com/go-openapi/swag/mangling v0.28.0 h1:pH8eyeNO9SLYsTMWJrurnNfKmDa28XrlA+HePVD53VM= -github.com/go-openapi/swag/mangling v0.28.0/go.mod h1:jtBE2+V+3pILxOR7Vgce+Cwp6A2PgZbvVqfNntbVs0w= -github.com/go-openapi/swag/netutils v0.28.0 h1:YXN6TALEi2pzts8/8GNm6T61HTAZsieukGZidap989k= -github.com/go-openapi/swag/netutils v0.28.0/go.mod h1:J+WYyFMLtvtCGqa6jLv+YNUmIKI3ZRQRrvfNDMoQoEQ= -github.com/go-openapi/swag/pools v0.28.0 h1:HPMZWSAfce3rdVTFcjFiCIBtDg9h4x2QlRrHipwhxeU= -github.com/go-openapi/swag/pools v0.28.0/go.mod h1:kVQefhSK5RWuRe7BXsL8htgBPAMpN7HDGpGEknqugeE= -github.com/go-openapi/swag/stringutils v0.28.0 h1:ixsc9iYgDPubHL/8nSkbnryEHpD2VRlBMLKpQyPXcDU= -github.com/go-openapi/swag/stringutils v0.28.0/go.mod h1:lzRN95CxXmA03XcDWHLOb6nOMcxCqR5rGY0lOgsfRoM= -github.com/go-openapi/swag/typeutils v0.28.0 h1:nRBKSBXjDgf01VDPB3fWeD9nQuhCOVeIYAkUx2tbkyY= -github.com/go-openapi/swag/typeutils v0.28.0/go.mod h1:Srm0xFNRZ1Y+vCxJclo5qzx8aj+1pAKda/YfFPrG0dQ= -github.com/go-openapi/swag/yamlutils v0.28.0 h1:TV3JXH6DS46KUroDtMLAYHGkdWf5VDq3wVWFirmzROY= -github.com/go-openapi/swag/yamlutils v0.28.0/go.mod h1:x0q/yndZHEgk9Rx3DyDqzFUmHy55KTvIZldvF2dTJXs= -github.com/google/gnostic-models v0.7.1 h1:SisTfuFKJSKM5CPZkffwi6coztzzeYUhc3v4yxLWH8c= -github.com/google/gnostic-models v0.7.1/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= @@ -50,64 +18,42 @@ github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjY github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8= github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY= -github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE= +github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ= go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ= go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw= go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg= -golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA= -golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w= -golang.org/x/mod v0.40.0 h1:hUv+3cXcdRHz08UmSiOob7sadHig73uo5bkXxQ/tvUs= -golang.org/x/mod v0.40.0/go.mod h1:0/weTWkPWGBikyTWAX3dkjVztMmBA5hM0DH6BElSupE= -golang.org/x/net v0.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To= -golang.org/x/net v0.58.0/go.mod h1:YwCddHnFlT7eLQqVprV19OnhLGtc5xOKgE0RyqgfWAU= -golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= -golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= -golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek= -golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= -golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= -golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= -golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8= -golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M= -golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= -golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= -golang.org/x/tools v0.49.0 h1:3NI7VXzL9+1WZD52Dx2ttoPwD5DWrFGpl9mFZDlmisI= -golang.org/x/tools v0.49.0/go.mod h1:SJNXV9DBKT0UbdttsQjbfJlAE/q+y36++zo3uL3N0Oo= -google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fwxc= -google.golang.org/protobuf v1.36.12/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +golang.org/x/mod v0.42.0 h1:ICeL3t2L30ljSPHHxDnR4fqVcBt/zR2zJAb9juavmQg= +golang.org/x/mod v0.42.0/go.mod h1:u5fzIrRInkL8MaO1cbRkUHT64v303YIT5KEqpNr7jRs= +golang.org/x/net v0.61.0 h1:RR+6j/BTBCrggOLugxa+66R8FvoN6MHlzDlZIbN9YFw= +golang.org/x/net v0.61.0/go.mod h1:WgDCOTH8iwtB66T4/GfI94AHo/hhdb7Hah9NDseQfiI= +golang.org/x/sync v0.24.0 h1:iqiDrGoDgyuvAKrhLQthUEzU/R378N78gXbBjxed7oA= +golang.org/x/sync v0.24.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0= +golang.org/x/text v0.43.0 h1:1QivrlhwAsnMOcqgOdl9my7s9OyZl8O5/UGUTJVXoWc= +golang.org/x/text v0.43.0/go.mod h1:hbSIYA/amXcRXTFSZqh8tWHEoqRvvbKPJ2uz/KU+a+s= +golang.org/x/tools v0.52.0 h1:ryxg4U63oaUYk7uOLTF37n43f4qgtGvqV4Ioit1BaEs= +golang.org/x/tools v0.52.0/go.mod h1:ItcP5UNaVGG+Yc4eIYmaFLNJ4soEopkx5Y2d4lcEurs= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= k8s.io/apimachinery v0.36.0-alpha.2.0.20260904054801-5a60cf1c6b81 h1:RwCPehCTEZyBA64K7xaHTyXwIwsoBdsLmq21B0P/VcY= k8s.io/apimachinery v0.36.0-alpha.2.0.20260904054801-5a60cf1c6b81/go.mod h1:1Um7tr4O65Pd822NFLn+MI08uCzUIPahh2lCPsWzNgk= -k8s.io/apimachinery v0.37.0-rc.0 h1:z92lapcEJUiMb38pzUIp81kEXT6lIWXhs6auvm8+/s4= -k8s.io/apimachinery v0.37.0-rc.0/go.mod h1:mhq6CPCzI6XJNHSiek+w7Ws9/rP9qL5s+7aBrh5ODSI= k8s.io/code-generator v0.30.0-alpha.3.0.20260904060942-0cddac4383e4 h1:WgL8fyPUJi6VbagUkeOhvYYKCIB/ChMCtIalZ9TBh1Q= k8s.io/code-generator v0.30.0-alpha.3.0.20260904060942-0cddac4383e4/go.mod h1:AA8MJmGgNcxTDTeJgSh2SFpNtAqq9KvjpYooMOcZmDk= -k8s.io/code-generator v0.35.0 h1:TvrtfKYZTm9oDF2z+veFKSCcgZE3Igv0svY+ehCmjHQ= -k8s.io/code-generator v0.35.0/go.mod h1:iS1gvVf3c/T71N5DOGYO+Gt3PdJ6B9LYSvIyQ4FHzgc= -k8s.io/code-generator v0.37.0-rc.0 h1:d0i65nsz5LS2HUl3CgJcjxFr3IvLBGs82IZth2UuQEk= -k8s.io/code-generator v0.37.0-rc.0/go.mod h1:gwljmdmKalNet8U3+3tu8dyV0+etqGStU2uXJy+NgD8= -k8s.io/gengo/v2 v2.0.0-20250922181213-ec3ebc5fd46b h1:gMplByicHV/TJBizHd9aVEsTYoJBnnUAT5MHlTkbjhQ= -k8s.io/gengo/v2 v2.0.0-20250922181213-ec3ebc5fd46b/go.mod h1:CgujABENc3KuTrcsdpGmrrASjtQsWCT7R99mEV4U/fM= k8s.io/gengo/v2 v2.0.0-20260408192533-25e2208e0dc3 h1:3L6PNkMLXkU/pz3jWzaaIUz0Rs2V9h+5O51AeRC7poc= k8s.io/gengo/v2 v2.0.0-20260408192533-25e2208e0dc3/go.mod h1:yvyl3l9E+UxlqOMUULdKTAYB0rEhsmjr7+2Vb/1pCSo= -k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= -k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= k8s.io/klog/v2 v2.140.0 h1:Tf+J3AH7xnUzZyVVXhTgGhEKnFqye14aadWv7bzXdzc= k8s.io/klog/v2 v2.140.0/go.mod h1:o+/RWfJ6PwpnFn7OyAG3QnO47BFsymfEfrz6XyYSSp0= k8s.io/kube-openapi v0.0.0-20260721132016-d427ff9ee9ad h1:oXImqH8mQNk7PmvzKhmN3ddJoY6OnyM225MXwGHPm0A= k8s.io/kube-openapi v0.0.0-20260721132016-d427ff9ee9ad/go.mod h1:0/mqHCVhlumdJ3BhCfnjSZQE037nAhNodh1/hK0T8/I= -k8s.io/streaming v0.37.0-rc.0 h1:zDBjrKCnSLQyznohS8DyT1vmiD1s7XEcj5B9kyfN1WQ= -k8s.io/streaming v0.37.0-rc.0/go.mod h1:APlJR26ZWRcVy5bIEj0QRrKUXROtBHPcxl2NT7EAzPU= -k8s.io/streaming v0.37.0 h1:iPBUZLZiKt5bV+lxJurASMOV07VuBhNpiwJt2//AWrM= -k8s.io/streaming v0.37.0/go.mod h1:APlJR26ZWRcVy5bIEj0QRrKUXROtBHPcxl2NT7EAzPU= k8s.io/utils v0.0.0-20260707023825-cf1189d6abe3 h1:jVkFFVfXdXP74B/zbO3hM3hpSFD0xvhQ5U686DPurkE= k8s.io/utils v0.0.0-20260707023825-cf1189d6abe3/go.mod h1:M2s5JB1lIYP3jzZdorPLHXIPJzt9vv2muW5a6L9DtNM= sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg= diff --git a/hack/tools/controller-gen/go.mod b/hack/tools/controller-gen/go.mod index 1772888e8a..977f2766bf 100644 --- a/hack/tools/controller-gen/go.mod +++ b/hack/tools/controller-gen/go.mod @@ -24,12 +24,12 @@ require ( github.com/x448/float16 v0.8.4 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/mod v0.35.0 // indirect - golang.org/x/net v0.55.0 // indirect - golang.org/x/sync v0.20.0 // indirect - golang.org/x/sys v0.45.0 // indirect - golang.org/x/text v0.37.0 // indirect - golang.org/x/tools v0.44.0 // indirect + golang.org/x/mod v0.42.0 // indirect + golang.org/x/net v0.61.0 // indirect + golang.org/x/sync v0.24.0 // indirect + golang.org/x/sys v0.49.0 // indirect + golang.org/x/text v0.43.0 // indirect + golang.org/x/tools v0.52.0 // indirect google.golang.org/protobuf v1.36.8 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect diff --git a/hack/tools/controller-gen/go.sum b/hack/tools/controller-gen/go.sum index 42aba42267..00fbaaf79b 100644 --- a/hack/tools/controller-gen/go.sum +++ b/hack/tools/controller-gen/go.sum @@ -145,26 +145,26 @@ go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8= golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= -golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM= -golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU= -golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8= -golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww= +golang.org/x/mod v0.42.0 h1:ICeL3t2L30ljSPHHxDnR4fqVcBt/zR2zJAb9juavmQg= +golang.org/x/mod v0.42.0/go.mod h1:u5fzIrRInkL8MaO1cbRkUHT64v303YIT5KEqpNr7jRs= +golang.org/x/net v0.61.0 h1:RR+6j/BTBCrggOLugxa+66R8FvoN6MHlzDlZIbN9YFw= +golang.org/x/net v0.61.0/go.mod h1:WgDCOTH8iwtB66T4/GfI94AHo/hhdb7Hah9NDseQfiI= golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= -golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= -golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sync v0.24.0 h1:iqiDrGoDgyuvAKrhLQthUEzU/R378N78gXbBjxed7oA= +golang.org/x/sync v0.24.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY= -golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= -golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4= -golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk= -golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= -golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= +golang.org/x/sys v0.49.0 h1:XbzkgYJHdqh/8m2Uu0W/dQv8nktxx4BFHp1M0gROTXA= +golang.org/x/sys v0.49.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og= +golang.org/x/term v0.47.0 h1:cpyOwQkr2Nzxe2p6KnrTG7Ei2wtzjsuSjhpXXepZ/BM= +golang.org/x/term v0.47.0/go.mod h1:efAfAuG6RKABvg1ArGebRkvX6LXul14SM67o2e4mUCY= +golang.org/x/text v0.43.0 h1:1QivrlhwAsnMOcqgOdl9my7s9OyZl8O5/UGUTJVXoWc= +golang.org/x/text v0.43.0/go.mod h1:hbSIYA/amXcRXTFSZqh8tWHEoqRvvbKPJ2uz/KU+a+s= golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY= golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= -golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c= -golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI= +golang.org/x/tools v0.52.0 h1:ryxg4U63oaUYk7uOLTF37n43f4qgtGvqV4Ioit1BaEs= +golang.org/x/tools v0.52.0/go.mod h1:ItcP5UNaVGG+Yc4eIYmaFLNJ4soEopkx5Y2d4lcEurs= golang.org/x/tools/go/expect v0.1.1-deprecated h1:jpBZDwmgPhXsKZC6WhL20P4b/wmnpsEAGHaNy0n/rJM= golang.org/x/tools/go/expect v0.1.1-deprecated/go.mod h1:eihoPOH+FgIqa3FpoTwguz/bVUSGBlGQU67vpBeOrBY= golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated h1:1h2MnaIAIXISqTFKdENegdpAgUXz6NrPEsbIeWaBRvM= diff --git a/hack/tools/go-licenses/go.mod b/hack/tools/go-licenses/go.mod index f0482391d2..432b1753f3 100644 --- a/hack/tools/go-licenses/go.mod +++ b/hack/tools/go-licenses/go.mod @@ -14,12 +14,12 @@ require ( github.com/spf13/cobra v1.7.0 // indirect github.com/spf13/pflag v1.0.5 // indirect go.opencensus.io v0.24.0 // indirect - golang.org/x/mod v0.35.0 // indirect - golang.org/x/net v0.55.0 // indirect - golang.org/x/sync v0.20.0 // indirect - golang.org/x/sys v0.45.0 // indirect - golang.org/x/text v0.37.0 // indirect - golang.org/x/tools v0.44.0 // indirect + golang.org/x/mod v0.42.0 // indirect + golang.org/x/net v0.61.0 // indirect + golang.org/x/sync v0.24.0 // indirect + golang.org/x/sys v0.49.0 // indirect + golang.org/x/text v0.43.0 // indirect + golang.org/x/tools v0.52.0 // indirect k8s.io/klog/v2 v2.90.1 // indirect ) diff --git a/hack/tools/go-licenses/go.sum b/hack/tools/go-licenses/go.sum index d6038883ed..28369d2a31 100644 --- a/hack/tools/go-licenses/go.sum +++ b/hack/tools/go-licenses/go.sum @@ -81,39 +81,39 @@ golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM= -golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU= +golang.org/x/mod v0.42.0 h1:ICeL3t2L30ljSPHHxDnR4fqVcBt/zR2zJAb9juavmQg= +golang.org/x/mod v0.42.0/go.mod h1:u5fzIrRInkL8MaO1cbRkUHT64v303YIT5KEqpNr7jRs= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8= -golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww= +golang.org/x/net v0.61.0 h1:RR+6j/BTBCrggOLugxa+66R8FvoN6MHlzDlZIbN9YFw= +golang.org/x/net v0.61.0/go.mod h1:WgDCOTH8iwtB66T4/GfI94AHo/hhdb7Hah9NDseQfiI= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= -golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sync v0.24.0 h1:iqiDrGoDgyuvAKrhLQthUEzU/R378N78gXbBjxed7oA= +golang.org/x/sync v0.24.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY= -golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/sys v0.49.0 h1:XbzkgYJHdqh/8m2Uu0W/dQv8nktxx4BFHp1M0gROTXA= +golang.org/x/sys v0.49.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= -golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= +golang.org/x/text v0.43.0 h1:1QivrlhwAsnMOcqgOdl9my7s9OyZl8O5/UGUTJVXoWc= +golang.org/x/text v0.43.0/go.mod h1:hbSIYA/amXcRXTFSZqh8tWHEoqRvvbKPJ2uz/KU+a+s= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c= -golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI= +golang.org/x/tools v0.52.0 h1:ryxg4U63oaUYk7uOLTF37n43f4qgtGvqV4Ioit1BaEs= +golang.org/x/tools v0.52.0/go.mod h1:ItcP5UNaVGG+Yc4eIYmaFLNJ4soEopkx5Y2d4lcEurs= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= diff --git a/hack/tools/golangci-lint/go.mod b/hack/tools/golangci-lint/go.mod index c545ab22c8..2593d4f0b0 100644 --- a/hack/tools/golangci-lint/go.mod +++ b/hack/tools/golangci-lint/go.mod @@ -204,11 +204,11 @@ require ( go.uber.org/zap v1.27.0 // indirect go.yaml.in/yaml/v3 v3.0.5 // indirect golang.org/x/exp/typeparams v0.0.0-20260811152304-ee035b5b010f // indirect - golang.org/x/mod v0.40.0 // indirect - golang.org/x/sync v0.22.0 // indirect - golang.org/x/sys v0.47.0 // indirect - golang.org/x/text v0.40.0 // indirect - golang.org/x/tools v0.49.0 // indirect + golang.org/x/mod v0.42.0 // indirect + golang.org/x/sync v0.24.0 // indirect + golang.org/x/sys v0.49.0 // indirect + golang.org/x/text v0.43.0 // indirect + golang.org/x/tools v0.52.0 // indirect google.golang.org/protobuf v1.36.11 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect diff --git a/hack/tools/golangci-lint/go.sum b/hack/tools/golangci-lint/go.sum index 67cd6a0cff..fd75e99e95 100644 --- a/hack/tools/golangci-lint/go.sum +++ b/hack/tools/golangci-lint/go.sum @@ -689,8 +689,8 @@ golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.40.0 h1:hUv+3cXcdRHz08UmSiOob7sadHig73uo5bkXxQ/tvUs= -golang.org/x/mod v0.40.0/go.mod h1:0/weTWkPWGBikyTWAX3dkjVztMmBA5hM0DH6BElSupE= +golang.org/x/mod v0.42.0 h1:ICeL3t2L30ljSPHHxDnR4fqVcBt/zR2zJAb9juavmQg= +golang.org/x/mod v0.42.0/go.mod h1:u5fzIrRInkL8MaO1cbRkUHT64v303YIT5KEqpNr7jRs= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -725,8 +725,8 @@ golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qx golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To= -golang.org/x/net v0.58.0/go.mod h1:YwCddHnFlT7eLQqVprV19OnhLGtc5xOKgE0RyqgfWAU= +golang.org/x/net v0.61.0 h1:RR+6j/BTBCrggOLugxa+66R8FvoN6MHlzDlZIbN9YFw= +golang.org/x/net v0.61.0/go.mod h1:WgDCOTH8iwtB66T4/GfI94AHo/hhdb7Hah9NDseQfiI= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -746,8 +746,8 @@ golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek= -golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sync v0.24.0 h1:iqiDrGoDgyuvAKrhLQthUEzU/R378N78gXbBjxed7oA= +golang.org/x/sync v0.24.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -792,8 +792,8 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= -golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/sys v0.49.0 h1:XbzkgYJHdqh/8m2Uu0W/dQv8nktxx4BFHp1M0gROTXA= +golang.org/x/sys v0.49.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= @@ -805,8 +805,8 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs= -golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY= +golang.org/x/text v0.43.0 h1:1QivrlhwAsnMOcqgOdl9my7s9OyZl8O5/UGUTJVXoWc= +golang.org/x/text v0.43.0/go.mod h1:hbSIYA/amXcRXTFSZqh8tWHEoqRvvbKPJ2uz/KU+a+s= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -857,8 +857,8 @@ golang.org/x/tools v0.1.1-0.20210302220138-2ac05c832e1a/go.mod h1:9bzcO0MWcOuT0t golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.49.0 h1:3NI7VXzL9+1WZD52Dx2ttoPwD5DWrFGpl9mFZDlmisI= -golang.org/x/tools v0.49.0/go.mod h1:SJNXV9DBKT0UbdttsQjbfJlAE/q+y36++zo3uL3N0Oo= +golang.org/x/tools v0.52.0 h1:ryxg4U63oaUYk7uOLTF37n43f4qgtGvqV4Ioit1BaEs= +golang.org/x/tools v0.52.0/go.mod h1:ItcP5UNaVGG+Yc4eIYmaFLNJ4soEopkx5Y2d4lcEurs= golang.org/x/tools/go/expect v0.1.1-deprecated h1:jpBZDwmgPhXsKZC6WhL20P4b/wmnpsEAGHaNy0n/rJM= golang.org/x/tools/go/expect v0.1.1-deprecated/go.mod h1:eihoPOH+FgIqa3FpoTwguz/bVUSGBlGQU67vpBeOrBY= golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated h1:1h2MnaIAIXISqTFKdENegdpAgUXz6NrPEsbIeWaBRvM= diff --git a/hack/tools/gotestsum/go.mod b/hack/tools/gotestsum/go.mod index 47a5c0ceba..cf0ca9cfd1 100644 --- a/hack/tools/gotestsum/go.mod +++ b/hack/tools/gotestsum/go.mod @@ -12,11 +12,11 @@ require ( github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect - golang.org/x/mod v0.27.0 // indirect - golang.org/x/sync v0.17.0 // indirect - golang.org/x/sys v0.36.0 // indirect - golang.org/x/term v0.35.0 // indirect - golang.org/x/text v0.17.0 // indirect - golang.org/x/tools v0.36.0 // indirect + golang.org/x/mod v0.42.0 // indirect + golang.org/x/sync v0.24.0 // indirect + golang.org/x/sys v0.49.0 // indirect + golang.org/x/term v0.47.0 // indirect + golang.org/x/text v0.43.0 // indirect + golang.org/x/tools v0.52.0 // indirect gotest.tools/gotestsum v1.13.0 // indirect ) diff --git a/hack/tools/gotestsum/go.sum b/hack/tools/gotestsum/go.sum index 9d4a067145..482ec2295b 100644 --- a/hack/tools/gotestsum/go.sum +++ b/hack/tools/gotestsum/go.sum @@ -17,20 +17,20 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= -golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ= -golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc= -golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= -golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/mod v0.42.0 h1:ICeL3t2L30ljSPHHxDnR4fqVcBt/zR2zJAb9juavmQg= +golang.org/x/mod v0.42.0/go.mod h1:u5fzIrRInkL8MaO1cbRkUHT64v303YIT5KEqpNr7jRs= +golang.org/x/sync v0.24.0 h1:iqiDrGoDgyuvAKrhLQthUEzU/R378N78gXbBjxed7oA= +golang.org/x/sync v0.24.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= -golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/term v0.35.0 h1:bZBVKBudEyhRcajGcNc3jIfWPqV4y/Kt2XcoigOWtDQ= -golang.org/x/term v0.35.0/go.mod h1:TPGtkTLesOwf2DE8CgVYiZinHAOuy5AYUYT1lENIZnA= -golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= -golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= -golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg= -golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s= +golang.org/x/sys v0.49.0 h1:XbzkgYJHdqh/8m2Uu0W/dQv8nktxx4BFHp1M0gROTXA= +golang.org/x/sys v0.49.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og= +golang.org/x/term v0.47.0 h1:cpyOwQkr2Nzxe2p6KnrTG7Ei2wtzjsuSjhpXXepZ/BM= +golang.org/x/term v0.47.0/go.mod h1:efAfAuG6RKABvg1ArGebRkvX6LXul14SM67o2e4mUCY= +golang.org/x/text v0.43.0 h1:1QivrlhwAsnMOcqgOdl9my7s9OyZl8O5/UGUTJVXoWc= +golang.org/x/text v0.43.0/go.mod h1:hbSIYA/amXcRXTFSZqh8tWHEoqRvvbKPJ2uz/KU+a+s= +golang.org/x/tools v0.52.0 h1:ryxg4U63oaUYk7uOLTF37n43f4qgtGvqV4Ioit1BaEs= +golang.org/x/tools v0.52.0/go.mod h1:ItcP5UNaVGG+Yc4eIYmaFLNJ4soEopkx5Y2d4lcEurs= gotest.tools/gotestsum v1.13.0 h1:+Lh454O9mu9AMG1APV4o0y7oDYKyik/3kBOiCqiEpRo= gotest.tools/gotestsum v1.13.0/go.mod h1:7f0NS5hFb0dWr4NtcsAsF0y1kzjEFfAil0HiBQJE03Q= gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= diff --git a/hack/tools/kind/go.mod b/hack/tools/kind/go.mod index 1343de508d..9100fa9ad0 100644 --- a/hack/tools/kind/go.mod +++ b/hack/tools/kind/go.mod @@ -15,7 +15,7 @@ require ( github.com/spf13/cobra v1.8.0 // indirect github.com/spf13/pflag v1.0.5 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/sys v0.6.0 // indirect + golang.org/x/sys v0.49.0 // indirect sigs.k8s.io/kind v0.33.0 // indirect sigs.k8s.io/yaml v1.4.0 // indirect ) diff --git a/hack/tools/kind/go.sum b/hack/tools/kind/go.sum index 73d1641ca0..af4c849a30 100644 --- a/hack/tools/kind/go.sum +++ b/hack/tools/kind/go.sum @@ -30,8 +30,9 @@ github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= -golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.49.0 h1:XbzkgYJHdqh/8m2Uu0W/dQv8nktxx4BFHp1M0gROTXA= +golang.org/x/sys v0.49.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b h1:QRR6H1YWRnHb4Y/HeNFCTJLFVxaq6wH4YuVdsUOr75U= gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/hack/tools/ko/go.mod b/hack/tools/ko/go.mod index e0cc73266c..bc6d2f33ec 100644 --- a/hack/tools/ko/go.mod +++ b/hack/tools/ko/go.mod @@ -140,15 +140,15 @@ require ( go.yaml.in/yaml/v2 v2.4.4 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect go.yaml.in/yaml/v4 v4.0.0-rc.6 // indirect - golang.org/x/crypto v0.55.0 // indirect - golang.org/x/mod v0.38.0 // indirect - golang.org/x/net v0.58.0 // indirect - golang.org/x/oauth2 v0.36.0 // indirect - golang.org/x/sync v0.22.0 // indirect - golang.org/x/sys v0.47.0 // indirect - golang.org/x/term v0.45.0 // indirect - golang.org/x/text v0.41.0 // indirect - golang.org/x/tools v0.48.0 // indirect + golang.org/x/crypto v0.58.0 // indirect + golang.org/x/mod v0.42.0 // indirect + golang.org/x/net v0.61.0 // indirect + golang.org/x/oauth2 v0.37.0 // indirect + golang.org/x/sync v0.24.0 // indirect + golang.org/x/sys v0.49.0 // indirect + golang.org/x/term v0.47.0 // indirect + golang.org/x/text v0.43.0 // indirect + golang.org/x/tools v0.52.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa // indirect google.golang.org/grpc v1.83.2 // indirect diff --git a/hack/tools/ko/go.sum b/hack/tools/ko/go.sum index 86ab6c74e5..4f80ab511d 100644 --- a/hack/tools/ko/go.sum +++ b/hack/tools/ko/go.sum @@ -483,27 +483,27 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= -golang.org/x/crypto v0.55.0 h1:+KWHjbgOaAQ66dh/YlkZKHlz9ZUlq61AFirAR9ntP8M= -golang.org/x/crypto v0.55.0/go.mod h1:uq0V9dE/fzQuJtbnL+2EhWOE63vo164FY8xqEnV9xis= +golang.org/x/crypto v0.58.0 h1:COkYLr4k7nDI4r6QtMML2AfzeIswWHAJthLS6K/M/54= +golang.org/x/crypto v0.58.0/go.mod h1:Xh+kl5A+M0gMiWDytH2Hhr5Z7WbNCpv/eB/EFgfSk+w= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.38.0 h1:MECBjubtXD7yj4HrhIUcywNaGeNVUdfVnxmPajOk4yk= -golang.org/x/mod v0.38.0/go.mod h1:V6Xz0pq8TQ3dGqVQ1FVHuelZpAL0uNhSkk9ogYP3c40= +golang.org/x/mod v0.42.0 h1:ICeL3t2L30ljSPHHxDnR4fqVcBt/zR2zJAb9juavmQg= +golang.org/x/mod v0.42.0/go.mod h1:u5fzIrRInkL8MaO1cbRkUHT64v303YIT5KEqpNr7jRs= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/net v0.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To= -golang.org/x/net v0.58.0/go.mod h1:YwCddHnFlT7eLQqVprV19OnhLGtc5xOKgE0RyqgfWAU= -golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= -golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= +golang.org/x/net v0.61.0 h1:RR+6j/BTBCrggOLugxa+66R8FvoN6MHlzDlZIbN9YFw= +golang.org/x/net v0.61.0/go.mod h1:WgDCOTH8iwtB66T4/GfI94AHo/hhdb7Hah9NDseQfiI= +golang.org/x/oauth2 v0.37.0 h1:JUlcxA8oAtauLfiH8FX2/FkAWHAdi0QtGCGc+hofE98= +golang.org/x/oauth2 v0.37.0/go.mod h1:IxwZNxUULJmpBFf9K/9NTMSIfZZuvuTy1gGxhigP/58= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek= -golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sync v0.24.0 h1:iqiDrGoDgyuvAKrhLQthUEzU/R378N78gXbBjxed7oA= +golang.org/x/sync v0.24.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -516,15 +516,15 @@ golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= -golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/sys v0.49.0 h1:XbzkgYJHdqh/8m2Uu0W/dQv8nktxx4BFHp1M0gROTXA= +golang.org/x/sys v0.49.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0= -golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0= -golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w= +golang.org/x/term v0.47.0 h1:cpyOwQkr2Nzxe2p6KnrTG7Ei2wtzjsuSjhpXXepZ/BM= +golang.org/x/term v0.47.0/go.mod h1:efAfAuG6RKABvg1ArGebRkvX6LXul14SM67o2e4mUCY= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= @@ -532,16 +532,16 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8= -golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M= +golang.org/x/text v0.43.0 h1:1QivrlhwAsnMOcqgOdl9my7s9OyZl8O5/UGUTJVXoWc= +golang.org/x/text v0.43.0/go.mod h1:hbSIYA/amXcRXTFSZqh8tWHEoqRvvbKPJ2uz/KU+a+s= golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.48.0 h1:3+hClM1aLL5mjMKm5ovokw9epgRXPuu2tILgismM6RE= -golang.org/x/tools v0.48.0/go.mod h1:08xX0orndb/F7jJxGDicx061tyd5pcMto75YMAXr6lk= +golang.org/x/tools v0.52.0 h1:ryxg4U63oaUYk7uOLTF37n43f4qgtGvqV4Ioit1BaEs= +golang.org/x/tools v0.52.0/go.mod h1:ItcP5UNaVGG+Yc4eIYmaFLNJ4soEopkx5Y2d4lcEurs= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= diff --git a/hack/tools/kube-api-linter/go.mod b/hack/tools/kube-api-linter/go.mod index c8ccbc74dd..24e6a02533 100644 --- a/hack/tools/kube-api-linter/go.mod +++ b/hack/tools/kube-api-linter/go.mod @@ -199,11 +199,11 @@ require ( go.uber.org/multierr v1.10.0 // indirect go.uber.org/zap v1.27.0 // indirect golang.org/x/exp/typeparams v0.0.0-20250911091902-df9299821621 // indirect - golang.org/x/mod v0.39.0 // indirect - golang.org/x/sync v0.22.0 // indirect - golang.org/x/sys v0.47.0 // indirect - golang.org/x/text v0.29.0 // indirect - golang.org/x/tools v0.49.0 // indirect + golang.org/x/mod v0.42.0 // indirect + golang.org/x/sync v0.24.0 // indirect + golang.org/x/sys v0.49.0 // indirect + golang.org/x/text v0.43.0 // indirect + golang.org/x/tools v0.52.0 // indirect google.golang.org/protobuf v1.36.6 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect diff --git a/hack/tools/kube-api-linter/go.sum b/hack/tools/kube-api-linter/go.sum index 42fbbe46da..fcca1574bd 100644 --- a/hack/tools/kube-api-linter/go.sum +++ b/hack/tools/kube-api-linter/go.sum @@ -687,8 +687,8 @@ golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91 golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.13.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -golang.org/x/mod v0.39.0 h1:UF5zwQdCRRUpHfyPwr7d4UrGiVeldIsogtzWVnczL74= -golang.org/x/mod v0.39.0/go.mod h1:bvIbwjQ0HUFFf5AKukeeYQG4ZBUG9yxQbR9aEweIwYY= +golang.org/x/mod v0.42.0 h1:ICeL3t2L30ljSPHHxDnR4fqVcBt/zR2zJAb9juavmQg= +golang.org/x/mod v0.42.0/go.mod h1:u5fzIrRInkL8MaO1cbRkUHT64v303YIT5KEqpNr7jRs= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -727,8 +727,8 @@ golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= golang.org/x/net v0.16.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= -golang.org/x/net v0.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To= -golang.org/x/net v0.58.0/go.mod h1:YwCddHnFlT7eLQqVprV19OnhLGtc5xOKgE0RyqgfWAU= +golang.org/x/net v0.61.0 h1:RR+6j/BTBCrggOLugxa+66R8FvoN6MHlzDlZIbN9YFw= +golang.org/x/net v0.61.0/go.mod h1:WgDCOTH8iwtB66T4/GfI94AHo/hhdb7Hah9NDseQfiI= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -750,8 +750,8 @@ golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= -golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek= -golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sync v0.24.0 h1:iqiDrGoDgyuvAKrhLQthUEzU/R378N78gXbBjxed7oA= +golang.org/x/sync v0.24.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -803,8 +803,8 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= -golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/sys v0.49.0 h1:XbzkgYJHdqh/8m2Uu0W/dQv8nktxx4BFHp1M0gROTXA= +golang.org/x/sys v0.49.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= @@ -821,8 +821,8 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= -golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk= -golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4= +golang.org/x/text v0.43.0 h1:1QivrlhwAsnMOcqgOdl9my7s9OyZl8O5/UGUTJVXoWc= +golang.org/x/text v0.43.0/go.mod h1:hbSIYA/amXcRXTFSZqh8tWHEoqRvvbKPJ2uz/KU+a+s= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -876,8 +876,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= golang.org/x/tools v0.14.0/go.mod h1:uYBEerGOWcJyEORxN+Ek8+TT266gXkNlHdJBwexUsBg= -golang.org/x/tools v0.49.0 h1:3NI7VXzL9+1WZD52Dx2ttoPwD5DWrFGpl9mFZDlmisI= -golang.org/x/tools v0.49.0/go.mod h1:SJNXV9DBKT0UbdttsQjbfJlAE/q+y36++zo3uL3N0Oo= +golang.org/x/tools v0.52.0 h1:ryxg4U63oaUYk7uOLTF37n43f4qgtGvqV4Ioit1BaEs= +golang.org/x/tools v0.52.0/go.mod h1:ItcP5UNaVGG+Yc4eIYmaFLNJ4soEopkx5Y2d4lcEurs= golang.org/x/tools/go/expect v0.1.1-deprecated h1:jpBZDwmgPhXsKZC6WhL20P4b/wmnpsEAGHaNy0n/rJM= golang.org/x/tools/go/expect v0.1.1-deprecated/go.mod h1:eihoPOH+FgIqa3FpoTwguz/bVUSGBlGQU67vpBeOrBY= golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated h1:1h2MnaIAIXISqTFKdENegdpAgUXz6NrPEsbIeWaBRvM= diff --git a/hack/tools/setup-envtest/go.mod b/hack/tools/setup-envtest/go.mod index 7d3df4e35f..e80f2207cd 100644 --- a/hack/tools/setup-envtest/go.mod +++ b/hack/tools/setup-envtest/go.mod @@ -10,7 +10,7 @@ require ( go.uber.org/multierr v1.10.0 // indirect go.uber.org/zap v1.27.0 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect - golang.org/x/text v0.33.0 // indirect + golang.org/x/text v0.43.0 // indirect sigs.k8s.io/controller-runtime/tools/setup-envtest v0.0.0-20260405152528-6210f847b2c1 // indirect sigs.k8s.io/yaml v1.6.0 // indirect ) diff --git a/hack/tools/setup-envtest/go.sum b/hack/tools/setup-envtest/go.sum index 39bc915854..f5e331412a 100644 --- a/hack/tools/setup-envtest/go.sum +++ b/hack/tools/setup-envtest/go.sum @@ -34,18 +34,18 @@ go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= -golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c= -golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU= +golang.org/x/mod v0.41.0 h1:qJmnOUb4YB+FsEuM3HcWucdZASCPGhsX6uljO6pog0c= +golang.org/x/mod v0.41.0/go.mod h1:Ek9pY8RKWXwsWvd3rQiHYtMqkjSUV+s1Rj7j4H5Ur6o= golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= -golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= -golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sync v0.24.0 h1:iqiDrGoDgyuvAKrhLQthUEzU/R378N78gXbBjxed7oA= +golang.org/x/sync v0.24.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0= golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= -golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= -golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA= -golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc= +golang.org/x/text v0.43.0 h1:1QivrlhwAsnMOcqgOdl9my7s9OyZl8O5/UGUTJVXoWc= +golang.org/x/text v0.43.0/go.mod h1:hbSIYA/amXcRXTFSZqh8tWHEoqRvvbKPJ2uz/KU+a+s= +golang.org/x/tools v0.51.0 h1:k4Xc/1Om9jwkBJBo4NVLMSARBoWtK10mx+W5BnXCeAI= +golang.org/x/tools v0.51.0/go.mod h1:9eEncMayCV6zRMGhR5eZEC2iBx98qWcF1HZ9Z7wJOoA= google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af h1:+5/Sw3GsDNlEmu7TfklWKPdQ0Ykja5VEmq2i817+jbI= google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= diff --git a/manifests/ate-install/atenet-router.yaml b/manifests/ate-install/atenet-router.yaml index cb077f9685..f45791688d 100644 --- a/manifests/ate-install/atenet-router.yaml +++ b/manifests/ate-install/atenet-router.yaml @@ -321,7 +321,7 @@ spec: - name: "drain-signal" mountPath: "/var/run/atenet" - name: envoy - image: envoyproxy/envoy:v1.39-latest@sha256:57e14a549d7bd43c8d3f6d03e8cfa653e037d4b38e133acd9b54f38c524401b4 + image: envoyproxy/envoy:v1.39.4@sha256:74bd4eb5629708d3fadca1a5e6b85d208f338d7bb54e06b69565905c5294cc01 command: - "/usr/local/bin/envoy" - "-c" From 4e083653090bf8a2b9707fb7c01f273c7e2d1e40 Mon Sep 17 00:00:00 2001 From: Luiz Oliveira Date: Tue, 13 Oct 2026 20:10:38 +0000 Subject: [PATCH 18/42] Rename ContainerWakeupProbe to WakeupProbe (#2575) Fixes #2409 #### Breaking change Go clients of `pkg/proto/ateapipb`: the `ContainerWakeupProbe` message is renamed to `WakeupProbe`. Replace `ateapipb.ContainerWakeupProbe` with `ateapipb.WakeupProbe`. The field name (`wakeupProbe`) and the wire format are unchanged, so manifests, JSON requests and stored templates need no change. --- .../apivalidation/actor_template_test.go | 22 +-- .../apivalidation/zz_generated.validation.go | 156 +++++++++--------- .../controlapi/template_reconciler_test.go | 4 +- .../internal/controlapi/workload_spec.go | 2 +- .../internal/controlapi/workload_spec_test.go | 2 +- cmd/ateapi/internal/defaults/defaults_test.go | 20 +-- .../internal/cmd/actortemplate_test.go | 2 +- docs/api-guide.md | 2 +- pkg/proto/ateapipb/ateapi.pb.go | 42 ++--- pkg/proto/ateapipb/ateapi.proto | 6 +- 10 files changed, 129 insertions(+), 129 deletions(-) diff --git a/cmd/ateapi/internal/apivalidation/actor_template_test.go b/cmd/ateapi/internal/apivalidation/actor_template_test.go index fe044102bf..bd1261c479 100644 --- a/cmd/ateapi/internal/apivalidation/actor_template_test.go +++ b/cmd/ateapi/internal/apivalidation/actor_template_test.go @@ -600,7 +600,7 @@ func TestValidateActorTemplate(t *testing.T) { }, { name: "valid wakeup probe", mutate: func(tmpl *ateapipb.ActorTemplate) { - tmpl.Containers[0].WakeupProbe = &ateapipb.ContainerWakeupProbe{ + tmpl.Containers[0].WakeupProbe = &ateapipb.WakeupProbe{ HttpGet: &ateapipb.HttpGetAction{Path: "/healthz", Port: 8080}, TimeoutSeconds: 60, } @@ -608,25 +608,25 @@ func TestValidateActorTemplate(t *testing.T) { }, { name: "wakeup probe missing http_get", mutate: func(tmpl *ateapipb.ActorTemplate) { - tmpl.Containers[0].WakeupProbe = &ateapipb.ContainerWakeupProbe{TimeoutSeconds: 60} + tmpl.Containers[0].WakeupProbe = &ateapipb.WakeupProbe{TimeoutSeconds: 60} }, want: field.ErrorList{field.Required(field.NewPath("containers").Index(0).Child("wakeup_probe", "http_get"), "")}, }, { name: "missing wakeup probe timeout_seconds", mutate: func(tmpl *ateapipb.ActorTemplate) { - tmpl.Containers[0].WakeupProbe = &ateapipb.ContainerWakeupProbe{HttpGet: &ateapipb.HttpGetAction{Path: "/healthz", Port: 8080}} + tmpl.Containers[0].WakeupProbe = &ateapipb.WakeupProbe{HttpGet: &ateapipb.HttpGetAction{Path: "/healthz", Port: 8080}} }, want: field.ErrorList{field.Required(field.NewPath("containers").Index(0).Child("wakeup_probe", "timeout_seconds"), "")}, }, { name: "missing wakeup probe http_get.path", mutate: func(tmpl *ateapipb.ActorTemplate) { - tmpl.Containers[0].WakeupProbe = &ateapipb.ContainerWakeupProbe{HttpGet: &ateapipb.HttpGetAction{Port: 8080}, TimeoutSeconds: 60} + tmpl.Containers[0].WakeupProbe = &ateapipb.WakeupProbe{HttpGet: &ateapipb.HttpGetAction{Port: 8080}, TimeoutSeconds: 60} }, want: field.ErrorList{field.Required(field.NewPath("containers").Index(0).Child("wakeup_probe", "http_get", "path"), "")}, }, { name: "wakeup probe timeout_seconds out of range", mutate: func(tmpl *ateapipb.ActorTemplate) { - tmpl.Containers[0].WakeupProbe = &ateapipb.ContainerWakeupProbe{ + tmpl.Containers[0].WakeupProbe = &ateapipb.WakeupProbe{ HttpGet: &ateapipb.HttpGetAction{Path: "/healthz", Port: 8080}, TimeoutSeconds: 3601, } @@ -635,7 +635,7 @@ func TestValidateActorTemplate(t *testing.T) { }, { name: "negative wakeup probe timeout_seconds", mutate: func(tmpl *ateapipb.ActorTemplate) { - tmpl.Containers[0].WakeupProbe = &ateapipb.ContainerWakeupProbe{ + tmpl.Containers[0].WakeupProbe = &ateapipb.WakeupProbe{ HttpGet: &ateapipb.HttpGetAction{Path: "/healthz", Port: 8080}, TimeoutSeconds: -1, } @@ -644,7 +644,7 @@ func TestValidateActorTemplate(t *testing.T) { }, { name: "wakeup probe missing port", mutate: func(tmpl *ateapipb.ActorTemplate) { - tmpl.Containers[0].WakeupProbe = &ateapipb.ContainerWakeupProbe{ + tmpl.Containers[0].WakeupProbe = &ateapipb.WakeupProbe{ HttpGet: &ateapipb.HttpGetAction{Path: "/healthz"}, TimeoutSeconds: 60, } @@ -653,7 +653,7 @@ func TestValidateActorTemplate(t *testing.T) { }, { name: "negative wakeup probe port", mutate: func(tmpl *ateapipb.ActorTemplate) { - tmpl.Containers[0].WakeupProbe = &ateapipb.ContainerWakeupProbe{ + tmpl.Containers[0].WakeupProbe = &ateapipb.WakeupProbe{ HttpGet: &ateapipb.HttpGetAction{Path: "/healthz", Port: -1}, TimeoutSeconds: 60, } @@ -662,7 +662,7 @@ func TestValidateActorTemplate(t *testing.T) { }, { name: "wakeup probe port out of range", mutate: func(tmpl *ateapipb.ActorTemplate) { - tmpl.Containers[0].WakeupProbe = &ateapipb.ContainerWakeupProbe{ + tmpl.Containers[0].WakeupProbe = &ateapipb.WakeupProbe{ HttpGet: &ateapipb.HttpGetAction{Path: "/healthz", Port: 65536}, TimeoutSeconds: 60, } @@ -671,7 +671,7 @@ func TestValidateActorTemplate(t *testing.T) { }, { name: "wakeup probe path with query string", mutate: func(tmpl *ateapipb.ActorTemplate) { - tmpl.Containers[0].WakeupProbe = &ateapipb.ContainerWakeupProbe{ + tmpl.Containers[0].WakeupProbe = &ateapipb.WakeupProbe{ HttpGet: &ateapipb.HttpGetAction{Path: "/readyz?verbose=1", Port: 8080}, TimeoutSeconds: 60, } @@ -680,7 +680,7 @@ func TestValidateActorTemplate(t *testing.T) { }, { name: "wakeup probe path not starting with slash", mutate: func(tmpl *ateapipb.ActorTemplate) { - tmpl.Containers[0].WakeupProbe = &ateapipb.ContainerWakeupProbe{ + tmpl.Containers[0].WakeupProbe = &ateapipb.WakeupProbe{ HttpGet: &ateapipb.HttpGetAction{Path: "readyz", Port: 8080}, TimeoutSeconds: 60, } diff --git a/cmd/ateapi/internal/apivalidation/zz_generated.validation.go b/cmd/ateapi/internal/apivalidation/zz_generated.validation.go index 904e5fd626..7d480f8d8d 100644 --- a/cmd/ateapi/internal/apivalidation/zz_generated.validation.go +++ b/cmd/ateapi/internal/apivalidation/zz_generated.validation.go @@ -1752,7 +1752,7 @@ func Validate_Container( { // field ateapipb.Container.WakeupProbe fn := func( fldPath *field.Path, - obj, oldObj *ateapipb.ContainerWakeupProbe, + obj, oldObj *ateapipb.WakeupProbe, oldValueCorrelated bool) (errs field.ErrorList) { // don't revalidate unchanged data if oldValueCorrelated && op.Type == operation.Update { @@ -1769,11 +1769,11 @@ func Validate_Container( return // do not proceed } // call the type's validation function - errs = append(errs, Validate_ContainerWakeupProbe(ctx, op, fldPath, obj, oldObj)...) + errs = append(errs, Validate_WakeupProbe(ctx, op, fldPath, obj, oldObj)...) return } oldVal := safe.Field(oldObj, - func(oldObj *ateapipb.Container) *ateapipb.ContainerWakeupProbe { + func(oldObj *ateapipb.Container) *ateapipb.WakeupProbe { return oldObj.WakeupProbe }) errs = append(errs, fn(fldPath.Child("wakeup_probe"), obj.WakeupProbe, oldVal, oldObj != nil)...) @@ -1892,81 +1892,6 @@ func Validate_Container( return errs } -// Validate_ContainerWakeupProbe validates an instance of ContainerWakeupProbe according -// to declarative validation rules in the API schema. -func Validate_ContainerWakeupProbe( - ctx context.Context, op operation.Operation, fldPath *field.Path, - obj, oldObj *ateapipb.ContainerWakeupProbe) (errs field.ErrorList) { - - { // field ateapipb.ContainerWakeupProbe.HttpGet - fn := func( - fldPath *field.Path, - obj, oldObj *ateapipb.HttpGetAction, - oldValueCorrelated bool) (errs field.ErrorList) { - // don't revalidate unchanged data - if oldValueCorrelated && op.Type == operation.Update { - if ateDeepEqual(obj, oldObj) { - return nil - } - } - // call field-attached validations - earlyReturn := false - if e := validate.RequiredPointer(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { - errs = append(errs, e...) - earlyReturn = true - } - if earlyReturn { - return // do not proceed - } - // call the type's validation function - errs = append(errs, Validate_HttpGetAction(ctx, op, fldPath, obj, oldObj)...) - return - } - oldVal := safe.Field(oldObj, - func(oldObj *ateapipb.ContainerWakeupProbe) *ateapipb.HttpGetAction { - return oldObj.HttpGet - }) - errs = append(errs, fn(fldPath.Child("http_get"), obj.HttpGet, oldVal, oldObj != nil)...) - } - - { // field ateapipb.ContainerWakeupProbe.TimeoutSeconds - fn := func( - fldPath *field.Path, - obj, oldObj *int32, - oldValueCorrelated bool) (errs field.ErrorList) { - // don't revalidate unchanged data - if oldValueCorrelated && op.Type == operation.Update { - if obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj) { - return nil - } - } - // call field-attached validations - earlyReturn := false - if e := validate.RequiredValue(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { - errs = append(errs, e...) - earlyReturn = true - } - if earlyReturn { - return // do not proceed - } - if e := validate.Maximum(ctx, op, fldPath, obj, oldObj, 3600); len(e) != 0 { - errs = append(errs, e...) - } - if e := validate.Minimum(ctx, op, fldPath, obj, oldObj, 1); len(e) != 0 { - errs = append(errs, e...) - } - return - } - oldVal := safe.Field(oldObj, - func(oldObj *ateapipb.ContainerWakeupProbe) *int32 { - return &oldObj.TimeoutSeconds - }) - errs = append(errs, fn(fldPath.Child("timeout_seconds"), &obj.TimeoutSeconds, oldVal, oldObj != nil)...) - } - - return errs -} - // Validate_CreateActorEgressPolicyRequest validates an instance of CreateActorEgressPolicyRequest according // to declarative validation rules in the API schema. func Validate_CreateActorEgressPolicyRequest( @@ -9025,6 +8950,81 @@ func Validate_VolumeMount( return errs } +// Validate_WakeupProbe validates an instance of WakeupProbe according +// to declarative validation rules in the API schema. +func Validate_WakeupProbe( + ctx context.Context, op operation.Operation, fldPath *field.Path, + obj, oldObj *ateapipb.WakeupProbe) (errs field.ErrorList) { + + { // field ateapipb.WakeupProbe.HttpGet + fn := func( + fldPath *field.Path, + obj, oldObj *ateapipb.HttpGetAction, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if ateDeepEqual(obj, oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.RequiredPointer(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + // call the type's validation function + errs = append(errs, Validate_HttpGetAction(ctx, op, fldPath, obj, oldObj)...) + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateapipb.WakeupProbe) *ateapipb.HttpGetAction { + return oldObj.HttpGet + }) + errs = append(errs, fn(fldPath.Child("http_get"), obj.HttpGet, oldVal, oldObj != nil)...) + } + + { // field ateapipb.WakeupProbe.TimeoutSeconds + fn := func( + fldPath *field.Path, + obj, oldObj *int32, + oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update { + if obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj) { + return nil + } + } + // call field-attached validations + earlyReturn := false + if e := validate.RequiredValue(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + if e := validate.Maximum(ctx, op, fldPath, obj, oldObj, 3600); len(e) != 0 { + errs = append(errs, e...) + } + if e := validate.Minimum(ctx, op, fldPath, obj, oldObj, 1); len(e) != 0 { + errs = append(errs, e...) + } + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateapipb.WakeupProbe) *int32 { + return &oldObj.TimeoutSeconds + }) + errs = append(errs, fn(fldPath.Child("timeout_seconds"), &obj.TimeoutSeconds, oldVal, oldObj != nil)...) + } + + return errs +} + // Validate_Worker validates an instance of Worker according // to declarative validation rules in the API schema. func Validate_Worker( diff --git a/cmd/ateapi/internal/controlapi/template_reconciler_test.go b/cmd/ateapi/internal/controlapi/template_reconciler_test.go index 972ced7075..d8a70e47f5 100644 --- a/cmd/ateapi/internal/controlapi/template_reconciler_test.go +++ b/cmd/ateapi/internal/controlapi/template_reconciler_test.go @@ -324,7 +324,7 @@ func testTemplate(opts ...func(*ateapipb.ActorTemplate)) *ateapipb.ActorTemplate tmpl := &ateapipb.ActorTemplate{ Metadata: &ateapipb.ResourceMetadata{Atespace: testAtespace, Name: testTemplateName, Uid: testTemplateUID, Version: 1}, Containers: []*ateapipb.Container{ - {Name: "main", Image: "img", WakeupProbe: &ateapipb.ContainerWakeupProbe{}}, + {Name: "main", Image: "img", WakeupProbe: &ateapipb.WakeupProbe{}}, }, SnapshotConfig: &ateapipb.SnapshotConfig{ GoldenSnapshotConfig: &ateapipb.GoldenSnapshotConfig{Mode: ateapipb.GoldenSnapshotMode_GOLDEN_SNAPSHOT_MODE_ENABLED}, @@ -379,7 +379,7 @@ func newTestTemplateReconciler(persistence templateReconcilerStore, control gold } func TestGoldenSnapshotWarmupFor(t *testing.T) { - probe := &ateapipb.ContainerWakeupProbe{} + probe := &ateapipb.WakeupProbe{} tests := []struct { name string containers []*ateapipb.Container diff --git a/cmd/ateapi/internal/controlapi/workload_spec.go b/cmd/ateapi/internal/controlapi/workload_spec.go index bd1df172ce..749cb0b497 100644 --- a/cmd/ateapi/internal/controlapi/workload_spec.go +++ b/cmd/ateapi/internal/controlapi/workload_spec.go @@ -220,7 +220,7 @@ func toAteletActorMetadataField(in ateapipb.ActorMetadataField) ateletpb.ActorMe // toAteletWakeupProbe projects the template wakeup probe field onto the ateletpb wire // type. Returns nil when the source is nil so containers without a probe // stay unchanged on the wire. -func toAteletWakeupProbe(in *ateapipb.ContainerWakeupProbe) *ateletpb.WakeupProbe { +func toAteletWakeupProbe(in *ateapipb.WakeupProbe) *ateletpb.WakeupProbe { if in == nil { return nil } diff --git a/cmd/ateapi/internal/controlapi/workload_spec_test.go b/cmd/ateapi/internal/controlapi/workload_spec_test.go index 68d569b5b6..f84f6f5e36 100644 --- a/cmd/ateapi/internal/controlapi/workload_spec_test.go +++ b/cmd/ateapi/internal/controlapi/workload_spec_test.go @@ -341,7 +341,7 @@ func TestWorkloadSpecFromActorTemplatePropagatesWakeupProbe(t *testing.T) { { Name: "with-probe", Image: "main", - WakeupProbe: &ateapipb.ContainerWakeupProbe{ + WakeupProbe: &ateapipb.WakeupProbe{ HttpGet: &ateapipb.HttpGetAction{Path: "/health", Port: 8080}, TimeoutSeconds: 45, }, diff --git a/cmd/ateapi/internal/defaults/defaults_test.go b/cmd/ateapi/internal/defaults/defaults_test.go index 0aa3646204..5ccb5ba159 100644 --- a/cmd/ateapi/internal/defaults/defaults_test.go +++ b/cmd/ateapi/internal/defaults/defaults_test.go @@ -64,10 +64,10 @@ func TestApply(t *testing.T) { }, { name: "wakeup probe gets timeout and path", in: &ateapipb.ActorTemplate{Containers: []*ateapipb.Container{ - {Name: "main", WakeupProbe: &ateapipb.ContainerWakeupProbe{HttpGet: &ateapipb.HttpGetAction{Port: 8080}}}, + {Name: "main", WakeupProbe: &ateapipb.WakeupProbe{HttpGet: &ateapipb.HttpGetAction{Port: 8080}}}, }}, want: &ateapipb.ActorTemplate{Containers: []*ateapipb.Container{ - {Name: "main", WakeupProbe: &ateapipb.ContainerWakeupProbe{ + {Name: "main", WakeupProbe: &ateapipb.WakeupProbe{ HttpGet: &ateapipb.HttpGetAction{Port: 8080, Path: "/"}, TimeoutSeconds: 30, }}, @@ -75,21 +75,21 @@ func TestApply(t *testing.T) { }, { name: "wakeup probe without http_get gets only the timeout", in: &ateapipb.ActorTemplate{Containers: []*ateapipb.Container{ - {Name: "main", WakeupProbe: &ateapipb.ContainerWakeupProbe{}}, + {Name: "main", WakeupProbe: &ateapipb.WakeupProbe{}}, }}, want: &ateapipb.ActorTemplate{Containers: []*ateapipb.Container{ - {Name: "main", WakeupProbe: &ateapipb.ContainerWakeupProbe{TimeoutSeconds: 30}}, + {Name: "main", WakeupProbe: &ateapipb.WakeupProbe{TimeoutSeconds: 30}}, }}, }, { name: "set wakeup probe fields are kept", in: &ateapipb.ActorTemplate{Containers: []*ateapipb.Container{ - {Name: "main", WakeupProbe: &ateapipb.ContainerWakeupProbe{ + {Name: "main", WakeupProbe: &ateapipb.WakeupProbe{ HttpGet: &ateapipb.HttpGetAction{Port: 8080, Path: "/healthz"}, TimeoutSeconds: 5, }}, }}, want: &ateapipb.ActorTemplate{Containers: []*ateapipb.Container{ - {Name: "main", WakeupProbe: &ateapipb.ContainerWakeupProbe{ + {Name: "main", WakeupProbe: &ateapipb.WakeupProbe{ HttpGet: &ateapipb.HttpGetAction{Port: 8080, Path: "/healthz"}, TimeoutSeconds: 5, }}, @@ -97,14 +97,14 @@ func TestApply(t *testing.T) { }, { name: "every container is defaulted", in: &ateapipb.ActorTemplate{Containers: []*ateapipb.Container{ - {Name: "a", WakeupProbe: &ateapipb.ContainerWakeupProbe{HttpGet: &ateapipb.HttpGetAction{Port: 1}}}, + {Name: "a", WakeupProbe: &ateapipb.WakeupProbe{HttpGet: &ateapipb.HttpGetAction{Port: 1}}}, {Name: "b"}, - {Name: "c", WakeupProbe: &ateapipb.ContainerWakeupProbe{HttpGet: &ateapipb.HttpGetAction{Port: 3}}}, + {Name: "c", WakeupProbe: &ateapipb.WakeupProbe{HttpGet: &ateapipb.HttpGetAction{Port: 3}}}, }}, want: &ateapipb.ActorTemplate{Containers: []*ateapipb.Container{ - {Name: "a", WakeupProbe: &ateapipb.ContainerWakeupProbe{HttpGet: &ateapipb.HttpGetAction{Port: 1, Path: "/"}, TimeoutSeconds: 30}}, + {Name: "a", WakeupProbe: &ateapipb.WakeupProbe{HttpGet: &ateapipb.HttpGetAction{Port: 1, Path: "/"}, TimeoutSeconds: 30}}, {Name: "b"}, - {Name: "c", WakeupProbe: &ateapipb.ContainerWakeupProbe{HttpGet: &ateapipb.HttpGetAction{Port: 3, Path: "/"}, TimeoutSeconds: 30}}, + {Name: "c", WakeupProbe: &ateapipb.WakeupProbe{HttpGet: &ateapipb.HttpGetAction{Port: 3, Path: "/"}, TimeoutSeconds: 30}}, }}, }, { name: "actor has no defaults", diff --git a/cmd/kubectl-ate/internal/cmd/actortemplate_test.go b/cmd/kubectl-ate/internal/cmd/actortemplate_test.go index fe48767df8..2ea0c8b234 100644 --- a/cmd/kubectl-ate/internal/cmd/actortemplate_test.go +++ b/cmd/kubectl-ate/internal/cmd/actortemplate_test.go @@ -80,7 +80,7 @@ func TestActorTemplateFromManifest(t *testing.T) { Name: "counter", Image: "ko://github.com/agent-substrate/substrate/demos/counter", Command: []string{"/ko-app/counter", "--extra-port=9090"}, - WakeupProbe: &ateapipb.ContainerWakeupProbe{ + WakeupProbe: &ateapipb.WakeupProbe{ HttpGet: &ateapipb.HttpGetAction{Path: "/readyz", Port: 80}, }, VolumeMounts: []*ateapipb.VolumeMount{{Name: "data", MountPath: "/home/counter"}}, diff --git a/docs/api-guide.md b/docs/api-guide.md index d87e300aa6..a3f6b37b2c 100644 --- a/docs/api-guide.md +++ b/docs/api-guide.md @@ -253,7 +253,7 @@ Each entry in `containers` describes one process to run in the actor's sandbox. | `command` | `[]string` | Optional. Entrypoint array. If unset, the image's `ENTRYPOINT` is used. If set, it replaces **both** the image's `ENTRYPOINT` and `CMD`. | | `args` | `[]string` | Optional. Arguments to the entrypoint. If unset, the image's `CMD` is used (unless `command` is set, which discards the image's `CMD`). If set, it replaces the image's `CMD`. | | `env` | `[]EnvVar` | Optional. Literal `value` entries. | -| `wakeupProbe` | `ContainerWakeupProbe` | Optional. HTTP wakeup probe — see [Container Wakeup Probe](#container-wakeup-probe-wakeupprobe). | +| `wakeupProbe` | `WakeupProbe` | Optional. HTTP wakeup probe — see [Container Wakeup Probe](#container-wakeup-probe-wakeupprobe). | | `volumeMounts` | `[]VolumeMount` | Optional. Mounts a `volumes` entry (e.g. `durableDir`) into this container. | | `securityContext` | `SecurityContext` | Optional. Security settings for the container process — see [Container Capabilities](#container-capabilities-securitycontextcapabilities). | | `resources` | `ContainerResources` | Optional. Compute limits for this container, enforced inside the actor's sandbox. Only `limits` is supported, and only `cpu` and `memory`. See [Per-container limits](#per-container-limits). | diff --git a/pkg/proto/ateapipb/ateapi.pb.go b/pkg/proto/ateapipb/ateapi.pb.go index be65d3853e..c7f3035e7e 100644 --- a/pkg/proto/ateapipb/ateapi.pb.go +++ b/pkg/proto/ateapipb/ateapi.pb.go @@ -3185,7 +3185,7 @@ type Container struct { // as soon as the container has started, which may be before it listens. // // +k8s:optional - WakeupProbe *ContainerWakeupProbe `protobuf:"bytes,6,opt,name=wakeup_probe,json=wakeupProbe,proto3" json:"wakeup_probe,omitempty"` + WakeupProbe *WakeupProbe `protobuf:"bytes,6,opt,name=wakeup_probe,json=wakeupProbe,proto3" json:"wakeup_probe,omitempty"` // Keyed by mount_path: each path hosts exactly one mount, while a volume // may be mounted at multiple paths. // @@ -3274,7 +3274,7 @@ func (x *Container) GetEnv() []*EnvVar { return nil } -func (x *Container) GetWakeupProbe() *ContainerWakeupProbe { +func (x *Container) GetWakeupProbe() *WakeupProbe { if x != nil { return x.WakeupProbe } @@ -3484,11 +3484,11 @@ func (x *EnvVar) GetValue() string { return "" } -// ContainerWakeupProbe tells the platform when a container is serving. It is +// WakeupProbe tells the platform when a container is serving. It is // polled on every wakeup, whether the actor cold-boots or is restored from a // snapshot, and not again while the actor stays running. A probe that does // not answer 200 within timeout_seconds crashes the actor. -type ContainerWakeupProbe struct { +type WakeupProbe struct { state protoimpl.MessageState `protogen:"open.v1"` // http_get is the request to poll. // @@ -3505,20 +3505,20 @@ type ContainerWakeupProbe struct { sizeCache protoimpl.SizeCache } -func (x *ContainerWakeupProbe) Reset() { - *x = ContainerWakeupProbe{} +func (x *WakeupProbe) Reset() { + *x = WakeupProbe{} mi := &file_ateapi_proto_msgTypes[36] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *ContainerWakeupProbe) String() string { +func (x *WakeupProbe) String() string { return protoimpl.X.MessageStringOf(x) } -func (*ContainerWakeupProbe) ProtoMessage() {} +func (*WakeupProbe) ProtoMessage() {} -func (x *ContainerWakeupProbe) ProtoReflect() protoreflect.Message { +func (x *WakeupProbe) ProtoReflect() protoreflect.Message { mi := &file_ateapi_proto_msgTypes[36] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) @@ -3530,19 +3530,19 @@ func (x *ContainerWakeupProbe) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use ContainerWakeupProbe.ProtoReflect.Descriptor instead. -func (*ContainerWakeupProbe) Descriptor() ([]byte, []int) { +// Deprecated: Use WakeupProbe.ProtoReflect.Descriptor instead. +func (*WakeupProbe) Descriptor() ([]byte, []int) { return file_ateapi_proto_rawDescGZIP(), []int{36} } -func (x *ContainerWakeupProbe) GetHttpGet() *HttpGetAction { +func (x *WakeupProbe) GetHttpGet() *HttpGetAction { if x != nil { return x.HttpGet } return nil } -func (x *ContainerWakeupProbe) GetTimeoutSeconds() int32 { +func (x *WakeupProbe) GetTimeoutSeconds() int32 { if x != nil { return x.TimeoutSeconds } @@ -8766,14 +8766,14 @@ const file_ateapi_proto_rawDesc = "" + "\x12preferred_fidelity\x18\x02 \x01(\x0e2\x18.ateapi.SnapshotFidelityR\x11preferredFidelity\x12R\n" + "\x16golden_snapshot_config\x18\x05 \x01(\v2\x1c.ateapi.GoldenSnapshotConfigR\x14goldenSnapshotConfig\"F\n" + "\x14GoldenSnapshotConfig\x12.\n" + - "\x04mode\x18\x01 \x01(\x0e2\x1a.ateapi.GoldenSnapshotModeR\x04mode\"\xf5\x02\n" + + "\x04mode\x18\x01 \x01(\x0e2\x1a.ateapi.GoldenSnapshotModeR\x04mode\"\xec\x02\n" + "\tContainer\x12\x12\n" + "\x04name\x18\x01 \x01(\tR\x04name\x12\x14\n" + "\x05image\x18\x02 \x01(\tR\x05image\x12\x18\n" + "\acommand\x18\x03 \x03(\tR\acommand\x12\x12\n" + "\x04args\x18\x04 \x03(\tR\x04args\x12 \n" + - "\x03env\x18\x05 \x03(\v2\x0e.ateapi.EnvVarR\x03env\x12?\n" + - "\fwakeup_probe\x18\x06 \x01(\v2\x1c.ateapi.ContainerWakeupProbeR\vwakeupProbe\x128\n" + + "\x03env\x18\x05 \x03(\v2\x0e.ateapi.EnvVarR\x03env\x126\n" + + "\fwakeup_probe\x18\x06 \x01(\v2\x13.ateapi.WakeupProbeR\vwakeupProbe\x128\n" + "\rvolume_mounts\x18\a \x03(\v2\x13.ateapi.VolumeMountR\fvolumeMounts\x12B\n" + "\x10security_context\x18\b \x01(\v2\x17.ateapi.SecurityContextR\x0fsecurityContext\x12/\n" + "\tresources\x18\t \x01(\v2\x11.ateapi.ResourcesR\tresources\"K\n" + @@ -8784,8 +8784,8 @@ const file_ateapi_proto_rawDesc = "" + "\x04drop\x18\x02 \x03(\tR\x04drop\"7\n" + "\x06EnvVar\x12\x12\n" + "\x04name\x18\x01 \x01(\tR\x04name\x12\x19\n" + - "\x05value\x18\x02 \x01(\tB\x03\x80\x01\x01R\x05value\"q\n" + - "\x14ContainerWakeupProbe\x120\n" + + "\x05value\x18\x02 \x01(\tB\x03\x80\x01\x01R\x05value\"h\n" + + "\vWakeupProbe\x120\n" + "\bhttp_get\x18\x01 \x01(\v2\x15.ateapi.HttpGetActionR\ahttpGet\x12'\n" + "\x0ftimeout_seconds\x18\x02 \x01(\x05R\x0etimeoutSeconds\"7\n" + "\rHttpGetAction\x12\x12\n" + @@ -9206,7 +9206,7 @@ var file_ateapi_proto_goTypes = []any{ (*SecurityContext)(nil), // 43: ateapi.SecurityContext (*Capabilities)(nil), // 44: ateapi.Capabilities (*EnvVar)(nil), // 45: ateapi.EnvVar - (*ContainerWakeupProbe)(nil), // 46: ateapi.ContainerWakeupProbe + (*WakeupProbe)(nil), // 46: ateapi.WakeupProbe (*HttpGetAction)(nil), // 47: ateapi.HttpGetAction (*Volume)(nil), // 48: ateapi.Volume (*ImageVolumeSource)(nil), // 49: ateapi.ImageVolumeSource @@ -9353,12 +9353,12 @@ var file_ateapi_proto_depIdxs = []int32{ 41, // 55: ateapi.SnapshotConfig.golden_snapshot_config:type_name -> ateapi.GoldenSnapshotConfig 6, // 56: ateapi.GoldenSnapshotConfig.mode:type_name -> ateapi.GoldenSnapshotMode 45, // 57: ateapi.Container.env:type_name -> ateapi.EnvVar - 46, // 58: ateapi.Container.wakeup_probe:type_name -> ateapi.ContainerWakeupProbe + 46, // 58: ateapi.Container.wakeup_probe:type_name -> ateapi.WakeupProbe 57, // 59: ateapi.Container.volume_mounts:type_name -> ateapi.VolumeMount 43, // 60: ateapi.Container.security_context:type_name -> ateapi.SecurityContext 35, // 61: ateapi.Container.resources:type_name -> ateapi.Resources 44, // 62: ateapi.SecurityContext.capabilities:type_name -> ateapi.Capabilities - 47, // 63: ateapi.ContainerWakeupProbe.http_get:type_name -> ateapi.HttpGetAction + 47, // 63: ateapi.WakeupProbe.http_get:type_name -> ateapi.HttpGetAction 50, // 64: ateapi.Volume.durable_dir:type_name -> ateapi.DurableDirVolumeSource 51, // 65: ateapi.Volume.external_volume_template:type_name -> ateapi.ExternalVolumeTemplate 52, // 66: ateapi.Volume.system_info:type_name -> ateapi.SystemInfoVolumeSource diff --git a/pkg/proto/ateapipb/ateapi.proto b/pkg/proto/ateapipb/ateapi.proto index 77e1b967ab..c0d3588141 100644 --- a/pkg/proto/ateapipb/ateapi.proto +++ b/pkg/proto/ateapipb/ateapi.proto @@ -1201,7 +1201,7 @@ message Container { // as soon as the container has started, which may be before it listens. // // +k8s:optional - ContainerWakeupProbe wakeup_probe = 6; + WakeupProbe wakeup_probe = 6; // Keyed by mount_path: each path hosts exactly one mount, while a volume // may be mounted at multiple paths. @@ -1276,11 +1276,11 @@ message EnvVar { string value = 2 [ debug_redact = true ]; } -// ContainerWakeupProbe tells the platform when a container is serving. It is +// WakeupProbe tells the platform when a container is serving. It is // polled on every wakeup, whether the actor cold-boots or is restored from a // snapshot, and not again while the actor stays running. A probe that does // not answer 200 within timeout_seconds crashes the actor. -message ContainerWakeupProbe { +message WakeupProbe { // http_get is the request to poll. // // +k8s:required From d849b2a22919ef436e8d68c8269f6619d77176e9 Mon Sep 17 00:00:00 2001 From: Haven Xia Date: Tue, 13 Oct 2026 21:10:03 +0000 Subject: [PATCH 19/42] ate-setup: refuse to point an external-database install at the bundled one (#2588) `ate-setup` doesn't remember an external PostgreSQL's connection string between runs. The install record leaves it out intentionally because it can carry a password. If a run forgets `ATE_API_POSTGRES_READ_WRITE_CONNECTION_STRING`, `ate-setup` assumes the bundled database and writes its address into the API server's Secret: - `deploy apiserver` fails waiting for a bundled PostgreSQL that doesn't exist, and the next API server restart can't reach the database. - `deploy ate-system` starts an empty bundled PostgreSQL and moves the API server onto it, so every actor looks gone. ------ Now, if the run names no database and the Secret already holds an address that isn't the bundled one, `ate-setup` stops before it changes the database settings and names the missing variable. A fresh installs, bundled installs and Cloud SQL work as before, and so does leaving Cloud SQL with an empty `ATE_API_POSTGRES_CLOUDSQL_INSTANCE`. A second commit does the same for `ATE_API_POSTGRES_OWNER_CONNECTION_STRING`. If the install uses a separate owner login and a run gives only the read-write string, ate-setup stops instead of replacing the owner login. Found while reviewing the upgrade runbook (#1844). Part of #1686 - [ ] Tests pass - [ ] Appropriate changes to documentation are included in the PR --- cmd/ate-setup/internal/steps/apienv.go | 42 +++++++- cmd/ate-setup/internal/steps/apienv_test.go | 108 ++++++++++++++++++++ cmd/ate-setup/internal/steps/postgres.go | 13 ++- 3 files changed, 160 insertions(+), 3 deletions(-) diff --git a/cmd/ate-setup/internal/steps/apienv.go b/cmd/ate-setup/internal/steps/apienv.go index 43e2e26dea..da988eb528 100644 --- a/cmd/ate-setup/internal/steps/apienv.go +++ b/cmd/ate-setup/internal/steps/apienv.go @@ -81,12 +81,18 @@ func (e *Env) CreateAPIServerEnvVars(ctx context.Context) error { return err } } else { + if err := e.refuseBundledOverExternal(ctx); err != nil { + return err + } if readWriteDSN, ownerDSN, err = e.postgresReadWriteConnectionStrings(); err != nil { return err } } } if ownerDSN == "" { + if err := e.refuseDroppingOwnerLogin(ctx); err != nil { + return err + } ownerDSN = readWriteDSN } schema := e.Cfg.PostgresSchemaName() @@ -156,8 +162,40 @@ func buildAPIServerEnvVars(readWriteDSN, ownerDSN, schema string) map[string]str } } -// recordedConnectionStrings reads the credentials paired with an adopted Cloud -// SQL instance. +// refuseBundledOverExternal stops a run that names no database from moving an +// install off its external PostgreSQL, since records hold no connection strings. +func (e *Env) refuseBundledOverExternal(ctx context.Context) error { + // An explicitly empty Cloud SQL instance asks for the bundled database. + if e.Cfg.CloudSQL.InstanceSet { + return nil + } + recorded, _, err := e.recordedConnectionStrings(ctx) + if err != nil { + return err + } + if recorded == "" || usesBundledPostgres(recorded) { + return nil + } + return fmt.Errorf("ate-api-server uses an external PostgreSQL, but ATE_API_POSTGRES_READ_WRITE_CONNECTION_STRING is not set") +} + +// refuseDroppingOwnerLogin stops a run that names no owner connection string +// from replacing a separate owner login with the read-write one. The bundled +// database's owner login is ate-setup's own, so it does not count. +func (e *Env) refuseDroppingOwnerLogin(ctx context.Context) error { + recordedReadWrite, recordedOwner, err := e.recordedConnectionStrings(ctx) + if err != nil { + return err + } + if recordedOwner == recordedReadWrite || usesBundledPostgres(recordedOwner) { + return nil + } + return fmt.Errorf("ate-api-server uses a separate PostgreSQL owner login, but ATE_API_POSTGRES_OWNER_CONNECTION_STRING is not set; " + + "set it to the read-write connection string to use one login") +} + +// recordedConnectionStrings reads the connection strings in the cluster's +// Secret. func (e *Env) recordedConnectionStrings(ctx context.Context) (string, string, error) { secret, err := e.Kube.GetSecret(ctx, e.Namespace(), SecretAPIEnvVars) if err != nil || secret == nil { diff --git a/cmd/ate-setup/internal/steps/apienv_test.go b/cmd/ate-setup/internal/steps/apienv_test.go index 16b28d78c3..903aeeef17 100644 --- a/cmd/ate-setup/internal/steps/apienv_test.go +++ b/cmd/ate-setup/internal/steps/apienv_test.go @@ -151,6 +151,114 @@ func TestCreateAPIServerEnvVarsRejectsCustomBundledIdentity(t *testing.T) { } } +// A run without a connection string must not point an install that uses an +// external PostgreSQL at the bundled one: records hold no connection strings, +// so a forgotten export is the common way to get here. +func TestCreateAPIServerEnvVarsKeepsExternalPostgres(t *testing.T) { + for _, tc := range []struct { + name string + cloudSQL config.CloudSQLConfig + recorded string + wantErr bool + }{ + {name: "fresh install", recorded: ""}, + {name: "bundled install", recorded: bundledPostgresDSN(postgressetup.ReadWriteUser)}, + {name: "external install", recorded: "postgres://runtime@database/atepg", wantErr: true}, + { + // An explicitly empty instance is how an operator leaves Cloud SQL. + name: "leaving Cloud SQL", + cloudSQL: config.CloudSQLConfig{InstanceSet: true}, + recorded: "user=svc@p.iam host=127.0.0.1 port=5432 dbname=atepg sslmode=disable", + }, + } { + t.Run(tc.name, func(t *testing.T) { + cfg := config.Config{ + PostgresReadWriteRole: config.DefaultPostgresReadWriteRole, PostgresOwnerRole: config.DefaultPostgresOwnerRole, + CloudSQL: tc.cloudSQL, + } + e := &Env{Cfg: &cfg, Kube: fakeKube(t, + &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: NamespaceAteSystem}}, + &corev1.ConfigMap{ObjectMeta: metav1.ObjectMeta{Name: ConfigMapAPIEnvVars, Namespace: NamespaceAteSystem}}, + &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{Name: SecretAPIEnvVars, Namespace: NamespaceAteSystem}, + Data: map[string][]byte{"ATE_API_POSTGRES_READ_WRITE_CONNECTION_STRING": []byte(tc.recorded)}, + }, + )} + err := e.CreateAPIServerEnvVars(t.Context()) + if !tc.wantErr { + if err != nil { + t.Fatal(err) + } + return + } + if err == nil || !strings.Contains(err.Error(), "ATE_API_POSTGRES_READ_WRITE_CONNECTION_STRING is not set") { + t.Fatalf("CreateAPIServerEnvVars() error = %v, want the missing connection string named", err) + } + secret, err := e.Kube.GetSecret(t.Context(), NamespaceAteSystem, SecretAPIEnvVars) + if err != nil { + t.Fatal(err) + } + if got := string(secret.Data["ATE_API_POSTGRES_READ_WRITE_CONNECTION_STRING"]); got != tc.recorded || len(secret.StringData) != 0 { + t.Fatalf("Secret rewritten: Data %q, StringData %v", got, secret.StringData) + } + }) + } +} + +// The owner login is no more recorded than the read-write one, so a run that +// gives only the read-write connection string must not replace a separate owner. +func TestCreateAPIServerEnvVarsKeepsOwnerLogin(t *testing.T) { + const readWrite = "postgres://runtime@database/atepg" + for _, tc := range []struct { + name string + owner string + recordedRW string + recordedOwner string + wantErr bool + }{ + {name: "separate owner not given", recordedRW: readWrite, recordedOwner: "postgres://owner@database/atepg", wantErr: true}, + {name: "separate owner given", owner: "postgres://owner@database/atepg", recordedRW: readWrite, recordedOwner: "postgres://owner@database/atepg"}, + {name: "one login", recordedRW: readWrite, recordedOwner: readWrite}, + { + name: "moving off the bundled database", + recordedRW: bundledPostgresDSN(postgressetup.ReadWriteUser), + recordedOwner: bundledPostgresDSN(postgressetup.OwnerUser), + }, + } { + t.Run(tc.name, func(t *testing.T) { + cfg := config.Config{PostgresReadWriteConnectionString: readWrite, PostgresOwnerConnectionString: tc.owner} + e := &Env{Cfg: &cfg, Kube: fakeKube(t, + &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: NamespaceAteSystem}}, + &corev1.ConfigMap{ObjectMeta: metav1.ObjectMeta{Name: ConfigMapAPIEnvVars, Namespace: NamespaceAteSystem}}, + &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{Name: SecretAPIEnvVars, Namespace: NamespaceAteSystem}, + Data: map[string][]byte{ + "ATE_API_POSTGRES_READ_WRITE_CONNECTION_STRING": []byte(tc.recordedRW), + "ATE_API_POSTGRES_OWNER_CONNECTION_STRING": []byte(tc.recordedOwner), + }, + }, + )} + err := e.CreateAPIServerEnvVars(t.Context()) + if !tc.wantErr { + if err != nil { + t.Fatal(err) + } + return + } + if err == nil || !strings.Contains(err.Error(), "ATE_API_POSTGRES_OWNER_CONNECTION_STRING is not set") { + t.Fatalf("CreateAPIServerEnvVars() error = %v, want the missing owner connection string named", err) + } + secret, err := e.Kube.GetSecret(t.Context(), NamespaceAteSystem, SecretAPIEnvVars) + if err != nil { + t.Fatal(err) + } + if got := string(secret.Data["ATE_API_POSTGRES_OWNER_CONNECTION_STRING"]); got != tc.recordedOwner || len(secret.StringData) != 0 { + t.Fatalf("Secret rewritten: owner %q, StringData %v", got, secret.StringData) + } + }) + } +} + func TestCreateAPIServerEnvVarsPoolSize(t *testing.T) { cfg := config.Config{ PostgresReadWriteConnectionString: "postgres://runtime@postgres/atepg", diff --git a/cmd/ate-setup/internal/steps/postgres.go b/cmd/ate-setup/internal/steps/postgres.go index dea4883edf..5067d6a4c8 100644 --- a/cmd/ate-setup/internal/steps/postgres.go +++ b/cmd/ate-setup/internal/steps/postgres.go @@ -18,6 +18,7 @@ import ( "bytes" "context" "fmt" + "net/url" "os" "strings" @@ -31,10 +32,20 @@ import ( "github.com/agent-substrate/substrate/pkg/postgressetup" ) +// bundledPostgresHost is where ate-api-server reaches the bundled PostgreSQL. +const bundledPostgresHost = "postgres.ate-system.svc:5432" + // Each bundled connection presents a certificate for its own login user. func bundledPostgresDSN(user string) string { bundle := "/run/postgres.podcert.ate.dev/" + user + ".pem" - return fmt.Sprintf("postgresql://%s@postgres.ate-system.svc:5432/atepg?sslmode=verify-full&sslrootcert=/run/servicedns.podcert.ate.dev/trust-bundle.pem&sslcert=%s&sslkey=%s", user, bundle, bundle) + return fmt.Sprintf("postgresql://%s@%s/atepg?sslmode=verify-full&sslrootcert=/run/servicedns.podcert.ate.dev/trust-bundle.pem&sslcert=%s&sslkey=%s", user, bundledPostgresHost, bundle, bundle) +} + +// usesBundledPostgres reports whether a connection string names the bundled +// PostgreSQL. +func usesBundledPostgres(dsn string) bool { + u, err := url.Parse(dsn) + return err == nil && u.Host == bundledPostgresHost } func (e *Env) postgresReadWriteConnectionStrings() (string, string, error) { From 02d885e8d76c270f4778d78de52a1b79c9df674f Mon Sep 17 00:00:00 2001 From: Haven Xia Date: Tue, 13 Oct 2026 21:17:48 +0000 Subject: [PATCH 20/42] Remove the substrate version label from the install (#1812) `ate-setup` no longer labels nodes with `ate.dev/substrate-version`. The atelet DaemonSet is now named `atelet` and runs on every node, instead of `atelet-` pinned to nodes carrying its version. The label let two Substrate versions run side by side on disjoint sets of nodes, which the clone-WorkerPool upgrade runbook relied on. The GA upgrade doesn't split nodes by version: atelet rolls in place and worker pools roll as ordinary Deployment updates. With the label, deploying a new atelet creates a second DaemonSet pinned to a version no node carries, so it schedules nowhere. This merges together with #1844, which replaces the label-based runbook in `docs/upgrade.md`. #### Breaking change Due to the removal of version label system. An install from v0.4 or earlier can't be upgraded in place. Reinstall it, as with every pre-GA minor release. Part of #1686 - [ ] Tests pass - [x] Appropriate changes to documentation are included in the PR --- README.md | 14 -- cmd/ate-setup/differences.md | 8 +- cmd/ate-setup/internal/cmd/root.go | 4 +- .../internal/demos/demotest/demotest.go | 3 - cmd/ate-setup/internal/demos/render.go | 8 +- cmd/ate-setup/internal/images/prebuilt.go | 9 +- cmd/ate-setup/internal/kube/resources.go | 15 -- cmd/ate-setup/internal/steps/csi.go | 9 +- cmd/ate-setup/internal/steps/delete.go | 11 +- cmd/ate-setup/internal/steps/deploy.go | 38 +---- cmd/ate-setup/internal/steps/env.go | 5 - cmd/ate-setup/internal/steps/overlay.go | 13 +- cmd/ate-setup/internal/steps/overlay_test.go | 7 +- cmd/ate-setup/internal/steps/publish.go | 10 +- cmd/ate-setup/internal/steps/version.go | 137 ++---------------- cmd/ate-setup/internal/steps/version_test.go | 66 ++------- cmd/objectstore-plugin/main_test.go | 2 +- demos/counter/counter-microvm.yaml.tmpl | 2 - demos/counter/counter.yaml.tmpl | 2 - docs/api-guide.md | 23 --- hack/microvm-assets/README.md | 2 +- hack/setup-csi-hostpath-kind.sh | 3 +- hack/setup-csi-nfs-kind.sh | 3 +- internal/versionlabel/cmd/main.go | 40 ----- internal/versionlabel/versionlabel.go | 99 ------------- internal/versionlabel/versionlabel_test.go | 103 ------------- manifests/ate-install/atelet.yaml | 16 +- .../kind/atelet/kustomization.yaml | 2 +- tools/setup-gcp/README.md | 10 -- .../dashboards/ate-e2e-latency-dashboard.json | 2 +- .../dashboards/ate-grpc-dashboard.json | 6 +- .../dashboards/ate-snapshot-dashboard.json | 10 +- 32 files changed, 65 insertions(+), 617 deletions(-) delete mode 100644 internal/versionlabel/cmd/main.go delete mode 100644 internal/versionlabel/versionlabel.go delete mode 100644 internal/versionlabel/versionlabel_test.go diff --git a/README.md b/README.md index aa85a02f00..3f6d4b021b 100644 --- a/README.md +++ b/README.md @@ -142,15 +142,6 @@ curl -X POST \ -i http://localhost:8000/ ``` -Worker capacity is versioned: the dataplane (the atelet DaemonSet and the -worker pods) schedules only on nodes that carry the -`ate.dev/substrate-version` label, and the install stamps it on every node -that exists when it runs. A node added later hosts no workers until you label -it with the installed version -(`kubectl label node ate.dev/substrate-version=`). -`kubectl get ds -n ate-system -l app=atelet -L ate.dev/substrate-version` -prints the installed version, off the atelet DaemonSet the install created. - ### GKE Quickstart (Development) 1. Create and configure your environment file: @@ -194,11 +185,6 @@ prints the installed version, off the atelet DaemonSet the install created. grant each atespace the namespaces it may read; see [Enable it](docs/egress-credential-injection.md#enable-it). - Nodes that GKE adds later (autoscaling, auto-repair, node upgrades) are - born with the node pool's labels, so the pool needs - `ate.dev/substrate-version` too; see - [Node version labels](tools/setup-gcp/README.md). - 5. You can then deploy the sample applications. See [demos/counter/README.md](demos/counter/README.md) or [demos/sandbox/README.md](demos/sandbox/README.md) for detailed walkthroughs. ```bash ./hack/install-ate.sh --deploy-demo-counter diff --git a/cmd/ate-setup/differences.md b/cmd/ate-setup/differences.md index 6e92de84d5..832ca52fea 100644 --- a/cmd/ate-setup/differences.md +++ b/cmd/ate-setup/differences.md @@ -127,13 +127,7 @@ unpinned reference is rejected by admission and every demo fails to deploy. Pinning also gives a pre-built install the property a `ko` install had for free, that a tag moving afterwards cannot change what is running. -That tag also becomes the substrate version, which names the atelet DaemonSet -and sets the node label partitioning nodes across coexisting versions. It has -to: that label must describe the atelet actually running, which came from the -image, not the checkout `git describe` happens to be sitting on. `VERSION` -overrides it, as it always has. - -The rewrite is textual, one token at a time, matching `SubstituteVersion` above. +The rewrite is textual, one token at a time. A reference is replaced wherever it appears, so the code needs no list of which fields hold images — `workerImage:` in a CRD spec is rewritten like any other — and the applied manifest differs from the checked-in YAML only in the image diff --git a/cmd/ate-setup/internal/cmd/root.go b/cmd/ate-setup/internal/cmd/root.go index 2b1fd8995f..33c23026e2 100644 --- a/cmd/ate-setup/internal/cmd/root.go +++ b/cmd/ate-setup/internal/cmd/root.go @@ -164,9 +164,7 @@ func recordRun(cmd *cobra.Command, runErr error) { // failed before it had an environment to ask. var version string if env != nil { - if v, _, err := env.SubstrateVersion(); err == nil { - version = v - } + version = env.SubstrateVersion() } var path string diff --git a/cmd/ate-setup/internal/demos/demotest/demotest.go b/cmd/ate-setup/internal/demos/demotest/demotest.go index f320794be0..66395ed54e 100644 --- a/cmd/ate-setup/internal/demos/demotest/demotest.go +++ b/cmd/ate-setup/internal/demos/demotest/demotest.go @@ -32,9 +32,6 @@ import ( // configuration, so they can be exercised without a kubeconfig. func Env(t *testing.T) *steps.Env { t.Helper() - // Pin VERSION so rendering does not depend on whatever tags (e.g. - // archive/*) happen to be in the local git checkout. - t.Setenv("VERSION", "v0.0.0-test") root, err := config.RepoRoot() if err != nil { t.Fatalf("RepoRoot: %v", err) diff --git a/cmd/ate-setup/internal/demos/render.go b/cmd/ate-setup/internal/demos/render.go index 970ecab412..d5061fd43e 100644 --- a/cmd/ate-setup/internal/demos/render.go +++ b/cmd/ate-setup/internal/demos/render.go @@ -33,16 +33,10 @@ var ExternalVolumePlaceholders = []string{ "EXTERNAL_VOLUMES", } -// Render expands a demo template with the configured bucket name and the -// build version (pool templates pin worker pods to version-labeled nodes). +// Render expands a demo template with the configured bucket name. func Render(e *steps.Env, relPath string, extraValues map[string]string, drop []string) ([]byte, error) { - version, _, err := e.SubstrateVersion() - if err != nil { - return nil, err - } values := map[string]string{ bucketNamePlaceholder: e.Cfg.BucketName, - "SUBSTRATE_VERSION": version, } for k, v := range extraValues { values[k] = v diff --git a/cmd/ate-setup/internal/images/prebuilt.go b/cmd/ate-setup/internal/images/prebuilt.go index 5ffdb39b45..9f3a5403bb 100644 --- a/cmd/ate-setup/internal/images/prebuilt.go +++ b/cmd/ate-setup/internal/images/prebuilt.go @@ -123,11 +123,10 @@ func (p *Prebuilt) Pin(ctx context.Context, image string) (string, error) { // rewrite replaces every ko:// reference with the image it maps to. // -// Substitution is textual, the same way SubstituteVersion fills -// ${SUBSTRATE_VERSION}. References appear in CRD fields as well as pod specs (a -// WorkerPool's spec.workerImage, an ActorTemplate's image), so a schema walk -// would need to know every such field, and rewriting the bytes leaves -// everything else -- the multi-kilobyte Envoy configuration blocks in +// Substitution is textual. References appear in CRD fields as well as pod +// specs (a WorkerPool's spec.workerImage, an ActorTemplate's image), so a +// schema walk would need to know every such field, and rewriting the bytes +// leaves everything else -- the multi-kilobyte Envoy configuration blocks in // particular -- exactly as committed. func (p *Prebuilt) rewrite(ctx context.Context, manifest []byte) ([]byte, error) { // Collect the failures rather than stopping at the first. Diagnosing them diff --git a/cmd/ate-setup/internal/kube/resources.go b/cmd/ate-setup/internal/kube/resources.go index 8319b6349e..d959a16baf 100644 --- a/cmd/ate-setup/internal/kube/resources.go +++ b/cmd/ate-setup/internal/kube/resources.go @@ -155,21 +155,6 @@ func (c *Client) PatchDeployment(ctx context.Context, namespace, name string, pa return nil } -// DaemonSetNames lists the DaemonSets in a namespace matching a label -// selector. The atelet DaemonSet name carries a substrate version suffix, so -// the installed versions can only be found by label. -func (c *Client) DaemonSetNames(ctx context.Context, namespace, selector string) ([]string, error) { - list, err := c.Typed.AppsV1().DaemonSets(namespace).List(ctx, metav1.ListOptions{LabelSelector: selector}) - if err != nil { - return nil, fmt.Errorf("while listing daemonsets in %s matching %q: %w", namespace, selector, err) - } - names := make([]string, 0, len(list.Items)) - for _, ds := range list.Items { - names = append(names, ds.Name) - } - return names, nil -} - // SetServiceAccountAnnotation adds or removes one annotation on a // ServiceAccount; an empty value removes it. A missing ServiceAccount is not // an error, matching the `|| true` the shell installer removed one under. diff --git a/cmd/ate-setup/internal/steps/csi.go b/cmd/ate-setup/internal/steps/csi.go index 4dcf0914a8..129bdd4b5b 100644 --- a/cmd/ate-setup/internal/steps/csi.go +++ b/cmd/ate-setup/internal/steps/csi.go @@ -20,6 +20,7 @@ import ( "os" "os/exec" "strings" + "time" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -290,8 +291,8 @@ spec: // The image cache directories under ateomHostDir were just wiped, so // atelet has to recreate them. - log.Infof("Restarting the atelet DaemonSets (if present)...") - return e.RestartAteletDaemonSets(ctx) + log.Infof("Restarting the atelet DaemonSet (if present)...") + return e.Kube.RolloutRestart(ctx, e.Namespace(), "atelet", time.Now()) } func (e *Env) setupCSINFS(ctx context.Context) error { @@ -422,8 +423,8 @@ spec: return err } - log.Infof("Restarting the atelet DaemonSets (if present)...") - return e.RestartAteletDaemonSets(ctx) + log.Infof("Restarting the atelet DaemonSet (if present)...") + return e.Kube.RolloutRestart(ctx, e.Namespace(), "atelet", time.Now()) } // checkNFSDSupport verifies the host kernel can serve NFS. The in-cluster NFS diff --git a/cmd/ate-setup/internal/steps/delete.go b/cmd/ate-setup/internal/steps/delete.go index 74568bf8b4..84b4aeecb4 100644 --- a/cmd/ate-setup/internal/steps/delete.go +++ b/cmd/ate-setup/internal/steps/delete.go @@ -64,12 +64,6 @@ func (e *Env) DeleteAteSystem(ctx context.Context) error { return err } - // atelet DaemonSet names carry a version suffix. - if err := e.Kube.Typed.AppsV1().DaemonSets(e.Namespace()).DeleteCollection(ctx, - metav1.DeleteOptions{}, metav1.ListOptions{LabelSelector: "app=atelet"}); err != nil { - return fmt.Errorf("while deleting atelet daemonsets: %w", err) - } - for _, path := range [][]string{ {"components", "agentgateway", "configmap.yaml"}, {"postgres", "postgres.yaml"}, @@ -94,10 +88,7 @@ func (e *Env) DeleteAteSystem(ctx context.Context) error { if err := e.Kube.WaitDeleted(ctx, namespace, "", NamespacePodCert, e.Cfg.WaitTimeout(BootstrapTimeout)); err != nil { return err } - if err := e.DeleteTrustBundles(ctx); err != nil { - return err - } - return e.UnlabelNodesSubstrateVersion(ctx) + return e.DeleteTrustBundles(ctx) } // releaseSandboxConfigs removes the WorkerPool protection finalizer from diff --git a/cmd/ate-setup/internal/steps/deploy.go b/cmd/ate-setup/internal/steps/deploy.go index 77a9f98a2e..b72a90281c 100644 --- a/cmd/ate-setup/internal/steps/deploy.go +++ b/cmd/ate-setup/internal/steps/deploy.go @@ -64,11 +64,8 @@ func (e *Env) DeployAteSystem(ctx context.Context, opts DeployOptions) error { if err := e.RequireCanonicalNamespace("deploy ate-system"); err != nil { return err } - // Fail fast on an unusable build version or credential provider selection - // before touching the cluster. - if _, _, err := e.SubstrateVersion(); err != nil { - return err - } + // Fail fast on an unusable credential provider selection before touching + // the cluster. provider, err := e.Cfg.CredentialProvider() if err != nil { return err @@ -85,12 +82,6 @@ func (e *Env) DeployAteSystem(ctx context.Context, opts DeployOptions) error { return err } - // Before the bundle: the atelet DaemonSet applied below and the demo - // WorkerPools' version-pinned pods schedule only to version-labeled nodes. - if err := e.LabelNodesSubstrateVersion(ctx); err != nil { - return err - } - // DeployCRDs, not EnsureCRDs: an existence check would skip upgrades, // stranding stale CRD schemas and RBAC (role.yaml has no other apply // path). @@ -139,12 +130,6 @@ func (e *Env) DeployAteSystem(ctx context.Context, opts DeployOptions) error { if err != nil { return err } - // The atelet DaemonSet in the bundle is version-keyed; fill its - // placeholders after the render (kustomize and ko pass them through). - manifests, err = e.SubstituteVersion(manifests) - if err != nil { - return err - } if err := e.applyWithAPIServerEnvHash(ctx, manifests); err != nil { return err } @@ -172,10 +157,6 @@ func (e *Env) DeployAteSystem(ctx context.Context, opts DeployOptions) error { return err } - ateletName, err := e.AteletDaemonSetName() - if err != nil { - return err - } log.Step("Waiting for ATE system components to be ready...") type rollout struct{ kind, name string } var waits []rollout @@ -184,7 +165,7 @@ func (e *Env) DeployAteSystem(ctx context.Context, opts DeployOptions) error { rollout{kube.KindDeployment, "ate-controller"}, rollout{kube.KindDeployment, "atenet-router"}, rollout{kube.KindDeployment, "atenet-egress"}, - rollout{kube.KindDaemonSet, ateletName}, + rollout{kube.KindDaemonSet, "atelet"}, ) if provider.Kubernetes() { waits = append(waits, rollout{kube.KindDeployment, k8sCredentialProviderDeployment}) @@ -354,9 +335,6 @@ func (e *Env) DeployAtelet(ctx context.Context) error { if err := e.EnsureAteSystemNamespace(ctx); err != nil { return err } - if err := e.LabelNodesSubstrateVersion(ctx); err != nil { - return err - } if err := e.applyOtelConfig(ctx); err != nil { return err } @@ -375,18 +353,10 @@ func (e *Env) DeployAtelet(ctx context.Context) error { if err != nil { return err } - manifest, err = e.SubstituteVersion(manifest) - if err != nil { - return err - } if err := e.Kube.ApplyBytes(ctx, manifest); err != nil { return err } - ateletName, err := e.AteletDaemonSetName() - if err != nil { - return err - } - return e.Kube.RolloutStatus(ctx, kube.KindDaemonSet, e.Namespace(), ateletName, e.Cfg.RolloutTimeout) + return e.Kube.RolloutStatus(ctx, kube.KindDaemonSet, e.Namespace(), "atelet", e.Cfg.RolloutTimeout) } // DeployAtenet redeploys the atenet dataplane: router and egress. diff --git a/cmd/ate-setup/internal/steps/env.go b/cmd/ate-setup/internal/steps/env.go index 1e22e5b889..1591e77fd8 100644 --- a/cmd/ate-setup/internal/steps/env.go +++ b/cmd/ate-setup/internal/steps/env.go @@ -81,11 +81,6 @@ type Env struct { // work on a machine with no container registry credentials configured, and // the build-from-source resolver fails at construction without them. resolver imageResolver - - // substrateVersion caches the derived build version and its object-name - // suffix; see SubstrateVersion. - substrateVersion string - substrateVersionSuffix string } // Namespace is the namespace the control plane is installed into. It is diff --git a/cmd/ate-setup/internal/steps/overlay.go b/cmd/ate-setup/internal/steps/overlay.go index 9f884d5ee6..7e5081fc53 100644 --- a/cmd/ate-setup/internal/steps/overlay.go +++ b/cmd/ate-setup/internal/steps/overlay.go @@ -529,16 +529,5 @@ func (e *Env) applyOtelEndpointOverride(ctx context.Context) error { return err } } - // atelet DaemonSet names carry a version suffix; restart whichever - // versions are installed. - daemonSets, err := e.Kube.DaemonSetNames(ctx, e.Namespace(), "app=atelet") - if err != nil { - return err - } - for _, name := range daemonSets { - if err := e.Kube.RolloutRestart(ctx, e.Namespace(), name, now); err != nil { - return err - } - } - return nil + return e.Kube.RolloutRestart(ctx, e.Namespace(), "atelet", now) } diff --git a/cmd/ate-setup/internal/steps/overlay_test.go b/cmd/ate-setup/internal/steps/overlay_test.go index b213fbfa96..57881c6ae8 100644 --- a/cmd/ate-setup/internal/steps/overlay_test.go +++ b/cmd/ate-setup/internal/steps/overlay_test.go @@ -772,13 +772,10 @@ func TestApplyOtelEndpointOverride(t *testing.T) { }) t.Run("patches and restarts the consumers", func(t *testing.T) { - // The atelet DaemonSet name carries a substrate version suffix, so it - // can only be found by label. atelet := &appsv1.DaemonSet{ ObjectMeta: metav1.ObjectMeta{ Namespace: NamespaceAteSystem, - Name: "atelet-v1-2-3", - Labels: map[string]string{"app": "atelet"}, + Name: "atelet", }, } e := &Env{ @@ -799,7 +796,7 @@ func TestApplyOtelEndpointOverride(t *testing.T) { if !restartedAt(t, e, "deployment", "ate-api-server") { t.Error("ate-api-server was not restarted") } - if !restartedAt(t, e, "daemonset", "atelet-v1-2-3") { + if !restartedAt(t, e, "daemonset", "atelet") { t.Error("the atelet DaemonSet was not restarted") } }) diff --git a/cmd/ate-setup/internal/steps/publish.go b/cmd/ate-setup/internal/steps/publish.go index e43f207a6f..d25ed6c493 100644 --- a/cmd/ate-setup/internal/steps/publish.go +++ b/cmd/ate-setup/internal/steps/publish.go @@ -34,10 +34,7 @@ var workerImages = []string{"ateom-gvisor", "ateom-microvm"} // output. A WorkerPool moves to this build by pointing its workerImage at // the ref. func (e *Env) PublishWorkerImages(ctx context.Context, w io.Writer) error { - version, _, err := e.SubstrateVersion() - if err != nil { - return err - } + version := e.SubstrateVersion() log.Stepf("publish_worker_images (%s)", version) runner, err := e.koRunner() if err != nil { @@ -71,10 +68,7 @@ func (e *Env) PublishReleaseImages(ctx context.Context, w io.Writer) error { if e.Cfg.KODockerRepo == "" { return fmt.Errorf("publishing release images needs a registry to push to; set KO_DOCKER_REPO or --ko-docker-repo") } - tag, _, err := e.SubstrateVersion() - if err != nil { - return err - } + tag := e.SubstrateVersion() log.Stepf("publish_release_images (%s)", tag) runner, err := e.koRunner() if err != nil { diff --git a/cmd/ate-setup/internal/steps/version.go b/cmd/ate-setup/internal/steps/version.go index 8ac5ac72b7..5fb94f21d9 100644 --- a/cmd/ate-setup/internal/steps/version.go +++ b/cmd/ate-setup/internal/steps/version.go @@ -15,139 +15,28 @@ package steps import ( - "context" - "fmt" "os" - "regexp" "strings" - "time" - - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/types" "github.com/agent-substrate/substrate/cmd/ate-setup/internal/ko" - "github.com/agent-substrate/substrate/cmd/ate-setup/internal/log" - "github.com/agent-substrate/substrate/internal/versionlabel" ) -// SubstrateVersion returns the build version and the object-name suffix for atelet. -// The version is the same one ko stamps into the binaries. +// SubstrateVersion returns the build version, the same one ko stamps into the +// binaries. // // With --image-repo nothing is built, so `git describe` would report the -// checkout rather than the images being installed. The image tag is the version -// in that case. It has to be: the version names the atelet DaemonSet and sets -// the node label that partitions nodes across coexisting versions, so it must -// describe the atelet that is actually running, which came from the image. -// VERSION still wins over both, so a tag that is not a valid label value can be -// worked around the same way. -func (e *Env) SubstrateVersion() (version, suffix string, err error) { - if e.substrateVersion != "" { - return e.substrateVersion, e.substrateVersionSuffix, nil - } - var raw string +// checkout rather than the images being installed. The image tag is the +// version in that case. VERSION still wins over both. +func (e *Env) SubstrateVersion() string { if e.Cfg.Images.IsPrebuilt() { - raw = os.Getenv("VERSION") - if raw == "" { - // The tag may carry its digest (v1@sha256:...); the version - // is the tag alone. - raw, _, _ = strings.Cut(e.Cfg.Images.Tag, "@") - } - } else { - // ko.BuildVersion consults VERSION itself before `git describe`. - raw = ko.BuildVersion(e.Cfg.Root) - } - if v := versionlabel.Value(raw); v != raw { - return "", "", fmt.Errorf("build version %q is not a valid label value (it would sanitize to %q); pin a label-safe one with the VERSION env var", raw, v) - } - e.substrateVersion = raw - e.substrateVersionSuffix = versionlabel.NameSuffix(raw) - return e.substrateVersion, e.substrateVersionSuffix, nil -} - -func (e *Env) AteletDaemonSetName() (string, error) { - _, suffix, err := e.SubstrateVersion() - if err != nil { - return "", err - } - return "atelet-" + suffix, nil -} - -// unquotedVersionScalar matches a whole-scalar ${SUBSTRATE_VERSION} value -// kustomize re-emitted unquoted. It is re-quoted before substitution so an -// all-digit version lands as a YAML string, not an integer. -var unquotedVersionScalar = regexp.MustCompile(`(?m)^(\s*[^:\n]+): \$\{SUBSTRATE_VERSION\}$`) - -// SubstituteVersion fills the ${SUBSTRATE_VERSION} and -// ${SUBSTRATE_VERSION_SUFFIX} placeholders in a rendered manifest. -func (e *Env) SubstituteVersion(manifest []byte) ([]byte, error) { - version, suffix, err := e.SubstrateVersion() - if err != nil { - return nil, err - } - s := unquotedVersionScalar.ReplaceAllString(string(manifest), `$1: "$${SUBSTRATE_VERSION}"`) - s = strings.ReplaceAll(s, "${SUBSTRATE_VERSION}", version) - s = strings.ReplaceAll(s, "${SUBSTRATE_VERSION_SUFFIX}", suffix) - return []byte(s), nil -} - -// RestartAteletDaemonSets pod-restarts every atelet DaemonSet by label. -func (e *Env) RestartAteletDaemonSets(ctx context.Context) error { - list, err := e.Kube.Typed.AppsV1().DaemonSets(e.Namespace()).List(ctx, metav1.ListOptions{ - LabelSelector: "app=atelet", - }) - if err != nil { - return fmt.Errorf("while listing atelet daemonsets: %w", err) - } - for _, ds := range list.Items { - if err := e.Kube.RolloutRestart(ctx, e.Namespace(), ds.Name, time.Now()); err != nil { - return err - } - } - return nil -} - -// LabelNodesSubstrateVersion stamps ate.dev/substrate-version on every node -// that does not carry it yet. Nodes that already carry the label keep their -// value: during a rolling upgrade the operator owns the per-node value, and -// an install must not yank nodes across versions behind its back. -func (e *Env) LabelNodesSubstrateVersion(ctx context.Context) error { - version, _, err := e.SubstrateVersion() - if err != nil { - return err - } - log.Stepf("label_nodes_substrate_version (%s)", version) - nodes, err := e.Kube.Typed.CoreV1().Nodes().List(ctx, metav1.ListOptions{ - LabelSelector: "!" + versionlabel.Key, - }) - if err != nil { - return fmt.Errorf("while listing unlabeled nodes: %w", err) - } - patch := fmt.Sprintf(`{"metadata":{"labels":{%q:%q}}}`, versionlabel.Key, version) - for _, node := range nodes.Items { - if _, err := e.Kube.Typed.CoreV1().Nodes().Patch(ctx, node.Name, types.StrategicMergePatchType, []byte(patch), metav1.PatchOptions{}); err != nil { - return fmt.Errorf("while labeling node %s: %w", node.Name, err) - } - } - return nil -} - -// UnlabelNodesSubstrateVersion clears ate.dev/substrate-version from every -// node that carries it. Uninstall removes the label with the system: the -// install won't relabels a labeled node, so a leftover label would pin the -// next install to the old version world. -func (e *Env) UnlabelNodesSubstrateVersion(ctx context.Context) error { - log.Step("unlabel_nodes_substrate_version") - nodes, err := e.Kube.Typed.CoreV1().Nodes().List(ctx, metav1.ListOptions{ - LabelSelector: versionlabel.Key, - }) - if err != nil { - return fmt.Errorf("while listing labeled nodes: %w", err) - } - patch := fmt.Sprintf(`{"metadata":{"labels":{%q:null}}}`, versionlabel.Key) - for _, node := range nodes.Items { - if _, err := e.Kube.Typed.CoreV1().Nodes().Patch(ctx, node.Name, types.StrategicMergePatchType, []byte(patch), metav1.PatchOptions{}); err != nil { - return fmt.Errorf("while unlabeling node %s: %w", node.Name, err) + if v := os.Getenv("VERSION"); v != "" { + return v } + // The tag may carry its digest (v1@sha256:...); the version + // is the tag alone. + v, _, _ := strings.Cut(e.Cfg.Images.Tag, "@") + return v } - return nil + // ko.BuildVersion consults VERSION itself before `git describe`. + return ko.BuildVersion(e.Cfg.Root) } diff --git a/cmd/ate-setup/internal/steps/version_test.go b/cmd/ate-setup/internal/steps/version_test.go index d0772c1095..c1674e7eab 100644 --- a/cmd/ate-setup/internal/steps/version_test.go +++ b/cmd/ate-setup/internal/steps/version_test.go @@ -21,72 +21,36 @@ import ( "github.com/agent-substrate/substrate/cmd/ate-setup/internal/images" ) -// TestSubstrateVersionPrebuilt covers the version a prebuilt install stamps. +// TestSubstrateVersionPrebuilt covers the version a prebuilt install reports. // Root points at a directory that is not a checkout, so `git describe` has // nothing to say and only the image tag can supply it. func TestSubstrateVersionPrebuilt(t *testing.T) { t.Setenv("VERSION", "") cases := []struct { - name string - source images.Source - want string - wantSuffix string + name string + source images.Source + want string }{{ - name: "tag is the version", - source: images.Source{Repo: "example.com/substrate", Tag: "v0.0.0-503-4b3423c0"}, - want: "v0.0.0-503-4b3423c0", - wantSuffix: "v0-0-0-503-4b3423c0", + name: "tag is the version", + source: images.Source{Repo: "example.com/substrate", Tag: "v0.0.0-503-4b3423c0"}, + want: "v0.0.0-503-4b3423c0", }, { - name: "a tag carrying its digest is the version without it", - source: images.Source{Repo: "example.com/substrate", Tag: "v0.0.0@sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"}, - want: "v0.0.0", - wantSuffix: "v0-0-0", + name: "a tag carrying its digest is the version without it", + source: images.Source{Repo: "example.com/substrate", Tag: "v0.0.0@sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"}, + want: "v0.0.0", }, { - name: "built from source falls back to git", - source: images.Source{}, - want: "dev", - wantSuffix: "dev", + name: "built from source falls back to git", + source: images.Source{}, + want: "dev", }} for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { e := &Env{Cfg: &config.Config{Root: t.TempDir(), Images: tc.source}} - got, suffix, err := e.SubstrateVersion() - if err != nil { - t.Fatalf("SubstrateVersion: %v", err) - } - if got != tc.want || suffix != tc.wantSuffix { - t.Fatalf("SubstrateVersion = %q, %q; want %q, %q", got, suffix, tc.want, tc.wantSuffix) + if got := e.SubstrateVersion(); got != tc.want { + t.Fatalf("SubstrateVersion = %q, want %q", got, tc.want) } }) } } - -func TestSubstituteVersion(t *testing.T) { - e := &Env{substrateVersion: "v1.2.3", substrateVersionSuffix: "v1-2-3"} - in := "" + - "metadata:\n" + - " name: atelet-${SUBSTRATE_VERSION_SUFFIX}\n" + - " labels:\n" + - " ate.dev/substrate-version: ${SUBSTRATE_VERSION}\n" + - "nodeSelector:\n" + - " ate.dev/substrate-version: \"${SUBSTRATE_VERSION}\"\n" - // The unquoted scalar is re-quoted (an all-digit version must land as a - // YAML string); already-quoted values and name suffixes pass through. - want := "" + - "metadata:\n" + - " name: atelet-v1-2-3\n" + - " labels:\n" + - " ate.dev/substrate-version: \"v1.2.3\"\n" + - "nodeSelector:\n" + - " ate.dev/substrate-version: \"v1.2.3\"\n" - - got, err := e.SubstituteVersion([]byte(in)) - if err != nil { - t.Fatalf("SubstituteVersion: %v", err) - } - if string(got) != want { - t.Fatalf("SubstituteVersion mismatch:\ngot:\n%s\nwant:\n%s", got, want) - } -} diff --git a/cmd/objectstore-plugin/main_test.go b/cmd/objectstore-plugin/main_test.go index 07b9cd1f9b..a939cc614e 100644 --- a/cmd/objectstore-plugin/main_test.go +++ b/cmd/objectstore-plugin/main_test.go @@ -58,7 +58,7 @@ func TestInstallStagingCapacityFitsVolume(t *testing.T) { if err := yaml.Unmarshal([]byte(doc), &obj); err != nil { t.Fatal(err) } - if obj.Kind == "DaemonSet" && strings.HasPrefix(obj.Name, "atelet-") { + if obj.Kind == "DaemonSet" && obj.Name == "atelet" { ds = &obj } } diff --git a/demos/counter/counter-microvm.yaml.tmpl b/demos/counter/counter-microvm.yaml.tmpl index aa68bdd1f0..08e1048abb 100644 --- a/demos/counter/counter-microvm.yaml.tmpl +++ b/demos/counter/counter-microvm.yaml.tmpl @@ -45,8 +45,6 @@ spec: name: microvm workerImage: ko://github.com/agent-substrate/substrate/cmd/ateom-microvm template: - nodeSelector: - ate.dev/substrate-version: "${SUBSTRATE_VERSION}" # Per-worker resources: these size the worker POD and advertise its # scheduling capacity (the per-actor ceiling). The micro-VM sandbox itself is # sized by the ActorTemplate's resources in counter-microvm-template.yaml.tmpl, diff --git a/demos/counter/counter.yaml.tmpl b/demos/counter/counter.yaml.tmpl index dea8cb2541..4cc0ea0516 100644 --- a/demos/counter/counter.yaml.tmpl +++ b/demos/counter/counter.yaml.tmpl @@ -44,8 +44,6 @@ spec: sandboxClasses: - name: gvisor template: - nodeSelector: - ate.dev/substrate-version: "${SUBSTRATE_VERSION}" # Per-worker resources: these size the worker POD and advertise its # scheduling capacity (the per-actor ceiling). The sandbox itself is sized by # the ActorTemplate's resources in counter-template.yaml.tmpl, not by these diff --git a/docs/api-guide.md b/docs/api-guide.md index a3f6b37b2c..b3f6703dcd 100644 --- a/docs/api-guide.md +++ b/docs/api-guide.md @@ -37,29 +37,6 @@ syntax. metadata; they do not affect actor scheduling. Actor selectors match `WorkerPool.metadata.labels`, not `WorkerPool.spec.template.labels`. -#### Pin pools to the installed substrate version (`template.nodeSelector`) - -The installation labels every node with the substrate build version it deployed. -Set the same label as a `nodeSelector` on every pool, so its workers only run -on nodes of that version: - -```yaml -spec: - template: - nodeSelector: - ate.dev/substrate-version: "" -``` - -Read the installed version off the atelet DaemonSet: - -```bash -kubectl get ds -n ate-system -l app=atelet -L ate.dev/substrate-version -``` - -The pin is critical to make a rolling upgrade possible. An upgrade moves nodes to -the new version one at a time, deleting each node's old worker pods once it -moves. A pinned pool cannot put those pods back on a moved node, so the old -version drains away node by node. An unpinned pool breaks this constraints. #### Worker Capacity (`spec.template.resources`) Setting `resources.limits` (CPU and Memory) on a `WorkerPool` establishes each worker pod's **capacity** — the envelope its actor sandboxes share, taken from the `ateom` container's limits. The scheduler only places an actor on a worker whose remaining capacity is `>=` the actor's declared resource limits (see [Sandbox Right-Sizing](#sandbox-right-sizing-resources) on the `ActorTemplate`). diff --git a/hack/microvm-assets/README.md b/hack/microvm-assets/README.md index d4b9f26af8..3fe5b35534 100644 --- a/hack/microvm-assets/README.md +++ b/hack/microvm-assets/README.md @@ -42,7 +42,7 @@ it as a device, which is what places micro-VM workers there. 4. **Apply the demo + drive it:** ```sh - BUCKET_NAME=ate-snapshots SUBSTRATE_VERSION="$(git describe --tags --always --dirty)" envsubst < demos/counter/counter-microvm.yaml.tmpl | kubectl apply -f - # the pool pins workers to nodes labeled with this version + BUCKET_NAME=ate-snapshots envsubst < demos/counter/counter-microvm.yaml.tmpl | kubectl apply -f - ``` Create an actor from `counter-microvm`, hit the in-RAM counter to increment it, suspend (checkpoint), resume on a different worker pod, and confirm the count continues — proving the diff --git a/hack/setup-csi-hostpath-kind.sh b/hack/setup-csi-hostpath-kind.sh index 514d9ae64c..f0a0a6e95b 100755 --- a/hack/setup-csi-hostpath-kind.sh +++ b/hack/setup-csi-hostpath-kind.sh @@ -134,8 +134,7 @@ spec: EOF # 9. Restart atelet to recreate image cache directories if they were wiped. -# atelet DaemonSet names carry a build-version suffix, so select by label. echo "Restarting atelet DaemonSet (if present)..." -kubectl rollout restart daemonset -l app=atelet -n ate-system >/dev/null 2>&1 || true +kubectl rollout restart daemonset/atelet -n ate-system >/dev/null 2>&1 || true echo "CSI Hostpath setup complete!" diff --git a/hack/setup-csi-nfs-kind.sh b/hack/setup-csi-nfs-kind.sh index 68a41e011b..eeec5a61ac 100755 --- a/hack/setup-csi-nfs-kind.sh +++ b/hack/setup-csi-nfs-kind.sh @@ -119,8 +119,7 @@ spec: EOF # 9. Restart atelet to ensure it reconnects to the new CSI socket. -# atelet DaemonSet names carry a build-version suffix, so select by label. echo "Restarting atelet DaemonSet (if present)..." -kubectl rollout restart daemonset -l app=atelet -n ate-system >/dev/null 2>&1 || true +kubectl rollout restart daemonset/atelet -n ate-system >/dev/null 2>&1 || true echo "CSI NFS setup complete!" diff --git a/internal/versionlabel/cmd/main.go b/internal/versionlabel/cmd/main.go deleted file mode 100644 index afe127f070..0000000000 --- a/internal/versionlabel/cmd/main.go +++ /dev/null @@ -1,40 +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. - -// Command cmd prints "