From e5f344a13a1dab3ff702716d683dbcfbe0a256fd Mon Sep 17 00:00:00 2001 From: ILLUM1N0X Date: Thu, 15 Oct 2026 16:31:35 +0000 Subject: [PATCH 01/31] oidcjwt: take signing keys from a KeySource, and expose Substrate claims (#2671) Part of #777 The objectstore control plugin will verify actor JWTs minted by ate-api-server. No component serves the JWKS yet, ate-idp-server has not landed, so the verifier needs keys from somewhere other than discovery, and it must not be handed the signing keys to get them. `oidcjwt.NewVerifier` now takes a `KeySource`. `HTTPKeySource` is the existing discovery fetch. `FileKeySource` reads a JWK set from a file with the same parser. The `actor-id-jwt-pool` Secret carries that set under `jwks` next to `pool`: ate-setup, `make-jwt-pool`, and `add-jwt-key` write both in one update, and ate-setup brings `jwks` in step with the pool on every deploy, for a Secret from before the key existed or one rewritten by an older tool. The sidecar will mount only the `jwks` item, in the manifests PR. `Claims` gains `SubstrateClaims`, decoded only after the signature check: `Kind`, actor or worker-for-actor, and the claim it names, with a token carrying both refused. Decoding them is opt-in through `WithSubstrateClaims`, which only the actor-id verifier sets, so tokens from other issuers verify as on main whatever they carry under those names. No behavior change for ate-api-server, its call site wraps the discovery client in `HTTPKeySource`. --- cmd/ate-setup/internal/steps/create.go | 38 +++- cmd/ate-setup/internal/steps/create_test.go | 56 ++++- cmd/ate-setup/internal/steps/prereqs.go | 4 +- cmd/ateapi/main.go | 2 +- cmd/kubectl-ate/README.md | 4 +- cmd/kubectl-ate/internal/cmd/admin.go | 11 + cmd/kubectl-ate/internal/cmd/admin_test.go | 4 + internal/localjwtauthority/jwks.go | 21 ++ .../localjwtauthority/jwks_filesource_test.go | 102 ++++++++++ internal/oidcjwt/oidcjwt.go | 192 +++++++++++++++--- internal/oidcjwt/oidcjwt_test.go | 192 +++++++++++++++++- 11 files changed, 582 insertions(+), 44 deletions(-) create mode 100644 internal/localjwtauthority/jwks_filesource_test.go diff --git a/cmd/ate-setup/internal/steps/create.go b/cmd/ate-setup/internal/steps/create.go index 6cc21de88b..f8420c520b 100644 --- a/cmd/ate-setup/internal/steps/create.go +++ b/cmd/ate-setup/internal/steps/create.go @@ -15,6 +15,7 @@ package steps import ( + "bytes" "context" "fmt" "strings" @@ -51,6 +52,7 @@ const ( const caValidity = 365 * 24 * time.Hour // CreateJWTAuthorityPoolSecret generates the actor-identity JWT signing pool. +// An existing pool is kept, and its "jwks" key is brought in step with it. // This is the make-jwt-pool call create_jwt_authority_pool_secret shelled out // to kubectl-ate for. func (e *Env) CreateJWTAuthorityPoolSecret(ctx context.Context) error { @@ -254,13 +256,13 @@ func newCAPoolSecretData(id string, keyType localca.KeyType) (map[string][]byte, // with the CA pools, an existing pool is preserved: rotating the signing key // would invalidate every token already minted from it. func (e *Env) createJWTPool(ctx context.Context, namespace, name string) error { - exists, err := e.Kube.SecretExists(ctx, namespace, name) + existing, err := e.Kube.GetSecret(ctx, namespace, name) if err != nil { return err } - if exists { + if existing != nil { log.Infof(" JWT authority pool %s/%s already exists; keeping it", namespace, name) - return nil + return e.publishJWTPoolKeys(ctx, existing) } data, err := newJWTPoolSecretData(e.Cfg.ActorJWTAlgorithm) @@ -270,14 +272,40 @@ func (e *Env) createJWTPool(ctx context.Context, namespace, name string) error { return e.createPoolSecret(ctx, namespace, name, corev1.SecretTypeOpaque, data) } +// publishJWTPoolKeys makes the "jwks" key of a pool Secret match its pool: +// missing on a Secret from before the key existed, or stale after a tool that +// did not know it rewrote the pool. A stale set would keep a removed key +// trusted, so it is recomputed from the pool every run. A matching Secret is +// left alone. +func (e *Env) publishJWTPoolKeys(ctx context.Context, secret *corev1.Secret) error { + jwks, err := localjwtauthority.PublicJWKS(secret.Data["pool"]) + if err != nil { + return fmt.Errorf("while publishing the public keys of %s/%s: %w", secret.Namespace, secret.Name, err) + } + if bytes.Equal(secret.Data["jwks"], jwks) { + return nil + } + secret.Data["jwks"] = jwks + if _, err := e.Kube.Typed.CoreV1().Secrets(secret.Namespace).Update(ctx, secret, metav1.UpdateOptions{}); err != nil { + return fmt.Errorf("while writing jwks to %s/%s: %w", secret.Namespace, secret.Name, err) + } + log.Infof(" published the public keys of %s/%s as jwks", secret.Namespace, secret.Name) + return nil +} + // newJWTPoolSecretData generates a pool with one active authority for -// algorithm, keyed by its thumbprint. +// algorithm, keyed by its thumbprint. "jwks" holds the pool's public keys for +// verifiers that must not see the pool. func newJWTPoolSecretData(algorithm string) (map[string][]byte, error) { poolBytes, _, err := localjwtauthority.GeneratePool(algorithm, "") if err != nil { return nil, fmt.Errorf("while generating the JWT pool: %w", err) } - return map[string][]byte{"pool": poolBytes}, nil + jwks, err := localjwtauthority.PublicJWKS(poolBytes) + if err != nil { + return nil, fmt.Errorf("while publishing the JWT pool's public keys: %w", err) + } + return map[string][]byte{"pool": poolBytes, "jwks": jwks}, nil } // createPoolSecret writes pool state. diff --git a/cmd/ate-setup/internal/steps/create_test.go b/cmd/ate-setup/internal/steps/create_test.go index 67fe9d280b..d4a744e98a 100644 --- a/cmd/ate-setup/internal/steps/create_test.go +++ b/cmd/ate-setup/internal/steps/create_test.go @@ -16,6 +16,7 @@ package steps import ( "bytes" + "context" "crypto/tls" "crypto/x509" "maps" @@ -25,7 +26,11 @@ import ( "github.com/google/go-cmp/cmp" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/kubernetes/fake" + k8stesting "k8s.io/client-go/testing" + "github.com/agent-substrate/substrate/cmd/ate-setup/internal/kube" "github.com/agent-substrate/substrate/internal/localca" "github.com/agent-substrate/substrate/internal/localjwtauthority" ) @@ -171,7 +176,7 @@ func TestNewJWTPoolSecretData(t *testing.T) { if err != nil { t.Fatalf("newJWTPoolSecretData() error = %v", err) } - if diff := cmp.Diff([]string{"pool"}, slices.Sorted(maps.Keys(data))); diff != "" { + if diff := cmp.Diff([]string{"jwks", "pool"}, slices.Sorted(maps.Keys(data))); diff != "" { t.Errorf("secret keys differ (-want +got):\n%s", diff) } @@ -179,6 +184,9 @@ func TestNewJWTPoolSecretData(t *testing.T) { if err != nil { t.Fatalf("Unmarshal() error = %v", err) } + if jwks, err := pool.JWKS(); err != nil || !bytes.Equal(jwks, data["jwks"]) { + t.Errorf("jwks = %s, want the pool's public keys (%v)", data["jwks"], err) + } if len(pool.Authorities) != 1 { t.Fatalf("pool has %d authorities, want 1", len(pool.Authorities)) } @@ -231,3 +239,49 @@ func TestEnsurePodCertificateCAs(t *testing.T) { } } } + +func TestCreateJWTPoolPublishesKeysOfExistingPool(t *testing.T) { + poolBytes, _, err := localjwtauthority.GeneratePool("ES256", "") + if err != nil { + t.Fatal(err) + } + wantJWKS, err := localjwtauthority.PublicJWKS(poolBytes) + if err != nil { + t.Fatal(err) + } + for _, tc := range []struct { + name string + data map[string][]byte + wantUpdates int + }{ + {"pool from before jwks", map[string][]byte{"pool": poolBytes}, 1}, + {"pool with a stale jwks", map[string][]byte{"pool": poolBytes, "jwks": []byte(`{"keys":[]}`)}, 1}, + {"pool with jwks", map[string][]byte{"pool": poolBytes, "jwks": wantJWKS}, 0}, + } { + t.Run(tc.name, func(t *testing.T) { + kc := fake.NewSimpleClientset(&corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{Namespace: "ate-system", Name: SecretActorIDJWTPool}, + Data: tc.data, + }) + updates := 0 + kc.PrependReactor("update", "secrets", func(k8stesting.Action) (bool, runtime.Object, error) { + updates++ + return false, nil, nil + }) + e := &Env{Kube: &kube.Client{Typed: kc}} + if err := e.createJWTPool(context.Background(), "ate-system", SecretActorIDJWTPool); err != nil { + t.Fatalf("createJWTPool: %v", err) + } + got, err := kc.CoreV1().Secrets("ate-system").Get(context.Background(), SecretActorIDJWTPool, metav1.GetOptions{}) + if err != nil { + t.Fatal(err) + } + if !bytes.Equal(got.Data["pool"], poolBytes) || !bytes.Equal(got.Data["jwks"], wantJWKS) { + t.Errorf("secret data = %v keys, want the same pool and its jwks", slices.Sorted(maps.Keys(got.Data))) + } + if updates != tc.wantUpdates { + t.Errorf("updates = %d, want %d", updates, tc.wantUpdates) + } + }) + } +} diff --git a/cmd/ate-setup/internal/steps/prereqs.go b/cmd/ate-setup/internal/steps/prereqs.go index cf2dd0b4e1..b0ccc5a6e1 100644 --- a/cmd/ate-setup/internal/steps/prereqs.go +++ b/cmd/ate-setup/internal/steps/prereqs.go @@ -35,7 +35,9 @@ var trustBundleNames = []string{ func (e *Env) EnsureAPIServerPrerequisites(ctx context.Context) error { log.Step("ensure_apiserver_prerequisites") - if err := e.ensureSecret(ctx, e.Namespace(), SecretActorIDJWTPool, e.CreateJWTAuthorityPoolSecret); err != nil { + // Not through ensureSecret: an existing pool Secret may still need its + // public keys published, which CreateJWTAuthorityPoolSecret handles. + if err := e.CreateJWTAuthorityPoolSecret(ctx); err != nil { return err } if err := e.ensureSecret(ctx, e.Namespace(), SecretActorIDCAPool, e.CreateActorIDCAPoolSecret); err != nil { diff --git a/cmd/ateapi/main.go b/cmd/ateapi/main.go index ca3fdd7624..27d44e8c60 100644 --- a/cmd/ateapi/main.go +++ b/cmd/ateapi/main.go @@ -641,7 +641,7 @@ func buildJWTProviders(ctx context.Context, cfg *apiauthn.AuthenticationConfig) if err != nil { return apiauthn.ServerConfig{}, fmt.Errorf("initialize JWT provider %q: %w", providerCfg.Name, err) } - verifier := oidcjwt.NewVerifier(providerCfg.Issuer, providerCfg.Audiences, httpClient) + verifier := oidcjwt.NewVerifier(providerCfg.Issuer, providerCfg.Audiences, oidcjwt.HTTPKeySource(providerCfg.Issuer, httpClient)) serverCfg.JWTProviders = append(serverCfg.JWTProviders, apiauthn.JWTProvider{ Name: providerCfg.Name, Issuer: providerCfg.Issuer, diff --git a/cmd/kubectl-ate/README.md b/cmd/kubectl-ate/README.md index 5b058683a5..6c07170a49 100644 --- a/cmd/kubectl-ate/README.md +++ b/cmd/kubectl-ate/README.md @@ -322,7 +322,9 @@ kubectl ate admin make-ca-pool \ # Generate a new JWT authority pool and push it to a Kubernetes Secret. The # key is ES256 (--alg RS256, a 4096-bit RSA key, for relying parties that don't # support ES256) and its ID defaults to the base64url SHA-256 of the public -# key's PKIX encoding (--key-id to override). +# key's PKIX encoding (--key-id to override). The Secret holds the pool under +# "pool" and its public keys as a JWK set under "jwks", for verifiers that +# must not see the signing keys. Every command below keeps the two in step. kubectl ate admin make-jwt-pool \ --name actor-id-jwt-pool \ --secret-namespace ate-system diff --git a/cmd/kubectl-ate/internal/cmd/admin.go b/cmd/kubectl-ate/internal/cmd/admin.go index 3c9c0c1faf..239b8531ee 100644 --- a/cmd/kubectl-ate/internal/cmd/admin.go +++ b/cmd/kubectl-ate/internal/cmd/admin.go @@ -264,7 +264,13 @@ func updateJWTPool(ctx context.Context, errOut io.Writer, secrets typedcorev1.Se if err != nil { return fmt.Errorf("while marshaling pool: %w", err) } + jwks, err := pool.JWKS() + if err != nil { + return fmt.Errorf("while publishing the pool's public keys: %w", err) + } + // Written together so a verifier reading "jwks" never lags "pool". secret.Data["pool"] = wire + secret.Data["jwks"] = jwks if _, err := secrets.Update(ctx, secret, metav1.UpdateOptions{}); err != nil { return fmt.Errorf("while writing pool secret: %w", err) } @@ -324,6 +330,10 @@ func newJWTPoolSecret(namespace, name, algorithm, keyID string) (*corev1.Secret, if err != nil { return nil, "", fmt.Errorf("while generating JWT authority pool: %w", err) } + jwks, err := localjwtauthority.PublicJWKS(poolBytes) + if err != nil { + return nil, "", fmt.Errorf("while publishing the pool's public keys: %w", err) + } return &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ @@ -332,6 +342,7 @@ func newJWTPoolSecret(namespace, name, algorithm, keyID string) (*corev1.Secret, }, Data: map[string][]byte{ "pool": poolBytes, + "jwks": jwks, }, }, id, nil } diff --git a/cmd/kubectl-ate/internal/cmd/admin_test.go b/cmd/kubectl-ate/internal/cmd/admin_test.go index a7a5fe681e..103aad9f11 100644 --- a/cmd/kubectl-ate/internal/cmd/admin_test.go +++ b/cmd/kubectl-ate/internal/cmd/admin_test.go @@ -15,6 +15,7 @@ package cmd import ( + "bytes" "context" "io" "strings" @@ -94,6 +95,9 @@ func poolState(t *testing.T, kc *fake.Clientset) ([]string, string) { if err != nil { t.Fatal(err) } + if jwks, err := pool.JWKS(); err != nil || !bytes.Equal(jwks, secret.Data["jwks"]) { + t.Errorf("jwks = %s, want the pool's public keys (%v)", secret.Data["jwks"], err) + } var ids []string for _, a := range pool.Authorities { ids = append(ids, a.ID) diff --git a/internal/localjwtauthority/jwks.go b/internal/localjwtauthority/jwks.go index 6dd29d5e3c..8f857ba972 100644 --- a/internal/localjwtauthority/jwks.go +++ b/internal/localjwtauthority/jwks.go @@ -42,6 +42,27 @@ type jwk struct { E string `json:"e,omitempty"` } +// JWKS returns the JWK set, as JSON, that publishes the pool's public keys for +// signature verification. It carries no private material, so it is what a +// verifier that cannot use discovery is given instead of the pool itself. +func (p *ConcretePool) JWKS() ([]byte, error) { + keys, err := p.VerificationKeys() + if err != nil { + return nil, err + } + return JWKS(keys) +} + +// PublicJWKS returns the JWK set for a serialized pool, for writers that hold +// the pool only as bytes. +func PublicJWKS(poolBytes []byte) ([]byte, error) { + pool, err := Unmarshal(poolBytes) + if err != nil { + return nil, fmt.Errorf("while reading the pool: %w", err) + } + return pool.JWKS() +} + // JWKS returns the JWK set, as JSON, that publishes keys for signature // verification, sorted by key ID. Each key's algorithm must fit its type: // ES256 for a P-256 EC key, or RS256, RS384, or RS512 for an RSA key. diff --git a/internal/localjwtauthority/jwks_filesource_test.go b/internal/localjwtauthority/jwks_filesource_test.go new file mode 100644 index 0000000000..455e62cbac --- /dev/null +++ b/internal/localjwtauthority/jwks_filesource_test.go @@ -0,0 +1,102 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package localjwtauthority + +import ( + "os" + "path/filepath" + "testing" + "time" + + "github.com/agent-substrate/substrate/internal/actoridjwt" + "github.com/agent-substrate/substrate/internal/oidcjwt" +) + +const fileSourceTestIssuer = "https://idp.ate-system.svc" + +func writeJWKS(t *testing.T, path string, pool *ConcretePool) { + t.Helper() + b, err := pool.JWKS() + if err != nil { + t.Fatalf("JWKS: %v", err) + } + if err := os.WriteFile(path, b, 0o600); err != nil { + t.Fatalf("write JWKS: %v", err) + } +} + +func signTestJWT(t *testing.T, pool *ConcretePool, now time.Time) string { + t.Helper() + jwt, err := pool.SignJWT(&actoridjwt.Claims{ + Kind: actoridjwt.KindWorkerForActor, + Issuer: fileSourceTestIssuer, + Subject: "worker-for-actor/a/b", + Audiences: []string{"aud"}, + Expiration: now.Add(time.Hour), + NotBefore: now.Add(-5 * time.Minute), + IssuedAt: now, + Substrate: actoridjwt.SubstrateClaims{Atespace: "a", ActorName: "b", ActorUID: "u"}, + }) + if err != nil { + t.Fatalf("SignJWT: %v", err) + } + return jwt +} + +// TestJWKS_FeedsFileKeySource proves the pool's published JWKS is what the +// verifier's file source needs: tokens the pool signs verify against it, +// across a rotation, and tokens from a stranger do not. +func TestJWKS_FeedsFileKeySource(t *testing.T) { + first, err := GenerateAuthority("ES256", "first") + if err != nil { + t.Fatal(err) + } + pool := &ConcretePool{Authorities: []*Authority{first}, ActiveForSigning: "first"} + path := filepath.Join(t.TempDir(), "jwks.json") + writeJWKS(t, path, pool) + + now := time.Now() + verifier := oidcjwt.NewVerifier(fileSourceTestIssuer, []string{"aud"}, oidcjwt.FileKeySource(path), oidcjwt.WithSubstrateClaims()) + claims, err := verifier.Verify(t.Context(), signTestJWT(t, pool, now), now) + if err != nil { + t.Fatalf("Verify: %v", err) + } + if claims.Subject != "worker-for-actor/a/b" { + t.Errorf("sub = %q", claims.Subject) + } + if claims.Kind != actoridjwt.KindWorkerForActor || claims.Substrate == nil || claims.Substrate.Atespace != "a" { + t.Errorf("Substrate claims = %+v, want worker-for-actor", claims.SubstrateClaims) + } + + next, err := GenerateAuthority("ES256", "next") + if err != nil { + t.Fatal(err) + } + pool.Authorities = append(pool.Authorities, next) + pool.ActiveForSigning = "next" + writeJWKS(t, path, pool) + if _, err := verifier.Verify(t.Context(), signTestJWT(t, pool, now), now); err != nil { + t.Fatalf("Verify after rotation: %v", err) + } + + stranger, err := GenerateAuthority("ES256", "stranger") + if err != nil { + t.Fatal(err) + } + strangerPool := &ConcretePool{Authorities: []*Authority{stranger}, ActiveForSigning: "stranger"} + if _, err := verifier.Verify(t.Context(), signTestJWT(t, strangerPool, now.Add(2*time.Minute)), now.Add(2*time.Minute)); err == nil { + t.Fatal("Verify of a token from an unknown authority succeeded, want error") + } +} diff --git a/internal/oidcjwt/oidcjwt.go b/internal/oidcjwt/oidcjwt.go index d2e87982ad..cd6ce5e8a4 100644 --- a/internal/oidcjwt/oidcjwt.go +++ b/internal/oidcjwt/oidcjwt.go @@ -23,16 +23,20 @@ import ( "crypto/rsa" "encoding/base64" "encoding/json" + "errors" "fmt" "hash" "io" "log/slog" "math/big" "net/http" + "os" "slices" "strings" "sync" "time" + + "github.com/agent-substrate/substrate/internal/actoridjwt" ) // KeyAndID wraps a crypto.PublicKey and its JWK key ID. @@ -60,6 +64,12 @@ type parseClaims struct { // Kubernetes bound token claims. BoundClaims parseBoundClaims `json:"kubernetes.io,omitempty"` + // Substrate's own claims, under the claim name for the token's kind. Kept + // raw until the signature is verified, so an unverified payload shapes no + // parsing beyond this struct. + Actor json.RawMessage `json:"actor.ate.dev,omitempty"` + WorkerForActor json.RawMessage `json:"worker-for-actor.ate.dev,omitempty"` + // Kubernetes legacy token claims. LegacyNamespace string `json:"kubernetes.io/serviceaccount/namespace,omitempty"` LegacySecretName string `json:"kubernetes.io/serviceaccount/secret.name,omitempty"` @@ -82,7 +92,7 @@ type parseBoundObjectReference struct { } // Claims contains standard JWT claims and, when present, Kubernetes -// ServiceAccount claims. +// ServiceAccount claims and Substrate's own claims. type Claims struct { // Claims from RFC7519 Issuer string @@ -94,6 +104,21 @@ type Claims struct { JTI string KubernetesClaims + SubstrateClaims +} + +// SubstrateClaims are the claims the actor-id JWT issuer adds to the tokens it +// mints. Kind says which claim the token carried, KindActor for the actor's +// own token or KindWorkerForActor for a Substrate component acting on the +// actor's behalf, and Substrate holds it. A token carrying neither has +// KindUnspecified and a nil Substrate. A token carrying both is refused. +// +// Only a Verifier built with WithSubstrateClaims fills these in. Any issuer can +// put the claim names in a token, so a verifier for another issuer leaves them +// alone, whatever they hold. +type SubstrateClaims struct { + Kind actoridjwt.Kind + Substrate *actoridjwt.WireSubstrateClaims } // KubernetesClaims contains claims added to Kubernetes ServiceAccount tokens. @@ -119,13 +144,38 @@ var ( defaultHTTPClient = &http.Client{Timeout: 10 * time.Second} ) +// KeySource supplies the public keys a Verifier checks signatures with. The +// Verifier calls Keys on first use, every jwksRefreshInterval, and once per +// jwksRefreshRetryInterval when a token names a key ID it does not have, so a +// source may read its backing store on every call. +type KeySource interface { + Keys(ctx context.Context) ([]*KeyAndID, error) +} + +type httpKeySource struct { + issuer string + client *http.Client +} + +func (s httpKeySource) Keys(ctx context.Context) ([]*KeyAndID, error) { + return discoverKeysForIssuer(ctx, s.client, s.issuer) +} + +// HTTPKeySource fetches the issuer's keys through OpenID Connect Discovery: +// /.well-known/openid-configuration, then its jwks_uri. +func HTTPKeySource(issuer string, client *http.Client) KeySource { + return httpKeySource{issuer: issuer, client: client} +} + // Verifier verifies JWTs from one trusted OIDC issuer and caches its signing // keys. Keys are refreshed periodically and, at a bounded rate, when a token // names an unknown key ID. type Verifier struct { - issuer string - audiences []string - httpClient *http.Client + issuer string + audiences []string + source KeySource + // substrate says whether Verify decodes Substrate's claims. + substrate bool mu sync.RWMutex keys []*KeyAndID @@ -134,10 +184,50 @@ type Verifier struct { lastUnknownKeyRefresh time.Time } +type fileKeySource struct { + path string +} + +func (s fileKeySource) Keys(ctx context.Context) ([]*KeyAndID, error) { + b, err := os.ReadFile(s.path) + if err != nil { + return nil, fmt.Errorf("while reading JWKS file: %w", err) + } + var set jwkSetT + if err := json.Unmarshal(b, &set); err != nil { + return nil, fmt.Errorf("while parsing JWKS file %s: %w", s.path, err) + } + return keysFromJWKSet(ctx, set, "file "+s.path) +} + +// FileKeySource reads a JWK set from a file, for a verifier that is handed the +// issuer's public keys directly rather than fetching them over discovery. The +// file is re-read on every call, so a rotated set is picked up on the +// verifier's next refresh. For that to work the file must be replaced +// atomically, as a projected volume mounted without subPath is: an in-place +// rewrite can be read half-written, and a subPath mount never sees the +// rotation. The file holds public keys only. +func FileKeySource(path string) KeySource { + return fileKeySource{path: path} +} + +// Option configures a Verifier. +type Option func(*Verifier) + +// WithSubstrateClaims makes Verify decode Substrate's claims into +// Claims.SubstrateClaims and refuse a token that carries both or a malformed +// one. It is for the verifier of the actor-id JWT issuer, whose tokens are the +// only ones those claims mean anything on. +func WithSubstrateClaims() Option { return func(v *Verifier) { v.substrate = true } } + // NewVerifier returns a verifier for issuer. A token is accepted when at least // one of its audiences matches audiences. -func NewVerifier(issuer string, audiences []string, httpClient *http.Client) *Verifier { - return &Verifier{issuer: issuer, audiences: slices.Clone(audiences), httpClient: httpClient} +func NewVerifier(issuer string, audiences []string, source KeySource, opts ...Option) *Verifier { + v := &Verifier{issuer: issuer, audiences: slices.Clone(audiences), source: source} + for _, o := range opts { + o(v) + } + return v } // Verify verifies and extracts claims from a JWT. @@ -246,14 +336,22 @@ func (v *Verifier) Verify(ctx context.Context, jwt string, now time.Time) (*Clai return nil, fmt.Errorf("jwt claims to have been issued in the future") } + var substrate SubstrateClaims + if v.substrate { + if substrate, err = decodeSubstrateClaims(rawClaims); err != nil { + return nil, err + } + } + return &Claims{ - Issuer: rawClaims.Issuer, - Audiences: audiences, - Subject: rawClaims.Subject, - Expiration: expiration, - NotBefore: notBefore, - IssuedAt: issuedAt, - JTI: rawClaims.JTI, + Issuer: rawClaims.Issuer, + Audiences: audiences, + Subject: rawClaims.Subject, + Expiration: expiration, + NotBefore: notBefore, + IssuedAt: issuedAt, + JTI: rawClaims.JTI, + SubstrateClaims: substrate, KubernetesClaims: KubernetesClaims{ Namespace: rawClaims.BoundClaims.Namespace, ServiceAccountName: rawClaims.BoundClaims.ServiceAccount.Name, @@ -269,6 +367,41 @@ func (v *Verifier) Verify(ctx context.Context, jwt string, now time.Time) (*Clai }, nil } +// decodeSubstrateClaims parses Substrate's claims from a verified payload and +// applies actoridjwt's rule that a token carries one of them or neither. +func decodeSubstrateClaims(raw parseClaims) (SubstrateClaims, error) { + wire := actoridjwt.WireClaims{} + var err error + if wire.Actor, err = decodeSubstrateClaim("actor.ate.dev", raw.Actor); err != nil { + return SubstrateClaims{}, err + } + if wire.WorkerForActor, err = decodeSubstrateClaim("worker-for-actor.ate.dev", raw.WorkerForActor); err != nil { + return SubstrateClaims{}, err + } + if wire.Actor == nil && wire.WorkerForActor == nil { + return SubstrateClaims{}, nil + } + kind, claims, ok := wire.Substrate() + if !ok { + return SubstrateClaims{}, errors.New("token carries both actor.ate.dev and worker-for-actor.ate.dev") + } + return SubstrateClaims{Kind: kind, Substrate: claims}, nil +} + +// decodeSubstrateClaim parses one of Substrate's claims. An absent or null +// claim is nil. A present claim that is not an object is an error, since a +// trusted issuer never emits one. +func decodeSubstrateClaim(name string, raw json.RawMessage) (*actoridjwt.WireSubstrateClaims, error) { + if len(raw) == 0 || string(raw) == "null" { + return nil, nil + } + var claim actoridjwt.WireSubstrateClaims + if err := json.Unmarshal(raw, &claim); err != nil { + return nil, fmt.Errorf("while parsing the %s claim: %w", name, err) + } + return &claim, nil +} + func (v *Verifier) key(ctx context.Context, keyID string, now time.Time) (crypto.PublicKey, error) { v.mu.RLock() key := findKey(v.keys, keyID) @@ -308,14 +441,14 @@ func (v *Verifier) key(ctx context.Context, keyID string, now time.Time) (crypto if key == nil && len(v.keys) > 0 { v.lastUnknownKeyRefresh = now } - keys, err := discoverKeysForIssuer(ctx, v.httpClient, v.issuer) + keys, err := v.source.Keys(ctx) if err != nil { v.lastFailedRefresh = now if key != nil { - slog.WarnContext(ctx, "Could not refresh OIDC signing keys; using cached key", slog.String("issuer", v.issuer), slog.Any("err", err)) + slog.WarnContext(ctx, "Could not refresh JWT signing keys; using cached key", slog.String("issuer", v.issuer), slog.Any("err", err)) return key.PublicKey, nil } - return nil, fmt.Errorf("while discovering keys from issuer: %w", err) + return nil, fmt.Errorf("while loading signing keys for issuer: %w", err) } v.keys = keys v.lastRefresh = now @@ -480,17 +613,24 @@ func discoverKeysForIssuer(ctx context.Context, httpClient *http.Client, issuer slog.InfoContext(ctx, "Fetched JWK set", slog.Any("jwkSet", jwkSet)) + return keysFromJWKSet(ctx, jwkSet, "issuer "+issuer) +} + +// keysFromJWKSet converts a JWK set into verification keys. origin names where +// the set came from, for errors and logs. +func keysFromJWKSet(ctx context.Context, jwkSet jwkSetT, origin string) ([]*KeyAndID, error) { var ret []*KeyAndID skipped := 0 for _, jwk := range jwkSet.Keys { key, err := parseJWK(jwk) if err != nil { - // Skip an unusable key instead of failing the whole issuer; it's safe because - // keys are selected by kid and the signature is still verified, so a skipped - // key can't be abused. Debug, not Warn: unsupported key types are a normal - // issuer configuration and periodic refreshes would otherwise spam warnings. - slog.DebugContext(ctx, "Skipping unusable JWK from issuer", - slog.String("kid", jwk.KeyID), slog.String("kty", jwk.KeyType), slog.Any("err", err)) + // Skip an unusable key instead of failing the whole set. That is safe + // because keys are selected by kid and the signature is still verified, + // so a skipped key can't be abused. Debug, not Warn: unsupported key + // types are a normal issuer configuration and periodic refreshes would + // otherwise spam warnings. + slog.DebugContext(ctx, "Skipping unusable JWK", + slog.String("origin", origin), slog.String("kid", jwk.KeyID), slog.String("kty", jwk.KeyType), slog.Any("err", err)) skipped++ continue } @@ -501,14 +641,14 @@ func discoverKeysForIssuer(ctx context.Context, httpClient *http.Client, issuer // that fails later as a vaguer "unknown key ID". if len(ret) == 0 { if len(jwkSet.Keys) == 0 { - return nil, fmt.Errorf("issuer %q published an empty JWKS", issuer) + return nil, fmt.Errorf("%s published an empty JWKS", origin) } - return nil, fmt.Errorf("no usable keys in JWKS for issuer %q (%d skipped)", issuer, skipped) + return nil, fmt.Errorf("no usable keys in JWKS from %s (%d skipped)", origin, skipped) } if skipped > 0 { - slog.DebugContext(ctx, "Skipped unusable JWKs from issuer", - slog.String("issuer", issuer), slog.Int("skipped", skipped), slog.Int("usable", len(ret))) + slog.DebugContext(ctx, "Skipped unusable JWKs", + slog.String("origin", origin), slog.Int("skipped", skipped), slog.Int("usable", len(ret))) } return ret, nil diff --git a/internal/oidcjwt/oidcjwt_test.go b/internal/oidcjwt/oidcjwt_test.go index 28e97cf536..91027b85d4 100644 --- a/internal/oidcjwt/oidcjwt_test.go +++ b/internal/oidcjwt/oidcjwt_test.go @@ -28,11 +28,15 @@ import ( "math/big" "net/http" "net/http/httptest" + "os" + "path/filepath" "strings" "sync" "sync/atomic" "testing" "time" + + "github.com/agent-substrate/substrate/internal/actoridjwt" ) const testAudience = "ate-api" @@ -76,7 +80,7 @@ func TestVerifierRetriesInitialDiscoveryFailure(t *testing.T) { key := testRSAKey(t) ti.addRSA("key", &key.PublicKey) ti.jwksFailures.Store(1) - verifier := NewVerifier(ti.issuer(), []string{testAudience}, ti.server.Client()) + verifier := NewVerifier(ti.issuer(), []string{testAudience}, HTTPKeySource(ti.issuer(), ti.server.Client())) token := mintJWT(t, "RS256", "key", key, validClaims(ti.issuer())) now := time.Now() @@ -98,7 +102,7 @@ func TestVerifierUsesCachedKeyWhenRefreshFails(t *testing.T) { ti := newTestIssuer(t) key := testRSAKey(t) ti.addRSA("key", &key.PublicKey) - verifier := NewVerifier(ti.issuer(), []string{testAudience}, ti.server.Client()) + verifier := NewVerifier(ti.issuer(), []string{testAudience}, HTTPKeySource(ti.issuer(), ti.server.Client())) token := mintJWT(t, "RS256", "key", key, validClaims(ti.issuer())) now := time.Now() @@ -128,7 +132,7 @@ func TestVerifierReturnsKubernetesClaims(t *testing.T) { } token := mintJWT(t, "RS256", "key", key, claims) - got, err := NewVerifier(ti.issuer(), []string{testAudience}, ti.server.Client()).Verify(t.Context(), token, time.Now()) + got, err := NewVerifier(ti.issuer(), []string{testAudience}, HTTPKeySource(ti.issuer(), ti.server.Client())).Verify(t.Context(), token, time.Now()) if err != nil { t.Fatal(err) } @@ -141,7 +145,7 @@ func TestVerifierCachesAndRefreshesKeys(t *testing.T) { ti := newTestIssuer(t) key1 := testRSAKey(t) ti.addRSA("key-1", &key1.PublicKey) - verifier := NewVerifier(ti.issuer(), []string{"other", testAudience}, ti.server.Client()) + verifier := NewVerifier(ti.issuer(), []string{"other", testAudience}, HTTPKeySource(ti.issuer(), ti.server.Client())) now := time.Now() token1 := mintJWT(t, "RS256", "key-1", key1, validClaims(ti.issuer())) @@ -359,7 +363,7 @@ func TestVerifyECDSA(t *testing.T) { ti.addEC(t, "ec-1", tc.crv, &key.PublicKey) tok := mintJWT(t, tc.alg, "ec-1", key, validClaims(ti.issuer())) - if _, err := NewVerifier(ti.issuer(), []string{testAudience}, ti.server.Client()).Verify(context.Background(), tok, time.Now()); err != nil { + if _, err := NewVerifier(ti.issuer(), []string{testAudience}, HTTPKeySource(ti.issuer(), ti.server.Client())).Verify(context.Background(), tok, time.Now()); err != nil { t.Fatalf("Verify(%s) = %v, want nil", tc.alg, err) } }) @@ -379,7 +383,7 @@ func TestVerifyRejectsECKeyForRSAlg(t *testing.T) { // RS256 header pointing at the EC key; the RSA signing key is irrelevant because // the key-type check fails before signature verification. tok := mintJWT(t, "RS256", "ec-1", testRSAKey(t), validClaims(ti.issuer())) - if _, err := NewVerifier(ti.issuer(), []string{testAudience}, ti.server.Client()).Verify(context.Background(), tok, time.Now()); err == nil { + if _, err := NewVerifier(ti.issuer(), []string{testAudience}, HTTPKeySource(ti.issuer(), ti.server.Client())).Verify(context.Background(), tok, time.Now()); err == nil { t.Fatal("Verify accepted an RS256 token whose kid names an EC key") } } @@ -398,7 +402,7 @@ func TestVerifyMixedJWKS(t *testing.T) { ti.addEC(t, "ec-1", "P-256", &ecKey.PublicKey) tok := mintJWT(t, "RS256", "rsa-1", rsaKey, validClaims(ti.issuer())) - if _, err := NewVerifier(ti.issuer(), []string{testAudience}, ti.server.Client()).Verify(context.Background(), tok, time.Now()); err != nil { + if _, err := NewVerifier(ti.issuer(), []string{testAudience}, HTTPKeySource(ti.issuer(), ti.server.Client())).Verify(context.Background(), tok, time.Now()); err != nil { t.Fatalf("Verify with a mixed RSA+EC JWKS = %v, want nil", err) } } @@ -417,12 +421,12 @@ func TestVerifyUnusableJWKSKeySkipped(t *testing.T) { }) good := mintJWT(t, "RS256", "rsa-1", rsaKey, validClaims(ti.issuer())) - if _, err := NewVerifier(ti.issuer(), []string{testAudience}, ti.server.Client()).Verify(context.Background(), good, time.Now()); err != nil { + if _, err := NewVerifier(ti.issuer(), []string{testAudience}, HTTPKeySource(ti.issuer(), ti.server.Client())).Verify(context.Background(), good, time.Now()); err != nil { t.Fatalf("Verify with an unusable key in the JWKS = %v, want nil (bad key should be skipped)", err) } referencesSkipped := mintJWT(t, "RS256", "ec-bad", rsaKey, validClaims(ti.issuer())) - if _, err := NewVerifier(ti.issuer(), []string{testAudience}, ti.server.Client()).Verify(context.Background(), referencesSkipped, time.Now()); err == nil { + if _, err := NewVerifier(ti.issuer(), []string{testAudience}, HTTPKeySource(ti.issuer(), ti.server.Client())).Verify(context.Background(), referencesSkipped, time.Now()); err == nil { t.Fatal("Verify accepted a token whose kid names a key that was skipped") } } @@ -489,3 +493,173 @@ func TestEllipticCurveForJWK(t *testing.T) { t.Error("ellipticCurveForJWK(P-192) = nil, want error") } } + +// writeJWKSFile writes the issuer's current JWK set to a file and returns the +// path, standing in for the public key file a signer publishes next to its +// pool. +func writeJWKSFile(t *testing.T, ti *testIssuer) string { + t.Helper() + b, err := json.Marshal(ti.jwks) + if err != nil { + t.Fatal(err) + } + path := filepath.Join(t.TempDir(), "jwks.json") + if err := os.WriteFile(path, b, 0o600); err != nil { + t.Fatal(err) + } + return path +} + +func TestFileKeySource(t *testing.T) { + ti := newTestIssuer(t) + key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + t.Fatal(err) + } + ti.addEC(t, "ec-1", "P-256", &key.PublicKey) + path := writeJWKSFile(t, ti) + + v := NewVerifier(ti.issuer(), []string{testAudience}, FileKeySource(path)) + if _, err := v.Verify(context.Background(), mintJWT(t, "ES256", "ec-1", key, validClaims(ti.issuer())), time.Now()); err != nil { + t.Fatalf("Verify = %v, want nil", err) + } + if ti.jwksRequests.Load() != 0 { + t.Errorf("the file source fetched the JWKS over HTTP %d times", ti.jwksRequests.Load()) + } + + // Rotation: a key added to the file is found on the unknown-kid refresh. + next, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + t.Fatal(err) + } + ti.addEC(t, "ec-2", "P-256", &next.PublicKey) + b, _ := json.Marshal(ti.jwks) + if err := os.WriteFile(path, b, 0o600); err != nil { + t.Fatal(err) + } + if _, err := v.Verify(context.Background(), mintJWT(t, "ES256", "ec-2", next, validClaims(ti.issuer())), time.Now()); err != nil { + t.Fatalf("Verify after rotation = %v, want nil", err) + } +} + +func TestFileKeySourceErrors(t *testing.T) { + dir := t.TempDir() + empty := filepath.Join(dir, "empty.json") + if err := os.WriteFile(empty, []byte(`{"keys":[]}`), 0o600); err != nil { + t.Fatal(err) + } + garbage := filepath.Join(dir, "garbage.json") + if err := os.WriteFile(garbage, []byte(`not json`), 0o600); err != nil { + t.Fatal(err) + } + for name, path := range map[string]string{ + "missing file": filepath.Join(dir, "absent.json"), + "empty set": empty, + "not JSON": garbage, + } { + t.Run(name, func(t *testing.T) { + if _, err := FileKeySource(path).Keys(context.Background()); err == nil { + t.Fatal("Keys succeeded, want error") + } + }) + } +} + +func TestVerifySubstrateClaims(t *testing.T) { + ti := newTestIssuer(t) + key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + t.Fatal(err) + } + ti.addEC(t, "ec-1", "P-256", &key.PublicKey) + v := NewVerifier(ti.issuer(), []string{testAudience}, HTTPKeySource(ti.issuer(), ti.server.Client()), WithSubstrateClaims()) + + withClaim := func(name string, value any) map[string]any { + c := validClaims(ti.issuer()) + c[name] = value + return c + } + for _, tc := range []struct { + name string + claims map[string]any + wantErr string + check func(*Claims) bool + }{ + {"absent", validClaims(ti.issuer()), "", func(c *Claims) bool { return c.Kind == actoridjwt.KindUnspecified && c.Substrate == nil }}, + {"actor", withClaim("actor.ate.dev", map[string]string{"atespace": "a", "actorName": "b", "actorUID": "u"}), "", + func(c *Claims) bool { + return c.Kind == actoridjwt.KindActor && c.Substrate != nil && c.Substrate.ActorUID == "u" + }}, + {"worker-for-actor", withClaim("worker-for-actor.ate.dev", map[string]string{"atespace": "a"}), "", + func(c *Claims) bool { + return c.Kind == actoridjwt.KindWorkerForActor && c.Substrate != nil && c.Substrate.Atespace == "a" + }}, + {"not an object", withClaim("actor.ate.dev", "junk"), "actor.ate.dev", nil}, + {"null", withClaim("actor.ate.dev", nil), "", func(c *Claims) bool { return c.Kind == actoridjwt.KindUnspecified && c.Substrate == nil }}, + {"both", func() map[string]any { + c := withClaim("actor.ate.dev", map[string]string{"atespace": "a"}) + c["worker-for-actor.ate.dev"] = map[string]string{"atespace": "a"} + return c + }(), "both", nil}, + } { + t.Run(tc.name, func(t *testing.T) { + got, err := v.Verify(context.Background(), mintJWT(t, "ES256", "ec-1", key, tc.claims), time.Now()) + if tc.wantErr != "" { + if err == nil || !strings.Contains(err.Error(), tc.wantErr) { + t.Fatalf("err = %v, want it to mention %q", err, tc.wantErr) + } + return + } + if err != nil { + t.Fatalf("Verify = %v", err) + } + if !tc.check(got) { + t.Errorf("claims = %+v", got.SubstrateClaims) + } + }) + } +} + +func TestVerifyIgnoresSubstrateClaimsOnBadSignature(t *testing.T) { + // A malformed Substrate claim on a token with a bad signature must fail on + // the signature, proving the claim is not parsed before verification. + ti := newTestIssuer(t) + key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + t.Fatal(err) + } + ti.addEC(t, "ec-1", "P-256", &key.PublicKey) + stranger, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + t.Fatal(err) + } + claims := validClaims(ti.issuer()) + claims["actor.ate.dev"] = "junk" + v := NewVerifier(ti.issuer(), []string{testAudience}, HTTPKeySource(ti.issuer(), ti.server.Client()), WithSubstrateClaims()) + _, err = v.Verify(context.Background(), mintJWT(t, "ES256", "ec-1", stranger, claims), time.Now()) + if err == nil || !strings.Contains(err.Error(), "signature") || strings.Contains(err.Error(), "actor.ate.dev") { + t.Fatalf("err = %v, want a signature error that does not mention the claim", err) + } +} + +func TestVerifyLeavesSubstrateClaimsAloneByDefault(t *testing.T) { + // A verifier for another issuer must not fail on, or expose, whatever that + // issuer put under Substrate's claim names. + ti := newTestIssuer(t) + key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + t.Fatal(err) + } + ti.addEC(t, "ec-1", "P-256", &key.PublicKey) + v := NewVerifier(ti.issuer(), []string{testAudience}, HTTPKeySource(ti.issuer(), ti.server.Client())) + claims := validClaims(ti.issuer()) + claims["actor.ate.dev"] = "junk" + claims["worker-for-actor.ate.dev"] = map[string]string{"atespace": "a"} + got, err := v.Verify(context.Background(), mintJWT(t, "ES256", "ec-1", key, claims), time.Now()) + if err != nil { + t.Fatalf("Verify = %v, want nil", err) + } + if got.Kind != actoridjwt.KindUnspecified || got.Substrate != nil { + t.Errorf("Substrate claims = %+v, want none", got.SubstrateClaims) + } +} From 0c2776a6db8225c9acf777be364fac0ebbda0f13 Mon Sep 17 00:00:00 2001 From: Davanum Srinivas Date: Thu, 15 Oct 2026 16:32:09 +0000 Subject: [PATCH 02/31] hack: ATE_KIND_RESERVED_CPU reserves kubelet and system CPU on kind nodes (#2495) Dense nodes starve the kubelet's heartbeat; after the 300 s toleration the taint manager evicts the node's pods. - `ATE_KIND_RESERVED_CPU=` adds `systemReserved.cpu` and `kubeReserved.cpu` to the kind `KubeletConfiguration` patch. Unset, nothing changes. Not verified at the density that triggered it. `shellcheck` clean. --- hack/create-kind-cluster.sh | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/hack/create-kind-cluster.sh b/hack/create-kind-cluster.sh index 9d5a0e42e5..e908917121 100755 --- a/hack/create-kind-cluster.sh +++ b/hack/create-kind-cluster.sh @@ -29,8 +29,9 @@ if [[ $# -gt 0 ]]; then echo "Creates the kind cluster '${KIND_CLUSTER_NAME}' and a local registry container on port ${reg_port}." echo echo "Configured through the environment:" - echo " KIND_CLUSTER_NAME Name of the cluster to create (default: kind)." - echo " IP_FAMILY Address families for pods and Services: ipv4, ipv6 or dual (default: ipv4)." + echo " KIND_CLUSTER_NAME Name of the cluster to create (default: kind)." + echo " IP_FAMILY Address families for pods and Services: ipv4, ipv6 or dual (default: ipv4)." + echo " ATE_KIND_RESERVED_CPU CPU reserved for the kubelet and the system on each node (default: none)." exit 0 ;; esac @@ -124,6 +125,17 @@ kubeadmConfigPatches: serializeImagePulls: false maxParallelImagePulls: 4 EOF +# Hundreds of sandboxes per node can starve the kubelet's node heartbeat; after +# the 300 s toleration the taint manager evicts every pod on the node. Opt in to +# a CPU reservation for the kubelet and the system, e.g. ATE_KIND_RESERVED_CPU=2. +if [[ -n "${ATE_KIND_RESERVED_CPU:-}" ]]; then +cat <> "${ROOT}/bin/kind-config.yaml" + systemReserved: + cpu: "${ATE_KIND_RESERVED_CPU}" + kubeReserved: + cpu: "${ATE_KIND_RESERVED_CPU}" +EOF +fi echo "Deleting existing kind cluster '${KIND_CLUSTER_NAME}' if it exists..." "${ROOT}"/hack/kind.sh delete cluster --name "${KIND_CLUSTER_NAME}" || true From 35c6ab2aa0cfad999127048d557b405de56f7ff8 Mon Sep 17 00:00:00 2001 From: igooch Date: Thu, 15 Oct 2026 16:42:15 +0000 Subject: [PATCH 03/31] ate-api-server applies atelet's ReportActorSnapshotState reports (#2622) Part of Actor Lifecycle v2 #2069. Second PR for #2570, after #2612 added the API. This serves `WorkerService.ReportActorSnapshotState`. With asynchronous suspend, atelet uploads a suspended actor's snapshot and later detaches the actor from its node, on its own timers. The report is how ate-api-server learns about both. Nothing sends reports yet; that comes with atelet's asynchronous upload work. A report lists the snapshots atelet holds for an actor on its node. ate-api-server acts on two things in it: - **`latest_durable` means an upload finished.** If it names a recorded snapshot with the same uuid and upload URI, that snapshot becomes `COMPLETED`, with its manifest's fidelity. - **A report with neither `latest_local` nor `uploading` means atelet deleted its copies (a detach).** It covers this node's snapshots up to the reported `latest_durable` generation. Those localities are cleared, and a suspended actor with no snapshot left on the node is released from it. ate-api-server ignores the rest of the report.
Flowchart: how a report is applied ```mermaid flowchart TD A["WorkerService.ReportActorSnapshotState(ReportActorSnapshotStateRequest)
TODO: atelet does not send it yet"] --> B{"Caller presents atelet's certificate?"} B -- no --> X1["Unauthenticated or PermissionDenied"] B -- yes --> C{"Request passes validation?"} C -- no --> X2["InvalidArgument, including a report with none of
latest_local, uploading, and latest_durable"] C -- yes --> E{"Actor exists, actor_uid equals Actor.metadata.uid,
and Actor is on the caller's node?"} E -- no --> X4["NotFound"] E -- yes --> D{"Actor's lease free?"} D -- no --> X3["Aborted, atelet retries"] D -- yes --> E2{"Same check again, under the lease"} E2 -- no --> X4 E2 -- yes --> S{"status.state?"} S -- "CRASHED" --> X9["FailedPrecondition"] S -- "SUSPENDING, RESUMING,
DELETING, REVERTING" --> X10["Aborted, atelet retries"] S -- "any other state" --> X12["Internal"] S -- "SUSPENDED or RUNNING" --> G{"latest_durable set?"} G -- no --> K{"latest_local and uploading both unset?"} G -- yes --> GF{"At or below the latest COMPLETED generation?"} GF -- yes --> K GF -- no --> GR{"Its generation is in status.snapshots?"} GR -- "no, and not newer than
last_assigned_generation" --> K GR -- "no, and newer" --> X5["FailedPrecondition"] GR -- yes --> U{"Same uuid, and an upload URI is recorded?"} U -- no --> X6["FailedPrecondition"] U -- yes --> V{"Reported URI equals the recorded one?"} V -- no --> X7["InvalidArgument"] V -- yes --> H["Mark it COMPLETED"] H --> K K -- yes --> K2{"Latest snapshot on this node, up to
latest_durable's generation, is durable?"} K2 -- no --> X8["FailedPrecondition, nothing recorded"] K2 -- yes --> J["Detach: clear this node's localities up to that generation.
Clear assigned_node if status.state is SUSPENDED, it is this node,
and no snapshot is left on this node"] K -- no --> P{"status changed?"} J --> P P -- no --> Q["Done, nothing written"] P -- yes --> R["Write the actor"] ```
## Outcomes | Report | Result | | --- | --- | | Any report for a `CRASHED` actor | `FailedPrecondition` | | Any report for a `SUSPENDING`, `RESUMING`, `DELETING`, or `REVERTING` actor | `Aborted`, retried | | `latest_durable` for a recorded snapshot with matching uuid and upload URI | That snapshot becomes `COMPLETED`, with the fidelity its manifest records | | `latest_durable` at or below the latest `COMPLETED` generation | No change | | `latest_durable` for a generation `status.snapshots` no longer has, such as one a revert discarded | No change | | `latest_durable` for an unrecorded generation newer than any assigned | `FailedPrecondition` | | `latest_durable` with another uuid, or for a snapshot with no recorded upload URI | `FailedPrecondition` | | `latest_durable` uploaded to a URI other than the recorded one | `InvalidArgument` | | None of `latest_local`, `uploading`, and `latest_durable` set | `InvalidArgument` | | Neither `latest_local` nor `uploading` set (a detach) | Clears this node's localities up to the `latest_durable` generation. Clears `assigned_node` if the actor is `SUSPENDED` there and no snapshot is left on this node. Refused with `FailedPrecondition` while the latest covered snapshot isn't durable | | A late detach, after a newer snapshot on this node | Leaves the newer snapshot and `assigned_node` alone | | A detach that's refused | Nothing is recorded, including an upload the same report carries | | A report that changes nothing | Nothing is written | ## Notes - **Nothing is pruned or deleted.** The existing workflows prune old snapshots and delete their objects at the next suspend or delete. - **A detach covers only generations up to its `latest_durable`,** so a late or repeated report can't clear a newer snapshot. Repeating any report is safe. - **A detach waits for a durable snapshot.** Clearing the locality of a snapshot that isn't durable would let the actor resume from an older one. - **A report applies as a whole or not at all.** atelet's detach uploads the latest snapshot first, so a refused detach is rare. - **The actor is checked before the lease is taken,** and again under it, so an atelet can't hold the lease of an actor on another node. - **Reports apply only to a `SUSPENDED` or `RUNNING` actor.** A transitioning actor's snapshots belong to its workflow, and a `CRASHED` actor's wait for a revert. - **Every UID or node refusal is `NotFound`,** so an atelet learns nothing about actors on other nodes. - [ ] Tests pass - [ ] Appropriate changes to documentation are included in the PR --- cmd/ateapi/internal/apivalidation/worker.go | 8 +- .../internal/apivalidation/worker_test.go | 8 +- .../internal/controlapi/snapshot_report.go | 204 ++++++++ .../controlapi/snapshot_report_test.go | 439 ++++++++++++++++++ cmd/ateapi/internal/workerservice/report.go | 43 ++ .../internal/workerservice/report_test.go | 106 +++++ cmd/ateapi/internal/workerservice/service.go | 2 + .../internal/workerservice/service_test.go | 17 + pkg/proto/ateapipb/ateapi.proto | 22 +- pkg/proto/ateapipb/ateapi_grpc.pb.go | 40 +- 10 files changed, 853 insertions(+), 36 deletions(-) create mode 100644 cmd/ateapi/internal/controlapi/snapshot_report.go create mode 100644 cmd/ateapi/internal/controlapi/snapshot_report_test.go create mode 100644 cmd/ateapi/internal/workerservice/report.go create mode 100644 cmd/ateapi/internal/workerservice/report_test.go diff --git a/cmd/ateapi/internal/apivalidation/worker.go b/cmd/ateapi/internal/apivalidation/worker.go index c56056a361..b13769cfe5 100644 --- a/cmd/ateapi/internal/apivalidation/worker.go +++ b/cmd/ateapi/internal/apivalidation/worker.go @@ -96,7 +96,13 @@ func ValidateMarkWorkerUnavailableRequest(ctx context.Context, req *ateapipb.Mar func ValidateReportActorSnapshotStateRequest(ctx context.Context, req *ateapipb.ReportActorSnapshotStateRequest) field.ErrorList { op := operation.Operation{Type: operation.Create} - return Validate_ReportActorSnapshotStateRequest(ctx, op, nil, req, nil) + errs := Validate_ReportActorSnapshotStateRequest(ctx, op, nil, req, nil) + // A report with neither latest_local nor uploading is a detach, which + // names the snapshot it made durable. + if st := req.GetState(); st != nil && st.GetLatestLocal() == nil && st.GetUploading() == nil && st.GetLatestDurable() == nil { + errs = append(errs, field.Required(field.NewPath("state", "latest_durable"), "a report with neither latest_local nor uploading must set latest_durable")) + } + return errs } // validateWorkerUpdate validates a Worker against the previous stored value. diff --git a/cmd/ateapi/internal/apivalidation/worker_test.go b/cmd/ateapi/internal/apivalidation/worker_test.go index 30c246ebdc..13e868fbe3 100644 --- a/cmd/ateapi/internal/apivalidation/worker_test.go +++ b/cmd/ateapi/internal/apivalidation/worker_test.go @@ -697,8 +697,14 @@ func TestValidateReportActorSnapshotStateRequest(t *testing.T) { r.State.UploadDueTime = nil }), }, { - name: "valid empty state", + name: "empty state", req: valid(func(r *ateapipb.ReportActorSnapshotStateRequest) { r.State = &ateapipb.ActorSnapshotState{} }), + want: field.ErrorList{field.Required(field.NewPath("state", "latest_durable"), "")}, + }, { + name: "valid upload in flight with no local snapshot", + req: valid(func(r *ateapipb.ReportActorSnapshotStateRequest) { + r.State = &ateapipb.ActorSnapshotState{Uploading: r.State.GetUploading()} + }), }, { name: "missing actor", req: valid(func(r *ateapipb.ReportActorSnapshotStateRequest) { r.Actor = nil }), diff --git a/cmd/ateapi/internal/controlapi/snapshot_report.go b/cmd/ateapi/internal/controlapi/snapshot_report.go new file mode 100644 index 0000000000..8efd8a3fa8 --- /dev/null +++ b/cmd/ateapi/internal/controlapi/snapshot_report.go @@ -0,0 +1,204 @@ +// 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 controlapi + +import ( + "context" + "errors" + "log/slog" + + "google.golang.org/protobuf/proto" + + "github.com/agent-substrate/substrate/cmd/ateapi/internal/store" + "github.com/agent-substrate/substrate/internal/apierror" + "github.com/agent-substrate/substrate/internal/preview" + "github.com/agent-substrate/substrate/internal/resources" + "github.com/agent-substrate/substrate/pkg/proto/ateapipb" +) + +// ApplyActorSnapshotState records the snapshot state that the atelet on node +// reports for the Actor. The caller has authenticated that atelet. +func (s *RPCService) ApplyActorSnapshotState(ctx context.Context, actorRef resources.ActorRef, actorUID, node string, state *ateapipb.ActorSnapshotState) error { + setSpanActorRefAttributes(ctx, actorRef) + return s.actorWorkflow.ApplyReportedSnapshotState(ctx, actorRef, actorUID, node, state) +} + +// ApplyReportedSnapshotState applies a report from the atelet on node to the +// Actor. It holds the Actor's lease and applies reports only to a SUSPENDED or +// RUNNING Actor, so a report never interleaves with a workflow. A report that +// changes nothing writes nothing. +func (w *ActorWorkflow) ApplyReportedSnapshotState(ctx context.Context, actorRef resources.ActorRef, actorUID, node string, state *ateapipb.ActorSnapshotState) error { + ctx = preview.ContextWithOptIn(ctx) + // Check before taking the lease, so an atelet can't hold the lease of an + // Actor that isn't on its node. Check again under the lease. + if _, err := w.loadReportedActor(ctx, actorRef, actorUID, node); err != nil { + return err + } + leaseCtx, lease, err := w.acquireActorLease(ctx, actorRef) + if err != nil { + return err + } + defer lease.Close() + before, err := w.loadReportedActor(leaseCtx, actorRef, actorUID, node) + if err != nil { + return err + } + + // A workflow owns a transitioning Actor's snapshots, even after its lease + // lapses. A crashed Actor's snapshots wait for a revert. + switch st := before.GetStatus().GetState(); st { + case ateapipb.ActorState_ACTOR_STATE_SUSPENDED, ateapipb.ActorState_ACTOR_STATE_RUNNING: + case ateapipb.ActorState_ACTOR_STATE_CRASHED: + return apierror.FailedPrecondition("actor %s is %s; revert it first", actorRef, st) + case ateapipb.ActorState_ACTOR_STATE_SUSPENDING, ateapipb.ActorState_ACTOR_STATE_RESUMING, + ateapipb.ActorState_ACTOR_STATE_DELETING, ateapipb.ActorState_ACTOR_STATE_REVERTING: + return apierror.Aborted("actor %s is %s; retry once that finishes", actorRef, st) + default: + return apierror.Internal("actor %s has unexpected state %s", actorRef, st) + } + + applied := proto.Clone(before).(*ateapipb.Actor) + if err := applyActorSnapshotState(applied.GetStatus(), node, state); err != nil { + return err + } + if proto.Equal(applied.GetStatus(), before.GetStatus()) { + return nil + } + if _, err := w.store.UpdateActor(leaseCtx, actorRef, store.PreconditionFrom(before), func(toUpdate *ateapipb.Actor) error { + toUpdate.Status = applied.GetStatus() + return nil + }); err != nil { + if errors.Is(err, store.ErrVersionConflict) { + return apierror.Aborted("concurrent update conflict, please retry") + } + return err + } + return nil +} + +// loadReportedActor returns the Actor if it is the incarnation actorUID names +// and is on node. Otherwise it returns NotFound, so an atelet learns nothing +// about Actors on other nodes. +func (w *ActorWorkflow) loadReportedActor(ctx context.Context, actorRef resources.ActorRef, actorUID, node string) (*ateapipb.Actor, error) { + actor, err := w.store.GetActor(ctx, actorRef) + if err != nil { + if errors.Is(err, store.ErrNotFound) { + return nil, apierror.NotFound("Actor %s not found", actorRef) + } + return nil, err + } + if got := actor.GetMetadata().GetUid(); got != actorUID { + slog.WarnContext(ctx, "Refusing a snapshot state report naming a stale actor incarnation", + slog.Any("actor", actorRef), + slog.String("requested_uid", actorUID), + slog.String("actual_uid", got), + slog.String("caller_node", node)) + return nil, apierror.NotFound("Actor %s not found", actorRef) + } + if !actorOnNode(actor, node) { + slog.WarnContext(ctx, "Refusing a snapshot state report for an actor not on the caller's node", + slog.Any("actor", actorRef), + slog.String("caller_node", node), + slog.String("assigned_node", actor.GetStatus().GetAssignedNode())) + return nil, apierror.NotFound("Actor %s not found", actorRef) + } + return actor, nil +} + +// actorOnNode reports whether the Actor is placed on node or has a local +// snapshot there. Unlike HostingNode, it includes a node that only holds the +// Actor's snapshots, since that node's atelet reports them. +func actorOnNode(actor *ateapipb.Actor, node string) bool { + if actor.GetStatus().GetAssignedNode() == node { + return true + } + for _, snap := range actor.GetStatus().GetSnapshots() { + if snap.GetLocality() == node { + return true + } + } + return false +} + +// applyActorSnapshotState applies the two facts a report carries: an upload +// finished, and the atelet on node detached the Actor. A report with neither +// latest_local nor uploading is a detach. +func applyActorSnapshotState(status *ateapipb.ActorStatus, node string, state *ateapipb.ActorSnapshotState) error { + if err := applyLatestDurable(status, state.GetLatestDurable()); err != nil { + return err + } + if state.GetLatestLocal() == nil && state.GetUploading() == nil { + return applyDetach(status, node, state.GetLatestDurable().GetGeneration()) + } + return nil +} + +// applyLatestDurable marks the reported durable snapshot COMPLETED and records +// the fidelity from its manifest. Its uuid and upload URI must match the +// recorded ones. A generation at or below the latest COMPLETED one changes +// nothing, and so does one status no longer has, such as one a revert +// discarded. +func applyLatestDurable(status *ateapipb.ActorStatus, reported *ateapipb.SnapshotReport) error { + if reported == nil { + return nil + } + if latest := findLatestDurableSnapshot(status, ateapipb.SnapshotStorageStatus_SNAPSHOT_STORAGE_STATUS_COMPLETED); latest.GetGeneration() >= reported.GetGeneration() { + return nil + } + snap := findSnapshotByGeneration(status.GetSnapshots(), reported.GetGeneration()) + if snap == nil { + if reported.GetGeneration() <= status.GetLastAssignedGeneration() { + return nil + } + return apierror.FailedPrecondition("snapshot generation %d (%s) is not recorded for this actor", reported.GetGeneration(), reported.GetUuid()) + } + if snap.GetUuid() != reported.GetUuid() { + return apierror.FailedPrecondition("snapshot generation %d is recorded as %s, not %s", reported.GetGeneration(), snap.GetUuid(), reported.GetUuid()) + } + want := snap.GetDurableSnapshot().GetObject().GetSnapshotUri() + if want == "" { + return apierror.FailedPrecondition("snapshot generation %d has no upload location recorded", reported.GetGeneration()) + } + if got := reported.GetObject().GetSnapshotUri(); got != want { + return apierror.InvalidArgument("snapshot generation %d was uploaded to %q, want %q", reported.GetGeneration(), got, want) + } + snap.Fidelity = reported.GetFidelity() + snap.DurableSnapshot.Status = ateapipb.SnapshotStorageStatus_SNAPSHOT_STORAGE_STATUS_COMPLETED + return nil +} + +// applyDetach records that the atelet on node deleted its copies of the +// Actor's snapshots up to generation upTo, the latest snapshot it reported +// durable. A newer snapshot on node postdates the detach, so it stays. A +// SUSPENDED Actor is released from node once none of its snapshots are there. +// It refuses if the latest deleted snapshot isn't durable, so the Actor can't +// resume from an older one. +func applyDetach(status *ateapipb.ActorStatus, node string, upTo int32) error { + deleted := func(s *ateapipb.Snapshot) bool { return s.GetLocality() == node && s.GetGeneration() <= upTo } + if latest := latestSnapshot(status, deleted); latest != nil && + latest.GetDurableSnapshot().GetStatus() != ateapipb.SnapshotStorageStatus_SNAPSHOT_STORAGE_STATUS_COMPLETED { + return apierror.FailedPrecondition("snapshot generation %d on node %s is not durable", latest.GetGeneration(), node) + } + for _, snap := range status.GetSnapshots() { + if deleted(snap) { + snap.Locality = "" + } + } + onNode := latestSnapshot(status, func(s *ateapipb.Snapshot) bool { return s.GetLocality() == node }) + if status.GetState() == ateapipb.ActorState_ACTOR_STATE_SUSPENDED && status.GetAssignedNode() == node && onNode == nil { + status.AssignedNode = "" + } + return nil +} diff --git a/cmd/ateapi/internal/controlapi/snapshot_report_test.go b/cmd/ateapi/internal/controlapi/snapshot_report_test.go new file mode 100644 index 0000000000..7d7be9b3cf --- /dev/null +++ b/cmd/ateapi/internal/controlapi/snapshot_report_test.go @@ -0,0 +1,439 @@ +// 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 controlapi + +import ( + "context" + "fmt" + "testing" + + "github.com/google/go-cmp/cmp" + "google.golang.org/grpc/codes" + "google.golang.org/protobuf/testing/protocmp" + + "github.com/agent-substrate/substrate/cmd/ateapi/internal/store/storetest" + "github.com/agent-substrate/substrate/internal/apierror" + "github.com/agent-substrate/substrate/internal/resources" + "github.com/agent-substrate/substrate/pkg/proto/ateapipb" +) + +const ( + reportAtespace = "team-a" + reportActorUID = "5f2c1a90-7b34-4e6d-8a11-0c3e9d5b7f42" + reportNode = "node-a" + reportOther = "node-b" +) + +func snapName(gen int32) string { return fmt.Sprintf("snap-%d", gen) } + +// ownedURI returns the upload URI of actorUID's snapshot of generation gen. +func ownedURI(t *testing.T, actorUID string, gen int32) string { + t.Helper() + uri, err := resources.NewActorSnapshotURI("gs://snapshots", reportAtespace, actorUID, snapName(gen)) + if err != nil { + t.Fatal(err) + } + return uri.String() +} + +// localOnly returns a Snapshot held on locality with no durable copy. +func localOnly(gen int32, locality string) *ateapipb.Snapshot { + return &ateapipb.Snapshot{Uuid: snapName(gen), Generation: gen, Locality: locality, Fidelity: ateapipb.SnapshotFidelity_SNAPSHOT_FIDELITY_MEMORY} +} + +// withDurable returns a Snapshot with a durable copy in the given storage +// status. +func withDurable(t *testing.T, gen int32, locality string, storage ateapipb.SnapshotStorageStatus) *ateapipb.Snapshot { + t.Helper() + snap := localOnly(gen, locality) + snap.DurableSnapshot = &ateapipb.SnapshotStorage{ + Status: storage, + Object: &ateapipb.ObjectSnapshot{SnapshotUri: ownedURI(t, reportActorUID, gen)}, + } + return snap +} + +func reported(t *testing.T, gen int32) *ateapipb.SnapshotReport { + t.Helper() + return reportedAt(t, gen, ownedURI(t, reportActorUID, gen)) +} + +func reportedAt(t *testing.T, gen int32, uri string) *ateapipb.SnapshotReport { + t.Helper() + return &ateapipb.SnapshotReport{ + Uuid: snapName(gen), + Generation: gen, + Fidelity: ateapipb.SnapshotFidelity_SNAPSHOT_FIDELITY_MEMORY, + Object: &ateapipb.ObjectSnapshot{SnapshotUri: uri}, + } +} + +func TestApplyActorSnapshotState(t *testing.T) { + const ( + pending = ateapipb.SnapshotStorageStatus_SNAPSHOT_STORAGE_STATUS_PENDING + completed = ateapipb.SnapshotStorageStatus_SNAPSHOT_STORAGE_STATUS_COMPLETED + suspended = ateapipb.ActorState_ACTOR_STATE_SUSPENDED + running = ateapipb.ActorState_ACTOR_STATE_RUNNING + ) + tests := []struct { + name string + status *ateapipb.ActorStatus + state *ateapipb.ActorSnapshotState + want *ateapipb.ActorStatus + wantCode codes.Code + }{{ + name: "upload marks the pending snapshot COMPLETED and keeps it local", + status: &ateapipb.ActorStatus{State: suspended, AssignedNode: reportNode, Snapshots: []*ateapipb.Snapshot{ + withDurable(t, 2, reportNode, pending), + }}, + state: &ateapipb.ActorSnapshotState{LatestLocal: reported(t, 2), LatestDurable: reported(t, 2)}, + want: &ateapipb.ActorStatus{State: suspended, AssignedNode: reportNode, Snapshots: []*ateapipb.Snapshot{ + withDurable(t, 2, reportNode, completed), + }}, + }, { + name: "upload records the fidelity the manifest reports", + status: &ateapipb.ActorStatus{State: suspended, AssignedNode: reportNode, Snapshots: []*ateapipb.Snapshot{ + withDurable(t, 2, reportNode, pending), + }}, + state: &ateapipb.ActorSnapshotState{LatestLocal: reported(t, 2), LatestDurable: func() *ateapipb.SnapshotReport { + r := reported(t, 2) + r.Fidelity = ateapipb.SnapshotFidelity_SNAPSHOT_FIDELITY_ROOTFS + return r + }()}, + want: &ateapipb.ActorStatus{State: suspended, AssignedNode: reportNode, Snapshots: []*ateapipb.Snapshot{ + func() *ateapipb.Snapshot { + snap := withDurable(t, 2, reportNode, completed) + snap.Fidelity = ateapipb.SnapshotFidelity_SNAPSHOT_FIDELITY_ROOTFS + return snap + }(), + }}, + }, { + name: "a snapshot already COMPLETED changes nothing", + status: &ateapipb.ActorStatus{State: suspended, AssignedNode: reportNode, Snapshots: []*ateapipb.Snapshot{ + withDurable(t, 2, reportNode, completed), + }}, + state: &ateapipb.ActorSnapshotState{LatestLocal: reported(t, 2), LatestDurable: reported(t, 2)}, + want: &ateapipb.ActorStatus{State: suspended, AssignedNode: reportNode, Snapshots: []*ateapipb.Snapshot{ + withDurable(t, 2, reportNode, completed), + }}, + }, { + name: "a generation below the latest COMPLETED one changes nothing", + status: &ateapipb.ActorStatus{State: suspended, AssignedNode: reportNode, LastAssignedGeneration: 2, Snapshots: []*ateapipb.Snapshot{ + withDurable(t, 1, reportNode, pending), + withDurable(t, 2, reportNode, completed), + }}, + state: &ateapipb.ActorSnapshotState{LatestLocal: reported(t, 2), LatestDurable: func() *ateapipb.SnapshotReport { + r := reported(t, 1) + r.Fidelity = ateapipb.SnapshotFidelity_SNAPSHOT_FIDELITY_ROOTFS + return r + }()}, + want: &ateapipb.ActorStatus{State: suspended, AssignedNode: reportNode, LastAssignedGeneration: 2, Snapshots: []*ateapipb.Snapshot{ + withDurable(t, 1, reportNode, pending), + withDurable(t, 2, reportNode, completed), + }}, + }, { + name: "a generation status no longer has changes nothing", + status: &ateapipb.ActorStatus{State: suspended, AssignedNode: reportNode, LastAssignedGeneration: 3, Snapshots: []*ateapipb.Snapshot{ + withDurable(t, 3, reportNode, pending), + }}, + state: &ateapipb.ActorSnapshotState{LatestLocal: reported(t, 3), LatestDurable: reported(t, 2)}, + want: &ateapipb.ActorStatus{State: suspended, AssignedNode: reportNode, LastAssignedGeneration: 3, Snapshots: []*ateapipb.Snapshot{ + withDurable(t, 3, reportNode, pending), + }}, + }, { + name: "a generation newer than any assigned is refused", + status: &ateapipb.ActorStatus{State: suspended, AssignedNode: reportNode, LastAssignedGeneration: 2, Snapshots: []*ateapipb.Snapshot{ + withDurable(t, 2, reportNode, pending), + }}, + state: &ateapipb.ActorSnapshotState{LatestLocal: reported(t, 2), LatestDurable: reported(t, 5)}, + wantCode: codes.FailedPrecondition, + }, { + name: "a uuid other than the recorded one is refused", + status: &ateapipb.ActorStatus{State: suspended, AssignedNode: reportNode, Snapshots: []*ateapipb.Snapshot{ + withDurable(t, 2, reportNode, pending), + }}, + state: &ateapipb.ActorSnapshotState{LatestLocal: reported(t, 2), LatestDurable: func() *ateapipb.SnapshotReport { + r := reported(t, 2) + r.Uuid = "other" + return r + }()}, + wantCode: codes.FailedPrecondition, + }, { + name: "a snapshot with no recorded upload URI is refused", + status: &ateapipb.ActorStatus{State: suspended, AssignedNode: reportNode, Snapshots: []*ateapipb.Snapshot{ + localOnly(2, reportNode), + }}, + state: &ateapipb.ActorSnapshotState{LatestLocal: reported(t, 2), LatestDurable: reported(t, 2)}, + wantCode: codes.FailedPrecondition, + }, { + name: "an upload URI other than the recorded one is refused", + status: &ateapipb.ActorStatus{State: suspended, AssignedNode: reportNode, Snapshots: []*ateapipb.Snapshot{ + withDurable(t, 2, reportNode, pending), + }}, + state: &ateapipb.ActorSnapshotState{LatestLocal: reported(t, 2), LatestDurable: reportedAt(t, 2, ownedURI(t, reportActorUID, 9))}, + wantCode: codes.InvalidArgument, + }, { + name: "latest_local and uploading are not checked", + status: &ateapipb.ActorStatus{State: suspended, AssignedNode: reportNode, Snapshots: []*ateapipb.Snapshot{ + withDurable(t, 2, reportNode, pending), + }}, + state: &ateapipb.ActorSnapshotState{LatestLocal: reported(t, 7), Uploading: reported(t, 8)}, + want: &ateapipb.ActorStatus{State: suspended, AssignedNode: reportNode, Snapshots: []*ateapipb.Snapshot{ + withDurable(t, 2, reportNode, pending), + }}, + }, { + name: "an upload in flight with no local snapshot is not a detach", + status: &ateapipb.ActorStatus{State: suspended, AssignedNode: reportNode, Snapshots: []*ateapipb.Snapshot{ + withDurable(t, 2, reportNode, pending), + }}, + state: &ateapipb.ActorSnapshotState{Uploading: reported(t, 2)}, + want: &ateapipb.ActorStatus{State: suspended, AssignedNode: reportNode, Snapshots: []*ateapipb.Snapshot{ + withDurable(t, 2, reportNode, pending), + }}, + }, { + name: "detach releases a suspended actor from the node", + status: &ateapipb.ActorStatus{State: suspended, AssignedNode: reportNode, Snapshots: []*ateapipb.Snapshot{ + withDurable(t, 1, "", completed), + withDurable(t, 2, reportNode, completed), + }}, + state: &ateapipb.ActorSnapshotState{LatestDurable: reported(t, 2)}, + want: &ateapipb.ActorStatus{State: suspended, Snapshots: []*ateapipb.Snapshot{ + withDurable(t, 1, "", completed), + withDurable(t, 2, "", completed), + }}, + }, { + name: "a detach can complete the upload it reports", + status: &ateapipb.ActorStatus{State: suspended, AssignedNode: reportNode, Snapshots: []*ateapipb.Snapshot{ + withDurable(t, 2, reportNode, pending), + }}, + state: &ateapipb.ActorSnapshotState{LatestDurable: reported(t, 2)}, + want: &ateapipb.ActorStatus{State: suspended, Snapshots: []*ateapipb.Snapshot{ + withDurable(t, 2, "", completed), + }}, + }, { + name: "a late detach leaves a newer snapshot on the node and keeps the actor placed", + status: &ateapipb.ActorStatus{State: suspended, AssignedNode: reportNode, Snapshots: []*ateapipb.Snapshot{ + withDurable(t, 2, reportNode, completed), + withDurable(t, 3, reportNode, completed), + }}, + state: &ateapipb.ActorSnapshotState{LatestDurable: reported(t, 2)}, + want: &ateapipb.ActorStatus{State: suspended, AssignedNode: reportNode, Snapshots: []*ateapipb.Snapshot{ + withDurable(t, 2, "", completed), + withDurable(t, 3, reportNode, completed), + }}, + }, { + name: "a late detach leaves a newer local-only snapshot on the node", + status: &ateapipb.ActorStatus{State: suspended, AssignedNode: reportNode, Snapshots: []*ateapipb.Snapshot{ + withDurable(t, 1, "", completed), + localOnly(2, reportNode), + }}, + state: &ateapipb.ActorSnapshotState{LatestDurable: reported(t, 1)}, + want: &ateapipb.ActorStatus{State: suspended, AssignedNode: reportNode, Snapshots: []*ateapipb.Snapshot{ + withDurable(t, 1, "", completed), + localOnly(2, reportNode), + }}, + }, { + name: "a detach of a snapshot that isn't durable is refused", + status: &ateapipb.ActorStatus{State: suspended, AssignedNode: reportNode, LastAssignedGeneration: 2, Snapshots: []*ateapipb.Snapshot{ + localOnly(1, reportNode), + }}, + state: &ateapipb.ActorSnapshotState{LatestDurable: reported(t, 2)}, + wantCode: codes.FailedPrecondition, + }, { + name: "a detach from a node that holds no snapshot only releases the actor", + status: &ateapipb.ActorStatus{State: suspended, AssignedNode: reportNode, Snapshots: []*ateapipb.Snapshot{ + withDurable(t, 2, "", completed), + }}, + state: &ateapipb.ActorSnapshotState{LatestDurable: reported(t, 2)}, + want: &ateapipb.ActorStatus{State: suspended, Snapshots: []*ateapipb.Snapshot{ + withDurable(t, 2, "", completed), + }}, + }, { + name: "a running actor's detach keeps it placed on the node", + status: &ateapipb.ActorStatus{State: running, AssignedNode: reportNode, Snapshots: []*ateapipb.Snapshot{ + withDurable(t, 2, reportNode, completed), + }}, + state: &ateapipb.ActorSnapshotState{LatestDurable: reported(t, 2)}, + want: &ateapipb.ActorStatus{State: running, AssignedNode: reportNode, Snapshots: []*ateapipb.Snapshot{ + withDurable(t, 2, "", completed), + }}, + }, { + name: "a detach from one node leaves another node alone", + status: &ateapipb.ActorStatus{State: suspended, AssignedNode: reportOther, Snapshots: []*ateapipb.Snapshot{ + withDurable(t, 2, reportNode, completed), + localOnly(3, reportOther), + }}, + state: &ateapipb.ActorSnapshotState{LatestDurable: reported(t, 2)}, + want: &ateapipb.ActorStatus{State: suspended, AssignedNode: reportOther, Snapshots: []*ateapipb.Snapshot{ + withDurable(t, 2, "", completed), + localOnly(3, reportOther), + }}, + }} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := applyActorSnapshotState(tt.status, reportNode, tt.state) + if tt.wantCode != codes.OK { + if got := apierror.Code(err); got != tt.wantCode { + t.Fatalf("applyActorSnapshotState() error = %v, want code %v", err, tt.wantCode) + } + return + } + if err != nil { + t.Fatalf("applyActorSnapshotState() error = %v", err) + } + if diff := cmp.Diff(tt.want, tt.status, protocmp.Transform()); diff != "" { + t.Errorf("status mismatch (-want +got):\n%s", diff) + } + }) + } +} + +// A report is applied to the stored actor under its lease. A refused report, +// and a report that changes nothing, write nothing. +func TestApplyReportedSnapshotState(t *testing.T) { + ctx := context.Background() + persistence := newTestPersistence(t) + w := NewActorWorkflow(persistence, nil, nil, nil, nil, nil, "", nil, nil) + tmpl := &ateapipb.ActorTemplate{SnapshotConfig: &ateapipb.SnapshotConfig{StorageLocation: testStorageLocation}} + + actorRef := resources.ActorRef{Atespace: reportAtespace, Name: "actor-1"} + actor := storetest.MustCreateActor(t, ctx, persistence, &ateapipb.Actor{ + Metadata: &ateapipb.ResourceMetadata{Atespace: actorRef.Atespace, Name: actorRef.Name}, + Status: &ateapipb.ActorStatus{State: ateapipb.ActorState_ACTOR_STATE_SUSPENDED}, + }) + uid := actor.GetMetadata().GetUid() + pending := mustActorSnapshotURI(t, tmpl, actor, "snap-2") + actor = mustUpdateActorStatus(t, ctx, persistence, actor, func(s *ateapipb.ActorStatus) { + s.AssignedNode = reportNode + s.LastAssignedGeneration = 2 + snap := newDurableSnapshot(2, ateapipb.SnapshotOwner_SNAPSHOT_OWNER_ACTOR, ateapipb.SnapshotFidelity_SNAPSHOT_FIDELITY_MEMORY, "", pending.Name(), pending.String(), ateapipb.SnapshotStorageStatus_SNAPSHOT_STORAGE_STATUS_PENDING) + snap.Locality = reportNode + s.Snapshots = []*ateapipb.Snapshot{snap} + }) + uploaded := reportedAt(t, 2, pending.String()) + state := &ateapipb.ActorSnapshotState{LatestLocal: uploaded, LatestDurable: uploaded} + + assertUnwritten := func(t *testing.T) { + t.Helper() + got, err := persistence.GetActor(ctx, actorRef) + if err != nil { + t.Fatal(err) + } + if got.GetMetadata().GetVersion() != actor.GetMetadata().GetVersion() { + t.Errorf("a refused report wrote the actor: version %d, want %d", got.GetMetadata().GetVersion(), actor.GetMetadata().GetVersion()) + } + } + + t.Run("refused while another operation holds the actor", func(t *testing.T) { + lease, err := persistence.AcquireLease(ctx, actorLeaseKey(actorRef)) + if err != nil { + t.Fatalf("AcquireLease() failed: %v", err) + } + defer lease.Close() + if err := w.ApplyReportedSnapshotState(ctx, actorRef, uid, reportNode, state); apierror.Code(err) != codes.Aborted { + t.Fatalf("ApplyReportedSnapshotState() = %v, want Aborted", err) + } + assertUnwritten(t) + }) + for _, tc := range []struct { + name, uid, node string + ref resources.ActorRef + }{ + {"refused for another node", uid, reportOther, actorRef}, + {"refused for a stale incarnation", "1a7e4c83-6d20-4f95-b3c8-9e0a2f6d4b17", reportNode, actorRef}, + {"refused for a missing actor", uid, reportNode, resources.ActorRef{Atespace: reportAtespace, Name: "no-such-actor"}}, + } { + t.Run(tc.name, func(t *testing.T) { + if err := w.ApplyReportedSnapshotState(ctx, tc.ref, tc.uid, tc.node, state); apierror.Code(err) != codes.NotFound { + t.Fatalf("ApplyReportedSnapshotState() = %v, want NotFound", err) + } + assertUnwritten(t) + }) + } + // A transitioning actor gets Aborted, and a crashed one FailedPrecondition. + for _, tc := range []struct { + state ateapipb.ActorState + want codes.Code + }{ + {ateapipb.ActorState_ACTOR_STATE_SUSPENDING, codes.Aborted}, + {ateapipb.ActorState_ACTOR_STATE_RESUMING, codes.Aborted}, + {ateapipb.ActorState_ACTOR_STATE_DELETING, codes.Aborted}, + {ateapipb.ActorState_ACTOR_STATE_REVERTING, codes.Aborted}, + {ateapipb.ActorState_ACTOR_STATE_CRASHED, codes.FailedPrecondition}, + } { + t.Run("refused for a "+tc.state.String()+" actor", func(t *testing.T) { + actor = mustUpdateActorStatus(t, ctx, persistence, actor, func(s *ateapipb.ActorStatus) { s.State = tc.state }) + defer func() { + actor = mustUpdateActorStatus(t, ctx, persistence, actor, func(s *ateapipb.ActorStatus) { + s.State = ateapipb.ActorState_ACTOR_STATE_SUSPENDED + }) + }() + if err := w.ApplyReportedSnapshotState(ctx, actorRef, uid, reportNode, state); apierror.Code(err) != tc.want { + t.Fatalf("ApplyReportedSnapshotState() = %v, want %v", err, tc.want) + } + assertUnwritten(t) + }) + } + t.Run("refused for an upload to the wrong URI", func(t *testing.T) { + wrong := &ateapipb.ActorSnapshotState{LatestLocal: uploaded, LatestDurable: reportedAt(t, 2, ownedURI(t, uid, 9))} + if err := w.ApplyReportedSnapshotState(ctx, actorRef, uid, reportNode, wrong); apierror.Code(err) != codes.InvalidArgument { + t.Fatalf("ApplyReportedSnapshotState() = %v, want InvalidArgument", err) + } + assertUnwritten(t) + }) + + if err := w.ApplyReportedSnapshotState(ctx, actorRef, uid, reportNode, state); err != nil { + t.Fatalf("ApplyReportedSnapshotState() failed: %v", err) + } + got, err := persistence.GetActor(ctx, actorRef) + if err != nil { + t.Fatal(err) + } + if st := got.GetStatus().GetSnapshots()[0].GetDurableSnapshot().GetStatus(); st != ateapipb.SnapshotStorageStatus_SNAPSHOT_STORAGE_STATUS_COMPLETED { + t.Errorf("durable snapshot status = %v, want COMPLETED", st) + } + + // A report that changes nothing writes nothing. + actor = got + if err := w.ApplyReportedSnapshotState(ctx, actorRef, uid, reportNode, state); err != nil { + t.Fatalf("repeated ApplyReportedSnapshotState() failed: %v", err) + } + assertUnwritten(t) +} + +func TestActorOnNode(t *testing.T) { + actor := func(assigned string, localities ...string) *ateapipb.Actor { + status := &ateapipb.ActorStatus{AssignedNode: assigned} + for i, l := range localities { + status.Snapshots = append(status.Snapshots, localOnly(int32(i+1), l)) + } + return &ateapipb.Actor{Status: status} + } + for _, tc := range []struct { + name string + actor *ateapipb.Actor + want bool + }{ + {"placed on the node", actor(reportNode), true}, + {"holds a snapshot on the node", actor(reportOther, "", reportNode), true}, + {"neither", actor(reportOther, reportOther), false}, + } { + t.Run(tc.name, func(t *testing.T) { + if got := actorOnNode(tc.actor, reportNode); got != tc.want { + t.Errorf("actorOnNode() = %v, want %v", got, tc.want) + } + }) + } +} diff --git a/cmd/ateapi/internal/workerservice/report.go b/cmd/ateapi/internal/workerservice/report.go new file mode 100644 index 0000000000..3cc6b92d35 --- /dev/null +++ b/cmd/ateapi/internal/workerservice/report.go @@ -0,0 +1,43 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package workerservice + +import ( + "context" + + "github.com/agent-substrate/substrate/cmd/ateapi/internal/apivalidation" + "github.com/agent-substrate/substrate/cmd/ateapi/internal/ateletauth" + "github.com/agent-substrate/substrate/internal/resources" + "github.com/agent-substrate/substrate/pkg/proto/ateapipb" +) + +// ReportActorSnapshotState records the snapshot state that the calling atelet +// reports for an Actor on its node. +func (s *Server) ReportActorSnapshotState(ctx context.Context, req *ateapipb.ReportActorSnapshotStateRequest) (*ateapipb.ReportActorSnapshotStateResponse, error) { + // Authz doesn't cover WorkerService, so authenticate before reading the + // request. + caller, err := ateletauth.Authenticate(ctx, s.ateletSPIFFEID) + if err != nil { + return nil, err + } + if errs := apivalidation.ValidateReportActorSnapshotStateRequest(ctx, req); len(errs) > 0 { + return nil, resources.ToAPIError(errs) + } + actorRef := resources.ActorRefFromObjectRef(req.GetActor()) + if err := s.suspender.ApplyActorSnapshotState(ctx, actorRef, req.GetActorUid(), caller.NodeName, req.GetState()); err != nil { + return nil, err + } + return &ateapipb.ReportActorSnapshotStateResponse{}, nil +} diff --git a/cmd/ateapi/internal/workerservice/report_test.go b/cmd/ateapi/internal/workerservice/report_test.go new file mode 100644 index 0000000000..398e5517a1 --- /dev/null +++ b/cmd/ateapi/internal/workerservice/report_test.go @@ -0,0 +1,106 @@ +// 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 workerservice + +import ( + "context" + "testing" + + "google.golang.org/grpc/codes" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/agent-substrate/substrate/cmd/ateapi/internal/ateletauth/ateletauthtest" + "github.com/agent-substrate/substrate/internal/apierror" + "github.com/agent-substrate/substrate/internal/resources" + "github.com/agent-substrate/substrate/pkg/proto/ateapipb" +) + +const reportActorUID = "5f2c1a90-7b34-4e6d-8a11-0c3e9d5b7f42" + +func reportRequest() *ateapipb.ReportActorSnapshotStateRequest { + durable := &ateapipb.SnapshotReport{ + Uuid: "snap-1", + Generation: 1, + Fidelity: ateapipb.SnapshotFidelity_SNAPSHOT_FIDELITY_MEMORY, + Object: &ateapipb.ObjectSnapshot{SnapshotUri: "gs://bucket/actors/a/snap-1"}, + LocalCommitTime: timestamppb.Now(), + } + return &ateapipb.ReportActorSnapshotStateRequest{ + Actor: &ateapipb.ObjectRef{Atespace: suspendAtespace, Name: suspendActor}, + ActorUid: reportActorUID, + State: &ateapipb.ActorSnapshotState{LatestDurable: durable}, + } +} + +// A report reaches the control plane with the node from the caller's +// certificate. The handler reads nothing from the store. +func TestReportActorSnapshotState(t *testing.T) { + suspender := &fakeSuspender{} + s := New(nil, suspender, testAteletSPIFFEID, nil, "", nil) + + req := reportRequest() + if _, err := s.ReportActorSnapshotState(ateletauthtest.ContextWith(ateletauthtest.CertOn(t, testNode)), req); err != nil { + t.Fatalf("ReportActorSnapshotState() failed: %v", err) + } + if len(suspender.reports) != 1 { + t.Fatalf("got %d applied reports, want 1", len(suspender.reports)) + } + got := suspender.reports[0] + if want := (resources.ActorRef{Atespace: suspendAtespace, Name: suspendActor}); got.actorRef != want { + t.Errorf("actorRef = %v, want %v", got.actorRef, want) + } + if got.actorUID != reportActorUID || got.node != testNode { + t.Errorf("applied (uid %q, node %q), want (%q, %q)", got.actorUID, got.node, reportActorUID, testNode) + } + if !proto.Equal(got.state, req.GetState()) { + t.Errorf("applied state = %v, want %v", got.state, req.GetState()) + } +} + +// A caller that isn't atelet, or a malformed request, doesn't reach the control +// plane. A refusal from the control plane reaches atelet unchanged. +func TestReportActorSnapshotState_Refused(t *testing.T) { + onNode := ateletauthtest.ContextWith(ateletauthtest.CertOn(t, testNode)) + invalid := reportRequest() + invalid.State = nil + + tests := []struct { + name string + ctx context.Context + req *ateapipb.ReportActorSnapshotStateRequest + controlErr error + want codes.Code + wantApplied bool + }{ + {name: "unauthenticated", ctx: ateletauthtest.ContextWith(nil), req: reportRequest(), want: codes.Unauthenticated}, + {name: "not atelet", ctx: ateletauthtest.ContextWith(ateletauthtest.CertIn(t, "other-namespace", testNode)), req: reportRequest(), want: codes.PermissionDenied}, + {name: "invalid request", ctx: onNode, req: invalid, want: codes.InvalidArgument}, + {name: "control plane refuses", ctx: onNode, req: reportRequest(), controlErr: apierror.NotFound("Actor not found"), want: codes.NotFound, wantApplied: true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + suspender := &fakeSuspender{reportErr: tt.controlErr} + s := New(nil, suspender, testAteletSPIFFEID, nil, "", nil) + _, err := s.ReportActorSnapshotState(tt.ctx, tt.req) + if got := apierror.Code(err); got != tt.want { + t.Fatalf("ReportActorSnapshotState() error = %v, want code %v", err, tt.want) + } + if applied := len(suspender.reports) > 0; applied != tt.wantApplied { + t.Errorf("report reached the control plane = %v, want %v", applied, tt.wantApplied) + } + }) + } +} diff --git a/cmd/ateapi/internal/workerservice/service.go b/cmd/ateapi/internal/workerservice/service.go index 435b1113ff..657eedff84 100644 --- a/cmd/ateapi/internal/workerservice/service.go +++ b/cmd/ateapi/internal/workerservice/service.go @@ -22,6 +22,7 @@ import ( "github.com/agent-substrate/substrate/cmd/ateapi/internal/store" "github.com/agent-substrate/substrate/internal/localca" "github.com/agent-substrate/substrate/internal/localjwtauthority" + "github.com/agent-substrate/substrate/internal/resources" "github.com/agent-substrate/substrate/pkg/proto/ateapipb" ) @@ -54,6 +55,7 @@ type Server struct { // drives. *controlapi.RPCService satisfies it. type actorSuspender interface { SuspendActor(ctx context.Context, req *ateapipb.SuspendActorRequest) (*ateapipb.SuspendActorResponse, error) + ApplyActorSnapshotState(ctx context.Context, actorRef resources.ActorRef, actorUID, node string, state *ateapipb.ActorSnapshotState) error } var _ ateapipb.WorkerServiceServer = (*Server)(nil) diff --git a/cmd/ateapi/internal/workerservice/service_test.go b/cmd/ateapi/internal/workerservice/service_test.go index 8e26beebdc..71455d4dec 100644 --- a/cmd/ateapi/internal/workerservice/service_test.go +++ b/cmd/ateapi/internal/workerservice/service_test.go @@ -20,6 +20,7 @@ import ( "github.com/agent-substrate/substrate/cmd/ateapi/internal/store" "github.com/agent-substrate/substrate/internal/installdefaults" + "github.com/agent-substrate/substrate/internal/resources" "github.com/agent-substrate/substrate/pkg/proto/ateapipb" ) @@ -41,6 +42,9 @@ type fakeSuspender struct { calls []*ateapipb.SuspendActorRequest resp *ateapipb.SuspendActorResponse err error + + reports []appliedReport + reportErr error } func (f *fakeSuspender) SuspendActor(_ context.Context, req *ateapipb.SuspendActorRequest) (*ateapipb.SuspendActorResponse, error) { @@ -48,6 +52,19 @@ func (f *fakeSuspender) SuspendActor(_ context.Context, req *ateapipb.SuspendAct return f.resp, f.err } +func (f *fakeSuspender) ApplyActorSnapshotState(_ context.Context, actorRef resources.ActorRef, actorUID, node string, state *ateapipb.ActorSnapshotState) error { + f.reports = append(f.reports, appliedReport{actorRef: actorRef, actorUID: actorUID, node: node, state: state}) + return f.reportErr +} + +// appliedReport is one ApplyActorSnapshotState call. +type appliedReport struct { + actorRef resources.ActorRef + actorUID string + node string + state *ateapipb.ActorSnapshotState +} + // seedReportedWorker registers a Worker on nodeName that has already reported // capacity and its default runtime: identity from the pod, the way the syncer // writes it, plus the result of an earlier registration. A Worker that has diff --git a/pkg/proto/ateapipb/ateapi.proto b/pkg/proto/ateapipb/ateapi.proto index 233b3fb63e..8e7c492f09 100644 --- a/pkg/proto/ateapipb/ateapi.proto +++ b/pkg/proto/ateapipb/ateapi.proto @@ -2566,18 +2566,16 @@ service WorkerService { // rather than a repeat of its success. rpc RequestActorSuspend(RequestActorSuspendRequest) returns (RequestActorSuspendResponse); - // ReportActorSnapshotState records which snapshots of an Actor the calling - // atelet holds on its node, and which of them are durable. Only atelet can - // observe when an upload finishes or a local copy is deleted, and the - // control plane needs both to decide where the Actor can resume. - // - // Only atelet should call this, with its own client certificate. The - // control plane serves it only for an Actor on the caller's node. - // Idempotent: each report carries the whole state and replaces the previous - // one. Returns ABORTED while another operation holds the Actor; retry. - // - // TODO(#2570): not served yet. Every call returns UNIMPLEMENTED until the - // handler lands. + // ReportActorSnapshotState reports an Actor's snapshot state on the caller's + // node. The control plane records two facts from it: an upload finished, and + // atelet detached the Actor from the node. Only atelet observes either. + // + // atelet calls this with its own client certificate, for an Actor on its + // node. Each report carries the whole state, so repeating one is safe. + // Returns ABORTED while another operation is changing the Actor; retry. + // Returns NOT_FOUND once the Actor is gone or no longer on the caller's + // node. Returns FAILED_PRECONDITION or INVALID_ARGUMENT when the report + // conflicts with the record; don't retry. rpc ReportActorSnapshotState(ReportActorSnapshotStateRequest) returns (ReportActorSnapshotStateResponse); } diff --git a/pkg/proto/ateapipb/ateapi_grpc.pb.go b/pkg/proto/ateapipb/ateapi_grpc.pb.go index 4043f05a5a..68e65e97a1 100644 --- a/pkg/proto/ateapipb/ateapi_grpc.pb.go +++ b/pkg/proto/ateapipb/ateapi_grpc.pb.go @@ -1990,18 +1990,16 @@ type WorkerServiceClient interface { // retries a suspend it had already been granted therefore sees a failure // rather than a repeat of its success. RequestActorSuspend(ctx context.Context, in *RequestActorSuspendRequest, opts ...grpc.CallOption) (*RequestActorSuspendResponse, error) - // ReportActorSnapshotState records which snapshots of an Actor the calling - // atelet holds on its node, and which of them are durable. Only atelet can - // observe when an upload finishes or a local copy is deleted, and the - // control plane needs both to decide where the Actor can resume. + // ReportActorSnapshotState reports an Actor's snapshot state on the caller's + // node. The control plane records two facts from it: an upload finished, and + // atelet detached the Actor from the node. Only atelet observes either. // - // Only atelet should call this, with its own client certificate. The - // control plane serves it only for an Actor on the caller's node. - // Idempotent: each report carries the whole state and replaces the previous - // one. Returns ABORTED while another operation holds the Actor; retry. - // - // TODO(#2570): not served yet. Every call returns UNIMPLEMENTED until the - // handler lands. + // atelet calls this with its own client certificate, for an Actor on its + // node. Each report carries the whole state, so repeating one is safe. + // Returns ABORTED while another operation is changing the Actor; retry. + // Returns NOT_FOUND once the Actor is gone or no longer on the caller's + // node. Returns FAILED_PRECONDITION or INVALID_ARGUMENT when the report + // conflicts with the record; don't retry. ReportActorSnapshotState(ctx context.Context, in *ReportActorSnapshotStateRequest, opts ...grpc.CallOption) (*ReportActorSnapshotStateResponse, error) } @@ -2119,18 +2117,16 @@ type WorkerServiceServer interface { // retries a suspend it had already been granted therefore sees a failure // rather than a repeat of its success. RequestActorSuspend(context.Context, *RequestActorSuspendRequest) (*RequestActorSuspendResponse, error) - // ReportActorSnapshotState records which snapshots of an Actor the calling - // atelet holds on its node, and which of them are durable. Only atelet can - // observe when an upload finishes or a local copy is deleted, and the - // control plane needs both to decide where the Actor can resume. - // - // Only atelet should call this, with its own client certificate. The - // control plane serves it only for an Actor on the caller's node. - // Idempotent: each report carries the whole state and replaces the previous - // one. Returns ABORTED while another operation holds the Actor; retry. + // ReportActorSnapshotState reports an Actor's snapshot state on the caller's + // node. The control plane records two facts from it: an upload finished, and + // atelet detached the Actor from the node. Only atelet observes either. // - // TODO(#2570): not served yet. Every call returns UNIMPLEMENTED until the - // handler lands. + // atelet calls this with its own client certificate, for an Actor on its + // node. Each report carries the whole state, so repeating one is safe. + // Returns ABORTED while another operation is changing the Actor; retry. + // Returns NOT_FOUND once the Actor is gone or no longer on the caller's + // node. Returns FAILED_PRECONDITION or INVALID_ARGUMENT when the report + // conflicts with the record; don't retry. ReportActorSnapshotState(context.Context, *ReportActorSnapshotStateRequest) (*ReportActorSnapshotStateResponse, error) mustEmbedUnimplementedWorkerServiceServer() } From f4f7b806255ffdc824391b857dbbf329a2556402 Mon Sep 17 00:00:00 2001 From: Yuan Gao Date: Thu, 15 Oct 2026 17:01:44 +0000 Subject: [PATCH 04/31] hack/verify: run the benchmarking Python suites (#2709) Nothing in `.github/workflows` or `hack/verify` ran the benchmarking Python tests. The four suites under `benchmarking/analysis`, `automation`, `locust/unit_tests` and `nighthawk-ingress/tests` only ran when someone remembered to, and the egress-tunnels benchmark that follows adds a fifth. A test nothing runs is not a test. `hack/verify/benchmarking-python.sh` gives each suite a venv from its own `requirements.txt` plus pytest and runs it from the suite directory, because the tests import their modules by bare name; pytest rather than unittest because the nighthawk-ingress tests are plain functions. Before running, it stubs `docker`, `gcloud` and `kubectl` on `PATH` to fail and points `KUBECONFIG` at nothing, so a test that escapes its mocks cannot reach a cluster or a registry with the developer's credentials. `hack/verify-all.sh` picks the script up by glob. Testing: the script passes 107 tests across the four suites locally; shellcheck, boilerplate and the rest of `hack/verify/*.sh` are green. Prepared with help from an AI coding agent; reviewed and verified by the author. --- hack/verify/benchmarking-python.sh | 60 ++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100755 hack/verify/benchmarking-python.sh diff --git a/hack/verify/benchmarking-python.sh b/hack/verify/benchmarking-python.sh new file mode 100755 index 0000000000..a206887fb2 --- /dev/null +++ b/hack/verify/benchmarking-python.sh @@ -0,0 +1,60 @@ +#!/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. + +# Runs the benchmarking Python test suites under pytest. Each suite gets a venv +# from its own requirements.txt plus pytest, and runs with the suite directory +# as the working directory because the tests import their modules by bare name. +# pytest is the runner because the nighthawk-ingress tests are plain functions, +# which unittest does not collect. + +set -o errexit -o nounset -o pipefail + +ROOT="$(git rev-parse --show-toplevel)" +cd "${ROOT}" +source hack/util/venv.sh + +# . A suite without a +# requirements.txt gets pytest alone. +SUITES=( + "benchmarking/analysis ." + "benchmarking/automation ." + "benchmarking/locust unit_tests" + "benchmarking/nighthawk-ingress tests" +) + +# A test that escapes its mocks must not reach a cluster or a registry with +# the developer's credentials: the commands the suites shell out to are +# stubbed to fail, and the kubernetes client finds no kubeconfig. +stubs="$(mktemp -d)" +trap 'rm -rf "${stubs}"' EXIT +for cmd in docker gcloud kubectl; do + printf '#!/bin/sh\necho "%s: refusing to run %s from a unit test" >&2\nexit 1\n' \ + "$(basename "$0")" "${cmd}" >"${stubs}/${cmd}" + chmod +x "${stubs}/${cmd}" +done +export PATH="${stubs}:${PATH}" KUBECONFIG=/nonexistent + +for entry in "${SUITES[@]}"; do + read -r dir tests <<<"${entry}" + venv="${dir}/venv" + ensure_venv "${venv}" + if [[ -f "${dir}/requirements.txt" ]]; then + venv_sync_requirements "${venv}" "${dir}/requirements.txt" + fi + "${venv}/bin/pip" install --quiet pytest + echo "Running ${dir}/${tests}" + (cd "${dir}" && "${ROOT}/${venv}/bin/python" -m pytest -q "${tests}") +done From 7a51ad17962143f63f3f2fac3c7570449cebf7f3 Mon Sep 17 00:00:00 2001 From: Zoe Zhao <35669350+zoez7@users.noreply.github.com> Date: Thu, 15 Oct 2026 18:35:54 +0000 Subject: [PATCH 05/31] Remove the pause operation label (#2701) Pause and suspend are being merged into one workflow, so PauseActor and SuspendActor become one lifecycle operation. Label both as "suspend" in metrics, state-change events and crash records, and drop the "pause" value from the ate.actor.operation.name registry. --- cmd/ateapi/internal/controlapi/actor_test.go | 3 +- .../controlapi/functionaltest/actor_test.go | 4 +-- cmd/ateapi/internal/controlapi/metrics.go | 2 +- .../internal/controlapi/metrics_test.go | 2 +- .../internal/controlapi/workflow_pause.go | 16 +++++----- .../controlapi/workflow_pause_test.go | 16 ++++++---- .../internal/controlapi/workflow_test.go | 29 +++++++++++++++++-- .../controlapi/workflow_worker_delete.go | 3 -- .../controlapi/workflow_worker_delete_test.go | 14 ++------- docs/metrics/registry/metrics.yaml | 20 +++++-------- docs/observability.md | 4 +-- internal/ateattr/ateattr.go | 2 -- internal/ateattr/ateattr_test.go | 5 ++-- internal/e2e/suites/metrics/metrics_test.go | 2 +- 14 files changed, 69 insertions(+), 53 deletions(-) diff --git a/cmd/ateapi/internal/controlapi/actor_test.go b/cmd/ateapi/internal/controlapi/actor_test.go index 479cb03204..7d3ae9b7b3 100644 --- a/cmd/ateapi/internal/controlapi/actor_test.go +++ b/cmd/ateapi/internal/controlapi/actor_test.go @@ -30,6 +30,7 @@ import ( "github.com/agent-substrate/substrate/cmd/ateapi/internal/store" "github.com/agent-substrate/substrate/cmd/ateapi/internal/store/storetest" "github.com/agent-substrate/substrate/internal/apierror" + "github.com/agent-substrate/substrate/internal/ateattr" "github.com/agent-substrate/substrate/internal/localca" "github.com/agent-substrate/substrate/internal/resources" "github.com/agent-substrate/substrate/internal/substratex509" @@ -102,7 +103,7 @@ func TestValidateActorStatusCrash(t *testing.T) { name: "truncated crash message fits", oldVal: validActor(withActorStatus()), newVal: validActor(withCrash(func(c *ateapipb.ActorCrash) { - c.Message = newActorCrash("pause", strings.Repeat("x", 2*maxCrashMessageBytes)).GetMessage() + c.Message = newActorCrash(ateattr.OperationSuspend, strings.Repeat("x", 2*maxCrashMessageBytes)).GetMessage() })), }, { diff --git a/cmd/ateapi/internal/controlapi/functionaltest/actor_test.go b/cmd/ateapi/internal/controlapi/functionaltest/actor_test.go index 02f6acd32d..8ee4796b6a 100644 --- a/cmd/ateapi/internal/controlapi/functionaltest/actor_test.go +++ b/cmd/ateapi/internal/controlapi/functionaltest/actor_test.go @@ -5032,7 +5032,7 @@ func TestCheckpointFailureCrashes(t *testing.T) { _, err := tc.client.PauseActor(context.Background(), &ateapipb.PauseActorRequest{Actor: ref}) return err }, - wantMessage: "pause failed: atelet Checkpoint: injected checkpoint failure", + wantMessage: "suspend failed: atelet Checkpoint: injected checkpoint failure", }, { name: "suspend", @@ -5214,7 +5214,7 @@ func TestLifecycleOpPoolAttributesOnSuccess(t *testing.T) { }, { name: "pause", - op: ateattr.OperationPause, + op: ateattr.OperationSuspend, run: func(t *testing.T, tc *testContext, actor *ateapipb.ObjectRef) { t.Helper() if _, err := tc.client.PauseActor(context.Background(), &ateapipb.PauseActorRequest{Actor: actor}); err != nil { diff --git a/cmd/ateapi/internal/controlapi/metrics.go b/cmd/ateapi/internal/controlapi/metrics.go index 709b272a56..0bf5490f0d 100644 --- a/cmd/ateapi/internal/controlapi/metrics.go +++ b/cmd/ateapi/internal/controlapi/metrics.go @@ -161,7 +161,7 @@ func NewInstruments(meter metric.Meter) (*Instruments, error) { lifecycleOpDuration, err := meter.Float64Histogram( lifecycleOpDurationMetric, metric.WithUnit("s"), - metric.WithDescription("Duration of an actor lifecycle operation (create, resume, suspend, pause, delete) handled by ateapi."), + metric.WithDescription("Duration of an actor lifecycle operation (create, resume, suspend, delete, revert) handled by ateapi."), metric.WithExplicitBucketBoundaries(0.005, 0.01, 0.025, 0.05, 0.075, 0.1, 0.15, 0.25, 0.5, 1, 2.5, 5, 10, 30), ) if err != nil { diff --git a/cmd/ateapi/internal/controlapi/metrics_test.go b/cmd/ateapi/internal/controlapi/metrics_test.go index 6c67bfb96c..8448b17f64 100644 --- a/cmd/ateapi/internal/controlapi/metrics_test.go +++ b/cmd/ateapi/internal/controlapi/metrics_test.go @@ -321,7 +321,7 @@ func TestRecordLifecycleOp_OutcomeClassification(t *testing.T) { {name: "wrapped apierror", op: ateattr.OperationResume, err: fmt.Errorf("workflow failed at step Load: %w", apierror.NotFound("missing")), wantErrorType: "NotFound"}, {name: "upstream status is what the caller gets", op: ateattr.OperationResume, err: fmt.Errorf("actor crashed: %w", status.Error(codes.Unavailable, "atelet down")), wantErrorType: "Internal"}, {name: "context deadline", op: ateattr.OperationSuspend, err: fmt.Errorf("while checkpointing: %w", context.DeadlineExceeded), wantErrorType: "DeadlineExceeded"}, - {name: "plain error", op: ateattr.OperationPause, err: errors.New("store is down"), wantErrorType: "Internal"}, + {name: "plain error", op: ateattr.OperationSuspend, err: errors.New("store is down"), wantErrorType: "Internal"}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/cmd/ateapi/internal/controlapi/workflow_pause.go b/cmd/ateapi/internal/controlapi/workflow_pause.go index 072b721afb..56d9fbc42b 100644 --- a/cmd/ateapi/internal/controlapi/workflow_pause.go +++ b/cmd/ateapi/internal/controlapi/workflow_pause.go @@ -50,7 +50,7 @@ func (w *ActorWorkflow) PauseActor(ctx context.Context, actorRef resources.Actor if attrs == nil { attrs = lifecycleOpAttrs(actor, actorTemplate, "", wireFidelity) } - w.instruments.recordLifecycleOp(ctx, ateattr.OperationPause, start, err, attrs...) + w.instruments.recordLifecycleOp(ctx, ateattr.OperationSuspend, start, err, attrs...) }() leaseCtx, lease, err := w.acquireActorLease(ctx, actorRef) @@ -82,7 +82,7 @@ func (w *ActorWorkflow) PauseActor(ctx context.Context, actorRef resources.Actor // TODO: There is no difference between suspend and pause for now, but we // could optimize pause by not detaching. We would need to make sure Resume // is idempotent. - if err = w.ensureVolumesDetached(leaseCtx, actor, actorTemplate, "DetachVolumesForPause", ateattr.OperationPause); err != nil { + if err = w.ensureVolumesDetached(leaseCtx, actor, actorTemplate, "DetachVolumesForPause", ateattr.OperationSuspend); err != nil { return nil, err } // FinalizePaused clears the WorkerAssignment the labels read, so snapshot @@ -160,7 +160,7 @@ func (w *ActorWorkflow) ensureMarkedPausing(ctx context.Context, actorRef resour } return nil, err } - logActorStateChanged(ctx, storedActor, ateattr.OperationPause) + logActorStateChanged(ctx, storedActor, ateattr.OperationSuspend) return storedActor, nil } @@ -177,7 +177,7 @@ func (w *ActorWorkflow) ensureAteletPaused(ctx context.Context, actorRef resourc assignment := actor.GetStatus().GetWorkerAssignment() if assignment == nil { // Missing active worker pod reference mid-pause indicates corrupted store state. - return "", crashActorAndFail(ctx, w.store, actorRef, ateattr.OperationPause, crashMessageWorkerAssignmentMissing) + return "", crashActorAndFail(ctx, w.store, actorRef, ateattr.OperationSuspend, crashMessageWorkerAssignmentMissing) } ateletConn, err := w.dialer.DialForAteletOnNode(assignment.GetNodeName()) @@ -215,7 +215,7 @@ func (w *ActorWorkflow) ensureAteletPaused(ctx context.Context, actorRef resourc wireFidelity = ateattr.SnapshotFidelityValue(req.GetSnapshot().GetFidelity()) if _, err = client.Checkpoint(ctx, req); err != nil { - return wireFidelity, handleAteletError(ctx, w.store, actorRef, ateattr.OperationPause, "Checkpoint", false, err) + return wireFidelity, handleAteletError(ctx, w.store, actorRef, ateattr.OperationSuspend, "Checkpoint", false, err) } return wireFidelity, nil } @@ -247,10 +247,10 @@ func (w *ActorWorkflow) ensurePausedFinalized(ctx context.Context, actorRef reso // so the actor can never be resumed (the scheduler would search for a // worker on an unknown node forever). Crash it instead of committing // SUSPENDED with a snapshot nothing can find. - return nil, crashActorAndFail(ctx, w.store, actorRef, ateattr.OperationPause, crashMessageLocalSnapshotNodeUnknown) + return nil, crashActorAndFail(ctx, w.store, actorRef, ateattr.OperationSuspend, crashMessageLocalSnapshotNodeUnknown) } if snapshotAtLatestGeneration(latestActor.GetStatus()) == nil { - return nil, crashActorAndFail(ctx, w.store, actorRef, ateattr.OperationPause, crashMessageLocalSnapshotMissing) + return nil, crashActorAndFail(ctx, w.store, actorRef, ateattr.OperationSuspend, crashMessageLocalSnapshotMissing) } worker, err := w.store.GetWorker(ctx, assignment.GetWorker().GetName()) @@ -289,7 +289,7 @@ func (w *ActorWorkflow) ensurePausedFinalized(ctx context.Context, actorRef reso } return nil, err } - logActorStateChanged(ctx, storedActor, ateattr.OperationPause) + logActorStateChanged(ctx, storedActor, ateattr.OperationSuspend) latestActor = storedActor } diff --git a/cmd/ateapi/internal/controlapi/workflow_pause_test.go b/cmd/ateapi/internal/controlapi/workflow_pause_test.go index 76be4a5088..d73bf836e9 100644 --- a/cmd/ateapi/internal/controlapi/workflow_pause_test.go +++ b/cmd/ateapi/internal/controlapi/workflow_pause_test.go @@ -71,7 +71,7 @@ func TestPauseActor_Finalized_WorkerGone(t *testing.T) { if got.GetStatus().GetState() != ateapipb.ActorState_ACTOR_STATE_CRASHED { t.Errorf("state = %v, want CRASHED (node name unknown, cannot resume safely)", got.GetStatus().GetState()) } - if msg, want := got.GetStatus().GetCrash().GetMessage(), "pause failed: "+crashMessageLocalSnapshotNodeUnknown; msg != want { + if msg, want := got.GetStatus().GetCrash().GetMessage(), "suspend failed: "+crashMessageLocalSnapshotNodeUnknown; msg != want { t.Errorf("crash message = %q, want %q", msg, want) } if got.GetStatus().GetAssignedNode() != "" { @@ -92,6 +92,9 @@ func TestPauseActor_Finalized_WorkerGone(t *testing.T) { if got := (*records)[0].attrs[string(ateattr.ActorUIDKey)]; got == "" { t.Error("crash record carries no ate.actor.uid") } + if got := (*records)[0].attrs[string(ateattr.ActorOperationNameKey)]; got != ateattr.OperationSuspend { + t.Errorf("crash record %s = %q, want %q", ateattr.ActorOperationNameKey, got, ateattr.OperationSuspend) + } } // TestPauseActor_Finalized_AlreadyCrashed verifies that if the actor was @@ -118,7 +121,7 @@ func TestPauseActor_Finalized_AlreadyCrashed(t *testing.T) { }, }, }) - originalCrash := newActorCrash(ateattr.OperationPause, "original crash reason") + originalCrash := newActorCrash(ateattr.OperationSuspend, "original crash reason") updateDuringCheckpoint(t, atelet, st, actorRef, func(s *ateapipb.ActorStatus) { s.State = ateapipb.ActorState_ACTOR_STATE_CRASHED s.Crash = originalCrash @@ -168,7 +171,7 @@ func TestPauseActor_Finalized_KeepsCrashFromWorkerDelete(t *testing.T) { w, _ := newWireCaptureWorkflow(t, workerDeletedDuringReleaseStore{Interface: st}) _, err := w.PauseActor(ctx, actorRef) - assertConcurrentCrashKept(t, ctx, st, actorRef, err, "pause failed: "+crashMessageWorkerPodGone) + assertConcurrentCrashKept(t, ctx, st, actorRef, err, "suspend failed: "+crashMessageWorkerPodGone) } // TestPauseActor_Finalized_RecordsFidelity verifies pause records the template's @@ -291,7 +294,7 @@ func TestPauseActor_Finalized_MissingLocalSnapshotClearsAssignment(t *testing.T) if got.GetStatus().GetState() != ateapipb.ActorState_ACTOR_STATE_CRASHED { t.Errorf("state = %v, want CRASHED", got.GetStatus().GetState()) } - if msg, want := got.GetStatus().GetCrash().GetMessage(), "pause failed: "+crashMessageLocalSnapshotMissing; msg != want { + if msg, want := got.GetStatus().GetCrash().GetMessage(), "suspend failed: "+crashMessageLocalSnapshotMissing; msg != want { t.Errorf("crash message = %q, want %q", msg, want) } if got.GetStatus().GetWorkerAssignment() != nil { @@ -300,6 +303,9 @@ func TestPauseActor_Finalized_MissingLocalSnapshotClearsAssignment(t *testing.T) if len(*records) != 1 { t.Fatalf("got %d crash records, want 1", len(*records)) } + if got := (*records)[0].attrs[string(ateattr.ActorOperationNameKey)]; got != ateattr.OperationSuspend { + t.Errorf("crash record %s = %q, want %q", ateattr.ActorOperationNameKey, got, ateattr.OperationSuspend) + } } // TestPauseActorWorkflow_RejectedAndIdempotentPaths covers the two @@ -545,7 +551,7 @@ func TestPauseActor_CrashesWhenPausingActorMissingWorkerPod(t *testing.T) { if got.GetStatus().GetState() != ateapipb.ActorState_ACTOR_STATE_CRASHED { t.Errorf("stored state = %v, want %v", got.GetStatus().GetState(), ateapipb.ActorState_ACTOR_STATE_CRASHED) } - if msg, want := got.GetStatus().GetCrash().GetMessage(), "pause failed: "+crashMessageWorkerAssignmentMissing; msg != want { + if msg, want := got.GetStatus().GetCrash().GetMessage(), "suspend failed: "+crashMessageWorkerAssignmentMissing; msg != want { t.Errorf("crash message = %q, want %q", msg, want) } } diff --git a/cmd/ateapi/internal/controlapi/workflow_test.go b/cmd/ateapi/internal/controlapi/workflow_test.go index 1841db374b..3b42a501d2 100644 --- a/cmd/ateapi/internal/controlapi/workflow_test.go +++ b/cmd/ateapi/internal/controlapi/workflow_test.go @@ -38,6 +38,7 @@ func TestActorStateChangeRecords(t *testing.T) { tests := []struct { name string seedState ateapipb.ActorState + seedOpts []func(*ateapipb.Actor) // transition runs the workflow step that commits the new state. transition func(t *testing.T, w *ActorWorkflow, ref resources.ActorRef, actor *ateapipb.Actor, tmpl *ateapipb.ActorTemplate) wantOp string @@ -60,10 +61,34 @@ func TestActorStateChangeRecords(t *testing.T) { transition: func(t *testing.T, w *ActorWorkflow, ref resources.ActorRef, actor *ateapipb.Actor, _ *ateapipb.ActorTemplate) { markOnly(t, w, ref, actor, w.PauseActor) }, - wantOp: ateattr.OperationPause, + wantOp: ateattr.OperationSuspend, wantState: ateattr.ActorStateSuspending, wantStored: ateapipb.ActorState_ACTOR_STATE_SUSPENDING, }, + { + name: "finalize pause marks the actor suspended", + seedState: ateapipb.ActorState_ACTOR_STATE_SUSPENDING, + seedOpts: []func(*ateapipb.Actor){ + withPauseInProgress(), + func(a *ateapipb.Actor) { + a.Status.AssignedNode = "node1" + a.Status.WorkerAssignment = &ateapipb.WorkerAssignment{ + Worker: &ateapipb.ObjectRef{Name: "worker-gone"}, + WorkerNamespace: "default", + WorkerPool: "pool1", + WorkerPod: "worker-pod-1", + } + }, + }, + transition: func(t *testing.T, w *ActorWorkflow, ref resources.ActorRef, _ *ateapipb.Actor, _ *ateapipb.ActorTemplate) { + if _, err := w.ensurePausedFinalized(context.Background(), ref); err != nil { + t.Fatalf("ensurePausedFinalized: %v", err) + } + }, + wantOp: ateattr.OperationSuspend, + wantState: ateattr.ActorStateSuspended, + wantStored: ateapipb.ActorState_ACTOR_STATE_SUSPENDED, + }, { name: "delete marks the actor deleting", seedState: ateapipb.ActorState_ACTOR_STATE_SUSPENDED, @@ -96,7 +121,7 @@ func TestActorStateChangeRecords(t *testing.T) { } actorRef := resources.ActorRef{Atespace: "team-a", Name: "id1"} - seedWorkflowActor(t, ctx, persistence, actorRef, tmplAtespace, tmplName, tt.seedState) + seedWorkflowActor(t, ctx, persistence, actorRef, tmplAtespace, tmplName, tt.seedState, tt.seedOpts...) actor, err := persistence.GetActor(ctx, actorRef) if err != nil { t.Fatalf("get actor: %v", err) diff --git a/cmd/ateapi/internal/controlapi/workflow_worker_delete.go b/cmd/ateapi/internal/controlapi/workflow_worker_delete.go index eddf952f6c..90c5e82e97 100644 --- a/cmd/ateapi/internal/controlapi/workflow_worker_delete.go +++ b/cmd/ateapi/internal/controlapi/workflow_worker_delete.go @@ -209,9 +209,6 @@ func (w *WorkerWorkflow) crashBoundActor(ctx context.Context, worker *ateapipb.W opName = ateattr.OperationResume case ateapipb.ActorState_ACTOR_STATE_SUSPENDING: opName = ateattr.OperationSuspend - if latestSnapshotIsPendingLocal(actor.GetStatus()) { - opName = ateattr.OperationPause - } case ateapipb.ActorState_ACTOR_STATE_REVERTING: opName = ateattr.OperationRevert case ateapipb.ActorState_ACTOR_STATE_DELETING: diff --git a/cmd/ateapi/internal/controlapi/workflow_worker_delete_test.go b/cmd/ateapi/internal/controlapi/workflow_worker_delete_test.go index 5c1493d4db..fab6da3256 100644 --- a/cmd/ateapi/internal/controlapi/workflow_worker_delete_test.go +++ b/cmd/ateapi/internal/controlapi/workflow_worker_delete_test.go @@ -159,11 +159,8 @@ func TestDeleteWorkerWorkflow_ReleasesBoundActor(t *testing.T) { // crashed is released again without being counted twice. func TestDeleteWorkerWorkflow_ReleasedActorStateTransitions(t *testing.T) { tests := []struct { - name string - start ateapipb.ActorState - // pausing seeds the pause-in-progress shape (a pending node-local - // snapshot), which labels a SUSPENDING crash as a pause. - pausing bool + name string + start ateapipb.ActorState wantState ateapipb.ActorState wantOp string wantMetric bool @@ -171,7 +168,6 @@ func TestDeleteWorkerWorkflow_ReleasedActorStateTransitions(t *testing.T) { {name: "running becomes crashed", start: ateapipb.ActorState_ACTOR_STATE_RUNNING, wantState: ateapipb.ActorState_ACTOR_STATE_CRASHED, wantOp: ateattr.OperationUnknown, wantMetric: true}, {name: "resuming becomes crashed", start: ateapipb.ActorState_ACTOR_STATE_RESUMING, wantState: ateapipb.ActorState_ACTOR_STATE_CRASHED, wantOp: ateattr.OperationResume, wantMetric: true}, {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_SUSPENDING, pausing: true, 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}, @@ -187,11 +183,7 @@ func TestDeleteWorkerWorkflow_ReleasedActorStateTransitions(t *testing.T) { ctx := context.Background() wf, persistence := newWorkerDeleteWorkflow(t) seedAPIWorker(t, ctx, persistence, validWorker(apiWorkerName)) - var opts []func(*ateapipb.Actor) - if tc.pausing { - opts = append(opts, withPauseInProgress()) - } - actor := seedAPIActor(t, ctx, persistence, tc.start, opts...) + actor := seedAPIActor(t, ctx, persistence, tc.start) assignAPIWorker(t, ctx, persistence, apiWorkerName, actor.GetMetadata().GetUid()) if _, err := wf.DeleteWorker(ctx, apiWorkerName, store.DeletePreconditions{}); err != nil { diff --git a/docs/metrics/registry/metrics.yaml b/docs/metrics/registry/metrics.yaml index 3d4a2f0f32..850cb3d1e0 100644 --- a/docs/metrics/registry/metrics.yaml +++ b/docs/metrics/registry/metrics.yaml @@ -150,11 +150,7 @@ groups: - id: suspend stability: development value: suspend - brief: ateapi wrote the durable snapshot and released the worker. - - id: pause - stability: development - value: pause - brief: ateapi wrote a node-local snapshot. + brief: ateapi wrote a snapshot, durable or node-local, and released the worker. - id: delete stability: development value: delete @@ -167,9 +163,9 @@ groups: stability: development value: unknown 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. + No lifecycle operation is running, such as when a running 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 @@ -817,14 +813,14 @@ groups: 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 + resume and a suspend, the load step also reads the ActorTemplate. - ref: ate.template.name 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 + resume and a suspend, the load step also reads the ActorTemplate. - ref: ate.workerpool.namespace requirement_level: @@ -835,7 +831,7 @@ groups: - ref: ate.sandbox.class requirement_level: conditionally_required: > - The operation is resume, suspend, pause or revert, and ateapi found + The operation is resume, suspend 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 @@ -843,7 +839,7 @@ groups: 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 operation is resume, suspend or pause, and the fidelity is known. + conditionally_required: The operation is resume or suspend, and the fidelity is known. - ref: error.type requirement_level: conditionally_required: The operation failed. If this key is absent, the operation was correct. diff --git a/docs/observability.md b/docs/observability.md index c1b460be12..36b868560f 100644 --- a/docs/observability.md +++ b/docs/observability.md @@ -173,7 +173,7 @@ ateapi's `Actor state changed` is written once per committed actor state transit `ate.actor.state` takes the `ateapipb.ActorState` values lowercased, so the log vocabulary and the state machine cannot fork. The last record for an actor's uid is the state it is in now, and its timestamp is when that state began. Query it per actor, not in aggregate: neither key is a metric label, because both only ever appear beside actor identity, which [the cardinality rules](#the-metric-registry) keep off metrics entirely. -`ate.actor.operation.name` says which operation drove the transition, which the state alone does not: an actor reaches `suspended` from a suspend and `paused` from a pause, and the two differ in whether the worker was released. +`ate.actor.operation.name` says which operation drove the transition, which the state alone does not: an actor reaches `suspended` both when it is created and when it is suspended. PauseActor and SuspendActor are one operation and are both labeled `suspend`. The record goes out after the store commit, never before, and the state is read straight off the committed record rather than named by the caller. Every state commit has a version check too, so if two writers race, the one that lost writes nothing. You will never see a state here that the store did not actually hold. @@ -279,7 +279,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, 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.actor.lifecycle.operation.duration` | ateapi | histogram | how long each actor operation (create/resume/suspend/delete/revert; a pause is labeled suspend) 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 and suspend; 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 ateworker or to the upload (same labels as the restore histogram) | diff --git a/internal/ateattr/ateattr.go b/internal/ateattr/ateattr.go index ce27d09ee8..97aa5d730a 100644 --- a/internal/ateattr/ateattr.go +++ b/internal/ateattr/ateattr.go @@ -276,7 +276,6 @@ const ( OperationCreate = "create" OperationResume = "resume" OperationSuspend = "suspend" - OperationPause = "pause" OperationDelete = "delete" OperationRevert = "revert" OperationUnknown = "unknown" @@ -287,7 +286,6 @@ var AllOperations = []string{ OperationCreate, OperationResume, OperationSuspend, - OperationPause, OperationDelete, OperationRevert, } diff --git a/internal/ateattr/ateattr_test.go b/internal/ateattr/ateattr_test.go index b57f8275e1..8c1ec78826 100644 --- a/internal/ateattr/ateattr_test.go +++ b/internal/ateattr/ateattr_test.go @@ -377,8 +377,8 @@ func TestMetricLabelValues(t *testing.T) { {OperationCreate, "create"}, {OperationResume, "resume"}, {OperationSuspend, "suspend"}, - {OperationPause, "pause"}, {OperationDelete, "delete"}, + {OperationRevert, "revert"}, {OperationUnknown, "unknown"}, {ImageCacheOutcomeHit, "hit"}, @@ -721,8 +721,9 @@ func TestNormalizeOperationName(t *testing.T) { {OperationCreate, OperationCreate}, {OperationResume, OperationResume}, {OperationSuspend, OperationSuspend}, - {OperationPause, OperationPause}, + {"pause", OperationUnknown}, {OperationDelete, OperationDelete}, + {OperationRevert, OperationRevert}, {"", OperationUnknown}, {"invalid", OperationUnknown}, {"crash", OperationUnknown}, diff --git a/internal/e2e/suites/metrics/metrics_test.go b/internal/e2e/suites/metrics/metrics_test.go index 737e2257e1..ed15fbbd1d 100644 --- a/internal/e2e/suites/metrics/metrics_test.go +++ b/internal/e2e/suites/metrics/metrics_test.go @@ -200,7 +200,7 @@ func TestPlatformMetricsEmitted(t *testing.T) { if opVal == "" { crashErrs = append(crashErrs, "ate_actor_operation_name label is missing or empty") } else if ateattr.NormalizeOperationName(opVal) != opVal { - crashErrs = append(crashErrs, fmt.Sprintf("ate_actor_operation_name %q is invalid (must be one of {create, resume, suspend, pause, delete, unknown})", opVal)) + crashErrs = append(crashErrs, fmt.Sprintf("ate_actor_operation_name %q is invalid (must be one of {create, resume, suspend, delete, revert, unknown})", opVal)) } if tmplAtespaceVal == "" { From c673b8a3faa8a023daa4f160ed01f0cb13a6e9bd Mon Sep 17 00:00:00 2001 From: Haven Xia Date: Thu, 15 Oct 2026 18:55:41 +0000 Subject: [PATCH 06/31] Update unknown request rules in api-style-guide (#2711) Match the actual behavior of handling unknown fields: reject if server side doesn't know, preserve if client doesn't know. Found this when iterating version skew doc. Part of #1185 - [ ] Tests pass - [ ] Appropriate changes to documentation are included in the PR --- docs/api-style-guide.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api-style-guide.md b/docs/api-style-guide.md index 641c110a11..c78189b57f 100644 --- a/docs/api-style-guide.md +++ b/docs/api-style-guide.md @@ -282,7 +282,7 @@ Rules: - **Immutable** fields — caller-set at creation but fixed thereafter (`atespace`, `name`, and resource-specific ones such as an actor's `source_snapshot_tag`). A request that changes one - including by omitting it, which would clear it - **must** return `INVALID_ARGUMENT` naming the field. - The embedded resource field **must** be named after the resource's own snake_case type name (e.g. `actor` for `Actor`), not a generic name like `resource` or `body`. - The request **must** have no other fields, apart from its parent's `ObjectRef` for a subresource (see section #2.4). -- Unknown fields in the request are preserved, so a client built against a newer schema does not lose data by round-tripping through an older one. +- A request carrying a field the server does not know, at any level of nesting, **must** return `INVALID_ARGUMENT` naming the field, as for every RPC. Fields only the client does not know are a different case: a client that modifies the resource it read sends them back unchanged, so an update from an older client does not clear fields a newer server set. - The resource's `atespace` and `name` identify the resource to update; they are not themselves updatable. - If the resource does not exist: return `NOT_FOUND`. - The `version` and `uid` fields in the embedded resource's `metadata` are **required** preconditions (see section #7). A request that leaves either unset is a blind write and **must** return `INVALID_ARGUMENT`. They are control fields the client echoes back as guards, not fields it edits. From 8e157bb972678949b8b005955bf4de0cee9b2446 Mon Sep 17 00:00:00 2001 From: Pancho Trujillo Date: Thu, 15 Oct 2026 19:13:52 +0000 Subject: [PATCH 07/31] ate-setup: use the envoy-dataplane image in atenet-router (#2655) Replaces the stock `envoyproxy/envoy` image reference in `manifests/ate-install/atenet-router.yaml` with the `${ENVOY_DATAPLANE_IMAGE}` placeholder so both `atenet-egress` and `atenet-router` deploy the same `envoy-dataplane` image built from `cmd/dataplane/envoy/Dockerfile` (or pinned from a release via `--image-repo`). - Updates `renderAtenetRouterManifest` and `renderSystemManifests` in `cmd/ate-setup` to resolve and substitute `${ENVOY_DATAPLANE_IMAGE}`. - Caches resolved Dockerfile image references on `steps.Env` so deployments rendering multiple manifests (`atenet-router` + `atenet-egress` + system overlay) only build or pin `envoy-dataplane` once. - Extends `TestRenderAtenetEgressManifestPrebuilt` to verify `${ENVOY_DATAPLANE_IMAGE}` substitution across router, egress, and system manifests. Part of #1330 AI was used in the development of this change. - [x] Tests pass - [x] Appropriate changes to documentation are included in the PR --- cmd/ate-setup/internal/steps/env.go | 38 ++++++++++++---- cmd/ate-setup/internal/steps/overlay.go | 18 +++++++- cmd/ate-setup/internal/steps/overlay_test.go | 48 ++++++++++++++------ manifests/ate-install/atenet-router.yaml | 3 +- 4 files changed, 81 insertions(+), 26 deletions(-) diff --git a/cmd/ate-setup/internal/steps/env.go b/cmd/ate-setup/internal/steps/env.go index dc51749a5c..714dc08fb2 100644 --- a/cmd/ate-setup/internal/steps/env.go +++ b/cmd/ate-setup/internal/steps/env.go @@ -80,6 +80,11 @@ 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 + + // dockerfileImages caches resolved Dockerfile image references by image + // name so a multi-manifest deploy (e.g., router + egress) builds or pins + // each Dockerfile image once. + dockerfileImages map[string]string } // Namespace is the namespace the control plane is installed into. It is @@ -163,22 +168,35 @@ func (e *Env) koRunner() (*ko.Runner, error) { // That only works if the release published it there, which // `ate-setup publish release-images` does. func (e *Env) dockerfileImage(ctx context.Context, name, contextDir string) (string, error) { + if ref, ok := e.dockerfileImages[name]; ok { + return ref, nil + } + var ref string + var err error if !e.Cfg.Images.IsPrebuilt() { - return images.BuildDockerfileImage(ctx, e.Cfg.Root, e.Cfg.KODockerRepo, name, e.Cfg.Path(contextDir), e.Cfg.KODefaultPlatforms, e.Cfg.DockerBuildFlags) + ref, err = images.BuildDockerfileImage(ctx, e.Cfg.Root, e.Cfg.KODockerRepo, name, e.Cfg.Path(contextDir), e.Cfg.KODefaultPlatforms, e.Cfg.DockerBuildFlags) + } else { + resolver, rErr := e.imageResolver() + if rErr != nil { + return "", rErr + } + prebuilt, ok := resolver.(*images.Prebuilt) + if !ok { + return "", fmt.Errorf("image resolver is %T, not a pre-built resolver", resolver) + } + ref, err = prebuilt.Pin(ctx, name) + if err != nil { + return "", fmt.Errorf("%s is built from a Dockerfile, not with ko, so a release has to publish it "+ + "alongside the other images (ate-setup publish release-images): %w", name, err) + } } - resolver, err := e.imageResolver() if err != nil { return "", err } - prebuilt, ok := resolver.(*images.Prebuilt) - if !ok { - return "", fmt.Errorf("image resolver is %T, not a pre-built resolver", resolver) - } - ref, err := prebuilt.Pin(ctx, name) - if err != nil { - return "", fmt.Errorf("%s is built from a Dockerfile, not with ko, so a release has to publish it "+ - "alongside the other images (ate-setup publish release-images): %w", name, err) + if e.dockerfileImages == nil { + e.dockerfileImages = make(map[string]string) } + e.dockerfileImages[name] = ref return ref, nil } diff --git a/cmd/ate-setup/internal/steps/overlay.go b/cmd/ate-setup/internal/steps/overlay.go index 4d38363c01..cdf2f854b7 100644 --- a/cmd/ate-setup/internal/steps/overlay.go +++ b/cmd/ate-setup/internal/steps/overlay.go @@ -147,6 +147,13 @@ func (e *Env) renderSystemManifests(ctx context.Context) ([]byte, error) { if err != nil { return nil, err } + if e.Cfg.Router != config.RouterAgentgateway { + imageReference, err := e.dockerfileImage(ctx, envoyDataplaneImage, envoyDataplaneDockefile) + if err != nil { + return nil, err + } + manifest = e.patchEnvoyDataplaneImage(manifest, imageReference) + } return e.ResolveManifestBytes(ctx, manifest) } @@ -156,7 +163,16 @@ func (e *Env) renderAtenetRouterManifest(ctx context.Context) ([]byte, error) { if e.Cfg.Router == config.RouterAgentgateway { return e.renderResolve(ctx, e.Cfg.Path(installDir+"/agentgateway-router")) } - return e.renderResolve(ctx, e.Cfg.Manifest("atenet-router.yaml")) + manifest, err := e.render(e.Cfg.Manifest("atenet-router.yaml")) + if err != nil { + return nil, err + } + imageReference, err := e.dockerfileImage(ctx, envoyDataplaneImage, envoyDataplaneDockefile) + if err != nil { + return nil, err + } + manifest = e.patchEnvoyDataplaneImage(manifest, imageReference) + return e.ResolveManifestBytes(ctx, manifest) } // atenetEgressManifestPath returns the envoy egress gateway manifest. diff --git a/cmd/ate-setup/internal/steps/overlay_test.go b/cmd/ate-setup/internal/steps/overlay_test.go index d3ec4ad261..fdb7cd7d4f 100644 --- a/cmd/ate-setup/internal/steps/overlay_test.go +++ b/cmd/ate-setup/internal/steps/overlay_test.go @@ -1134,9 +1134,9 @@ func TestApplyOtelEndpointOverride(t *testing.T) { }) } -// A pre-built install renders the envoy egress manifest without building -// anything: envoy-dataplane is pinned from the release like every ko image, so -// neither docker nor KO_DOCKER_REPO is needed. +// A pre-built install renders the envoy router, egress, and system manifests +// without building anything: envoy-dataplane is pinned from the release like +// every ko image, so neither docker nor KO_DOCKER_REPO is needed. func TestRenderAtenetEgressManifestPrebuilt(t *testing.T) { const digest = "sha256:2222222222222222222222222222222222222222222222222222222222222222" src := images.Source{Repo: "example.com/substrate", Tag: "v1.2.3"} @@ -1149,18 +1149,38 @@ func TestRenderAtenetEgressManifestPrebuilt(t *testing.T) { }), } - out, err := e.renderAtenetEgressManifest(t.Context(), config.CredentialProvider{}) - if err != nil { - t.Fatalf("renderAtenetEgressManifest() error = %v", err) - } want := "example.com/substrate/envoy-dataplane:v1.2.3@" + digest - if !strings.Contains(string(out), "image: "+want) { - t.Errorf("rendered manifest does not install %s:\n%s", want, out) - } - for _, leftover := range []string{"${ENVOY_DATAPLANE_IMAGE}", "ko://"} { - if strings.Contains(string(out), leftover) { - t.Errorf("rendered manifest still contains %q", leftover) - } + for _, tc := range []struct { + name string + render func() ([]byte, error) + }{ + { + name: "egress", + render: func() ([]byte, error) { return e.renderAtenetEgressManifest(t.Context(), config.CredentialProvider{}) }, + }, + { + name: "router", + render: func() ([]byte, error) { return e.renderAtenetRouterManifest(t.Context()) }, + }, + { + name: "system", + render: func() ([]byte, error) { return e.renderSystemManifests(t.Context()) }, + }, + } { + t.Run(tc.name, func(t *testing.T) { + out, err := tc.render() + if err != nil { + t.Fatalf("render() error = %v", err) + } + if !strings.Contains(string(out), "image: "+want) { + t.Errorf("rendered manifest does not install %s:\n%s", want, out) + } + for _, leftover := range []string{"${ENVOY_DATAPLANE_IMAGE}", "ko://"} { + if strings.Contains(string(out), leftover) { + t.Errorf("rendered manifest still contains %q", leftover) + } + } + }) } if !slices.Contains(looked, "example.com/substrate/envoy-dataplane:v1.2.3") { t.Errorf("registry lookups = %v, want one for envoy-dataplane", looked) diff --git a/manifests/ate-install/atenet-router.yaml b/manifests/ate-install/atenet-router.yaml index 3310864aac..7eb2de55ea 100644 --- a/manifests/ate-install/atenet-router.yaml +++ b/manifests/ate-install/atenet-router.yaml @@ -321,7 +321,8 @@ spec: - name: "drain-signal" mountPath: "/var/run/atenet" - name: envoy - image: envoyproxy/envoy:v1.39.4@sha256:74bd4eb5629708d3fadca1a5e6b85d208f338d7bb54e06b69565905c5294cc01 + # ENVOY_DATAPLANE_IMAGE is substituted for the image URI by ate-setup + image: ${ENVOY_DATAPLANE_IMAGE} command: - "/usr/local/bin/envoy" - "-c" From b83d607cbbc9442e683f22af3eb099246bfa6ce7 Mon Sep 17 00:00:00 2001 From: Taahir Ahmed Date: Thu, 15 Oct 2026 19:40:12 +0000 Subject: [PATCH 08/31] Router authn/z part 2.5: podcertcontroller: Don't load pod in podidentity signer (#2389) We should rely on the checks enforced by kube-apiserver. We don't need to double-check whether the pod exists. Upcoming e2e test changes will rely on the fact that non-Kubelet clients are allowed to create arbitary PodCertificateRequests, even if the pod or service account they reference does not exist. This will allow the test code running in Github Actions (or on the developer's machine) to authenticate to the router running within the cluster. --- .../podidentitysigner/podidentitysigner.go | 20 ++----- .../podidentitysigner_test.go | 58 +++++-------------- cmd/podcertcontroller/main.go | 2 +- 3 files changed, 22 insertions(+), 58 deletions(-) diff --git a/cmd/podcertcontroller/internal/podidentitysigner/podidentitysigner.go b/cmd/podcertcontroller/internal/podidentitysigner/podidentitysigner.go index ffd03105af..b253468558 100644 --- a/cmd/podcertcontroller/internal/podidentitysigner/podidentitysigner.go +++ b/cmd/podcertcontroller/internal/podidentitysigner/podidentitysigner.go @@ -33,7 +33,6 @@ import ( certsv1 "k8s.io/api/certificates/v1" certsv1beta1 "k8s.io/api/certificates/v1beta1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/client-go/kubernetes" "k8s.io/utils/ptr" ) @@ -41,14 +40,12 @@ const Name = "podidentity.podcert.ate.dev/identity" const CTBPrefix = "podidentity.podcert.ate.dev:identity:" type Impl struct { - kc kubernetes.Interface pcrClient *podcertificate.Client caPool localca.Pool } -func NewImpl(kc kubernetes.Interface, caPool localca.Pool, pcrClient *podcertificate.Client) *Impl { +func NewImpl(caPool localca.Pool, pcrClient *podcertificate.Client) *Impl { return &Impl{ - kc: kc, pcrClient: pcrClient, caPool: caPool, } @@ -96,16 +93,11 @@ func (h *Impl) DesiredClusterTrustBundles() ([]*certsv1.ClusterTrustBundle, erro } func (h *Impl) MakeCert(ctx context.Context, pcr *certsv1beta1.PodCertificateRequest) error { - // Fetch the pod to get its ServiceAccount - pod, err := h.kc.CoreV1().Pods(pcr.ObjectMeta.Namespace).Get(ctx, pcr.Spec.PodName, metav1.GetOptions{}) - if err != nil { - return fmt.Errorf("while getting pod %s/%s: %w", pcr.ObjectMeta.Namespace, pcr.Spec.PodName, err) - } - - if pod.ObjectMeta.UID != pcr.Spec.PodUID { - return fmt.Errorf("pod UID mismatch: expected %s, got %s", pcr.Spec.PodUID, pod.ObjectMeta.UID) - } - + // The identity comes from the PCR spec alone. kube-apiserver attests it + // for kubelet-created requests (the pod, ServiceAccount and node exist and + // belong together); anyone else needs RBAC permission to create + // PodCertificateRequests and may name any identity, which is how + // non-kubelet clients such as the e2e suites obtain certificates. subjectPublicKey, err := podcertificate.PublicKey(pcr) if err != nil { return err diff --git a/cmd/podcertcontroller/internal/podidentitysigner/podidentitysigner_test.go b/cmd/podcertcontroller/internal/podidentitysigner/podidentitysigner_test.go index 0de8d3d0e5..cf14602149 100644 --- a/cmd/podcertcontroller/internal/podidentitysigner/podidentitysigner_test.go +++ b/cmd/podcertcontroller/internal/podidentitysigner/podidentitysigner_test.go @@ -31,7 +31,6 @@ import ( "github.com/agent-substrate/substrate/internal/localca" "github.com/agent-substrate/substrate/internal/substratex509" certsv1beta1 "k8s.io/api/certificates/v1beta1" - corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" @@ -40,25 +39,19 @@ import ( "k8s.io/utils/ptr" ) -// makePodAndPCR returns a pod and a matching PodCertificateRequest with no -// key material set; callers fill in StubPKCS10Request. -func makePodAndPCR(namespace, podName, serviceAccount string, maxExpirationSeconds int32) (*corev1.Pod, *certsv1beta1.PodCertificateRequest) { - pod := &corev1.Pod{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: namespace, - Name: podName, - UID: types.UID("pod-uid-1"), - }, - } - pcr := &certsv1beta1.PodCertificateRequest{ +// makePCR returns a PodCertificateRequest with no key material set; callers +// fill in StubPKCS10Request. The pod, ServiceAccount and node it names exist +// nowhere: the signer takes the identity from the request alone. +func makePCR(namespace, podName, serviceAccount string, maxExpirationSeconds int32) *certsv1beta1.PodCertificateRequest { + return &certsv1beta1.PodCertificateRequest{ ObjectMeta: metav1.ObjectMeta{ Namespace: namespace, Name: "req-1", }, Spec: certsv1beta1.PodCertificateRequestSpec{ SignerName: Name, - PodName: pod.ObjectMeta.Name, - PodUID: pod.ObjectMeta.UID, + PodName: podName, + PodUID: types.UID("pod-uid-1"), ServiceAccountName: serviceAccount, ServiceAccountUID: types.UID("sa-uid-1"), NodeName: types.NodeName("node-1"), @@ -66,7 +59,6 @@ func makePodAndPCR(namespace, podName, serviceAccount string, maxExpirationSecon MaxExpirationSeconds: ptr.To(maxExpirationSeconds), }, } - return pod, pcr } // stubCSR returns a stub PKCS#10 request carrying priv's public key. @@ -85,7 +77,6 @@ func TestMakeCert(t *testing.T) { namespace string podName string serviceAccount string - podLabels map[string]string maxExpirationSeconds int32 wantLifetime time.Duration wantURI string @@ -118,7 +109,6 @@ func TestMakeCert(t *testing.T) { namespace: "ate-demo-counter", podName: "counter-abcde", serviceAccount: "default", - podLabels: map[string]string{"ate.dev/worker-pool": "counter"}, maxExpirationSeconds: 86400, wantLifetime: 24 * time.Hour, wantURI: "spiffe://cluster.local/ns/ate-demo-counter/sa/default", @@ -206,12 +196,12 @@ func TestMakeCert(t *testing.T) { t.Fatalf("while generating subject key: %v", err) } - pod, pcr := makePodAndPCR(tc.namespace, tc.podName, tc.serviceAccount, tc.maxExpirationSeconds) - pod.ObjectMeta.Labels = tc.podLabels + pcr := makePCR(tc.namespace, tc.podName, tc.serviceAccount, tc.maxExpirationSeconds) pcr.Spec.StubPKCS10Request = stubCSR(t, subjectPriv) - kc := fake.NewSimpleClientset(pod, pcr) - impl := NewImpl(kc, caPool, betaClient(t, kc)) + // Only the PCR exists: the signer must not need the pod. + kc := fake.NewSimpleClientset(pcr) + impl := NewImpl(caPool, betaClient(t, kc)) if err := impl.MakeCert(context.Background(), pcr); err != nil { t.Fatalf("MakeCert: %v", err) @@ -296,28 +286,15 @@ func TestMakeCert(t *testing.T) { func TestMakeCertErrors(t *testing.T) { testCases := []struct { name string - omitPod bool - podUID types.UID omitKey bool failUpdate bool }{ - { - name: "pod not found", - omitPod: true, - podUID: "pod-uid-1", - }, - { - name: "pod UID mismatch", - podUID: "other-uid", - }, { name: "no key material in PCR", - podUID: "pod-uid-1", omitKey: true, }, { name: "status update fails", - podUID: "pod-uid-1", failUpdate: true, }, } @@ -330,8 +307,7 @@ func TestMakeCertErrors(t *testing.T) { } caPool := &localca.ConcretePool{CAs: []*localca.CA{ca}} - pod, pcr := makePodAndPCR("ate-system", "atelet-abcde", "atelet", 86400) - pod.ObjectMeta.UID = tc.podUID + pcr := makePCR("ate-system", "atelet-abcde", "atelet", 86400) if !tc.omitKey { _, subjectPriv, err := ed25519.GenerateKey(rand.Reader) if err != nil { @@ -340,17 +316,13 @@ func TestMakeCertErrors(t *testing.T) { pcr.Spec.StubPKCS10Request = stubCSR(t, subjectPriv) } - objects := []runtime.Object{pcr} - if !tc.omitPod { - objects = append(objects, pod) - } - kc := fake.NewSimpleClientset(objects...) + kc := fake.NewSimpleClientset(pcr) if tc.failUpdate { kc.PrependReactor("update", "podcertificaterequests", func(action k8stesting.Action) (bool, runtime.Object, error) { return true, nil, errors.New("injected update failure") }) } - impl := NewImpl(kc, caPool, betaClient(t, kc)) + impl := NewImpl(caPool, betaClient(t, kc)) if err := impl.MakeCert(context.Background(), pcr); err == nil { t.Fatalf("MakeCert: got nil error, want error") @@ -377,7 +349,7 @@ func TestDesiredClusterTrustBundles(t *testing.T) { t.Fatalf("while generating CA 2: %v", err) } caPool := &localca.ConcretePool{CAs: []*localca.CA{ca1, ca2}} - impl := NewImpl(nil, caPool, nil) + impl := NewImpl(caPool, nil) ctbs, err := impl.DesiredClusterTrustBundles() if err != nil { diff --git a/cmd/podcertcontroller/main.go b/cmd/podcertcontroller/main.go index 73b7197ad7..6a240bae9a 100644 --- a/cmd/podcertcontroller/main.go +++ b/cmd/podcertcontroller/main.go @@ -199,7 +199,7 @@ func main() { slog.ErrorContext(ctx, "Error loading podidentity.podcert.ate.dev/identity CA pool state", slog.Any("err", err)) os.Exit(1) } - podIdentitySignerController := signercontroller.New(clock.RealClock{}, podidentitysigner.NewImpl(kc, podIdentityCAPool, pcrClient), hasher, pcrClient, trustBundles) + podIdentitySignerController := signercontroller.New(clock.RealClock{}, podidentitysigner.NewImpl(podIdentityCAPool, pcrClient), hasher, pcrClient, trustBundles) postgresCAPool, err := localca.NewRefreshingPool(*postgresCAPoolFile) if err != nil { slog.ErrorContext(ctx, "Error loading PostgreSQL CA pool state", slog.Any("err", err)) From a68c7e1d9fdf773ebf7b78e49a879e1ee7ecb31e Mon Sep 17 00:00:00 2001 From: igooch Date: Thu, 15 Oct 2026 20:57:30 +0000 Subject: [PATCH 09/31] ate-api-server can apply the snapshot state Checkpoint returns, and logs stale snapshot reports (#2698) Part of Actor Lifecycle v2 #2069, for #2570. atelet's `Checkpoint` response carries the actor's snapshot state. ate-api-server discards it today. This adds what's needed to apply it, with the same rules `ReportActorSnapshotState` uses. It also logs reports that change nothing. - **Conversion.** atelet's `SnapshotReport` nests an atelet `Snapshot`. The control plane's carries the same fields inline, with its own fidelity enum and object type. `snapshotReportFromAtelet` converts one to the other, and `fidelityFromAtelet` maps the fidelity back. - **`applyCheckpointState`** records an upload the state says finished, through `applyLatestDurable`. atelet echoes the `Snapshot` it was asked for, so the fidelity recorded is the one requested. Unlike a report, a state with no local snapshot is not a detach, since the workflow that called `Checkpoint` records where its snapshot lives. - **Stale reports are logged at Info.** A `latest_durable` older than the latest `COMPLETED` generation, or at a generation the actor no longer records, changes nothing. That's expected for a late report, so each case is logged rather than refused. A repeat of the latest isn't logged, since atelet retries reports freely. Nothing calls `applyCheckpointState` yet. A follow-up applies it in the suspend workflow. - [ ] Tests pass - [ ] Appropriate changes to documentation are included in the PR --- cmd/ateapi/internal/controlapi/converter.go | 36 ++++ .../internal/controlapi/converter_test.go | 82 +++++++ .../internal/controlapi/snapshot_report.go | 37 +++- .../controlapi/snapshot_report_test.go | 202 +++++++++++++++++- 4 files changed, 351 insertions(+), 6 deletions(-) diff --git a/cmd/ateapi/internal/controlapi/converter.go b/cmd/ateapi/internal/controlapi/converter.go index e30d192a01..bc391b03e6 100644 --- a/cmd/ateapi/internal/controlapi/converter.go +++ b/cmd/ateapi/internal/controlapi/converter.go @@ -34,3 +34,39 @@ func fidelityToAtelet(in ateapipb.SnapshotFidelity) ateletpb.SnapshotFidelity { return ateletpb.SnapshotFidelity_SNAPSHOT_FIDELITY_UNSPECIFIED } } + +// fidelityFromAtelet maps atelet's fidelity enum back onto the identical +// public one. Anything else comes back UNSPECIFIED. +func fidelityFromAtelet(in ateletpb.SnapshotFidelity) ateapipb.SnapshotFidelity { + switch in { + case ateletpb.SnapshotFidelity_SNAPSHOT_FIDELITY_VOLUMES: + return ateapipb.SnapshotFidelity_SNAPSHOT_FIDELITY_VOLUMES + case ateletpb.SnapshotFidelity_SNAPSHOT_FIDELITY_ROOTFS: + return ateapipb.SnapshotFidelity_SNAPSHOT_FIDELITY_ROOTFS + case ateletpb.SnapshotFidelity_SNAPSHOT_FIDELITY_MEMORY: + return ateapipb.SnapshotFidelity_SNAPSHOT_FIDELITY_MEMORY + default: + return ateapipb.SnapshotFidelity_SNAPSHOT_FIDELITY_UNSPECIFIED + } +} + +// snapshotReportFromAtelet converts a snapshot atelet reports into the form +// ReportActorSnapshotState takes, which carries the snapshot's fields inline. +// A report that names no snapshot converts to nil. +func snapshotReportFromAtelet(r *ateletpb.SnapshotReport) *ateapipb.SnapshotReport { + if r.GetSnapshot() == nil { + return nil + } + snap := r.GetSnapshot() + out := &ateapipb.SnapshotReport{ + Uuid: snap.GetSnapshotUuid(), + Generation: snap.GetGeneration(), + Fidelity: fidelityFromAtelet(snap.GetFidelity()), + LocalCommitTime: r.GetLocalCommitTime(), + DurableCompleteTime: r.GetDurableCompleteTime(), + } + if obj := snap.GetObject(); obj != nil { + out.Object = &ateapipb.ObjectSnapshot{SnapshotUri: obj.GetSnapshotUri()} + } + return out +} diff --git a/cmd/ateapi/internal/controlapi/converter_test.go b/cmd/ateapi/internal/controlapi/converter_test.go index c9ef4c0e4a..e99ed00583 100644 --- a/cmd/ateapi/internal/controlapi/converter_test.go +++ b/cmd/ateapi/internal/controlapi/converter_test.go @@ -16,6 +16,11 @@ package controlapi import ( "testing" + "time" + + "github.com/google/go-cmp/cmp" + "google.golang.org/protobuf/testing/protocmp" + "google.golang.org/protobuf/types/known/timestamppb" "github.com/agent-substrate/substrate/internal/proto/ateletpb" "github.com/agent-substrate/substrate/pkg/proto/ateapipb" @@ -65,3 +70,80 @@ func TestFidelityToAtelet(t *testing.T) { }) } } + +// TestFidelityFromAtelet verifies each atelet fidelity maps back to the same +// public level, and anything else to UNSPECIFIED. +func TestFidelityFromAtelet(t *testing.T) { + tests := []struct { + name string + in ateletpb.SnapshotFidelity + want ateapipb.SnapshotFidelity + }{ + {"memory", ateletpb.SnapshotFidelity_SNAPSHOT_FIDELITY_MEMORY, ateapipb.SnapshotFidelity_SNAPSHOT_FIDELITY_MEMORY}, + {"rootfs", ateletpb.SnapshotFidelity_SNAPSHOT_FIDELITY_ROOTFS, ateapipb.SnapshotFidelity_SNAPSHOT_FIDELITY_ROOTFS}, + {"volumes", ateletpb.SnapshotFidelity_SNAPSHOT_FIDELITY_VOLUMES, ateapipb.SnapshotFidelity_SNAPSHOT_FIDELITY_VOLUMES}, + {"unspecified stays unspecified", ateletpb.SnapshotFidelity_SNAPSHOT_FIDELITY_UNSPECIFIED, ateapipb.SnapshotFidelity_SNAPSHOT_FIDELITY_UNSPECIFIED}, + {"value outside the enum", ateletpb.SnapshotFidelity(99), ateapipb.SnapshotFidelity_SNAPSHOT_FIDELITY_UNSPECIFIED}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := fidelityFromAtelet(tt.in); got != tt.want { + t.Errorf("fidelityFromAtelet(%v) = %v, want %v", tt.in, got, tt.want) + } + }) + } +} + +func TestSnapshotReportFromAtelet(t *testing.T) { + committed := timestamppb.New(time.Date(2026, 10, 14, 12, 0, 0, 0, time.UTC)) + uploaded := timestamppb.New(time.Date(2026, 10, 14, 12, 5, 0, 0, time.UTC)) + tests := []struct { + name string + in *ateletpb.SnapshotReport + want *ateapipb.SnapshotReport + }{{ + name: "every field", + in: &ateletpb.SnapshotReport{ + Snapshot: &ateletpb.Snapshot{ + SnapshotUuid: "snap-2", + Generation: 2, + Fidelity: ateletpb.SnapshotFidelity_SNAPSHOT_FIDELITY_ROOTFS, + Object: &ateletpb.ObjectStorage{SnapshotUri: "gs://snapshots/snap-2"}, + }, + LocalCommitTime: committed, + DurableCompleteTime: uploaded, + }, + want: &ateapipb.SnapshotReport{ + Uuid: "snap-2", + Generation: 2, + Fidelity: ateapipb.SnapshotFidelity_SNAPSHOT_FIDELITY_ROOTFS, + Object: &ateapipb.ObjectSnapshot{SnapshotUri: "gs://snapshots/snap-2"}, + LocalCommitTime: committed, + DurableCompleteTime: uploaded, + }, + }, { + name: "a local snapshot has no object", + in: &ateletpb.SnapshotReport{ + Snapshot: &ateletpb.Snapshot{SnapshotUuid: "snap-3", Generation: 3, Fidelity: ateletpb.SnapshotFidelity_SNAPSHOT_FIDELITY_MEMORY}, + LocalCommitTime: committed, + }, + want: &ateapipb.SnapshotReport{ + Uuid: "snap-3", + Generation: 3, + Fidelity: ateapipb.SnapshotFidelity_SNAPSHOT_FIDELITY_MEMORY, + LocalCommitTime: committed, + }, + }, { + name: "a report that names no snapshot converts to nil", + in: &ateletpb.SnapshotReport{LocalCommitTime: committed}, + }, { + name: "nil stays nil", + }} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if diff := cmp.Diff(tt.want, snapshotReportFromAtelet(tt.in), protocmp.Transform()); diff != "" { + t.Errorf("snapshotReportFromAtelet mismatch (-want +got):\n%s", diff) + } + }) + } +} diff --git a/cmd/ateapi/internal/controlapi/snapshot_report.go b/cmd/ateapi/internal/controlapi/snapshot_report.go index 8efd8a3fa8..4d3cd4015b 100644 --- a/cmd/ateapi/internal/controlapi/snapshot_report.go +++ b/cmd/ateapi/internal/controlapi/snapshot_report.go @@ -24,6 +24,7 @@ import ( "github.com/agent-substrate/substrate/cmd/ateapi/internal/store" "github.com/agent-substrate/substrate/internal/apierror" "github.com/agent-substrate/substrate/internal/preview" + "github.com/agent-substrate/substrate/internal/proto/ateletpb" "github.com/agent-substrate/substrate/internal/resources" "github.com/agent-substrate/substrate/pkg/proto/ateapipb" ) @@ -70,7 +71,7 @@ func (w *ActorWorkflow) ApplyReportedSnapshotState(ctx context.Context, actorRef } applied := proto.Clone(before).(*ateapipb.Actor) - if err := applyActorSnapshotState(applied.GetStatus(), node, state); err != nil { + if err := applyActorSnapshotState(leaseCtx, applied.GetStatus(), node, state); err != nil { return err } if proto.Equal(applied.GetStatus(), before.GetStatus()) { @@ -132,11 +133,17 @@ func actorOnNode(actor *ateapipb.Actor, node string) bool { return false } +// Messages logged for a reported durable snapshot that changes nothing. +const ( + msgDurableOlder = "Ignoring a durable snapshot older than the latest completed one" + msgDurableNotRecorded = "Ignoring a durable snapshot at a generation the actor no longer records" +) + // applyActorSnapshotState applies the two facts a report carries: an upload // finished, and the atelet on node detached the Actor. A report with neither // latest_local nor uploading is a detach. -func applyActorSnapshotState(status *ateapipb.ActorStatus, node string, state *ateapipb.ActorSnapshotState) error { - if err := applyLatestDurable(status, state.GetLatestDurable()); err != nil { +func applyActorSnapshotState(ctx context.Context, status *ateapipb.ActorStatus, node string, state *ateapipb.ActorSnapshotState) error { + if err := applyLatestDurable(ctx, status, state.GetLatestDurable()); err != nil { return err } if state.GetLatestLocal() == nil && state.GetUploading() == nil { @@ -145,21 +152,41 @@ func applyActorSnapshotState(status *ateapipb.ActorStatus, node string, state *a return nil } +// applyCheckpointState records an upload that atelet's state, as returned by +// Checkpoint or PromoteSnapshot, says finished, with the same rules as a +// report. atelet echoes the Snapshot it was asked for, so the fidelity recorded +// is the one requested, not read from the manifest. Unlike a report, a state +// with no local snapshot is not a detach: the workflow that made the call +// records where its snapshot lives. +func applyCheckpointState(ctx context.Context, status *ateapipb.ActorStatus, state *ateletpb.ActorSnapshotState) error { + return applyLatestDurable(ctx, status, snapshotReportFromAtelet(state.GetLatestDurable())) +} + // applyLatestDurable marks the reported durable snapshot COMPLETED and records // the fidelity from its manifest. Its uuid and upload URI must match the // recorded ones. A generation at or below the latest COMPLETED one changes // nothing, and so does one status no longer has, such as one a revert -// discarded. -func applyLatestDurable(status *ateapipb.ActorStatus, reported *ateapipb.SnapshotReport) error { +// discarded. A late report is expected, so an older generation and a +// discarded one are logged at Info. A repeat of the latest isn't logged. +func applyLatestDurable(ctx context.Context, status *ateapipb.ActorStatus, reported *ateapipb.SnapshotReport) error { if reported == nil { return nil } if latest := findLatestDurableSnapshot(status, ateapipb.SnapshotStorageStatus_SNAPSHOT_STORAGE_STATUS_COMPLETED); latest.GetGeneration() >= reported.GetGeneration() { + if latest.GetGeneration() > reported.GetGeneration() { + slog.InfoContext(ctx, msgDurableOlder, + slog.Int("generation", int(reported.GetGeneration())), + slog.String("uuid", reported.GetUuid()), + slog.Int("latest_completed_generation", int(latest.GetGeneration()))) + } return nil } snap := findSnapshotByGeneration(status.GetSnapshots(), reported.GetGeneration()) if snap == nil { if reported.GetGeneration() <= status.GetLastAssignedGeneration() { + slog.InfoContext(ctx, msgDurableNotRecorded, + slog.Int("generation", int(reported.GetGeneration())), + slog.String("uuid", reported.GetUuid())) return nil } return apierror.FailedPrecondition("snapshot generation %d (%s) is not recorded for this actor", reported.GetGeneration(), reported.GetUuid()) diff --git a/cmd/ateapi/internal/controlapi/snapshot_report_test.go b/cmd/ateapi/internal/controlapi/snapshot_report_test.go index 7d7be9b3cf..bd8ba5c036 100644 --- a/cmd/ateapi/internal/controlapi/snapshot_report_test.go +++ b/cmd/ateapi/internal/controlapi/snapshot_report_test.go @@ -17,6 +17,7 @@ package controlapi import ( "context" "fmt" + "log/slog" "testing" "github.com/google/go-cmp/cmp" @@ -25,6 +26,7 @@ import ( "github.com/agent-substrate/substrate/cmd/ateapi/internal/store/storetest" "github.com/agent-substrate/substrate/internal/apierror" + "github.com/agent-substrate/substrate/internal/proto/ateletpb" "github.com/agent-substrate/substrate/internal/resources" "github.com/agent-substrate/substrate/pkg/proto/ateapipb" ) @@ -283,7 +285,7 @@ func TestApplyActorSnapshotState(t *testing.T) { }} for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - err := applyActorSnapshotState(tt.status, reportNode, tt.state) + err := applyActorSnapshotState(context.Background(), tt.status, reportNode, tt.state) if tt.wantCode != codes.OK { if got := apierror.Code(err); got != tt.wantCode { t.Fatalf("applyActorSnapshotState() error = %v, want code %v", err, tt.wantCode) @@ -300,6 +302,86 @@ func TestApplyActorSnapshotState(t *testing.T) { } } +// A reported durable snapshot older than the latest COMPLETED one, or at a +// generation status no longer has, is logged at Info, once, with the +// generation it named. A repeat of the latest, and one that is applied, aren't +// logged. +func TestApplyLatestDurableLogsStaleReports(t *testing.T) { + const ( + pending = ateapipb.SnapshotStorageStatus_SNAPSHOT_STORAGE_STATUS_PENDING + completed = ateapipb.SnapshotStorageStatus_SNAPSHOT_STORAGE_STATUS_COMPLETED + ) + tests := []struct { + name string + status *ateapipb.ActorStatus + gen int32 + wantMsg string + wantAttrs map[string]string + }{{ + name: "below the latest COMPLETED generation", + status: &ateapipb.ActorStatus{LastAssignedGeneration: 2, Snapshots: []*ateapipb.Snapshot{ + withDurable(t, 1, reportNode, pending), + withDurable(t, 2, reportNode, completed), + }}, + gen: 1, + wantMsg: msgDurableOlder, + wantAttrs: map[string]string{"generation": "1", "uuid": snapName(1), "latest_completed_generation": "2"}, + }, { + name: "a generation status no longer has", + status: &ateapipb.ActorStatus{LastAssignedGeneration: 3, Snapshots: []*ateapipb.Snapshot{ + withDurable(t, 3, reportNode, pending), + }}, + gen: 2, + wantMsg: msgDurableNotRecorded, + wantAttrs: map[string]string{"generation": "2", "uuid": snapName(2)}, + }, { + name: "the latest COMPLETED generation itself", + status: &ateapipb.ActorStatus{LastAssignedGeneration: 2, Snapshots: []*ateapipb.Snapshot{ + withDurable(t, 2, reportNode, completed), + }}, + gen: 2, + }, { + name: "applied", + status: &ateapipb.ActorStatus{LastAssignedGeneration: 2, Snapshots: []*ateapipb.Snapshot{ + withDurable(t, 2, reportNode, pending), + }}, + gen: 2, + }} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + type logged struct { + msg string + attrs map[string]string + } + var got []logged + prev := slog.Default() + slog.SetDefault(slog.New(slogHandlerFunc(func(r slog.Record) { + if r.Message != msgDurableOlder && r.Message != msgDurableNotRecorded { + return + } + l := logged{msg: r.Message, attrs: map[string]string{}} + r.Attrs(func(a slog.Attr) bool { + l.attrs[a.Key] = a.Value.String() + return true + }) + got = append(got, l) + }))) + t.Cleanup(func() { slog.SetDefault(prev) }) + + if err := applyLatestDurable(context.Background(), tt.status, reported(t, tt.gen)); err != nil { + t.Fatalf("applyLatestDurable() error = %v", err) + } + var want []logged + if tt.wantMsg != "" { + want = []logged{{msg: tt.wantMsg, attrs: tt.wantAttrs}} + } + if diff := cmp.Diff(want, got, cmp.AllowUnexported(logged{})); diff != "" { + t.Errorf("logged records mismatch (-want +got):\n%s", diff) + } + }) + } +} + // A report is applied to the stored actor under its lease. A refused report, // and a report that changes nothing, write nothing. func TestApplyReportedSnapshotState(t *testing.T) { @@ -437,3 +519,121 @@ func TestActorOnNode(t *testing.T) { }) } } + +// ateletReported is reported(t, gen) as atelet returns it. +func ateletReported(t *testing.T, gen int32) *ateletpb.SnapshotReport { + t.Helper() + return &ateletpb.SnapshotReport{Snapshot: &ateletpb.Snapshot{ + SnapshotUuid: snapName(gen), + Generation: gen, + Fidelity: ateletpb.SnapshotFidelity_SNAPSHOT_FIDELITY_MEMORY, + Object: &ateletpb.ObjectStorage{SnapshotUri: ownedURI(t, reportActorUID, gen)}, + }} +} + +func TestApplyCheckpointState(t *testing.T) { + const ( + pending = ateapipb.SnapshotStorageStatus_SNAPSHOT_STORAGE_STATUS_PENDING + completed = ateapipb.SnapshotStorageStatus_SNAPSHOT_STORAGE_STATUS_COMPLETED + suspended = ateapipb.ActorState_ACTOR_STATE_SUSPENDED + suspending = ateapipb.ActorState_ACTOR_STATE_SUSPENDING + ) + tests := []struct { + name string + status *ateapipb.ActorStatus + state *ateletpb.ActorSnapshotState + want *ateapipb.ActorStatus + wantCode codes.Code + }{{ + name: "an immediate checkpoint marks its snapshot COMPLETED", + status: &ateapipb.ActorStatus{State: suspending, AssignedNode: reportNode, LastAssignedGeneration: 2, Snapshots: []*ateapipb.Snapshot{ + withDurable(t, 2, "", pending), + }}, + state: &ateletpb.ActorSnapshotState{LatestDurable: ateletReported(t, 2)}, + want: &ateapipb.ActorStatus{State: suspending, AssignedNode: reportNode, LastAssignedGeneration: 2, Snapshots: []*ateapipb.Snapshot{ + withDurable(t, 2, "", completed), + }}, + }, { + name: "an immediate checkpoint records the fidelity it reports", + status: &ateapipb.ActorStatus{State: suspending, AssignedNode: reportNode, LastAssignedGeneration: 2, Snapshots: []*ateapipb.Snapshot{ + withDurable(t, 2, "", pending), + }}, + state: &ateletpb.ActorSnapshotState{LatestDurable: func() *ateletpb.SnapshotReport { + r := ateletReported(t, 2) + r.Snapshot.Fidelity = ateletpb.SnapshotFidelity_SNAPSHOT_FIDELITY_ROOTFS + return r + }()}, + want: &ateapipb.ActorStatus{State: suspending, AssignedNode: reportNode, LastAssignedGeneration: 2, Snapshots: []*ateapipb.Snapshot{ + func() *ateapipb.Snapshot { + snap := withDurable(t, 2, "", completed) + snap.Fidelity = ateapipb.SnapshotFidelity_SNAPSHOT_FIDELITY_ROOTFS + return snap + }(), + }}, + }, { + name: "a deferred checkpoint changes nothing", + status: &ateapipb.ActorStatus{State: suspending, AssignedNode: reportNode, LastAssignedGeneration: 2, Snapshots: []*ateapipb.Snapshot{ + localOnly(2, ""), + }}, + state: &ateletpb.ActorSnapshotState{LatestLocal: ateletReported(t, 2)}, + want: &ateapipb.ActorStatus{State: suspending, AssignedNode: reportNode, LastAssignedGeneration: 2, Snapshots: []*ateapipb.Snapshot{ + localOnly(2, ""), + }}, + }, { + name: "a deferred checkpoint records an older upload that finished", + status: &ateapipb.ActorStatus{State: suspending, AssignedNode: reportNode, LastAssignedGeneration: 2, Snapshots: []*ateapipb.Snapshot{ + withDurable(t, 1, reportNode, pending), + localOnly(2, ""), + }}, + state: &ateletpb.ActorSnapshotState{LatestLocal: ateletReported(t, 2), LatestDurable: ateletReported(t, 1)}, + want: &ateapipb.ActorStatus{State: suspending, AssignedNode: reportNode, LastAssignedGeneration: 2, Snapshots: []*ateapipb.Snapshot{ + withDurable(t, 1, reportNode, completed), + localOnly(2, ""), + }}, + }, { + name: "no local snapshot is not a detach", + status: &ateapipb.ActorStatus{State: suspended, AssignedNode: reportNode, LastAssignedGeneration: 2, Snapshots: []*ateapipb.Snapshot{ + withDurable(t, 2, reportNode, completed), + }}, + state: &ateletpb.ActorSnapshotState{LatestDurable: ateletReported(t, 2)}, + want: &ateapipb.ActorStatus{State: suspended, AssignedNode: reportNode, LastAssignedGeneration: 2, Snapshots: []*ateapipb.Snapshot{ + withDurable(t, 2, reportNode, completed), + }}, + }, { + name: "an upload URI other than the recorded one is refused", + status: &ateapipb.ActorStatus{State: suspending, AssignedNode: reportNode, LastAssignedGeneration: 2, Snapshots: []*ateapipb.Snapshot{ + withDurable(t, 2, "", pending), + }}, + state: &ateletpb.ActorSnapshotState{LatestDurable: func() *ateletpb.SnapshotReport { + r := ateletReported(t, 2) + r.Snapshot.Object.SnapshotUri = ownedURI(t, reportActorUID, 9) + return r + }()}, + wantCode: codes.InvalidArgument, + }, { + name: "an unset state changes nothing", + status: &ateapipb.ActorStatus{State: suspending, AssignedNode: reportNode, LastAssignedGeneration: 2, Snapshots: []*ateapipb.Snapshot{ + withDurable(t, 2, "", pending), + }}, + want: &ateapipb.ActorStatus{State: suspending, AssignedNode: reportNode, LastAssignedGeneration: 2, Snapshots: []*ateapipb.Snapshot{ + withDurable(t, 2, "", pending), + }}, + }} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := applyCheckpointState(context.Background(), tt.status, tt.state) + if tt.wantCode != codes.OK { + if got := apierror.Code(err); got != tt.wantCode { + t.Fatalf("applyCheckpointState() error = %v, want code %v", err, tt.wantCode) + } + return + } + if err != nil { + t.Fatalf("applyCheckpointState() error = %v", err) + } + if diff := cmp.Diff(tt.want, tt.status, protocmp.Transform()); diff != "" { + t.Errorf("status mismatch (-want +got):\n%s", diff) + } + }) + } +} From 316ac867ae091685596600bdf65774b82c466ddb Mon Sep 17 00:00:00 2001 From: Bowei Du Date: Thu, 15 Oct 2026 21:11:32 +0000 Subject: [PATCH 10/31] atunnel refactor (#2720) - [x] Tests pass - [x] Appropriate changes to documentation are included in the PR --- cmd/atenet/README.md | 2 +- cmd/atenet/internal/router/ingress/ingress.go | 4 +- .../internal/router/ingress/ingress_test.go | 6 +- cmd/atenet/internal/router/xds.go | 10 +- cmd/atenet/internal/router/xds_test.go | 8 +- .../router/xds_upstream_connect_test.go | 6 +- cmd/ateworker-gvisor/hosted.go | 4 +- cmd/ateworker-gvisor/sandboxnet.go | 3 +- cmd/ateworker-microvm/hosted.go | 4 +- cmd/ateworker-microvm/net_linux_test.go | 7 +- cmd/ateworker-microvm/sandboxnet.go | 3 +- .../actornet/{dns => }/resolvconf.go | 42 +----- .../actornet/{dns => }/resolvconf_test.go | 49 +------ internal/ateworker/actornet/sandbox.go | 4 +- .../actornet/sandbox_atunnel_linux_test.go | 6 +- internal/ateworker/tunnel/egresspolicy.go | 2 +- internal/ateworker/tunnel/tunnel.go | 38 ++--- .../actornet => atunnel}/dns/dnshandler.go | 2 +- .../dns/dnshandler_test.go | 2 +- .../actornet => atunnel}/dns/evaluator.go | 0 .../dns/evaluator_test.go | 0 .../actornet => atunnel}/dns/freemap/doc.go | 0 .../dns/freemap/freemap64k.go | 0 .../dns/freemap/freemap64k_test.go | 0 .../actornet => atunnel}/dns/limiter.go | 0 .../actornet => atunnel}/dns/limiter_test.go | 0 .../actornet => atunnel}/dns/pendingreq.go | 4 +- .../dns/pendingreq_test.go | 0 .../actornet => atunnel}/dns/policy.go | 0 .../actornet => atunnel}/dns/policy_test.go | 2 +- .../actornet => atunnel}/dns/protocol/doc.go | 0 .../dns/protocol/protocol.go | 0 .../dns/protocol/protocol_test.go | 0 .../actornet => atunnel}/dns/protocol/tcp.go | 0 .../dns/protocol/tcp_test.go | 0 .../actornet => atunnel}/dns/readme.md | 0 .../actornet => atunnel}/dns/relay.go | 0 .../actornet => atunnel}/dns/relay_linux.go | 0 .../actornet => atunnel}/dns/relay_test.go | 0 internal/atunnel/dns/resolvconf.go | 58 ++++++++ internal/atunnel/dns/resolvconf_test.go | 70 +++++++++ .../dns/serve_linux_test.go | 0 .../actornet => atunnel}/dns/server.go | 0 .../actornet => atunnel}/dns/tcphandler.go | 2 +- .../dns/tcphandler_test.go | 2 +- .../actornet => atunnel}/dns/udphandler.go | 0 .../dns/udphandler_test.go | 0 internal/atunnel/{ => egress}/client.go | 74 +++++----- internal/atunnel/{ => egress}/client_test.go | 30 ++-- internal/atunnel/{ => egress}/credential.go | 24 +-- .../atunnel/{ => egress}/credential_test.go | 2 +- internal/atunnel/{ => egress}/egress.go | 89 ++++++----- internal/atunnel/{ => egress}/egress_test.go | 52 +++---- internal/atunnel/egress/testhelpers_test.go | 138 ++++++++++++++++++ internal/atunnel/{ => ingress}/ingress.go | 69 +++++---- .../atunnel/{ => ingress}/ingress_test.go | 30 ++-- .../e2e/fixtures/testserver/egressprobe.go | 10 +- internal/e2e/router_client.go | 4 +- .../suites/networking/arbitraryport_test.go | 2 +- internal/linuxsys/doc.go | 17 +++ .../original_dst_format_linux_test.go | 2 +- .../original_dst_linux.go | 14 +- .../original_dst_linux_test.go | 2 +- 63 files changed, 559 insertions(+), 340 deletions(-) rename internal/ateworker/actornet/{dns => }/resolvconf.go (66%) rename internal/ateworker/actornet/{dns => }/resolvconf_test.go (73%) rename internal/{ateworker/actornet => atunnel}/dns/dnshandler.go (99%) rename internal/{ateworker/actornet => atunnel}/dns/dnshandler_test.go (99%) rename internal/{ateworker/actornet => atunnel}/dns/evaluator.go (100%) rename internal/{ateworker/actornet => atunnel}/dns/evaluator_test.go (100%) rename internal/{ateworker/actornet => atunnel}/dns/freemap/doc.go (100%) rename internal/{ateworker/actornet => atunnel}/dns/freemap/freemap64k.go (100%) rename internal/{ateworker/actornet => atunnel}/dns/freemap/freemap64k_test.go (100%) rename internal/{ateworker/actornet => atunnel}/dns/limiter.go (100%) rename internal/{ateworker/actornet => atunnel}/dns/limiter_test.go (100%) rename internal/{ateworker/actornet => atunnel}/dns/pendingreq.go (97%) rename internal/{ateworker/actornet => atunnel}/dns/pendingreq_test.go (100%) rename internal/{ateworker/actornet => atunnel}/dns/policy.go (100%) rename internal/{ateworker/actornet => atunnel}/dns/policy_test.go (99%) rename internal/{ateworker/actornet => atunnel}/dns/protocol/doc.go (100%) rename internal/{ateworker/actornet => atunnel}/dns/protocol/protocol.go (100%) rename internal/{ateworker/actornet => atunnel}/dns/protocol/protocol_test.go (100%) rename internal/{ateworker/actornet => atunnel}/dns/protocol/tcp.go (100%) rename internal/{ateworker/actornet => atunnel}/dns/protocol/tcp_test.go (100%) rename internal/{ateworker/actornet => atunnel}/dns/readme.md (100%) rename internal/{ateworker/actornet => atunnel}/dns/relay.go (100%) rename internal/{ateworker/actornet => atunnel}/dns/relay_linux.go (100%) rename internal/{ateworker/actornet => atunnel}/dns/relay_test.go (100%) create mode 100644 internal/atunnel/dns/resolvconf.go create mode 100644 internal/atunnel/dns/resolvconf_test.go rename internal/{ateworker/actornet => atunnel}/dns/serve_linux_test.go (100%) rename internal/{ateworker/actornet => atunnel}/dns/server.go (100%) rename internal/{ateworker/actornet => atunnel}/dns/tcphandler.go (98%) rename internal/{ateworker/actornet => atunnel}/dns/tcphandler_test.go (98%) rename internal/{ateworker/actornet => atunnel}/dns/udphandler.go (100%) rename internal/{ateworker/actornet => atunnel}/dns/udphandler_test.go (100%) rename internal/atunnel/{ => egress}/client.go (90%) rename internal/atunnel/{ => egress}/client_test.go (96%) rename internal/atunnel/{ => egress}/credential.go (85%) rename internal/atunnel/{ => egress}/credential_test.go (99%) rename internal/atunnel/{ => egress}/egress.go (75%) rename internal/atunnel/{ => egress}/egress_test.go (91%) create mode 100644 internal/atunnel/egress/testhelpers_test.go rename internal/atunnel/{ => ingress}/ingress.go (89%) rename internal/atunnel/{ => ingress}/ingress_test.go (98%) create mode 100644 internal/linuxsys/doc.go rename internal/{atunnel => linuxsys}/original_dst_format_linux_test.go (99%) rename internal/{atunnel => linuxsys}/original_dst_linux.go (85%) rename internal/{atunnel => linuxsys}/original_dst_linux_test.go (99%) diff --git a/cmd/atenet/README.md b/cmd/atenet/README.md index 8f17a3855c..d72b0f6a71 100644 --- a/cmd/atenet/README.md +++ b/cmd/atenet/README.md @@ -29,7 +29,7 @@ likely be split in the future for better scalability.) than a header. `atunnel` can't read that metadata directly, though -- it's a separate process on the worker pod, not part of Envoy -- so the port to reach on the actor itself (its default port, or an arbitrary one for - CONNECT) still travels as a real header, `atunnel.TargetPortHeader`. + CONNECT) still travels as a real header, `TargetPortHeader` in `internal/atunnel/ingress`. `ate-target-actor: /` identifies the Actor independently of `:authority`/`Host`. * Termination: the router drains gracefully on SIGTERM (readiness flip → diff --git a/cmd/atenet/internal/router/ingress/ingress.go b/cmd/atenet/internal/router/ingress/ingress.go index 6b68011cea..5294d90688 100644 --- a/cmd/atenet/internal/router/ingress/ingress.go +++ b/cmd/atenet/internal/router/ingress/ingress.go @@ -38,7 +38,7 @@ import ( "github.com/agent-substrate/substrate/cmd/atenet/internal/router/extproc" "github.com/agent-substrate/substrate/internal/atenet" - "github.com/agent-substrate/substrate/internal/atunnel" + atunnelingress "github.com/agent-substrate/substrate/internal/atunnel/ingress" "github.com/agent-substrate/substrate/pkg/proto/ateapipb" ) @@ -100,7 +100,7 @@ func (h *Handler) HandleRequestHeaders(ctx context.Context, md *extproc.RequestM targetAuthority = md.Host } if _, portStr, err := net.SplitHostPort(targetAuthority); err == nil { - if p, ok := atunnel.ParsePort(portStr); ok { + if p, ok := atunnelingress.ParsePort(portStr); ok { targetPort = p } } diff --git a/cmd/atenet/internal/router/ingress/ingress_test.go b/cmd/atenet/internal/router/ingress/ingress_test.go index 35c940dba1..fc4d5fe032 100644 --- a/cmd/atenet/internal/router/ingress/ingress_test.go +++ b/cmd/atenet/internal/router/ingress/ingress_test.go @@ -36,7 +36,7 @@ import ( "github.com/agent-substrate/substrate/cmd/atenet/internal/router/extproc" "github.com/agent-substrate/substrate/internal/ateattr" "github.com/agent-substrate/substrate/internal/atenet" - "github.com/agent-substrate/substrate/internal/atunnel" + atunnelingress "github.com/agent-substrate/substrate/internal/atunnel/ingress" "github.com/agent-substrate/substrate/pkg/proto/ateapipb" ) @@ -441,7 +441,7 @@ func TestHandleRequestHeaders(t *testing.T) { for _, headerOption := range mutation.GetSetHeaders() { gotMutations[strings.ToLower(headerOption.Header.Key)] = string(headerOption.Header.RawValue) } - if _, ok := gotMutations[strings.ToLower(atunnel.TargetPortHeader)]; ok { + if _, ok := gotMutations[strings.ToLower(atunnelingress.TargetPortHeader)]; ok { t.Errorf("target port must be emitted only as dynamic metadata") } if got, want := gotMutations[strings.ToLower(atenet.TargetActorHeader)], "team-a/"+testUUID; got != want { @@ -463,7 +463,7 @@ func TestHandleRequestHeaders(t *testing.T) { // produces the same ":443" original-dst mutation as an ordinary // request (the router only ever dials the worker's atunnel server), and // reports the arbitrary port itself in dynamic metadata for Envoy to write to -// atunnel.TargetPortHeader. +// atunnelingress.TargetPortHeader. func TestHandleRequestHeadersHandlesConnectMethod(t *testing.T) { const testUUID = "123e4567-e89b-12d3-a456-426614174000" authority := testUUID + ".team-a.actors.resources.substrate.ate.dev:9090" diff --git a/cmd/atenet/internal/router/xds.go b/cmd/atenet/internal/router/xds.go index a3b3089426..6f0d4b4cd4 100644 --- a/cmd/atenet/internal/router/xds.go +++ b/cmd/atenet/internal/router/xds.go @@ -73,7 +73,7 @@ import ( "github.com/agent-substrate/substrate/cmd/atenet/internal/router/extproc" "github.com/agent-substrate/substrate/cmd/atenet/internal/router/ingress" - "github.com/agent-substrate/substrate/internal/atunnel" + atunnelingress "github.com/agent-substrate/substrate/internal/atunnel/ingress" ) const ( @@ -409,7 +409,7 @@ func (x *XdsServer) SetUpstreamTls(credentialBundlePath, trustBundlePath, spiffe } // SetUpstreamConnect makes actor traffic reach atunnel through CONNECT tunnels -// on atunnel.DefaultConnectPort, one per actor and port. It needs +// on atunnelingress.DefaultConnectPort, one per actor and port. It needs // SetUpstreamTls, since that listener only accepts mTLS. func (x *XdsServer) SetUpstreamConnect(enabled bool) { x.mu.Lock() @@ -974,7 +974,7 @@ func (x *XdsServer) buildActorConnectCluster() *clusterv3.Cluster { LbPolicy: clusterv3.Cluster_CLUSTER_PROVIDED, LbConfig: &clusterv3.Cluster_OriginalDstLbConfig_{ OriginalDstLbConfig: &clusterv3.Cluster_OriginalDstLbConfig{ - UpstreamPortOverride: wrapperspb.UInt32(atunnel.DefaultConnectPort), + UpstreamPortOverride: wrapperspb.UInt32(atunnelingress.DefaultConnectPort), }, }, TransportSocket: x.buildUpstreamTransportSocket(), @@ -1022,7 +1022,7 @@ func (x *XdsServer) buildRoutes() *routev3.RouteConfiguration { RequestHeadersToAdd: []*corev3.HeaderValueOption{ { Header: &corev3.HeaderValue{ - Key: atunnel.TargetPortHeader, + Key: atunnelingress.TargetPortHeader, Value: fmt.Sprintf("%%DYNAMIC_METADATA(%s:%s)%%", ingress.OriginalDstMetadataKey, ingress.OriginalDstPortKey), }, @@ -1577,7 +1577,7 @@ func (x *XdsServer) buildHttpsListener() *listenerv3.Listener { // gRPC requires a negotiated "h2"; http/1.1 keeps plain HTTPS clients // working alongside it. HTTP/1.1-only actors are safe either way: atunnel // downgrades every non-gRPC request to HTTP/1.1 on the actor leg (see - // atunnel.protocolMirrorTransport), so offering h2 at the edge cannot + // atunnelingress.protocolMirrorTransport), so offering h2 at the edge cannot // change what an actor receives. alpn := []string{"h2", "http/1.1"} diff --git a/cmd/atenet/internal/router/xds_test.go b/cmd/atenet/internal/router/xds_test.go index f028070623..0718b53c30 100644 --- a/cmd/atenet/internal/router/xds_test.go +++ b/cmd/atenet/internal/router/xds_test.go @@ -50,7 +50,7 @@ import ( "github.com/agent-substrate/substrate/cmd/atenet/internal/router/extproc" "github.com/agent-substrate/substrate/cmd/atenet/internal/router/ingress" - "github.com/agent-substrate/substrate/internal/atunnel" + atunnelingress "github.com/agent-substrate/substrate/internal/atunnel/ingress" ) func TestActorRoutingFilterStateFilter(t *testing.T) { @@ -1172,7 +1172,7 @@ func TestXdsServer_BuildOriginalDstCluster_UsesMetadataKey(t *testing.T) { // TestXdsServer_ActorClusterProtocolOptions pins where downstream-protocol // mirroring is allowed: only on the mTLS atunnel leg, where atunnel guards // HTTP/1.1-only actors by downgrading non-gRPC HTTP/2 (see -// atunnel.protocolMirrorTransport). The legacy plaintext cluster dials the +// atunnelingress.protocolMirrorTransport). The legacy plaintext cluster dials the // actor directly with no such guard, so it must carry no protocol options at // all — Envoy's implicit HTTP/1.1. func TestXdsServer_ActorClusterProtocolOptions(t *testing.T) { @@ -1229,7 +1229,7 @@ func TestXdsServer_BuildRoutesWritesTargetPortHeader(t *testing.T) { t.Fatalf("route adds request headers %v, want exactly one", headers) } header := headers[0] - if got, want := header.GetHeader().GetKey(), atunnel.TargetPortHeader; got != want { + if got, want := header.GetHeader().GetKey(), atunnelingress.TargetPortHeader; got != want { t.Errorf("header key = %q, want %q", got, want) } if got, want := header.GetHeader().GetValue(), "%DYNAMIC_METADATA(envoy.filters.listener.original_dst:port)%"; got != want { @@ -1455,7 +1455,7 @@ func downstreamTLS(t *testing.T, raw any) *tlsv3.DownstreamTlsContext { // listener always offers h2 before http/1.1: gRPC over TLS requires a // negotiated "h2", and the offer is unconditional because atunnel downgrades // every non-gRPC request to HTTP/1.1 on the actor leg (see -// atunnel.protocolMirrorTransport and TestProtocolMirrorTransport), so an +// atunnelingress.protocolMirrorTransport and TestProtocolMirrorTransport), so an // HTTP/1.1-only actor cannot tell what the client negotiated at the edge. The // CONNECT-TLS listener stays ALPN-free: its clients speak HTTP/1.1 CONNECT, // and an h2 offer there would move them onto extended CONNECT the tunnel path diff --git a/cmd/atenet/internal/router/xds_upstream_connect_test.go b/cmd/atenet/internal/router/xds_upstream_connect_test.go index c243c3777a..9647989f67 100644 --- a/cmd/atenet/internal/router/xds_upstream_connect_test.go +++ b/cmd/atenet/internal/router/xds_upstream_connect_test.go @@ -36,7 +36,7 @@ import ( "github.com/agent-substrate/substrate/cmd/atenet/internal/router/extproc" "github.com/agent-substrate/substrate/internal/atenet" - "github.com/agent-substrate/substrate/internal/atunnel" + atunnelingress "github.com/agent-substrate/substrate/internal/atunnel/ingress" ) // upstreamConnectSnapshot also enables the CONNECT listener, so main_internal @@ -396,8 +396,8 @@ func TestXdsServer_ActorTunnelClusters(t *testing.T) { 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) + if got := lb.GetUpstreamPortOverride().GetValue(); got != atunnelingress.DefaultConnectPort { + t.Errorf("actor_connect port = %d, want atunnel's CONNECT port %d", got, atunnelingress.DefaultConnectPort) } upstreamTLS := &tlsv3.UpstreamTlsContext{} if err := c.GetTransportSocket().GetTypedConfig().UnmarshalTo(upstreamTLS); err != nil { diff --git a/cmd/ateworker-gvisor/hosted.go b/cmd/ateworker-gvisor/hosted.go index e236f5a1c9..6785ab02b8 100644 --- a/cmd/ateworker-gvisor/hosted.go +++ b/cmd/ateworker-gvisor/hosted.go @@ -26,7 +26,7 @@ import ( "github.com/agent-substrate/substrate/internal/apierror" "github.com/agent-substrate/substrate/internal/ateworker/actornet" - "github.com/agent-substrate/substrate/internal/atunnel" + "github.com/agent-substrate/substrate/internal/atunnel/ingress" "github.com/agent-substrate/substrate/internal/proto/ateworkerpb" "github.com/agent-substrate/substrate/internal/resources" "github.com/agent-substrate/substrate/internal/workerstats" @@ -186,7 +186,7 @@ func (s *AteWorkerService) setSession(actorUID string, session *workloadSession) } // sandboxDialer looks up the actor on each dial. -func (s *AteWorkerService) sandboxDialer(actorUID string) atunnel.DialFunc { +func (s *AteWorkerService) sandboxDialer(actorUID string) ingress.DialFunc { return func(ctx context.Context, network, address string) (net.Conn, error) { s.actorsMu.RLock() var session *actornet.SandboxSession diff --git a/cmd/ateworker-gvisor/sandboxnet.go b/cmd/ateworker-gvisor/sandboxnet.go index 5da536dddc..36d6b8f962 100644 --- a/cmd/ateworker-gvisor/sandboxnet.go +++ b/cmd/ateworker-gvisor/sandboxnet.go @@ -26,7 +26,6 @@ import ( "path/filepath" "github.com/agent-substrate/substrate/internal/ateworker/actornet" - "github.com/agent-substrate/substrate/internal/ateworker/actornet/dns" "github.com/agent-substrate/substrate/internal/proto/ateworkerpb" ) @@ -40,7 +39,7 @@ func actorResolvConf(actorDirs *ateworkerpb.ActorDirs) (string, error) { if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil { return "", fmt.Errorf("creating the actor directory: %w", err) } - if err := os.WriteFile(path, dns.SandboxResolvConf(actornet.ActorVethGateway, pod), 0o644); err != nil { + if err := os.WriteFile(path, actornet.SandboxResolvConf(actornet.ActorVethGateway, pod), 0o644); err != nil { return "", fmt.Errorf("writing the actor resolv.conf: %w", err) } return path, nil diff --git a/cmd/ateworker-microvm/hosted.go b/cmd/ateworker-microvm/hosted.go index 0c33ae705b..c6cb501584 100644 --- a/cmd/ateworker-microvm/hosted.go +++ b/cmd/ateworker-microvm/hosted.go @@ -27,7 +27,7 @@ import ( "github.com/agent-substrate/substrate/internal/apierror" "github.com/agent-substrate/substrate/internal/ateworker/actornet" "github.com/agent-substrate/substrate/internal/ateworker/cgroup" - "github.com/agent-substrate/substrate/internal/atunnel" + "github.com/agent-substrate/substrate/internal/atunnel/ingress" "github.com/agent-substrate/substrate/internal/netns" "github.com/agent-substrate/substrate/internal/resources" "github.com/agent-substrate/substrate/internal/sizing" @@ -234,7 +234,7 @@ func (s *AteWorkerService) sandboxNetNS(actorUID string) netns.Handle { } // sandboxDialer connects through the actor's gateway namespace. -func (s *AteWorkerService) sandboxDialer(actorUID string) atunnel.DialFunc { +func (s *AteWorkerService) sandboxDialer(actorUID string) ingress.DialFunc { return func(ctx context.Context, network, address string) (net.Conn, error) { s.actorsMu.RLock() hosted := s.actors[actorUID] diff --git a/cmd/ateworker-microvm/net_linux_test.go b/cmd/ateworker-microvm/net_linux_test.go index 0f6b220985..46ec52695c 100644 --- a/cmd/ateworker-microvm/net_linux_test.go +++ b/cmd/ateworker-microvm/net_linux_test.go @@ -24,7 +24,8 @@ import ( "github.com/agent-substrate/substrate/internal/ateworker/actornet" "github.com/agent-substrate/substrate/internal/ateworker/tunnel" - "github.com/agent-substrate/substrate/internal/atunnel" + "github.com/agent-substrate/substrate/internal/atunnel/egress" + "github.com/agent-substrate/substrate/internal/linuxsys" "github.com/agent-substrate/substrate/internal/netns" "github.com/agent-substrate/substrate/internal/nodepath" "github.com/agent-substrate/substrate/internal/resources" @@ -36,12 +37,12 @@ import ( func TestHostActorReplacesSameActor(t *testing.T) { roottest.Require(t, "creates network namespaces") ctx := context.Background() - egress, err := atunnel.NewEgress(atunnel.TCPOriginalDestination) + egressProxy, err := egress.New(linuxsys.TCPOriginalDestination) if err != nil { t.Fatal(err) } service := &AteWorkerService{ - tunnel: &tunnel.Tunnel{Egress: egress, EgressPort: 15001}, + tunnel: &tunnel.Tunnel{Egress: egressProxy, EgressPort: 15001}, actors: map[string]*hostedActor{}, maxActors: 1, } diff --git a/cmd/ateworker-microvm/sandboxnet.go b/cmd/ateworker-microvm/sandboxnet.go index 3749205ce5..a4595b7e13 100644 --- a/cmd/ateworker-microvm/sandboxnet.go +++ b/cmd/ateworker-microvm/sandboxnet.go @@ -21,7 +21,6 @@ import ( "os" "github.com/agent-substrate/substrate/internal/ateworker/actornet" - "github.com/agent-substrate/substrate/internal/ateworker/actornet/dns" ) // writeActorResolvConf points the guest resolver at its fixed gateway address. @@ -30,5 +29,5 @@ func writeActorResolvConf(rootfs string) error { if err != nil { return fmt.Errorf("reading the worker pod resolv.conf: %w", err) } - return dns.WriteRootfsResolvConf(rootfs, dns.SandboxResolvConf(actornet.ActorVethGateway, pod)) + return actornet.WriteRootfsResolvConf(rootfs, actornet.SandboxResolvConf(actornet.ActorVethGateway, pod)) } diff --git a/internal/ateworker/actornet/dns/resolvconf.go b/internal/ateworker/actornet/resolvconf.go similarity index 66% rename from internal/ateworker/actornet/dns/resolvconf.go rename to internal/ateworker/actornet/resolvconf.go index 26f6fd805c..32959d3171 100644 --- a/internal/ateworker/actornet/dns/resolvconf.go +++ b/internal/ateworker/actornet/resolvconf.go @@ -12,55 +12,17 @@ // See the License for the specific language governing permissions and // limitations under the License. -// Package dns answers an actor's DNS from its sandbox's gateway namespace and -// writes the resolv.conf that points the actor at it. -package dns +package actornet import ( - "bufio" "errors" "fmt" "io/fs" - "net" "os" "path/filepath" "strings" ) -// resolvConfNameservers reads nameservers from resolv.conf as "host:53". -func resolvConfNameservers(path string) ([]string, error) { - f, err := os.Open(path) - if err != nil { - return nil, fmt.Errorf("dns: reading resolv.conf: %w", err) - } - defer f.Close() - - var out []string - scanner := bufio.NewScanner(f) - for scanner.Scan() { - line := strings.TrimSpace(scanner.Text()) - if i := strings.IndexAny(line, "#;"); i >= 0 { - line = strings.TrimSpace(line[:i]) - } - rest, ok := strings.CutPrefix(line, "nameserver") - if !ok { - continue - } - address := strings.TrimSpace(rest) - if address == "" || net.ParseIP(address) == nil { - continue - } - out = append(out, net.JoinHostPort(address, "53")) - } - if err := scanner.Err(); err != nil { - return nil, fmt.Errorf("dns: reading resolv.conf: %w", err) - } - if len(out) == 0 { - return nil, fmt.Errorf("dns: %s names no usable nameserver", path) - } - return out, nil -} - // SandboxResolvConf replaces the pod's nameservers with nameserver, the // address the sandbox's DNS is served on, while preserving the pod's search // domains and options for Kubernetes DNS. @@ -84,7 +46,7 @@ func SandboxResolvConf(nameserver string, podResolvConf []byte) []byte { // os.Root confines path traversal; unlinking prevents writes through existing links. func WriteRootfsResolvConf(rootfs string, content []byte) error { if len(content) == 0 { - return fmt.Errorf("dns: refusing to write an empty resolv.conf") + return fmt.Errorf("refusing to write an empty resolv.conf") } root, err := os.OpenRoot(rootfs) if err != nil { diff --git a/internal/ateworker/actornet/dns/resolvconf_test.go b/internal/ateworker/actornet/resolvconf_test.go similarity index 73% rename from internal/ateworker/actornet/dns/resolvconf_test.go rename to internal/ateworker/actornet/resolvconf_test.go index edafe46c5c..6d664e41ca 100644 --- a/internal/ateworker/actornet/dns/resolvconf_test.go +++ b/internal/ateworker/actornet/resolvconf_test.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package dns +package actornet import ( "os" @@ -23,53 +23,6 @@ import ( "github.com/google/go-cmp/cmp" ) -func TestResolvConfNameservers(t *testing.T) { - for _, tc := range []struct { - name string - content string - want []string - wantErr bool - }{ - { - name: "cluster resolv.conf", - content: "search ate-system.svc.cluster.local svc.cluster.local\nnameserver 10.96.0.10\noptions ndots:5\n", - want: []string{"10.96.0.10:53"}, - }, - { - name: "several, in order", - content: "nameserver 10.96.0.10\nnameserver 8.8.8.8\n", - want: []string{"10.96.0.10:53", "8.8.8.8:53"}, - }, - { - name: "comments and blanks", - content: "# generated\n\n nameserver 10.96.0.10 # cluster\n;nameserver 1.1.1.1\n", - want: []string{"10.96.0.10:53"}, - }, - {name: "no nameserver", content: "search cluster.local\n", wantErr: true}, - {name: "unparsable address", content: "nameserver not-an-ip\n", wantErr: true}, - } { - t.Run(tc.name, func(t *testing.T) { - path := filepath.Join(t.TempDir(), "resolv.conf") - if err := os.WriteFile(path, []byte(tc.content), 0o644); err != nil { - t.Fatal(err) - } - got, err := resolvConfNameservers(path) - if tc.wantErr { - if err == nil { - t.Fatalf("resolvConfNameservers() = %v, want an error", got) - } - return - } - if err != nil { - t.Fatalf("resolvConfNameservers: %v", err) - } - if diff := cmp.Diff(tc.want, got); diff != "" { - t.Errorf("nameservers mismatch (-want +got):\n%s", diff) - } - }) - } -} - func TestSandboxResolvConf(t *testing.T) { // The shape kubelet writes into a pod. pod := "nameserver 10.96.0.10\n" + diff --git a/internal/ateworker/actornet/sandbox.go b/internal/ateworker/actornet/sandbox.go index 3e08da95bd..60fa98c680 100644 --- a/internal/ateworker/actornet/sandbox.go +++ b/internal/ateworker/actornet/sandbox.go @@ -26,7 +26,7 @@ import ( "net/netip" "sync" - "github.com/agent-substrate/substrate/internal/ateworker/actornet/dns" + "github.com/agent-substrate/substrate/internal/atunnel/dns" "github.com/agent-substrate/substrate/internal/netns" "github.com/agent-substrate/substrate/internal/nodepath" @@ -364,7 +364,7 @@ func CleanupSandboxNetwork(network *SandboxNetwork) error { } // egressServer serves one actor's captured connections. Satisfied by -// atunnel.Egress; an interface so this package does not depend on it. +// egress.Proxy; an interface so this package does not depend on it. type egressServer interface { Bind(actorUID string) (func(context.Context, net.Listener) error, error) } diff --git a/internal/ateworker/actornet/sandbox_atunnel_linux_test.go b/internal/ateworker/actornet/sandbox_atunnel_linux_test.go index 3109a8d6da..50a342fb5a 100644 --- a/internal/ateworker/actornet/sandbox_atunnel_linux_test.go +++ b/internal/ateworker/actornet/sandbox_atunnel_linux_test.go @@ -26,7 +26,7 @@ import ( "time" "github.com/agent-substrate/substrate/internal/ateworker/actornet" - "github.com/agent-substrate/substrate/internal/atunnel" + "github.com/agent-substrate/substrate/internal/linuxsys" "github.com/agent-substrate/substrate/internal/netns" "github.com/agent-substrate/substrate/internal/roottest" ) @@ -59,7 +59,7 @@ func TestSandboxEgressReachesAtunnelOnAnyPort(t *testing.T) { return } defer c.Close() - dst, err := atunnel.TCPOriginalDestination(c) + dst, err := linuxsys.TCPOriginalDestination(c) if err != nil { t.Errorf("TCPOriginalDestination: %v", err) return @@ -164,7 +164,7 @@ func TestSandboxLinkLocalDoesNotReachAtunnel(t *testing.T) { t.Fatalf("accept allowed connection: %v", err) } defer conn.Close() - destination, err := atunnel.TCPOriginalDestination(conn) + destination, err := linuxsys.TCPOriginalDestination(conn) if err != nil { t.Fatalf("TCPOriginalDestination: %v", err) } diff --git a/internal/ateworker/tunnel/egresspolicy.go b/internal/ateworker/tunnel/egresspolicy.go index 0e5ff7e898..7e62f133be 100644 --- a/internal/ateworker/tunnel/egresspolicy.go +++ b/internal/ateworker/tunnel/egresspolicy.go @@ -27,7 +27,7 @@ import ( "github.com/agent-substrate/substrate/internal/ateattr" "github.com/agent-substrate/substrate/internal/ateletdial" - "github.com/agent-substrate/substrate/internal/ateworker/actornet/dns" + "github.com/agent-substrate/substrate/internal/atunnel/dns" "github.com/agent-substrate/substrate/internal/nodepath" "github.com/agent-substrate/substrate/internal/proto/ateletpb" "github.com/agent-substrate/substrate/internal/resources" diff --git a/internal/ateworker/tunnel/tunnel.go b/internal/ateworker/tunnel/tunnel.go index ecb2ceb617..7cf52b17bb 100644 --- a/internal/ateworker/tunnel/tunnel.go +++ b/internal/ateworker/tunnel/tunnel.go @@ -30,10 +30,12 @@ import ( "time" "github.com/agent-substrate/substrate/internal/ateletdial" - "github.com/agent-substrate/substrate/internal/ateworker/actornet/dns" - "github.com/agent-substrate/substrate/internal/atunnel" + "github.com/agent-substrate/substrate/internal/atunnel/dns" + "github.com/agent-substrate/substrate/internal/atunnel/egress" + "github.com/agent-substrate/substrate/internal/atunnel/ingress" "github.com/agent-substrate/substrate/internal/egresspolicy" "github.com/agent-substrate/substrate/internal/installdefaults" + "github.com/agent-substrate/substrate/internal/linuxsys" "github.com/agent-substrate/substrate/internal/nodepath" "github.com/agent-substrate/substrate/internal/proto/ateworkerpb" "github.com/agent-substrate/substrate/internal/resources" @@ -94,9 +96,9 @@ func RegisterFlags(fs FlagSet) *Config { // Tunnel is the atunnel state one worker shares across its actors. type Tunnel struct { // Ingress proxies router traffic to the active actors. - Ingress *atunnel.Server + Ingress *ingress.Server // Egress tunnels the active actors' outbound TCP to the egress gateway. - Egress *atunnel.Egress + Egress *egress.Proxy // EgressPort is the local atunnel listener used as the target of the // actor network's transparent TCP redirect. EgressPort uint16 @@ -130,7 +132,7 @@ func Start(ctx context.Context, cfg Config, upstream string) (*Tunnel, error) { } // Ingress - ingress, err := atunnel.NewServer(atunnel.Config{ + ingressServer, err := ingress.NewServer(ingress.Config{ CredentialBundlePath: cfg.CredentialBundle, TrustBundlePath: cfg.TrustBundle, AllowedClientID: cfg.ClientIdentity, @@ -139,26 +141,26 @@ func Start(ctx context.Context, cfg Config, upstream string) (*Tunnel, error) { if err != nil { return nil, fmt.Errorf("configuring ingress: %w", err) } - if err := serve(ctx, "atunnel", cfg.ListenAddress, ingress.Serve); err != nil { + if err := serve(ctx, "atunnel", cfg.ListenAddress, ingressServer.Serve); err != nil { return nil, err } - if err := serve(ctx, "atunnel CONNECT", cfg.ConnectListenAddress, ingress.ServeConnect); err != nil { + if err := serve(ctx, "atunnel CONNECT", cfg.ConnectListenAddress, ingressServer.ServeConnect); err != nil { return nil, err } // Egress - egress, err := atunnel.NewEgress(atunnel.TCPOriginalDestination) + egressProxy, err := egress.New(linuxsys.TCPOriginalDestination) if err != nil { return nil, fmt.Errorf("configuring egress: %w", err) } - egressPort, err := atunnel.EgressPort(cfg.EgressListenAddress) + egressPort, err := egress.PortFromListenAddr(cfg.EgressListenAddress) if err != nil { return nil, err } return &Tunnel{ - Ingress: ingress, - Egress: egress, + Ingress: ingressServer, + Egress: egressProxy, EgressPort: egressPort, DNSRelay: dnsRelay, cfg: cfg, @@ -185,9 +187,9 @@ func serve(ctx context.Context, name, address string, serveFn func(context.Conte // ActorEgress is an actor's tunneled egress, ready to activate. type ActorEgress struct { // client presents the actor certificate to the remote egress gateway. - client *atunnel.Client + client *egress.Client // certificateSource owns the actor key and renews its certificate via atelet. - certificateSource *atunnel.BrokerCertificateSource + certificateSource *egress.BrokerCertificateSource expiresAt time.Time } @@ -204,7 +206,7 @@ func (t *Tunnel) PrepareEgress(ctx context.Context, actor resources.ActorAttribu if err != nil { return nil, fmt.Errorf("invalid egress gateway address %q: %w", gateway.GetAddress(), err) } - certificateSource, err := atunnel.NewBrokerCertificateSource(atunnel.BrokerConfig{ + certificateSource, err := egress.NewBrokerCertificateSource(egress.BrokerConfig{ SocketPath: nodepath.WorkerSupportSocket, CredentialBundlePath: t.cfg.CredentialBundle, TrustBundlePath: t.cfg.TrustBundle, @@ -222,7 +224,7 @@ func (t *Tunnel) PrepareEgress(ctx context.Context, actor resources.ActorAttribu if err != nil { return nil, fmt.Errorf("while obtaining actor certificate: %w", err) } - gatewayClient, err := atunnel.NewClient(atunnel.ClientConfig{ + gatewayClient, err := egress.NewClient(egress.ClientConfig{ GatewayAddress: gateway.GetAddress(), ServerName: serverName, GetClientCertificate: certificateSource.GetClientCertificate, @@ -236,14 +238,14 @@ func (t *Tunnel) PrepareEgress(ctx context.Context, actor resources.ActorAttribu // Activate starts admitting the actor's traffic. Ingress reaches the actor // through dial; egress is activated only when it was prepared. -func (t *Tunnel) Activate(actor resources.ActorAttribution, dial atunnel.DialFunc, egress *ActorEgress) error { +func (t *Tunnel) Activate(actor resources.ActorAttribution, dial ingress.DialFunc, actorEgress *ActorEgress) error { if err := t.Ingress.Activate(actor.Ref.Atespace, actor.Ref.Name, actor.UID, dial); err != nil { return fmt.Errorf("while activating actor ingress: %w", err) } - if egress == nil { + if actorEgress == nil { return nil } - if err := t.Egress.Activate(actor.UID, egress.client, egress.certificateSource, egress.expiresAt); err != nil { + if err := t.Egress.Activate(actor.UID, actorEgress.client, actorEgress.certificateSource, actorEgress.expiresAt); err != nil { return fmt.Errorf("while activating actor egress: %w", err) } return nil diff --git a/internal/ateworker/actornet/dns/dnshandler.go b/internal/atunnel/dns/dnshandler.go similarity index 99% rename from internal/ateworker/actornet/dns/dnshandler.go rename to internal/atunnel/dns/dnshandler.go index 665e5a30b0..33c6f3c689 100644 --- a/internal/ateworker/actornet/dns/dnshandler.go +++ b/internal/atunnel/dns/dnshandler.go @@ -21,7 +21,7 @@ import ( "net" "time" - "github.com/agent-substrate/substrate/internal/ateworker/actornet/dns/protocol" + "github.com/agent-substrate/substrate/internal/atunnel/dns/protocol" "golang.org/x/net/dns/dnsmessage" ) diff --git a/internal/ateworker/actornet/dns/dnshandler_test.go b/internal/atunnel/dns/dnshandler_test.go similarity index 99% rename from internal/ateworker/actornet/dns/dnshandler_test.go rename to internal/atunnel/dns/dnshandler_test.go index 1551212e2a..09cbbd64d7 100644 --- a/internal/ateworker/actornet/dns/dnshandler_test.go +++ b/internal/atunnel/dns/dnshandler_test.go @@ -20,7 +20,7 @@ import ( "net" "testing" - "github.com/agent-substrate/substrate/internal/ateworker/actornet/dns/protocol" + "github.com/agent-substrate/substrate/internal/atunnel/dns/protocol" "golang.org/x/net/dns/dnsmessage" ) diff --git a/internal/ateworker/actornet/dns/evaluator.go b/internal/atunnel/dns/evaluator.go similarity index 100% rename from internal/ateworker/actornet/dns/evaluator.go rename to internal/atunnel/dns/evaluator.go diff --git a/internal/ateworker/actornet/dns/evaluator_test.go b/internal/atunnel/dns/evaluator_test.go similarity index 100% rename from internal/ateworker/actornet/dns/evaluator_test.go rename to internal/atunnel/dns/evaluator_test.go diff --git a/internal/ateworker/actornet/dns/freemap/doc.go b/internal/atunnel/dns/freemap/doc.go similarity index 100% rename from internal/ateworker/actornet/dns/freemap/doc.go rename to internal/atunnel/dns/freemap/doc.go diff --git a/internal/ateworker/actornet/dns/freemap/freemap64k.go b/internal/atunnel/dns/freemap/freemap64k.go similarity index 100% rename from internal/ateworker/actornet/dns/freemap/freemap64k.go rename to internal/atunnel/dns/freemap/freemap64k.go diff --git a/internal/ateworker/actornet/dns/freemap/freemap64k_test.go b/internal/atunnel/dns/freemap/freemap64k_test.go similarity index 100% rename from internal/ateworker/actornet/dns/freemap/freemap64k_test.go rename to internal/atunnel/dns/freemap/freemap64k_test.go diff --git a/internal/ateworker/actornet/dns/limiter.go b/internal/atunnel/dns/limiter.go similarity index 100% rename from internal/ateworker/actornet/dns/limiter.go rename to internal/atunnel/dns/limiter.go diff --git a/internal/ateworker/actornet/dns/limiter_test.go b/internal/atunnel/dns/limiter_test.go similarity index 100% rename from internal/ateworker/actornet/dns/limiter_test.go rename to internal/atunnel/dns/limiter_test.go diff --git a/internal/ateworker/actornet/dns/pendingreq.go b/internal/atunnel/dns/pendingreq.go similarity index 97% rename from internal/ateworker/actornet/dns/pendingreq.go rename to internal/atunnel/dns/pendingreq.go index 65a04e7c5e..801cead055 100644 --- a/internal/ateworker/actornet/dns/pendingreq.go +++ b/internal/atunnel/dns/pendingreq.go @@ -21,8 +21,8 @@ import ( "sync" "time" - "github.com/agent-substrate/substrate/internal/ateworker/actornet/dns/freemap" - "github.com/agent-substrate/substrate/internal/ateworker/actornet/dns/protocol" + "github.com/agent-substrate/substrate/internal/atunnel/dns/freemap" + "github.com/agent-substrate/substrate/internal/atunnel/dns/protocol" "golang.org/x/net/dns/dnsmessage" ) diff --git a/internal/ateworker/actornet/dns/pendingreq_test.go b/internal/atunnel/dns/pendingreq_test.go similarity index 100% rename from internal/ateworker/actornet/dns/pendingreq_test.go rename to internal/atunnel/dns/pendingreq_test.go diff --git a/internal/ateworker/actornet/dns/policy.go b/internal/atunnel/dns/policy.go similarity index 100% rename from internal/ateworker/actornet/dns/policy.go rename to internal/atunnel/dns/policy.go diff --git a/internal/ateworker/actornet/dns/policy_test.go b/internal/atunnel/dns/policy_test.go similarity index 99% rename from internal/ateworker/actornet/dns/policy_test.go rename to internal/atunnel/dns/policy_test.go index aec5aabb8e..3648853ec0 100644 --- a/internal/ateworker/actornet/dns/policy_test.go +++ b/internal/atunnel/dns/policy_test.go @@ -25,7 +25,7 @@ import ( "github.com/google/go-cmp/cmp" "golang.org/x/net/dns/dnsmessage" - "github.com/agent-substrate/substrate/internal/ateworker/actornet/dns/protocol" + "github.com/agent-substrate/substrate/internal/atunnel/dns/protocol" "github.com/agent-substrate/substrate/internal/proto/ateletpb" ) diff --git a/internal/ateworker/actornet/dns/protocol/doc.go b/internal/atunnel/dns/protocol/doc.go similarity index 100% rename from internal/ateworker/actornet/dns/protocol/doc.go rename to internal/atunnel/dns/protocol/doc.go diff --git a/internal/ateworker/actornet/dns/protocol/protocol.go b/internal/atunnel/dns/protocol/protocol.go similarity index 100% rename from internal/ateworker/actornet/dns/protocol/protocol.go rename to internal/atunnel/dns/protocol/protocol.go diff --git a/internal/ateworker/actornet/dns/protocol/protocol_test.go b/internal/atunnel/dns/protocol/protocol_test.go similarity index 100% rename from internal/ateworker/actornet/dns/protocol/protocol_test.go rename to internal/atunnel/dns/protocol/protocol_test.go diff --git a/internal/ateworker/actornet/dns/protocol/tcp.go b/internal/atunnel/dns/protocol/tcp.go similarity index 100% rename from internal/ateworker/actornet/dns/protocol/tcp.go rename to internal/atunnel/dns/protocol/tcp.go diff --git a/internal/ateworker/actornet/dns/protocol/tcp_test.go b/internal/atunnel/dns/protocol/tcp_test.go similarity index 100% rename from internal/ateworker/actornet/dns/protocol/tcp_test.go rename to internal/atunnel/dns/protocol/tcp_test.go diff --git a/internal/ateworker/actornet/dns/readme.md b/internal/atunnel/dns/readme.md similarity index 100% rename from internal/ateworker/actornet/dns/readme.md rename to internal/atunnel/dns/readme.md diff --git a/internal/ateworker/actornet/dns/relay.go b/internal/atunnel/dns/relay.go similarity index 100% rename from internal/ateworker/actornet/dns/relay.go rename to internal/atunnel/dns/relay.go diff --git a/internal/ateworker/actornet/dns/relay_linux.go b/internal/atunnel/dns/relay_linux.go similarity index 100% rename from internal/ateworker/actornet/dns/relay_linux.go rename to internal/atunnel/dns/relay_linux.go diff --git a/internal/ateworker/actornet/dns/relay_test.go b/internal/atunnel/dns/relay_test.go similarity index 100% rename from internal/ateworker/actornet/dns/relay_test.go rename to internal/atunnel/dns/relay_test.go diff --git a/internal/atunnel/dns/resolvconf.go b/internal/atunnel/dns/resolvconf.go new file mode 100644 index 0000000000..dbdd9d87be --- /dev/null +++ b/internal/atunnel/dns/resolvconf.go @@ -0,0 +1,58 @@ +// 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 dns answers an actor's DNS from its sandbox's gateway namespace. +package dns + +import ( + "bufio" + "fmt" + "net" + "os" + "strings" +) + +// resolvConfNameservers reads nameservers from resolv.conf as "host:53". +func resolvConfNameservers(path string) ([]string, error) { + f, err := os.Open(path) + if err != nil { + return nil, fmt.Errorf("dns: reading resolv.conf: %w", err) + } + defer f.Close() + + var out []string + scanner := bufio.NewScanner(f) + for scanner.Scan() { + line := strings.TrimSpace(scanner.Text()) + if i := strings.IndexAny(line, "#;"); i >= 0 { + line = strings.TrimSpace(line[:i]) + } + rest, ok := strings.CutPrefix(line, "nameserver") + if !ok { + continue + } + address := strings.TrimSpace(rest) + if address == "" || net.ParseIP(address) == nil { + continue + } + out = append(out, net.JoinHostPort(address, "53")) + } + if err := scanner.Err(); err != nil { + return nil, fmt.Errorf("dns: reading resolv.conf: %w", err) + } + if len(out) == 0 { + return nil, fmt.Errorf("dns: %s names no usable nameserver", path) + } + return out, nil +} diff --git a/internal/atunnel/dns/resolvconf_test.go b/internal/atunnel/dns/resolvconf_test.go new file mode 100644 index 0000000000..a9a789cbf8 --- /dev/null +++ b/internal/atunnel/dns/resolvconf_test.go @@ -0,0 +1,70 @@ +// 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 dns + +import ( + "os" + "path/filepath" + "testing" + + "github.com/google/go-cmp/cmp" +) + +func TestResolvConfNameservers(t *testing.T) { + for _, tc := range []struct { + name string + content string + want []string + wantErr bool + }{ + { + name: "cluster resolv.conf", + content: "search ate-system.svc.cluster.local svc.cluster.local\nnameserver 10.96.0.10\noptions ndots:5\n", + want: []string{"10.96.0.10:53"}, + }, + { + name: "several, in order", + content: "nameserver 10.96.0.10\nnameserver 8.8.8.8\n", + want: []string{"10.96.0.10:53", "8.8.8.8:53"}, + }, + { + name: "comments and blanks", + content: "# generated\n\n nameserver 10.96.0.10 # cluster\n;nameserver 1.1.1.1\n", + want: []string{"10.96.0.10:53"}, + }, + {name: "no nameserver", content: "search cluster.local\n", wantErr: true}, + {name: "unparsable address", content: "nameserver not-an-ip\n", wantErr: true}, + } { + t.Run(tc.name, func(t *testing.T) { + path := filepath.Join(t.TempDir(), "resolv.conf") + if err := os.WriteFile(path, []byte(tc.content), 0o644); err != nil { + t.Fatal(err) + } + got, err := resolvConfNameservers(path) + if tc.wantErr { + if err == nil { + t.Fatalf("resolvConfNameservers() = %v, want an error", got) + } + return + } + if err != nil { + t.Fatalf("resolvConfNameservers: %v", err) + } + if diff := cmp.Diff(tc.want, got); diff != "" { + t.Errorf("nameservers mismatch (-want +got):\n%s", diff) + } + }) + } +} diff --git a/internal/ateworker/actornet/dns/serve_linux_test.go b/internal/atunnel/dns/serve_linux_test.go similarity index 100% rename from internal/ateworker/actornet/dns/serve_linux_test.go rename to internal/atunnel/dns/serve_linux_test.go diff --git a/internal/ateworker/actornet/dns/server.go b/internal/atunnel/dns/server.go similarity index 100% rename from internal/ateworker/actornet/dns/server.go rename to internal/atunnel/dns/server.go diff --git a/internal/ateworker/actornet/dns/tcphandler.go b/internal/atunnel/dns/tcphandler.go similarity index 98% rename from internal/ateworker/actornet/dns/tcphandler.go rename to internal/atunnel/dns/tcphandler.go index e3eab1c011..109e5071b1 100644 --- a/internal/ateworker/actornet/dns/tcphandler.go +++ b/internal/atunnel/dns/tcphandler.go @@ -24,7 +24,7 @@ import ( "sync" "time" - "github.com/agent-substrate/substrate/internal/ateworker/actornet/dns/protocol" + "github.com/agent-substrate/substrate/internal/atunnel/dns/protocol" ) const ( diff --git a/internal/ateworker/actornet/dns/tcphandler_test.go b/internal/atunnel/dns/tcphandler_test.go similarity index 98% rename from internal/ateworker/actornet/dns/tcphandler_test.go rename to internal/atunnel/dns/tcphandler_test.go index d79bd266f9..32bf69c582 100644 --- a/internal/ateworker/actornet/dns/tcphandler_test.go +++ b/internal/atunnel/dns/tcphandler_test.go @@ -22,7 +22,7 @@ import ( "testing" "time" - "github.com/agent-substrate/substrate/internal/ateworker/actornet/dns/protocol" + "github.com/agent-substrate/substrate/internal/atunnel/dns/protocol" "golang.org/x/net/dns/dnsmessage" ) diff --git a/internal/ateworker/actornet/dns/udphandler.go b/internal/atunnel/dns/udphandler.go similarity index 100% rename from internal/ateworker/actornet/dns/udphandler.go rename to internal/atunnel/dns/udphandler.go diff --git a/internal/ateworker/actornet/dns/udphandler_test.go b/internal/atunnel/dns/udphandler_test.go similarity index 100% rename from internal/ateworker/actornet/dns/udphandler_test.go rename to internal/atunnel/dns/udphandler_test.go diff --git a/internal/atunnel/client.go b/internal/atunnel/egress/client.go similarity index 90% rename from internal/atunnel/client.go rename to internal/atunnel/egress/client.go index cc6077c839..5c844c1f1a 100644 --- a/internal/atunnel/client.go +++ b/internal/atunnel/egress/client.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package atunnel +package egress import ( "bufio" @@ -43,13 +43,13 @@ type ClientConfig struct { TrustBundlePath string } -// DialFunc dials a network address. It matches net.Dialer.DialContext. -type DialFunc func(ctx context.Context, network, address string) (net.Conn, error) +// dialFunc dials a network address. It matches net.Dialer.DialContext. +type dialFunc func(ctx context.Context, network, address string) (net.Conn, error) // ErrGatewayHandshake reports that the gateway's front door refused the // connection at TLS: it rejected the client certificate, or its own // certificate did not verify. -var ErrGatewayHandshake = errors.New("atunnel: egress gateway TLS handshake") +var ErrGatewayHandshake = errors.New("egress: gateway TLS handshake") // ConnectRejectedError reports a CONNECT the gateway answered with a non-2xx // status. The caller authenticated successfully and the request was declined @@ -58,23 +58,23 @@ var ErrGatewayHandshake = errors.New("atunnel: egress gateway TLS handshake") // caller can act on. type ConnectRejectedError struct { StatusCode int - // Status is the full status line, e.g. "403 Forbidden". - Status string - // Message is the response body, or the status text when the body is empty. - Message string + // status is the full status line, e.g. "403 Forbidden". + status string + // message is the response body, or the status text when the body is empty. + message string } func (e *ConnectRejectedError) Error() string { - return fmt.Sprintf("atunnel: egress gateway rejected CONNECT with %s: %s", e.Status, e.Message) + return fmt.Sprintf("egress: gateway rejected CONNECT with %s: %s", e.status, e.message) } -// ClientOption customizes a Client beyond its configuration. Production +// clientOption customizes a Client beyond its configuration. Production // callers need none of these. -type ClientOption func(*Client) +type clientOption func(*Client) -// WithDialer overrides how the client reaches the egress gateway. It exists so +// withDialer overrides how the client reaches the egress gateway. It exists so // tests can substitute a transport; by default a net.Dialer is used. -func WithDialer(dial DialFunc) ClientOption { +func withDialer(dial dialFunc) clientOption { return func(c *Client) { if dial != nil { c.dialContext = dial @@ -86,7 +86,7 @@ func WithDialer(dial DialFunc) ClientOption { type Client struct { gatewayAddress string tlsConfig *tls.Config - dialContext DialFunc + dialContext dialFunc mu sync.Mutex closed bool @@ -94,30 +94,30 @@ type Client struct { dialing *dialCall } -// Client implements egressDialer. -var _ egressDialer = (*Client)(nil) +// Client implements tunnelDialer. +var _ tunnelDialer = (*Client)(nil) // NewClient creates an egress CONNECT client and validates its TLS material. -func NewClient(cfg ClientConfig, opts ...ClientOption) (*Client, error) { +func NewClient(cfg ClientConfig, opts ...clientOption) (*Client, error) { if _, _, err := net.SplitHostPort(cfg.GatewayAddress); err != nil { - return nil, fmt.Errorf("atunnel: invalid egress gateway address %q: %w", cfg.GatewayAddress, err) + return nil, fmt.Errorf("egress: invalid egress gateway address %q: %w", cfg.GatewayAddress, err) } if cfg.ServerName == "" { - return nil, fmt.Errorf("atunnel: egress gateway server name is required") + return nil, fmt.Errorf("egress: gateway server name is required") } if cfg.GetClientCertificate == nil { - return nil, fmt.Errorf("atunnel: client certificate source is required") + return nil, fmt.Errorf("egress: client certificate source is required") } if cfg.TrustBundlePath == "" { - return nil, fmt.Errorf("atunnel: trust bundle path is required") + return nil, fmt.Errorf("egress: trust bundle path is required") } trustPEM, err := os.ReadFile(cfg.TrustBundlePath) if err != nil { - return nil, fmt.Errorf("atunnel: reading trust bundle: %w", err) + return nil, fmt.Errorf("egress: reading trust bundle: %w", err) } rootCAs := x509.NewCertPool() if !rootCAs.AppendCertsFromPEM(trustPEM) { - return nil, fmt.Errorf("atunnel: trust bundle %q contains no certificates", cfg.TrustBundlePath) + return nil, fmt.Errorf("egress: trust bundle %q contains no certificates", cfg.TrustBundlePath) } client := &Client{ @@ -197,7 +197,7 @@ type dialCall struct { func (c *Client) acquireConn(ctx context.Context) (*h2ClientConn, bool, *tls.Conn, error) { for { if err := ctx.Err(); err != nil { - return nil, false, nil, fmt.Errorf("atunnel: connecting to egress gateway: %w", err) + return nil, false, nil, fmt.Errorf("egress: connecting to egress gateway: %w", err) } // First check if there is an existing H/2 connection with available stream capacity. @@ -205,7 +205,7 @@ func (c *Client) acquireConn(ctx context.Context) (*h2ClientConn, bool, *tls.Con c.mu.Lock() if c.closed { c.mu.Unlock() - return nil, false, nil, fmt.Errorf("atunnel: connecting to egress gateway: %w", net.ErrClosed) + return nil, false, nil, fmt.Errorf("egress: connecting to egress gateway: %w", net.ErrClosed) } keep := c.h2Conns[:0] var reserved *h2ClientConn @@ -275,7 +275,7 @@ func (c *Client) acquireConn(ctx context.Context) (*h2ClientConn, bool, *tls.Con if err != nil { if ctx.Err() != nil { - return nil, false, nil, fmt.Errorf("atunnel: connecting to egress gateway: %w", ctx.Err()) + return nil, false, nil, fmt.Errorf("egress: connecting to egress gateway: %w", ctx.Err()) } return nil, false, nil, err } @@ -309,7 +309,7 @@ func (c *Client) acquireConn(ctx context.Context) (*h2ClientConn, bool, *tls.Con if orphan != nil { _ = orphan.Close() } - return nil, false, nil, fmt.Errorf("atunnel: connecting to egress gateway: %w", ctx.Err()) + return nil, false, nil, fmt.Errorf("egress: connecting to egress gateway: %w", ctx.Err()) } } } @@ -325,7 +325,7 @@ func (c *Client) runDial(dialCtx context.Context, call *dialCall) { ) rawConn, dialErr := c.dialContext(dialCtx, "tcp", c.gatewayAddress) if dialErr != nil { - err = fmt.Errorf("atunnel: connecting to egress gateway: %w", dialErr) + err = fmt.Errorf("egress: connecting to egress gateway: %w", dialErr) } else { tlsConn := tls.Client(rawConn, c.tlsConfig.Clone()) if hsErr := tlsConn.HandshakeContext(dialCtx); hsErr != nil { @@ -345,7 +345,7 @@ func (c *Client) runDial(dialCtx context.Context, call *dialCall) { } if c.closed || call.waiters == 0 { if c.closed && err == nil { - call.err = fmt.Errorf("atunnel: connecting to egress gateway: %w", net.ErrClosed) + call.err = fmt.Errorf("egress: connecting to egress gateway: %w", net.ErrClosed) } else { call.err = err } @@ -509,8 +509,8 @@ func newConnectRejectedError(resp *http.Response, body []byte) *ConnectRejectedE } return &ConnectRejectedError{ StatusCode: resp.StatusCode, - Status: resp.Status, - Message: message, + status: resp.Status, + message: message, } } @@ -997,7 +997,7 @@ func connectExchangeError(op string, err error) error { if gatewayHungUp(err) { return fmt.Errorf("%w: %s: %w", ErrGatewayHandshake, op, err) } - return fmt.Errorf("atunnel: %s: %w", op, err) + return fmt.Errorf("egress: %s: %w", op, err) } // gatewayHungUp reports whether err is the peer tearing the connection down, @@ -1025,16 +1025,16 @@ func gatewayHungUp(err error) bool { func validateDestination(destination string) error { host, port, err := net.SplitHostPort(destination) if err != nil { - return fmt.Errorf("atunnel: invalid egress destination %q: %w", destination, err) + return fmt.Errorf("egress: invalid egress destination %q: %w", destination, err) } if host == "" { - return fmt.Errorf("atunnel: invalid egress destination %q: host is empty", destination) + return fmt.Errorf("egress: invalid egress destination %q: host is empty", destination) } if net.ParseIP(host) == nil { - return fmt.Errorf("atunnel: invalid egress destination %q: host must be an IP address", destination) + return fmt.Errorf("egress: invalid egress destination %q: host must be an IP address", destination) } - if _, ok := ParsePort(port); !ok { - return fmt.Errorf("atunnel: invalid egress destination %q: port must be between 1 and 65535", destination) + if _, ok := parsePort(port); !ok { + return fmt.Errorf("egress: invalid egress destination %q: port must be between 1 and 65535", destination) } return nil } diff --git a/internal/atunnel/client_test.go b/internal/atunnel/egress/client_test.go similarity index 96% rename from internal/atunnel/client_test.go rename to internal/atunnel/egress/client_test.go index c4114ea7ac..8054cc31e0 100644 --- a/internal/atunnel/client_test.go +++ b/internal/atunnel/egress/client_test.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package atunnel +package egress import ( "bufio" @@ -57,7 +57,7 @@ func TestClientDialContext(t *testing.T) { t.Errorf("tunneled payload = %q, want ping", payload) } }) - client := newTestClient(t, ca, WithDialer(dialFixedAddress(gatewayAddress))) + client := newTestClient(t, ca, withDialer(dialFixedAddress(gatewayAddress))) conn, err := client.DialContext(context.Background(), "192.0.2.10:443") if err != nil { @@ -125,7 +125,7 @@ func TestClientDialContextHTTP2(t *testing.T) { } flusher.Flush() })) - client := newTestClient(t, ca, WithDialer(dialFixedAddress(gatewayAddress))) + client := newTestClient(t, ca, withDialer(dialFixedAddress(gatewayAddress))) dialCtx, cancelDial := context.WithCancel(context.Background()) conn, err := client.DialContext(dialCtx, "192.0.2.10:443") @@ -214,7 +214,7 @@ func TestClientDialContextHTTP2MultiplexesStreams(t *testing.T) { })) var tcpDials atomic.Int32 - client := newTestClient(t, ca, WithDialer(func(ctx context.Context, network, _ string) (net.Conn, error) { + client := newTestClient(t, ca, withDialer(func(ctx context.Context, network, _ string) (net.Conn, error) { tcpDials.Add(1) return (&net.Dialer{}).DialContext(ctx, network, gatewayAddress) })) @@ -272,7 +272,7 @@ func TestClientDialContextHTTP2ReconnectsAfterConnectionClose(t *testing.T) { })) var tcpDials atomic.Int32 - client := newTestClient(t, ca, WithDialer(func(ctx context.Context, network, _ string) (net.Conn, error) { + client := newTestClient(t, ca, withDialer(func(ctx context.Context, network, _ string) (net.Conn, error) { tcpDials.Add(1) return (&net.Dialer{}).DialContext(ctx, network, gatewayAddress) })) @@ -320,7 +320,7 @@ func TestClientDialContextHTTP2Deadlines(t *testing.T) { <-releaseServerSend _, _ = io.WriteString(w, "delayed") })) - client := newTestClient(t, ca, WithDialer(dialFixedAddress(gatewayAddress))) + client := newTestClient(t, ca, withDialer(dialFixedAddress(gatewayAddress))) conn, err := client.DialContext(context.Background(), "192.0.2.10:443") if err != nil { @@ -356,7 +356,7 @@ func TestClientDialContextRejected(t *testing.T) { body := "denied by policy" _, _ = fmt.Fprintf(conn, "HTTP/1.1 403 Forbidden\r\nContent-Length: %d\r\n\r\n%s", len(body), body) }) - client := newTestClient(t, ca, WithDialer(dialFixedAddress(gatewayAddress))) + client := newTestClient(t, ca, withDialer(dialFixedAddress(gatewayAddress))) _, err := client.DialContext(context.Background(), "192.0.2.10:443") if err == nil || !strings.Contains(err.Error(), "denied by policy") { @@ -377,7 +377,7 @@ func TestClientDialContextRejected(t *testing.T) { } })) var tcpDials atomic.Int32 - client := newTestClient(t, ca, WithDialer(func(ctx context.Context, network, _ string) (net.Conn, error) { + client := newTestClient(t, ca, withDialer(func(ctx context.Context, network, _ string) (net.Conn, error) { tcpDials.Add(1) return (&net.Dialer{}).DialContext(ctx, network, gatewayAddress) })) @@ -387,7 +387,7 @@ func TestClientDialContextRejected(t *testing.T) { if !errors.As(err, &rejected) { t.Fatalf("DialContext error = %v, want *ConnectRejectedError", err) } - if rejected.StatusCode != http.StatusForbidden || rejected.Message != "denied by policy" { + if rejected.StatusCode != http.StatusForbidden || rejected.message != "denied by policy" { t.Errorf("ConnectRejectedError = %+v, want 403 denied by policy", rejected) } @@ -431,7 +431,7 @@ func TestClientDialContextGatewayRefusesClientCertificate(t *testing.T) { // client's certificate, which is the shape of an actor presenting a // podidentity credential to a door that only accepts actor identity. gatewayAddress := serveTestRefusingGateway(t, ca, newTestCA(t), tt.maxTLSVersion, tt.nextProtos) - client := newTestClient(t, ca, WithDialer(dialFixedAddress(gatewayAddress))) + client := newTestClient(t, ca, withDialer(dialFixedAddress(gatewayAddress))) _, err := client.DialContext(context.Background(), "192.0.2.10:443") if err == nil { @@ -461,7 +461,7 @@ func TestClientDialContextGatewayHangsUpBeforeResponding(t *testing.T) { gatewayAddress := serveTestConnectGateway(t, ca, func(conn net.Conn, _ *http.Request) { _ = conn.Close() }) - client := newTestClient(t, ca, WithDialer(dialFixedAddress(gatewayAddress))) + client := newTestClient(t, ca, withDialer(dialFixedAddress(gatewayAddress))) _, err := client.DialContext(context.Background(), "192.0.2.10:443") if err == nil { @@ -500,7 +500,7 @@ func TestClientDialContextGatewayHangsUpBeforeResponding(t *testing.T) { _ = tlsConn.Close() }() - client := newTestClient(t, ca, WithDialer(dialFixedAddress(listener.Addr().String()))) + client := newTestClient(t, ca, withDialer(dialFixedAddress(listener.Addr().String()))) _, err = client.DialContext(context.Background(), "192.0.2.10:443") if err == nil { t.Fatal("DialContext succeeded against an HTTP/2 gateway that hung up") @@ -600,7 +600,7 @@ func TestClientClose(t *testing.T) { })) connClosed := make(chan struct{}) - client := newTestClient(t, ca, WithDialer(func(ctx context.Context, network, _ string) (net.Conn, error) { + client := newTestClient(t, ca, withDialer(func(ctx context.Context, network, _ string) (net.Conn, error) { c, err := (&net.Dialer{}).DialContext(ctx, network, gatewayAddr) if err != nil { return nil, err @@ -628,13 +628,13 @@ func TestClientClose(t *testing.T) { // dialFixedAddress ignores the requested address and connects to address, so // tests can point a client at a listener on an ephemeral port. -func dialFixedAddress(address string) DialFunc { +func dialFixedAddress(address string) dialFunc { return func(ctx context.Context, network, _ string) (net.Conn, error) { return (&net.Dialer{}).DialContext(ctx, network, address) } } -func newTestClient(t *testing.T, ca *testCA, opts ...ClientOption) *Client { +func newTestClient(t *testing.T, ca *testCA, opts ...clientOption) *Client { t.Helper() dir := t.TempDir() trustPath := filepath.Join(dir, "trust.pem") diff --git a/internal/atunnel/credential.go b/internal/atunnel/egress/credential.go similarity index 85% rename from internal/atunnel/credential.go rename to internal/atunnel/egress/credential.go index 6a2c861422..ed8cd1ab0b 100644 --- a/internal/atunnel/credential.go +++ b/internal/atunnel/egress/credential.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package atunnel +package egress import ( "context" @@ -80,15 +80,15 @@ func NewBrokerCertificateSource(cfg BrokerConfig) (*BrokerCertificateSource, err } if cfg.AteletSPIFFEID == "" { - return nil, fmt.Errorf("atunnel: expected atelet SPIFFE ID is required") + return nil, fmt.Errorf("egress: expected atelet SPIFFE ID is required") } tlsConfig, err := ateletdial.TLSConfig(cfg.CredentialBundlePath, cfg.TrustBundlePath, cfg.AteletSPIFFEID) if err != nil { - return nil, fmt.Errorf("atunnel: %w", err) + return nil, fmt.Errorf("egress: %w", err) } privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) if err != nil { - return nil, fmt.Errorf("atunnel: generate actor private key: %w", err) + return nil, fmt.Errorf("egress: generate actor private key: %w", err) } return &BrokerCertificateSource{ @@ -106,7 +106,7 @@ func NewBrokerCertificateSource(cfg BrokerConfig) (*BrokerCertificateSource, err func (s *BrokerCertificateSource) MintWorkerCertificate(ctx context.Context) (time.Time, error) { csr, err := x509.CreateCertificateRequest(rand.Reader, &x509.CertificateRequest{}, s.privateKey) if err != nil { - return time.Time{}, fmt.Errorf("atunnel: create actor CSR: %w", err) + return time.Time{}, fmt.Errorf("egress: create actor CSR: %w", err) } // TODO(identity): We should not be re-establishing a gRPC connection for @@ -126,7 +126,7 @@ func (s *BrokerCertificateSource) MintWorkerCertificate(ctx context.Context) (ti CertificateSigningRequest: csr, }) if err != nil { - return time.Time{}, fmt.Errorf("atunnel: mint actor certificate: %w", err) + return time.Time{}, fmt.Errorf("egress: mint actor certificate: %w", err) } // TODO(identity): I don't think we need to check all this stuff. We can @@ -134,21 +134,21 @@ func (s *BrokerCertificateSource) MintWorkerCertificate(ctx context.Context) (ti chain := resp.GetActorCertificates() if len(chain) == 0 { - return time.Time{}, fmt.Errorf("atunnel: credential broker returned no actor certificate") + return time.Time{}, fmt.Errorf("egress: credential broker returned no actor certificate") } leaf, err := x509.ParseCertificate(chain[0]) if err != nil { - return time.Time{}, fmt.Errorf("atunnel: parse actor certificate: %w", err) + return time.Time{}, fmt.Errorf("egress: parse actor certificate: %w", err) } if !s.privateKey.PublicKey.Equal(leaf.PublicKey) { - return time.Time{}, fmt.Errorf("atunnel: actor certificate does not match private key") + return time.Time{}, fmt.Errorf("egress: actor certificate does not match private key") } now := time.Now() if now.Before(leaf.NotBefore) || !leaf.NotAfter.After(now) { - return time.Time{}, fmt.Errorf("atunnel: credential broker returned an invalid actor certificate lifetime") + return time.Time{}, fmt.Errorf("egress: credential broker returned an invalid actor certificate lifetime") } if !slices.Contains(leaf.ExtKeyUsage, x509.ExtKeyUsageClientAuth) { - return time.Time{}, fmt.Errorf("atunnel: actor certificate cannot authenticate a TLS client") + return time.Time{}, fmt.Errorf("egress: actor certificate cannot authenticate a TLS client") } cert := &tls.Certificate{Certificate: chain, PrivateKey: s.privateKey, Leaf: leaf} s.mu.Lock() @@ -163,7 +163,7 @@ func (s *BrokerCertificateSource) GetClientCertificate(*tls.CertificateRequestIn s.mu.RLock() defer s.mu.RUnlock() if s.certificate == nil || !s.certificate.Leaf.NotAfter.After(time.Now()) { - return nil, fmt.Errorf("atunnel: no valid actor certificate") + return nil, fmt.Errorf("egress: no valid actor certificate") } return s.certificate, nil } diff --git a/internal/atunnel/credential_test.go b/internal/atunnel/egress/credential_test.go similarity index 99% rename from internal/atunnel/credential_test.go rename to internal/atunnel/egress/credential_test.go index 381a6ee3b4..fb201c14a0 100644 --- a/internal/atunnel/credential_test.go +++ b/internal/atunnel/egress/credential_test.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package atunnel +package egress import ( "context" diff --git a/internal/atunnel/egress.go b/internal/atunnel/egress/egress.go similarity index 75% rename from internal/atunnel/egress.go rename to internal/atunnel/egress/egress.go index 422ce1d928..c3d7017555 100644 --- a/internal/atunnel/egress.go +++ b/internal/atunnel/egress/egress.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package atunnel +package egress import ( "context" @@ -22,6 +22,7 @@ import ( "log/slog" "math/rand/v2" "net" + "strconv" "sync" "time" @@ -29,8 +30,8 @@ import ( "google.golang.org/grpc/status" ) -// egressDialer opens an authenticated tunnel to an original destination. -type egressDialer interface { +// tunnelDialer opens an authenticated tunnel to an original destination. +type tunnelDialer interface { DialContext(context.Context, string) (net.Conn, error) Close() error } @@ -39,25 +40,25 @@ type actorCertificateSource interface { MintWorkerCertificate(context.Context) (time.Time, error) } -// OriginalDestination returns the address that a transparently intercepted +// originalDestinationFunc returns the address that a transparently intercepted // connection originally targeted. -type OriginalDestination func(net.Conn) (string, error) +type originalDestinationFunc func(net.Conn) (string, error) -// Egress proxies actor TCP connections through an egress CONNECT dialer. It is +// Proxy proxies actor TCP connections through an egress CONNECT dialer. It is // long-lived across actor activations, but only carries traffic while an actor // is assigned to its worker. -type Egress struct { - originalDestination OriginalDestination +type Proxy struct { + originalDestination originalDestinationFunc // tunnelOpenTimeout bounds the wait for the gateway's CONNECT answer. tunnelOpenTimeout time.Duration mu sync.Mutex // Keyed by actor UID, supplied by the namespace-specific listener. - active map[string]*egressActivation + active map[string]*activation } -type egressActivation struct { - dialer egressDialer +type activation struct { + dialer tunnelDialer certificateSource actorCertificateSource expiresAt time.Time @@ -68,26 +69,26 @@ type egressActivation struct { wg sync.WaitGroup } -// NewEgress creates an activation-aware egress proxy. -func NewEgress(originalDestination OriginalDestination) (*Egress, error) { +// New creates an activation-aware egress proxy. +func New(originalDestination originalDestinationFunc) (*Proxy, error) { if originalDestination == nil { - return nil, fmt.Errorf("atunnel: original destination resolver is required") + return nil, fmt.Errorf("egress: original destination resolver is required") } // The gateway answers within its 5s authorization timeout; a longer wait // means it has queued the CONNECT behind its connection cap, and closing // the connection is what frees that queue slot. const tunnelOpenTimeout = 10 * time.Second - return &Egress{ + return &Proxy{ originalDestination: originalDestination, tunnelOpenTimeout: tunnelOpenTimeout, - active: map[string]*egressActivation{}, + active: map[string]*activation{}, }, nil } // Bind captures the actor's activation before its listeners start serving. -func (e *Egress) Bind(actorUID string) (func(context.Context, net.Listener) error, error) { +func (e *Proxy) Bind(actorUID string) (func(context.Context, net.Listener) error, error) { if actorUID == "" { - return nil, fmt.Errorf("atunnel: actor UID is required") + return nil, fmt.Errorf("egress: actor UID is required") } e.mu.Lock() active := e.activationLocked(actorUID) @@ -105,17 +106,17 @@ func (e *Egress) Bind(actorUID string) (func(context.Context, net.Listener) erro }, nil } -func (e *Egress) activationLocked(actorUID string) *egressActivation { +func (e *Proxy) activationLocked(actorUID string) *activation { active := e.active[actorUID] if active == nil { ctx, cancel := context.WithCancel(context.Background()) - active = &egressActivation{ctx: ctx, cancel: cancel} + active = &activation{ctx: ctx, cancel: cancel} e.active[actorUID] = active } return active } -func (e *Egress) serve(ctx context.Context, listener net.Listener, active *egressActivation) error { +func (e *Proxy) serve(ctx context.Context, listener net.Listener, active *activation) error { done := make(chan struct{}) go func() { select { @@ -134,31 +135,31 @@ func (e *Egress) serve(ctx context.Context, listener net.Listener, active *egres if ctx.Err() != nil || errors.Is(err, net.ErrClosed) { return nil } - return fmt.Errorf("atunnel: accepting actor egress connection: %w", err) + return fmt.Errorf("egress: accepting actor egress connection: %w", err) } e.handle(conn, active) } } // Activate enables the actor's egress and certificate renewal until deactivation. -func (e *Egress) Activate(actorUID string, dialer egressDialer, certificateSource actorCertificateSource, expiresAt time.Time) error { +func (e *Proxy) Activate(actorUID string, dialer tunnelDialer, certificateSource actorCertificateSource, expiresAt time.Time) error { if actorUID == "" { - return fmt.Errorf("atunnel: actor UID is required") + return fmt.Errorf("egress: actor UID is required") } if dialer == nil { - return fmt.Errorf("atunnel: egress dialer is required") + return fmt.Errorf("egress: dialer is required") } if certificateSource == nil { - return fmt.Errorf("atunnel: actor certificate source is required") + return fmt.Errorf("egress: actor certificate source is required") } if !expiresAt.After(time.Now()) { - return fmt.Errorf("atunnel: valid actor certificate is required") + return fmt.Errorf("egress: valid actor certificate is required") } e.mu.Lock() defer e.mu.Unlock() active := e.activationLocked(actorUID) if active.dialer != nil { - return fmt.Errorf("atunnel: actor %s already has active egress", actorUID) + return fmt.Errorf("egress: actor %s already has active egress", actorUID) } active.dialer = dialer active.certificateSource = certificateSource @@ -168,7 +169,7 @@ func (e *Egress) Activate(actorUID string, dialer egressDialer, certificateSourc return nil } -func (e *Egress) renew(active *egressActivation, expiresAt time.Time) { +func (e *Proxy) renew(active *activation, expiresAt time.Time) { defer active.wg.Done() // Schedule from the credential's remaining lifetime: renew at 90%, then // keep retrying after expiry so egress can recover without reactivation. @@ -249,7 +250,7 @@ func waitForRenewal(ctx context.Context, delay time.Duration) bool { } // Deactivate disables the actor's egress and closes and drains its streams. -func (e *Egress) Deactivate(ctx context.Context, actorUID string) error { +func (e *Proxy) Deactivate(ctx context.Context, actorUID string) error { e.mu.Lock() active := e.active[actorUID] delete(e.active, actorUID) @@ -271,7 +272,7 @@ func (e *Egress) Deactivate(ctx context.Context, actorUID string) error { select { case <-done: case <-ctx.Done(): - err = fmt.Errorf("atunnel: waiting for active egress streams to stop: %w", ctx.Err()) + err = fmt.Errorf("egress: waiting for active egress streams to stop: %w", ctx.Err()) } if active.dialer != nil { err = errors.Join(err, active.dialer.Close()) @@ -279,7 +280,7 @@ func (e *Egress) Deactivate(ctx context.Context, actorUID string) error { return err } -func (e *Egress) handle(downstream net.Conn, active *egressActivation) { +func (e *Proxy) handle(downstream net.Conn, active *activation) { e.mu.Lock() if active == nil || active.ctx.Err() != nil { e.mu.Unlock() @@ -346,17 +347,27 @@ func closeWrite(conn net.Conn) { } } -// EgressPort is the port from a listen address, which each sandbox's redirect -// aims at. The address itself is never bound: egress is served from inside the -// sandbox namespaces. -func EgressPort(listenAddress string) (uint16, error) { +// PortFromListenAddr is the port from a listen address, which each sandbox's +// redirect aims at. The address itself is never bound: egress is served from +// inside the sandbox namespaces. +func PortFromListenAddr(listenAddress string) (uint16, error) { _, port, err := net.SplitHostPort(listenAddress) if err != nil { - return 0, fmt.Errorf("atunnel: egress listen address %q: %w", listenAddress, err) + return 0, fmt.Errorf("egress: listen address %q: %w", listenAddress, err) } - p, ok := ParsePort(port) + p, ok := parsePort(port) if !ok { - return 0, fmt.Errorf("atunnel: egress listen address %q has no usable port", listenAddress) + return 0, fmt.Errorf("egress: listen address %q has no usable port", listenAddress) } return uint16(p), nil } + +// parsePort parses s as a TCP port number, returning ok=false for anything +// outside the valid 1-65535 range (including non-numeric input). +func parsePort(s string) (port int, ok bool) { + p, err := strconv.Atoi(s) + if err != nil || p < 1 || p > 65535 { + return 0, false + } + return p, true +} diff --git a/internal/atunnel/egress_test.go b/internal/atunnel/egress/egress_test.go similarity index 91% rename from internal/atunnel/egress_test.go rename to internal/atunnel/egress/egress_test.go index 75de28f6d0..e3b067e5fc 100644 --- a/internal/atunnel/egress_test.go +++ b/internal/atunnel/egress/egress_test.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package atunnel +package egress import ( "context" @@ -55,7 +55,7 @@ func (listener *delayedEgressListener) Accept() (net.Conn, error) { func TestEgressBindingDoesNotOutliveItsActivation(t *testing.T) { for _, lateServe := range []bool{false, true} { t.Run(fmt.Sprintf("lateServe=%t", lateServe), func(t *testing.T) { - egress, err := NewEgress(func(net.Conn) (string, error) { return "192.0.2.10:443", nil }) + egress, err := New(func(net.Conn) (string, error) { return "192.0.2.10:443", nil }) if err != nil { t.Fatal(err) } @@ -63,7 +63,7 @@ func TestEgressBindingDoesNotOutliveItsActivation(t *testing.T) { dialed := make(chan string, 2) activate := func(generation string) { t.Helper() - if err := egress.Activate(testActorUID, egressDialerFunc(func(context.Context, string) (net.Conn, error) { + if err := egress.Activate(testActorUID, tunnelDialerFunc(func(context.Context, string) (net.Conn, error) { dialed <- generation return nil, errors.New("test dial") }), fakeActorCertificateSource{}, time.Now().Add(time.Hour)); err != nil { @@ -148,7 +148,7 @@ func TestEgressBindingDoesNotOutliveItsActivation(t *testing.T) { } func TestEgressUnactivatedBindingCloses(t *testing.T) { - egress, err := NewEgress(func(net.Conn) (string, error) { return "", nil }) + egress, err := New(func(net.Conn) (string, error) { return "", nil }) if err != nil { t.Fatal(err) } @@ -173,11 +173,11 @@ func TestEgressUnactivatedBindingCloses(t *testing.T) { } func TestEgressActivationFailsClosed(t *testing.T) { - egress, err := NewEgress(func(net.Conn) (string, error) { return "", nil }) + egress, err := New(func(net.Conn) (string, error) { return "", nil }) if err != nil { t.Fatal(err) } - dialer := egressDialerFunc(func(context.Context, string) (net.Conn, error) { + dialer := tunnelDialerFunc(func(context.Context, string) (net.Conn, error) { t.Fatal("dialed after failed activation") return nil, nil }) @@ -200,11 +200,11 @@ func TestEgressExpiryRejectsNewButPreservesEstablished(t *testing.T) { defer upstreamGateway.Close() var dials atomic.Int32 var mints atomic.Int32 - dialer := egressDialerFunc(func(context.Context, string) (net.Conn, error) { + dialer := tunnelDialerFunc(func(context.Context, string) (net.Conn, error) { dials.Add(1) return upstreamProxy, nil }) - egress, err := NewEgress(func(net.Conn) (string, error) { return "192.0.2.10:443", nil }) + egress, err := New(func(net.Conn) (string, error) { return "192.0.2.10:443", nil }) if err != nil { t.Fatal(err) } @@ -257,10 +257,10 @@ func TestEgressRenewsBeforeExpiry(t *testing.T) { } upstream, gateway := net.Pipe() defer gateway.Close() - dialer := egressDialerFunc(func(context.Context, string) (net.Conn, error) { + dialer := tunnelDialerFunc(func(context.Context, string) (net.Conn, error) { return upstream, nil }) - egress, err := NewEgress(func(net.Conn) (string, error) { return "192.0.2.10:443", nil }) + egress, err := New(func(net.Conn) (string, error) { return "192.0.2.10:443", nil }) if err != nil { t.Fatal(err) } @@ -303,11 +303,11 @@ func TestEgressStopsAfterTerminalRenewalFailure(t *testing.T) { for _, code := range []codes.Code{codes.Aborted, codes.FailedPrecondition, codes.PermissionDenied} { t.Run(code.String(), func(t *testing.T) { called := make(chan struct{}, 1) - egress, err := NewEgress(func(net.Conn) (string, error) { return "", nil }) + egress, err := New(func(net.Conn) (string, error) { return "", nil }) if err != nil { t.Fatal(err) } - if err := egress.Activate(testActorUID, egressDialerFunc(func(context.Context, string) (net.Conn, error) { + if err := egress.Activate(testActorUID, tunnelDialerFunc(func(context.Context, string) (net.Conn, error) { t.Fatal("dialed after renewal was denied") return nil, nil }), fakeActorCertificateSource{ @@ -343,11 +343,11 @@ func TestEgressStopsAfterTerminalRenewalFailure(t *testing.T) { func TestEgressDeactivationDropsConcurrentRenewal(t *testing.T) { started := make(chan struct{}, 1) release := make(chan struct{}) - egress, err := NewEgress(func(net.Conn) (string, error) { return "", nil }) + egress, err := New(func(net.Conn) (string, error) { return "", nil }) if err != nil { t.Fatal(err) } - dialer := egressDialerFunc(func(context.Context, string) (net.Conn, error) { return nil, nil }) + dialer := tunnelDialerFunc(func(context.Context, string) (net.Conn, error) { return nil, nil }) if err := egress.Activate(testActorUID, dialer, fakeActorCertificateSource{ expiresAt: time.Now().Add(time.Hour), called: started, @@ -409,9 +409,9 @@ func TestEgressEndToEnd(t *testing.T) { t.Errorf("writing gateway payload: %v", err) } }) - client := newTestClient(t, ca, WithDialer(dialFixedAddress(gatewayAddress))) + client := newTestClient(t, ca, withDialer(dialFixedAddress(gatewayAddress))) - egress, err := NewEgress(func(net.Conn) (string, error) { + egress, err := New(func(net.Conn) (string, error) { return "192.0.2.10:443", nil }) if err != nil { @@ -462,7 +462,7 @@ func TestEgressEndToEnd(t *testing.T) { } func TestEgressRejectsInactiveConnection(t *testing.T) { - egress, err := NewEgress(func(net.Conn) (string, error) { + egress, err := New(func(net.Conn) (string, error) { t.Fatal("inactive egress resolved destination") return "", nil }) @@ -492,9 +492,9 @@ func TestEgressBoundsHowLongATunnelWaitsToOpen(t *testing.T) { <-r.Context().Done() withdrawn <- struct{}{} })) - client := newTestClient(t, ca, WithDialer(dialFixedAddress(gatewayAddress))) + client := newTestClient(t, ca, withDialer(dialFixedAddress(gatewayAddress))) - egress, err := NewEgress(func(net.Conn) (string, error) { return "192.0.2.10:443", nil }) + egress, err := New(func(net.Conn) (string, error) { return "192.0.2.10:443", nil }) if err != nil { t.Fatal(err) } @@ -523,13 +523,13 @@ func TestEgressBoundsHowLongATunnelWaitsToOpen(t *testing.T) { } } -type egressDialerFunc func(context.Context, string) (net.Conn, error) +type tunnelDialerFunc func(context.Context, string) (net.Conn, error) -func (f egressDialerFunc) DialContext(ctx context.Context, destination string) (net.Conn, error) { +func (f tunnelDialerFunc) DialContext(ctx context.Context, destination string) (net.Conn, error) { return f(ctx, destination) } -func (f egressDialerFunc) Close() error { +func (f tunnelDialerFunc) Close() error { return nil } @@ -560,12 +560,12 @@ func (s fakeActorCertificateSource) MintWorkerCertificate(context.Context) (time // Each actor's egress is armed and disarmed on its own: one actor's teardown // must not cut off another's tunnels. func TestEgressIsArmedPerActor(t *testing.T) { - egress, err := NewEgress(func(net.Conn) (string, error) { return "example.com:443", nil }) + egress, err := New(func(net.Conn) (string, error) { return "example.com:443", nil }) if err != nil { t.Fatal(err) } source := fakeActorCertificateSource{expiresAt: time.Now().Add(time.Hour)} - dialer := egressDialerFunc(func(context.Context, string) (net.Conn, error) { + dialer := tunnelDialerFunc(func(context.Context, string) (net.Conn, error) { return nil, errors.New("not dialed in this test") }) for _, uid := range []string{"actor-uid-1", "actor-uid-2"} { @@ -601,7 +601,7 @@ func TestEgressDeactivateClosesIdleH2Connection(t *testing.T) { })) connClosed := make(chan struct{}) - client := newTestClient(t, ca, WithDialer(func(ctx context.Context, network, _ string) (net.Conn, error) { + client := newTestClient(t, ca, withDialer(func(ctx context.Context, network, _ string) (net.Conn, error) { c, err := (&net.Dialer{}).DialContext(ctx, network, gatewayAddr) if err != nil { return nil, err @@ -609,7 +609,7 @@ func TestEgressDeactivateClosesIdleH2Connection(t *testing.T) { return &closeNotifyingConn{Conn: c, closed: connClosed}, nil })) - egress, err := NewEgress(func(net.Conn) (string, error) { return "192.0.2.10:443", nil }) + egress, err := New(func(net.Conn) (string, error) { return "192.0.2.10:443", nil }) if err != nil { t.Fatal(err) } diff --git a/internal/atunnel/egress/testhelpers_test.go b/internal/atunnel/egress/testhelpers_test.go new file mode 100644 index 0000000000..91708eecf3 --- /dev/null +++ b/internal/atunnel/egress/testhelpers_test.go @@ -0,0 +1,138 @@ +// 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 egress + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/tls" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "math/big" + "net/url" + "os" + "testing" + "time" +) + +type testCA struct { + cert *x509.Certificate + key *ecdsa.PrivateKey + certPEM []byte +} + +func newTestCA(t *testing.T) *testCA { + t.Helper() + key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + t.Fatal(err) + } + now := time.Now() + template := &x509.Certificate{ + SerialNumber: big.NewInt(now.UnixNano()), + Subject: pkix.Name{CommonName: "test CA"}, + NotBefore: now.Add(-time.Minute), + NotAfter: now.Add(time.Hour), + IsCA: true, + KeyUsage: x509.KeyUsageCertSign, + BasicConstraintsValid: true, + } + der, err := x509.CreateCertificate(rand.Reader, template, template, &key.PublicKey, key) + if err != nil { + t.Fatal(err) + } + cert, err := x509.ParseCertificate(der) + if err != nil { + t.Fatal(err) + } + return &testCA{ + cert: cert, + key: key, + certPEM: pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der}), + } +} + +func (ca *testCA) issue(t *testing.T, spiffeID string, usages []x509.ExtKeyUsage) tls.Certificate { + t.Helper() + key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + t.Fatal(err) + } + now := time.Now() + template := &x509.Certificate{ + SerialNumber: big.NewInt(now.UnixNano()), + NotBefore: now.Add(-time.Minute), + NotAfter: now.Add(time.Hour), + KeyUsage: x509.KeyUsageDigitalSignature, + ExtKeyUsage: usages, + } + if spiffeID != "" { + uri, err := url.Parse(spiffeID) + if err != nil { + t.Fatal(err) + } + template.URIs = []*url.URL{uri} + } + der, err := x509.CreateCertificate(rand.Reader, template, ca.cert, &key.PublicKey, ca.key) + if err != nil { + t.Fatal(err) + } + keyDER, err := x509.MarshalPKCS8PrivateKey(key) + if err != nil { + t.Fatal(err) + } + cert, err := tls.X509KeyPair( + append(pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der}), ca.certPEM...), + pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: keyDER}), + ) + if err != nil { + t.Fatal(err) + } + return cert +} + +func writeCredentialBundle(t *testing.T, path string, cert tls.Certificate) { + t.Helper() + key, ok := cert.PrivateKey.(*ecdsa.PrivateKey) + if !ok { + t.Fatalf("private key has type %T", cert.PrivateKey) + } + keyDER, err := x509.MarshalPKCS8PrivateKey(key) + if err != nil { + t.Fatal(err) + } + var bundle []byte + for _, der := range cert.Certificate { + bundle = append(bundle, pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der})...) + } + bundle = append(bundle, pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: keyDER})...) + if err := os.WriteFile(path, bundle, 0o600); err != nil { + t.Fatal(err) + } +} + +func receiveWithin[T any](t *testing.T, channel <-chan T, description string) T { + t.Helper() + select { + case value := <-channel: + return value + case <-time.After(5 * time.Second): + t.Fatalf("timed out waiting for %s", description) + var zero T + return zero + } +} diff --git a/internal/atunnel/ingress.go b/internal/atunnel/ingress/ingress.go similarity index 89% rename from internal/atunnel/ingress.go rename to internal/atunnel/ingress/ingress.go index e921725438..070c80a002 100644 --- a/internal/atunnel/ingress.go +++ b/internal/atunnel/ingress/ingress.go @@ -12,8 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. -// Package atunnel carries actor ingress and egress through a worker pod. -package atunnel +// Package ingress carries actor ingress through a worker pod. +package ingress import ( "context" @@ -42,9 +42,9 @@ const ( // mTLS CONNECT tunnels from the ingress router. DefaultConnectPort = 8443 - // StaleAssignmentHeader distinguishes an atunnel routing rejection from a + // staleAssignmentHeader distinguishes an atunnel routing rejection from a // 421 returned by the actor application itself. - StaleAssignmentHeader = "X-Ate-Assignment-Stale" + staleAssignmentHeader = "X-Ate-Assignment-Stale" // TargetPortHeader carries the port to reach on the actor: the CONNECT // :authority's port for arbitrary-port ingress, or the default 80 // otherwise (see atenet-router's HandleRequestHeaders). cfg.Upstream is @@ -63,6 +63,9 @@ func ParsePort(s string) (port int, ok bool) { return p, true } +// DialFunc dials a network address. It matches net.Dialer.DialContext. +type DialFunc func(ctx context.Context, network, address string) (net.Conn, error) + // Config configures an ingress Server. type Config struct { CredentialBundlePath string @@ -101,16 +104,16 @@ type activation struct { // NewServer creates a Server and validates its TLS material. func NewServer(cfg Config) (*Server, error) { if cfg.CredentialBundlePath == "" { - return nil, fmt.Errorf("atunnel: credential bundle path is required") + return nil, fmt.Errorf("ingress: credential bundle path is required") } if cfg.TrustBundlePath == "" { - return nil, fmt.Errorf("atunnel: trust bundle path is required") + return nil, fmt.Errorf("ingress: trust bundle path is required") } if cfg.AllowedClientID == "" { - return nil, fmt.Errorf("atunnel: allowed client identity is required") + return nil, fmt.Errorf("ingress: allowed client identity is required") } if cfg.Upstream == nil || cfg.Upstream.Scheme == "" || cfg.Upstream.Host == "" { - return nil, fmt.Errorf("atunnel: upstream URL is required") + return nil, fmt.Errorf("ingress: upstream URL is required") } // Load once at startup so a malformed or missing projection fails the pod @@ -121,7 +124,7 @@ func NewServer(cfg Config) (*Server, error) { } loadClientCAs := credbundle.PoolLoader(cfg.TrustBundlePath) if _, err := loadClientCAs(); err != nil { - return nil, fmt.Errorf("atunnel: loading trust bundle: %w", err) + return nil, fmt.Errorf("ingress: loading trust bundle: %w", err) } s := &Server{ @@ -134,14 +137,14 @@ func NewServer(cfg Config) (*Server, error) { } verifyConnection := func(cs tls.ConnectionState) error { if len(cs.PeerCertificates) == 0 { - return fmt.Errorf("atunnel: client certificate is required") + return fmt.Errorf("ingress: client certificate is required") } for _, uri := range cs.PeerCertificates[0].URIs { if uri.String() == cfg.AllowedClientID { return nil } } - return fmt.Errorf("atunnel: client is not %q", cfg.AllowedClientID) + return fmt.Errorf("ingress: client is not %q", cfg.AllowedClientID) } s.tlsConfig = &tls.Config{ MinVersion: tls.VersionTLS12, @@ -191,7 +194,7 @@ func newActorProxy(upstream *url.URL, dial DialFunc) *httputil.ReverseProxy { // Prevent untrusted actor responses from spoofing routing rejections // to trigger cache evictions and stream restarts in the ingress router. ModifyResponse: func(resp *http.Response) error { - resp.Header.Del(StaleAssignmentHeader) + resp.Header.Del(staleAssignmentHeader) return nil }, Transport: newProtocolMirrorTransport(dial), @@ -268,11 +271,11 @@ func (t protocolMirrorTransport) CloseIdleConnections() { func loadCredentialBundle(path string) (*tls.Certificate, error) { pemBytes, err := os.ReadFile(path) if err != nil { - return nil, fmt.Errorf("atunnel: reading credential bundle: %w", err) + return nil, fmt.Errorf("ingress: reading credential bundle: %w", err) } cert, err := tls.X509KeyPair(pemBytes, pemBytes) if err != nil { - return nil, fmt.Errorf("atunnel: parsing credential bundle: %w", err) + return nil, fmt.Errorf("ingress: parsing credential bundle: %w", err) } return &cert, nil } @@ -293,7 +296,7 @@ func (s *Server) Serve(ctx context.Context, lis net.Listener) error { // decisions move to the router's route config, leaving atunnel with no // request parsing at all. func (s *Server) ServeConnect(ctx context.Context, lis net.Listener) error { - return s.serve(ctx, lis, http.HandlerFunc(s.ServeConnectHTTP), s.tlsConfig) + return s.serve(ctx, lis, http.HandlerFunc(s.serveConnectHTTP), s.tlsConfig) } func (s *Server) serve(ctx context.Context, lis net.Listener, handler http.Handler, tlsConfig *tls.Config) error { @@ -318,9 +321,9 @@ func (s *Server) serve(ctx context.Context, lis net.Listener, handler http.Handl return err } -// ServeConnectHTTP accepts a router-authenticated CONNECT request and relays +// serveConnectHTTP accepts a router-authenticated CONNECT request and relays // its tunnel to the named port on the currently active actor. -func (s *Server) ServeConnectHTTP(w http.ResponseWriter, r *http.Request) { +func (s *Server) serveConnectHTTP(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodConnect { http.Error(w, "CONNECT required", http.StatusMethodNotAllowed) return @@ -328,7 +331,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) + sendRejectResponse(w) return } defer release() @@ -382,7 +385,7 @@ func (s *Server) serveH1Connect(w http.ResponseWriter, upstream net.Conn, ctx co return } - relayIngressWithHalfClose(ctx, log, upstream, rw, downstream, downstream) + relayWithHalfClose(ctx, log, upstream, rw, downstream, downstream) } func (s *Server) serveH2Connect(w http.ResponseWriter, r *http.Request, upstream net.Conn, ctx context.Context, log *slog.Logger) { @@ -394,14 +397,14 @@ func (s *Server) serveH2Connect(w http.ResponseWriter, r *http.Request, upstream return } - relayIngressWithHalfClose(ctx, log, upstream, r.Body, flushingWriter{ResponseWriter: w}, r.Body) + relayWithHalfClose(ctx, log, upstream, r.Body, flushingWriter{ResponseWriter: w}, r.Body) } -// relayIngressWithHalfClose copies a CONNECT stream between the router +// relayWithHalfClose 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) { +func relayWithHalfClose(ctx context.Context, log *slog.Logger, upstream net.Conn, downstreamReader io.Reader, downstreamWriter io.Writer, downstreamCloser io.Closer) { start := time.Now() go func() { n, err := io.Copy(upstream, downstreamReader) @@ -443,10 +446,10 @@ func (w flushingWriter) Write(p []byte) (int, error) { // same name is replaced. func (s *Server) Activate(atespace, actorName, actorUID string, dial DialFunc) error { if !resources.IsValidResourceName(atespace) || !resources.IsValidResourceName(actorName) { - return fmt.Errorf("atunnel: invalid actor reference %q/%q", atespace, actorName) + return fmt.Errorf("ingress: invalid actor reference %q/%q", atespace, actorName) } if dial == nil { - return fmt.Errorf("atunnel: actor dialer is required") + return fmt.Errorf("ingress: actor dialer is required") } ref := resources.ActorRef{Atespace: atespace, Name: actorName} @@ -454,7 +457,7 @@ func (s *Server) Activate(atespace, actorName, actorUID string, dial DialFunc) e defer s.mu.Unlock() if old, ok := s.active[ref]; ok { if old.uid == actorUID { - return fmt.Errorf("atunnel: actor %s is already active", ref) + return fmt.Errorf("ingress: actor %s is already active", ref) } old.cancel() closeIdleUpstreamConnections(old) @@ -521,7 +524,7 @@ func (s *Server) Deactivate(ctx context.Context, atespace, actorName, actorUID s return nil case <-ctx.Done(): closeIdleUpstreamConnections(active) - return fmt.Errorf("atunnel: waiting for active requests to stop: %w", ctx.Err()) + return fmt.Errorf("ingress: waiting for active requests to stop: %w", ctx.Err()) } } @@ -535,7 +538,7 @@ func closeIdleUpstreamConnections(active *activation) { func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { active, requestCtx, release, ok := s.authorize(r) if !ok { - s.reject(w) + sendRejectResponse(w) return } defer release() @@ -573,9 +576,15 @@ func (s *Server) authorize(r *http.Request) (*activation, context.Context, func( // 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") +var errActorDeactivated = errors.New("ingress: actor deactivated") -func (s *Server) reject(w http.ResponseWriter) { - w.Header().Set(StaleAssignmentHeader, "true") +func sendRejectResponse(w http.ResponseWriter) { + w.Header().Set(staleAssignmentHeader, "true") http.Error(w, "misdirected request", http.StatusMisdirectedRequest) } + +func closeWrite(conn net.Conn) { + if conn, ok := conn.(interface{ CloseWrite() error }); ok { + _ = conn.CloseWrite() + } +} diff --git a/internal/atunnel/ingress_test.go b/internal/atunnel/ingress/ingress_test.go similarity index 98% rename from internal/atunnel/ingress_test.go rename to internal/atunnel/ingress/ingress_test.go index ca25e299e3..9c33faa85b 100644 --- a/internal/atunnel/ingress_test.go +++ b/internal/atunnel/ingress/ingress_test.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package atunnel +package ingress import ( "bytes" @@ -131,7 +131,7 @@ func TestDeactivateIsolatesLateDials(t *testing.T) { } } -func TestRelayIngressWithHalfClose(t *testing.T) { +func TestRelayWithHalfClose(t *testing.T) { listener, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { t.Fatal(err) @@ -158,7 +158,7 @@ func TestRelayIngressWithHalfClose(t *testing.T) { var downstreamOutput bytes.Buffer done := make(chan struct{}) go func() { - relayIngressWithHalfClose(context.Background(), slog.New(slog.DiscardHandler), upstream, downstreamReader, &downstreamOutput, downstreamReader) + relayWithHalfClose(context.Background(), slog.New(slog.DiscardHandler), upstream, downstreamReader, &downstreamOutput, downstreamReader) close(done) }() @@ -207,7 +207,7 @@ func TestRelayIngressClosesDownstreamWhenActorCloses(t *testing.T) { 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) + relayWithHalfClose(context.Background(), log, upstream, downstreamReader, &downstreamOutput, downstreamReader) close(done) }() @@ -243,7 +243,7 @@ func TestRelayIngressCancellationClosesBothSides(t *testing.T) { log := slog.New(slog.NewTextHandler(&logs, &slog.HandlerOptions{Level: slog.LevelDebug})) done := make(chan struct{}) go func() { - relayIngressWithHalfClose(ctx, log, upstream, downstreamReader, io.Discard, downstreamReader) + relayWithHalfClose(ctx, log, upstream, downstreamReader, io.Discard, downstreamReader) close(done) }() @@ -261,7 +261,7 @@ func TestRelayIngressCancellationClosesBothSides(t *testing.T) { 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) { + if want := `msg="atunnel CONNECT canceled" cause="ingress: actor deactivated"`; !strings.Contains(logs.String(), want) { t.Errorf("relay logs = %q, want a line containing %q", logs.String(), want) } } @@ -337,8 +337,8 @@ func TestServeHTTP(t *testing.T) { if rec.Code != tt.wantStatus { t.Fatalf("status = %d, want %d", rec.Code, tt.wantStatus) } - if tt.wantStatus == http.StatusMisdirectedRequest && rec.Header().Get(StaleAssignmentHeader) != "true" { - t.Errorf("missing %s response header", StaleAssignmentHeader) + if tt.wantStatus == http.StatusMisdirectedRequest && rec.Header().Get(staleAssignmentHeader) != "true" { + t.Errorf("missing %s response header", staleAssignmentHeader) } }) } @@ -411,8 +411,8 @@ func TestServerRejectsDuplicateOrCommaTargetActorHeaders(t *testing.T) { if rec.Code != http.StatusMisdirectedRequest { t.Fatalf("status = %d, want %d", rec.Code, http.StatusMisdirectedRequest) } - if rec.Header().Get(StaleAssignmentHeader) != "true" { - t.Errorf("missing %s response header", StaleAssignmentHeader) + if rec.Header().Get(staleAssignmentHeader) != "true" { + t.Errorf("missing %s response header", staleAssignmentHeader) } if upstreamCalled { t.Errorf("upstream was dialed, expected request to be rejected") @@ -430,7 +430,7 @@ func TestServeHTTPStripsStaleAssignmentHeaderFromActor(t *testing.T) { s := newTestServer(t, upstreamURL) actorTransport := roundTripFunc(func(r *http.Request) (*http.Response, error) { h := make(http.Header) - h.Set(StaleAssignmentHeader, "true") + h.Set(staleAssignmentHeader, "true") return &http.Response{ StatusCode: http.StatusOK, Header: h, @@ -450,8 +450,8 @@ func TestServeHTTPStripsStaleAssignmentHeaderFromActor(t *testing.T) { if rec.Code != http.StatusOK { t.Fatalf("status = %d, want %d", rec.Code, http.StatusOK) } - if got := rec.Header().Get(StaleAssignmentHeader); got != "" { - t.Errorf("expected %s to be stripped from actor response, got %q", StaleAssignmentHeader, got) + if got := rec.Header().Get(staleAssignmentHeader); got != "" { + t.Errorf("expected %s to be stripped from actor response, got %q", staleAssignmentHeader, got) } } @@ -546,7 +546,7 @@ func TestServeConnectHTTPValidatesMethodAndAuthority(t *testing.T) { req.Header.Set(atenet.TargetActorHeader, "team-a/actor-2") } rec := httptest.NewRecorder() - s.ServeConnectHTTP(rec, req) + s.serveConnectHTTP(rec, req) if rec.Code != tt.want { t.Fatalf("status = %d, want %d", rec.Code, tt.want) } @@ -1260,7 +1260,7 @@ func TestServeConnectHTTPDialsTheSandbox(t *testing.T) { req.Host = "actor-1.team-a.actors.resources.substrate.ate.dev:9090" req.Header.Set(atenet.TargetActorHeader, "team-a/actor-1") rec := httptest.NewRecorder() - s.ServeConnectHTTP(rec, req) + s.serveConnectHTTP(rec, req) if rec.Code != http.StatusOK { t.Fatalf("status = %d, want %d: the tunnel never reached the sandbox", rec.Code, http.StatusOK) diff --git a/internal/e2e/fixtures/testserver/egressprobe.go b/internal/e2e/fixtures/testserver/egressprobe.go index a91541b505..04aa43c631 100644 --- a/internal/e2e/fixtures/testserver/egressprobe.go +++ b/internal/e2e/fixtures/testserver/egressprobe.go @@ -28,7 +28,7 @@ import ( "github.com/spf13/cobra" - "github.com/agent-substrate/substrate/internal/atunnel" + "github.com/agent-substrate/substrate/internal/atunnel/egress" "github.com/agent-substrate/substrate/internal/credbundle" ) @@ -122,7 +122,7 @@ func handshake(w http.ResponseWriter, r *http.Request, cfg probeConfig) { if err != nil { result.Stage = stage result.Error = err.Error() - var rejected *atunnel.ConnectRejectedError + var rejected *egress.ConnectRejectedError if errors.As(err, &rejected) { result.ConnectStatus = rejected.StatusCode } @@ -145,7 +145,7 @@ func encodeChain(chain []*x509.Certificate) string { // chain it was served. On failure it also returns the stage that failed, since // which hop said no is the assertion most callers are actually making. func fetchChain(ctx context.Context, sni, credentialBundle string, cfg probeConfig) ([]*x509.Certificate, string, error) { - client, err := atunnel.NewClient(atunnel.ClientConfig{ + client, err := egress.NewClient(egress.ClientConfig{ GatewayAddress: cfg.gatewayAddress, ServerName: serverName(cfg.gatewayAddress), GetClientCertificate: credbundle.ClientLoader(credentialBundle), @@ -183,9 +183,9 @@ func fetchChain(ctx context.Context, sni, credentialBundle string, cfg probeConf // typed errors rather than its messages, so the mapping survives a reworded // error. func dialStage(err error) string { - var rejected *atunnel.ConnectRejectedError + var rejected *egress.ConnectRejectedError switch { - case errors.Is(err, atunnel.ErrGatewayHandshake): + case errors.Is(err, egress.ErrGatewayHandshake): return stageGatewayTLS case errors.As(err, &rejected): return stageConnect diff --git a/internal/e2e/router_client.go b/internal/e2e/router_client.go index 9b8c68f10a..2a461f24a7 100644 --- a/internal/e2e/router_client.go +++ b/internal/e2e/router_client.go @@ -133,7 +133,7 @@ func (c *RouterClient) request(ctx context.Context, method string, actorRef reso // travels in the CONNECT authority (e.g. "my-actor.team-a...:9090"), the same // way a real client reaches a port other than an actor's primary one. On a // non-2xx response the returned error carries the status and body, mirroring -// atunnel.Client.DialContext's handling of the same failure mode on the +// egress.Client.DialContext's handling of the same failure mode on the // egress side. The caller owns the returned connection and must Close it; // the underlying port-forward is torn down by RouterClient.Close. func (c *RouterClient) Connect(ctx context.Context, actorRef resources.ActorRef, port int) (net.Conn, error) { @@ -206,7 +206,7 @@ func (c *RouterClient) ensureConnectPortForward(ctx context.Context) error { } // bufferedConn recovers bytes http.ReadResponse buffered past the header -// boundary, mirroring internal/atunnel/client.go's identical need on the +// boundary, mirroring internal/atunnel/egress/client.go's identical need on the // egress CONNECT client. type bufferedConn struct { net.Conn diff --git a/internal/e2e/suites/networking/arbitraryport_test.go b/internal/e2e/suites/networking/arbitraryport_test.go index 4ea12d5127..1e7326c4df 100644 --- a/internal/e2e/suites/networking/arbitraryport_test.go +++ b/internal/e2e/suites/networking/arbitraryport_test.go @@ -40,7 +40,7 @@ const counterExtraPort = 9090 // other than an actor's primary one via HTTP CONNECT, with the target port // carried in the CONNECT authority (e.g. ":9090") rather than a // separate header -- see ingress.Handler.HandleRequestHeaders and -// atunnel.TargetPortHeader. +// TargetPortHeader in internal/atunnel/ingress. // // CONNECT establishment itself (RouterClient.Connect) succeeds as soon as // atenet-router's connect_terminate listener reinjects the tunnel into diff --git a/internal/linuxsys/doc.go b/internal/linuxsys/doc.go new file mode 100644 index 0000000000..1d52cd25e2 --- /dev/null +++ b/internal/linuxsys/doc.go @@ -0,0 +1,17 @@ +// 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 linuxsys adapts Linux-specific syscalls and socket options for the +// rest of Substrate. +package linuxsys diff --git a/internal/atunnel/original_dst_format_linux_test.go b/internal/linuxsys/original_dst_format_linux_test.go similarity index 99% rename from internal/atunnel/original_dst_format_linux_test.go rename to internal/linuxsys/original_dst_format_linux_test.go index 069f481df9..2bebc5bb71 100644 --- a/internal/atunnel/original_dst_format_linux_test.go +++ b/internal/linuxsys/original_dst_format_linux_test.go @@ -14,7 +14,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package atunnel +package linuxsys import ( "encoding/binary" diff --git a/internal/atunnel/original_dst_linux.go b/internal/linuxsys/original_dst_linux.go similarity index 85% rename from internal/atunnel/original_dst_linux.go rename to internal/linuxsys/original_dst_linux.go index 0c2d44bef9..5544aaaef3 100644 --- a/internal/atunnel/original_dst_linux.go +++ b/internal/linuxsys/original_dst_linux.go @@ -14,7 +14,7 @@ //go:build linux -package atunnel +package linuxsys import ( "encoding/binary" @@ -35,18 +35,18 @@ const ip6tSOOriginalDst = 80 func TCPOriginalDestination(conn net.Conn) (string, error) { tcpConn, ok := conn.(*net.TCPConn) if !ok { - return "", fmt.Errorf("atunnel: original destination requires a TCP connection, got %T", conn) + return "", fmt.Errorf("linuxsys: original destination requires a TCP connection, got %T", conn) } rawConn, err := tcpConn.SyscallConn() if err != nil { - return "", fmt.Errorf("atunnel: acquiring TCP syscall connection: %w", err) + return "", fmt.Errorf("linuxsys: acquiring TCP syscall connection: %w", err) } // The IPv6 option is only meaningful on an AF_INET6 socket: on AF_INET the // kernel returns EOPNOTSUPP, which would mask the real IPv4 error. A // v4-mapped local address still means an IPv4 flow, so To4 is the test. local, ok := tcpConn.LocalAddr().(*net.TCPAddr) if !ok { - return "", fmt.Errorf("atunnel: original destination requires a TCP local address, got %T", tcpConn.LocalAddr()) + return "", fmt.Errorf("linuxsys: original destination requires a TCP local address, got %T", tcpConn.LocalAddr()) } isIPv6 := local.IP.To4() == nil @@ -59,14 +59,14 @@ func TCPOriginalDestination(conn net.Conn) (string, error) { } destination, sockoptErr = originalIPv4Destination(fd) }); err != nil { - return "", fmt.Errorf("atunnel: accessing TCP socket: %w", err) + return "", fmt.Errorf("linuxsys: accessing TCP socket: %w", err) } if sockoptErr != nil { family := "IPv4" if isIPv6 { family = "IPv6" } - return "", fmt.Errorf("atunnel: reading original %s TCP destination: %w", family, sockoptErr) + return "", fmt.Errorf("linuxsys: reading original %s TCP destination: %w", family, sockoptErr) } return destination, nil } @@ -108,7 +108,7 @@ func formatOriginalDestination(ip []byte, rawPort uint16) (string, error) { portBytes := (*[2]byte)(unsafe.Pointer(&rawPort)) port := binary.BigEndian.Uint16(portBytes[:]) if port == 0 { - return "", fmt.Errorf("atunnel: original TCP destination has port zero") + return "", fmt.Errorf("linuxsys: original TCP destination has port zero") } return net.JoinHostPort(net.IP(ip).String(), strconv.Itoa(int(port))), nil } diff --git a/internal/atunnel/original_dst_linux_test.go b/internal/linuxsys/original_dst_linux_test.go similarity index 99% rename from internal/atunnel/original_dst_linux_test.go rename to internal/linuxsys/original_dst_linux_test.go index 1e71bc95f4..105095443d 100644 --- a/internal/atunnel/original_dst_linux_test.go +++ b/internal/linuxsys/original_dst_linux_test.go @@ -14,7 +14,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package atunnel +package linuxsys import ( "context" From 46f89c876888ee4cb3dfebdc620ddd54958fb34b Mon Sep 17 00:00:00 2001 From: igooch Date: Thu, 15 Oct 2026 21:30:17 +0000 Subject: [PATCH 11/31] atelet API: add DetachActor, which uploads an actor's latest local snapshot and frees the node (#2725) Part of Actor Lifecycle v2 #2069. **API change (atelet-internal):** adds `Atelet.DetachActor(DetachActorRequest) returns (DetachActorResponse)` to `internal/proto/ateletpb`. No public API changes. The control plane needs to detach an actor from a node on demand. The first caller is resuming an actor on another node when its latest snapshot is only local: the detach uploads that snapshot first, so the actor can restore from object storage anywhere. `DetachActor` makes the actor's latest local snapshot durable, then deletes the actor's local snapshots on the node. atelet also runs this detach on its own once a suspended actor has stayed on the node for its detach delay. The RPC: - does nothing when atelet holds no local snapshot for the actor; - waits for an operation already in progress on the actor, such as an upload or a detach; - returns `ABORTED` while a Run, Restore, or Checkpoint of the actor is in progress or the actor runs on the node; - returns the actor's `ActorSnapshotState`, with `latest_local` unset and `latest_durable` set to its latest snapshot, if atelet knows one. That's the shape of a detach report, so the control plane can apply it with the `ReportActorSnapshotState` rules. The request names the actor by atespace, name, and UID. Golden actors aren't rejected, since an `IMMEDIATE` checkpoint keeps a local copy too. This PR adds the API and its validation only. atelet embeds `UnimplementedAteletServer`, so it returns `UNIMPLEMENTED` until the snapshot tracker's detach lands. The handler then calls the tracker's detach and returns its state. - [ ] Tests pass - [ ] Appropriate changes to documentation are included in the PR --- cmd/atelet/internal/apivalidation/validate.go | 5 + .../internal/apivalidation/validation_test.go | 52 +++++ .../apivalidation/zz_generated.validation.go | 105 +++++++++ internal/proto/ateletpb/atelet.pb.go | 211 ++++++++++++++---- internal/proto/ateletpb/atelet.proto | 35 ++- internal/proto/ateletpb/atelet_grpc.pb.go | 56 +++++ 6 files changed, 421 insertions(+), 43 deletions(-) diff --git a/cmd/atelet/internal/apivalidation/validate.go b/cmd/atelet/internal/apivalidation/validate.go index 63f9f143b2..0b99daa8de 100644 --- a/cmd/atelet/internal/apivalidation/validate.go +++ b/cmd/atelet/internal/apivalidation/validate.go @@ -65,6 +65,11 @@ func ValidatePromoteSnapshotRequest(ctx context.Context, req *ateletpb.PromoteSn return Validate_PromoteSnapshotRequest(ctx, operation.Operation{Type: operation.Create}, nil, req, nil) } +// ValidateDetachActorRequest runs the generated validation for req. +func ValidateDetachActorRequest(ctx context.Context, req *ateletpb.DetachActorRequest) field.ErrorList { + return Validate_DetachActorRequest(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) diff --git a/cmd/atelet/internal/apivalidation/validation_test.go b/cmd/atelet/internal/apivalidation/validation_test.go index d84212e8cb..f6478abb63 100644 --- a/cmd/atelet/internal/apivalidation/validation_test.go +++ b/cmd/atelet/internal/apivalidation/validation_test.go @@ -1068,6 +1068,58 @@ func TestValidateTerminateRequest(t *testing.T) { } } +func TestValidateDetachActorRequest(t *testing.T) { + valid := func(mutate ...func(*ateletpb.DetachActorRequest)) *ateletpb.DetachActorRequest { + r := &ateletpb.DetachActorRequest{ + Atespace: "team-a", + ActorName: "actor-1", + ActorUid: "01234567-89ab-cdef-0123-456789abcdef", + } + for _, m := range mutate { + m(r) + } + return r + } + + tests := []struct { + name string + obj *ateletpb.DetachActorRequest + want field.ErrorList + }{{ + name: "valid", + obj: valid(), + }, { + name: "missing atespace", + obj: valid(func(r *ateletpb.DetachActorRequest) { r.Atespace = "" }), + want: field.ErrorList{field.Required(field.NewPath("atespace"), "")}, + }, { + name: "invalid atespace: uppercase", + obj: valid(func(r *ateletpb.DetachActorRequest) { 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.DetachActorRequest) { r.ActorName = "" }), + want: field.ErrorList{field.Required(field.NewPath("actor_name"), "")}, + }, { + name: "invalid actor_name: path escape", + obj: valid(func(r *ateletpb.DetachActorRequest) { 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.DetachActorRequest) { r.ActorUid = "" }), + want: field.ErrorList{field.Required(field.NewPath("actor_uid"), "")}, + }, { + name: "invalid actor_uid: not a uuid", + obj: valid(func(r *ateletpb.DetachActorRequest) { r.ActorUid = "not-a-uuid" }), + want: field.ErrorList{field.Invalid(field.NewPath("actor_uid"), nil, "").WithOrigin("format=k8s-uuid")}, + }} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assertValidateErr(t, ValidateDetachActorRequest(context.Background(), tt.obj), tt.want) + }) + } +} + func TestValidateRegisterWorkerRequest(t *testing.T) { testRuntime := func() *ateletpb.SandboxRuntime { return &ateletpb.SandboxRuntime{ diff --git a/cmd/atelet/internal/apivalidation/zz_generated.validation.go b/cmd/atelet/internal/apivalidation/zz_generated.validation.go index 8cbdbae51c..620287d0a7 100644 --- a/cmd/atelet/internal/apivalidation/zz_generated.validation.go +++ b/cmd/atelet/internal/apivalidation/zz_generated.validation.go @@ -1102,6 +1102,111 @@ func Validate_Container( return errs } +// Validate_DetachActorRequest validates an instance of DetachActorRequest according +// to declarative validation rules in the API schema. +func Validate_DetachActorRequest( + ctx context.Context, op operation.Operation, fldPath *field.Path, + obj, oldObj *ateletpb.DetachActorRequest) (errs field.ErrorList) { + + { // field ateletpb.DetachActorRequest.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.DetachActorRequest) *string { + return &oldObj.Atespace + }) + errs = append(errs, fn(fldPath.Child("atespace"), &obj.Atespace, oldVal, oldObj != nil)...) + } + + { // field ateletpb.DetachActorRequest.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.DetachActorRequest) *string { + return &oldObj.ActorName + }) + errs = append(errs, fn(fldPath.Child("actor_name"), &obj.ActorName, oldVal, oldObj != nil)...) + } + + { // field ateletpb.DetachActorRequest.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.DetachActorRequest) *string { + return &oldObj.ActorUid + }) + errs = append(errs, fn(fldPath.Child("actor_uid"), &obj.ActorUid, oldVal, oldObj != nil)...) + } + + return errs +} + // Validate_EgressGateway validates an instance of EgressGateway according // to declarative validation rules in the API schema. func Validate_EgressGateway( diff --git a/internal/proto/ateletpb/atelet.pb.go b/internal/proto/ateletpb/atelet.pb.go index b05826b2a6..885c0f06d9 100644 --- a/internal/proto/ateletpb/atelet.pb.go +++ b/internal/proto/ateletpb/atelet.pb.go @@ -3291,7 +3291,7 @@ func (x *SnapshotReport) GetDurableCompleteTime() *timestamppb.Timestamp { } // ActorSnapshotState is what atelet holds for one actor on this node. -// Returned by Checkpoint, PromoteSnapshot. +// Returned by Checkpoint, PromoteSnapshot, DetachActor. type ActorSnapshotState struct { state protoimpl.MessageState `protogen:"open.v1"` // The snapshot atelet restores the actor from locally. Unset when the @@ -3980,6 +3980,120 @@ func (x *PromoteSnapshotResponse) GetState() *ActorSnapshotState { return nil } +type DetachActorRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Golden actors are accepted: an IMMEDIATE checkpoint keeps a local copy. + // + // +k8s:required + // +k8s:format=k8s-short-name + 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"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DetachActorRequest) Reset() { + *x = DetachActorRequest{} + mi := &file_atelet_proto_msgTypes[56] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DetachActorRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DetachActorRequest) ProtoMessage() {} + +func (x *DetachActorRequest) ProtoReflect() protoreflect.Message { + mi := &file_atelet_proto_msgTypes[56] + 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 DetachActorRequest.ProtoReflect.Descriptor instead. +func (*DetachActorRequest) Descriptor() ([]byte, []int) { + return file_atelet_proto_rawDescGZIP(), []int{56} +} + +func (x *DetachActorRequest) GetAtespace() string { + if x != nil { + return x.Atespace + } + return "" +} + +func (x *DetachActorRequest) GetActorName() string { + if x != nil { + return x.ActorName + } + return "" +} + +func (x *DetachActorRequest) GetActorUid() string { + if x != nil { + return x.ActorUid + } + return "" +} + +type DetachActorResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The actor's state once detached: latest_local is unset, and latest_durable + // is its latest snapshot, if atelet knows one. + State *ActorSnapshotState `protobuf:"bytes,1,opt,name=state,proto3" json:"state,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DetachActorResponse) Reset() { + *x = DetachActorResponse{} + mi := &file_atelet_proto_msgTypes[57] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DetachActorResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DetachActorResponse) ProtoMessage() {} + +func (x *DetachActorResponse) ProtoReflect() protoreflect.Message { + mi := &file_atelet_proto_msgTypes[57] + 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 DetachActorResponse.ProtoReflect.Descriptor instead. +func (*DetachActorResponse) Descriptor() ([]byte, []int) { + return file_atelet_proto_rawDescGZIP(), []int{57} +} + +func (x *DetachActorResponse) GetState() *ActorSnapshotState { + if x != nil { + return x.State + } + return nil +} + var File_atelet_proto protoreflect.FileDescriptor const file_atelet_proto_rawDesc = "" + @@ -4221,6 +4335,13 @@ const file_atelet_proto_rawDesc = "" + "\x13actor_template_name\x18\x05 \x01(\tR\x11actorTemplateName\x12,\n" + "\bsnapshot\x18\x06 \x01(\v2\x10.atelet.SnapshotR\bsnapshot\"K\n" + "\x17PromoteSnapshotResponse\x120\n" + + "\x05state\x18\x01 \x01(\v2\x1a.atelet.ActorSnapshotStateR\x05state\"l\n" + + "\x12DetachActorRequest\x12\x1a\n" + + "\batespace\x18\x01 \x01(\tR\batespace\x12\x1d\n" + + "\n" + + "actor_name\x18\x02 \x01(\tR\tactorName\x12\x1b\n" + + "\tactor_uid\x18\x03 \x01(\tR\bactorUid\"G\n" + + "\x13DetachActorResponse\x120\n" + "\x05state\x18\x01 \x01(\v2\x1a.atelet.ActorSnapshotStateR\x05state*\x9a\x01\n" + "\x12ActorMetadataField\x12$\n" + " ACTOR_METADATA_FIELD_UNSPECIFIED\x10\x00\x12\x1d\n" + @@ -4241,13 +4362,14 @@ const file_atelet_proto_rawDesc = "" + "\x14MintActorCertificate\x12#.atelet.MintActorCertificateRequest\x1a$.atelet.MintActorCertificateResponse\"\x00\x12Q\n" + "\x0eRegisterWorker\x12\x1d.atelet.RegisterWorkerRequest\x1a\x1e.atelet.RegisterWorkerResponse\"\x00\x12`\n" + "\x13RequestActorSuspend\x12\".atelet.RequestActorSuspendRequest\x1a#.atelet.RequestActorSuspendResponse\"\x00\x12c\n" + - "\x14GetActorEgressPolicy\x12#.atelet.GetActorEgressPolicyRequest\x1a$.atelet.GetActorEgressPolicyResponse\"\x002\xd9\x02\n" + + "\x14GetActorEgressPolicy\x12#.atelet.GetActorEgressPolicyRequest\x1a$.atelet.GetActorEgressPolicyResponse\"\x002\xa3\x03\n" + "\x06Atelet\x120\n" + "\x03Run\x12\x12.atelet.RunRequest\x1a\x13.atelet.RunResponse\"\x00\x12E\n" + "\n" + "Checkpoint\x12\x19.atelet.CheckpointRequest\x1a\x1a.atelet.CheckpointResponse\"\x00\x12<\n" + "\aRestore\x12\x16.atelet.RestoreRequest\x1a\x17.atelet.RestoreResponse\"\x00\x12T\n" + - "\x0fPromoteSnapshot\x12\x1e.atelet.PromoteSnapshotRequest\x1a\x1f.atelet.PromoteSnapshotResponse\"\x00\x12B\n" + + "\x0fPromoteSnapshot\x12\x1e.atelet.PromoteSnapshotRequest\x1a\x1f.atelet.PromoteSnapshotResponse\"\x00\x12H\n" + + "\vDetachActor\x12\x1a.atelet.DetachActorRequest\x1a\x1b.atelet.DetachActorResponse\"\x00\x12B\n" + "\tTerminate\x12\x18.atelet.TerminateRequest\x1a\x19.atelet.TerminateResponse\"\x00B>Z atelet.WorkerResources @@ -4349,12 +4473,12 @@ var file_atelet_proto_depIdxs = []int32{ 29, // 15: atelet.RunRequest.spec:type_name -> atelet.WorkloadSpec 28, // 16: atelet.RunRequest.sandbox_assets:type_name -> atelet.SandboxAssets 25, // 17: atelet.RunRequest.egress_gateway:type_name -> atelet.EgressGateway - 59, // 18: atelet.ArchAssets.files:type_name -> atelet.ArchAssets.FilesEntry - 60, // 19: atelet.SandboxAssets.assets:type_name -> atelet.SandboxAssets.AssetsEntry + 61, // 18: atelet.ArchAssets.files:type_name -> atelet.ArchAssets.FilesEntry + 62, // 19: atelet.SandboxAssets.assets:type_name -> atelet.SandboxAssets.AssetsEntry 40, // 20: atelet.WorkloadSpec.containers:type_name -> atelet.Container 38, // 21: atelet.WorkloadSpec.volumes:type_name -> atelet.Volume - 61, // 22: atelet.ExternalVolumeSource.volume_context:type_name -> atelet.ExternalVolumeSource.VolumeContextEntry - 62, // 23: atelet.ExternalVolumeSource.publish_context:type_name -> atelet.ExternalVolumeSource.PublishContextEntry + 63, // 22: atelet.ExternalVolumeSource.volume_context:type_name -> atelet.ExternalVolumeSource.VolumeContextEntry + 64, // 23: atelet.ExternalVolumeSource.publish_context:type_name -> atelet.ExternalVolumeSource.PublishContextEntry 0, // 24: atelet.ActorMetadataItem.field:type_name -> atelet.ActorMetadataField 33, // 25: atelet.ActorMetadataDataSource.items:type_name -> atelet.ActorMetadataItem 34, // 26: atelet.SystemInfoDataSource.actor_metadata:type_name -> atelet.ActorMetadataDataSource @@ -4374,12 +4498,12 @@ var file_atelet_proto_depIdxs = []int32{ 1, // 40: atelet.Snapshot.fidelity:type_name -> atelet.SnapshotFidelity 48, // 41: atelet.Snapshot.object:type_name -> atelet.ObjectStorage 49, // 42: atelet.SnapshotReport.snapshot:type_name -> atelet.Snapshot - 63, // 43: atelet.SnapshotReport.local_commit_time:type_name -> google.protobuf.Timestamp - 63, // 44: atelet.SnapshotReport.durable_complete_time:type_name -> google.protobuf.Timestamp + 65, // 43: atelet.SnapshotReport.local_commit_time:type_name -> google.protobuf.Timestamp + 65, // 44: atelet.SnapshotReport.durable_complete_time:type_name -> google.protobuf.Timestamp 50, // 45: atelet.ActorSnapshotState.latest_local:type_name -> atelet.SnapshotReport 50, // 46: atelet.ActorSnapshotState.uploading:type_name -> atelet.SnapshotReport 50, // 47: atelet.ActorSnapshotState.latest_durable:type_name -> atelet.SnapshotReport - 63, // 48: atelet.ActorSnapshotState.upload_due_time:type_name -> google.protobuf.Timestamp + 65, // 48: atelet.ActorSnapshotState.upload_due_time:type_name -> google.protobuf.Timestamp 2, // 49: atelet.UploadConfig.mode:type_name -> atelet.UploadMode 29, // 50: atelet.CheckpointRequest.spec:type_name -> atelet.WorkloadSpec 49, // 51: atelet.CheckpointRequest.snapshot:type_name -> atelet.Snapshot @@ -4391,31 +4515,34 @@ var file_atelet_proto_depIdxs = []int32{ 28, // 57: atelet.RestoreRequest.sandbox_assets:type_name -> atelet.SandboxAssets 49, // 58: atelet.PromoteSnapshotRequest.snapshot:type_name -> atelet.Snapshot 51, // 59: atelet.PromoteSnapshotResponse.state:type_name -> atelet.ActorSnapshotState - 26, // 60: atelet.ArchAssets.FilesEntry.value:type_name -> atelet.AssetFile - 27, // 61: atelet.SandboxAssets.AssetsEntry.value:type_name -> atelet.ArchAssets - 20, // 62: atelet.WorkerSupport.MintActorCertificate:input_type -> atelet.MintActorCertificateRequest - 3, // 63: atelet.WorkerSupport.RegisterWorker:input_type -> atelet.RegisterWorkerRequest - 11, // 64: atelet.WorkerSupport.RequestActorSuspend:input_type -> atelet.RequestActorSuspendRequest - 13, // 65: atelet.WorkerSupport.GetActorEgressPolicy:input_type -> atelet.GetActorEgressPolicyRequest - 24, // 66: atelet.Atelet.Run:input_type -> atelet.RunRequest - 53, // 67: atelet.Atelet.Checkpoint:input_type -> atelet.CheckpointRequest - 55, // 68: atelet.Atelet.Restore:input_type -> atelet.RestoreRequest - 57, // 69: atelet.Atelet.PromoteSnapshot:input_type -> atelet.PromoteSnapshotRequest - 22, // 70: atelet.Atelet.Terminate:input_type -> atelet.TerminateRequest - 21, // 71: atelet.WorkerSupport.MintActorCertificate:output_type -> atelet.MintActorCertificateResponse - 10, // 72: atelet.WorkerSupport.RegisterWorker:output_type -> atelet.RegisterWorkerResponse - 12, // 73: atelet.WorkerSupport.RequestActorSuspend:output_type -> atelet.RequestActorSuspendResponse - 14, // 74: atelet.WorkerSupport.GetActorEgressPolicy:output_type -> atelet.GetActorEgressPolicyResponse - 47, // 75: atelet.Atelet.Run:output_type -> atelet.RunResponse - 54, // 76: atelet.Atelet.Checkpoint:output_type -> atelet.CheckpointResponse - 56, // 77: atelet.Atelet.Restore:output_type -> atelet.RestoreResponse - 58, // 78: atelet.Atelet.PromoteSnapshot:output_type -> atelet.PromoteSnapshotResponse - 23, // 79: atelet.Atelet.Terminate:output_type -> atelet.TerminateResponse - 71, // [71:80] is the sub-list for method output_type - 62, // [62:71] is the sub-list for method input_type - 62, // [62:62] is the sub-list for extension type_name - 62, // [62:62] is the sub-list for extension extendee - 0, // [0:62] is the sub-list for field type_name + 51, // 60: atelet.DetachActorResponse.state:type_name -> atelet.ActorSnapshotState + 26, // 61: atelet.ArchAssets.FilesEntry.value:type_name -> atelet.AssetFile + 27, // 62: atelet.SandboxAssets.AssetsEntry.value:type_name -> atelet.ArchAssets + 20, // 63: atelet.WorkerSupport.MintActorCertificate:input_type -> atelet.MintActorCertificateRequest + 3, // 64: atelet.WorkerSupport.RegisterWorker:input_type -> atelet.RegisterWorkerRequest + 11, // 65: atelet.WorkerSupport.RequestActorSuspend:input_type -> atelet.RequestActorSuspendRequest + 13, // 66: atelet.WorkerSupport.GetActorEgressPolicy:input_type -> atelet.GetActorEgressPolicyRequest + 24, // 67: atelet.Atelet.Run:input_type -> atelet.RunRequest + 53, // 68: atelet.Atelet.Checkpoint:input_type -> atelet.CheckpointRequest + 55, // 69: atelet.Atelet.Restore:input_type -> atelet.RestoreRequest + 57, // 70: atelet.Atelet.PromoteSnapshot:input_type -> atelet.PromoteSnapshotRequest + 59, // 71: atelet.Atelet.DetachActor:input_type -> atelet.DetachActorRequest + 22, // 72: atelet.Atelet.Terminate:input_type -> atelet.TerminateRequest + 21, // 73: atelet.WorkerSupport.MintActorCertificate:output_type -> atelet.MintActorCertificateResponse + 10, // 74: atelet.WorkerSupport.RegisterWorker:output_type -> atelet.RegisterWorkerResponse + 12, // 75: atelet.WorkerSupport.RequestActorSuspend:output_type -> atelet.RequestActorSuspendResponse + 14, // 76: atelet.WorkerSupport.GetActorEgressPolicy:output_type -> atelet.GetActorEgressPolicyResponse + 47, // 77: atelet.Atelet.Run:output_type -> atelet.RunResponse + 54, // 78: atelet.Atelet.Checkpoint:output_type -> atelet.CheckpointResponse + 56, // 79: atelet.Atelet.Restore:output_type -> atelet.RestoreResponse + 58, // 80: atelet.Atelet.PromoteSnapshot:output_type -> atelet.PromoteSnapshotResponse + 60, // 81: atelet.Atelet.DetachActor:output_type -> atelet.DetachActorResponse + 23, // 82: atelet.Atelet.Terminate:output_type -> atelet.TerminateResponse + 73, // [73:83] is the sub-list for method output_type + 63, // [63:73] is the sub-list for method input_type + 63, // [63:63] is the sub-list for extension type_name + 63, // [63:63] is the sub-list for extension extendee + 0, // [0:63] is the sub-list for field type_name } func init() { file_atelet_proto_init() } @@ -4431,7 +4558,7 @@ func file_atelet_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_atelet_proto_rawDesc), len(file_atelet_proto_rawDesc)), NumEnums: 3, - NumMessages: 60, + NumMessages: 62, NumExtensions: 0, NumServices: 2, }, diff --git a/internal/proto/ateletpb/atelet.proto b/internal/proto/ateletpb/atelet.proto index 7608234b8e..515659d5a7 100644 --- a/internal/proto/ateletpb/atelet.proto +++ b/internal/proto/ateletpb/atelet.proto @@ -342,6 +342,17 @@ service Atelet { // caller, the suspend of a paused actor, relies on the deletion. rpc PromoteSnapshot(PromoteSnapshotRequest) returns (PromoteSnapshotResponse) {} + // DetachActor makes the actor's latest local snapshot durable, then deletes + // the actor's local snapshots on this node, so it can resume on any node. + // atelet also detaches a suspended actor on its own after its detach delay. + // Waits for an operation already in progress on the actor, such as an + // upload or a detach. Does nothing when atelet holds no local snapshot for + // the actor. Returns ABORTED while a Run, Restore, or Checkpoint of the + // actor is in progress or the actor runs on this node. + // + // TODO: Temporarily, atelet returns UNIMPLEMENTED. + rpc DetachActor(DetachActorRequest) returns (DetachActorResponse) {} + // Terminate tells atelet to terminate/kill any running workload for an actor, // unmount its volumes, and clean up actor state on the node. rpc Terminate(TerminateRequest) returns (TerminateResponse) {} @@ -926,7 +937,7 @@ message SnapshotReport { } // ActorSnapshotState is what atelet holds for one actor on this node. -// Returned by Checkpoint, PromoteSnapshot. +// Returned by Checkpoint, PromoteSnapshot, DetachActor. message ActorSnapshotState { // The snapshot atelet restores the actor from locally. Unset when the // latest checkpoint kept no local copy, and once a detach has made the @@ -1162,3 +1173,25 @@ message PromoteSnapshotResponse { // and latest_local remains set if atelet still holds the local copy. ActorSnapshotState state = 1; } + +message DetachActorRequest { + // Golden actors are accepted: an IMMEDIATE checkpoint keeps a local copy. + // + // +k8s:required + // +k8s:format=k8s-short-name + string atespace = 1; + + // +k8s:required + // +k8s:format=k8s-short-name + string actor_name = 2; + + // +k8s:required + // +k8s:format=k8s-uuid + string actor_uid = 3; +} + +message DetachActorResponse { + // The actor's state once detached: latest_local is unset, and latest_durable + // is its latest snapshot, if atelet knows one. + ActorSnapshotState state = 1; +} diff --git a/internal/proto/ateletpb/atelet_grpc.pb.go b/internal/proto/ateletpb/atelet_grpc.pb.go index 6a9edd52c7..2282a4651b 100644 --- a/internal/proto/ateletpb/atelet_grpc.pb.go +++ b/internal/proto/ateletpb/atelet_grpc.pb.go @@ -301,6 +301,7 @@ const ( Atelet_Checkpoint_FullMethodName = "/atelet.Atelet/Checkpoint" Atelet_Restore_FullMethodName = "/atelet.Atelet/Restore" Atelet_PromoteSnapshot_FullMethodName = "/atelet.Atelet/PromoteSnapshot" + Atelet_DetachActor_FullMethodName = "/atelet.Atelet/DetachActor" Atelet_Terminate_FullMethodName = "/atelet.Atelet/Terminate" ) @@ -339,6 +340,16 @@ type AteletClient interface { // the upload and does not check whether the actor is running. Its only // caller, the suspend of a paused actor, relies on the deletion. PromoteSnapshot(ctx context.Context, in *PromoteSnapshotRequest, opts ...grpc.CallOption) (*PromoteSnapshotResponse, error) + // DetachActor makes the actor's latest local snapshot durable, then deletes + // the actor's local snapshots on this node, so it can resume on any node. + // atelet also detaches a suspended actor on its own after its detach delay. + // Waits for an operation already in progress on the actor, such as an + // upload or a detach. Does nothing when atelet holds no local snapshot for + // the actor. Returns ABORTED while a Run, Restore, or Checkpoint of the + // actor is in progress or the actor runs on this node. + // + // TODO: Temporarily, atelet returns UNIMPLEMENTED. + DetachActor(ctx context.Context, in *DetachActorRequest, opts ...grpc.CallOption) (*DetachActorResponse, error) // Terminate tells atelet to terminate/kill any running workload for an actor, // unmount its volumes, and clean up actor state on the node. Terminate(ctx context.Context, in *TerminateRequest, opts ...grpc.CallOption) (*TerminateResponse, error) @@ -392,6 +403,16 @@ func (c *ateletClient) PromoteSnapshot(ctx context.Context, in *PromoteSnapshotR return out, nil } +func (c *ateletClient) DetachActor(ctx context.Context, in *DetachActorRequest, opts ...grpc.CallOption) (*DetachActorResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(DetachActorResponse) + err := c.cc.Invoke(ctx, Atelet_DetachActor_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + func (c *ateletClient) Terminate(ctx context.Context, in *TerminateRequest, opts ...grpc.CallOption) (*TerminateResponse, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(TerminateResponse) @@ -437,6 +458,16 @@ type AteletServer interface { // the upload and does not check whether the actor is running. Its only // caller, the suspend of a paused actor, relies on the deletion. PromoteSnapshot(context.Context, *PromoteSnapshotRequest) (*PromoteSnapshotResponse, error) + // DetachActor makes the actor's latest local snapshot durable, then deletes + // the actor's local snapshots on this node, so it can resume on any node. + // atelet also detaches a suspended actor on its own after its detach delay. + // Waits for an operation already in progress on the actor, such as an + // upload or a detach. Does nothing when atelet holds no local snapshot for + // the actor. Returns ABORTED while a Run, Restore, or Checkpoint of the + // actor is in progress or the actor runs on this node. + // + // TODO: Temporarily, atelet returns UNIMPLEMENTED. + DetachActor(context.Context, *DetachActorRequest) (*DetachActorResponse, error) // Terminate tells atelet to terminate/kill any running workload for an actor, // unmount its volumes, and clean up actor state on the node. Terminate(context.Context, *TerminateRequest) (*TerminateResponse, error) @@ -462,6 +493,9 @@ func (UnimplementedAteletServer) Restore(context.Context, *RestoreRequest) (*Res func (UnimplementedAteletServer) PromoteSnapshot(context.Context, *PromoteSnapshotRequest) (*PromoteSnapshotResponse, error) { return nil, status.Error(codes.Unimplemented, "method PromoteSnapshot not implemented") } +func (UnimplementedAteletServer) DetachActor(context.Context, *DetachActorRequest) (*DetachActorResponse, error) { + return nil, status.Error(codes.Unimplemented, "method DetachActor not implemented") +} func (UnimplementedAteletServer) Terminate(context.Context, *TerminateRequest) (*TerminateResponse, error) { return nil, status.Error(codes.Unimplemented, "method Terminate not implemented") } @@ -558,6 +592,24 @@ func _Atelet_PromoteSnapshot_Handler(srv interface{}, ctx context.Context, dec f return interceptor(ctx, in, info, handler) } +func _Atelet_DetachActor_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(DetachActorRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(AteletServer).DetachActor(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: Atelet_DetachActor_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(AteletServer).DetachActor(ctx, req.(*DetachActorRequest)) + } + return interceptor(ctx, in, info, handler) +} + func _Atelet_Terminate_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(TerminateRequest) if err := dec(in); err != nil { @@ -599,6 +651,10 @@ var Atelet_ServiceDesc = grpc.ServiceDesc{ MethodName: "PromoteSnapshot", Handler: _Atelet_PromoteSnapshot_Handler, }, + { + MethodName: "DetachActor", + Handler: _Atelet_DetachActor_Handler, + }, { MethodName: "Terminate", Handler: _Atelet_Terminate_Handler, From eb7b034071b798e3c1d1b816d90426efa7f3cbfc Mon Sep 17 00:00:00 2001 From: ILLUM1N0X Date: Thu, 15 Oct 2026 21:32:01 +0000 Subject: [PATCH 12/31] objectstore-plugin: derive actor storage grants and mint downscoped GCS tokens (#2672) Part of #777 Two packages the control plugin's credential exchange will use. Nothing calls them yet, the endpoint comes in a follow-up PR. `actorscope.FromActor` reads `status.snapshots` and returns the prefixes an actor may touch. Every durable snapshot URI names the storage location it sits under, and for each location the actor gets read-write on its own prefix there, built with the same helper ate-api-server places snapshots with, so the grant exists from the actor's first generation on. Each completed TAG-owned generation adds read-only on the tag's prefix, taken as is. This parses URIs until `Actor.status` carries a base snapshot URI, as agreed with Julian. An owner that is neither ACTOR nor TAG is an error, so a new kind is never silently dropped. The status is otherwise taken as ate-api-server wrote it. `gcsboundary.FromGrants` turns grants into Credential Access Boundary rules, one per prefix, `objectAdmin` for read-write and `objectViewer` for read-only. `gcsissuer` exchanges the pod's credential at STS through `golang.org/x/oauth2/google/downscope` and caches tokens by actor UID in a bounded LRU. Each entry keeps the grants it was minted for and is served only when they cover the request, so the grants narrowing after the first suspend is a hit and a repoint to a new location mints. That means the exchange derives the grants, `GetActor` included, before it consults the cache. A cached token is served only while it has twenty minutes left, a minted token shorter than that is returned but not cached, and a forced request skips the cache for the node plugin's retry after a 403. The downscoped token expires with the source credential. On GKE the metadata server serves a cached source token until shortly before it expires, so for a stretch at the end of each hour every exchange reaches STS and returns a token shorter than the cache floor, which is handed out but not cached. Minting the source directly is a follow-up. Verified on a GKE cluster as the ate-api-server service account: a token with these rules reads and writes inside its prefixes and gets 403 outside them, and its expiry equals the source token's. `golang.org/x/oauth2` moves from an indirect to a direct dependency and `google/downscope` is vendored. --- go.mod | 2 +- .../actorscope/actorscope.go | 122 ++++++ .../actorscope/actorscope_test.go | 158 ++++++++ .../objectstoreplugin/gcsboundary/boundary.go | 85 ++++ .../gcsboundary/boundary_test.go | 93 +++++ .../objectstoreplugin/gcsissuer/issuer.go | 225 +++++++++++ .../gcsissuer/issuer_test.go | 363 ++++++++++++++++++ .../x/oauth2/google/downscope/downscoping.go | 233 +++++++++++ vendor/modules.txt | 1 + 9 files changed, 1281 insertions(+), 1 deletion(-) create mode 100644 internal/objectstoreplugin/actorscope/actorscope.go create mode 100644 internal/objectstoreplugin/actorscope/actorscope_test.go create mode 100644 internal/objectstoreplugin/gcsboundary/boundary.go create mode 100644 internal/objectstoreplugin/gcsboundary/boundary_test.go create mode 100644 internal/objectstoreplugin/gcsissuer/issuer.go create mode 100644 internal/objectstoreplugin/gcsissuer/issuer_test.go create mode 100644 vendor/golang.org/x/oauth2/google/downscope/downscoping.go diff --git a/go.mod b/go.mod index 0594da0cf5..ec85d0fee5 100644 --- a/go.mod +++ b/go.mod @@ -62,6 +62,7 @@ require ( go.opentelemetry.io/otel/trace v1.45.0 go.opentelemetry.io/proto/otlp v1.11.0 golang.org/x/net v0.60.0 + golang.org/x/oauth2 v0.36.0 golang.org/x/sync v0.23.0 golang.org/x/sys v0.48.0 golang.org/x/time v0.15.0 @@ -243,7 +244,6 @@ require ( go.yaml.in/yaml/v3 v3.0.5 // indirect golang.org/x/crypto v0.57.0 // indirect golang.org/x/exp v0.0.0-20260718201538-764159d718ef // indirect - golang.org/x/oauth2 v0.36.0 // indirect golang.org/x/term v0.46.0 // indirect golang.org/x/text v0.42.0 // indirect gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect diff --git a/internal/objectstoreplugin/actorscope/actorscope.go b/internal/objectstoreplugin/actorscope/actorscope.go new file mode 100644 index 0000000000..7fc57d2ee9 --- /dev/null +++ b/internal/objectstoreplugin/actorscope/actorscope.go @@ -0,0 +1,122 @@ +// 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 actorscope derives the object-storage prefixes an actor's transfers +// may touch from the actor record, with the access each needs. It is the +// cloud-neutral half of the credential exchange. A provider turns the grants +// into a scoped credential, for GCS a Credential Access Boundary. +package actorscope + +import ( + "fmt" + "sort" + + "github.com/agent-substrate/substrate/internal/resources" + "github.com/agent-substrate/substrate/pkg/proto/ateapipb" +) + +// Access is what a grant permits under its prefix. +type Access int + +const ( + // ReadOnly covers the tag or golden snapshot the actor was created from. + ReadOnly Access = iota + 1 + // ReadWrite covers the actor's own snapshots. + ReadWrite +) + +func (a Access) String() string { + switch a { + case ReadOnly: + return "ro" + case ReadWrite: + return "rw" + } + return fmt.Sprintf("Access(%d)", int(a)) +} + +// Grant is one prefix and the access allowed under it. Prefix is the actor's +// own prefix or a tag's prefix, as the control plane built it, so a provider +// derives its bucket and path with the same code the rest of Substrate uses. +type Grant struct { + Prefix resources.StoragePrefix + Access Access +} + +// FromActor derives the grants from status.snapshots. Every snapshot the +// control plane writes for the actor carries a URI the control plane built, so +// the storage location and the tag prefixes come from those URIs, parsed with +// the same code that built them, and the actor's own prefix is built with the +// helper the control plane places snapshots with. +// +// - Every durable URI names the storage location it sits under. For each +// location seen, the actor gets ReadWrite on its own prefix there, +// /atespaces//actors/. So the grant exists from +// the first generation on, before the actor has uploaded anything, and +// a repointed actor keeps both locations. +// - Each TAG-owned generation with a COMPLETED durable URI grants ReadOnly +// on the tag's prefix. That is the golden or tag the actor was created +// from, listed until the actor's own first snapshot completes. +// +// The status is taken as ate-api-server wrote it. An owner this code does not +// know is an error, so a new kind is never silently dropped. Local-only +// generations have no URI and contribute nothing. +func FromActor(actor *ateapipb.Actor) ([]Grant, error) { + meta := actor.GetMetadata() + self := resources.ActorSnapshotOwner(meta.GetAtespace(), meta.GetUid()) + seen := map[Grant]bool{} + var grants []Grant + add := func(prefix resources.StoragePrefix, access Access) { + g := Grant{Prefix: prefix, Access: access} + if !seen[g] { + seen[g] = true + grants = append(grants, g) + } + } + for _, gen := range actor.GetStatus().GetSnapshots() { + durable := gen.GetDurableSnapshot() + uri := durable.GetObject().GetSnapshotUri() + if uri == "" { + continue + } + parsed, err := resources.ParseSnapshotURI(uri) + if err != nil { + return nil, fmt.Errorf("snapshot generation %d of actor %s/%s: %w", gen.GetGeneration(), meta.GetAtespace(), meta.GetName(), err) + } + own, err := self.Prefix(parsed.Location()) + if err != nil { + return nil, fmt.Errorf("snapshot generation %d of actor %s/%s: %w", gen.GetGeneration(), meta.GetAtespace(), meta.GetName(), err) + } + add(own, ReadWrite) + switch gen.GetOwner() { + case ateapipb.SnapshotOwner_SNAPSHOT_OWNER_ACTOR: + // Its own prefix is already granted above. + case ateapipb.SnapshotOwner_SNAPSHOT_OWNER_TAG: + if durable.GetStatus() != ateapipb.SnapshotStorageStatus_SNAPSHOT_STORAGE_STATUS_COMPLETED { + continue + } + add(parsed.OwnerPrefix(), ReadOnly) + default: + return nil, fmt.Errorf("snapshot generation %d of actor %s/%s has unknown owner %v", gen.GetGeneration(), meta.GetAtespace(), meta.GetName(), gen.GetOwner()) + } + } + // ReadWrite first, then by prefix, so equal inputs give equal output. + sort.SliceStable(grants, func(i, j int) bool { + if grants[i].Access != grants[j].Access { + return grants[i].Access > grants[j].Access + } + return grants[i].Prefix.String() < grants[j].Prefix.String() + }) + return grants, nil +} diff --git a/internal/objectstoreplugin/actorscope/actorscope_test.go b/internal/objectstoreplugin/actorscope/actorscope_test.go new file mode 100644 index 0000000000..d049d6f446 --- /dev/null +++ b/internal/objectstoreplugin/actorscope/actorscope_test.go @@ -0,0 +1,158 @@ +// 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 actorscope + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + + "github.com/agent-substrate/substrate/internal/resources" + "github.com/agent-substrate/substrate/pkg/proto/ateapipb" +) + +const ( + loc = "gs://snaps/ate-demo" + atespace = "team-a" + uid = "9f1e0b2a-9e4f-4c3d-8a6b-5f2e1d0c9b8a" + tagUID = "7a3c0b2a-9e4f-4c3d-8a6b-5f2e1d0c9b8a" +) + +func actorURI(location, name string) string { + return location + "/atespaces/" + atespace + "/actors/" + uid + "/snapshots/" + name +} + +func gen(n int32, owner ateapipb.SnapshotOwner, status ateapipb.SnapshotStorageStatus, uri string) *ateapipb.Snapshot { + s := &ateapipb.Snapshot{Generation: n, Owner: owner} + if uri != "" { + s.DurableSnapshot = &ateapipb.SnapshotStorage{Status: status, Object: &ateapipb.ObjectSnapshot{SnapshotUri: uri}} + } else { + s.Locality = "node-1" + } + return s +} + +func actorWith(snaps ...*ateapipb.Snapshot) *ateapipb.Actor { + return &ateapipb.Actor{ + Metadata: &ateapipb.ResourceMetadata{Atespace: atespace, Name: "bot", Uid: uid}, + Status: &ateapipb.ActorStatus{Snapshots: snaps}, + } +} + +// prefix builds the StoragePrefix a grant carries, from the string form the +// control plane records. +func prefix(uri string) resources.StoragePrefix { + p, err := resources.ParseStoragePrefix(uri) + if err != nil { + panic(err) + } + return p +} + +var ( + tagURI = loc + "/atespaces/ate-golden/tags/" + tagUID + tagGrant = Grant{Prefix: prefix(tagURI), Access: ReadOnly} + actorGrant = Grant{Prefix: prefix(loc + "/atespaces/" + atespace + "/actors/" + uid), Access: ReadWrite} + completed = ateapipb.SnapshotStorageStatus_SNAPSHOT_STORAGE_STATUS_COMPLETED + pending = ateapipb.SnapshotStorageStatus_SNAPSHOT_STORAGE_STATUS_PENDING + ownerActor = ateapipb.SnapshotOwner_SNAPSHOT_OWNER_ACTOR + ownerTag = ateapipb.SnapshotOwner_SNAPSHOT_OWNER_TAG +) + +func TestFromActor(t *testing.T) { + for _, tc := range []struct { + name string + actor *ateapipb.Actor + want []Grant + }{ + { + name: "no generations", + actor: actorWith(), + want: nil, + }, + { + name: "tag only, before the first suspend", + actor: actorWith(gen(1, ownerTag, completed, tagURI)), + want: []Grant{actorGrant, tagGrant}, + }, + { + name: "tag plus pending actor generation, during the first suspend", + actor: actorWith( + gen(1, ownerTag, completed, tagURI), + gen(2, ownerActor, pending, actorURI(loc, "c0de")), + ), + want: []Grant{actorGrant, tagGrant}, + }, + { + name: "actor only, after the first suspend", + actor: actorWith(gen(2, ownerActor, completed, actorURI(loc, "c0de"))), + want: []Grant{actorGrant}, + }, + { + name: "local-only generation adds nothing", + actor: actorWith( + gen(2, ownerActor, completed, actorURI(loc, "c0de")), + gen(3, ownerActor, completed, ""), + ), + want: []Grant{actorGrant}, + }, + { + name: "two actor generations under one prefix give one grant", + actor: actorWith( + gen(2, ownerActor, completed, actorURI(loc, "c0de")), + gen(3, ownerActor, pending, actorURI(loc, "d00d")), + ), + want: []Grant{actorGrant}, + }, + { + name: "a repointed actor keeps both locations", + actor: actorWith( + gen(2, ownerActor, completed, actorURI(loc, "c0de")), + gen(3, ownerActor, pending, actorURI("gs://other-bucket/root", "d00d")), + ), + want: []Grant{ + {Prefix: prefix("gs://other-bucket/root/atespaces/" + atespace + "/actors/" + uid), Access: ReadWrite}, + actorGrant, + }, + }, + { + name: "a pending tag copy is not yet readable, but names the location", + actor: actorWith(gen(1, ownerTag, pending, tagURI)), + want: []Grant{actorGrant}, + }, + } { + t.Run(tc.name, func(t *testing.T) { + got, err := FromActor(tc.actor) + if err != nil { + t.Fatalf("FromActor: %v", err) + } + if diff := cmp.Diff(tc.want, got, cmp.AllowUnexported(resources.StoragePrefix{})); diff != "" { + t.Errorf("grants (-want +got):\n%s", diff) + } + }) + } +} + +func TestFromActor_RefusesUnknownOwner(t *testing.T) { + if _, err := FromActor(actorWith(gen(1, ateapipb.SnapshotOwner_SNAPSHOT_OWNER_UNSPECIFIED, completed, tagURI))); err == nil { + t.Fatal("a generation with an unspecified owner was accepted") + } +} + +func TestFromActor_RefusesMalformedURI(t *testing.T) { + if _, err := FromActor(actorWith(gen(2, ownerActor, completed, "gs://snaps/not/a/snapshot"))); err == nil { + t.Fatal("a URI outside the snapshot layout was accepted") + } +} diff --git a/internal/objectstoreplugin/gcsboundary/boundary.go b/internal/objectstoreplugin/gcsboundary/boundary.go new file mode 100644 index 0000000000..9e4e356889 --- /dev/null +++ b/internal/objectstoreplugin/gcsboundary/boundary.go @@ -0,0 +1,85 @@ +// 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 gcsboundary turns actor grants into the Credential Access Boundary +// rules a downscoped GCS token carries. One rule per grant, each limited to its +// bucket and an object-name prefix. +package gcsboundary + +import ( + "fmt" + "strings" + + "golang.org/x/oauth2/google/downscope" + + "github.com/agent-substrate/substrate/internal/objectstore" + "github.com/agent-substrate/substrate/internal/objectstoreplugin/actorscope" + "github.com/agent-substrate/substrate/internal/resources" +) + +const ( + roleObjectViewer = "inRole:roles/storage.objectViewer" + // objectAdmin, not objectCreator: an upload overwrites on retry and deletes + // parts after a compose. + roleObjectAdmin = "inRole:roles/storage.objectAdmin" +) + +// FromGrants builds the rules. Each grant's prefix must be a gs:// prefix. The +// condition's path ends in "/", so a match cannot spill into a sibling whose +// name merely starts the same way. No objectListPrefix term is added: the node +// plugin never lists objects. downscope.NewTokenSource enforces the rule +// count, between one and ten, see +// https://docs.cloud.google.com/iam/docs/downscoping-short-lived-credentials. +func FromGrants(grants []actorscope.Grant) ([]downscope.AccessBoundaryRule, error) { + var rules []downscope.AccessBoundaryRule + for _, g := range grants { + bucket, path, err := splitGCS(g.Prefix) + if err != nil { + return nil, err + } + var role string + switch g.Access { + case actorscope.ReadOnly: + role = roleObjectViewer + case actorscope.ReadWrite: + role = roleObjectAdmin + default: + return nil, fmt.Errorf("grant on %s has unknown access %v", g.Prefix, g.Access) + } + rules = append(rules, downscope.AccessBoundaryRule{ + AvailableResource: "//storage.googleapis.com/projects/_/buckets/" + bucket, + AvailablePermissions: []string{role}, + Condition: &downscope.AvailabilityCondition{ + Expression: fmt.Sprintf("resource.name.startsWith('projects/_/buckets/%s/objects/%s')", celEscape(bucket), celEscape(path)), + }, + }) + } + return rules, nil +} + +// splitGCS returns the bucket and object path of a gs:// prefix, the path with +// its trailing slash. +func splitGCS(prefix resources.StoragePrefix) (bucket, path string, err error) { + if !strings.HasPrefix(prefix.String(), "gs://") { + return "", "", fmt.Errorf("prefix %q is not a gs:// prefix", prefix) + } + return objectstore.BucketPrefix(prefix) +} + +// celEscaper quotes the two characters that would end or alter a CEL +// single-quoted string literal. A storage location may contain a quote, since +// Substrate accepts one, so it is escaped rather than refused. +var celEscaper = strings.NewReplacer(`\`, `\\`, `'`, `\'`) + +func celEscape(s string) string { return celEscaper.Replace(s) } diff --git a/internal/objectstoreplugin/gcsboundary/boundary_test.go b/internal/objectstoreplugin/gcsboundary/boundary_test.go new file mode 100644 index 0000000000..f3b718c028 --- /dev/null +++ b/internal/objectstoreplugin/gcsboundary/boundary_test.go @@ -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. + +package gcsboundary + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + "golang.org/x/oauth2/google/downscope" + + "github.com/agent-substrate/substrate/internal/objectstoreplugin/actorscope" + "github.com/agent-substrate/substrate/internal/resources" +) + +func prefix(t *testing.T, uri string) resources.StoragePrefix { + t.Helper() + p, err := resources.ParseStoragePrefix(uri) + if err != nil { + t.Fatal(err) + } + return p +} + +func TestFromGrants(t *testing.T) { + grants := []actorscope.Grant{ + {Prefix: prefix(t, "gs://snaps/ate-demo/atespaces/team-a/actors/9f1e"), Access: actorscope.ReadWrite}, + {Prefix: prefix(t, "gs://snaps/ate-demo/atespaces/ate-golden/tags/7a3c"), Access: actorscope.ReadOnly}, + } + b, err := FromGrants(grants) + if err != nil { + t.Fatalf("FromGrants: %v", err) + } + want := []downscope.AccessBoundaryRule{ + { + AvailableResource: "//storage.googleapis.com/projects/_/buckets/snaps", + AvailablePermissions: []string{"inRole:roles/storage.objectAdmin"}, + Condition: &downscope.AvailabilityCondition{Expression: "resource.name.startsWith('projects/_/buckets/snaps/objects/ate-demo/atespaces/team-a/actors/9f1e/')"}, + }, + { + AvailableResource: "//storage.googleapis.com/projects/_/buckets/snaps", + AvailablePermissions: []string{"inRole:roles/storage.objectViewer"}, + Condition: &downscope.AvailabilityCondition{Expression: "resource.name.startsWith('projects/_/buckets/snaps/objects/ate-demo/atespaces/ate-golden/tags/7a3c/')"}, + }, + } + if diff := cmp.Diff(want, b); diff != "" { + t.Errorf("rules (-want +got):\n%s", diff) + } +} + +func TestFromGrants_Refuses(t *testing.T) { + for _, tc := range []struct { + name string + grants []actorscope.Grant + }{ + {"not gs", []actorscope.Grant{{Prefix: prefix(t, "s3://b/p"), Access: actorscope.ReadOnly}}}, + {"zero prefix", []actorscope.Grant{{Prefix: resources.StoragePrefix{}, Access: actorscope.ReadOnly}}}, + {"unknown access", []actorscope.Grant{{Prefix: prefix(t, "gs://b/p"), Access: 0}}}, + } { + t.Run(tc.name, func(t *testing.T) { + if _, err := FromGrants(tc.grants); err == nil { + t.Fatal("accepted, want error") + } + }) + } +} + +func TestFromGrants_EscapesTheCELLiteral(t *testing.T) { + // Substrate accepts a quote in a storage location, so the condition must + // escape it rather than let it end the string literal. + rules, err := FromGrants([]actorscope.Grant{{Prefix: prefix(t, `gs://snaps/tenant's/atespaces/team-a/actors/9f1e`), Access: actorscope.ReadWrite}}) + if err != nil { + t.Fatalf("FromGrants: %v", err) + } + want := `resource.name.startsWith('projects/_/buckets/snaps/objects/tenant\'s/atespaces/team-a/actors/9f1e/')` + if got := rules[0].Condition.Expression; got != want { + t.Errorf("expression = %s, want %s", got, want) + } + if got := celEscape(`a\b'c`); got != `a\\b\'c` { + t.Errorf("celEscape = %s", got) + } +} diff --git a/internal/objectstoreplugin/gcsissuer/issuer.go b/internal/objectstoreplugin/gcsissuer/issuer.go new file mode 100644 index 0000000000..372b7f8f35 --- /dev/null +++ b/internal/objectstoreplugin/gcsissuer/issuer.go @@ -0,0 +1,225 @@ +// 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 gcsissuer mints GCS tokens downscoped to an actor's grants. It is the +// GCS half of the control plugin's credential exchange: actorscope decides what +// an actor may touch, this package turns that into a credential GCS enforces. +// +// The source credential comes from the default credential chain, which on GKE +// with Workload Identity is the federated token. The downscope package +// exchanges it at STS with a Credential Access Boundary and returns an access +// token GCS limits to the boundary. STS reports no lifetime for that token, so +// the package falls back to the source token's expiry, and the two expire +// together. +package gcsissuer + +import ( + "context" + "errors" + "fmt" + "net/http" + "slices" + "time" + + "golang.org/x/oauth2" + "golang.org/x/oauth2/google" + "golang.org/x/oauth2/google/downscope" + "k8s.io/utils/lru" + + "github.com/agent-substrate/substrate/internal/objectstoreplugin/actorscope" + "github.com/agent-substrate/substrate/internal/objectstoreplugin/gcsboundary" +) + +const ( + // cloudPlatformScope is the scope the source token is requested with. The + // boundary narrows it to the actor's prefixes. + cloudPlatformScope = "https://www.googleapis.com/auth/cloud-platform" + + // minServeLifetime is the least remaining lifetime a token is cached and + // served with, so a transfer never starts on a token about to expire. There + // is no mid-transfer refresh. + minServeLifetime = 20 * time.Minute + // defaultCacheSize bounds the per-actor cache. A miss costs an exchange, + // never correctness. + defaultCacheSize = 100_000 + // stsTimeout bounds one exchange. The downscope package does not take a + // per-call context, so the client's timeout is the only bound. + stsTimeout = 30 * time.Second +) + +// Token is a downscoped GCS access token and when it expires. +type Token struct { + AccessToken string + Expiry time.Time +} + +// Issuer exchanges the pod's credential for per-actor downscoped tokens and +// caches them by actor UID. +type Issuer struct { + source oauth2.TokenSource + client *http.Client + now func() time.Time + cache *lru.Cache +} + +// entry is a cached token with the grants it was minted for, so a request can +// tell whether the token covers it. +type entry struct { + token Token + grants []actorscope.Grant +} + +// Option configures an Issuer. +type Option func(*Issuer) + +// WithTokenSource overrides the source credential, for tests. +func WithTokenSource(ts oauth2.TokenSource) Option { return func(i *Issuer) { i.source = ts } } + +// WithHTTPClient overrides the client used for STS, for tests. +func WithHTTPClient(c *http.Client) Option { return func(i *Issuer) { i.client = c } } + +// WithClock overrides the clock, for tests. +func WithClock(now func() time.Time) Option { return func(i *Issuer) { i.now = now } } + +// WithCacheSize bounds the token cache. A size below one is treated as one, +// since lru.New(0) would mean no bound at all. +func WithCacheSize(n int) Option { + return func(i *Issuer) { i.cache = lru.New(max(n, 1)) } +} + +// New returns an Issuer whose source credential is the default credential +// chain, unless an option replaces it. The credential keeps refreshing for the +// Issuer's whole life, so ctx is detached from cancellation. +func New(ctx context.Context, opts ...Option) (*Issuer, error) { + i := &Issuer{ + client: &http.Client{Timeout: stsTimeout}, + now: time.Now, + cache: lru.New(defaultCacheSize), + } + for _, o := range opts { + o(i) + } + if i.source == nil { + creds, err := google.FindDefaultCredentialsWithParams(context.WithoutCancel(ctx), google.CredentialsParams{ + Scopes: []string{cloudPlatformScope}, + }) + if err != nil { + return nil, fmt.Errorf("while loading the default credential: %w", err) + } + i.source = creds.TokenSource + } + return i, nil +} + +// Issue returns a token downscoped to grants for the actor. A cached token is +// returned while it covers the grants and has at least minServeLifetime left, +// unless force is set, which mints regardless and is what the node plugin +// sends after GCS refused a token. A minted token with less than +// minServeLifetime left is handed out anyway, since a short token beats a +// failed transfer, but it is not cached. That happens for a stretch at the +// end of every hour of the source credential's life: the downscoped token +// expires with its source, and the GKE metadata server serves the same cached +// source token until shortly before it expires, so no early refresh helps. In +// that stretch every exchange reaches STS and the node plugin re-exchanges +// once if a transfer outlives its token. +// +// The cache is keyed by actor UID and each entry remembers its grants. Grants +// narrow when the tag an actor was created from drops out after its first +// completed suspend, and the cached token still covers that, a hit. They +// widen when a never-suspended actor is repointed to another storage +// location, and the cached token lacks it, a mint. So the common path costs +// nothing and a repoint never hands out a token GCS would refuse. +func (i *Issuer) Issue(ctx context.Context, actorUID string, grants []actorscope.Grant, force bool) (Token, error) { + if len(grants) == 0 { + // Checked before the cache: an empty set is covered by any cached + // token, but an actor with no grants must get nothing. + return Token{}, errors.New("actor has no storage grants") + } + if !force { + if v, ok := i.cache.Get(actorUID); ok { + if e := v.(entry); covers(e.grants, grants) && e.token.Expiry.Sub(i.now()) >= minServeLifetime { + return e.token, nil + } + } + } + tok, err := i.mint(ctx, grants) + if err != nil { + return Token{}, err + } + if tok.Expiry.Sub(i.now()) >= minServeLifetime { + i.cache.Add(actorUID, entry{token: tok, grants: slices.Clone(grants)}) + } + return tok, nil +} + +// covers reports whether a token minted with the cached grants can serve a +// request for the requested grants: each requested prefix must appear in the +// cached grants with the same or higher access. Prefixes are compared as +// written, since both sides build them with the same helper. +func covers(cached, requested []actorscope.Grant) bool { + for _, r := range requested { + if !slices.ContainsFunc(cached, func(c actorscope.Grant) bool { + return c.Prefix == r.Prefix && c.Access >= r.Access + }) { + return false + } + } + return true +} + +func (i *Issuer) mint(ctx context.Context, grants []actorscope.Grant) (Token, error) { + rules, err := gcsboundary.FromGrants(grants) + if err != nil { + return Token{}, fmt.Errorf("while building the access boundary: %w", err) + } + // The downscope package reads its HTTP client from the context but sends + // the STS request without one, so the caller's deadline is attached by the + // transport instead. + ctx = context.WithValue(ctx, oauth2.HTTPClient, &http.Client{ + Transport: contextTransport{ctx: ctx, base: i.client.Transport}, + Timeout: i.client.Timeout, + }) + ts, err := downscope.NewTokenSource(ctx, downscope.DownscopingConfig{RootSource: i.source, Rules: rules}) + if err != nil { + return Token{}, fmt.Errorf("while building the access boundary: %w", err) + } + tok, err := ts.Token() + if err != nil { + return Token{}, fmt.Errorf("while exchanging at STS: %w", err) + } + if tok.AccessToken == "" { + return Token{}, errors.New("STS returned no access token") + } + if tok.Expiry.IsZero() { + // Without an expiry the node plugin could not know when to stop using + // the token. + return Token{}, errors.New("downscoped token has no expiry") + } + return Token{AccessToken: tok.AccessToken, Expiry: tok.Expiry}, nil +} + +// contextTransport sends every request with ctx, so a cancelled or expired +// caller stops an STS exchange in flight instead of waiting out the timeout. +type contextTransport struct { + ctx context.Context + base http.RoundTripper +} + +func (t contextTransport) RoundTrip(req *http.Request) (*http.Response, error) { + base := t.base + if base == nil { + base = http.DefaultTransport + } + return base.RoundTrip(req.WithContext(t.ctx)) +} diff --git a/internal/objectstoreplugin/gcsissuer/issuer_test.go b/internal/objectstoreplugin/gcsissuer/issuer_test.go new file mode 100644 index 0000000000..eb94f58be9 --- /dev/null +++ b/internal/objectstoreplugin/gcsissuer/issuer_test.go @@ -0,0 +1,363 @@ +// 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 gcsissuer + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "net/http/httptest" + "net/url" + "strings" + "testing" + "time" + + "github.com/google/go-cmp/cmp" + "golang.org/x/oauth2" + "golang.org/x/oauth2/google/downscope" + + "github.com/agent-substrate/substrate/internal/objectstoreplugin/actorscope" + "github.com/agent-substrate/substrate/internal/objectstoreplugin/gcsboundary" + "github.com/agent-substrate/substrate/internal/resources" +) + +func prefix(uri string) resources.StoragePrefix { + p, err := resources.ParseStoragePrefix(uri) + if err != nil { + panic(err) + } + return p +} + +var ( + rwGrant = actorscope.Grant{Prefix: prefix("gs://snaps/root/atespaces/team-a/actors/9f1e"), Access: actorscope.ReadWrite} + roGrant = actorscope.Grant{Prefix: prefix("gs://snaps/root/atespaces/ate-golden/tags/7a3c"), Access: actorscope.ReadOnly} + testGrants = []actorscope.Grant{rwGrant, roGrant} +) + +// fakeSTS records the last exchange request and answers with a token. +type fakeSTS struct { + t *testing.T + calls int + status int + body string + lastURL string + last map[string]string +} + +func (f *fakeSTS) handler(w http.ResponseWriter, r *http.Request) { + f.calls++ + if err := r.ParseForm(); err != nil { + f.t.Errorf("parse form: %v", err) + } + f.last = map[string]string{} + for k := range r.PostForm { + f.last[k] = r.PostForm.Get(k) + } + if f.status != 0 { + w.WriteHeader(f.status) + } + body := f.body + if body == "" { + body = `{"access_token":"downscoped-` + strings.Repeat("x", 8) + `","issued_token_type":"urn:ietf:params:oauth:token-type:access_token","token_type":"Bearer"}` + } + _, _ = w.Write([]byte(body)) +} + +// redirect sends every request to the fake STS, recording where it was bound. +type redirect struct { + sts *fakeSTS + target *url.URL +} + +func (r redirect) RoundTrip(req *http.Request) (*http.Response, error) { + r.sts.lastURL = req.URL.String() + req = req.Clone(req.Context()) + req.URL.Scheme, req.URL.Host = r.target.Scheme, r.target.Host + return http.DefaultTransport.RoundTrip(req) +} + +func newTestIssuer(t *testing.T, sts *fakeSTS, src oauth2.TokenSource, now func() time.Time) *Issuer { + t.Helper() + srv := httptest.NewServer(http.HandlerFunc(sts.handler)) + t.Cleanup(srv.Close) + target, _ := url.Parse(srv.URL) + client := &http.Client{Transport: redirect{sts: sts, target: target}} + i, err := New(t.Context(), WithTokenSource(src), WithHTTPClient(client), WithClock(now), WithCacheSize(2)) + if err != nil { + t.Fatalf("New: %v", err) + } + return i +} + +func staticSource(expiry time.Time) oauth2.TokenSource { + return oauth2.StaticTokenSource(&oauth2.Token{AccessToken: "source-token", Expiry: expiry}) +} + +func TestIssue_ExchangesWithTheBoundary(t *testing.T) { + now := time.Date(2026, 10, 14, 10, 0, 0, 0, time.UTC) + expiry := now.Add(time.Hour) + sts := &fakeSTS{t: t} + i := newTestIssuer(t, sts, staticSource(expiry), func() time.Time { return now }) + + tok, err := i.Issue(t.Context(), "9f1e", testGrants, false) + if err != nil { + t.Fatalf("Issue: %v", err) + } + if !strings.HasPrefix(tok.AccessToken, "downscoped-") { + t.Errorf("AccessToken = %q", tok.AccessToken) + } + if !tok.Expiry.Equal(expiry) { + t.Errorf("Expiry = %v, want the source token's %v", tok.Expiry, expiry) + } + if sts.lastURL != "https://sts.googleapis.com/v1/token" { + t.Errorf("exchanged at %s", sts.lastURL) + } + for k, want := range map[string]string{ + "grant_type": "urn:ietf:params:oauth:grant-type:token-exchange", + "subject_token_type": "urn:ietf:params:oauth:token-type:access_token", + "requested_token_type": "urn:ietf:params:oauth:token-type:access_token", + "subject_token": "source-token", + } { + if got := sts.last[k]; got != want { + t.Errorf("form %s = %q, want %q", k, got, want) + } + } + var options struct { + AccessBoundary struct { + AccessBoundaryRules []downscope.AccessBoundaryRule `json:"accessBoundaryRules"` + } `json:"accessBoundary"` + } + if err := json.Unmarshal([]byte(sts.last["options"]), &options); err != nil { + t.Fatalf("options is not a boundary: %v", err) + } + want, _ := gcsboundary.FromGrants(testGrants) + if diff := cmp.Diff(want, options.AccessBoundary.AccessBoundaryRules); diff != "" { + t.Errorf("rules on the wire (-want +got):\n%s", diff) + } +} + +func TestIssue_CachesPerActorUntilNearExpiry(t *testing.T) { + now := time.Date(2026, 10, 14, 10, 0, 0, 0, time.UTC) + clock := now + sts := &fakeSTS{t: t} + i := newTestIssuer(t, sts, staticSource(now.Add(time.Hour)), func() time.Time { return clock }) + + first, err := i.Issue(t.Context(), "9f1e", testGrants, false) + if err != nil { + t.Fatal(err) + } + second, err := i.Issue(t.Context(), "9f1e", testGrants, false) + if err != nil { + t.Fatal(err) + } + if sts.calls != 1 || second != first { + t.Fatalf("second Issue made %d STS calls in total and returned a different token", sts.calls) + } + if _, err := i.Issue(t.Context(), "other", testGrants, false); err != nil { + t.Fatal(err) + } + if sts.calls != 2 { + t.Fatalf("a different actor should exchange, STS calls = %d", sts.calls) + } + // With less than minServeLifetime left the cached token is not served. + clock = now.Add(time.Hour - minServeLifetime + time.Second) + if _, err := i.Issue(t.Context(), "9f1e", testGrants, false); err != nil { + t.Fatal(err) + } + if sts.calls != 3 { + t.Fatalf("a token with under %v left should be re-minted, STS calls = %d", minServeLifetime, sts.calls) + } +} + +func TestIssue_ForceBypassesTheCache(t *testing.T) { + now := time.Date(2026, 10, 14, 10, 0, 0, 0, time.UTC) + sts := &fakeSTS{t: t} + i := newTestIssuer(t, sts, staticSource(now.Add(time.Hour)), func() time.Time { return now }) + for _, force := range []bool{false, true} { + if _, err := i.Issue(t.Context(), "9f1e", testGrants, force); err != nil { + t.Fatal(err) + } + } + if sts.calls != 2 { + t.Fatalf("force should exchange again, STS calls = %d", sts.calls) + } +} + +func TestIssue_CacheIsBounded(t *testing.T) { + now := time.Date(2026, 10, 14, 10, 0, 0, 0, time.UTC) + sts := &fakeSTS{t: t} + i := newTestIssuer(t, sts, staticSource(now.Add(time.Hour)), func() time.Time { return now }) + for _, uid := range []string{"a", "b", "c", "a"} { + if _, err := i.Issue(t.Context(), uid, testGrants, false); err != nil { + t.Fatal(err) + } + } + // Size 2: c evicted a, so the last call for a exchanged again. + if sts.calls != 4 || i.cache.Len() != 2 { + t.Fatalf("STS calls = %d, cache len = %d, want 4 and 2", sts.calls, i.cache.Len()) + } +} + +func TestIssue_ServesTheCachedTokenWhenItCoversTheGrants(t *testing.T) { + now := time.Date(2026, 10, 14, 10, 0, 0, 0, time.UTC) + rwOther := actorscope.Grant{Prefix: prefix("gs://other/root/atespaces/team-a/actors/9f1e"), Access: actorscope.ReadWrite} + roOnOwn := actorscope.Grant{Prefix: rwGrant.Prefix, Access: actorscope.ReadOnly} + for _, tc := range []struct { + name string + first []actorscope.Grant + second []actorscope.Grant + wantMints int + }{ + {"same grants", testGrants, testGrants, 1}, + {"tag dropped after the first suspend", testGrants, []actorscope.Grant{rwGrant}, 1}, + {"read-only covered by read-write", testGrants, []actorscope.Grant{roOnOwn}, 1}, + {"repoint adds a location", testGrants, []actorscope.Grant{rwGrant, rwOther, roGrant}, 2}, + {"read-write not covered by read-only", []actorscope.Grant{roOnOwn}, []actorscope.Grant{rwGrant}, 2}, + } { + t.Run(tc.name, func(t *testing.T) { + sts := &fakeSTS{t: t} + i := newTestIssuer(t, sts, staticSource(now.Add(time.Hour)), func() time.Time { return now }) + for _, grants := range [][]actorscope.Grant{tc.first, tc.second} { + if _, err := i.Issue(t.Context(), "9f1e", grants, false); err != nil { + t.Fatal(err) + } + } + if sts.calls != tc.wantMints { + t.Fatalf("STS calls = %d, want %d", sts.calls, tc.wantMints) + } + }) + } +} + +func TestIssue_RefusesEmptyGrantsEvenWhenCached(t *testing.T) { + // An actor that lost its generations derives no grants. The cached token + // would cover an empty request, so the refusal has to come first. + now := time.Date(2026, 10, 14, 10, 0, 0, 0, time.UTC) + sts := &fakeSTS{t: t} + i := newTestIssuer(t, sts, staticSource(now.Add(time.Hour)), func() time.Time { return now }) + if _, err := i.Issue(t.Context(), "9f1e", testGrants, false); err != nil { + t.Fatal(err) + } + if _, err := i.Issue(t.Context(), "9f1e", nil, false); err == nil { + t.Fatal("Issue with no grants served the cached token") + } + if sts.calls != 1 { + t.Fatalf("STS calls = %d, want 1", sts.calls) + } +} + +func TestIssue_StopsWhenTheCallerGivesUp(t *testing.T) { + // STS hangs. A cancelled caller must get an error at once, not after the + // client timeout. + now := time.Date(2026, 10, 14, 10, 0, 0, 0, time.UTC) + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + select { + case <-r.Context().Done(): + case <-time.After(3 * time.Second): + } + })) + t.Cleanup(srv.Close) + target, _ := url.Parse(srv.URL) + client := &http.Client{Transport: redirect{sts: &fakeSTS{t: t}, target: target}, Timeout: time.Minute} + i, err := New(t.Context(), WithTokenSource(staticSource(now.Add(time.Hour))), WithHTTPClient(client), WithClock(func() time.Time { return now })) + if err != nil { + t.Fatal(err) + } + ctx, cancel := context.WithTimeout(t.Context(), 100*time.Millisecond) + defer cancel() + start := time.Now() + if _, err := i.Issue(ctx, "9f1e", testGrants, false); err == nil { + t.Fatal("Issue succeeded against a hanging STS") + } + if took := time.Since(start); took > 2*time.Second { + t.Fatalf("Issue took %v to notice the cancelled context", took) + } +} + +func TestWithCacheSize_ZeroIsBounded(t *testing.T) { + now := time.Date(2026, 10, 14, 10, 0, 0, 0, time.UTC) + sts := &fakeSTS{t: t} + srv := httptest.NewServer(http.HandlerFunc(sts.handler)) + t.Cleanup(srv.Close) + target, _ := url.Parse(srv.URL) + i, err := New(t.Context(), WithTokenSource(staticSource(now.Add(time.Hour))), WithHTTPClient(&http.Client{Transport: redirect{sts: sts, target: target}}), WithClock(func() time.Time { return now }), WithCacheSize(0)) + if err != nil { + t.Fatal(err) + } + for _, uid := range []string{"a", "b"} { + if _, err := i.Issue(t.Context(), uid, testGrants, false); err != nil { + t.Fatal(err) + } + } + if i.cache.Len() != 1 { + t.Fatalf("cache len = %d, want 1", i.cache.Len()) + } +} + +func TestIssue_DoesNotCacheAShortLivedToken(t *testing.T) { + // The source credential can hand out a token with less than the floor + // left. It is returned, so the transfer can try, but not cached. + now := time.Date(2026, 10, 14, 10, 0, 0, 0, time.UTC) + sts := &fakeSTS{t: t} + i := newTestIssuer(t, sts, staticSource(now.Add(minServeLifetime-time.Minute)), func() time.Time { return now }) + for range 2 { + tok, err := i.Issue(t.Context(), "9f1e", testGrants, false) + if err != nil { + t.Fatal(err) + } + if tok.AccessToken == "" { + t.Fatal("no token returned") + } + } + if sts.calls != 2 || i.cache.Len() != 0 { + t.Fatalf("STS calls = %d, cache len = %d, want 2 and 0", sts.calls, i.cache.Len()) + } +} + +func TestIssue_Errors(t *testing.T) { + now := time.Date(2026, 10, 14, 10, 0, 0, 0, time.UTC) + clock := func() time.Time { return now } + for _, tc := range []struct { + name string + sts *fakeSTS + source oauth2.TokenSource + grants []actorscope.Grant + want string + }{ + {"source without expiry", &fakeSTS{t: t}, staticSource(time.Time{}), testGrants, "no expiry"}, + {"STS refuses", &fakeSTS{t: t, status: 400, body: `{"error":"invalid_grant"}`}, staticSource(now.Add(time.Hour)), testGrants, "400"}, + {"STS answers without a token", &fakeSTS{t: t, body: `{}`}, staticSource(now.Add(time.Hour)), testGrants, "no access token"}, + {"no grants", &fakeSTS{t: t}, staticSource(now.Add(time.Hour)), nil, "no storage grants"}, + {"too many grants", &fakeSTS{t: t}, staticSource(now.Add(time.Hour)), manyGrants(11), "greater than 10"}, + } { + t.Run(tc.name, func(t *testing.T) { + i := newTestIssuer(t, tc.sts, tc.source, clock) + _, err := i.Issue(t.Context(), "9f1e", tc.grants, false) + if err == nil || !strings.Contains(err.Error(), tc.want) { + t.Fatalf("err = %v, want it to mention %q", err, tc.want) + } + }) + } +} + +func manyGrants(n int) []actorscope.Grant { + var gs []actorscope.Grant + for i := range n { + gs = append(gs, actorscope.Grant{Prefix: prefix(fmt.Sprintf("gs://b/p%d", i)), Access: actorscope.ReadOnly}) + } + return gs +} diff --git a/vendor/golang.org/x/oauth2/google/downscope/downscoping.go b/vendor/golang.org/x/oauth2/google/downscope/downscoping.go new file mode 100644 index 0000000000..f704f7eef6 --- /dev/null +++ b/vendor/golang.org/x/oauth2/google/downscope/downscoping.go @@ -0,0 +1,233 @@ +// Copyright 2021 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +/* +Package downscope implements the ability to downscope, or restrict, the +Identity and Access Management permissions that a short-lived Token +can use. Please note that only Google Cloud Storage supports this feature. +For complete documentation, see https://cloud.google.com/iam/docs/downscoping-short-lived-credentials + +To downscope permissions of a source credential, you need to define +a Credential Access Boundary. Said Boundary specifies which resources +the newly created credential can access, an upper bound on the permissions +it has over those resources, and optionally attribute-based conditional +access to the aforementioned resources. For more information on IAM +Conditions, see https://cloud.google.com/iam/docs/conditions-overview. + +This functionality can be used to provide a third party with +limited access to and permissions on resources held by the owner of the root +credential or internally in conjunction with the principle of least privilege +to ensure that internal services only hold the minimum necessary privileges +for their function. + +For example, a token broker can be set up on a server in a private network. +Various workloads (token consumers) in the same network will send authenticated +requests to that broker for downscoped tokens to access or modify specific google +cloud storage buckets. See the NewTokenSource example for an example of how a +token broker would use this package. + +The broker will use the functionality in this package to generate a downscoped +token with the requested configuration, and then pass it back to the token +consumer. These downscoped access tokens can then be used to access Google +Storage resources. For instance, you can create a NewClient from the +"cloud.google.com/go/storage" package and pass in option.WithTokenSource(yourTokenSource)) +*/ +package downscope + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "strings" + "time" + + "golang.org/x/oauth2" +) + +const ( + universeDomainPlaceholder = "UNIVERSE_DOMAIN" + identityBindingEndpointTemplate = "https://sts.UNIVERSE_DOMAIN/v1/token" + defaultUniverseDomain = "googleapis.com" +) + +type accessBoundary struct { + AccessBoundaryRules []AccessBoundaryRule `json:"accessBoundaryRules"` +} + +// An AvailabilityCondition restricts access to a given Resource. +type AvailabilityCondition struct { + // An Expression specifies the Cloud Storage objects where + // permissions are available. For further documentation, see + // https://cloud.google.com/iam/docs/conditions-overview + Expression string `json:"expression"` + // Title is short string that identifies the purpose of the condition. Optional. + Title string `json:"title,omitempty"` + // Description details about the purpose of the condition. Optional. + Description string `json:"description,omitempty"` +} + +// An AccessBoundaryRule Sets the permissions (and optionally conditions) +// that the new token has on given resource. +type AccessBoundaryRule struct { + // AvailableResource is the full resource name of the Cloud Storage bucket that the rule applies to. + // Use the format //storage.googleapis.com/projects/_/buckets/bucket-name. + AvailableResource string `json:"availableResource"` + // AvailablePermissions is a list that defines the upper bound on the available permissions + // for the resource. Each value is the identifier for an IAM predefined role or custom role, + // with the prefix inRole:. For example: inRole:roles/storage.objectViewer. + // Only the permissions in these roles will be available. + AvailablePermissions []string `json:"availablePermissions"` + // An Condition restricts the availability of permissions + // to specific Cloud Storage objects. Optional. + // + // A Condition can be used to make permissions available for specific objects, + // rather than all objects in a Cloud Storage bucket. + Condition *AvailabilityCondition `json:"availabilityCondition,omitempty"` +} + +type downscopedTokenResponse struct { + AccessToken string `json:"access_token"` + IssuedTokenType string `json:"issued_token_type"` + TokenType string `json:"token_type"` + ExpiresIn int `json:"expires_in"` +} + +// DownscopingConfig specifies the information necessary to request a downscoped token. +type DownscopingConfig struct { + // RootSource is the TokenSource used to create the downscoped token. + // The downscoped token therefore has some subset of the accesses of + // the original RootSource. + RootSource oauth2.TokenSource + // Rules defines the accesses held by the new + // downscoped Token. One or more AccessBoundaryRules are required to + // define permissions for the new downscoped token. Each one defines an + // access (or set of accesses) that the new token has to a given resource. + // There can be a maximum of 10 AccessBoundaryRules. + Rules []AccessBoundaryRule + // UniverseDomain is the default service domain for a given Cloud universe. + // The default value is "googleapis.com". Optional. + UniverseDomain string +} + +// identityBindingEndpoint returns the identity binding endpoint with the +// configured universe domain. +func (dc *DownscopingConfig) identityBindingEndpoint() string { + if dc.UniverseDomain == "" { + return strings.Replace(identityBindingEndpointTemplate, universeDomainPlaceholder, defaultUniverseDomain, 1) + } + return strings.Replace(identityBindingEndpointTemplate, universeDomainPlaceholder, dc.UniverseDomain, 1) +} + +// A downscopingTokenSource is used to retrieve a downscoped token with restricted +// permissions compared to the root Token that is used to generate it. +type downscopingTokenSource struct { + // ctx is the context used to query the API to retrieve a downscoped Token. + ctx context.Context + // config holds the information necessary to generate a downscoped Token. + config DownscopingConfig + // identityBindingEndpoint is the identity binding endpoint with the + // configured universe domain. + identityBindingEndpoint string +} + +// NewTokenSource returns a configured downscopingTokenSource. +func NewTokenSource(ctx context.Context, conf DownscopingConfig) (oauth2.TokenSource, error) { + if conf.RootSource == nil { + return nil, fmt.Errorf("downscope: rootSource cannot be nil") + } + if len(conf.Rules) == 0 { + return nil, fmt.Errorf("downscope: length of AccessBoundaryRules must be at least 1") + } + if len(conf.Rules) > 10 { + return nil, fmt.Errorf("downscope: length of AccessBoundaryRules may not be greater than 10") + } + for _, val := range conf.Rules { + if val.AvailableResource == "" { + return nil, fmt.Errorf("downscope: all rules must have a nonempty AvailableResource: %+v", val) + } + if len(val.AvailablePermissions) == 0 { + return nil, fmt.Errorf("downscope: all rules must provide at least one permission: %+v", val) + } + } + return downscopingTokenSource{ + ctx: ctx, + config: conf, + identityBindingEndpoint: conf.identityBindingEndpoint(), + }, nil +} + +// Token() uses a downscopingTokenSource to generate an oauth2 Token. +// Do note that the returned TokenSource is an oauth2.StaticTokenSource. If you wish +// to refresh this token automatically, then initialize a locally defined +// TokenSource struct with the Token held by the StaticTokenSource and wrap +// that TokenSource in an oauth2.ReuseTokenSource. +func (dts downscopingTokenSource) Token() (*oauth2.Token, error) { + + downscopedOptions := struct { + Boundary accessBoundary `json:"accessBoundary"` + }{ + Boundary: accessBoundary{ + AccessBoundaryRules: dts.config.Rules, + }, + } + + tok, err := dts.config.RootSource.Token() + if err != nil { + return nil, fmt.Errorf("downscope: unable to obtain root token: %v", err) + } + + b, err := json.Marshal(downscopedOptions) + if err != nil { + return nil, fmt.Errorf("downscope: unable to marshal AccessBoundary payload %v", err) + } + + form := url.Values{} + form.Add("grant_type", "urn:ietf:params:oauth:grant-type:token-exchange") + form.Add("subject_token_type", "urn:ietf:params:oauth:token-type:access_token") + form.Add("requested_token_type", "urn:ietf:params:oauth:token-type:access_token") + form.Add("subject_token", tok.AccessToken) + form.Add("options", string(b)) + + myClient := oauth2.NewClient(dts.ctx, nil) + resp, err := myClient.PostForm(dts.identityBindingEndpoint, form) + if err != nil { + return nil, fmt.Errorf("unable to generate POST Request %v", err) + } + defer resp.Body.Close() + respBody, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("downscope: unable to read response body: %v", err) + } + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("downscope: unable to exchange token; %v. Server responded: %s", resp.StatusCode, respBody) + } + + var tresp downscopedTokenResponse + + err = json.Unmarshal(respBody, &tresp) + if err != nil { + return nil, fmt.Errorf("downscope: unable to unmarshal response body: %v", err) + } + + // an exchanged token that is derived from a service account (2LO) has an expired_in value + // a token derived from a users token (3LO) does not. + // The following code uses the time remaining on rootToken for a user as the value for the + // derived token's lifetime + var expiryTime time.Time + if tresp.ExpiresIn > 0 { + expiryTime = time.Now().Add(time.Duration(tresp.ExpiresIn) * time.Second) + } else { + expiryTime = tok.Expiry + } + + newToken := &oauth2.Token{ + AccessToken: tresp.AccessToken, + TokenType: tresp.TokenType, + Expiry: expiryTime, + } + return newToken, nil +} diff --git a/vendor/modules.txt b/vendor/modules.txt index 67eb30ed51..1c6bba7903 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -1290,6 +1290,7 @@ golang.org/x/net/websocket golang.org/x/oauth2 golang.org/x/oauth2/authhandler golang.org/x/oauth2/google +golang.org/x/oauth2/google/downscope golang.org/x/oauth2/google/externalaccount golang.org/x/oauth2/google/internal/externalaccountauthorizeduser golang.org/x/oauth2/google/internal/impersonate From ac296df67731f6a90dc0c7eed3fecc7f4a8a58f5 Mon Sep 17 00:00:00 2001 From: Taahir Ahmed Date: Thu, 15 Oct 2026 21:35:51 +0000 Subject: [PATCH 13/31] ate-api-server: Create one unified configuration file (#2021) Initially, it hosts nothing but logging config. A follow-on PR will move the authentication config into the unified config. --- cmd/ate-setup/commands.md | 2 + cmd/ate-setup/internal/cmd/create.go | 5 + cmd/ate-setup/internal/config/config.go | 7 + cmd/ate-setup/internal/config/config_test.go | 44 +++++ .../internal/config/coverage_test.go | 2 + cmd/ate-setup/internal/config/load.go | 8 + cmd/ate-setup/internal/config/setting.go | 6 + cmd/ate-setup/internal/steps/create.go | 42 ++++ cmd/ate-setup/internal/steps/prereqs.go | 24 ++- cmd/ate-setup/internal/steps/prereqs_test.go | 86 ++++++++ cmd/ate-setup/shim_test.go | 6 + cmd/ateapi/internal/apiconfig/loader.go | 56 ++++++ cmd/ateapi/internal/apiconfig/loader_test.go | 50 +++++ cmd/ateapi/main.go | 26 ++- docs/operator-install.md | 1 + hack/install-ate.sh | 14 ++ internal/protoredact/protoredact_test.go | 2 + manifests/ate-install/ate-api-server.yaml | 7 + pkg/proto/ateconfigpb/apiconfig.pb.go | 187 ++++++++++++++++++ pkg/proto/ateconfigpb/apiconfig.proto | 29 +++ 20 files changed, 600 insertions(+), 4 deletions(-) create mode 100644 cmd/ate-setup/internal/steps/prereqs_test.go create mode 100644 cmd/ateapi/internal/apiconfig/loader.go create mode 100644 cmd/ateapi/internal/apiconfig/loader_test.go create mode 100644 pkg/proto/ateconfigpb/apiconfig.pb.go create mode 100644 pkg/proto/ateconfigpb/apiconfig.proto diff --git a/cmd/ate-setup/commands.md b/cmd/ate-setup/commands.md index b7fde07919..ae716ff032 100644 --- a/cmd/ate-setup/commands.md +++ b/cmd/ate-setup/commands.md @@ -33,6 +33,7 @@ a pre-scan pass, so they may appear anywhere on its command line. | `--fake-data-plane` | `--fake-data-plane` | Install the fake data plane for control-plane benchmarks: fake-atelet takes atelet's place in its DaemonSet, taking its objectstore-plugin's storage settings, ate-controller is scaled to zero, and fake-workersync honors every WorkerPool with fake Workers on the fake-atelet nodes. Reaches `deploy ate-system`, `deploy atelet`, and `deploy ate-controller`; only `deploy ate-system` switches a cluster between the real and fake data planes, and the other two refuse a data plane other than the installed one. `deploy ate-system --fake-data-plane` refuses a cluster whose WorkerPools have worker pods. fake-atelet runs with its defaults; a benchmark tunes it through the optional `fake-atelet-tuning` ConfigMap, whose `FAKE_ATELET_*` variables set its flags, and which no install creates or resets. Actors do not exist; never use it on a cluster that serves real actors. `ATE_INSTALL_FAKE_DATA_PLANE=true` when the flag is absent | | `--experimental-additional-egress-extproc-service NS/SVC:PORT` | `--experimental-additional-egress-extproc-service NS/SVC:PORT` | External processor authorization filter | | `--credential-provider JSON` | `--credential-provider JSON`, or `ATE_CREDENTIAL_PROVIDER` | Required by `deploy ate-system` and `deploy atenet`. A JSON object: `{"name":"k8s.io"}` deploys and uses the bundled Kubernetes Secrets provider, with a NetworkPolicy that admits only the egress gateway; `{"enabled":false}` turns egress credential injection off; `{"name":"","address":":"}` uses a provider you deploy yourself. See [`docs/egress-credential-injection.md`](../../docs/egress-credential-injection.md) | +| `--ateapi-log-level LEVEL` | `--ateapi-log-level LEVEL`, or `ATE_API_LOG_LEVEL=LEVEL` | ate-api-server log level: `debug`, `info`, `warn`, or `error`. Written to the `ate-api-config` ConfigMap on every deploy when set; unset keeps the installed level (`info` on a new install). The server picks up a change without restarting | | `--otlp-endpoint URL` | `--otlp-endpoint URL`, or `ATE_OTLP_ENDPOINT=URL` | Send control plane telemetry to `URL` instead of the cluster default (see [`benchmarking/telemetry/README.md`](../../benchmarking/telemetry/README.md)) | | `--preview GATES` | `--preview GATES`, or `ATE_PREVIEW=GATES` | Preview gates passed as `--preview` to every substrate component: comma-separated gate names, or `ALL` for all. `ATE_PREVIEW` or `config.preview` when the flag is absent. An explicit `--preview=''` turns them all off, even when `ATE_PREVIEW` is set | | `--context NAME` | `KUBECTL_CONTEXT=NAME` | Kubeconfig context; still defaults to `KUBECTL_CONTEXT` | @@ -153,6 +154,7 @@ Individual secrets and config that `deploy ate-system` creates automatically. | `create podcertificate-controller-cas` | `--create-podcertificate-controller-cas` | | `create api-server-env-vars` | `--create-api-server-env-vars` | | `create api-authentication-config` | `--create-api-authentication-config` | +| `create api-config` | `--create-api-config` | ## Setup diff --git a/cmd/ate-setup/internal/cmd/create.go b/cmd/ate-setup/internal/cmd/create.go index a0b4da52e6..e58f95b084 100644 --- a/cmd/ate-setup/internal/cmd/create.go +++ b/cmd/ate-setup/internal/cmd/create.go @@ -82,6 +82,11 @@ func init() { short: "Create the default ate-api-server authentication config", run: (*steps.Env).CreateAPIAuthenticationConfig, }, + { + use: "api-config", + short: "Create the ate-api-server config file, including its log level", + run: (*steps.Env).CreateAPIConfig, + }, } { run := sub.run createCmd.AddCommand(&cobra.Command{ diff --git a/cmd/ate-setup/internal/config/config.go b/cmd/ate-setup/internal/config/config.go index 6e43606974..c24aa5f1b3 100644 --- a/cmd/ate-setup/internal/config/config.go +++ b/cmd/ate-setup/internal/config/config.go @@ -222,9 +222,16 @@ type Config struct { // OtlpEndpoint is where the control plane ships telemetry // (ATE_OTLP_ENDPOINT). Benchmark actors are pointed at it too. OtlpEndpoint string + // PreviewGates is the --preview value (ATE_PREVIEW) every substrate // component is deployed with: gate names, or "ALL" for all of them. PreviewGates []string + + // APILogLevel is the ate-api-server log level: debug, info, warn, or + // error. Empty means none was asked for, which keeps the level an + // existing install already has. + APILogLevel string + // BenchmarkActorMemory is the memory limit for benchmark actors // (BENCHMARK_ACTOR_MEMORY). Empty leaves the workload default in place. BenchmarkActorMemory string diff --git a/cmd/ate-setup/internal/config/config_test.go b/cmd/ate-setup/internal/config/config_test.go index 20a2abd650..584b6fbf35 100644 --- a/cmd/ate-setup/internal/config/config_test.go +++ b/cmd/ate-setup/internal/config/config_test.go @@ -46,6 +46,7 @@ func loadEnv(t *testing.T) { "ACTOR_JWT_ISSUER", "ANTHROPIC_API_KEY", "ATE_ADDITIONAL_EGRESS_EXTPROC_SERVICE", + "ATE_API_LOG_LEVEL", "ATE_API_POSTGRES_CLOUDSQL_GSA", "ATE_API_POSTGRES_CLOUDSQL_IAM_AUTH", "ATE_API_POSTGRES_CLOUDSQL_IP_TYPE", @@ -557,6 +558,49 @@ func TestLoadExpectedJWTIssuer(t *testing.T) { } } +// An unset level stays empty, so that a deploy keeps the level an existing +// install has rather than resetting it to the default. +func TestLoadAPILogLevel(t *testing.T) { + for _, tt := range []struct { + name string + env string + flag string + want string + wantErr bool + }{ + {name: "unset", want: ""}, + {name: "environment", env: "debug", want: "debug"}, + {name: "flag beats environment", env: "debug", flag: "warn", want: "warn"}, + {name: "unsupported", env: "verbose", wantErr: true}, + } { + t.Run(tt.name, func(t *testing.T) { + loadEnv(t) + t.Setenv("ATE_API_LOG_LEVEL", tt.env) + fs := pflag.NewFlagSet("test", pflag.ContinueOnError) + BindFlags(fs) + if tt.flag != "" { + if err := fs.Set("ateapi-log-level", tt.flag); err != nil { + t.Fatalf("Set(--ateapi-log-level) error = %v", err) + } + } + + cfg, err := LoadFlags(fs, LoadOptions{NoDevEnv: true}) + if tt.wantErr { + if err == nil { + t.Fatalf("LoadFlags() with ATE_API_LOG_LEVEL=%q returned nil error", tt.env) + } + return + } + if err != nil { + t.Fatalf("LoadFlags() error = %v", err) + } + if cfg.APILogLevel != tt.want { + t.Errorf("APILogLevel = %q, want %q", cfg.APILogLevel, tt.want) + } + }) + } +} + func TestLoadActorJWTAlgorithm(t *testing.T) { for _, tt := range []struct { name string diff --git a/cmd/ate-setup/internal/config/coverage_test.go b/cmd/ate-setup/internal/config/coverage_test.go index f8a45bba42..036b0739f0 100644 --- a/cmd/ate-setup/internal/config/coverage_test.go +++ b/cmd/ate-setup/internal/config/coverage_test.go @@ -102,6 +102,8 @@ func valueFor(key string) string { return "ES256" case "actorJWT.issuer": return "https://idp.example.com" + case "ateapi.logLevel": + return "debug" case "csi.setup": return "none" case "preview": diff --git a/cmd/ate-setup/internal/config/load.go b/cmd/ate-setup/internal/config/load.go index 9ce6e53b47..a339f0b287 100644 --- a/cmd/ate-setup/internal/config/load.go +++ b/cmd/ate-setup/internal/config/load.go @@ -266,6 +266,7 @@ func buildConfig(root string, env map[string]string, r *Resolved) (*Config, erro CredentialProviderJSON: r.String("atenet.egress.credentialProvider"), AnthropicAPIKey: r.String("demo.anthropicAPIKey"), OtlpEndpoint: r.String("otlpEndpoint"), + APILogLevel: r.String("ateapi.logLevel"), BenchmarkActorMemory: r.String("benchmark.actorMemory"), PreviewGates: splitList(r.String("preview")), kubeconfigEnv: kubeconfigEnv, @@ -403,6 +404,13 @@ func validateResolved(cfg *Config, r *Resolved) error { return &InvalidError{Value: authMode, Want: IngressAuthDeprecatedInsecure + " or " + IngressAuthStaticMTLS} } + switch cfg.APILogLevel { + case "", "debug", "info", "warn", "error": + default: + level, _ := r.Value("ateapi.logLevel") + return &InvalidError{Value: level, Want: "debug, info, warn, or error"} + } + switch cfg.ActorJWTAlgorithm { case "ES256", "RS256": default: diff --git a/cmd/ate-setup/internal/config/setting.go b/cmd/ate-setup/internal/config/setting.go index 1de019c875..48c3fdeda2 100644 --- a/cmd/ate-setup/internal/config/setting.go +++ b/cmd/ate-setup/internal/config/setting.go @@ -162,6 +162,12 @@ var Registry = []Setting{ "(requires --atenet-dataplane=envoy)", }, + { + // No default: an unset level keeps the one an existing install has, + // and a new install gets ate-setup's default. + Key: "ateapi.logLevel", Env: "ATE_API_LOG_LEVEL", Flag: "ateapi-log-level", Kind: KindString, + Usage: "ate-api-server log level: debug, info, warn, or error (default: keep the installed level, else info)", + }, { Key: "ateapi.expectedJWTIssuer", Env: "EXPECTED_JWT_ISSUER", Flag: "ateapi-expected-jwt-issuer", Kind: KindString, diff --git a/cmd/ate-setup/internal/steps/create.go b/cmd/ate-setup/internal/steps/create.go index f8420c520b..5c4fce43f1 100644 --- a/cmd/ate-setup/internal/steps/create.go +++ b/cmd/ate-setup/internal/steps/create.go @@ -21,12 +21,14 @@ import ( "strings" "time" + "google.golang.org/protobuf/encoding/prototext" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "github.com/agent-substrate/substrate/cmd/ate-setup/internal/log" "github.com/agent-substrate/substrate/internal/localca" "github.com/agent-substrate/substrate/internal/localjwtauthority" + "github.com/agent-substrate/substrate/pkg/proto/ateconfigpb" ) // Secret and ConfigMap names the control plane reads. @@ -42,6 +44,7 @@ const ( SecretPostgresServerCA = "postgres-server-ca" ConfigMapAPIEnvVars = "ate-api-server-envvars" ConfigMapAPIAuthn = "ate-api-authentication" + ConfigMapAPIConfig = "ate-api-config" // poolKeyID is the identifier given to the first CA in a new pool, // matching the --ca-id the shell scripts passed. poolKeyID = "1" @@ -125,6 +128,45 @@ func (e *Env) CreateActorIDCACertsSecret(ctx context.Context) error { }) } +// defaultAPILogLevel is the ate-api-server log level a new install gets when +// none is asked for. +const defaultAPILogLevel = "info" + +// CreateAPIConfig writes the ate-api-server config file. +func (e *Env) CreateAPIConfig(ctx context.Context) error { + log.Step("create_api_config") + if err := e.Kube.EnsureNamespace(ctx, e.Namespace()); err != nil { + return err + } + + logLevel := e.Cfg.APILogLevel + if logLevel == "" { + logLevel = defaultAPILogLevel + } + apiConfig, err := buildAPIConfig(logLevel) + if err != nil { + return err + } + log.Infof("%s apiconfig.textproto:", ConfigMapAPIConfig) + for _, line := range strings.Split(strings.TrimRight(apiConfig, "\n"), "\n") { + log.Infof(" | %s", line) + } + return e.Kube.ApplyConfigMap(ctx, e.Namespace(), ConfigMapAPIConfig, map[string]string{ + "apiconfig.textproto": apiConfig, + }) +} + +// buildAPIConfig renders apiconfig.textproto. +func buildAPIConfig(logLevel string) (string, error) { + b, err := prototext.MarshalOptions{Multiline: true}.Marshal(&ateconfigpb.APIConfig{ + Logging: &ateconfigpb.APILogging{Level: logLevel}, + }) + if err != nil { + return "", fmt.Errorf("while marshaling the ate-api-server config: %w", err) + } + return string(b), nil +} + // CreateAPIAuthenticationConfig writes the default ate-api-server // authentication config, pointing it at the cluster's service account issuer. func (e *Env) CreateAPIAuthenticationConfig(ctx context.Context) error { diff --git a/cmd/ate-setup/internal/steps/prereqs.go b/cmd/ate-setup/internal/steps/prereqs.go index b0ccc5a6e1..0fdae5187e 100644 --- a/cmd/ate-setup/internal/steps/prereqs.go +++ b/cmd/ate-setup/internal/steps/prereqs.go @@ -31,7 +31,8 @@ var trustBundleNames = []string{ } // EnsureAPIServerPrerequisites creates the secrets and config ate-api-server -// needs, skipping anything already present. +// needs, skipping anything already present. The PostgreSQL settings and an +// explicitly requested log level are rewritten every time. func (e *Env) EnsureAPIServerPrerequisites(ctx context.Context) error { log.Step("ensure_apiserver_prerequisites") @@ -53,6 +54,10 @@ func (e *Env) EnsureAPIServerPrerequisites(ctx context.Context) error { return err } + if err := e.ensureAPIConfig(ctx); err != nil { + return err + } + exists, err := e.Kube.ConfigMapExists(ctx, e.Namespace(), ConfigMapAPIAuthn) if err != nil { return err @@ -63,6 +68,23 @@ func (e *Env) EnsureAPIServerPrerequisites(ctx context.Context) error { return nil } +// ensureAPIConfig writes the ate-api-server config file when it is missing or +// a log level was asked for explicitly. Otherwise an existing config, possibly +// edited by hand, is kept. +func (e *Env) ensureAPIConfig(ctx context.Context) error { + if e.Cfg.APILogLevel != "" { + return e.CreateAPIConfig(ctx) + } + exists, err := e.Kube.ConfigMapExists(ctx, e.Namespace(), ConfigMapAPIConfig) + if err != nil { + return err + } + if !exists { + return e.CreateAPIConfig(ctx) + } + return nil +} + // EnsurePodCertificateCAs creates the podcertificate signer pools if any is // missing. func (e *Env) EnsurePodCertificateCAs(ctx context.Context) error { diff --git a/cmd/ate-setup/internal/steps/prereqs_test.go b/cmd/ate-setup/internal/steps/prereqs_test.go new file mode 100644 index 0000000000..fa220ff639 --- /dev/null +++ b/cmd/ate-setup/internal/steps/prereqs_test.go @@ -0,0 +1,86 @@ +// 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 ( + "testing" + + "github.com/google/go-cmp/cmp" + "google.golang.org/protobuf/encoding/prototext" + "google.golang.org/protobuf/testing/protocmp" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/kubernetes/fake" + + "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/pkg/proto/ateconfigpb" +) + +// A deploy without --ateapi-log-level must not undo a level set by hand or by +// an earlier deploy, but an explicit level always wins. +func TestEnsureAPIConfig(t *testing.T) { + const handEdited = `logging { level: "warn" }` + existing := &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{Namespace: NamespaceAteSystem, Name: ConfigMapAPIConfig}, + Data: map[string]string{"apiconfig.textproto": handEdited}, + } + + for _, tc := range []struct { + name string + existing runtime.Object + logLevel string + wantLevel string + }{ + {name: "new install defaults to info", wantLevel: "info"}, + {name: "new install takes the requested level", logLevel: "debug", wantLevel: "debug"}, + {name: "existing config is kept", existing: existing, wantLevel: "warn"}, + {name: "requested level overwrites existing config", existing: existing, logLevel: "error", wantLevel: "error"}, + } { + t.Run(tc.name, func(t *testing.T) { + // NewClientset, unlike the NewSimpleClientset behind fakeKube, + // supports the server-side apply CreateAPIConfig uses. + objects := []runtime.Object{} + if tc.existing != nil { + objects = append(objects, tc.existing) + } + e := &Env{ + Cfg: &config.Config{APILogLevel: tc.logLevel}, + Kube: &kube.Client{Typed: fake.NewClientset(objects...)}, + } + + if err := e.ensureAPIConfig(t.Context()); err != nil { + t.Fatalf("ensureAPIConfig() error = %v", err) + } + + cm, err := e.Kube.GetConfigMap(t.Context(), NamespaceAteSystem, ConfigMapAPIConfig) + if err != nil { + t.Fatalf("GetConfigMap() error = %v", err) + } + if cm == nil { + t.Fatalf("ConfigMap %s was not created", ConfigMapAPIConfig) + } + got := &ateconfigpb.APIConfig{} + if err := prototext.Unmarshal([]byte(cm.Data["apiconfig.textproto"]), got); err != nil { + t.Fatalf("apiconfig.textproto does not parse: %v", err) + } + want := &ateconfigpb.APIConfig{Logging: &ateconfigpb.APILogging{Level: tc.wantLevel}} + if diff := cmp.Diff(want, got, protocmp.Transform()); diff != "" { + t.Errorf("apiconfig.textproto mismatch (-want +got):\n%s", diff) + } + }) + } +} diff --git a/cmd/ate-setup/shim_test.go b/cmd/ate-setup/shim_test.go index 159a3e210a..185245482d 100644 --- a/cmd/ate-setup/shim_test.go +++ b/cmd/ate-setup/shim_test.go @@ -225,6 +225,10 @@ func TestShimTranslatesFlags(t *testing.T) { name: "--cluster-size takes an attached value", args: []string{"--cluster-size=size10", "--deploy-ate-apiserver"}, want: []string{"--cluster-size=size10 deploy apiserver"}, + }, { + name: "--ateapi-log-level is forwarded in either value form", + args: []string{"--ateapi-log-level", "debug", "--deploy-ate-apiserver", "--ateapi-log-level=warn"}, + want: []string{"--ateapi-log-level=debug --ateapi-log-level=warn deploy apiserver"}, }, { name: "--credential-provider is forwarded with a separate value", args: []string{"--deploy-atenet", "--credential-provider", `{"name":"custom","address":"cred.ate-system.svc:50051"}`}, @@ -251,6 +255,7 @@ func TestShimTranslatesFlags(t *testing.T) { "--create-podcertificate-controller-cas", "--create-api-server-env-vars", "--create-api-authentication-config", + "--create-api-config", }, want: []string{ "create jwt-authority-pool", @@ -260,6 +265,7 @@ func TestShimTranslatesFlags(t *testing.T) { "create podcertificate-controller-cas", "create api-server-env-vars", "create api-authentication-config", + "create api-config", }, }, { name: "delete flags", diff --git a/cmd/ateapi/internal/apiconfig/loader.go b/cmd/ateapi/internal/apiconfig/loader.go new file mode 100644 index 0000000000..d6f839c777 --- /dev/null +++ b/cmd/ateapi/internal/apiconfig/loader.go @@ -0,0 +1,56 @@ +// 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 apiconfig + +import ( + "fmt" + "os" + "path/filepath" + + "github.com/agent-substrate/substrate/pkg/proto/ateconfigpb" + "google.golang.org/protobuf/encoding/prototext" + "google.golang.org/protobuf/proto" +) + +func LoadConfig(configFile string) (*ateconfigpb.APIConfig, error) { + configBytes, err := os.ReadFile(configFile) + if err != nil { + return nil, fmt.Errorf("while reading config file: %w", err) + } + + config := &ateconfigpb.APIConfig{} + switch ext := filepath.Ext(configFile); ext { + case ".pb": + if err := proto.Unmarshal(configBytes, config); err != nil { + return nil, fmt.Errorf("while unmarshaling protobuf: %w", err) + } + case ".textproto", ".textpb": + if err := prototext.Unmarshal(configBytes, config); err != nil { + return nil, fmt.Errorf("while unmarshaling prototext: %w", err) + } + default: + return nil, fmt.Errorf("unrecognized config extension %q", ext) + } + + // Validate log level + switch config.GetLogging().GetLevel() { + case "debug", "info", "warn", "error": + // Do nothing + default: + return nil, fmt.Errorf("logging.level: invalid value %q", config.GetLogging().GetLevel()) + } + + return config, nil +} diff --git a/cmd/ateapi/internal/apiconfig/loader_test.go b/cmd/ateapi/internal/apiconfig/loader_test.go new file mode 100644 index 0000000000..c55b9b951b --- /dev/null +++ b/cmd/ateapi/internal/apiconfig/loader_test.go @@ -0,0 +1,50 @@ +// 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 apiconfig + +import ( + "os" + "path/filepath" + "testing" +) + +func TestLoadConfigValidatesLogLevel(t *testing.T) { + testCases := []struct { + name string + content string + wantErr bool + }{ + {name: "debug", content: `logging { level: "debug" }`}, + {name: "info", content: `logging { level: "info" }`}, + {name: "warn", content: `logging { level: "warn" }`}, + {name: "error", content: `logging { level: "error" }`}, + {name: "invalid level", content: `logging { level: "verbose" }`, wantErr: true}, + {name: "missing level", content: ``, wantErr: true}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + configFile := filepath.Join(t.TempDir(), "config.textproto") + if err := os.WriteFile(configFile, []byte(tc.content), 0o600); err != nil { + t.Fatalf("Failed to write config file: %v", err) + } + + _, err := LoadConfig(configFile) + if gotErr := err != nil; gotErr != tc.wantErr { + t.Errorf("LoadConfig() error = %v, wantErr %v", err, tc.wantErr) + } + }) + } +} diff --git a/cmd/ateapi/main.go b/cmd/ateapi/main.go index 27d44e8c60..6205bcf6b7 100644 --- a/cmd/ateapi/main.go +++ b/cmd/ateapi/main.go @@ -28,6 +28,7 @@ import ( "time" "github.com/agent-substrate/substrate/cmd/ateapi/internal/apiauthn" + "github.com/agent-substrate/substrate/cmd/ateapi/internal/apiconfig" "github.com/agent-substrate/substrate/cmd/ateapi/internal/authz" "github.com/agent-substrate/substrate/cmd/ateapi/internal/controlapi" "github.com/agent-substrate/substrate/cmd/ateapi/internal/store/atepg" @@ -70,6 +71,8 @@ const maxRPCDeadline = 10 * time.Minute const minResyncInterval = 250 * time.Millisecond var ( + configFile = pflag.String("config-file", "", "API server config file") + 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.") @@ -110,30 +113,47 @@ var ( templateResyncInterval = pflag.Duration("template-resync-interval", 20*time.Second, fmt.Sprintf("Interval between actor template resyncs. Must be at least %s.", minResyncInterval)) showVersion = pflag.Bool("version", false, "Print version and exit.") - logLevelFlag = pflag.String("log-level", "info", "Minimum log level: debug, info, warn, or error.") previewFlags = pflag.StringSlice("preview", nil, "Preview gates to enable.") ) func main() { pflag.Parse() + if *showVersion { fmt.Println(version.String()) return } + ctx := context.Background() + serverboot.InitLogger() - if err := serverboot.SetLogLevel(*logLevelFlag); err != nil { - serverboot.Fatal(ctx, "Invalid --log-level", err) + + // Load unified config. + if *configFile == "" { + serverboot.Fatal(ctx, "--config-file must be specified", nil) + } + + config, err := apiconfig.LoadConfig(*configFile) + if err != nil { + serverboot.Fatal(ctx, "Error while loading config file", err) + } + + if err := serverboot.SetLogLevel(config.GetLogging().GetLevel()); err != nil { + serverboot.Fatal(ctx, "Invalid log level in config file", err) } + if err := loadFlagsFromEnv(); err != nil { serverboot.Fatal(ctx, "Invalid PostgreSQL configuration", err) } + if err := preview.Init(*previewFlags...); err != nil { serverboot.Fatal(ctx, "Invalid --preview", err) } + if err := rejectStorageEnv(); err != nil { serverboot.Fatal(ctx, "Storage settings moved to the objectstore-plugin sidecar", err) } + slog.InfoContext(ctx, "ateapi starting", slog.String("version", version.Version)) if *templateResyncInterval < minResyncInterval { serverboot.Fatal(ctx, "Invalid --template-resync-interval", fmt.Errorf("must be at least %s", minResyncInterval)) diff --git a/docs/operator-install.md b/docs/operator-install.md index ad84e7cb50..9d64e3411c 100644 --- a/docs/operator-install.md +++ b/docs/operator-install.md @@ -275,6 +275,7 @@ demos, the [benchmarks](../benchmarking/README.md#settings), and the | `actorJWT.algorithm` | `ACTOR_JWT_ALGORITHM` | `--actor-jwt-algorithm` | `ES256` | (global) | | `actorJWT.issuer` | `ACTOR_JWT_ISSUER` | `--actor-jwt-issuer` | — | (global) | | `ateapi.expectedJWTIssuer` | `EXPECTED_JWT_ISSUER` | `--ateapi-expected-jwt-issuer` | — | (global) | +| `ateapi.logLevel` | `ATE_API_LOG_LEVEL` | `--ateapi-log-level` | — | (global) | | `ateapi.postgres.cloudsql.gsa` | `ATE_API_POSTGRES_CLOUDSQL_GSA` | `--ateapi-postgres-cloudsql-gsa` | — | (global) | | `ateapi.postgres.cloudsql.iamAuth` | `ATE_API_POSTGRES_CLOUDSQL_IAM_AUTH` | `--ateapi-postgres-cloudsql-iam-auth` | — | (global) | | `ateapi.postgres.cloudsql.instance` | `ATE_API_POSTGRES_CLOUDSQL_INSTANCE` | `--ateapi-postgres-cloudsql-instance` | — | (global) | diff --git a/hack/install-ate.sh b/hack/install-ate.sh index 1438b1f8ee..2eae504550 100755 --- a/hack/install-ate.sh +++ b/hack/install-ate.sh @@ -112,6 +112,8 @@ usage() { echo " ports; static-mtls requires the certificate and --atenet-dataplane=envoy." echo " --rollout-timeout DURATION Per-workload readiness wait timeout, kubectl-style Go duration (default: 60s)" echo " --otlp-endpoint URL Send all control plane telemetry to URL, not to the cluster default (see benchmarking/telemetry/README.md)" + echo " --ateapi-log-level LEVEL ate-api-server log level: debug, info, warn, or error" + echo " (default: keep the installed level, else info)" echo "" echo "Experiments:" echo "" @@ -147,6 +149,7 @@ usage() { echo " --create-podcertificate-controller-cas Create podcertificate controller CAs" echo " --create-api-server-env-vars Create ate-api-server env vars" echo " --create-api-authentication-config Create the default ate-api-server authentication config" + echo " --create-api-config Create the ate-api-server config file (log level from --ateapi-log-level)" echo "" echo "PostgreSQL configuration (a connection DSN or Cloud SQL instance selects" echo "an external database and skips the bundled instance):" @@ -346,6 +349,14 @@ for ((i = 0; i < ${#prescan_args[@]}; i++)); do fi GLOBAL_FLAGS+=("--otlp-endpoint=${prescan_args[$((i + 1))]}") ;; + --ateapi-log-level=*) GLOBAL_FLAGS+=("${prescan_args[i]}") ;; + --ateapi-log-level) + if (( i + 1 >= ${#prescan_args[@]} )); then + echo "Error: --ateapi-log-level requires debug, info, warn, or error" >&2 + exit 1 + fi + GLOBAL_FLAGS+=("--ateapi-log-level=${prescan_args[$((i + 1))]}") + ;; --benchmark-worker-count=*) BENCHMARK_FLAGS+=("--worker-count=${prescan_args[i]#*=}") ;; --benchmark-worker-count) BENCHMARK_FLAGS+=("--worker-count=${prescan_args[i+1]:-1}") @@ -389,12 +400,14 @@ while [[ "$#" -gt 0 ]]; do --cluster-size|--ingress-auth-mode) shift ;; --experimental-additional-egress-extproc-service) shift ;; --credential-provider) shift ;; + --ateapi-log-level) shift ;; --benchmark-worker-count|--benchmark-sandbox-class|--benchmark-actor-memory) shift ;; --atenet-dataplane=*|--podcert-workers-per-signer=*|--rollout-timeout=*|--otlp-endpoint=*) ;; --fake-data-plane|--fake-data-plane=*) ;; --cluster-size=*|--cordon-control-plane|--cordon-control-plane=*|--ingress-auth-mode=*) ;; --experimental-additional-egress-extproc-service=*) ;; --credential-provider=*) ;; + --ateapi-log-level=*) ;; --benchmark-worker-count=*|--benchmark-sandbox-class=*|--benchmark-actor-memory=*) ;; --preview) shift ;; --preview=*) ;; @@ -426,6 +439,7 @@ while [[ "$#" -gt 0 ]]; do --create-podcertificate-controller-cas) ate_setup create podcertificate-controller-cas ;; --create-api-server-env-vars) ate_setup create api-server-env-vars ;; --create-api-authentication-config) ate_setup create api-authentication-config ;; + --create-api-config) ate_setup create api-config ;; # The one demo flag that is not just a demo name. --deploy-demo-counter-with-external-volume) diff --git a/internal/protoredact/protoredact_test.go b/internal/protoredact/protoredact_test.go index d3a6425e95..9750ddb4bf 100644 --- a/internal/protoredact/protoredact_test.go +++ b/internal/protoredact/protoredact_test.go @@ -29,6 +29,7 @@ import ( "github.com/agent-substrate/substrate/internal/proto/grpcechopb" "github.com/agent-substrate/substrate/internal/protoredact" "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "github.com/agent-substrate/substrate/pkg/proto/ateconfigpb" "github.com/agent-substrate/substrate/pkg/proto/credproviderpb" objectstorev1 "github.com/agent-substrate/substrate/pkg/proto/objectstorepb/v1" "google.golang.org/protobuf/proto" @@ -380,6 +381,7 @@ var ourProtoFiles = []protoreflect.FileDescriptor{ ateapipb.File_ateapi_proto, ateletpb.File_atelet_proto, ateworkerpb.File_ateworker_proto, + ateconfigpb.File_apiconfig_proto, credproviderpb.File_credprovider_proto, glutton.File_glutton_proto, grpcechopb.File_grpcecho_proto, diff --git a/manifests/ate-install/ate-api-server.yaml b/manifests/ate-install/ate-api-server.yaml index bc2beac704..c2ddc394d6 100644 --- a/manifests/ate-install/ate-api-server.yaml +++ b/manifests/ate-install/ate-api-server.yaml @@ -129,6 +129,7 @@ spec: image: ko://github.com/agent-substrate/substrate/cmd/ateapi args: - "--grpc-server-cred-bundle=/run/servicedns.podcert.ate.dev/credential-bundle.pem" + - --config-file=/run/apiconfig.ate.dev/apiconfig.textproto - --authentication-config=/etc/ateapi/authentication/authentication.yaml - --postgres-read-write-connection-string=@env - --postgres-owner-connection-string=@env @@ -204,6 +205,9 @@ spec: - name: "actor-id-ca-pool" mountPath: "/run/actor-id-ca-pool" readOnly: true + - name: "api-config" + mountPath: "/run/apiconfig.ate.dev" + readOnly: true - name: "authentication-config" mountPath: "/etc/ateapi/authentication" readOnly: true @@ -315,6 +319,9 @@ spec: items: - key: "pool" path: "pool.json" + - name: "api-config" + configMap: + name: "ate-api-config" - name: "authentication-config" configMap: name: "ate-api-authentication" diff --git a/pkg/proto/ateconfigpb/apiconfig.pb.go b/pkg/proto/ateconfigpb/apiconfig.pb.go new file mode 100644 index 0000000000..3b56ebd0e0 --- /dev/null +++ b/pkg/proto/ateconfigpb/apiconfig.pb.go @@ -0,0 +1,187 @@ +// 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. + +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11-devel +// protoc v4.25.3 +// source: apiconfig.proto + +package ateconfigpb + +import ( + 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) +) + +// The unified configuration object for ate-api-server. +type APIConfig struct { + state protoimpl.MessageState `protogen:"open.v1"` + Logging *APILogging `protobuf:"bytes,1,opt,name=logging,proto3" json:"logging,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *APIConfig) Reset() { + *x = APIConfig{} + mi := &file_apiconfig_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *APIConfig) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*APIConfig) ProtoMessage() {} + +func (x *APIConfig) ProtoReflect() protoreflect.Message { + mi := &file_apiconfig_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 APIConfig.ProtoReflect.Descriptor instead. +func (*APIConfig) Descriptor() ([]byte, []int) { + return file_apiconfig_proto_rawDescGZIP(), []int{0} +} + +func (x *APIConfig) GetLogging() *APILogging { + if x != nil { + return x.Logging + } + return nil +} + +type APILogging struct { + state protoimpl.MessageState `protogen:"open.v1"` + // debug, info, warn, or error + Level string `protobuf:"bytes,1,opt,name=level,proto3" json:"level,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *APILogging) Reset() { + *x = APILogging{} + mi := &file_apiconfig_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *APILogging) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*APILogging) ProtoMessage() {} + +func (x *APILogging) ProtoReflect() protoreflect.Message { + mi := &file_apiconfig_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 APILogging.ProtoReflect.Descriptor instead. +func (*APILogging) Descriptor() ([]byte, []int) { + return file_apiconfig_proto_rawDescGZIP(), []int{1} +} + +func (x *APILogging) GetLevel() string { + if x != nil { + return x.Level + } + return "" +} + +var File_apiconfig_proto protoreflect.FileDescriptor + +const file_apiconfig_proto_rawDesc = "" + + "\n" + + "\x0fapiconfig.proto\x12\vateconfigpb\">\n" + + "\tAPIConfig\x121\n" + + "\alogging\x18\x01 \x01(\v2\x17.ateconfigpb.APILoggingR\alogging\"\"\n" + + "\n" + + "APILogging\x12\x14\n" + + "\x05level\x18\x01 \x01(\tR\x05levelB ateconfigpb.APILogging + 1, // [1:1] is the sub-list for method output_type + 1, // [1:1] is the sub-list for method input_type + 1, // [1:1] is the sub-list for extension type_name + 1, // [1:1] is the sub-list for extension extendee + 0, // [0:1] is the sub-list for field type_name +} + +func init() { file_apiconfig_proto_init() } +func file_apiconfig_proto_init() { + if File_apiconfig_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_apiconfig_proto_rawDesc), len(file_apiconfig_proto_rawDesc)), + NumEnums: 0, + NumMessages: 2, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_apiconfig_proto_goTypes, + DependencyIndexes: file_apiconfig_proto_depIdxs, + MessageInfos: file_apiconfig_proto_msgTypes, + }.Build() + File_apiconfig_proto = out.File + file_apiconfig_proto_goTypes = nil + file_apiconfig_proto_depIdxs = nil +} diff --git a/pkg/proto/ateconfigpb/apiconfig.proto b/pkg/proto/ateconfigpb/apiconfig.proto new file mode 100644 index 0000000000..0018732249 --- /dev/null +++ b/pkg/proto/ateconfigpb/apiconfig.proto @@ -0,0 +1,29 @@ +// 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. + +syntax = "proto3"; + +package ateconfigpb; + +option go_package = "github.com/agent-substrate/substrate/pkg/proto/ateconfigpb"; + +// The unified configuration object for ate-api-server. +message APIConfig { + APILogging logging = 1; +} + +message APILogging { + // debug, info, warn, or error + string level = 1; +} From 088381b6320436aca3557ba9b194e69ab1025b83 Mon Sep 17 00:00:00 2001 From: Lior Lieberman Date: Thu, 15 Oct 2026 22:41:14 +0000 Subject: [PATCH 14/31] atenet: Router checks the routed worker's SPIFFE ID on atunnel's cert (#2678) Part of #2078 (Envoy dataplane, @EItanya @howardjohn for agw) The router accepted any podidentity cert in the trust domain as atunnel, so any pod in the cluster could pose as a worker. It now requires the exact SPIFFE ID of the worker each request is routed to. - atecontroller's syncer records the worker pod's SPIFFE ID on the Worker (new `worker_spiffe_id`, built from the pod's namespace and ServiceAccount), and ateapi copies it into the actor's worker assignment. ateapi rejects a value that isn't a valid SPIFFE ID. - ext_proc passes that ID to Envoy as is and refuses to route without it. Envoy requires that exact SAN, on both the :443 and CONNECT paths. `--upstream-spiffe-prefix` is removed; the cluster still checks the trust domain. - The Worker carries the SPIFFE ID rather than the ServiceAccount, so the router doesn't need to know how the ID is built. A different ID scheme only changes atecontroller. - WorkerPools that don't set serviceAccountName now run as an `ateworker` ServiceAccount the controller creates, instead of `default`, so the worker's ID isn't shared with every other pod in the namespace. Remaining gap: another pod running as the same ServiceAccount in the same namespace can still pose as the worker. Closing it needs a signer that puts the pod in the worker's SPIFFE ID; atecontroller would then record that ID, with no router change. #### Breaking change - The router rejects workers registered before this change until their pods are replaced. Pools on the default ServiceAccount roll automatically; pools that set `spec.template.serviceAccountName` need a restart. Upgrade atecontroller and ate-api-server before atenet-router. - Workers now run as an `ateworker` ServiceAccount by default instead of `default`. RBAC, Workload Identity bindings and imagePullSecrets attached to `default` no longer apply to them; set `serviceAccountName: default` or move those to `ateworker`. - atenet-router no longer has `--upstream-spiffe-prefix`. - atecontroller now needs create and patch on serviceaccounts (included in the generated role). > 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/ateapi/internal/apivalidation/actor.go | 5 + .../internal/apivalidation/actor_test.go | 15 +++ cmd/ateapi/internal/apivalidation/common.go | 10 ++ cmd/ateapi/internal/apivalidation/worker.go | 5 + .../internal/apivalidation/worker_test.go | 17 ++++ .../apivalidation/zz_generated.validation.go | 74 ++++++++++++++ .../internal/controlapi/workflow_resume.go | 1 + .../controlapi/workflow_resume_test.go | 10 ++ .../internal/controllers/workerpool_apply.go | 2 +- .../controllers/workerpool_apply_test.go | 2 +- .../controllers/workerpool_controller.go | 18 ++++ .../controllers/workerpool_controller_test.go | 62 +++++++++++- .../internal/workersync/syncer.go | 7 ++ .../internal/workersync/syncer_test.go | 18 ++++ cmd/atenet/internal/router/cmd.go | 1 - cmd/atenet/internal/router/config.go | 3 - cmd/atenet/internal/router/dataplane.go | 2 +- cmd/atenet/internal/router/ingress/ingress.go | 13 ++- .../internal/router/ingress/ingress_test.go | 44 +++++++-- cmd/atenet/internal/router/xds.go | 54 ++++++---- cmd/atenet/internal/router/xds_test.go | 98 ++++++++++++++++--- .../router/xds_upstream_connect_test.go | 13 +-- docs/api-guide.md | 2 +- docs/threat-model.md | 2 +- internal/installdefaults/installdefaults.go | 4 + manifests/ate-install/atenet-router.yaml | 3 +- .../generated/ate.dev_workerpools.yaml | 4 +- manifests/ate-install/generated/role.yaml | 7 ++ pkg/api/v1alpha1/workerpool_types.go | 4 +- pkg/proto/ateapipb/ateapi.pb.go | 40 +++++++- pkg/proto/ateapipb/ateapi.proto | 16 +++ 31 files changed, 485 insertions(+), 71 deletions(-) diff --git a/cmd/ateapi/internal/apivalidation/actor.go b/cmd/ateapi/internal/apivalidation/actor.go index a77942fcd3..2e21446792 100644 --- a/cmd/ateapi/internal/apivalidation/actor.go +++ b/cmd/ateapi/internal/apivalidation/actor.go @@ -138,6 +138,11 @@ func ValidateCustom_WorkerAssignment_WorkerPodIps(_ context.Context, _ operation return validateDualStackIPs(fldPath, value) } +// This is needed because DV doesn't have a format for SPIFFE IDs. +func ValidateCustom_WorkerAssignment_WorkerSpiffeId(_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ *string) field.ErrorList { + return validateSPIFFEID(fldPath, *value) +} + func ValidateCustom_ExternalVolume_VolumeType(_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ *string) field.ErrorList { return resources.ValidateVolumeType(fldPath, *value) } diff --git a/cmd/ateapi/internal/apivalidation/actor_test.go b/cmd/ateapi/internal/apivalidation/actor_test.go index dea6ab5882..a163c114ed 100644 --- a/cmd/ateapi/internal/apivalidation/actor_test.go +++ b/cmd/ateapi/internal/apivalidation/actor_test.go @@ -354,6 +354,21 @@ func TestValidateActorUpdate(t *testing.T) { want: field.ErrorList{ field.TooMany(field.NewPath("status", "worker_assignment", "worker_pod_ips"), 3, 2).WithOrigin("maxItems"), }, + }, { + name: "set valid actor.status.worker_assignment.worker_spiffe_id", + oldVal: validInput(withStatus()), + newVal: validOutput(withStatus(withWorkerAssignment(func(wa *ateapipb.WorkerAssignment) { + wa.WorkerSpiffeId = "spiffe://cluster.local/ns/ns/sa/ateworker" + }))), + }, { + name: "invalid actor.status.worker_assignment.worker_spiffe_id", + oldVal: validInput(withStatus()), + newVal: validOutput(withStatus(withWorkerAssignment(func(wa *ateapipb.WorkerAssignment) { + wa.WorkerSpiffeId = "ns/ns/sa/ateworker" + }))), + want: field.ErrorList{ + field.Invalid(field.NewPath("status", "worker_assignment", "worker_spiffe_id"), nil, ""), + }, }, { name: "clear actor.status.worker_assignment", oldVal: validInput(withStatus(withWorkerAssignment())), diff --git a/cmd/ateapi/internal/apivalidation/common.go b/cmd/ateapi/internal/apivalidation/common.go index 73c8dcd959..3abc92ed9e 100644 --- a/cmd/ateapi/internal/apivalidation/common.go +++ b/cmd/ateapi/internal/apivalidation/common.go @@ -22,6 +22,7 @@ import ( "github.com/agent-substrate/substrate/internal/preview" "github.com/agent-substrate/substrate/internal/resources" "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "github.com/spiffe/go-spiffe/v2/spiffeid" "k8s.io/apimachinery/pkg/api/operation" "k8s.io/apimachinery/pkg/util/validation" "k8s.io/apimachinery/pkg/util/validation/field" @@ -119,3 +120,12 @@ func validateDualStackIPs(fldPath *field.Path, ips []string) field.ErrorList { } return errs } + +// validateSPIFFEID checks that id is a SPIFFE ID. Presence and length are +// enforced by tags. +func validateSPIFFEID(fldPath *field.Path, id string) field.ErrorList { + if _, err := spiffeid.FromString(id); err != nil { + return field.ErrorList{field.Invalid(fldPath, id, err.Error())} + } + return nil +} diff --git a/cmd/ateapi/internal/apivalidation/worker.go b/cmd/ateapi/internal/apivalidation/worker.go index b13769cfe5..f4b57d6e09 100644 --- a/cmd/ateapi/internal/apivalidation/worker.go +++ b/cmd/ateapi/internal/apivalidation/worker.go @@ -134,6 +134,11 @@ func ValidateCustom_Worker_Ips(_ context.Context, _ operation.Operation, fldPath return validateDualStackIPs(fldPath, value) } +// This is needed because DV doesn't have a format for SPIFFE IDs. +func ValidateCustom_Worker_WorkerSpiffeId(_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ *string) field.ErrorList { + return validateSPIFFEID(fldPath, *value) +} + // epoch only moves forward: a lower value would make Actors placed during the // current run look older than it, and crash them. func ValidateCustom_Worker_Epoch(_ context.Context, op operation.Operation, fldPath *field.Path, value, oldValue *int64) field.ErrorList { diff --git a/cmd/ateapi/internal/apivalidation/worker_test.go b/cmd/ateapi/internal/apivalidation/worker_test.go index 13e868fbe3..cdcc84f694 100644 --- a/cmd/ateapi/internal/apivalidation/worker_test.go +++ b/cmd/ateapi/internal/apivalidation/worker_test.go @@ -142,6 +142,23 @@ func TestValidateCreateWorkerRequest(t *testing.T) { w.Ips = []string{"10.1.2.3", "fd00::1", "10.1.2.4"} })), want: field.ErrorList{field.TooMany(field.NewPath("worker", "ips"), 3, 2).WithOrigin("maxItems")}, + }, { + name: "valid worker_spiffe_id", + req: validReq(validWorker(apiWorkerName, func(w *ateapipb.Worker) { + w.WorkerSpiffeId = "spiffe://cluster.local/ns/ate-system/sa/ateworker" + })), + }, { + name: "invalid worker_spiffe_id", + req: validReq(validWorker(apiWorkerName, func(w *ateapipb.Worker) { + w.WorkerSpiffeId = "https://cluster.local/ns/ate-system/sa/ateworker" + })), + want: field.ErrorList{field.Invalid(field.NewPath("worker", "worker_spiffe_id"), nil, "")}, + }, { + name: "worker_spiffe_id too long", + req: validReq(validWorker(apiWorkerName, func(w *ateapipb.Worker) { + w.WorkerSpiffeId = "spiffe://cluster.local/" + strings.Repeat("a", 2048) + })), + want: field.ErrorList{field.TooLong(field.NewPath("worker", "worker_spiffe_id"), nil, 2048).WithOrigin("maxLength")}, }, { name: "sandbox_class too long", req: validReq(validWorker(apiWorkerName, func(w *ateapipb.Worker) { w.SandboxClass = strings.Repeat("x", 64) })), diff --git a/cmd/ateapi/internal/apivalidation/zz_generated.validation.go b/cmd/ateapi/internal/apivalidation/zz_generated.validation.go index d6e8ce53b7..b5124f6261 100644 --- a/cmd/ateapi/internal/apivalidation/zz_generated.validation.go +++ b/cmd/ateapi/internal/apivalidation/zz_generated.validation.go @@ -9892,6 +9892,45 @@ func Validate_Worker( errs = append(errs, fn(fldPath.Child("ips"), obj.Ips, oldVal, oldObj != nil)...) } + { // field ateapipb.Worker.WorkerSpiffeId + 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.Immutable(ctx, op, fldPath, obj, oldObj).MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + 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_Worker_WorkerSpiffeId(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 *ateapipb.Worker) *string { + return &oldObj.WorkerSpiffeId + }) + errs = append(errs, fn(fldPath.Child("worker_spiffe_id"), &obj.WorkerSpiffeId, oldVal, oldObj != nil)...) + } + { // field ateapipb.Worker.SandboxClass fn := func( fldPath *field.Path, @@ -10356,6 +10395,41 @@ func Validate_WorkerAssignment( errs = append(errs, fn(fldPath.Child("sandbox_runtime"), obj.SandboxRuntime, oldVal, oldObj != nil)...) } + { // field ateapipb.WorkerAssignment.WorkerSpiffeId + 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_WorkerAssignment_WorkerSpiffeId(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 *ateapipb.WorkerAssignment) *string { + return &oldObj.WorkerSpiffeId + }) + errs = append(errs, fn(fldPath.Child("worker_spiffe_id"), &obj.WorkerSpiffeId, oldVal, oldObj != nil)...) + } + return errs } diff --git a/cmd/ateapi/internal/controlapi/workflow_resume.go b/cmd/ateapi/internal/controlapi/workflow_resume.go index cb82726ec9..670dd8f4ec 100644 --- a/cmd/ateapi/internal/controlapi/workflow_resume.go +++ b/cmd/ateapi/internal/controlapi/workflow_resume.go @@ -610,6 +610,7 @@ func workerAssignmentFrom(w *ateapipb.Worker, snapRuntime *ateapipb.SandboxRunti WorkerPodUid: w.GetWorkerPodUid(), WorkerPodIps: slices.Clone(w.GetIps()), NodeName: w.GetNodeName(), + WorkerSpiffeId: w.GetWorkerSpiffeId(), // The runtime the actor is started or restored under. Snapshots taken // during this assignment are stamped with it, so a worker that later // changes its default does not relabel state captured on the old one. diff --git a/cmd/ateapi/internal/controlapi/workflow_resume_test.go b/cmd/ateapi/internal/controlapi/workflow_resume_test.go index c089f36a12..0d10cf915f 100644 --- a/cmd/ateapi/internal/controlapi/workflow_resume_test.go +++ b/cmd/ateapi/internal/controlapi/workflow_resume_test.go @@ -2689,3 +2689,13 @@ func TestCanRelaxRuntime(t *testing.T) { }) } } + +// atenet-router requires the assignment's SPIFFE ID on atunnel's certificate, +// so the worker's has to reach it. +func TestWorkerAssignmentFromCopiesSPIFFEID(t *testing.T) { + const id = "spiffe://cluster.local/ns/workers/sa/substrate-worker" + got := workerAssignmentFrom(&ateapipb.Worker{WorkerSpiffeId: id}, nil) + if got.GetWorkerSpiffeId() != id { + t.Errorf("worker SPIFFE ID = %q, want %q", got.GetWorkerSpiffeId(), id) + } +} diff --git a/cmd/atecontroller/internal/controllers/workerpool_apply.go b/cmd/atecontroller/internal/controllers/workerpool_apply.go index 728e106a9a..aa3428167d 100644 --- a/cmd/atecontroller/internal/controllers/workerpool_apply.go +++ b/cmd/atecontroller/internal/controllers/workerpool_apply.go @@ -551,7 +551,7 @@ func applyWorkerPoolPodTemplate( podSpecAC.NodeSelector = map[string]string{} podSpecAC.Tolerations = []corev1ac.TolerationApplyConfiguration{} podSpecAC.WithPriorityClassName("") - podSpecAC.WithServiceAccountName("default") + podSpecAC.WithServiceAccountName(installdefaults.WorkerServiceAccount) podSpecAC.WithAffinity(corev1ac.Affinity()) resourcesAC := corev1ac.ResourceRequirements() containerAC.WithResources(resourcesAC) diff --git a/cmd/atecontroller/internal/controllers/workerpool_apply_test.go b/cmd/atecontroller/internal/controllers/workerpool_apply_test.go index 237811ba14..f550c0e4d8 100644 --- a/cmd/atecontroller/internal/controllers/workerpool_apply_test.go +++ b/cmd/atecontroller/internal/controllers/workerpool_apply_test.go @@ -905,7 +905,7 @@ func expectedDeploymentApplyConfig(mutatePodSpec func(*corev1ac.PodSpecApplyConf sandboxClassTolerationAC(atev1alpha1.SandboxClassGvisor), } podSpecAC.WithPriorityClassName("") - podSpecAC.WithServiceAccountName("default") + podSpecAC.WithServiceAccountName("ateworker") podSpecAC.WithAffinity(corev1ac.Affinity()) podSpecAC.WithTerminationGracePeriodSeconds(workerTerminationGracePeriodSeconds) if mutatePodSpec != nil { diff --git a/cmd/atecontroller/internal/controllers/workerpool_controller.go b/cmd/atecontroller/internal/controllers/workerpool_controller.go index 873f5b5126..bff2342c83 100644 --- a/cmd/atecontroller/internal/controllers/workerpool_controller.go +++ b/cmd/atecontroller/internal/controllers/workerpool_controller.go @@ -29,6 +29,7 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" + corev1ac "k8s.io/client-go/applyconfigurations/core/v1" "k8s.io/client-go/tools/events" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" @@ -38,6 +39,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/reconcile" "github.com/agent-substrate/substrate/internal/ateattr" + "github.com/agent-substrate/substrate/internal/installdefaults" atev1alpha1 "github.com/agent-substrate/substrate/pkg/api/v1alpha1" ) @@ -97,6 +99,7 @@ type WorkerPoolReconciler struct { //+kubebuilder:rbac:groups=ate.dev,resources=workerpools/status,verbs=get;update;patch //+kubebuilder:rbac:groups=ate.dev,resources=workerpools/finalizers,verbs=update //+kubebuilder:rbac:groups=apps,resources=deployments,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups=core,resources=serviceaccounts,verbs=create;patch //+kubebuilder:rbac:groups=ate.dev,resources=sandboxconfigs,verbs=get;list;watch;patch //+kubebuilder:rbac:groups=events.k8s.io,resources=events,verbs=create;patch @@ -317,6 +320,11 @@ func (r *WorkerPoolReconciler) recordSandboxConfigEvents(wp *atev1alpha1.WorkerP } func (r *WorkerPoolReconciler) applyDeployment(ctx context.Context, wp *atev1alpha1.WorkerPool) error { + if wp.Spec.Template == nil || wp.Spec.Template.ServiceAccountName == nil { + if err := r.applyWorkerServiceAccount(ctx, wp.Namespace); err != nil { + return err + } + } depAC := buildDeploymentApplyConfig(wp, workerOTelSettings{ Endpoint: r.OTelEndpoint, MetricExportInterval: r.OTelMetricExportInterval, @@ -331,6 +339,16 @@ func (r *WorkerPoolReconciler) applyDeployment(ctx context.Context, wp *atev1alp return nil } +// applyWorkerServiceAccount creates the default ServiceAccount worker pods run +// as. The pools in a namespace share it, so none of them owns it. +func (r *WorkerPoolReconciler) applyWorkerServiceAccount(ctx context.Context, namespace string) error { + saAC := corev1ac.ServiceAccount(installdefaults.WorkerServiceAccount, namespace) + if err := r.Apply(ctx, saAC, client.FieldOwner(workerPoolFieldOwner), client.ForceOwnership); err != nil { + return fmt.Errorf("failed to apply worker ServiceAccount: %w", err) + } + return nil +} + // syncStatus writes wp's status: replica counts and selector from dep (left // as they are when dep is nil) and sandboxClasses from classes. func (r *WorkerPoolReconciler) syncStatus(ctx context.Context, wp *atev1alpha1.WorkerPool, dep *appsv1.Deployment, classes []atev1alpha1.WorkerPoolSandboxClassStatus) error { diff --git a/cmd/atecontroller/internal/controllers/workerpool_controller_test.go b/cmd/atecontroller/internal/controllers/workerpool_controller_test.go index f7e97579c1..dc272bf781 100644 --- a/cmd/atecontroller/internal/controllers/workerpool_controller_test.go +++ b/cmd/atecontroller/internal/controllers/workerpool_controller_test.go @@ -390,6 +390,66 @@ func sampleWorkerPoolPodTemplate() *atev1alpha1.WorkerPoolPodTemplate { } } +// TestWorkerPoolRunsWorkersAsWorkerServiceAccount checks that a pool naming no +// ServiceAccount runs its workers as the default one, which the controller +// creates. +func TestWorkerPoolRunsWorkersAsWorkerServiceAccount(t *testing.T) { + t.Parallel() + ctx := t.Context() + wp := makeWorkerPool("test-worker-sa", "default", 1, "worker:v1") + if err := k8sClient.Create(ctx, wp); err != nil { + t.Fatalf("create WorkerPool: %v", err) + } + deleteOnCleanup(t, wp) + + eventually(t, func(ctx context.Context) (bool, error) { + dep, err := getDeployment(ctx, wp) + if err != nil { + return false, nil + } + return dep.Spec.Template.Spec.ServiceAccountName == installdefaults.WorkerServiceAccount, nil + }) + // Applied before the Deployment, so it exists by now. + sa := &corev1.ServiceAccount{} + if err := k8sClient.Get(ctx, types.NamespacedName{Name: installdefaults.WorkerServiceAccount, Namespace: wp.Namespace}, sa); err != nil { + t.Fatalf("get worker ServiceAccount: %v", err) + } + // Pools share it, so deleting one must not delete it. + if len(sa.OwnerReferences) != 0 { + t.Errorf("worker ServiceAccount has owners %v, want none", sa.OwnerReferences) + } +} + +// TestWorkerPoolWithServiceAccountSkipsDefault checks that a pool naming its own +// ServiceAccount does not get the default one created. +func TestWorkerPoolWithServiceAccountSkipsDefault(t *testing.T) { + t.Parallel() + ctx := t.Context() + // A namespace of its own, so no other pool creates the default account. + ns := &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "test-custom-worker-sa"}} + if err := client.IgnoreAlreadyExists(k8sClient.Create(ctx, ns)); err != nil { + t.Fatalf("create namespace: %v", err) + } + wp := makeWorkerPool("test-custom-sa", ns.Name, 1, "worker:v1") + wp.Spec.Template = &atev1alpha1.WorkerPoolPodTemplate{ServiceAccountName: ptr.To("substrate-worker")} + if err := k8sClient.Create(ctx, wp); err != nil { + t.Fatalf("create WorkerPool: %v", err) + } + deleteOnCleanup(t, wp) + + eventually(t, func(ctx context.Context) (bool, error) { + dep, err := getDeployment(ctx, wp) + if err != nil { + return false, nil + } + return dep.Spec.Template.Spec.ServiceAccountName == "substrate-worker", nil + }) + err := k8sClient.Get(ctx, types.NamespacedName{Name: installdefaults.WorkerServiceAccount, Namespace: ns.Name}, &corev1.ServiceAccount{}) + if !k8errors.IsNotFound(err) { + t.Errorf("get default worker ServiceAccount: %v, want NotFound", err) + } +} + // TestWorkerPoolPodTemplatePropagation verifies that template fields propagate // to the managed Deployment pod template. func TestWorkerPoolPodTemplatePropagation(t *testing.T) { @@ -547,7 +607,7 @@ func TestWorkerPoolPodTemplateClearAll(t *testing.T) { len(podSpec.Tolerations) == 1 && podSpec.Tolerations[0].Key == sandboxClassTaintKey && podSpec.PriorityClassName == "" && - podSpec.ServiceAccountName == "default" && + podSpec.ServiceAccountName == "ateworker" && (podSpec.Affinity == nil || podSpec.Affinity.NodeAffinity == nil) && len(container.Resources.Limits) == 0 && len(container.Resources.Requests) == 0, nil diff --git a/cmd/atecontroller/internal/workersync/syncer.go b/cmd/atecontroller/internal/workersync/syncer.go index 09eb6a3d3c..b8ed597c82 100644 --- a/cmd/atecontroller/internal/workersync/syncer.go +++ b/cmd/atecontroller/internal/workersync/syncer.go @@ -24,6 +24,7 @@ import ( "slices" "time" + "github.com/agent-substrate/substrate/internal/installdefaults" atev1alpha1 "github.com/agent-substrate/substrate/pkg/api/v1alpha1" "github.com/agent-substrate/substrate/pkg/proto/ateapipb" "google.golang.org/grpc/codes" @@ -333,6 +334,7 @@ func (s *WorkerPoolSyncer) createOrUpdateWorker(ctx context.Context, key workerK Ips: podIPs(pod), WorkerPodUid: string(pod.UID), NodeName: pod.Spec.NodeName, + WorkerSpiffeId: workerSPIFFEID(pod), SandboxClass: string(pool.Spec.DefaultSandboxClass()), Labels: pool.GetLabels(), Epoch: podEpoch(pod), @@ -477,6 +479,11 @@ func podIPs(pod *corev1.Pod) []string { return ips } +// workerSPIFFEID returns the SPIFFE ID the podidentity signer issues to pod. +func workerSPIFFEID(pod *corev1.Pod) string { + return installdefaults.SPIFFEID(pod.Namespace, pod.Spec.ServiceAccountName) +} + // markWorkerDraining transitions a worker to STATE_DRAINING so the scheduler // stops routing new actors to it while its pod is Terminating. DrainWorker is // idempotent, so a worker already draining costs nothing. If the worker is diff --git a/cmd/atecontroller/internal/workersync/syncer_test.go b/cmd/atecontroller/internal/workersync/syncer_test.go index 8d8b6d4494..ac142441ec 100644 --- a/cmd/atecontroller/internal/workersync/syncer_test.go +++ b/cmd/atecontroller/internal/workersync/syncer_test.go @@ -555,6 +555,24 @@ func TestSyncer_CreateWorker_DualStack(t *testing.T) { } } +// atenet-router requires atunnel's certificate to carry the SPIFFE ID the +// worker registers with. +func TestSyncer_CreateWorker_SPIFFEID(t *testing.T) { + ctx := context.Background() + ns, podName, poolName := "ns-syncer-sa", "worker-unit-sa", "pool-sa" + api := newFakeControl() + s, pods, _ := setupReconcileTest(t, api, workerPool(ns, poolName, "gvisor", nil)) + pod := workerPod(ns, podName, poolName, testPodUID, "10.0.0.1") + pod.Spec.ServiceAccountName = "substrate-worker" + key := seedPod(t, pods, pod) + + mustReconcile(t, ctx, s, key) + want := "spiffe://cluster.local/ns/ns-syncer-sa/sa/substrate-worker" + if got := api.get(testPodUID).GetWorkerSpiffeId(); got != want { + t.Fatalf("worker SPIFFE ID = %q, want %q", got, want) + } +} + // A concurrent write between the syncer's read and its update loses the version // precondition. The key requeues, and the retry must re-read rather than replay // its stale copy — which is what lets the concurrent change and the syncer's own diff --git a/cmd/atenet/internal/router/cmd.go b/cmd/atenet/internal/router/cmd.go index 7bd65c2d3d..bb412a545b 100644 --- a/cmd/atenet/internal/router/cmd.go +++ b/cmd/atenet/internal/router/cmd.go @@ -84,7 +84,6 @@ 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) and needs --upstream-connect=false.") 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", true, "Reach atunnel through mTLS CONNECT tunnels on port 8443; false reaches it over HTTPS on 443 instead (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 55c9cb00da..26143ccf22 100644 --- a/cmd/atenet/internal/router/config.go +++ b/cmd/atenet/internal/router/config.go @@ -171,9 +171,6 @@ type routerConfig struct { // validate that server. Empty UpstreamCredentialBundlePath disables upstream mTLS. UpstreamCredentialBundlePath string UpstreamTrustBundlePath string - // 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 diff --git a/cmd/atenet/internal/router/dataplane.go b/cmd/atenet/internal/router/dataplane.go index 7bae70d171..06aae8cde1 100644 --- a/cmd/atenet/internal/router/dataplane.go +++ b/cmd/atenet/internal/router/dataplane.go @@ -122,7 +122,7 @@ func (s *RouterServer) newEnvoyXdsServer(ctx context.Context, parkCfg ingress.Pa if s.cfg.IngressAuth.mode() == IngressAuthDeprecatedInsecure { 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.SetUpstreamTls(s.cfg.UpstreamCredentialBundlePath, s.cfg.UpstreamTrustBundlePath) xdsSrv.SetUpstreamConnect(s.cfg.UpstreamConnect) // The snapshot is a pure function of the configuration applied above, so it diff --git a/cmd/atenet/internal/router/ingress/ingress.go b/cmd/atenet/internal/router/ingress/ingress.go index 5294d90688..334395f134 100644 --- a/cmd/atenet/internal/router/ingress/ingress.go +++ b/cmd/atenet/internal/router/ingress/ingress.go @@ -54,6 +54,8 @@ const ( OriginalDstAddressKey = "local" // OriginalDstPortKey is the actor's target port. OriginalDstPortKey = "port" + // WorkerSPIFFEIDKey is the SPIFFE ID atunnel's certificate must carry. + WorkerSPIFFEIDKey = "worker_spiffe_id" ) // Handler routes ingress requests to the worker hosting their actor. @@ -121,16 +123,20 @@ func (h *Handler) HandleRequestHeaders(ctx context.Context, md *extproc.RequestM // The first IP is in the cluster's primary IP family. // TODO: choose the IP family that matches the dataplane's own address. + assignment := actor.GetStatus().GetWorkerAssignment() var workerIP string - if ips := actor.GetStatus().GetWorkerAssignment().GetWorkerPodIps(); len(ips) > 0 { + if ips := assignment.GetWorkerPodIps(); len(ips) > 0 { workerIP = ips[0] } + workerSPIFFEID := assignment.GetWorkerSpiffeId() slog.InfoContext(ctx, "ResumeActor result", slog.Any("actor", actorRef), slog.String("state", actor.GetStatus().GetState().String()), - slog.String("workerIP", workerIP)) + slog.String("workerIP", workerIP), + slog.String("workerSPIFFEID", workerSPIFFEID)) - if ip := net.ParseIP(workerIP); ip == nil { + // Without the SPIFFE ID, atunnel's identity can't be checked. + if ip := net.ParseIP(workerIP); ip == nil || workerSPIFFEID == "" { return res, extproc.NewReqError(envoy_type.StatusCode_InternalServerError, "actor %s routing failed", actorRef) } @@ -147,6 +153,7 @@ func (h *Handler) HandleRequestHeaders(ctx context.Context, md *extproc.RequestM OriginalDstMetadataKey: map[string]any{ OriginalDstAddressKey: targetAddr, OriginalDstPortKey: strconv.Itoa(targetPort), + WorkerSPIFFEIDKey: workerSPIFFEID, }, }) if err != nil { diff --git a/cmd/atenet/internal/router/ingress/ingress_test.go b/cmd/atenet/internal/router/ingress/ingress_test.go index fc4d5fe032..b53ac89412 100644 --- a/cmd/atenet/internal/router/ingress/ingress_test.go +++ b/cmd/atenet/internal/router/ingress/ingress_test.go @@ -66,7 +66,7 @@ func TestHandleRequestHeadersAcceptsMixedCaseRoutingHeaders(t *testing.T) { t.Errorf("atespace = %q, want %q", got, want) } return &ateapipb.ResumeActorResponse{Actor: &ateapipb.Actor{ - Status: &ateapipb.ActorStatus{WorkerAssignment: &ateapipb.WorkerAssignment{WorkerPodIps: []string{"10.0.0.52"}}}, + Status: &ateapipb.ActorStatus{WorkerAssignment: &ateapipb.WorkerAssignment{WorkerSpiffeId: testWorkerSPIFFEID, WorkerPodIps: []string{"10.0.0.52"}}}, }}, nil }, } @@ -158,7 +158,7 @@ func TestHandleRequestHeadersConnectAttributeOverridesInnerHeader(t *testing.T) resumeFn: func(_ context.Context, in *ateapipb.ResumeActorRequest, _ ...grpc.CallOption) (*ateapipb.ResumeActorResponse, error) { gotActorRef = in.GetActor().GetAtespace() + "/" + in.GetActor().GetName() return &ateapipb.ResumeActorResponse{Actor: &ateapipb.Actor{ - Status: &ateapipb.ActorStatus{WorkerAssignment: &ateapipb.WorkerAssignment{WorkerPodIps: []string{"10.0.0.52"}}}, + Status: &ateapipb.ActorStatus{WorkerAssignment: &ateapipb.WorkerAssignment{WorkerSpiffeId: testWorkerSPIFFEID, WorkerPodIps: []string{"10.0.0.52"}}}, }}, nil }, } @@ -207,6 +207,15 @@ func dynamicMetadataPort(dynamicMetadata *structpb.Struct) string { return dynamicMetadata.GetFields()[OriginalDstMetadataKey].GetStructValue().GetFields()[OriginalDstPortKey].GetStringValue() } +// testWorkerSPIFFEID is the SPIFFE ID the test assignments give their worker. +const testWorkerSPIFFEID = "spiffe://cluster.local/ns/workers/sa/substrate-worker" + +// dynamicMetadataWorkerSPIFFEID extracts the SPIFFE ID HandleRequestHeaders +// requires of atunnel. +func dynamicMetadataWorkerSPIFFEID(dynamicMetadata *structpb.Struct) string { + return dynamicMetadata.GetFields()[OriginalDstMetadataKey].GetStructValue().GetFields()[WorkerSPIFFEIDKey].GetStringValue() +} + func TestHandleRequestHeadersDoesNotLogSensitiveData(t *testing.T) { const testUUID = "123e4567-e89b-12d3-a456-426614174000" const secret = "do-not-log-me" @@ -219,7 +228,7 @@ func TestHandleRequestHeadersDoesNotLogSensitiveData(t *testing.T) { h := New(&mockClient{ resumeFn: func(ctx context.Context, in *ateapipb.ResumeActorRequest, opts ...grpc.CallOption) (*ateapipb.ResumeActorResponse, error) { - return &ateapipb.ResumeActorResponse{Actor: &ateapipb.Actor{Status: &ateapipb.ActorStatus{WorkerAssignment: &ateapipb.WorkerAssignment{WorkerPodIps: []string{"10.0.0.52"}}}}}, nil + return &ateapipb.ResumeActorResponse{Actor: &ateapipb.Actor{Status: &ateapipb.ActorStatus{WorkerAssignment: &ateapipb.WorkerAssignment{WorkerSpiffeId: testWorkerSPIFFEID, WorkerPodIps: []string{"10.0.0.52"}}}}}, nil }, }, ParkedRequestConfig{}, nil) @@ -325,7 +334,7 @@ func TestHandleRequestHeaders(t *testing.T) { authority: testUUID + ".team-a.actors.resources.substrate.ate.dev", resumeResp: &ateapipb.ResumeActorResponse{ Actor: &ateapipb.Actor{ - Status: &ateapipb.ActorStatus{WorkerAssignment: &ateapipb.WorkerAssignment{WorkerPodIps: []string{"invalid-ip"}}}, + Status: &ateapipb.ActorStatus{WorkerAssignment: &ateapipb.WorkerAssignment{WorkerSpiffeId: testWorkerSPIFFEID, WorkerPodIps: []string{"invalid-ip"}}}, }, }, expectErr: true, @@ -344,12 +353,24 @@ func TestHandleRequestHeaders(t *testing.T) { expectedErrStr: `actor team-a/123e4567-e89b-12d3-a456-426614174000 routing failed`, expectedStatus: envoy_type.StatusCode_InternalServerError, }, + { + name: "No worker SPIFFE ID from resume returns 500", + authority: testUUID + ".team-a.actors.resources.substrate.ate.dev", + resumeResp: &ateapipb.ResumeActorResponse{ + Actor: &ateapipb.Actor{ + Status: &ateapipb.ActorStatus{WorkerAssignment: &ateapipb.WorkerAssignment{WorkerPodIps: []string{"10.0.0.52"}}}, + }, + }, + expectErr: true, + expectedErrStr: `actor team-a/123e4567-e89b-12d3-a456-426614174000 routing failed`, + expectedStatus: envoy_type.StatusCode_InternalServerError, + }, { name: "successful resume ignores host and port for actor routing", authority: "127.0.0.1:44681", resumeResp: &ateapipb.ResumeActorResponse{ Actor: &ateapipb.Actor{ - Status: &ateapipb.ActorStatus{WorkerAssignment: &ateapipb.WorkerAssignment{WorkerPodIps: []string{"10.0.0.52"}}}, + Status: &ateapipb.ActorStatus{WorkerAssignment: &ateapipb.WorkerAssignment{WorkerSpiffeId: testWorkerSPIFFEID, WorkerPodIps: []string{"10.0.0.52"}}}, }, }, expectErr: false, @@ -361,7 +382,7 @@ func TestHandleRequestHeaders(t *testing.T) { authority: "127.0.0.1:44681", resumeResp: &ateapipb.ResumeActorResponse{ Actor: &ateapipb.Actor{ - Status: &ateapipb.ActorStatus{WorkerAssignment: &ateapipb.WorkerAssignment{WorkerPodIps: []string{"fd00::52", "10.0.0.52"}}}, + Status: &ateapipb.ActorStatus{WorkerAssignment: &ateapipb.WorkerAssignment{WorkerSpiffeId: testWorkerSPIFFEID, WorkerPodIps: []string{"fd00::52", "10.0.0.52"}}}, }, }, expectErr: false, @@ -453,6 +474,9 @@ func TestHandleRequestHeaders(t *testing.T) { if got := dynamicMetadataPort(res.DynamicMetadata); got != tc.expectedTargetPort { t.Errorf("dynamic metadata port = %q, want %q", got, tc.expectedTargetPort) } + if got, want := dynamicMetadataWorkerSPIFFEID(res.DynamicMetadata), testWorkerSPIFFEID; got != want { + t.Errorf("dynamic metadata worker SPIFFE ID = %q, want %q", got, want) + } }) } } @@ -470,7 +494,7 @@ func TestHandleRequestHeadersHandlesConnectMethod(t *testing.T) { clientMock := &mockClient{ resumeFn: func(ctx context.Context, in *ateapipb.ResumeActorRequest, opts ...grpc.CallOption) (*ateapipb.ResumeActorResponse, error) { - return &ateapipb.ResumeActorResponse{Actor: &ateapipb.Actor{Status: &ateapipb.ActorStatus{WorkerAssignment: &ateapipb.WorkerAssignment{WorkerPodIps: []string{"10.0.0.52"}}}}}, nil + return &ateapipb.ResumeActorResponse{Actor: &ateapipb.Actor{Status: &ateapipb.ActorStatus{WorkerAssignment: &ateapipb.WorkerAssignment{WorkerSpiffeId: testWorkerSPIFFEID, WorkerPodIps: []string{"10.0.0.52"}}}}}, nil }, } h := New(clientMock, ParkedRequestConfig{}, nil) @@ -503,7 +527,7 @@ func TestHandleRequestHeadersUsesRetainedConnectAuthorityForPort(t *testing.T) { clientMock := &mockClient{ resumeFn: func(context.Context, *ateapipb.ResumeActorRequest, ...grpc.CallOption) (*ateapipb.ResumeActorResponse, error) { return &ateapipb.ResumeActorResponse{Actor: &ateapipb.Actor{ - Status: &ateapipb.ActorStatus{WorkerAssignment: &ateapipb.WorkerAssignment{WorkerPodIps: []string{"10.0.0.52"}}}, + Status: &ateapipb.ActorStatus{WorkerAssignment: &ateapipb.WorkerAssignment{WorkerSpiffeId: testWorkerSPIFFEID, WorkerPodIps: []string{"10.0.0.52"}}}, }}, nil }, } @@ -655,7 +679,7 @@ func TestHandleRequestHeadersConnectRequiresOuterTargetActor(t *testing.T) { atomic.AddInt32(&resumeCalls, 1) gotActorRef = in.GetActor().GetAtespace() + "/" + in.GetActor().GetName() return &ateapipb.ResumeActorResponse{Actor: &ateapipb.Actor{ - Status: &ateapipb.ActorStatus{WorkerAssignment: &ateapipb.WorkerAssignment{WorkerPodIps: []string{"10.0.0.52"}}}, + Status: &ateapipb.ActorStatus{WorkerAssignment: &ateapipb.WorkerAssignment{WorkerSpiffeId: testWorkerSPIFFEID, WorkerPodIps: []string{"10.0.0.52"}}}, }}, nil }, } @@ -720,7 +744,7 @@ func TestHandleRequestHeaders_FullLotServesRunningActor(t *testing.T) { Actor: &ateapipb.Actor{ Status: &ateapipb.ActorStatus{ State: ateapipb.ActorState_ACTOR_STATE_RUNNING, - WorkerAssignment: &ateapipb.WorkerAssignment{WorkerPodIps: []string{"10.0.0.1"}}, + WorkerAssignment: &ateapipb.WorkerAssignment{WorkerSpiffeId: testWorkerSPIFFEID, WorkerPodIps: []string{"10.0.0.1"}}, }, }, }, nil diff --git a/cmd/atenet/internal/router/xds.go b/cmd/atenet/internal/router/xds.go index 6f0d4b4cd4..2d06f2c0b2 100644 --- a/cmd/atenet/internal/router/xds.go +++ b/cmd/atenet/internal/router/xds.go @@ -35,6 +35,7 @@ import ( "google.golang.org/protobuf/types/known/wrapperspb" "github.com/agent-substrate/substrate/internal/atenet" + "github.com/agent-substrate/substrate/internal/installdefaults" accesslogv3 "github.com/envoyproxy/go-control-plane/envoy/config/accesslog/v3" clusterv3 "github.com/envoyproxy/go-control-plane/envoy/config/cluster/v3" @@ -202,6 +203,14 @@ const ( tunnelPortFilterStateKey = "dev.ate.tunnel.port" ) +// upstreamSANsFilterStateKey is Envoy's list of SANs the upstream certificate +// must carry, on top of the cluster's matchers. It does not split the tunnel pool. +const upstreamSANsFilterStateKey = "envoy.network.upstream_subject_alt_names" + +// trustDomainSPIFFEPrefix starts every SPIFFE ID in the pod certificates' trust +// domain. +const trustDomainSPIFFEPrefix = "spiffe://" + installdefaults.AteletTrustDomain + "/" + // tunnelCircuitBreakerLimit lifts Envoy's default of 1024: there is a tunnel // per active actor and port, and idle ones count too. const tunnelCircuitBreakerLimit = 65536 @@ -233,12 +242,6 @@ type XdsServer struct { // client cert and validates the atunnel server against upstreamTrustBundlePath. upstreamCredentialBundlePath string upstreamTrustBundlePath string - // upstreamSpiffePrefix, when set, makes the upstream validator accept the - // atunnel server cert by matching its SPIFFE URI SAN against this prefix - // (trust-domain match) instead of the actor's ephemeral pod IP. The atunnel - // 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 @@ -400,12 +403,16 @@ const otlpDefaultPort = "4317" // (cert+key concatenated) presented to the actor's atunnel ingress server; // trustBundlePath is the CA bundle used to validate that server. Empty // credentialBundlePath leaves the upstream as plaintext. -func (x *XdsServer) SetUpstreamTls(credentialBundlePath, trustBundlePath, spiffePrefix string) { +func (x *XdsServer) SetUpstreamTls(credentialBundlePath, trustBundlePath string) { x.mu.Lock() defer x.mu.Unlock() x.upstreamCredentialBundlePath = credentialBundlePath x.upstreamTrustBundlePath = trustBundlePath - x.upstreamSpiffePrefix = spiffePrefix +} + +// upstreamValidated reports whether the router verifies atunnel's certificate. +func (x *XdsServer) upstreamValidated() bool { + return x.upstreamCredentialBundlePath != "" && x.upstreamTrustBundlePath != "" } // SetUpstreamConnect makes actor traffic reach atunnel through CONNECT tunnels @@ -743,8 +750,8 @@ func (x *XdsServer) buildOtlpCollectorCluster() *clusterv3.Cluster { // for the ORIGINAL_DST actor cluster, or nil when upstream mTLS is not // configured. The router presents its podidentity credential bundle as the // client cert and validates the atunnel ingress server against the trust -// bundle. Validation is by the SPIFFE URI SAN prefix (see upstreamSpiffePrefix) -// rather than the dialed pod IP. +// bundle. The cluster accepts any SPIFFE ID in the trust domain; +// workerIdentityFilterStateFilter narrows each connection to the routed worker. // // Both the client cert and the trust bundle are referenced as SDS secrets // served by this same control plane over ADS, never inlined as file @@ -769,20 +776,15 @@ func (x *XdsServer) buildUpstreamTransportSocket() *corev3.TransportSocket { // tracks ClusterTrustBundle rotation, while the SAN matcher stays // inline in the default context. Envoy merges the two, which is why // this is a combined rather than a plain SDS validation context. - defaultCtx := &tlsv3.CertificateValidationContext{} - // Validate the atunnel server by its SPIFFE URI SAN (trust-domain - // prefix) rather than the dialed pod IP. Without this, Envoy checks the - // cert SAN against the ephemeral pod IP, which the SPIFFE-only cert - // never matches. - if x.upstreamSpiffePrefix != "" { - defaultCtx.MatchTypedSubjectAltNames = []*tlsv3.SubjectAltNameMatcher{ + defaultCtx := &tlsv3.CertificateValidationContext{ + MatchTypedSubjectAltNames: []*tlsv3.SubjectAltNameMatcher{ { SanType: tlsv3.SubjectAltNameMatcher_URI, Matcher: &matcherv3.StringMatcher{ - MatchPattern: &matcherv3.StringMatcher_Prefix{Prefix: x.upstreamSpiffePrefix}, + MatchPattern: &matcherv3.StringMatcher_Prefix{Prefix: trustDomainSPIFFEPrefix}, }, }, - } + }, } commonTls.ValidationContextType = &tlsv3.CommonTlsContext_CombinedValidationContext{ CombinedValidationContext: &tlsv3.CommonTlsContext_CombinedCertificateValidationContext{ @@ -1204,6 +1206,17 @@ func actorTunnelFilterStateFilter() *hcmv3.HttpFilter { ) } +// workerIdentityFilterStateFilter requires atunnel's certificate to carry the +// worker SPIFFE ID ext_proc resolved. Sharing it once with the upstream also +// covers the CONNECT tunnel's connection; missing metadata becomes "-", which +// never matches. +func workerIdentityFilterStateFilter() *hcmv3.HttpFilter { + workerID := "%DYNAMIC_METADATA(" + ingress.OriginalDstMetadataKey + ":" + ingress.WorkerSPIFFEIDKey + ")%" + return setFilterStateHTTPFilter( + filterStateValue(upstreamSANsFilterStateKey, "", workerID, setfilterstatecommonv3.FilterStateValue_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. @@ -1399,6 +1412,9 @@ func (x *XdsServer) buildHcm(statPrefix string, captureActorRouting bool) *anypb if x.upstreamConnect { httpFilters = append(httpFilters, actorTunnelFilterStateFilter()) } + if x.upstreamValidated() { + httpFilters = append(httpFilters, workerIdentityFilterStateFilter()) + } httpFilters = append(httpFilters, &hcmv3.HttpFilter{ Name: "envoy.filters.http.router", ConfigType: &hcmv3.HttpFilter_TypedConfig{ diff --git a/cmd/atenet/internal/router/xds_test.go b/cmd/atenet/internal/router/xds_test.go index 0718b53c30..8a62ff9b9e 100644 --- a/cmd/atenet/internal/router/xds_test.go +++ b/cmd/atenet/internal/router/xds_test.go @@ -39,6 +39,7 @@ import ( listenerv3 "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3" routev3 "github.com/envoyproxy/go-control-plane/envoy/config/route/v3" streamaccesslogv3 "github.com/envoyproxy/go-control-plane/envoy/extensions/access_loggers/stream/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" tlsv3 "github.com/envoyproxy/go-control-plane/envoy/extensions/transport_sockets/tls/v3" @@ -800,13 +801,12 @@ func rotateProjectedVolume(t *testing.T, dir, newTsDir, oldTsDir string, bundle // likewise never picked up. func TestUpstreamTransportSocket_DeliversCertsOverSds(t *testing.T) { const ( - credPath = "/run/podidentity.podcert.ate.dev/credential-bundle.pem" - trustPath = "/run/podidentity.podcert.ate.dev/trust-bundle.pem" - spiffePrefix = "spiffe://cluster.local/" + credPath = "/run/podidentity.podcert.ate.dev/credential-bundle.pem" + trustPath = "/run/podidentity.podcert.ate.dev/trust-bundle.pem" ) server := NewXdsServer(18000) - server.SetUpstreamTls(credPath, trustPath, spiffePrefix) + server.SetUpstreamTls(credPath, trustPath) ts := server.buildUpstreamTransportSocket() if ts == nil { @@ -849,10 +849,8 @@ func TestUpstreamTransportSocket_DeliversCertsOverSds(t *testing.T) { t.Error("Expected the trust bundle SDS config to use the ADS config source") } - // The SPIFFE SAN matcher has to survive the move into the combined - // context's default half. Losing it silently downgrades validation to a - // SAN check against the dialed pod IP, which the SPIFFE-only atunnel - // server cert never carries. + // The trust-domain matcher has to survive the move into the combined + // context: it still holds if the per-request filter is ever skipped. sans := combined.GetDefaultValidationContext().GetMatchTypedSubjectAltNames() if len(sans) != 1 { t.Fatalf("Expected 1 SAN matcher, got %d", len(sans)) @@ -860,8 +858,80 @@ func TestUpstreamTransportSocket_DeliversCertsOverSds(t *testing.T) { if got := sans[0].GetSanType(); got != tlsv3.SubjectAltNameMatcher_URI { t.Errorf("Expected a URI SAN matcher, got %v", got) } - if got := sans[0].GetMatcher().GetPrefix(); got != spiffePrefix { - t.Errorf("Expected SAN prefix '%s', got '%s'", spiffePrefix, got) + if got, want := sans[0].GetMatcher().GetPrefix(), "spiffe://cluster.local/"; got != want { + t.Errorf("Expected SAN prefix '%s', got '%s'", want, got) + } +} + +// TestXdsServer_WorkerIdentityFilter checks every ext_proc HCM requires the +// routed worker's identity from atunnel, on both the HTTPS and CONNECT paths. +func TestXdsServer_WorkerIdentityFilter(t *testing.T) { + hcmFilters := func(t *testing.T, l *listenerv3.Listener) []*hcmv3.HttpFilter { + t.Helper() + hcm := &hcmv3.HttpConnectionManager{} + if err := l.GetFilterChains()[0].GetFilters()[0].GetTypedConfig().UnmarshalTo(hcm); err != nil { + t.Fatalf("listener %s: unmarshal HCM: %v", l.GetName(), err) + } + return hcm.GetHttpFilters() + } + setsUpstreamSANs := func(t *testing.T, f *hcmv3.HttpFilter) *setfilterstatecommonv3.FilterStateValue { + t.Helper() + if f.GetName() != "envoy.filters.http.set_filter_state" { + return nil + } + config := &setfilterstatev3.Config{} + if err := f.GetTypedConfig().UnmarshalTo(config); err != nil { + t.Fatalf("unmarshal set_filter_state: %v", err) + } + for _, v := range config.GetOnRequestHeaders() { + if v.GetObjectKey() == "envoy.network.upstream_subject_alt_names" { + return v + } + } + return nil + } + + for _, upstreamConnect := range []bool{false, true} { + server := NewXdsServer(18000) + server.SetConfig(8085, 50053, "127.0.0.1") + server.SetConnectPorts(8081, 0) + server.SetUpstreamTls("/run/bundle.pem", "/run/trust.pem") + server.SetUpstreamConnect(upstreamConnect) + listeners := snapshotListeners(t, server) + for _, name := range []string{IngressHTTPListener, MainInternalName} { + filters := hcmFilters(t, listeners[name]) + n := len(filters) + if n < 2 || filters[n-1].GetName() != "envoy.filters.http.router" { + t.Fatalf("upstreamConnect=%v listener %s: filters do not end with the router", upstreamConnect, name) + } + // Last before the router, so ext_proc has written the metadata. + v := setsUpstreamSANs(t, filters[n-2]) + if v == nil { + t.Fatalf("upstreamConnect=%v listener %s: filter before the router does not set the upstream SANs", upstreamConnect, name) + } + if got, want := v.GetFormatString().GetTextFormatSource().GetInlineString(), "%DYNAMIC_METADATA(envoy.filters.listener.original_dst:worker_spiffe_id)%"; got != want { + t.Errorf("upstreamConnect=%v listener %s: upstream SANs = %q, want %q", upstreamConnect, name, got, want) + } + // ONCE carries it to the tunnel's own connection to atunnel. + if got := v.GetSharedWithUpstream(); got != setfilterstatecommonv3.FilterStateValue_ONCE { + t.Errorf("upstreamConnect=%v listener %s: shared with upstream = %v, want ONCE", upstreamConnect, name, got) + } + // A factory key would make set_filter_state build a plain string + // that Envoy never reads as the SAN list. + if got := v.GetFactoryKey(); got != "" { + t.Errorf("upstreamConnect=%v listener %s: factory key = %q, want none", upstreamConnect, name, got) + } + } + } + + // Without a trust bundle nothing is verified, so there is nothing to narrow. + server := NewXdsServer(18000) + server.SetConfig(8085, 50053, "127.0.0.1") + server.SetUpstreamTls("/run/bundle.pem", "") + for _, f := range hcmFilters(t, snapshotListeners(t, server)[IngressHTTPListener]) { + if setsUpstreamSANs(t, f) != nil { + t.Error("upstream SANs set although atunnel's certificate is not verified") + } } } @@ -878,7 +948,7 @@ func TestXdsServer_UpdateSnapshot_UpstreamSecrets(t *testing.T) { t.Run("PublishedWhenConfigured", func(t *testing.T) { server := NewXdsServer(18000) server.SetConfig(8085, 50053, "127.0.0.1") - server.SetUpstreamTls(credPath, trustPath, "spiffe://cluster.local/") + server.SetUpstreamTls(credPath, trustPath) secrets := snapshotSecrets(t, server) if len(secrets) != 2 { @@ -925,7 +995,7 @@ func TestXdsServer_UpdateSnapshot_UpstreamSecrets(t *testing.T) { t.Run("TrustSecretOmittedWithoutTrustBundle", func(t *testing.T) { server := NewXdsServer(18000) server.SetConfig(8085, 50053, "127.0.0.1") - server.SetUpstreamTls(credPath, "", "") + server.SetUpstreamTls(credPath, "") secrets := snapshotSecrets(t, server) if len(secrets) != 1 { @@ -953,7 +1023,7 @@ func TestUpstreamCertSecret_ProjectedVolumeRotation(t *testing.T) { writeProjectedVolume(t, dir, tsDirA, bundleA) server := NewXdsServer(18000) - server.SetUpstreamTls(credPath, "", "") + server.SetUpstreamTls(credPath, "") tlsCert := server.buildUpstreamCertSecret().GetTlsCertificate() chainPath := tlsCert.GetCertificateChain().GetFilename() @@ -1182,7 +1252,7 @@ func TestXdsServer_ActorClusterProtocolOptions(t *testing.T) { t.Errorf("legacy plaintext actor cluster has protocol options %v, want none (implicit HTTP/1.1)", opts) } - x.SetUpstreamTls("/run/bundle.pem", "/run/trust.pem", "spiffe://ate.dev/") + x.SetUpstreamTls("/run/bundle.pem", "/run/trust.pem") cluster := x.buildOriginalDstCluster() ts := cluster.GetTransportSocket() if ts == nil { diff --git a/cmd/atenet/internal/router/xds_upstream_connect_test.go b/cmd/atenet/internal/router/xds_upstream_connect_test.go index 9647989f67..25f3796d86 100644 --- a/cmd/atenet/internal/router/xds_upstream_connect_test.go +++ b/cmd/atenet/internal/router/xds_upstream_connect_test.go @@ -46,7 +46,7 @@ func upstreamConnectSnapshot(t *testing.T) *cachev3.Snapshot { 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.SetUpstreamTls("/run/bundle.pem", "/run/trust.pem") server.SetUpstreamConnect(true) if err := server.UpdateSnapshot(); err != nil { t.Fatalf("UpdateSnapshot failed: %v", err) @@ -173,7 +173,8 @@ func TestXdsServer_UpstreamConnectResources(t *testing.T) { } // The tunnel filter copies ext_proc's decision, and the router picks a - // tunnel by it. + // tunnel by it. The worker identity filter follows it (see + // TestXdsServer_WorkerIdentityFilter). for _, name := range []string{IngressHTTPListener, MainInternalName} { hcm := &hcmv3.HttpConnectionManager{} if err := listeners[name].(*listenerv3.Listener).GetFilterChains()[0].GetFilters()[0].GetTypedConfig().UnmarshalTo(hcm); err != nil { @@ -185,11 +186,11 @@ func TestXdsServer_UpstreamConnectResources(t *testing.T) { 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) + if n < 4 || names[n-4] != httpExtProcFilterName || names[n-3] != "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, the tunnel set_filter_state, the worker identity filter, router", name, names) } config := &setfilterstatev3.Config{} - if err := filters[n-2].GetTypedConfig().UnmarshalTo(config); err != nil { + if err := filters[n-3].GetTypedConfig().UnmarshalTo(config); err != nil { t.Fatalf("listener %s: unmarshal set_filter_state: %v", name, err) } if got := config.GetOnRequestHeaders()[0].GetObjectKey(); got != tunnelKeyFilterStateKey { @@ -387,7 +388,7 @@ func TestXdsServer_ActorTunnelClusters(t *testing.T) { } x := NewXdsServer(0) - x.SetUpstreamTls("/run/bundle.pem", "/run/trust.pem", "spiffe://cluster.local/") + x.SetUpstreamTls("/run/bundle.pem", "/run/trust.pem") c := x.buildActorConnectCluster() if c.GetType() != clusterv3.Cluster_ORIGINAL_DST { t.Errorf("actor_connect type = %v, want ORIGINAL_DST", c.GetType()) diff --git a/docs/api-guide.md b/docs/api-guide.md index 8958b99338..e44c1e7c4f 100644 --- a/docs/api-guide.md +++ b/docs/api-guide.md @@ -26,7 +26,7 @@ The `WorkerPool` defines the pool of physical "warm" compute capacity. It manage | `priorityClassName` | `string` | `spec.priorityClassName` | | `nodeAffinity` | `NodeAffinity` | `spec.affinity.nodeAffinity` | | `resources` | `ResourceRequirements` | `spec.containers[].resources` | -| `serviceAccountName` | `string` | `spec.serviceAccountName` | +| `serviceAccountName` | `string` | `spec.serviceAccountName`; defaults to `ateworker`, which the controller creates | Keys in `ate.dev/` and its subdomains (for example, `policy.ate.dev/`) are reserved for controllers and cannot be set in `template.labels` or diff --git a/docs/threat-model.md b/docs/threat-model.md index a8da7edf18..65db8822d0 100644 --- a/docs/threat-model.md +++ b/docs/threat-model.md @@ -40,7 +40,7 @@ Substrate is an early, fast moving product. It is full of debate and subject to * **Actor:** The core compute primitive, gets scheduled to/from worker via Run for cold start and Resume for snapshot resume. * **Actor Network:** `ateworker` creates a private point-to-point veth network for the active Actor inside a worker Pod. The Actor is not served directly from the worker Pod's port 80; ingress enters through `atunnel`'s authenticated listeners: mTLS `CONNECT` on port 8443, which the router uses by default, and HTTPS on port 443. * **Actor Routing:** Requests sent to `atenet-router` identify the target Actor with `ate-target-actor: /`. Host and authority values are application metadata and do not select the Actor. -* **atenet-router:** Substrate runs Envoy with an `ext_proc` external processor to handle Actor ingress. With `--ingress-auth-mode=static-mtls`, Envoy accepts connections only on its TLS listeners and only from clients presenting a certificate that chains to the configured client CA bundle and carries one of a static list of SPIFFE IDs; any listed client may reach any Actor. The default, `deprecated-insecure`, rejects a TLS client whose certificate fails those checks but accepts one that presents none, and also serves plaintext listeners that accept any client. The ext_proc reads and validates `ate-target-actor`, calls the Substrate API to resume the Actor and obtain its current worker assignment, and selects that worker as a dynamic backend. It overwrites the routing header before forwarding. The router then connects with mTLS to `atunnel`; `atunnel` validates the router identity and authorizes the header against the Actor currently assigned to that worker. +* **atenet-router:** Substrate runs Envoy with an `ext_proc` external processor to handle Actor ingress. With `--ingress-auth-mode=static-mtls`, Envoy accepts connections only on its TLS listeners and only from clients presenting a certificate that chains to the configured client CA bundle and carries one of a static list of SPIFFE IDs; any listed client may reach any Actor. The default, `deprecated-insecure`, rejects a TLS client whose certificate fails those checks but accepts one that presents none, and also serves plaintext listeners that accept any client. The ext_proc reads and validates `ate-target-actor`, calls the Substrate API to resume the Actor and obtain its current worker assignment, and selects that worker as a dynamic backend. It overwrites the routing header before forwarding. The router then connects with mTLS to `atunnel` and accepts it only with the SPIFFE ID of the assigned worker pod's namespace and ServiceAccount, so another pod running as that ServiceAccount in that namespace can still pose as the worker; `atunnel` validates the router identity and authorizes the header against the Actor currently assigned to that worker. * **Object Storage:** Used to store actor snapshots. * **Filesystem support:** Container local filesystem is saved in snapshots, future integrations likely to include networked storage. * **Substrate Database:** PostgreSQL. On GCP, PostgreSQL may be Cloud SQL reached through the Cloud SQL Auth Proxy sidecar (see [tools/setup-gcp/cloud-sql.md](../tools/setup-gcp/cloud-sql.md)); that path adds two egress flows from ate-api-server: HTTPS to `sqladmin.googleapis.com:443` and the proxy tunnel to the instance IP on port 3307, both authenticated via IAM and encrypted. diff --git a/internal/installdefaults/installdefaults.go b/internal/installdefaults/installdefaults.go index d0e6980c4e..3147aad5ef 100644 --- a/internal/installdefaults/installdefaults.go +++ b/internal/installdefaults/installdefaults.go @@ -51,6 +51,10 @@ const ( EgressServiceAccount = "atenet-egress" ControllerServiceAccount = "ate-controller" + // WorkerServiceAccount is what worker pods run as when their WorkerPool + // names none; atecontroller creates it in the pool's namespace. + WorkerServiceAccount = "ateworker" + // PodNamespaceEnv is the conventional env var name for the namespace // a pod is running in, exposed via Kubernetes' downward API. PodNamespaceEnv = "POD_NAMESPACE" diff --git a/manifests/ate-install/atenet-router.yaml b/manifests/ate-install/atenet-router.yaml index 7eb2de55ea..852a26245d 100644 --- a/manifests/ate-install/atenet-router.yaml +++ b/manifests/ate-install/atenet-router.yaml @@ -362,7 +362,8 @@ spec: # podidentity credential + trust bundles for actor-facing (upstream) # mTLS. Envoy presents this credential bundle as its client cert when # opening CONNECT tunnels to the worker's atunnel on :8443, and - # validates that server against the trust bundle. + # validates that server against the trust bundle and the SPIFFE ID of + # the worker pod's namespace and ServiceAccount. # The issued SPIFFE identity is # spiffe://cluster.local/ns/ate-system/sa/atenet-router, which atunnel's # --atunnel-client-identity allows. diff --git a/manifests/ate-install/generated/ate.dev_workerpools.yaml b/manifests/ate-install/generated/ate.dev_workerpools.yaml index 7d07f9c432..a29c928aab 100644 --- a/manifests/ate-install/generated/ate.dev_workerpools.yaml +++ b/manifests/ate-install/generated/ate.dev_workerpools.yaml @@ -435,8 +435,8 @@ spec: serviceAccountName: description: |- ServiceAccountName for the worker pods. The ServiceAccount must exist - in the WorkerPool's namespace. When omitted, the pods use the - namespace's default ServiceAccount. + in the WorkerPool's namespace. When omitted, the pods run as the + "ateworker" ServiceAccount, which the controller creates. minLength: 1 type: string tolerations: diff --git a/manifests/ate-install/generated/role.yaml b/manifests/ate-install/generated/role.yaml index 7614dac1f4..3298e88aa2 100644 --- a/manifests/ate-install/generated/role.yaml +++ b/manifests/ate-install/generated/role.yaml @@ -35,6 +35,13 @@ rules: - get - list - watch +- apiGroups: + - "" + resources: + - serviceaccounts + verbs: + - create + - patch - apiGroups: - apps resources: diff --git a/pkg/api/v1alpha1/workerpool_types.go b/pkg/api/v1alpha1/workerpool_types.go index cb2e450510..20782b0b72 100644 --- a/pkg/api/v1alpha1/workerpool_types.go +++ b/pkg/api/v1alpha1/workerpool_types.go @@ -77,8 +77,8 @@ type WorkerPoolPodTemplate struct { Resources *corev1.ResourceRequirements `json:"resources,omitempty"` // ServiceAccountName for the worker pods. The ServiceAccount must exist - // in the WorkerPool's namespace. When omitted, the pods use the - // namespace's default ServiceAccount. + // in the WorkerPool's namespace. When omitted, the pods run as the + // "ateworker" ServiceAccount, which the controller creates. // // +optional // +kubebuilder:validation:MinLength=1 diff --git a/pkg/proto/ateapipb/ateapi.pb.go b/pkg/proto/ateapipb/ateapi.pb.go index ad88e8685f..e85d01b84e 100644 --- a/pkg/proto/ateapipb/ateapi.pb.go +++ b/pkg/proto/ateapipb/ateapi.pb.go @@ -2178,6 +2178,13 @@ type WorkerAssignment struct { // // +k8s:optional SandboxRuntime *SandboxRuntime `protobuf:"bytes,9,opt,name=sandbox_runtime,json=sandboxRuntime,proto3" json:"sandbox_runtime,omitempty"` + // worker_spiffe_id is the SPIFFE ID worker_pod's certificate carries, copied + // from Worker.worker_spiffe_id. + // + // +k8s:optional + // +k8s:maxLength=2048 + // +k8s:customValidation # a SPIFFE ID + WorkerSpiffeId string `protobuf:"bytes,10,opt,name=worker_spiffe_id,json=workerSpiffeId,proto3" json:"worker_spiffe_id,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -2275,6 +2282,13 @@ func (x *WorkerAssignment) GetSandboxRuntime() *SandboxRuntime { return nil } +func (x *WorkerAssignment) GetWorkerSpiffeId() string { + if x != nil { + return x.WorkerSpiffeId + } + return "" +} + // TagStatus is the server-owned state of a Tag. // The tag is fixed to one external snapshot once its creation completes, so // the rest is immutable provenance. @@ -7191,6 +7205,14 @@ type Worker struct { // +k8s:customValidation # until `format=k8s-ip` is supported // +k8s:immutable Ips []string `protobuf:"bytes,7,rep,name=ips,proto3" json:"ips,omitempty"` + // worker_spiffe_id is the SPIFFE ID the worker pod's certificate carries. + // atenet-router requires it on the certificate of the worker's atunnel. + // + // +k8s:optional + // +k8s:maxLength=2048 + // +k8s:customValidation # a SPIFFE ID + // +k8s:immutable + WorkerSpiffeId string `protobuf:"bytes,13,opt,name=worker_spiffe_id,json=workerSpiffeId,proto3" json:"worker_spiffe_id,omitempty"` // sandbox_class mirrors the WorkerPool's default sandbox class // // +k8s:optional @@ -7311,6 +7333,13 @@ func (x *Worker) GetIps() []string { return nil } +func (x *Worker) GetWorkerSpiffeId() string { + if x != nil { + return x.WorkerSpiffeId + } + return "" +} + func (x *Worker) GetSandboxClass() string { if x != nil { return x.SandboxClass @@ -9447,7 +9476,7 @@ const file_ateapi_proto_rawDesc = "" + "ActorCrash\x12\x18\n" + "\amessage\x18\x01 \x01(\tR\amessage\x129\n" + "\n" + - "crash_time\x18\x02 \x01(\v2\x1a.google.protobuf.TimestampR\tcrashTime\"\xf5\x02\n" + + "crash_time\x18\x02 \x01(\v2\x1a.google.protobuf.TimestampR\tcrashTime\"\x9f\x03\n" + "\x10WorkerAssignment\x12)\n" + "\x06worker\x18\x06 \x01(\v2\x11.ateapi.ObjectRefR\x06worker\x12)\n" + "\x10worker_namespace\x18\x01 \x01(\tR\x0fworkerNamespace\x12\x1f\n" + @@ -9459,7 +9488,9 @@ const file_ateapi_proto_rawDesc = "" + "\x0eworker_pod_ips\x18\x05 \x03(\tR\fworkerPodIps\x12\x1b\n" + "\tnode_name\x18\a \x01(\tR\bnodeName\x12!\n" + "\fworker_epoch\x18\b \x01(\x03R\vworkerEpoch\x12?\n" + - "\x0fsandbox_runtime\x18\t \x01(\v2\x16.ateapi.SandboxRuntimeR\x0esandboxRuntime\"g\n" + + "\x0fsandbox_runtime\x18\t \x01(\v2\x16.ateapi.SandboxRuntimeR\x0esandboxRuntime\x12(\n" + + "\x10worker_spiffe_id\x18\n" + + " \x01(\tR\x0eworkerSpiffeId\"g\n" + "\tTagStatus\x12,\n" + "\bsnapshot\x18\x01 \x01(\v2\x10.ateapi.SnapshotR\bsnapshot\x12,\n" + "\x12actor_template_uid\x18\x02 \x01(\tR\x10actorTemplateUid\"\xd2\x01\n" + @@ -9699,7 +9730,7 @@ const file_ateapi_proto_rawDesc = "" + "\aoptions\x18\x02 \x01(\v2\x13.ateapi.ListOptionsR\aoptions\"c\n" + "\x12ListActorsResponse\x12%\n" + "\x06actors\x18\x01 \x03(\v2\r.ateapi.ActorR\x06actors\x12&\n" + - "\x0fnext_page_token\x18\x02 \x01(\tR\rnextPageToken\"\xe7\x03\n" + + "\x0fnext_page_token\x18\x02 \x01(\tR\rnextPageToken\"\x91\x04\n" + "\x06Worker\x124\n" + "\bmetadata\x18\x01 \x01(\v2\x18.ateapi.ResourceMetadataR\bmetadata\x12)\n" + "\x10worker_namespace\x18\x02 \x01(\tR\x0fworkerNamespace\x12\x1f\n" + @@ -9709,7 +9740,8 @@ const file_ateapi_proto_rawDesc = "" + "worker_pod\x18\x04 \x01(\tR\tworkerPod\x12$\n" + "\x0eworker_pod_uid\x18\x05 \x01(\tR\fworkerPodUid\x12\x1b\n" + "\tnode_name\x18\x06 \x01(\tR\bnodeName\x12\x10\n" + - "\x03ips\x18\a \x03(\tR\x03ips\x12#\n" + + "\x03ips\x18\a \x03(\tR\x03ips\x12(\n" + + "\x10worker_spiffe_id\x18\r \x01(\tR\x0eworkerSpiffeId\x12#\n" + "\rsandbox_class\x18\b \x01(\tR\fsandboxClass\x122\n" + "\x06labels\x18\t \x03(\v2\x1a.ateapi.Worker.LabelsEntryR\x06labels\x12\x14\n" + "\x05epoch\x18\f \x01(\x03R\x05epoch\x12,\n" + diff --git a/pkg/proto/ateapipb/ateapi.proto b/pkg/proto/ateapipb/ateapi.proto index 8e7c492f09..75534b2968 100644 --- a/pkg/proto/ateapipb/ateapi.proto +++ b/pkg/proto/ateapipb/ateapi.proto @@ -1007,6 +1007,14 @@ message WorkerAssignment { // // +k8s:optional SandboxRuntime sandbox_runtime = 9; + + // worker_spiffe_id is the SPIFFE ID worker_pod's certificate carries, copied + // from Worker.worker_spiffe_id. + // + // +k8s:optional + // +k8s:maxLength=2048 + // +k8s:customValidation # a SPIFFE ID + string worker_spiffe_id = 10; } // TagStatus is the server-owned state of a Tag. @@ -2281,6 +2289,14 @@ message Worker { // +k8s:customValidation # until `format=k8s-ip` is supported // +k8s:immutable repeated string ips = 7; + // worker_spiffe_id is the SPIFFE ID the worker pod's certificate carries. + // atenet-router requires it on the certificate of the worker's atunnel. + // + // +k8s:optional + // +k8s:maxLength=2048 + // +k8s:customValidation # a SPIFFE ID + // +k8s:immutable + string worker_spiffe_id = 13; // sandbox_class mirrors the WorkerPool's default sandbox class // From 84acd9a528c253748b2e137514575b69cc2423c0 Mon Sep 17 00:00:00 2001 From: Yuan Gao Date: Thu, 15 Oct 2026 22:42:35 +0000 Subject: [PATCH 15/31] atenet-egress: log every failed request, sample successes at 1% (#2693) Fixes #2685 The MITM and cleartext chains write one JSON access-log line per actor request. At 2,500 req/s on one replica that is 1.2 MB/s, and with the kubelet's default rotation only about 45 s of history survives, so a failure from a minute ago is already gone. Both chains now log a request only when its status is 400 or above, when it carries a response flag, or as a 1% sample of the rest. At the same rate the log drops to 25 lines/s and the rotation window is over an hour, with every failure still recorded. The threshold and the sample rate are runtime keys, `access_log.min_status` and `access_log.sample`, so an operator can restore full logging at the admin port without a rollout. The log no longer counts successful requests per host; `docs/metrics/substrate.yaml` records that. Testing: the manifest test compares each per-request log's filter against the expected YAML and asserts no other access log carries one. `go test -race ./cmd/atenet/...` and `hack/verify/*.sh` pass. Prepared with help from an AI coding agent; reviewed and verified by the author. --- .../internal/router/egressmanifest_test.go | 80 +++++++++++++++++++ docs/metrics/substrate.yaml | 8 ++ manifests/ate-install/atenet-egress.yaml | 48 +++++++++++ 3 files changed, 136 insertions(+) diff --git a/cmd/atenet/internal/router/egressmanifest_test.go b/cmd/atenet/internal/router/egressmanifest_test.go index 136b1bbe1c..b55796e5ae 100644 --- a/cmd/atenet/internal/router/egressmanifest_test.go +++ b/cmd/atenet/internal/router/egressmanifest_test.go @@ -16,10 +16,12 @@ package router import ( "os" + "slices" "strings" "testing" "time" + "github.com/google/go-cmp/cmp" "sigs.k8s.io/yaml" ) @@ -151,3 +153,81 @@ func envoyConfigFrom(t *testing.T, path, configMap string) string { t.Fatalf("%s has no ConfigMap named %s", path, configMap) return "" } + +// wantSampledLogFilter is the filter on the access logs of the per-request legs. +const wantSampledLogFilter = ` +or_filter: + filters: + - status_code_filter: + comparison: + op: GE + value: {default_value: 400, runtime_key: access_log.min_status} + - response_flag_filter: {} + - and_filter: + filters: + - header_filter: + header: + name: content-type + string_match: {prefix: application/grpc} + - grpc_status_filter: {exclude: true, statuses: [OK]} + - runtime_filter: + runtime_key: access_log.sample + percent_sampled: {numerator: 1, denominator: HUNDRED} + use_independent_randomness: true +` + +// Every actor request lands in the mitm or cleartext access log, so both keep +// failures and a sample of the rest behind runtime keys an operator can flip. +// No other filter chain's access log may filter, since each is the only record +// of its events. +func TestEgressManifestsSampleThePerRequestAccessLogs(t *testing.T) { + t.Parallel() + var want node + if err := yaml.Unmarshal([]byte(wantSampledLogFilter), &want); err != nil { + t.Fatalf("parsing wantSampledLogFilter: %v", err) + } + logs := chainAccessLogsByLeg(bootstrapTree(t, egressManifest)) + sampledLegs := []string{"mitm", "cleartext"} + for _, leg := range sampledLegs { + t.Run(leg, func(t *testing.T) { + t.Parallel() + if len(logs[leg]) == 0 { + t.Fatalf("no access log with leg %q; the manifest changed shape and this test is checking nothing", leg) + } + for _, al := range logs[leg] { + if diff := cmp.Diff(want, child(al, "filter")); diff != "" { + t.Errorf("access log %q filter mismatch (-want +got):\n%s", leg, diff) + } + } + }) + } + for leg, als := range logs { + if slices.Contains(sampledLegs, leg) { + continue + } + for _, al := range als { + if _, ok := al["filter"]; ok { + t.Errorf("access log %q has a filter; only %v may", leg, sampledLegs) + } + } + } +} + +// chainAccessLogsByLeg groups the access logs of every network filter in every +// filter chain by their json_format leg. The CONNECT log has a text format and +// no leg, so it is grouped as "connect". +func chainAccessLogsByLeg(tree node) map[string][]node { + out := map[string][]node{} + for _, lc := range allChains(tree) { + for _, f := range list(lc.chain, "filters") { + for _, al := range list(child(f, "typed_config"), "access_log") { + leg := str(child(child(child(al, "typed_config"), "log_format"), "json_format"), "leg") + if leg == "" { + leg = "connect" + } + out[leg] = append(out[leg], al) + } + } + } + return out +} diff --git a/docs/metrics/substrate.yaml b/docs/metrics/substrate.yaml index 6eb455f2a9..a7e229ddf4 100644 --- a/docs/metrics/substrate.yaml +++ b/docs/metrics/substrate.yaml @@ -229,6 +229,14 @@ blind_spots: - area: podcertcontroller code: cmd/podcertcontroller brief: This component has no metrics. + - area: egress gateway access log + code: manifests/ate-install/atenet-egress.yaml + brief: > + The MITM and cleartext chains log only requests with status 400 or + above, gRPC requests with a grpc-status other than OK, any response + flag, and a 1% sample of the rest. The runtime keys + access_log.min_status and access_log.sample on the admin port restore + full logging while you debug. - area: GCP Secret Manager credential provider code: internal/plugins/gcp-secret-manager brief: > diff --git a/manifests/ate-install/atenet-egress.yaml b/manifests/ate-install/atenet-egress.yaml index f2672cfeef..2563b568bc 100644 --- a/manifests/ate-install/atenet-egress.yaml +++ b/manifests/ate-install/atenet-egress.yaml @@ -406,6 +406,30 @@ data: # resolves and what both routes verify the origin against. access_log: - name: envoy.access_loggers.file + # Every actor request lands here, so log the ones that went + # wrong, including gRPC errors sent with HTTP 200, and a sample + # of the rest. The runtime keys let an operator turn full + # logging back on at the admin port: + # /runtime_modify?access_log.min_status=0. + filter: + or_filter: + filters: + - status_code_filter: + comparison: + op: GE + value: {default_value: 400, runtime_key: access_log.min_status} + - response_flag_filter: {} + - and_filter: + filters: + - header_filter: + header: + name: content-type + string_match: {prefix: application/grpc} + - grpc_status_filter: {exclude: true, statuses: [OK]} + - runtime_filter: + runtime_key: access_log.sample + percent_sampled: {numerator: 1, denominator: HUNDRED} + use_independent_randomness: true typed_config: "@type": type.googleapis.com/envoy.extensions.access_loggers.file.v3.FileAccessLog path: /dev/stdout @@ -641,6 +665,30 @@ data: stream_idle_timeout: 0s access_log: - name: envoy.access_loggers.file + # Every actor request lands here, so log the ones that went + # wrong, including gRPC errors sent with HTTP 200, and a sample + # of the rest. The runtime keys let an operator turn full + # logging back on at the admin port: + # /runtime_modify?access_log.min_status=0. + filter: + or_filter: + filters: + - status_code_filter: + comparison: + op: GE + value: {default_value: 400, runtime_key: access_log.min_status} + - response_flag_filter: {} + - and_filter: + filters: + - header_filter: + header: + name: content-type + string_match: {prefix: application/grpc} + - grpc_status_filter: {exclude: true, statuses: [OK]} + - runtime_filter: + runtime_key: access_log.sample + percent_sampled: {numerator: 1, denominator: HUNDRED} + use_independent_randomness: true typed_config: "@type": type.googleapis.com/envoy.extensions.access_loggers.file.v3.FileAccessLog path: /dev/stdout From d905e95a7598296b4b7d2db99acd1930dc6c4bc6 Mon Sep 17 00:00:00 2001 From: Greg Castle Date: Thu, 15 Oct 2026 23:01:04 +0000 Subject: [PATCH 16/31] docs: introduce zero-trust security as a co-equal pillar in README (#2652) Expand the top-level README messaging to give security equal billing with performance. Add a dedicated Security Model section highlighting VM-grade sandboxing, per-agent SPIFFE identity, zero ambient credentials, default-deny networking, and pluggable edge policy. Also some word cleanup on the performance claims. - [x] Tests pass - [x] Appropriate changes to documentation are included in the PR --- README.md | 54 ++++++++++++++++++++++++-------------------- docs/architecture.md | 2 +- docs/roadmap.md | 4 +++- docs/security.md | 4 +++- 4 files changed, 37 insertions(+), 27 deletions(-) diff --git a/README.md b/README.md index 41b9cf60bf..ccf43e918d 100644 --- a/README.md +++ b/README.md @@ -7,34 +7,33 @@ # Agent Substrate -**A secure-by-default runtime for running millions of agent sandboxes on Kubernetes.** +**Zero-trust sandboxing for millions of agents on Kubernetes.** [![License](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) -[Quickstart](#quickstart-development) · [Architecture](docs/architecture.md) · [Roadmap](docs/roadmap.md) · [Contributing](CONTRIBUTING.md) +[Quickstart](#quickstart-development) · [Security](docs/security.md) · [Architecture](docs/architecture.md) · [Roadmap](docs/roadmap.md) · [Contributing](CONTRIBUTING.md) ## What is Agent Substrate? -Agent Substrate is a secure-by-default agent execution runtime engineered to run millions of sandboxes with 10x higher density than standard container runtimes. Purpose-built for the era of autonomous agents, Substrate delivers sub-500ms resume operations at over 500 suspend/resume activations per second with native zero-trust kernel and network isolation. It supports multiple sandbox technologies including microVMs and gVisor, enabling consistent lifecycle operations for all sandbox types. +Agent Substrate is a security-hardened, zero-trust runtime for sandboxed agents, engineered to safely run millions of agents and tools with 10x higher density than standard containers. Purpose-built for the era of autonomous agents, Substrate delivers fast resume operations in less than 400ms, with more than 1500 activations per second. Agents get isolated compute, identity, networking, and storage. Sensitive credentials are kept out of reach from untrusted code. It supports multiple sandbox technologies, including microVMs and gVisor, while enabling a consistent lifecycle and management of all sandboxes. -At its core, Agent Substrate maps a larger set of “actors” (applications such as agents) onto a smaller set of ready “workers”, relying on the fact that agent-like applications tend to be idle most of the time to achieve heavy multiplexing. It provides functionality to manage an actor’s lifecycle (e.g. create/destroy, suspend/resume), to assign actors to workers in real time, and to route incoming traffic to them. +At its core, Agent Substrate maps a larger set of “actors” (stateful workloads such as agents) onto a smaller pool of ready “workers”. Because agents spend most of their lives waiting on models, tools, or humans, Substrate snapshots idle actors out of memory and restores them when a request arrives. -Agent Substrate is intended to be a low-opinion system. The workloads it manages don't have to be literal AI agents, but those are the best example of the kind of applications it is designed for. It is not an SDK for building agents, but rather a system for running them at scale. +Substrate is a low-opinion runtime, not an agent SDK. The workloads it runs do not have to be AI agents, though autonomous, untrusted, mostly-idle agents are the target for its isolation and scheduling model. It runs on standard Kubernetes: Pods, autoscaling, NetworkPolicies. It adds agent-specific scheduling, snapshotting, identity, and network mediation. This allows agentic, inference, training, and reinforcement learning workloads to share one infrastructure plane. -Agent Substrate leverages Kubernetes for the infrastructure provisioning and worker lifecycle management (Kubernetes Pods). It builds on top of Kubernetes features like Pods and Pod autoscaling, while Agent Substrate provides agent-specific scheduling and control to achieve lower latency. Using Kubernetes as the underlying system enables consistent infrastructure management across all workloads types that are required for end to end agentic deployments and allows holistic infrastructure optimizations for RL scenarios that span agentic, inference and training cycles. +## Security Model -## Star History +Substrate treats every actor as untrusted and mutually hostile, so tenants can share physical nodes without sharing a security boundary. Every control operates within the cold-start and resume budget. See the [Security Guide](docs/security.md) for the full list of security principles and threat model. - - - - Star history chart for agent-substrate/substrate - - +* **Always sandboxed, VM-grade boundaries:** Every actor runs inside a dedicated [gVisor](https://gvisor.dev/) (`runsc`) or [Cloud Hypervisor](https://www.cloudhypervisor.org/) microVM sandbox with segregated compute, network, and storage. Plain Linux containers are not supported. +* **Per-agent cryptographic identity:** Every active actor receives a short-lived [SPIFFE](https://spiffe.io/) X.509 certificate by default, and the egress gateway can mint per-actor JWTs on demand when an egress policy calls for one. Both carry the actor identity and atespace (the Substrate namespace grouping), and are designed to federate with external identity systems. +* **Zero credentials in the sandbox:** Actors never hold actor identity JWTs/certs, API keys, or cloud tokens. The built-in egress gateway ([`atenet-egress`](docs/egress-credential-injection.md)) injects credentials into approved outbound requests outside the sandbox, so a prompt-injected or compromised agent has nothing to exfiltrate. +* **Default-deny networking:** Actors start with a private network stack, no inbound path except through the ingress router (`atenet-router`), no lateral actor-to-actor or control-plane access, and outbound traffic [restricted by the egress gateway](docs/egress-traffic.md). +* **Pluggable policy at the edge:** The egress gateway acts as an external Policy Enforcement Point (PEP) that is fully replaceable, allowing you to attach your own authorization, DLP, prompt-injection scanning, or cloud IAM policy engine without placing policy code inside the untrusted sandbox. -## Demo +## Performance & Multiplexing [![Agent Substrate Demo](https://img.youtube.com/vi/ZEzkCFJkzjY/hq1.jpg)](https://www.youtube.com/watch?v=ZEzkCFJkzjY) @@ -42,22 +41,29 @@ Agent Substrate leverages Kubernetes for the infrastructure provisioning and wor This demo highlights the core developer experience and "Agentic Infrastructure" capabilities of Substrate: -1. **Actor Teleport:** High-performance suspend and resume of actors onto any available worker in the pool with sub-second activation. -2. **State Persistence:** Persistent working memory (volatile RAM) and filesystem state preserved perfectly across hibernation cycles via full-state snapshots. -3. **Agent Multiplexing:** Demonstrates 30x+ oversubscription by "juggling" a large registry of stateful actors onto a small pool of shared physical pods. +1. **Actor Teleport:** Suspend and resume actors onto any available worker in the pool, when needed. +2. **Full-State Persistence:** Working memory (RAM) and filesystem state are captured in snapshots and restored intact across hibernation cycles, without losing in-flight sessions or tool state. +3. **10x–30x Oversubscription:** Multiplex a large registry of stateful actors onto a small pool of warm worker pods, paying for compute only while an agent is actively thinking or executing a tool. -To reproduce this demo in your own cluster, please refer to the detailed walkthrough in the **[Counter Demo](demos/counter/README.md)**. +To reproduce this demo in your own cluster, see the **[Counter Demo](demos/counter/README.md)**. For more walkthroughs, visit the **[agent-substrate YouTube channel](https://www.youtube.com/channel/UCN9PPqlTtVxlcpbQ-NWpfZQ)**. -For more videos and walkthroughs, visit our YouTube channel: **[agent-substrate](https://www.youtube.com/channel/UCN9PPqlTtVxlcpbQ-NWpfZQ)**. +## Star History + + + + + Star history chart for agent-substrate/substrate + + ## Framework Agnostic & Compatibility -Agent Substrate is designed to be **framework and agent harness agnostic**. Because it manages standard OCI containers at the kernel level (via gVisor), it can host agents built on any stack. +Agent Substrate is **framework and agent harness agnostic**. Because it runs standard OCI container images inside gVisor or microVM sandboxes and enforces security outside the workload, it hosts agents built on any stack without SDK changes: -* **Agent Development Kit (ADK):** Support for ADK agents with session state preservation across invocations as actor state. Ideal for all types of agents and stateful tool or subagent calls. -* **LangChain:** Ideal execution environment for LangChain agents and tool calls. -* **Claude Code, CodeX, and Antigravity:** Support for high-density, stateful coding environments that preserve system state and filesystem state across sessions. -* **Model Context Protocol (MCP):** Support for deploying secure, sandboxed MCP servers as Substrate Actors to provide durable tools for any model. +* **Agent Development Kit (ADK):** Preserves session state across invocations as actor state. +* **LangChain:** Drop-in execution environment for LangChain agents and tool calls. +* **Claude Code, Codex, and Antigravity:** High-density, stateful coding environments that keep system and filesystem state across sessions while isolating untrusted code execution. +* **Model Context Protocol (MCP):** Deploy sandboxed MCP servers as Substrate Actors with per-actor identity and egress controls. ## Ecosystem & Examples diff --git a/docs/architecture.md b/docs/architecture.md index bd1a0b4a87..9b57095e8f 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -4,7 +4,7 @@ NOTE: Much of this architecture is aspirational, and is not yet implemented! ## Overview -Agent Substrate is a secure-by-default agent execution runtime engineered to run millions of sandboxes with 10x higher density than standard container runtimes. The agent substrate control plane provides full lifecycle management for agent sandboxes, delivering sub-second agent resume/suspend operations, and allows heavy multiplexing of agents onto the same compute infrastructure. It supports multiple sandbox technologies including microVMs and gVisor, enabling consistent lifecycle operations for all sandbox types. +Agent Substrate is a security-hardened, zero-trust runtime for sandboxed agents, engineered to safely run millions of agents and tools with 10x higher density than standard containers. Purpose-built for the era of autonomous agents, Substrate delivers fast resume operations in less than 400ms, with more than 1500 activations per second. Agents get isolated compute, identity, networking, and storage. Sensitive credentials are kept out of reach from untrusted code. It supports multiple sandbox technologies, including microVMs and gVisor, while enabling a consistent lifecycle and management of all sandboxes. At its core, Agent Substrate maps a larger set of “actors” (applications such as agents) onto a smaller set of ready “workers”, relying on diff --git a/docs/roadmap.md b/docs/roadmap.md index c460bd0e0d..c9e0426df9 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -2,7 +2,9 @@ ## Overview -Agent Substrate delivers a performant, high density runtime environment for large scale agent deployments. The agent substrate control plane provides full lifecycle management for agent sandboxes, delivering sub-second agent resume/suspend operations, and allows heavy multiplexing of agents onto the same compute infrastructure. It supports multiple sandbox technologies including microVMs and gVisor. Substrate builds on sandboxed, snapshottable Pods by adding the ability to update running containers in the Pod leveraging the high scale and low latency Agent Substrate control plane. Substrate enables these containers to rapidly cycle between active and inactive lifecycle phases with fast suspend and resume from snapshots in inexpensive cloud object storage, without losing in-memory or filesystem state. This is especially critical for agentic workloads, which frequently oscillate between active (handling user input) and idle (waiting for LLMs or tool calls to complete). With Substrate, resources can be reclaimed during these idle moments for use by other workloads. This enables both greater efficiency and greater scale than using Pods alone, while still leveraging Kubernetes to provision capacity, configure networking and storage, and manage Substrate itself. +Agent Substrate is a security-hardened, zero-trust runtime for sandboxed agents, engineered to safely run millions of agents and tools with 10x higher density than standard containers. Purpose-built for the era of autonomous agents, Substrate delivers fast resume operations in less than 400ms, with more than 1500 activations per second. Agents get isolated compute, identity, networking, and storage. Sensitive credentials are kept out of reach from untrusted code. It supports multiple sandbox technologies, including microVMs and gVisor, while enabling a consistent lifecycle and management of all sandboxes. + +Substrate builds on sandboxed, snapshottable Pods by adding the ability to update running containers in the Pod leveraging the high scale and low latency Agent Substrate control plane. Substrate enables these containers to rapidly cycle between active and inactive lifecycle phases with fast suspend and resume from snapshots in inexpensive cloud object storage, without losing in-memory or filesystem state. This is especially critical for agentic workloads, which frequently oscillate between active (handling user input) and idle (waiting for LLMs or tool calls to complete). With Substrate, resources can be reclaimed during these idle moments for use by other workloads. This enables both greater efficiency and greater scale than using Pods alone, while still leveraging Kubernetes to provision capacity, configure networking and storage, and manage Substrate itself. This project is trying to move very quickly to find the right set of capabilities for the ever-changing agentic workloads market. Below are our priorities. Any efforts which are not aligned with these priorities should probably be deferred. diff --git a/docs/security.md b/docs/security.md index dbde5ec01f..afbcf3a180 100644 --- a/docs/security.md +++ b/docs/security.md @@ -1,6 +1,8 @@ # Substrate Security -Agent Substrate executes untrusted actors at scale in a multi-tenant environment where workloads are dynamic and memory states migrate continuously across physical infrastructure. All security controls must operate within Substrate's sub-100ms cold-start and resume latency budget. This document defines the high-level security principles. More detailed security risks are documented [in the threat model](threat-model.md). +Substrate delivers zero trust for agents. Every agent has an identity, but no credentials, no open network, and no way out of the sandbox. + +Agent Substrate executes untrusted actors at scale in a multi-tenant environment where workloads are dynamic and memory states migrate continuously across physical infrastructure. All security controls must operate within Substrate's cold-start and resume latency budget. This document defines the high-level security principles. More detailed security risks are documented [in the threat model](threat-model.md). ## Security Scope From ef193d1d83d3c0913274923ecfaf6b8b20360870 Mon Sep 17 00:00:00 2001 From: Anish Ramasekar Date: Thu, 15 Oct 2026 23:37:06 +0000 Subject: [PATCH 17/31] Mint worker-for-actor certificates only for the Worker that the actor is assigned to (#2702) MintWorkerActorCertificate authenticated the atelet but ignored its node, and atelet did not say which worker was asking. Any node, or any worker on the actor's node, could mint the actor's certificate from its name and UID. This applies the same check MintWorkerActorJwt got in #2668. - ate-api-server mints only when the caller's node hosts the actor and the requesting worker is the one assigned to it. Otherwise it returns NotFound, before the stale-UID check. - `MintWorkerActorCertificateRequest` gains a required `worker` field. atelet fills it from the caller's certificate, as it does for RequestActorSuspend. A paused actor has no worker, so it gets no certificate. - `worker` is required with no fallback; upgrading atelet before ate-api-server, as docs/upgrade.md already orders it, covers the skew. Tested with unit tests for the node, worker, paused, release/rebook and stale-UID cases, plus controlapi tests against a real PostgreSQL store. `go test -race ./cmd/ateapi/internal/... ./cmd/atelet/...` passes. --- .../apivalidation/zz_generated.validation.go | 50 +++ cmd/ateapi/internal/controlapi/export_test.go | 137 +++++++ .../internal/controlapi/hosting_test.go | 127 ++----- .../controlapi/mint_after_release_test.go | 201 ++++++++++ .../internal/workerservice/certificate.go | 31 +- .../workerservice/certificate_test.go | 354 +++++++++++++++--- cmd/ateapi/internal/workerservice/jwt.go | 8 +- cmd/ateapi/internal/workerservice/jwt_test.go | 61 +++ cmd/atelet/workersupport.go | 12 +- cmd/atelet/workersupport_test.go | 2 + .../isolate/fake-atelet/herder.go | 8 +- .../isolate/fake-atelet/herder_test.go | 13 +- pkg/proto/ateapipb/ateapi.pb.go | 295 ++++++++------- pkg/proto/ateapipb/ateapi.proto | 19 +- pkg/proto/ateapipb/ateapi_grpc.pb.go | 16 + 15 files changed, 1019 insertions(+), 315 deletions(-) create mode 100644 cmd/ateapi/internal/controlapi/export_test.go create mode 100644 cmd/ateapi/internal/controlapi/mint_after_release_test.go diff --git a/cmd/ateapi/internal/apivalidation/zz_generated.validation.go b/cmd/ateapi/internal/apivalidation/zz_generated.validation.go index b5124f6261..4710aadc14 100644 --- a/cmd/ateapi/internal/apivalidation/zz_generated.validation.go +++ b/cmd/ateapi/internal/apivalidation/zz_generated.validation.go @@ -6046,6 +6046,56 @@ func Validate_MintWorkerActorCertificateRequest( errs = append(errs, fn(fldPath.Child("certificate_signing_request"), obj.CertificateSigningRequest, oldVal, oldObj != nil)...) } + { // field ateapipb.MintWorkerActorCertificateRequest.Worker + fn := func( + fldPath *field.Path, + obj, oldObj *ateapipb.ObjectRef, + 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 + } + func() { // cohort = "atespace" + earlyReturn := false + if e := validate.Subfield(ctx, op, fldPath, obj, oldObj, "atespace", + func(o *ateapipb.ObjectRef) *string { return &o.Atespace }, validate.DirectEqual, validate.ForbiddenValue).MarkBeta().MarkShortCircuit(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if e := validate.Subfield(ctx, op, fldPath, obj, oldObj, "atespace", + func(o *ateapipb.ObjectRef) *string { return &o.Atespace }, validate.DirectEqual, validate.OptionalValue).MarkBeta().MarkShortCircuit(); len(e) != 0 { + earlyReturn = true + } + if e := validate.Subfield(ctx, op, fldPath, obj, oldObj, "atespace", + func(o *ateapipb.ObjectRef) *string { return &o.Atespace }, validate.DirectEqual, validate.OptionalValue).MarkBeta().MarkShortCircuit(); len(e) != 0 { + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + }() + // call the type's validation function + errs = append(errs, Validate_ObjectRef(ctx, op, fldPath, obj, oldObj)...) + return + } + oldVal := safe.Field(oldObj, + func(oldObj *ateapipb.MintWorkerActorCertificateRequest) *ateapipb.ObjectRef { + return oldObj.Worker + }) + errs = append(errs, fn(fldPath.Child("worker"), obj.Worker, oldVal, oldObj != nil)...) + } + return errs } diff --git a/cmd/ateapi/internal/controlapi/export_test.go b/cmd/ateapi/internal/controlapi/export_test.go new file mode 100644 index 0000000000..c569ad55ed --- /dev/null +++ b/cmd/ateapi/internal/controlapi/export_test.go @@ -0,0 +1,137 @@ +// 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 controlapi + +import ( + "context" + "testing" + + "github.com/agent-substrate/substrate/cmd/ateapi/internal/scheduling" + "github.com/agent-substrate/substrate/cmd/ateapi/internal/store" + "github.com/agent-substrate/substrate/internal/ateattr" + "github.com/agent-substrate/substrate/internal/resources" + "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "google.golang.org/grpc/codes" +) + +// Hooks for the controlapi_test package, whose tests call WorkerService, which +// imports controlapi. + +var ( + NewTestPersistence = newTestPersistence + SeedAssignment = seedAssignment + WireTestAssignment = wireTestAssignment + SeedSuspendTemplate = seedSuspendTemplate + SuspendTestTemplateRef = suspendTestTemplateRef +) + +// TestStorageLocation is the storage location ReleaseWindowWorkflows' +// durable snapshots are under. +const TestStorageLocation = testStorageLocation + +// ConflictingUpdateStore fails every actor update with a version conflict. +type ConflictingUpdateStore = conflictingUpdateStore + +// ReleaseWindowWorkflow is a workflow step that releases an actor's worker +// before it records the actor's new state. +type ReleaseWindowWorkflow struct { + Name string + // State is the actor's state while the worker holds it. + State ateapipb.ActorState + // Snapshot, when set, is the actor's latest snapshot. + Snapshot func(t *testing.T) *ateapipb.Snapshot + // WorkerClass is the worker's sandbox class. Templates ask for gVisor, + // so "microvm" makes the worker ineligible. + WorkerClass string + // Finalize runs the workflow or step that releases the worker and then + // records the actor's new state against st. Pause and suspend reach the + // fake atelet newWireCaptureWorkflow serves for the worker + // WireTestAssignment names, so the actor must be assigned that worker + // and run from the template SeedSuspendTemplate stores. + Finalize func(t *testing.T, ctx context.Context, st store.Interface, actorRef resources.ActorRef, actor *ateapipb.Actor) error + // WantRetry is the code of Finalize retried against the real store. + WantRetry codes.Code + // WantFinalState is the actor's state once the retry commits. + WantFinalState ateapipb.ActorState +} + +// ReleaseWindowWorkflows returns pause, suspend, crash, and a resume onto an +// ineligible worker: the workflows that release a worker before rewriting the +// actor's status while the actor is in a state HostingNode resolves through +// the worker assignment. Revert and delete release theirs in REVERTING and +// DELETING, which HostingNode never resolves to a node. +func ReleaseWindowWorkflows() []ReleaseWindowWorkflow { + gvisorTemplate := &ateapipb.ActorTemplate{SandboxConfig: &ateapipb.SandboxConfig{SandboxClass: "gvisor"}} + return []ReleaseWindowWorkflow{ + { + Name: "pause", + State: ateapipb.ActorState_ACTOR_STATE_SUSPENDING, + Snapshot: func(*testing.T) *ateapipb.Snapshot { + // A pause in progress: node-local and not yet captured. + return newLocalSnapshot(1, ateapipb.SnapshotFidelity_SNAPSHOT_FIDELITY_MEMORY, "", "local-snap-1", "") + }, + WorkerClass: "gvisor", + // The actor is seeded SUSPENDING, so the pause skips its mark step + // and its first actor update is the finalize commit. + Finalize: func(t *testing.T, ctx context.Context, st store.Interface, actorRef resources.ActorRef, _ *ateapipb.Actor) error { + w, _ := newWireCaptureWorkflow(t, st) + _, err := w.PauseActor(ctx, actorRef) + return err + }, + WantRetry: codes.OK, + WantFinalState: ateapipb.ActorState_ACTOR_STATE_SUSPENDED, + }, + { + Name: "suspend", + State: ateapipb.ActorState_ACTOR_STATE_SUSPENDING, + Snapshot: func(t *testing.T) *ateapipb.Snapshot { + return newDurableSnapshot(1, ateapipb.SnapshotOwner_SNAPSHOT_OWNER_ACTOR, ateapipb.SnapshotFidelity_SNAPSHOT_FIDELITY_MEMORY, "", "snapshot-1", someActorSnapshotURI(t, testStorageLocation, "team-a", "snapshot-1"), ateapipb.SnapshotStorageStatus_SNAPSHOT_STORAGE_STATUS_PENDING) + }, + WorkerClass: "gvisor", + // As for the pause, the finalize commit is the first actor update. + Finalize: func(t *testing.T, ctx context.Context, st store.Interface, actorRef resources.ActorRef, _ *ateapipb.Actor) error { + w, _ := newWireCaptureWorkflow(t, st) + _, err := w.SuspendActor(ctx, actorRef) + return err + }, + WantRetry: codes.OK, + WantFinalState: ateapipb.ActorState_ACTOR_STATE_SUSPENDED, + }, + { + Name: "crash", + State: ateapipb.ActorState_ACTOR_STATE_RUNNING, + WorkerClass: "gvisor", + Finalize: func(_ *testing.T, ctx context.Context, st store.Interface, actorRef resources.ActorRef, _ *ateapipb.Actor) error { + return crashActor(ctx, st, actorRef, ateattr.OperationResume, "test crash") + }, + WantRetry: codes.OK, + WantFinalState: ateapipb.ActorState_ACTOR_STATE_CRASHED, + }, + { + // A resume that finds its worker ineligible releases it, then + // crashes the actor. The retry finds the worker no longer hosts + // the actor and crashes it. + Name: "resume onto an ineligible worker", + State: ateapipb.ActorState_ACTOR_STATE_RESUMING, + WorkerClass: "microvm", + Finalize: func(_ *testing.T, ctx context.Context, st store.Interface, actorRef resources.ActorRef, actor *ateapipb.Actor) error { + _, err := (&ActorWorkflow{store: st, scheduler: scheduling.New(nil)}).validateAssignedWorker(ctx, actorRef, actor, gvisorTemplate) + return err + }, + WantRetry: codes.Aborted, + WantFinalState: ateapipb.ActorState_ACTOR_STATE_CRASHED, + }, + } +} diff --git a/cmd/ateapi/internal/controlapi/hosting_test.go b/cmd/ateapi/internal/controlapi/hosting_test.go index 8e6e0b130e..820049e96a 100644 --- a/cmd/ateapi/internal/controlapi/hosting_test.go +++ b/cmd/ateapi/internal/controlapi/hosting_test.go @@ -25,107 +25,28 @@ import ( "github.com/agent-substrate/substrate/cmd/ateapi/internal/store/storetest" "github.com/agent-substrate/substrate/cmd/ateapi/internal/workercache" "github.com/agent-substrate/substrate/internal/apierror" - "github.com/agent-substrate/substrate/internal/ateattr" "github.com/agent-substrate/substrate/internal/resources" "github.com/agent-substrate/substrate/pkg/proto/ateapipb" - "google.golang.org/grpc/codes" "google.golang.org/protobuf/proto" ) -// TestHostingNodeAfterWorkerRelease drives each workflow that releases an -// actor's worker before it rewrites the actor's status into the window -// between the two, by failing the second write. The status still names the -// worker there, and keeps doing so until the workflow is retried. The worker's -// node must not count as hosting the actor in that window, nor once the worker -// hosts another actor. +// TestHostingNodeAfterWorkerRelease drives each of ReleaseWindowWorkflows into +// the window between releasing the actor's worker and rewriting its status, by +// failing the second write. The status still names the worker there, and keeps +// doing so until the workflow is retried. The worker's node must not count as +// hosting the actor in that window, nor once the worker hosts another actor. func TestHostingNodeAfterWorkerRelease(t *testing.T) { // The worker is the one wireTestAssignment names, so a pause or suspend // reaches newWireCaptureWorkflow's fake atelet on its node. wa := wireTestAssignment() node := wa.GetNodeName() - gvisorTemplate := &ateapipb.ActorTemplate{SandboxConfig: &ateapipb.SandboxConfig{SandboxClass: "gvisor"}} - tests := []struct { - name string - state ateapipb.ActorState - // snapshot, when set, is the actor's latest snapshot. - snapshot func(t *testing.T) *ateapipb.Snapshot - // workerClass is the worker's sandbox class. A template asks for - // gVisor, so "microvm" makes the worker ineligible. - workerClass string - // finalize runs the workflow or step that releases the worker and - // then records the actor's new state against st. - finalize func(t *testing.T, ctx context.Context, st store.Interface, actorRef resources.ActorRef, actor *ateapipb.Actor) error - // wantRetry is the code of the retried finalize against the real - // store. - wantRetry codes.Code - // wantFinalState and wantFinalNode are the actor's state and - // HostingNode once the retry commits. - wantFinalState ateapipb.ActorState - wantFinalNode string - }{ - { - name: "pause", - state: ateapipb.ActorState_ACTOR_STATE_SUSPENDING, - snapshot: func(*testing.T) *ateapipb.Snapshot { - // A pause in progress: node-local and not yet captured. - return newLocalSnapshot(1, ateapipb.SnapshotFidelity_SNAPSHOT_FIDELITY_MEMORY, "", "local-snap-1", "") - }, - workerClass: "gvisor", - // The actor is seeded SUSPENDING, so the pause skips its mark step - // and its first actor update is the finalize commit. - finalize: func(t *testing.T, ctx context.Context, st store.Interface, actorRef resources.ActorRef, _ *ateapipb.Actor) error { - w, _ := newWireCaptureWorkflow(t, st) - _, err := w.PauseActor(ctx, actorRef) - return err - }, - wantRetry: codes.OK, - wantFinalState: ateapipb.ActorState_ACTOR_STATE_SUSPENDED, - // Paused: the node holding the local snapshot. - wantFinalNode: node, - }, - { - name: "suspend", - state: ateapipb.ActorState_ACTOR_STATE_SUSPENDING, - snapshot: func(t *testing.T) *ateapipb.Snapshot { - return newDurableSnapshot(1, ateapipb.SnapshotOwner_SNAPSHOT_OWNER_ACTOR, ateapipb.SnapshotFidelity_SNAPSHOT_FIDELITY_MEMORY, "", "snapshot-1", someActorSnapshotURI(t, testStorageLocation, "team-a", "snapshot-1"), ateapipb.SnapshotStorageStatus_SNAPSHOT_STORAGE_STATUS_PENDING) - }, - workerClass: "gvisor", - // As for the pause, the finalize commit is the first actor update. - finalize: func(t *testing.T, ctx context.Context, st store.Interface, actorRef resources.ActorRef, _ *ateapipb.Actor) error { - w, _ := newWireCaptureWorkflow(t, st) - _, err := w.SuspendActor(ctx, actorRef) - return err - }, - wantRetry: codes.OK, - wantFinalState: ateapipb.ActorState_ACTOR_STATE_SUSPENDED, - }, - { - name: "crash", - state: ateapipb.ActorState_ACTOR_STATE_RUNNING, - workerClass: "gvisor", - finalize: func(_ *testing.T, ctx context.Context, st store.Interface, actorRef resources.ActorRef, _ *ateapipb.Actor) error { - return crashActor(ctx, st, actorRef, ateattr.OperationResume, "test crash") - }, - wantRetry: codes.OK, - wantFinalState: ateapipb.ActorState_ACTOR_STATE_CRASHED, - }, - { - // A resume that finds its worker ineligible releases it, then - // crashes the actor. The retry finds the worker no longer hosts - // the actor and crashes it. - name: "resume onto an ineligible worker", - state: ateapipb.ActorState_ACTOR_STATE_RESUMING, - workerClass: "microvm", - finalize: func(_ *testing.T, ctx context.Context, st store.Interface, actorRef resources.ActorRef, actor *ateapipb.Actor) error { - _, err := (&ActorWorkflow{store: st, scheduler: scheduling.New(nil)}).validateAssignedWorker(ctx, actorRef, actor, gvisorTemplate) - return err - }, - wantRetry: codes.Aborted, - wantFinalState: ateapipb.ActorState_ACTOR_STATE_CRASHED, - }, + // wantFinalNode is HostingNode once the retry commits, by workflow name. + wantFinalNode := map[string]string{ + // Paused: the node holding the local snapshot. + "pause": node, } - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { + for _, tc := range ReleaseWindowWorkflows() { + t.Run(tc.Name, func(t *testing.T) { ctx := context.Background() persistence := newTestPersistence(t) seedSuspendTemplate(t, ctx, persistence, &ateapipb.SnapshotConfig{StorageLocation: testStorageLocation}) @@ -138,19 +59,19 @@ func TestHostingNodeAfterWorkerRelease(t *testing.T) { WorkerPod: wa.GetWorkerPod(), WorkerPodUid: wa.GetWorkerPodUid(), NodeName: node, - SandboxClass: tc.workerClass, + SandboxClass: tc.WorkerClass, Status: &ateapipb.WorkerStatus{State: ateapipb.WorkerState_WORKER_STATE_ACTIVE, Capacity: &ateapipb.WorkerResources{Actors: 1}}, }); err != nil { t.Fatalf("CreateWorker: %v", err) } status := &ateapipb.ActorStatus{ - State: tc.state, + State: tc.State, AssignedNode: node, WorkerAssignment: wireTestAssignment(), LastAssignedGeneration: 1, } - if tc.snapshot != nil { - status.Snapshots = []*ateapipb.Snapshot{tc.snapshot(t)} + if tc.Snapshot != nil { + status.Snapshots = []*ateapipb.Snapshot{tc.Snapshot(t)} } created := storetest.MustCreateActor(t, ctx, persistence, &ateapipb.Actor{ Metadata: &ateapipb.ResourceMetadata{Atespace: actorRef.Atespace, Name: actorRef.Name}, @@ -184,15 +105,15 @@ func TestHostingNodeAfterWorkerRelease(t *testing.T) { } // The release commits; recording the new state fails. - if err := tc.finalize(t, ctx, conflictingUpdateStore{Interface: persistence}, actorRef, actor); err == nil { + if err := tc.Finalize(t, ctx, conflictingUpdateStore{Interface: persistence}, actorRef, actor); err == nil { t.Fatal("finalize succeeded through a store that fails every actor update") } if _, err := persistence.GetWorkerAssignment(ctx, workerName, uid); !errors.Is(err, store.ErrNotFound) { t.Fatalf("worker assignment after release: err %v, want ErrNotFound", err) } got, actor = hostingNode("released, status not finalized") - if actor.GetStatus().GetState() != tc.state || actor.GetStatus().GetWorkerAssignment() == nil { - t.Fatalf("actor after the failed update: state %v, worker assignment %v; want %v naming the worker", actor.GetStatus().GetState(), actor.GetStatus().GetWorkerAssignment(), tc.state) + if actor.GetStatus().GetState() != tc.State || actor.GetStatus().GetWorkerAssignment() == nil { + t.Fatalf("actor after the failed update: state %v, worker assignment %v; want %v naming the worker", actor.GetStatus().GetState(), actor.GetStatus().GetWorkerAssignment(), tc.State) } if got != "" { t.Errorf("HostingNode after the worker was released = %q, want \"\"", got) @@ -208,15 +129,15 @@ func TestHostingNodeAfterWorkerRelease(t *testing.T) { } // A retry records the new state. - if err := tc.finalize(t, ctx, persistence, actorRef, actor); apierror.Code(err) != tc.wantRetry { - t.Fatalf("finalize retry: code %v, want %v (err: %v)", apierror.Code(err), tc.wantRetry, err) + if err := tc.Finalize(t, ctx, persistence, actorRef, actor); apierror.Code(err) != tc.WantRetry { + t.Fatalf("finalize retry: code %v, want %v (err: %v)", apierror.Code(err), tc.WantRetry, err) } got, actor = hostingNode("finalized") - if actor.GetStatus().GetState() != tc.wantFinalState { - t.Errorf("actor state once finalized = %v, want %v", actor.GetStatus().GetState(), tc.wantFinalState) + if actor.GetStatus().GetState() != tc.WantFinalState { + t.Errorf("actor state once finalized = %v, want %v", actor.GetStatus().GetState(), tc.WantFinalState) } - if got != tc.wantFinalNode { - t.Errorf("HostingNode once finalized = %q, want %q", got, tc.wantFinalNode) + if want := wantFinalNode[tc.Name]; got != want { + t.Errorf("HostingNode once finalized = %q, want %q", got, want) } }) } diff --git a/cmd/ateapi/internal/controlapi/mint_after_release_test.go b/cmd/ateapi/internal/controlapi/mint_after_release_test.go new file mode 100644 index 0000000000..3c2b808ece --- /dev/null +++ b/cmd/ateapi/internal/controlapi/mint_after_release_test.go @@ -0,0 +1,201 @@ +// 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 controlapi_test + +import ( + "context" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/x509" + "errors" + "testing" + "time" + + "github.com/agent-substrate/substrate/cmd/ateapi/internal/ateletauth/ateletauthtest" + "github.com/agent-substrate/substrate/cmd/ateapi/internal/controlapi" + "github.com/agent-substrate/substrate/cmd/ateapi/internal/store" + "github.com/agent-substrate/substrate/cmd/ateapi/internal/store/storetest" + "github.com/agent-substrate/substrate/cmd/ateapi/internal/workerservice" + "github.com/agent-substrate/substrate/internal/apierror" + "github.com/agent-substrate/substrate/internal/installdefaults" + "github.com/agent-substrate/substrate/internal/localca" + "github.com/agent-substrate/substrate/internal/resources" + "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "google.golang.org/grpc/codes" +) + +// TestMintWorkerActorCertificateAfterWorkerRelease drives each of +// ReleaseWindowWorkflows into the window between releasing the actor's worker +// and rewriting its status, by failing the second write. The status still +// names the worker there, and keeps doing so until the workflow is retried. +// The worker must not get a worker-for-actor certificate for the actor in that +// window, nor once it hosts another actor or the status is finalized, and +// another worker on the node never gets one. +func TestMintWorkerActorCertificateAfterWorkerRelease(t *testing.T) { + // The worker is the one WireTestAssignment names, so a pause or suspend + // reaches the workflow's fake atelet on its node. + wa := controlapi.WireTestAssignment() + node := wa.GetNodeName() + for _, wf := range controlapi.ReleaseWindowWorkflows() { + t.Run(wf.Name, func(t *testing.T) { + ctx := context.Background() + persistence := controlapi.NewTestPersistence(t) + controlapi.SeedSuspendTemplate(t, ctx, persistence, &ateapipb.SnapshotConfig{StorageLocation: controlapi.TestStorageLocation}) + actorRef := resources.ActorRef{Atespace: "team-a", Name: "actor-1"} + // wa's worker hosts the actor; the neighbor is another worker on + // its node. + workerName := wa.GetWorker().GetName() + neighborName := "neighbor-1" + for _, w := range []struct{ name, pod, podUID string }{ + {workerName, wa.GetWorkerPod(), wa.GetWorkerPodUid()}, + {neighborName, "neighbor-pod", "neighbor-pod-uid"}, + } { + if _, err := persistence.CreateWorker(ctx, &ateapipb.Worker{ + Metadata: &ateapipb.ResourceMetadata{Name: w.name}, + WorkerNamespace: wa.GetWorkerNamespace(), + WorkerPool: wa.GetWorkerPool(), + WorkerPod: w.pod, + WorkerPodUid: w.podUID, + NodeName: node, + SandboxClass: wf.WorkerClass, + Status: &ateapipb.WorkerStatus{State: ateapipb.WorkerState_WORKER_STATE_ACTIVE, Capacity: &ateapipb.WorkerResources{Actors: 1}}, + }); err != nil { + t.Fatalf("CreateWorker(%s): %v", w.name, err) + } + } + status := &ateapipb.ActorStatus{ + State: wf.State, + AssignedNode: node, + WorkerAssignment: controlapi.WireTestAssignment(), + LastAssignedGeneration: 1, + } + if wf.Snapshot != nil { + status.Snapshots = []*ateapipb.Snapshot{wf.Snapshot(t)} + } + created := storetest.MustCreateActor(t, ctx, persistence, &ateapipb.Actor{ + Metadata: &ateapipb.ResourceMetadata{Atespace: actorRef.Atespace, Name: actorRef.Name}, + ActorTemplate: controlapi.SuspendTestTemplateRef(), + Status: status, + }) + uid := created.GetMetadata().GetUid() + controlapi.SeedAssignment(t, persistence, workerName, &ateapipb.ActorAssignment{ + Actor: &ateapipb.ObjectRef{Atespace: actorRef.Atespace, Name: actorRef.Name}, + ActorUid: uid, + }) + + minter := newTestMinter(t, persistence) + csr := newTestCSR(t) + // mintAs asks for the actor's certificate as worker, through the + // atelet on node. + mintAs := func(worker string) codes.Code { + t.Helper() + _, err := minter.MintWorkerActorCertificate(ateletauthtest.ContextWith(ateletauthtest.CertOn(t, node)), &ateapipb.MintWorkerActorCertificateRequest{ + Actor: &ateapipb.ObjectRef{Atespace: actorRef.Atespace, Name: actorRef.Name}, + ActorUid: uid, + CertificateSigningRequest: csr, + Worker: &ateapipb.ObjectRef{Name: worker}, + }) + return apierror.Code(err) + } + // mint asks for the actor's certificate as the worker that the + // actor is assigned to and returns the actor as the mint saw it + // and the mint's code. + mint := func(stage string) (*ateapipb.Actor, codes.Code) { + t.Helper() + code := mintAs(workerName) + actor, gerr := persistence.GetActor(ctx, actorRef) + if gerr != nil { + t.Fatalf("GetActor: %v", gerr) + } + t.Logf("%s: actor_state=%s status_worker_set=%t cert_code=%s", + stage, actor.GetStatus().GetState(), actor.GetStatus().GetWorkerAssignment() != nil, code) + return actor, code + } + + actor, got := mint("hosted") + if got != codes.OK { + t.Fatalf("certificate mint while the worker holds the actor: code %v, want OK", got) + } + if got := mintAs(neighborName); got != codes.NotFound { + t.Errorf("certificate mint by another worker on the node: code %v, want NotFound", got) + } + + // The release commits; recording the new state fails. + if err := wf.Finalize(t, ctx, controlapi.ConflictingUpdateStore{Interface: persistence}, actorRef, actor); err == nil { + t.Fatal("finalize succeeded through a store that fails every actor update") + } + if _, err := persistence.GetWorkerAssignment(ctx, workerName, uid); !errors.Is(err, store.ErrNotFound) { + t.Fatalf("worker assignment after release: err %v, want ErrNotFound", err) + } + actor, got = mint("released, status not finalized") + if actor.GetStatus().GetState() != wf.State || actor.GetStatus().GetWorkerAssignment() == nil { + t.Fatalf("actor after the failed update: state %v, worker assignment %v; want %v naming the worker", actor.GetStatus().GetState(), actor.GetStatus().GetWorkerAssignment(), wf.State) + } + if got != codes.NotFound { + t.Errorf("certificate mint after the worker was released: code %v, want NotFound", got) + } + + // The worker takes another actor. + controlapi.SeedAssignment(t, persistence, workerName, &ateapipb.ActorAssignment{ + Actor: &ateapipb.ObjectRef{Atespace: actorRef.Atespace, Name: "actor-2"}, + ActorUid: "5c1d2e3f-4a5b-4c6d-8e7f-9a0b1c2d3e4f", + }) + if _, got := mint("worker rebooked for another actor"); got != codes.NotFound { + t.Errorf("certificate mint after the worker was rebooked: code %v, want NotFound", got) + } + + // A retry records the new state. + if err := wf.Finalize(t, ctx, persistence, actorRef, actor); apierror.Code(err) != wf.WantRetry { + t.Fatalf("finalize retry: code %v, want %v (err: %v)", apierror.Code(err), wf.WantRetry, err) + } + actor, got = mint("finalized") + if actor.GetStatus().GetState() != wf.WantFinalState { + t.Errorf("actor state once finalized = %v, want %v", actor.GetStatus().GetState(), wf.WantFinalState) + } + // No worker runs the actor once finalized, paused or not. + if got != codes.NotFound { + t.Errorf("certificate mint once finalized: code %v, want NotFound", got) + } + }) + } +} + +// newTestMinter returns a WorkerService that mints worker-for-actor +// certificates against st for the atelets ateletauthtest certifies. +func newTestMinter(t *testing.T, st store.Interface) *workerservice.Server { + t.Helper() + ca, err := localca.GenerateCA("1", localca.KeyTypeECDSAP256, 24*time.Hour) + if err != nil { + t.Fatalf("GenerateCA: %v", err) + } + caPool := &localca.ConcretePool{CAs: []*localca.CA{ca}, ActiveForSigning: "1"} + ateletID := installdefaults.SPIFFEID(installdefaults.SystemNamespace, installdefaults.AteletServiceAccount) + return workerservice.New(st, nil, ateletID, caPool, "", nil) +} + +// newTestCSR returns a certificate signing request for a fresh actor key. +func newTestCSR(t *testing.T) []byte { + t.Helper() + key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + t.Fatalf("GenerateKey: %v", err) + } + csr, err := x509.CreateCertificateRequest(rand.Reader, &x509.CertificateRequest{}, key) + if err != nil { + t.Fatalf("CreateCertificateRequest: %v", err) + } + return csr +} diff --git a/cmd/ateapi/internal/workerservice/certificate.go b/cmd/ateapi/internal/workerservice/certificate.go index c6737bf419..53f60d1276 100644 --- a/cmd/ateapi/internal/workerservice/certificate.go +++ b/cmd/ateapi/internal/workerservice/certificate.go @@ -19,6 +19,7 @@ import ( "crypto/x509" "errors" "fmt" + "log/slog" "net/url" "path" "time" @@ -40,23 +41,39 @@ func (s *Server) MintWorkerActorCertificate(ctx context.Context, req *ateapipb.M } // TODO(identity): This check should be handled by OpenFGA. - if _, err := ateletauth.Authenticate(ctx, s.ateletSPIFFEID); err != nil { + caller, err := ateletauth.Authenticate(ctx, s.ateletSPIFFEID) + if err != nil { return nil, err } - // TODO(authz): Authorization layer needs to check whether the caller has - // the mintActorCertificate permission/relation with this actor. This - // could be an atelet (via the relationship of the atelet running the - // actor), or the egress gateway (via a cluster-level grant?) + // TODO(authz): The egress gateway may also need certificates for actors + // (via a cluster-level grant?). - // Verify that this actor exists in the store. It doesn't need to be - // running, since we may need to issue certificates during actor boot / resume. dbActor, err := s.store.GetActor(ctx, resources.ActorRefFromObjectRef(req.GetActor())) if errors.Is(err, store.ErrNotFound) { return nil, apierror.NotFound("actor not found") } else if err != nil { return nil, fmt.Errorf("while retrieving actor: %w", err) } + // Only the assigned worker may mint, via its node's atelet, checked before the + // UID so other callers get NotFound. Unlike MintWorkerActorJwt, a paused actor + // gets NotFound too: it has no worker to bind the certificate to. + if err := s.checkActorOnNode(ctx, dbActor, caller.NodeName, "certificate"); err != nil { + return nil, err + } + // The status must name the requesting worker. When it names one, + // checkActorOnNode has confirmed in the store's worker assignments that + // this worker holds the actor. When it names none (a paused actor, or a + // resume that has bound a worker but not yet recorded it), checkActorOnNode + // read assigned_node alone, and no worker runs the actor yet. + if assigned := dbActor.GetStatus().GetWorkerAssignment().GetWorker().GetName(); assigned == "" || assigned != req.GetWorker().GetName() { + slog.WarnContext(ctx, "Refusing a worker-for-actor certificate for an actor not assigned to the calling worker", + slog.String("actor", resources.ActorRefFromActor(dbActor).String()), + slog.String("assigned_worker", assigned), + slog.String("caller_worker", req.GetWorker().GetName()), + slog.String("caller_node", caller.NodeName)) + return nil, apierror.NotFound("actor not found") + } if dbActor.GetMetadata().GetUid() != req.GetActorUid() { return nil, apierror.Aborted("conflict; actor has been deleted and recreated") } diff --git a/cmd/ateapi/internal/workerservice/certificate_test.go b/cmd/ateapi/internal/workerservice/certificate_test.go index 5ff978b5aa..a13796a651 100644 --- a/cmd/ateapi/internal/workerservice/certificate_test.go +++ b/cmd/ateapi/internal/workerservice/certificate_test.go @@ -20,6 +20,7 @@ import ( "crypto/elliptic" "crypto/rand" "crypto/x509" + "errors" "testing" "time" @@ -138,6 +139,7 @@ func TestMintWorkerActorCertificate(t *testing.T) { }, ActorUid: actor.GetMetadata().GetUid(), CertificateSigningRequest: csr, + Worker: &ateapipb.ObjectRef{Name: "worker-1"}, }) if err != nil { t.Fatalf("MintWorkerActorCertificate() failed: %v", err) @@ -182,77 +184,331 @@ func TestMintWorkerActorCertificate_Errors(t *testing.T) { csr, _ := generateTestCSR(t) authed := ateletauthtest.ContextWith(ateletauthtest.CertOn(t, "node-1")) + otherNode := ateletauthtest.ContextWith(ateletauthtest.CertOn(t, "node-2")) + valid := func() *ateapipb.MintWorkerActorCertificateRequest { + return &ateapipb.MintWorkerActorCertificateRequest{ + Actor: &ateapipb.ObjectRef{Atespace: "team-a", Name: "my-actor"}, + ActorUid: actor.GetMetadata().GetUid(), + CertificateSigningRequest: csr, + Worker: &ateapipb.ObjectRef{Name: "worker-1"}, + } + } + const ( + staleUID = "7d1c0b2a-9e4f-4c3d-8a6b-5f2e1d0c9b8a" + otherActorUID = "9a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d" + ) + // onWorker is an actor status naming worker-1, which runs on node-1. + onWorker := func(state ateapipb.ActorState) *ateapipb.ActorStatus { + return &ateapipb.ActorStatus{State: state, WorkerAssignment: &ateapipb.WorkerAssignment{Worker: &ateapipb.ObjectRef{Name: "worker-1"}}} + } + // pausingOnWorker is a pause in progress: SUSPENDING on worker-1 with a + // latest snapshot that is node-local and not yet captured. + pausingOnWorker := func() *ateapipb.ActorStatus { + status := onWorker(ateapipb.ActorState_ACTOR_STATE_SUSPENDING) + status.AssignedNode = "node-1" + status.LastAssignedGeneration = 1 + status.Snapshots = []*ateapipb.Snapshot{{Uuid: "snap-1", Generation: 1, Owner: ateapipb.SnapshotOwner_SNAPSHOT_OWNER_ACTOR}} + return status + } + released := map[string]string{} + rebooked := map[string]string{otherActorUID: "worker-1"} tests := []struct { - name string - ctx context.Context - req *ateapipb.MintWorkerActorCertificateRequest - want codes.Code + name string + ctx context.Context + mutate func(*ateapipb.MintWorkerActorCertificateRequest) + // status, when set, replaces the actor's status for the case. + status *ateapipb.ActorStatus + // assignments, when set, replaces the store's worker assignments + // (actor UID to worker) for the case. By default worker-1 holds the + // actor. + assignments map[string]string + // assignmentErr, when set, fails the store's worker assignment + // lookup. + assignmentErr error + want codes.Code }{ { - name: "unauthenticated", - ctx: ateletauthtest.ContextWith(nil), - req: &ateapipb.MintWorkerActorCertificateRequest{ - Actor: &ateapipb.ObjectRef{Atespace: "team-a", Name: "my-actor"}, - ActorUid: actor.GetMetadata().GetUid(), - CertificateSigningRequest: csr, - }, - want: codes.Unauthenticated, + name: "running actor on its worker's node", + ctx: authed, + status: onWorker(ateapipb.ActorState_ACTOR_STATE_RUNNING), + want: codes.OK, }, { - name: "missing actor", - ctx: authed, - req: &ateapipb.MintWorkerActorCertificateRequest{ - ActorUid: actor.GetMetadata().GetUid(), - CertificateSigningRequest: csr, - }, - want: codes.InvalidArgument, + // atunnel mints while the actor resumes, before it runs. + name: "resuming actor on its worker's node", + ctx: authed, + status: onWorker(ateapipb.ActorState_ACTOR_STATE_RESUMING), + want: codes.OK, }, { - name: "missing actor uid", - ctx: authed, - req: &ateapipb.MintWorkerActorCertificateRequest{ - Actor: &ateapipb.ObjectRef{Atespace: "team-a", Name: "my-actor"}, - CertificateSigningRequest: csr, - }, - want: codes.InvalidArgument, + name: "running actor being suspended on its worker's node", + ctx: authed, + status: onWorker(ateapipb.ActorState_ACTOR_STATE_SUSPENDING), + want: codes.OK, }, { - name: "missing csr", - ctx: authed, - req: &ateapipb.MintWorkerActorCertificateRequest{ - Actor: &ateapipb.ObjectRef{Atespace: "team-a", Name: "my-actor"}, - ActorUid: actor.GetMetadata().GetUid(), - }, - want: codes.InvalidArgument, + name: "running actor being paused on its worker's node", + ctx: authed, + status: pausingOnWorker(), + want: codes.OK, }, { - name: "actor not found", - ctx: authed, - req: &ateapipb.MintWorkerActorCertificateRequest{ - Actor: &ateapipb.ObjectRef{Atespace: "team-a", Name: "nonexistent"}, - ActorUid: actor.GetMetadata().GetUid(), - CertificateSigningRequest: csr, - }, + // Pause, suspend and crash release the worker before they clear the + // actor's status, which keeps naming the worker until they do. + name: "running actor whose worker was released", + ctx: authed, + status: onWorker(ateapipb.ActorState_ACTOR_STATE_RUNNING), + assignments: released, + want: codes.NotFound, + }, + { + // A resume that finds its worker ineligible releases it, then + // crashes the actor. + name: "resuming actor whose worker was released", + ctx: authed, + status: onWorker(ateapipb.ActorState_ACTOR_STATE_RESUMING), + assignments: released, + want: codes.NotFound, + }, + { + name: "suspending actor whose worker was released", + ctx: authed, + status: onWorker(ateapipb.ActorState_ACTOR_STATE_SUSPENDING), + assignments: released, + want: codes.NotFound, + }, + { + name: "pausing actor whose worker was released", + ctx: authed, + status: pausingOnWorker(), + assignments: released, + want: codes.NotFound, + }, + { + // The released worker took another actor. Its node hosts that one, + // not this one. + name: "running actor whose worker was rebooked for another actor", + ctx: authed, + status: onWorker(ateapipb.ActorState_ACTOR_STATE_RUNNING), + assignments: rebooked, + want: codes.NotFound, + }, + { + name: "resuming actor whose worker was rebooked for another actor", + ctx: authed, + status: onWorker(ateapipb.ActorState_ACTOR_STATE_RESUMING), + assignments: rebooked, + want: codes.NotFound, + }, + { + name: "suspending actor whose worker was rebooked for another actor", + ctx: authed, + status: onWorker(ateapipb.ActorState_ACTOR_STATE_SUSPENDING), + assignments: rebooked, + want: codes.NotFound, + }, + { + name: "pausing actor whose worker was rebooked for another actor", + ctx: authed, + status: pausingOnWorker(), + assignments: rebooked, + want: codes.NotFound, + }, + { + // The store binds the actor to a different worker than its status + // names, so the status's worker does not host it. + name: "actor held by a worker other than the one its status names", + ctx: authed, + status: onWorker(ateapipb.ActorState_ACTOR_STATE_RUNNING), + assignments: map[string]string{actor.GetMetadata().GetUid(): "worker-2"}, + want: codes.NotFound, + }, + { + // A failed lookup refuses the mint rather than trusting the status. + name: "worker assignment lookup fails", + ctx: authed, + status: onWorker(ateapipb.ActorState_ACTOR_STATE_RUNNING), + assignmentErr: errors.New("connection reset"), + want: codes.Internal, + }, + { + // The actor's worker runs on node-1 and the caller proves it is on node-2. + name: "actor hosted on another node", + ctx: otherNode, want: codes.NotFound, }, { - name: "actor uid mismatch", + // A foreign node must not learn that the actor exists from the UID + // check answering Aborted: the node check runs first. + name: "actor hosted on another node, stale uid", + ctx: otherNode, + mutate: func(r *ateapipb.MintWorkerActorCertificateRequest) { r.ActorUid = staleUID }, + want: codes.NotFound, + }, + { + // worker-2 runs on the actor's node, but the actor is not its own. + name: "another worker on the actor's node", + ctx: authed, + mutate: func(r *ateapipb.MintWorkerActorCertificateRequest) { r.Worker.Name = "worker-2" }, + want: codes.NotFound, + }, + { + // The worker check also runs before the UID check. + name: "another worker on the actor's node, stale uid", ctx: authed, - req: &ateapipb.MintWorkerActorCertificateRequest{ - Actor: &ateapipb.ObjectRef{Atespace: "team-a", Name: "my-actor"}, - ActorUid: "00000000-0000-0000-0000-000000000000", - CertificateSigningRequest: csr, + mutate: func(r *ateapipb.MintWorkerActorCertificateRequest) { + r.Worker.Name = "worker-2" + r.ActorUid = staleUID }, - want: codes.Aborted, + want: codes.NotFound, + }, + { + // MintWorkerActorJwt serves a paused actor's checkpoint node. No + // worker runs a paused actor, so no worker gets its certificate. + name: "paused actor on this node", + ctx: authed, + status: pausedStatus(ateapipb.ActorState_ACTOR_STATE_SUSPENDED, "node-1"), + assignments: released, + want: codes.NotFound, + }, + { + name: "paused actor being suspended on this node", + ctx: authed, + status: pausedStatus(ateapipb.ActorState_ACTOR_STATE_SUSPENDING, "node-1"), + assignments: released, + want: codes.NotFound, + }, + { + // Resume binds the worker in the store before it writes the + // assignment to the actor's status, and starts the actor only + // after that. In between, the worker holds the store's assignment + // but runs nothing, and HostingNode trusts assigned_node without + // reading the store, so only the status comparison refuses. + name: "paused actor bound to a worker on this node before its status names the worker", + ctx: authed, + status: pausedStatus(ateapipb.ActorState_ACTOR_STATE_SUSPENDED, "node-1"), + assignments: map[string]string{actor.GetMetadata().GetUid(): "worker-1"}, + want: codes.NotFound, + }, + { + name: "paused actor on another node", + ctx: authed, + status: pausedStatus(ateapipb.ActorState_ACTOR_STATE_SUSPENDED, "node-2"), + assignments: released, + want: codes.NotFound, + }, + { + // An empty worker name must not match a paused actor's empty + // assignment. + name: "paused actor, empty worker name", + ctx: authed, + status: pausedStatus(ateapipb.ActorState_ACTOR_STATE_SUSPENDED, "node-1"), + assignments: released, + mutate: func(r *ateapipb.MintWorkerActorCertificateRequest) { r.Worker.Name = "" }, + want: codes.InvalidArgument, + }, + { + // A crash clears the worker but leaves assigned_node on the last + // node. Nothing is hosted there, so nothing may be minted. + name: "crashed actor on its last node", + ctx: authed, + status: &ateapipb.ActorStatus{State: ateapipb.ActorState_ACTOR_STATE_CRASHED, AssignedNode: "node-1"}, + want: codes.NotFound, + }, + { + // A revert releases the worker but keeps the assignment until it + // finalizes. The node no longer runs the actor. + name: "reverting actor with a stale assignment", + ctx: authed, + status: &ateapipb.ActorStatus{State: ateapipb.ActorState_ACTOR_STATE_REVERTING, WorkerAssignment: &ateapipb.WorkerAssignment{Worker: &ateapipb.ObjectRef{Name: "worker-1"}}}, + want: codes.NotFound, + }, + { + name: "actor hosted by no node", + ctx: authed, + status: &ateapipb.ActorStatus{State: ateapipb.ActorState_ACTOR_STATE_SUSPENDED}, + want: codes.NotFound, + }, + { + // The assignment still binds worker-gone, but its Worker record + // is gone. + name: "actor assigned to a worker the store has lost", + ctx: authed, + status: &ateapipb.ActorStatus{State: ateapipb.ActorState_ACTOR_STATE_RUNNING, WorkerAssignment: &ateapipb.WorkerAssignment{Worker: &ateapipb.ObjectRef{Name: "worker-gone"}}}, + assignments: map[string]string{actor.GetMetadata().GetUid(): "worker-gone"}, + want: codes.NotFound, + }, + { + name: "unauthenticated", + ctx: ateletauthtest.ContextWith(nil), + want: codes.Unauthenticated, + }, + { + name: "missing actor", + ctx: authed, + mutate: func(r *ateapipb.MintWorkerActorCertificateRequest) { r.Actor = nil }, + want: codes.InvalidArgument, + }, + { + name: "missing actor uid", + ctx: authed, + mutate: func(r *ateapipb.MintWorkerActorCertificateRequest) { r.ActorUid = "" }, + want: codes.InvalidArgument, + }, + { + name: "missing csr", + ctx: authed, + mutate: func(r *ateapipb.MintWorkerActorCertificateRequest) { r.CertificateSigningRequest = nil }, + want: codes.InvalidArgument, + }, + { + name: "missing worker", + ctx: authed, + mutate: func(r *ateapipb.MintWorkerActorCertificateRequest) { r.Worker = nil }, + want: codes.InvalidArgument, + }, + { + name: "worker with an atespace", + ctx: authed, + mutate: func(r *ateapipb.MintWorkerActorCertificateRequest) { r.Worker.Atespace = "team-a" }, + want: codes.InvalidArgument, + }, + { + name: "actor not found", + ctx: authed, + mutate: func(r *ateapipb.MintWorkerActorCertificateRequest) { r.Actor.Name = "nonexistent" }, + want: codes.NotFound, + }, + { + // The assigned worker asks for an incarnation that was deleted and + // recreated. + name: "stale actor uid", + ctx: authed, + mutate: func(r *ateapipb.MintWorkerActorCertificateRequest) { r.ActorUid = staleUID }, + want: codes.Aborted, }, } - + hostedStatus := actor.GetStatus() + hostedAssignments := st.assignments for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - _, err := s.MintWorkerActorCertificate(tc.ctx, tc.req) + actor.Status = hostedStatus + if tc.status != nil { + actor.Status = tc.status + } + st.assignments = hostedAssignments + if tc.assignments != nil { + st.assignments = tc.assignments + } + st.assignmentErr = tc.assignmentErr + req := valid() + if tc.mutate != nil { + tc.mutate(req) + } + _, err := s.MintWorkerActorCertificate(tc.ctx, req) if got := apierror.Code(err); got != tc.want { - t.Errorf("code = %v (err %v), want %v", got, err, tc.want) + t.Errorf("MintWorkerActorCertificate() code = %v, want %v (err: %v)", got, tc.want, err) } }) } diff --git a/cmd/ateapi/internal/workerservice/jwt.go b/cmd/ateapi/internal/workerservice/jwt.go index be85b4666b..a5c35b0cf3 100644 --- a/cmd/ateapi/internal/workerservice/jwt.go +++ b/cmd/ateapi/internal/workerservice/jwt.go @@ -59,7 +59,7 @@ func (s *Server) MintWorkerActorJwt(ctx context.Context, req *ateapipb.MintWorke // The node check comes first so a node that does not host the actor gets // NotFound whatever UID it sends, and cannot use the UID check to learn // that the actor exists. - if err := s.checkActorOnNode(ctx, dbActor, caller.NodeName); err != nil { + if err := s.checkActorOnNode(ctx, dbActor, caller.NodeName, "JWT"); err != nil { return nil, err } // A stale UID is Aborted, as in MintWorkerActorCertificate. @@ -101,14 +101,14 @@ func (s *Server) MintWorkerActorJwt(ctx context.Context, req *ateapipb.MintWorke // checkActorOnNode refuses a mint from an atelet whose node does not host the // actor, as RequestActorSuspend does. Authoritative state decides, never the // request. The refusal is NotFound, so a node learns nothing about actors it -// does not host. -func (s *Server) checkActorOnNode(ctx context.Context, actor *ateapipb.Actor, callerNode string) error { +// does not host. credential names what the caller mints, for the refusal log. +func (s *Server) checkActorOnNode(ctx context.Context, actor *ateapipb.Actor, callerNode, credential string) error { hostingNode, err := controlapi.HostingNode(ctx, s.store, actor) if err != nil { return err } if hostingNode == "" || hostingNode != callerNode { - slog.WarnContext(ctx, "Refusing a worker-for-actor JWT for an actor the caller's node does not host", + slog.WarnContext(ctx, "Refusing a worker-for-actor "+credential+" for an actor the caller's node does not host", slog.String("actor", resources.ActorRefFromActor(actor).String()), slog.String("hosting_node", hostingNode), slog.String("caller_node", callerNode)) diff --git a/cmd/ateapi/internal/workerservice/jwt_test.go b/cmd/ateapi/internal/workerservice/jwt_test.go index 4f17f2b459..11ce3fbe63 100644 --- a/cmd/ateapi/internal/workerservice/jwt_test.go +++ b/cmd/ateapi/internal/workerservice/jwt_test.go @@ -15,10 +15,12 @@ package workerservice import ( + "bytes" "context" "encoding/base64" "encoding/json" "errors" + "log/slog" "strings" "testing" "time" @@ -409,3 +411,62 @@ func TestMintWorkerActorJwt_Errors(t *testing.T) { }) } } + +// TestNodeRefusalLogNamesCredential checks that each mint's node refusal logs +// the credential that RPC mints. +func TestNodeRefusalLogNamesCredential(t *testing.T) { + var buf bytes.Buffer + prev := slog.Default() + slog.SetDefault(slog.New(slog.NewTextHandler(&buf, nil))) + t.Cleanup(func() { slog.SetDefault(prev) }) + + st, actor := newFakeStoreWithActor("team-a", "my-actor", "3b9f1e77-2c4d-4a80-91be-6d5c8f0a7e21") + csr, _ := generateTestCSR(t) + s := New(st, &fakeSuspender{}, testAteletSPIFFEID, newTestCAPool(t), testActorJWTIssuer, newTestJWTPool(t)) + // The actor's worker runs on node-1. + otherNode := ateletauthtest.ContextWith(ateletauthtest.CertOn(t, "node-2")) + actorRef := &ateapipb.ObjectRef{Atespace: "team-a", Name: "my-actor"} + + tests := []struct { + name string + mint func() error + want string + }{ + { + name: "MintWorkerActorJwt", + mint: func() error { + _, err := s.MintWorkerActorJwt(otherNode, &ateapipb.MintWorkerActorJwtRequest{ + Actor: actorRef, + ActorUid: actor.GetMetadata().GetUid(), + Audiences: []string{"snapshot-plugin"}, + }) + return err + }, + want: "Refusing a worker-for-actor JWT for an actor the caller's node does not host", + }, + { + name: "MintWorkerActorCertificate", + mint: func() error { + _, err := s.MintWorkerActorCertificate(otherNode, &ateapipb.MintWorkerActorCertificateRequest{ + Actor: actorRef, + ActorUid: actor.GetMetadata().GetUid(), + CertificateSigningRequest: csr, + Worker: &ateapipb.ObjectRef{Name: "worker-1"}, + }) + return err + }, + want: "Refusing a worker-for-actor certificate for an actor the caller's node does not host", + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + buf.Reset() + if got := apierror.Code(tc.mint()); got != codes.NotFound { + t.Fatalf("code = %v, want %v", got, codes.NotFound) + } + if !strings.Contains(buf.String(), tc.want) { + t.Errorf("log = %q, want it to contain %q", buf.String(), tc.want) + } + }) + } +} diff --git a/cmd/atelet/workersupport.go b/cmd/atelet/workersupport.go index aa3bcad262..0e05b7cc48 100644 --- a/cmd/atelet/workersupport.go +++ b/cmd/atelet/workersupport.go @@ -38,8 +38,10 @@ type workerSupportServer struct { } func (b *workerSupportServer) MintActorCertificate(ctx context.Context, req *ateletpb.MintActorCertificateRequest) (*ateletpb.MintActorCertificateResponse, error) { - // Check which worker is calling. - _, err := authenticatedWorkerIdentity(ctx) + // Identity comes only from the mTLS certificate, never from the request: + // the control plane mints only for the Worker that the actor is assigned + // to, so a worker gets certificates for the actor it hosts and no other. + workerIdentity, err := authenticatedWorkerIdentity(ctx) if err != nil { return nil, err } @@ -50,11 +52,9 @@ func (b *workerSupportServer) MintActorCertificate(ctx context.Context, req *ate return nil, resources.ToGRPCStatusError(errs) } - // TODO(identity): Check that we believe that this worker is running the - // requested actor? ate-api-server will further check that we (the atelet) - // are allowed to request a certificate for the actor. - resp, err := b.workers.MintWorkerActorCertificate(ctx, &ateapipb.MintWorkerActorCertificateRequest{ + // Workers are global-scoped and named by their pod UID. + Worker: &ateapipb.ObjectRef{Name: workerIdentity.PodUID}, Actor: &ateapipb.ObjectRef{ Atespace: req.GetActorAtespace(), Name: req.GetActorName(), diff --git a/cmd/atelet/workersupport_test.go b/cmd/atelet/workersupport_test.go index 2215bbb79c..29c98e6c4d 100644 --- a/cmd/atelet/workersupport_test.go +++ b/cmd/atelet/workersupport_test.go @@ -333,6 +333,8 @@ func TestMintActorCertificateForwardsToWorkerService(t *testing.T) { } want := []*ateapipb.MintWorkerActorCertificateRequest{{ + // The worker comes from the caller's certificate. + Worker: &ateapipb.ObjectRef{Name: "pod-a"}, Actor: &ateapipb.ObjectRef{ Atespace: "team-a", Name: "actor-1", diff --git a/cmd/benchmarking/isolate/fake-atelet/herder.go b/cmd/benchmarking/isolate/fake-atelet/herder.go index ec25740f43..9006b6889d 100644 --- a/cmd/benchmarking/isolate/fake-atelet/herder.go +++ b/cmd/benchmarking/isolate/fake-atelet/herder.go @@ -95,7 +95,7 @@ func newActorCSR() ([]byte, error) { // egress gateway, as worker does before it starts the workload. Its time is // ate-api-server's, so it adds to the call's delay rather than running inside // it; a failure fails the call, as tunneled egress fails closed in worker. -func (h *herder) mintActorCertificate(ctx context.Context, atespace, name, uid string, gateway *ateletpb.EgressGateway) error { +func (h *herder) mintActorCertificate(ctx context.Context, workerPodUID, atespace, name, uid string, gateway *ateletpb.EgressGateway) error { if h.minter == nil || gateway == nil { return nil } @@ -103,6 +103,8 @@ func (h *herder) mintActorCertificate(ctx context.Context, atespace, name, uid s Actor: &ateapipb.ObjectRef{Atespace: atespace, Name: name}, ActorUid: uid, CertificateSigningRequest: h.csr, + // Workers are global-scoped and named by their pod UID. + Worker: &ateapipb.ObjectRef{Name: workerPodUID}, }); err != nil { return fmt.Errorf("while minting the certificate of actor %s/%s: %w", atespace, name, err) } @@ -150,7 +152,7 @@ func (h *herder) writePlaceholder(ctx context.Context, start time.Time, d time.D } func (h *herder) Run(ctx context.Context, req *ateletpb.RunRequest) (*ateletpb.RunResponse, error) { - if err := h.mintActorCertificate(ctx, req.GetAtespace(), req.GetActorName(), req.GetActorUid(), req.GetEgressGateway()); err != nil { + if err := h.mintActorCertificate(ctx, req.GetWorkerPodUid(), req.GetAtespace(), req.GetActorName(), req.GetActorUid(), req.GetEgressGateway()); err != nil { return nil, err } if err := wait(ctx, time.Now(), h.delays.run); err != nil { @@ -160,7 +162,7 @@ func (h *herder) Run(ctx context.Context, req *ateletpb.RunRequest) (*ateletpb.R } func (h *herder) Restore(ctx context.Context, req *ateletpb.RestoreRequest) (*ateletpb.RestoreResponse, error) { - if err := h.mintActorCertificate(ctx, req.GetAtespace(), req.GetActorName(), req.GetActorUid(), req.GetEgressGateway()); err != nil { + if err := h.mintActorCertificate(ctx, req.GetWorkerPodUid(), req.GetAtespace(), req.GetActorName(), req.GetActorUid(), req.GetEgressGateway()); err != nil { return nil, err } if err := wait(ctx, time.Now(), h.delays.restore); err != nil { diff --git a/cmd/benchmarking/isolate/fake-atelet/herder_test.go b/cmd/benchmarking/isolate/fake-atelet/herder_test.go index 0268c0f6c6..f2e197fc58 100644 --- a/cmd/benchmarking/isolate/fake-atelet/herder_test.go +++ b/cmd/benchmarking/isolate/fake-atelet/herder_test.go @@ -285,7 +285,10 @@ func (m *recordingMinter) mints() []*ateapipb.MintWorkerActorCertificateRequest return slices.Clone(m.reqs) } -const testActorUID = "0f9c1d2e-3b4a-4c5d-8e6f-7a8b9c0d1e2f" +const ( + testActorUID = "0f9c1d2e-3b4a-4c5d-8e6f-7a8b9c0d1e2f" + testWorkerPodUID = "5d4c3b2a-1f0e-4d9c-8b7a-6f5e4d3c2b1a" +) var testGateway = &ateletpb.EgressGateway{Address: "atenet-egress.ate-system.svc:443"} @@ -293,11 +296,11 @@ var testGateway = &ateletpb.EgressGateway{Address: "atenet-egress.ate-system.svc func activations(h *herder, gateway *ateletpb.EgressGateway) map[string]func(context.Context) error { return map[string]func(context.Context) error{ "Run": func(ctx context.Context) error { - _, err := h.Run(ctx, &ateletpb.RunRequest{Atespace: "team-a", ActorName: "sleep-1", ActorUid: testActorUID, EgressGateway: gateway}) + _, err := h.Run(ctx, &ateletpb.RunRequest{WorkerPodUid: testWorkerPodUID, Atespace: "team-a", ActorName: "sleep-1", ActorUid: testActorUID, EgressGateway: gateway}) return err }, "Restore": func(ctx context.Context) error { - _, err := h.Restore(ctx, &ateletpb.RestoreRequest{Atespace: "team-a", ActorName: "sleep-1", ActorUid: testActorUID, EgressGateway: gateway}) + _, err := h.Restore(ctx, &ateletpb.RestoreRequest{WorkerPodUid: testWorkerPodUID, Atespace: "team-a", ActorName: "sleep-1", ActorUid: testActorUID, EgressGateway: gateway}) return err }, } @@ -330,6 +333,10 @@ func TestHerderMintsActorCertificateOnActivation(t *testing.T) { if !proto.Equal(got[0].GetActor(), want) || got[0].GetActorUid() != testActorUID { t.Errorf("%s minted for %v/%q, want %v/%q", name, got[0].GetActor(), got[0].GetActorUid(), want, testActorUID) } + // The control plane mints only for the worker hosting the actor. + if got, want := got[0].GetWorker(), (&ateapipb.ObjectRef{Name: testWorkerPodUID}); !proto.Equal(got, want) { + t.Errorf("%s minted as worker %v, want %v", name, got, want) + } // ate-api-server parses the CSR and checks its signature. csr, err := x509.ParseCertificateRequest(got[0].GetCertificateSigningRequest()) if err != nil { diff --git a/pkg/proto/ateapipb/ateapi.pb.go b/pkg/proto/ateapipb/ateapi.pb.go index e85d01b84e..1af170a359 100644 --- a/pkg/proto/ateapipb/ateapi.pb.go +++ b/pkg/proto/ateapipb/ateapi.pb.go @@ -8008,8 +8008,16 @@ type MintWorkerActorCertificateRequest struct { // +k8s:required // +k8s:maxBytes=16384 CertificateSigningRequest []byte `protobuf:"bytes,3,opt,name=certificate_signing_request,json=certificateSigningRequest,proto3" json:"certificate_signing_request,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // The Worker requesting the certificate. atespace is always empty; Workers + // are global-scoped. atelet fills this in from its caller's certificate, so + // it names the Worker that certificate proves the request came from. It + // must be the Worker that the actor is assigned to. + // + // +k8s:required + // +k8s:beta(since: "0.0")=+k8s:subfield(atespace)=+k8s:forbidden # TODO: get rid of beta prefix + Worker *ObjectRef `protobuf:"bytes,4,opt,name=worker,proto3" json:"worker,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *MintWorkerActorCertificateRequest) Reset() { @@ -8063,6 +8071,13 @@ func (x *MintWorkerActorCertificateRequest) GetCertificateSigningRequest() []byt return nil } +func (x *MintWorkerActorCertificateRequest) GetWorker() *ObjectRef { + if x != nil { + return x.Worker + } + return nil +} + // Response message for MintWorkerActorCertificate. type MintWorkerActorCertificateResponse struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -8259,7 +8274,7 @@ type RequestActorSuspendRequest struct { state protoimpl.MessageState `protogen:"open.v1"` // The Worker making the request. atespace is always empty; Workers are // global-scoped. atelet fills this in from its caller's certificate, so it - // names the Worker the request provably came from. + // names the Worker that certificate proves the request came from. // // +k8s:required // +k8s:beta(since: "0.0")=+k8s:subfield(atespace)=+k8s:forbidden # TODO: get rid of beta prefix @@ -9783,11 +9798,12 @@ const file_ateapi_proto_rawDesc = "" + "\x0fdefault_runtime\x18\x03 \x01(\v2\x16.ateapi.SandboxRuntimeR\x0edefaultRuntime\x12G\n" + "\x13restorable_runtimes\x18\x04 \x03(\v2\x16.ateapi.SandboxRuntimeR\x12restorableRuntimes\"@\n" + "\x16RegisterWorkerResponse\x12&\n" + - "\x06worker\x18\x01 \x01(\v2\x0e.ateapi.WorkerR\x06worker\"\xa9\x01\n" + + "\x06worker\x18\x01 \x01(\v2\x0e.ateapi.WorkerR\x06worker\"\xd4\x01\n" + "!MintWorkerActorCertificateRequest\x12'\n" + "\x05actor\x18\x01 \x01(\v2\x11.ateapi.ObjectRefR\x05actor\x12\x1b\n" + "\tactor_uid\x18\x02 \x01(\tR\bactorUid\x12>\n" + - "\x1bcertificate_signing_request\x18\x03 \x01(\fR\x19certificateSigningRequest\"S\n" + + "\x1bcertificate_signing_request\x18\x03 \x01(\fR\x19certificateSigningRequest\x12)\n" + + "\x06worker\x18\x04 \x01(\v2\x11.ateapi.ObjectRefR\x06worker\"S\n" + "\"MintWorkerActorCertificateResponse\x12-\n" + "\x12actor_certificates\x18\x01 \x03(\fR\x11actorCertificates\"\x7f\n" + "\x19MintWorkerActorJwtRequest\x12'\n" + @@ -10323,140 +10339,141 @@ var file_ateapi_proto_depIdxs = []int32{ 113, // 150: ateapi.RegisterWorkerRequest.restorable_runtimes:type_name -> ateapi.SandboxRuntime 111, // 151: ateapi.RegisterWorkerResponse.worker:type_name -> ateapi.Worker 32, // 152: ateapi.MintWorkerActorCertificateRequest.actor:type_name -> ateapi.ObjectRef - 32, // 153: ateapi.MintWorkerActorJwtRequest.actor:type_name -> ateapi.ObjectRef - 144, // 154: ateapi.MintWorkerActorJwtResponse.expire_time:type_name -> google.protobuf.Timestamp - 32, // 155: ateapi.RequestActorSuspendRequest.worker:type_name -> ateapi.ObjectRef - 32, // 156: ateapi.RequestActorSuspendRequest.actor:type_name -> ateapi.ObjectRef - 15, // 157: ateapi.RequestActorSuspendResponse.actor:type_name -> ateapi.Actor - 32, // 158: ateapi.ReportActorSnapshotStateRequest.actor:type_name -> ateapi.ObjectRef - 128, // 159: ateapi.ReportActorSnapshotStateRequest.state:type_name -> ateapi.ActorSnapshotState - 129, // 160: ateapi.ActorSnapshotState.latest_local:type_name -> ateapi.SnapshotReport - 129, // 161: ateapi.ActorSnapshotState.uploading:type_name -> ateapi.SnapshotReport - 129, // 162: ateapi.ActorSnapshotState.latest_durable:type_name -> ateapi.SnapshotReport - 144, // 163: ateapi.ActorSnapshotState.upload_due_time:type_name -> google.protobuf.Timestamp - 2, // 164: ateapi.SnapshotReport.fidelity:type_name -> ateapi.SnapshotFidelity - 9, // 165: ateapi.SnapshotReport.object:type_name -> ateapi.ObjectSnapshot - 144, // 166: ateapi.SnapshotReport.local_commit_time:type_name -> google.protobuf.Timestamp - 144, // 167: ateapi.SnapshotReport.durable_complete_time:type_name -> google.protobuf.Timestamp - 13, // 168: ateapi.AccessPolicy.metadata:type_name -> ateapi.ResourceMetadata - 131, // 169: ateapi.AccessPolicy.bindings:type_name -> ateapi.Binding - 130, // 170: ateapi.CreateGlobalAccessPolicyRequest.access_policy:type_name -> ateapi.AccessPolicy - 130, // 171: ateapi.UpdateGlobalAccessPolicyRequest.access_policy:type_name -> ateapi.AccessPolicy - 32, // 172: ateapi.GetAtespaceAccessPolicyRequest.atespace:type_name -> ateapi.ObjectRef - 32, // 173: ateapi.CreateAtespaceAccessPolicyRequest.atespace:type_name -> ateapi.ObjectRef - 130, // 174: ateapi.CreateAtespaceAccessPolicyRequest.access_policy:type_name -> ateapi.AccessPolicy - 32, // 175: ateapi.UpdateAtespaceAccessPolicyRequest.atespace:type_name -> ateapi.ObjectRef - 130, // 176: ateapi.UpdateAtespaceAccessPolicyRequest.access_policy:type_name -> ateapi.AccessPolicy - 32, // 177: ateapi.DeleteAtespaceAccessPolicyRequest.atespace:type_name -> ateapi.ObjectRef - 93, // 178: ateapi.DeleteAtespaceAccessPolicyRequest.options:type_name -> ateapi.DeleteOptions - 8, // 179: ateapi.ResourceAnnotation.scope:type_name -> ateapi.Scope - 145, // 180: ateapi.resource:extendee -> google.protobuf.MessageOptions - 146, // 181: ateapi.method:extendee -> google.protobuf.MethodOptions - 139, // 182: ateapi.resource:type_name -> ateapi.ResourceAnnotation - 140, // 183: ateapi.method:type_name -> ateapi.MethodAnnotation - 67, // 184: ateapi.Control.GetActor:input_type -> ateapi.GetActorRequest - 68, // 185: ateapi.Control.CreateActor:input_type -> ateapi.CreateActorRequest - 69, // 186: ateapi.Control.UpdateActor:input_type -> ateapi.UpdateActorRequest - 70, // 187: ateapi.Control.SuspendActor:input_type -> ateapi.SuspendActorRequest - 72, // 188: ateapi.Control.PauseActor:input_type -> ateapi.PauseActorRequest - 74, // 189: ateapi.Control.ResumeActor:input_type -> ateapi.ResumeActorRequest - 76, // 190: ateapi.Control.RevertActor:input_type -> ateapi.RevertActorRequest - 78, // 191: ateapi.Control.DeleteActor:input_type -> ateapi.DeleteActorRequest - 79, // 192: ateapi.Control.GetActorEgressPolicy:input_type -> ateapi.GetActorEgressPolicyRequest - 80, // 193: ateapi.Control.CreateActorEgressPolicy:input_type -> ateapi.CreateActorEgressPolicyRequest - 81, // 194: ateapi.Control.UpdateActorEgressPolicy:input_type -> ateapi.UpdateActorEgressPolicyRequest - 82, // 195: ateapi.Control.DeleteActorEgressPolicy:input_type -> ateapi.DeleteActorEgressPolicyRequest - 84, // 196: ateapi.Control.MintActorJwt:input_type -> ateapi.MintActorJwtRequest - 86, // 197: ateapi.Control.MintActorCertificate:input_type -> ateapi.MintActorCertificateRequest - 90, // 198: ateapi.Control.CreateTag:input_type -> ateapi.CreateTagRequest - 83, // 199: ateapi.Control.GetTag:input_type -> ateapi.GetTagRequest - 88, // 200: ateapi.Control.ListTags:input_type -> ateapi.ListTagsRequest - 91, // 201: ateapi.Control.UpdateTag:input_type -> ateapi.UpdateTagRequest - 92, // 202: ateapi.Control.DeleteTag:input_type -> ateapi.DeleteTagRequest - 97, // 203: ateapi.Control.ListWorkers:input_type -> ateapi.ListWorkersRequest - 99, // 204: ateapi.Control.GetWorker:input_type -> ateapi.GetWorkerRequest - 100, // 205: ateapi.Control.CreateWorker:input_type -> ateapi.CreateWorkerRequest - 101, // 206: ateapi.Control.UpdateWorker:input_type -> ateapi.UpdateWorkerRequest - 102, // 207: ateapi.Control.DeleteWorker:input_type -> ateapi.DeleteWorkerRequest - 103, // 208: ateapi.Control.DrainWorker:input_type -> ateapi.DrainWorkerRequest - 105, // 209: ateapi.Control.MarkWorkerAvailable:input_type -> ateapi.MarkWorkerAvailableRequest - 107, // 210: ateapi.Control.MarkWorkerUnavailable:input_type -> ateapi.MarkWorkerUnavailableRequest - 95, // 211: ateapi.Control.ListWorkerActorAssignments:input_type -> ateapi.ListWorkerActorAssignmentsRequest - 109, // 212: ateapi.Control.ListActors:input_type -> ateapi.ListActorsRequest - 57, // 213: ateapi.Control.CreateAtespace:input_type -> ateapi.CreateAtespaceRequest - 58, // 214: ateapi.Control.GetAtespace:input_type -> ateapi.GetAtespaceRequest - 59, // 215: ateapi.Control.ListAtespaces:input_type -> ateapi.ListAtespacesRequest - 61, // 216: ateapi.Control.DeleteAtespace:input_type -> ateapi.DeleteAtespaceRequest - 62, // 217: ateapi.Control.CreateActorTemplate:input_type -> ateapi.CreateActorTemplateRequest - 63, // 218: ateapi.Control.GetActorTemplate:input_type -> ateapi.GetActorTemplateRequest - 64, // 219: ateapi.Control.ListActorTemplates:input_type -> ateapi.ListActorTemplatesRequest - 66, // 220: ateapi.Control.DeleteActorTemplate:input_type -> ateapi.DeleteActorTemplateRequest - 132, // 221: ateapi.Control.GetGlobalAccessPolicy:input_type -> ateapi.GetGlobalAccessPolicyRequest - 133, // 222: ateapi.Control.CreateGlobalAccessPolicy:input_type -> ateapi.CreateGlobalAccessPolicyRequest - 134, // 223: ateapi.Control.UpdateGlobalAccessPolicy:input_type -> ateapi.UpdateGlobalAccessPolicyRequest - 135, // 224: ateapi.Control.GetAtespaceAccessPolicy:input_type -> ateapi.GetAtespaceAccessPolicyRequest - 136, // 225: ateapi.Control.CreateAtespaceAccessPolicy:input_type -> ateapi.CreateAtespaceAccessPolicyRequest - 137, // 226: ateapi.Control.UpdateAtespaceAccessPolicy:input_type -> ateapi.UpdateAtespaceAccessPolicyRequest - 138, // 227: ateapi.Control.DeleteAtespaceAccessPolicy:input_type -> ateapi.DeleteAtespaceAccessPolicyRequest - 118, // 228: ateapi.WorkerService.RegisterWorker:input_type -> ateapi.RegisterWorkerRequest - 120, // 229: ateapi.WorkerService.MintWorkerActorCertificate:input_type -> ateapi.MintWorkerActorCertificateRequest - 122, // 230: ateapi.WorkerService.MintWorkerActorJwt:input_type -> ateapi.MintWorkerActorJwtRequest - 124, // 231: ateapi.WorkerService.RequestActorSuspend:input_type -> ateapi.RequestActorSuspendRequest - 126, // 232: ateapi.WorkerService.ReportActorSnapshotState:input_type -> ateapi.ReportActorSnapshotStateRequest - 15, // 233: ateapi.Control.GetActor:output_type -> ateapi.Actor - 15, // 234: ateapi.Control.CreateActor:output_type -> ateapi.Actor - 15, // 235: ateapi.Control.UpdateActor:output_type -> ateapi.Actor - 71, // 236: ateapi.Control.SuspendActor:output_type -> ateapi.SuspendActorResponse - 73, // 237: ateapi.Control.PauseActor:output_type -> ateapi.PauseActorResponse - 75, // 238: ateapi.Control.ResumeActor:output_type -> ateapi.ResumeActorResponse - 77, // 239: ateapi.Control.RevertActor:output_type -> ateapi.RevertActorResponse - 15, // 240: ateapi.Control.DeleteActor:output_type -> ateapi.Actor - 16, // 241: ateapi.Control.GetActorEgressPolicy:output_type -> ateapi.EgressPolicy - 16, // 242: ateapi.Control.CreateActorEgressPolicy:output_type -> ateapi.EgressPolicy - 16, // 243: ateapi.Control.UpdateActorEgressPolicy:output_type -> ateapi.EgressPolicy - 16, // 244: ateapi.Control.DeleteActorEgressPolicy:output_type -> ateapi.EgressPolicy - 85, // 245: ateapi.Control.MintActorJwt:output_type -> ateapi.MintActorJwtResponse - 87, // 246: ateapi.Control.MintActorCertificate:output_type -> ateapi.MintActorCertificateResponse - 30, // 247: ateapi.Control.CreateTag:output_type -> ateapi.Tag - 30, // 248: ateapi.Control.GetTag:output_type -> ateapi.Tag - 89, // 249: ateapi.Control.ListTags:output_type -> ateapi.ListTagsResponse - 30, // 250: ateapi.Control.UpdateTag:output_type -> ateapi.Tag - 30, // 251: ateapi.Control.DeleteTag:output_type -> ateapi.Tag - 98, // 252: ateapi.Control.ListWorkers:output_type -> ateapi.ListWorkersResponse - 111, // 253: ateapi.Control.GetWorker:output_type -> ateapi.Worker - 111, // 254: ateapi.Control.CreateWorker:output_type -> ateapi.Worker - 111, // 255: ateapi.Control.UpdateWorker:output_type -> ateapi.Worker - 111, // 256: ateapi.Control.DeleteWorker:output_type -> ateapi.Worker - 104, // 257: ateapi.Control.DrainWorker:output_type -> ateapi.DrainWorkerResponse - 106, // 258: ateapi.Control.MarkWorkerAvailable:output_type -> ateapi.MarkWorkerAvailableResponse - 108, // 259: ateapi.Control.MarkWorkerUnavailable:output_type -> ateapi.MarkWorkerUnavailableResponse - 96, // 260: ateapi.Control.ListWorkerActorAssignments:output_type -> ateapi.ListWorkerActorAssignmentsResponse - 110, // 261: ateapi.Control.ListActors:output_type -> ateapi.ListActorsResponse - 31, // 262: ateapi.Control.CreateAtespace:output_type -> ateapi.Atespace - 31, // 263: ateapi.Control.GetAtespace:output_type -> ateapi.Atespace - 60, // 264: ateapi.Control.ListAtespaces:output_type -> ateapi.ListAtespacesResponse - 31, // 265: ateapi.Control.DeleteAtespace:output_type -> ateapi.Atespace - 33, // 266: ateapi.Control.CreateActorTemplate:output_type -> ateapi.ActorTemplate - 33, // 267: ateapi.Control.GetActorTemplate:output_type -> ateapi.ActorTemplate - 65, // 268: ateapi.Control.ListActorTemplates:output_type -> ateapi.ListActorTemplatesResponse - 33, // 269: ateapi.Control.DeleteActorTemplate:output_type -> ateapi.ActorTemplate - 130, // 270: ateapi.Control.GetGlobalAccessPolicy:output_type -> ateapi.AccessPolicy - 130, // 271: ateapi.Control.CreateGlobalAccessPolicy:output_type -> ateapi.AccessPolicy - 130, // 272: ateapi.Control.UpdateGlobalAccessPolicy:output_type -> ateapi.AccessPolicy - 130, // 273: ateapi.Control.GetAtespaceAccessPolicy:output_type -> ateapi.AccessPolicy - 130, // 274: ateapi.Control.CreateAtespaceAccessPolicy:output_type -> ateapi.AccessPolicy - 130, // 275: ateapi.Control.UpdateAtespaceAccessPolicy:output_type -> ateapi.AccessPolicy - 130, // 276: ateapi.Control.DeleteAtespaceAccessPolicy:output_type -> ateapi.AccessPolicy - 119, // 277: ateapi.WorkerService.RegisterWorker:output_type -> ateapi.RegisterWorkerResponse - 121, // 278: ateapi.WorkerService.MintWorkerActorCertificate:output_type -> ateapi.MintWorkerActorCertificateResponse - 123, // 279: ateapi.WorkerService.MintWorkerActorJwt:output_type -> ateapi.MintWorkerActorJwtResponse - 125, // 280: ateapi.WorkerService.RequestActorSuspend:output_type -> ateapi.RequestActorSuspendResponse - 127, // 281: ateapi.WorkerService.ReportActorSnapshotState:output_type -> ateapi.ReportActorSnapshotStateResponse - 233, // [233:282] is the sub-list for method output_type - 184, // [184:233] is the sub-list for method input_type - 182, // [182:184] is the sub-list for extension type_name - 180, // [180:182] is the sub-list for extension extendee - 0, // [0:180] is the sub-list for field type_name + 32, // 153: ateapi.MintWorkerActorCertificateRequest.worker:type_name -> ateapi.ObjectRef + 32, // 154: ateapi.MintWorkerActorJwtRequest.actor:type_name -> ateapi.ObjectRef + 144, // 155: ateapi.MintWorkerActorJwtResponse.expire_time:type_name -> google.protobuf.Timestamp + 32, // 156: ateapi.RequestActorSuspendRequest.worker:type_name -> ateapi.ObjectRef + 32, // 157: ateapi.RequestActorSuspendRequest.actor:type_name -> ateapi.ObjectRef + 15, // 158: ateapi.RequestActorSuspendResponse.actor:type_name -> ateapi.Actor + 32, // 159: ateapi.ReportActorSnapshotStateRequest.actor:type_name -> ateapi.ObjectRef + 128, // 160: ateapi.ReportActorSnapshotStateRequest.state:type_name -> ateapi.ActorSnapshotState + 129, // 161: ateapi.ActorSnapshotState.latest_local:type_name -> ateapi.SnapshotReport + 129, // 162: ateapi.ActorSnapshotState.uploading:type_name -> ateapi.SnapshotReport + 129, // 163: ateapi.ActorSnapshotState.latest_durable:type_name -> ateapi.SnapshotReport + 144, // 164: ateapi.ActorSnapshotState.upload_due_time:type_name -> google.protobuf.Timestamp + 2, // 165: ateapi.SnapshotReport.fidelity:type_name -> ateapi.SnapshotFidelity + 9, // 166: ateapi.SnapshotReport.object:type_name -> ateapi.ObjectSnapshot + 144, // 167: ateapi.SnapshotReport.local_commit_time:type_name -> google.protobuf.Timestamp + 144, // 168: ateapi.SnapshotReport.durable_complete_time:type_name -> google.protobuf.Timestamp + 13, // 169: ateapi.AccessPolicy.metadata:type_name -> ateapi.ResourceMetadata + 131, // 170: ateapi.AccessPolicy.bindings:type_name -> ateapi.Binding + 130, // 171: ateapi.CreateGlobalAccessPolicyRequest.access_policy:type_name -> ateapi.AccessPolicy + 130, // 172: ateapi.UpdateGlobalAccessPolicyRequest.access_policy:type_name -> ateapi.AccessPolicy + 32, // 173: ateapi.GetAtespaceAccessPolicyRequest.atespace:type_name -> ateapi.ObjectRef + 32, // 174: ateapi.CreateAtespaceAccessPolicyRequest.atespace:type_name -> ateapi.ObjectRef + 130, // 175: ateapi.CreateAtespaceAccessPolicyRequest.access_policy:type_name -> ateapi.AccessPolicy + 32, // 176: ateapi.UpdateAtespaceAccessPolicyRequest.atespace:type_name -> ateapi.ObjectRef + 130, // 177: ateapi.UpdateAtespaceAccessPolicyRequest.access_policy:type_name -> ateapi.AccessPolicy + 32, // 178: ateapi.DeleteAtespaceAccessPolicyRequest.atespace:type_name -> ateapi.ObjectRef + 93, // 179: ateapi.DeleteAtespaceAccessPolicyRequest.options:type_name -> ateapi.DeleteOptions + 8, // 180: ateapi.ResourceAnnotation.scope:type_name -> ateapi.Scope + 145, // 181: ateapi.resource:extendee -> google.protobuf.MessageOptions + 146, // 182: ateapi.method:extendee -> google.protobuf.MethodOptions + 139, // 183: ateapi.resource:type_name -> ateapi.ResourceAnnotation + 140, // 184: ateapi.method:type_name -> ateapi.MethodAnnotation + 67, // 185: ateapi.Control.GetActor:input_type -> ateapi.GetActorRequest + 68, // 186: ateapi.Control.CreateActor:input_type -> ateapi.CreateActorRequest + 69, // 187: ateapi.Control.UpdateActor:input_type -> ateapi.UpdateActorRequest + 70, // 188: ateapi.Control.SuspendActor:input_type -> ateapi.SuspendActorRequest + 72, // 189: ateapi.Control.PauseActor:input_type -> ateapi.PauseActorRequest + 74, // 190: ateapi.Control.ResumeActor:input_type -> ateapi.ResumeActorRequest + 76, // 191: ateapi.Control.RevertActor:input_type -> ateapi.RevertActorRequest + 78, // 192: ateapi.Control.DeleteActor:input_type -> ateapi.DeleteActorRequest + 79, // 193: ateapi.Control.GetActorEgressPolicy:input_type -> ateapi.GetActorEgressPolicyRequest + 80, // 194: ateapi.Control.CreateActorEgressPolicy:input_type -> ateapi.CreateActorEgressPolicyRequest + 81, // 195: ateapi.Control.UpdateActorEgressPolicy:input_type -> ateapi.UpdateActorEgressPolicyRequest + 82, // 196: ateapi.Control.DeleteActorEgressPolicy:input_type -> ateapi.DeleteActorEgressPolicyRequest + 84, // 197: ateapi.Control.MintActorJwt:input_type -> ateapi.MintActorJwtRequest + 86, // 198: ateapi.Control.MintActorCertificate:input_type -> ateapi.MintActorCertificateRequest + 90, // 199: ateapi.Control.CreateTag:input_type -> ateapi.CreateTagRequest + 83, // 200: ateapi.Control.GetTag:input_type -> ateapi.GetTagRequest + 88, // 201: ateapi.Control.ListTags:input_type -> ateapi.ListTagsRequest + 91, // 202: ateapi.Control.UpdateTag:input_type -> ateapi.UpdateTagRequest + 92, // 203: ateapi.Control.DeleteTag:input_type -> ateapi.DeleteTagRequest + 97, // 204: ateapi.Control.ListWorkers:input_type -> ateapi.ListWorkersRequest + 99, // 205: ateapi.Control.GetWorker:input_type -> ateapi.GetWorkerRequest + 100, // 206: ateapi.Control.CreateWorker:input_type -> ateapi.CreateWorkerRequest + 101, // 207: ateapi.Control.UpdateWorker:input_type -> ateapi.UpdateWorkerRequest + 102, // 208: ateapi.Control.DeleteWorker:input_type -> ateapi.DeleteWorkerRequest + 103, // 209: ateapi.Control.DrainWorker:input_type -> ateapi.DrainWorkerRequest + 105, // 210: ateapi.Control.MarkWorkerAvailable:input_type -> ateapi.MarkWorkerAvailableRequest + 107, // 211: ateapi.Control.MarkWorkerUnavailable:input_type -> ateapi.MarkWorkerUnavailableRequest + 95, // 212: ateapi.Control.ListWorkerActorAssignments:input_type -> ateapi.ListWorkerActorAssignmentsRequest + 109, // 213: ateapi.Control.ListActors:input_type -> ateapi.ListActorsRequest + 57, // 214: ateapi.Control.CreateAtespace:input_type -> ateapi.CreateAtespaceRequest + 58, // 215: ateapi.Control.GetAtespace:input_type -> ateapi.GetAtespaceRequest + 59, // 216: ateapi.Control.ListAtespaces:input_type -> ateapi.ListAtespacesRequest + 61, // 217: ateapi.Control.DeleteAtespace:input_type -> ateapi.DeleteAtespaceRequest + 62, // 218: ateapi.Control.CreateActorTemplate:input_type -> ateapi.CreateActorTemplateRequest + 63, // 219: ateapi.Control.GetActorTemplate:input_type -> ateapi.GetActorTemplateRequest + 64, // 220: ateapi.Control.ListActorTemplates:input_type -> ateapi.ListActorTemplatesRequest + 66, // 221: ateapi.Control.DeleteActorTemplate:input_type -> ateapi.DeleteActorTemplateRequest + 132, // 222: ateapi.Control.GetGlobalAccessPolicy:input_type -> ateapi.GetGlobalAccessPolicyRequest + 133, // 223: ateapi.Control.CreateGlobalAccessPolicy:input_type -> ateapi.CreateGlobalAccessPolicyRequest + 134, // 224: ateapi.Control.UpdateGlobalAccessPolicy:input_type -> ateapi.UpdateGlobalAccessPolicyRequest + 135, // 225: ateapi.Control.GetAtespaceAccessPolicy:input_type -> ateapi.GetAtespaceAccessPolicyRequest + 136, // 226: ateapi.Control.CreateAtespaceAccessPolicy:input_type -> ateapi.CreateAtespaceAccessPolicyRequest + 137, // 227: ateapi.Control.UpdateAtespaceAccessPolicy:input_type -> ateapi.UpdateAtespaceAccessPolicyRequest + 138, // 228: ateapi.Control.DeleteAtespaceAccessPolicy:input_type -> ateapi.DeleteAtespaceAccessPolicyRequest + 118, // 229: ateapi.WorkerService.RegisterWorker:input_type -> ateapi.RegisterWorkerRequest + 120, // 230: ateapi.WorkerService.MintWorkerActorCertificate:input_type -> ateapi.MintWorkerActorCertificateRequest + 122, // 231: ateapi.WorkerService.MintWorkerActorJwt:input_type -> ateapi.MintWorkerActorJwtRequest + 124, // 232: ateapi.WorkerService.RequestActorSuspend:input_type -> ateapi.RequestActorSuspendRequest + 126, // 233: ateapi.WorkerService.ReportActorSnapshotState:input_type -> ateapi.ReportActorSnapshotStateRequest + 15, // 234: ateapi.Control.GetActor:output_type -> ateapi.Actor + 15, // 235: ateapi.Control.CreateActor:output_type -> ateapi.Actor + 15, // 236: ateapi.Control.UpdateActor:output_type -> ateapi.Actor + 71, // 237: ateapi.Control.SuspendActor:output_type -> ateapi.SuspendActorResponse + 73, // 238: ateapi.Control.PauseActor:output_type -> ateapi.PauseActorResponse + 75, // 239: ateapi.Control.ResumeActor:output_type -> ateapi.ResumeActorResponse + 77, // 240: ateapi.Control.RevertActor:output_type -> ateapi.RevertActorResponse + 15, // 241: ateapi.Control.DeleteActor:output_type -> ateapi.Actor + 16, // 242: ateapi.Control.GetActorEgressPolicy:output_type -> ateapi.EgressPolicy + 16, // 243: ateapi.Control.CreateActorEgressPolicy:output_type -> ateapi.EgressPolicy + 16, // 244: ateapi.Control.UpdateActorEgressPolicy:output_type -> ateapi.EgressPolicy + 16, // 245: ateapi.Control.DeleteActorEgressPolicy:output_type -> ateapi.EgressPolicy + 85, // 246: ateapi.Control.MintActorJwt:output_type -> ateapi.MintActorJwtResponse + 87, // 247: ateapi.Control.MintActorCertificate:output_type -> ateapi.MintActorCertificateResponse + 30, // 248: ateapi.Control.CreateTag:output_type -> ateapi.Tag + 30, // 249: ateapi.Control.GetTag:output_type -> ateapi.Tag + 89, // 250: ateapi.Control.ListTags:output_type -> ateapi.ListTagsResponse + 30, // 251: ateapi.Control.UpdateTag:output_type -> ateapi.Tag + 30, // 252: ateapi.Control.DeleteTag:output_type -> ateapi.Tag + 98, // 253: ateapi.Control.ListWorkers:output_type -> ateapi.ListWorkersResponse + 111, // 254: ateapi.Control.GetWorker:output_type -> ateapi.Worker + 111, // 255: ateapi.Control.CreateWorker:output_type -> ateapi.Worker + 111, // 256: ateapi.Control.UpdateWorker:output_type -> ateapi.Worker + 111, // 257: ateapi.Control.DeleteWorker:output_type -> ateapi.Worker + 104, // 258: ateapi.Control.DrainWorker:output_type -> ateapi.DrainWorkerResponse + 106, // 259: ateapi.Control.MarkWorkerAvailable:output_type -> ateapi.MarkWorkerAvailableResponse + 108, // 260: ateapi.Control.MarkWorkerUnavailable:output_type -> ateapi.MarkWorkerUnavailableResponse + 96, // 261: ateapi.Control.ListWorkerActorAssignments:output_type -> ateapi.ListWorkerActorAssignmentsResponse + 110, // 262: ateapi.Control.ListActors:output_type -> ateapi.ListActorsResponse + 31, // 263: ateapi.Control.CreateAtespace:output_type -> ateapi.Atespace + 31, // 264: ateapi.Control.GetAtespace:output_type -> ateapi.Atespace + 60, // 265: ateapi.Control.ListAtespaces:output_type -> ateapi.ListAtespacesResponse + 31, // 266: ateapi.Control.DeleteAtespace:output_type -> ateapi.Atespace + 33, // 267: ateapi.Control.CreateActorTemplate:output_type -> ateapi.ActorTemplate + 33, // 268: ateapi.Control.GetActorTemplate:output_type -> ateapi.ActorTemplate + 65, // 269: ateapi.Control.ListActorTemplates:output_type -> ateapi.ListActorTemplatesResponse + 33, // 270: ateapi.Control.DeleteActorTemplate:output_type -> ateapi.ActorTemplate + 130, // 271: ateapi.Control.GetGlobalAccessPolicy:output_type -> ateapi.AccessPolicy + 130, // 272: ateapi.Control.CreateGlobalAccessPolicy:output_type -> ateapi.AccessPolicy + 130, // 273: ateapi.Control.UpdateGlobalAccessPolicy:output_type -> ateapi.AccessPolicy + 130, // 274: ateapi.Control.GetAtespaceAccessPolicy:output_type -> ateapi.AccessPolicy + 130, // 275: ateapi.Control.CreateAtespaceAccessPolicy:output_type -> ateapi.AccessPolicy + 130, // 276: ateapi.Control.UpdateAtespaceAccessPolicy:output_type -> ateapi.AccessPolicy + 130, // 277: ateapi.Control.DeleteAtespaceAccessPolicy:output_type -> ateapi.AccessPolicy + 119, // 278: ateapi.WorkerService.RegisterWorker:output_type -> ateapi.RegisterWorkerResponse + 121, // 279: ateapi.WorkerService.MintWorkerActorCertificate:output_type -> ateapi.MintWorkerActorCertificateResponse + 123, // 280: ateapi.WorkerService.MintWorkerActorJwt:output_type -> ateapi.MintWorkerActorJwtResponse + 125, // 281: ateapi.WorkerService.RequestActorSuspend:output_type -> ateapi.RequestActorSuspendResponse + 127, // 282: ateapi.WorkerService.ReportActorSnapshotState:output_type -> ateapi.ReportActorSnapshotStateResponse + 234, // [234:283] is the sub-list for method output_type + 185, // [185:234] is the sub-list for method input_type + 183, // [183:185] is the sub-list for extension type_name + 181, // [181:183] is the sub-list for extension extendee + 0, // [0:181] is the sub-list for field type_name } func init() { file_ateapi_proto_init() } diff --git a/pkg/proto/ateapipb/ateapi.proto b/pkg/proto/ateapipb/ateapi.proto index 75534b2968..08196878a0 100644 --- a/pkg/proto/ateapipb/ateapi.proto +++ b/pkg/proto/ateapipb/ateapi.proto @@ -2547,6 +2547,14 @@ service WorkerService { // on behalf of a particular actor. // // SPIFFE URI: spiffe://${trustdomain}/worker-for-actor/${atespace}/${actor} + // + // atelet calls this with its own client certificate, as it does for + // RequestActorSuspend, naming the Worker its caller proved itself to be. + // The control plane mints only for the Worker that the Actor is assigned + // to, and only to an atelet on that Worker's node. A request from any + // other node or Worker gets NotFound; one from the assigned Worker with a + // stale actor_uid gets Aborted. Unlike MintWorkerActorJwt, a paused Actor + // gets NotFound, since it has no Worker to bind the certificate to. rpc MintWorkerActorCertificate(MintWorkerActorCertificateRequest) returns (MintWorkerActorCertificateResponse); // MintWorkerActorJwt creates a Substrate-issued JWT that asserts a worker @@ -2655,6 +2663,15 @@ message MintWorkerActorCertificateRequest { // +k8s:required // +k8s:maxBytes=16384 bytes certificate_signing_request = 3; + + // The Worker requesting the certificate. atespace is always empty; Workers + // are global-scoped. atelet fills this in from its caller's certificate, so + // it names the Worker that certificate proves the request came from. It + // must be the Worker that the actor is assigned to. + // + // +k8s:required + // +k8s:beta(since: "0.0")=+k8s:subfield(atespace)=+k8s:forbidden # TODO: get rid of beta prefix + ObjectRef worker = 4; } // Response message for MintWorkerActorCertificate. @@ -2714,7 +2731,7 @@ message MintWorkerActorJwtResponse { message RequestActorSuspendRequest { // The Worker making the request. atespace is always empty; Workers are // global-scoped. atelet fills this in from its caller's certificate, so it - // names the Worker the request provably came from. + // names the Worker that certificate proves the request came from. // // +k8s:required // +k8s:beta(since: "0.0")=+k8s:subfield(atespace)=+k8s:forbidden # TODO: get rid of beta prefix diff --git a/pkg/proto/ateapipb/ateapi_grpc.pb.go b/pkg/proto/ateapipb/ateapi_grpc.pb.go index 68e65e97a1..ea0042a580 100644 --- a/pkg/proto/ateapipb/ateapi_grpc.pb.go +++ b/pkg/proto/ateapipb/ateapi_grpc.pb.go @@ -1958,6 +1958,14 @@ type WorkerServiceClient interface { // on behalf of a particular actor. // // SPIFFE URI: spiffe://${trustdomain}/worker-for-actor/${atespace}/${actor} + // + // atelet calls this with its own client certificate, as it does for + // RequestActorSuspend, naming the Worker its caller proved itself to be. + // The control plane mints only for the Worker that the Actor is assigned + // to, and only to an atelet on that Worker's node. A request from any + // other node or Worker gets NotFound; one from the assigned Worker with a + // stale actor_uid gets Aborted. Unlike MintWorkerActorJwt, a paused Actor + // gets NotFound, since it has no Worker to bind the certificate to. MintWorkerActorCertificate(ctx context.Context, in *MintWorkerActorCertificateRequest, opts ...grpc.CallOption) (*MintWorkerActorCertificateResponse, error) // MintWorkerActorJwt creates a Substrate-issued JWT that asserts a worker // acting on behalf of a particular actor: the JWT counterpart of @@ -2085,6 +2093,14 @@ type WorkerServiceServer interface { // on behalf of a particular actor. // // SPIFFE URI: spiffe://${trustdomain}/worker-for-actor/${atespace}/${actor} + // + // atelet calls this with its own client certificate, as it does for + // RequestActorSuspend, naming the Worker its caller proved itself to be. + // The control plane mints only for the Worker that the Actor is assigned + // to, and only to an atelet on that Worker's node. A request from any + // other node or Worker gets NotFound; one from the assigned Worker with a + // stale actor_uid gets Aborted. Unlike MintWorkerActorJwt, a paused Actor + // gets NotFound, since it has no Worker to bind the certificate to. MintWorkerActorCertificate(context.Context, *MintWorkerActorCertificateRequest) (*MintWorkerActorCertificateResponse, error) // MintWorkerActorJwt creates a Substrate-issued JWT that asserts a worker // acting on behalf of a particular actor: the JWT counterpart of From 9bb68caa0f0351770ac660ab1dbb8f661a6d9bac Mon Sep 17 00:00:00 2001 From: Max Smythe Date: Thu, 15 Oct 2026 23:43:17 +0000 Subject: [PATCH 18/31] benchmarking: plumb the actor CPU limit through deploy.sh and the orchestrator (#2717) The actor CPU limit was never piped through the benchmark deploy path. Now that every ActorTemplate must declare one, the templates default to 1 vCPU, which caps how many actors a worker hosts; this makes it configurable the same way actor memory already is. Fixes # > 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 --- benchmarking/automation/orchestrator.py | 7 +++++++ benchmarking/automation/test_orchestrator.py | 3 +++ benchmarking/automation/tests.yaml | 4 ++++ benchmarking/deploy_locust.sh | 9 +++++++++ benchmarking/workloads/deploy.sh | 15 ++++++++++++++- .../glutton-durdir-data-template.yaml.tmpl | 4 ++-- .../glutton-durdir-full-template.yaml.tmpl | 4 ++-- .../manifests/glutton-storage-template.yaml.tmpl | 4 ++-- .../manifests/glutton-template.yaml.tmpl | 4 ++-- .../workloads/manifests/sleep-template.yaml.tmpl | 4 ++-- cmd/ate-setup/internal/steps/substrate_test.go | 1 + 11 files changed, 48 insertions(+), 11 deletions(-) diff --git a/benchmarking/automation/orchestrator.py b/benchmarking/automation/orchestrator.py index 2539b2e33a..473d0d2910 100644 --- a/benchmarking/automation/orchestrator.py +++ b/benchmarking/automation/orchestrator.py @@ -366,6 +366,7 @@ def deploy_workloads( wait_timeout_secs: int | str = "", worker_memory: str = "", fake_atelet_args: Iterable[str] = (), + actor_cpu: str = "", ) -> None: cmd = [ "benchmarking/workloads/deploy.sh", @@ -379,6 +380,11 @@ def deploy_workloads( # minimum); RAM-consuming suites set actorMemory in tests.yaml. if actor_memory: cmd += ["--actor-memory", actor_memory] + # Empty keeps the default in workloads/deploy.sh (1 CPU). The scheduler + # books the limit against worker CPU capacity, so dense suites set + # actorCPU in tests.yaml to fit more actors per worker. + if actor_cpu: + cmd += ["--actor-cpu", actor_cpu] # Empty leaves the worker pods unsized; set workerMemory in tests.yaml to # control how many workers the scheduler packs onto a node. if worker_memory: @@ -566,6 +572,7 @@ def main() -> None: test.get("workerWaitTimeout", ""), test.get("workerMemory", ""), isolate_workload_args(test), + test.get("actorCPU", ""), ) try: status = run_test( diff --git a/benchmarking/automation/test_orchestrator.py b/benchmarking/automation/test_orchestrator.py index b85c368107..8d334dd532 100644 --- a/benchmarking/automation/test_orchestrator.py +++ b/benchmarking/automation/test_orchestrator.py @@ -89,6 +89,7 @@ def test_all_options(self, run): actor_memory="1536Mi", wait_timeout_secs=600, worker_memory="5Gi", + actor_cpu="250m", ) run.assert_called_once_with( [ @@ -100,6 +101,8 @@ def test_all_options(self, run): "microvm", "--actor-memory", "1536Mi", + "--actor-cpu", + "250m", "--worker-memory", "5Gi", "--wait-timeout", diff --git a/benchmarking/automation/tests.yaml b/benchmarking/automation/tests.yaml index b59f3734a8..6bc63d6495 100644 --- a/benchmarking/automation/tests.yaml +++ b/benchmarking/automation/tests.yaml @@ -35,6 +35,10 @@ # actorMemory Optional (default "256Mi", the smallest size microvm # admits). Memory limit on the benchmark ActorTemplates; # raise it for suites that exercise glutton's WriteRAM path. +# actorCPU Optional (default "1"). CPU limit on the benchmark +# ActorTemplates. The scheduler books it against each +# worker's CPU capacity, so it caps the actors a worker +# hosts at once; lower it for dense suites. # workerMemory Optional (default unset: the worker pods have no requests # or limits). Memory request and limit on each WorkerPool # pod. Use it to fix how many workers share a node: one per diff --git a/benchmarking/deploy_locust.sh b/benchmarking/deploy_locust.sh index a431870a76..fb4d335bdb 100755 --- a/benchmarking/deploy_locust.sh +++ b/benchmarking/deploy_locust.sh @@ -31,6 +31,8 @@ SKIP_BUILD=0 OTLP_ENDPOINT="" # Empty keeps the default in workloads/deploy.sh (256Mi, the microvm minimum). ACTOR_MEMORY="" +# Empty keeps the default in workloads/deploy.sh (1 CPU). +ACTOR_CPU="" # Empty leaves the WorkerPool pods unsized. WORKER_MEMORY="" STORAGE_CLASS_NAME="" @@ -49,6 +51,8 @@ usage() { echo " instrumented actor container sends telemetry." echo " --actor-memory SIZE Forwarded to workloads/deploy.sh. Memory limit for the" echo " benchmark ActorTemplates (default: 256Mi, the microvm minimum)." + echo " --actor-cpu CPU Forwarded to workloads/deploy.sh. CPU limit for the benchmark" + echo " ActorTemplates (default: 1)." echo " --worker-memory SIZE Forwarded to workloads/deploy.sh. Memory request and limit" echo " for each WorkerPool pod (default: unset, the pod is unsized)." echo " --storage-class-name S Forwarded to workloads/deploy.sh. StorageClass for the glutton-storage" @@ -81,6 +85,8 @@ while [[ "$#" -gt 0 ]]; do --otlp-endpoint=*) OTLP_ENDPOINT="${1#*=}" ;; --actor-memory) shift; ACTOR_MEMORY="$1" ;; --actor-memory=*) ACTOR_MEMORY="${1#*=}" ;; + --actor-cpu) shift; ACTOR_CPU="$1" ;; + --actor-cpu=*) ACTOR_CPU="${1#*=}" ;; --worker-memory) shift; WORKER_MEMORY="$1" ;; --worker-memory=*) WORKER_MEMORY="${1#*=}" ;; --storage-class-name) shift; STORAGE_CLASS_NAME="$1" ;; @@ -123,6 +129,9 @@ if [[ "${action}" == "deploy" ]]; then if [[ -n "${ACTOR_MEMORY}" ]]; then workload_args+=(--actor-memory "${ACTOR_MEMORY}") fi + if [[ -n "${ACTOR_CPU}" ]]; then + workload_args+=(--actor-cpu "${ACTOR_CPU}") + fi if [[ -n "${WORKER_MEMORY}" ]]; then workload_args+=(--worker-memory "${WORKER_MEMORY}") fi diff --git a/benchmarking/workloads/deploy.sh b/benchmarking/workloads/deploy.sh index 9fe68389bc..6e3c4a4d29 100755 --- a/benchmarking/workloads/deploy.sh +++ b/benchmarking/workloads/deploy.sh @@ -51,6 +51,10 @@ SANDBOX_CLASS="gvisor" # since a bigger guest drags more page cache into every memory snapshot. Raise # it for RAM-consuming suites. ACTOR_MEMORY="256Mi" +# Actor CPU limit (ActorTemplate resources.limits.cpu). The scheduler books +# it against the worker's CPU capacity, so it bounds how many actors a worker +# hosts at once as much as the memory limit does. +ACTOR_CPU="1" # Worker pod memory (WorkerPool spec.template.resources), set as both request # and limit. Empty leaves the pod unsized. The limit is also the memory the # worker reports as its actor capacity. @@ -80,6 +84,7 @@ usage() { echo " microvm requires hack/install-microvm-deps.sh --install to have run." echo " --actor-memory SIZE Memory limit for the benchmark ActorTemplates (default: 256Mi," echo " the smallest size microvm admits)" + echo " --actor-cpu CPU CPU limit for the benchmark ActorTemplates (default: 1)" echo " --worker-memory SIZE Memory request and limit for each WorkerPool pod" echo " (default: unset, the pod is unsized)" echo " --storage-class-name NAME StorageClass for the glutton-storage external volume. Setting it" @@ -155,6 +160,7 @@ substitute() { -e "s|\${SANDBOX_CONFIG_NAME}|${sandbox_config_name}|g" \ -e "s|\${OTLP_ENDPOINT}|${OTLP_ENDPOINT}|g" \ -e "s|\${ACTOR_MEMORY}|${ACTOR_MEMORY}|g" \ + -e "s|\${ACTOR_CPU}|${ACTOR_CPU}|g" \ -e "s|\${WORKER_RESOURCES}|${worker_resources}|g" \ -e "s|\${SWEPERF_IMAGE}|${SWEPERF_IMAGE:-}|g" \ -e "s|\${STORAGE_CLASS_NAME}|${STORAGE_CLASS_NAME}|g" \ @@ -249,7 +255,7 @@ deploy() { echo "Error: --fake-atelet-env needs substrate installed with --fake-data-plane" >&2 exit 1 fi - echo "Deploying workloads (worker_count=${WORKER_COUNT}, actor_memory=${ACTOR_MEMORY}, worker_memory=${WORKER_MEMORY:-unset}, storage_class=${STORAGE_CLASS_NAME:-none}, otlp_endpoint=${OTLP_ENDPOINT})..." + echo "Deploying workloads (worker_count=${WORKER_COUNT}, actor_memory=${ACTOR_MEMORY}, actor_cpu=${ACTOR_CPU}, worker_memory=${WORKER_MEMORY:-unset}, storage_class=${STORAGE_CLASS_NAME:-none}, otlp_endpoint=${OTLP_ENDPOINT})..." substitute "${POOL_MANIFEST}" | hack/run-tool.sh ko apply -f - echo "Waiting for worker pool to be ready (timeout: ${WAIT_TIMEOUT_SECS}s)..." if fake_data_plane; then @@ -346,6 +352,13 @@ while [[ "$#" -gt 0 ]]; do --actor-memory=*) ACTOR_MEMORY="${1#*=}" ;; + --actor-cpu) + shift + ACTOR_CPU="$1" + ;; + --actor-cpu=*) + ACTOR_CPU="${1#*=}" + ;; --worker-memory) shift WORKER_MEMORY="$1" diff --git a/benchmarking/workloads/manifests/glutton-durdir-data-template.yaml.tmpl b/benchmarking/workloads/manifests/glutton-durdir-data-template.yaml.tmpl index d77db84139..5f8810126e 100644 --- a/benchmarking/workloads/manifests/glutton-durdir-data-template.yaml.tmpl +++ b/benchmarking/workloads/manifests/glutton-durdir-data-template.yaml.tmpl @@ -47,9 +47,9 @@ volumes: resources: limits: - name: cpu - quantity: "1" + quantity: "${ACTOR_CPU}" - name: memory - quantity: ${ACTOR_MEMORY} + quantity: "${ACTOR_MEMORY}" snapshotConfig: preferredFidelity: SNAPSHOT_FIDELITY_VOLUMES storageLocation: gs://${BUCKET_NAME}/benchmark-workloads/glutton-durdir-data/ diff --git a/benchmarking/workloads/manifests/glutton-durdir-full-template.yaml.tmpl b/benchmarking/workloads/manifests/glutton-durdir-full-template.yaml.tmpl index a4860bfc47..b8b6104bdb 100644 --- a/benchmarking/workloads/manifests/glutton-durdir-full-template.yaml.tmpl +++ b/benchmarking/workloads/manifests/glutton-durdir-full-template.yaml.tmpl @@ -44,9 +44,9 @@ volumes: resources: limits: - name: cpu - quantity: "1" + quantity: "${ACTOR_CPU}" - name: memory - quantity: ${ACTOR_MEMORY} + quantity: "${ACTOR_MEMORY}" snapshotConfig: preferredFidelity: SNAPSHOT_FIDELITY_MEMORY storageLocation: gs://${BUCKET_NAME}/benchmark-workloads/glutton-durdir-full/ diff --git a/benchmarking/workloads/manifests/glutton-storage-template.yaml.tmpl b/benchmarking/workloads/manifests/glutton-storage-template.yaml.tmpl index fee943693e..9246754e9b 100644 --- a/benchmarking/workloads/manifests/glutton-storage-template.yaml.tmpl +++ b/benchmarking/workloads/manifests/glutton-storage-template.yaml.tmpl @@ -53,9 +53,9 @@ containers: resources: limits: - name: cpu - quantity: "1" + quantity: "${ACTOR_CPU}" - name: memory - quantity: ${ACTOR_MEMORY} + quantity: "${ACTOR_MEMORY}" snapshotConfig: onCommit: SNAPSHOT_CONTENT_SCOPE_FULL storageLocation: gs://${BUCKET_NAME}/benchmark-workloads/glutton-storage/ diff --git a/benchmarking/workloads/manifests/glutton-template.yaml.tmpl b/benchmarking/workloads/manifests/glutton-template.yaml.tmpl index dc249366db..04061540bf 100644 --- a/benchmarking/workloads/manifests/glutton-template.yaml.tmpl +++ b/benchmarking/workloads/manifests/glutton-template.yaml.tmpl @@ -58,9 +58,9 @@ containers: resources: limits: - name: cpu - quantity: "1" + quantity: "${ACTOR_CPU}" - name: memory - quantity: ${ACTOR_MEMORY} + quantity: "${ACTOR_MEMORY}" snapshotConfig: preferredFidelity: SNAPSHOT_FIDELITY_MEMORY storageLocation: gs://${BUCKET_NAME}/benchmark-workloads/glutton/ diff --git a/benchmarking/workloads/manifests/sleep-template.yaml.tmpl b/benchmarking/workloads/manifests/sleep-template.yaml.tmpl index 72becd7126..0a30855682 100644 --- a/benchmarking/workloads/manifests/sleep-template.yaml.tmpl +++ b/benchmarking/workloads/manifests/sleep-template.yaml.tmpl @@ -33,9 +33,9 @@ containers: resources: limits: - name: cpu - quantity: "1" + quantity: "${ACTOR_CPU}" - name: memory - quantity: ${ACTOR_MEMORY} + quantity: "${ACTOR_MEMORY}" snapshotConfig: preferredFidelity: SNAPSHOT_FIDELITY_MEMORY storageLocation: gs://${BUCKET_NAME}/benchmark-workloads/sleep/ diff --git a/cmd/ate-setup/internal/steps/substrate_test.go b/cmd/ate-setup/internal/steps/substrate_test.go index c61ba66fe8..39fe8ce4cd 100644 --- a/cmd/ate-setup/internal/steps/substrate_test.go +++ b/cmd/ate-setup/internal/steps/substrate_test.go @@ -43,6 +43,7 @@ func TestActorTemplateFromManifestDemos(t *testing.T) { // The values benchmarking/workloads/deploy.sh substitutes, beyond the // ${BUCKET_NAME} every installer provides. benchmarkValues := map[string]string{ + "${ACTOR_CPU}": "1", "${ACTOR_MEMORY}": "256Mi", "${OTLP_ENDPOINT}": "http://otel-collector.otel-system:4317", "${SANDBOX_CLASS}": "gvisor", From def6e2b55a4e5de916f7af39af5158d84bd32844 Mon Sep 17 00:00:00 2001 From: pmandewalkar Date: Thu, 15 Oct 2026 23:55:36 +0000 Subject: [PATCH 19/31] benchmarking/sweperf: run each trajectory on a fresh actor (#2323) Each time an actor finishes its trajectory, sweperf now deletes it and starts a new actor to run the trajectory again. Before, the same actor replayed the trajectory over and over. The last cycle is not suspended: the actor is deleted straight from running with DeleteActor(AnyState). A failed delete is retried once with a fresh timeout, then logged with the actor name. The atespace is created once per worker, and again only if CreateActor reports it missing (FailedPrecondition). Liveness on a new actor is polled at --sweperf-poll-interval-ms, and a failed start is logged with its actor and template. Shutdown: - It logs "sweperf shutdown started", blocks new iterations and cancels the ones in flight, including slow liveness polls. - It waits (bounded) for in-flight iterations, then deletes every remaining actor directly within the pod's termination grace period. - Calls cut short by this cancellation record no failure rows, and no task rows are recorded once shutdown has begun, so the end of a run will not show up as a burst of failures. This also fixes a liveness failure decrementing locust_users twice. Metric changes (README updated): - Cycle 1 runs on a freshly started actor, so its no-op resume is not recorded: one fewer ResumeActor sample per task. - A task of n cycles records n-1 SuspendActor samples. - DeleteActor now times deleting a running actor, including sandbox teardown, so it is not comparable with earlier runs. Follow-ups (since this PR is large): - Bring back ActorStartup that liveness polls at the job interval. - Bound resume retries: a resume that keeps failing retries the same cycle forever. That was already true before this change. - Workers tend to recreate actors around the same time, which can cause bursts of creates. - Record actor startup failures. An actor whose sandbox never answers liveness is deleted and logged, but no failure row is recorded. That was **already true**, but since every task now starts an actor, startup failures can happen per task and are invisible in failure ratios. The ActorStartup row will add both its latency and its failures. Tested with go test -race and on a GKE cluster with the astropy template. - [X] Tests pass - [X] Appropriate changes to documentation are included in the PR --- benchmarking/README.md | 30 +- .../benchmarking/boomer/sweperf/sweperf.go | 338 ++++++++--- .../boomer/sweperf/sweperf_test.go | 551 +++++++++++++++++- 3 files changed, 806 insertions(+), 113 deletions(-) diff --git a/benchmarking/README.md b/benchmarking/README.md index 393e0c33e1..308843dc78 100644 --- a/benchmarking/README.md +++ b/benchmarking/README.md @@ -157,7 +157,9 @@ and state restoration latency when a durable directory is attached to the actor. The sweperf benchmark replays a recorded SWE-Perf task inside an actor, suspending and resuming between cycles to measure the cost of actor state transitions under a realistic -agent workload. One task is four cycles by default. +agent workload. One task is four cycles by default. Each task runs on a fresh actor: once +the last cycle finishes, the actor is deleted and the user creates a new one for the next +task. #### Sweperf Reported Metrics @@ -167,7 +169,8 @@ ran inside the sandbox, as reported by `replay.py`. * `ResumeToFirstExec`: Resume RPC start until the sandbox accepts the cycle's `/execute`. * `CycleCEL`: CEL for one cycle. * `TaskCEL`: CEL summed over one task. -* `TaskWallClock`: Client wall clock for one task, excluding inter-cycle think time. +* `TaskWallClock`: Client wall clock for one task, excluding inter-cycle think time and actor + startup. * `CreateAtespace`, `CreateActor`, `ResumeActor`, `SuspendActor`, `DeleteActor`: Server-side elapsed time for each control-plane RPC from the response trailer, or client time without one. * `_rtt`: Client round trip for the RPC of the same name, recorded only when the trailer is @@ -176,9 +179,26 @@ ran inside the sandbox, as reported by `replay.py`. the job finishes. Polled every `--sweperf-poll-interval-ms` (default 100), so this row sits up to one interval above the job's actual end. -The liveness check at session start already has the actor running, so the first cycle's -resume is a no-op. Its successful `ResumeActor`, `ResumeActor_rtt` and `ResumeToFirstExec` -samples are not recorded; failures still are. +The liveness check on a new actor already has it running, so the first cycle of every task +runs on a freshly started actor and its resume is a no-op. Its successful `ResumeActor`, +`ResumeActor_rtt` and `ResumeToFirstExec` samples are not recorded; failures still are. + +The last cycle of a task is not suspended: the actor is deleted straight from running. A +task of `n` cycles therefore records `n - 1` `SuspendActor` samples, and `TaskWallClock` +does not include a final suspend. `DeleteActor` therefore times deleting a running actor, +including its sandbox teardown, so it is not comparable with older runs that suspended the +actor before deleting it. + +`locust_users` counts live actors rather than virtual users: it rises when a user starts an +actor and falls when that actor is deleted, so it dips between tasks while a user waits to +start its next actor. + +`CreateAtespace` is sent once per worker, not per task, and again only if `CreateActor` +reports the atespace missing. + +When the run ends, shutdown cancels cycles still in flight. Calls cut short this way record +nothing, and the interrupted task records no `TaskCEL` or `TaskWallClock` row, so the end of +a run does not show up as a burst of failures. ### Agent-Session Benchmark diff --git a/internal/benchmarking/boomer/sweperf/sweperf.go b/internal/benchmarking/boomer/sweperf/sweperf.go index 37dd467bb1..022fe84866 100644 --- a/internal/benchmarking/boomer/sweperf/sweperf.go +++ b/internal/benchmarking/boomer/sweperf/sweperf.go @@ -15,7 +15,8 @@ // Package sweperf implements the boomer-Go implementation of the // SweperfUser locust test. Each user creates an actor from a SWE-bench workload // template and drives it through a trajectory of steps that are partitioned into -// cycles. This is workload-agnostic and can be used to benchmark any SWE-bench +// cycles. Once the trajectory is done the actor is deleted and a fresh one is +// created to run it again. This is workload-agnostic and can be used to benchmark any SWE-bench // workload by providing the appropriate template and dynamic configuration. package sweperf @@ -32,6 +33,7 @@ import ( "net/http" "strings" "sync" + "sync/atomic" "time" "github.com/agent-substrate/substrate/internal/ateinterceptors" @@ -126,24 +128,75 @@ func generateDynamicChunks(totalSteps int, numCycles int) []chunk { // initSweperf creates a runtime tied to cfg and returns a boomer-compatible // task function plus a shutdown hook the caller should run before exit (it -// suspend+deletes every actor this worker created). +// deletes every actor this worker created). func initSweperf(cfg *userclass.Config) (taskFn func(), shutdown func(context.Context)) { if cfg.Tracer == nil { cfg.Tracer = otel.Tracer("substrate-boomer/sweperf") } - rt := &sweperfRuntime{ - cfg: cfg, - } + rt := newSweperfRuntime(cfg) return rt.iterate, rt.shutdown } +// newSweperfRuntime returns a runtime whose run context lives until shutdown. +func newSweperfRuntime(cfg *userclass.Config) *sweperfRuntime { + ctx, cancel := context.WithCancel(context.Background()) + return &sweperfRuntime{cfg: cfg, runCtx: ctx, cancelRun: cancel} +} + // sweperfRuntime is the per-worker state shared by every boomer goroutine. // Each goroutine keeps its own session in users, keyed by goroutine ID, // because boomer offers no per-VU context. type sweperfRuntime struct { cfg *userclass.Config users sync.Map // goroutineID -> *sweperfUser + // runCtx carries every startup and cycle; shutdown cancels it so a slow + // liveness poll or cycle does not hold up teardown. + runCtx context.Context + cancelRun context.CancelFunc + // mu orders enter against shutdown, so no inflight.Add can race the + // inflight.Wait in shutdown. + mu sync.Mutex + // stopping is set once by shutdown, under mu, and never cleared. + stopping bool + // inflight counts iterate calls in progress, each of which may create or + // delete an actor. shutdown waits for them before sweeping users. + inflight sync.WaitGroup + // atespaceReady is set once CreateAtespace succeeds, so later actors skip + // it. Cleared when CreateActor reports the atespace missing, so the next + // start recreates it. + atespaceReady atomic.Bool +} + +// enter registers an iterate call with inflight, or reports false once +// shutdown has begun. A true result must be paired with inflight.Done. +func (r *sweperfRuntime) enter() bool { + r.mu.Lock() + defer r.mu.Unlock() + if r.stopping { + return false + } + r.inflight.Add(1) + return true +} + +// teardownTimeout bounds deleting an actor once the run context may already +// be canceled. A var so tests can shorten it. +var teardownTimeout = 30 * time.Second + +// teardownCtx returns a context for deleting an actor that outlives the run +// context, so cancellation by shutdown does not strand the actor. +func (r *sweperfRuntime) teardownCtx() (context.Context, context.CancelFunc) { + return context.WithTimeout(context.WithoutCancel(r.runCtx), teardownTimeout) +} + +// canceled reports whether ctx was canceled, which only happens when shutdown +// cuts a call short, so the call says nothing about the system under test. +// It checks the context, not the error: gRPC returns a codes.Canceled status +// that errors.Is does not match. Timeouts are DeadlineExceeded, so teardown +// calls that run out of time are still recorded. +func canceled(ctx context.Context) bool { + return errors.Is(ctx.Err(), context.Canceled) } // resolveConfig returns the template, total step count and cycle count for a @@ -194,44 +247,62 @@ func (r *sweperfRuntime) dynamicWait() time.Duration { // iterate is the boomer task function, one call per goroutine per iteration. // It binds a session to the calling goroutine on first use and runs one cycle -// per call thereafter, looping back to the first cycle when the trace is -// exhausted so the actor keeps serving load. A session that fails to start is -// retried on the next iteration. +// per call thereafter. When the trace is exhausted the session's actor is +// deleted and the goroutine is unbound, so the next call starts a fresh actor +// that runs the trajectory again. A session that fails to start is retried on +// the next iteration. Once shutdown has begun no new actor is created. func (r *sweperfRuntime) iterate() { + if !r.enter() { + return + } + r.runCycle() + time.Sleep(r.dynamicWait()) +} + +// runCycle is the part of iterate that shutdown waits for: it may start, +// cycle or delete an actor, but excludes the think time. +func (r *sweperfRuntime) runCycle() { + defer r.inflight.Done() gid := boomerutil.GoroutineID() val, loaded := r.users.Load(gid) + user, _ := val.(*sweperfUser) if !loaded { - u, err := r.startUser(context.Background()) - if err != nil { - slog.Warn("sweperf on_start failed; goroutine will retry next iter", - slog.String("err", err.Error())) - time.Sleep(r.dynamicWait()) + if user = r.bindUser(gid); user == nil { return } - val, _ = r.users.LoadOrStore(gid, u) } - user := val.(*sweperfUser) + if r.runCtx.Err() != nil { + return // shutdown began; its sweep tears the actor down + } - ctx := context.Background() - user.step(ctx) + user.step(r.runCtx) if user.isDone() { - user.recordTaskMetrics() - user.resetCycles() - slog.Info("Actor finished all cycles and reset session back to cycle 1", - slog.String("actor", user.actorName), - slog.Int("cycles", len(user.chunks)), - ) + if r.runCtx.Err() == nil { // a task finished as shutdown began records nothing + user.recordTaskMetrics() + } + r.retireUser(gid, user) } +} - time.Sleep(r.dynamicWait()) +// bindUser starts a session and publishes it for goroutine gid. It returns +// nil if the session failed to start. shutdown waits for the calling +// runCycle, so it always sees a published session. +func (r *sweperfRuntime) bindUser(gid int64) *sweperfUser { + u, err := r.startUser(r.runCtx) + if err != nil { + return nil // startUser logged it + } + r.users.Store(gid, u) + return u } // startUser creates an actor and blocks until its sandbox serves, so the // caller gets a session that is ready to take cycles. Resolves the config per // session, which lets a value changed mid-run apply to later sessions. On -// failure it tears the actor down. -func (r *sweperfRuntime) startUser(ctx context.Context) (*sweperfUser, error) { +// failure it tears the actor down and logs the error with the actor and +// template, so a failed start can be tied to its task. +func (r *sweperfRuntime) startUser(ctx context.Context) (_ *sweperfUser, err error) { tmpl, totalSteps, numCycles := r.resolveConfig() chunks := generateDynamicChunks(totalSteps, numCycles) @@ -248,22 +319,46 @@ func (r *sweperfRuntime) startUser(ctx context.Context) (*sweperfUser, error) { slog.String("actor", u.actorName), slog.String("template", u.templateName), ) + defer func() { + if err != nil { + slog.Warn("sweperf on_start failed; goroutine will retry next iter", + slog.String("actor", u.actorName), + slog.String("template", u.templateName), + slog.String("err", err.Error()), + ) + } + }() bmetrics.UpdateUsers(u.userClass, 1) - if err := u.ensureAtespace(ctx); err != nil { - bmetrics.UpdateUsers(u.userClass, -1) - return nil, fmt.Errorf("ensureAtespace: %w", err) + if !r.atespaceReady.Load() { + if err := u.ensureAtespace(ctx); err != nil { + bmetrics.UpdateUsers(u.userClass, -1) + return nil, fmt.Errorf("ensureAtespace: %w", err) + } + r.atespaceReady.Store(true) } - if err := u.create(ctx); err != nil { + // CreateActor runs detached from ctx: canceled mid-call, the actor might + // exist without this session knowing to delete it. + createCtx, cancelCreate := r.teardownCtx() + err = u.create(createCtx) + cancelCreate() + if err != nil { + // ate-api answers FailedPrecondition when the atespace is missing. + if status.Code(err) == codes.FailedPrecondition { + r.atespaceReady.Store(false) + } bmetrics.UpdateUsers(u.userClass, -1) return nil, fmt.Errorf("createActor: %w", err) } // poll status for liveness if err := u.pollLiveness(ctx); err != nil { - u.suspendAndDelete(ctx) - bmetrics.UpdateUsers(u.userClass, -1) + // ctx may be canceled by shutdown; tear down regardless. + tctx, cancel := r.teardownCtx() + defer cancel() + // teardown also decrements the user gauge. + u.teardown(tctx) return nil, fmt.Errorf("pollLiveness: %w", err) } @@ -274,23 +369,98 @@ func (r *sweperfRuntime) startUser(ctx context.Context) (*sweperfUser, error) { return u, nil } -// shutdown suspends and deletes every actor this worker created. Boomer has -// no per-VU stop hook, so a mid-run decrease in user count leaks actors until -// this runs — acceptable for a run that ramps up, holds, then tears down. +// retireUser deletes the actor of a session that finished its trajectory and +// unbinds it from goroutine gid, so the next iterate starts a fresh actor. The +// final cycle leaves the actor running, and DeleteActor with AnyState removes +// it from any state, so no suspend is sent first. The delete runs detached +// from the run context so shutdown cannot cut it short. A failed delete is +// retried once, with a fresh timeout; if that also fails the actor is left +// behind and logged. +func (r *sweperfRuntime) retireUser(gid int64, u *sweperfUser) { + r.users.CompareAndDelete(gid, u) + if !u.cleanedUp.CompareAndSwap(false, true) { + return // shutdown already owns this actor's teardown + } + bmetrics.UpdateUsers(u.userClass, -1) + deleteActor := func() error { + ctx, cancel := r.teardownCtx() + defer cancel() + return u.delete(ctx) + } + if err := deleteActor(); err != nil { + if err := deleteActor(); err != nil { + slog.Error("DeleteActor failed twice for finished actor; it is leaked", + slog.String("actor", u.actorName), + slog.String("err", err.Error()), + ) + return + } + } + slog.Info("Actor finished all cycles and was deleted; a new actor will run the trajectory next", + slog.String("actor", u.actorName), + slog.Int("cycles", len(u.chunks)), + ) +} + +// inflightWaitTimeout caps how long shutdown waits for in-flight iterations. +// They are canceled and normally end at once; the cap leaves the rest of the +// shutdown budget for the sweep. A var so tests can shorten it. +var inflightWaitTimeout = 10 * time.Second + +// shutdownConcurrency bounds how many actors shutdown deletes at once. +// In-cluster the pod's 30s termination grace period, not boomer-worker's +// 60s shutdown context, is the real budget, and a serial sweep over thousands +// of sessions would overrun it. +const shutdownConcurrency = 64 + +// shutdown stops new iterations, cancels in-flight startups and cycles, waits +// up to inflightWaitTimeout for them to finish, then deletes every actor still +// bound to a goroutine, shutdownConcurrency at a time, until done or ctx +// expires. Boomer has no per-VU stop hook, so a mid-run decrease in user +// count leaks actors until this runs — acceptable for a run that ramps up, +// holds, then tears down. func (r *sweperfRuntime) shutdown(ctx context.Context) { + slog.Info("sweperf shutdown started") + r.mu.Lock() + r.stopping = true + r.mu.Unlock() + r.cancelRun() + + waitCtx, cancelWait := context.WithTimeout(ctx, inflightWaitTimeout) + defer cancelWait() + done := make(chan struct{}) + go func() { + r.inflight.Wait() + close(done) + }() + select { + case <-done: + case <-waitCtx.Done(): + slog.Warn("shutdown timed out waiting for in-flight iterations; their actors may leak") + } + + var wg sync.WaitGroup + sem := make(chan struct{}, shutdownConcurrency) r.users.Range(func(_, val any) bool { - u := val.(*sweperfUser) - if !u.cleanedUp { - u.suspendAndDelete(ctx) - u.cleanedUp = true + select { + case sem <- struct{}{}: + case <-ctx.Done(): + return false } + wg.Add(1) + go func(u *sweperfUser) { + defer wg.Done() + defer func() { <-sem }() + u.teardown(ctx) + }(val.(*sweperfUser)) return true }) + wg.Wait() } // sweperfUser is one benchmark session: a single actor, the cycle plan it -// replays, and its progress through that plan. Owned by one boomer goroutine, -// so its fields need no locking. +// runs once, and its progress through that plan. Owned by one boomer goroutine, +// so its fields need no locking, except cleanedUp, which shutdown also claims. type sweperfUser struct { cfg *userclass.Config actorName string @@ -298,7 +468,9 @@ type sweperfUser struct { userClass string chunks []chunk cycleIndex int - cleanedUp bool + // cleanedUp is claimed by whichever of the owning goroutine or shutdown + // tears the actor down first, so the actor is deleted once. + cleanedUp atomic.Bool // awake is set while the actor is still running from pollLiveness; the // next resume is then a no-op and its success samples are not recorded. awake bool @@ -390,9 +562,9 @@ func (u *sweperfUser) suspend(ctx context.Context) bool { } // delete removes the actor. Errors are recorded as a failed DeleteActor row -// and otherwise ignored, since the only caller is already tearing down. -func (u *sweperfUser) delete(ctx context.Context) { - _ = u.tracedCall(ctx, "DeleteActor", func(callCtx context.Context, tr *metadata.MD) error { +// and returned so the caller can retry. +func (u *sweperfUser) delete(ctx context.Context) error { + return u.tracedCall(ctx, "DeleteActor", func(callCtx context.Context, tr *metadata.MD) error { _, err := u.cfg.APIStub.DeleteActor(callCtx, &ateapipb.DeleteActorRequest{ Actor: u.ref(), // Teardown discards the actor, so skip the SUSPENDED precondition. @@ -402,20 +574,16 @@ func (u *sweperfUser) delete(ctx context.Context) { }) } -// suspendAndDelete releases the actor and the worker it holds, and decrements -// the user gauge. It suspends first because a worker only frees an actor it -// can account for, and an actor still awake at delete risks being left -// CRASHED. Safe to call more than once: it is a no-op after the first. -func (u *sweperfUser) suspendAndDelete(ctx context.Context) { - if u.cleanedUp { +// teardown releases the actor and the worker it holds, and decrements the +// user gauge. Like retireUser, it deletes with AnyState and sends no suspend +// first. Safe to call more than once and from several goroutines: only the +// first call does anything. +func (u *sweperfUser) teardown(ctx context.Context) { + if !u.cleanedUp.CompareAndSwap(false, true) { return } - _, _ = u.cfg.APIStub.SuspendActor(ctx, &ateapipb.SuspendActorRequest{ - Actor: u.ref(), - }) - u.delete(ctx) + _ = u.delete(ctx) bmetrics.UpdateUsers(u.userClass, -1) - u.cleanedUp = true } // tracedCall runs one control-plane RPC under a span named name and records @@ -437,6 +605,9 @@ func (u *sweperfUser) tracedCallRecord(ctx context.Context, name string, recordS var tr metadata.MD err := do(ctx, &tr) clientLatency := time.Since(start) + if err != nil && canceled(ctx) { + return err + } latency, source := boomerutil.ElapsedFromMD(tr, ateinterceptors.ServerElapsedTrailer, clientLatency) if source == boomerutil.SourceServer { @@ -471,15 +642,20 @@ type statusResponse struct { Status string `json:"status"` } +// livenessTimeout bounds how long pollLiveness waits for a new actor. +const livenessTimeout = time.Minute + // pollLiveness waits for the actor's in-sandbox server to answer /status with -// "up", for up to a minute. The first request through the router is also what -// wakes a newly created actor, so this doubles as the implicit resume. +// "up", for up to livenessTimeout. The first request through the router is +// also what wakes a newly created actor, so this doubles as the implicit +// resume. It polls at the job-completion interval, since every task starts +// a fresh actor. func (u *sweperfUser) pollLiveness(ctx context.Context) error { statusURL := u.cfg.RouterURL + "/status" - maxRetries := 30 - retryInterval := 2 * time.Second + retryInterval := pollInterval(u.cfg) + deadline := time.Now().Add(livenessTimeout) - for attempt := 0; attempt < maxRetries; attempt++ { + for attempt := 0; time.Now().Before(deadline); attempt++ { req, err := http.NewRequestWithContext(ctx, http.MethodGet, statusURL, nil) if err != nil { return err @@ -517,14 +693,9 @@ func (u *sweperfUser) isDone() bool { return u.cycleIndex >= len(u.chunks) } -// resetCycles rewinds to the first cycle so the same actor replays the trace -// again, which is how one session keeps producing load for the whole run. -func (u *sweperfUser) resetCycles() { - u.cycleIndex = 0 -} - // step runs one cycle: resume the actor, replay that cycle's slice of the -// trace, then suspend it again. +// trace, then suspend it again. The final cycle skips the suspend, since the +// actor is deleted straight after. func (u *sweperfUser) step(ctx context.Context) { if u.cycleIndex >= len(u.chunks) { return @@ -545,7 +716,11 @@ func (u *sweperfUser) step(ctx context.Context) { resumeStart := time.Now() // Summed per cycle so the between-cycle think time stays out of the total. defer func() { u.loopWall += time.Since(resumeStart) }() + // A cycle cut short by shutdown returns without recording or advancing. if !u.resume(ctx) { + if canceled(ctx) { + return + } u.loopFailed = true bmetrics.RecordFailure(derivedMetricMethod, resumeToFirstExecMetric, u.userClass, time.Since(resumeStart), "ResumeActor failed") @@ -556,6 +731,9 @@ func (u *sweperfUser) step(ctx context.Context) { // 2. Execute step range chunk inside sandbox ackAt, execDur, err := u.execute(ctx, cycleNum, chunk.start, chunk.end) + if err != nil && canceled(ctx) { + return + } if err != nil { u.loopFailed = true slog.Error("execute steps failed", @@ -572,8 +750,11 @@ func (u *sweperfUser) step(ctx context.Context) { } u.recordCycleCEL(execDur, err) - // 3. Suspend actor - if !u.suspend(ctx) { + // 3. Suspend actor, unless this was the last cycle + if cycleNum < len(u.chunks) && !u.suspend(ctx) { + if canceled(ctx) { + return + } u.loopFailed = true } @@ -759,14 +940,19 @@ func (u *sweperfUser) execute(ctx context.Context, cycleNum int, startIdx int, e // httpJSONCall posts body to route on the actor's in-sandbox server and // records the result under metricName. validate inspects the response body and -// decides the outcome. +// decides the outcome. A call canceled by shutdown records no failure. func (u *sweperfUser) httpJSONCall(ctx context.Context, metricName, route string, body []byte, validate func([]byte) error) ([]byte, error) { ctx, span := u.cfg.Tracer.Start(ctx, metricName) defer span.End() + recordFailure := func(latency time.Duration, msg string) { + if !canceled(ctx) { + bmetrics.RecordFailure("http", metricName, u.userClass, latency, msg) + } + } httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, u.cfg.RouterURL+route, bytes.NewReader(body)) if err != nil { - bmetrics.RecordFailure("http", metricName, u.userClass, 0, err.Error()) + recordFailure(0, err.Error()) return nil, err } u.setActorRouting(httpReq) @@ -777,14 +963,14 @@ func (u *sweperfUser) httpJSONCall(ctx context.Context, metricName, route string resp, err := u.cfg.HTTPClient.Do(httpReq) clientLatency := time.Since(start) if err != nil { - bmetrics.RecordFailure("http", metricName, u.userClass, clientLatency, err.Error()) + recordFailure(clientLatency, err.Error()) return nil, err } defer resp.Body.Close() respBody, readErr := io.ReadAll(resp.Body) if readErr != nil { - bmetrics.RecordFailure("http", metricName, u.userClass, clientLatency, readErr.Error()) + recordFailure(clientLatency, readErr.Error()) return nil, readErr } @@ -796,7 +982,7 @@ func (u *sweperfUser) httpJSONCall(ctx context.Context, metricName, route string if resp.StatusCode >= 400 { httpErr := fmt.Errorf("HTTP %d: %s", resp.StatusCode, strings.TrimSpace(string(respBody))) boomerutil.LogSampledTrace(span, metricName, clientLatency, boomerutil.SourceClient, httpErr) - bmetrics.RecordFailure("http", metricName, u.userClass, clientLatency, httpErr.Error()) + recordFailure(clientLatency, httpErr.Error()) return nil, httpErr } @@ -804,7 +990,7 @@ func (u *sweperfUser) httpJSONCall(ctx context.Context, metricName, route string if err := validate(respBody); err != nil { elapsed := time.Since(start) boomerutil.LogSampledTrace(span, metricName, elapsed, boomerutil.SourceClient, err) - bmetrics.RecordFailure("http", metricName, u.userClass, elapsed, err.Error()) + recordFailure(elapsed, err.Error()) return nil, err } } diff --git a/internal/benchmarking/boomer/sweperf/sweperf_test.go b/internal/benchmarking/boomer/sweperf/sweperf_test.go index 0993c52589..970921c0ef 100644 --- a/internal/benchmarking/boomer/sweperf/sweperf_test.go +++ b/internal/benchmarking/boomer/sweperf/sweperf_test.go @@ -15,10 +15,12 @@ package sweperf import ( + "bytes" "context" "encoding/json" "errors" "fmt" + "log/slog" "net/http" "net/http/httptest" "reflect" @@ -107,6 +109,8 @@ type fakeControlClient struct { resumeElapsedUs string // AnyState carried by the most recent DeleteActor request. deleteAnyState bool + // beforeDelete, if set, runs at the start of each DeleteActor, outside mu. + beforeDelete func() } func (f *fakeControlClient) CreateAtespace(ctx context.Context, in *ateapipb.CreateAtespaceRequest, opts ...grpc.CallOption) (*ateapipb.Atespace, error) { @@ -163,10 +167,16 @@ func (f *fakeControlClient) SuspendActor(ctx context.Context, in *ateapipb.Suspe } func (f *fakeControlClient) DeleteActor(ctx context.Context, in *ateapipb.DeleteActorRequest, opts ...grpc.CallOption) (*ateapipb.Actor, error) { + if f.beforeDelete != nil { + f.beforeDelete() + } f.mu.Lock() defer f.mu.Unlock() f.calls = append(f.calls, "DeleteActor") f.deleteAnyState = in.GetAnyState() + if err := ctx.Err(); err != nil { + return nil, err + } if f.deleteErr != nil { return nil, f.deleteErr } @@ -425,6 +435,43 @@ func TestEnsureAtespaceHandling(t *testing.T) { }) } +func TestAtespaceCreatedOncePerWorker(t *testing.T) { + cfg, _, fakeCtrl := newTestConfig(t, asyncJobHandler()) + r := newSweperfRuntime(cfg) + countCreateAtespace := func() int { + n := 0 + for _, c := range fakeCtrl.recordedCalls() { + if c == "CreateAtespace" { + n++ + } + } + return n + } + + for range 2 { + if _, err := r.startUser(context.Background()); err != nil { + t.Fatalf("startUser: %v", err) + } + } + if n := countCreateAtespace(); n != 1 { + t.Errorf("CreateAtespace sent %d times for two actors, want 1", n) + } + + // A missing atespace comes back from CreateActor as FailedPrecondition; + // the next start recreates it. + fakeCtrl.createActorErr = status.Error(codes.FailedPrecondition, "Atespace benchmark-test not found") + if _, err := r.startUser(context.Background()); err == nil { + t.Fatalf("startUser succeeded despite CreateActor failing") + } + fakeCtrl.createActorErr = nil + if _, err := r.startUser(context.Background()); err != nil { + t.Fatalf("startUser: %v", err) + } + if n := countCreateAtespace(); n != 2 { + t.Errorf("CreateAtespace sent %d times, want 2 after the atespace went missing", n) + } +} + func TestControlClientErrors(t *testing.T) { t.Run("ResumeActor error returns false", func(t *testing.T) { cfg, _, fakeCtrl := newTestConfig(t, http.HandlerFunc(nil)) @@ -517,6 +564,66 @@ func TestSweperfPollLiveness(t *testing.T) { t.Errorf("pollLiveness expected error on timeout, got nil") } }) + + t.Run("retries at the sweperf poll interval", func(t *testing.T) { + var mu sync.Mutex + calls := 0 + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + mu.Lock() + calls++ + n := calls + mu.Unlock() + if n < 3 { + w.WriteHeader(http.StatusServiceUnavailable) + return + } + json.NewEncoder(w).Encode(statusResponse{Status: "up"}) + }) + cfg, _, _ := newTestConfig(t, handler) + cfg.Dyn = dynconfig.NewHolder(dynconfig.Config{SweperfPollIntervalMs: 10}) + u := &sweperfUser{cfg: cfg, actorName: "act"} + + start := time.Now() + if err := u.pollLiveness(context.Background()); err != nil { + t.Fatalf("pollLiveness: %v", err) + } + // Two retries at a fixed 2s interval would take 4s. + if d := time.Since(start); d > time.Second { + t.Errorf("pollLiveness took %v for two retries at a 10ms interval", d) + } + }) +} + +func TestStartupFailureLogsActorAndTemplate(t *testing.T) { + cfg, _, fakeCtrl := newTestConfig(t, asyncJobHandler()) + fakeCtrl.createActorErr = errors.New("quota exceeded") + r := newSweperfRuntime(cfg) + + var buf bytes.Buffer + prev := slog.Default() + slog.SetDefault(slog.New(slog.NewJSONHandler(&buf, nil))) + t.Cleanup(func() { slog.SetDefault(prev) }) + + if _, err := r.startUser(context.Background()); err == nil { + t.Fatalf("startUser succeeded despite CreateActor failing") + } + + var failure map[string]any + for _, line := range strings.Split(strings.TrimSpace(buf.String()), "\n") { + var rec map[string]any + if json.Unmarshal([]byte(line), &rec) == nil && strings.Contains(fmt.Sprint(rec["msg"]), "on_start failed") { + failure = rec + } + } + if failure == nil { + t.Fatalf("no startup failure logged; got:\n%s", buf.String()) + } + if actor, _ := failure["actor"].(string); !strings.HasPrefix(actor, "sb-") { + t.Errorf("failure log actor = %q, want the new actor's name", actor) + } + if got := failure["template"]; got != defaultSweperfTemplate { + t.Errorf("failure log template = %v, want %q", got, defaultSweperfTemplate) + } } func TestSweperfPollJobCompletion(t *testing.T) { @@ -556,7 +663,7 @@ func TestSweperfPollJobCompletion(t *testing.T) { }) } -func TestSweperfUserLifecycleAndReset(t *testing.T) { +func TestSweperfUserIsDone(t *testing.T) { u := &sweperfUser{ chunks: []chunk{{0, 5}, {5, 10}}, cycleIndex: 0, @@ -570,11 +677,6 @@ func TestSweperfUserLifecycleAndReset(t *testing.T) { if !u.isDone() { t.Errorf("isDone = false, want true") } - - u.resetCycles() - if u.cycleIndex != 0 || u.isDone() { - t.Errorf("resetCycles failed: cycleIndex = %d, isDone = %v", u.cycleIndex, u.isDone()) - } } func TestDynamicWait(t *testing.T) { @@ -660,7 +762,7 @@ func asyncJobHandler() http.HandlerFunc { func TestNoopFirstResumeNotRecorded(t *testing.T) { cfg, _, fakeCtrl := newTestConfig(t, asyncJobHandler()) fakeCtrl.resumeElapsedUs = "5000" - rt := &sweperfRuntime{cfg: cfg} + rt := newSweperfRuntime(cfg) u, err := rt.startUser(context.Background()) if err != nil { t.Fatalf("startUser: %v", err) @@ -690,16 +792,6 @@ func TestNoopFirstResumeNotRecorded(t *testing.T) { t.Errorf("cycle 2: %s recorded %d samples, want 1", name, got.count) } } - - // The next pass starts from a real suspend, so its first resume counts. - u.resetCycles() - before = readStats(t) - u.step(context.Background()) - for _, name := range resumeRows { - if got := recordedSince(t, before, name, "success"); got.count != 1 { - t.Errorf("next pass cycle 1: %s recorded %d samples, want 1", name, got.count) - } - } } func TestNoopFirstResumeFailureStillRecorded(t *testing.T) { @@ -722,7 +814,7 @@ func TestNoopFirstResumeFailureStillRecorded(t *testing.T) { } } -func TestSweperfBootstrapFailureSuspendsBeforeDelete(t *testing.T) { +func TestStartupFailureDeletesWithoutSuspend(t *testing.T) { handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { http.Error(w, "server not ready", http.StatusInternalServerError) }) @@ -731,19 +823,18 @@ func TestSweperfBootstrapFailureSuspendsBeforeDelete(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond) defer cancel() - rt := &sweperfRuntime{cfg: cfg} + rt := newSweperfRuntime(cfg) _, err := rt.startUser(ctx) if err == nil { t.Fatalf("startUser expected error on liveness failure, got nil") } - calls := fakeCtrl.recordedCalls() - if len(calls) < 2 || calls[len(calls)-2] != "SuspendActor" || calls[len(calls)-1] != "DeleteActor" { - t.Errorf("recordedCalls must end with [SuspendActor, DeleteActor], got %v", calls) + if got, want := fakeCtrl.recordedCalls(), []string{"CreateAtespace", "CreateActor", "DeleteActor"}; !reflect.DeepEqual(got, want) { + t.Errorf("recordedCalls = %v, want %v", got, want) } } -func TestSweperfShutdownSuspendsBeforeDelete(t *testing.T) { +func TestShutdownDeletesWithoutSuspend(t *testing.T) { cfg, _, fakeCtrl := newTestConfig(t, http.HandlerFunc(nil)) u := &sweperfUser{ @@ -752,13 +843,12 @@ func TestSweperfShutdownSuspendsBeforeDelete(t *testing.T) { userClass: sweperfUserClass, } - rt := &sweperfRuntime{cfg: cfg} + rt := newSweperfRuntime(cfg) rt.users.Store("goroutine-1", u) rt.shutdown(context.Background()) - calls := fakeCtrl.recordedCalls() - if len(calls) < 2 || calls[len(calls)-2] != "SuspendActor" || calls[len(calls)-1] != "DeleteActor" { - t.Errorf("recordedCalls must end with [SuspendActor, DeleteActor], got %v", calls) + if got, want := fakeCtrl.recordedCalls(), []string{"DeleteActor"}; !reflect.DeepEqual(got, want) { + t.Errorf("recordedCalls = %v, want %v", got, want) } // Without AnyState the server refuses to delete an actor caught mid-cycle. if !fakeCtrl.deleteAnyState { @@ -869,7 +959,7 @@ func TestTaskCELOnlyOnFullTrajectory(t *testing.T) { } }) cfg, _, _ := newTestConfig(t, handler) - r := &sweperfRuntime{cfg: cfg} + r := newSweperfRuntime(cfg) u := &sweperfUser{ cfg: cfg, actorName: "act", @@ -903,9 +993,129 @@ func TestTaskCELOnlyOnFullTrajectory(t *testing.T) { if gotWall := recordedSince(t, before, "TaskWallClock", "success"); gotWall.count != 1 { t.Errorf("TaskWallClock recorded %d samples, want 1", gotWall.count) } - if u.loopCEL != 0 || u.loopWall != 0 || u.loopFailed || u.cycleIndex != 0 { - t.Errorf("after the task loopCEL=%v loopWall=%v loopFailed=%v cycleIndex=%d, want all rearmed", - u.loopCEL, u.loopWall, u.loopFailed, u.cycleIndex) + if u.loopCEL != 0 || u.loopWall != 0 || u.loopFailed { + t.Errorf("after the task loopCEL=%v loopWall=%v loopFailed=%v, want all rearmed", + u.loopCEL, u.loopWall, u.loopFailed) + } +} + +func TestFinishedTrajectoryDeletesActorAndStartsFresh(t *testing.T) { + cfg, _, fakeCtrl := newTestConfig(t, asyncJobHandler()) + r := newSweperfRuntime(cfg) + gid := boomerutil.GoroutineID() + u := &sweperfUser{ + cfg: cfg, + actorName: "act-1", + userClass: sweperfUserClass, + chunks: []chunk{{0, 5}}, + } + r.users.Store(gid, u) + + // The single cycle finishes the trajectory, so the actor must go. + r.iterate() + if !u.cleanedUp.Load() { + t.Errorf("cleanedUp = false after the trajectory finished; the actor was not deleted") + } + // The final cycle is not suspended; the actor is deleted straight away. + if got, want := fakeCtrl.recordedCalls(), []string{"ResumeActor", "DeleteActor"}; !reflect.DeepEqual(got, want) { + t.Errorf("recordedCalls = %v, want %v", got, want) + } + if _, ok := r.users.Load(gid); ok { + t.Fatalf("finished session still bound to its goroutine; the next iterate would reuse it") + } + + // The next iterate creates a brand-new actor for the next trajectory. + before := len(fakeCtrl.recordedCalls()) + r.iterate() + val, ok := r.users.Load(gid) + if !ok { + t.Fatalf("no session bound after the next iterate; want a fresh actor") + } + next := val.(*sweperfUser) + if next == u || next.actorName == u.actorName { + t.Errorf("next session reuses actor %q, want a new one", u.actorName) + } + newCalls := fakeCtrl.recordedCalls()[before:] + if len(newCalls) < 2 || newCalls[0] != "CreateAtespace" || newCalls[1] != "CreateActor" { + t.Errorf("next iterate calls = %v, want to start with CreateAtespace, CreateActor", newCalls) + } + r.shutdown(context.Background()) +} + +func TestRetireRetriesFailedDeleteOnce(t *testing.T) { + cfg, _, fakeCtrl := newTestConfig(t, asyncJobHandler()) + fakeCtrl.deleteErr = errors.New("unavailable") + r := newSweperfRuntime(cfg) + u := &sweperfUser{cfg: cfg, actorName: "act-1", userClass: sweperfUserClass, chunks: []chunk{{0, 5}}} + r.users.Store(boomerutil.GoroutineID(), u) + + r.iterate() + + // One retry, then the actor is given up on rather than retried forever. + want := []string{"ResumeActor", "DeleteActor", "DeleteActor"} + if got := fakeCtrl.recordedCalls(); !reflect.DeepEqual(got, want) { + t.Errorf("recordedCalls = %v, want %v", got, want) + } +} + +func TestRetireRetryGetsFreshTeardownCtx(t *testing.T) { + prev := teardownTimeout + teardownTimeout = 50 * time.Millisecond + t.Cleanup(func() { teardownTimeout = prev }) + + cfg, _, fakeCtrl := newTestConfig(t, asyncJobHandler()) + // The first DeleteActor outlasts its teardown context; the retry is quick. + var once sync.Once + fakeCtrl.beforeDelete = func() { once.Do(func() { time.Sleep(100 * time.Millisecond) }) } + r := newSweperfRuntime(cfg) + u := &sweperfUser{cfg: cfg, actorName: "act-1", userClass: sweperfUserClass, chunks: []chunk{{0, 5}}} + r.users.Store(boomerutil.GoroutineID(), u) + + before := readStats(t) + r.iterate() + + if got := recordedSince(t, before, "DeleteActor", "failure"); got.count != 1 { + t.Errorf("DeleteActor recorded %d failures, want 1 for the timed-out first attempt", got.count) + } + if got := recordedSince(t, before, "DeleteActor", "success"); got.count != 1 { + t.Errorf("DeleteActor recorded %d successes, want the retry to succeed on a fresh context", got.count) + } +} + +func TestOnlyFinalCycleSkipsSuspend(t *testing.T) { + cfg, _, fakeCtrl := newTestConfig(t, asyncJobHandler()) + r := newSweperfRuntime(cfg) + u := &sweperfUser{cfg: cfg, actorName: "act-1", userClass: sweperfUserClass, chunks: []chunk{{0, 2}, {2, 5}}} + r.users.Store(boomerutil.GoroutineID(), u) + + r.iterate() + r.iterate() + + want := []string{"ResumeActor", "SuspendActor", "ResumeActor", "DeleteActor"} + if got := fakeCtrl.recordedCalls(); !reflect.DeepEqual(got, want) { + t.Errorf("recordedCalls = %v, want %v", got, want) + } +} + +func TestShutdownAndRetireDeleteOnce(t *testing.T) { + cfg, _, fakeCtrl := newTestConfig(t, asyncJobHandler()) + r := newSweperfRuntime(cfg) + gid := boomerutil.GoroutineID() + u := &sweperfUser{cfg: cfg, actorName: "act-1", userClass: sweperfUserClass, chunks: []chunk{{0, 5}}} + r.users.Store(gid, u) + + // Shutdown claims the actor first; a late retire must not delete it again. + r.shutdown(context.Background()) + r.retireUser(gid, u) + + n := 0 + for _, c := range fakeCtrl.recordedCalls() { + if c == "DeleteActor" { + n++ + } + } + if n != 1 { + t.Errorf("DeleteActor sent %d times, want 1", n) } } @@ -956,7 +1166,8 @@ func TestStepSuspendFailureFailsTask(t *testing.T) { cfg: cfg, actorName: "act", userClass: sweperfUserClass, - chunks: []chunk{{0, 5}}, + // Two cycles: the final one is never suspended. + chunks: []chunk{{0, 2}, {2, 5}}, } u.step(context.Background()) @@ -991,3 +1202,279 @@ func TestStepSyncExitSkipsResumeToFirstExec(t *testing.T) { t.Errorf("loopFailed = false after the command exited non-zero") } } + +// usersGauge reads the locust_users gauge for the sweperf user class. The +// registry is global to the test binary, so tests using it must not run in +// parallel. +func usersGauge(t *testing.T) float64 { + t.Helper() + families, err := prometheus.DefaultGatherer.Gather() + if err != nil { + t.Fatalf("gather metrics: %v", err) + } + for _, mf := range families { + if mf.GetName() != "locust_users" { + continue + } + for _, m := range mf.GetMetric() { + for _, lp := range m.GetLabel() { + if lp.GetName() == "user_class" && lp.GetValue() == sweperfUserClass { + return m.GetGauge().GetValue() + } + } + } + } + return 0 +} + +func TestPollLivenessFailureDecrementsUsersOnce(t *testing.T) { + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + http.Error(w, "server not ready", http.StatusInternalServerError) + }) + cfg, _, _ := newTestConfig(t, handler) + r := newSweperfRuntime(cfg) + + ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond) + defer cancel() + + before := usersGauge(t) + if _, err := r.startUser(ctx); err == nil { + t.Fatalf("startUser expected error on liveness failure, got nil") + } + if got := usersGauge(t); got != before { + t.Errorf("locust_users = %v after a failed start, want %v unchanged", got, before) + } +} + +func TestNoNewActorAfterShutdown(t *testing.T) { + cfg, _, fakeCtrl := newTestConfig(t, asyncJobHandler()) + r := newSweperfRuntime(cfg) + + r.shutdown(context.Background()) + r.iterate() + + if calls := fakeCtrl.recordedCalls(); len(calls) != 0 { + t.Errorf("iterate after shutdown sent %v, want no calls", calls) + } + if _, ok := r.users.Load(boomerutil.GoroutineID()); ok { + t.Errorf("iterate after shutdown bound a session") + } +} + +func TestShutdownCancelsSlowStartup(t *testing.T) { + entered := make(chan struct{}) + var once sync.Once + handler := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + once.Do(func() { close(entered) }) + <-req.Context().Done() // the sandbox never comes up + }) + cfg, _, fakeCtrl := newTestConfig(t, handler) + r := newSweperfRuntime(cfg) + + before := usersGauge(t) + go r.iterate() + <-entered + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + start := time.Now() + r.shutdown(ctx) + if d := time.Since(start); d > 2*time.Second { + t.Errorf("shutdown took %v; the liveness poll was not canceled", d) + } + + if got, want := fakeCtrl.recordedCalls(), []string{"CreateAtespace", "CreateActor", "DeleteActor"}; !reflect.DeepEqual(got, want) { + t.Errorf("recordedCalls = %v, want %v: the starting actor deleted despite cancellation", got, want) + } + if got := usersGauge(t); got != before { + t.Errorf("locust_users = %v after shutdown, want %v unchanged", got, before) + } +} + +func TestTracedCallSkipsFailureWhenCanceled(t *testing.T) { + cfg, _, fakeCtrl := newTestConfig(t, http.HandlerFunc(nil)) + // gRPC reports cancellation as a status, which errors.Is does not match. + fakeCtrl.resumeErr = status.Error(codes.Canceled, "context canceled") + fakeCtrl.resumeElapsedUs = "5000" + u := &sweperfUser{cfg: cfg, actorName: "act", userClass: sweperfUserClass} + ctx, cancel := context.WithCancel(context.Background()) + cancel() + + before := readStats(t) + if ok := u.resume(ctx); ok { + t.Fatalf("resume = true on RPC failure, want false") + } + for _, name := range []string{"ResumeActor", "ResumeActor_rtt"} { + if got := recordedSince(t, before, name, "failure"); got.count != 0 { + t.Errorf("%s recorded %d failures for a canceled call, want 0", name, got.count) + } + } +} + +func TestShutdownMidCycleRecordsNoFailures(t *testing.T) { + entered := make(chan struct{}) + var once sync.Once + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch { + case r.URL.Path == "/execute": + json.NewEncoder(w).Encode(executeResponse{JobID: "job-1", Status: "ACCEPTED"}) + case r.URL.Path == "/status" && r.URL.Query().Get("job_id") != "": + once.Do(func() { close(entered) }) + <-r.Context().Done() // the job never finishes + default: + http.NotFound(w, r) + } + }) + cfg, _, fakeCtrl := newTestConfig(t, handler) + cfg.Dyn = dynconfig.NewHolder(dynconfig.Config{SweperfPollIntervalMs: 10}) + r := newSweperfRuntime(cfg) + // The last cycle, so an interrupted task would also emit task rows. + u := &sweperfUser{cfg: cfg, actorName: "act-1", userClass: sweperfUserClass, chunks: []chunk{{0, 5}}} + + before := readStats(t) + go func() { + r.users.Store(boomerutil.GoroutineID(), u) + r.iterate() + }() + <-entered // the cycle is polling its job + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + r.shutdown(ctx) + + for _, name := range []string{ + "ResumeActor", "ResumeActor_rtt", "ResumeToFirstExec", "Workload_Cycle_1", + "CycleCEL", "TaskCEL", "TaskWallClock", "SuspendActor", "DeleteActor", + } { + if got := recordedSince(t, before, name, "failure"); got.count != 0 { + t.Errorf("%s recorded %d failures after shutdown canceled the cycle, want 0", name, got.count) + } + } + if got, want := fakeCtrl.recordedCalls(), []string{"ResumeActor", "DeleteActor"}; !reflect.DeepEqual(got, want) { + t.Errorf("recordedCalls = %v, want %v: the interrupted actor deleted by the sweep", got, want) + } +} + +func TestShutdownWaitsForRetire(t *testing.T) { + cfg, _, fakeCtrl := newTestConfig(t, asyncJobHandler()) + entered := make(chan struct{}) + release := make(chan struct{}) + var once sync.Once + fakeCtrl.beforeDelete = func() { + once.Do(func() { close(entered) }) + <-release + } + unblock := sync.OnceFunc(func() { close(release) }) + t.Cleanup(unblock) + r := newSweperfRuntime(cfg) + u := &sweperfUser{cfg: cfg, actorName: "act-1", userClass: sweperfUserClass, chunks: []chunk{{0, 5}}} + + // Sessions are keyed by goroutine, so bind u on the goroutine that iterates. + go func() { + r.users.Store(boomerutil.GoroutineID(), u) + r.iterate() + }() + <-entered // retireUser is inside DeleteActor + + done := make(chan struct{}) + go func() { + r.shutdown(context.Background()) + close(done) + }() + select { + case <-done: + t.Fatalf("shutdown returned mid-DeleteActor; the process would exit and leak the actor") + case <-time.After(50 * time.Millisecond): + } + + unblock() + <-done + n := 0 + for _, c := range fakeCtrl.recordedCalls() { + if c == "DeleteActor" { + n++ + } + } + if n != 1 { + t.Errorf("DeleteActor sent %d times, want 1", n) + } +} + +func TestTeardownClaimsOnce(t *testing.T) { + cfg, _, fakeCtrl := newTestConfig(t, http.HandlerFunc(nil)) + u := &sweperfUser{cfg: cfg, actorName: "act", userClass: sweperfUserClass} + + // Shutdown's sweep and a goroutine it stopped waiting for may both try to + // tear the actor down; only one may. + var wg sync.WaitGroup + for range 8 { + wg.Add(1) + go func() { + defer wg.Done() + u.teardown(context.Background()) + }() + } + wg.Wait() + + n := 0 + for _, c := range fakeCtrl.recordedCalls() { + if c == "DeleteActor" { + n++ + } + } + if n != 1 { + t.Errorf("DeleteActor sent %d times, want 1", n) + } +} + +func TestShutdownSweepsConcurrently(t *testing.T) { + cfg, _, fakeCtrl := newTestConfig(t, http.HandlerFunc(nil)) + fakeCtrl.beforeDelete = func() { time.Sleep(50 * time.Millisecond) } + r := newSweperfRuntime(cfg) + const n = 20 + for i := range n { + r.users.Store(int64(i), &sweperfUser{cfg: cfg, actorName: fmt.Sprintf("act-%d", i), userClass: sweperfUserClass}) + } + + start := time.Now() + r.shutdown(context.Background()) + + // A serial sweep would take n * 50ms = 1s. + if d := time.Since(start); d > 500*time.Millisecond { + t.Errorf("shutdown took %v for %d actors; the sweep is not concurrent", d, n) + } + deletes := 0 + for _, c := range fakeCtrl.recordedCalls() { + if c == "DeleteActor" { + deletes++ + } + } + if deletes != n { + t.Errorf("DeleteActor sent %d times, want %d", deletes, n) + } +} + +func TestShutdownSweepsAfterInflightWaitCap(t *testing.T) { + prev := inflightWaitTimeout + inflightWaitTimeout = 50 * time.Millisecond + t.Cleanup(func() { inflightWaitTimeout = prev }) + + cfg, _, fakeCtrl := newTestConfig(t, http.HandlerFunc(nil)) + r := newSweperfRuntime(cfg) + r.users.Store(int64(1), &sweperfUser{cfg: cfg, actorName: "act-1", userClass: sweperfUserClass}) + // An iteration that never finishes. + r.inflight.Add(1) + t.Cleanup(r.inflight.Done) + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + start := time.Now() + r.shutdown(ctx) + + if d := time.Since(start); d > time.Second { + t.Errorf("shutdown took %v; the in-flight wait used the whole budget", d) + } + if got, want := fakeCtrl.recordedCalls(), []string{"DeleteActor"}; !reflect.DeepEqual(got, want) { + t.Errorf("recordedCalls = %v, want the bound actor swept despite the stuck iteration", got) + } +} From 024190e074bbd719d503a79a5783289a8a5ab8d0 Mon Sep 17 00:00:00 2001 From: Eitan Yarmush Date: Fri, 16 Oct 2026 00:02:26 +0000 Subject: [PATCH 20/31] Limit memory used by concurrent snapshot uploads (#2708) When several agents idle-suspend at the same time, their snapshot uploads allocate compression and storage buffers independently. Ten simultaneous snapshots with eight files each can therefore produce a large memory spike in the objectstore plugin. This adds a shared upload memory budget across requests, with a 512 MiB default configurable through `--upload-memory-budget`. Uploads reserve capacity for compression and S3/GCS buffers before starting, reduce compression concurrency when needed, and wait when the budget is exhausted. Cancellation and failures release capacity after workers finish. Manifests included with data files are uploaded only after those files succeed. The budget limits reserved upload buffers and compression state; it is not a process RSS limit. Queuing trades burst throughput for lower peak memory. The plugin README documents configuration, minimum budgets, and accounting limits. Part of #2553. This addresses simultaneous snapshot uploads and is independent of the small-file allocation optimization in #2633. Validation: - `hack/verify-all.sh` passed. - Object storage, plugin, and affected atelet snapshot/upload tests passed twice with the race detector. Coverage includes concurrent snapshots, cancellation, failures, manifest ordering, and S3/GCS buffering through mocked endpoints. - The full race suite (`go test -race -p 2 ./... -timeout=5m`) hit the existing `TestServerTLSConfigReloadsCACertsWithoutRestart` flake in the benchmarking fake-atelet, reproduced on unchanged upstream (3/5 attempts). The production atelet package passed. - Color-formatting tests failed with this shell's `NO_COLOR=1` and passed twice with that variable unset. - No live cluster or cloud throughput tests were run. One synthetic run with 80 small files and a simulated 20 ms destination stall measured the following tradeoff: | Admission | Sampled peak heap growth | Batch completion | | --- | ---: | ---: | | Shared 512 MiB budget | 279.7 MiB | 225.146 ms | | All 80 uploads admitted | 1,305 MiB | 44.592 ms | These are compression backpressure measurements, not cloud throughput or process RSS measurements. - [ ] Tests pass (affected tests pass; existing TLS flake noted above) - [x] Appropriate changes to documentation are included in the PR --------- Signed-off-by: Eitan Yarmush --- cmd/atelet/snapshot_transfer_test.go | 4 +- cmd/objectstore-plugin/README.md | 44 +++ cmd/objectstore-plugin/main.go | 9 +- internal/objectstoreplugin/node.go | 81 +++--- internal/objectstoreplugin/plugin_test.go | 2 +- internal/objectstoreplugin/upload_test.go | 200 ++++++++++++++ pkg/objectstorage/gcssparse.go | 6 +- pkg/objectstorage/objects.go | 97 +++---- pkg/objectstorage/objects_test.go | 13 +- pkg/objectstorage/parzstd.go | 17 +- pkg/objectstorage/sparseparts_test.go | 3 +- pkg/objectstorage/sparsezstd.go | 17 +- pkg/objectstorage/sparsezstd_test.go | 3 +- pkg/objectstorage/upload.go | 169 ++++++++++++ pkg/objectstorage/upload_bench_test.go | 124 +++++++++ pkg/objectstorage/upload_gcs_test.go | 178 ++++++++++++ pkg/objectstorage/upload_s3_test.go | 151 +++++++++++ pkg/objectstorage/upload_test.go | 317 ++++++++++++++++++++++ 18 files changed, 1313 insertions(+), 122 deletions(-) create mode 100644 cmd/objectstore-plugin/README.md create mode 100644 internal/objectstoreplugin/upload_test.go create mode 100644 pkg/objectstorage/upload.go create mode 100644 pkg/objectstorage/upload_bench_test.go create mode 100644 pkg/objectstorage/upload_gcs_test.go create mode 100644 pkg/objectstorage/upload_s3_test.go create mode 100644 pkg/objectstorage/upload_test.go diff --git a/cmd/atelet/snapshot_transfer_test.go b/cmd/atelet/snapshot_transfer_test.go index fb34b56e26..b0c8bbaba6 100644 --- a/cmd/atelet/snapshot_transfer_test.go +++ b/cmd/atelet/snapshot_transfer_test.go @@ -51,7 +51,7 @@ import ( func newPluginHerder(t *testing.T, store objectstorage.ObjectStorage) *Atelet { t.Helper() // Test directories come from t.TempDir, so admit any absolute path. - plugin, err := objectstoreplugin.NewNodePlugin(store, "/") + plugin, err := objectstoreplugin.NewNodePlugin(store, "/", objectstorage.DefaultUploadMemory) if err != nil { t.Fatal(err) } @@ -320,7 +320,7 @@ type pluginSidecar struct { func newPluginSidecar(t *testing.T, store objectstorage.ObjectStorage) *pluginSidecar { t.Helper() - plugin, err := objectstoreplugin.NewNodePlugin(store, "/") + plugin, err := objectstoreplugin.NewNodePlugin(store, "/", objectstorage.DefaultUploadMemory) if err != nil { t.Fatal(err) } diff --git a/cmd/objectstore-plugin/README.md b/cmd/objectstore-plugin/README.md new file mode 100644 index 0000000000..9e0b5d2fcb --- /dev/null +++ b/cmd/objectstore-plugin/README.md @@ -0,0 +1,44 @@ +# Object-store plugin upload memory + +The node plugin shares one upload memory budget across all snapshot requests. +`--upload-memory-budget` accepts a Kubernetes quantity and defaults to `512Mi`. +Uploads wait before allocating compressor or backend buffers. Waiting requests +cancel with their RPC, and failed uploads release their reservations after their +compression and upload workers stop. + +Reservations cover parallel compression input buffers, queued compressed frames, +zstd state, and the storage client's upload buffers. GCS range uploads reserve +serial encoders and resumable-upload buffers together. Large files reduce their +compression or range parallelism to fit the budget; file size does not need to fit +in memory. The sparse format and download readers are unchanged. + +With the current buffer sizes, the minimum budget is `384Mi` for GCS and `224Mi` +for S3. Startup rejects smaller values. The accounting is conservative: GCS +reserves for a composite upload even when an object ultimately fits in one +request. Raise the budget to trade memory for more simultaneous uploads. + +The budget is **not a process memory limit**. The Go runtime, garbage awaiting +collection, connections, downloads, and asset transfers need additional memory. +Size the sidecar's Kubernetes memory request and limit for those costs as well; +the existing `128Mi` request is not a bound on its usage. If setting `GOMEMLIMIT`, +leave headroom below the container limit for memory outside the Go heap. + +A manifest included with data files is uploaded only after all those files +succeed. The existing two-RPC flow (data, then manifest) also uses the same budget. + +## Validation + +```sh +go test -race ./pkg/objectstorage ./internal/objectstoreplugin ./cmd/objectstore-plugin -count=2 +go test ./pkg/objectstorage -run '^$' -bench '^BenchmarkConcurrentSnapshotUploads$' -benchtime=1x -count=1 +``` + +The tests cover ten overlapping snapshots with eight data files each, canceled +waiters, source and destination failures, retry after failure, manifest ordering, +large sparse sources, and slow uploads through the real S3 upload manager and GCS +writers. Their network peers are simulated; they do not require cloud credentials. + +The benchmark compares admitting all 80 tiny-file uploads with sharing the +budget, using a destination that stalls for 20 ms. It samples peak heap growth +and reports files per second. It isolates compression backpressure; it is not a +measurement of cloud throughput or the sidecar's total resident memory. diff --git a/cmd/objectstore-plugin/main.go b/cmd/objectstore-plugin/main.go index fba93439c2..1da25a553c 100644 --- a/cmd/objectstore-plugin/main.go +++ b/cmd/objectstore-plugin/main.go @@ -58,6 +58,7 @@ func main() { flags := pflag.NewFlagSet(mode, pflag.ExitOnError) socket := flags.String("socket", "", "Unix socket to serve on, or to check in healthcheck mode") root := flags.String("root", nodepath.BasePath, "node mode: the only directory tree local snapshot and asset files may be read from or written to") + uploadMemory := flags.String("upload-memory-budget", resource.NewQuantity(objectstorage.DefaultUploadMemory, resource.BinarySI).String(), "node mode: shared memory reservations for snapshot compression and upload buffers, as a Kubernetes quantity; uploads wait for capacity") assetStagingDir := flags.String("asset-staging-dir", "/var/lib/objectstore-plugin/asset-staging", "node mode: directory sandbox assets are downloaded into and verified before they are copied below --root; must not be below --root or readable by the caller") assetStagingCapacity := flags.String("asset-staging-capacity", "8Gi", "node mode: the most sandbox asset downloads in --asset-staging-dir may hold at once, as a Kubernetes quantity; downloads wait for space, and an asset larger than this is refused") timeout := flags.Duration("timeout", 5*time.Second, "healthcheck mode: how long to wait for the plugin to report that it is serving") @@ -98,9 +99,13 @@ func main() { if err != nil { serverboot.Fatal(ctx, "Failed to set up the object storage backend", err) } - plugin, err := objectstoreplugin.NewNodePlugin(objects, *root) + uploadCapacity, err := parseCapacity(*uploadMemory) if err != nil { - serverboot.Fatal(ctx, "Invalid --root", err) + serverboot.Fatal(ctx, "Invalid --upload-memory-budget", err) + } + plugin, err := objectstoreplugin.NewNodePlugin(objects, *root, uploadCapacity) + if err != nil { + serverboot.Fatal(ctx, "Invalid node upload configuration", err) } objectstorev1.RegisterNodeProviderServer(srv, plugin) capacity, err := parseCapacity(*assetStagingCapacity) diff --git a/internal/objectstoreplugin/node.go b/internal/objectstoreplugin/node.go index 99df8b87fc..c4180f65d1 100644 --- a/internal/objectstoreplugin/node.go +++ b/internal/objectstoreplugin/node.go @@ -25,9 +25,9 @@ import ( "context" "errors" "fmt" - "io" "os" "path/filepath" + "slices" "github.com/agent-substrate/substrate/internal/resources" "github.com/agent-substrate/substrate/pkg/objectstorage" @@ -47,17 +47,23 @@ type NodePlugin struct { objectstorev1.UnimplementedNodeProviderServer gcsClient objectstorage.ObjectStorage + uploader *objectstorage.Uploader // root confines every local path a caller names. root string } // NewNodePlugin returns a NodePlugin that reads and writes local files only -// below root. -func NewNodePlugin(gcsClient objectstorage.ObjectStorage, root string) (*NodePlugin, error) { +// below root. All upload requests share uploadMemory bytes of reservations for +// compression and backend buffers. +func NewNodePlugin(gcsClient objectstorage.ObjectStorage, root string, uploadMemory int64) (*NodePlugin, error) { if !filepath.IsAbs(root) { return nil, fmt.Errorf("root %q is not an absolute path", root) } - return &NodePlugin{gcsClient: gcsClient, root: filepath.Clean(root)}, nil + uploader, err := objectstorage.NewUploader(gcsClient, uploadMemory) + if err != nil { + return nil, err + } + return &NodePlugin{gcsClient: gcsClient, uploader: uploader, root: filepath.Clean(root)}, nil } // FetchSnapshot downloads the requested snapshot files into write_path. @@ -119,38 +125,47 @@ func (p *NodePlugin) UploadSnapshot(ctx context.Context, req *objectstorev1.Uplo } defer root.Close() + upload := func(ctx context.Context, fileName string) error { + local, err := root.Open(fileName) + if err != nil { + return fmt.Errorf("while opening %s in snapshot directory: %w", fileName, err) + } + defer local.Close() + info, err := local.Stat() + if err != nil { + return fmt.Errorf("while inspecting %s in snapshot directory: %w", fileName, err) + } + if !info.Mode().IsRegular() { + return fmt.Errorf("snapshot file %s is not a regular file", fileName) + } + if fileName == manifestFile { + return p.uploadManifest(ctx, uri, local) + } + objectURI, err := uri.ObjectURI(fileName + ".zstd") + if err != nil { + return fmt.Errorf("while addressing %s in GCS: %w", fileName, err) + } + if err := p.uploader.SendFile(ctx, objectURI, local); err != nil { + return fmt.Errorf("while uploading %s to GCS: %w", fileName, err) + } + return nil + } g, gCtx := errgroup.WithContext(ctx) for _, fileName := range req.GetFiles() { - g.Go(func() error { - local, err := root.Open(fileName) - if err != nil { - return fmt.Errorf("while opening %s in snapshot directory: %w", fileName, err) - } - defer local.Close() - info, err := local.Stat() - if err != nil { - return fmt.Errorf("while inspecting %s in snapshot directory: %w", fileName, err) - } - if !info.Mode().IsRegular() { - return fmt.Errorf("snapshot file %s is not a regular file", fileName) - } - if fileName == manifestFile { - return p.uploadManifest(gCtx, uri, local) - } - - objectURI, err := uri.ObjectURI(fileName + ".zstd") - if err != nil { - return fmt.Errorf("while addressing %s in GCS: %w", fileName, err) - } - if err := objectstorage.SendFileToGCSWithZstd(gCtx, p.gcsClient, objectURI, local); err != nil { - return fmt.Errorf("while uploading %s to GCS: %w", fileName, err) - } - return nil - }) + if fileName != manifestFile { + g.Go(func() error { return upload(gCtx, fileName) }) + } } if err := g.Wait(); err != nil { return nil, toStatus(err) } + // A manifest commits the snapshot only after every requested data file has + // succeeded. Use ctx: errgroup cancels gCtx when Wait returns. + if slices.Contains(req.GetFiles(), manifestFile) { + if err := upload(ctx, manifestFile); err != nil { + return nil, toStatus(err) + } + } return &objectstorev1.UploadSnapshotResponse{}, nil } @@ -172,15 +187,11 @@ func (p *NodePlugin) fetchManifest(ctx context.Context, uri resources.SnapshotUR // uploadManifest uploads the manifest from local uncompressed. func (p *NodePlugin) uploadManifest(ctx context.Context, uri resources.SnapshotURI, local *os.File) error { - manifest, err := io.ReadAll(local) - if err != nil { - return fmt.Errorf("while reading %s in snapshot directory: %w", manifestFile, err) - } manifestURI, err := uri.ObjectURI(manifestFile) if err != nil { return fmt.Errorf("while addressing snapshot manifest in GCS: %w", err) } - if err := objectstorage.SendBytesToGCS(ctx, p.gcsClient, manifestURI, manifest); err != nil { + if err := p.uploader.Send(ctx, manifestURI, local); err != nil { return fmt.Errorf("while uploading snapshot manifest: %w", err) } return nil diff --git a/internal/objectstoreplugin/plugin_test.go b/internal/objectstoreplugin/plugin_test.go index d9b0b12121..6793586348 100644 --- a/internal/objectstoreplugin/plugin_test.go +++ b/internal/objectstoreplugin/plugin_test.go @@ -124,7 +124,7 @@ func serve(t *testing.T, backend *memObjects, root string) *grpc.ClientConn { if err != nil { t.Fatal(err) } - node, err := NewNodePlugin(backend, root) + node, err := NewNodePlugin(backend, root, objectstorage.DefaultUploadMemory) if err != nil { t.Fatal(err) } diff --git a/internal/objectstoreplugin/upload_test.go b/internal/objectstoreplugin/upload_test.go new file mode 100644 index 0000000000..216530ac05 --- /dev/null +++ b/internal/objectstoreplugin/upload_test.go @@ -0,0 +1,200 @@ +// 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 objectstoreplugin + +import ( + "context" + "errors" + "fmt" + "io" + "os" + "path/filepath" + "strings" + "sync/atomic" + "testing" + "testing/synctest" + + objectstorev1 "github.com/agent-substrate/substrate/pkg/proto/objectstorepb/v1" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +type slowSnapshotStore struct { + *memObjects + release <-chan struct{} + active atomic.Int64 + peak atomic.Int64 + manifests atomic.Int64 + fail bool +} + +func (s *slowSnapshotStore) PutObject(ctx context.Context, bucket, object string, r io.Reader) error { + if strings.HasSuffix(object, "/"+manifestFile) { + s.manifests.Add(1) + return s.memObjects.PutObject(ctx, bucket, object, r) + } + n := s.active.Add(1) + defer s.active.Add(-1) + for old := s.peak.Load(); n > old && !s.peak.CompareAndSwap(old, n); old = s.peak.Load() { + } + select { + case <-s.release: + case <-ctx.Done(): + return ctx.Err() + } + if s.fail { + return errors.New("upload failed") + } + return s.memObjects.PutObject(ctx, bucket, object, r) +} + +func snapshotUploadFiles(t *testing.T, dir string, n int) []string { + t.Helper() + if err := os.MkdirAll(dir, 0o700); err != nil { + t.Fatal(err) + } + files := []string{manifestFile} + for i := range n { + files = append(files, fmt.Sprintf("file%d", i)) + } + for _, name := range files { + if err := os.WriteFile(filepath.Join(dir, name), []byte("snapshot payload"), 0o600); err != nil { + t.Fatal(err) + } + } + return files +} + +func TestUploadBudgetSharedAcrossSnapshots(t *testing.T) { + root := t.TempDir() + const snapshots, filesPerSnapshot = 10, 8 + requests := make([]*objectstorev1.UploadSnapshotRequest, snapshots) + for i := range snapshots { + dir := filepath.Join(root, fmt.Sprintf("src%d", i)) + requests[i] = &objectstorev1.UploadSnapshotRequest{ + SnapshotUri: fmt.Sprintf("%s%d", testURI, i), LocalPath: dir, + Files: snapshotUploadFiles(t, dir, filesPerSnapshot), + } + } + synctest.Test(t, func(t *testing.T) { + gate := make(chan struct{}) + store := &slowSnapshotStore{memObjects: newMemObjects(), release: gate} + // This backend has no SDK buffers. Two compression reservations fit. + node, err := NewNodePlugin(store, root, 128<<20) + if err != nil { + t.Fatal(err) + } + done := make(chan error, snapshots) + for _, req := range requests { + go func() { _, err := node.UploadSnapshot(t.Context(), req); done <- err }() + } + synctest.Wait() + if got := store.active.Load(); got != 2 { + t.Errorf("active uploads = %d, want 2 across all requests", got) + } + if got := store.manifests.Load(); got != 0 { + t.Errorf("committed %d manifests before data finished", got) + } + close(gate) + synctest.Wait() + for range snapshots { + if err := <-done; err != nil { + t.Fatal(err) + } + } + if got := store.peak.Load(); got != 2 { + t.Errorf("peak uploads = %d, want 2", got) + } + if got := len(store.keys()); got != snapshots*(filesPerSnapshot+1) { + t.Fatalf("uploaded %d objects", got) + } + // Existing callers also send manifests in a separate RPC. Verify a + // combined request's stored data still downloads through the same reader. + dst := filepath.Join(root, "restored") + if err := os.Mkdir(dst, 0o700); err != nil { + t.Fatal(err) + } + if _, err := node.FetchSnapshot(t.Context(), &objectstorev1.FetchSnapshotRequest{ + SnapshotUri: requests[0].SnapshotUri, WritePath: dst, Files: requests[0].Files, + }); err != nil { + t.Fatal(err) + } + for _, name := range requests[0].Files { + data, err := os.ReadFile(filepath.Join(dst, name)) + if err != nil || string(data) != "snapshot payload" { + t.Fatalf("%s: %q, %v", name, data, err) + } + } + }) +} + +func TestUploadFailureDoesNotCommitManifest(t *testing.T) { + root := t.TempDir() + dir := filepath.Join(root, "src") + files := snapshotUploadFiles(t, dir, 3) + gate := make(chan struct{}) + close(gate) + store := &slowSnapshotStore{memObjects: newMemObjects(), release: gate, fail: true} + node, err := NewNodePlugin(store, root, 64<<20) + if err != nil { + t.Fatal(err) + } + if _, err := node.UploadSnapshot(t.Context(), &objectstorev1.UploadSnapshotRequest{ + SnapshotUri: testURI, LocalPath: dir, Files: files, + }); status.Code(err) != codes.Internal { + t.Fatalf("got %v, want upload error", err) + } + if got := store.manifests.Load(); got != 0 { + t.Fatalf("committed %d manifests after data failed", got) + } + store.fail = false + if _, err := node.UploadSnapshot(t.Context(), &objectstorev1.UploadSnapshotRequest{ + SnapshotUri: testURI, LocalPath: dir, Files: files, + }); err != nil { + t.Fatalf("retry after failure: %v", err) + } +} + +func TestUploadCanceledWaiterReturnsCanceled(t *testing.T) { + root := t.TempDir() + dir := filepath.Join(root, "src") + files := snapshotUploadFiles(t, dir, 1) + synctest.Test(t, func(t *testing.T) { + gate := make(chan struct{}) + store := &slowSnapshotStore{memObjects: newMemObjects(), release: gate} + node, err := NewNodePlugin(store, root, 64<<20) + if err != nil { + t.Fatal(err) + } + req := &objectstorev1.UploadSnapshotRequest{SnapshotUri: testURI, LocalPath: dir, Files: files} + first := make(chan error, 1) + go func() { _, err := node.UploadSnapshot(t.Context(), req); first <- err }() + synctest.Wait() + ctx, cancel := context.WithCancel(t.Context()) + waiting := make(chan error, 1) + go func() { _, err := node.UploadSnapshot(ctx, req); waiting <- err }() + synctest.Wait() + cancel() + synctest.Wait() + if err := <-waiting; status.Code(err) != codes.Canceled { + t.Errorf("waiting upload: %v", err) + } + close(gate) + synctest.Wait() + if err := <-first; err != nil { + t.Fatal(err) + } + }) +} diff --git a/pkg/objectstorage/gcssparse.go b/pkg/objectstorage/gcssparse.go index cb1ea0602f..b19f2f52a9 100644 --- a/pkg/objectstorage/gcssparse.go +++ b/pkg/objectstorage/gcssparse.go @@ -42,7 +42,7 @@ const partWriterChunk = 16 << 20 // The parts are consecutive byte ranges of the same sparse-extent stream — part 0 // carries the header, the last carries the end sentinel — so the composed object is // exactly what the single-stream writer would have produced. -func (g *gcsClient) PutSparseFile(ctx context.Context, bucket, object string, f *os.File) (res writeContentResult, err error) { +func (g *gcsClient) PutSparseFile(ctx context.Context, bucket, object string, f *os.File, maxParts int) (res writeContentResult, err error) { fi, err := f.Stat() if err != nil { return res, err @@ -54,7 +54,7 @@ func (g *gcsClient) PutSparseFile(ctx context.Context, bucket, object string, f } res = writeContentResult{logicalBytes: size, populatedBytes: populated, sparse: true} - n := sparsePartCount(populated) + n := min(sparsePartCount(populated), maxParts) if n < 2 { // Too small to be worth splitting; the plain streaming path handles it. return res, errSparseTooSmall @@ -88,7 +88,7 @@ func (g *gcsClient) PutSparseFile(ctx context.Context, bucket, object string, f obj := g.uploadClient(gctx, i).Bucket(bucket).Object(parts[i].ObjectName()) w := obj.NewWriter(gctx) w.ChunkSize = partWriterChunk - if err := writeSparsePart(w, f, size, ranges, i == 0, i == len(groups)-1); err != nil { + if err := writeSparsePart(contextWriter{ctx: gctx, dst: w}, f, size, ranges, i == 0, i == len(groups)-1); err != nil { _ = w.Close() return fmt.Errorf("while writing part %d of %q: %w", i, object, err) } diff --git a/pkg/objectstorage/objects.go b/pkg/objectstorage/objects.go index 6e697747cb..c7c696df97 100644 --- a/pkg/objectstorage/objects.go +++ b/pkg/objectstorage/objects.go @@ -24,7 +24,6 @@ import ( "log/slog" "net/url" "os" - "runtime" "strings" "time" @@ -130,22 +129,6 @@ func Open(ctx context.Context, client ObjectStorage, gsURL string) (io.ReadClose return rc, nil } -// SendBytesToGCS uploads the given bytes (uncompressed) to gsURL. Intended for -// small objects such as the snapshot manifest. -func SendBytesToGCS(ctx context.Context, client ObjectStorage, gsURL string, content []byte) error { - ctx, span := tracer.Start(ctx, "sendBytesToGCS") - defer span.End() - - bucket, object, err := parseGCSURL(gsURL) - if err != nil { - return fmt.Errorf("while parsing url: %w", err) - } - if err := client.PutObject(ctx, bucket, object, bytes.NewReader(content)); err != nil { - return fmt.Errorf("while putting object bucket=%q object=%q: %w", bucket, object, err) - } - return nil -} - func SendLocalFileToGCSWithZstd(ctx context.Context, client ObjectStorage, gsURL string, localFilePath string) (err error) { ctx, span := tracer.Start(ctx, "sendLocalFileToGCSWithZstd") defer span.End() @@ -171,24 +154,12 @@ func SendLocalFileToGCSWithZstd(ctx context.Context, client ObjectStorage, gsURL return nil } -// SendFileToGCSWithZstd compresses and uploads an already-open file. The -// caller retains ownership of localFile. -func SendFileToGCSWithZstd(ctx context.Context, client ObjectStorage, gsURL string, localFile *os.File) error { - ctx, span := tracer.Start(ctx, "sendFileToGCSWithZstd") - defer span.End() - - if err := sendZstd(ctx, client, gsURL, localFile); err != nil { - return fmt.Errorf("in sendZstd: %w", err) - } - return nil -} - // sparseFilePutter marks a backend that can upload a sparse FILE directly, splitting // compression and upload by file range instead of piping one compressed stream into a // part splitter. Reports errSparseTooSmall when the file is not worth splitting, in // which case the caller falls back to the streaming path. type sparseFilePutter interface { - PutSparseFile(ctx context.Context, bucket, object string, f *os.File) (writeContentResult, error) + PutSparseFile(ctx context.Context, bucket, object string, f *os.File, maxParts int) (writeContentResult, error) } // streamingPutter marks an ObjectStorage whose PutObject accepts a non-seekable @@ -212,15 +183,15 @@ type writeContentResult struct { // seekable *os.File (compress only the populated extents, skip the holes) or a // plain zstd stream otherwise. It touches only io, so it is unit-testable without // an object store, and is shared by the buffered and streaming upload paths. -func writeContent(out io.Writer, content io.Reader) (writeContentResult, error) { +func writeContent(ctx context.Context, out io.Writer, content io.Reader, workers int) (writeContentResult, error) { if f, ok := content.(*os.File); ok { - logical, populated, err := writeSparseZstd(out, f) + logical, populated, err := writeSparseZstd(ctx, out, f, workers) if err != nil { return writeContentResult{}, err } return writeContentResult{logicalBytes: logical, populatedBytes: populated, sparse: true}, nil } - logical, err := plainZstd(out, content) + logical, err := plainZstd(ctx, out, content, workers) if err != nil { return writeContentResult{}, err } @@ -230,27 +201,40 @@ func writeContent(out io.Writer, content io.Reader) (writeContentResult, error) // sendZstd zstd-compresses content and uploads it to gsURL. // // The snapshot memory-ranges is the large object here (the whole guest RAM image, -// mostly zero) on the SUSPEND critical path, so we compress with SpeedFastest across -// all CPUs — high-ratio levels scan the multi-GiB image far slower for little size -// gain on near-zero data, and the decoder auto-detects the level so restore + older +// mostly zero) on the SUSPEND critical path, so we compress with SpeedFastest within +// the shared upload memory budget. High-ratio levels scan the multi-GiB image +// far slower for little size gain on near-zero data, and the decoder auto-detects the level so restore + older // snapshots are unaffected. // // Upload strategy depends on the backend: -// - Streaming backends (GCS) accept a non-seekable body, so we pipe the compressor +// - Streaming backends (GCS and S3) accept a non-seekable body, so we pipe the compressor // straight into PutObject: the compress overlaps the network PUT and we never // stage the ~100MiB compressed payload to a temp file. -// - S3/rustfs PutObject hands the body to the AWS SDK, which needs a seekable body -// to sign + set Content-Length (a non-seekable pipe hangs there), so we compress -// to a SEEKABLE temp file first. +// - Other backends receive a seekable temporary file containing the compressed +// content. func sendZstd(ctx context.Context, client ObjectStorage, gsURL string, content io.Reader) error { + u := &Uploader{client: client, budget: defaultUploadBudget} + return u.sendZstd(ctx, gsURL, content) +} + +func (u *Uploader) sendZstd(ctx context.Context, gsURL string, content io.Reader) error { + client := u.client bucket, object, err := parseGCSURL(gsURL) if err != nil { return fmt.Errorf("while parsing URL: %w", err) } + plan, err := u.plan(content) + if err != nil { + return err + } + if err := u.budget.reserved.Acquire(ctx, plan.memory); err != nil { + return fmt.Errorf("waiting for upload memory: %w", err) + } + defer u.budget.reserved.Release(plan.memory) tStart := time.Now() if sp, ok := client.(sparseFilePutter); ok { if f, isFile := content.(*os.File); isFile { - res, err := sp.PutSparseFile(ctx, bucket, object, f) + res, err := sp.PutSparseFile(ctx, bucket, object, f, plan.parts) switch { case err == nil: slog.InfoContext(ctx, "Compressed zstd upload", @@ -270,15 +254,15 @@ func sendZstd(ctx context.Context, client ObjectStorage, gsURL string, content i } } if _, ok := client.(streamingPutter); ok { - return sendStreamingZstd(ctx, client, bucket, object, content, tStart) + return sendStreamingZstd(ctx, client, bucket, object, content, tStart, plan.workers) } - return sendBufferedZstd(ctx, client, bucket, object, content, tStart) + return sendBufferedZstd(ctx, client, bucket, object, content, tStart, plan.workers) } // sendBufferedZstd compresses content to a seekable temp file, then uploads it. -// Used for backends (S3/rustfs) whose PutObject needs a seekable body to sign and -// set Content-Length; the streaming counterpart is sendStreamingZstd. -func sendBufferedZstd(ctx context.Context, client ObjectStorage, bucket, object string, content io.Reader, tStart time.Time) error { +// Used for backends that require a seekable body; the streaming counterpart is +// sendStreamingZstd. +func sendBufferedZstd(ctx context.Context, client ObjectStorage, bucket, object string, content io.Reader, tStart time.Time, workers int) error { tmpFile, err := os.CreateTemp("", "substrate-upload-compress-") if err != nil { return fmt.Errorf("while creating temp compress file: %w", err) @@ -287,7 +271,7 @@ func sendBufferedZstd(ctx context.Context, client ObjectStorage, bucket, object defer tmpFile.Close() t0 := time.Now() - res, err := writeContent(tmpFile, content) + res, err := writeContent(ctx, tmpFile, content, workers) if err != nil { return fmt.Errorf("while compressing %q: %w", object, err) } @@ -310,8 +294,10 @@ func sendBufferedZstd(ctx context.Context, client ObjectStorage, bucket, object // goroutine writes the (sparse-extent or plain) zstd stream into an io.Pipe while // PutObject streams the read end to the object store. No seekable temp file, and // the compress runs concurrently with the network PUT. Used only for streaming -// backends (GCS); see sendZstd. -func sendStreamingZstd(ctx context.Context, client ObjectStorage, bucket, object string, content io.Reader, tStart time.Time) error { +// backends (GCS and S3); see sendZstd. +func sendStreamingZstd(ctx context.Context, client ObjectStorage, bucket, object string, content io.Reader, tStart time.Time, workers int) error { + ctx, cancel := context.WithCancel(ctx) + defer cancel() type result struct { res writeContentResult err error @@ -319,7 +305,7 @@ func sendStreamingZstd(ctx context.Context, client ObjectStorage, bucket, object pr, pw := io.Pipe() ch := make(chan result, 1) go func() { - res, err := writeContent(pw, content) + res, err := writeContent(ctx, pw, content, workers) // Closing the writer delivers EOF (or the compress error) to PutObject. _ = pw.CloseWithError(err) ch <- result{res: res, err: err} @@ -327,6 +313,7 @@ func sendStreamingZstd(ctx context.Context, client ObjectStorage, bucket, object putErr := client.PutObject(ctx, bucket, object, pr) if putErr != nil { + cancel() // PutObject bailed (e.g. mid-stream); unblock the compressor goroutine so it // can finish and we don't deadlock on the channel receive below. _ = pr.CloseWithError(putErr) @@ -345,16 +332,16 @@ func sendStreamingZstd(ctx context.Context, client ObjectStorage, bucket, object return nil } -// plainZstd writes src to w as a single plain zstd stream (SpeedFastest, all -// cores) and returns the uncompressed byte count. -func plainZstd(w io.Writer, src io.Reader) (int64, error) { +// plainZstd writes src to w as a single plain zstd stream and returns the +// uncompressed byte count, using only the reserved compression workers. +func plainZstd(ctx context.Context, w io.Writer, src io.Reader, workers int) (int64, error) { zw, err := zstd.NewWriter(w, zstd.WithEncoderLevel(zstd.SpeedFastest), - zstd.WithEncoderConcurrency(runtime.GOMAXPROCS(0))) + zstd.WithEncoderConcurrency(workers)) if err != nil { return 0, err } - n, err := io.Copy(zw, src) + n, err := io.Copy(contextWriter{ctx: ctx, dst: zw}, src) if err != nil { zw.Close() return n, err diff --git a/pkg/objectstorage/objects_test.go b/pkg/objectstorage/objects_test.go index 53ae63fdd7..4b74497962 100644 --- a/pkg/objectstorage/objects_test.go +++ b/pkg/objectstorage/objects_test.go @@ -72,7 +72,7 @@ func (s *closeErrorStore) GetObject(ctx context.Context, bucket, object string) func TestFetchFromGCSWithZstdCloseError(t *testing.T) { var compressed bytes.Buffer - if _, err := writeContent(&compressed, bytes.NewBufferString("snapshot payload")); err != nil { + if _, err := writeContent(context.Background(), &compressed, bytes.NewBufferString("snapshot payload"), 2); err != nil { t.Fatal(err) } closeErr := errors.New("object reader close failed") @@ -253,7 +253,7 @@ func TestSparseVersionRejected(t *testing.T) { t.Fatal(err) } var buf bytes.Buffer - if _, _, err := writeSparseZstd(&buf, f); err != nil { + if _, _, err := writeSparseZstd(context.Background(), &buf, f, 2); err != nil { t.Fatalf("writeSparseZstd: %v", err) } f.Close() @@ -307,7 +307,7 @@ func TestWriteSparseZstdSkipsHoles(t *testing.T) { t.Fatal(err) } var buf bytes.Buffer - logical, dataBytes, err := writeSparseZstd(&buf, src) + logical, dataBytes, err := writeSparseZstd(context.Background(), &buf, src, 2) if err != nil { t.Fatalf("writeSparseZstd: %v", err) } @@ -328,8 +328,7 @@ func TestPlainZstdBackwardCompatRoundTrip(t *testing.T) { store := newMemStore() ctx := context.Background() const gsURL = "gs://bucket/snap/config.json.zstd" - // SendBytesToGCS is uncompressed; use sendZstd with a non-file reader to - // hit the plain-zstd branch (no magic). + // Use a non-file reader to hit the plain-zstd branch (no magic). if err := sendZstd(ctx, store, gsURL, bytes.NewReader(want)); err != nil { t.Fatalf("upload: %v", err) } @@ -451,7 +450,7 @@ func TestWriteDecodeContentRoundTrip(t *testing.T) { } var buf bytes.Buffer - wres, err := writeContent(&buf, src) + wres, err := writeContent(context.Background(), &buf, src, 2) if err != nil { t.Fatalf("writeContent: %v", err) } @@ -495,7 +494,7 @@ func TestWriteDecodeContentRoundTrip(t *testing.T) { t.Run("plain reader", func(t *testing.T) { want := bytes.Repeat([]byte("substrate payload\n"), 1000) var buf bytes.Buffer - wres, err := writeContent(&buf, bytes.NewReader(want)) + wres, err := writeContent(context.Background(), &buf, bytes.NewReader(want), 2) if err != nil { t.Fatalf("writeContent: %v", err) } diff --git a/pkg/objectstorage/parzstd.go b/pkg/objectstorage/parzstd.go index 5b8a91f465..894017c3aa 100644 --- a/pkg/objectstorage/parzstd.go +++ b/pkg/objectstorage/parzstd.go @@ -17,6 +17,7 @@ package objectstorage import ( "io" "runtime" + "sync" "github.com/klauspost/compress/zstd" ) @@ -47,12 +48,13 @@ type parZstd struct { dst io.Writer workers int - buf []byte - free chan []byte - jobs chan parZstdJob - ordered chan chan []byte - done chan struct{} - err error + buf []byte + free chan []byte + jobs chan parZstdJob + ordered chan chan []byte + done chan struct{} + err error + workerWG sync.WaitGroup } type parZstdJob struct { @@ -77,7 +79,7 @@ func newParZstd(dst io.Writer, workers int) *parZstd { p.free <- make([]byte, 0, parZstdChunk) } for range workers { - go p.worker() + p.workerWG.Go(p.worker) } go p.writer() p.buf = <-p.free @@ -142,6 +144,7 @@ func (p *parZstd) Close() error { close(p.jobs) close(p.ordered) <-p.done + p.workerWG.Wait() return p.err } diff --git a/pkg/objectstorage/sparseparts_test.go b/pkg/objectstorage/sparseparts_test.go index 02b9871d5b..fdaf959b0a 100644 --- a/pkg/objectstorage/sparseparts_test.go +++ b/pkg/objectstorage/sparseparts_test.go @@ -16,6 +16,7 @@ package objectstorage import ( "bytes" + "context" "io" "math/rand" "os" @@ -57,7 +58,7 @@ func TestSparsePartsRoundTrip(t *testing.T) { // The parts must land the same bytes as the single-stream writer. var want bytes.Buffer - if _, _, err := writeSparseZstd(&want, src); err != nil { + if _, _, err := writeSparseZstd(context.Background(), &want, src, 2); err != nil { t.Fatal(err) } if g, w := decodeSparse(t, &got), decodeSparse(t, &want); !bytes.Equal(g, w) { diff --git a/pkg/objectstorage/sparsezstd.go b/pkg/objectstorage/sparsezstd.go index dc78ddd6d6..05b18643e0 100644 --- a/pkg/objectstorage/sparsezstd.go +++ b/pkg/objectstorage/sparsezstd.go @@ -16,6 +16,7 @@ package objectstorage import ( "bufio" + "context" "encoding/binary" "fmt" "io" @@ -59,7 +60,7 @@ const sparseEndOffset int64 = -1 // compress from "scan the whole logical image" (e.g. 2GiB) to "scan the resident // set" (e.g. ~150MiB). Returns the logical size and the populated (pre-compression) // byte count. All integers are little-endian. -func writeSparseZstd(dst io.Writer, src *os.File) (logical, dataBytes int64, err error) { +func writeSparseZstd(ctx context.Context, dst io.Writer, src *os.File, workers int) (logical, dataBytes int64, err error) { fi, err := src.Stat() if err != nil { return 0, 0, err @@ -78,14 +79,14 @@ func writeSparseZstd(dst io.Writer, src *os.File) (logical, dataBytes int64, err return 0, 0, err } - // One worker per chunk the file can fill, so a small object does not pay for a pool. - zw := newParZstd(dst, int(size/parZstdChunk)+1) + zw := newParZstd(dst, workers) + cw := contextWriter{ctx: ctx, dst: zw} // fail closes the encoder before returning err (Close flushes/frees state). fail := func(e error) (int64, int64, error) { zw.Close() return 0, 0, e } - if err := binary.Write(zw, binary.LittleEndian, size); err != nil { + if err := binary.Write(cw, binary.LittleEndian, size); err != nil { return fail(err) } @@ -104,23 +105,23 @@ func writeSparseZstd(dst io.Writer, src *os.File) (logical, dataBytes int64, err return fail(fmt.Errorf("SEEK_HOLE: %w", serr)) } length := de - ds - if err := binary.Write(zw, binary.LittleEndian, ds); err != nil { + if err := binary.Write(cw, binary.LittleEndian, ds); err != nil { return fail(err) } - if err := binary.Write(zw, binary.LittleEndian, length); err != nil { + if err := binary.Write(cw, binary.LittleEndian, length); err != nil { return fail(err) } if _, err := src.Seek(ds, io.SeekStart); err != nil { return fail(err) } - n, cerr := io.CopyN(zw, src, length) + n, cerr := io.CopyN(cw, src, length) dataBytes += n if cerr != nil { return fail(fmt.Errorf("reading extent @%d+%d: %w", ds, length, cerr)) } off = de } - if err := binary.Write(zw, binary.LittleEndian, sparseEndOffset); err != nil { + if err := binary.Write(cw, binary.LittleEndian, sparseEndOffset); err != nil { return fail(err) } if err := zw.Close(); err != nil { diff --git a/pkg/objectstorage/sparsezstd_test.go b/pkg/objectstorage/sparsezstd_test.go index d26928a0d4..5fbb146c11 100644 --- a/pkg/objectstorage/sparsezstd_test.go +++ b/pkg/objectstorage/sparsezstd_test.go @@ -16,6 +16,7 @@ package objectstorage import ( "bytes" + "context" "encoding/binary" "os" "path/filepath" @@ -91,7 +92,7 @@ func TestSparseZstdRoundTrip(t *testing.T) { defer src.Close() var buf bytes.Buffer - logical, _, err := writeSparseZstd(&buf, src) + logical, _, err := writeSparseZstd(context.Background(), &buf, src, 2) if err != nil { t.Fatalf("writeSparseZstd: %v", err) } diff --git a/pkg/objectstorage/upload.go b/pkg/objectstorage/upload.go new file mode 100644 index 0000000000..ce116acfd3 --- /dev/null +++ b/pkg/objectstorage/upload.go @@ -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. + +package objectstorage + +import ( + "context" + "fmt" + "io" + "os" + "runtime" + + "golang.org/x/sync/semaphore" +) + +// DefaultUploadMemory is the shared reservation budget for snapshot compression +// and upload buffers. It is not a limit on the process's total memory. +const DefaultUploadMemory int64 = 512 << 20 + +const ( + // Each parallel encoder owns input buffers, queued compressed frames, and + // zstd state. Include the writer's frame and the producer's pending frame + // even when there is only one worker. + parallelWorkerMemory int64 = (parZstdQueue*3 + 2) * parZstdChunk + // A serial encoder has a 4 MiB window, history, blocks, and a copy buffer. + serialWorkerMemory int64 = 32 << 20 + sparseUploadMemory = serialWorkerMemory + partWriterChunk +) + +type uploadBudget struct { + capacity int64 + reserved *semaphore.Weighted +} + +var defaultUploadBudget = &uploadBudget{ + capacity: DefaultUploadMemory, + reserved: semaphore.NewWeighted(DefaultUploadMemory), +} + +// Uploader shares a memory reservation budget across concurrent uploads. Reuse +// one Uploader for all snapshot requests served by a node. Reservations cover +// compression and the GCS/S3 clients' buffering, and last until both finish. +// Other ObjectStorage implementations must bound their own upload buffers. +// Runtime overhead, downloads, and memory awaiting garbage collection are not +// covered by the reservation budget. +type Uploader struct { + client ObjectStorage + budget *uploadBudget +} + +// NewUploader creates an uploader with capacity bytes of reservations. Capacity +// must fit one compression worker plus the backend's upload buffers; larger files +// use fewer workers when necessary rather than waiting for an impossible request. +func NewUploader(client ObjectStorage, capacity int64) (*Uploader, error) { + minimum := uploadBufferMemory(client) + parallelWorkerMemory + if capacity < minimum { + return nil, fmt.Errorf("upload memory budget must be at least %d bytes for this backend", minimum) + } + return &Uploader{client: client, budget: &uploadBudget{ + capacity: capacity, + reserved: semaphore.NewWeighted(capacity), + }}, nil +} + +// uploadBufferMemory includes buffers retained while the compressor is running. +func uploadBufferMemory(client ObjectStorage) int64 { + switch client.(type) { + case *s3Client: + // The SDK holds one producer buffer in addition to the active parts. + return (uploadConcurrency + 1) * uploadPartSize + case *gcsClient: + // Composite uploads retain the lookahead, our part pool, and the SDK's + // copy of each part. Single uploads fit within this reservation too. + return uploadCompositeMin + 2*uploadConcurrency*uploadPartSize + default: + return 0 + } +} + +type uploadPlan struct { + workers int + parts int + memory int64 +} + +func (u *Uploader) plan(content io.Reader) (uploadPlan, error) { + buffers := uploadBufferMemory(u.client) + workers := min(int64(runtime.GOMAXPROCS(0)), (u.budget.capacity-buffers)/parallelWorkerMemory) + parts := 1 + if f, ok := content.(*os.File); ok { + info, err := f.Stat() + if err != nil { + return uploadPlan{}, err + } + position, err := f.Seek(0, io.SeekCurrent) + if err != nil { + return uploadPlan{}, err + } + _, populated, err := sparseExtents(f, info.Size()) + _, seekErr := f.Seek(position, io.SeekStart) + if err != nil { + return uploadPlan{}, err + } + if seekErr != nil { + return uploadPlan{}, seekErr + } + // Holes never enter the compressor or range uploads. + workers = min(workers, populated/parZstdChunk+1) + parts = min(sparsePartCount(populated), int(u.budget.capacity/sparseUploadMemory)) + } + memory := buffers + workers*parallelWorkerMemory + if _, ok := u.client.(sparseFilePutter); ok { + // The range-parallel path and streaming fallback are alternatives, + // so reserve the larger requirement rather than adding both. + memory = max(memory, int64(parts)*sparseUploadMemory) + } + return uploadPlan{workers: int(workers), parts: parts, memory: memory}, nil +} + +// SendFile compresses and uploads an already-open snapshot file. The caller keeps +// ownership of the file. Waiting for memory is canceled with ctx. +func (u *Uploader) SendFile(ctx context.Context, url string, file *os.File) error { + ctx, span := tracer.Start(ctx, "sendFileToGCSWithZstd") + defer span.End() + return u.sendZstd(ctx, url, file) +} + +// Send uploads uncompressed content, using the same budget as SendFile. This is +// used for snapshot manifests; the reader is streamed without retaining its body. +func (u *Uploader) Send(ctx context.Context, url string, content io.Reader) error { + ctx, span := tracer.Start(ctx, "sendBytesToGCS") + defer span.End() + + bucket, object, err := parseGCSURL(url) + if err != nil { + return err + } + memory := uploadBufferMemory(u.client) + if err := u.budget.reserved.Acquire(ctx, memory); err != nil { + return err + } + defer u.budget.reserved.Release(memory) + return u.client.PutObject(ctx, bucket, object, content) +} + +// contextWriter stops compression between writes even after an asynchronous +// destination has failed and is draining queued frames. +type contextWriter struct { + ctx context.Context + dst io.Writer +} + +func (w contextWriter) Write(b []byte) (int, error) { + if err := w.ctx.Err(); err != nil { + return 0, err + } + return w.dst.Write(b) +} diff --git a/pkg/objectstorage/upload_bench_test.go b/pkg/objectstorage/upload_bench_test.go new file mode 100644 index 0000000000..40beb8ae93 --- /dev/null +++ b/pkg/objectstorage/upload_bench_test.go @@ -0,0 +1,124 @@ +// 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 objectstorage + +import ( + "context" + "fmt" + "io" + "log/slog" + "os" + "path/filepath" + "runtime" + "sync" + "sync/atomic" + "testing" + "time" +) + +type delayedSnapshotStore struct{} + +func (delayedSnapshotStore) supportsStreamingPut() {} +func (delayedSnapshotStore) GetObject(context.Context, string, string) (io.ReadCloser, error) { + return nil, fmt.Errorf("unused") +} +func (delayedSnapshotStore) PutObject(ctx context.Context, _, _ string, r io.Reader) error { + var header [len(sparseMagic) + 4]byte + if _, err := io.ReadFull(r, header[:]); err != nil { + return err + } + // Keep compression buffers live while the destination applies backpressure. + select { + case <-time.After(20 * time.Millisecond): + case <-ctx.Done(): + return ctx.Err() + } + _, err := io.Copy(io.Discard, r) + return err +} + +// BenchmarkConcurrentSnapshotUploads compares the shared compression budget with +// admitting all 80 uploads at once. It samples peak heap, not cumulative allocation. +// SDK buffering is covered separately by the slow GCS and S3 tests. +func BenchmarkConcurrentSnapshotUploads(b *testing.B) { + const files = 80 + path := filepath.Join(b.TempDir(), "metadata") + if err := os.WriteFile(path, make([]byte, 36), 0o600); err != nil { + b.Fatal(err) + } + logger := slog.Default() + slog.SetDefault(slog.New(slog.NewTextHandler(io.Discard, nil))) + defer slog.SetDefault(logger) + for _, tc := range []struct { + name string + capacity int64 + }{ + {"shared_512MiB", DefaultUploadMemory}, + {"all_80_admitted", files * parallelWorkerMemory}, + } { + b.Run(tc.name, func(b *testing.B) { + u, err := NewUploader(delayedSnapshotStore{}, tc.capacity) + if err != nil { + b.Fatal(err) + } + runtime.GC() + var baseline runtime.MemStats + runtime.ReadMemStats(&baseline) + var peak atomic.Uint64 + peak.Store(baseline.HeapAlloc) + stop, stopped := make(chan struct{}), make(chan struct{}) + go func() { + defer close(stopped) + tick := time.NewTicker(time.Millisecond) + defer tick.Stop() + for { + select { + case <-stop: + return + case <-tick.C: + var stats runtime.MemStats + runtime.ReadMemStats(&stats) + if stats.HeapAlloc > peak.Load() { + peak.Store(stats.HeapAlloc) + } + } + } + }() + b.ResetTimer() + for range b.N { + var wg sync.WaitGroup + for i := range files { + wg.Go(func() { + f, err := os.Open(path) + if err != nil { + b.Error(err) + return + } + defer f.Close() + if err := u.SendFile(b.Context(), fmt.Sprintf("gs://bucket/file%d", i), f); err != nil { + b.Error(err) + } + }) + } + wg.Wait() + } + b.StopTimer() + close(stop) + <-stopped + b.ReportMetric(float64(peak.Load()-baseline.HeapAlloc)/(1<<20), "peak-heap-MiB") + b.ReportMetric(float64(files*b.N)/b.Elapsed().Seconds(), "files/s") + }) + } +} diff --git a/pkg/objectstorage/upload_gcs_test.go b/pkg/objectstorage/upload_gcs_test.go new file mode 100644 index 0000000000..ea865f978b --- /dev/null +++ b/pkg/objectstorage/upload_gcs_test.go @@ -0,0 +1,178 @@ +// 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 objectstorage + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "math/rand" + "net/http" + "net/url" + "os" + "path/filepath" + "strings" + "testing" + "testing/synctest" + + "google.golang.org/api/option" +) + +type slowGCSTransport struct { + parts chan string + release <-chan struct{} +} + +func (s *slowGCSTransport) RoundTrip(r *http.Request) (*http.Response, error) { + reply := func(body string) *http.Response { + return &http.Response{StatusCode: http.StatusOK, Header: http.Header{"Content-Type": {"application/json"}}, Body: io.NopCloser(strings.NewReader(body)), Request: r} + } + switch { + case r.URL.Query().Get("uploadType") == "resumable": + var metadata struct { + Name string `json:"name"` + } + if err := json.NewDecoder(r.Body).Decode(&metadata); err != nil { + return nil, err + } + response := reply("") + response.Header.Set("Location", "https://upload.test/session/"+url.PathEscape(metadata.Name)) + return response, nil + case strings.HasPrefix(r.URL.Path, "/session/"), r.URL.Query().Get("uploadType") == "multipart": + name := strings.TrimPrefix(r.URL.Path, "/session/") + if r.URL.Query().Get("uploadType") == "multipart" { + name = r.URL.Query().Get("name") + } + if _, err := io.Copy(io.Discard, r.Body); err != nil { + return nil, err + } + s.parts <- name + select { + case <-s.release: + case <-r.Context().Done(): + return nil, r.Context().Err() + } + return reply(fmt.Sprintf(`{"name":%q,"bucket":"bucket"}`, name)), nil + case strings.HasSuffix(r.URL.Path, "/compose"): + return reply(`{"name":"composed","bucket":"bucket"}`), nil + case r.Method == http.MethodDelete: + return reply(""), nil + default: + return nil, fmt.Errorf("unexpected GCS request: %s %s", r.Method, r.URL) + } +} + +func TestUploaderSlowGCSComposite(t *testing.T) { + // Use real GCS writers and the real composite part pool, replacing only + // HTTP transport so every part can be held without wall-clock sleeps. + synctest.Test(t, func(t *testing.T) { + gate := make(chan struct{}) + transport := &slowGCSTransport{parts: make(chan string, 16), release: gate} + client, err := NewGCSClient(t.Context(), option.WithoutAuthentication(), option.WithHTTPClient(&http.Client{Transport: transport})) + if err != nil { + t.Fatal(err) + } + g := client.(*gcsClient) + defer func() { + g.client.Close() + for _, c := range g.pool { + c.Close() + } + }() + u, err := NewUploader(client, uploadBufferMemory(client)+parallelWorkerMemory) + if err != nil { + t.Fatal(err) + } + send := func(name string) <-chan error { + done := make(chan error, 1) + go func() { + // A raw body just over the lookahead requires three actual + // composite parts, each with the SDK's retry buffer. + body := io.LimitReader(rand.New(rand.NewSource(2553)), uploadCompositeMin+1) + done <- u.Send(context.Background(), "gs://bucket/"+name, body) + }() + return done + } + first := send("first") + synctest.Wait() + if got := len(transport.parts); got != 3 { + t.Errorf("started %d GCS parts, want 3", got) + } + next := send("next") + synctest.Wait() + if got := len(transport.parts); got != 3 { + t.Errorf("next upload started before the first released its buffers: %d parts", got) + } + close(gate) + synctest.Wait() + if err := <-first; err != nil { + t.Error(err) + } + if err := <-next; err != nil { + t.Error(err) + } + if got := len(transport.parts); got != 6 { + t.Errorf("uploaded %d parts, want 6", got) + } + }) +} + +func TestGCSSparseUploadHonorsReservedParts(t *testing.T) { + file, err := os.Create(filepath.Join(t.TempDir(), "memory")) + if err != nil { + t.Fatal(err) + } + defer file.Close() + block := bytes.Repeat([]byte{0x5a}, 1<<20) + for range 3 * sparsePartTarget / len(block) { + if _, err := file.Write(block); err != nil { + t.Fatal(err) + } + } + if err := file.Truncate(2 << 30); err != nil { + t.Fatal(err) + } + if err := file.Sync(); err != nil { + t.Fatal(err) + } + synctest.Test(t, func(t *testing.T) { + gate := make(chan struct{}) + transport := &slowGCSTransport{parts: make(chan string, 8), release: gate} + client, err := NewGCSClient(t.Context(), option.WithoutAuthentication(), option.WithHTTPClient(&http.Client{Transport: transport})) + if err != nil { + t.Fatal(err) + } + g := client.(*gcsClient) + defer func() { + g.client.Close() + for _, c := range g.pool { + c.Close() + } + }() + done := make(chan error, 1) + go func() { _, err := g.PutSparseFile(t.Context(), "bucket", "memory", file, 2); done <- err }() + synctest.Wait() + if got := len(transport.parts); got != 2 { + t.Errorf("started %d sparse parts, want reserved limit 2", got) + } + close(gate) + synctest.Wait() + if err := <-done; err != nil { + t.Fatal(err) + } + }) +} diff --git a/pkg/objectstorage/upload_s3_test.go b/pkg/objectstorage/upload_s3_test.go new file mode 100644 index 0000000000..fefed3d3db --- /dev/null +++ b/pkg/objectstorage/upload_s3_test.go @@ -0,0 +1,151 @@ +// 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 objectstorage + +import ( + "context" + "errors" + "io" + "math/rand" + "os" + "path/filepath" + "sync/atomic" + "testing" + "testing/synctest" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/feature/s3/manager" + "github.com/aws/aws-sdk-go-v2/service/s3" +) + +// The real SDK uploader owns its normal part buffers; only its network calls +// are replaced, so a slow multipart upload exercises the production pipeline. +type slowMultipartAPI struct { + created chan string + parts chan string + release <-chan struct{} + fail string + aborted atomic.Int64 +} + +func (s *slowMultipartAPI) CreateMultipartUpload(_ context.Context, in *s3.CreateMultipartUploadInput, _ ...func(*s3.Options)) (*s3.CreateMultipartUploadOutput, error) { + s.created <- *in.Key + return &s3.CreateMultipartUploadOutput{UploadId: in.Key}, nil +} +func (s *slowMultipartAPI) UploadPart(ctx context.Context, in *s3.UploadPartInput, _ ...func(*s3.Options)) (*s3.UploadPartOutput, error) { + if _, err := io.Copy(io.Discard, in.Body); err != nil { + return nil, err + } + s.parts <- *in.Key + select { + case <-s.release: + case <-ctx.Done(): + return nil, ctx.Err() + } + if *in.Key == s.fail { + return nil, errors.New("part upload failed") + } + return &s3.UploadPartOutput{ETag: aws.String("etag")}, nil +} +func (s *slowMultipartAPI) CompleteMultipartUpload(context.Context, *s3.CompleteMultipartUploadInput, ...func(*s3.Options)) (*s3.CompleteMultipartUploadOutput, error) { + return &s3.CompleteMultipartUploadOutput{}, nil +} +func (s *slowMultipartAPI) AbortMultipartUpload(context.Context, *s3.AbortMultipartUploadInput, ...func(*s3.Options)) (*s3.AbortMultipartUploadOutput, error) { + s.aborted.Add(1) + return &s3.AbortMultipartUploadOutput{}, nil +} +func (*slowMultipartAPI) PutObject(context.Context, *s3.PutObjectInput, ...func(*s3.Options)) (*s3.PutObjectOutput, error) { + return nil, errors.New("expected a multipart upload") +} + +func TestUploaderSlowS3Multipart(t *testing.T) { + path := filepath.Join(t.TempDir(), "sparse-memory") + f, err := os.Create(path) + if err != nil { + t.Fatal(err) + } + // Incompressible data forces multiple SDK parts; the remaining 2 GiB is a + // hole, exercising sparse compression without allocating the logical size. + if _, err := io.CopyN(f, rand.New(rand.NewSource(2553)), 40<<20); err != nil { + t.Fatal(err) + } + if err := f.Truncate(2 << 30); err != nil { + t.Fatal(err) + } + if err := f.Close(); err != nil { + t.Fatal(err) + } + for _, failure := range []bool{false, true} { + t.Run(map[bool]string{false: "success", true: "failure"}[failure], func(t *testing.T) { + synctest.Test(t, func(t *testing.T) { + gate := make(chan struct{}) + api := &slowMultipartAPI{created: make(chan string, 4), parts: make(chan string, 8), release: gate} + if failure { + api.fail = "first" + } + //nolint:staticcheck // SA1019: use the same SDK uploader as s3Client. + client := &s3Client{uploader: manager.NewUploader(api, func(u *manager.Uploader) { + u.PartSize = uploadPartSize + u.Concurrency = uploadConcurrency + })} + u, err := NewUploader(client, uploadBufferMemory(client)+parallelWorkerMemory) + if err != nil { + t.Fatal(err) + } + send := func(name string) <-chan error { + done := make(chan error, 1) + go func() { + file, err := os.Open(path) + if err == nil { + defer file.Close() + err = u.SendFile(t.Context(), "gs://bucket/"+name, file) + } + done <- err + }() + return done + } + first := send("first") + synctest.Wait() + if got := len(api.parts); got != 2 { + t.Errorf("active SDK parts = %d, want 2", got) + } + if got := <-api.created; got != "first" { + t.Error(got) + } + next := send("next") + synctest.Wait() + select { + case got := <-api.created: + t.Errorf("%s started while first held SDK buffers", got) + default: + } + close(gate) + synctest.Wait() + if err := <-first; (err != nil) != failure { + t.Errorf("first: %v", err) + } + if err := <-next; err != nil { + t.Errorf("next: %v", err) + } + if got := <-api.created; got != "next" { + t.Error(got) + } + if failure && api.aborted.Load() != 1 { + t.Error("failed multipart upload was not aborted") + } + }) + }) + } +} diff --git a/pkg/objectstorage/upload_test.go b/pkg/objectstorage/upload_test.go new file mode 100644 index 0000000000..c0136d14d6 --- /dev/null +++ b/pkg/objectstorage/upload_test.go @@ -0,0 +1,317 @@ +// 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 objectstorage + +import ( + "context" + "errors" + "io" + "os" + "path/filepath" + "runtime" + "strings" + "testing" + "testing/synctest" +) + +func TestUploaderBudgetFitsBothPaths(t *testing.T) { + f, err := os.Create(filepath.Join(t.TempDir(), "sparse")) + if err != nil { + t.Fatal(err) + } + defer f.Close() + if err := f.Truncate(8 << 30); err != nil { + t.Fatal(err) + } + for _, client := range []ObjectStorage{&s3Client{}, &gcsClient{}, newMemStore()} { + minimum := uploadBufferMemory(client) + parallelWorkerMemory + if _, err := NewUploader(client, minimum-1); err == nil { + t.Fatalf("%T accepted a budget smaller than one upload", client) + } + for _, capacity := range []int64{minimum, DefaultUploadMemory} { + u, err := NewUploader(client, capacity) + if err != nil { + t.Fatal(err) + } + plan, err := u.plan(f) + if err != nil { + t.Fatal(err) + } + if plan.workers < 1 || plan.workers > runtime.GOMAXPROCS(0) || plan.memory > capacity { + t.Fatalf("%T: invalid plan %+v for capacity %d", client, plan, capacity) + } + if plan.memory < uploadBufferMemory(client)+int64(plan.workers)*parallelWorkerMemory { + t.Fatalf("%T: streaming pipeline under-reserved: %+v", client, plan) + } + if _, ok := client.(sparseFilePutter); ok && (plan.parts < 1 || int64(plan.parts)*sparseUploadMemory > plan.memory) { + t.Fatalf("%T: sparse pipeline under-reserved: %+v", client, plan) + } + } + } +} + +func TestUploaderPlansFromPopulatedBytes(t *testing.T) { + previous := runtime.GOMAXPROCS(8) + defer runtime.GOMAXPROCS(previous) + for _, tc := range []struct { + name string + populatedMiB int + gcs, s3 uploadPlan + }{ + {"all_holes", 0, uploadPlan{1, 1, 384 << 20}, uploadPlan{1, 1, 224 << 20}}, + {"small", 1, uploadPlan{1, 1, 384 << 20}, uploadPlan{1, 1, 224 << 20}}, + {"two_chunks", 9, uploadPlan{2, 1, 448 << 20}, uploadPlan{2, 1, 288 << 20}}, + {"multiple_ranges", 192, uploadPlan{3, 3, 512 << 20}, uploadPlan{5, 3, 480 << 20}}, + } { + t.Run(tc.name, func(t *testing.T) { + offsets := make([]int64, tc.populatedMiB) + for i := range offsets { + offsets[i] = int64(i) << 20 + } + f := sparseFileWith(t, 2<<30, offsets) + for _, backend := range []struct { + name string + client ObjectStorage + want uploadPlan + }{ + {"gcs", &gcsClient{}, tc.gcs}, + {"s3", &s3Client{}, tc.s3}, + } { + t.Run(backend.name, func(t *testing.T) { + u, err := NewUploader(backend.client, DefaultUploadMemory) + if err != nil { + t.Fatal(err) + } + if _, err := f.Seek(7, io.SeekStart); err != nil { + t.Fatal(err) + } + got, err := u.plan(f) + if err != nil { + t.Fatal(err) + } + if got != backend.want { + t.Errorf("plan = %+v, want %+v", got, backend.want) + } + if position, err := f.Seek(0, io.SeekCurrent); err != nil || position != 7 { + t.Errorf("planning changed file position: position=%d, err=%v", position, err) + } + }) + } + }) + } +} + +func TestUploaderSmallSparseFilesShareBudget(t *testing.T) { + previous := runtime.GOMAXPROCS(2) + defer runtime.GOMAXPROCS(previous) + files := []*os.File{ + sparseFileWith(t, 2<<30, []int64{1 << 30}), + sparseFileWith(t, 2<<30, []int64{1 << 30}), + } + synctest.Test(t, func(t *testing.T) { + gate := make(chan struct{}) + store := &gatedUploadStore{started: make(chan string, len(files)), release: gate} + u, err := NewUploader(store, 2*parallelWorkerMemory) + if err != nil { + t.Fatal(err) + } + done := make(chan error, len(files)) + for _, f := range files { + go func() { done <- u.SendFile(t.Context(), "gs://bucket/"+filepath.Base(f.Name()), f) }() + } + synctest.Wait() + if got := len(store.started); got != len(files) { + t.Errorf("started %d uploads, want both small resident sets to fit the budget", got) + } + close(gate) + for range files { + if err := <-done; err != nil { + t.Error(err) + } + } + }) +} + +type gatedUploadStore struct { + started chan string + release <-chan struct{} + err error +} + +func (*gatedUploadStore) supportsStreamingPut() {} +func (*gatedUploadStore) GetObject(context.Context, string, string) (io.ReadCloser, error) { + return nil, errors.New("unused") +} +func (s *gatedUploadStore) PutObject(ctx context.Context, _, object string, r io.Reader) error { + // Drain compression first: the reservation must remain held while the + // destination is slow even after the producer has finished. + if _, err := io.Copy(io.Discard, r); err != nil { + return err + } + s.started <- object + select { + case <-s.release: + return s.err + case <-ctx.Done(): + return ctx.Err() + } +} + +func TestUploaderWaitCancelAndRelease(t *testing.T) { + for _, failure := range []bool{false, true} { + t.Run(map[bool]string{false: "success", true: "failure"}[failure], func(t *testing.T) { + synctest.Test(t, func(t *testing.T) { + gate := make(chan struct{}) + store := &gatedUploadStore{started: make(chan string, 3), release: gate} + if failure { + store.err = errors.New("upload failed") + } + u, err := NewUploader(store, parallelWorkerMemory) + if err != nil { + t.Fatal(err) + } + send := func(ctx context.Context, name string) <-chan error { + done := make(chan error, 1) + go func() { done <- u.sendZstd(ctx, "gs://bucket/"+name, strings.NewReader("payload")) }() + return done + } + first := send(t.Context(), "first") + synctest.Wait() + if got := <-store.started; got != "first" { + t.Fatal(got) + } + ctx, cancel := context.WithCancel(t.Context()) + waiting := send(ctx, "waiting") + synctest.Wait() + select { + case got := <-store.started: + t.Fatalf("%s bypassed budget", got) + default: + } + cancel() + synctest.Wait() + if err := <-waiting; !errors.Is(err, context.Canceled) { + t.Fatalf("waiting upload: %v", err) + } + // Cancellation of a waiter must not release the first upload's reservation. + next := send(t.Context(), "next") + synctest.Wait() + select { + case got := <-store.started: + t.Fatalf("%s bypassed held budget", got) + default: + } + close(gate) + synctest.Wait() + if err := <-first; !errors.Is(err, store.err) { + t.Fatalf("first: %v", err) + } + if err := <-next; !errors.Is(err, store.err) { + t.Fatalf("next: %v", err) + } + if got := <-store.started; got != "next" { + t.Fatal(got) + } + }) + }) + } +} + +func TestUploaderCancellationReleasesActiveReservation(t *testing.T) { + synctest.Test(t, func(t *testing.T) { + gate := make(chan struct{}) + store := &gatedUploadStore{started: make(chan string, 2), release: gate} + u, err := NewUploader(store, parallelWorkerMemory) + if err != nil { + t.Fatal(err) + } + ctx, cancel := context.WithCancel(t.Context()) + done := make(chan error, 1) + go func() { done <- u.sendZstd(ctx, "gs://bucket/canceled", strings.NewReader("payload")) }() + synctest.Wait() + <-store.started + cancel() + synctest.Wait() + if err := <-done; !errors.Is(err, context.Canceled) { + t.Fatal(err) + } + close(gate) + if err := u.sendZstd(t.Context(), "gs://bucket/next", strings.NewReader("payload")); err != nil { + t.Fatal(err) + } + }) +} + +func TestUploaderReadFailureReleasesReservation(t *testing.T) { + u, err := NewUploader(newMemStore(), parallelWorkerMemory) + if err != nil { + t.Fatal(err) + } + want := errors.New("source failed") + if err := u.sendZstd(t.Context(), "gs://bucket/bad", failingUploadReader{want}); !errors.Is(err, want) { + t.Fatalf("got %v, want source error", err) + } + if err := u.sendZstd(t.Context(), "gs://bucket/next", strings.NewReader("payload")); err != nil { + t.Fatal(err) + } +} + +type failingUploadReader struct{ err error } + +func (r failingUploadReader) Read([]byte) (int, error) { return 0, r.err } + +type earlyUploadFailure struct{ err error } + +func (*earlyUploadFailure) supportsStreamingPut() {} +func (*earlyUploadFailure) GetObject(context.Context, string, string) (io.ReadCloser, error) { + return nil, errors.New("unused") +} +func (s *earlyUploadFailure) PutObject(_ context.Context, _, _ string, r io.Reader) error { + header := make([]byte, len(sparseMagic)+4) + if _, err := io.ReadFull(r, header); err != nil { + return err + } + return s.err +} + +func TestUploaderDestinationFailureStopsCompression(t *testing.T) { + path := filepath.Join(t.TempDir(), "source") + writeSparseSource(t, path, 2<<30, []region{{0, 8 << 20, 0x5a}}) + f, err := os.Open(path) + if err != nil { + t.Fatal(err) + } + defer f.Close() + want := errors.New("destination failed before payload") + u, err := NewUploader(&earlyUploadFailure{want}, parallelWorkerMemory) + if err != nil { + t.Fatal(err) + } + if err := u.SendFile(t.Context(), "gs://bucket/object", f); !errors.Is(err, want) { + t.Fatalf("got %v, want destination error", err) + } + // Compression must stop before reading the full populated extent. + at, err := f.Seek(0, io.SeekCurrent) + if err != nil { + t.Fatal(err) + } + if at >= 8<<20 { + t.Fatalf("read the entire extent after destination failure: %d", at) + } + if !u.budget.reserved.TryAcquire(u.budget.capacity) { + t.Fatal("failed upload retained its reservation") + } + u.budget.reserved.Release(u.budget.capacity) +} From a153e1012f4be2e6bb23e8a4bb8b79b7f8822c3e Mon Sep 17 00:00:00 2001 From: Chenyi Wang Date: Fri, 16 Oct 2026 00:40:01 +0000 Subject: [PATCH 21/31] fix(ateom-gvisor): isolate and evict checkpoint page cache to prevent actor cgroup OOM (#1917) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary When a gVisor (`runsc`) sandbox is paused or suspended under a tight memory limit (`ActorMemory`), writing the snapshot (`pages.img`) inside the actor's own cgroup doubles memory pressure (`guest RAM + snapshot page cache`), causing severe swap thrashing or cgroup OOM-kills. This PR decouples snapshot I/O from the actor's memory cgroup and deterministically reclaims snapshot page cache after restore: 1. **Checkpoint Cgroup Isolation**: Run `runsc checkpoint` in a dedicated sibling cgroup(-_ckpt) so snapshot page cache is not charged against the actor's memory limit during pause or suspend. 2. **Post-Restore Cache Reclamation**: After `runsc wait -restore` completes, evict snapshot page cache (guarded by `cachestat(2)` so `FADV_DONTNEED` never forces writeback). Staged files in `restore_dir` are removed only when `preserve_restore_dir` is unset (GCS `restore-state/`); for local resume (#1876) the files are kept and only their cache is evicted. ## Notes Rebased on #1876: file cleanup now keys off `preserve_restore_dir` instead of `Nlink`, since a local restore no longer has a hard-link. `DATA_ON_GOLDEN` case dropped after #2105. ## Benchmark ### 1. Memory-Constrained Local Pause/Resume (`glutton_1user_500mi_gvisor_pause`) 1 user, 500 MiB active guest RAM (64 MiB dirtied before each pause), 600 MiB actor memory cgroup limit (`ActorMemory`), 1m duration. | Metric | Baseline (`main`) | With Fix (#1917) | Improvement | | :--- | :---: | :---: | :---: | | **Completed Cycles (in 60s)** | 6 (1 actor OOM-crashed) | **23** | **3.8x throughput** | | **`PauseActor` Failure Rate** | 25.0% (2/8 failed, `runsc checkpoint` OOM) | **0.0% (0/23)** | **Eliminates checkpoint OOM** | | **`PauseActor` P50 / Avg / P95** | 3,400 ms / 3,245 ms / 5,800 ms | **620 ms / 980 ms / 2,900 ms** | **-82% P50 (-70% Avg, -50% P95)** | | **`ResumeActor` P50 / Avg / P95** | 3,000 ms / 2,957 ms / 3,000 ms | **370 ms / 404 ms / 660 ms** | **-88% P50 (8.1x faster)** | ### 2. Memory-Constrained GCS Suspend/Resume (`glutton_1user_500mi_gvisor_suspend`) 1 user, 500 MiB active guest RAM (64 MiB dirtied before each suspend), 600 MiB actor memory cgroup limit (`ActorMemory`), 1m duration. | Metric | Baseline (`main`) | With Fix (#1917) | Improvement | | :--- | :---: | :---: | :---: | | **Completed Cycles (in 60s)** | 2 (5 actors OOM-crashed) | **9 (10 suspends)** | **4.5x–5x throughput** | | **`SuspendActor` Failure Rate** | 83.3% (10/12 failed, `runsc checkpoint` OOM) | **0.0% (0/10)** | **Eliminates checkpoint OOM** | | **`SuspendActor` Latency (successful)** | 7,700 ms – 8,844 ms | **2,800 ms P50 / 3,105 ms Avg / 4,400 ms P95** | **-64% to -68%** | | **`ResumeActor` P50 / Avg / P95** | 1,800 ms / 1,715 ms / 1,815 ms ($N=2$) | **1,500 ms / 1,605 ms / 2,300 ms ($N=9$)** | **-17% P50 (-6% Avg)** | #### 3. Multi-User Local Pause/Resume (`glutton_mem_512mi_4_user_gvisor_pause`) 4 concurrent users, 512 MiB active guest RAM (64 MiB dirtied before each pause), 768 MiB actor memory cgroup limit (`ActorMemory`), 1m duration. | Metric | Baseline (`main`) | With Fix (`#1917`) | Improvement | | :--- | :---: | :---: | :---: | | **Completed Cycles (in 60s)** | `20` (2 actors OOM-crashed) | **`27`** | **+35% throughput** | | **`PauseActor` Failure Rate** | **`15.4%`** (4/26 failed, `runsc checkpoint` OOM) | **`0.0%`** (0/30) | **Eliminates checkpoint OOM** | | **`PauseActor` P50 / Avg / P95** | `3,100 ms / 3,272 ms / 8,600 ms` | **`1,000 ms / 1,008 ms / 1,400 ms`** | **-68% P50 (-69% Avg, -84% P95)** | | **`ResumeActor` P50 / Avg / P95** | `3,800 ms / 3,698 ms / 6,000 ms` ($N=20$) | **`5,000 ms / 4,365 ms / 5,600 ms`** ($N=27$) | **-7% P95** | > **Note:** `ResumeActor` P50/Avg is slightly higher because `main` OOM-crashes 2 of the 4 actors during `PauseActor`, whereas with `#1917` all 4 actors stay alive and share the worker pod's 2-vCPU limit concurrently (+35% more completed cycles). - [x] Tests pass - [ ] Appropriate changes to documentation are included in the PR --- cmd/ateworker-gvisor/ckptcgroup.go | 251 +++++++++ cmd/ateworker-gvisor/ckptcgroup_test.go | 552 +++++++++++++++++++ cmd/ateworker-gvisor/durable.go | 22 + cmd/ateworker-gvisor/durable_test.go | 33 ++ cmd/ateworker-gvisor/hosted.go | 27 +- cmd/ateworker-gvisor/main.go | 105 ++-- cmd/ateworker-gvisor/runsc.go | 28 +- cmd/ateworker-gvisor/runsc_test.go | 28 + cmd/ateworker-microvm/restore.go | 6 +- cmd/ateworker-microvm/snapshotcache.go | 5 +- internal/snapshotcache/snapshotcache.go | 53 +- internal/snapshotcache/snapshotcache_test.go | 12 +- 12 files changed, 1045 insertions(+), 77 deletions(-) create mode 100644 cmd/ateworker-gvisor/ckptcgroup.go create mode 100644 cmd/ateworker-gvisor/ckptcgroup_test.go diff --git a/cmd/ateworker-gvisor/ckptcgroup.go b/cmd/ateworker-gvisor/ckptcgroup.go new file mode 100644 index 0000000000..1a4663b43d --- /dev/null +++ b/cmd/ateworker-gvisor/ckptcgroup.go @@ -0,0 +1,251 @@ +//go:build linux + +// 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 main + +import ( + "context" + "errors" + "fmt" + "io/fs" + "log/slog" + "os" + "path/filepath" + "slices" + "strconv" + "strings" + + "github.com/agent-substrate/substrate/internal/ocispec" + "github.com/agent-substrate/substrate/internal/proto/ateworkerpb" + "github.com/agent-substrate/substrate/internal/snapshotcache" + "golang.org/x/sys/unix" +) + +// During checkpoint, moving the sentry from -_pause into a sibling +// -_ckpt cgroup (memory.max = "max", cpu.max copied from -_pause) +// prevents newly written pages.img cache from evicting clean pages or +// OOM-killing the sentry under the actor's memory.max. Removing -_ckpt +// after sandbox teardown reparents the cache to the worker scope for fast +// local Resume, and dropActorCheckpointCacheAsync unlinks non-preserved files +// in the restore dir or evicts preserved checkpoint page cache via +// POSIX_FADV_DONTNEED once background restore page loading completes. + +const ( + // checkpointCgroupSuffix uses a leading underscore like _pause so it cannot + // collide with a DNS-label application container leaf (-). + checkpointCgroupSuffix = "-_ckpt" +) + +type checkpointCgroup struct { + leaf string + dir string + pid int +} + +func checkpointCgroupDir(root, actorUID string) string { + return filepath.Join(root, actorUID+checkpointCgroupSuffix) +} + +// isolateCheckpointCache moves the actor's sentry from its pause leaf into a +// sibling -_ckpt cgroup, preserving the pause leaf's cpu.max so checkpoint +// serialization remains capped to the actor's CPU limit. +func isolateCheckpointCache(root string, actorDirs *ateworkerpb.ActorDirs, actorUID string) (*checkpointCgroup, error) { + pid, err := readSandboxPID(actorDirs) + if err != nil { + return nil, err + } + + leaf := filepath.Join(root, ocispec.GVisorCgroupLeaf(actorUID, ocispec.PauseContainer)) + if !cgroupHasPID(leaf, pid) { + return nil, fmt.Errorf("sandbox pid %d is not in cgroup %q", pid, leaf) + } + dir := checkpointCgroupDir(root, actorUID) + if err := os.Mkdir(dir, 0o755); err != nil && !errors.Is(err, fs.ErrExist) { + return nil, fmt.Errorf("while creating cgroup %q: %w", dir, err) + } + if cpuMax, err := os.ReadFile(filepath.Join(leaf, "cpu.max")); err == nil { + if err := os.WriteFile(filepath.Join(dir, "cpu.max"), cpuMax, 0o644); err != nil { + slog.Debug("Failed to copy cpu.max to checkpoint cgroup", slog.String("cgroup", dir), slog.Any("err", err)) + } + } + c := &checkpointCgroup{leaf: leaf, dir: dir, pid: pid} + if err := movePID(dir, pid); err != nil { + return nil, errors.Join(err, c.remove()) + } + return c, nil +} + +func readSandboxPID(actorDirs *ateworkerpb.ActorDirs) (int, error) { + pidFile := pidFilePath(actorDirs, ocispec.PauseContainer) + b, err := os.ReadFile(pidFile) + if err != nil { + return 0, fmt.Errorf("while reading sandbox pid file %q: %w", pidFile, err) + } + pid, err := strconv.Atoi(strings.TrimSpace(string(b))) + if err != nil || pid <= 0 { + return 0, fmt.Errorf("invalid sandbox pid %q in %q: %w", string(b), pidFile, err) + } + return pid, nil +} + +func cgroupHasPID(dir string, pid int) bool { + procs, err := os.ReadFile(filepath.Join(dir, "cgroup.procs")) + if err != nil { + return false + } + return slices.Contains(strings.Fields(string(procs)), strconv.Itoa(pid)) +} + +// movePID is a var so tests can model cgroup2 PID migration on a temp dir. +var movePID = writeCgroupPID + +func writeCgroupPID(dir string, pid int) error { + if err := os.WriteFile(filepath.Join(dir, "cgroup.procs"), []byte(strconv.Itoa(pid)), 0o644); err != nil { + return fmt.Errorf("while moving PID %d into cgroup %q: %w", pid, dir, err) + } + return nil +} + +// restore moves the sentry back into its pause leaf on checkpoint failure. It +// returns nil once the sentry has exited; any other failure leaves the sentry +// alive in the checkpoint cgroup, so the caller must terminate it. +func (c *checkpointCgroup) restore() error { + if c == nil { + return nil + } + if !cgroupHasPID(c.dir, c.pid) { + return nil + } + err := movePID(c.leaf, c.pid) + if errors.Is(err, unix.ESRCH) { + return nil + } + return err +} + +// remove deletes the empty sibling cgroup, reparenting its page cache. +func (c *checkpointCgroup) remove() error { + if c == nil { + return nil + } + if err := os.Remove(c.dir); err != nil && !errors.Is(err, fs.ErrNotExist) { + return fmt.Errorf("while removing cgroup %q: %w", c.dir, err) + } + return nil +} + +// checkpointIsolated isolates the sentry into a sibling cgroup (when the +// worker's cgroup is delegated), runs checkpoint, and tears down the workload +// before removing the sibling cgroup so its page cache reparents cleanly. If +// checkpoint fails, it moves the sentry back into its pause leaf (or terminates +// the workload if that rollback fails). +func checkpointIsolated( + ctx context.Context, + cgroupDelegated bool, + cgroupRoot, actorUID string, + actorDirs *ateworkerpb.ActorDirs, + checkpoint func(context.Context) error, + terminate func() error, +) error { + var ckptCgroup *checkpointCgroup + if cgroupDelegated { + var err error + ckptCgroup, err = isolateCheckpointCache(cgroupRoot, actorDirs, actorUID) + if err != nil { + slog.WarnContext(ctx, "Checkpointing without page cache isolation", + slog.String("actorUID", actorUID), slog.Any("err", err)) + } + } + terminateOnExit := true + defer func() { + if terminateOnExit { + _ = terminate() + } + err := ckptCgroup.remove() + if err != nil { + // If terminate left the sentry alive, move it back to its pause leaf. + if rerr := ckptCgroup.restore(); rerr != nil { + slog.WarnContext(ctx, "Failed to move the sentry back into its pause leaf", + slog.String("actorUID", actorUID), slog.Any("err", rerr)) + } else { + err = ckptCgroup.remove() + } + } + if err != nil { + slog.WarnContext(ctx, "Failed to remove the checkpoint cgroup", + slog.String("actorUID", actorUID), slog.Any("err", err)) + } + }() + + if err := checkpoint(ctx); err != nil { + if rerr := ckptCgroup.restore(); rerr != nil { + slog.WarnContext(ctx, "Failed to move the sentry back into its pause leaf; terminating workload", + slog.String("actorUID", actorUID), slog.Any("err", rerr)) + } else { + terminateOnExit = false + } + return err + } + return nil +} + +// removeStaleCheckpointCgroups removes empty *-_ckpt cgroups left behind on restart. +func removeStaleCheckpointCgroups(ctx context.Context, root string) { + dirs, err := filepath.Glob(filepath.Join(root, "*"+checkpointCgroupSuffix)) + if err != nil { + return + } + for _, dir := range dirs { + if err := os.Remove(dir); err != nil && !errors.Is(err, fs.ErrNotExist) { + slog.WarnContext(ctx, "Failed to remove a stale checkpoint cgroup", slog.String("cgroup", dir), slog.Any("err", err)) + } + } +} + +func isCheckpointDataFile(name string) bool { + ext := filepath.Ext(name) + return ext == ".img" || ext == ".tar" +} + +// evictCheckpointCache is a var so tests can observe eviction. +var evictCheckpointCache = snapshotcache.EvictWithRetries + +// dropActorCheckpointCacheAsync pins the snapshot data files in restoreDir +// while the caller holds the actor lock. When the restore dir is not preserved, +// it unlinks them immediately (the kernel frees pages and blocks once the +// sentry closes its open FDs). When the restore dir is preserved, it waits in +// the background (bounded by actorCtx) for async restore page loading to finish +// and evicts clean pages from the host page cache. +func dropActorCheckpointCacheAsync(actorCtx context.Context, actorUID, restoreDir string, preserveRestoreDir bool, waitRestore func(context.Context) error) { + targets := snapshotcache.FindFiles(restoreDir, isCheckpointDataFile) + if len(targets) == 0 { + return + } + if !preserveRestoreDir { + snapshotcache.DropFiles(targets) + return + } + go func() { + if waitRestore != nil { + if err := waitRestore(actorCtx); err != nil { + slog.Warn("Skipping checkpoint page cache eviction after restore wait error", + slog.String("actorUID", actorUID), slog.Any("err", err)) + return + } + } + evictCheckpointCache(targets) + }() +} diff --git a/cmd/ateworker-gvisor/ckptcgroup_test.go b/cmd/ateworker-gvisor/ckptcgroup_test.go new file mode 100644 index 0000000000..970538fceb --- /dev/null +++ b/cmd/ateworker-gvisor/ckptcgroup_test.go @@ -0,0 +1,552 @@ +//go:build linux + +// 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 main + +import ( + "context" + "errors" + "os" + "path/filepath" + "slices" + "strconv" + "strings" + "testing" + "time" + + "github.com/agent-substrate/substrate/internal/ocispec" + "github.com/agent-substrate/substrate/internal/proto/ateworkerpb" + "github.com/agent-substrate/substrate/internal/snapshotcache" + "golang.org/x/sys/unix" +) + +const testActorUID = "0b5f3c1e-7a2d-4e8f-9c61-2d4a8b7e5f10" + +type ckptFixture struct { + root, leaf string + actorDirs *ateworkerpb.ActorDirs +} + +func newCkptFixture(t *testing.T, sandboxPID string) ckptFixture { + t.Helper() + actorRoot := t.TempDir() + f := ckptFixture{ + root: t.TempDir(), + actorDirs: &ateworkerpb.ActorDirs{ + RootDir: actorRoot, + CheckpointDir: t.TempDir(), + }, + } + f.leaf = filepath.Join(f.root, ocispec.GVisorCgroupLeaf(testActorUID, ocispec.PauseContainer)) + mustMkdir(t, f.leaf) + mustWrite(t, filepath.Join(f.leaf, "cgroup.procs"), "100\n"+sandboxPID+"\n") + mustWrite(t, filepath.Join(f.leaf, "memory.max"), "805306368\n") + mustWrite(t, filepath.Join(f.leaf, "cpu.max"), "200000 100000\n") + + if sandboxPID != "" { + pidFile := pidFilePath(f.actorDirs, ocispec.PauseContainer) + mustMkdir(t, filepath.Dir(pidFile)) + mustWrite(t, pidFile, sandboxPID+"\n") + } + + origMovePID := movePID + t.Cleanup(func() { movePID = origMovePID }) + movePID = func(dir string, pid int) error { + if err := writeCgroupPID(dir, pid); err != nil { + return err + } + // Emulate cgroup2 migration out of the sibling checkpoint cgroup so rmdir + // succeeds once the sentry moves back into its pause leaf. + ckptDir := checkpointCgroupDir(f.root, testActorUID) + if dir != ckptDir { + _ = os.Remove(filepath.Join(ckptDir, "cgroup.procs")) + _ = os.Remove(filepath.Join(ckptDir, "cpu.max")) + } + return nil + } + return f +} + +func mustMkdir(t *testing.T, dir string) { + t.Helper() + if err := os.MkdirAll(dir, 0o755); err != nil { + t.Fatal(err) + } +} + +func mustWrite(t *testing.T, path, data string) { + t.Helper() + if err := os.WriteFile(path, []byte(data), 0o644); err != nil { + t.Fatal(err) + } +} + +func mustRead(t *testing.T, path string) string { + t.Helper() + b, err := os.ReadFile(path) + if err != nil { + t.Fatal(err) + } + return string(b) +} + +func TestIsolateCheckpointCacheMovesOnlyTheSentry(t *testing.T) { + f := newCkptFixture(t, "101") + + c, err := isolateCheckpointCache(f.root, f.actorDirs, testActorUID) + if err != nil { + t.Fatalf("isolateCheckpointCache: %v", err) + } + dir := checkpointCgroupDir(f.root, testActorUID) + if got := mustRead(t, filepath.Join(dir, "cgroup.procs")); got != "101" { + t.Errorf("checkpoint cgroup.procs = %q, want the sentry's PID 101", got) + } + if got := mustRead(t, filepath.Join(dir, "cpu.max")); got != "200000 100000\n" { + t.Errorf("checkpoint cpu.max = %q, want \"200000 100000\\n\"", got) + } + if _, err := os.Stat(filepath.Join(dir, "memory.max")); !os.IsNotExist(err) { + t.Errorf("checkpoint memory.max should not be set, stat err = %v", err) + } + + if err := c.restore(); err != nil { + t.Fatalf("restore: %v", err) + } + if got := mustRead(t, filepath.Join(f.leaf, "cgroup.procs")); got != "101" { + t.Errorf("pause leaf cgroup.procs after restore = %q, want 101", got) + } + if err := c.remove(); err != nil { + t.Fatalf("remove: %v", err) + } +} + +func TestIsolateCheckpointCacheWithoutPIDFile(t *testing.T) { + f := newCkptFixture(t, "") + + if _, err := isolateCheckpointCache(f.root, f.actorDirs, testActorUID); err == nil { + t.Fatal("isolateCheckpointCache succeeded with no pause pid file") + } + if _, err := os.Stat(checkpointCgroupDir(f.root, testActorUID)); !os.IsNotExist(err) { + t.Errorf("checkpoint cgroup left behind: stat err = %v", err) + } +} + +func TestIsolateCheckpointCacheRejectsPIDOutsidePauseLeaf(t *testing.T) { + f := newCkptFixture(t, "101") + mustWrite(t, filepath.Join(f.leaf, "cgroup.procs"), "100\n") + + if _, err := isolateCheckpointCache(f.root, f.actorDirs, testActorUID); err == nil { + t.Fatal("isolateCheckpointCache succeeded with the pid outside the pause leaf") + } + if _, err := os.Stat(checkpointCgroupDir(f.root, testActorUID)); !os.IsNotExist(err) { + t.Errorf("checkpoint cgroup created for a reused pid: stat err = %v", err) + } + if got := mustRead(t, filepath.Join(f.leaf, "cgroup.procs")); got != "100\n" { + t.Errorf("pause leaf cgroup.procs = %q, want unchanged \"100\\n\"", got) + } +} + +func TestCheckpointCgroupRestoreAfterLeafGone(t *testing.T) { + f := newCkptFixture(t, "101") + c, err := isolateCheckpointCache(f.root, f.actorDirs, testActorUID) + if err != nil { + t.Fatalf("isolateCheckpointCache: %v", err) + } + if err := os.RemoveAll(f.leaf); err != nil { + t.Fatal(err) + } + // The sentry is still alive in the checkpoint cgroup with nowhere to go + // back to; the caller must terminate it rather than leave it unlimited. + if err := c.restore(); !errors.Is(err, os.ErrNotExist) { + t.Errorf("restore with the leaf gone = %v, want ErrNotExist", err) + } + if got := mustRead(t, filepath.Join(c.dir, "cgroup.procs")); got != "101" { + t.Errorf("checkpoint cgroup.procs = %q, want the live sentry 101", got) + } +} + +func TestCheckpointCgroupRestoreSkipsExitedSentry(t *testing.T) { + f := newCkptFixture(t, "101") + c, err := isolateCheckpointCache(f.root, f.actorDirs, testActorUID) + if err != nil { + t.Fatalf("isolateCheckpointCache: %v", err) + } + + // If the sentry exited (no longer in ckpt cgroup.procs) and its PID was + // recycled elsewhere, restore must not move it into the pause leaf. + mustWrite(t, filepath.Join(c.dir, "cgroup.procs"), "") + mustWrite(t, filepath.Join(f.leaf, "cgroup.procs"), "100\n") + if err := c.restore(); err != nil { + t.Fatalf("restore with exited sentry: %v", err) + } + if got := mustRead(t, filepath.Join(f.leaf, "cgroup.procs")); got != "100\n" { + t.Errorf("pause leaf cgroup.procs = %q, want unchanged \"100\\n\"", got) + } + + // If the sentry exits between cgroupHasPID and movePID (ESRCH), restore returns nil. + mustWrite(t, filepath.Join(c.dir, "cgroup.procs"), "101\n") + movePID = func(string, int) error { return unix.ESRCH } + if err := c.restore(); err != nil { + t.Errorf("restore on ESRCH = %v, want nil", err) + } +} + +func TestCheckpointCgroupNilIsNoop(t *testing.T) { + var c *checkpointCgroup + if err := c.restore(); err != nil { + t.Errorf("nil restore = %v", err) + } + if err := c.remove(); err != nil { + t.Errorf("nil remove = %v", err) + } +} + +func TestCheckpointIsolated(t *testing.T) { + t.Run("terminates before removing checkpoint cgroup on success", func(t *testing.T) { + f := newCkptFixture(t, "101") + ckptDir := checkpointCgroupDir(f.root, testActorUID) + var terminated bool + + err := checkpointIsolated( + context.Background(), + true, f.root, testActorUID, f.actorDirs, + func(context.Context) error { + if got := mustRead(t, filepath.Join(ckptDir, "cgroup.procs")); got != "101" { + t.Errorf("checkpoint cgroup.procs during checkpoint = %q, want 101", got) + } + return nil + }, + func() error { + terminated = true + // Emulate container teardown emptying the checkpoint cgroup so rmdir succeeds. + _ = os.Remove(filepath.Join(ckptDir, "cpu.max")) + return os.Remove(filepath.Join(ckptDir, "cgroup.procs")) + }, + ) + if err != nil { + t.Fatalf("checkpointIsolated: %v", err) + } + if !terminated { + t.Error("terminate was not called") + } + if _, err := os.Stat(ckptDir); !os.IsNotExist(err) { + t.Errorf("checkpoint cgroup still exists after checkpointIsolated: stat err = %v", err) + } + }) + + t.Run("skips cgroup isolation when cgroup is not delegated", func(t *testing.T) { + f := newCkptFixture(t, "") + ckptDir := checkpointCgroupDir(f.root, testActorUID) + var terminated bool + + err := checkpointIsolated( + context.Background(), + false, f.root, testActorUID, f.actorDirs, + func(context.Context) error { + if _, err := os.Stat(ckptDir); !os.IsNotExist(err) { + t.Errorf("checkpoint cgroup should not be created when !cgroupDelegated: %v", err) + } + return nil + }, + func() error { + terminated = true + return nil + }, + ) + if err != nil { + t.Fatalf("checkpointIsolated: %v", err) + } + if !terminated { + t.Error("terminate was not called") + } + }) + + t.Run("restores sentry and keeps workload alive when checkpoint fails", func(t *testing.T) { + f := newCkptFixture(t, "101") + ckptDir := checkpointCgroupDir(f.root, testActorUID) + var terminated bool + + err := checkpointIsolated( + context.Background(), + true, f.root, testActorUID, f.actorDirs, + func(context.Context) error { + return errors.New("checkpoint boom") + }, + func() error { + terminated = true + return nil + }, + ) + if err == nil || !strings.Contains(err.Error(), "checkpoint boom") { + t.Fatalf("checkpointIsolated err = %v, want checkpoint boom", err) + } + if terminated { + t.Error("terminate called after successful cgroup rollback") + } + if got := mustRead(t, filepath.Join(f.leaf, "cgroup.procs")); got != "101" { + t.Errorf("pause leaf cgroup.procs = %q, want 101", got) + } + if _, err := os.Stat(ckptDir); !os.IsNotExist(err) { + t.Errorf("checkpoint cgroup left behind: stat err = %v", err) + } + }) + + t.Run("terminates workload if both checkpoint and cgroup restore fail", func(t *testing.T) { + f := newCkptFixture(t, "101") + ckptDir := checkpointCgroupDir(f.root, testActorUID) + var terminated bool + + err := checkpointIsolated( + context.Background(), + true, f.root, testActorUID, f.actorDirs, + func(context.Context) error { + movePID = func(string, int) error { return unix.EACCES } + return errors.New("checkpoint boom") + }, + func() error { + terminated = true + _ = os.Remove(filepath.Join(ckptDir, "cpu.max")) + return os.Remove(filepath.Join(ckptDir, "cgroup.procs")) + }, + ) + if err == nil { + t.Fatal("checkpointIsolated succeeded on checkpoint error") + } + if !terminated { + t.Error("terminate was not called when restore failed; sentry would stay in unlimited cgroup") + } + if _, err := os.Stat(ckptDir); !os.IsNotExist(err) { + t.Errorf("checkpoint cgroup left behind: stat err = %v", err) + } + }) + + t.Run("terminates workload if checkpoint fails and the pause leaf is gone", func(t *testing.T) { + f := newCkptFixture(t, "101") + ckptDir := checkpointCgroupDir(f.root, testActorUID) + var terminated bool + + err := checkpointIsolated( + context.Background(), + true, f.root, testActorUID, f.actorDirs, + func(context.Context) error { + if err := os.RemoveAll(f.leaf); err != nil { + t.Fatal(err) + } + return errors.New("checkpoint boom") + }, + func() error { + terminated = true + _ = os.Remove(filepath.Join(ckptDir, "cpu.max")) + return os.Remove(filepath.Join(ckptDir, "cgroup.procs")) + }, + ) + if err == nil { + t.Fatal("checkpointIsolated succeeded on checkpoint error") + } + if !terminated { + t.Error("terminate was not called with the pause leaf gone; sentry would stay in unlimited cgroup") + } + if _, err := os.Stat(ckptDir); !os.IsNotExist(err) { + t.Errorf("checkpoint cgroup left behind: stat err = %v", err) + } + }) + + t.Run("moves the sentry back into its pause leaf when terminate leaves it running", func(t *testing.T) { + f := newCkptFixture(t, "101") + ckptDir := checkpointCgroupDir(f.root, testActorUID) + + err := checkpointIsolated( + context.Background(), + true, f.root, testActorUID, f.actorDirs, + func(context.Context) error { return nil }, + func() error { + // Non-empty ckptDir emulates a sentry still alive in the cgroup. + return errors.New("terminate boom") + }, + ) + if err != nil { + t.Fatalf("checkpointIsolated: %v", err) + } + if got := mustRead(t, filepath.Join(f.leaf, "cgroup.procs")); got != "101" { + t.Errorf("pause leaf cgroup.procs = %q, want the live sentry 101 back under its memory.max", got) + } + if _, err := os.Stat(ckptDir); !os.IsNotExist(err) { + t.Errorf("checkpoint cgroup left behind: stat err = %v", err) + } + }) +} + +func TestRemoveStaleCheckpointCgroups(t *testing.T) { + root := t.TempDir() + stale := checkpointCgroupDir(root, testActorUID) + mustMkdir(t, stale) + busy := checkpointCgroupDir(root, "busy") + mustMkdir(t, busy) + // A non-empty directory stands in for a cgroup that still has processes: + // rmdir fails on both. + mustWrite(t, filepath.Join(busy, "cgroup.procs"), strconv.Itoa(42)) + leaf := filepath.Join(root, ocispec.GVisorCgroupLeaf(testActorUID, ocispec.PauseContainer)) + mustMkdir(t, leaf) + + removeStaleCheckpointCgroups(context.Background(), root) + + if _, err := os.Stat(stale); !os.IsNotExist(err) { + t.Errorf("stale checkpoint cgroup not removed: stat err = %v", err) + } + if _, err := os.Stat(busy); err != nil { + t.Errorf("busy checkpoint cgroup removed: %v", err) + } + if _, err := os.Stat(leaf); err != nil { + t.Errorf("pause leaf removed: %v", err) + } +} + +func newRestoreDir(t *testing.T) string { + t.Helper() + restoreDir := filepath.Join(t.TempDir(), "restore-dir") + mustMkdir(t, filepath.Join(restoreDir, "fs")) + for _, name := range []string{"checkpoint.img", "pages.img", "fs/pages.img", "fs/fscheckpoint.pb", "manifest.json"} { + mustWrite(t, filepath.Join(restoreDir, name), "staged data") + } + return restoreDir +} + +func captureEviction(t *testing.T) <-chan []snapshotcache.File { + t.Helper() + evicted := make(chan []snapshotcache.File, 1) + orig := evictCheckpointCache + t.Cleanup(func() { evictCheckpointCache = orig }) + evictCheckpointCache = func(targets []snapshotcache.File) { evicted <- targets } + return evicted +} + +func TestDropStagedRestoreFiles(t *testing.T) { + restoreDir := newRestoreDir(t) + evicted := captureEviction(t) + + waitCalled := false + dropActorCheckpointCacheAsync(context.Background(), testActorUID, restoreDir, false, func(context.Context) error { + waitCalled = true + return errors.New("waitRestore should not be called when !preserveRestoreDir") + }) + if waitCalled { + t.Fatal("waitRestore was called when !preserveRestoreDir") + } + + for _, name := range []string{"checkpoint.img", "pages.img", "fs/pages.img"} { + if _, err := os.Stat(filepath.Join(restoreDir, name)); !os.IsNotExist(err) { + t.Fatalf("staged %s still exists after dropActorCheckpointCacheAsync: err=%v", name, err) + } + } + for _, name := range []string{"fs/fscheckpoint.pb", "manifest.json"} { + if _, err := os.Stat(filepath.Join(restoreDir, name)); err != nil { + t.Fatalf("%s should be preserved: %v", name, err) + } + } + select { + case <-evicted: + t.Fatal("dropped files were also evicted") + default: + } +} + +func TestEvictPreservedRestoreDir(t *testing.T) { + t.Run("evicts once the restore wait completes", func(t *testing.T) { + restoreDir := newRestoreDir(t) + evicted := captureEviction(t) + restoreDone := make(chan struct{}) + + dropActorCheckpointCacheAsync(context.Background(), testActorUID, restoreDir, true, func(ctx context.Context) error { + select { + case <-restoreDone: + return nil + case <-ctx.Done(): + return ctx.Err() + } + }) + + select { + case <-evicted: + t.Fatal("evicted before the restore wait completed") + case <-time.After(50 * time.Millisecond): + } + close(restoreDone) + select { + case targets := <-evicted: + var names []string + for _, f := range targets { + rel, _ := filepath.Rel(restoreDir, f.Path) + names = append(names, rel) + } + slices.Sort(names) + if want := []string{"checkpoint.img", "fs/pages.img", "pages.img"}; !slices.Equal(names, want) { + t.Errorf("evicted %v, want %v", names, want) + } + case <-time.After(5 * time.Second): + t.Fatal("eviction did not run after the restore wait completed") + } + for _, name := range []string{"checkpoint.img", "pages.img", "fs/pages.img"} { + if _, err := os.Stat(filepath.Join(restoreDir, name)); err != nil { + t.Errorf("preserved %s was dropped: %v", name, err) + } + } + }) + + t.Run("skips eviction when the restore wait fails", func(t *testing.T) { + restoreDir := newRestoreDir(t) + evicted := captureEviction(t) + waited := make(chan struct{}) + + dropActorCheckpointCacheAsync(context.Background(), testActorUID, restoreDir, true, func(context.Context) error { + defer close(waited) + return errors.New("runsc wait boom") + }) + + <-waited + select { + case <-evicted: + t.Fatal("evicted after the restore wait failed") + case <-time.After(50 * time.Millisecond): + } + }) + + t.Run("waits no longer than the actor is hosted", func(t *testing.T) { + restoreDir := newRestoreDir(t) + evicted := captureEviction(t) + actorCtx, unhost := context.WithCancel(context.Background()) + waited := make(chan error, 1) + + dropActorCheckpointCacheAsync(actorCtx, testActorUID, restoreDir, true, func(ctx context.Context) error { + <-ctx.Done() + waited <- ctx.Err() + return ctx.Err() + }) + + unhost() + select { + case err := <-waited: + if !errors.Is(err, context.Canceled) { + t.Errorf("restore wait ended with %v, want context.Canceled", err) + } + case <-time.After(5 * time.Second): + t.Fatal("restore wait did not end when the actor was unhosted") + } + select { + case <-evicted: + t.Fatal("evicted after the actor was unhosted") + case <-time.After(50 * time.Millisecond): + } + }) +} diff --git a/cmd/ateworker-gvisor/durable.go b/cmd/ateworker-gvisor/durable.go index 9ae6ce0338..dfe6051ef6 100644 --- a/cmd/ateworker-gvisor/durable.go +++ b/cmd/ateworker-gvisor/durable.go @@ -70,3 +70,25 @@ func fsRestoreArgs(checkpointDir string) ([]string, error) { } return []string{"--fs-restore-image-path", fsDir}, nil } + +// fsRestoreContainers returns the container names that own the master mount of +// at least one durable-dir volume (the first container to mount each volume), +// in order. Only those containers register an entry in runsc's fsRestore +// waitMap for `runsc wait -fsrestore `. +func fsRestoreContainers(containers []*ateworkerpb.Container) []string { + seen := make(map[string]bool) + var out []string + for _, c := range containers { + ownsMount := false + for _, m := range c.GetDurableDirVolumeMounts() { + if !seen[m.GetVolumeName()] { + seen[m.GetVolumeName()] = true + ownsMount = true + } + } + if ownsMount { + out = append(out, c.GetName()) + } + } + return out +} diff --git a/cmd/ateworker-gvisor/durable_test.go b/cmd/ateworker-gvisor/durable_test.go index 1806bc7245..fc5303c15f 100644 --- a/cmd/ateworker-gvisor/durable_test.go +++ b/cmd/ateworker-gvisor/durable_test.go @@ -148,3 +148,36 @@ func TestFSRestoreArgs(t *testing.T) { } }) } + +func TestFSRestoreContainers(t *testing.T) { + containers := []*ateworkerpb.Container{ + { + Name: "init", + }, + { + Name: "app", + DurableDirVolumeMounts: []*ateworkerpb.DurableDirVolumeMount{ + {VolumeName: "data", MountPath: "/var/data"}, + }, + }, + { + Name: "sidecar-shared-only", + DurableDirVolumeMounts: []*ateworkerpb.DurableDirVolumeMount{ + {VolumeName: "data", MountPath: "/mnt/data"}, + }, + }, + { + Name: "sidecar-with-own-vol", + DurableDirVolumeMounts: []*ateworkerpb.DurableDirVolumeMount{ + {VolumeName: "data", MountPath: "/mnt/data"}, + {VolumeName: "logs", MountPath: "/var/log"}, + }, + }, + } + + got := fsRestoreContainers(containers) + want := []string{"app", "sidecar-with-own-vol"} + if !slices.Equal(got, want) { + t.Errorf("fsRestoreContainers() = %v, want %v", got, want) + } +} diff --git a/cmd/ateworker-gvisor/hosted.go b/cmd/ateworker-gvisor/hosted.go index 6785ab02b8..b219f498cd 100644 --- a/cmd/ateworker-gvisor/hosted.go +++ b/cmd/ateworker-gvisor/hosted.go @@ -45,6 +45,9 @@ type hostedActor struct { usage *workerstats.Activation // deadReported keeps the usage sweep to one warning per activation. deadReported atomic.Bool + // ctx is canceled when the actor is replaced or unhosted. + ctx context.Context + cancel context.CancelFunc } // admitActor reserves capacity before network setup. An actor that is already @@ -55,12 +58,14 @@ func (s *AteWorkerService) admitActor(attribution resources.ActorAttribution) (* var stale *actornet.SandboxSession if old, ok := s.actors[attribution.UID]; ok { stale = old.network + old.cancel() } else if len(s.actors)+s.draining >= s.maxActors { return nil, nil, apierror.ResourceExhausted("worker is full: %d actors", s.maxActors) } // The sandbox cgroup is new with every activation, so its counters start // at zero. - hosted := &hostedActor{attribution: attribution, usage: workerstats.NewActivation(time.Now(), false)} + ctx, cancel := context.WithCancel(context.Background()) + hosted := &hostedActor{attribution: attribution, usage: workerstats.NewActivation(time.Now(), false), ctx: ctx, cancel: cancel} s.actors[attribution.UID] = hosted return hosted, stale, nil } @@ -76,11 +81,16 @@ func (s *AteWorkerService) hostActor(ctx context.Context, attribution resources. if err != nil { return nil, err } + // evict drops the reservation when network setup fails. + evict := func() { + hosted.cancel() + s.actorsMu.Lock() + delete(s.actors, uid) + s.actorsMu.Unlock() + } if stale != nil { if err := stale.Close(ctx); err != nil { - s.actorsMu.Lock() - delete(s.actors, uid) - s.actorsMu.Unlock() + evict() return nil, fmt.Errorf("while clearing stale state for actor %s: %w", uid, err) } } @@ -92,9 +102,7 @@ func (s *AteWorkerService) hostActor(ctx context.Context, attribution resources. DNSPolicy: s.tunnel.ActorEgressPolicy(attribution), }, s.tunnel.Egress, s.tunnel.DNSRelay) if err != nil { - s.actorsMu.Lock() - delete(s.actors, uid) - s.actorsMu.Unlock() + evict() return nil, fmt.Errorf("while setting up the sandbox network: %w", err) } @@ -102,9 +110,7 @@ func (s *AteWorkerService) hostActor(ctx context.Context, attribution resources. resolvConf, err := actorResolvConf(actorDirs) if err != nil { _ = session.Close(ctx) - s.actorsMu.Lock() - delete(s.actors, uid) - s.actorsMu.Unlock() + evict() return nil, err } @@ -128,6 +134,7 @@ func (s *AteWorkerService) unhostActor(ctx context.Context, actorUID string) err if !ok { return nil } + hosted.cancel() defer func() { s.actorsMu.Lock() s.draining-- diff --git a/cmd/ateworker-gvisor/main.go b/cmd/ateworker-gvisor/main.go index be216acb61..f0cc20cd47 100644 --- a/cmd/ateworker-gvisor/main.go +++ b/cmd/ateworker-gvisor/main.go @@ -202,9 +202,13 @@ func do(ctx context.Context) error { // Prepare the pod cgroup so runsc can create per-actor-container leaves under // it with real accounting. - if _, err := cgroup.Delegate(ctx); err != nil { + cgroupDelegated, err := cgroup.Delegate(ctx) + if err != nil { return fmt.Errorf("while setting up cgroup delegation: %w", err) } + if cgroupDelegated { + removeStaleCheckpointCgroups(ctx, defaultCgroupRoot) + } // Clean up any old socket. sockPath := nodepath.WorkerSocketPath(*podUID) @@ -233,6 +237,7 @@ func do(ctx context.Context) error { return err } ateworkerService := NewService(tunnel, actorLogger, *maxActors, flags) + ateworkerService.cgroupDelegated = cgroupDelegated // The controller sets both from the downward API. pool := workerstats.Pool{Namespace: os.Getenv("POD_NAMESPACE"), Name: os.Getenv("WORKER_POOL_NAME")} if pool.Namespace == "" || pool.Name == "" { @@ -340,6 +345,10 @@ type AteWorkerService struct { // than a constant so tests can point GetWorkloadStats at a fixture tree. cgroupRoot string + // cgroupDelegated is set when the worker's cgroup is delegated (false on + // privileged workers that inherit the host cgroup namespace). + cgroupDelegated bool + // readSandboxCgroup overrides cgroupstats.Read when set. Only tests set it: // it is the seam that lets them interleave a lifecycle transition with the // stats handlers' lock-free read, the way containerStatsReader does for the @@ -781,52 +790,69 @@ func (s *AteWorkerService) CheckpointWorkload(ctx context.Context, req *ateworke containers := req.GetSpec().GetContainers() durablePaths := durableFSCheckpointPaths(containers) + + // Cleanup the containers after checkpointing. This also unhosts the actor, + // before the snapshot listing below can fail. + // This is best-effort cleanup for actor containers that may have been left behind after checkpointing. + terminate := func() error { + cleanupCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), 30*time.Second) + defer cancel() + err := s.terminateWorkload(cleanupCtx, attribution.Ref, attribution.UID, req.GetRunscPath(), req.GetActorDirs(), containers) + if err != nil { + slog.WarnContext(cleanupCtx, "failed to terminate workload after checkpoint", + slog.String("actor", attribution.Ref.String()), + slog.String("actorUID", attribution.UID), + slog.Any("err", err)) + } + timing.teardown = lap(&tLast) + s.recordFinalIfEnded(cleanupCtx, hosted) + return err + } + + var checkpointFn func(context.Context) error switch fidelity { case ateworkerpb.SnapshotFidelity_SNAPSHOT_FIDELITY_VOLUMES: if len(durablePaths) == 0 { return nil, fmt.Errorf("no durable-dir volumes found for VOLUMES snapshot") } - err := rcmd.cmdFsCheckpoint(ctx, ocispec.PauseContainer, checkpointPath, durablePaths) - timing.durableDir = lap(&tLast) - if err != nil { - return nil, fmt.Errorf("while checkpointing durable-dir volumes: %w", err) + checkpointFn = func(ctx context.Context) error { + err := rcmd.cmdFsCheckpoint(ctx, ocispec.PauseContainer, checkpointPath, durablePaths) + timing.durableDir = lap(&tLast) + if err != nil { + return fmt.Errorf("while checkpointing durable-dir volumes: %w", err) + } + return nil } case ateworkerpb.SnapshotFidelity_SNAPSHOT_FIDELITY_MEMORY: // Checkpoint pause container (root of the sandbox), splitting durable-dir // volumes into /fs when present. - sandbox, sbErr := openSandboxProcess(req.GetActorDirs()) - if sbErr != nil { - slog.WarnContext(ctx, "Cannot watch the sandbox exit after checkpoint", slog.Any("err", sbErr)) - } else { - defer sandbox.Close() - } - err := rcmd.cmdCheckpoint(ctx, ocispec.PauseContainer, checkpointPath, durablePaths) - if err != nil { - timing.checkpoint = lap(&tLast) - return nil, fmt.Errorf("while checkpointing pause: %w", err) - } - // the checkpoint stops the sandbox, runsc delete fails until it is reaped - if sandbox != nil { - if err := sandbox.waitGone(sandboxExitTimeout); err != nil { - slog.WarnContext(ctx, "Sandbox did not go away after checkpoint", slog.Any("err", err)) + checkpointFn = func(ctx context.Context) error { + sandbox, sbErr := openSandboxProcess(req.GetActorDirs()) + if sbErr != nil { + slog.WarnContext(ctx, "Cannot watch the sandbox exit after checkpoint", slog.Any("err", sbErr)) + } else { + defer sandbox.Close() } + err := rcmd.cmdCheckpoint(ctx, ocispec.PauseContainer, checkpointPath, durablePaths) + if err != nil { + timing.checkpoint = lap(&tLast) + return fmt.Errorf("while checkpointing pause: %w", err) + } + // the checkpoint stops the sandbox, runsc delete fails until it is reaped + if sandbox != nil { + if err := sandbox.waitGone(sandboxExitTimeout); err != nil { + slog.WarnContext(ctx, "Sandbox did not go away after checkpoint", slog.Any("err", err)) + } + } + timing.checkpoint = lap(&tLast) + return nil } - timing.checkpoint = lap(&tLast) default: return nil, fmt.Errorf("unsupported snapshot fidelity: %v", fidelity) } - - // Cleanup the containers after checkpointing. This also unhosts the actor, - // before the snapshot listing below can fail. - // This is best-effort cleanup for actor containers that may have been left behind after checkpointing. - if err := s.terminateWorkload(ctx, attribution.Ref, attribution.UID, req.GetRunscPath(), req.GetActorDirs(), req.GetSpec().GetContainers()); err != nil { - slog.WarnContext(ctx, "failed to terminate workload after checkpoint", - slog.String("actor", attribution.Ref.String()), - slog.String("actorUID", attribution.UID), - slog.Any("err", err)) + if err := checkpointIsolated(ctx, s.cgroupDelegated, s.cgroupRoot, req.GetActorUid(), req.GetActorDirs(), checkpointFn, terminate); err != nil { + return nil, err } - timing.teardown = lap(&tLast) - s.recordFinalIfEnded(ctx, hosted) // Report exactly the files runsc wrote so atelet ships precisely this set // (checkpoint.img plus any pages images and fs/ durable-dir files), rather @@ -1119,6 +1145,21 @@ func (s *AteWorkerService) RestoreWorkload(ctx context.Context, req *ateworkerpb if err != nil { return nil, err } + dropActorCheckpointCacheAsync(hosted.ctx, req.GetActorUid(), checkpointDir, req.GetPreserveRestoreDir(), func(waitCtx context.Context) error { + if fidelity == ateworkerpb.SnapshotFidelity_SNAPSHOT_FIDELITY_MEMORY { + if err := rcmd.cmdWaitRestore(waitCtx, ocispec.PauseContainer); err != nil { + return err + } + } + if len(restoreArgs) > 0 { + for _, ctr := range fsRestoreContainers(containers) { + if err := rcmd.cmdWaitFSRestore(waitCtx, ctr); err != nil { + return err + } + } + } + return nil + }) s.actorLogger.EmitLifecycleLog(ctx, "Actor restored", attribution) s.setSession(req.GetActorUid(), &workloadSession{rcmd: rcmd, containers: containerNames(containers)}) diff --git a/cmd/ateworker-gvisor/runsc.go b/cmd/ateworker-gvisor/runsc.go index 6b5da7101e..9157e65d20 100644 --- a/cmd/ateworker-gvisor/runsc.go +++ b/cmd/ateworker-gvisor/runsc.go @@ -296,12 +296,10 @@ func (r *runsc) cmdKill(ctx context.Context, containerName, signal string) error return nil } -// cmdWait blocks until the given container's process exits. Used during -// graceful shutdown to confirm the actor has stopped before ateworker exits. -func (r *runsc) cmdWait(ctx context.Context, containerName string) error { - slog.InfoContext(ctx, "About to run runsc wait", slog.String("container", containerName)) - - cmd := exec.CommandContext(ctx, r.path, r.buildArgs("wait", containerName)...) +// runWait runs `runsc wait [extraArgs...] ` and blocks until the +// waited-on condition completes. +func (r *runsc) runWait(ctx context.Context, containerName string, extraArgs ...string) error { + cmd := exec.CommandContext(ctx, r.path, r.buildArgs("wait", containerName, extraArgs...)...) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr if err := cmd.Run(); err != nil { @@ -309,3 +307,21 @@ func (r *runsc) cmdWait(ctx context.Context, containerName string) error { } return nil } + +// cmdWait blocks until the given container's process exits. Used during +// graceful shutdown to confirm the actor has stopped before ateworker exits. +func (r *runsc) cmdWait(ctx context.Context, containerName string) error { + slog.InfoContext(ctx, "About to run runsc wait", slog.String("container", containerName)) + return r.runWait(ctx, containerName) +} + +// cmdWaitRestore blocks until background page loading from `runsc restore -background` completes. +func (r *runsc) cmdWaitRestore(ctx context.Context, containerName string) error { + return r.runWait(ctx, containerName, "-restore") +} + +// cmdWaitFSRestore blocks until background filesystem page loading from +// `--fs-restore-image-path` completes for the given container. +func (r *runsc) cmdWaitFSRestore(ctx context.Context, containerName string) error { + return r.runWait(ctx, containerName, "-fsrestore") +} diff --git a/cmd/ateworker-gvisor/runsc_test.go b/cmd/ateworker-gvisor/runsc_test.go index 94deffdbd7..057cf7cb1f 100644 --- a/cmd/ateworker-gvisor/runsc_test.go +++ b/cmd/ateworker-gvisor/runsc_test.go @@ -155,6 +155,34 @@ func TestBuildArgs(t *testing.T) { "my-container", }, }, + { + name: "wait -restore", + command: "wait", + containerName: "my-container", + extraArgs: []string{"-restore"}, + want: []string{ + "-global1", + "-global2", + "-root", "/node/actors/test-actor-123/runsc-state", + "wait", + "-restore", + "my-container", + }, + }, + { + name: "wait -fsrestore", + command: "wait", + containerName: "my-container", + extraArgs: []string{"-fsrestore"}, + want: []string{ + "-global1", + "-global2", + "-root", "/node/actors/test-actor-123/runsc-state", + "wait", + "-fsrestore", + "my-container", + }, + }, { name: "list", command: "list", diff --git a/cmd/ateworker-microvm/restore.go b/cmd/ateworker-microvm/restore.go index 02bf2971bf..fa14efa91e 100644 --- a/cmd/ateworker-microvm/restore.go +++ b/cmd/ateworker-microvm/restore.go @@ -405,10 +405,8 @@ func (s *AteWorkerService) restoreMemoryFidelity(ctx context.Context, p actorBoo // page cache before resuming the VM so guest RAM and snapshot page cache do // not overlap during Resume and wakeup probe gating. // - // Dropping ahead of Resume means a Resume, agent dial, or wakeup failure leaves - // restore-state empty; atelet re-downloads with O_TRUNC and resetActorDirs - // wipes the directory on retry, so this costs a re-download on retry rather - // than correctness. + // When the restore dir is not preserved, dropping ahead of Resume means a + // failure after this point costs a re-download of the snapshot on retry. maybeDropStagedMemoryImage(ctx, restoreDir, memMode, preserveRestoreDir) dropActorSnapshotCacheAsync(restoreDir, memMode == ch.MemRestoreEager) if err := client.Resume(ctx); err != nil { diff --git a/cmd/ateworker-microvm/snapshotcache.go b/cmd/ateworker-microvm/snapshotcache.go index 6dbc711082..2f62457643 100644 --- a/cmd/ateworker-microvm/snapshotcache.go +++ b/cmd/ateworker-microvm/snapshotcache.go @@ -43,8 +43,9 @@ func dropStagedMemoryImage(ctx context.Context, restoreDir string) { if len(targets) == 0 { return } - snapshotcache.DropFiles(targets) - slog.InfoContext(ctx, "dropped the staged memory image (eager restore needs no merge base)") + if snapshotcache.DropFiles(targets) { + slog.InfoContext(ctx, "dropped the staged memory image (eager restore needs no merge base)") + } } // dropActorSnapshotCacheAsync pins the actor's snapshot files in restoreDir diff --git a/internal/snapshotcache/snapshotcache.go b/internal/snapshotcache/snapshotcache.go index a18773a649..a1e2c710db 100644 --- a/internal/snapshotcache/snapshotcache.go +++ b/internal/snapshotcache/snapshotcache.go @@ -39,7 +39,7 @@ type File struct { } var ( - cachestatUnsupportedOnce sync.Once + cachestatWarnOnce sync.Once // cachestatFn and fadviseFn are seams for unit tests so eviction logic can // be verified deterministically regardless of the backing filesystem (ext4, @@ -77,34 +77,44 @@ func FindFiles(dir string, match func(name string) bool) []File { return targets } -// DropFile unlinks t.Path if it still refers to the same inode and size. -func DropFile(t File) { +// DropFile unlinks t.Path if it still refers to the same inode and size, and +// reports whether the file was removed. +func DropFile(t File) bool { st, err := os.Stat(t.Path) if err != nil { if !errors.Is(err, os.ErrNotExist) { slog.Debug("could not stat staged snapshot file", slog.String("path", t.Path), slog.Any("err", err)) } - return + return false } if !os.SameFile(st, t.Info) || st.Size() != t.Info.Size() { - return + return false } - if err := os.Remove(t.Path); err != nil && !errors.Is(err, os.ErrNotExist) { - slog.Warn("could not drop staged snapshot file", slog.String("path", t.Path), slog.Any("err", err)) + if err := os.Remove(t.Path); err != nil { + if !errors.Is(err, os.ErrNotExist) { + slog.Warn("could not drop staged snapshot file", slog.String("path", t.Path), slog.Any("err", err)) + } + return false } + return true } -// DropFiles calls DropFile for each file in files. -func DropFiles(files []File) { +// DropFiles calls DropFile for each file in files and reports whether any file +// was removed. +func DropFiles(files []File) bool { + dropped := false for _, t := range files { - DropFile(t) + if DropFile(t) { + dropped = true + } } + return dropped } // EvictFiles evicts clean cached pages of targets via POSIX_FADV_DONTNEED and -// returns true if any target still exists and has pages (clean or dirty) -// remaining in cache. Files with dirty or writeback pages are skipped so -// FADV_DONTNEED never forces synchronous disk writeback I/O. +// returns true if any target still has dirty or writeback pages remaining in +// cache (or if fadvise failed). Files with dirty or writeback pages are skipped +// so FADV_DONTNEED never forces synchronous disk writeback I/O. func EvictFiles(targets []File) bool { anyRemaining := false for _, t := range targets { @@ -116,10 +126,13 @@ func EvictFiles(targets []File) bool { if err == nil && os.SameFile(info, t.Info) && info.Size() == t.Info.Size() { var cstat unix.Cachestat_t if err := cachestatFn(uint(f.Fd()), &unix.CachestatRange{}, &cstat, 0); err != nil { - if errors.Is(err, unix.ENOSYS) { - cachestatUnsupportedOnce.Do(func() { - slog.Warn("cachestat(2) syscall not supported by kernel; skipping preserved snapshot page-cache eviction", slog.Any("err", err)) - }) + loggedWarn := false + cachestatWarnOnce.Do(func() { + loggedWarn = true + slog.Warn("cachestat(2) syscall failed; skipping preserved snapshot page-cache eviction", slog.String("path", t.Path), slog.Any("err", err)) + }) + if !loggedWarn { + slog.Debug("cachestat(2) syscall failed; skipping preserved snapshot page-cache eviction", slog.String("path", t.Path), slog.Any("err", err)) } _ = f.Close() continue @@ -128,13 +141,15 @@ func EvictFiles(targets []File) bool { _ = f.Close() continue } - anyRemaining = true if cstat.Dirty > 0 || cstat.Writeback > 0 { + anyRemaining = true _ = f.Close() continue } // length zero extends the Fadvise to the whole file. - _ = fadviseFn(int(f.Fd()), 0, 0, unix.FADV_DONTNEED) + if err := fadviseFn(int(f.Fd()), 0, 0, unix.FADV_DONTNEED); err != nil { + anyRemaining = true + } } _ = f.Close() } diff --git a/internal/snapshotcache/snapshotcache_test.go b/internal/snapshotcache/snapshotcache_test.go index d4843aaa2d..ca2770962e 100644 --- a/internal/snapshotcache/snapshotcache_test.go +++ b/internal/snapshotcache/snapshotcache_test.go @@ -43,7 +43,9 @@ func TestFindAndDropFiles(t *testing.T) { t.Fatalf("FindFiles() = %d files, want 3", len(files)) } - DropFiles(files) + if !DropFiles(files) { + t.Fatalf("DropFiles() = false, want true") + } for _, name := range []string{"checkpoint.img", "pages.img", "durable-dir.tar"} { if _, err := os.Stat(filepath.Join(dir, name)); !os.IsNotExist(err) { @@ -74,7 +76,9 @@ func TestDropFileSkipsReplacedInode(t *testing.T) { t.Fatal(err) } - DropFiles(files) + if DropFiles(files) { + t.Fatalf("DropFiles() = true after inode replacement, want false") + } got, err := os.ReadFile(path) if err != nil { t.Fatalf("replaced file was deleted: %v", err) @@ -115,8 +119,8 @@ func TestEvictFiles(t *testing.T) { fadviseCalls++ return nil } - if !EvictFiles(targets) { - t.Errorf("EvictFiles() = false, want true when pages were cached") + if EvictFiles(targets) { + t.Errorf("EvictFiles() = true, want false when clean pages were evicted") } if fadviseCalls != 1 { t.Errorf("fadviseCalls = %d, want 1", fadviseCalls) From 03ab78e78cae67d2cee1970a6b92a645ff7cc033 Mon Sep 17 00:00:00 2001 From: pmandewalkar Date: Fri, 16 Oct 2026 01:00:34 +0000 Subject: [PATCH 22/31] benchmarking: generate sweperf ActorTemplates from an image catalog (#2379) Each SWE-perf task is now one line in `manifests/sweperf/sweperf-images.json`: the name, the image pinned by digest, and the step count. `generate_sweperf_templates.py` renders `sweperf-template.yaml.j2` into `manifests/sweperf/generated/`. Pass `'*'` to generate every task, or give a task name to generate just that one. It rejects catalog entries that are incomplete or not pinned by digest. The generated templates are checked in, and `--check` fails if any are missing, out of date, or no longer in the catalog. To add a task, add it to the catalog, run the generator and commit the new file. `deploy.sh` now reads `sweperf-*` templates from `manifests/sweperf/generated/`. Before touching the cluster, it checks that every requested template has a manifest, and if a sweperf one is missing it points you to the generator. Because images are pinned in the catalog, the `${SWEPERF_IMAGE}` substitution is gone. The hand-written `swebench-astropy-7336` template is replaced by the generated `sweperf-astropy-7336`, which is now the default `--sweperf-template`. The client still reads the step count from `--sweperf-total-steps`, so set it to match the catalog. The template keeps the value only for reference. Reading it straight from the template will be a follow-up. Testing: `make verify` and `test_generate_sweperf_templates.py` pass, and `--check`. On GKE I deployed `sweperf-astropy-7336` and `sweperf-django-11099`, both got golden snapshots, and a Locust run created actors from the astropy template. - [X ] Tests pass - [ X] Appropriate changes to documentation are included in the PR --- benchmarking/README.md | 27 ++++ benchmarking/locust/common/sweperf_config.py | 4 +- benchmarking/workloads/deploy.sh | 32 +++- .../swebench-astropy-7336-template.yaml.tmpl | 62 -------- .../sweperf/generate_sweperf_templates.py | 140 ++++++++++++++++++ .../sweperf-astropy-7336-template.yaml.tmpl | 55 +++++++ .../sweperf-django-11099-template.yaml.tmpl | 55 +++++++ ...weperf-matplotlib-20826-template.yaml.tmpl | 55 +++++++ .../sweperf-sphinx-8638-template.yaml.tmpl | 55 +++++++ .../sweperf-sympy-13647-template.yaml.tmpl | 55 +++++++ .../manifests/sweperf/requirements.txt | 15 ++ .../manifests/sweperf/sweperf-images.json | 27 ++++ .../sweperf/sweperf-template.yaml.j2 | 55 +++++++ .../test_generate_sweperf_templates.py | 76 ++++++++++ hack/verify/benchmarking-python.sh | 1 + .../boomer/dynconfig/dynconfig_test.go | 6 +- .../benchmarking/boomer/sweperf/sweperf.go | 2 +- 17 files changed, 650 insertions(+), 72 deletions(-) delete mode 100644 benchmarking/workloads/manifests/swebench-astropy-7336-template.yaml.tmpl create mode 100644 benchmarking/workloads/manifests/sweperf/generate_sweperf_templates.py create mode 100644 benchmarking/workloads/manifests/sweperf/generated/sweperf-astropy-7336-template.yaml.tmpl create mode 100644 benchmarking/workloads/manifests/sweperf/generated/sweperf-django-11099-template.yaml.tmpl create mode 100644 benchmarking/workloads/manifests/sweperf/generated/sweperf-matplotlib-20826-template.yaml.tmpl create mode 100644 benchmarking/workloads/manifests/sweperf/generated/sweperf-sphinx-8638-template.yaml.tmpl create mode 100644 benchmarking/workloads/manifests/sweperf/generated/sweperf-sympy-13647-template.yaml.tmpl create mode 100644 benchmarking/workloads/manifests/sweperf/requirements.txt create mode 100644 benchmarking/workloads/manifests/sweperf/sweperf-images.json create mode 100644 benchmarking/workloads/manifests/sweperf/sweperf-template.yaml.j2 create mode 100644 benchmarking/workloads/manifests/sweperf/test_generate_sweperf_templates.py diff --git a/benchmarking/README.md b/benchmarking/README.md index 308843dc78..851d15939f 100644 --- a/benchmarking/README.md +++ b/benchmarking/README.md @@ -161,6 +161,33 @@ agent workload. One task is four cycles by default. Each task runs on a fresh ac the last cycle finishes, the actor is deleted and the user creates a new one for the next task. +#### Sweperf Tasks + +Each task is one entry (name, digest-pinned image, step count) in +`workloads/manifests/sweperf/sweperf-images.json`. Its ActorTemplate, `sweperf-`, +is generated into `workloads/manifests/sweperf/generated/` and checked in. To add or change +a task, edit the catalog and regenerate: + +```bash +SWEPERF=benchmarking/workloads/manifests/sweperf +python3 -m venv "${SWEPERF}/venv" && "${SWEPERF}/venv/bin/pip" install -r "${SWEPERF}/requirements.txt" +"${SWEPERF}/venv/bin/python" "${SWEPERF}/generate_sweperf_templates.py" '*' +"${SWEPERF}/venv/bin/python" "${SWEPERF}/generate_sweperf_templates.py" --check # fail if generated/ is missing or stale +hack/verify/benchmarking-python.sh # unit tests, as CI runs them +``` + +Deploy a task and the sweperf user class: + +```bash +WORKLOAD_TEMPLATES=sweperf-django-11099 ./benchmarking/workloads/deploy.sh --deploy +./benchmarking/locust/deploy.sh --deploy --user-class sweperf +``` + +Then start the run with `--sweperf-template sweperf-django-11099` and +`--sweperf-total-steps 15`, set in the Locust web UI or through `LOCUST_SWEPERF_TEMPLATE` +and `LOCUST_SWEPERF_TOTAL_STEPS`. The client takes the step count from that option, not +from the template, so it must match the catalog. + #### Sweperf Reported Metrics All rows are in milliseconds. CEL (command execution latency) is the time the trace commands diff --git a/benchmarking/locust/common/sweperf_config.py b/benchmarking/locust/common/sweperf_config.py index ddb90dea2a..e4810ca870 100644 --- a/benchmarking/locust/common/sweperf_config.py +++ b/benchmarking/locust/common/sweperf_config.py @@ -23,9 +23,9 @@ def add_sweperf_arguments(parser: LocustArgumentParser) -> None: parser.add_argument( "--sweperf-template", type=str, - default="swebench-astropy-7336", + default="sweperf-astropy-7336", env_var="LOCUST_SWEPERF_TEMPLATE", - help="ActorTemplate name for the sweperf workload (default: swebench-astropy-7336)", + help="ActorTemplate name for the sweperf workload (default: sweperf-astropy-7336)", include_in_web_ui=True, ) parser.add_argument( diff --git a/benchmarking/workloads/deploy.sh b/benchmarking/workloads/deploy.sh index 6e3c4a4d29..59c4d15b21 100755 --- a/benchmarking/workloads/deploy.sh +++ b/benchmarking/workloads/deploy.sh @@ -31,14 +31,25 @@ fi MANIFEST_DIR="benchmarking/workloads/manifests" POOL_MANIFEST="${MANIFEST_DIR}/workloads.yaml.tmpl" +# Generated sweperf ActorTemplates; see manifests/sweperf/generate_sweperf_templates.py. +SWEPERF_TEMPLATE_DIR="${MANIFEST_DIR}/sweperf/generated" # The benchmark ActorTemplates: -template.yaml.tmpl each, created -# through the ate API in the benchmark-workloads atespace. WORKLOAD_TEMPLATES +# through the ate API in the benchmark-workloads atespace. sweperf-* names are +# read from SWEPERF_TEMPLATE_DIR, the rest from MANIFEST_DIR. WORKLOAD_TEMPLATES # overrides the default set — the usermem and kernelmem templates (for the # matching locust tests) are not deployed by default. The glutton-storage # template is added when a StorageClass is set, because it needs a CSI driver # that not every cluster has. read -r -a TEMPLATES <<<"${WORKLOAD_TEMPLATES:-sleep glutton glutton-durdir-data glutton-durdir-full}" +# template_manifest prints the manifest path for ActorTemplate NAME. +template_manifest() { + case "$1" in + sweperf-*) echo "${SWEPERF_TEMPLATE_DIR}/$1-template.yaml.tmpl" ;; + *) echo "${MANIFEST_DIR}/$1-template.yaml.tmpl" ;; + esac +} + if [[ ! -f "${POOL_MANIFEST}" ]]; then echo "Error: ${POOL_MANIFEST} not found in $(pwd)" >&2 exit 1 @@ -162,7 +173,6 @@ substitute() { -e "s|\${ACTOR_MEMORY}|${ACTOR_MEMORY}|g" \ -e "s|\${ACTOR_CPU}|${ACTOR_CPU}|g" \ -e "s|\${WORKER_RESOURCES}|${worker_resources}|g" \ - -e "s|\${SWEPERF_IMAGE}|${SWEPERF_IMAGE:-}|g" \ -e "s|\${STORAGE_CLASS_NAME}|${STORAGE_CLASS_NAME}|g" \ "${manifest}" } @@ -247,6 +257,21 @@ tune_fake_atelet() { } deploy() { + # Check every requested template has a manifest before anything on the + # cluster is deleted or created. + local template manifest + for template in "${TEMPLATES[@]}"; do + manifest="$(template_manifest "${template}")" + if [[ ! -f "${manifest}" ]]; then + echo "Error: no manifest for template ${template}: ${manifest} not found" >&2 + if [[ "${template}" == sweperf-* ]]; then + echo "sweperf templates come from ${MANIFEST_DIR}/sweperf/sweperf-images.json; add the task" >&2 + echo "there and run generate_sweperf_templates.py (see \"Sweperf Tasks\" in benchmarking/README.md)" >&2 + fi + exit 1 + fi + done + resolve_otlp_endpoint # Before the pool, so its fake Workers are served by the tuned fake-atelet. if fake_data_plane; then @@ -282,13 +307,12 @@ deploy() { # the old template first. This removal is safe, because the benchmark # automation deletes the actors between the tests; it also removes the old # golden actor and snapshot server-side. - local template for template in "${TEMPLATES[@]}"; do run_kubectl_ate delete actor-template "${template}" -a benchmark-workloads \ >/dev/null 2>&1 || true # ko resolve builds the ko:// image references and replaces them with # pushed digests before the manifest reaches kubectl-ate. - substitute "${MANIFEST_DIR}/${template}-template.yaml.tmpl" \ + substitute "$(template_manifest "${template}")" \ | hack/run-tool.sh ko resolve -f - \ | run_kubectl_ate create actor-template -f - done diff --git a/benchmarking/workloads/manifests/swebench-astropy-7336-template.yaml.tmpl b/benchmarking/workloads/manifests/swebench-astropy-7336-template.yaml.tmpl deleted file mode 100644 index 5f8455216e..0000000000 --- a/benchmarking/workloads/manifests/swebench-astropy-7336-template.yaml.tmpl +++ /dev/null @@ -1,62 +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. - -metadata: - atespace: benchmark-workloads - name: swebench-astropy-7336 -workerSelector: - matchLabels: - workload: benchmark-worker -containers: -- name: astropy - # The task image is built from the SWE-Perf repo - # https://github.com/gke-labs/sweperf, which supplies both the server - # entrypoint and the trajectory under test; the command below runs that - # entrypoint against that trace, so the image has to be the one built for - # this task. - # - # Supplied via the SWEPERF_IMAGE environment variable, substituted by - # benchmarking/workloads/deploy.sh. Pin by digest rather than tag: these - # images are commonly rebuilt in place under the same tag. - # - # Build the image from sweperf c30c0d6 (2026-09-21) or later. The - # asynchronous HTTP execution API this benchmark drives was added on - # 2026-08-31; images older than that ship a replay.py with no HTTP server at - # all and fail on the client's first GET /status. c30c0d6 adds - # execution_duration_ms to the job status, the in-sandbox time of each cycle. - image: "${SWEPERF_IMAGE}" - # generate.py writes the replay server to /replay.py and the trajectory to - # /trace.json at the image root; these are also the image's own ENTRYPOINT. - # Nothing in that repo produces /opt/swebench/. - command: - - "python3" - - "/replay.py" - - "/trace.json" - env: - - name: SWEPERF_DISABLE_INTERNAL_SLEEP - value: "1" -# Sandbox size: enough for the astropy test runs the trace replays. The cpu -# limit caps the measured execution time, so keep it fixed across runs. -resources: - limits: - - name: cpu - quantity: "2" - - name: memory - quantity: 4Gi -sandboxConfig: - sandboxClass: ${SANDBOX_CLASS} - configName: ${SANDBOX_CONFIG_NAME} -snapshotConfig: - preferredFidelity: SNAPSHOT_FIDELITY_MEMORY - storageLocation: gs://${BUCKET_NAME}/benchmark-workloads/swebench-astropy-7336/ diff --git a/benchmarking/workloads/manifests/sweperf/generate_sweperf_templates.py b/benchmarking/workloads/manifests/sweperf/generate_sweperf_templates.py new file mode 100644 index 0000000000..b96a955d81 --- /dev/null +++ b/benchmarking/workloads/manifests/sweperf/generate_sweperf_templates.py @@ -0,0 +1,140 @@ +#!/usr/bin/env python3 +# 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. + +"""Renders sweperf ActorTemplates from sweperf-images.json. + +Usage: + generate_sweperf_templates.py '*' # every task + generate_sweperf_templates.py astropy-7336 # one task + generate_sweperf_templates.py 'django*' -o /tmp/out # glob, custom output directory + generate_sweperf_templates.py --check # fail if generated/ is stale + +Writes sweperf--template.yaml.tmpl per task into generated/ next to +this script, where deploy.sh looks for every sweperf-* template. The +generated files are checked in: regenerate after editing the catalog or the +.j2, and use --check to find files that are missing or stale. Images in +sweperf-images.json must be pinned by digest. +""" + +import argparse +import collections +import fnmatch +import json +import pathlib +import sys + +import jinja2 + +HERE = pathlib.Path(__file__).resolve().parent + + +def load_catalog(path: pathlib.Path) -> list[dict]: + """Reads the catalog and rejects entries deploy.sh or the client can't use.""" + tasks = json.loads(path.read_text()) + for t in tasks: + missing = {"name", "image", "steps"} - t.keys() + if missing: + raise ValueError(f"{path}: entry {t} is missing {sorted(missing)}") + if not isinstance(t["steps"], int) or t["steps"] <= 0: + raise ValueError(f"{path}: {t['name']}: steps must be a positive integer") + if "@sha256:" not in t["image"]: + raise ValueError(f"{path}: {t['name']}: image must be pinned by digest (repo@sha256:...)") + # render() keys its output by file name, so a duplicate would silently + # overwrite the earlier entry. + names = collections.Counter(t["name"] for t in tasks) + duplicates = sorted(n for n, c in names.items() if c > 1) + if duplicates: + raise ValueError(f"{path}: duplicate task names: {duplicates}") + return tasks + + +def render(tasks: list[dict]) -> dict[str, str]: + """Returns the generated file name and contents for each task.""" + template = jinja2.Environment( + loader=jinja2.FileSystemLoader(HERE), + undefined=jinja2.StrictUndefined, + keep_trailing_newline=True, + ).get_template("sweperf-template.yaml.j2") + out = {} + for t in tasks: + name = f"sweperf-{t['name']}" + out[f"{name}-template.yaml.tmpl"] = template.render( + name=name, + container=t["name"].split("-")[0], # astropy-7336 -> astropy + image=t["image"], + steps=t["steps"], + ) + return out + + +def check(rendered: dict[str, str], directory: pathlib.Path) -> list[str]: + """Returns how directory differs from what the catalog renders.""" + problems = [] + for fname, content in rendered.items(): + path = directory / fname + if not path.exists(): + problems.append(f"missing: {path}") + elif path.read_text() != content: + problems.append(f"stale: {path}") + for path in sorted(directory.glob("*-template.yaml.tmpl")): + if path.name not in rendered: + problems.append(f"not in catalog: {path}") + return problems + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter) + parser.add_argument("task", nargs="?", help="task name to render, or a glob such as '*'") + parser.add_argument("-o", "--out", type=pathlib.Path, default=HERE / "generated", + help="output directory (default: %(default)s)") + parser.add_argument("--catalog", type=pathlib.Path, default=HERE / "sweperf-images.json") + parser.add_argument("--check", action="store_true", + help="fail if the output directory is missing, stale or has extra templates") + args = parser.parse_args() + + try: + tasks = load_catalog(args.catalog) + except ValueError as e: + print(e, file=sys.stderr) + return 1 + + if args.check: + problems = check(render(tasks), args.out) + for p in problems: + print(p, file=sys.stderr) + if problems: + print(f"regenerate with: {sys.argv[0]} '*'", file=sys.stderr) + return 1 + return 0 + + if not args.task: + parser.error("give a task name or glob, or --check") + selected = [t for t in tasks if fnmatch.fnmatch(t["name"], args.task)] + if not selected: + names = ", ".join(t["name"] for t in tasks) + print(f"no task matches {args.task!r}; known: {names}", file=sys.stderr) + return 1 + + args.out.mkdir(parents=True, exist_ok=True) + for fname, content in render(selected).items(): + path = args.out / fname + path.write_text(content) + print(path) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/benchmarking/workloads/manifests/sweperf/generated/sweperf-astropy-7336-template.yaml.tmpl b/benchmarking/workloads/manifests/sweperf/generated/sweperf-astropy-7336-template.yaml.tmpl new file mode 100644 index 0000000000..050f1d9676 --- /dev/null +++ b/benchmarking/workloads/manifests/sweperf/generated/sweperf-astropy-7336-template.yaml.tmpl @@ -0,0 +1,55 @@ +# 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. + +# Generated by benchmarking/workloads/manifests/sweperf/generate_sweperf_templates.py +# from sweperf-images.json and sweperf-template.yaml.j2; edit those, not this +# file. The ${...} values are filled in by benchmarking/workloads/deploy.sh. +metadata: + atespace: benchmark-workloads + name: sweperf-astropy-7336 +workerSelector: + matchLabels: + workload: benchmark-worker +containers: +- name: astropy + # Built from https://github.com/gke-labs/sweperf c30c0d6 or later (older + # images have no HTTP server and fail the client's first GET /status), pinned + # by digest because these images are rebuilt in place under the same tag. + image: "us-docker.pkg.dev/gke-substrate-release/benchmark/sweperf@sha256:c265bfba3bea40c99e7baad734579b75d02dab6dbe6910cb1f9ed71c8b08f5b1" + command: + - "python3" + - "/replay.py" + - "/trace.json" + env: + - name: SWEPERF_DISABLE_INTERNAL_SLEEP + value: "1" + # The trajectory's step count, for reference only: the sweperf client takes + # it from --sweperf-total-steps, so run this template with + # --sweperf-total-steps=21. + - name: SWEPERF_TOTAL_STEPS + value: "21" +# Sandbox size: enough for the test runs the traces replay. The cpu limit caps +# the measured execution time, so keep it fixed across runs. +resources: + limits: + - name: cpu + quantity: "2" + - name: memory + quantity: 4Gi +sandboxConfig: + sandboxClass: ${SANDBOX_CLASS} + configName: ${SANDBOX_CONFIG_NAME} +snapshotConfig: + preferredFidelity: SNAPSHOT_FIDELITY_MEMORY + storageLocation: gs://${BUCKET_NAME}/benchmark-workloads/sweperf-astropy-7336/ diff --git a/benchmarking/workloads/manifests/sweperf/generated/sweperf-django-11099-template.yaml.tmpl b/benchmarking/workloads/manifests/sweperf/generated/sweperf-django-11099-template.yaml.tmpl new file mode 100644 index 0000000000..43ce70e1ff --- /dev/null +++ b/benchmarking/workloads/manifests/sweperf/generated/sweperf-django-11099-template.yaml.tmpl @@ -0,0 +1,55 @@ +# 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. + +# Generated by benchmarking/workloads/manifests/sweperf/generate_sweperf_templates.py +# from sweperf-images.json and sweperf-template.yaml.j2; edit those, not this +# file. The ${...} values are filled in by benchmarking/workloads/deploy.sh. +metadata: + atespace: benchmark-workloads + name: sweperf-django-11099 +workerSelector: + matchLabels: + workload: benchmark-worker +containers: +- name: django + # Built from https://github.com/gke-labs/sweperf c30c0d6 or later (older + # images have no HTTP server and fail the client's first GET /status), pinned + # by digest because these images are rebuilt in place under the same tag. + image: "us-docker.pkg.dev/gke-substrate-release/benchmark/sweperf@sha256:7e4baaf61a7482d6d09e2fcccecd1529f47488aa52821d507cc8d6ad0ba31eea" + command: + - "python3" + - "/replay.py" + - "/trace.json" + env: + - name: SWEPERF_DISABLE_INTERNAL_SLEEP + value: "1" + # The trajectory's step count, for reference only: the sweperf client takes + # it from --sweperf-total-steps, so run this template with + # --sweperf-total-steps=15. + - name: SWEPERF_TOTAL_STEPS + value: "15" +# Sandbox size: enough for the test runs the traces replay. The cpu limit caps +# the measured execution time, so keep it fixed across runs. +resources: + limits: + - name: cpu + quantity: "2" + - name: memory + quantity: 4Gi +sandboxConfig: + sandboxClass: ${SANDBOX_CLASS} + configName: ${SANDBOX_CONFIG_NAME} +snapshotConfig: + preferredFidelity: SNAPSHOT_FIDELITY_MEMORY + storageLocation: gs://${BUCKET_NAME}/benchmark-workloads/sweperf-django-11099/ diff --git a/benchmarking/workloads/manifests/sweperf/generated/sweperf-matplotlib-20826-template.yaml.tmpl b/benchmarking/workloads/manifests/sweperf/generated/sweperf-matplotlib-20826-template.yaml.tmpl new file mode 100644 index 0000000000..315add0315 --- /dev/null +++ b/benchmarking/workloads/manifests/sweperf/generated/sweperf-matplotlib-20826-template.yaml.tmpl @@ -0,0 +1,55 @@ +# 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. + +# Generated by benchmarking/workloads/manifests/sweperf/generate_sweperf_templates.py +# from sweperf-images.json and sweperf-template.yaml.j2; edit those, not this +# file. The ${...} values are filled in by benchmarking/workloads/deploy.sh. +metadata: + atespace: benchmark-workloads + name: sweperf-matplotlib-20826 +workerSelector: + matchLabels: + workload: benchmark-worker +containers: +- name: matplotlib + # Built from https://github.com/gke-labs/sweperf c30c0d6 or later (older + # images have no HTTP server and fail the client's first GET /status), pinned + # by digest because these images are rebuilt in place under the same tag. + image: "us-docker.pkg.dev/gke-substrate-release/benchmark/sweperf@sha256:74989c4921831f9fbf3fb95924e210bf6616ede00cf30cc9cf0bad945342174b" + command: + - "python3" + - "/replay.py" + - "/trace.json" + env: + - name: SWEPERF_DISABLE_INTERNAL_SLEEP + value: "1" + # The trajectory's step count, for reference only: the sweperf client takes + # it from --sweperf-total-steps, so run this template with + # --sweperf-total-steps=49. + - name: SWEPERF_TOTAL_STEPS + value: "49" +# Sandbox size: enough for the test runs the traces replay. The cpu limit caps +# the measured execution time, so keep it fixed across runs. +resources: + limits: + - name: cpu + quantity: "2" + - name: memory + quantity: 4Gi +sandboxConfig: + sandboxClass: ${SANDBOX_CLASS} + configName: ${SANDBOX_CONFIG_NAME} +snapshotConfig: + preferredFidelity: SNAPSHOT_FIDELITY_MEMORY + storageLocation: gs://${BUCKET_NAME}/benchmark-workloads/sweperf-matplotlib-20826/ diff --git a/benchmarking/workloads/manifests/sweperf/generated/sweperf-sphinx-8638-template.yaml.tmpl b/benchmarking/workloads/manifests/sweperf/generated/sweperf-sphinx-8638-template.yaml.tmpl new file mode 100644 index 0000000000..df415cc62e --- /dev/null +++ b/benchmarking/workloads/manifests/sweperf/generated/sweperf-sphinx-8638-template.yaml.tmpl @@ -0,0 +1,55 @@ +# 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. + +# Generated by benchmarking/workloads/manifests/sweperf/generate_sweperf_templates.py +# from sweperf-images.json and sweperf-template.yaml.j2; edit those, not this +# file. The ${...} values are filled in by benchmarking/workloads/deploy.sh. +metadata: + atespace: benchmark-workloads + name: sweperf-sphinx-8638 +workerSelector: + matchLabels: + workload: benchmark-worker +containers: +- name: sphinx + # Built from https://github.com/gke-labs/sweperf c30c0d6 or later (older + # images have no HTTP server and fail the client's first GET /status), pinned + # by digest because these images are rebuilt in place under the same tag. + image: "us-docker.pkg.dev/gke-substrate-release/benchmark/sweperf@sha256:ca801ef886f07f92598f576b875e6f5b5ce4d7d8ed4da607d210e7f0caa23210" + command: + - "python3" + - "/replay.py" + - "/trace.json" + env: + - name: SWEPERF_DISABLE_INTERNAL_SLEEP + value: "1" + # The trajectory's step count, for reference only: the sweperf client takes + # it from --sweperf-total-steps, so run this template with + # --sweperf-total-steps=64. + - name: SWEPERF_TOTAL_STEPS + value: "64" +# Sandbox size: enough for the test runs the traces replay. The cpu limit caps +# the measured execution time, so keep it fixed across runs. +resources: + limits: + - name: cpu + quantity: "2" + - name: memory + quantity: 4Gi +sandboxConfig: + sandboxClass: ${SANDBOX_CLASS} + configName: ${SANDBOX_CONFIG_NAME} +snapshotConfig: + preferredFidelity: SNAPSHOT_FIDELITY_MEMORY + storageLocation: gs://${BUCKET_NAME}/benchmark-workloads/sweperf-sphinx-8638/ diff --git a/benchmarking/workloads/manifests/sweperf/generated/sweperf-sympy-13647-template.yaml.tmpl b/benchmarking/workloads/manifests/sweperf/generated/sweperf-sympy-13647-template.yaml.tmpl new file mode 100644 index 0000000000..12310ad3fe --- /dev/null +++ b/benchmarking/workloads/manifests/sweperf/generated/sweperf-sympy-13647-template.yaml.tmpl @@ -0,0 +1,55 @@ +# 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. + +# Generated by benchmarking/workloads/manifests/sweperf/generate_sweperf_templates.py +# from sweperf-images.json and sweperf-template.yaml.j2; edit those, not this +# file. The ${...} values are filled in by benchmarking/workloads/deploy.sh. +metadata: + atespace: benchmark-workloads + name: sweperf-sympy-13647 +workerSelector: + matchLabels: + workload: benchmark-worker +containers: +- name: sympy + # Built from https://github.com/gke-labs/sweperf c30c0d6 or later (older + # images have no HTTP server and fail the client's first GET /status), pinned + # by digest because these images are rebuilt in place under the same tag. + image: "us-docker.pkg.dev/gke-substrate-release/benchmark/sweperf@sha256:89ed518b85efaf063cd3a515c33bc293a4883964d9af7cc21c4e34824ee171f1" + command: + - "python3" + - "/replay.py" + - "/trace.json" + env: + - name: SWEPERF_DISABLE_INTERNAL_SLEEP + value: "1" + # The trajectory's step count, for reference only: the sweperf client takes + # it from --sweperf-total-steps, so run this template with + # --sweperf-total-steps=33. + - name: SWEPERF_TOTAL_STEPS + value: "33" +# Sandbox size: enough for the test runs the traces replay. The cpu limit caps +# the measured execution time, so keep it fixed across runs. +resources: + limits: + - name: cpu + quantity: "2" + - name: memory + quantity: 4Gi +sandboxConfig: + sandboxClass: ${SANDBOX_CLASS} + configName: ${SANDBOX_CONFIG_NAME} +snapshotConfig: + preferredFidelity: SNAPSHOT_FIDELITY_MEMORY + storageLocation: gs://${BUCKET_NAME}/benchmark-workloads/sweperf-sympy-13647/ diff --git a/benchmarking/workloads/manifests/sweperf/requirements.txt b/benchmarking/workloads/manifests/sweperf/requirements.txt new file mode 100644 index 0000000000..96707f9b84 --- /dev/null +++ b/benchmarking/workloads/manifests/sweperf/requirements.txt @@ -0,0 +1,15 @@ +# 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. + +jinja2>=3.1 diff --git a/benchmarking/workloads/manifests/sweperf/sweperf-images.json b/benchmarking/workloads/manifests/sweperf/sweperf-images.json new file mode 100644 index 0000000000..88037e587d --- /dev/null +++ b/benchmarking/workloads/manifests/sweperf/sweperf-images.json @@ -0,0 +1,27 @@ +[ + { + "name": "astropy-7336", + "image": "us-docker.pkg.dev/gke-substrate-release/benchmark/sweperf@sha256:c265bfba3bea40c99e7baad734579b75d02dab6dbe6910cb1f9ed71c8b08f5b1", + "steps": 21 + }, + { + "name": "django-11099", + "image": "us-docker.pkg.dev/gke-substrate-release/benchmark/sweperf@sha256:7e4baaf61a7482d6d09e2fcccecd1529f47488aa52821d507cc8d6ad0ba31eea", + "steps": 15 + }, + { + "name": "matplotlib-20826", + "image": "us-docker.pkg.dev/gke-substrate-release/benchmark/sweperf@sha256:74989c4921831f9fbf3fb95924e210bf6616ede00cf30cc9cf0bad945342174b", + "steps": 49 + }, + { + "name": "sphinx-8638", + "image": "us-docker.pkg.dev/gke-substrate-release/benchmark/sweperf@sha256:ca801ef886f07f92598f576b875e6f5b5ce4d7d8ed4da607d210e7f0caa23210", + "steps": 64 + }, + { + "name": "sympy-13647", + "image": "us-docker.pkg.dev/gke-substrate-release/benchmark/sweperf@sha256:89ed518b85efaf063cd3a515c33bc293a4883964d9af7cc21c4e34824ee171f1", + "steps": 33 + } +] diff --git a/benchmarking/workloads/manifests/sweperf/sweperf-template.yaml.j2 b/benchmarking/workloads/manifests/sweperf/sweperf-template.yaml.j2 new file mode 100644 index 0000000000..47a259b7ee --- /dev/null +++ b/benchmarking/workloads/manifests/sweperf/sweperf-template.yaml.j2 @@ -0,0 +1,55 @@ +# 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. + +# Generated by benchmarking/workloads/manifests/sweperf/generate_sweperf_templates.py +# from sweperf-images.json and sweperf-template.yaml.j2; edit those, not this +# file. The ${...} values are filled in by benchmarking/workloads/deploy.sh. +metadata: + atespace: benchmark-workloads + name: {{ name }} +workerSelector: + matchLabels: + workload: benchmark-worker +containers: +- name: {{ container }} + # Built from https://github.com/gke-labs/sweperf c30c0d6 or later (older + # images have no HTTP server and fail the client's first GET /status), pinned + # by digest because these images are rebuilt in place under the same tag. + image: "{{ image }}" + command: + - "python3" + - "/replay.py" + - "/trace.json" + env: + - name: SWEPERF_DISABLE_INTERNAL_SLEEP + value: "1" + # The trajectory's step count, for reference only: the sweperf client takes + # it from --sweperf-total-steps, so run this template with + # --sweperf-total-steps={{ steps }}. + - name: SWEPERF_TOTAL_STEPS + value: "{{ steps }}" +# Sandbox size: enough for the test runs the traces replay. The cpu limit caps +# the measured execution time, so keep it fixed across runs. +resources: + limits: + - name: cpu + quantity: "2" + - name: memory + quantity: 4Gi +sandboxConfig: + sandboxClass: ${SANDBOX_CLASS} + configName: ${SANDBOX_CONFIG_NAME} +snapshotConfig: + preferredFidelity: SNAPSHOT_FIDELITY_MEMORY + storageLocation: gs://${BUCKET_NAME}/benchmark-workloads/{{ name }}/ diff --git a/benchmarking/workloads/manifests/sweperf/test_generate_sweperf_templates.py b/benchmarking/workloads/manifests/sweperf/test_generate_sweperf_templates.py new file mode 100644 index 0000000000..2d67908e01 --- /dev/null +++ b/benchmarking/workloads/manifests/sweperf/test_generate_sweperf_templates.py @@ -0,0 +1,76 @@ +# 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. + +"""Tests for generate_sweperf_templates.py: +python3 benchmarking/workloads/manifests/sweperf/test_generate_sweperf_templates.py +""" + +import json +import pathlib +import tempfile +import unittest + +import generate_sweperf_templates as gen + +PINNED = "example.com/sweperf@sha256:" + "0" * 64 + + +class GenerateTest(unittest.TestCase): + + def test_generated_matches_catalog(self): + # Fails when sweperf-images.json or the .j2 changed without regenerating. + rendered = gen.render(gen.load_catalog(gen.HERE / "sweperf-images.json")) + self.assertEqual(gen.check(rendered, gen.HERE / "generated"), [], + "run generate_sweperf_templates.py '*'") + + def test_render_fills_task_values(self): + out = gen.render([{"name": "django-11099", "image": PINNED, "steps": 15}]) + content = out["sweperf-django-11099-template.yaml.tmpl"] + self.assertIn("name: sweperf-django-11099", content) + self.assertIn(f'image: "{PINNED}"', content) + self.assertIn('value: "15"', content) + # deploy.sh fills these, so they must survive rendering. + self.assertIn("${BUCKET_NAME}", content) + + def test_check_reports_missing_stale_and_orphaned_files(self): + rendered = gen.render([{"name": n, "image": PINNED, "steps": 1} + for n in ("fresh-1", "stale-1", "missing-1")]) + with tempfile.TemporaryDirectory() as d: + d = pathlib.Path(d) + (d / "sweperf-fresh-1-template.yaml.tmpl").write_text( + rendered["sweperf-fresh-1-template.yaml.tmpl"]) + (d / "sweperf-stale-1-template.yaml.tmpl").write_text("old") + (d / "sweperf-gone-1-template.yaml.tmpl").write_text("old") + problems = gen.check(rendered, d) + kinds = sorted(p.split(":")[0] for p in problems) + self.assertEqual(kinds, ["missing", "not in catalog", "stale"], problems) + + def test_catalog_rejects_bad_entries(self): + cases = { + "missing key": [{"name": "a", "image": PINNED}], + "zero steps": [{"name": "a", "image": PINNED, "steps": 0}], + "unpinned image": [{"name": "a", "image": "example.com/sweperf:a", "steps": 1}], + "duplicate name": [{"name": "a", "image": PINNED, "steps": 1}, + {"name": "a", "image": PINNED, "steps": 2}], + } + for label, catalog in cases.items(): + with self.subTest(label), tempfile.NamedTemporaryFile("w", suffix=".json") as f: + json.dump(catalog, f) + f.flush() + with self.assertRaises(ValueError): + gen.load_catalog(pathlib.Path(f.name)) + + +if __name__ == "__main__": + unittest.main() diff --git a/hack/verify/benchmarking-python.sh b/hack/verify/benchmarking-python.sh index a206887fb2..7b615a94f4 100755 --- a/hack/verify/benchmarking-python.sh +++ b/hack/verify/benchmarking-python.sh @@ -33,6 +33,7 @@ SUITES=( "benchmarking/automation ." "benchmarking/locust unit_tests" "benchmarking/nighthawk-ingress tests" + "benchmarking/workloads/manifests/sweperf ." ) # A test that escapes its mocks must not reach a cluster or a registry with diff --git a/internal/benchmarking/boomer/dynconfig/dynconfig_test.go b/internal/benchmarking/boomer/dynconfig/dynconfig_test.go index 8479594179..eff1e5c332 100644 --- a/internal/benchmarking/boomer/dynconfig/dynconfig_test.go +++ b/internal/benchmarking/boomer/dynconfig/dynconfig_test.go @@ -41,7 +41,7 @@ func TestParseValid(t *testing.T) { "durdir_template": "glutton-durdir-data", "cpu_cores": 2, "cpu_duty_cycle": 0.1, - "sweperf_template": "swebench-astropy-7336", + "sweperf_template": "sweperf-astropy-7336", "sweperf_total_steps": 21, "sweperf_num_cycles": 4, "sweperf_poll_interval_ms": 100, @@ -94,8 +94,8 @@ func TestParseValid(t *testing.T) { if cfg.CPUDutyCycle != 0.1 { t.Errorf("CPUDutyCycle: got %f, want 0.1", cfg.CPUDutyCycle) } - if cfg.SweperfTemplate != "swebench-astropy-7336" { - t.Errorf("SweperfTemplate: got %q, want swebench-astropy-7336", cfg.SweperfTemplate) + if cfg.SweperfTemplate != "sweperf-astropy-7336" { + t.Errorf("SweperfTemplate: got %q, want sweperf-astropy-7336", cfg.SweperfTemplate) } if cfg.SweperfTotalSteps != 21 { t.Errorf("SweperfTotalSteps: got %d, want 21", cfg.SweperfTotalSteps) diff --git a/internal/benchmarking/boomer/sweperf/sweperf.go b/internal/benchmarking/boomer/sweperf/sweperf.go index 022fe84866..0087c8857d 100644 --- a/internal/benchmarking/boomer/sweperf/sweperf.go +++ b/internal/benchmarking/boomer/sweperf/sweperf.go @@ -62,7 +62,7 @@ const ( // environment supplies a value. const ( sweperfUserClass = "SweperfUser" - defaultSweperfTemplate = "swebench-astropy-7336" + defaultSweperfTemplate = "sweperf-astropy-7336" defaultSweperfTotalSteps = 21 defaultSweperfNumCycles = 4 // defaultSweperfPollInterval trades CycleCEL-to-ack accuracy against From a1471f062847fa36a067ae82a3d88d001e094ace Mon Sep 17 00:00:00 2001 From: Luomu Date: Fri, 16 Oct 2026 01:06:05 +0000 Subject: [PATCH 23/31] Suspend and pause preserve concurrent actor crashes and report failure (#2707) Fixes #2676. If worker deletion or another crash writer marks an actor CRASHED after checkpoint succeeds but before finalize re-reads it, SuspendActor currently overwrites that crash with SUSPENDED, while PauseActor reports success for the CRASHED actor. Both finalize steps now require the re-read actor to be SUSPENDING before validating snapshots, releasing the worker, or writing the final state. Otherwise they return FailedPrecondition and preserve the existing state, crash reason, and snapshot records. The existing version precondition still rejects a concurrent crash after the read with Aborted. Completed suspend and pause calls retain their existing success paths at workflow entry. The full-workflow regression now checks FailedPrecondition and preservation of the original crash in both modes, updating the behavior pinned by #2688. The already-crashed pause fixture also clears WorkerAssignment, matching a real crash; it previously reached FailedPrecondition only through the missing-node branch. Validation of the production change on Linux with CI=true and real PostgreSQL test containers: - RED: before the production fix, the corrected already-crashed pause test and both modes of the full-workflow regression fail because the calls incorrectly return OK. - GREEN: `go test -race -count=1 -json ./...` passes: 158 test-bearing packages and 9,670 test entries, including all 763 controlapi entries and 169 API functional entries. - `go build ./cmd/ateapi ./cmd/atelet ./cmd/kubectl-ate`, `hack/verify-all.sh`, and the tools/apitool race tests pass. The verifier leaves the source tree unchanged. - The full suite skips 41 tests requiring root privileges, IPv6 loopback, or configured external object storage. The affected PostgreSQL regressions do not skip. E2E follow-up: The original CI passed both gVisor suites but failed both microVM suites in `TestGracefulWorkerTerminationSuspend`. That test unconditionally expected SuspendActor to succeed after worker deletion, even when deletion had already crashed the actor. The test now ties the RPC result to the stored state. OK requires SUSPENDED with no crash record. FailedPrecondition or Aborted requires CRASHED, a crash reason, and the previous completed durable snapshot. Both outcomes require the worker and its assignment to be removed. All other errors fail the test. Follow-up validation on Linux with CI=true and real PostgreSQL and envtest: - The four affected controlapi regressions pass with `-race -count=3`. - All 763 controlapi test entries and all 169 functionaltest entries pass, with no skips. The functional package was rerun after initializing the isolated source directory's Git metadata, which envtest setup requires. - `go test -race -c ./internal/e2e/suites/demo` and `go vet ./internal/e2e/suites/demo` pass. Native compilation, gofmt, and whitespace checks also pass. - Real runtime E2E has not run at this head. The new [pr-workflow run](https://github.com/agent-substrate/substrate/actions/runs/38817919692) is `action_required` and needs maintainer approval. Draft #2694 refactors these same workflows and currently preserves an out-of-band state while returning success. This focused fix also reports FailedPrecondition, as required by #2676; that behavior should carry forward into the refactor. - [x] Local validation passes; runtime E2E awaits workflow approval. - [x] Documentation reviewed; finalize and regression-test comments describe the corrected behavior. No guide change is needed. --- .../internal/controlapi/workflow_pause.go | 7 +++- .../controlapi/workflow_pause_test.go | 1 + .../internal/controlapi/workflow_suspend.go | 8 +++- .../controlapi/workflow_suspend_test.go | 36 ++++++++--------- internal/e2e/suites/demo/termination_test.go | 40 ++++++++++++++++--- 5 files changed, 64 insertions(+), 28 deletions(-) diff --git a/cmd/ateapi/internal/controlapi/workflow_pause.go b/cmd/ateapi/internal/controlapi/workflow_pause.go index 56d9fbc42b..9949f76475 100644 --- a/cmd/ateapi/internal/controlapi/workflow_pause.go +++ b/cmd/ateapi/internal/controlapi/workflow_pause.go @@ -229,8 +229,8 @@ func (w *ActorWorkflow) ensureAteletPaused(ctx context.Context, actorRef resourc // can never be resumed from local state, so it commits CRASHED instead and // fails the step with FailedPrecondition. It re-reads the actor first so an // out-of-band transition (e.g. the syncer crashing the actor after its worker -// died) is not overwritten: with no assignment left there is nothing to -// finalize. +// died) is not overwritten: an actor that is no longer SUSPENDING is left as +// it is and the step fails with FailedPrecondition. func (w *ActorWorkflow) ensurePausedFinalized(ctx context.Context, actorRef resources.ActorRef) (_ *ateapipb.Actor, err error) { ctx, done := stepSpan(ctx, "FinalizePaused") defer func() { err = done(err) }() @@ -239,6 +239,9 @@ func (w *ActorWorkflow) ensurePausedFinalized(ctx context.Context, actorRef reso if err != nil { return nil, err } + if state := latestActor.GetStatus().GetState(); state != ateapipb.ActorState_ACTOR_STATE_SUSPENDING { + return nil, apierror.FailedPrecondition("FinalizePaused prerequisite not met for Actor: %s (got: %v, want %s)", actorRef, state, ateapipb.ActorState_ACTOR_STATE_SUSPENDING) + } // 1. Free the worker (if it hasn't been freed yet) if assignment := latestActor.GetStatus().GetWorkerAssignment(); assignment != nil { diff --git a/cmd/ateapi/internal/controlapi/workflow_pause_test.go b/cmd/ateapi/internal/controlapi/workflow_pause_test.go index d73bf836e9..89743a9fa5 100644 --- a/cmd/ateapi/internal/controlapi/workflow_pause_test.go +++ b/cmd/ateapi/internal/controlapi/workflow_pause_test.go @@ -125,6 +125,7 @@ func TestPauseActor_Finalized_AlreadyCrashed(t *testing.T) { updateDuringCheckpoint(t, atelet, st, actorRef, func(s *ateapipb.ActorStatus) { s.State = ateapipb.ActorState_ACTOR_STATE_CRASHED s.Crash = originalCrash + s.WorkerAssignment = nil }) if _, err := w.PauseActor(ctx, actorRef); apierror.Code(err) != codes.FailedPrecondition { diff --git a/cmd/ateapi/internal/controlapi/workflow_suspend.go b/cmd/ateapi/internal/controlapi/workflow_suspend.go index b2655daa16..f8ae72fa36 100644 --- a/cmd/ateapi/internal/controlapi/workflow_suspend.go +++ b/cmd/ateapi/internal/controlapi/workflow_suspend.go @@ -385,8 +385,9 @@ func (w *ActorWorkflow) ensureVolumesDetached(ctx context.Context, actor *ateapi // When the latest snapshot has no durable snapshot to record, it commits // CRASHED instead and fails the step. // It re-reads the actor first so an out-of-band transition (e.g. the syncer -// crashing the actor after its worker died) is not overwritten: with no -// assignment left there is nothing to finalize. +// crashing the actor after its worker died) is not overwritten: an actor that +// is no longer SUSPENDING is left as it is and the step fails with +// FailedPrecondition. func (w *ActorWorkflow) ensureSuspendedFinalized(ctx context.Context, actorRef resources.ActorRef) (_ *ateapipb.Actor, err error) { ctx, done := stepSpan(ctx, "FinalizeSuspended") defer func() { err = done(err) }() @@ -413,6 +414,9 @@ func (w *ActorWorkflow) ensureSuspendedFinalized(ctx context.Context, actorRef r if err != nil { return nil, err } + if state := latestActor.GetStatus().GetState(); state != ateapipb.ActorState_ACTOR_STATE_SUSPENDING { + return nil, apierror.FailedPrecondition("FinalizeSuspended prerequisite not met for Actor: %s (got: %v, want %s)", actorRef, state, ateapipb.ActorState_ACTOR_STATE_SUSPENDING) + } if snapshotAtLatestGeneration(latestActor.GetStatus()).GetDurableSnapshot() == nil { return nil, crashActorAndFail(ctx, w.store, actorRef, ateattr.OperationSuspend, crashMessageDurableSnapshotMissing) } diff --git a/cmd/ateapi/internal/controlapi/workflow_suspend_test.go b/cmd/ateapi/internal/controlapi/workflow_suspend_test.go index 8bed842237..4f629f4874 100644 --- a/cmd/ateapi/internal/controlapi/workflow_suspend_test.go +++ b/cmd/ateapi/internal/controlapi/workflow_suspend_test.go @@ -1315,18 +1315,10 @@ func TestSuspendAndPause_CharacteristicMatrix(t *testing.T) { } } -// TestSuspendAndPause_FinalizeWithoutAssignment pins where the two finalize -// steps differ when the DeleteWorker workflow crashes the actor, clearing its -// worker assignment, between the atelet call and finalize. Both sides pin -// bugs tracked in #2676: the suspend commits SUSPENDED over the crash, and -// the pause keeps the crash but returns OK for an actor left CRASHED. The fix -// changes the sync wantState to CRASHED and both codes to FailedPrecondition, -// replacing the err != nil fatal below. +// TestSuspendAndPause_FinalizeWithoutAssignment verifies that both workflows +// preserve an out-of-band crash and return FailedPrecondition when the crash +// clears the worker assignment between the atelet call and finalize's read. func TestSuspendAndPause_FinalizeWithoutAssignment(t *testing.T) { - wantState := map[string]ateapipb.ActorState{ - "sync": ateapipb.ActorState_ACTOR_STATE_SUSPENDED, - "async": ateapipb.ActorState_ACTOR_STATE_CRASHED, - } for _, mode := range suspendModes { t.Run(mode.name, func(t *testing.T) { ctx := context.Background() @@ -1343,21 +1335,29 @@ func TestSuspendAndPause_FinalizeWithoutAssignment(t *testing.T) { WorkerAssignment: wireTestAssignment(), }, }) + // What a worker delete or the syncer commits: CRASHED with the + // crash recorded and the assignment cleared, landing after the + // atelet call and before finalize re-reads the actor. + originalCrash := newActorCrash(ateattr.OperationDelete, "original crash reason") updateDuringCheckpoint(t, atelet, persistence, actorRef, func(s *ateapipb.ActorStatus) { s.State = ateapipb.ActorState_ACTOR_STATE_CRASHED + s.Crash = originalCrash s.WorkerAssignment = nil }) returned, err := mode.run(w, ctx, actorRef) - // #2676: both modes should return FailedPrecondition here. - if err != nil { - t.Fatalf("%s: %v", mode.name, err) + if got := apierror.Code(err); got != codes.FailedPrecondition { + t.Fatalf("%s: code = %v (err %v), want FailedPrecondition", mode.name, got, err) + } + if returned != nil { + t.Errorf("%s: returned actor = %v, want nil", mode.name, returned) } - if got := returned.GetStatus().GetState(); got != wantState[mode.name] { - t.Errorf("returned state = %v, want %v", got, wantState[mode.name]) + stored := mustReadActor(t, ctx, persistence, actorRef) + if got := stored.GetStatus().GetState(); got != ateapipb.ActorState_ACTOR_STATE_CRASHED { + t.Errorf("%s: stored state = %v, want CRASHED", mode.name, got) } - if got := mustReadActor(t, ctx, persistence, actorRef).GetStatus().GetState(); got != wantState[mode.name] { - t.Errorf("stored state = %v, want %v", got, wantState[mode.name]) + if got, want := stored.GetStatus().GetCrash().GetMessage(), originalCrash.GetMessage(); got != want { + t.Errorf("%s: crash message = %q, want %q kept", mode.name, got, want) } }) } diff --git a/internal/e2e/suites/demo/termination_test.go b/internal/e2e/suites/demo/termination_test.go index 763179cc96..369c258eb0 100644 --- a/internal/e2e/suites/demo/termination_test.go +++ b/internal/e2e/suites/demo/termination_test.go @@ -22,6 +22,8 @@ import ( "github.com/agent-substrate/substrate/internal/e2e" "github.com/agent-substrate/substrate/internal/resources" "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -142,7 +144,8 @@ func waitForWorkerRemoved(ctx context.Context, t *testing.T, clients *e2e.Client // TestGracefulWorkerTerminationSuspend exercises the case where a worker pod is // deleted (evicted), and while the container is in its SIGTERM shutdown phase, -// we initiate a suspend. Suspend should succeed. +// we initiate a suspend. Suspend either completes before worker deletion crashes +// the actor, or reports that concurrent crash without overwriting it. func TestGracefulWorkerTerminationSuspend(t *testing.T) { t.Parallel() nsObj := e2e.CreateNamespace(t) @@ -195,6 +198,10 @@ func TestGracefulWorkerTerminationSuspend(t *testing.T) { t.Fatalf("running actor has no bound worker pod: ns=%q name=%q", podNS, podName) } t.Logf("Actor %q bound to worker pod %s/%s", actorID, podNS, podName) + previousSnapshotURI := durableSnapshotURI(running.GetStatus()) + if previousSnapshotURI == "" { + t.Fatal("running actor has no completed durable snapshot") + } // Evict the worker pod. The kubelet sends SIGTERM to worker, which propagates // it into the sandbox; the container hangs for 30s. @@ -207,10 +214,20 @@ func TestGracefulWorkerTerminationSuspend(t *testing.T) { // Suspend the actor. t.Logf("Suspending actor %q during termination", actorID) - if _, err := clients.SubstrateAPI.SuspendActor(ctx, &ateapipb.SuspendActorRequest{ + _, suspendErr := clients.SubstrateAPI.SuspendActor(ctx, &ateapipb.SuspendActorRequest{ Actor: &ateapipb.ObjectRef{Atespace: demoAtespace, Name: actorID}, - }); err != nil { - t.Fatalf("failed to suspend Actor: %v", err) + }) + wantState := ateapipb.ActorState_ACTOR_STATE_SUSPENDED + switch status.Code(suspendErr) { + case codes.OK: + case codes.FailedPrecondition, codes.Aborted: + // Worker deletion can crash the actor before finalize reads it, or + // after that read but before its version-guarded update. Both must + // preserve the crash and report failure instead of returning success. + wantState = ateapipb.ActorState_ACTOR_STATE_CRASHED + t.Logf("Suspend reported concurrent worker deletion: %v", suspendErr) + default: + t.Fatalf("failed to suspend Actor: %v", suspendErr) } // The worker record must eventually be removed once the pod is gone. @@ -218,8 +235,8 @@ func TestGracefulWorkerTerminationSuspend(t *testing.T) { t.Fatalf("worker %s not removed after pod deletion: %v", podName, err) } - // Verify the actor lands in ACTOR_STATE_SUSPENDED - waitForActorState(ctx, t, clients, actorID, ateapipb.ActorState_ACTOR_STATE_SUSPENDED) + // The RPC result and persisted state must agree after the worker is gone. + waitForActorState(ctx, t, clients, actorID, wantState) // Verify the pod assignment was cleared. actor, err := clients.SubstrateAPI.GetActor(ctx, &ateapipb.GetActorRequest{ @@ -231,4 +248,15 @@ func TestGracefulWorkerTerminationSuspend(t *testing.T) { if pod := actor.GetStatus().GetWorkerAssignment().GetWorkerPod(); pod != "" { t.Errorf("actor still bound to worker pod %q, expected empty", pod) } + if got := durableSnapshotURI(actor.GetStatus()); got == "" { + t.Error("actor lost its completed durable snapshot") + } else if wantState == ateapipb.ActorState_ACTOR_STATE_CRASHED && got != previousSnapshotURI { + t.Errorf("crashed actor snapshot URI = %q, want previous snapshot %q", got, previousSnapshotURI) + } + if wantState == ateapipb.ActorState_ACTOR_STATE_CRASHED && actor.GetStatus().GetCrash().GetMessage() == "" { + t.Error("crashed actor lost its crash reason") + } + if wantState == ateapipb.ActorState_ACTOR_STATE_SUSPENDED && actor.GetStatus().GetCrash() != nil { + t.Errorf("successful suspend retained a crash record: %v", actor.GetStatus().GetCrash()) + } } From 321332eb735bdc1c8f76c237a668d2a280873ec8 Mon Sep 17 00:00:00 2001 From: Davanum Srinivas Date: Fri, 16 Oct 2026 01:59:20 +0000 Subject: [PATCH 24/31] imagecache: read mountinfo lines of any length; fix two tests for 64 KiB pages and root (#2624) Both unit tests failed on an arm64 host with 64 KiB pages where the test user is root, and neither failure pointed at the code under test. - `TestSetupBundleRootfs_ManyLayers` built 64 production-length lowerdir paths, about 8 KiB: more than a 4 KiB page, less than a 64 KiB one, so its own guard failed it. Each path is now padded to the 255 bytes one fsconfig(2) string value carries, and the layer count follows `os.Getpagesize()`: 17 layers on 4 KiB pages, 257 on 64 KiB pages. - A chain that long makes the overlay's `/proc/self/mountinfo` record longer than `bufio.Scanner`'s 64 KiB default token, so `UnmountAllUnder` failed with "token too long". `mountPointsIn` now reads lines with no length cap, with a regression test. - `TestGetFileToPublishedCopyIsWriteProtected` expects a write to a 0444 file to fail. Root bypasses file permissions, so the test skips as root, like the other permission-based tests. - [x] Tests pass: `go test -race ./internal/imagecache/... ./cmd/atelet/internal/filecache/...` and `golangci-lint` are clean; the root-gated overlay test needs Linux root and runs under `hack/run-root-tests.sh`, which CI does under sudo. - [x] Appropriate changes to documentation are included in the PR: none needed. --- .../internal/filecache/getfileto_test.go | 3 ++ internal/imagecache/bundle_linux_test.go | 20 ++++++-- internal/imagecache/mountinfo.go | 48 +++++++++++++++---- internal/imagecache/mountinfo_test.go | 45 +++++++++++++++++ 4 files changed, 103 insertions(+), 13 deletions(-) diff --git a/cmd/atelet/internal/filecache/getfileto_test.go b/cmd/atelet/internal/filecache/getfileto_test.go index 25c0d993d6..168641ed84 100644 --- a/cmd/atelet/internal/filecache/getfileto_test.go +++ b/cmd/atelet/internal/filecache/getfileto_test.go @@ -365,6 +365,9 @@ func TestGetFileToHitTouchesLastUse(t *testing.T) { } func TestGetFileToPublishedCopyIsWriteProtected(t *testing.T) { + if os.Geteuid() == 0 { + t.Skip("root ignores file permissions, so the write cannot fail") + } s := newTestStore(t) key := URIKey("gs://bucket/golden", "mem.img") fetch, _ := countingFetcher("precious") diff --git a/internal/imagecache/bundle_linux_test.go b/internal/imagecache/bundle_linux_test.go index 3d35206047..71c6c0b678 100644 --- a/internal/imagecache/bundle_linux_test.go +++ b/internal/imagecache/bundle_linux_test.go @@ -22,6 +22,7 @@ import ( "fmt" "os" "path/filepath" + "strings" "testing" "golang.org/x/sys/unix" @@ -254,18 +255,27 @@ func TestSetupBundleRootfs_MountAndUnmount(t *testing.T) { // limit. Needs CAP_SYS_ADMIN. func TestSetupBundleRootfs_ManyLayers(t *testing.T) { roottest.Require(t, "mount/unmount") - // Digest-length dir names so each path matches production length (~114 - // bytes); 64 of them comfortably exceed the page that motivated this. + // Overlayfs caps a chain at 500 lower layers, and production-length + // paths (~114 bytes) cannot outgrow a 64 KiB page within that cap. So + // each digest-named layer dir is padded until its path fills the 255 + // bytes one fsconfig(2) string value carries, and the layer count + // follows the page size: 17 layers on 4 KiB pages, 257 on 64 KiB pages. pool := filepath.Join(t.TempDir(), "sha256") - const n = 64 + const lowerLen = 255 + pad := lowerLen - len(filepath.Join(pool, strings.Repeat("0", 64), layerFSDirName)) + if pad < 0 { + t.Fatalf("temp dir %q is too long to pad a lowerdir path to %d bytes", pool, lowerLen) + } + pageSize := os.Getpagesize() + n := pageSize/(lowerLen+1) + 1 layers := make([]string, n) joined := 0 for i := range layers { - layers[i] = filepath.Join(pool, fmt.Sprintf("%064d", i)) + layers[i] = filepath.Join(pool, fmt.Sprintf("%064d", i)+strings.Repeat("p", pad)) writeLayer(t, layers[i], map[string]string{fmt.Sprintf("from-layer-%02d.txt", i): "x"}, nil) joined += len(layers[i]) + len("/fs") + 1 } - if pageSize := os.Getpagesize(); joined <= pageSize { + if joined <= pageSize { t.Fatalf("test layers join to %d bytes, not exceeding the %d-byte page this test guards against", joined, pageSize) } diff --git a/internal/imagecache/mountinfo.go b/internal/imagecache/mountinfo.go index 0f874fa5af..dca27a73f8 100644 --- a/internal/imagecache/mountinfo.go +++ b/internal/imagecache/mountinfo.go @@ -20,28 +20,45 @@ package imagecache import ( "bufio" + "bytes" "fmt" "io" + "math" "path/filepath" "strings" ) +// mountinfo octal-escapes space, tab, newline and backslash in paths (\040, +// \011, \012, \134); these decode and encode that form. +var ( + mountPathUnescaper = strings.NewReplacer(`\040`, " ", `\011`, "\t", `\012`, "\n", `\134`, `\`) + mountPathEscaper = strings.NewReplacer(" ", `\040`, "\t", `\011`, "\n", `\012`, `\`, `\134`) +) + // mountPointsIn returns the mount points at or below dir found in r, which // must be in /proc/[pid]/mountinfo format: // // ID parentID major:minor root MOUNTPOINT options... +// +// mountinfo can run to several MB on a dense worker and is read on every +// teardown, so the loop allocates only for matching lines. func mountPointsIn(r io.Reader, dir string) ([]string, error) { - dir = filepath.Clean(dir) + // Compare in mountinfo's escaped form, so only matches are decoded. The + // escaping is per character and leaves '/' alone, so a prefix match on + // the escaped path is a prefix match on the decoded one. + esc := []byte(mountPathEscaper.Replace(filepath.Clean(dir))) var points []string scanner := bufio.NewScanner(r) + // No line-length cap: an overlay's record lists every lowerdir path, and + // a long chain outgrows the 64 KiB default token size. + scanner.Buffer(nil, math.MaxInt) for scanner.Scan() { - fields := strings.Fields(scanner.Text()) - if len(fields) < 5 { + mp, ok := mountinfoField(scanner.Bytes(), 4) + if !ok || !bytes.HasPrefix(mp, esc) { continue } - mp := unescapeMountPath(fields[4]) - if mp == dir || strings.HasPrefix(mp, dir+"/") { - points = append(points, mp) + if rest := mp[len(esc):]; len(rest) == 0 || rest[0] == '/' { + points = append(points, unescapeMountPath(string(mp))) } } if err := scanner.Err(); err != nil { @@ -50,12 +67,27 @@ func mountPointsIn(r io.Reader, dir string) ([]string, error) { return points, nil } +// mountinfoField returns the nth (0-based) field of line. The kernel +// separates fields with a single space. +func mountinfoField(line []byte, n int) ([]byte, bool) { + for ; n > 0; n-- { + i := bytes.IndexByte(line, ' ') + if i < 0 { + return nil, false + } + line = line[i+1:] + } + if i := bytes.IndexByte(line, ' '); i >= 0 { + line = line[:i] + } + return line, len(line) > 0 +} + // unescapeMountPath decodes the octal escapes mountinfo uses for space, tab, // newline, and backslash. func unescapeMountPath(s string) string { if !strings.Contains(s, `\`) { return s } - r := strings.NewReplacer(`\040`, " ", `\011`, "\t", `\012`, "\n", `\134`, `\`) - return r.Replace(s) + return mountPathUnescaper.Replace(s) } diff --git a/internal/imagecache/mountinfo_test.go b/internal/imagecache/mountinfo_test.go index cc7dfa492c..42f2dd6be5 100644 --- a/internal/imagecache/mountinfo_test.go +++ b/internal/imagecache/mountinfo_test.go @@ -15,6 +15,7 @@ package imagecache import ( + "bufio" "slices" "strings" "testing" @@ -58,6 +59,50 @@ func TestMountPointsIn_ExactDirMatch(t *testing.T) { } } +// dir is compared in mountinfo's escaped form; a dir with a space must match +// its own mounts and not a longer sibling. +func TestMountPointsIn_EscapedDir(t *testing.T) { + mountinfo := strings.Join([]string{ + `40 21 8:1 / /mnt/odd\040name rw - ext4 /dev/sda1 rw`, + `41 21 8:1 / /mnt/odd\040name/rootfs rw - overlay overlay rw`, + `42 21 8:1 / /mnt/odd\040name2/rootfs rw - overlay overlay rw`, + ``, + }, "\n") + + got, err := mountPointsIn(strings.NewReader(mountinfo), "/mnt/odd name") + if err != nil { + t.Fatalf("mountPointsIn: %v", err) + } + want := []string{"/mnt/odd name", "/mnt/odd name/rootfs"} + if !slices.Equal(got, want) { + t.Errorf("mount points = %v, want %v", got, want) + } +} + +// An overlay's record lists every lowerdir path, so one line can be longer +// than bufio.Scanner's default token size; the lines after it must still be +// read. +func TestMountPointsIn_LongLine(t *testing.T) { + long := `40 21 8:1 / /mnt/target/rootfs rw - overlay overlay rw,lowerdir=` + strings.Repeat("/l", 40_000) + if len(long) <= bufio.MaxScanTokenSize { + t.Fatalf("test line is %d bytes, not exceeding bufio's %d-byte default", len(long), bufio.MaxScanTokenSize) + } + mountinfo := strings.Join([]string{ + long, + `41 21 8:1 / /mnt/target/volumes/agent rw - overlay overlay rw`, + ``, + }, "\n") + + got, err := mountPointsIn(strings.NewReader(mountinfo), "/mnt/target") + if err != nil { + t.Fatalf("mountPointsIn: %v", err) + } + want := []string{"/mnt/target/rootfs", "/mnt/target/volumes/agent"} + if !slices.Equal(got, want) { + t.Errorf("mount points = %v, want %v", got, want) + } +} + func TestUnescapeMountPath(t *testing.T) { tests := []struct{ in, want string }{ {`/plain/path`, `/plain/path`}, From 97de17b1ac9ce181426af52f4416282f214c267b Mon Sep 17 00:00:00 2001 From: Davanum Srinivas Date: Fri, 16 Oct 2026 02:27:12 +0000 Subject: [PATCH 25/31] atelet: make durable-dir volumes writable by non-root containers (#1906) Durable-dir volumes were created `0700`, so a container running as a non-root user could not write its own volume. Now `0777`, as Kubernetes creates an emptyDir: one actor's containers can run as different users, and the `0700` actor directory above keeps other actors out. A restore keeps the mode the snapshot recorded, as emptyDir keeps a pod's own chmod. Volumes created earlier stay `0700`, fine while every container runs as root. No effect until an image's `USER` is honored (#1918). Cleanup of files a non-root container leaves behind landed in #1910. Tests: `TestPrepareDurableDirVolume`. Exercised on kind with the micro-VM class: uid 65532 writes a durable dir, then suspend and resume. - [x] Tests pass - [x] Appropriate changes to documentation are included in the PR --- cmd/atelet/durabledir_test.go | 41 +++++++++++++++++++++++++++++++++++ cmd/atelet/main.go | 19 +++++++++++++--- docs/api-guide.md | 2 +- 3 files changed, 58 insertions(+), 4 deletions(-) create mode 100644 cmd/atelet/durabledir_test.go diff --git a/cmd/atelet/durabledir_test.go b/cmd/atelet/durabledir_test.go new file mode 100644 index 0000000000..90fce1215c --- /dev/null +++ b/cmd/atelet/durabledir_test.go @@ -0,0 +1,41 @@ +// 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 main + +import ( + "os" + "path/filepath" + "testing" +) + +// Chmod, not MkdirAll's mode: the umask narrows a fresh dir. +func TestPrepareDurableDirVolume(t *testing.T) { + path := filepath.Join(t.TempDir(), "vol") + if err := prepareDurableDirVolume(path); err != nil { + t.Fatal(err) + } + if got := dirMode(t, path); got != 0o777 { + t.Errorf("mode = %o, want 777", got) + } +} + +func dirMode(t *testing.T, path string) os.FileMode { + t.Helper() + fi, err := os.Stat(path) + if err != nil { + t.Fatal(err) + } + return fi.Mode().Perm() +} diff --git a/cmd/atelet/main.go b/cmd/atelet/main.go index 230a06b99d..2399a065f4 100644 --- a/cmd/atelet/main.go +++ b/cmd/atelet/main.go @@ -1594,6 +1594,20 @@ func (s *Atelet) downloadExternalCheckpoint(ctx context.Context, snapshotURI str return nil } +// prepareDurableDirVolume creates a durable-dir volume's host directory as +// 0777, as Kubernetes creates an emptyDir: one actor's containers can run as +// different users. Chmod, because MkdirAll applies the umask. A restore then +// re-applies the mode the snapshot recorded. +func prepareDurableDirVolume(path string) error { + if err := os.MkdirAll(path, 0o777); err != nil { + return fmt.Errorf("while creating %q: %w", path, err) + } + if err := os.Chmod(path, 0o777); err != nil { + return fmt.Errorf("while setting mode of %q: %w", path, err) + } + return nil +} + // prepareOCIBundles pulls images and assembles OCI bundles for the pause // container and every application container in spec, in parallel. pauseImage // comes from the sandbox record, not the workload spec: it is sandbox @@ -1611,9 +1625,8 @@ func (s *Atelet) prepareOCIBundles( // Prepare host folders for volume types that need them. for _, vol := range spec.GetVolumes() { 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) + if err := prepareDurableDirVolume(ateletpath.DurableDirVolumeMountPoint(actorUID, vol.GetName())); err != nil { + return nil, err } } } diff --git a/docs/api-guide.md b/docs/api-guide.md index e44c1e7c4f..70afb02311 100644 --- a/docs/api-guide.md +++ b/docs/api-guide.md @@ -102,7 +102,7 @@ The `ActorTemplate` defines the code, environment, and state-management policies | `sandboxConfig` | `SandboxConfig` | **Required.** The sandbox runtime selection: `sandboxClass` (**required**, `gvisor` or `microvm`) picks the runtime family this template's actors require — only `WorkerPool`s whose `sandboxClasses` entry matches are eligible — and `configName` (**required**) names the cluster-scoped [`SandboxConfig`](#3-sandboxconfig-the-sandbox-itself) object supplying the sandbox binaries. It must reference an existing config of the matching class; `CreateActorTemplate` rejects the template otherwise. | | `workerSelector` | `*Selector` | Optional. Gates which `WorkerPool`s actors from this template may use, by matching against each pool's labels (`matchLabels`). If unset, all pools are eligible (subject to the actor's own `worker_selector`). | | `snapshotConfig` | `SnapshotConfig` | **Required.** The base object-storage location snapshots are written under, the snapshot fidelity (`preferredFidelity`) every Pause and Suspend captures (`SNAPSHOT_FIDELITY_ROOTFS` requires `sandboxClass: microvm`), and automated golden snapshot creation (`goldenSnapshotConfig.mode`). See [Snapshot Storage Layout](#snapshot-storage-layout). | -| `volumes` | `[]Volume` | Optional. Volumes the containers may mount, each a `durableDir`, an `externalVolumeTemplate` (see [CSI Volumes Guide](csi-volumes.md)), or a `systemInfo` volume (see [SystemInfo Volumes](#systeminfo-volumes)). Every declared volume must be mounted by at least one container. A `microvm` template may declare several `durableDir` volumes; a `gvisor` template is limited to one. | +| `volumes` | `[]Volume` | Optional. Volumes the containers may mount, each a `durableDir`, an `externalVolumeTemplate` (see [CSI Volumes Guide](csi-volumes.md)), or a `systemInfo` volume (see [SystemInfo Volumes](#systeminfo-volumes)). Every declared volume must be mounted by at least one container. A `microvm` template may declare several `durableDir` volumes; a `gvisor` template is limited to one. A `durableDir` is created `0777`, as Kubernetes creates an emptyDir, so a container running as any user can write it. A snapshot restores it with the mode it had, so the container's own chmod survives suspend and resume. | | `resources` | `*ResourceRequirements` | Required. Declares each actor's compute size via `limits`, which must set both `cpu` and `memory` — see [Sandbox Right-Sizing](#sandbox-right-sizing-resources). Immutable, like the rest of the template. | The sandbox itself — the binaries (e.g. the gVisor `runsc` binary) and the `pauseImage` holding the sandbox's namespaces — comes from the cluster-scoped [`SandboxConfig`](#3-sandboxconfig-the-sandbox-itself) object the template names via `sandboxConfig.configName`. An actor always resolves the config from its current template — repointing the actor at another template requires the same config. From 5d7a89070fabee769656d23877353b29c98777b8 Mon Sep 17 00:00:00 2001 From: Nishanth Kotla <86459764+Nishanth29@users.noreply.github.com> Date: Fri, 16 Oct 2026 02:55:05 +0000 Subject: [PATCH 26/31] benchmarking: record Kubernetes version, worker capacity and run metadata in trial_summary (#2232) Part of #1590 Adds facts to each run's `trial_summary` row so runs can be compared and traced later, even after the cluster is deleted. **New keys** - `kubernetes_version`: the worker nodes' Kubernetes version, as the kubelet reports it. - `worker_actor_capacity`, `worker_memory_limit_gb`, `sandbox_class`: what each worker pod declares (ateworker `--max-actors`, the container memory limit, and gvisor or microvm from the worker image). Each value is read on its own, so an unreadable one is skipped (and logged once) without dropping the others. - `multi_actor_worker`: `true` when `actors_per_pod_p50` > 1. - `run_start`, `run_end`: Unix seconds bracketing the run, for finding its server-side metrics afterwards. - `metadata_`: one per key of a new free-form `--metadata ''` runner flag, for facts only the caller knows. **Orchestrator** - Passes `--metadata` to locust Jobs with the target cluster's `cluster_name`, `cluster_location` and `project_id`, merged with the `BENCHMARK_METADATA` env var (a JSON object), if it's set. - A test's own `--metadata` in `tests.yaml` is merged in too, its keys winning on conflict, so the runner gets a single `--metadata`. - Fails at startup if `BENCHMARK_METADATA` or a test's `--metadata` is not a valid JSON object. - Nighthawk Jobs are unchanged (that runner has no `--metadata`). **Example** (from a test run): ``` "kubernetes_version": "v1.37.1-gke.1506000", "worker_actor_capacity": "1000", "worker_memory_limit_gb": null, "sandbox_class": "gvisor", "multi_actor_worker": "true", "run_start": "1791245420", "run_end": "1791246036", "metadata_cluster_name": "", "metadata_cluster_location": "us-east1-b", "metadata_project_id": "", "metadata_test": "manual", ``` - [x] Tests pass - [x] Appropriate changes to documentation are included in the PR --- benchmarking/README.md | 20 ++- benchmarking/automation/README.md | 4 + benchmarking/automation/orchestrator.py | 71 ++++++++- benchmarking/automation/test_orchestrator.py | 84 ++++++++++ benchmarking/locust/cluster_facts.py | 124 ++++++++++++++- benchmarking/locust/runner.py | 24 ++- .../locust/unit_tests/test_cluster_facts.py | 150 +++++++++++++++++- 7 files changed, 462 insertions(+), 15 deletions(-) diff --git a/benchmarking/README.md b/benchmarking/README.md index 851d15939f..5d5b449d02 100644 --- a/benchmarking/README.md +++ b/benchmarking/README.md @@ -594,6 +594,22 @@ map holds the raw facts and the derived numbers side by side. whole cluster, so a separate infrastructure pool is not counted. They are recorded so the ratios below can be re-derived later, or recomputed against a different denominator. +* `kubernetes_version`: the worker nodes' Kubernetes version (kubelet version). +* `worker_actor_capacity`, `worker_memory_limit_gb` (GiB): the capacity each + worker pod declares, its ateworker `--max-actors` (1000 when unset) and its + container memory limit (null when unset, as the node bounds it instead). + `sandbox_class`: `gvisor` or `microvm`, from the worker image. A mixed pool + gives a sorted comma-joined list for each. An unreadable value is skipped + without affecting the others, and logged once. +* `multi_actor_worker`: `true` when `actors_per_pod_p50` is above 1. +* `run_start`, `run_end`: Unix seconds bracketing the run, for finding its + server-side metrics after the cluster is deleted. +* `metadata_`: one per key of the free-form `--metadata ''` object, + for facts only the caller knows. Non-string values are stored as JSON, and + null values are skipped. The orchestrator passes `cluster_name`, + `cluster_location` and `project_id` from the target cluster, plus any keys in + the `BENCHMARK_METADATA` environment variable (a JSON object) and in the + test's own `--metadata` flag, whose keys win on conflict. * `actors_per_node`, `actors_per_vcpu`, `actors_per_gb_ram`: the most actors Locust reported running, over the matching capacity. The `-u` flag only stands in when no sample was read. @@ -619,7 +635,9 @@ comparing numbers across runs: count rather than a resident one. * **The denominators are read once, after the run.** A cluster that autoscaled mid-run is measured at its final size, so the ratio pairs a peak from one - moment with a capacity from another. + moment with a capacity from another. The same holds for every worker and + node fact above, including `kubernetes_version` and the worker capacity, and + so for `multi_actor_worker`, which is derived from `actors_per_pod`. * **The peak assumes every actor is alive at once.** A workload that creates and deletes actors as it goes never holds them all at the same time, so its real density is lower than reported. diff --git a/benchmarking/automation/README.md b/benchmarking/automation/README.md index 9a152df4f0..f5b64c9fe8 100644 --- a/benchmarking/automation/README.md +++ b/benchmarking/automation/README.md @@ -38,6 +38,10 @@ the router capacity benchmark — see - Submits a Job using the just-built image for the test's type (`runner-job.yaml.tmpl` for locust, `nighthawk-ingress-runner-job.yaml.tmpl` for nighthawk-ingress). + A locust Job also gets `--metadata` with the target cluster's name, + location and project, merged with the `BENCHMARK_METADATA` environment + variable (a JSON object) if set. A `--metadata` in the test's own + `flags` is merged in too, its keys winning on conflict. - Polls until complete/failed/timeout; tails logs; deletes the Job. - Tears down workloads + micro-VM deps (if any) + substrate. - If not the last test, redeploys them so the next run starts clean. diff --git a/benchmarking/automation/orchestrator.py b/benchmarking/automation/orchestrator.py index 473d0d2910..a9fac23d77 100644 --- a/benchmarking/automation/orchestrator.py +++ b/benchmarking/automation/orchestrator.py @@ -185,6 +185,63 @@ def test_type(test: dict[str, Any]) -> str: return test["type"] +def _json_object(raw: str, what: str) -> dict[str, Any]: + """Parses raw as a JSON object, naming `what` in the error otherwise.""" + try: + value = json.loads(raw) + except json.JSONDecodeError as e: + raise ValueError(f"{what} is not valid JSON: {e}") from e + if not isinstance(value, dict): + raise ValueError(f"{what} must be a JSON object") + return value + + +def run_metadata() -> dict[str, Any]: + """Free-form run facts handed to the runner as --metadata. + + Read per test, after apply_target_cluster has reset os.environ; + BENCHMARK_METADATA survives that reset because it is part of the startup + env (_ORIG_ENV). + """ + md: dict[str, Any] = { + k.lower(): os.environ[k] + for k in ("CLUSTER_NAME", "CLUSTER_LOCATION", "PROJECT_ID") + if os.environ.get(k) + } + raw = os.environ.get("BENCHMARK_METADATA") + if raw: + md.update(_json_object(raw, "BENCHMARK_METADATA")) + return md + + +def split_metadata_flag(test: dict[str, Any]) -> tuple[list[Any], dict[str, Any]]: + """Separates a test's own --metadata from the rest of its flags. + + Accepts `--metadata ` and `--metadata=`. With more than one, + later keys win. Raises ValueError when the value is missing or is not a + JSON object, rather than dropping it. + """ + name = test.get("name") + rest: list[Any] = [] + md: dict[str, Any] = {} + flags = list(test.get("flags", [])) + i = 0 + while i < len(flags): + flag = str(flags[i]) + if flag == "--metadata": + if i + 1 >= len(flags): + raise ValueError(f"test {name!r}: --metadata needs a JSON value") + raw, i = str(flags[i + 1]), i + 2 + elif flag.startswith("--metadata="): + raw, i = flag.split("=", 1)[1], i + 1 + else: + rest.append(flags[i]) + i += 1 + continue + md.update(_json_object(raw, f"test {name!r}: --metadata")) + return rest, md + + def wait_for_docker(timeout: int = 120) -> None: print("Waiting for DIND sidecar...", flush=True) start = time.time() @@ -326,6 +383,9 @@ def validate_and_normalize_tests(tests: list[dict[str, Any]]) -> None: f"(want one of {list(TEST_TYPES)})" ) TYPES[ttype].validate(t) + if ttype == "locust": + # A malformed --metadata fails here rather than mid-sweep. + split_metadata_flag(t) isolate_block(t) @@ -422,7 +482,14 @@ def run_test( mod = TYPES[test_type(test)] tmpl = mod.job_tmpl(manifests_dir) subs.update(mod.job_subs(test)) - manifest = render_template(tmpl, subs, test.get("flags", [])) + flags = list(test.get("flags", [])) + # Only the locust runner knows --metadata. It keeps only the last one it + # is given, so a test's own --metadata is folded into a single merged + # flag; the test's keys win over the run's. + if test_type(test) == "locust": + flags, own = split_metadata_flag(test) + flags += ["--metadata", json.dumps({**run_metadata(), **own})] + manifest = render_template(tmpl, subs, flags) wait_for_no_active_runners() print(f"Submitting Job {job}", flush=True) subprocess.run( @@ -504,6 +571,8 @@ def main() -> None: print(f"Running {len(tests)} test(s)", flush=True) try: validate_and_normalize_tests(tests) + # Checks BENCHMARK_METADATA only; cluster vars may not be set yet. + run_metadata() except ValueError as e: sys.exit(str(e)) diff --git a/benchmarking/automation/test_orchestrator.py b/benchmarking/automation/test_orchestrator.py index 8d334dd532..569b69388e 100644 --- a/benchmarking/automation/test_orchestrator.py +++ b/benchmarking/automation/test_orchestrator.py @@ -14,6 +14,7 @@ """Unit tests for orchestrator.py: python3 benchmarking/automation/test_orchestrator.py""" +import json import os import unittest from unittest import mock @@ -202,5 +203,88 @@ def test_two_runs_of_the_same_long_test_differ(self): ) +CLUSTER_ENV = {"CLUSTER_NAME": "c", "CLUSTER_LOCATION": "l", "PROJECT_ID": "p"} + + +class RunMetadataTest(unittest.TestCase): + def test_cluster_env(self): + with mock.patch.dict(os.environ, CLUSTER_ENV, clear=True): + self.assertEqual( + orchestrator.run_metadata(), + {"cluster_name": "c", "cluster_location": "l", "project_id": "p"}, + ) + + def test_benchmark_metadata_is_merged(self): + env = {**CLUSTER_ENV, "BENCHMARK_METADATA": '{"build_id": "1"}'} + with mock.patch.dict(os.environ, env, clear=True): + self.assertEqual(orchestrator.run_metadata()["build_id"], "1") + + def test_bad_benchmark_metadata(self): + for bad in ("not json", "[1]", '"text"'): + env = {**CLUSTER_ENV, "BENCHMARK_METADATA": bad} + with self.subTest(bad=bad), mock.patch.dict(os.environ, env, clear=True), \ + self.assertRaisesRegex(ValueError, "BENCHMARK_METADATA"): + orchestrator.run_metadata() + + def submitted_args(self, ttype, flags=("--x",)): + """The extra args run_test hands to render_template for a test type.""" + + class Rendered(Exception): + pass + + fake = mock.Mock(job_tmpl=mock.Mock(return_value="t"), job_subs=mock.Mock(return_value={})) + test = {"name": "n", "type": ttype, "duration": "1m", "flags": list(flags)} + with mock.patch.dict(orchestrator.TYPES, {ttype: fake}), \ + mock.patch.dict(os.environ, CLUSTER_ENV, clear=True), \ + mock.patch.object(orchestrator, "render_template", side_effect=Rendered) as render, \ + self.assertRaises(Rendered): + orchestrator.run_test(test, "img", "dest", "abcdef0123") + return render.call_args.args[2] + + def test_locust_job_gets_metadata(self): + args = self.submitted_args("locust") + self.assertEqual(args[:2], ["--x", "--metadata"]) + self.assertEqual(json.loads(args[2])["cluster_name"], "c") + + def test_other_types_do_not(self): + self.assertEqual(self.submitted_args("nighthawk-ingress"), ["--x"]) + + def test_test_metadata_is_merged_not_overridden(self): + for own in (["--metadata", '{"cluster_name": "mine", "suite": "s"}'], + ['--metadata={"cluster_name": "mine", "suite": "s"}']): + with self.subTest(own=own): + args = self.submitted_args("locust", ["--x", *own, "--y"]) + # One merged --metadata, after the test's other flags. + self.assertEqual(args.count("--metadata"), 1) + self.assertFalse([a for a in args if a.startswith("--metadata=")]) + self.assertEqual(args[:3], ["--x", "--y", "--metadata"]) + md = json.loads(args[3]) + # Test keys win; run keys the test does not set are kept. + self.assertEqual(md["cluster_name"], "mine") + self.assertEqual(md["suite"], "s") + self.assertEqual(md["project_id"], "p") + + def test_bad_test_metadata_fails_clearly(self): + for own in (["--metadata", "not json"], ["--metadata=[1]"], ["--metadata"]): + with self.subTest(own=own), \ + self.assertRaisesRegex(ValueError, "test 'n': --metadata"): + orchestrator.split_metadata_flag({"name": "n", "flags": own}) + + # Caught by validation, before any cluster work. + test = {"name": "n", "type": "locust", "targetCluster": "t", + "file": "f", "duration": "1m", "users": 1, + "flags": ["--metadata", "{bad"]} + with self.assertRaisesRegex(ValueError, "not valid JSON"): + orchestrator.validate_and_normalize_tests([test]) + + def test_metadata_survives_yaml(self): + md = json.dumps({"cluster_name": "c: d", "q": "\"'"}) + subs = {"JOB_NAME": "j", "IMAGE": "i", "TAG": "t", "NAME": "n", "DEST": "d"} + subs.update(locust.job_subs({"file": "f", "duration": "1m", "users": 1})) + text = orchestrator.render_template(RunnerSizingTest.TMPL, subs, ["--metadata", md]) + job = next(d for d in yaml.safe_load_all(text) if d and d.get("kind") == "Job") + self.assertEqual(job["spec"]["template"]["spec"]["containers"][0]["args"][-1], md) + + if __name__ == "__main__": unittest.main() diff --git a/benchmarking/locust/cluster_facts.py b/benchmarking/locust/cluster_facts.py index 449b31f8ee..8bb03d9a82 100644 --- a/benchmarking/locust/cluster_facts.py +++ b/benchmarking/locust/cluster_facts.py @@ -17,7 +17,9 @@ Reads allocatable CPU/RAM, node count and worker pod count from the Kubernetes API, then derives the actor-density frontiers (actors per node / vCPU / GB RAM -and the actors-per-pod percentiles) for a completed trial. +and the actors-per-pod percentiles) for a completed trial. Also records the +nodes' Kubernetes version, the worker pods' declared capacity and sandbox +class, the run window, and any caller-supplied --metadata. """ import argparse @@ -36,15 +38,26 @@ LIVE_POD_PHASES = ("Running", "Pending") MACHINE_TYPE_LABEL = "node.kubernetes.io/instance-type" +# The worker pod's container, as named by the atecontroller. +WORKER_CONTAINER = "worker" +# ateworker's own --max-actors default (cmd/ateworker-gvisor, cmd/ateworker-microvm). +ATEWORKER_DEFAULT_MAX_ACTORS = 1000 +# The worker image is ateworker- (benchmarking/automation/README.md). +SANDBOX_CLASS_IMAGE = re.compile(r"ateworker-(gvisor|microvm)") + # Shape returned when the cluster cannot be read, or when discovery is skipped # with --no-cluster-facts. Keeping one definition means a trial_summary row has -# the same hardware keys either way, so consumers never have to special-case it. +# the same fact keys either way, so consumers never have to special-case it. EMPTY_FACTS: dict[str, Any] = { "machine_type": None, "node_count": None, "allocatable_cores": None, "allocatable_ram_gb": None, "worker_pod_count": None, + "kubernetes_version": None, + "worker_actor_capacity": None, + "worker_memory_limit_gb": None, + "sandbox_class": None, } @@ -101,8 +114,80 @@ def _list_worker_pods( return None +def _joined(values: set[Any]) -> str | None: + """A single value, or a mixed pool's values sorted and comma-joined.""" + return ",".join(str(v) for v in sorted(values)) or None + + +def _max_actors(args: list[str]) -> int | None: + """The --max-actors value on an ateworker command line, or ateworker's default. + + Returns None when the flag is present but its value is not an integer. + """ + for i, arg in enumerate(args): + value = None + if arg.startswith("--max-actors="): + value = arg.split("=", 1)[1] + elif arg == "--max-actors" and i + 1 < len(args): + value = args[i + 1] + if value is not None: + try: + return int(value) + except ValueError: + return None + return ATEWORKER_DEFAULT_MAX_ACTORS + + +def _worker_container_facts( + pods: list[Any], logs: TextIO | None = None +) -> dict[str, str | None]: + """Reads the capacity and sandbox class each worker pod declares. + + A worker reports its capacity as --max-actors and its container memory + limit. With no memory limit set the worker is bounded by its node instead, + so the limit stays None. + + Each fact is read on its own, so a malformed value skips only that value + and leaves the other facts intact. + """ + capacities: set[int] = set() + memory_gb: set[float] = set() + sandbox_classes: set[str] = set() + # Logged once rather than per pod, so a large pool does not flood the log. + unreadable: set[str] = set() + for p in pods: + for c in p.spec.containers or []: + if c.name != WORKER_CONTAINER: + continue + capacity = _max_actors(c.args or []) + if capacity is None: + unreadable.add("--max-actors") + else: + capacities.add(capacity) + limits = (c.resources.limits if c.resources else None) or {} + if "memory" in limits: + try: + # GiB, the same as allocatable_ram_gb. + memory_gb.add(round( + int(parse_quantity(limits["memory"])) / (1024**3), 2)) + except ValueError: + unreadable.add(f"memory limit {limits['memory']!r}") + match = SANDBOX_CLASS_IMAGE.search(c.image or "") + if match: + sandbox_classes.add(match.group(1)) + if unreadable: + _log(logs, "Notice: skipped unreadable worker container values: " + + ", ".join(sorted(unreadable))) + return { + "worker_actor_capacity": _joined(capacities), + "worker_memory_limit_gb": _joined(memory_gb), + "sandbox_class": _joined(sandbox_classes), + } + + def get_cluster_hardware_facts(logs: TextIO | None = None) -> dict[str, Any]: - """Reads the worker pool size and the capacity of the nodes it runs on. + """Reads the worker pool size, the capacity and Kubernetes version of the + nodes it runs on, and what each worker pod declares. Capacity is scoped to the nodes carrying worker pods, so a cluster that keeps its infrastructure on a separate pool does not count that pool's @@ -126,6 +211,12 @@ def get_cluster_hardware_facts(logs: TextIO | None = None) -> dict[str, Any]: facts["worker_pod_count"] = len(pods) + # Malformed values are skipped inside; this guards an unexpected pod shape. + try: + facts.update(_worker_container_facts(pods, logs)) + except Exception as e: + _log(logs, f"Notice: could not read worker container specs: {e}") + try: # A Pending pod may not be scheduled yet, so it counts toward the pool # size without contributing a node. @@ -139,6 +230,7 @@ def get_cluster_hardware_facts(logs: TextIO | None = None) -> dict[str, Any]: total_cores = 0.0 total_ram_bytes = 0 machine_types = set() + versions = set() for node in nodes: metadata = node.metadata if metadata is None or metadata.name not in worker_nodes: @@ -150,13 +242,17 @@ def get_cluster_hardware_facts(logs: TextIO | None = None) -> dict[str, Any]: machine_type = (metadata.labels or {}).get(MACHINE_TYPE_LABEL) if machine_type: machine_types.add(machine_type) + if node.status.node_info and node.status.node_info.kubelet_version: + versions.add(node.status.node_info.kubelet_version) facts["node_count"] = node_count facts["allocatable_cores"] = round(total_cores, 2) # GiB, as the apiserver and kubectl quote it. facts["allocatable_ram_gb"] = round(total_ram_bytes / (1024**3), 2) # Kept so results stay comparable across hardware changes. A mixed pool # is a sorted comma-joined list rather than one node picked at random. - facts["machine_type"] = ",".join(sorted(machine_types)) or None + facts["machine_type"] = _joined(machine_types) + # The worker nodes' Kubernetes version, e.g. v1.33.5-gke.1080000. + facts["kubernetes_version"] = _joined(versions) except Exception as e: reason = getattr(e, "reason", e) _log(logs, f"Notice: could not read node capacity: {reason}") @@ -172,6 +268,8 @@ def append_trial_summary( data_ts: str, facts: dict[str, Any], logs: TextIO | None = None, + run_start: int | None = None, + run_end: int | None = None, ) -> None: # Locust's own User Count samples. The -u flag is a request; under a custom # load shape what actually ran is whatever the shape asked for. @@ -249,8 +347,26 @@ def append_trial_summary( else: _log(logs, f"Notice: {stats_csv} not found; failure ratios unknown") + # Whether workers hosted more than one actor at once (a multi-actor + # worker), going by the median actors per pod. + multi_actor_worker = ( + None if actors_per_pod_p50 is None + else str(actors_per_pod_p50 > 1).lower() + ) + measurements = { **{k: facts.get(k) for k in EMPTY_FACTS}, + # Unix seconds bracketing the run, for finding its server-side metrics + # in Cloud Monitoring. + "run_start": run_start, + "run_end": run_end, + # Caller-supplied --metadata, one key each so values stay plain strings. + **{ + f"metadata_{k}": v if isinstance(v, str) else json.dumps(v) + for k, v in (getattr(args, "metadata", None) or {}).items() + if v is not None + }, + "multi_actor_worker": multi_actor_worker, "actors_per_node": actors_per_node, "actors_per_vcpu": actors_per_vcpu, "actors_per_gb_ram": actors_per_gb_ram, diff --git a/benchmarking/locust/runner.py b/benchmarking/locust/runner.py index 42cdcc8ddb..5f94e78dc2 100644 --- a/benchmarking/locust/runner.py +++ b/benchmarking/locust/runner.py @@ -80,6 +80,17 @@ ) +def json_object(value: str) -> dict[str, Any]: + """argparse type for --metadata: a JSON object, checked before the run.""" + try: + obj = json.loads(value) + except json.JSONDecodeError as e: + raise argparse.ArgumentTypeError(f"not valid JSON: {e}") from e + if not isinstance(obj, dict): + raise argparse.ArgumentTypeError("must be a JSON object") + return obj + + def parse_args() -> argparse.Namespace: p = argparse.ArgumentParser(description=__doc__) p.add_argument("-f", required=True, dest="file", help="Locust test file (-f)") @@ -147,6 +158,15 @@ def parse_args() -> argparse.Namespace: "--no-cluster-facts to skip Kubernetes API discovery" ), ) + p.add_argument( + "--metadata", + type=json_object, + default={}, + help=( + "Free-form JSON object describing the run, such as where it ran. " + "Each key is recorded in trial_summary as metadata_" + ), + ) p.add_argument( "--prometheus-url", default=DEFAULT_PROMETHEUS_URL, @@ -490,7 +510,7 @@ def upload(src: Path, dest: str) -> None: def collect_cluster_facts( args: argparse.Namespace, logs: TextIO ) -> dict[str, Any]: - """Returns cluster hardware facts, or empty facts when discovery is off.""" + """Returns cluster facts, or empty facts when discovery is off.""" if not args.cluster_facts: tee(logs, "Skipping cluster hardware discovery (--no-cluster-facts)") return dict(EMPTY_FACTS) @@ -576,6 +596,8 @@ def main() -> None: data_ts, facts, logs, + run_start=run_ts, + run_end=run_end_ts, ) except Exception as e: tee(logs, f"Warning: Failed to record cluster facts: {e}") diff --git a/benchmarking/locust/unit_tests/test_cluster_facts.py b/benchmarking/locust/unit_tests/test_cluster_facts.py index 21ef770291..713748b009 100644 --- a/benchmarking/locust/unit_tests/test_cluster_facts.py +++ b/benchmarking/locust/unit_tests/test_cluster_facts.py @@ -51,17 +51,30 @@ "--tag", "unit", "--name", "unit-run", "--dest", "/tmp/unit"] +KUBERNETES_VERSION = "v1.33.5-gke.1080000" + + def node(machine_type="c3-standard-4", name="node-a"): labels = {} if machine_type is None else { cluster_facts.MACHINE_TYPE_LABEL: machine_type} return SimpleNamespace( metadata=SimpleNamespace(labels=labels, name=name), - status=SimpleNamespace(allocatable={"cpu": NODE_CPU, "memory": NODE_MEMORY})) + status=SimpleNamespace( + allocatable={"cpu": NODE_CPU, "memory": NODE_MEMORY}, + node_info=SimpleNamespace(kubelet_version=KUBERNETES_VERSION))) + +def container(name="worker", args=(), memory=None, + image="registry/ateworker-gvisor-0123abcd@sha256:00"): + limits = None if memory is None else {"memory": memory} + return SimpleNamespace(name=name, args=list(args), image=image, + resources=SimpleNamespace(limits=limits)) -def pod(phase="Running", node_name="node-a"): - return SimpleNamespace(spec=SimpleNamespace(node_name=node_name), - status=SimpleNamespace(phase=phase)) + +def pod(phase="Running", node_name="node-a", containers=()): + return SimpleNamespace( + spec=SimpleNamespace(node_name=node_name, containers=list(containers)), + status=SimpleNamespace(phase=phase)) def fake_api(nodes=None, ns_pods=None): @@ -86,7 +99,8 @@ def discover(api): def summarize(facts, directory, stats=STATS_HEADER + ",Aggregated,100,25\n", - users=10, user_counts=None, actors_per_user=None): + users=10, user_counts=None, actors_per_user=None, + run_start=None, run_end=None, metadata=None): """Writes CSV inputs and returns the emitted trial_summary row.""" d = Path(directory) if stats is not None: @@ -102,8 +116,9 @@ def summarize(facts, directory, stats=STATS_HEADER + ",Aggregated,100,25\n", cluster_facts.append_trial_summary( out, d / "stats.csv", d / "stats_history.csv", argparse.Namespace(users=users, tag="unit", name="unit-run", - actors_per_user=actors_per_user), - "2026-01-01", facts) + actors_per_user=actors_per_user, + metadata=metadata), + "2026-01-01", facts, run_start=run_start, run_end=run_end) return json.loads(out.read_text().splitlines()[0]) @@ -195,8 +210,127 @@ def tripwire(*_args, **_kwargs): with mock.patch.object(runner, "get_cluster_hardware_facts", return_value={"node_count": 1}) as discovery, \ contextlib.redirect_stdout(io.StringIO()): - runner.collect_cluster_facts(parse(), io.StringIO()) + facts = runner.collect_cluster_facts(parse(), io.StringIO()) discovery.assert_called_once() + self.assertEqual(facts, {"node_count": 1}) + + def test_worker_container_facts(self): + facts = discover(fake_api(nodes=[node()], ns_pods=[ + pod(containers=[container(args=["--pod-uid=x", "--max-actors=50"], + memory="16Gi"), + container(name="sidecar", args=["--max-actors=7"])]), + pod(containers=[container(args=["--max-actors", "50"], + memory="17179869184")]), + ])) + self.assertEqual(facts["worker_actor_capacity"], "50") + self.assertEqual(facts["worker_memory_limit_gb"], "16.0") # GiB + self.assertEqual(facts["sandbox_class"], "gvisor") + self.assertEqual(facts["kubernetes_version"], KUBERNETES_VERSION) + + # Unset flag is ateworker's default; no memory limit is unmeasured; a mixed + # pool lists every value. + facts = discover(fake_api(nodes=[node()], ns_pods=[ + pod(containers=[container()]), + pod(containers=[container( + args=["--max-actors=1"], + image="registry/ateworker-microvm-4567@sha256:00")]), + ])) + self.assertEqual(facts["worker_actor_capacity"], "1,1000") + self.assertIsNone(facts["worker_memory_limit_gb"]) + self.assertEqual(facts["sandbox_class"], "gvisor,microvm") + + # No worker pods -> nothing to read. + facts = discover(fake_api(nodes=[node()], ns_pods=[])) + self.assertIsNone(facts["worker_actor_capacity"]) + self.assertIsNone(facts["sandbox_class"]) + + def test_worker_facts_fail_independently(self): + # A bad --max-actors still yields the memory limit, sandbox class and + # node capacity. + for args in (["--max-actors=lots"], ["--max-actors", "lots"]): + with self.subTest(args=args): + facts = discover(fake_api(nodes=[node()], ns_pods=[ + pod(containers=[container(args=args, memory="16Gi")])])) + self.assertIsNone(facts["worker_actor_capacity"]) + self.assertEqual(facts["worker_memory_limit_gb"], "16.0") + self.assertEqual(facts["sandbox_class"], "gvisor") + self.assertEqual(facts["node_count"], 1) + + # A bad memory quantity still yields the capacity and sandbox class. + facts = discover(fake_api(nodes=[node()], ns_pods=[ + pod(containers=[container(args=["--max-actors=50"], + memory="lots")])])) + self.assertEqual(facts["worker_actor_capacity"], "50") + self.assertIsNone(facts["worker_memory_limit_gb"]) + self.assertEqual(facts["sandbox_class"], "gvisor") + + # Only the bad pod's value is skipped; a healthy pod still counts. + facts = discover(fake_api(nodes=[node()], ns_pods=[ + pod(containers=[container(args=["--max-actors=lots"], + memory="lots")]), + pod(containers=[container(args=["--max-actors=50"], + memory="16Gi")])])) + self.assertEqual(facts["worker_actor_capacity"], "50") + self.assertEqual(facts["worker_memory_limit_gb"], "16.0") + + def test_unreadable_worker_values_are_logged_once(self): + logs = io.StringIO() + bad = pod(containers=[container(args=["--max-actors=lots"], + memory="lots")]) + with contextlib.redirect_stdout(io.StringIO()): + cluster_facts._worker_container_facts([bad, bad, bad], logs) + notices = logs.getvalue().splitlines() + self.assertEqual(len(notices), 1) + self.assertIn("--max-actors", notices[0]) + self.assertIn("'lots'", notices[0]) + + def test_metadata_flag(self): + self.assertEqual(parse().metadata, {}) + got = parse("--metadata", '{"cluster_name": "c", "nodes": 3}') + self.assertEqual(got.metadata, {"cluster_name": "c", "nodes": 3}) + self.assertNotIn("--metadata", got.locust_extra) + + # Rejected before the run starts, not after it. + for bad in ("not json", "[1, 2]", '"text"'): + with self.subTest(bad=bad), \ + contextlib.redirect_stderr(io.StringIO()), \ + self.assertRaises(SystemExit): + parse("--metadata", bad) + + def test_metadata_in_trial_summary(self): + md = {"cluster_name": "sweperf-c3hm176nested", "nodes": 3, + "labels": {"a": 1}, "unset": None} + with tempfile.TemporaryDirectory() as td: + m = summarize(FACTS, td, metadata=md)["measurements"] + self.assertEqual(m["metadata_cluster_name"], "sweperf-c3hm176nested") + # Non-strings are stored as JSON so BigQuery can parse them. + self.assertEqual(m["metadata_nodes"], "3") + self.assertEqual(json.loads(m["metadata_labels"]), {"a": 1}) + self.assertNotIn("metadata_unset", m) + + # No metadata, no metadata_* keys. + with tempfile.TemporaryDirectory() as td: + m = summarize(FACTS, td)["measurements"] + self.assertFalse([k for k in m if k.startswith("metadata_")]) + + def test_run_window_and_multi_actor_worker(self): + facts = {**FACTS, "worker_actor_capacity": 1000} + with tempfile.TemporaryDirectory() as td: + m = summarize(facts, td, run_start=1788914580, + run_end=1788915180)["measurements"] + self.assertEqual(m["run_start"], "1788914580") + self.assertEqual(m["run_end"], "1788915180") + self.assertEqual(m["worker_actor_capacity"], "1000") + self.assertEqual(m["multi_actor_worker"], "true") # 2 actors per pod + + # One actor per pod is not multi-actor; no pod count is unknown. + with tempfile.TemporaryDirectory() as td: + m = summarize({**FACTS, "worker_pod_count": 10}, td)["measurements"] + self.assertEqual(m["multi_actor_worker"], "false") # 10 users / 10 pods + self.assertIsNone(m["run_start"]) + with tempfile.TemporaryDirectory() as td: + m = summarize(dict(cluster_facts.EMPTY_FACTS), td)["measurements"] + self.assertIsNone(m["multi_actor_worker"]) def test_trial_summary(self): with tempfile.TemporaryDirectory() as td: From 2b3a40632dd89e92438642d9baf6a704196d7db7 Mon Sep 17 00:00:00 2001 From: Zoe Zhao <35669350+zoez7@users.noreply.github.com> Date: Fri, 16 Oct 2026 03:46:07 +0000 Subject: [PATCH 27/31] Merge the pause workflow into the suspend workflow (#2694) This is the next step toward [async suspend](https://github.com/zoez7/substrate/blob/async-suspend-design/docs/dev/async-suspend-en.md), following #2688. `SuspendActor` and `PauseActor` now run one set of shared steps behind a `suspendMode`: - `suspendSync` (`SuspendActor`): the snapshot is durable before the workflow returns. - `suspendAsync` (`PauseActor`): the snapshot stays on the worker's node. Both APIs stay for now. `workflow_pause.go` is removed. Behavior is unchanged. The tests #2688 moved testing of ensurePaused* and ensureSuspended* functions onto `PauseActor` and `SuspendActor`. The only test edit is in `workflow_test.go`, which called the removed `ensurePausedFinalized`. The fix for #2676 (finalize not checking that the actor is still `SUSPENDING`) will come in a separate PR. - [x] Tests pass - [ ] Appropriate changes to documentation are included in the PR --- .../internal/controlapi/workflow_pause.go | 300 ------------------ .../controlapi/workflow_pause_test.go | 5 + .../internal/controlapi/workflow_suspend.go | 225 +++++++++---- .../internal/controlapi/workflow_test.go | 15 +- 4 files changed, 170 insertions(+), 375 deletions(-) delete mode 100644 cmd/ateapi/internal/controlapi/workflow_pause.go diff --git a/cmd/ateapi/internal/controlapi/workflow_pause.go b/cmd/ateapi/internal/controlapi/workflow_pause.go deleted file mode 100644 index 9949f76475..0000000000 --- a/cmd/ateapi/internal/controlapi/workflow_pause.go +++ /dev/null @@ -1,300 +0,0 @@ -// Copyright 2026 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package controlapi - -import ( - "context" - "errors" - "fmt" - "log/slog" - "time" - - "github.com/agent-substrate/substrate/cmd/ateapi/internal/store" - "github.com/agent-substrate/substrate/internal/apierror" - "github.com/agent-substrate/substrate/internal/ateattr" - "github.com/agent-substrate/substrate/internal/preview" - "github.com/agent-substrate/substrate/internal/proto/ateletpb" - "github.com/agent-substrate/substrate/internal/resources" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" - "go.opentelemetry.io/otel/attribute" - "google.golang.org/protobuf/proto" -) - -// PauseActor executes the workflow to pause a running actor. Idempotent: -// a re-entered workflow fast-forwards past the steps a previous attempt -// completed, deriving progress from the persisted actor alone. -func (w *ActorWorkflow) PauseActor(ctx context.Context, actorRef resources.ActorRef) (_ *ateapipb.Actor, err error) { - ctx = preview.ContextWithOptIn(ctx) - start := time.Now() - var actor *ateapipb.Actor - var actorTemplate *ateapipb.ActorTemplate - var wireFidelity string - // Set just before finalize; nil until then, so earlier exits label - // themselves from the record they hold. - var finalAttrs []attribute.KeyValue - - defer func() { - attrs := finalAttrs - if attrs == nil { - attrs = lifecycleOpAttrs(actor, actorTemplate, "", wireFidelity) - } - w.instruments.recordLifecycleOp(ctx, ateattr.OperationSuspend, start, err, attrs...) - }() - - leaseCtx, lease, err := w.acquireActorLease(ctx, actorRef) - if err != nil { - return nil, err - } - defer lease.Close() - - actor, actorTemplate, err = w.loadActorForPause(leaseCtx, actorRef) - if err != nil { - return nil, err - } - if actor.GetStatus().GetState() == ateapipb.ActorState_ACTOR_STATE_SUSPENDED && latestSnapshotIsLocalOnly(actor.GetStatus()) { - // Fully paused already: FinalizePaused commits SUSPENDED, the local - // snapshot's locality, and the cleared worker assignment in a single - // update, so there is nothing left to do. - // This success reports no pool, and cannot: the previous attempt - // released the worker, so the record names none (#957). - return actor, nil - } - var marked *ateapipb.Actor - if marked, err = w.ensureMarkedPausing(leaseCtx, actorRef, actor, actorTemplate); err != nil { - return nil, err - } - actor = marked - if wireFidelity, err = w.ensureAteletPaused(leaseCtx, actorRef, actor, actorTemplate); err != nil { - return nil, err - } - // TODO: There is no difference between suspend and pause for now, but we - // could optimize pause by not detaching. We would need to make sure Resume - // is idempotent. - if err = w.ensureVolumesDetached(leaseCtx, actor, actorTemplate, "DetachVolumesForPause", ateattr.OperationSuspend); err != nil { - return nil, err - } - // FinalizePaused clears the WorkerAssignment the labels read, so snapshot - // them here, as crash.go does for the crash counter. - finalAttrs = lifecycleOpAttrs(actor, actorTemplate, "", wireFidelity) - var finalized *ateapipb.Actor - if finalized, err = w.ensurePausedFinalized(leaseCtx, actorRef); err != nil { - return nil, err - } - actor = finalized - return actor, nil -} - -// loadActorForPause fetches the current actor record and its template. -func (w *ActorWorkflow) loadActorForPause(ctx context.Context, actorRef resources.ActorRef) (_ *ateapipb.Actor, _ *ateapipb.ActorTemplate, err error) { - ctx, done := stepSpan(ctx, "LoadActorForPause") - defer func() { err = done(err) }() - - actor, err := w.store.GetActor(ctx, actorRef) - if err != nil { - return nil, nil, err - } - actorTemplate, err := resolveActorTemplate(ctx, w.store, actor) - if err != nil { - return nil, nil, err - } - return actor, actorTemplate, nil -} - -// ensureMarkedPausing transitions a RUNNING actor to SUSPENDING, minting the -// local snapshot name. A pause in progress is a SUSPENDING actor whose latest -// generation is a pending node-local snapshot; a suspend in progress has a -// pending durable one instead. Skips when a previous attempt already marked -// the actor; the persisted name then stays authoritative for the rest of the -// workflow. -func (w *ActorWorkflow) ensureMarkedPausing(ctx context.Context, actorRef resources.ActorRef, actor *ateapipb.Actor, actorTemplate *ateapipb.ActorTemplate) (_ *ateapipb.Actor, err error) { - ctx, done := stepSpan(ctx, "MarkPausing") - defer func() { err = done(err) }() - - if actor.GetStatus().GetState() == ateapipb.ActorState_ACTOR_STATE_SUSPENDING && latestSnapshotIsPendingLocal(actor.GetStatus()) { - markSkipped(ctx, "actor already SUSPENDING for a pause") - return actor, nil - } - // The pause edge only exists from RUNNING. A SUSPENDING actor that is not - // pausing is suspending, and that suspend owns it. - if actor.GetStatus().GetState() != ateapipb.ActorState_ACTOR_STATE_RUNNING { - return nil, apierror.FailedPrecondition("MarkPausing prerequisite not met for Actor: %s (got: %v, want %s)", actorRef, actor.GetStatus().GetState(), ateapipb.ActorState_ACTOR_STATE_RUNNING) - } - // By design a golden actor cannot be paused — it can only be suspended - // (committed). - if actorRef.Atespace == resources.GoldenActorAtespace { - return nil, apierror.FailedPrecondition("actors in atespace %q are golden actors, which cannot be paused", actorRef.Atespace) - } - - snapshotUUID := resources.NewSnapshotName() - storedActor, err := w.store.UpdateActor(ctx, actorRef, store.PreconditionFrom(actor), func(toUpdate *ateapipb.Actor) error { - toUpdate.Status.State = ateapipb.ActorState_ACTOR_STATE_SUSPENDING - // Increment last_assigned_generation for the new Pause request. - gen := toUpdate.Status.LastAssignedGeneration + 1 - toUpdate.Status.LastAssignedGeneration = gen - snap := newLocalSnapshot( - gen, - actorTemplate.GetSnapshotConfig().GetPreferredFidelity(), - actorTemplate.GetMetadata().GetUid(), - snapshotUUID, - "", - ) - snap.SandboxRuntime = proto.CloneOf(toUpdate.GetStatus().GetWorkerAssignment().GetSandboxRuntime()) - toUpdate.Status.Snapshots = append(toUpdate.Status.Snapshots, snap) - return nil - }) - if err != nil { - if errors.Is(err, store.ErrVersionConflict) { - return nil, apierror.Aborted("concurrent update conflict, please retry") - } - return nil, err - } - logActorStateChanged(ctx, storedActor, ateattr.OperationSuspend) - return storedActor, nil -} - -// ensureAteletPaused checkpoints the workload locally on the worker node -// under the actor's persisted snapshot name. This is the atelet reentrancy -// seam (#372): the request is keyed by the actor UID, the worker pod UID, and -// the once-minted snapshot name, so a re-entered workflow re-sends the same -// semantic request; once atelet's Checkpoint is idempotent on those keys this -// step becomes fully reentrant with no changes here. -func (w *ActorWorkflow) ensureAteletPaused(ctx context.Context, actorRef resources.ActorRef, actor *ateapipb.Actor, actorTemplate *ateapipb.ActorTemplate) (wireFidelity string, err error) { - ctx, done := stepSpan(ctx, "CallAteletPause") - defer func() { err = done(err) }() - - assignment := actor.GetStatus().GetWorkerAssignment() - if assignment == nil { - // Missing active worker pod reference mid-pause indicates corrupted store state. - return "", crashActorAndFail(ctx, w.store, actorRef, ateattr.OperationSuspend, crashMessageWorkerAssignmentMissing) - } - - ateletConn, err := w.dialer.DialForAteletOnNode(assignment.GetNodeName()) - if err != nil { - return "", fmt.Errorf("while getting atelet conn for node %q: %w", assignment.GetNodeName(), err) - } - client := ateletpb.NewAteletClient(ateletConn) - - workloadSpec, err := workloadSpecFromActorTemplate(actorTemplate, actor, nil) - if err != nil { - return "", err - } - - // Checkpoint does not carry the sandbox config: atelet uses the version the - // actor is currently running (recorded on-node at Run/Restore) and pins it - // into the snapshot manifest. - inProgressSnap := snapshotAtLatestGeneration(actor.GetStatus()) - req := &ateletpb.CheckpointRequest{ - WorkerPodUid: assignment.GetWorkerPodUid(), - Atespace: actor.GetMetadata().GetAtespace(), - ActorName: actor.GetMetadata().GetName(), - ActorTemplateAtespace: actor.GetActorTemplate().GetAtespace(), - ActorTemplateName: actor.GetActorTemplate().GetName(), - Spec: workloadSpec, - Snapshot: &ateletpb.Snapshot{ - SnapshotUuid: inProgressSnap.GetUuid(), - Generation: inProgressSnap.GetGeneration(), - Fidelity: fidelityToAtelet(actorTemplate.GetSnapshotConfig().GetPreferredFidelity()), - }, - UploadConfig: &ateletpb.UploadConfig{ - Mode: ateletpb.UploadMode_UPLOAD_MODE_DEFERRED, - }, - ActorUid: actor.GetMetadata().Uid, - } - wireFidelity = ateattr.SnapshotFidelityValue(req.GetSnapshot().GetFidelity()) - - if _, err = client.Checkpoint(ctx, req); err != nil { - return wireFidelity, handleAteletError(ctx, w.store, actorRef, ateattr.OperationSuspend, "Checkpoint", false, err) - } - return wireFidelity, nil -} - -// ensurePausedFinalized releases the actor's worker (only when it is still -// owned by this actor), records where the local snapshot lives, and commits -// SUSPENDED with the assignment cleared in a single update. The actor keeps -// its assigned node: a paused actor is a SUSPENDED actor whose latest -// snapshot is node-local, and it resumes on that node. When the worker's -// node name was lost or the latest local snapshot entry is missing, the actor -// can never be resumed from local state, so it commits CRASHED instead and -// fails the step with FailedPrecondition. It re-reads the actor first so an -// out-of-band transition (e.g. the syncer crashing the actor after its worker -// died) is not overwritten: an actor that is no longer SUSPENDING is left as -// it is and the step fails with FailedPrecondition. -func (w *ActorWorkflow) ensurePausedFinalized(ctx context.Context, actorRef resources.ActorRef) (_ *ateapipb.Actor, err error) { - ctx, done := stepSpan(ctx, "FinalizePaused") - defer func() { err = done(err) }() - - latestActor, err := w.store.GetActor(ctx, actorRef) - if err != nil { - return nil, err - } - if state := latestActor.GetStatus().GetState(); state != ateapipb.ActorState_ACTOR_STATE_SUSPENDING { - return nil, apierror.FailedPrecondition("FinalizePaused prerequisite not met for Actor: %s (got: %v, want %s)", actorRef, state, ateapipb.ActorState_ACTOR_STATE_SUSPENDING) - } - - // 1. Free the worker (if it hasn't been freed yet) - if assignment := latestActor.GetStatus().GetWorkerAssignment(); assignment != nil { - if latestActor.GetStatus().GetAssignedNode() == "" { - // Without a node name we cannot record where the local snapshot lives, - // so the actor can never be resumed (the scheduler would search for a - // worker on an unknown node forever). Crash it instead of committing - // SUSPENDED with a snapshot nothing can find. - return nil, crashActorAndFail(ctx, w.store, actorRef, ateattr.OperationSuspend, crashMessageLocalSnapshotNodeUnknown) - } - if snapshotAtLatestGeneration(latestActor.GetStatus()) == nil { - return nil, crashActorAndFail(ctx, w.store, actorRef, ateattr.OperationSuspend, crashMessageLocalSnapshotMissing) - } - - worker, err := w.store.GetWorker(ctx, assignment.GetWorker().GetName()) - if err != nil { - if !errors.Is(err, store.ErrNotFound) { - return nil, fmt.Errorf("while getting worker for release: %w", err) - } - slog.Warn("Worker already gone during finalize pause, skipping release", "worker", assignment.GetWorkerPod()) - } else { - // Drop just this actor's assignment; any other actors the worker - // hosts keep theirs. - _, err := w.store.ReleaseActorFromWorker(ctx, worker.GetMetadata().GetName(), latestActor.GetMetadata().GetUid()) - if err != nil { - if errors.Is(err, store.ErrVersionConflict) { - return nil, apierror.Aborted("concurrent update conflict, please retry") - } - return nil, err - } - } - - // 2. Clear the actor's assignment, now that the worker is freed. The - // precondition pins the version checked above, so an out-of-band write - // since then fails the update instead of being overwritten. - storedActor, err := w.store.UpdateActor(ctx, actorRef, store.PreconditionFrom(latestActor), func(toUpdate *ateapipb.Actor) error { - toUpdate.Status.State = ateapipb.ActorState_ACTOR_STATE_SUSPENDED - // Record the node locality of the local checkpoint and clear older local checkpoints. - snap := snapshotAtLatestGeneration(toUpdate.Status) - snap.Locality = toUpdate.GetStatus().GetAssignedNode() - pruneSnapshots(toUpdate.Status, snap) - toUpdate.Status.WorkerAssignment = nil - return nil - }) - if err != nil { - if errors.Is(err, store.ErrVersionConflict) { - return nil, apierror.Aborted("concurrent update conflict, please retry") - } - return nil, err - } - logActorStateChanged(ctx, storedActor, ateattr.OperationSuspend) - latestActor = storedActor - } - - return latestActor, nil -} diff --git a/cmd/ateapi/internal/controlapi/workflow_pause_test.go b/cmd/ateapi/internal/controlapi/workflow_pause_test.go index 89743a9fa5..5965d224b8 100644 --- a/cmd/ateapi/internal/controlapi/workflow_pause_test.go +++ b/cmd/ateapi/internal/controlapi/workflow_pause_test.go @@ -17,6 +17,7 @@ package controlapi import ( "context" "fmt" + "strings" "testing" "github.com/agent-substrate/substrate/cmd/ateapi/internal/store/storetest" @@ -408,6 +409,10 @@ func TestPauseActor_MarkedPausing_StateMatrix(t *testing.T) { if got := apierror.Code(err); got != codes.FailedPrecondition { t.Errorf("state %v: code = %v, want FailedPrecondition (err: %v)", seedState, got, err) } + // Only a sync suspend accepts a paused actor, so a pause must not offer it. + if strings.Contains(err.Error(), "node-local snapshot") { + t.Errorf("state %v: err = %v, want no mention of a node-local snapshot", seedState, err) + } if got := stored.GetStatus().GetState(); got != seedState { t.Errorf("state %v: stored state = %v, want it untouched", seedState, got) } diff --git a/cmd/ateapi/internal/controlapi/workflow_suspend.go b/cmd/ateapi/internal/controlapi/workflow_suspend.go index f8ae72fa36..4dfdb02d0b 100644 --- a/cmd/ateapi/internal/controlapi/workflow_suspend.go +++ b/cmd/ateapi/internal/controlapi/workflow_suspend.go @@ -32,13 +32,50 @@ import ( "google.golang.org/protobuf/proto" ) +// suspendMode selects which of the two suspends the suspend workflow runs. +// Both take the same steps; the mode only decides the shape of the snapshot +// they take. +type suspendMode int + +const ( + // suspendSync makes the snapshot durable before the workflow returns: + // the actor ends SUSPENDED holding a durable snapshot (SuspendActor). + suspendSync suspendMode = iota + // suspendAsync leaves the snapshot on the worker's node, to be uploaded + // later: the actor ends SUSPENDED holding a node-local snapshot and + // pinned to that node (PauseActor). Nothing uploads it yet. + suspendAsync +) + +// uploadMode is when atelet makes the checkpoint durable. +func (m suspendMode) uploadMode() ateletpb.UploadMode { + if m == suspendAsync { + return ateletpb.UploadMode_UPLOAD_MODE_DEFERRED + } + return ateletpb.UploadMode_UPLOAD_MODE_IMMEDIATE +} + // SuspendActor executes the workflow to suspend a running or paused actor: // a running actor is checkpointed on its worker, a paused actor's node-local // snapshot is uploaded. Idempotent: a re-entered workflow fast-forwards past // the steps a previous attempt completed, deriving progress from the // persisted actor alone. -func (w *ActorWorkflow) SuspendActor(ctx context.Context, actorRef resources.ActorRef) (_ *ateapipb.Actor, err error) { +func (w *ActorWorkflow) SuspendActor(ctx context.Context, actorRef resources.ActorRef) (*ateapipb.Actor, error) { + ctx = preview.ContextWithOptIn(ctx) + return w.suspendActor(ctx, actorRef, suspendSync) +} + +// PauseActor executes the workflow to pause a running actor: it is +// checkpointed on its worker and the snapshot is kept on that node. +// Idempotent: a re-entered workflow fast-forwards past the steps a previous +// attempt completed, deriving progress from the persisted actor alone. +func (w *ActorWorkflow) PauseActor(ctx context.Context, actorRef resources.ActorRef) (*ateapipb.Actor, error) { ctx = preview.ContextWithOptIn(ctx) + return w.suspendActor(ctx, actorRef, suspendAsync) +} + +// suspendActor runs the suspend workflow behind SuspendActor and PauseActor. +func (w *ActorWorkflow) suspendActor(ctx context.Context, actorRef resources.ActorRef, mode suspendMode) (_ *ateapipb.Actor, err error) { start := time.Now() var actor *ateapipb.Actor var actorTemplate *ateapipb.ActorTemplate @@ -65,31 +102,39 @@ func (w *ActorWorkflow) SuspendActor(ctx context.Context, actorRef resources.Act if err != nil { return nil, err } - if actor.GetStatus().GetState() == ateapipb.ActorState_ACTOR_STATE_SUSPENDED && !latestSnapshotIsLocalOnly(actor.GetStatus()) { - // Fully suspended already: FinalizeSuspended commits SUSPENDED and the - // cleared worker assignment in a single update, so there is nothing - // left to do. A SUSPENDED actor whose latest snapshot is node-local is - // paused, not suspended, and goes on to upload it. This success - // reports no pool, and cannot: the previous attempt released the - // worker, so the record names none (#957). - return actor, nil + // FinalizeSuspended commits SUSPENDED and the cleared worker assignment in + // a single update, so an actor already holding the snapshot this mode + // leaves behind has nothing left to do. These successes report no pool, + // and cannot: the previous attempt released the worker, so the record + // names none (#957). + if actor.GetStatus().GetState() == ateapipb.ActorState_ACTOR_STATE_SUSPENDED { + if mode == suspendAsync && latestSnapshotIsLocalOnly(actor.GetStatus()) { + return actor, nil // fully paused + } + if mode == suspendSync && !latestSnapshotIsLocalOnly(actor.GetStatus()) { + return actor, nil // fully suspended + } + // A sync suspend of a paused actor goes on to upload its node-local + // snapshot. An async suspend of a suspended actor is rejected by MarkSuspending. } // Decided before marking: once SUSPENDING is committed, the loaded status // alone can no longer tell the two origins apart. - fromPaused := isPausedOriginSuspend(actor) + // TODO(paused-suspend): remove the paused -> suspended transition before GA. + fromPaused := mode == suspendSync && isPausedOriginSuspend(actor) var marked *ateapipb.Actor - if marked, err = w.ensureMarkedSuspending(leaseCtx, actorRef, actor, actorTemplate); err != nil { + if marked, err = w.ensureMarkedSuspending(leaseCtx, actorRef, actor, actorTemplate, mode); err != nil { return nil, err } actor = marked if fromPaused { wireFidelity, err = w.ensurePausedSnapshotUploaded(leaseCtx, actorRef, actor, actorTemplate) } else { - wireFidelity, err = w.ensureAteletSuspended(leaseCtx, actorRef, actor, actorTemplate) + wireFidelity, err = w.ensureAteletSuspended(leaseCtx, actorRef, actor, actorTemplate, mode) } if err != nil { return nil, err } + // TODO(#2734): An async suspend should skip detaching here, since the actor can resume on the same node. if err = w.ensureVolumesDetached(leaseCtx, actor, actorTemplate, "DetachVolumes", ateattr.OperationSuspend); err != nil { return nil, err } @@ -97,7 +142,7 @@ func (w *ActorWorkflow) SuspendActor(ctx context.Context, actorRef resources.Act // them here, as crash.go does for the crash counter. finalAttrs = lifecycleOpAttrs(actor, actorTemplate, "", wireFidelity) var finalized *ateapipb.Actor - if finalized, err = w.ensureSuspendedFinalized(leaseCtx, actorRef); err != nil { + if finalized, err = w.ensureSuspendedFinalized(leaseCtx, actorRef, mode); err != nil { return nil, err } actor = finalized @@ -121,28 +166,44 @@ func (w *ActorWorkflow) loadActorForSuspend(ctx context.Context, actorRef resour } // ensureMarkedSuspending transitions a RUNNING or paused actor to -// SUSPENDING, minting the in-progress snapshot location and recording the -// actor version the snapshot will capture. Skips when a previous attempt -// already marked the actor; the persisted location and source version then -// stay authoritative for the rest of the workflow. -func (w *ActorWorkflow) ensureMarkedSuspending(ctx context.Context, actorRef resources.ActorRef, actor *ateapipb.Actor, actorTemplate *ateapipb.ActorTemplate) (_ *ateapipb.Actor, err error) { +// SUSPENDING, minting the in-progress snapshot and recording the actor +// version the snapshot will capture. A sync suspend mints a pending durable +// snapshot; an async one mints a node-local snapshot with no locality yet. +// Skips when a previous attempt of the same mode already marked the actor; +// the persisted snapshot and source version then stay authoritative for the +// rest of the workflow. A mark left by the other mode is rejected: that +// workflow owns the actor until it finishes. +func (w *ActorWorkflow) ensureMarkedSuspending(ctx context.Context, actorRef resources.ActorRef, actor *ateapipb.Actor, actorTemplate *ateapipb.ActorTemplate, mode suspendMode) (_ *ateapipb.Actor, err error) { ctx, done := stepSpan(ctx, "MarkSuspending") defer func() { err = done(err) }() if actor.GetStatus().GetState() == ateapipb.ActorState_ACTOR_STATE_SUSPENDING { - if latestSnapshotIsPendingLocal(actor.GetStatus()) { - // The pause workflow owns the actor until it finishes; a suspend - // can run once the actor is paused. - return nil, apierror.FailedPrecondition("MarkSuspending prerequisite not met for Actor: %s (a pause is in progress)", actorRef) + // An async mark's latest snapshot is node-local with no locality yet, + // a sync mark's has a pending durable copy. + if latestSnapshotIsPendingLocal(actor.GetStatus()) != (mode == suspendAsync) { + inProgress := "Pause" + if mode == suspendAsync { + inProgress = "Suspend" + } + return nil, apierror.FailedPrecondition("MarkSuspending prerequisite not met for Actor: %s (a previous %s is in progress)", actorRef, inProgress) } markSkipped(ctx, "actor already SUSPENDING") return actor, nil } state := actor.GetStatus().GetState() - paused := isPausedOriginSuspend(actor) + // Only a sync suspend takes a paused actor. + paused := mode == suspendSync && isPausedOriginSuspend(actor) // TODO(paused-suspend) if state != ateapipb.ActorState_ACTOR_STATE_RUNNING && !paused { + if mode == suspendAsync { + return nil, apierror.FailedPrecondition("MarkSuspending prerequisite not met for Actor: %s (got: %v, want %s)", actorRef, state, ateapipb.ActorState_ACTOR_STATE_RUNNING) + } return nil, apierror.FailedPrecondition("MarkSuspending prerequisite not met for Actor: %s (got: %v, want %s or %s with a node-local snapshot)", actorRef, state, ateapipb.ActorState_ACTOR_STATE_RUNNING, ateapipb.ActorState_ACTOR_STATE_SUSPENDED) } + // Golden actors are only suspended, never paused. + // TODO: Remove this check: the suspend workflow should not special-case golden actors. + if mode == suspendAsync && actorRef.Atespace == resources.GoldenActorAtespace { + return nil, apierror.FailedPrecondition("actors in atespace %q are golden actors, which cannot be paused", actorRef.Atespace) + } // A paused actor's durable snapshot is an upload of its local checkpoint, // so it keeps that checkpoint's name; a running actor's is a new one. var snapshotName string @@ -154,10 +215,14 @@ func (w *ActorWorkflow) ensureMarkedSuspending(ctx context.Context, actorRef res } // Fail here rather than at checkpoint time if the template's location - // cannot produce a usable URI: nothing has been written yet. - uri, err := newInProgressSnapshotURI(actorTemplate, actor, snapshotName) - if err != nil { - return nil, err + // cannot produce a usable URI: nothing has been written yet. An async + // suspend keeps its snapshot on the node, so it needs no URI. + var uri resources.SnapshotURI + if mode == suspendSync { + // TODO: Always populate the in-progress URI. + if uri, err = newInProgressSnapshotURI(actorTemplate, actor, snapshotName); err != nil { + return nil, err + } } storedActor, err := w.store.UpdateActor(ctx, actorRef, store.PreconditionFrom(actor), func(toUpdate *ateapipb.Actor) error { toUpdate.Status.State = ateapipb.ActorState_ACTOR_STATE_SUSPENDING @@ -176,18 +241,25 @@ func (w *ActorWorkflow) ensureMarkedSuspending(ctx context.Context, actorRef res return nil } // Increment last_assigned_generation for the new Suspend request and create - // a pending snapshot for that generation. + // a pending snapshot for that generation: durable for a sync suspend, + // node-local with no locality until finalize for an async one. gen := toUpdate.Status.LastAssignedGeneration + 1 toUpdate.Status.LastAssignedGeneration = gen - snap := newDurableSnapshot( - gen, - ateapipb.SnapshotOwner_SNAPSHOT_OWNER_ACTOR, - preferredFidelity(actorRef.Atespace, actorTemplate), - actorTemplate.GetMetadata().GetUid(), - uri.Name(), - uri.String(), - ateapipb.SnapshotStorageStatus_SNAPSHOT_STORAGE_STATUS_PENDING, - ) + + fidelity := preferredFidelity(actorRef.Atespace, actorTemplate) + templateUID := actorTemplate.GetMetadata().GetUid() + snap := newLocalSnapshot(gen, fidelity, templateUID, snapshotName, "") + if mode == suspendSync { + snap = newDurableSnapshot( + gen, + ateapipb.SnapshotOwner_SNAPSHOT_OWNER_ACTOR, + fidelity, + templateUID, + uri.Name(), + uri.String(), + ateapipb.SnapshotStorageStatus_SNAPSHOT_STORAGE_STATUS_PENDING, + ) + } snap.SandboxRuntime = proto.CloneOf(toUpdate.GetStatus().GetWorkerAssignment().GetSandboxRuntime()) toUpdate.Status.Snapshots = append(toUpdate.Status.Snapshots, snap) return nil @@ -213,6 +285,7 @@ func preferredFidelity(atespace string, tmpl *ateapipb.ActorTemplate) ateapipb.S return tmpl.GetSnapshotConfig().GetPreferredFidelity() } +// TODO(paused-suspend): remove with the paused -> suspended transition. // isPausedOriginSuspend reports whether the suspend must upload a paused // actor's node-local snapshot instead of checkpointing a running workload. // A local snapshot alone does not mean paused-origin: resume restores from it @@ -232,12 +305,14 @@ func isPausedOriginSuspend(actor *ateapipb.Actor) bool { } // ensureAteletSuspended checkpoints the workload to the actor's persisted -// in-progress snapshot location. This is the atelet reentrancy seam (#372): +// in-progress snapshot location. A sync suspend has atelet upload it before +// returning; an async suspend has atelet keep it on the node. +// This is the atelet reentrancy seam (#372): // the request is keyed by the actor UID, the worker pod UID, and the // once-minted snapshot location, so a re-entered workflow re-sends the same // semantic request; once atelet's Checkpoint is idempotent on those keys this // step becomes fully reentrant with no changes here. -func (w *ActorWorkflow) ensureAteletSuspended(ctx context.Context, actorRef resources.ActorRef, actor *ateapipb.Actor, actorTemplate *ateapipb.ActorTemplate) (wireFidelity string, err error) { +func (w *ActorWorkflow) ensureAteletSuspended(ctx context.Context, actorRef resources.ActorRef, actor *ateapipb.Actor, actorTemplate *ateapipb.ActorTemplate, mode suspendMode) (wireFidelity string, err error) { ctx, done := stepSpan(ctx, "CallAteletSuspend") defer func() { err = done(err) }() @@ -263,6 +338,11 @@ func (w *ActorWorkflow) ensureAteletSuspended(ctx context.Context, actorRef reso // into the snapshot manifest. inProgressSnap := snapshotAtLatestGeneration(actor.GetStatus()) fidelity := fidelityToAtelet(preferredFidelity(actor.GetMetadata().GetAtespace(), actorTemplate)) + // An async suspend's snapshot has no durable copy, so it stays on the node. + var object *ateletpb.ObjectStorage + if uri := inProgressSnap.GetDurableSnapshot().GetObject().GetSnapshotUri(); uri != "" { + object = &ateletpb.ObjectStorage{SnapshotUri: uri} + } req := &ateletpb.CheckpointRequest{ WorkerPodUid: assignment.GetWorkerPodUid(), Atespace: actor.GetMetadata().GetAtespace(), @@ -274,16 +354,10 @@ func (w *ActorWorkflow) ensureAteletSuspended(ctx context.Context, actorRef reso SnapshotUuid: inProgressSnap.GetUuid(), Generation: inProgressSnap.GetGeneration(), Fidelity: fidelity, - Object: &ateletpb.ObjectStorage{ - SnapshotUri: inProgressSnap.GetDurableSnapshot().GetObject().GetSnapshotUri(), - }, + Object: object, }, - // FinalizeSuspended marks this snapshot COMPLETED, so it must be - // durable before Checkpoint returns. IMMEDIATE temporarily keeps no - // local copy either (see UPLOAD_MODE_IMMEDIATE), which matches - // FinalizeSuspended dropping the actor's local snapshots. UploadConfig: &ateletpb.UploadConfig{ - Mode: ateletpb.UploadMode_UPLOAD_MODE_IMMEDIATE, + Mode: mode.uploadMode(), }, ActorUid: actor.GetMetadata().Uid, } @@ -295,6 +369,7 @@ func (w *ActorWorkflow) ensureAteletSuspended(ctx context.Context, actorRef reso return wireFidelity, nil } +// TODO(paused-suspend): remove with the paused -> suspended transition. // ensurePausedSnapshotUploaded suspends a paused actor by telling the atelet // on the node holding the local pause snapshot to upload it to the actor's // persisted in-progress snapshot location; no workload runs, so there is no @@ -311,8 +386,8 @@ func (w *ActorWorkflow) ensurePausedSnapshotUploaded(ctx context.Context, actorR } nodeName := actor.GetStatus().GetAssignedNode() if nodeName == "" { - // Without the node the snapshot can never be found (mirrors - // FinalizePaused, which crashes rather than record an unknown node). + // Without the node the snapshot can never be found (mirrors an async + // FinalizeSuspended, which crashes rather than record an unknown node). return "", crashActorAndFail(ctx, w.store, actorRef, ateattr.OperationSuspend, crashMessageLocalSnapshotNodeUnknown) } @@ -363,7 +438,7 @@ func newInProgressSnapshotURI(actorTemplate *ateapipb.ActorTemplate, actor *atea // ensureVolumesDetached detaches the actor's mounted external volumes from // its worker node. Detachment is idempotent, so a re-entered workflow safely -// runs it again. spanName distinguishes the suspend and pause steps in +// runs it again. spanName distinguishes the suspend and revert steps in // traces; op labels the volume metrics. // TODO replace re-execution with a proper check on the volumes' attach state. func (w *ActorWorkflow) ensureVolumesDetached(ctx context.Context, actor *ateapipb.Actor, actorTemplate *ateapipb.ActorTemplate, spanName, op string) (err error) { @@ -380,15 +455,16 @@ func (w *ActorWorkflow) ensureVolumesDetached(ctx context.Context, actor *ateapi // ensureSuspendedFinalized releases the actor's worker (only when it is still // owned by this actor), records the in-progress snapshot as the actor's -// durable snapshot, and commits SUSPENDED with the assignment cleared in a -// single update, then releases the durable snapshot that update replaced. -// When the latest snapshot has no durable snapshot to record, it commits -// CRASHED instead and fails the step. +// durable snapshot (sync) or node-local snapshot (async), and commits +// SUSPENDED with the assignment cleared in a single update, then releases the +// durable snapshot that update replaced. +// When the latest snapshot cannot be recorded, it commits CRASHED instead and +// fails the step. // It re-reads the actor first so an out-of-band transition (e.g. the syncer // crashing the actor after its worker died) is not overwritten: an actor that // is no longer SUSPENDING is left as it is and the step fails with // FailedPrecondition. -func (w *ActorWorkflow) ensureSuspendedFinalized(ctx context.Context, actorRef resources.ActorRef) (_ *ateapipb.Actor, err error) { +func (w *ActorWorkflow) ensureSuspendedFinalized(ctx context.Context, actorRef resources.ActorRef, mode suspendMode) (_ *ateapipb.Actor, err error) { ctx, done := stepSpan(ctx, "FinalizeSuspended") defer func() { err = done(err) }() @@ -417,8 +493,19 @@ func (w *ActorWorkflow) ensureSuspendedFinalized(ctx context.Context, actorRef r if state := latestActor.GetStatus().GetState(); state != ateapipb.ActorState_ACTOR_STATE_SUSPENDING { return nil, apierror.FailedPrecondition("FinalizeSuspended prerequisite not met for Actor: %s (got: %v, want %s)", actorRef, state, ateapipb.ActorState_ACTOR_STATE_SUSPENDING) } - if snapshotAtLatestGeneration(latestActor.GetStatus()).GetDurableSnapshot() == nil { - return nil, crashActorAndFail(ctx, w.store, actorRef, ateattr.OperationSuspend, crashMessageDurableSnapshotMissing) + switch mode { + case suspendSync: + if snapshotAtLatestGeneration(latestActor.GetStatus()).GetDurableSnapshot() == nil { + return nil, crashActorAndFail(ctx, w.store, actorRef, ateattr.OperationSuspend, crashMessageDurableSnapshotMissing) + } + case suspendAsync: + // A local snapshot on an unknown node can never be found by the scheduler. + if latestActor.GetStatus().GetAssignedNode() == "" { + return nil, crashActorAndFail(ctx, w.store, actorRef, ateattr.OperationSuspend, crashMessageLocalSnapshotNodeUnknown) + } + if snapshotAtLatestGeneration(latestActor.GetStatus()) == nil { + return nil, crashActorAndFail(ctx, w.store, actorRef, ateattr.OperationSuspend, crashMessageLocalSnapshotMissing) + } } // 1. Free the worker (if it hasn't been freed yet) @@ -431,7 +518,7 @@ func (w *ActorWorkflow) ensureSuspendedFinalized(ctx context.Context, actorRef r } } - // 2. Finalize the actor: record its new durable snapshot and mark it SUSPENDED. This + // 2. Finalize the actor: record its new snapshot and mark it SUSPENDED. This // must run even with no worker assignment (nothing to free), or the actor // would be left SUSPENDING forever with the workflow reporting success. // The precondition pins the version checked above, so an out-of-band write @@ -439,14 +526,22 @@ func (w *ActorWorkflow) ensureSuspendedFinalized(ctx context.Context, actorRef r t = time.Now() storedActor, err := w.store.UpdateActor(ctx, actorRef, store.PreconditionFrom(latestActor), func(toUpdate *ateapipb.Actor) error { toUpdate.Status.State = ateapipb.ActorState_ACTOR_STATE_SUSPENDED - inProgressSt := snapshotAtLatestGeneration(toUpdate.Status).GetDurableSnapshot() - inProgressSt.Status = ateapipb.SnapshotStorageStatus_SNAPSHOT_STORAGE_STATUS_COMPLETED - // Remove all local snapshots since they are superseded by the new durable snapshot. - // TODO: This will change when we merge pause and suspend and allow for both local - // and durable snapshots to be kept. - pruneSnapshots(toUpdate.Status, nil) + snap := snapshotAtLatestGeneration(toUpdate.Status) + switch mode { + case suspendSync: + snap.GetDurableSnapshot().Status = ateapipb.SnapshotStorageStatus_SNAPSHOT_STORAGE_STATUS_COMPLETED + // Remove all local snapshots since they are superseded by the new durable snapshot. + // TODO: This will change when an async suspend uploads its snapshot + // and both local and durable snapshots are kept. + pruneSnapshots(toUpdate.Status, nil) + toUpdate.Status.AssignedNode = "" + case suspendAsync: + // Record the node locality of the local checkpoint and clear older local checkpoints. + // AssignedNode is kept: a paused actor resumes on that node. + snap.Locality = toUpdate.GetStatus().GetAssignedNode() + pruneSnapshots(toUpdate.Status, snap) + } toUpdate.Status.WorkerAssignment = nil - toUpdate.Status.AssignedNode = "" return nil }) dUpdateActor = time.Since(t) diff --git a/cmd/ateapi/internal/controlapi/workflow_test.go b/cmd/ateapi/internal/controlapi/workflow_test.go index 3b42a501d2..f60920fc40 100644 --- a/cmd/ateapi/internal/controlapi/workflow_test.go +++ b/cmd/ateapi/internal/controlapi/workflow_test.go @@ -71,18 +71,13 @@ func TestActorStateChangeRecords(t *testing.T) { seedOpts: []func(*ateapipb.Actor){ withPauseInProgress(), func(a *ateapipb.Actor) { - a.Status.AssignedNode = "node1" - a.Status.WorkerAssignment = &ateapipb.WorkerAssignment{ - Worker: &ateapipb.ObjectRef{Name: "worker-gone"}, - WorkerNamespace: "default", - WorkerPool: "pool1", - WorkerPod: "worker-pod-1", - } + a.Status.AssignedNode = "node-1" + a.Status.WorkerAssignment = wireTestAssignment() }, }, transition: func(t *testing.T, w *ActorWorkflow, ref resources.ActorRef, _ *ateapipb.Actor, _ *ateapipb.ActorTemplate) { - if _, err := w.ensurePausedFinalized(context.Background(), ref); err != nil { - t.Fatalf("ensurePausedFinalized: %v", err) + if _, err := w.PauseActor(context.Background(), ref); err != nil { + t.Fatalf("PauseActor: %v", err) } }, wantOp: ateattr.OperationSuspend, @@ -131,7 +126,7 @@ func TestActorStateChangeRecords(t *testing.T) { t.Fatalf("get template: %v", err) } - w := &ActorWorkflow{store: persistence} + w, _ := newWireCaptureWorkflow(t, persistence) tt.transition(t, w, actorRef, actor, tmpl) // The logs exporter is on, so the record goes to OTLP only. From 68a082c365cdec227ee1adeadcff7c496f4f510a Mon Sep 17 00:00:00 2001 From: Tim Hockin Date: Fri, 16 Oct 2026 04:37:38 +0000 Subject: [PATCH 28/31] Use a Go vanity import in the "main" substrate repo: go.ate.dev/substrate (#2181) This commit changes everything except HTTP(s) links. github.com/agent-substrate/substrate -> go.ate.dev/substrate I chose "go.ate.dev" for brevity, but "pkg.ate.dev" would be OK, too. This is implemented as "CloudFlare Pages" on my personal account. If we like it, we need to make it more "community operated". This does not yet convert other repos. This breaks a TON of open PRs, so we should consider if/when we want to merge it. /cc @BenTheElder @rakyll @EItanya --- .golangci.yaml | 2 +- .ko.yaml | 4 +- .../nighthawk-egress/traffic-driver.yaml.tmpl | 2 +- .../glutton-durdir-data-template.yaml.tmpl | 2 +- .../glutton-durdir-full-template.yaml.tmpl | 2 +- .../glutton-storage-template.yaml.tmpl | 2 +- .../manifests/glutton-template.yaml.tmpl | 2 +- .../workloads/manifests/workloads.yaml.tmpl | 2 +- cmd/ate-setup/internal/cmd/benchmarks.go | 4 +- cmd/ate-setup/internal/cmd/benchmarks_test.go | 4 +- cmd/ate-setup/internal/cmd/create.go | 2 +- cmd/ate-setup/internal/cmd/csi_test.go | 4 +- cmd/ate-setup/internal/cmd/delete.go | 2 +- cmd/ate-setup/internal/cmd/demo.go | 6 +- cmd/ate-setup/internal/cmd/deploy.go | 4 +- cmd/ate-setup/internal/cmd/record_test.go | 4 +- cmd/ate-setup/internal/cmd/recordname_test.go | 4 +- cmd/ate-setup/internal/cmd/root.go | 8 +-- cmd/ate-setup/internal/cmd/setup.go | 2 +- cmd/ate-setup/internal/config/config.go | 6 +- cmd/ate-setup/internal/config/config_test.go | 4 +- cmd/ate-setup/internal/config/credentials.go | 2 +- cmd/ate-setup/internal/config/document.go | 2 +- cmd/ate-setup/internal/config/load.go | 8 +-- cmd/ate-setup/internal/config/setting.go | 2 +- cmd/ate-setup/internal/demos/all/all.go | 20 +++--- cmd/ate-setup/internal/demos/all/all_test.go | 6 +- .../autoscaledworkerpool.go | 10 +-- .../demos/claudemultiplex/claudemultiplex.go | 10 +-- .../claudemultiplex/claudemultiplex_test.go | 4 +- .../internal/demos/counter/counter.go | 8 +-- .../internal/demos/counter/counter_test.go | 6 +- .../demos/countermicrovm/countermicrovm.go | 6 +- cmd/ate-setup/internal/demos/demos.go | 4 +- .../internal/demos/demotest/demotest.go | 8 +-- cmd/ate-setup/internal/demos/egress/egress.go | 6 +- .../demos/egressmicrovm/egressmicrovm.go | 6 +- .../internal/demos/jupyter/jupyter.go | 6 +- .../demos/multitemplate/multitemplate.go | 6 +- .../internal/demos/parking/parking.go | 6 +- cmd/ate-setup/internal/demos/render.go | 4 +- cmd/ate-setup/internal/demos/render_test.go | 6 +- .../internal/demos/sandbox/sandbox.go | 6 +- cmd/ate-setup/internal/demos/substrate.go | 10 +-- cmd/ate-setup/internal/images/images.go | 2 +- cmd/ate-setup/internal/images/images_test.go | 4 +- .../internal/images/prebuilt_test.go | 48 +++++++------- cmd/ate-setup/internal/ko/ko.go | 4 +- cmd/ate-setup/internal/ko/ko_test.go | 4 +- cmd/ate-setup/internal/kube/apply.go | 2 +- cmd/ate-setup/internal/kube/capool.go | 2 +- cmd/ate-setup/internal/kube/client.go | 2 +- cmd/ate-setup/internal/kube/wait.go | 4 +- cmd/ate-setup/internal/steps/actors.go | 4 +- cmd/ate-setup/internal/steps/apienv.go | 4 +- cmd/ate-setup/internal/steps/apienv_test.go | 6 +- cmd/ate-setup/internal/steps/benchmarks.go | 4 +- .../internal/steps/benchmarks_test.go | 2 +- cmd/ate-setup/internal/steps/cloudsql.go | 4 +- cmd/ate-setup/internal/steps/cloudsql_test.go | 4 +- cmd/ate-setup/internal/steps/create.go | 8 +-- cmd/ate-setup/internal/steps/create_test.go | 6 +- cmd/ate-setup/internal/steps/credprovider.go | 2 +- .../internal/steps/credprovider_test.go | 4 +- cmd/ate-setup/internal/steps/csi.go | 8 +-- cmd/ate-setup/internal/steps/csi_test.go | 4 +- cmd/ate-setup/internal/steps/delete.go | 6 +- cmd/ate-setup/internal/steps/delete_test.go | 8 +-- cmd/ate-setup/internal/steps/deploy.go | 4 +- cmd/ate-setup/internal/steps/deploy_test.go | 4 +- .../internal/steps/egress_dns_test.go | 4 +- cmd/ate-setup/internal/steps/env.go | 12 ++-- .../internal/steps/namespace_test.go | 2 +- cmd/ate-setup/internal/steps/overlay.go | 6 +- cmd/ate-setup/internal/steps/overlay_test.go | 6 +- cmd/ate-setup/internal/steps/postgres.go | 8 +-- cmd/ate-setup/internal/steps/postgres_test.go | 6 +- cmd/ate-setup/internal/steps/prereqs.go | 2 +- cmd/ate-setup/internal/steps/prereqs_test.go | 6 +- cmd/ate-setup/internal/steps/preview.go | 2 +- cmd/ate-setup/internal/steps/preview_test.go | 16 ++--- cmd/ate-setup/internal/steps/publish.go | 4 +- cmd/ate-setup/internal/steps/publish_test.go | 4 +- cmd/ate-setup/internal/steps/substrate.go | 10 +-- .../internal/steps/substrate_test.go | 8 +-- .../steps/systemnetworkpolicy_test.go | 6 +- cmd/ate-setup/internal/steps/version.go | 2 +- cmd/ate-setup/internal/steps/version_test.go | 4 +- cmd/ate-setup/main.go | 2 +- cmd/ate-setup/preflight_test.go | 2 +- cmd/ate-setup/shim_test.go | 8 +-- cmd/ateapi/internal/apiauthn/apiauthn.go | 2 +- cmd/ateapi/internal/apiauthn/apiauthn_test.go | 2 +- cmd/ateapi/internal/apiconfig/loader.go | 2 +- .../internal/apivalidation/access_policy.go | 4 +- .../apivalidation/access_policy_test.go | 6 +- cmd/ateapi/internal/apivalidation/actor.go | 4 +- .../internal/apivalidation/actor_template.go | 6 +- .../apivalidation/actor_template_test.go | 6 +- .../internal/apivalidation/actor_test.go | 2 +- cmd/ateapi/internal/apivalidation/atespace.go | 2 +- .../internal/apivalidation/atespace_test.go | 2 +- cmd/ateapi/internal/apivalidation/common.go | 6 +- .../internal/apivalidation/common_test.go | 2 +- cmd/ateapi/internal/apivalidation/doc.go | 2 +- .../internal/apivalidation/egress_policy.go | 4 +- .../apivalidation/egress_policy_test.go | 6 +- cmd/ateapi/internal/apivalidation/tag.go | 2 +- cmd/ateapi/internal/apivalidation/tag_test.go | 2 +- .../internal/apivalidation/validation_test.go | 2 +- cmd/ateapi/internal/apivalidation/worker.go | 4 +- .../internal/apivalidation/worker_test.go | 2 +- .../apivalidation/zz_generated.validation.go | 26 ++++---- cmd/ateapi/internal/ateletauth/ateletauth.go | 4 +- .../internal/ateletauth/ateletauth_test.go | 6 +- .../ateletauthtest/ateletauthtest.go | 4 +- cmd/ateapi/internal/authz/authorizer.go | 4 +- cmd/ateapi/internal/authz/authz_test.go | 4 +- cmd/ateapi/internal/authz/authz_util_test.go | 2 +- cmd/ateapi/internal/authz/interceptor.go | 4 +- cmd/ateapi/internal/authz/interceptor_test.go | 6 +- cmd/ateapi/internal/authz/policy_manager.go | 2 +- cmd/ateapi/internal/authz/registry.go | 6 +- cmd/ateapi/internal/authz/registry_test.go | 4 +- .../internal/controlapi/access_policy.go | 12 ++-- .../controlapi/access_policy_errors_test.go | 4 +- .../internal/controlapi/access_policy_test.go | 12 ++-- cmd/ateapi/internal/controlapi/actor.go | 20 +++--- .../internal/controlapi/actor_sizing_test.go | 2 +- .../internal/controlapi/actor_template.go | 12 ++-- .../controlapi/actor_template_test.go | 16 ++--- cmd/ateapi/internal/controlapi/actor_test.go | 18 ++--- cmd/ateapi/internal/controlapi/atespace.go | 12 ++-- .../internal/controlapi/atespace_test.go | 14 ++-- cmd/ateapi/internal/controlapi/common_test.go | 4 +- cmd/ateapi/internal/controlapi/converter.go | 4 +- .../internal/controlapi/converter_test.go | 4 +- cmd/ateapi/internal/controlapi/crash.go | 12 ++-- cmd/ateapi/internal/controlapi/crash_test.go | 14 ++-- .../internal/controlapi/delete_options.go | 4 +- cmd/ateapi/internal/controlapi/dialer.go | 8 +-- cmd/ateapi/internal/controlapi/dialer_test.go | 4 +- .../internal/controlapi/egress_policy.go | 12 ++-- .../internal/controlapi/egress_policy_test.go | 10 +-- cmd/ateapi/internal/controlapi/export_test.go | 10 +-- .../functionaltest/access_policy_test.go | 4 +- .../functionaltest/actor_template_test.go | 8 +-- .../controlapi/functionaltest/actor_test.go | 20 +++--- .../functionaltest/atespace_test.go | 2 +- .../controlapi/functionaltest/common_test.go | 36 +++++----- .../functionaltest/egress_policy_test.go | 4 +- .../controlapi/functionaltest/main_test.go | 10 +-- .../controlapi/functionaltest/tag_test.go | 8 +-- .../functionaltest/worker_contention_test.go | 6 +- .../controlapi/functionaltest/worker_test.go | 4 +- cmd/ateapi/internal/controlapi/hosting.go | 4 +- .../internal/controlapi/hosting_test.go | 14 ++-- cmd/ateapi/internal/controlapi/main_test.go | 2 +- cmd/ateapi/internal/controlapi/metrics.go | 8 +-- .../internal/controlapi/metrics_test.go | 8 +-- .../controlapi/mint_after_release_test.go | 20 +++--- cmd/ateapi/internal/controlapi/pagination.go | 6 +- .../internal/controlapi/pagination_test.go | 4 +- .../internal/controlapi/sandbox_assets.go | 10 +-- .../controlapi/sandbox_assets_test.go | 6 +- cmd/ateapi/internal/controlapi/service.go | 20 +++--- .../internal/controlapi/snapshot_helpers.go | 2 +- .../controlapi/snapshot_helpers_test.go | 2 +- .../internal/controlapi/snapshot_report.go | 12 ++-- .../controlapi/snapshot_report_test.go | 10 +-- .../internal/controlapi/span_identity.go | 6 +- .../internal/controlapi/span_identity_test.go | 6 +- cmd/ateapi/internal/controlapi/tag.go | 12 ++-- cmd/ateapi/internal/controlapi/tag_test.go | 10 +-- .../controlapi/template_reconciler.go | 8 +-- .../controlapi/template_reconciler_test.go | 10 +-- cmd/ateapi/internal/controlapi/validate.go | 4 +- cmd/ateapi/internal/controlapi/volumes.go | 6 +- .../internal/controlapi/volumes_test.go | 6 +- cmd/ateapi/internal/controlapi/worker.go | 12 ++-- .../worker_assignment_reconciler.go | 4 +- .../worker_assignment_reconciler_test.go | 6 +- .../internal/controlapi/worker_assignments.go | 4 +- cmd/ateapi/internal/controlapi/worker_test.go | 8 +-- cmd/ateapi/internal/controlapi/workflow.go | 22 +++---- .../internal/controlapi/workflow_delete.go | 14 ++-- .../controlapi/workflow_delete_test.go | 14 ++-- .../workflow_objectstore_plugin_test.go | 6 +- .../controlapi/workflow_pause_test.go | 12 ++-- .../workflow_reconcile_assignments.go | 8 +-- .../workflow_reconcile_assignments_test.go | 14 ++-- .../internal/controlapi/workflow_resume.go | 20 +++--- .../controlapi/workflow_resume_test.go | 26 ++++---- .../internal/controlapi/workflow_revert.go | 12 ++-- .../controlapi/workflow_revert_test.go | 8 +-- .../internal/controlapi/workflow_suspend.go | 14 ++-- .../controlapi/workflow_suspend_test.go | 16 ++--- .../internal/controlapi/workflow_tag.go | 10 +-- .../internal/controlapi/workflow_tag_test.go | 12 ++-- .../controlapi/workflow_template_delete.go | 10 +-- .../internal/controlapi/workflow_test.go | 12 ++-- .../controlapi/workflow_testutil_test.go | 16 ++--- .../controlapi/workflow_worker_delete.go | 12 ++-- .../controlapi/workflow_worker_delete_test.go | 12 ++-- .../internal/controlapi/workload_spec.go | 4 +- .../internal/controlapi/workload_spec_test.go | 4 +- cmd/ateapi/internal/defaults/defaults.go | 2 +- cmd/ateapi/internal/defaults/defaults_test.go | 2 +- cmd/ateapi/internal/scheduling/scheduling.go | 6 +- .../internal/scheduling/scheduling_test.go | 4 +- .../internal/store/atepg/access_policy.go | 4 +- .../store/atepg/access_policy_test.go | 6 +- cmd/ateapi/internal/store/atepg/actor.go | 6 +- .../internal/store/atepg/actor_template.go | 6 +- .../store/atepg/actor_template_test.go | 6 +- cmd/ateapi/internal/store/atepg/actor_test.go | 6 +- cmd/ateapi/internal/store/atepg/atepg.go | 8 +-- cmd/ateapi/internal/store/atepg/atepg_test.go | 4 +- cmd/ateapi/internal/store/atepg/atespace.go | 4 +- .../internal/store/atepg/cert_auth_test.go | 4 +- .../internal/store/atepg/common_test.go | 6 +- .../internal/store/atepg/contract_test.go | 4 +- .../internal/store/atepg/egress_policy.go | 6 +- cmd/ateapi/internal/store/atepg/lease.go | 2 +- cmd/ateapi/internal/store/atepg/lease_test.go | 2 +- cmd/ateapi/internal/store/atepg/main_test.go | 2 +- cmd/ateapi/internal/store/atepg/outbox.go | 4 +- .../internal/store/atepg/outbox_test.go | 4 +- cmd/ateapi/internal/store/atepg/pagetoken.go | 2 +- .../internal/store/atepg/partition_test.go | 4 +- .../store/atepg/postgressetup_test.go | 2 +- cmd/ateapi/internal/store/atepg/tag.go | 6 +- cmd/ateapi/internal/store/atepg/tag_test.go | 6 +- cmd/ateapi/internal/store/atepg/worker.go | 4 +- .../internal/store/atepg/worker_assignment.go | 6 +- .../store/atepg/worker_assignment_test.go | 4 +- cmd/ateapi/internal/store/store.go | 4 +- cmd/ateapi/internal/store/store_test.go | 2 +- .../internal/store/storecontract/contract.go | 6 +- .../internal/store/storetest/storetest.go | 12 ++-- .../internal/workercache/workercache.go | 4 +- .../internal/workercache/workercache_test.go | 6 +- .../internal/workerservice/certificate.go | 14 ++-- .../workerservice/certificate_test.go | 14 ++-- cmd/ateapi/internal/workerservice/jwt.go | 16 ++--- cmd/ateapi/internal/workerservice/jwt_test.go | 10 +-- .../internal/workerservice/main_test.go | 2 +- cmd/ateapi/internal/workerservice/register.go | 12 ++-- .../internal/workerservice/register_test.go | 10 +-- cmd/ateapi/internal/workerservice/report.go | 8 +-- .../internal/workerservice/report_test.go | 8 +-- cmd/ateapi/internal/workerservice/service.go | 10 +-- .../internal/workerservice/service_test.go | 8 +-- cmd/ateapi/internal/workerservice/suspend.go | 12 ++-- .../internal/workerservice/suspend_test.go | 12 ++-- cmd/ateapi/main.go | 48 +++++++------- .../controllers/egressmitmtrust_controller.go | 4 +- .../egressmitmtrust_controller_test.go | 6 +- .../controllers/networkpolicy_controller.go | 4 +- .../networkpolicy_controller_test.go | 4 +- .../sandboxconfig_protection_controller.go | 2 +- ...andboxconfig_protection_controller_test.go | 2 +- .../internal/controllers/workerpool_apply.go | 12 ++-- .../controllers/workerpool_apply_test.go | 14 ++-- .../controllers/workerpool_controller.go | 6 +- .../controllers/workerpool_controller_test.go | 8 +-- .../controllers/workerpool_sandboxconfig.go | 2 +- .../workerpool_sandboxconfig_test.go | 2 +- .../internal/workersync/fakecontrol_test.go | 2 +- .../internal/workersync/syncer.go | 6 +- .../workersync/syncer_restart_test.go | 2 +- .../internal/workersync/syncer_test.go | 8 +-- cmd/atecontroller/main.go | 24 +++---- cmd/atelet/deviceplugin.go | 2 +- cmd/atelet/egresspolicy.go | 4 +- cmd/atelet/egresspolicy_test.go | 4 +- cmd/atelet/imagegc.go | 4 +- cmd/atelet/imagegc_test.go | 4 +- cmd/atelet/imagevolume_test.go | 4 +- cmd/atelet/internal/apivalidation/doc.go | 2 +- cmd/atelet/internal/apivalidation/validate.go | 4 +- .../internal/apivalidation/validation_test.go | 4 +- .../internal/apivalidation/workloadspec.go | 4 +- .../apivalidation/zz_generated.validation.go | 10 +-- cmd/atelet/internal/ateletpath/ateletpath.go | 4 +- .../internal/filecache/getfilecopyto.go | 2 +- cmd/atelet/internal/snapshottracker/actor.go | 2 +- .../snapshottracker/background_test.go | 6 +- .../internal/snapshottracker/lifecycle.go | 2 +- cmd/atelet/internal/snapshottracker/state.go | 6 +- .../internal/snapshottracker/tracker.go | 4 +- .../internal/snapshottracker/tracker_test.go | 6 +- .../internal/trustbundle/trustbundle.go | 2 +- cmd/atelet/lifecycle_test.go | 16 ++--- cmd/atelet/local_checkpoints.go | 2 +- cmd/atelet/main.go | 66 +++++++++---------- cmd/atelet/main_test.go | 24 +++---- cmd/atelet/metrics.go | 6 +- cmd/atelet/metrics_test.go | 6 +- cmd/atelet/oci.go | 6 +- cmd/atelet/oci_test.go | 2 +- cmd/atelet/rpc_logging_test.go | 8 +-- cmd/atelet/sandbox_assets.go | 18 ++--- cmd/atelet/sandbox_assets_plugin_test.go | 20 +++--- cmd/atelet/sandbox_assets_test.go | 2 +- cmd/atelet/sandbox_prewarm.go | 6 +- cmd/atelet/sandbox_prewarm_test.go | 14 ++-- cmd/atelet/snapshot_transfer.go | 4 +- cmd/atelet/snapshot_transfer_test.go | 20 +++--- cmd/atelet/snapshotlog.go | 6 +- cmd/atelet/snapshotlog_test.go | 4 +- cmd/atelet/statspoller.go | 8 +-- cmd/atelet/statspoller_test.go | 2 +- cmd/atelet/systeminfovolume.go | 10 +-- cmd/atelet/systeminfovolume_test.go | 8 +-- cmd/atelet/volumes.go | 8 +-- cmd/atelet/volumes_test.go | 12 ++-- cmd/atelet/worker_sandbox_syncer.go | 6 +- cmd/atelet/worker_sandbox_syncer_test.go | 10 +-- cmd/atelet/worker_sandbox_versions.go | 10 +-- cmd/atelet/worker_sandbox_versions_test.go | 6 +- cmd/atelet/workersupport.go | 10 +-- cmd/atelet/workersupport_test.go | 8 +-- cmd/atenet/internal/root.go | 8 +-- cmd/atenet/internal/router/cmd.go | 6 +- cmd/atenet/internal/router/config.go | 2 +- cmd/atenet/internal/router/config_test.go | 2 +- cmd/atenet/internal/router/dataplane.go | 4 +- cmd/atenet/internal/router/drain.go | 2 +- cmd/atenet/internal/router/drain_test.go | 2 +- .../router/egress/actorjwt/actorjwt.go | 4 +- .../router/egress/actorjwt/actorjwt_test.go | 4 +- .../internal/router/egress/credentials.go | 10 +-- .../router/egress/credentials_test.go | 6 +- cmd/atenet/internal/router/egress/egress.go | 12 ++-- .../internal/router/egress/egress_test.go | 8 +-- .../internal/router/egress/policycache.go | 6 +- .../router/egress/policycache_test.go | 4 +- cmd/atenet/internal/router/egress/provider.go | 2 +- cmd/atenet/internal/router/egress/request.go | 6 +- .../internal/router/egress/request_test.go | 4 +- .../router/egresspolicymanifest_test.go | 2 +- .../router/extproc/egress_websocket_test.go | 4 +- cmd/atenet/internal/router/extproc/handler.go | 2 +- .../internal/router/extproc/metadata_test.go | 2 +- cmd/atenet/internal/router/extproc/metrics.go | 2 +- .../internal/router/extproc/metrics_test.go | 2 +- cmd/atenet/internal/router/health.go | 2 +- cmd/atenet/internal/router/health_test.go | 2 +- cmd/atenet/internal/router/ingress/errors.go | 6 +- .../internal/router/ingress/errors_test.go | 4 +- cmd/atenet/internal/router/ingress/flight.go | 2 +- cmd/atenet/internal/router/ingress/ingress.go | 8 +-- .../internal/router/ingress/ingress_test.go | 10 +-- cmd/atenet/internal/router/ingress/metrics.go | 4 +- .../internal/router/ingress/metrics_test.go | 2 +- .../internal/router/ingress/parking_test.go | 2 +- cmd/atenet/internal/router/ingress/resumer.go | 8 +-- .../internal/router/ingress/resumer_test.go | 8 +-- cmd/atenet/internal/router/router.go | 18 ++--- cmd/atenet/internal/router/status.go | 4 +- cmd/atenet/internal/router/status_test.go | 4 +- cmd/atenet/internal/router/xds.go | 10 +-- cmd/atenet/internal/router/xds_test.go | 6 +- .../router/xds_upstream_connect_test.go | 6 +- cmd/atenet/internal/sdsmint/minter.go | 2 +- cmd/atenet/internal/sdsmint/minter_test.go | 2 +- cmd/atenet/internal/sdsmint/run.go | 4 +- cmd/atenet/main.go | 2 +- cmd/ateworker-gvisor/actorpath.go | 4 +- cmd/ateworker-gvisor/actorpath_test.go | 4 +- cmd/ateworker-gvisor/ckptcgroup.go | 6 +- cmd/ateworker-gvisor/ckptcgroup_test.go | 6 +- cmd/ateworker-gvisor/cleanup_test.go | 2 +- cmd/ateworker-gvisor/durable.go | 4 +- cmd/ateworker-gvisor/durable_test.go | 2 +- cmd/ateworker-gvisor/fidelity_test.go | 4 +- cmd/ateworker-gvisor/hosted.go | 12 ++-- cmd/ateworker-gvisor/hosted_test.go | 6 +- .../internal/cdiinject/inject.go | 2 +- .../internal/cdiinject/inject_test.go | 2 +- cmd/ateworker-gvisor/main.go | 48 +++++++------- cmd/ateworker-gvisor/main_test.go | 14 ++-- cmd/ateworker-gvisor/phaselog.go | 4 +- cmd/ateworker-gvisor/phaselog_test.go | 6 +- cmd/ateworker-gvisor/runsc.go | 8 +-- cmd/ateworker-gvisor/runsc_test.go | 8 +-- cmd/ateworker-gvisor/sandboxexit.go | 4 +- cmd/ateworker-gvisor/sandboxexit_test.go | 4 +- cmd/ateworker-gvisor/sandboxnet.go | 4 +- cmd/ateworker-gvisor/stats.go | 14 ++-- cmd/ateworker-gvisor/stats_test.go | 14 ++-- cmd/ateworker-gvisor/usage_test.go | 12 ++-- cmd/ateworker-microvm/actorpath.go | 2 +- cmd/ateworker-microvm/checkpoint.go | 18 ++--- cmd/ateworker-microvm/csi.go | 6 +- cmd/ateworker-microvm/csi_test.go | 2 +- cmd/ateworker-microvm/durable.go | 8 +-- cmd/ateworker-microvm/durable_test.go | 4 +- cmd/ateworker-microvm/fidelity.go | 2 +- cmd/ateworker-microvm/fidelity_test.go | 4 +- cmd/ateworker-microvm/hosted.go | 18 ++--- cmd/ateworker-microvm/hosted_test.go | 4 +- .../internal/agentstats/agentstats.go | 2 +- .../internal/agentstats/agentstats_test.go | 2 +- .../internal/kata/agentclient.go | 2 +- .../internal/kata/agentclient_linux_test.go | 2 +- .../internal/kata/cleanup_linux.go | 2 +- .../internal/kata/cleanup_linux_test.go | 2 +- .../internal/kata/overlay_linux.go | 4 +- .../internal/kata/overlay_linux_test.go | 2 +- .../internal/kata/specconv.go | 2 +- .../internal/third_party/kata/PROVENANCE.md | 2 +- .../third_party/kata/agentpb/agent.pb.go | 2 +- .../third_party/kata/agentpb/agent.proto | 2 +- .../third_party/kata/agentpb/csi.pb.go | 2 +- .../third_party/kata/agentpb/csi.proto | 2 +- .../third_party/kata/agentpb/oci.pb.go | 2 +- .../third_party/kata/agentpb/oci.proto | 2 +- .../third_party/kata/agentpb/types.pb.go | 2 +- .../third_party/kata/agentpb/types.proto | 2 +- cmd/ateworker-microvm/main.go | 36 +++++----- cmd/ateworker-microvm/net.go | 4 +- cmd/ateworker-microvm/net_linux_test.go | 16 ++--- cmd/ateworker-microvm/phaselog.go | 2 +- cmd/ateworker-microvm/restore.go | 26 ++++---- cmd/ateworker-microvm/restore_test.go | 10 +-- cmd/ateworker-microvm/rootfsupper.go | 6 +- cmd/ateworker-microvm/rootfsupper_test.go | 4 +- cmd/ateworker-microvm/run.go | 28 ++++---- cmd/ateworker-microvm/run_test.go | 2 +- cmd/ateworker-microvm/sandboxnet.go | 2 +- cmd/ateworker-microvm/shutdown.go | 2 +- cmd/ateworker-microvm/snapshotcache.go | 2 +- cmd/ateworker-microvm/snapshotcache_test.go | 4 +- cmd/ateworker-microvm/spec.go | 4 +- cmd/ateworker-microvm/spec_test.go | 6 +- cmd/ateworker-microvm/stats.go | 14 ++-- cmd/ateworker-microvm/stats_test.go | 12 ++-- cmd/ateworker-microvm/systeminfo.go | 6 +- cmd/ateworker-microvm/usage_test.go | 10 +-- cmd/benchmarking/boomer-worker/main.go | 18 ++--- cmd/benchmarking/glutton/main.go | 6 +- .../isolate/fake-atelet/herder.go | 8 +-- .../isolate/fake-atelet/herder_test.go | 4 +- cmd/benchmarking/isolate/fake-atelet/main.go | 20 +++--- cmd/benchmarking/isolate/fake-atelet/relay.go | 4 +- .../isolate/fake-atelet/relay_test.go | 6 +- .../isolate/fake-workersync/controller.go | 10 +-- .../fake-workersync/controller_test.go | 6 +- .../isolate/fake-workersync/main.go | 18 ++--- .../isolate/fake-workersync/relayclient.go | 6 +- .../fake-workersync/relayclient_test.go | 8 +-- .../fakeworkertest/fakeworkertest.go | 2 +- .../kubernetes-secrets/kubeprovider.go | 4 +- .../kubernetes-secrets/kubeprovider_test.go | 2 +- .../kubernetes-secrets/main.go | 12 ++-- .../kubernetes-secrets/main_test.go | 2 +- .../kubernetes-secrets/statusz.go | 2 +- .../kubernetes-secrets/statusz_test.go | 2 +- cmd/kubectl-ate/internal/cmd/actor.go | 10 +-- cmd/kubectl-ate/internal/cmd/actor_test.go | 4 +- cmd/kubectl-ate/internal/cmd/actortemplate.go | 4 +- .../internal/cmd/actortemplate_test.go | 6 +- cmd/kubectl-ate/internal/cmd/admin.go | 4 +- cmd/kubectl-ate/internal/cmd/admin_test.go | 2 +- cmd/kubectl-ate/internal/cmd/atespace.go | 4 +- cmd/kubectl-ate/internal/cmd/egress_policy.go | 6 +- .../internal/cmd/egress_policy_test.go | 6 +- cmd/kubectl-ate/internal/cmd/resourceref.go | 4 +- .../internal/cmd/resourceref_test.go | 2 +- cmd/kubectl-ate/internal/cmd/revert_actor.go | 6 +- cmd/kubectl-ate/internal/cmd/root.go | 4 +- cmd/kubectl-ate/internal/cmd/root_test.go | 2 +- cmd/kubectl-ate/internal/cmd/tag.go | 4 +- cmd/kubectl-ate/internal/cmd/tag_test.go | 2 +- cmd/kubectl-ate/internal/cmd/verbose.go | 2 +- cmd/kubectl-ate/internal/cmd/verbose_test.go | 4 +- cmd/kubectl-ate/internal/cmd/worker.go | 6 +- cmd/kubectl-ate/internal/cmd/worker_test.go | 4 +- .../internal/printer/egress_policy.go | 2 +- .../internal/printer/egress_policy_test.go | 2 +- cmd/kubectl-ate/internal/printer/printer.go | 2 +- .../internal/printer/printer_test.go | 2 +- cmd/kubectl-ate/main.go | 2 +- cmd/objectstore-plugin/main.go | 16 ++--- .../podidentitysigner/podidentitysigner.go | 8 +-- .../podidentitysigner_test.go | 6 +- .../internal/postgressigner/postgressigner.go | 8 +-- .../postgressigner/postgressigner_test.go | 8 +-- .../servicednssigner/servicednssigner.go | 6 +- .../signercontroller/signercontroller.go | 6 +- .../signercontroller/signercontroller_test.go | 2 +- cmd/podcertcontroller/main.go | 22 +++---- .../autoscaled-workerpool-template.yaml.tmpl | 2 +- .../autoscaled-workerpool.yaml.tmpl | 2 +- .../claude-code-multiplex.yaml.tmpl | 2 +- demos/claude-code-multiplex/ui/server.go | 4 +- .../counter-microvm-csi-test-template.yaml | 2 +- demos/counter/counter-microvm-csi-test.yaml | 2 +- .../counter-microvm-template.yaml.tmpl | 2 +- demos/counter/counter-microvm.yaml.tmpl | 2 +- demos/counter/counter-template.yaml.tmpl | 2 +- demos/counter/counter.yaml.tmpl | 2 +- .../egress/egress-microvm-template.yaml.tmpl | 2 +- demos/egress/egress-microvm.yaml.tmpl | 2 +- demos/egress/egress-template.yaml.tmpl | 2 +- demos/egress/egress.yaml.tmpl | 2 +- demos/egress/main.go | 2 +- demos/egress/main_test.go | 4 +- demos/jupyter/jupyter.yaml.tmpl | 2 +- .../multi-template/counter-template.yaml.tmpl | 2 +- .../fspersist-template.yaml.tmpl | 2 +- demos/multi-template/multi-template.yaml.tmpl | 2 +- demos/parking/parking-template.yaml.tmpl | 2 +- demos/parking/parking.yaml.tmpl | 2 +- demos/sandbox/client/main.go | 6 +- demos/sandbox/manual-test-multi-template.yaml | 2 +- demos/sandbox/manual-test-multi.yaml | 2 +- demos/sandbox/sandbox-template.yaml.tmpl | 2 +- demos/sandbox/sandbox.yaml.tmpl | 2 +- docs/api-guide.md | 4 +- docs/csi-volumes.md | 2 +- docs/dev/best-practices/tracing.md | 2 +- docs/dev/code-layout.md | 4 +- docs/observability.md | 2 +- go.mod | 2 +- hack/run-root-tests.sh | 2 +- hack/tools/code-generator/go.mod | 2 +- hack/tools/controller-gen/go.mod | 2 +- hack/tools/go-licenses/go.mod | 2 +- hack/tools/golangci-lint/go.mod | 2 +- hack/tools/gotestsum/go.mod | 2 +- hack/tools/kind/go.mod | 2 +- hack/tools/ko/go.mod | 2 +- hack/tools/kube-api-linter/go.mod | 2 +- hack/tools/protoc-gen-go-grpc/go.mod | 2 +- hack/tools/protoc-gen-go/go.mod | 2 +- hack/tools/setup-envtest/go.mod | 2 +- hack/update/licenses.sh | 2 +- internal/actorevent/actorevent.go | 4 +- internal/actorevent/actorevent_test.go | 4 +- internal/actorlog/logger.go | 4 +- internal/actorlog/logger_test.go | 4 +- internal/ateapiauth/client.go | 4 +- internal/ateattr/ateattr.go | 8 +-- internal/ateattr/ateattr_test.go | 8 +-- internal/ateclient/builder.go | 8 +-- internal/ateclient/builder_test.go | 2 +- internal/ateinterceptors/ateinterceptors.go | 4 +- .../ateinterceptors/ateinterceptors_test.go | 12 ++-- internal/ateinterceptors/rejectunknown.go | 2 +- .../ateinterceptors/rejectunknown_test.go | 6 +- internal/ateletdial/ateletdial.go | 4 +- internal/ateletdial/ateletdial_test.go | 2 +- internal/atenet/headers.go | 2 +- internal/ateworker/actornet/sandbox.go | 6 +- .../actornet/sandbox_atunnel_linux_test.go | 8 +-- .../ateworker/actornet/sandbox_linux_test.go | 6 +- internal/ateworker/cgroup/actor_linux_test.go | 2 +- internal/ateworker/phaselog/phaselog.go | 8 +-- internal/ateworker/phaselog/phaselog_test.go | 6 +- internal/ateworker/register/register.go | 10 +-- internal/ateworker/register/register_test.go | 6 +- internal/ateworker/suspend/suspend.go | 4 +- internal/ateworker/suspend/suspend_test.go | 2 +- internal/ateworker/tunnel/egresspolicy.go | 12 ++-- .../ateworker/tunnel/egresspolicy_test.go | 8 +-- internal/ateworker/tunnel/tunnel.go | 22 +++---- internal/ateworker/tunnel/tunnel_test.go | 8 +-- internal/atunnel/dns/dnshandler.go | 2 +- internal/atunnel/dns/dnshandler_test.go | 2 +- internal/atunnel/dns/evaluator.go | 4 +- internal/atunnel/dns/evaluator_test.go | 2 +- internal/atunnel/dns/pendingreq.go | 4 +- internal/atunnel/dns/policy.go | 2 +- internal/atunnel/dns/policy_test.go | 4 +- internal/atunnel/dns/relay_linux.go | 2 +- internal/atunnel/dns/serve_linux_test.go | 4 +- internal/atunnel/dns/tcphandler.go | 2 +- internal/atunnel/dns/tcphandler_test.go | 2 +- internal/atunnel/egress/credential.go | 4 +- internal/atunnel/egress/credential_test.go | 6 +- internal/atunnel/ingress/ingress.go | 6 +- internal/atunnel/ingress/ingress_test.go | 4 +- .../boomer/agentsession/agentsession.go | 18 ++--- .../boomer/agentsession/agentsession_test.go | 8 +-- .../benchmarking/boomer/boomerutil/client.go | 6 +- .../boomer/glutton/cpuload_test.go | 4 +- .../benchmarking/boomer/glutton/durdir.go | 16 ++--- .../boomer/glutton/durdir_test.go | 10 +-- .../boomer/glutton/fixture_test.go | 8 +-- .../benchmarking/boomer/glutton/lifecycle.go | 16 ++--- .../boomer/glutton/lifecycle_test.go | 8 +-- .../boomer/glutton/memfill_test.go | 8 +-- .../boomer/glutton/resume_test.go | 6 +- internal/benchmarking/boomer/glutton/spawn.go | 14 ++-- .../benchmarking/boomer/glutton/spawn_test.go | 8 +-- .../benchmarking/boomer/glutton/wait_test.go | 4 +- .../boomer/resumepaused/resumepaused.go | 8 +-- .../boomer/resumepaused/resumepaused_test.go | 6 +- .../benchmarking/boomer/sweperf/sweperf.go | 12 ++-- .../boomer/sweperf/sweperf_test.go | 12 ++-- .../benchmarking/boomer/userclass/config.go | 4 +- internal/benchmarking/glutton/cpu_load.go | 2 +- .../benchmarking/glutton/cpu_load_test.go | 2 +- internal/benchmarking/glutton/fake/server.go | 6 +- internal/benchmarking/glutton/gossip.go | 2 +- internal/benchmarking/glutton/ram_test.go | 2 +- internal/benchmarking/glutton/server.go | 4 +- internal/benchmarking/glutton/server_test.go | 4 +- internal/benchmarking/glutton/service.go | 2 +- internal/benchmarking/glutton/service_test.go | 2 +- .../benchmarking/glutton/session_ops_test.go | 2 +- internal/benchmarking/glutton/sizes_test.go | 2 +- internal/contextlogging/contextlogging.go | 4 +- .../contextlogging/contextlogging_test.go | 6 +- internal/e2e/clients.go | 4 +- internal/e2e/collector_metrics.go | 4 +- internal/e2e/credprovider.go | 2 +- internal/e2e/egresspolicy.go | 4 +- internal/e2e/env.go | 2 +- internal/e2e/fixture.go | 2 +- .../capabilities-templates.yaml.tmpl | 4 +- internal/e2e/fixtures/probe/main_test.go | 2 +- .../probe/probe-multiactor-template.yaml.tmpl | 2 +- .../probe/probe-parking-template.yaml.tmpl | 2 +- .../probe/probe-sized-template.yaml.tmpl | 2 +- .../fixtures/probe/probe-template.yaml.tmpl | 2 +- .../security/atespace-template.yaml.tmpl | 2 +- .../e2e/fixtures/testserver/common_test.go | 2 +- .../e2e/fixtures/testserver/egressprobe.go | 4 +- .../fixtures/testserver/egressprobe.yaml.tmpl | 2 +- internal/e2e/fixtures/testserver/grpc.go | 2 +- internal/e2e/fixtures/testserver/grpc_test.go | 2 +- .../testserver/grpcecho-template.yaml.tmpl | 2 +- .../testserver/websocket-template.yaml.tmpl | 2 +- internal/e2e/pod.go | 2 +- internal/e2e/preflight.go | 2 +- internal/e2e/preview.go | 2 +- internal/e2e/probe.go | 2 +- internal/e2e/probe_test.go | 2 +- internal/e2e/resume.go | 2 +- internal/e2e/router_client.go | 8 +-- internal/e2e/router_client_test.go | 4 +- internal/e2e/sandbox.go | 4 +- internal/e2e/sandbox_test.go | 4 +- internal/e2e/serverpod_test.go | 14 ++-- internal/e2e/statusz.go | 4 +- .../suites/capabilities/capabilities_test.go | 6 +- .../e2e/suites/capabilities/testmain_test.go | 2 +- .../combinedvolumes/combinedvolumes_test.go | 8 +-- .../suites/combinedvolumes/testmain_test.go | 2 +- internal/e2e/suites/demo/demo_test.go | 16 ++--- internal/e2e/suites/demo/termination_test.go | 6 +- internal/e2e/suites/demo/testmain_test.go | 2 +- .../e2e/suites/demo/updatetemplate_test.go | 6 +- .../suites/egressauthz/actoridentity_test.go | 4 +- .../suites/egressauthz/egressauthz_test.go | 6 +- .../e2e/suites/egressauthz/testmain_test.go | 2 +- .../egresscredinject/egresscredinject_test.go | 8 +-- .../suites/egresscredinject/testmain_test.go | 2 +- .../e2e/suites/egressmitm/egressmitm_test.go | 8 +-- .../e2e/suites/egressmitm/testmain_test.go | 2 +- internal/e2e/suites/example/testmain_test.go | 2 +- internal/e2e/suites/identity/identity_test.go | 6 +- internal/e2e/suites/identity/testmain_test.go | 2 +- internal/e2e/suites/metrics/metrics_test.go | 8 +-- internal/e2e/suites/metrics/testmain_test.go | 2 +- .../e2e/suites/multiactor/multiactor_test.go | 6 +- .../e2e/suites/multiactor/testmain_test.go | 2 +- .../e2e/suites/networking/accesslog_test.go | 2 +- .../suites/networking/arbitraryport_test.go | 6 +- .../suites/networking/egresspolicy_test.go | 6 +- .../e2e/suites/networking/grpcegress_test.go | 6 +- .../e2e/suites/networking/grpcingress_test.go | 12 ++-- .../suites/networking/microvmclock_test.go | 6 +- .../e2e/suites/networking/networking_test.go | 20 +++--- .../e2e/suites/networking/testmain_test.go | 2 +- .../suites/networking/websocketegress_test.go | 6 +- .../networking/websocketingress_test.go | 6 +- .../networkpolicy/networkpolicy_test.go | 14 ++-- .../e2e/suites/networkpolicy/testmain_test.go | 2 +- internal/e2e/suites/parking/parking_test.go | 6 +- internal/e2e/suites/parking/testmain_test.go | 2 +- .../suites/sandboxconfig/classdefault_test.go | 4 +- .../e2e/suites/sandboxconfig/testmain_test.go | 2 +- .../e2e/suites/security/crossatespace_test.go | 8 +-- internal/e2e/suites/security/testmain_test.go | 2 +- internal/e2e/suites/sizing/sizing_test.go | 6 +- internal/e2e/suites/sizing/testmain_test.go | 2 +- internal/e2e/template.go | 4 +- internal/e2e/template_test.go | 4 +- internal/e2e/trustbundle.go | 4 +- internal/e2e/trustbundle_test.go | 4 +- internal/egresspolicy/egresspolicy.go | 2 +- internal/egresspolicy/egresspolicy_test.go | 2 +- internal/hardware/hardware.go | 4 +- internal/hardware/hardware_test.go | 2 +- internal/imagecache/bundle_linux_test.go | 2 +- internal/imagecache/imagecache.go | 2 +- internal/imagecache/metrics.go | 2 +- internal/imagecache/metrics_test.go | 2 +- internal/linuxsys/original_dst_linux_test.go | 4 +- .../localjwtauthority/jwks_filesource_test.go | 4 +- .../localjwtauthority/localjwtauthority.go | 2 +- .../localjwtauthority_test.go | 2 +- internal/netns/netns_linux_test.go | 2 +- internal/objectstore/gcs_test.go | 2 +- internal/objectstore/objectstore.go | 2 +- internal/objectstore/objectstore_test.go | 6 +- .../objectstoretest/objectstoretest.go | 4 +- internal/objectstore/s3_test.go | 2 +- .../actorscope/actorscope.go | 4 +- .../actorscope/actorscope_test.go | 4 +- internal/objectstoreplugin/asset.go | 6 +- internal/objectstoreplugin/asset_test.go | 6 +- internal/objectstoreplugin/control.go | 6 +- .../objectstoreplugin/gcsboundary/boundary.go | 6 +- .../gcsboundary/boundary_test.go | 4 +- .../objectstoreplugin/gcsissuer/issuer.go | 4 +- .../gcsissuer/issuer_test.go | 6 +- internal/objectstoreplugin/node.go | 6 +- .../objectstoreplugintest.go | 6 +- internal/objectstoreplugin/plugin_test.go | 6 +- internal/objectstoreplugin/status.go | 4 +- internal/objectstoreplugin/status_test.go | 2 +- internal/objectstoreplugin/upload_test.go | 2 +- internal/ocispec/gvisor.go | 4 +- internal/ocispec/microvm.go | 4 +- internal/ocispec/microvm_test.go | 2 +- internal/ocispec/ocispec.go | 6 +- internal/ocispec/ocispec_test.go | 6 +- internal/ocispec/parity_test.go | 4 +- internal/oidcjwt/oidcjwt.go | 2 +- internal/oidcjwt/oidcjwt_test.go | 2 +- internal/otlprelay/e2e_test.go | 2 +- .../cmd/gsm-credential-provider/clientpool.go | 2 +- .../gsm-credential-provider/imports_test.go | 2 +- .../cmd/gsm-credential-provider/main.go | 6 +- .../cmd/gsm-credential-provider/main_test.go | 2 +- .../gcp-secret-manager/config/deployment.yaml | 2 +- internal/plugins/gcp-secret-manager/go.mod | 6 +- .../gcp-secret-manager/internal/mtls/mtls.go | 2 +- .../internal/provider/provider.go | 2 +- .../internal/provider/provider_test.go | 2 +- internal/pluginsocket/calls_test.go | 2 +- internal/pluginsocket/pluginsocket.go | 2 +- internal/pluginsocket/pluginsocket_test.go | 2 +- internal/proto/ateletpb/atelet.pb.go | 2 +- internal/proto/ateletpb/atelet.proto | 2 +- internal/proto/ateworkerpb/ateworker.pb.go | 2 +- internal/proto/ateworkerpb/ateworker.proto | 2 +- internal/proto/glutton/glutton.pb.go | 2 +- internal/proto/glutton/glutton.proto | 2 +- internal/proto/grpcechopb/grpcecho.pb.go | 2 +- internal/proto/grpcechopb/grpcecho.proto | 2 +- internal/protoredact/protoredact_test.go | 18 ++--- internal/resources/actorattribution.go | 2 +- internal/resources/quantities.go | 2 +- internal/resources/quantities_test.go | 2 +- internal/resources/resourceref.go | 2 +- internal/resources/resourceref_test.go | 2 +- internal/resources/validate.go | 8 +-- internal/resources/validate_test.go | 6 +- internal/resources/worker.go | 2 +- internal/resources/worker_test.go | 2 +- internal/serverboot/logging.go | 2 +- internal/serverboot/logging_test.go | 8 +-- internal/serverboot/serverboot.go | 4 +- internal/serverboot/serverboot_test.go | 2 +- internal/tarutil/tarutil_test.go | 2 +- internal/testcert/servertls.go | 2 +- internal/testcert/servertls_test.go | 2 +- internal/volume/csi/plugin.go | 8 +-- internal/volume/csi/plugin_test.go | 2 +- internal/volume/csi/tls_test.go | 4 +- internal/volume/mock.go | 2 +- internal/wakeupprobe/wakeupprobe.go | 2 +- internal/wakeupprobe/wakeupprobe_test.go | 2 +- internal/workerstats/activation.go | 2 +- internal/workerstats/activation_test.go | 2 +- internal/workerstats/actorattribution.go | 4 +- internal/workerstats/actorattribution_test.go | 4 +- internal/workerstats/usage.go | 10 +-- internal/workerstats/usage_test.go | 6 +- .../workerstatstest/workerstatstest.go | 4 +- manifests/ate-install/ate-api-server.yaml | 4 +- manifests/ate-install/ate-controller.yaml | 2 +- manifests/ate-install/atelet.yaml | 4 +- manifests/ate-install/atenet-egress.yaml | 4 +- manifests/ate-install/atenet-router.yaml | 2 +- .../fake-data-plane/fake-workersync.yaml | 2 +- .../fake-data-plane/kustomization.yaml | 2 +- .../pod-certificate-controller.yaml | 2 +- .../k8s-credential-provider.yaml | 2 +- pkg/api/v1alpha1/gen.go | 6 +- pkg/api/v1alpha1/main_test.go | 2 +- pkg/api/v1alpha1/sandboxconfig_types.go | 2 +- pkg/assetfetch/assetfetch.go | 2 +- pkg/assetfetch/assetfetch_test.go | 4 +- pkg/client/clientset/versioned/clientset.go | 2 +- .../versioned/fake/clientset_generated.go | 6 +- .../clientset/versioned/fake/register.go | 2 +- .../clientset/versioned/scheme/register.go | 2 +- .../typed/api/v1alpha1/api_client.go | 4 +- .../typed/api/v1alpha1/csidriverconfig.go | 4 +- .../api/v1alpha1/fake/fake_api_client.go | 2 +- .../api/v1alpha1/fake/fake_csidriverconfig.go | 4 +- .../api/v1alpha1/fake/fake_sandboxconfig.go | 4 +- .../api/v1alpha1/fake/fake_workerpool.go | 4 +- .../typed/api/v1alpha1/sandboxconfig.go | 4 +- .../typed/api/v1alpha1/workerpool.go | 4 +- .../externalversions/api/interface.go | 4 +- .../api/v1alpha1/csidriverconfig.go | 8 +-- .../api/v1alpha1/interface.go | 2 +- .../api/v1alpha1/sandboxconfig.go | 8 +-- .../api/v1alpha1/workerpool.go | 8 +-- .../informers/externalversions/factory.go | 6 +- .../informers/externalversions/generic.go | 2 +- .../internalinterfaces/factory_interfaces.go | 2 +- .../listers/api/v1alpha1/csidriverconfig.go | 2 +- .../listers/api/v1alpha1/sandboxconfig.go | 2 +- pkg/client/listers/api/v1alpha1/workerpool.go | 2 +- pkg/proto/ateapipb/ateapi.pb.go | 2 +- pkg/proto/ateapipb/ateapi.proto | 2 +- pkg/proto/ateconfigpb/apiconfig.pb.go | 2 +- pkg/proto/ateconfigpb/apiconfig.proto | 2 +- pkg/proto/credproviderpb/credprovider.pb.go | 2 +- pkg/proto/credproviderpb/credprovider.proto | 2 +- pkg/proto/objectstorepb/v1/objectstore.pb.go | 2 +- pkg/proto/objectstorepb/v1/objectstore.proto | 2 +- tools/apitool/go.mod | 6 +- tools/apitool/internal/cmd/validate.go | 6 +- .../internal/exemption/exemption_test.go | 2 +- tools/apitool/internal/lint/documented.go | 2 +- .../apitool/internal/lint/documented_test.go | 4 +- tools/apitool/internal/lint/fields.go | 2 +- tools/apitool/internal/lint/fields_test.go | 2 +- tools/apitool/internal/lint/fixtures_test.go | 4 +- tools/apitool/internal/lint/lint.go | 2 +- tools/apitool/internal/lint/method.go | 2 +- tools/apitool/internal/lint/method_test.go | 2 +- tools/apitool/internal/lint/naming.go | 2 +- tools/apitool/internal/lint/naming_test.go | 4 +- tools/apitool/internal/lint/no_oneofs.go | 2 +- tools/apitool/internal/lint/no_oneofs_test.go | 4 +- tools/apitool/internal/lint/resources.go | 2 +- tools/apitool/internal/lint/resources_test.go | 4 +- .../apitool/internal/lint/standard_methods.go | 4 +- .../internal/lint/standard_methods_test.go | 4 +- tools/apitool/internal/model/model.go | 2 +- tools/apitool/internal/model/model_test.go | 4 +- tools/apitool/internal/validate/validate.go | 10 +-- .../internal/validate/validate_test.go | 4 +- tools/apitool/main.go | 2 +- tools/junittool/go.mod | 2 +- tools/setup-gcp/main.go | 2 +- tools/validate-image-cache/main.go | 4 +- 859 files changed, 2407 insertions(+), 2407 deletions(-) diff --git a/.golangci.yaml b/.golangci.yaml index 96c1f42822..3d70dd8b22 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -43,7 +43,7 @@ linters: - "!**/cmd/ateworker-*/**" - "!**/internal/ateworker/**" deny: - - pkg: github.com/agent-substrate/substrate/internal/ateworker + - pkg: go.ate.dev/substrate/internal/ateworker desc: only the ateworker binaries may import internal/ateworker errcheck: diff --git a/.ko.yaml b/.ko.yaml index 7dd90c96e7..a16adb0c23 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.2@sha256:294b683cb724975bec92580e1e685676bd4b50bda910ddb8c51d4cabeaec77e6 + go.ate.dev/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 + go.ate.dev/substrate/cmd/atelet: gcr.io/distroless/base-debian13:latest@sha256:389cad21f73e4c37b94ffe5b13736d5a92bd5bd3c6c6b38c2be1c881e14ba2bd diff --git a/benchmarking/nighthawk-egress/traffic-driver.yaml.tmpl b/benchmarking/nighthawk-egress/traffic-driver.yaml.tmpl index 3e08db74af..478dd935eb 100644 --- a/benchmarking/nighthawk-egress/traffic-driver.yaml.tmpl +++ b/benchmarking/nighthawk-egress/traffic-driver.yaml.tmpl @@ -33,6 +33,6 @@ metadata: workload: traffic-driver spec: replicas: 1 - workerImage: ko://github.com/agent-substrate/substrate/cmd/ateworker-gvisor + workerImage: ko://go.ate.dev/substrate/cmd/ateworker-gvisor sandboxClasses: - name: gvisor diff --git a/benchmarking/workloads/manifests/glutton-durdir-data-template.yaml.tmpl b/benchmarking/workloads/manifests/glutton-durdir-data-template.yaml.tmpl index 5f8810126e..02ad6c98d7 100644 --- a/benchmarking/workloads/manifests/glutton-durdir-data-template.yaml.tmpl +++ b/benchmarking/workloads/manifests/glutton-durdir-data-template.yaml.tmpl @@ -24,7 +24,7 @@ workerSelector: workload: benchmark-worker containers: - name: glutton - image: ko://github.com/agent-substrate/substrate/cmd/benchmarking/glutton + image: ko://go.ate.dev/substrate/cmd/benchmarking/glutton command: - "/ko-app/glutton" - "--grpc-listen-addr=:80" diff --git a/benchmarking/workloads/manifests/glutton-durdir-full-template.yaml.tmpl b/benchmarking/workloads/manifests/glutton-durdir-full-template.yaml.tmpl index b8b6104bdb..6e738624ab 100644 --- a/benchmarking/workloads/manifests/glutton-durdir-full-template.yaml.tmpl +++ b/benchmarking/workloads/manifests/glutton-durdir-full-template.yaml.tmpl @@ -23,7 +23,7 @@ workerSelector: workload: benchmark-worker containers: - name: glutton - image: ko://github.com/agent-substrate/substrate/cmd/benchmarking/glutton + image: ko://go.ate.dev/substrate/cmd/benchmarking/glutton command: - "/ko-app/glutton" - "--grpc-listen-addr=:80" diff --git a/benchmarking/workloads/manifests/glutton-storage-template.yaml.tmpl b/benchmarking/workloads/manifests/glutton-storage-template.yaml.tmpl index 9246754e9b..c64e36e26a 100644 --- a/benchmarking/workloads/manifests/glutton-storage-template.yaml.tmpl +++ b/benchmarking/workloads/manifests/glutton-storage-template.yaml.tmpl @@ -29,7 +29,7 @@ volumes: storageClassName: ${STORAGE_CLASS_NAME} containers: - name: glutton-storage - image: ko://github.com/agent-substrate/substrate/cmd/benchmarking/glutton + image: ko://go.ate.dev/substrate/cmd/benchmarking/glutton command: - "/ko-app/glutton" - "--grpc-listen-addr=:80" diff --git a/benchmarking/workloads/manifests/glutton-template.yaml.tmpl b/benchmarking/workloads/manifests/glutton-template.yaml.tmpl index 04061540bf..8d26c6c643 100644 --- a/benchmarking/workloads/manifests/glutton-template.yaml.tmpl +++ b/benchmarking/workloads/manifests/glutton-template.yaml.tmpl @@ -24,7 +24,7 @@ workerSelector: workload: benchmark-worker containers: - name: glutton - image: ko://github.com/agent-substrate/substrate/cmd/benchmarking/glutton + image: ko://go.ate.dev/substrate/cmd/benchmarking/glutton # Port 80 is the only inbound port nftables forwards from the worker pod # into the actor sandbox (see cmd/ateworker-gvisor/main.go). HTTP/1.1 mode # lets the atenet router (which forwards HTTP/1.1 upstream) reach Ping. diff --git a/benchmarking/workloads/manifests/workloads.yaml.tmpl b/benchmarking/workloads/manifests/workloads.yaml.tmpl index d8043c4d76..e72b93d18a 100644 --- a/benchmarking/workloads/manifests/workloads.yaml.tmpl +++ b/benchmarking/workloads/manifests/workloads.yaml.tmpl @@ -35,7 +35,7 @@ spec: replicas: ${WORKER_COUNT} sandboxClasses: - name: ${SANDBOX_CLASS} - workerImage: ko://github.com/agent-substrate/substrate/cmd/ateworker-${SANDBOX_CLASS} + workerImage: ko://go.ate.dev/substrate/cmd/ateworker-${SANDBOX_CLASS} template: # Lets gvisor and microvm workers share a node tainted ate.dev/sandboxClass=shared. tolerations: diff --git a/cmd/ate-setup/internal/cmd/benchmarks.go b/cmd/ate-setup/internal/cmd/benchmarks.go index 5b7d48fba7..d92d1c4b6e 100644 --- a/cmd/ate-setup/internal/cmd/benchmarks.go +++ b/cmd/ate-setup/internal/cmd/benchmarks.go @@ -17,8 +17,8 @@ package cmd import ( "github.com/spf13/cobra" - "github.com/agent-substrate/substrate/cmd/ate-setup/internal/config" - "github.com/agent-substrate/substrate/cmd/ate-setup/internal/steps" + "go.ate.dev/substrate/cmd/ate-setup/internal/config" + "go.ate.dev/substrate/cmd/ate-setup/internal/steps" ) // The command paths the benchmark settings bind their flags to. Both accept diff --git a/cmd/ate-setup/internal/cmd/benchmarks_test.go b/cmd/ate-setup/internal/cmd/benchmarks_test.go index 00bdc9760a..731148618c 100644 --- a/cmd/ate-setup/internal/cmd/benchmarks_test.go +++ b/cmd/ate-setup/internal/cmd/benchmarks_test.go @@ -18,8 +18,8 @@ import ( "strings" "testing" - "github.com/agent-substrate/substrate/cmd/ate-setup/internal/config" - "github.com/agent-substrate/substrate/cmd/ate-setup/internal/steps" + "go.ate.dev/substrate/cmd/ate-setup/internal/config" + "go.ate.dev/substrate/cmd/ate-setup/internal/steps" ) // The benchmark settings reach three channels, so a rejection has to name the diff --git a/cmd/ate-setup/internal/cmd/create.go b/cmd/ate-setup/internal/cmd/create.go index e58f95b084..9c5c5c193e 100644 --- a/cmd/ate-setup/internal/cmd/create.go +++ b/cmd/ate-setup/internal/cmd/create.go @@ -19,7 +19,7 @@ import ( "github.com/spf13/cobra" - "github.com/agent-substrate/substrate/cmd/ate-setup/internal/steps" + "go.ate.dev/substrate/cmd/ate-setup/internal/steps" ) var createCmd = &cobra.Command{ diff --git a/cmd/ate-setup/internal/cmd/csi_test.go b/cmd/ate-setup/internal/cmd/csi_test.go index f2d03d6312..393a4dfc5d 100644 --- a/cmd/ate-setup/internal/cmd/csi_test.go +++ b/cmd/ate-setup/internal/cmd/csi_test.go @@ -21,9 +21,9 @@ import ( "github.com/spf13/cobra" - "github.com/agent-substrate/substrate/cmd/ate-setup/internal/steps" + "go.ate.dev/substrate/cmd/ate-setup/internal/steps" - "github.com/agent-substrate/substrate/cmd/ate-setup/internal/config" + "go.ate.dev/substrate/cmd/ate-setup/internal/config" ) // runRoot executes the command tree with args, quietly. diff --git a/cmd/ate-setup/internal/cmd/delete.go b/cmd/ate-setup/internal/cmd/delete.go index 60c9c03759..7bb9df466e 100644 --- a/cmd/ate-setup/internal/cmd/delete.go +++ b/cmd/ate-setup/internal/cmd/delete.go @@ -17,7 +17,7 @@ package cmd import ( "github.com/spf13/cobra" - "github.com/agent-substrate/substrate/cmd/ate-setup/internal/demos" + "go.ate.dev/substrate/cmd/ate-setup/internal/demos" ) var deleteCmd = &cobra.Command{ diff --git a/cmd/ate-setup/internal/cmd/demo.go b/cmd/ate-setup/internal/cmd/demo.go index e3dc080deb..49b882fe71 100644 --- a/cmd/ate-setup/internal/cmd/demo.go +++ b/cmd/ate-setup/internal/cmd/demo.go @@ -19,10 +19,10 @@ import ( "github.com/spf13/cobra" - "github.com/agent-substrate/substrate/cmd/ate-setup/internal/config" - "github.com/agent-substrate/substrate/cmd/ate-setup/internal/demos" + "go.ate.dev/substrate/cmd/ate-setup/internal/config" + "go.ate.dev/substrate/cmd/ate-setup/internal/demos" // Registers every bundled demo; see the package doc. - _ "github.com/agent-substrate/substrate/cmd/ate-setup/internal/demos/all" + _ "go.ate.dev/substrate/cmd/ate-setup/internal/demos/all" ) // demoArg is the short name a demo is addressed by on the command line: diff --git a/cmd/ate-setup/internal/cmd/deploy.go b/cmd/ate-setup/internal/cmd/deploy.go index 08ab2d7bb6..20cddbe891 100644 --- a/cmd/ate-setup/internal/cmd/deploy.go +++ b/cmd/ate-setup/internal/cmd/deploy.go @@ -17,8 +17,8 @@ package cmd import ( "github.com/spf13/cobra" - "github.com/agent-substrate/substrate/cmd/ate-setup/internal/config" - "github.com/agent-substrate/substrate/cmd/ate-setup/internal/steps" + "go.ate.dev/substrate/cmd/ate-setup/internal/config" + "go.ate.dev/substrate/cmd/ate-setup/internal/steps" ) var deployCmd = &cobra.Command{ diff --git a/cmd/ate-setup/internal/cmd/record_test.go b/cmd/ate-setup/internal/cmd/record_test.go index e9b2996728..0787e14669 100644 --- a/cmd/ate-setup/internal/cmd/record_test.go +++ b/cmd/ate-setup/internal/cmd/record_test.go @@ -23,8 +23,8 @@ import ( "sigs.k8s.io/yaml" - "github.com/agent-substrate/substrate/cmd/ate-setup/internal/config" - "github.com/agent-substrate/substrate/cmd/ate-setup/internal/steps" + "go.ate.dev/substrate/cmd/ate-setup/internal/config" + "go.ate.dev/substrate/cmd/ate-setup/internal/steps" ) // A record describes how the system was installed, so only the commands that diff --git a/cmd/ate-setup/internal/cmd/recordname_test.go b/cmd/ate-setup/internal/cmd/recordname_test.go index 25ead4161b..e1b82adca3 100644 --- a/cmd/ate-setup/internal/cmd/recordname_test.go +++ b/cmd/ate-setup/internal/cmd/recordname_test.go @@ -20,8 +20,8 @@ import ( "strings" "testing" - "github.com/agent-substrate/substrate/cmd/ate-setup/internal/config" - "github.com/agent-substrate/substrate/cmd/ate-setup/internal/steps" + "go.ate.dev/substrate/cmd/ate-setup/internal/config" + "go.ate.dev/substrate/cmd/ate-setup/internal/steps" ) // recordFor runs the record writer the way PersistentPreRunE does and returns diff --git a/cmd/ate-setup/internal/cmd/root.go b/cmd/ate-setup/internal/cmd/root.go index 33c23026e2..e08a206739 100644 --- a/cmd/ate-setup/internal/cmd/root.go +++ b/cmd/ate-setup/internal/cmd/root.go @@ -22,10 +22,10 @@ import ( "github.com/spf13/cobra" - "github.com/agent-substrate/substrate/cmd/ate-setup/internal/config" - "github.com/agent-substrate/substrate/cmd/ate-setup/internal/log" - "github.com/agent-substrate/substrate/cmd/ate-setup/internal/steps" - "github.com/agent-substrate/substrate/internal/version" + "go.ate.dev/substrate/cmd/ate-setup/internal/config" + "go.ate.dev/substrate/cmd/ate-setup/internal/log" + "go.ate.dev/substrate/cmd/ate-setup/internal/steps" + "go.ate.dev/substrate/internal/version" ) // Inputs that select where configuration comes from, as opposed to the diff --git a/cmd/ate-setup/internal/cmd/setup.go b/cmd/ate-setup/internal/cmd/setup.go index 09506b28c8..6b14b92b4a 100644 --- a/cmd/ate-setup/internal/cmd/setup.go +++ b/cmd/ate-setup/internal/cmd/setup.go @@ -17,7 +17,7 @@ package cmd import ( "github.com/spf13/cobra" - "github.com/agent-substrate/substrate/cmd/ate-setup/internal/steps" + "go.ate.dev/substrate/cmd/ate-setup/internal/steps" ) var setupCmd = &cobra.Command{ diff --git a/cmd/ate-setup/internal/config/config.go b/cmd/ate-setup/internal/config/config.go index c24aa5f1b3..f687bab32b 100644 --- a/cmd/ate-setup/internal/config/config.go +++ b/cmd/ate-setup/internal/config/config.go @@ -32,9 +32,9 @@ import ( "k8s.io/apimachinery/pkg/util/validation" - "github.com/agent-substrate/substrate/cmd/ate-setup/internal/images" - "github.com/agent-substrate/substrate/internal/installdefaults" - "github.com/agent-substrate/substrate/pkg/postgressetup" + "go.ate.dev/substrate/cmd/ate-setup/internal/images" + "go.ate.dev/substrate/internal/installdefaults" + "go.ate.dev/substrate/pkg/postgressetup" ) // Enumerated values for the install-shaping flags. diff --git a/cmd/ate-setup/internal/config/config_test.go b/cmd/ate-setup/internal/config/config_test.go index 584b6fbf35..387c0313c8 100644 --- a/cmd/ate-setup/internal/config/config_test.go +++ b/cmd/ate-setup/internal/config/config_test.go @@ -26,8 +26,8 @@ import ( "github.com/spf13/pflag" - "github.com/agent-substrate/substrate/cmd/ate-setup/internal/images" - "github.com/agent-substrate/substrate/internal/preview" + "go.ate.dev/substrate/cmd/ate-setup/internal/images" + "go.ate.dev/substrate/internal/preview" ) // loadEnv isolates Load from the ambient environment. diff --git a/cmd/ate-setup/internal/config/credentials.go b/cmd/ate-setup/internal/config/credentials.go index 33e64132fa..d0c14f601e 100644 --- a/cmd/ate-setup/internal/config/credentials.go +++ b/cmd/ate-setup/internal/config/credentials.go @@ -21,7 +21,7 @@ import ( "os/exec" "time" - "github.com/agent-substrate/substrate/cmd/ate-setup/internal/log" + "go.ate.dev/substrate/cmd/ate-setup/internal/log" ) // EnsureClusterCredentials fetches GKE credentials when the configuration diff --git a/cmd/ate-setup/internal/config/document.go b/cmd/ate-setup/internal/config/document.go index e2e65fdeeb..e02842386c 100644 --- a/cmd/ate-setup/internal/config/document.go +++ b/cmd/ate-setup/internal/config/document.go @@ -22,7 +22,7 @@ import ( "sigs.k8s.io/yaml" - "github.com/agent-substrate/substrate/internal/version" + "go.ate.dev/substrate/internal/version" ) // Outcome is how the run that produced a document ended. diff --git a/cmd/ate-setup/internal/config/load.go b/cmd/ate-setup/internal/config/load.go index a339f0b287..301f89a9b3 100644 --- a/cmd/ate-setup/internal/config/load.go +++ b/cmd/ate-setup/internal/config/load.go @@ -23,10 +23,10 @@ import ( "github.com/spf13/pflag" - "github.com/agent-substrate/substrate/cmd/ate-setup/internal/images" - "github.com/agent-substrate/substrate/internal/installdefaults" - "github.com/agent-substrate/substrate/internal/oidcdiscovery" - "github.com/agent-substrate/substrate/internal/preview" + "go.ate.dev/substrate/cmd/ate-setup/internal/images" + "go.ate.dev/substrate/internal/installdefaults" + "go.ate.dev/substrate/internal/oidcdiscovery" + "go.ate.dev/substrate/internal/preview" ) // ConfigPathEnv names the configuration document when --config is not given. diff --git a/cmd/ate-setup/internal/config/setting.go b/cmd/ate-setup/internal/config/setting.go index 48c3fdeda2..664a40967f 100644 --- a/cmd/ate-setup/internal/config/setting.go +++ b/cmd/ate-setup/internal/config/setting.go @@ -24,7 +24,7 @@ import ( "github.com/spf13/pflag" - "github.com/agent-substrate/substrate/internal/installdefaults" + "go.ate.dev/substrate/internal/installdefaults" ) // ValueKind is the type of a setting's value. diff --git a/cmd/ate-setup/internal/demos/all/all.go b/cmd/ate-setup/internal/demos/all/all.go index 6fa8022b74..49fec9da2c 100644 --- a/cmd/ate-setup/internal/demos/all/all.go +++ b/cmd/ate-setup/internal/demos/all/all.go @@ -21,14 +21,14 @@ package all import ( - _ "github.com/agent-substrate/substrate/cmd/ate-setup/internal/demos/autoscaledworkerpool" - _ "github.com/agent-substrate/substrate/cmd/ate-setup/internal/demos/claudemultiplex" - _ "github.com/agent-substrate/substrate/cmd/ate-setup/internal/demos/counter" - _ "github.com/agent-substrate/substrate/cmd/ate-setup/internal/demos/countermicrovm" - _ "github.com/agent-substrate/substrate/cmd/ate-setup/internal/demos/egress" - _ "github.com/agent-substrate/substrate/cmd/ate-setup/internal/demos/egressmicrovm" - _ "github.com/agent-substrate/substrate/cmd/ate-setup/internal/demos/jupyter" - _ "github.com/agent-substrate/substrate/cmd/ate-setup/internal/demos/multitemplate" - _ "github.com/agent-substrate/substrate/cmd/ate-setup/internal/demos/parking" - _ "github.com/agent-substrate/substrate/cmd/ate-setup/internal/demos/sandbox" + _ "go.ate.dev/substrate/cmd/ate-setup/internal/demos/autoscaledworkerpool" + _ "go.ate.dev/substrate/cmd/ate-setup/internal/demos/claudemultiplex" + _ "go.ate.dev/substrate/cmd/ate-setup/internal/demos/counter" + _ "go.ate.dev/substrate/cmd/ate-setup/internal/demos/countermicrovm" + _ "go.ate.dev/substrate/cmd/ate-setup/internal/demos/egress" + _ "go.ate.dev/substrate/cmd/ate-setup/internal/demos/egressmicrovm" + _ "go.ate.dev/substrate/cmd/ate-setup/internal/demos/jupyter" + _ "go.ate.dev/substrate/cmd/ate-setup/internal/demos/multitemplate" + _ "go.ate.dev/substrate/cmd/ate-setup/internal/demos/parking" + _ "go.ate.dev/substrate/cmd/ate-setup/internal/demos/sandbox" ) diff --git a/cmd/ate-setup/internal/demos/all/all_test.go b/cmd/ate-setup/internal/demos/all/all_test.go index f3e640f00b..61ed686895 100644 --- a/cmd/ate-setup/internal/demos/all/all_test.go +++ b/cmd/ate-setup/internal/demos/all/all_test.go @@ -19,9 +19,9 @@ import ( "path/filepath" "testing" - "github.com/agent-substrate/substrate/cmd/ate-setup/internal/config" - "github.com/agent-substrate/substrate/cmd/ate-setup/internal/demos" - _ "github.com/agent-substrate/substrate/cmd/ate-setup/internal/demos/all" + "go.ate.dev/substrate/cmd/ate-setup/internal/config" + "go.ate.dev/substrate/cmd/ate-setup/internal/demos" + _ "go.ate.dev/substrate/cmd/ate-setup/internal/demos/all" ) // nonDemoPackages are the directories under internal/demos that are support diff --git a/cmd/ate-setup/internal/demos/autoscaledworkerpool/autoscaledworkerpool.go b/cmd/ate-setup/internal/demos/autoscaledworkerpool/autoscaledworkerpool.go index 5f20d048a7..4b246f6b16 100644 --- a/cmd/ate-setup/internal/demos/autoscaledworkerpool/autoscaledworkerpool.go +++ b/cmd/ate-setup/internal/demos/autoscaledworkerpool/autoscaledworkerpool.go @@ -23,11 +23,11 @@ package autoscaledworkerpool import ( "context" - "github.com/agent-substrate/substrate/cmd/ate-setup/internal/demos" - "github.com/agent-substrate/substrate/cmd/ate-setup/internal/kube" - "github.com/agent-substrate/substrate/cmd/ate-setup/internal/log" - "github.com/agent-substrate/substrate/cmd/ate-setup/internal/steps" - "github.com/agent-substrate/substrate/internal/resources" + "go.ate.dev/substrate/cmd/ate-setup/internal/demos" + "go.ate.dev/substrate/cmd/ate-setup/internal/kube" + "go.ate.dev/substrate/cmd/ate-setup/internal/log" + "go.ate.dev/substrate/cmd/ate-setup/internal/steps" + "go.ate.dev/substrate/internal/resources" ) // namespace holds the demo workload, its prometheus-adapter, and the HPA; diff --git a/cmd/ate-setup/internal/demos/claudemultiplex/claudemultiplex.go b/cmd/ate-setup/internal/demos/claudemultiplex/claudemultiplex.go index 0555d79417..f8b7db51b3 100644 --- a/cmd/ate-setup/internal/demos/claudemultiplex/claudemultiplex.go +++ b/cmd/ate-setup/internal/demos/claudemultiplex/claudemultiplex.go @@ -24,11 +24,11 @@ import ( "context" "fmt" - "github.com/agent-substrate/substrate/cmd/ate-setup/internal/config" - "github.com/agent-substrate/substrate/cmd/ate-setup/internal/demos" - "github.com/agent-substrate/substrate/cmd/ate-setup/internal/images" - "github.com/agent-substrate/substrate/cmd/ate-setup/internal/steps" - "github.com/agent-substrate/substrate/internal/resources" + "go.ate.dev/substrate/cmd/ate-setup/internal/config" + "go.ate.dev/substrate/cmd/ate-setup/internal/demos" + "go.ate.dev/substrate/cmd/ate-setup/internal/images" + "go.ate.dev/substrate/cmd/ate-setup/internal/steps" + "go.ate.dev/substrate/internal/resources" ) const ( diff --git a/cmd/ate-setup/internal/demos/claudemultiplex/claudemultiplex_test.go b/cmd/ate-setup/internal/demos/claudemultiplex/claudemultiplex_test.go index fd16cf12d4..a3b15a609e 100644 --- a/cmd/ate-setup/internal/demos/claudemultiplex/claudemultiplex_test.go +++ b/cmd/ate-setup/internal/demos/claudemultiplex/claudemultiplex_test.go @@ -17,8 +17,8 @@ package claudemultiplex import ( "testing" - "github.com/agent-substrate/substrate/cmd/ate-setup/internal/demos" - "github.com/agent-substrate/substrate/cmd/ate-setup/internal/demos/demotest" + "go.ate.dev/substrate/cmd/ate-setup/internal/demos" + "go.ate.dev/substrate/cmd/ate-setup/internal/demos/demotest" ) // TestRenderManifests covers this demo's manifests, which the shared render diff --git a/cmd/ate-setup/internal/demos/counter/counter.go b/cmd/ate-setup/internal/demos/counter/counter.go index 39f5848036..a424529a2b 100644 --- a/cmd/ate-setup/internal/demos/counter/counter.go +++ b/cmd/ate-setup/internal/demos/counter/counter.go @@ -20,10 +20,10 @@ package counter import ( "context" - "github.com/agent-substrate/substrate/cmd/ate-setup/internal/config" - "github.com/agent-substrate/substrate/cmd/ate-setup/internal/demos" - "github.com/agent-substrate/substrate/cmd/ate-setup/internal/steps" - "github.com/agent-substrate/substrate/internal/resources" + "go.ate.dev/substrate/cmd/ate-setup/internal/config" + "go.ate.dev/substrate/cmd/ate-setup/internal/demos" + "go.ate.dev/substrate/cmd/ate-setup/internal/steps" + "go.ate.dev/substrate/internal/resources" ) const ( diff --git a/cmd/ate-setup/internal/demos/counter/counter_test.go b/cmd/ate-setup/internal/demos/counter/counter_test.go index ada06a8977..05630230c2 100644 --- a/cmd/ate-setup/internal/demos/counter/counter_test.go +++ b/cmd/ate-setup/internal/demos/counter/counter_test.go @@ -19,9 +19,9 @@ import ( "strings" "testing" - "github.com/agent-substrate/substrate/cmd/ate-setup/internal/demos" - "github.com/agent-substrate/substrate/cmd/ate-setup/internal/demos/demotest" - "github.com/agent-substrate/substrate/internal/resources" + "go.ate.dev/substrate/cmd/ate-setup/internal/demos" + "go.ate.dev/substrate/cmd/ate-setup/internal/demos/demotest" + "go.ate.dev/substrate/internal/resources" ) // TestExternalVolumeRenders covers the substitution branch of the counter diff --git a/cmd/ate-setup/internal/demos/countermicrovm/countermicrovm.go b/cmd/ate-setup/internal/demos/countermicrovm/countermicrovm.go index e23225370a..52a5036e62 100644 --- a/cmd/ate-setup/internal/demos/countermicrovm/countermicrovm.go +++ b/cmd/ate-setup/internal/demos/countermicrovm/countermicrovm.go @@ -20,9 +20,9 @@ package countermicrovm import ( - "github.com/agent-substrate/substrate/cmd/ate-setup/internal/demos" - "github.com/agent-substrate/substrate/cmd/ate-setup/internal/steps" - "github.com/agent-substrate/substrate/internal/resources" + "go.ate.dev/substrate/cmd/ate-setup/internal/demos" + "go.ate.dev/substrate/cmd/ate-setup/internal/steps" + "go.ate.dev/substrate/internal/resources" ) // namespace is the pool's k8s namespace; it doubles as the atespace holding diff --git a/cmd/ate-setup/internal/demos/demos.go b/cmd/ate-setup/internal/demos/demos.go index 4bd90dbd1a..ff99a65f92 100644 --- a/cmd/ate-setup/internal/demos/demos.go +++ b/cmd/ate-setup/internal/demos/demos.go @@ -27,8 +27,8 @@ import ( "sort" "strings" - "github.com/agent-substrate/substrate/cmd/ate-setup/internal/config" - "github.com/agent-substrate/substrate/cmd/ate-setup/internal/steps" + "go.ate.dev/substrate/cmd/ate-setup/internal/config" + "go.ate.dev/substrate/cmd/ate-setup/internal/steps" ) // Demo is one installable example workload. diff --git a/cmd/ate-setup/internal/demos/demotest/demotest.go b/cmd/ate-setup/internal/demos/demotest/demotest.go index 66395ed54e..9c5550caf4 100644 --- a/cmd/ate-setup/internal/demos/demotest/demotest.go +++ b/cmd/ate-setup/internal/demos/demotest/demotest.go @@ -22,10 +22,10 @@ import ( "strings" "testing" - "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/steps" - "github.com/agent-substrate/substrate/internal/resources" + "go.ate.dev/substrate/cmd/ate-setup/internal/config" + "go.ate.dev/substrate/cmd/ate-setup/internal/kube" + "go.ate.dev/substrate/cmd/ate-setup/internal/steps" + "go.ate.dev/substrate/internal/resources" ) // Env builds an Env with no cluster connection. The render paths only read diff --git a/cmd/ate-setup/internal/demos/egress/egress.go b/cmd/ate-setup/internal/demos/egress/egress.go index b4f4e926cd..1766301125 100644 --- a/cmd/ate-setup/internal/demos/egress/egress.go +++ b/cmd/ate-setup/internal/demos/egress/egress.go @@ -22,9 +22,9 @@ package egress import ( - "github.com/agent-substrate/substrate/cmd/ate-setup/internal/demos" - "github.com/agent-substrate/substrate/cmd/ate-setup/internal/steps" - "github.com/agent-substrate/substrate/internal/resources" + "go.ate.dev/substrate/cmd/ate-setup/internal/demos" + "go.ate.dev/substrate/cmd/ate-setup/internal/steps" + "go.ate.dev/substrate/internal/resources" ) // namespace is the pool's k8s namespace; it doubles as the atespace holding diff --git a/cmd/ate-setup/internal/demos/egressmicrovm/egressmicrovm.go b/cmd/ate-setup/internal/demos/egressmicrovm/egressmicrovm.go index 67f8c4342a..7c8146f494 100644 --- a/cmd/ate-setup/internal/demos/egressmicrovm/egressmicrovm.go +++ b/cmd/ate-setup/internal/demos/egressmicrovm/egressmicrovm.go @@ -23,9 +23,9 @@ package egressmicrovm import ( - "github.com/agent-substrate/substrate/cmd/ate-setup/internal/demos" - "github.com/agent-substrate/substrate/cmd/ate-setup/internal/steps" - "github.com/agent-substrate/substrate/internal/resources" + "go.ate.dev/substrate/cmd/ate-setup/internal/demos" + "go.ate.dev/substrate/cmd/ate-setup/internal/steps" + "go.ate.dev/substrate/internal/resources" ) // namespace is the pool's k8s namespace; it doubles as the atespace holding diff --git a/cmd/ate-setup/internal/demos/jupyter/jupyter.go b/cmd/ate-setup/internal/demos/jupyter/jupyter.go index b18bb4fddc..6dd6057354 100644 --- a/cmd/ate-setup/internal/demos/jupyter/jupyter.go +++ b/cmd/ate-setup/internal/demos/jupyter/jupyter.go @@ -18,9 +18,9 @@ package jupyter import ( - "github.com/agent-substrate/substrate/cmd/ate-setup/internal/demos" - "github.com/agent-substrate/substrate/cmd/ate-setup/internal/steps" - "github.com/agent-substrate/substrate/internal/resources" + "go.ate.dev/substrate/cmd/ate-setup/internal/demos" + "go.ate.dev/substrate/cmd/ate-setup/internal/steps" + "go.ate.dev/substrate/internal/resources" ) // namespace is the pool's k8s namespace; it doubles as the atespace holding diff --git a/cmd/ate-setup/internal/demos/multitemplate/multitemplate.go b/cmd/ate-setup/internal/demos/multitemplate/multitemplate.go index 8f36dbb91b..45dd1b9085 100644 --- a/cmd/ate-setup/internal/demos/multitemplate/multitemplate.go +++ b/cmd/ate-setup/internal/demos/multitemplate/multitemplate.go @@ -17,9 +17,9 @@ package multitemplate import ( - "github.com/agent-substrate/substrate/cmd/ate-setup/internal/demos" - "github.com/agent-substrate/substrate/cmd/ate-setup/internal/steps" - "github.com/agent-substrate/substrate/internal/resources" + "go.ate.dev/substrate/cmd/ate-setup/internal/demos" + "go.ate.dev/substrate/cmd/ate-setup/internal/steps" + "go.ate.dev/substrate/internal/resources" ) func init() { diff --git a/cmd/ate-setup/internal/demos/parking/parking.go b/cmd/ate-setup/internal/demos/parking/parking.go index e002f99340..80d24d0474 100644 --- a/cmd/ate-setup/internal/demos/parking/parking.go +++ b/cmd/ate-setup/internal/demos/parking/parking.go @@ -17,9 +17,9 @@ package parking import ( - "github.com/agent-substrate/substrate/cmd/ate-setup/internal/demos" - "github.com/agent-substrate/substrate/cmd/ate-setup/internal/steps" - "github.com/agent-substrate/substrate/internal/resources" + "go.ate.dev/substrate/cmd/ate-setup/internal/demos" + "go.ate.dev/substrate/cmd/ate-setup/internal/steps" + "go.ate.dev/substrate/internal/resources" ) // namespace is the pool's k8s namespace; it doubles as the atespace holding diff --git a/cmd/ate-setup/internal/demos/render.go b/cmd/ate-setup/internal/demos/render.go index d5061fd43e..941d1d3dc1 100644 --- a/cmd/ate-setup/internal/demos/render.go +++ b/cmd/ate-setup/internal/demos/render.go @@ -15,8 +15,8 @@ package demos import ( - "github.com/agent-substrate/substrate/cmd/ate-setup/internal/render" - "github.com/agent-substrate/substrate/cmd/ate-setup/internal/steps" + "go.ate.dev/substrate/cmd/ate-setup/internal/render" + "go.ate.dev/substrate/cmd/ate-setup/internal/steps" ) // bucketNamePlaceholder is substituted into every demo template with the diff --git a/cmd/ate-setup/internal/demos/render_test.go b/cmd/ate-setup/internal/demos/render_test.go index 144ea11e77..ebd48583bb 100644 --- a/cmd/ate-setup/internal/demos/render_test.go +++ b/cmd/ate-setup/internal/demos/render_test.go @@ -19,9 +19,9 @@ package demos_test import ( "testing" - "github.com/agent-substrate/substrate/cmd/ate-setup/internal/demos" - _ "github.com/agent-substrate/substrate/cmd/ate-setup/internal/demos/all" - "github.com/agent-substrate/substrate/cmd/ate-setup/internal/demos/demotest" + "go.ate.dev/substrate/cmd/ate-setup/internal/demos" + _ "go.ate.dev/substrate/cmd/ate-setup/internal/demos/all" + "go.ate.dev/substrate/cmd/ate-setup/internal/demos/demotest" ) // TestDemoTemplatesRender guards against drift between the demo templates and diff --git a/cmd/ate-setup/internal/demos/sandbox/sandbox.go b/cmd/ate-setup/internal/demos/sandbox/sandbox.go index ca412d489c..2b8ceae726 100644 --- a/cmd/ate-setup/internal/demos/sandbox/sandbox.go +++ b/cmd/ate-setup/internal/demos/sandbox/sandbox.go @@ -17,9 +17,9 @@ package sandbox import ( - "github.com/agent-substrate/substrate/cmd/ate-setup/internal/demos" - "github.com/agent-substrate/substrate/cmd/ate-setup/internal/steps" - "github.com/agent-substrate/substrate/internal/resources" + "go.ate.dev/substrate/cmd/ate-setup/internal/demos" + "go.ate.dev/substrate/cmd/ate-setup/internal/steps" + "go.ate.dev/substrate/internal/resources" ) // namespace is the pool's k8s namespace; it doubles as the atespace holding diff --git a/cmd/ate-setup/internal/demos/substrate.go b/cmd/ate-setup/internal/demos/substrate.go index 2c129d0e64..e5812d847b 100644 --- a/cmd/ate-setup/internal/demos/substrate.go +++ b/cmd/ate-setup/internal/demos/substrate.go @@ -18,11 +18,11 @@ import ( "context" "time" - "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/log" - "github.com/agent-substrate/substrate/cmd/ate-setup/internal/steps" - "github.com/agent-substrate/substrate/internal/resources" + "go.ate.dev/substrate/cmd/ate-setup/internal/config" + "go.ate.dev/substrate/cmd/ate-setup/internal/kube" + "go.ate.dev/substrate/cmd/ate-setup/internal/log" + "go.ate.dev/substrate/cmd/ate-setup/internal/steps" + "go.ate.dev/substrate/internal/resources" ) // MicroVMGoldenTimeout is the golden-snapshot budget the micro-VM demos set as diff --git a/cmd/ate-setup/internal/images/images.go b/cmd/ate-setup/internal/images/images.go index 0cfa2e4354..6cc862dd7c 100644 --- a/cmd/ate-setup/internal/images/images.go +++ b/cmd/ate-setup/internal/images/images.go @@ -28,7 +28,7 @@ import ( // ModulePath is this repository's module path. A ko:// reference is it joined // with a component's package. -const ModulePath = "github.com/agent-substrate/substrate" +const ModulePath = "go.ate.dev/substrate" // Components is every package the installer can deploy a published image for, // as an import path within the module. diff --git a/cmd/ate-setup/internal/images/images_test.go b/cmd/ate-setup/internal/images/images_test.go index 523e01950f..293da7e64d 100644 --- a/cmd/ate-setup/internal/images/images_test.go +++ b/cmd/ate-setup/internal/images/images_test.go @@ -25,8 +25,8 @@ import ( "strings" "testing" - "github.com/agent-substrate/substrate/cmd/ate-setup/internal/config" - "github.com/agent-substrate/substrate/cmd/ate-setup/internal/images" + "go.ate.dev/substrate/cmd/ate-setup/internal/config" + "go.ate.dev/substrate/cmd/ate-setup/internal/images" ) // installTrees are the directories the installer reads manifests from. diff --git a/cmd/ate-setup/internal/images/prebuilt_test.go b/cmd/ate-setup/internal/images/prebuilt_test.go index cbf6acc74c..1264caa09a 100644 --- a/cmd/ate-setup/internal/images/prebuilt_test.go +++ b/cmd/ate-setup/internal/images/prebuilt_test.go @@ -25,9 +25,9 @@ import ( "strings" "testing" - "github.com/agent-substrate/substrate/cmd/ate-setup/internal/config" - "github.com/agent-substrate/substrate/cmd/ate-setup/internal/images" - "github.com/agent-substrate/substrate/cmd/ate-setup/internal/kube" + "go.ate.dev/substrate/cmd/ate-setup/internal/config" + "go.ate.dev/substrate/cmd/ate-setup/internal/images" + "go.ate.dev/substrate/cmd/ate-setup/internal/kube" ) // testSource is the repo/tag every rewrite test resolves against. @@ -68,29 +68,29 @@ func TestPrebuiltResolveBytes(t *testing.T) { }{ { name: "bare scalar", - in: " image: ko://github.com/agent-substrate/substrate/cmd/ateapi\n", + in: " image: ko://go.ate.dev/substrate/cmd/ateapi\n", want: " image: example.com/substrate/ateapi:v1.2.3" + digestSuffix + "\n", }, { name: "double quoted", - in: `image: "ko://github.com/agent-substrate/substrate/cmd/atelet"`, + in: `image: "ko://go.ate.dev/substrate/cmd/atelet"`, want: `image: "example.com/substrate/atelet:v1.2.3` + digestSuffix + `"`, }, { name: "single quoted", - in: `image: 'ko://github.com/agent-substrate/substrate/cmd/atenet'`, + in: `image: 'ko://go.ate.dev/substrate/cmd/atenet'`, want: `image: 'example.com/substrate/atenet:v1.2.3` + digestSuffix + `'`, }, { name: "flow sequence", - in: `images: [ko://github.com/agent-substrate/substrate/demos/counter, other]`, + in: `images: [ko://go.ate.dev/substrate/demos/counter, other]`, want: `images: [example.com/substrate/counter:v1.2.3` + digestSuffix + `, other]`, }, { // The reference is a plain CRD field here, not a pod spec, which is // why the rewrite cannot be driven off a Kubernetes schema. name: "CRD field", - in: "spec:\n workerImage: ko://github.com/agent-substrate/substrate/cmd/ateworker-gvisor\n", + in: "spec:\n workerImage: ko://go.ate.dev/substrate/cmd/ateworker-gvisor\n", want: "spec:\n workerImage: example.com/substrate/ateworker-gvisor:v1.2.3" + digestSuffix + "\n", }, { @@ -99,17 +99,17 @@ func TestPrebuiltResolveBytes(t *testing.T) { // expected output is also what catches a redundant lookup. name: "tag already carries a digest", src: images.Source{Repo: "example.com/substrate", Tag: "v1.2.3" + digestSuffix}, - in: "image: ko://github.com/agent-substrate/substrate/cmd/ateapi\n", + in: "image: ko://go.ate.dev/substrate/cmd/ateapi\n", want: "image: example.com/substrate/ateapi:v1.2.3" + digestSuffix + "\n", }, { name: "nested package maps to its image name", - in: "image: ko://github.com/agent-substrate/substrate/demos/multi-template/fspersist\n", + in: "image: ko://go.ate.dev/substrate/demos/multi-template/fspersist\n", want: "image: example.com/substrate/fspersist:v1.2.3" + digestSuffix + "\n", }, { name: "two references on one line", - in: "a: ko://github.com/agent-substrate/substrate/cmd/ateapi, b: ko://github.com/agent-substrate/substrate/demos/egress\n", + in: "a: ko://go.ate.dev/substrate/cmd/ateapi, b: ko://go.ate.dev/substrate/demos/egress\n", want: "a: example.com/substrate/ateapi:v1.2.3" + digestSuffix + ", b: example.com/substrate/egress:v1.2.3" + digestSuffix + "\n", }, { @@ -133,14 +133,14 @@ func TestPrebuiltResolveBytes(t *testing.T) { // message and nothing else: the truncation cannot collide with a // listed package, because every one of those ends before the "$". name: "unexpanded placeholder", - in: "image: ko://github.com/agent-substrate/substrate/cmd/ateworker-${SANDBOX_CLASS}\n", + in: "image: ko://go.ate.dev/substrate/cmd/ateworker-${SANDBOX_CLASS}\n", error: "cmd/ateworker-${SANDBOX_CLASS has no published image", }, { // A reference extending a known one must not inherit its image. A // literal replacement would leave "-sidecar" dangling on the tag. name: "reference extending a known one", - in: "image: ko://github.com/agent-substrate/substrate/cmd/atelet-sidecar\n", + in: "image: ko://go.ate.dev/substrate/cmd/atelet-sidecar\n", error: "cmd/atelet-sidecar has no published image", }, { @@ -168,20 +168,20 @@ func TestPrebuiltResolveBytes(t *testing.T) { }, { name: "unknown component", - in: "image: ko://github.com/agent-substrate/substrate/cmd/notacomponent\n", + in: "image: ko://go.ate.dev/substrate/cmd/notacomponent\n", error: "cmd/notacomponent has no published image", }, { // The e2e fixtures are not part of an install, so a manifest // reaching the installer with one is a mistake worth reporting. name: "e2e fixture is not installable", - in: "image: ko://github.com/agent-substrate/substrate/internal/e2e/fixtures/probe\n", + in: "image: ko://go.ate.dev/substrate/internal/e2e/fixtures/probe\n", error: "e2e/fixtures/probe has no published image", }, { // Every unmappable reference is named, not just the first. name: "all failures are reported", - in: "a: ko://github.com/agent-substrate/substrate/cmd/one\nb: ko://github.com/agent-substrate/substrate/cmd/two\n", + in: "a: ko://go.ate.dev/substrate/cmd/one\nb: ko://go.ate.dev/substrate/cmd/two\n", error: "cmd/two has no published image", }, } @@ -217,9 +217,9 @@ func TestPrebuiltResolveBytes(t *testing.T) { // Every unmappable reference is reported at once, so a manifest with several // problems takes one install attempt to diagnose rather than several. func TestPrebuiltReportsEveryFailure(t *testing.T) { - in := "a: ko://github.com/agent-substrate/substrate/cmd/ateworker-${SANDBOX_CLASS}\n" + + in := "a: ko://go.ate.dev/substrate/cmd/ateworker-${SANDBOX_CLASS}\n" + "b: ko://github.com/example/other/cmd/thing\n" + - "c: ko://github.com/agent-substrate/substrate/cmd/nope\n" + "c: ko://go.ate.dev/substrate/cmd/nope\n" _, err := images.NewPrebuilt(testSource, newStubRegistry().digest). ResolveBytes(context.Background(), []byte(in)) @@ -235,7 +235,7 @@ func TestPrebuiltReportsEveryFailure(t *testing.T) { // A repeated bad reference is one problem, not one per occurrence. func TestPrebuiltDeduplicatesFailures(t *testing.T) { - ref := "image: ko://github.com/agent-substrate/substrate/cmd/nope\n" + ref := "image: ko://go.ate.dev/substrate/cmd/nope\n" _, err := images.NewPrebuilt(testSource, newStubRegistry().digest). ResolveBytes(context.Background(), []byte(ref+ref+ref)) if err == nil { @@ -253,9 +253,9 @@ func TestPrebuiltLooksUpEachImageOnce(t *testing.T) { registry := newStubRegistry() resolver := images.NewPrebuilt(testSource, registry.digest) - in := "a: ko://github.com/agent-substrate/substrate/cmd/ateapi\n" + - "b: ko://github.com/agent-substrate/substrate/cmd/atelet\n" + - "c: ko://github.com/agent-substrate/substrate/cmd/ateapi\n" + in := "a: ko://go.ate.dev/substrate/cmd/ateapi\n" + + "b: ko://go.ate.dev/substrate/cmd/atelet\n" + + "c: ko://go.ate.dev/substrate/cmd/ateapi\n" for range 2 { if _, err := resolver.ResolveBytes(context.Background(), []byte(in)); err != nil { t.Fatalf("ResolveBytes() error = %v", err) @@ -278,8 +278,8 @@ func TestPrebuiltReportsDigestFailures(t *testing.T) { registry := newStubRegistry() registry.err = errors.New("UNAUTHORIZED") - in := "a: ko://github.com/agent-substrate/substrate/cmd/ateapi\n" + - "b: ko://github.com/agent-substrate/substrate/cmd/atelet\n" + in := "a: ko://go.ate.dev/substrate/cmd/ateapi\n" + + "b: ko://go.ate.dev/substrate/cmd/atelet\n" got, err := images.NewPrebuilt(testSource, registry.digest). ResolveBytes(context.Background(), []byte(in)) if err == nil { diff --git a/cmd/ate-setup/internal/ko/ko.go b/cmd/ate-setup/internal/ko/ko.go index 9c64428929..5b6fea21e4 100644 --- a/cmd/ate-setup/internal/ko/ko.go +++ b/cmd/ate-setup/internal/ko/ko.go @@ -33,11 +33,11 @@ import ( "strings" "time" - "github.com/agent-substrate/substrate/cmd/ate-setup/internal/log" + "go.ate.dev/substrate/cmd/ate-setup/internal/log" ) // versionPkg is the package whose Version variable receives the build stamp. -const versionPkg = "github.com/agent-substrate/substrate/internal/version" +const versionPkg = "go.ate.dev/substrate/internal/version" // baseImportPaths publishes each image as the last element of its import path, // so cmd/atelet becomes $KO_DOCKER_REPO/atelet. diff --git a/cmd/ate-setup/internal/ko/ko_test.go b/cmd/ate-setup/internal/ko/ko_test.go index 911eed5bb3..dfe5d250af 100644 --- a/cmd/ate-setup/internal/ko/ko_test.go +++ b/cmd/ate-setup/internal/ko/ko_test.go @@ -38,13 +38,13 @@ func TestArgsAlwaysRequestBaseImportPaths(t *testing.T) { }{ {"resolve", r.args("resolve", "-f", "manifests/ate-install")}, {"resolve from stdin", r.args("resolve", "-f", "-")}, - {"build", r.args("build", "github.com/agent-substrate/substrate/cmd/ateworker-gvisor")}, + {"build", r.args("build", "go.ate.dev/substrate/cmd/ateworker-gvisor")}, } { t.Run(tc.name, func(t *testing.T) { if !slices.Contains(tc.args, "--base-import-paths") { t.Errorf("args = %v, want --base-import-paths", tc.args) } - if !slices.Contains(tc.args, "--ldflags=-X=github.com/agent-substrate/substrate/internal/version.Version=v0.0.0-test") { + if !slices.Contains(tc.args, "--ldflags=-X=go.ate.dev/substrate/internal/version.Version=v0.0.0-test") { t.Errorf("args = %v, want the version stamp", tc.args) } }) diff --git a/cmd/ate-setup/internal/kube/apply.go b/cmd/ate-setup/internal/kube/apply.go index f0d7f5f502..9d92dbb08f 100644 --- a/cmd/ate-setup/internal/kube/apply.go +++ b/cmd/ate-setup/internal/kube/apply.go @@ -30,7 +30,7 @@ import ( "k8s.io/apimachinery/pkg/util/wait" "k8s.io/client-go/util/retry" - "github.com/agent-substrate/substrate/cmd/ate-setup/internal/log" + "go.ate.dev/substrate/cmd/ate-setup/internal/log" ) // crdKind is the kind whose creation invalidates discovery for later objects. diff --git a/cmd/ate-setup/internal/kube/capool.go b/cmd/ate-setup/internal/kube/capool.go index 15bc9f60af..17b22d0833 100644 --- a/cmd/ate-setup/internal/kube/capool.go +++ b/cmd/ate-setup/internal/kube/capool.go @@ -20,7 +20,7 @@ import ( "encoding/pem" "fmt" - "github.com/agent-substrate/substrate/internal/localca" + "go.ate.dev/substrate/internal/localca" ) // CAPoolRootPEM extracts a CA pool Secret's root certificates and returns them diff --git a/cmd/ate-setup/internal/kube/client.go b/cmd/ate-setup/internal/kube/client.go index 5d89ea2253..a165cbdddc 100644 --- a/cmd/ate-setup/internal/kube/client.go +++ b/cmd/ate-setup/internal/kube/client.go @@ -29,7 +29,7 @@ import ( "k8s.io/client-go/rest" "k8s.io/client-go/restmapper" - "github.com/agent-substrate/substrate/internal/ateclient" + "go.ate.dev/substrate/internal/ateclient" ) // FieldManager identifies ate-setup's writes in managedFields. diff --git a/cmd/ate-setup/internal/kube/wait.go b/cmd/ate-setup/internal/kube/wait.go index 7ba1fc6cc6..d14b0b54cd 100644 --- a/cmd/ate-setup/internal/kube/wait.go +++ b/cmd/ate-setup/internal/kube/wait.go @@ -19,8 +19,8 @@ import ( "fmt" "time" - "github.com/agent-substrate/substrate/cmd/ate-setup/internal/log" - "github.com/agent-substrate/substrate/internal/clustertrustbundle" + "go.ate.dev/substrate/cmd/ate-setup/internal/log" + "go.ate.dev/substrate/internal/clustertrustbundle" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" diff --git a/cmd/ate-setup/internal/steps/actors.go b/cmd/ate-setup/internal/steps/actors.go index a8d5658c9a..fccfabb556 100644 --- a/cmd/ate-setup/internal/steps/actors.go +++ b/cmd/ate-setup/internal/steps/actors.go @@ -17,8 +17,8 @@ package steps import ( "context" - "github.com/agent-substrate/substrate/internal/ateclient" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "go.ate.dev/substrate/internal/ateclient" + "go.ate.dev/substrate/pkg/proto/ateapipb" ) // TemplateRef identifies a demo's ActorTemplate. Actors are deleted by matching diff --git a/cmd/ate-setup/internal/steps/apienv.go b/cmd/ate-setup/internal/steps/apienv.go index da988eb528..96f194174b 100644 --- a/cmd/ate-setup/internal/steps/apienv.go +++ b/cmd/ate-setup/internal/steps/apienv.go @@ -25,8 +25,8 @@ import ( "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "github.com/agent-substrate/substrate/cmd/ate-setup/internal/kube" - "github.com/agent-substrate/substrate/cmd/ate-setup/internal/log" + "go.ate.dev/substrate/cmd/ate-setup/internal/kube" + "go.ate.dev/substrate/cmd/ate-setup/internal/log" ) // envHashAnnotation carries a digest of the apiserver's environment on the pod diff --git a/cmd/ate-setup/internal/steps/apienv_test.go b/cmd/ate-setup/internal/steps/apienv_test.go index 903aeeef17..1fae0650de 100644 --- a/cmd/ate-setup/internal/steps/apienv_test.go +++ b/cmd/ate-setup/internal/steps/apienv_test.go @@ -23,9 +23,9 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "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/pkg/postgressetup" + "go.ate.dev/substrate/cmd/ate-setup/internal/config" + "go.ate.dev/substrate/cmd/ate-setup/internal/kube" + "go.ate.dev/substrate/pkg/postgressetup" ) // The digest exists to turn an envFrom change into a rollout, so what matters diff --git a/cmd/ate-setup/internal/steps/benchmarks.go b/cmd/ate-setup/internal/steps/benchmarks.go index abc8d6e920..68c51d020c 100644 --- a/cmd/ate-setup/internal/steps/benchmarks.go +++ b/cmd/ate-setup/internal/steps/benchmarks.go @@ -21,8 +21,8 @@ import ( "os/exec" "strconv" - "github.com/agent-substrate/substrate/cmd/ate-setup/internal/config" - "github.com/agent-substrate/substrate/cmd/ate-setup/internal/log" + "go.ate.dev/substrate/cmd/ate-setup/internal/config" + "go.ate.dev/substrate/cmd/ate-setup/internal/log" ) // The benchmark and micro-VM stacks are still driven by shell. They orchestrate diff --git a/cmd/ate-setup/internal/steps/benchmarks_test.go b/cmd/ate-setup/internal/steps/benchmarks_test.go index ce85a93748..221e94c7b3 100644 --- a/cmd/ate-setup/internal/steps/benchmarks_test.go +++ b/cmd/ate-setup/internal/steps/benchmarks_test.go @@ -18,7 +18,7 @@ import ( "slices" "testing" - "github.com/agent-substrate/substrate/cmd/ate-setup/internal/config" + "go.ate.dev/substrate/cmd/ate-setup/internal/config" ) // deploy_locust.sh takes these as flags only, so anything not forwarded here is diff --git a/cmd/ate-setup/internal/steps/cloudsql.go b/cmd/ate-setup/internal/steps/cloudsql.go index d2b0c9892d..94308571a3 100644 --- a/cmd/ate-setup/internal/steps/cloudsql.go +++ b/cmd/ate-setup/internal/steps/cloudsql.go @@ -20,8 +20,8 @@ import ( "os" "strings" - "github.com/agent-substrate/substrate/cmd/ate-setup/internal/config" - "github.com/agent-substrate/substrate/cmd/ate-setup/internal/log" + "go.ate.dev/substrate/cmd/ate-setup/internal/config" + "go.ate.dev/substrate/cmd/ate-setup/internal/log" ) // Keys the Cloud SQL Auth Proxy sidecar reads out of the ate-api-server-envvars diff --git a/cmd/ate-setup/internal/steps/cloudsql_test.go b/cmd/ate-setup/internal/steps/cloudsql_test.go index 4c1f6e2414..69a901340e 100644 --- a/cmd/ate-setup/internal/steps/cloudsql_test.go +++ b/cmd/ate-setup/internal/steps/cloudsql_test.go @@ -28,8 +28,8 @@ import ( "k8s.io/client-go/kubernetes/fake" "sigs.k8s.io/yaml" - "github.com/agent-substrate/substrate/cmd/ate-setup/internal/config" - "github.com/agent-substrate/substrate/cmd/ate-setup/internal/kube" + "go.ate.dev/substrate/cmd/ate-setup/internal/config" + "go.ate.dev/substrate/cmd/ate-setup/internal/kube" ) // fakeKube builds a kube.Client backed by the fake clientset, seeded with diff --git a/cmd/ate-setup/internal/steps/create.go b/cmd/ate-setup/internal/steps/create.go index 5c4fce43f1..7c1c9127c2 100644 --- a/cmd/ate-setup/internal/steps/create.go +++ b/cmd/ate-setup/internal/steps/create.go @@ -25,10 +25,10 @@ import ( corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "github.com/agent-substrate/substrate/cmd/ate-setup/internal/log" - "github.com/agent-substrate/substrate/internal/localca" - "github.com/agent-substrate/substrate/internal/localjwtauthority" - "github.com/agent-substrate/substrate/pkg/proto/ateconfigpb" + "go.ate.dev/substrate/cmd/ate-setup/internal/log" + "go.ate.dev/substrate/internal/localca" + "go.ate.dev/substrate/internal/localjwtauthority" + "go.ate.dev/substrate/pkg/proto/ateconfigpb" ) // Secret and ConfigMap names the control plane reads. diff --git a/cmd/ate-setup/internal/steps/create_test.go b/cmd/ate-setup/internal/steps/create_test.go index d4a744e98a..c818bfcd58 100644 --- a/cmd/ate-setup/internal/steps/create_test.go +++ b/cmd/ate-setup/internal/steps/create_test.go @@ -30,9 +30,9 @@ import ( "k8s.io/client-go/kubernetes/fake" k8stesting "k8s.io/client-go/testing" - "github.com/agent-substrate/substrate/cmd/ate-setup/internal/kube" - "github.com/agent-substrate/substrate/internal/localca" - "github.com/agent-substrate/substrate/internal/localjwtauthority" + "go.ate.dev/substrate/cmd/ate-setup/internal/kube" + "go.ate.dev/substrate/internal/localca" + "go.ate.dev/substrate/internal/localjwtauthority" ) // ate-api-server requires both connection strings and its schema in the diff --git a/cmd/ate-setup/internal/steps/credprovider.go b/cmd/ate-setup/internal/steps/credprovider.go index b201c74f27..1ec8f2ab18 100644 --- a/cmd/ate-setup/internal/steps/credprovider.go +++ b/cmd/ate-setup/internal/steps/credprovider.go @@ -17,7 +17,7 @@ package steps import ( "context" - "github.com/agent-substrate/substrate/cmd/ate-setup/internal/log" + "go.ate.dev/substrate/cmd/ate-setup/internal/log" ) // Manifests for the bundled Kubernetes Secrets credential provider. They sit diff --git a/cmd/ate-setup/internal/steps/credprovider_test.go b/cmd/ate-setup/internal/steps/credprovider_test.go index 176d615616..1539f8ac3f 100644 --- a/cmd/ate-setup/internal/steps/credprovider_test.go +++ b/cmd/ate-setup/internal/steps/credprovider_test.go @@ -19,8 +19,8 @@ import ( "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "github.com/agent-substrate/substrate/cmd/ate-setup/internal/config" - "github.com/agent-substrate/substrate/cmd/ate-setup/internal/kube" + "go.ate.dev/substrate/cmd/ate-setup/internal/config" + "go.ate.dev/substrate/cmd/ate-setup/internal/kube" ) // The deploy steps refer to the provider's objects by name. This checks those diff --git a/cmd/ate-setup/internal/steps/csi.go b/cmd/ate-setup/internal/steps/csi.go index 7455a49e12..1fccb87793 100644 --- a/cmd/ate-setup/internal/steps/csi.go +++ b/cmd/ate-setup/internal/steps/csi.go @@ -29,10 +29,10 @@ import ( "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/yaml" - "github.com/agent-substrate/substrate/cmd/ate-setup/internal/kube" - "github.com/agent-substrate/substrate/cmd/ate-setup/internal/kustomize" - "github.com/agent-substrate/substrate/cmd/ate-setup/internal/log" - "github.com/agent-substrate/substrate/internal/nodepath" + "go.ate.dev/substrate/cmd/ate-setup/internal/kube" + "go.ate.dev/substrate/cmd/ate-setup/internal/kustomize" + "go.ate.dev/substrate/cmd/ate-setup/internal/log" + "go.ate.dev/substrate/internal/nodepath" ) // workerHostDir is the host directory atelet mounts sandbox images under. Both diff --git a/cmd/ate-setup/internal/steps/csi_test.go b/cmd/ate-setup/internal/steps/csi_test.go index aded1b9905..e558bfb9a2 100644 --- a/cmd/ate-setup/internal/steps/csi_test.go +++ b/cmd/ate-setup/internal/steps/csi_test.go @@ -23,8 +23,8 @@ import ( "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "github.com/agent-substrate/substrate/cmd/ate-setup/internal/config" - "github.com/agent-substrate/substrate/cmd/ate-setup/internal/kube" + "go.ate.dev/substrate/cmd/ate-setup/internal/config" + "go.ate.dev/substrate/cmd/ate-setup/internal/kube" ) // setupCSIWithoutACluster runs SetupCSI against an Env holding no cluster diff --git a/cmd/ate-setup/internal/steps/delete.go b/cmd/ate-setup/internal/steps/delete.go index 6ba85d1516..b8cb7a0adf 100644 --- a/cmd/ate-setup/internal/steps/delete.go +++ b/cmd/ate-setup/internal/steps/delete.go @@ -26,9 +26,9 @@ import ( "k8s.io/client-go/dynamic" "k8s.io/client-go/util/retry" - "github.com/agent-substrate/substrate/cmd/ate-setup/internal/log" - "github.com/agent-substrate/substrate/internal/clustertrustbundle" - atev1alpha1 "github.com/agent-substrate/substrate/pkg/api/v1alpha1" + "go.ate.dev/substrate/cmd/ate-setup/internal/log" + "go.ate.dev/substrate/internal/clustertrustbundle" + atev1alpha1 "go.ate.dev/substrate/pkg/api/v1alpha1" ) // DeleteAteSystem removes the control plane. diff --git a/cmd/ate-setup/internal/steps/delete_test.go b/cmd/ate-setup/internal/steps/delete_test.go index 0ff7eb6ee8..a2f97f47de 100644 --- a/cmd/ate-setup/internal/steps/delete_test.go +++ b/cmd/ate-setup/internal/steps/delete_test.go @@ -36,10 +36,10 @@ import ( ktesting "k8s.io/client-go/testing" "k8s.io/client-go/util/retry" - "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/internal/clustertrustbundle" - atev1alpha1 "github.com/agent-substrate/substrate/pkg/api/v1alpha1" + "go.ate.dev/substrate/cmd/ate-setup/internal/config" + "go.ate.dev/substrate/cmd/ate-setup/internal/kube" + "go.ate.dev/substrate/internal/clustertrustbundle" + atev1alpha1 "go.ate.dev/substrate/pkg/api/v1alpha1" ) // fakeSandboxConfigs is an in-memory dynamic.ResourceInterface implementing diff --git a/cmd/ate-setup/internal/steps/deploy.go b/cmd/ate-setup/internal/steps/deploy.go index dd015eb2bb..02ab4737a4 100644 --- a/cmd/ate-setup/internal/steps/deploy.go +++ b/cmd/ate-setup/internal/steps/deploy.go @@ -27,8 +27,8 @@ import ( "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime/schema" - "github.com/agent-substrate/substrate/cmd/ate-setup/internal/kube" - "github.com/agent-substrate/substrate/cmd/ate-setup/internal/log" + "go.ate.dev/substrate/cmd/ate-setup/internal/kube" + "go.ate.dev/substrate/cmd/ate-setup/internal/log" ) // crdGVK is the kind of a CustomResourceDefinition. diff --git a/cmd/ate-setup/internal/steps/deploy_test.go b/cmd/ate-setup/internal/steps/deploy_test.go index 4edffc0012..3777fceb9b 100644 --- a/cmd/ate-setup/internal/steps/deploy_test.go +++ b/cmd/ate-setup/internal/steps/deploy_test.go @@ -25,8 +25,8 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "github.com/agent-substrate/substrate/cmd/ate-setup/internal/config" - "github.com/agent-substrate/substrate/cmd/ate-setup/internal/kube" + "go.ate.dev/substrate/cmd/ate-setup/internal/config" + "go.ate.dev/substrate/cmd/ate-setup/internal/kube" ) // podcertObjects loads the podcertificate-controller manifest as diff --git a/cmd/ate-setup/internal/steps/egress_dns_test.go b/cmd/ate-setup/internal/steps/egress_dns_test.go index 996d9ca424..26bc77137c 100644 --- a/cmd/ate-setup/internal/steps/egress_dns_test.go +++ b/cmd/ate-setup/internal/steps/egress_dns_test.go @@ -29,8 +29,8 @@ import ( k8syaml "k8s.io/apimachinery/pkg/util/yaml" "sigs.k8s.io/yaml" - "github.com/agent-substrate/substrate/cmd/ate-setup/internal/config" - "github.com/agent-substrate/substrate/cmd/ate-setup/internal/kustomize" + "go.ate.dev/substrate/cmd/ate-setup/internal/config" + "go.ate.dev/substrate/cmd/ate-setup/internal/kustomize" ) // TestEgressDNSLookupFamily requires ALL on every dynamic forward proxy DNS diff --git a/cmd/ate-setup/internal/steps/env.go b/cmd/ate-setup/internal/steps/env.go index 714dc08fb2..63b81920ae 100644 --- a/cmd/ate-setup/internal/steps/env.go +++ b/cmd/ate-setup/internal/steps/env.go @@ -26,12 +26,12 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime/schema" - "github.com/agent-substrate/substrate/cmd/ate-setup/internal/config" - "github.com/agent-substrate/substrate/cmd/ate-setup/internal/images" - "github.com/agent-substrate/substrate/cmd/ate-setup/internal/ko" - "github.com/agent-substrate/substrate/cmd/ate-setup/internal/kube" - "github.com/agent-substrate/substrate/cmd/ate-setup/internal/kustomize" - "github.com/agent-substrate/substrate/cmd/ate-setup/internal/log" + "go.ate.dev/substrate/cmd/ate-setup/internal/config" + "go.ate.dev/substrate/cmd/ate-setup/internal/images" + "go.ate.dev/substrate/cmd/ate-setup/internal/ko" + "go.ate.dev/substrate/cmd/ate-setup/internal/kube" + "go.ate.dev/substrate/cmd/ate-setup/internal/kustomize" + "go.ate.dev/substrate/cmd/ate-setup/internal/log" ) // Timeouts carried over from the --timeout values in the shell installer. diff --git a/cmd/ate-setup/internal/steps/namespace_test.go b/cmd/ate-setup/internal/steps/namespace_test.go index a769c6dcdc..42a46146cc 100644 --- a/cmd/ate-setup/internal/steps/namespace_test.go +++ b/cmd/ate-setup/internal/steps/namespace_test.go @@ -18,7 +18,7 @@ import ( "strings" "testing" - "github.com/agent-substrate/substrate/cmd/ate-setup/internal/config" + "go.ate.dev/substrate/cmd/ate-setup/internal/config" ) func TestEnvNamespace(t *testing.T) { diff --git a/cmd/ate-setup/internal/steps/overlay.go b/cmd/ate-setup/internal/steps/overlay.go index cdf2f854b7..46edf55c70 100644 --- a/cmd/ate-setup/internal/steps/overlay.go +++ b/cmd/ate-setup/internal/steps/overlay.go @@ -24,9 +24,9 @@ import ( "strings" "time" - "github.com/agent-substrate/substrate/cmd/ate-setup/internal/config" - "github.com/agent-substrate/substrate/cmd/ate-setup/internal/kustomize" - "github.com/agent-substrate/substrate/cmd/ate-setup/internal/log" + "go.ate.dev/substrate/cmd/ate-setup/internal/config" + "go.ate.dev/substrate/cmd/ate-setup/internal/kustomize" + "go.ate.dev/substrate/cmd/ate-setup/internal/log" ) const ( diff --git a/cmd/ate-setup/internal/steps/overlay_test.go b/cmd/ate-setup/internal/steps/overlay_test.go index fdb7cd7d4f..98bb29d613 100644 --- a/cmd/ate-setup/internal/steps/overlay_test.go +++ b/cmd/ate-setup/internal/steps/overlay_test.go @@ -37,9 +37,9 @@ import ( kyaml "sigs.k8s.io/kustomize/kyaml/yaml" "sigs.k8s.io/yaml" - "github.com/agent-substrate/substrate/cmd/ate-setup/internal/config" - "github.com/agent-substrate/substrate/cmd/ate-setup/internal/images" - "github.com/agent-substrate/substrate/cmd/ate-setup/internal/kube" + "go.ate.dev/substrate/cmd/ate-setup/internal/config" + "go.ate.dev/substrate/cmd/ate-setup/internal/images" + "go.ate.dev/substrate/cmd/ate-setup/internal/kube" ) // Splicing the provider selection into the real egress manifest replaces the diff --git a/cmd/ate-setup/internal/steps/postgres.go b/cmd/ate-setup/internal/steps/postgres.go index 9287a75db2..3e8be19b4a 100644 --- a/cmd/ate-setup/internal/steps/postgres.go +++ b/cmd/ate-setup/internal/steps/postgres.go @@ -26,10 +26,10 @@ import ( "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "sigs.k8s.io/yaml" - "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/log" - "github.com/agent-substrate/substrate/pkg/postgressetup" + "go.ate.dev/substrate/cmd/ate-setup/internal/config" + "go.ate.dev/substrate/cmd/ate-setup/internal/kube" + "go.ate.dev/substrate/cmd/ate-setup/internal/log" + "go.ate.dev/substrate/pkg/postgressetup" ) // bundledPostgresHost is where ate-api-server reaches the bundled PostgreSQL. diff --git a/cmd/ate-setup/internal/steps/postgres_test.go b/cmd/ate-setup/internal/steps/postgres_test.go index 7b326f92fb..d0022be87b 100644 --- a/cmd/ate-setup/internal/steps/postgres_test.go +++ b/cmd/ate-setup/internal/steps/postgres_test.go @@ -27,9 +27,9 @@ import ( "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "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/pkg/postgressetup" + "go.ate.dev/substrate/cmd/ate-setup/internal/config" + "go.ate.dev/substrate/cmd/ate-setup/internal/kube" + "go.ate.dev/substrate/pkg/postgressetup" ) // postgresObjects loads the bundled postgres manifest of the given diff --git a/cmd/ate-setup/internal/steps/prereqs.go b/cmd/ate-setup/internal/steps/prereqs.go index 0fdae5187e..1a28b15dcd 100644 --- a/cmd/ate-setup/internal/steps/prereqs.go +++ b/cmd/ate-setup/internal/steps/prereqs.go @@ -18,7 +18,7 @@ import ( "context" "fmt" - "github.com/agent-substrate/substrate/cmd/ate-setup/internal/log" + "go.ate.dev/substrate/cmd/ate-setup/internal/log" ) // trustBundleNames are the ClusterTrustBundles the podcertificate controller diff --git a/cmd/ate-setup/internal/steps/prereqs_test.go b/cmd/ate-setup/internal/steps/prereqs_test.go index fa220ff639..8bde7919ad 100644 --- a/cmd/ate-setup/internal/steps/prereqs_test.go +++ b/cmd/ate-setup/internal/steps/prereqs_test.go @@ -25,9 +25,9 @@ import ( "k8s.io/apimachinery/pkg/runtime" "k8s.io/client-go/kubernetes/fake" - "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/pkg/proto/ateconfigpb" + "go.ate.dev/substrate/cmd/ate-setup/internal/config" + "go.ate.dev/substrate/cmd/ate-setup/internal/kube" + "go.ate.dev/substrate/pkg/proto/ateconfigpb" ) // A deploy without --ateapi-log-level must not undo a level set by hand or by diff --git a/cmd/ate-setup/internal/steps/preview.go b/cmd/ate-setup/internal/steps/preview.go index 0520ec8a5a..090391019a 100644 --- a/cmd/ate-setup/internal/steps/preview.go +++ b/cmd/ate-setup/internal/steps/preview.go @@ -25,7 +25,7 @@ import ( // substrateImagePrefix marks a container as running a substrate binary. Every // such binary accepts --preview. -const substrateImagePrefix = "ko://github.com/agent-substrate/substrate/cmd/" +const substrateImagePrefix = "ko://go.ate.dev/substrate/cmd/" const previewFlag = "--preview" diff --git a/cmd/ate-setup/internal/steps/preview_test.go b/cmd/ate-setup/internal/steps/preview_test.go index 18de8c6ef8..102836e4df 100644 --- a/cmd/ate-setup/internal/steps/preview_test.go +++ b/cmd/ate-setup/internal/steps/preview_test.go @@ -28,8 +28,8 @@ import ( "sigs.k8s.io/kustomize/kyaml/kio" "sigs.k8s.io/kustomize/kyaml/yaml" - "github.com/agent-substrate/substrate/cmd/ate-setup/internal/config" - "github.com/agent-substrate/substrate/cmd/ate-setup/internal/kube" + "go.ate.dev/substrate/cmd/ate-setup/internal/config" + "go.ate.dev/substrate/cmd/ate-setup/internal/kube" ) // containerArgs maps "//" to the container's args. @@ -78,7 +78,7 @@ kind: ConfigMap metadata: name: cfg data: - image: ko://github.com/agent-substrate/substrate/cmd/ateapi + image: ko://go.ate.dev/substrate/cmd/ateapi --- apiVersion: apps/v1 kind: Deployment @@ -89,18 +89,18 @@ spec: spec: initContainers: - name: sidecar - image: ko://github.com/agent-substrate/substrate/cmd/atenet + image: ko://go.ate.dev/substrate/cmd/atenet restartPolicy: Always args: - "sdsmint" containers: - name: api - image: ko://github.com/agent-substrate/substrate/cmd/ateapi + image: ko://go.ate.dev/substrate/cmd/ateapi args: - --port=8080 # keep me - --preview=Stale - name: bare - image: ko://github.com/agent-substrate/substrate/cmd/atelet + image: ko://go.ate.dev/substrate/cmd/atelet - name: envoy image: ${ENVOY_DATAPLANE_IMAGE} args: @@ -113,7 +113,7 @@ metadata: spec: containers: - name: ctl - image: ko://github.com/agent-substrate/substrate/cmd/atecontroller + image: ko://go.ate.dev/substrate/cmd/atecontroller ` func TestInjectPreviewArg(t *testing.T) { @@ -147,7 +147,7 @@ func TestInjectPreviewArg(t *testing.T) { } // Everything else survives: comments, placeholders, and the // ConfigMap that merely mentions a substrate image. - for _, s := range []string{"# keep me", "${ENVOY_DATAPLANE_IMAGE}", "kind: ConfigMap", "image: ko://github.com/agent-substrate/substrate/cmd/ateapi\n---"} { + for _, s := range []string{"# keep me", "${ENVOY_DATAPLANE_IMAGE}", "kind: ConfigMap", "image: ko://go.ate.dev/substrate/cmd/ateapi\n---"} { if !strings.Contains(string(out), s) { t.Errorf("output lost %q:\n%s", s, out) } diff --git a/cmd/ate-setup/internal/steps/publish.go b/cmd/ate-setup/internal/steps/publish.go index f3004188c9..52c5d264bc 100644 --- a/cmd/ate-setup/internal/steps/publish.go +++ b/cmd/ate-setup/internal/steps/publish.go @@ -19,8 +19,8 @@ import ( "fmt" "io" - "github.com/agent-substrate/substrate/cmd/ate-setup/internal/images" - "github.com/agent-substrate/substrate/cmd/ate-setup/internal/log" + "go.ate.dev/substrate/cmd/ate-setup/internal/images" + "go.ate.dev/substrate/cmd/ate-setup/internal/log" ) // workerImages are the worker images a WorkerPool points at through diff --git a/cmd/ate-setup/internal/steps/publish_test.go b/cmd/ate-setup/internal/steps/publish_test.go index 99f4add104..f47a47549a 100644 --- a/cmd/ate-setup/internal/steps/publish_test.go +++ b/cmd/ate-setup/internal/steps/publish_test.go @@ -19,8 +19,8 @@ import ( "strings" "testing" - "github.com/agent-substrate/substrate/cmd/ate-setup/internal/config" - "github.com/agent-substrate/substrate/cmd/ate-setup/internal/images" + "go.ate.dev/substrate/cmd/ate-setup/internal/config" + "go.ate.dev/substrate/cmd/ate-setup/internal/images" ) // TestKoRunnerPrebuilt covers the one resolver that cannot build. A pre-built diff --git a/cmd/ate-setup/internal/steps/substrate.go b/cmd/ate-setup/internal/steps/substrate.go index a9d5999f50..94b70b04aa 100644 --- a/cmd/ate-setup/internal/steps/substrate.go +++ b/cmd/ate-setup/internal/steps/substrate.go @@ -25,11 +25,11 @@ import ( "google.golang.org/protobuf/encoding/protojson" "sigs.k8s.io/yaml" - "github.com/agent-substrate/substrate/cmd/ate-setup/internal/log" - "github.com/agent-substrate/substrate/internal/ateclient" - "github.com/agent-substrate/substrate/internal/preview" - "github.com/agent-substrate/substrate/internal/resources" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "go.ate.dev/substrate/cmd/ate-setup/internal/log" + "go.ate.dev/substrate/internal/ateclient" + "go.ate.dev/substrate/internal/preview" + "go.ate.dev/substrate/internal/resources" + "go.ate.dev/substrate/pkg/proto/ateapipb" ) // AteClient connects to the ate-api-server, port-forwarding if needed. It diff --git a/cmd/ate-setup/internal/steps/substrate_test.go b/cmd/ate-setup/internal/steps/substrate_test.go index 39fe8ce4cd..13bc86c561 100644 --- a/cmd/ate-setup/internal/steps/substrate_test.go +++ b/cmd/ate-setup/internal/steps/substrate_test.go @@ -24,10 +24,10 @@ import ( "google.golang.org/grpc" - "github.com/agent-substrate/substrate/cmd/ate-setup/internal/config" - "github.com/agent-substrate/substrate/internal/ateclient" - "github.com/agent-substrate/substrate/internal/resources" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "go.ate.dev/substrate/cmd/ate-setup/internal/config" + "go.ate.dev/substrate/internal/ateclient" + "go.ate.dev/substrate/internal/resources" + "go.ate.dev/substrate/pkg/proto/ateapipb" ) // TestActorTemplateFromManifestDemos parses the protojson ActorTemplate diff --git a/cmd/ate-setup/internal/steps/systemnetworkpolicy_test.go b/cmd/ate-setup/internal/steps/systemnetworkpolicy_test.go index 26feb30709..31a2ba629b 100644 --- a/cmd/ate-setup/internal/steps/systemnetworkpolicy_test.go +++ b/cmd/ate-setup/internal/steps/systemnetworkpolicy_test.go @@ -27,9 +27,9 @@ import ( "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" + "go.ate.dev/substrate/cmd/ate-setup/internal/config" + "go.ate.dev/substrate/cmd/ate-setup/internal/kube" + "go.ate.dev/substrate/cmd/ate-setup/internal/kustomize" ) // systemPod is one ate-system workload's pod template, as the NetworkPolicies diff --git a/cmd/ate-setup/internal/steps/version.go b/cmd/ate-setup/internal/steps/version.go index 5fb94f21d9..3fb02ae390 100644 --- a/cmd/ate-setup/internal/steps/version.go +++ b/cmd/ate-setup/internal/steps/version.go @@ -18,7 +18,7 @@ import ( "os" "strings" - "github.com/agent-substrate/substrate/cmd/ate-setup/internal/ko" + "go.ate.dev/substrate/cmd/ate-setup/internal/ko" ) // SubstrateVersion returns the build version, the same one ko stamps into the diff --git a/cmd/ate-setup/internal/steps/version_test.go b/cmd/ate-setup/internal/steps/version_test.go index c1674e7eab..055b75cecb 100644 --- a/cmd/ate-setup/internal/steps/version_test.go +++ b/cmd/ate-setup/internal/steps/version_test.go @@ -17,8 +17,8 @@ package steps import ( "testing" - "github.com/agent-substrate/substrate/cmd/ate-setup/internal/config" - "github.com/agent-substrate/substrate/cmd/ate-setup/internal/images" + "go.ate.dev/substrate/cmd/ate-setup/internal/config" + "go.ate.dev/substrate/cmd/ate-setup/internal/images" ) // TestSubstrateVersionPrebuilt covers the version a prebuilt install reports. diff --git a/cmd/ate-setup/main.go b/cmd/ate-setup/main.go index 134d837aeb..d115f75c5c 100644 --- a/cmd/ate-setup/main.go +++ b/cmd/ate-setup/main.go @@ -20,7 +20,7 @@ package main import ( - "github.com/agent-substrate/substrate/cmd/ate-setup/internal/cmd" + "go.ate.dev/substrate/cmd/ate-setup/internal/cmd" ) func main() { diff --git a/cmd/ate-setup/preflight_test.go b/cmd/ate-setup/preflight_test.go index a81d142761..f54785ee71 100644 --- a/cmd/ate-setup/preflight_test.go +++ b/cmd/ate-setup/preflight_test.go @@ -28,7 +28,7 @@ import ( "testing" "time" - "github.com/agent-substrate/substrate/cmd/ate-setup/internal/cmd" + "go.ate.dev/substrate/cmd/ate-setup/internal/cmd" ) // Each invocation gets its own command tree and flags, including invocations diff --git a/cmd/ate-setup/shim_test.go b/cmd/ate-setup/shim_test.go index 185245482d..d3430bbe54 100644 --- a/cmd/ate-setup/shim_test.go +++ b/cmd/ate-setup/shim_test.go @@ -30,11 +30,11 @@ import ( "strings" "testing" - "github.com/agent-substrate/substrate/cmd/ate-setup/internal/cmd" - "github.com/agent-substrate/substrate/cmd/ate-setup/internal/config" - "github.com/agent-substrate/substrate/cmd/ate-setup/internal/demos" + "go.ate.dev/substrate/cmd/ate-setup/internal/cmd" + "go.ate.dev/substrate/cmd/ate-setup/internal/config" + "go.ate.dev/substrate/cmd/ate-setup/internal/demos" // Registers every bundled demo, so demos.All() is the real list. - _ "github.com/agent-substrate/substrate/cmd/ate-setup/internal/demos/all" + _ "go.ate.dev/substrate/cmd/ate-setup/internal/demos/all" ) // stubGo is a `go` that records the arguments of every `go run ./cmd/ate-setup` diff --git a/cmd/ateapi/internal/apiauthn/apiauthn.go b/cmd/ateapi/internal/apiauthn/apiauthn.go index 56f37f68da..e26a0c9a92 100644 --- a/cmd/ateapi/internal/apiauthn/apiauthn.go +++ b/cmd/ateapi/internal/apiauthn/apiauthn.go @@ -27,7 +27,7 @@ import ( "log/slog" "strings" - "github.com/agent-substrate/substrate/internal/principal" + "go.ate.dev/substrate/internal/principal" "google.golang.org/grpc" "google.golang.org/grpc/codes" "google.golang.org/grpc/credentials" diff --git a/cmd/ateapi/internal/apiauthn/apiauthn_test.go b/cmd/ateapi/internal/apiauthn/apiauthn_test.go index fed6cdf14f..6708c620cf 100644 --- a/cmd/ateapi/internal/apiauthn/apiauthn_test.go +++ b/cmd/ateapi/internal/apiauthn/apiauthn_test.go @@ -22,7 +22,7 @@ import ( "net/url" "testing" - "github.com/agent-substrate/substrate/internal/principal" + "go.ate.dev/substrate/internal/principal" "google.golang.org/grpc" "google.golang.org/grpc/codes" "google.golang.org/grpc/credentials" diff --git a/cmd/ateapi/internal/apiconfig/loader.go b/cmd/ateapi/internal/apiconfig/loader.go index d6f839c777..904a033a7d 100644 --- a/cmd/ateapi/internal/apiconfig/loader.go +++ b/cmd/ateapi/internal/apiconfig/loader.go @@ -19,7 +19,7 @@ import ( "os" "path/filepath" - "github.com/agent-substrate/substrate/pkg/proto/ateconfigpb" + "go.ate.dev/substrate/pkg/proto/ateconfigpb" "google.golang.org/protobuf/encoding/prototext" "google.golang.org/protobuf/proto" ) diff --git a/cmd/ateapi/internal/apivalidation/access_policy.go b/cmd/ateapi/internal/apivalidation/access_policy.go index 726a0e1d8a..430e06f961 100644 --- a/cmd/ateapi/internal/apivalidation/access_policy.go +++ b/cmd/ateapi/internal/apivalidation/access_policy.go @@ -18,8 +18,8 @@ import ( "context" "slices" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/authz" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "go.ate.dev/substrate/cmd/ateapi/internal/authz" + "go.ate.dev/substrate/pkg/proto/ateapipb" "k8s.io/apimachinery/pkg/api/operation" "k8s.io/apimachinery/pkg/util/validation/field" ) diff --git a/cmd/ateapi/internal/apivalidation/access_policy_test.go b/cmd/ateapi/internal/apivalidation/access_policy_test.go index b88d0add8c..0190a2aa3f 100644 --- a/cmd/ateapi/internal/apivalidation/access_policy_test.go +++ b/cmd/ateapi/internal/apivalidation/access_policy_test.go @@ -19,9 +19,9 @@ import ( "fmt" "testing" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/authz" - "github.com/agent-substrate/substrate/internal/resources" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "go.ate.dev/substrate/cmd/ateapi/internal/authz" + "go.ate.dev/substrate/internal/resources" + "go.ate.dev/substrate/pkg/proto/ateapipb" "k8s.io/apimachinery/pkg/util/validation/field" ) diff --git a/cmd/ateapi/internal/apivalidation/actor.go b/cmd/ateapi/internal/apivalidation/actor.go index 2e21446792..92c9f0c7b4 100644 --- a/cmd/ateapi/internal/apivalidation/actor.go +++ b/cmd/ateapi/internal/apivalidation/actor.go @@ -17,8 +17,8 @@ package apivalidation import ( "context" - "github.com/agent-substrate/substrate/internal/resources" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "go.ate.dev/substrate/internal/resources" + "go.ate.dev/substrate/pkg/proto/ateapipb" "k8s.io/apimachinery/pkg/api/operation" "k8s.io/apimachinery/pkg/api/validate" "k8s.io/apimachinery/pkg/util/validation/field" diff --git a/cmd/ateapi/internal/apivalidation/actor_template.go b/cmd/ateapi/internal/apivalidation/actor_template.go index f906c2e8b5..7a8cced518 100644 --- a/cmd/ateapi/internal/apivalidation/actor_template.go +++ b/cmd/ateapi/internal/apivalidation/actor_template.go @@ -18,9 +18,9 @@ import ( "context" "fmt" - "github.com/agent-substrate/substrate/internal/resources" - atev1alpha1 "github.com/agent-substrate/substrate/pkg/api/v1alpha1" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "go.ate.dev/substrate/internal/resources" + atev1alpha1 "go.ate.dev/substrate/pkg/api/v1alpha1" + "go.ate.dev/substrate/pkg/proto/ateapipb" "k8s.io/apimachinery/pkg/api/operation" "k8s.io/apimachinery/pkg/api/resource" "k8s.io/apimachinery/pkg/util/sets" diff --git a/cmd/ateapi/internal/apivalidation/actor_template_test.go b/cmd/ateapi/internal/apivalidation/actor_template_test.go index ebbba44356..a0e1c883b6 100644 --- a/cmd/ateapi/internal/apivalidation/actor_template_test.go +++ b/cmd/ateapi/internal/apivalidation/actor_template_test.go @@ -20,9 +20,9 @@ import ( "strings" "testing" - "github.com/agent-substrate/substrate/internal/preview" - "github.com/agent-substrate/substrate/internal/resources" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "go.ate.dev/substrate/internal/preview" + "go.ate.dev/substrate/internal/resources" + "go.ate.dev/substrate/pkg/proto/ateapipb" "k8s.io/apimachinery/pkg/util/validation/field" ) diff --git a/cmd/ateapi/internal/apivalidation/actor_test.go b/cmd/ateapi/internal/apivalidation/actor_test.go index a163c114ed..f8540e86b5 100644 --- a/cmd/ateapi/internal/apivalidation/actor_test.go +++ b/cmd/ateapi/internal/apivalidation/actor_test.go @@ -20,7 +20,7 @@ import ( "strings" "testing" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "go.ate.dev/substrate/pkg/proto/ateapipb" "k8s.io/apimachinery/pkg/util/validation/field" ) diff --git a/cmd/ateapi/internal/apivalidation/atespace.go b/cmd/ateapi/internal/apivalidation/atespace.go index 4bc4c3060b..1e4126e9e6 100644 --- a/cmd/ateapi/internal/apivalidation/atespace.go +++ b/cmd/ateapi/internal/apivalidation/atespace.go @@ -17,7 +17,7 @@ package apivalidation import ( "context" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "go.ate.dev/substrate/pkg/proto/ateapipb" "k8s.io/apimachinery/pkg/util/validation/field" ) diff --git a/cmd/ateapi/internal/apivalidation/atespace_test.go b/cmd/ateapi/internal/apivalidation/atespace_test.go index 0f06d560b6..2a338fe436 100644 --- a/cmd/ateapi/internal/apivalidation/atespace_test.go +++ b/cmd/ateapi/internal/apivalidation/atespace_test.go @@ -19,7 +19,7 @@ import ( "strings" "testing" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "go.ate.dev/substrate/pkg/proto/ateapipb" "k8s.io/apimachinery/pkg/util/validation/field" ) diff --git a/cmd/ateapi/internal/apivalidation/common.go b/cmd/ateapi/internal/apivalidation/common.go index 3abc92ed9e..4e4cafcfc1 100644 --- a/cmd/ateapi/internal/apivalidation/common.go +++ b/cmd/ateapi/internal/apivalidation/common.go @@ -19,10 +19,10 @@ import ( "fmt" "net/netip" - "github.com/agent-substrate/substrate/internal/preview" - "github.com/agent-substrate/substrate/internal/resources" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" "github.com/spiffe/go-spiffe/v2/spiffeid" + "go.ate.dev/substrate/internal/preview" + "go.ate.dev/substrate/internal/resources" + "go.ate.dev/substrate/pkg/proto/ateapipb" "k8s.io/apimachinery/pkg/api/operation" "k8s.io/apimachinery/pkg/util/validation" "k8s.io/apimachinery/pkg/util/validation/field" diff --git a/cmd/ateapi/internal/apivalidation/common_test.go b/cmd/ateapi/internal/apivalidation/common_test.go index 150340911a..e829d811ba 100644 --- a/cmd/ateapi/internal/apivalidation/common_test.go +++ b/cmd/ateapi/internal/apivalidation/common_test.go @@ -21,7 +21,7 @@ import ( "strings" "testing" - "github.com/agent-substrate/substrate/internal/preview" + "go.ate.dev/substrate/internal/preview" "k8s.io/apimachinery/pkg/util/validation/field" ) diff --git a/cmd/ateapi/internal/apivalidation/doc.go b/cmd/ateapi/internal/apivalidation/doc.go index 9cc9849150..393bfea63a 100644 --- a/cmd/ateapi/internal/apivalidation/doc.go +++ b/cmd/ateapi/internal/apivalidation/doc.go @@ -16,7 +16,7 @@ // work. // +k8s:validation-gen=TypesWithSuffix=Request -// +k8s:validation-gen-input=github.com/agent-substrate/substrate/pkg/proto/ateapipb +// +k8s:validation-gen-input=go.ate.dev/substrate/pkg/proto/ateapipb // +k8s:validation-gen-scheme-registry=nil // +k8s:validation-gen-deep-equal-func=ateDeepEqual diff --git a/cmd/ateapi/internal/apivalidation/egress_policy.go b/cmd/ateapi/internal/apivalidation/egress_policy.go index c890c45179..be444b47cb 100644 --- a/cmd/ateapi/internal/apivalidation/egress_policy.go +++ b/cmd/ateapi/internal/apivalidation/egress_policy.go @@ -20,8 +20,8 @@ import ( "net/url" "strings" - "github.com/agent-substrate/substrate/internal/egresspolicy" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "go.ate.dev/substrate/internal/egresspolicy" + "go.ate.dev/substrate/pkg/proto/ateapipb" "k8s.io/apimachinery/pkg/api/operation" "k8s.io/apimachinery/pkg/util/validation" "k8s.io/apimachinery/pkg/util/validation/field" diff --git a/cmd/ateapi/internal/apivalidation/egress_policy_test.go b/cmd/ateapi/internal/apivalidation/egress_policy_test.go index 89829a165b..d6f0fa3701 100644 --- a/cmd/ateapi/internal/apivalidation/egress_policy_test.go +++ b/cmd/ateapi/internal/apivalidation/egress_policy_test.go @@ -20,9 +20,9 @@ import ( "strings" "testing" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/defaults" - "github.com/agent-substrate/substrate/internal/resources" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "go.ate.dev/substrate/cmd/ateapi/internal/defaults" + "go.ate.dev/substrate/internal/resources" + "go.ate.dev/substrate/pkg/proto/ateapipb" "google.golang.org/protobuf/proto" "k8s.io/apimachinery/pkg/util/validation/field" ) diff --git a/cmd/ateapi/internal/apivalidation/tag.go b/cmd/ateapi/internal/apivalidation/tag.go index add2300c6f..125e8cf54e 100644 --- a/cmd/ateapi/internal/apivalidation/tag.go +++ b/cmd/ateapi/internal/apivalidation/tag.go @@ -17,7 +17,7 @@ package apivalidation import ( "context" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "go.ate.dev/substrate/pkg/proto/ateapipb" "k8s.io/apimachinery/pkg/api/operation" "k8s.io/apimachinery/pkg/api/validate" "k8s.io/apimachinery/pkg/util/validation/field" diff --git a/cmd/ateapi/internal/apivalidation/tag_test.go b/cmd/ateapi/internal/apivalidation/tag_test.go index 742e48263d..9bc5892863 100644 --- a/cmd/ateapi/internal/apivalidation/tag_test.go +++ b/cmd/ateapi/internal/apivalidation/tag_test.go @@ -19,7 +19,7 @@ import ( "strings" "testing" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "go.ate.dev/substrate/pkg/proto/ateapipb" "k8s.io/apimachinery/pkg/util/validation/field" ) diff --git a/cmd/ateapi/internal/apivalidation/validation_test.go b/cmd/ateapi/internal/apivalidation/validation_test.go index 120335bb21..6e1451850e 100644 --- a/cmd/ateapi/internal/apivalidation/validation_test.go +++ b/cmd/ateapi/internal/apivalidation/validation_test.go @@ -21,7 +21,7 @@ import ( "strings" "testing" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "go.ate.dev/substrate/pkg/proto/ateapipb" "google.golang.org/protobuf/proto" "google.golang.org/protobuf/types/known/timestamppb" "k8s.io/apimachinery/pkg/api/operation" diff --git a/cmd/ateapi/internal/apivalidation/worker.go b/cmd/ateapi/internal/apivalidation/worker.go index f4b57d6e09..73ec35e07b 100644 --- a/cmd/ateapi/internal/apivalidation/worker.go +++ b/cmd/ateapi/internal/apivalidation/worker.go @@ -18,8 +18,8 @@ import ( "context" "fmt" - "github.com/agent-substrate/substrate/internal/resources" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "go.ate.dev/substrate/internal/resources" + "go.ate.dev/substrate/pkg/proto/ateapipb" "k8s.io/apimachinery/pkg/api/operation" "k8s.io/apimachinery/pkg/api/validate" "k8s.io/apimachinery/pkg/util/validation/field" diff --git a/cmd/ateapi/internal/apivalidation/worker_test.go b/cmd/ateapi/internal/apivalidation/worker_test.go index cdcc84f694..f9f198cdb6 100644 --- a/cmd/ateapi/internal/apivalidation/worker_test.go +++ b/cmd/ateapi/internal/apivalidation/worker_test.go @@ -20,7 +20,7 @@ import ( "strings" "testing" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "go.ate.dev/substrate/pkg/proto/ateapipb" "google.golang.org/protobuf/proto" "google.golang.org/protobuf/types/known/timestamppb" "k8s.io/apimachinery/pkg/util/validation/field" diff --git a/cmd/ateapi/internal/apivalidation/zz_generated.validation.go b/cmd/ateapi/internal/apivalidation/zz_generated.validation.go index 4710aadc14..dd490a1a36 100644 --- a/cmd/ateapi/internal/apivalidation/zz_generated.validation.go +++ b/cmd/ateapi/internal/apivalidation/zz_generated.validation.go @@ -22,7 +22,7 @@ package apivalidation import ( context "context" - ateapipb "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + ateapipb "go.ate.dev/substrate/pkg/proto/ateapipb" timestamppb "google.golang.org/protobuf/types/known/timestamppb" operation "k8s.io/apimachinery/pkg/api/operation" safe "k8s.io/apimachinery/pkg/api/safe" @@ -2454,7 +2454,7 @@ func Validate_CreateWorkerRequest( return errs } -var unionMembershipFor_github_com_agent_substrate_substrate_pkg_proto_ateapipb_CredentialHeader_ = validate.NewUnionMembership(validate.NewUnionMember("credential_uri"), validate.NewUnionMember("actor_jwt")) +var unionMembershipFor_go_ate_dev_substrate_pkg_proto_ateapipb_CredentialHeader_ = validate.NewUnionMembership(validate.NewUnionMember("credential_uri"), validate.NewUnionMember("actor_jwt")) // Validate_CredentialHeader validates an instance of CredentialHeader according // to declarative validation rules in the API schema. @@ -2462,7 +2462,7 @@ func Validate_CredentialHeader( ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *ateapipb.CredentialHeader) (errs field.ErrorList) { - if e := validate.Union(ctx, op, fldPath, obj, oldObj, unionMembershipFor_github_com_agent_substrate_substrate_pkg_proto_ateapipb_CredentialHeader_, + if e := validate.Union(ctx, op, fldPath, obj, oldObj, unionMembershipFor_go_ate_dev_substrate_pkg_proto_ateapipb_CredentialHeader_, func(obj *ateapipb.CredentialHeader) bool { if obj == nil { return false @@ -3553,7 +3553,7 @@ func Validate_EgressPolicy( return errs } -var unionMembershipFor_github_com_agent_substrate_substrate_pkg_proto_ateapipb_EgressRule_ = validate.NewUnionMembership(validate.NewUnionMember("http"), validate.NewUnionMember("https"), validate.NewUnionMember("tls_passthrough")) +var unionMembershipFor_go_ate_dev_substrate_pkg_proto_ateapipb_EgressRule_ = validate.NewUnionMembership(validate.NewUnionMember("http"), validate.NewUnionMember("https"), validate.NewUnionMember("tls_passthrough")) // Validate_EgressRule validates an instance of EgressRule according // to declarative validation rules in the API schema. @@ -3561,7 +3561,7 @@ func Validate_EgressRule( ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *ateapipb.EgressRule) (errs field.ErrorList) { - if e := validate.Union(ctx, op, fldPath, obj, oldObj, unionMembershipFor_github_com_agent_substrate_substrate_pkg_proto_ateapipb_EgressRule_, + if e := validate.Union(ctx, op, fldPath, obj, oldObj, unionMembershipFor_go_ate_dev_substrate_pkg_proto_ateapipb_EgressRule_, func(obj *ateapipb.EgressRule) bool { if obj == nil { return false @@ -6403,7 +6403,7 @@ func Validate_PauseActorRequest( return errs } -var unionMembershipFor_github_com_agent_substrate_substrate_pkg_proto_ateapipb_Ports_ = validate.NewUnionMembership(validate.NewUnionMember("all"), validate.NewUnionMember("numbers")) +var unionMembershipFor_go_ate_dev_substrate_pkg_proto_ateapipb_Ports_ = validate.NewUnionMembership(validate.NewUnionMember("all"), validate.NewUnionMember("numbers")) // Validate_Ports validates an instance of Ports according // to declarative validation rules in the API schema. @@ -6411,7 +6411,7 @@ func Validate_Ports( ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *ateapipb.Ports) (errs field.ErrorList) { - if e := validate.Union(ctx, op, fldPath, obj, oldObj, unionMembershipFor_github_com_agent_substrate_substrate_pkg_proto_ateapipb_Ports_, + if e := validate.Union(ctx, op, fldPath, obj, oldObj, unionMembershipFor_go_ate_dev_substrate_pkg_proto_ateapipb_Ports_, func(obj *ateapipb.Ports) bool { if obj == nil { return false @@ -7502,7 +7502,7 @@ func Validate_SecurityContext( return errs } -var unionMembershipFor_github_com_agent_substrate_substrate_pkg_proto_ateapipb_Selector_ = validate.NewUnionMembership(validate.NewUnionMember("match_labels")) +var unionMembershipFor_go_ate_dev_substrate_pkg_proto_ateapipb_Selector_ = validate.NewUnionMembership(validate.NewUnionMember("match_labels")) // Validate_Selector validates an instance of Selector according // to declarative validation rules in the API schema. @@ -7510,7 +7510,7 @@ func Validate_Selector( ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *ateapipb.Selector) (errs field.ErrorList) { - if e := validate.Union(ctx, op, fldPath, obj, oldObj, unionMembershipFor_github_com_agent_substrate_substrate_pkg_proto_ateapipb_Selector_, + if e := validate.Union(ctx, op, fldPath, obj, oldObj, unionMembershipFor_go_ate_dev_substrate_pkg_proto_ateapipb_Selector_, func(obj *ateapipb.Selector) bool { if obj == nil { return false @@ -8264,7 +8264,7 @@ func Validate_SuspendActorRequest( return errs } -var unionMembershipFor_github_com_agent_substrate_substrate_pkg_proto_ateapipb_SystemInfoDataSource_ = validate.NewUnionMembership(validate.NewUnionMember("actor_metadata"), validate.NewUnionMember("trust_bundle")) +var unionMembershipFor_go_ate_dev_substrate_pkg_proto_ateapipb_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. @@ -8272,7 +8272,7 @@ func Validate_SystemInfoDataSource( ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *ateapipb.SystemInfoDataSource) (errs field.ErrorList) { - if e := validate.Union(ctx, op, fldPath, obj, oldObj, unionMembershipFor_github_com_agent_substrate_substrate_pkg_proto_ateapipb_SystemInfoDataSource_, + if e := validate.Union(ctx, op, fldPath, obj, oldObj, unionMembershipFor_go_ate_dev_substrate_pkg_proto_ateapipb_SystemInfoDataSource_, func(obj *ateapipb.SystemInfoDataSource) bool { if obj == nil { return false @@ -9313,7 +9313,7 @@ func Validate_VersionedSandboxCompat( return errs } -var unionMembershipFor_github_com_agent_substrate_substrate_pkg_proto_ateapipb_Volume_ = validate.NewUnionMembership(validate.NewUnionMember("durable_dir"), validate.NewUnionMember("external_volume_template"), validate.NewUnionMember("system_info"), validate.NewUnionMember("image")) +var unionMembershipFor_go_ate_dev_substrate_pkg_proto_ateapipb_Volume_ = validate.NewUnionMembership(validate.NewUnionMember("durable_dir"), validate.NewUnionMember("external_volume_template"), validate.NewUnionMember("system_info"), validate.NewUnionMember("image")) // Validate_Volume validates an instance of Volume according // to declarative validation rules in the API schema. @@ -9321,7 +9321,7 @@ func Validate_Volume( ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *ateapipb.Volume) (errs field.ErrorList) { - if e := validate.Union(ctx, op, fldPath, obj, oldObj, unionMembershipFor_github_com_agent_substrate_substrate_pkg_proto_ateapipb_Volume_, + if e := validate.Union(ctx, op, fldPath, obj, oldObj, unionMembershipFor_go_ate_dev_substrate_pkg_proto_ateapipb_Volume_, func(obj *ateapipb.Volume) bool { if obj == nil { return false diff --git a/cmd/ateapi/internal/ateletauth/ateletauth.go b/cmd/ateapi/internal/ateletauth/ateletauth.go index 445cb44dd8..39f009c5a6 100644 --- a/cmd/ateapi/internal/ateletauth/ateletauth.go +++ b/cmd/ateapi/internal/ateletauth/ateletauth.go @@ -20,8 +20,8 @@ import ( "context" "log/slog" - "github.com/agent-substrate/substrate/internal/apierror" - "github.com/agent-substrate/substrate/internal/substratex509" + "go.ate.dev/substrate/internal/apierror" + "go.ate.dev/substrate/internal/substratex509" "google.golang.org/grpc/credentials" "google.golang.org/grpc/peer" ) diff --git a/cmd/ateapi/internal/ateletauth/ateletauth_test.go b/cmd/ateapi/internal/ateletauth/ateletauth_test.go index 93a5485511..96b7bfae3f 100644 --- a/cmd/ateapi/internal/ateletauth/ateletauth_test.go +++ b/cmd/ateapi/internal/ateletauth/ateletauth_test.go @@ -17,9 +17,9 @@ package ateletauth_test import ( "testing" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/ateletauth" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/ateletauth/ateletauthtest" - "github.com/agent-substrate/substrate/internal/installdefaults" + "go.ate.dev/substrate/cmd/ateapi/internal/ateletauth" + "go.ate.dev/substrate/cmd/ateapi/internal/ateletauth/ateletauthtest" + "go.ate.dev/substrate/internal/installdefaults" ) // TestAuthenticateHonorsConfiguredIdentity checks that the identity diff --git a/cmd/ateapi/internal/ateletauth/ateletauthtest/ateletauthtest.go b/cmd/ateapi/internal/ateletauth/ateletauthtest/ateletauthtest.go index d4bc203ca2..ec4cd6c313 100644 --- a/cmd/ateapi/internal/ateletauth/ateletauthtest/ateletauthtest.go +++ b/cmd/ateapi/internal/ateletauth/ateletauthtest/ateletauthtest.go @@ -29,8 +29,8 @@ import ( "testing" "time" - "github.com/agent-substrate/substrate/internal/installdefaults" - "github.com/agent-substrate/substrate/internal/substratex509" + "go.ate.dev/substrate/internal/installdefaults" + "go.ate.dev/substrate/internal/substratex509" "google.golang.org/grpc/credentials" "google.golang.org/grpc/peer" ) diff --git a/cmd/ateapi/internal/authz/authorizer.go b/cmd/ateapi/internal/authz/authorizer.go index 57d8a78056..68983aeef4 100644 --- a/cmd/ateapi/internal/authz/authorizer.go +++ b/cmd/ateapi/internal/authz/authorizer.go @@ -18,10 +18,10 @@ import ( "context" "strings" - "github.com/agent-substrate/substrate/internal/apierror" - "github.com/agent-substrate/substrate/internal/principal" openfgav1 "github.com/openfga/api/proto/openfga/v1" "github.com/openfga/openfga/pkg/server" + "go.ate.dev/substrate/internal/apierror" + "go.ate.dev/substrate/internal/principal" ) // Authorizer is the read-only policy decision point that evaluates runtime diff --git a/cmd/ateapi/internal/authz/authz_test.go b/cmd/ateapi/internal/authz/authz_test.go index 0a8a744049..7dec76c6e2 100644 --- a/cmd/ateapi/internal/authz/authz_test.go +++ b/cmd/ateapi/internal/authz/authz_test.go @@ -33,8 +33,8 @@ import ( "github.com/testcontainers/testcontainers-go/modules/postgres" "google.golang.org/grpc/codes" - "github.com/agent-substrate/substrate/internal/apierror" - "github.com/agent-substrate/substrate/internal/principal" + "go.ate.dev/substrate/internal/apierror" + "go.ate.dev/substrate/internal/principal" ) func configureDockerEnv(ctx context.Context) error { diff --git a/cmd/ateapi/internal/authz/authz_util_test.go b/cmd/ateapi/internal/authz/authz_util_test.go index 40ad23d1f2..66d165789d 100644 --- a/cmd/ateapi/internal/authz/authz_util_test.go +++ b/cmd/ateapi/internal/authz/authz_util_test.go @@ -19,8 +19,8 @@ import ( "strings" "testing" - "github.com/agent-substrate/substrate/internal/installdefaults" "github.com/google/go-cmp/cmp" + "go.ate.dev/substrate/internal/installdefaults" ) func testSystemPrincipals() SystemPrincipals { diff --git a/cmd/ateapi/internal/authz/interceptor.go b/cmd/ateapi/internal/authz/interceptor.go index 9fd77198c8..ee20395535 100644 --- a/cmd/ateapi/internal/authz/interceptor.go +++ b/cmd/ateapi/internal/authz/interceptor.go @@ -17,8 +17,8 @@ package authz import ( "context" - "github.com/agent-substrate/substrate/internal/apierror" - "github.com/agent-substrate/substrate/internal/principal" + "go.ate.dev/substrate/internal/apierror" + "go.ate.dev/substrate/internal/principal" "google.golang.org/grpc" ) diff --git a/cmd/ateapi/internal/authz/interceptor_test.go b/cmd/ateapi/internal/authz/interceptor_test.go index f7bf2c4fb6..350f947570 100644 --- a/cmd/ateapi/internal/authz/interceptor_test.go +++ b/cmd/ateapi/internal/authz/interceptor_test.go @@ -19,9 +19,9 @@ import ( "strings" "testing" - "github.com/agent-substrate/substrate/internal/apierror" - "github.com/agent-substrate/substrate/internal/principal" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "go.ate.dev/substrate/internal/apierror" + "go.ate.dev/substrate/internal/principal" + "go.ate.dev/substrate/pkg/proto/ateapipb" "google.golang.org/grpc" "google.golang.org/grpc/codes" ) diff --git a/cmd/ateapi/internal/authz/policy_manager.go b/cmd/ateapi/internal/authz/policy_manager.go index f6fc0d2d9f..99a9d11f81 100644 --- a/cmd/ateapi/internal/authz/policy_manager.go +++ b/cmd/ateapi/internal/authz/policy_manager.go @@ -19,10 +19,10 @@ import ( "errors" "fmt" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" "github.com/jackc/pgx/v5" openfgav1 "github.com/openfga/api/proto/openfga/v1" "github.com/openfga/openfga/pkg/server" + "go.ate.dev/substrate/pkg/proto/ateapipb" "google.golang.org/protobuf/types/known/wrapperspb" ) diff --git a/cmd/ateapi/internal/authz/registry.go b/cmd/ateapi/internal/authz/registry.go index 65231d9e9c..12d7401b08 100644 --- a/cmd/ateapi/internal/authz/registry.go +++ b/cmd/ateapi/internal/authz/registry.go @@ -17,9 +17,9 @@ package authz import ( "slices" - "github.com/agent-substrate/substrate/internal/apierror" - "github.com/agent-substrate/substrate/internal/resources" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "go.ate.dev/substrate/internal/apierror" + "go.ate.dev/substrate/internal/resources" + "go.ate.dev/substrate/pkg/proto/ateapipb" "k8s.io/apimachinery/pkg/util/validation/field" ) diff --git a/cmd/ateapi/internal/authz/registry_test.go b/cmd/ateapi/internal/authz/registry_test.go index ce3f8ceaf6..b042904319 100644 --- a/cmd/ateapi/internal/authz/registry_test.go +++ b/cmd/ateapi/internal/authz/registry_test.go @@ -19,8 +19,8 @@ import ( "strings" "testing" - "github.com/agent-substrate/substrate/internal/apierror" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "go.ate.dev/substrate/internal/apierror" + "go.ate.dev/substrate/pkg/proto/ateapipb" "google.golang.org/grpc/codes" ) diff --git a/cmd/ateapi/internal/controlapi/access_policy.go b/cmd/ateapi/internal/controlapi/access_policy.go index 1cdd5c0653..f83c5a3940 100644 --- a/cmd/ateapi/internal/controlapi/access_policy.go +++ b/cmd/ateapi/internal/controlapi/access_policy.go @@ -19,12 +19,12 @@ import ( "errors" "fmt" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/apivalidation" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/defaults" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/store" - "github.com/agent-substrate/substrate/internal/apierror" - "github.com/agent-substrate/substrate/internal/resources" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "go.ate.dev/substrate/cmd/ateapi/internal/apivalidation" + "go.ate.dev/substrate/cmd/ateapi/internal/defaults" + "go.ate.dev/substrate/cmd/ateapi/internal/store" + "go.ate.dev/substrate/internal/apierror" + "go.ate.dev/substrate/internal/resources" + "go.ate.dev/substrate/pkg/proto/ateapipb" "google.golang.org/protobuf/proto" "k8s.io/apimachinery/pkg/util/validation/field" ) diff --git a/cmd/ateapi/internal/controlapi/access_policy_errors_test.go b/cmd/ateapi/internal/controlapi/access_policy_errors_test.go index bf55298c63..fcc0f22603 100644 --- a/cmd/ateapi/internal/controlapi/access_policy_errors_test.go +++ b/cmd/ateapi/internal/controlapi/access_policy_errors_test.go @@ -19,8 +19,8 @@ import ( "fmt" "testing" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/store" - "github.com/agent-substrate/substrate/internal/apierror" + "go.ate.dev/substrate/cmd/ateapi/internal/store" + "go.ate.dev/substrate/internal/apierror" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" ) diff --git a/cmd/ateapi/internal/controlapi/access_policy_test.go b/cmd/ateapi/internal/controlapi/access_policy_test.go index bbc67e57d3..5cd72494ba 100644 --- a/cmd/ateapi/internal/controlapi/access_policy_test.go +++ b/cmd/ateapi/internal/controlapi/access_policy_test.go @@ -19,12 +19,12 @@ import ( "strings" "testing" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/authz" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/store/storetest" - "github.com/agent-substrate/substrate/internal/apierror" - "github.com/agent-substrate/substrate/internal/principal" - "github.com/agent-substrate/substrate/internal/resources" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "go.ate.dev/substrate/cmd/ateapi/internal/authz" + "go.ate.dev/substrate/cmd/ateapi/internal/store/storetest" + "go.ate.dev/substrate/internal/apierror" + "go.ate.dev/substrate/internal/principal" + "go.ate.dev/substrate/internal/resources" + "go.ate.dev/substrate/pkg/proto/ateapipb" "google.golang.org/grpc" "google.golang.org/grpc/codes" ) diff --git a/cmd/ateapi/internal/controlapi/actor.go b/cmd/ateapi/internal/controlapi/actor.go index b0591d18e0..9b0ad3f727 100644 --- a/cmd/ateapi/internal/controlapi/actor.go +++ b/cmd/ateapi/internal/controlapi/actor.go @@ -25,16 +25,16 @@ import ( "slices" "time" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/apivalidation" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/defaults" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/store" - "github.com/agent-substrate/substrate/internal/actoridjwt" - "github.com/agent-substrate/substrate/internal/apierror" - "github.com/agent-substrate/substrate/internal/ateattr" - "github.com/agent-substrate/substrate/internal/preview" - "github.com/agent-substrate/substrate/internal/resources" - "github.com/agent-substrate/substrate/internal/substratex509" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "go.ate.dev/substrate/cmd/ateapi/internal/apivalidation" + "go.ate.dev/substrate/cmd/ateapi/internal/defaults" + "go.ate.dev/substrate/cmd/ateapi/internal/store" + "go.ate.dev/substrate/internal/actoridjwt" + "go.ate.dev/substrate/internal/apierror" + "go.ate.dev/substrate/internal/ateattr" + "go.ate.dev/substrate/internal/preview" + "go.ate.dev/substrate/internal/resources" + "go.ate.dev/substrate/internal/substratex509" + "go.ate.dev/substrate/pkg/proto/ateapipb" "go.opentelemetry.io/otel/attribute" "google.golang.org/grpc/credentials" "google.golang.org/grpc/peer" diff --git a/cmd/ateapi/internal/controlapi/actor_sizing_test.go b/cmd/ateapi/internal/controlapi/actor_sizing_test.go index 573257cfe9..d6cec28580 100644 --- a/cmd/ateapi/internal/controlapi/actor_sizing_test.go +++ b/cmd/ateapi/internal/controlapi/actor_sizing_test.go @@ -17,7 +17,7 @@ package controlapi import ( "testing" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "go.ate.dev/substrate/pkg/proto/ateapipb" ) // TestActorResourceLimits covers the actor-side extraction: the CPU/memory limits diff --git a/cmd/ateapi/internal/controlapi/actor_template.go b/cmd/ateapi/internal/controlapi/actor_template.go index c625463e1b..d362038038 100644 --- a/cmd/ateapi/internal/controlapi/actor_template.go +++ b/cmd/ateapi/internal/controlapi/actor_template.go @@ -19,12 +19,12 @@ import ( "errors" "fmt" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/apivalidation" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/defaults" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/store" - "github.com/agent-substrate/substrate/internal/apierror" - "github.com/agent-substrate/substrate/internal/resources" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "go.ate.dev/substrate/cmd/ateapi/internal/apivalidation" + "go.ate.dev/substrate/cmd/ateapi/internal/defaults" + "go.ate.dev/substrate/cmd/ateapi/internal/store" + "go.ate.dev/substrate/internal/apierror" + "go.ate.dev/substrate/internal/resources" + "go.ate.dev/substrate/pkg/proto/ateapipb" "google.golang.org/protobuf/proto" "k8s.io/apimachinery/pkg/util/validation/field" ) diff --git a/cmd/ateapi/internal/controlapi/actor_template_test.go b/cmd/ateapi/internal/controlapi/actor_template_test.go index b28a4d5747..b6028de4d3 100644 --- a/cmd/ateapi/internal/controlapi/actor_template_test.go +++ b/cmd/ateapi/internal/controlapi/actor_template_test.go @@ -21,15 +21,15 @@ import ( "strings" "testing" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/store" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/store/storetest" - "github.com/agent-substrate/substrate/internal/apierror" - "github.com/agent-substrate/substrate/internal/objectstoreplugin/objectstoreplugintest" - "github.com/agent-substrate/substrate/internal/resources" - atev1alpha1 "github.com/agent-substrate/substrate/pkg/api/v1alpha1" - listersv1alpha1 "github.com/agent-substrate/substrate/pkg/client/listers/api/v1alpha1" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" "github.com/google/go-cmp/cmp" + "go.ate.dev/substrate/cmd/ateapi/internal/store" + "go.ate.dev/substrate/cmd/ateapi/internal/store/storetest" + "go.ate.dev/substrate/internal/apierror" + "go.ate.dev/substrate/internal/objectstoreplugin/objectstoreplugintest" + "go.ate.dev/substrate/internal/resources" + atev1alpha1 "go.ate.dev/substrate/pkg/api/v1alpha1" + listersv1alpha1 "go.ate.dev/substrate/pkg/client/listers/api/v1alpha1" + "go.ate.dev/substrate/pkg/proto/ateapipb" "google.golang.org/grpc/codes" "google.golang.org/protobuf/testing/protocmp" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" diff --git a/cmd/ateapi/internal/controlapi/actor_test.go b/cmd/ateapi/internal/controlapi/actor_test.go index 7d3ae9b7b3..3ba9b7079b 100644 --- a/cmd/ateapi/internal/controlapi/actor_test.go +++ b/cmd/ateapi/internal/controlapi/actor_test.go @@ -26,16 +26,16 @@ import ( "testing" "time" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/apivalidation" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/store" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/store/storetest" - "github.com/agent-substrate/substrate/internal/apierror" - "github.com/agent-substrate/substrate/internal/ateattr" - "github.com/agent-substrate/substrate/internal/localca" - "github.com/agent-substrate/substrate/internal/resources" - "github.com/agent-substrate/substrate/internal/substratex509" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" "github.com/google/go-cmp/cmp" + "go.ate.dev/substrate/cmd/ateapi/internal/apivalidation" + "go.ate.dev/substrate/cmd/ateapi/internal/store" + "go.ate.dev/substrate/cmd/ateapi/internal/store/storetest" + "go.ate.dev/substrate/internal/apierror" + "go.ate.dev/substrate/internal/ateattr" + "go.ate.dev/substrate/internal/localca" + "go.ate.dev/substrate/internal/resources" + "go.ate.dev/substrate/internal/substratex509" + "go.ate.dev/substrate/pkg/proto/ateapipb" "google.golang.org/grpc/codes" "google.golang.org/grpc/credentials" "google.golang.org/grpc/peer" diff --git a/cmd/ateapi/internal/controlapi/atespace.go b/cmd/ateapi/internal/controlapi/atespace.go index f3f809a99d..2d72b37eef 100644 --- a/cmd/ateapi/internal/controlapi/atespace.go +++ b/cmd/ateapi/internal/controlapi/atespace.go @@ -19,12 +19,12 @@ import ( "errors" "fmt" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/apivalidation" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/defaults" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/store" - "github.com/agent-substrate/substrate/internal/apierror" - "github.com/agent-substrate/substrate/internal/resources" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "go.ate.dev/substrate/cmd/ateapi/internal/apivalidation" + "go.ate.dev/substrate/cmd/ateapi/internal/defaults" + "go.ate.dev/substrate/cmd/ateapi/internal/store" + "go.ate.dev/substrate/internal/apierror" + "go.ate.dev/substrate/internal/resources" + "go.ate.dev/substrate/pkg/proto/ateapipb" ) func (s *RPCService) CreateAtespace(ctx context.Context, req *ateapipb.CreateAtespaceRequest) (*ateapipb.Atespace, error) { diff --git a/cmd/ateapi/internal/controlapi/atespace_test.go b/cmd/ateapi/internal/controlapi/atespace_test.go index 4c8bf5613e..4564485076 100644 --- a/cmd/ateapi/internal/controlapi/atespace_test.go +++ b/cmd/ateapi/internal/controlapi/atespace_test.go @@ -18,14 +18,14 @@ import ( "context" "testing" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/authz" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/store" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/store/storetest" - "github.com/agent-substrate/substrate/internal/apierror" - "github.com/agent-substrate/substrate/internal/principal" - "github.com/agent-substrate/substrate/internal/resources" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" openfgav1 "github.com/openfga/api/proto/openfga/v1" + "go.ate.dev/substrate/cmd/ateapi/internal/authz" + "go.ate.dev/substrate/cmd/ateapi/internal/store" + "go.ate.dev/substrate/cmd/ateapi/internal/store/storetest" + "go.ate.dev/substrate/internal/apierror" + "go.ate.dev/substrate/internal/principal" + "go.ate.dev/substrate/internal/resources" + "go.ate.dev/substrate/pkg/proto/ateapipb" "google.golang.org/grpc" "google.golang.org/grpc/codes" ) diff --git a/cmd/ateapi/internal/controlapi/common_test.go b/cmd/ateapi/internal/controlapi/common_test.go index 55e18b7c19..2d5ec8bd7a 100644 --- a/cmd/ateapi/internal/controlapi/common_test.go +++ b/cmd/ateapi/internal/controlapi/common_test.go @@ -18,8 +18,8 @@ import ( "context" "testing" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/store" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "go.ate.dev/substrate/cmd/ateapi/internal/store" + "go.ate.dev/substrate/pkg/proto/ateapipb" "google.golang.org/protobuf/testing/protocmp" "k8s.io/apimachinery/pkg/util/validation/field" ) diff --git a/cmd/ateapi/internal/controlapi/converter.go b/cmd/ateapi/internal/controlapi/converter.go index bc391b03e6..174c371cc3 100644 --- a/cmd/ateapi/internal/controlapi/converter.go +++ b/cmd/ateapi/internal/controlapi/converter.go @@ -15,8 +15,8 @@ package controlapi import ( - "github.com/agent-substrate/substrate/internal/proto/ateletpb" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "go.ate.dev/substrate/internal/proto/ateletpb" + "go.ate.dev/substrate/pkg/proto/ateapipb" ) // fidelityToAtelet maps the public fidelity ladder onto the identical atelet diff --git a/cmd/ateapi/internal/controlapi/converter_test.go b/cmd/ateapi/internal/controlapi/converter_test.go index e99ed00583..5266c46e38 100644 --- a/cmd/ateapi/internal/controlapi/converter_test.go +++ b/cmd/ateapi/internal/controlapi/converter_test.go @@ -22,8 +22,8 @@ import ( "google.golang.org/protobuf/testing/protocmp" "google.golang.org/protobuf/types/known/timestamppb" - "github.com/agent-substrate/substrate/internal/proto/ateletpb" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "go.ate.dev/substrate/internal/proto/ateletpb" + "go.ate.dev/substrate/pkg/proto/ateapipb" ) // TestFidelityToAtelet pins the level-by-level mapping onto the atelet enum: diff --git a/cmd/ateapi/internal/controlapi/crash.go b/cmd/ateapi/internal/controlapi/crash.go index c40628c995..5b505d0355 100644 --- a/cmd/ateapi/internal/controlapi/crash.go +++ b/cmd/ateapi/internal/controlapi/crash.go @@ -21,12 +21,12 @@ import ( "log/slog" "strings" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/store" - "github.com/agent-substrate/substrate/internal/actorevent" - "github.com/agent-substrate/substrate/internal/apierror" - "github.com/agent-substrate/substrate/internal/ateattr" - "github.com/agent-substrate/substrate/internal/resources" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "go.ate.dev/substrate/cmd/ateapi/internal/store" + "go.ate.dev/substrate/internal/actorevent" + "go.ate.dev/substrate/internal/apierror" + "go.ate.dev/substrate/internal/ateattr" + "go.ate.dev/substrate/internal/resources" + "go.ate.dev/substrate/pkg/proto/ateapipb" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" "google.golang.org/protobuf/types/known/timestamppb" diff --git a/cmd/ateapi/internal/controlapi/crash_test.go b/cmd/ateapi/internal/controlapi/crash_test.go index 67a06958c2..8e39921a7e 100644 --- a/cmd/ateapi/internal/controlapi/crash_test.go +++ b/cmd/ateapi/internal/controlapi/crash_test.go @@ -27,14 +27,14 @@ import ( "testing" "time" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/store" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/store/storetest" - "github.com/agent-substrate/substrate/internal/actorevent" - "github.com/agent-substrate/substrate/internal/apierror" - "github.com/agent-substrate/substrate/internal/ateattr" - "github.com/agent-substrate/substrate/internal/resources" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" "github.com/google/go-cmp/cmp" + "go.ate.dev/substrate/cmd/ateapi/internal/store" + "go.ate.dev/substrate/cmd/ateapi/internal/store/storetest" + "go.ate.dev/substrate/internal/actorevent" + "go.ate.dev/substrate/internal/apierror" + "go.ate.dev/substrate/internal/ateattr" + "go.ate.dev/substrate/internal/resources" + "go.ate.dev/substrate/pkg/proto/ateapipb" "go.opentelemetry.io/otel/attribute" otellog "go.opentelemetry.io/otel/log" "go.opentelemetry.io/otel/log/global" diff --git a/cmd/ateapi/internal/controlapi/delete_options.go b/cmd/ateapi/internal/controlapi/delete_options.go index f9f6086850..85a83f1930 100644 --- a/cmd/ateapi/internal/controlapi/delete_options.go +++ b/cmd/ateapi/internal/controlapi/delete_options.go @@ -15,8 +15,8 @@ package controlapi import ( - "github.com/agent-substrate/substrate/cmd/ateapi/internal/store" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "go.ate.dev/substrate/cmd/ateapi/internal/store" + "go.ate.dev/substrate/pkg/proto/ateapipb" ) func toDeletePreconditions(opts *ateapipb.DeleteOptions) store.DeletePreconditions { diff --git a/cmd/ateapi/internal/controlapi/dialer.go b/cmd/ateapi/internal/controlapi/dialer.go index e5ba03b62c..4b96a60f42 100644 --- a/cmd/ateapi/internal/controlapi/dialer.go +++ b/cmd/ateapi/internal/controlapi/dialer.go @@ -24,13 +24,13 @@ import ( "strconv" "sync" - "github.com/agent-substrate/substrate/internal/atelet" - "github.com/agent-substrate/substrate/internal/credbundle" - "github.com/agent-substrate/substrate/internal/installdefaults" - "github.com/agent-substrate/substrate/internal/substratex509" "github.com/spiffe/go-spiffe/v2/bundle/x509bundle" "github.com/spiffe/go-spiffe/v2/spiffeid" "github.com/spiffe/go-spiffe/v2/svid/x509svid" + "go.ate.dev/substrate/internal/atelet" + "go.ate.dev/substrate/internal/credbundle" + "go.ate.dev/substrate/internal/installdefaults" + "go.ate.dev/substrate/internal/substratex509" "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc" "google.golang.org/grpc" "google.golang.org/grpc/credentials" diff --git a/cmd/ateapi/internal/controlapi/dialer_test.go b/cmd/ateapi/internal/controlapi/dialer_test.go index 14354c9cc5..906106393a 100644 --- a/cmd/ateapi/internal/controlapi/dialer_test.go +++ b/cmd/ateapi/internal/controlapi/dialer_test.go @@ -28,10 +28,10 @@ import ( "testing" "time" - "github.com/agent-substrate/substrate/internal/installdefaults" - "github.com/agent-substrate/substrate/internal/substratex509" "github.com/spiffe/go-spiffe/v2/bundle/x509bundle" "github.com/spiffe/go-spiffe/v2/spiffeid" + "go.ate.dev/substrate/internal/installdefaults" + "go.ate.dev/substrate/internal/substratex509" "google.golang.org/grpc" "google.golang.org/grpc/connectivity" "google.golang.org/grpc/credentials" diff --git a/cmd/ateapi/internal/controlapi/egress_policy.go b/cmd/ateapi/internal/controlapi/egress_policy.go index a2404088e7..0072db889f 100644 --- a/cmd/ateapi/internal/controlapi/egress_policy.go +++ b/cmd/ateapi/internal/controlapi/egress_policy.go @@ -19,12 +19,12 @@ import ( "errors" "fmt" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/apivalidation" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/defaults" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/store" - "github.com/agent-substrate/substrate/internal/apierror" - "github.com/agent-substrate/substrate/internal/resources" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "go.ate.dev/substrate/cmd/ateapi/internal/apivalidation" + "go.ate.dev/substrate/cmd/ateapi/internal/defaults" + "go.ate.dev/substrate/cmd/ateapi/internal/store" + "go.ate.dev/substrate/internal/apierror" + "go.ate.dev/substrate/internal/resources" + "go.ate.dev/substrate/pkg/proto/ateapipb" "google.golang.org/protobuf/proto" "k8s.io/apimachinery/pkg/util/validation/field" ) diff --git a/cmd/ateapi/internal/controlapi/egress_policy_test.go b/cmd/ateapi/internal/controlapi/egress_policy_test.go index 90f7777ade..207aa3adfb 100644 --- a/cmd/ateapi/internal/controlapi/egress_policy_test.go +++ b/cmd/ateapi/internal/controlapi/egress_policy_test.go @@ -17,11 +17,11 @@ package controlapi import ( "testing" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/store" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/store/storetest" - "github.com/agent-substrate/substrate/internal/apierror" - "github.com/agent-substrate/substrate/internal/resources" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "go.ate.dev/substrate/cmd/ateapi/internal/store" + "go.ate.dev/substrate/cmd/ateapi/internal/store/storetest" + "go.ate.dev/substrate/internal/apierror" + "go.ate.dev/substrate/internal/resources" + "go.ate.dev/substrate/pkg/proto/ateapipb" "google.golang.org/grpc/codes" "google.golang.org/protobuf/proto" ) diff --git a/cmd/ateapi/internal/controlapi/export_test.go b/cmd/ateapi/internal/controlapi/export_test.go index c569ad55ed..df91876b22 100644 --- a/cmd/ateapi/internal/controlapi/export_test.go +++ b/cmd/ateapi/internal/controlapi/export_test.go @@ -18,11 +18,11 @@ import ( "context" "testing" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/scheduling" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/store" - "github.com/agent-substrate/substrate/internal/ateattr" - "github.com/agent-substrate/substrate/internal/resources" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "go.ate.dev/substrate/cmd/ateapi/internal/scheduling" + "go.ate.dev/substrate/cmd/ateapi/internal/store" + "go.ate.dev/substrate/internal/ateattr" + "go.ate.dev/substrate/internal/resources" + "go.ate.dev/substrate/pkg/proto/ateapipb" "google.golang.org/grpc/codes" ) diff --git a/cmd/ateapi/internal/controlapi/functionaltest/access_policy_test.go b/cmd/ateapi/internal/controlapi/functionaltest/access_policy_test.go index 70d15ed8db..0e73d0a5b4 100644 --- a/cmd/ateapi/internal/controlapi/functionaltest/access_policy_test.go +++ b/cmd/ateapi/internal/controlapi/functionaltest/access_policy_test.go @@ -18,9 +18,9 @@ import ( "context" "testing" - "github.com/agent-substrate/substrate/internal/resources" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" "github.com/google/go-cmp/cmp" + "go.ate.dev/substrate/internal/resources" + "go.ate.dev/substrate/pkg/proto/ateapipb" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" "google.golang.org/protobuf/proto" diff --git a/cmd/ateapi/internal/controlapi/functionaltest/actor_template_test.go b/cmd/ateapi/internal/controlapi/functionaltest/actor_template_test.go index 2ff8dbdd28..27df83d3f0 100644 --- a/cmd/ateapi/internal/controlapi/functionaltest/actor_template_test.go +++ b/cmd/ateapi/internal/controlapi/functionaltest/actor_template_test.go @@ -20,14 +20,14 @@ import ( "testing" "time" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/controlapi" - "github.com/agent-substrate/substrate/internal/preview" - "github.com/agent-substrate/substrate/internal/resources" + "go.ate.dev/substrate/cmd/ateapi/internal/controlapi" + "go.ate.dev/substrate/internal/preview" + "go.ate.dev/substrate/internal/resources" "google.golang.org/grpc/status" "k8s.io/apimachinery/pkg/util/wait" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" "github.com/google/go-cmp/cmp" + "go.ate.dev/substrate/pkg/proto/ateapipb" "google.golang.org/grpc/codes" "google.golang.org/grpc/metadata" "google.golang.org/protobuf/testing/protocmp" diff --git a/cmd/ateapi/internal/controlapi/functionaltest/actor_test.go b/cmd/ateapi/internal/controlapi/functionaltest/actor_test.go index 8ee4796b6a..b061f3d5b3 100644 --- a/cmd/ateapi/internal/controlapi/functionaltest/actor_test.go +++ b/cmd/ateapi/internal/controlapi/functionaltest/actor_test.go @@ -28,19 +28,19 @@ import ( "time" "cloud.google.com/go/storage" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/store" - "github.com/agent-substrate/substrate/internal/actoridjwt" - "github.com/agent-substrate/substrate/internal/apierror" - "github.com/agent-substrate/substrate/internal/ateattr" - "github.com/agent-substrate/substrate/internal/objectstore" - "github.com/agent-substrate/substrate/internal/preview" - "github.com/agent-substrate/substrate/internal/proto/ateletpb" - "github.com/agent-substrate/substrate/internal/resources" - "github.com/agent-substrate/substrate/internal/volume" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" "github.com/aws/aws-sdk-go-v2/service/s3/types" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" + "go.ate.dev/substrate/cmd/ateapi/internal/store" + "go.ate.dev/substrate/internal/actoridjwt" + "go.ate.dev/substrate/internal/apierror" + "go.ate.dev/substrate/internal/ateattr" + "go.ate.dev/substrate/internal/objectstore" + "go.ate.dev/substrate/internal/preview" + "go.ate.dev/substrate/internal/proto/ateletpb" + "go.ate.dev/substrate/internal/resources" + "go.ate.dev/substrate/internal/volume" + "go.ate.dev/substrate/pkg/proto/ateapipb" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/sdk/metric/metricdata" "google.golang.org/grpc/codes" diff --git a/cmd/ateapi/internal/controlapi/functionaltest/atespace_test.go b/cmd/ateapi/internal/controlapi/functionaltest/atespace_test.go index c79db5bcf0..b682b50cd6 100644 --- a/cmd/ateapi/internal/controlapi/functionaltest/atespace_test.go +++ b/cmd/ateapi/internal/controlapi/functionaltest/atespace_test.go @@ -19,8 +19,8 @@ import ( "testing" "time" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" "github.com/google/go-cmp/cmp" + "go.ate.dev/substrate/pkg/proto/ateapipb" "google.golang.org/grpc/codes" "google.golang.org/protobuf/testing/protocmp" "google.golang.org/protobuf/types/known/timestamppb" diff --git a/cmd/ateapi/internal/controlapi/functionaltest/common_test.go b/cmd/ateapi/internal/controlapi/functionaltest/common_test.go index e993d92472..898276a500 100644 --- a/cmd/ateapi/internal/controlapi/functionaltest/common_test.go +++ b/cmd/ateapi/internal/controlapi/functionaltest/common_test.go @@ -22,24 +22,24 @@ import ( "testing" "time" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/controlapi" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/store" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/store/storetest" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/workercache" - "github.com/agent-substrate/substrate/internal/ateinterceptors" - "github.com/agent-substrate/substrate/internal/installdefaults" - "github.com/agent-substrate/substrate/internal/localca" - "github.com/agent-substrate/substrate/internal/localjwtauthority" - "github.com/agent-substrate/substrate/internal/objectstore/objectstoretest" - "github.com/agent-substrate/substrate/internal/objectstoreplugin/objectstoreplugintest" - "github.com/agent-substrate/substrate/internal/preview" - "github.com/agent-substrate/substrate/internal/resources" - "github.com/agent-substrate/substrate/internal/volume" - atev1alpha1 "github.com/agent-substrate/substrate/pkg/api/v1alpha1" - "github.com/agent-substrate/substrate/pkg/client/clientset/versioned" - "github.com/agent-substrate/substrate/pkg/client/informers/externalversions" - listersv1alpha1 "github.com/agent-substrate/substrate/pkg/client/listers/api/v1alpha1" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "go.ate.dev/substrate/cmd/ateapi/internal/controlapi" + "go.ate.dev/substrate/cmd/ateapi/internal/store" + "go.ate.dev/substrate/cmd/ateapi/internal/store/storetest" + "go.ate.dev/substrate/cmd/ateapi/internal/workercache" + "go.ate.dev/substrate/internal/ateinterceptors" + "go.ate.dev/substrate/internal/installdefaults" + "go.ate.dev/substrate/internal/localca" + "go.ate.dev/substrate/internal/localjwtauthority" + "go.ate.dev/substrate/internal/objectstore/objectstoretest" + "go.ate.dev/substrate/internal/objectstoreplugin/objectstoreplugintest" + "go.ate.dev/substrate/internal/preview" + "go.ate.dev/substrate/internal/resources" + "go.ate.dev/substrate/internal/volume" + atev1alpha1 "go.ate.dev/substrate/pkg/api/v1alpha1" + "go.ate.dev/substrate/pkg/client/clientset/versioned" + "go.ate.dev/substrate/pkg/client/informers/externalversions" + listersv1alpha1 "go.ate.dev/substrate/pkg/client/listers/api/v1alpha1" + "go.ate.dev/substrate/pkg/proto/ateapipb" "go.opentelemetry.io/otel/attribute" sdkmetric "go.opentelemetry.io/otel/sdk/metric" sdktrace "go.opentelemetry.io/otel/sdk/trace" diff --git a/cmd/ateapi/internal/controlapi/functionaltest/egress_policy_test.go b/cmd/ateapi/internal/controlapi/functionaltest/egress_policy_test.go index 65c5693b9f..470a53b499 100644 --- a/cmd/ateapi/internal/controlapi/functionaltest/egress_policy_test.go +++ b/cmd/ateapi/internal/controlapi/functionaltest/egress_policy_test.go @@ -19,9 +19,9 @@ import ( "testing" "time" - "github.com/agent-substrate/substrate/internal/resources" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" "github.com/google/go-cmp/cmp" + "go.ate.dev/substrate/internal/resources" + "go.ate.dev/substrate/pkg/proto/ateapipb" "google.golang.org/grpc/codes" "google.golang.org/protobuf/proto" "google.golang.org/protobuf/testing/protocmp" diff --git a/cmd/ateapi/internal/controlapi/functionaltest/main_test.go b/cmd/ateapi/internal/controlapi/functionaltest/main_test.go index f169e2523f..9ea0ae0df7 100644 --- a/cmd/ateapi/internal/controlapi/functionaltest/main_test.go +++ b/cmd/ateapi/internal/controlapi/functionaltest/main_test.go @@ -24,11 +24,11 @@ import ( "testing" "time" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/store/storetest" - "github.com/agent-substrate/substrate/internal/objectstore/objectstoretest" - "github.com/agent-substrate/substrate/internal/proto/ateletpb" - "github.com/agent-substrate/substrate/internal/serverboot" - "github.com/agent-substrate/substrate/internal/testenv" + "go.ate.dev/substrate/cmd/ateapi/internal/store/storetest" + "go.ate.dev/substrate/internal/objectstore/objectstoretest" + "go.ate.dev/substrate/internal/proto/ateletpb" + "go.ate.dev/substrate/internal/serverboot" + "go.ate.dev/substrate/internal/testenv" "google.golang.org/grpc" "google.golang.org/protobuf/proto" corev1 "k8s.io/api/core/v1" diff --git a/cmd/ateapi/internal/controlapi/functionaltest/tag_test.go b/cmd/ateapi/internal/controlapi/functionaltest/tag_test.go index 54a268a0d8..4972f12fe7 100644 --- a/cmd/ateapi/internal/controlapi/functionaltest/tag_test.go +++ b/cmd/ateapi/internal/controlapi/functionaltest/tag_test.go @@ -19,11 +19,11 @@ import ( "fmt" "testing" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/store" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/store/storetest" - "github.com/agent-substrate/substrate/internal/resources" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" "github.com/google/go-cmp/cmp" + "go.ate.dev/substrate/cmd/ateapi/internal/store" + "go.ate.dev/substrate/cmd/ateapi/internal/store/storetest" + "go.ate.dev/substrate/internal/resources" + "go.ate.dev/substrate/pkg/proto/ateapipb" "google.golang.org/grpc/codes" "google.golang.org/protobuf/testing/protocmp" ) diff --git a/cmd/ateapi/internal/controlapi/functionaltest/worker_contention_test.go b/cmd/ateapi/internal/controlapi/functionaltest/worker_contention_test.go index 8325eb9c20..63863ffd06 100644 --- a/cmd/ateapi/internal/controlapi/functionaltest/worker_contention_test.go +++ b/cmd/ateapi/internal/controlapi/functionaltest/worker_contention_test.go @@ -23,9 +23,9 @@ import ( "k8s.io/apimachinery/pkg/util/wait" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/store" - "github.com/agent-substrate/substrate/internal/resources" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "go.ate.dev/substrate/cmd/ateapi/internal/store" + "go.ate.dev/substrate/internal/resources" + "go.ate.dev/substrate/pkg/proto/ateapipb" ) // TestResumeActor_ConcurrentOntoOneWorker is the regression test for a refusal diff --git a/cmd/ateapi/internal/controlapi/functionaltest/worker_test.go b/cmd/ateapi/internal/controlapi/functionaltest/worker_test.go index 810f5bb59f..01cc0e7306 100644 --- a/cmd/ateapi/internal/controlapi/functionaltest/worker_test.go +++ b/cmd/ateapi/internal/controlapi/functionaltest/worker_test.go @@ -20,9 +20,9 @@ import ( "testing" "time" - "github.com/agent-substrate/substrate/internal/resources" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" "github.com/google/go-cmp/cmp" + "go.ate.dev/substrate/internal/resources" + "go.ate.dev/substrate/pkg/proto/ateapipb" "google.golang.org/grpc/codes" "google.golang.org/protobuf/testing/protocmp" "k8s.io/apimachinery/pkg/util/wait" diff --git a/cmd/ateapi/internal/controlapi/hosting.go b/cmd/ateapi/internal/controlapi/hosting.go index 1fa19663c9..852fb8c149 100644 --- a/cmd/ateapi/internal/controlapi/hosting.go +++ b/cmd/ateapi/internal/controlapi/hosting.go @@ -19,8 +19,8 @@ import ( "errors" "fmt" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/store" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "go.ate.dev/substrate/cmd/ateapi/internal/store" + "go.ate.dev/substrate/pkg/proto/ateapipb" ) // HostingNode is the node that holds the actor, and "" when no node does. The diff --git a/cmd/ateapi/internal/controlapi/hosting_test.go b/cmd/ateapi/internal/controlapi/hosting_test.go index 820049e96a..0480a8c2b1 100644 --- a/cmd/ateapi/internal/controlapi/hosting_test.go +++ b/cmd/ateapi/internal/controlapi/hosting_test.go @@ -20,13 +20,13 @@ import ( "testing" "time" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/scheduling" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/store" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/store/storetest" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/workercache" - "github.com/agent-substrate/substrate/internal/apierror" - "github.com/agent-substrate/substrate/internal/resources" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "go.ate.dev/substrate/cmd/ateapi/internal/scheduling" + "go.ate.dev/substrate/cmd/ateapi/internal/store" + "go.ate.dev/substrate/cmd/ateapi/internal/store/storetest" + "go.ate.dev/substrate/cmd/ateapi/internal/workercache" + "go.ate.dev/substrate/internal/apierror" + "go.ate.dev/substrate/internal/resources" + "go.ate.dev/substrate/pkg/proto/ateapipb" "google.golang.org/protobuf/proto" ) diff --git a/cmd/ateapi/internal/controlapi/main_test.go b/cmd/ateapi/internal/controlapi/main_test.go index b9afb01cd8..d60fd4bc2e 100644 --- a/cmd/ateapi/internal/controlapi/main_test.go +++ b/cmd/ateapi/internal/controlapi/main_test.go @@ -17,7 +17,7 @@ package controlapi import ( "testing" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/store/storetest" + "go.ate.dev/substrate/cmd/ateapi/internal/store/storetest" ) func TestMain(m *testing.M) { diff --git a/cmd/ateapi/internal/controlapi/metrics.go b/cmd/ateapi/internal/controlapi/metrics.go index 0bf5490f0d..67d898e185 100644 --- a/cmd/ateapi/internal/controlapi/metrics.go +++ b/cmd/ateapi/internal/controlapi/metrics.go @@ -19,10 +19,10 @@ import ( "fmt" "time" - "github.com/agent-substrate/substrate/internal/apierror" - "github.com/agent-substrate/substrate/internal/ateattr" - atev1alpha1 "github.com/agent-substrate/substrate/pkg/api/v1alpha1" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "go.ate.dev/substrate/internal/apierror" + "go.ate.dev/substrate/internal/ateattr" + atev1alpha1 "go.ate.dev/substrate/pkg/api/v1alpha1" + "go.ate.dev/substrate/pkg/proto/ateapipb" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/metric" "k8s.io/apimachinery/pkg/labels" diff --git a/cmd/ateapi/internal/controlapi/metrics_test.go b/cmd/ateapi/internal/controlapi/metrics_test.go index 8448b17f64..eb8bfe7654 100644 --- a/cmd/ateapi/internal/controlapi/metrics_test.go +++ b/cmd/ateapi/internal/controlapi/metrics_test.go @@ -21,10 +21,10 @@ import ( "testing" "time" - "github.com/agent-substrate/substrate/internal/apierror" - "github.com/agent-substrate/substrate/internal/ateattr" - atev1alpha1 "github.com/agent-substrate/substrate/pkg/api/v1alpha1" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "go.ate.dev/substrate/internal/apierror" + "go.ate.dev/substrate/internal/ateattr" + atev1alpha1 "go.ate.dev/substrate/pkg/api/v1alpha1" + "go.ate.dev/substrate/pkg/proto/ateapipb" "go.opentelemetry.io/otel/attribute" sdkmetric "go.opentelemetry.io/otel/sdk/metric" "go.opentelemetry.io/otel/sdk/metric/metricdata" diff --git a/cmd/ateapi/internal/controlapi/mint_after_release_test.go b/cmd/ateapi/internal/controlapi/mint_after_release_test.go index 3c2b808ece..369f0bb616 100644 --- a/cmd/ateapi/internal/controlapi/mint_after_release_test.go +++ b/cmd/ateapi/internal/controlapi/mint_after_release_test.go @@ -24,16 +24,16 @@ import ( "testing" "time" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/ateletauth/ateletauthtest" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/controlapi" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/store" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/store/storetest" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/workerservice" - "github.com/agent-substrate/substrate/internal/apierror" - "github.com/agent-substrate/substrate/internal/installdefaults" - "github.com/agent-substrate/substrate/internal/localca" - "github.com/agent-substrate/substrate/internal/resources" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "go.ate.dev/substrate/cmd/ateapi/internal/ateletauth/ateletauthtest" + "go.ate.dev/substrate/cmd/ateapi/internal/controlapi" + "go.ate.dev/substrate/cmd/ateapi/internal/store" + "go.ate.dev/substrate/cmd/ateapi/internal/store/storetest" + "go.ate.dev/substrate/cmd/ateapi/internal/workerservice" + "go.ate.dev/substrate/internal/apierror" + "go.ate.dev/substrate/internal/installdefaults" + "go.ate.dev/substrate/internal/localca" + "go.ate.dev/substrate/internal/resources" + "go.ate.dev/substrate/pkg/proto/ateapipb" "google.golang.org/grpc/codes" ) diff --git a/cmd/ateapi/internal/controlapi/pagination.go b/cmd/ateapi/internal/controlapi/pagination.go index f525f56cb9..61e24e74fc 100644 --- a/cmd/ateapi/internal/controlapi/pagination.go +++ b/cmd/ateapi/internal/controlapi/pagination.go @@ -17,9 +17,9 @@ package controlapi import ( "errors" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/store" - "github.com/agent-substrate/substrate/internal/apierror" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "go.ate.dev/substrate/cmd/ateapi/internal/store" + "go.ate.dev/substrate/internal/apierror" + "go.ate.dev/substrate/pkg/proto/ateapipb" ) const maxPageSize = 1000 diff --git a/cmd/ateapi/internal/controlapi/pagination_test.go b/cmd/ateapi/internal/controlapi/pagination_test.go index c8414f454b..81325afe79 100644 --- a/cmd/ateapi/internal/controlapi/pagination_test.go +++ b/cmd/ateapi/internal/controlapi/pagination_test.go @@ -17,8 +17,8 @@ package controlapi import ( "testing" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/store" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "go.ate.dev/substrate/cmd/ateapi/internal/store" + "go.ate.dev/substrate/pkg/proto/ateapipb" ) func TestToStoreListOptions(t *testing.T) { diff --git a/cmd/ateapi/internal/controlapi/sandbox_assets.go b/cmd/ateapi/internal/controlapi/sandbox_assets.go index ab3b838f54..47593c8462 100644 --- a/cmd/ateapi/internal/controlapi/sandbox_assets.go +++ b/cmd/ateapi/internal/controlapi/sandbox_assets.go @@ -17,11 +17,11 @@ package controlapi import ( "fmt" - "github.com/agent-substrate/substrate/internal/apierror" - "github.com/agent-substrate/substrate/internal/proto/ateletpb" - atev1alpha1 "github.com/agent-substrate/substrate/pkg/api/v1alpha1" - listersv1alpha1 "github.com/agent-substrate/substrate/pkg/client/listers/api/v1alpha1" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "go.ate.dev/substrate/internal/apierror" + "go.ate.dev/substrate/internal/proto/ateletpb" + atev1alpha1 "go.ate.dev/substrate/pkg/api/v1alpha1" + listersv1alpha1 "go.ate.dev/substrate/pkg/client/listers/api/v1alpha1" + "go.ate.dev/substrate/pkg/proto/ateapipb" k8serrors "k8s.io/apimachinery/pkg/api/errors" ) diff --git a/cmd/ateapi/internal/controlapi/sandbox_assets_test.go b/cmd/ateapi/internal/controlapi/sandbox_assets_test.go index 3cd70e2329..4bc227eb55 100644 --- a/cmd/ateapi/internal/controlapi/sandbox_assets_test.go +++ b/cmd/ateapi/internal/controlapi/sandbox_assets_test.go @@ -18,9 +18,9 @@ import ( "strings" "testing" - atev1alpha1 "github.com/agent-substrate/substrate/pkg/api/v1alpha1" - listersv1alpha1 "github.com/agent-substrate/substrate/pkg/client/listers/api/v1alpha1" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + atev1alpha1 "go.ate.dev/substrate/pkg/api/v1alpha1" + listersv1alpha1 "go.ate.dev/substrate/pkg/client/listers/api/v1alpha1" + "go.ate.dev/substrate/pkg/proto/ateapipb" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/tools/cache" "k8s.io/utils/ptr" diff --git a/cmd/ateapi/internal/controlapi/service.go b/cmd/ateapi/internal/controlapi/service.go index 0c3ae2fc85..750c89f4c6 100644 --- a/cmd/ateapi/internal/controlapi/service.go +++ b/cmd/ateapi/internal/controlapi/service.go @@ -18,16 +18,16 @@ import ( "context" "sync" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/store" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/workercache" - "github.com/agent-substrate/substrate/internal/localca" - "github.com/agent-substrate/substrate/internal/localjwtauthority" - "github.com/agent-substrate/substrate/internal/resources" - "github.com/agent-substrate/substrate/internal/volume" - "github.com/agent-substrate/substrate/internal/volume/csi" - listersv1alpha1 "github.com/agent-substrate/substrate/pkg/client/listers/api/v1alpha1" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" - objectstorev1 "github.com/agent-substrate/substrate/pkg/proto/objectstorepb/v1" + "go.ate.dev/substrate/cmd/ateapi/internal/store" + "go.ate.dev/substrate/cmd/ateapi/internal/workercache" + "go.ate.dev/substrate/internal/localca" + "go.ate.dev/substrate/internal/localjwtauthority" + "go.ate.dev/substrate/internal/resources" + "go.ate.dev/substrate/internal/volume" + "go.ate.dev/substrate/internal/volume/csi" + listersv1alpha1 "go.ate.dev/substrate/pkg/client/listers/api/v1alpha1" + "go.ate.dev/substrate/pkg/proto/ateapipb" + objectstorev1 "go.ate.dev/substrate/pkg/proto/objectstorepb/v1" storagev1listers "k8s.io/client-go/listers/storage/v1" ) diff --git a/cmd/ateapi/internal/controlapi/snapshot_helpers.go b/cmd/ateapi/internal/controlapi/snapshot_helpers.go index 2350e95bd0..d952125079 100644 --- a/cmd/ateapi/internal/controlapi/snapshot_helpers.go +++ b/cmd/ateapi/internal/controlapi/snapshot_helpers.go @@ -15,7 +15,7 @@ package controlapi import ( - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "go.ate.dev/substrate/pkg/proto/ateapipb" ) // findSnapshotByGeneration returns the Snapshot in snapshots with the given diff --git a/cmd/ateapi/internal/controlapi/snapshot_helpers_test.go b/cmd/ateapi/internal/controlapi/snapshot_helpers_test.go index c6eed381cc..647d44b5fc 100644 --- a/cmd/ateapi/internal/controlapi/snapshot_helpers_test.go +++ b/cmd/ateapi/internal/controlapi/snapshot_helpers_test.go @@ -17,8 +17,8 @@ package controlapi import ( "testing" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" "github.com/google/go-cmp/cmp" + "go.ate.dev/substrate/pkg/proto/ateapipb" "google.golang.org/protobuf/testing/protocmp" ) diff --git a/cmd/ateapi/internal/controlapi/snapshot_report.go b/cmd/ateapi/internal/controlapi/snapshot_report.go index 4d3cd4015b..01a2b5fa30 100644 --- a/cmd/ateapi/internal/controlapi/snapshot_report.go +++ b/cmd/ateapi/internal/controlapi/snapshot_report.go @@ -21,12 +21,12 @@ import ( "google.golang.org/protobuf/proto" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/store" - "github.com/agent-substrate/substrate/internal/apierror" - "github.com/agent-substrate/substrate/internal/preview" - "github.com/agent-substrate/substrate/internal/proto/ateletpb" - "github.com/agent-substrate/substrate/internal/resources" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "go.ate.dev/substrate/cmd/ateapi/internal/store" + "go.ate.dev/substrate/internal/apierror" + "go.ate.dev/substrate/internal/preview" + "go.ate.dev/substrate/internal/proto/ateletpb" + "go.ate.dev/substrate/internal/resources" + "go.ate.dev/substrate/pkg/proto/ateapipb" ) // ApplyActorSnapshotState records the snapshot state that the atelet on node diff --git a/cmd/ateapi/internal/controlapi/snapshot_report_test.go b/cmd/ateapi/internal/controlapi/snapshot_report_test.go index bd8ba5c036..b2f3e704b9 100644 --- a/cmd/ateapi/internal/controlapi/snapshot_report_test.go +++ b/cmd/ateapi/internal/controlapi/snapshot_report_test.go @@ -24,11 +24,11 @@ import ( "google.golang.org/grpc/codes" "google.golang.org/protobuf/testing/protocmp" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/store/storetest" - "github.com/agent-substrate/substrate/internal/apierror" - "github.com/agent-substrate/substrate/internal/proto/ateletpb" - "github.com/agent-substrate/substrate/internal/resources" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "go.ate.dev/substrate/cmd/ateapi/internal/store/storetest" + "go.ate.dev/substrate/internal/apierror" + "go.ate.dev/substrate/internal/proto/ateletpb" + "go.ate.dev/substrate/internal/resources" + "go.ate.dev/substrate/pkg/proto/ateapipb" ) const ( diff --git a/cmd/ateapi/internal/controlapi/span_identity.go b/cmd/ateapi/internal/controlapi/span_identity.go index a344daedd4..417be0f2be 100644 --- a/cmd/ateapi/internal/controlapi/span_identity.go +++ b/cmd/ateapi/internal/controlapi/span_identity.go @@ -19,9 +19,9 @@ import ( "go.opentelemetry.io/otel/trace" - "github.com/agent-substrate/substrate/internal/ateattr" - "github.com/agent-substrate/substrate/internal/resources" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "go.ate.dev/substrate/internal/ateattr" + "go.ate.dev/substrate/internal/resources" + "go.ate.dev/substrate/pkg/proto/ateapipb" ) // setSpanActorAttributes annotates the RPC's server span (from ctx) with the diff --git a/cmd/ateapi/internal/controlapi/span_identity_test.go b/cmd/ateapi/internal/controlapi/span_identity_test.go index 5ece95cc38..9bcf83d420 100644 --- a/cmd/ateapi/internal/controlapi/span_identity_test.go +++ b/cmd/ateapi/internal/controlapi/span_identity_test.go @@ -22,9 +22,9 @@ import ( sdktrace "go.opentelemetry.io/otel/sdk/trace" "go.opentelemetry.io/otel/sdk/trace/tracetest" - "github.com/agent-substrate/substrate/internal/ateattr" - "github.com/agent-substrate/substrate/internal/resources" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "go.ate.dev/substrate/internal/ateattr" + "go.ate.dev/substrate/internal/resources" + "go.ate.dev/substrate/pkg/proto/ateapipb" ) // recordRootSpanAttrs runs fn under a fresh recording root span from a local diff --git a/cmd/ateapi/internal/controlapi/tag.go b/cmd/ateapi/internal/controlapi/tag.go index c356290af1..cd6187076e 100644 --- a/cmd/ateapi/internal/controlapi/tag.go +++ b/cmd/ateapi/internal/controlapi/tag.go @@ -19,12 +19,12 @@ import ( "errors" "fmt" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/apivalidation" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/defaults" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/store" - "github.com/agent-substrate/substrate/internal/apierror" - "github.com/agent-substrate/substrate/internal/resources" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "go.ate.dev/substrate/cmd/ateapi/internal/apivalidation" + "go.ate.dev/substrate/cmd/ateapi/internal/defaults" + "go.ate.dev/substrate/cmd/ateapi/internal/store" + "go.ate.dev/substrate/internal/apierror" + "go.ate.dev/substrate/internal/resources" + "go.ate.dev/substrate/pkg/proto/ateapipb" "google.golang.org/protobuf/proto" "k8s.io/apimachinery/pkg/util/validation/field" ) diff --git a/cmd/ateapi/internal/controlapi/tag_test.go b/cmd/ateapi/internal/controlapi/tag_test.go index 7af624d22d..347dc3f65f 100644 --- a/cmd/ateapi/internal/controlapi/tag_test.go +++ b/cmd/ateapi/internal/controlapi/tag_test.go @@ -22,11 +22,11 @@ import ( "google.golang.org/grpc/codes" "google.golang.org/protobuf/testing/protocmp" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/store" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/store/storetest" - "github.com/agent-substrate/substrate/internal/apierror" - "github.com/agent-substrate/substrate/internal/resources" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "go.ate.dev/substrate/cmd/ateapi/internal/store" + "go.ate.dev/substrate/cmd/ateapi/internal/store/storetest" + "go.ate.dev/substrate/internal/apierror" + "go.ate.dev/substrate/internal/resources" + "go.ate.dev/substrate/pkg/proto/ateapipb" ) // TestListTags checks the atespace scoping and the paging of the diff --git a/cmd/ateapi/internal/controlapi/template_reconciler.go b/cmd/ateapi/internal/controlapi/template_reconciler.go index 2a00e3439b..cbb583de08 100644 --- a/cmd/ateapi/internal/controlapi/template_reconciler.go +++ b/cmd/ateapi/internal/controlapi/template_reconciler.go @@ -22,10 +22,10 @@ import ( "time" "unicode/utf8" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/store" - "github.com/agent-substrate/substrate/internal/apierror" - "github.com/agent-substrate/substrate/internal/resources" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "go.ate.dev/substrate/cmd/ateapi/internal/store" + "go.ate.dev/substrate/internal/apierror" + "go.ate.dev/substrate/internal/resources" + "go.ate.dev/substrate/pkg/proto/ateapipb" "google.golang.org/grpc/codes" "google.golang.org/protobuf/types/known/timestamppb" "k8s.io/apimachinery/pkg/util/wait" diff --git a/cmd/ateapi/internal/controlapi/template_reconciler_test.go b/cmd/ateapi/internal/controlapi/template_reconciler_test.go index f4dfd27dd9..274aae5a45 100644 --- a/cmd/ateapi/internal/controlapi/template_reconciler_test.go +++ b/cmd/ateapi/internal/controlapi/template_reconciler_test.go @@ -25,11 +25,11 @@ import ( "time" "unicode/utf8" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/apivalidation" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/store" - "github.com/agent-substrate/substrate/internal/apierror" - "github.com/agent-substrate/substrate/internal/resources" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "go.ate.dev/substrate/cmd/ateapi/internal/apivalidation" + "go.ate.dev/substrate/cmd/ateapi/internal/store" + "go.ate.dev/substrate/internal/apierror" + "go.ate.dev/substrate/internal/resources" + "go.ate.dev/substrate/pkg/proto/ateapipb" "google.golang.org/grpc/codes" "google.golang.org/protobuf/proto" "google.golang.org/protobuf/types/known/timestamppb" diff --git a/cmd/ateapi/internal/controlapi/validate.go b/cmd/ateapi/internal/controlapi/validate.go index 1c03d1708d..c83b3fdac3 100644 --- a/cmd/ateapi/internal/controlapi/validate.go +++ b/cmd/ateapi/internal/controlapi/validate.go @@ -15,8 +15,8 @@ package controlapi import ( - "github.com/agent-substrate/substrate/internal/apierror" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "go.ate.dev/substrate/internal/apierror" + "go.ate.dev/substrate/pkg/proto/ateapipb" "k8s.io/apimachinery/pkg/util/validation/field" ) diff --git a/cmd/ateapi/internal/controlapi/volumes.go b/cmd/ateapi/internal/controlapi/volumes.go index 021a79ccf9..fa9e25194e 100644 --- a/cmd/ateapi/internal/controlapi/volumes.go +++ b/cmd/ateapi/internal/controlapi/volumes.go @@ -20,9 +20,9 @@ import ( "fmt" "log/slog" - "github.com/agent-substrate/substrate/internal/apierror" - "github.com/agent-substrate/substrate/internal/volume" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "go.ate.dev/substrate/internal/apierror" + "go.ate.dev/substrate/internal/volume" + "go.ate.dev/substrate/pkg/proto/ateapipb" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" k8serrors "k8s.io/apimachinery/pkg/api/errors" diff --git a/cmd/ateapi/internal/controlapi/volumes_test.go b/cmd/ateapi/internal/controlapi/volumes_test.go index 545384d569..129cee7193 100644 --- a/cmd/ateapi/internal/controlapi/volumes_test.go +++ b/cmd/ateapi/internal/controlapi/volumes_test.go @@ -26,9 +26,9 @@ import ( "google.golang.org/grpc/status" "google.golang.org/protobuf/testing/protocmp" - "github.com/agent-substrate/substrate/internal/apierror" - "github.com/agent-substrate/substrate/internal/volume" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "go.ate.dev/substrate/internal/apierror" + "go.ate.dev/substrate/internal/volume" + "go.ate.dev/substrate/pkg/proto/ateapipb" storagev1 "k8s.io/api/storage/v1" k8serrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" diff --git a/cmd/ateapi/internal/controlapi/worker.go b/cmd/ateapi/internal/controlapi/worker.go index 8fb2d47c83..c7778cd82f 100644 --- a/cmd/ateapi/internal/controlapi/worker.go +++ b/cmd/ateapi/internal/controlapi/worker.go @@ -19,12 +19,12 @@ import ( "errors" "fmt" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/apivalidation" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/defaults" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/store" - "github.com/agent-substrate/substrate/internal/apierror" - "github.com/agent-substrate/substrate/internal/resources" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "go.ate.dev/substrate/cmd/ateapi/internal/apivalidation" + "go.ate.dev/substrate/cmd/ateapi/internal/defaults" + "go.ate.dev/substrate/cmd/ateapi/internal/store" + "go.ate.dev/substrate/internal/apierror" + "go.ate.dev/substrate/internal/resources" + "go.ate.dev/substrate/pkg/proto/ateapipb" "google.golang.org/protobuf/proto" "k8s.io/apimachinery/pkg/util/validation/field" ) diff --git a/cmd/ateapi/internal/controlapi/worker_assignment_reconciler.go b/cmd/ateapi/internal/controlapi/worker_assignment_reconciler.go index 45c3c85583..3b5bca3ac9 100644 --- a/cmd/ateapi/internal/controlapi/worker_assignment_reconciler.go +++ b/cmd/ateapi/internal/controlapi/worker_assignment_reconciler.go @@ -21,8 +21,8 @@ import ( "log/slog" "time" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/store" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "go.ate.dev/substrate/cmd/ateapi/internal/store" + "go.ate.dev/substrate/pkg/proto/ateapipb" "k8s.io/apimachinery/pkg/util/wait" "k8s.io/client-go/util/workqueue" ) diff --git a/cmd/ateapi/internal/controlapi/worker_assignment_reconciler_test.go b/cmd/ateapi/internal/controlapi/worker_assignment_reconciler_test.go index 9e47a76230..6962f9a882 100644 --- a/cmd/ateapi/internal/controlapi/worker_assignment_reconciler_test.go +++ b/cmd/ateapi/internal/controlapi/worker_assignment_reconciler_test.go @@ -20,9 +20,9 @@ import ( "testing" "time" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/store" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/workercache" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "go.ate.dev/substrate/cmd/ateapi/internal/store" + "go.ate.dev/substrate/cmd/ateapi/internal/workercache" + "go.ate.dev/substrate/pkg/proto/ateapipb" ) // noWorkers is a workerWatcher that never delivers a Worker. diff --git a/cmd/ateapi/internal/controlapi/worker_assignments.go b/cmd/ateapi/internal/controlapi/worker_assignments.go index cb44bc0202..a4654c6cf6 100644 --- a/cmd/ateapi/internal/controlapi/worker_assignments.go +++ b/cmd/ateapi/internal/controlapi/worker_assignments.go @@ -19,8 +19,8 @@ import ( "errors" "fmt" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/store" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "go.ate.dev/substrate/cmd/ateapi/internal/store" + "go.ate.dev/substrate/pkg/proto/ateapipb" ) type workerAssignmentReader interface { diff --git a/cmd/ateapi/internal/controlapi/worker_test.go b/cmd/ateapi/internal/controlapi/worker_test.go index ab422b2853..8ec1a6dcd3 100644 --- a/cmd/ateapi/internal/controlapi/worker_test.go +++ b/cmd/ateapi/internal/controlapi/worker_test.go @@ -20,11 +20,11 @@ import ( "strings" "testing" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/store" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/store/storetest" - "github.com/agent-substrate/substrate/internal/apierror" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" "github.com/google/go-cmp/cmp" + "go.ate.dev/substrate/cmd/ateapi/internal/store" + "go.ate.dev/substrate/cmd/ateapi/internal/store/storetest" + "go.ate.dev/substrate/internal/apierror" + "go.ate.dev/substrate/pkg/proto/ateapipb" "google.golang.org/grpc/codes" "google.golang.org/protobuf/proto" "google.golang.org/protobuf/testing/protocmp" diff --git a/cmd/ateapi/internal/controlapi/workflow.go b/cmd/ateapi/internal/controlapi/workflow.go index eb824400a3..c6e7a2a548 100644 --- a/cmd/ateapi/internal/controlapi/workflow.go +++ b/cmd/ateapi/internal/controlapi/workflow.go @@ -20,17 +20,17 @@ import ( "fmt" "log/slog" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/scheduling" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/store" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/workercache" - "github.com/agent-substrate/substrate/internal/actorevent" - "github.com/agent-substrate/substrate/internal/apierror" - "github.com/agent-substrate/substrate/internal/ateattr" - "github.com/agent-substrate/substrate/internal/pluginsocket" - "github.com/agent-substrate/substrate/internal/resources" - listersv1alpha1 "github.com/agent-substrate/substrate/pkg/client/listers/api/v1alpha1" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" - objectstorev1 "github.com/agent-substrate/substrate/pkg/proto/objectstorepb/v1" + "go.ate.dev/substrate/cmd/ateapi/internal/scheduling" + "go.ate.dev/substrate/cmd/ateapi/internal/store" + "go.ate.dev/substrate/cmd/ateapi/internal/workercache" + "go.ate.dev/substrate/internal/actorevent" + "go.ate.dev/substrate/internal/apierror" + "go.ate.dev/substrate/internal/ateattr" + "go.ate.dev/substrate/internal/pluginsocket" + "go.ate.dev/substrate/internal/resources" + listersv1alpha1 "go.ate.dev/substrate/pkg/client/listers/api/v1alpha1" + "go.ate.dev/substrate/pkg/proto/ateapipb" + objectstorev1 "go.ate.dev/substrate/pkg/proto/objectstorepb/v1" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/codes" diff --git a/cmd/ateapi/internal/controlapi/workflow_delete.go b/cmd/ateapi/internal/controlapi/workflow_delete.go index d0402c9e91..de65c19a54 100644 --- a/cmd/ateapi/internal/controlapi/workflow_delete.go +++ b/cmd/ateapi/internal/controlapi/workflow_delete.go @@ -20,13 +20,13 @@ import ( "fmt" "log/slog" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/store" - "github.com/agent-substrate/substrate/internal/apierror" - "github.com/agent-substrate/substrate/internal/ateattr" - "github.com/agent-substrate/substrate/internal/preview" - "github.com/agent-substrate/substrate/internal/proto/ateletpb" - "github.com/agent-substrate/substrate/internal/resources" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "go.ate.dev/substrate/cmd/ateapi/internal/store" + "go.ate.dev/substrate/internal/apierror" + "go.ate.dev/substrate/internal/ateattr" + "go.ate.dev/substrate/internal/preview" + "go.ate.dev/substrate/internal/proto/ateletpb" + "go.ate.dev/substrate/internal/resources" + "go.ate.dev/substrate/pkg/proto/ateapipb" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" ) diff --git a/cmd/ateapi/internal/controlapi/workflow_delete_test.go b/cmd/ateapi/internal/controlapi/workflow_delete_test.go index 7a90002b3f..779f8f4c9b 100644 --- a/cmd/ateapi/internal/controlapi/workflow_delete_test.go +++ b/cmd/ateapi/internal/controlapi/workflow_delete_test.go @@ -20,13 +20,13 @@ import ( "strings" "testing" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/store" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/store/storetest" - "github.com/agent-substrate/substrate/internal/apierror" - "github.com/agent-substrate/substrate/internal/objectstore/objectstoretest" - "github.com/agent-substrate/substrate/internal/objectstoreplugin/objectstoreplugintest" - "github.com/agent-substrate/substrate/internal/resources" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "go.ate.dev/substrate/cmd/ateapi/internal/store" + "go.ate.dev/substrate/cmd/ateapi/internal/store/storetest" + "go.ate.dev/substrate/internal/apierror" + "go.ate.dev/substrate/internal/objectstore/objectstoretest" + "go.ate.dev/substrate/internal/objectstoreplugin/objectstoreplugintest" + "go.ate.dev/substrate/internal/resources" + "go.ate.dev/substrate/pkg/proto/ateapipb" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" ) diff --git a/cmd/ateapi/internal/controlapi/workflow_objectstore_plugin_test.go b/cmd/ateapi/internal/controlapi/workflow_objectstore_plugin_test.go index 35f011e889..d32191fd5c 100644 --- a/cmd/ateapi/internal/controlapi/workflow_objectstore_plugin_test.go +++ b/cmd/ateapi/internal/controlapi/workflow_objectstore_plugin_test.go @@ -18,9 +18,9 @@ import ( "context" "testing" - "github.com/agent-substrate/substrate/internal/ateinterceptors" - "github.com/agent-substrate/substrate/internal/resources" - objectstorev1 "github.com/agent-substrate/substrate/pkg/proto/objectstorepb/v1" + "go.ate.dev/substrate/internal/ateinterceptors" + "go.ate.dev/substrate/internal/resources" + objectstorev1 "go.ate.dev/substrate/pkg/proto/objectstorepb/v1" "google.golang.org/grpc" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" diff --git a/cmd/ateapi/internal/controlapi/workflow_pause_test.go b/cmd/ateapi/internal/controlapi/workflow_pause_test.go index 5965d224b8..92a2b50204 100644 --- a/cmd/ateapi/internal/controlapi/workflow_pause_test.go +++ b/cmd/ateapi/internal/controlapi/workflow_pause_test.go @@ -20,13 +20,13 @@ import ( "strings" "testing" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/store/storetest" - "github.com/agent-substrate/substrate/internal/apierror" - "github.com/agent-substrate/substrate/internal/ateattr" - "github.com/agent-substrate/substrate/internal/proto/ateletpb" - "github.com/agent-substrate/substrate/internal/resources" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" "github.com/google/go-cmp/cmp" + "go.ate.dev/substrate/cmd/ateapi/internal/store/storetest" + "go.ate.dev/substrate/internal/apierror" + "go.ate.dev/substrate/internal/ateattr" + "go.ate.dev/substrate/internal/proto/ateletpb" + "go.ate.dev/substrate/internal/resources" + "go.ate.dev/substrate/pkg/proto/ateapipb" "google.golang.org/grpc/codes" "google.golang.org/protobuf/testing/protocmp" ) diff --git a/cmd/ateapi/internal/controlapi/workflow_reconcile_assignments.go b/cmd/ateapi/internal/controlapi/workflow_reconcile_assignments.go index 6116c60591..fc101af121 100644 --- a/cmd/ateapi/internal/controlapi/workflow_reconcile_assignments.go +++ b/cmd/ateapi/internal/controlapi/workflow_reconcile_assignments.go @@ -19,10 +19,10 @@ import ( "errors" "fmt" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/store" - "github.com/agent-substrate/substrate/internal/preview" - "github.com/agent-substrate/substrate/internal/resources" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "go.ate.dev/substrate/cmd/ateapi/internal/store" + "go.ate.dev/substrate/internal/preview" + "go.ate.dev/substrate/internal/resources" + "go.ate.dev/substrate/pkg/proto/ateapipb" ) // needsAssignmentReconcile reports whether worker's epoch has risen past the diff --git a/cmd/ateapi/internal/controlapi/workflow_reconcile_assignments_test.go b/cmd/ateapi/internal/controlapi/workflow_reconcile_assignments_test.go index f70e5f76ea..20c9b04e9f 100644 --- a/cmd/ateapi/internal/controlapi/workflow_reconcile_assignments_test.go +++ b/cmd/ateapi/internal/controlapi/workflow_reconcile_assignments_test.go @@ -20,13 +20,13 @@ import ( "testing" "time" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/scheduling" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/store" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/store/storetest" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/workercache" - "github.com/agent-substrate/substrate/internal/apierror" - "github.com/agent-substrate/substrate/internal/resources" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "go.ate.dev/substrate/cmd/ateapi/internal/scheduling" + "go.ate.dev/substrate/cmd/ateapi/internal/store" + "go.ate.dev/substrate/cmd/ateapi/internal/store/storetest" + "go.ate.dev/substrate/cmd/ateapi/internal/workercache" + "go.ate.dev/substrate/internal/apierror" + "go.ate.dev/substrate/internal/resources" + "go.ate.dev/substrate/pkg/proto/ateapipb" "google.golang.org/grpc/codes" ) diff --git a/cmd/ateapi/internal/controlapi/workflow_resume.go b/cmd/ateapi/internal/controlapi/workflow_resume.go index 670dd8f4ec..ffe448b47a 100644 --- a/cmd/ateapi/internal/controlapi/workflow_resume.go +++ b/cmd/ateapi/internal/controlapi/workflow_resume.go @@ -22,16 +22,16 @@ import ( "slices" "time" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/scheduling" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/store" - "github.com/agent-substrate/substrate/internal/apierror" - "github.com/agent-substrate/substrate/internal/ateattr" - "github.com/agent-substrate/substrate/internal/hardware" - "github.com/agent-substrate/substrate/internal/preview" - "github.com/agent-substrate/substrate/internal/proto/ateletpb" - "github.com/agent-substrate/substrate/internal/resources" - "github.com/agent-substrate/substrate/internal/volume" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "go.ate.dev/substrate/cmd/ateapi/internal/scheduling" + "go.ate.dev/substrate/cmd/ateapi/internal/store" + "go.ate.dev/substrate/internal/apierror" + "go.ate.dev/substrate/internal/ateattr" + "go.ate.dev/substrate/internal/hardware" + "go.ate.dev/substrate/internal/preview" + "go.ate.dev/substrate/internal/proto/ateletpb" + "go.ate.dev/substrate/internal/resources" + "go.ate.dev/substrate/internal/volume" + "go.ate.dev/substrate/pkg/proto/ateapipb" "google.golang.org/protobuf/proto" "k8s.io/apimachinery/pkg/api/resource" "k8s.io/apimachinery/pkg/labels" diff --git a/cmd/ateapi/internal/controlapi/workflow_resume_test.go b/cmd/ateapi/internal/controlapi/workflow_resume_test.go index 0d10cf915f..31777f5adf 100644 --- a/cmd/ateapi/internal/controlapi/workflow_resume_test.go +++ b/cmd/ateapi/internal/controlapi/workflow_resume_test.go @@ -23,20 +23,20 @@ import ( "testing" "time" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/scheduling" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/store" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/store/storetest" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/workercache" - "github.com/agent-substrate/substrate/internal/apierror" - "github.com/agent-substrate/substrate/internal/ateattr" - "github.com/agent-substrate/substrate/internal/installdefaults" - "github.com/agent-substrate/substrate/internal/preview" - "github.com/agent-substrate/substrate/internal/proto/ateletpb" - "github.com/agent-substrate/substrate/internal/resources" - "github.com/agent-substrate/substrate/internal/volume" - atev1alpha1 "github.com/agent-substrate/substrate/pkg/api/v1alpha1" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" "github.com/google/go-cmp/cmp" + "go.ate.dev/substrate/cmd/ateapi/internal/scheduling" + "go.ate.dev/substrate/cmd/ateapi/internal/store" + "go.ate.dev/substrate/cmd/ateapi/internal/store/storetest" + "go.ate.dev/substrate/cmd/ateapi/internal/workercache" + "go.ate.dev/substrate/internal/apierror" + "go.ate.dev/substrate/internal/ateattr" + "go.ate.dev/substrate/internal/installdefaults" + "go.ate.dev/substrate/internal/preview" + "go.ate.dev/substrate/internal/proto/ateletpb" + "go.ate.dev/substrate/internal/resources" + "go.ate.dev/substrate/internal/volume" + atev1alpha1 "go.ate.dev/substrate/pkg/api/v1alpha1" + "go.ate.dev/substrate/pkg/proto/ateapipb" sdkmetric "go.opentelemetry.io/otel/sdk/metric" "go.opentelemetry.io/otel/sdk/metric/metricdata" "google.golang.org/grpc" diff --git a/cmd/ateapi/internal/controlapi/workflow_revert.go b/cmd/ateapi/internal/controlapi/workflow_revert.go index 696a0d3799..ddc7e02552 100644 --- a/cmd/ateapi/internal/controlapi/workflow_revert.go +++ b/cmd/ateapi/internal/controlapi/workflow_revert.go @@ -22,12 +22,12 @@ import ( "slices" "time" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/store" - "github.com/agent-substrate/substrate/internal/apierror" - "github.com/agent-substrate/substrate/internal/ateattr" - "github.com/agent-substrate/substrate/internal/preview" - "github.com/agent-substrate/substrate/internal/resources" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "go.ate.dev/substrate/cmd/ateapi/internal/store" + "go.ate.dev/substrate/internal/apierror" + "go.ate.dev/substrate/internal/ateattr" + "go.ate.dev/substrate/internal/preview" + "go.ate.dev/substrate/internal/resources" + "go.ate.dev/substrate/pkg/proto/ateapipb" "go.opentelemetry.io/otel/attribute" ) diff --git a/cmd/ateapi/internal/controlapi/workflow_revert_test.go b/cmd/ateapi/internal/controlapi/workflow_revert_test.go index ecf052fb4f..32c65a2d70 100644 --- a/cmd/ateapi/internal/controlapi/workflow_revert_test.go +++ b/cmd/ateapi/internal/controlapi/workflow_revert_test.go @@ -18,10 +18,10 @@ import ( "context" "testing" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/store/storetest" - "github.com/agent-substrate/substrate/internal/apierror" - "github.com/agent-substrate/substrate/internal/resources" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "go.ate.dev/substrate/cmd/ateapi/internal/store/storetest" + "go.ate.dev/substrate/internal/apierror" + "go.ate.dev/substrate/internal/resources" + "go.ate.dev/substrate/pkg/proto/ateapipb" "google.golang.org/grpc/codes" ) diff --git a/cmd/ateapi/internal/controlapi/workflow_suspend.go b/cmd/ateapi/internal/controlapi/workflow_suspend.go index 4dfdb02d0b..c4af95ee13 100644 --- a/cmd/ateapi/internal/controlapi/workflow_suspend.go +++ b/cmd/ateapi/internal/controlapi/workflow_suspend.go @@ -21,13 +21,13 @@ import ( "log/slog" "time" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/store" - "github.com/agent-substrate/substrate/internal/apierror" - "github.com/agent-substrate/substrate/internal/ateattr" - "github.com/agent-substrate/substrate/internal/preview" - "github.com/agent-substrate/substrate/internal/proto/ateletpb" - "github.com/agent-substrate/substrate/internal/resources" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "go.ate.dev/substrate/cmd/ateapi/internal/store" + "go.ate.dev/substrate/internal/apierror" + "go.ate.dev/substrate/internal/ateattr" + "go.ate.dev/substrate/internal/preview" + "go.ate.dev/substrate/internal/proto/ateletpb" + "go.ate.dev/substrate/internal/resources" + "go.ate.dev/substrate/pkg/proto/ateapipb" "go.opentelemetry.io/otel/attribute" "google.golang.org/protobuf/proto" ) diff --git a/cmd/ateapi/internal/controlapi/workflow_suspend_test.go b/cmd/ateapi/internal/controlapi/workflow_suspend_test.go index 4f629f4874..47a8252fb3 100644 --- a/cmd/ateapi/internal/controlapi/workflow_suspend_test.go +++ b/cmd/ateapi/internal/controlapi/workflow_suspend_test.go @@ -21,15 +21,15 @@ import ( "strings" "testing" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/store" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/store/storetest" - "github.com/agent-substrate/substrate/internal/apierror" - "github.com/agent-substrate/substrate/internal/ateattr" - "github.com/agent-substrate/substrate/internal/installdefaults" - "github.com/agent-substrate/substrate/internal/proto/ateletpb" - "github.com/agent-substrate/substrate/internal/resources" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" "github.com/google/go-cmp/cmp" + "go.ate.dev/substrate/cmd/ateapi/internal/store" + "go.ate.dev/substrate/cmd/ateapi/internal/store/storetest" + "go.ate.dev/substrate/internal/apierror" + "go.ate.dev/substrate/internal/ateattr" + "go.ate.dev/substrate/internal/installdefaults" + "go.ate.dev/substrate/internal/proto/ateletpb" + "go.ate.dev/substrate/internal/resources" + "go.ate.dev/substrate/pkg/proto/ateapipb" "google.golang.org/grpc/codes" "google.golang.org/protobuf/proto" "google.golang.org/protobuf/testing/protocmp" diff --git a/cmd/ateapi/internal/controlapi/workflow_tag.go b/cmd/ateapi/internal/controlapi/workflow_tag.go index 384e0084da..690d52bc84 100644 --- a/cmd/ateapi/internal/controlapi/workflow_tag.go +++ b/cmd/ateapi/internal/controlapi/workflow_tag.go @@ -19,11 +19,11 @@ import ( "errors" "fmt" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/store" - "github.com/agent-substrate/substrate/internal/apierror" - "github.com/agent-substrate/substrate/internal/preview" - "github.com/agent-substrate/substrate/internal/resources" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "go.ate.dev/substrate/cmd/ateapi/internal/store" + "go.ate.dev/substrate/internal/apierror" + "go.ate.dev/substrate/internal/preview" + "go.ate.dev/substrate/internal/resources" + "go.ate.dev/substrate/pkg/proto/ateapipb" "google.golang.org/protobuf/proto" ) diff --git a/cmd/ateapi/internal/controlapi/workflow_tag_test.go b/cmd/ateapi/internal/controlapi/workflow_tag_test.go index c2de668b39..425bd24bd4 100644 --- a/cmd/ateapi/internal/controlapi/workflow_tag_test.go +++ b/cmd/ateapi/internal/controlapi/workflow_tag_test.go @@ -21,13 +21,13 @@ import ( "sync" "testing" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/store" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/store/storetest" - "github.com/agent-substrate/substrate/internal/apierror" - "github.com/agent-substrate/substrate/internal/objectstore/objectstoretest" - "github.com/agent-substrate/substrate/internal/resources" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" "github.com/google/go-cmp/cmp" + "go.ate.dev/substrate/cmd/ateapi/internal/store" + "go.ate.dev/substrate/cmd/ateapi/internal/store/storetest" + "go.ate.dev/substrate/internal/apierror" + "go.ate.dev/substrate/internal/objectstore/objectstoretest" + "go.ate.dev/substrate/internal/resources" + "go.ate.dev/substrate/pkg/proto/ateapipb" "google.golang.org/grpc/codes" "google.golang.org/protobuf/testing/protocmp" ) diff --git a/cmd/ateapi/internal/controlapi/workflow_template_delete.go b/cmd/ateapi/internal/controlapi/workflow_template_delete.go index 91fce8361b..553ee0585a 100644 --- a/cmd/ateapi/internal/controlapi/workflow_template_delete.go +++ b/cmd/ateapi/internal/controlapi/workflow_template_delete.go @@ -19,11 +19,11 @@ import ( "errors" "fmt" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/store" - "github.com/agent-substrate/substrate/internal/apierror" - "github.com/agent-substrate/substrate/internal/preview" - "github.com/agent-substrate/substrate/internal/resources" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "go.ate.dev/substrate/cmd/ateapi/internal/store" + "go.ate.dev/substrate/internal/apierror" + "go.ate.dev/substrate/internal/preview" + "go.ate.dev/substrate/internal/resources" + "go.ate.dev/substrate/pkg/proto/ateapipb" "google.golang.org/grpc/codes" ) diff --git a/cmd/ateapi/internal/controlapi/workflow_test.go b/cmd/ateapi/internal/controlapi/workflow_test.go index f60920fc40..b40faa7a06 100644 --- a/cmd/ateapi/internal/controlapi/workflow_test.go +++ b/cmd/ateapi/internal/controlapi/workflow_test.go @@ -18,12 +18,12 @@ import ( "context" "testing" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/store" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/store/storetest" - "github.com/agent-substrate/substrate/internal/actorevent" - "github.com/agent-substrate/substrate/internal/ateattr" - "github.com/agent-substrate/substrate/internal/resources" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "go.ate.dev/substrate/cmd/ateapi/internal/store" + "go.ate.dev/substrate/cmd/ateapi/internal/store/storetest" + "go.ate.dev/substrate/internal/actorevent" + "go.ate.dev/substrate/internal/ateattr" + "go.ate.dev/substrate/internal/resources" + "go.ate.dev/substrate/pkg/proto/ateapipb" ) // TestActorStateChangeRecords drives each transition that commits a new state diff --git a/cmd/ateapi/internal/controlapi/workflow_testutil_test.go b/cmd/ateapi/internal/controlapi/workflow_testutil_test.go index 3468e5066e..a28a68c710 100644 --- a/cmd/ateapi/internal/controlapi/workflow_testutil_test.go +++ b/cmd/ateapi/internal/controlapi/workflow_testutil_test.go @@ -20,15 +20,15 @@ import ( "slices" "testing" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/store" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/store/storetest" - "github.com/agent-substrate/substrate/internal/apierror" - "github.com/agent-substrate/substrate/internal/ateattr" - "github.com/agent-substrate/substrate/internal/objectstore/objectstoretest" - "github.com/agent-substrate/substrate/internal/objectstoreplugin/objectstoreplugintest" - "github.com/agent-substrate/substrate/internal/resources" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" "github.com/google/uuid" + "go.ate.dev/substrate/cmd/ateapi/internal/store" + "go.ate.dev/substrate/cmd/ateapi/internal/store/storetest" + "go.ate.dev/substrate/internal/apierror" + "go.ate.dev/substrate/internal/ateattr" + "go.ate.dev/substrate/internal/objectstore/objectstoretest" + "go.ate.dev/substrate/internal/objectstoreplugin/objectstoreplugintest" + "go.ate.dev/substrate/internal/resources" + "go.ate.dev/substrate/pkg/proto/ateapipb" sdkmetric "go.opentelemetry.io/otel/sdk/metric" "google.golang.org/grpc/codes" ) diff --git a/cmd/ateapi/internal/controlapi/workflow_worker_delete.go b/cmd/ateapi/internal/controlapi/workflow_worker_delete.go index 90c5e82e97..5aeec6555d 100644 --- a/cmd/ateapi/internal/controlapi/workflow_worker_delete.go +++ b/cmd/ateapi/internal/controlapi/workflow_worker_delete.go @@ -20,12 +20,12 @@ import ( "fmt" "log/slog" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/store" - "github.com/agent-substrate/substrate/internal/apierror" - "github.com/agent-substrate/substrate/internal/ateattr" - "github.com/agent-substrate/substrate/internal/preview" - "github.com/agent-substrate/substrate/internal/resources" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "go.ate.dev/substrate/cmd/ateapi/internal/store" + "go.ate.dev/substrate/internal/apierror" + "go.ate.dev/substrate/internal/ateattr" + "go.ate.dev/substrate/internal/preview" + "go.ate.dev/substrate/internal/resources" + "go.ate.dev/substrate/pkg/proto/ateapipb" ) // DeleteWorker executes the workflow to deregister a Worker. The caller reaches diff --git a/cmd/ateapi/internal/controlapi/workflow_worker_delete_test.go b/cmd/ateapi/internal/controlapi/workflow_worker_delete_test.go index fab6da3256..0bd5037174 100644 --- a/cmd/ateapi/internal/controlapi/workflow_worker_delete_test.go +++ b/cmd/ateapi/internal/controlapi/workflow_worker_delete_test.go @@ -20,12 +20,12 @@ import ( "strings" "testing" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/store" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/store/storetest" - "github.com/agent-substrate/substrate/internal/apierror" - "github.com/agent-substrate/substrate/internal/ateattr" - "github.com/agent-substrate/substrate/internal/resources" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "go.ate.dev/substrate/cmd/ateapi/internal/store" + "go.ate.dev/substrate/cmd/ateapi/internal/store/storetest" + "go.ate.dev/substrate/internal/apierror" + "go.ate.dev/substrate/internal/ateattr" + "go.ate.dev/substrate/internal/resources" + "go.ate.dev/substrate/pkg/proto/ateapipb" sdkmetric "go.opentelemetry.io/otel/sdk/metric" "google.golang.org/grpc/codes" ) diff --git a/cmd/ateapi/internal/controlapi/workload_spec.go b/cmd/ateapi/internal/controlapi/workload_spec.go index bed3c3f602..790d6b086a 100644 --- a/cmd/ateapi/internal/controlapi/workload_spec.go +++ b/cmd/ateapi/internal/controlapi/workload_spec.go @@ -17,8 +17,8 @@ package controlapi import ( "fmt" - "github.com/agent-substrate/substrate/internal/proto/ateletpb" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "go.ate.dev/substrate/internal/proto/ateletpb" + "go.ate.dev/substrate/pkg/proto/ateapipb" "k8s.io/apimachinery/pkg/api/resource" ) diff --git a/cmd/ateapi/internal/controlapi/workload_spec_test.go b/cmd/ateapi/internal/controlapi/workload_spec_test.go index a1b640ef0f..f5adef117a 100644 --- a/cmd/ateapi/internal/controlapi/workload_spec_test.go +++ b/cmd/ateapi/internal/controlapi/workload_spec_test.go @@ -17,9 +17,9 @@ package controlapi import ( "testing" - "github.com/agent-substrate/substrate/internal/proto/ateletpb" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" "github.com/google/go-cmp/cmp" + "go.ate.dev/substrate/internal/proto/ateletpb" + "go.ate.dev/substrate/pkg/proto/ateapipb" "google.golang.org/protobuf/testing/protocmp" ) diff --git a/cmd/ateapi/internal/defaults/defaults.go b/cmd/ateapi/internal/defaults/defaults.go index ff31f792d4..73849186a5 100644 --- a/cmd/ateapi/internal/defaults/defaults.go +++ b/cmd/ateapi/internal/defaults/defaults.go @@ -21,7 +21,7 @@ package defaults import ( "fmt" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "go.ate.dev/substrate/pkg/proto/ateapipb" "google.golang.org/protobuf/proto" ) diff --git a/cmd/ateapi/internal/defaults/defaults_test.go b/cmd/ateapi/internal/defaults/defaults_test.go index 5ccb5ba159..9c82226087 100644 --- a/cmd/ateapi/internal/defaults/defaults_test.go +++ b/cmd/ateapi/internal/defaults/defaults_test.go @@ -17,8 +17,8 @@ package defaults import ( "testing" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" "github.com/google/go-cmp/cmp" + "go.ate.dev/substrate/pkg/proto/ateapipb" "google.golang.org/protobuf/encoding/protojson" "google.golang.org/protobuf/proto" "google.golang.org/protobuf/testing/protocmp" diff --git a/cmd/ateapi/internal/scheduling/scheduling.go b/cmd/ateapi/internal/scheduling/scheduling.go index d820911dca..58c4d04bc9 100644 --- a/cmd/ateapi/internal/scheduling/scheduling.go +++ b/cmd/ateapi/internal/scheduling/scheduling.go @@ -22,9 +22,9 @@ import ( "math/rand" "slices" - "github.com/agent-substrate/substrate/internal/hardware" - "github.com/agent-substrate/substrate/internal/resources" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "go.ate.dev/substrate/internal/hardware" + "go.ate.dev/substrate/internal/resources" + "go.ate.dev/substrate/pkg/proto/ateapipb" "k8s.io/apimachinery/pkg/labels" ) diff --git a/cmd/ateapi/internal/scheduling/scheduling_test.go b/cmd/ateapi/internal/scheduling/scheduling_test.go index 28a0b5b73c..bf296d6a6b 100644 --- a/cmd/ateapi/internal/scheduling/scheduling_test.go +++ b/cmd/ateapi/internal/scheduling/scheduling_test.go @@ -19,8 +19,8 @@ import ( "errors" "testing" - "github.com/agent-substrate/substrate/internal/resources" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "go.ate.dev/substrate/internal/resources" + "go.ate.dev/substrate/pkg/proto/ateapipb" "k8s.io/apimachinery/pkg/labels" ) diff --git a/cmd/ateapi/internal/store/atepg/access_policy.go b/cmd/ateapi/internal/store/atepg/access_policy.go index e09880f33d..93759d49dd 100644 --- a/cmd/ateapi/internal/store/atepg/access_policy.go +++ b/cmd/ateapi/internal/store/atepg/access_policy.go @@ -19,9 +19,9 @@ import ( "errors" "fmt" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/store" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" "github.com/jackc/pgx/v5" + "go.ate.dev/substrate/cmd/ateapi/internal/store" + "go.ate.dev/substrate/pkg/proto/ateapipb" "google.golang.org/protobuf/proto" ) diff --git a/cmd/ateapi/internal/store/atepg/access_policy_test.go b/cmd/ateapi/internal/store/atepg/access_policy_test.go index 48abda0138..e2e89775b6 100644 --- a/cmd/ateapi/internal/store/atepg/access_policy_test.go +++ b/cmd/ateapi/internal/store/atepg/access_policy_test.go @@ -20,10 +20,10 @@ import ( "sync" "testing" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/authz" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/store" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" "github.com/google/go-cmp/cmp" + "go.ate.dev/substrate/cmd/ateapi/internal/authz" + "go.ate.dev/substrate/cmd/ateapi/internal/store" + "go.ate.dev/substrate/pkg/proto/ateapipb" "google.golang.org/protobuf/testing/protocmp" ) diff --git a/cmd/ateapi/internal/store/atepg/actor.go b/cmd/ateapi/internal/store/atepg/actor.go index 538d263511..f370f911a2 100644 --- a/cmd/ateapi/internal/store/atepg/actor.go +++ b/cmd/ateapi/internal/store/atepg/actor.go @@ -19,10 +19,10 @@ import ( "errors" "fmt" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/store" - "github.com/agent-substrate/substrate/internal/resources" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" "github.com/jackc/pgx/v5" + "go.ate.dev/substrate/cmd/ateapi/internal/store" + "go.ate.dev/substrate/internal/resources" + "go.ate.dev/substrate/pkg/proto/ateapipb" "google.golang.org/protobuf/proto" ) diff --git a/cmd/ateapi/internal/store/atepg/actor_template.go b/cmd/ateapi/internal/store/atepg/actor_template.go index 8a480ade6e..d49f6edee0 100644 --- a/cmd/ateapi/internal/store/atepg/actor_template.go +++ b/cmd/ateapi/internal/store/atepg/actor_template.go @@ -19,10 +19,10 @@ import ( "errors" "fmt" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/store" - "github.com/agent-substrate/substrate/internal/resources" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" "github.com/jackc/pgx/v5" + "go.ate.dev/substrate/cmd/ateapi/internal/store" + "go.ate.dev/substrate/internal/resources" + "go.ate.dev/substrate/pkg/proto/ateapipb" "google.golang.org/protobuf/proto" ) diff --git a/cmd/ateapi/internal/store/atepg/actor_template_test.go b/cmd/ateapi/internal/store/atepg/actor_template_test.go index 6560c1d699..48954008fe 100644 --- a/cmd/ateapi/internal/store/atepg/actor_template_test.go +++ b/cmd/ateapi/internal/store/atepg/actor_template_test.go @@ -20,10 +20,10 @@ import ( "fmt" "testing" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/store" - "github.com/agent-substrate/substrate/internal/resources" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" "github.com/google/go-cmp/cmp" + "go.ate.dev/substrate/cmd/ateapi/internal/store" + "go.ate.dev/substrate/internal/resources" + "go.ate.dev/substrate/pkg/proto/ateapipb" "google.golang.org/protobuf/testing/protocmp" ) diff --git a/cmd/ateapi/internal/store/atepg/actor_test.go b/cmd/ateapi/internal/store/atepg/actor_test.go index 337a758663..713f91aa30 100644 --- a/cmd/ateapi/internal/store/atepg/actor_test.go +++ b/cmd/ateapi/internal/store/atepg/actor_test.go @@ -20,9 +20,9 @@ import ( "fmt" "testing" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/store" - "github.com/agent-substrate/substrate/internal/resources" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "go.ate.dev/substrate/cmd/ateapi/internal/store" + "go.ate.dev/substrate/internal/resources" + "go.ate.dev/substrate/pkg/proto/ateapipb" ) func TestUpdateActor_ConcurrentWriteReturnsConflict(t *testing.T) { diff --git a/cmd/ateapi/internal/store/atepg/atepg.go b/cmd/ateapi/internal/store/atepg/atepg.go index d5945f1f70..a84c7218ec 100644 --- a/cmd/ateapi/internal/store/atepg/atepg.go +++ b/cmd/ateapi/internal/store/atepg/atepg.go @@ -30,14 +30,14 @@ import ( "sync" "time" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/authz" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/defaults" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/store" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" "github.com/google/uuid" "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgconn" "github.com/jackc/pgx/v5/pgxpool" + "go.ate.dev/substrate/cmd/ateapi/internal/authz" + "go.ate.dev/substrate/cmd/ateapi/internal/defaults" + "go.ate.dev/substrate/cmd/ateapi/internal/store" + "go.ate.dev/substrate/pkg/proto/ateapipb" "google.golang.org/protobuf/proto" "google.golang.org/protobuf/types/known/timestamppb" ) diff --git a/cmd/ateapi/internal/store/atepg/atepg_test.go b/cmd/ateapi/internal/store/atepg/atepg_test.go index 6c0e6c0bb0..a1063ba6bd 100644 --- a/cmd/ateapi/internal/store/atepg/atepg_test.go +++ b/cmd/ateapi/internal/store/atepg/atepg_test.go @@ -19,8 +19,8 @@ import ( "strings" "testing" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/store" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "go.ate.dev/substrate/cmd/ateapi/internal/store" + "go.ate.dev/substrate/pkg/proto/ateapipb" ) // A listing fails as a whole on one undecodable row, so the error has to name diff --git a/cmd/ateapi/internal/store/atepg/atespace.go b/cmd/ateapi/internal/store/atepg/atespace.go index 9d18c70329..c948b74850 100644 --- a/cmd/ateapi/internal/store/atepg/atespace.go +++ b/cmd/ateapi/internal/store/atepg/atespace.go @@ -19,9 +19,9 @@ import ( "errors" "fmt" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/store" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" "github.com/jackc/pgx/v5" + "go.ate.dev/substrate/cmd/ateapi/internal/store" + "go.ate.dev/substrate/pkg/proto/ateapipb" "google.golang.org/protobuf/proto" ) diff --git a/cmd/ateapi/internal/store/atepg/cert_auth_test.go b/cmd/ateapi/internal/store/atepg/cert_auth_test.go index c333f44307..d20f819f54 100644 --- a/cmd/ateapi/internal/store/atepg/cert_auth_test.go +++ b/cmd/ateapi/internal/store/atepg/cert_auth_test.go @@ -32,13 +32,13 @@ import ( "testing" "time" - "github.com/agent-substrate/substrate/internal/localca" - "github.com/agent-substrate/substrate/pkg/postgressetup" "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgxpool" "github.com/testcontainers/testcontainers-go" tcexec "github.com/testcontainers/testcontainers-go/exec" "github.com/testcontainers/testcontainers-go/modules/postgres" + "go.ate.dev/substrate/internal/localca" + "go.ate.dev/substrate/pkg/postgressetup" "sigs.k8s.io/yaml" ) diff --git a/cmd/ateapi/internal/store/atepg/common_test.go b/cmd/ateapi/internal/store/atepg/common_test.go index f85e8741a6..16a171524f 100644 --- a/cmd/ateapi/internal/store/atepg/common_test.go +++ b/cmd/ateapi/internal/store/atepg/common_test.go @@ -18,9 +18,9 @@ import ( "context" "testing" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/authz" - "github.com/agent-substrate/substrate/internal/installdefaults" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "go.ate.dev/substrate/cmd/ateapi/internal/authz" + "go.ate.dev/substrate/internal/installdefaults" + "go.ate.dev/substrate/pkg/proto/ateapipb" ) // clearAll truncates every table so the next test starts from an empty store diff --git a/cmd/ateapi/internal/store/atepg/contract_test.go b/cmd/ateapi/internal/store/atepg/contract_test.go index 5aefaf1d0b..d5ea2cf7b9 100644 --- a/cmd/ateapi/internal/store/atepg/contract_test.go +++ b/cmd/ateapi/internal/store/atepg/contract_test.go @@ -17,8 +17,8 @@ package atepg import ( "testing" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/store" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/store/storecontract" + "go.ate.dev/substrate/cmd/ateapi/internal/store" + "go.ate.dev/substrate/cmd/ateapi/internal/store/storecontract" ) // setupPostgresStore adapts the concrete setup helper to the interface-typed diff --git a/cmd/ateapi/internal/store/atepg/egress_policy.go b/cmd/ateapi/internal/store/atepg/egress_policy.go index 70c679656b..763c8ff37e 100644 --- a/cmd/ateapi/internal/store/atepg/egress_policy.go +++ b/cmd/ateapi/internal/store/atepg/egress_policy.go @@ -19,10 +19,10 @@ import ( "errors" "fmt" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/store" - "github.com/agent-substrate/substrate/internal/resources" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" "github.com/jackc/pgx/v5" + "go.ate.dev/substrate/cmd/ateapi/internal/store" + "go.ate.dev/substrate/internal/resources" + "go.ate.dev/substrate/pkg/proto/ateapipb" "google.golang.org/protobuf/proto" ) diff --git a/cmd/ateapi/internal/store/atepg/lease.go b/cmd/ateapi/internal/store/atepg/lease.go index 4303cec0fe..f5aa783b64 100644 --- a/cmd/ateapi/internal/store/atepg/lease.go +++ b/cmd/ateapi/internal/store/atepg/lease.go @@ -21,9 +21,9 @@ import ( "log/slog" "time" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/store" "github.com/google/uuid" "github.com/jackc/pgx/v5" + "go.ate.dev/substrate/cmd/ateapi/internal/store" ) // defaultLeaseTTL is how long a lease may go unrenewed before another client diff --git a/cmd/ateapi/internal/store/atepg/lease_test.go b/cmd/ateapi/internal/store/atepg/lease_test.go index a68859b909..c3d65b09eb 100644 --- a/cmd/ateapi/internal/store/atepg/lease_test.go +++ b/cmd/ateapi/internal/store/atepg/lease_test.go @@ -21,7 +21,7 @@ import ( "testing" "time" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/store" + "go.ate.dev/substrate/cmd/ateapi/internal/store" ) // countLeases returns how many lease rows exist for key. diff --git a/cmd/ateapi/internal/store/atepg/main_test.go b/cmd/ateapi/internal/store/atepg/main_test.go index 8009525698..a0ca3b1bf8 100644 --- a/cmd/ateapi/internal/store/atepg/main_test.go +++ b/cmd/ateapi/internal/store/atepg/main_test.go @@ -25,7 +25,7 @@ import ( "github.com/jackc/pgx/v5/pgxpool" "github.com/testcontainers/testcontainers-go/modules/postgres" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/store/dockerenv" + "go.ate.dev/substrate/cmd/ateapi/internal/store/dockerenv" ) // One Postgres container serves every test in this package; each test gets diff --git a/cmd/ateapi/internal/store/atepg/outbox.go b/cmd/ateapi/internal/store/atepg/outbox.go index ca9bdca410..3d1e71de45 100644 --- a/cmd/ateapi/internal/store/atepg/outbox.go +++ b/cmd/ateapi/internal/store/atepg/outbox.go @@ -28,9 +28,9 @@ import ( "strings" "time" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/store" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" "github.com/jackc/pgx/v5" + "go.ate.dev/substrate/cmd/ateapi/internal/store" + "go.ate.dev/substrate/pkg/proto/ateapipb" "google.golang.org/protobuf/proto" ) diff --git a/cmd/ateapi/internal/store/atepg/outbox_test.go b/cmd/ateapi/internal/store/atepg/outbox_test.go index 1e133660e2..9bbcee22a6 100644 --- a/cmd/ateapi/internal/store/atepg/outbox_test.go +++ b/cmd/ateapi/internal/store/atepg/outbox_test.go @@ -31,8 +31,8 @@ import ( "google.golang.org/protobuf/proto" "google.golang.org/protobuf/testing/protocmp" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/store" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "go.ate.dev/substrate/cmd/ateapi/internal/store" + "go.ate.dev/substrate/pkg/proto/ateapipb" ) // TestConnect_DedicatedWatchPool covers the auxiliary-pool path only Connect diff --git a/cmd/ateapi/internal/store/atepg/pagetoken.go b/cmd/ateapi/internal/store/atepg/pagetoken.go index ebf56b5ae2..837a84c98b 100644 --- a/cmd/ateapi/internal/store/atepg/pagetoken.go +++ b/cmd/ateapi/internal/store/atepg/pagetoken.go @@ -19,7 +19,7 @@ import ( "encoding/json" "fmt" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/store" + "go.ate.dev/substrate/cmd/ateapi/internal/store" ) // pageTokenVersion guards against decoding a token produced by an incompatible diff --git a/cmd/ateapi/internal/store/atepg/partition_test.go b/cmd/ateapi/internal/store/atepg/partition_test.go index 20ba51fd29..3da1a7497b 100644 --- a/cmd/ateapi/internal/store/atepg/partition_test.go +++ b/cmd/ateapi/internal/store/atepg/partition_test.go @@ -26,8 +26,8 @@ import ( "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgxpool" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/store" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/store/storecontract" + "go.ate.dev/substrate/cmd/ateapi/internal/store" + "go.ate.dev/substrate/cmd/ateapi/internal/store/storecontract" ) // TestActorsTablePartitionable exists to keep it possible to partition the diff --git a/cmd/ateapi/internal/store/atepg/postgressetup_test.go b/cmd/ateapi/internal/store/atepg/postgressetup_test.go index b3cc113668..512d704616 100644 --- a/cmd/ateapi/internal/store/atepg/postgressetup_test.go +++ b/cmd/ateapi/internal/store/atepg/postgressetup_test.go @@ -25,7 +25,7 @@ import ( "github.com/jackc/pgx/v5/pgxpool" tcexec "github.com/testcontainers/testcontainers-go/exec" - "github.com/agent-substrate/substrate/pkg/postgressetup" + "go.ate.dev/substrate/pkg/postgressetup" ) func TestPostgresSetupScript(t *testing.T) { diff --git a/cmd/ateapi/internal/store/atepg/tag.go b/cmd/ateapi/internal/store/atepg/tag.go index 2a42f7fe8e..794c9fada7 100644 --- a/cmd/ateapi/internal/store/atepg/tag.go +++ b/cmd/ateapi/internal/store/atepg/tag.go @@ -19,10 +19,10 @@ import ( "errors" "fmt" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/store" - "github.com/agent-substrate/substrate/internal/resources" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" "github.com/jackc/pgx/v5" + "go.ate.dev/substrate/cmd/ateapi/internal/store" + "go.ate.dev/substrate/internal/resources" + "go.ate.dev/substrate/pkg/proto/ateapipb" "google.golang.org/protobuf/proto" ) diff --git a/cmd/ateapi/internal/store/atepg/tag_test.go b/cmd/ateapi/internal/store/atepg/tag_test.go index 9d4682dfc2..315fa8b68b 100644 --- a/cmd/ateapi/internal/store/atepg/tag_test.go +++ b/cmd/ateapi/internal/store/atepg/tag_test.go @@ -20,10 +20,10 @@ import ( "fmt" "testing" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/store" - "github.com/agent-substrate/substrate/internal/resources" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" "github.com/google/go-cmp/cmp" + "go.ate.dev/substrate/cmd/ateapi/internal/store" + "go.ate.dev/substrate/internal/resources" + "go.ate.dev/substrate/pkg/proto/ateapipb" "google.golang.org/protobuf/testing/protocmp" ) diff --git a/cmd/ateapi/internal/store/atepg/worker.go b/cmd/ateapi/internal/store/atepg/worker.go index a7428e1100..4e86bfed49 100644 --- a/cmd/ateapi/internal/store/atepg/worker.go +++ b/cmd/ateapi/internal/store/atepg/worker.go @@ -19,9 +19,9 @@ import ( "errors" "fmt" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/store" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" "github.com/jackc/pgx/v5" + "go.ate.dev/substrate/cmd/ateapi/internal/store" + "go.ate.dev/substrate/pkg/proto/ateapipb" "google.golang.org/protobuf/proto" ) diff --git a/cmd/ateapi/internal/store/atepg/worker_assignment.go b/cmd/ateapi/internal/store/atepg/worker_assignment.go index 4e85ae595a..b7e49f489f 100644 --- a/cmd/ateapi/internal/store/atepg/worker_assignment.go +++ b/cmd/ateapi/internal/store/atepg/worker_assignment.go @@ -19,10 +19,10 @@ import ( "errors" "fmt" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/store" - "github.com/agent-substrate/substrate/internal/resources" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" "github.com/jackc/pgx/v5" + "go.ate.dev/substrate/cmd/ateapi/internal/store" + "go.ate.dev/substrate/internal/resources" + "go.ate.dev/substrate/pkg/proto/ateapipb" "google.golang.org/protobuf/proto" ) diff --git a/cmd/ateapi/internal/store/atepg/worker_assignment_test.go b/cmd/ateapi/internal/store/atepg/worker_assignment_test.go index 7a26a5e26d..ae3ea6dec2 100644 --- a/cmd/ateapi/internal/store/atepg/worker_assignment_test.go +++ b/cmd/ateapi/internal/store/atepg/worker_assignment_test.go @@ -19,8 +19,8 @@ import ( "errors" "testing" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/store" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "go.ate.dev/substrate/cmd/ateapi/internal/store" + "go.ate.dev/substrate/pkg/proto/ateapipb" ) // TestSaveWorker_RejectsAStaleWrite proves the precondition saveWorker states diff --git a/cmd/ateapi/internal/store/store.go b/cmd/ateapi/internal/store/store.go index cc494082c1..254bf64f44 100644 --- a/cmd/ateapi/internal/store/store.go +++ b/cmd/ateapi/internal/store/store.go @@ -21,8 +21,8 @@ import ( "fmt" "sync" - "github.com/agent-substrate/substrate/internal/resources" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "go.ate.dev/substrate/internal/resources" + "go.ate.dev/substrate/pkg/proto/ateapipb" ) var ( diff --git a/cmd/ateapi/internal/store/store_test.go b/cmd/ateapi/internal/store/store_test.go index 477dc06f2e..b5646d4dfc 100644 --- a/cmd/ateapi/internal/store/store_test.go +++ b/cmd/ateapi/internal/store/store_test.go @@ -18,7 +18,7 @@ import ( "errors" "testing" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "go.ate.dev/substrate/pkg/proto/ateapipb" ) const ( diff --git a/cmd/ateapi/internal/store/storecontract/contract.go b/cmd/ateapi/internal/store/storecontract/contract.go index 2093146326..847e7cc926 100644 --- a/cmd/ateapi/internal/store/storecontract/contract.go +++ b/cmd/ateapi/internal/store/storecontract/contract.go @@ -35,9 +35,9 @@ import ( "google.golang.org/protobuf/testing/protocmp" "google.golang.org/protobuf/types/known/timestamppb" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/store" - "github.com/agent-substrate/substrate/internal/resources" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "go.ate.dev/substrate/cmd/ateapi/internal/store" + "go.ate.dev/substrate/internal/resources" + "go.ate.dev/substrate/pkg/proto/ateapipb" ) // testAtespace is the atespace used by tests that create a single actor. diff --git a/cmd/ateapi/internal/store/storetest/storetest.go b/cmd/ateapi/internal/store/storetest/storetest.go index 96d7e17428..9976457da9 100644 --- a/cmd/ateapi/internal/store/storetest/storetest.go +++ b/cmd/ateapi/internal/store/storetest/storetest.go @@ -29,14 +29,14 @@ import ( "testing" "time" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/authz" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/store" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/store/atepg" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/store/dockerenv" - "github.com/agent-substrate/substrate/internal/installdefaults" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" "github.com/jackc/pgx/v5/pgxpool" "github.com/testcontainers/testcontainers-go/modules/postgres" + "go.ate.dev/substrate/cmd/ateapi/internal/authz" + "go.ate.dev/substrate/cmd/ateapi/internal/store" + "go.ate.dev/substrate/cmd/ateapi/internal/store/atepg" + "go.ate.dev/substrate/cmd/ateapi/internal/store/dockerenv" + "go.ate.dev/substrate/internal/installdefaults" + "go.ate.dev/substrate/pkg/proto/ateapipb" ) var ( diff --git a/cmd/ateapi/internal/workercache/workercache.go b/cmd/ateapi/internal/workercache/workercache.go index d150f3cccc..140bb49f45 100644 --- a/cmd/ateapi/internal/workercache/workercache.go +++ b/cmd/ateapi/internal/workercache/workercache.go @@ -26,8 +26,8 @@ import ( "sync/atomic" "time" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/store" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "go.ate.dev/substrate/cmd/ateapi/internal/store" + "go.ate.dev/substrate/pkg/proto/ateapipb" "k8s.io/apimachinery/pkg/util/wait" ) diff --git a/cmd/ateapi/internal/workercache/workercache_test.go b/cmd/ateapi/internal/workercache/workercache_test.go index 0fc35c4e43..bc6d69ad2f 100644 --- a/cmd/ateapi/internal/workercache/workercache_test.go +++ b/cmd/ateapi/internal/workercache/workercache_test.go @@ -21,11 +21,11 @@ import ( "testing" "time" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/store" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/workercache" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" + "go.ate.dev/substrate/cmd/ateapi/internal/store" + "go.ate.dev/substrate/cmd/ateapi/internal/workercache" + "go.ate.dev/substrate/pkg/proto/ateapipb" "google.golang.org/protobuf/testing/protocmp" "k8s.io/apimachinery/pkg/util/wait" ) diff --git a/cmd/ateapi/internal/workerservice/certificate.go b/cmd/ateapi/internal/workerservice/certificate.go index 53f60d1276..7d75515811 100644 --- a/cmd/ateapi/internal/workerservice/certificate.go +++ b/cmd/ateapi/internal/workerservice/certificate.go @@ -24,13 +24,13 @@ import ( "path" "time" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/apivalidation" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/ateletauth" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/store" - "github.com/agent-substrate/substrate/internal/apierror" - "github.com/agent-substrate/substrate/internal/resources" - "github.com/agent-substrate/substrate/internal/substratex509" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "go.ate.dev/substrate/cmd/ateapi/internal/apivalidation" + "go.ate.dev/substrate/cmd/ateapi/internal/ateletauth" + "go.ate.dev/substrate/cmd/ateapi/internal/store" + "go.ate.dev/substrate/internal/apierror" + "go.ate.dev/substrate/internal/resources" + "go.ate.dev/substrate/internal/substratex509" + "go.ate.dev/substrate/pkg/proto/ateapipb" ) // MintWorkerActorCertificate mints a Substrate-issued SPIFFE certificate that asserts diff --git a/cmd/ateapi/internal/workerservice/certificate_test.go b/cmd/ateapi/internal/workerservice/certificate_test.go index a13796a651..afcbb1ea08 100644 --- a/cmd/ateapi/internal/workerservice/certificate_test.go +++ b/cmd/ateapi/internal/workerservice/certificate_test.go @@ -24,14 +24,14 @@ import ( "testing" "time" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/ateletauth/ateletauthtest" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/store" - "github.com/agent-substrate/substrate/internal/apierror" - "github.com/agent-substrate/substrate/internal/localca" - "github.com/agent-substrate/substrate/internal/resources" - "github.com/agent-substrate/substrate/internal/substratex509" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" "github.com/google/go-cmp/cmp" + "go.ate.dev/substrate/cmd/ateapi/internal/ateletauth/ateletauthtest" + "go.ate.dev/substrate/cmd/ateapi/internal/store" + "go.ate.dev/substrate/internal/apierror" + "go.ate.dev/substrate/internal/localca" + "go.ate.dev/substrate/internal/resources" + "go.ate.dev/substrate/internal/substratex509" + "go.ate.dev/substrate/pkg/proto/ateapipb" "google.golang.org/grpc/codes" ) diff --git a/cmd/ateapi/internal/workerservice/jwt.go b/cmd/ateapi/internal/workerservice/jwt.go index a5c35b0cf3..4889c43c8c 100644 --- a/cmd/ateapi/internal/workerservice/jwt.go +++ b/cmd/ateapi/internal/workerservice/jwt.go @@ -22,14 +22,14 @@ import ( "log/slog" "time" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/apivalidation" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/ateletauth" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/controlapi" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/store" - "github.com/agent-substrate/substrate/internal/actoridjwt" - "github.com/agent-substrate/substrate/internal/apierror" - "github.com/agent-substrate/substrate/internal/resources" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "go.ate.dev/substrate/cmd/ateapi/internal/apivalidation" + "go.ate.dev/substrate/cmd/ateapi/internal/ateletauth" + "go.ate.dev/substrate/cmd/ateapi/internal/controlapi" + "go.ate.dev/substrate/cmd/ateapi/internal/store" + "go.ate.dev/substrate/internal/actoridjwt" + "go.ate.dev/substrate/internal/apierror" + "go.ate.dev/substrate/internal/resources" + "go.ate.dev/substrate/pkg/proto/ateapipb" "google.golang.org/protobuf/types/known/timestamppb" ) diff --git a/cmd/ateapi/internal/workerservice/jwt_test.go b/cmd/ateapi/internal/workerservice/jwt_test.go index 11ce3fbe63..e3cb3e47dc 100644 --- a/cmd/ateapi/internal/workerservice/jwt_test.go +++ b/cmd/ateapi/internal/workerservice/jwt_test.go @@ -25,11 +25,11 @@ import ( "testing" "time" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/ateletauth/ateletauthtest" - "github.com/agent-substrate/substrate/internal/actoridjwt" - "github.com/agent-substrate/substrate/internal/apierror" - "github.com/agent-substrate/substrate/internal/localjwtauthority" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "go.ate.dev/substrate/cmd/ateapi/internal/ateletauth/ateletauthtest" + "go.ate.dev/substrate/internal/actoridjwt" + "go.ate.dev/substrate/internal/apierror" + "go.ate.dev/substrate/internal/localjwtauthority" + "go.ate.dev/substrate/pkg/proto/ateapipb" "google.golang.org/grpc/codes" ) diff --git a/cmd/ateapi/internal/workerservice/main_test.go b/cmd/ateapi/internal/workerservice/main_test.go index 04d3bc84b9..906f13b8b5 100644 --- a/cmd/ateapi/internal/workerservice/main_test.go +++ b/cmd/ateapi/internal/workerservice/main_test.go @@ -17,7 +17,7 @@ package workerservice import ( "testing" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/store/storetest" + "go.ate.dev/substrate/cmd/ateapi/internal/store/storetest" ) func TestMain(m *testing.M) { diff --git a/cmd/ateapi/internal/workerservice/register.go b/cmd/ateapi/internal/workerservice/register.go index e17f090548..625e8c7e6b 100644 --- a/cmd/ateapi/internal/workerservice/register.go +++ b/cmd/ateapi/internal/workerservice/register.go @@ -22,12 +22,12 @@ import ( "log/slog" "slices" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/apivalidation" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/ateletauth" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/store" - "github.com/agent-substrate/substrate/internal/apierror" - "github.com/agent-substrate/substrate/internal/resources" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "go.ate.dev/substrate/cmd/ateapi/internal/apivalidation" + "go.ate.dev/substrate/cmd/ateapi/internal/ateletauth" + "go.ate.dev/substrate/cmd/ateapi/internal/store" + "go.ate.dev/substrate/internal/apierror" + "go.ate.dev/substrate/internal/resources" + "go.ate.dev/substrate/pkg/proto/ateapipb" "google.golang.org/protobuf/proto" ) diff --git a/cmd/ateapi/internal/workerservice/register_test.go b/cmd/ateapi/internal/workerservice/register_test.go index 2209d25668..2dec5e845d 100644 --- a/cmd/ateapi/internal/workerservice/register_test.go +++ b/cmd/ateapi/internal/workerservice/register_test.go @@ -23,11 +23,11 @@ import ( "github.com/google/go-cmp/cmp/cmpopts" "google.golang.org/protobuf/testing/protocmp" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/ateletauth/ateletauthtest" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/store/storetest" - "github.com/agent-substrate/substrate/internal/apierror" - "github.com/agent-substrate/substrate/internal/resources" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "go.ate.dev/substrate/cmd/ateapi/internal/ateletauth/ateletauthtest" + "go.ate.dev/substrate/cmd/ateapi/internal/store/storetest" + "go.ate.dev/substrate/internal/apierror" + "go.ate.dev/substrate/internal/resources" + "go.ate.dev/substrate/pkg/proto/ateapipb" "google.golang.org/grpc/codes" ) diff --git a/cmd/ateapi/internal/workerservice/report.go b/cmd/ateapi/internal/workerservice/report.go index 3cc6b92d35..15888f5b11 100644 --- a/cmd/ateapi/internal/workerservice/report.go +++ b/cmd/ateapi/internal/workerservice/report.go @@ -17,10 +17,10 @@ package workerservice import ( "context" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/apivalidation" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/ateletauth" - "github.com/agent-substrate/substrate/internal/resources" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "go.ate.dev/substrate/cmd/ateapi/internal/apivalidation" + "go.ate.dev/substrate/cmd/ateapi/internal/ateletauth" + "go.ate.dev/substrate/internal/resources" + "go.ate.dev/substrate/pkg/proto/ateapipb" ) // ReportActorSnapshotState records the snapshot state that the calling atelet diff --git a/cmd/ateapi/internal/workerservice/report_test.go b/cmd/ateapi/internal/workerservice/report_test.go index 398e5517a1..e11447e7e3 100644 --- a/cmd/ateapi/internal/workerservice/report_test.go +++ b/cmd/ateapi/internal/workerservice/report_test.go @@ -22,10 +22,10 @@ import ( "google.golang.org/protobuf/proto" "google.golang.org/protobuf/types/known/timestamppb" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/ateletauth/ateletauthtest" - "github.com/agent-substrate/substrate/internal/apierror" - "github.com/agent-substrate/substrate/internal/resources" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "go.ate.dev/substrate/cmd/ateapi/internal/ateletauth/ateletauthtest" + "go.ate.dev/substrate/internal/apierror" + "go.ate.dev/substrate/internal/resources" + "go.ate.dev/substrate/pkg/proto/ateapipb" ) const reportActorUID = "5f2c1a90-7b34-4e6d-8a11-0c3e9d5b7f42" diff --git a/cmd/ateapi/internal/workerservice/service.go b/cmd/ateapi/internal/workerservice/service.go index 657eedff84..09ae137522 100644 --- a/cmd/ateapi/internal/workerservice/service.go +++ b/cmd/ateapi/internal/workerservice/service.go @@ -19,11 +19,11 @@ package workerservice import ( "context" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/store" - "github.com/agent-substrate/substrate/internal/localca" - "github.com/agent-substrate/substrate/internal/localjwtauthority" - "github.com/agent-substrate/substrate/internal/resources" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "go.ate.dev/substrate/cmd/ateapi/internal/store" + "go.ate.dev/substrate/internal/localca" + "go.ate.dev/substrate/internal/localjwtauthority" + "go.ate.dev/substrate/internal/resources" + "go.ate.dev/substrate/pkg/proto/ateapipb" ) // Server implements ateapipb.WorkerServiceServer. diff --git a/cmd/ateapi/internal/workerservice/service_test.go b/cmd/ateapi/internal/workerservice/service_test.go index 71455d4dec..0c29c10103 100644 --- a/cmd/ateapi/internal/workerservice/service_test.go +++ b/cmd/ateapi/internal/workerservice/service_test.go @@ -18,10 +18,10 @@ import ( "context" "testing" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/store" - "github.com/agent-substrate/substrate/internal/installdefaults" - "github.com/agent-substrate/substrate/internal/resources" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "go.ate.dev/substrate/cmd/ateapi/internal/store" + "go.ate.dev/substrate/internal/installdefaults" + "go.ate.dev/substrate/internal/resources" + "go.ate.dev/substrate/pkg/proto/ateapipb" ) // The Worker every test in this package calls about, and the node its atelet diff --git a/cmd/ateapi/internal/workerservice/suspend.go b/cmd/ateapi/internal/workerservice/suspend.go index 702e6f6b51..35d1a4c3f7 100644 --- a/cmd/ateapi/internal/workerservice/suspend.go +++ b/cmd/ateapi/internal/workerservice/suspend.go @@ -20,12 +20,12 @@ import ( "fmt" "log/slog" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/apivalidation" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/ateletauth" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/store" - "github.com/agent-substrate/substrate/internal/apierror" - "github.com/agent-substrate/substrate/internal/resources" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "go.ate.dev/substrate/cmd/ateapi/internal/apivalidation" + "go.ate.dev/substrate/cmd/ateapi/internal/ateletauth" + "go.ate.dev/substrate/cmd/ateapi/internal/store" + "go.ate.dev/substrate/internal/apierror" + "go.ate.dev/substrate/internal/resources" + "go.ate.dev/substrate/pkg/proto/ateapipb" ) // RequestActorSuspend suspends an Actor on behalf of the Worker hosting it. The diff --git a/cmd/ateapi/internal/workerservice/suspend_test.go b/cmd/ateapi/internal/workerservice/suspend_test.go index 7fa4c4a679..83866ec88b 100644 --- a/cmd/ateapi/internal/workerservice/suspend_test.go +++ b/cmd/ateapi/internal/workerservice/suspend_test.go @@ -22,12 +22,12 @@ import ( "google.golang.org/grpc/codes" "google.golang.org/protobuf/testing/protocmp" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/ateletauth/ateletauthtest" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/store" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/store/storetest" - "github.com/agent-substrate/substrate/internal/apierror" - "github.com/agent-substrate/substrate/internal/resources" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "go.ate.dev/substrate/cmd/ateapi/internal/ateletauth/ateletauthtest" + "go.ate.dev/substrate/cmd/ateapi/internal/store" + "go.ate.dev/substrate/cmd/ateapi/internal/store/storetest" + "go.ate.dev/substrate/internal/apierror" + "go.ate.dev/substrate/internal/resources" + "go.ate.dev/substrate/pkg/proto/ateapipb" ) const ( diff --git a/cmd/ateapi/main.go b/cmd/ateapi/main.go index 6205bcf6b7..6eef1e149f 100644 --- a/cmd/ateapi/main.go +++ b/cmd/ateapi/main.go @@ -27,32 +27,32 @@ import ( "syscall" "time" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/apiauthn" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/apiconfig" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/authz" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/controlapi" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/store/atepg" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/workercache" - "github.com/agent-substrate/substrate/cmd/ateapi/internal/workerservice" - "github.com/agent-substrate/substrate/internal/ateinterceptors" - "github.com/agent-substrate/substrate/internal/credbundle" - "github.com/agent-substrate/substrate/internal/installdefaults" - "github.com/agent-substrate/substrate/internal/localca" - "github.com/agent-substrate/substrate/internal/localjwtauthority" - "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/preview" - "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" - "github.com/agent-substrate/substrate/pkg/client/clientset/versioned" - "github.com/agent-substrate/substrate/pkg/client/informers/externalversions" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" - objectstorev1 "github.com/agent-substrate/substrate/pkg/proto/objectstorepb/v1" "github.com/jackc/pgx/v5/pgconn" "github.com/spf13/pflag" + "go.ate.dev/substrate/cmd/ateapi/internal/apiauthn" + "go.ate.dev/substrate/cmd/ateapi/internal/apiconfig" + "go.ate.dev/substrate/cmd/ateapi/internal/authz" + "go.ate.dev/substrate/cmd/ateapi/internal/controlapi" + "go.ate.dev/substrate/cmd/ateapi/internal/store/atepg" + "go.ate.dev/substrate/cmd/ateapi/internal/workercache" + "go.ate.dev/substrate/cmd/ateapi/internal/workerservice" + "go.ate.dev/substrate/internal/ateinterceptors" + "go.ate.dev/substrate/internal/credbundle" + "go.ate.dev/substrate/internal/installdefaults" + "go.ate.dev/substrate/internal/localca" + "go.ate.dev/substrate/internal/localjwtauthority" + "go.ate.dev/substrate/internal/oidcdiscovery" + "go.ate.dev/substrate/internal/oidcjwt" + "go.ate.dev/substrate/internal/pluginsocket" + "go.ate.dev/substrate/internal/preview" + "go.ate.dev/substrate/internal/resources" + "go.ate.dev/substrate/internal/serverboot" + "go.ate.dev/substrate/internal/version" + "go.ate.dev/substrate/internal/volume" + "go.ate.dev/substrate/pkg/client/clientset/versioned" + "go.ate.dev/substrate/pkg/client/informers/externalversions" + "go.ate.dev/substrate/pkg/proto/ateapipb" + objectstorev1 "go.ate.dev/substrate/pkg/proto/objectstorepb/v1" "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc" "go.opentelemetry.io/otel" "google.golang.org/grpc" diff --git a/cmd/atecontroller/internal/controllers/egressmitmtrust_controller.go b/cmd/atecontroller/internal/controllers/egressmitmtrust_controller.go index dae71830aa..2829539756 100644 --- a/cmd/atecontroller/internal/controllers/egressmitmtrust_controller.go +++ b/cmd/atecontroller/internal/controllers/egressmitmtrust_controller.go @@ -20,8 +20,8 @@ import ( "fmt" "strings" - "github.com/agent-substrate/substrate/internal/clustertrustbundle" - "github.com/agent-substrate/substrate/internal/localca" + "go.ate.dev/substrate/internal/clustertrustbundle" + "go.ate.dev/substrate/internal/localca" certsv1 "k8s.io/api/certificates/v1" certsv1beta1 "k8s.io/api/certificates/v1beta1" corev1 "k8s.io/api/core/v1" diff --git a/cmd/atecontroller/internal/controllers/egressmitmtrust_controller_test.go b/cmd/atecontroller/internal/controllers/egressmitmtrust_controller_test.go index 43e1e6ddcf..aa818267ff 100644 --- a/cmd/atecontroller/internal/controllers/egressmitmtrust_controller_test.go +++ b/cmd/atecontroller/internal/controllers/egressmitmtrust_controller_test.go @@ -21,9 +21,9 @@ import ( "testing" "time" - "github.com/agent-substrate/substrate/internal/clustertrustbundle" - "github.com/agent-substrate/substrate/internal/installdefaults" - "github.com/agent-substrate/substrate/internal/localca" + "go.ate.dev/substrate/internal/clustertrustbundle" + "go.ate.dev/substrate/internal/installdefaults" + "go.ate.dev/substrate/internal/localca" certsv1 "k8s.io/api/certificates/v1" certsv1beta1 "k8s.io/api/certificates/v1beta1" corev1 "k8s.io/api/core/v1" diff --git a/cmd/atecontroller/internal/controllers/networkpolicy_controller.go b/cmd/atecontroller/internal/controllers/networkpolicy_controller.go index 22c65bbbdd..a1f6b13a19 100644 --- a/cmd/atecontroller/internal/controllers/networkpolicy_controller.go +++ b/cmd/atecontroller/internal/controllers/networkpolicy_controller.go @@ -27,8 +27,8 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/log" - "github.com/agent-substrate/substrate/internal/resources" - atev1alpha1 "github.com/agent-substrate/substrate/pkg/api/v1alpha1" + "go.ate.dev/substrate/internal/resources" + atev1alpha1 "go.ate.dev/substrate/pkg/api/v1alpha1" ) const ( diff --git a/cmd/atecontroller/internal/controllers/networkpolicy_controller_test.go b/cmd/atecontroller/internal/controllers/networkpolicy_controller_test.go index 9d939ea33c..2e07170943 100644 --- a/cmd/atecontroller/internal/controllers/networkpolicy_controller_test.go +++ b/cmd/atecontroller/internal/controllers/networkpolicy_controller_test.go @@ -21,8 +21,8 @@ import ( networkingv1 "k8s.io/api/networking/v1" "k8s.io/apimachinery/pkg/types" - "github.com/agent-substrate/substrate/internal/installdefaults" - "github.com/agent-substrate/substrate/internal/resources" + "go.ate.dev/substrate/internal/installdefaults" + "go.ate.dev/substrate/internal/resources" ) func TestWorkerPoolCreatesNetworkPolicy(t *testing.T) { diff --git a/cmd/atecontroller/internal/controllers/sandboxconfig_protection_controller.go b/cmd/atecontroller/internal/controllers/sandboxconfig_protection_controller.go index ac0f937b39..355caeb90c 100644 --- a/cmd/atecontroller/internal/controllers/sandboxconfig_protection_controller.go +++ b/cmd/atecontroller/internal/controllers/sandboxconfig_protection_controller.go @@ -25,7 +25,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/handler" "sigs.k8s.io/controller-runtime/pkg/reconcile" - atev1alpha1 "github.com/agent-substrate/substrate/pkg/api/v1alpha1" + atev1alpha1 "go.ate.dev/substrate/pkg/api/v1alpha1" ) // workerPoolSandboxConfigIndex names the cache index from a SandboxConfig's diff --git a/cmd/atecontroller/internal/controllers/sandboxconfig_protection_controller_test.go b/cmd/atecontroller/internal/controllers/sandboxconfig_protection_controller_test.go index bd2bf5648f..0357664891 100644 --- a/cmd/atecontroller/internal/controllers/sandboxconfig_protection_controller_test.go +++ b/cmd/atecontroller/internal/controllers/sandboxconfig_protection_controller_test.go @@ -29,7 +29,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client/interceptor" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" - atev1alpha1 "github.com/agent-substrate/substrate/pkg/api/v1alpha1" + atev1alpha1 "go.ate.dev/substrate/pkg/api/v1alpha1" ) // TestSandboxConfigProtectionReconcile checks that the reconciler never adds diff --git a/cmd/atecontroller/internal/controllers/workerpool_apply.go b/cmd/atecontroller/internal/controllers/workerpool_apply.go index aa3428167d..4238d19f9c 100644 --- a/cmd/atecontroller/internal/controllers/workerpool_apply.go +++ b/cmd/atecontroller/internal/controllers/workerpool_apply.go @@ -26,12 +26,12 @@ import ( corev1ac "k8s.io/client-go/applyconfigurations/core/v1" metav1ac "k8s.io/client-go/applyconfigurations/meta/v1" - "github.com/agent-substrate/substrate/internal/deviceplugin" - "github.com/agent-substrate/substrate/internal/installdefaults" - "github.com/agent-substrate/substrate/internal/nodepath" - "github.com/agent-substrate/substrate/internal/preview" - "github.com/agent-substrate/substrate/internal/workercapacity" - atev1alpha1 "github.com/agent-substrate/substrate/pkg/api/v1alpha1" + "go.ate.dev/substrate/internal/deviceplugin" + "go.ate.dev/substrate/internal/installdefaults" + "go.ate.dev/substrate/internal/nodepath" + "go.ate.dev/substrate/internal/preview" + "go.ate.dev/substrate/internal/workercapacity" + atev1alpha1 "go.ate.dev/substrate/pkg/api/v1alpha1" ) // workerOTelResourceAttributes mirrors atelet.yaml; service.instance.id is the pod diff --git a/cmd/atecontroller/internal/controllers/workerpool_apply_test.go b/cmd/atecontroller/internal/controllers/workerpool_apply_test.go index f550c0e4d8..80f28e239d 100644 --- a/cmd/atecontroller/internal/controllers/workerpool_apply_test.go +++ b/cmd/atecontroller/internal/controllers/workerpool_apply_test.go @@ -30,13 +30,13 @@ import ( metav1ac "k8s.io/client-go/applyconfigurations/meta/v1" "k8s.io/utils/ptr" - "github.com/agent-substrate/substrate/internal/deviceplugin" - "github.com/agent-substrate/substrate/internal/egresspolicy" - "github.com/agent-substrate/substrate/internal/installdefaults" - "github.com/agent-substrate/substrate/internal/nodepath" - "github.com/agent-substrate/substrate/internal/preview" - "github.com/agent-substrate/substrate/internal/workercapacity" - atev1alpha1 "github.com/agent-substrate/substrate/pkg/api/v1alpha1" + "go.ate.dev/substrate/internal/deviceplugin" + "go.ate.dev/substrate/internal/egresspolicy" + "go.ate.dev/substrate/internal/installdefaults" + "go.ate.dev/substrate/internal/nodepath" + "go.ate.dev/substrate/internal/preview" + "go.ate.dev/substrate/internal/workercapacity" + atev1alpha1 "go.ate.dev/substrate/pkg/api/v1alpha1" ) func TestBuildDeploymentApplyConfig(t *testing.T) { diff --git a/cmd/atecontroller/internal/controllers/workerpool_controller.go b/cmd/atecontroller/internal/controllers/workerpool_controller.go index bff2342c83..96cb8d4f1b 100644 --- a/cmd/atecontroller/internal/controllers/workerpool_controller.go +++ b/cmd/atecontroller/internal/controllers/workerpool_controller.go @@ -38,9 +38,9 @@ import ( "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/reconcile" - "github.com/agent-substrate/substrate/internal/ateattr" - "github.com/agent-substrate/substrate/internal/installdefaults" - atev1alpha1 "github.com/agent-substrate/substrate/pkg/api/v1alpha1" + "go.ate.dev/substrate/internal/ateattr" + "go.ate.dev/substrate/internal/installdefaults" + atev1alpha1 "go.ate.dev/substrate/pkg/api/v1alpha1" ) const ( diff --git a/cmd/atecontroller/internal/controllers/workerpool_controller_test.go b/cmd/atecontroller/internal/controllers/workerpool_controller_test.go index dc272bf781..4760d13f5e 100644 --- a/cmd/atecontroller/internal/controllers/workerpool_controller_test.go +++ b/cmd/atecontroller/internal/controllers/workerpool_controller_test.go @@ -43,10 +43,10 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client/fake" metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" - "github.com/agent-substrate/substrate/internal/ateattr" - "github.com/agent-substrate/substrate/internal/installdefaults" - "github.com/agent-substrate/substrate/internal/testenv" - atev1alpha1 "github.com/agent-substrate/substrate/pkg/api/v1alpha1" + "go.ate.dev/substrate/internal/ateattr" + "go.ate.dev/substrate/internal/installdefaults" + "go.ate.dev/substrate/internal/testenv" + atev1alpha1 "go.ate.dev/substrate/pkg/api/v1alpha1" ) var ( diff --git a/cmd/atecontroller/internal/controllers/workerpool_sandboxconfig.go b/cmd/atecontroller/internal/controllers/workerpool_sandboxconfig.go index e9752713f9..1a23edd376 100644 --- a/cmd/atecontroller/internal/controllers/workerpool_sandboxconfig.go +++ b/cmd/atecontroller/internal/controllers/workerpool_sandboxconfig.go @@ -22,7 +22,7 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" - atev1alpha1 "github.com/agent-substrate/substrate/pkg/api/v1alpha1" + atev1alpha1 "go.ate.dev/substrate/pkg/api/v1alpha1" ) // resolveLatestSandboxConfig picks the SandboxConfig the spec of one sandbox diff --git a/cmd/atecontroller/internal/controllers/workerpool_sandboxconfig_test.go b/cmd/atecontroller/internal/controllers/workerpool_sandboxconfig_test.go index a1ac3b49f4..f01ba39756 100644 --- a/cmd/atecontroller/internal/controllers/workerpool_sandboxconfig_test.go +++ b/cmd/atecontroller/internal/controllers/workerpool_sandboxconfig_test.go @@ -42,7 +42,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" "sigs.k8s.io/controller-runtime/pkg/reconcile" - atev1alpha1 "github.com/agent-substrate/substrate/pkg/api/v1alpha1" + atev1alpha1 "go.ate.dev/substrate/pkg/api/v1alpha1" ) // testGvisorDefault is the gvisor class default TestMain creates. diff --git a/cmd/atecontroller/internal/workersync/fakecontrol_test.go b/cmd/atecontroller/internal/workersync/fakecontrol_test.go index b614a9ae31..a225348d83 100644 --- a/cmd/atecontroller/internal/workersync/fakecontrol_test.go +++ b/cmd/atecontroller/internal/workersync/fakecontrol_test.go @@ -22,7 +22,7 @@ import ( "strconv" "sync" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "go.ate.dev/substrate/pkg/proto/ateapipb" "google.golang.org/grpc" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" diff --git a/cmd/atecontroller/internal/workersync/syncer.go b/cmd/atecontroller/internal/workersync/syncer.go index b8ed597c82..b38595d078 100644 --- a/cmd/atecontroller/internal/workersync/syncer.go +++ b/cmd/atecontroller/internal/workersync/syncer.go @@ -24,9 +24,9 @@ import ( "slices" "time" - "github.com/agent-substrate/substrate/internal/installdefaults" - atev1alpha1 "github.com/agent-substrate/substrate/pkg/api/v1alpha1" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "go.ate.dev/substrate/internal/installdefaults" + atev1alpha1 "go.ate.dev/substrate/pkg/api/v1alpha1" + "go.ate.dev/substrate/pkg/proto/ateapipb" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" corev1 "k8s.io/api/core/v1" diff --git a/cmd/atecontroller/internal/workersync/syncer_restart_test.go b/cmd/atecontroller/internal/workersync/syncer_restart_test.go index dd049d018f..ec540dd996 100644 --- a/cmd/atecontroller/internal/workersync/syncer_restart_test.go +++ b/cmd/atecontroller/internal/workersync/syncer_restart_test.go @@ -18,7 +18,7 @@ import ( "context" "testing" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "go.ate.dev/substrate/pkg/proto/ateapipb" corev1 "k8s.io/api/core/v1" ) diff --git a/cmd/atecontroller/internal/workersync/syncer_test.go b/cmd/atecontroller/internal/workersync/syncer_test.go index ac142441ec..dcc37ab113 100644 --- a/cmd/atecontroller/internal/workersync/syncer_test.go +++ b/cmd/atecontroller/internal/workersync/syncer_test.go @@ -23,10 +23,10 @@ import ( "testing" "time" - atev1alpha1 "github.com/agent-substrate/substrate/pkg/api/v1alpha1" - atefake "github.com/agent-substrate/substrate/pkg/client/clientset/versioned/fake" - "github.com/agent-substrate/substrate/pkg/client/informers/externalversions" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + atev1alpha1 "go.ate.dev/substrate/pkg/api/v1alpha1" + atefake "go.ate.dev/substrate/pkg/client/clientset/versioned/fake" + "go.ate.dev/substrate/pkg/client/informers/externalversions" + "go.ate.dev/substrate/pkg/proto/ateapipb" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" corev1 "k8s.io/api/core/v1" diff --git a/cmd/atecontroller/main.go b/cmd/atecontroller/main.go index 7053be27c4..e7345b4fc9 100644 --- a/cmd/atecontroller/main.go +++ b/cmd/atecontroller/main.go @@ -19,20 +19,20 @@ import ( "log/slog" "os" - "github.com/agent-substrate/substrate/cmd/atecontroller/internal/controllers" - "github.com/agent-substrate/substrate/cmd/atecontroller/internal/workersync" - "github.com/agent-substrate/substrate/internal/ateapiauth" - "github.com/agent-substrate/substrate/internal/egresspolicy" - "github.com/agent-substrate/substrate/internal/installdefaults" - "github.com/agent-substrate/substrate/internal/preview" - "github.com/agent-substrate/substrate/internal/serverboot" - "github.com/agent-substrate/substrate/internal/version" - clientv1alpha1 "github.com/agent-substrate/substrate/pkg/api/v1alpha1" - "github.com/agent-substrate/substrate/pkg/client/clientset/versioned" - "github.com/agent-substrate/substrate/pkg/client/informers/externalversions" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" "github.com/go-logr/logr" "github.com/spf13/pflag" + "go.ate.dev/substrate/cmd/atecontroller/internal/controllers" + "go.ate.dev/substrate/cmd/atecontroller/internal/workersync" + "go.ate.dev/substrate/internal/ateapiauth" + "go.ate.dev/substrate/internal/egresspolicy" + "go.ate.dev/substrate/internal/installdefaults" + "go.ate.dev/substrate/internal/preview" + "go.ate.dev/substrate/internal/serverboot" + "go.ate.dev/substrate/internal/version" + clientv1alpha1 "go.ate.dev/substrate/pkg/api/v1alpha1" + "go.ate.dev/substrate/pkg/client/clientset/versioned" + "go.ate.dev/substrate/pkg/client/informers/externalversions" + "go.ate.dev/substrate/pkg/proto/ateapipb" "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc" "google.golang.org/grpc" corev1 "k8s.io/api/core/v1" diff --git a/cmd/atelet/deviceplugin.go b/cmd/atelet/deviceplugin.go index d4b3cce8b0..c63c42da47 100644 --- a/cmd/atelet/deviceplugin.go +++ b/cmd/atelet/deviceplugin.go @@ -22,7 +22,7 @@ import ( pluginapi "k8s.io/kubelet/pkg/apis/deviceplugin/v1beta1" - "github.com/agent-substrate/substrate/internal/deviceplugin" + "go.ate.dev/substrate/internal/deviceplugin" ) // hostDevRoot is where the node's /dev is mounted into atelet (see diff --git a/cmd/atelet/egresspolicy.go b/cmd/atelet/egresspolicy.go index 97769f38f7..a3456cd352 100644 --- a/cmd/atelet/egresspolicy.go +++ b/cmd/atelet/egresspolicy.go @@ -15,8 +15,8 @@ package main import ( - "github.com/agent-substrate/substrate/internal/proto/ateletpb" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "go.ate.dev/substrate/internal/proto/ateletpb" + "go.ate.dev/substrate/pkg/proto/ateapipb" ) // toAteletEgressPolicy copies the destinations each rule of the control diff --git a/cmd/atelet/egresspolicy_test.go b/cmd/atelet/egresspolicy_test.go index a1dcec0744..477a0e64c5 100644 --- a/cmd/atelet/egresspolicy_test.go +++ b/cmd/atelet/egresspolicy_test.go @@ -17,9 +17,9 @@ package main import ( "testing" - "github.com/agent-substrate/substrate/internal/proto/ateletpb" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" "github.com/google/go-cmp/cmp" + "go.ate.dev/substrate/internal/proto/ateletpb" + "go.ate.dev/substrate/pkg/proto/ateapipb" "google.golang.org/protobuf/testing/protocmp" ) diff --git a/cmd/atelet/imagegc.go b/cmd/atelet/imagegc.go index af81d261d4..0325c9b74a 100644 --- a/cmd/atelet/imagegc.go +++ b/cmd/atelet/imagegc.go @@ -35,9 +35,9 @@ import ( "strings" "time" - "github.com/agent-substrate/substrate/internal/imagecache" - "github.com/agent-substrate/substrate/internal/nodepath" "github.com/spf13/pflag" + "go.ate.dev/substrate/internal/imagecache" + "go.ate.dev/substrate/internal/nodepath" "golang.org/x/sys/unix" ) diff --git a/cmd/atelet/imagegc_test.go b/cmd/atelet/imagegc_test.go index 07ac664ec8..5555768af1 100644 --- a/cmd/atelet/imagegc_test.go +++ b/cmd/atelet/imagegc_test.go @@ -25,8 +25,8 @@ import ( "testing" "time" - "github.com/agent-substrate/substrate/internal/imagecache" - "github.com/agent-substrate/substrate/internal/nodepath" + "go.ate.dev/substrate/internal/imagecache" + "go.ate.dev/substrate/internal/nodepath" ) func TestImageCacheGCTarget(t *testing.T) { diff --git a/cmd/atelet/imagevolume_test.go b/cmd/atelet/imagevolume_test.go index d39c42332c..387688a823 100644 --- a/cmd/atelet/imagevolume_test.go +++ b/cmd/atelet/imagevolume_test.go @@ -26,8 +26,6 @@ import ( "strings" "testing" - "github.com/agent-substrate/substrate/internal/imagecache" - "github.com/agent-substrate/substrate/internal/proto/ateletpb" "github.com/google/go-containerregistry/pkg/name" "github.com/google/go-containerregistry/pkg/registry" v1 "github.com/google/go-containerregistry/pkg/v1" @@ -35,6 +33,8 @@ import ( "github.com/google/go-containerregistry/pkg/v1/mutate" "github.com/google/go-containerregistry/pkg/v1/remote" "github.com/google/go-containerregistry/pkg/v1/tarball" + "go.ate.dev/substrate/internal/imagecache" + "go.ate.dev/substrate/internal/proto/ateletpb" ) // imageVolumeTestRegistry starts an in-memory OCI registry. Its 127.0.0.1 host diff --git a/cmd/atelet/internal/apivalidation/doc.go b/cmd/atelet/internal/apivalidation/doc.go index 42a40299a0..bbb6bca873 100644 --- a/cmd/atelet/internal/apivalidation/doc.go +++ b/cmd/atelet/internal/apivalidation/doc.go @@ -19,7 +19,7 @@ // work. // // +k8s:validation-gen=TypesWithSuffix=Request -// +k8s:validation-gen-input=github.com/agent-substrate/substrate/internal/proto/ateletpb +// +k8s:validation-gen-input=go.ate.dev/substrate/internal/proto/ateletpb // +k8s:validation-gen-scheme-registry=nil // +k8s:validation-gen-deep-equal-func=ateDeepEqual package apivalidation diff --git a/cmd/atelet/internal/apivalidation/validate.go b/cmd/atelet/internal/apivalidation/validate.go index 0b99daa8de..17097a8959 100644 --- a/cmd/atelet/internal/apivalidation/validate.go +++ b/cmd/atelet/internal/apivalidation/validate.go @@ -18,8 +18,8 @@ import ( "context" "fmt" - "github.com/agent-substrate/substrate/internal/proto/ateletpb" - "github.com/agent-substrate/substrate/internal/resources" + "go.ate.dev/substrate/internal/proto/ateletpb" + "go.ate.dev/substrate/internal/resources" "k8s.io/apimachinery/pkg/api/operation" "k8s.io/apimachinery/pkg/util/validation/field" ) diff --git a/cmd/atelet/internal/apivalidation/validation_test.go b/cmd/atelet/internal/apivalidation/validation_test.go index f6478abb63..2e81af37e9 100644 --- a/cmd/atelet/internal/apivalidation/validation_test.go +++ b/cmd/atelet/internal/apivalidation/validation_test.go @@ -20,8 +20,8 @@ import ( "strings" "testing" - "github.com/agent-substrate/substrate/internal/proto/ateletpb" - "github.com/agent-substrate/substrate/internal/resources" + "go.ate.dev/substrate/internal/proto/ateletpb" + "go.ate.dev/substrate/internal/resources" "k8s.io/apimachinery/pkg/api/operation" "k8s.io/apimachinery/pkg/util/validation/field" ) diff --git a/cmd/atelet/internal/apivalidation/workloadspec.go b/cmd/atelet/internal/apivalidation/workloadspec.go index c0e3f94693..e3cfc456cf 100644 --- a/cmd/atelet/internal/apivalidation/workloadspec.go +++ b/cmd/atelet/internal/apivalidation/workloadspec.go @@ -22,8 +22,8 @@ package apivalidation import ( "context" - "github.com/agent-substrate/substrate/internal/proto/ateletpb" - "github.com/agent-substrate/substrate/internal/resources" + "go.ate.dev/substrate/internal/proto/ateletpb" + "go.ate.dev/substrate/internal/resources" "k8s.io/apimachinery/pkg/api/operation" "k8s.io/apimachinery/pkg/util/sets" "k8s.io/apimachinery/pkg/util/validation/field" diff --git a/cmd/atelet/internal/apivalidation/zz_generated.validation.go b/cmd/atelet/internal/apivalidation/zz_generated.validation.go index 620287d0a7..333cac6d1c 100644 --- a/cmd/atelet/internal/apivalidation/zz_generated.validation.go +++ b/cmd/atelet/internal/apivalidation/zz_generated.validation.go @@ -22,7 +22,7 @@ package apivalidation import ( context "context" - ateletpb "github.com/agent-substrate/substrate/internal/proto/ateletpb" + ateletpb "go.ate.dev/substrate/internal/proto/ateletpb" operation "k8s.io/apimachinery/pkg/api/operation" safe "k8s.io/apimachinery/pkg/api/safe" validate "k8s.io/apimachinery/pkg/api/validate" @@ -3615,7 +3615,7 @@ func Validate_Snapshot( return errs } -var unionMembershipFor_github_com_agent_substrate_substrate_internal_proto_ateletpb_SystemInfoDataSource_ = validate.NewUnionMembership(validate.NewUnionMember("actor_metadata"), validate.NewUnionMember("trust_bundle")) +var unionMembershipFor_go_ate_dev_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. @@ -3623,7 +3623,7 @@ 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_, + if e := validate.Union(ctx, op, fldPath, obj, oldObj, unionMembershipFor_go_ate_dev_substrate_internal_proto_ateletpb_SystemInfoDataSource_, func(obj *ateletpb.SystemInfoDataSource) bool { if obj == nil { return false @@ -4215,7 +4215,7 @@ func Validate_VersionedSandboxCompat( 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")) +var unionMembershipFor_go_ate_dev_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. @@ -4223,7 +4223,7 @@ 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_, + if e := validate.Union(ctx, op, fldPath, obj, oldObj, unionMembershipFor_go_ate_dev_substrate_internal_proto_ateletpb_Volume_, func(obj *ateletpb.Volume) bool { if obj == nil { return false diff --git a/cmd/atelet/internal/ateletpath/ateletpath.go b/cmd/atelet/internal/ateletpath/ateletpath.go index fdae2ec031..283c94307f 100644 --- a/cmd/atelet/internal/ateletpath/ateletpath.go +++ b/cmd/atelet/internal/ateletpath/ateletpath.go @@ -19,8 +19,8 @@ package ateletpath import ( "path/filepath" - "github.com/agent-substrate/substrate/internal/nodepath" - "github.com/agent-substrate/substrate/internal/proto/ateworkerpb" + "go.ate.dev/substrate/internal/nodepath" + "go.ate.dev/substrate/internal/proto/ateworkerpb" ) var ( diff --git a/cmd/atelet/internal/filecache/getfilecopyto.go b/cmd/atelet/internal/filecache/getfilecopyto.go index d3c49acc99..1f82de67c5 100644 --- a/cmd/atelet/internal/filecache/getfilecopyto.go +++ b/cmd/atelet/internal/filecache/getfilecopyto.go @@ -22,7 +22,7 @@ import ( "os" "time" - "github.com/agent-substrate/substrate/cmd/atelet/internal/sparsefile" + "go.ate.dev/substrate/cmd/atelet/internal/sparsefile" ) // GetFileCopyTo materializes the artifact identified by key at dst as a diff --git a/cmd/atelet/internal/snapshottracker/actor.go b/cmd/atelet/internal/snapshottracker/actor.go index ec4ecbb724..b7be0e9c36 100644 --- a/cmd/atelet/internal/snapshottracker/actor.go +++ b/cmd/atelet/internal/snapshottracker/actor.go @@ -18,7 +18,7 @@ import ( "encoding/json" "time" - "github.com/agent-substrate/substrate/internal/proto/ateletpb" + "go.ate.dev/substrate/internal/proto/ateletpb" "google.golang.org/protobuf/encoding/protojson" "google.golang.org/protobuf/proto" "google.golang.org/protobuf/types/known/timestamppb" diff --git a/cmd/atelet/internal/snapshottracker/background_test.go b/cmd/atelet/internal/snapshottracker/background_test.go index 4c4f40c005..eeb0842794 100644 --- a/cmd/atelet/internal/snapshottracker/background_test.go +++ b/cmd/atelet/internal/snapshottracker/background_test.go @@ -23,9 +23,9 @@ import ( "testing" "time" - "github.com/agent-substrate/substrate/cmd/atelet/internal/ateletpath" - "github.com/agent-substrate/substrate/internal/nodepath" - "github.com/agent-substrate/substrate/internal/proto/ateletpb" + "go.ate.dev/substrate/cmd/atelet/internal/ateletpath" + "go.ate.dev/substrate/internal/nodepath" + "go.ate.dev/substrate/internal/proto/ateletpb" "google.golang.org/protobuf/proto" clocktesting "k8s.io/utils/clock/testing" ) diff --git a/cmd/atelet/internal/snapshottracker/lifecycle.go b/cmd/atelet/internal/snapshottracker/lifecycle.go index fde83307b7..3a35b3ddd2 100644 --- a/cmd/atelet/internal/snapshottracker/lifecycle.go +++ b/cmd/atelet/internal/snapshottracker/lifecycle.go @@ -15,7 +15,7 @@ package snapshottracker import ( - "github.com/agent-substrate/substrate/internal/proto/ateletpb" + "go.ate.dev/substrate/internal/proto/ateletpb" ) // CommitLocal records snap, just stored locally under UPLOAD_MODE_DEFERRED, as diff --git a/cmd/atelet/internal/snapshottracker/state.go b/cmd/atelet/internal/snapshottracker/state.go index 1d9eef3d92..1184f6ec3b 100644 --- a/cmd/atelet/internal/snapshottracker/state.go +++ b/cmd/atelet/internal/snapshottracker/state.go @@ -21,9 +21,9 @@ import ( "log/slog" "os" - "github.com/agent-substrate/substrate/cmd/atelet/internal/ateletpath" - "github.com/agent-substrate/substrate/cmd/atelet/internal/atomicfile" - "github.com/agent-substrate/substrate/internal/nodepath" + "go.ate.dev/substrate/cmd/atelet/internal/ateletpath" + "go.ate.dev/substrate/cmd/atelet/internal/atomicfile" + "go.ate.dev/substrate/internal/nodepath" ) // saveLocked writes what atelet tracks of the actor to its snapshot state diff --git a/cmd/atelet/internal/snapshottracker/tracker.go b/cmd/atelet/internal/snapshottracker/tracker.go index 50a4045296..a973f22eb0 100644 --- a/cmd/atelet/internal/snapshottracker/tracker.go +++ b/cmd/atelet/internal/snapshottracker/tracker.go @@ -20,8 +20,8 @@ import ( "context" "sync" - "github.com/agent-substrate/substrate/internal/actorlock" - "github.com/agent-substrate/substrate/internal/proto/ateletpb" + "go.ate.dev/substrate/internal/actorlock" + "go.ate.dev/substrate/internal/proto/ateletpb" "k8s.io/client-go/util/workqueue" "k8s.io/utils/clock" ) diff --git a/cmd/atelet/internal/snapshottracker/tracker_test.go b/cmd/atelet/internal/snapshottracker/tracker_test.go index c8e2ec72d0..997bc0e04e 100644 --- a/cmd/atelet/internal/snapshottracker/tracker_test.go +++ b/cmd/atelet/internal/snapshottracker/tracker_test.go @@ -22,9 +22,9 @@ import ( "testing" "time" - "github.com/agent-substrate/substrate/cmd/atelet/internal/ateletpath" - "github.com/agent-substrate/substrate/internal/nodepath" - "github.com/agent-substrate/substrate/internal/proto/ateletpb" + "go.ate.dev/substrate/cmd/atelet/internal/ateletpath" + "go.ate.dev/substrate/internal/nodepath" + "go.ate.dev/substrate/internal/proto/ateletpb" "google.golang.org/protobuf/proto" "google.golang.org/protobuf/types/known/timestamppb" clocktesting "k8s.io/utils/clock/testing" diff --git a/cmd/atelet/internal/trustbundle/trustbundle.go b/cmd/atelet/internal/trustbundle/trustbundle.go index 7abbea9ee8..9c1d00fc76 100644 --- a/cmd/atelet/internal/trustbundle/trustbundle.go +++ b/cmd/atelet/internal/trustbundle/trustbundle.go @@ -21,7 +21,7 @@ import ( "fmt" "math/rand/v2" - "github.com/agent-substrate/substrate/internal/pemutil" + "go.ate.dev/substrate/internal/pemutil" certsv1 "k8s.io/api/certificates/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" ) diff --git a/cmd/atelet/lifecycle_test.go b/cmd/atelet/lifecycle_test.go index 12ce8befb0..cdf83b1087 100644 --- a/cmd/atelet/lifecycle_test.go +++ b/cmd/atelet/lifecycle_test.go @@ -27,14 +27,14 @@ import ( "strings" "testing" - "github.com/agent-substrate/substrate/cmd/atelet/internal/ateletpath" - "github.com/agent-substrate/substrate/cmd/atelet/internal/snapshottracker" - "github.com/agent-substrate/substrate/cmd/atelet/internal/trustbundle" - "github.com/agent-substrate/substrate/internal/apierror" - "github.com/agent-substrate/substrate/internal/nodepath" - "github.com/agent-substrate/substrate/internal/proto/ateletpb" - "github.com/agent-substrate/substrate/internal/proto/ateworkerpb" - "github.com/agent-substrate/substrate/internal/resources" + "go.ate.dev/substrate/cmd/atelet/internal/ateletpath" + "go.ate.dev/substrate/cmd/atelet/internal/snapshottracker" + "go.ate.dev/substrate/cmd/atelet/internal/trustbundle" + "go.ate.dev/substrate/internal/apierror" + "go.ate.dev/substrate/internal/nodepath" + "go.ate.dev/substrate/internal/proto/ateletpb" + "go.ate.dev/substrate/internal/proto/ateworkerpb" + "go.ate.dev/substrate/internal/resources" "google.golang.org/grpc" "google.golang.org/grpc/codes" "google.golang.org/protobuf/proto" diff --git a/cmd/atelet/local_checkpoints.go b/cmd/atelet/local_checkpoints.go index 1e2ff89b25..642122f700 100644 --- a/cmd/atelet/local_checkpoints.go +++ b/cmd/atelet/local_checkpoints.go @@ -22,7 +22,7 @@ import ( "os" "path/filepath" - "github.com/agent-substrate/substrate/cmd/atelet/internal/ateletpath" + "go.ate.dev/substrate/cmd/atelet/internal/ateletpath" ) // pruneLocalCheckpoints removes every local snapshot of the actor. A missing diff --git a/cmd/atelet/main.go b/cmd/atelet/main.go index 2399a065f4..7c1adecddf 100644 --- a/cmd/atelet/main.go +++ b/cmd/atelet/main.go @@ -30,41 +30,41 @@ 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/snapshottracker" - "github.com/agent-substrate/substrate/cmd/atelet/internal/sparsefile" - "github.com/agent-substrate/substrate/cmd/atelet/internal/trustbundle" - "github.com/agent-substrate/substrate/internal/apierror" - "github.com/agent-substrate/substrate/internal/ateapiauth" - "github.com/agent-substrate/substrate/internal/ateattr" - "github.com/agent-substrate/substrate/internal/ateinterceptors" - "github.com/agent-substrate/substrate/internal/atelet" - "github.com/agent-substrate/substrate/internal/clustertrustbundle" - "github.com/agent-substrate/substrate/internal/credbundle" - "github.com/agent-substrate/substrate/internal/imagecache" - "github.com/agent-substrate/substrate/internal/nodepath" - "github.com/agent-substrate/substrate/internal/ocispec" - "github.com/agent-substrate/substrate/internal/otlprelay" - "github.com/agent-substrate/substrate/internal/pluginsocket" - "github.com/agent-substrate/substrate/internal/preview" - "github.com/agent-substrate/substrate/internal/proto/ateletpb" - "github.com/agent-substrate/substrate/internal/proto/ateworkerpb" - "github.com/agent-substrate/substrate/internal/resources" - "github.com/agent-substrate/substrate/internal/serverboot" - "github.com/agent-substrate/substrate/internal/substratex509" - "github.com/agent-substrate/substrate/internal/version" - "github.com/agent-substrate/substrate/internal/volume" - "github.com/agent-substrate/substrate/internal/volume/csi" - atev1alpha1 "github.com/agent-substrate/substrate/pkg/api/v1alpha1" - "github.com/agent-substrate/substrate/pkg/client/clientset/versioned" - "github.com/agent-substrate/substrate/pkg/client/informers/externalversions" - "github.com/agent-substrate/substrate/pkg/objectstorage" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" - objectstorev1 "github.com/agent-substrate/substrate/pkg/proto/objectstorepb/v1" "github.com/google/go-containerregistry/pkg/authn" "github.com/spf13/pflag" + "go.ate.dev/substrate/cmd/atelet/internal/apivalidation" + "go.ate.dev/substrate/cmd/atelet/internal/ateletpath" + "go.ate.dev/substrate/cmd/atelet/internal/credentialprovider" + "go.ate.dev/substrate/cmd/atelet/internal/snapshottracker" + "go.ate.dev/substrate/cmd/atelet/internal/sparsefile" + "go.ate.dev/substrate/cmd/atelet/internal/trustbundle" + "go.ate.dev/substrate/internal/apierror" + "go.ate.dev/substrate/internal/ateapiauth" + "go.ate.dev/substrate/internal/ateattr" + "go.ate.dev/substrate/internal/ateinterceptors" + "go.ate.dev/substrate/internal/atelet" + "go.ate.dev/substrate/internal/clustertrustbundle" + "go.ate.dev/substrate/internal/credbundle" + "go.ate.dev/substrate/internal/imagecache" + "go.ate.dev/substrate/internal/nodepath" + "go.ate.dev/substrate/internal/ocispec" + "go.ate.dev/substrate/internal/otlprelay" + "go.ate.dev/substrate/internal/pluginsocket" + "go.ate.dev/substrate/internal/preview" + "go.ate.dev/substrate/internal/proto/ateletpb" + "go.ate.dev/substrate/internal/proto/ateworkerpb" + "go.ate.dev/substrate/internal/resources" + "go.ate.dev/substrate/internal/serverboot" + "go.ate.dev/substrate/internal/substratex509" + "go.ate.dev/substrate/internal/version" + "go.ate.dev/substrate/internal/volume" + "go.ate.dev/substrate/internal/volume/csi" + atev1alpha1 "go.ate.dev/substrate/pkg/api/v1alpha1" + "go.ate.dev/substrate/pkg/client/clientset/versioned" + "go.ate.dev/substrate/pkg/client/informers/externalversions" + "go.ate.dev/substrate/pkg/objectstorage" + "go.ate.dev/substrate/pkg/proto/ateapipb" + objectstorev1 "go.ate.dev/substrate/pkg/proto/objectstorepb/v1" "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/metric" diff --git a/cmd/atelet/main_test.go b/cmd/atelet/main_test.go index 4939569c84..1bc2e6941e 100644 --- a/cmd/atelet/main_test.go +++ b/cmd/atelet/main_test.go @@ -33,20 +33,20 @@ import ( "testing" "time" - "github.com/agent-substrate/substrate/cmd/atelet/internal/ateletpath" - "github.com/agent-substrate/substrate/internal/apierror" - "github.com/agent-substrate/substrate/internal/ateattr" - "github.com/agent-substrate/substrate/internal/atelet" - "github.com/agent-substrate/substrate/internal/imagecache" - "github.com/agent-substrate/substrate/internal/nodepath" - "github.com/agent-substrate/substrate/internal/ocispec" - "github.com/agent-substrate/substrate/internal/proto/ateletpb" - "github.com/agent-substrate/substrate/internal/proto/ateworkerpb" - "github.com/agent-substrate/substrate/internal/resources" - "github.com/agent-substrate/substrate/internal/serverboot" - "github.com/agent-substrate/substrate/pkg/objectstorage" "github.com/google/go-cmp/cmp" "github.com/spf13/pflag" + "go.ate.dev/substrate/cmd/atelet/internal/ateletpath" + "go.ate.dev/substrate/internal/apierror" + "go.ate.dev/substrate/internal/ateattr" + "go.ate.dev/substrate/internal/atelet" + "go.ate.dev/substrate/internal/imagecache" + "go.ate.dev/substrate/internal/nodepath" + "go.ate.dev/substrate/internal/ocispec" + "go.ate.dev/substrate/internal/proto/ateletpb" + "go.ate.dev/substrate/internal/proto/ateworkerpb" + "go.ate.dev/substrate/internal/resources" + "go.ate.dev/substrate/internal/serverboot" + "go.ate.dev/substrate/pkg/objectstorage" "golang.org/x/sys/unix" "google.golang.org/grpc" "google.golang.org/grpc/codes" diff --git a/cmd/atelet/metrics.go b/cmd/atelet/metrics.go index 7433497b8b..d7d65c2359 100644 --- a/cmd/atelet/metrics.go +++ b/cmd/atelet/metrics.go @@ -22,9 +22,9 @@ import ( "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/metric" - "github.com/agent-substrate/substrate/internal/ateattr" - "github.com/agent-substrate/substrate/internal/proto/ateletpb" - "github.com/agent-substrate/substrate/internal/resources" + "go.ate.dev/substrate/internal/ateattr" + "go.ate.dev/substrate/internal/proto/ateletpb" + "go.ate.dev/substrate/internal/resources" ) const ( diff --git a/cmd/atelet/metrics_test.go b/cmd/atelet/metrics_test.go index 4792a8f44c..51508cd6ee 100644 --- a/cmd/atelet/metrics_test.go +++ b/cmd/atelet/metrics_test.go @@ -26,9 +26,9 @@ import ( sdkmetric "go.opentelemetry.io/otel/sdk/metric" "go.opentelemetry.io/otel/sdk/metric/metricdata" - "github.com/agent-substrate/substrate/internal/ateattr" - "github.com/agent-substrate/substrate/internal/proto/ateletpb" - "github.com/agent-substrate/substrate/internal/resources" + "go.ate.dev/substrate/internal/ateattr" + "go.ate.dev/substrate/internal/proto/ateletpb" + "go.ate.dev/substrate/internal/resources" ) const ( diff --git a/cmd/atelet/oci.go b/cmd/atelet/oci.go index cc39c97d34..b9fcf4e903 100644 --- a/cmd/atelet/oci.go +++ b/cmd/atelet/oci.go @@ -22,10 +22,10 @@ import ( "sort" "strings" - "github.com/agent-substrate/substrate/cmd/atelet/internal/ateletpath" - "github.com/agent-substrate/substrate/internal/imagecache" - "github.com/agent-substrate/substrate/internal/proto/ateletpb" v1 "github.com/google/go-containerregistry/pkg/v1" + "go.ate.dev/substrate/cmd/atelet/internal/ateletpath" + "go.ate.dev/substrate/internal/imagecache" + "go.ate.dev/substrate/internal/proto/ateletpb" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/attribute" "golang.org/x/sync/errgroup" diff --git a/cmd/atelet/oci_test.go b/cmd/atelet/oci_test.go index 988534b4a0..c76d2d0d03 100644 --- a/cmd/atelet/oci_test.go +++ b/cmd/atelet/oci_test.go @@ -18,8 +18,8 @@ import ( "slices" "testing" - "github.com/agent-substrate/substrate/internal/proto/ateletpb" v1 "github.com/google/go-containerregistry/pkg/v1" + "go.ate.dev/substrate/internal/proto/ateletpb" ) func TestResolveActorEnv(t *testing.T) { diff --git a/cmd/atelet/rpc_logging_test.go b/cmd/atelet/rpc_logging_test.go index f505f04b41..74f52c604a 100644 --- a/cmd/atelet/rpc_logging_test.go +++ b/cmd/atelet/rpc_logging_test.go @@ -22,10 +22,10 @@ import ( "strings" "testing" - "github.com/agent-substrate/substrate/internal/apierror" - "github.com/agent-substrate/substrate/internal/proto/ateletpb" - "github.com/agent-substrate/substrate/internal/protoredact" - "github.com/agent-substrate/substrate/internal/serverboot" + "go.ate.dev/substrate/internal/apierror" + "go.ate.dev/substrate/internal/proto/ateletpb" + "go.ate.dev/substrate/internal/protoredact" + "go.ate.dev/substrate/internal/serverboot" "google.golang.org/grpc" "google.golang.org/grpc/codes" "google.golang.org/grpc/credentials/insecure" diff --git a/cmd/atelet/sandbox_assets.go b/cmd/atelet/sandbox_assets.go index f4ec42da35..ed3945217e 100644 --- a/cmd/atelet/sandbox_assets.go +++ b/cmd/atelet/sandbox_assets.go @@ -39,15 +39,15 @@ import ( "syscall" "time" - "github.com/agent-substrate/substrate/cmd/atelet/internal/ateletpath" - "github.com/agent-substrate/substrate/cmd/atelet/internal/atomicfile" - "github.com/agent-substrate/substrate/internal/apierror" - "github.com/agent-substrate/substrate/internal/nodepath" - "github.com/agent-substrate/substrate/internal/proto/ateletpb" - "github.com/agent-substrate/substrate/internal/resources" - "github.com/agent-substrate/substrate/pkg/api/v1alpha1" - "github.com/agent-substrate/substrate/pkg/objectstorage" - objectstorev1 "github.com/agent-substrate/substrate/pkg/proto/objectstorepb/v1" + "go.ate.dev/substrate/cmd/atelet/internal/ateletpath" + "go.ate.dev/substrate/cmd/atelet/internal/atomicfile" + "go.ate.dev/substrate/internal/apierror" + "go.ate.dev/substrate/internal/nodepath" + "go.ate.dev/substrate/internal/proto/ateletpb" + "go.ate.dev/substrate/internal/resources" + "go.ate.dev/substrate/pkg/api/v1alpha1" + "go.ate.dev/substrate/pkg/objectstorage" + objectstorev1 "go.ate.dev/substrate/pkg/proto/objectstorepb/v1" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" ) diff --git a/cmd/atelet/sandbox_assets_plugin_test.go b/cmd/atelet/sandbox_assets_plugin_test.go index 1610706dec..f1521bc563 100644 --- a/cmd/atelet/sandbox_assets_plugin_test.go +++ b/cmd/atelet/sandbox_assets_plugin_test.go @@ -30,16 +30,16 @@ import ( "testing" "time" - "github.com/agent-substrate/substrate/cmd/atelet/internal/ateletpath" - "github.com/agent-substrate/substrate/internal/apierror" - "github.com/agent-substrate/substrate/internal/ateinterceptors" - "github.com/agent-substrate/substrate/internal/nodepath" - "github.com/agent-substrate/substrate/internal/objectstoreplugin" - "github.com/agent-substrate/substrate/internal/objectstoreplugin/objectstoreplugintest" - "github.com/agent-substrate/substrate/internal/pluginsocket" - "github.com/agent-substrate/substrate/internal/serverboot" - "github.com/agent-substrate/substrate/pkg/objectstorage" - objectstorev1 "github.com/agent-substrate/substrate/pkg/proto/objectstorepb/v1" + "go.ate.dev/substrate/cmd/atelet/internal/ateletpath" + "go.ate.dev/substrate/internal/apierror" + "go.ate.dev/substrate/internal/ateinterceptors" + "go.ate.dev/substrate/internal/nodepath" + "go.ate.dev/substrate/internal/objectstoreplugin" + "go.ate.dev/substrate/internal/objectstoreplugin/objectstoreplugintest" + "go.ate.dev/substrate/internal/pluginsocket" + "go.ate.dev/substrate/internal/serverboot" + "go.ate.dev/substrate/pkg/objectstorage" + objectstorev1 "go.ate.dev/substrate/pkg/proto/objectstorepb/v1" "google.golang.org/grpc" "google.golang.org/grpc/codes" "google.golang.org/grpc/health" diff --git a/cmd/atelet/sandbox_assets_test.go b/cmd/atelet/sandbox_assets_test.go index 2e2d65bf8a..a99e709eda 100644 --- a/cmd/atelet/sandbox_assets_test.go +++ b/cmd/atelet/sandbox_assets_test.go @@ -25,9 +25,9 @@ import ( "strings" "testing" - "github.com/agent-substrate/substrate/internal/proto/ateletpb" "github.com/google/go-cmp/cmp" "github.com/klauspost/compress/zstd" + "go.ate.dev/substrate/internal/proto/ateletpb" ) func TestExtractTarArchive(t *testing.T) { diff --git a/cmd/atelet/sandbox_prewarm.go b/cmd/atelet/sandbox_prewarm.go index ab174bc1e0..12a722c4a2 100644 --- a/cmd/atelet/sandbox_prewarm.go +++ b/cmd/atelet/sandbox_prewarm.go @@ -28,9 +28,9 @@ import ( "k8s.io/client-go/tools/cache" "k8s.io/client-go/util/workqueue" - "github.com/agent-substrate/substrate/internal/imagecache" - "github.com/agent-substrate/substrate/pkg/api/v1alpha1" - listersv1alpha1 "github.com/agent-substrate/substrate/pkg/client/listers/api/v1alpha1" + "go.ate.dev/substrate/internal/imagecache" + "go.ate.dev/substrate/pkg/api/v1alpha1" + listersv1alpha1 "go.ate.dev/substrate/pkg/client/listers/api/v1alpha1" ) // prewarmMaxJitter spreads the fleet's asset downloads after a SandboxConfig diff --git a/cmd/atelet/sandbox_prewarm_test.go b/cmd/atelet/sandbox_prewarm_test.go index 5fbefc5c66..90439e08b9 100644 --- a/cmd/atelet/sandbox_prewarm_test.go +++ b/cmd/atelet/sandbox_prewarm_test.go @@ -32,19 +32,19 @@ import ( "k8s.io/client-go/tools/cache" "k8s.io/utils/ptr" - "github.com/agent-substrate/substrate/cmd/atelet/internal/ateletpath" - "github.com/agent-substrate/substrate/internal/imagecache" - "github.com/agent-substrate/substrate/internal/nodepath" - "github.com/agent-substrate/substrate/pkg/api/v1alpha1" - "github.com/agent-substrate/substrate/pkg/client/clientset/versioned/fake" - "github.com/agent-substrate/substrate/pkg/client/informers/externalversions" - listersv1alpha1 "github.com/agent-substrate/substrate/pkg/client/listers/api/v1alpha1" "github.com/google/go-containerregistry/pkg/name" "github.com/google/go-containerregistry/pkg/registry" v1 "github.com/google/go-containerregistry/pkg/v1" "github.com/google/go-containerregistry/pkg/v1/empty" "github.com/google/go-containerregistry/pkg/v1/mutate" "github.com/google/go-containerregistry/pkg/v1/remote" + "go.ate.dev/substrate/cmd/atelet/internal/ateletpath" + "go.ate.dev/substrate/internal/imagecache" + "go.ate.dev/substrate/internal/nodepath" + "go.ate.dev/substrate/pkg/api/v1alpha1" + "go.ate.dev/substrate/pkg/client/clientset/versioned/fake" + "go.ate.dev/substrate/pkg/client/informers/externalversions" + listersv1alpha1 "go.ate.dev/substrate/pkg/client/listers/api/v1alpha1" ) // pushPauseImage pushes a tiny image to ref and returns its manifest digest, diff --git a/cmd/atelet/snapshot_transfer.go b/cmd/atelet/snapshot_transfer.go index e8c54a4648..83fdfa5b35 100644 --- a/cmd/atelet/snapshot_transfer.go +++ b/cmd/atelet/snapshot_transfer.go @@ -20,8 +20,8 @@ import ( "os" "path/filepath" - "github.com/agent-substrate/substrate/internal/pluginsocket" - objectstorev1 "github.com/agent-substrate/substrate/pkg/proto/objectstorepb/v1" + "go.ate.dev/substrate/internal/pluginsocket" + objectstorev1 "go.ate.dev/substrate/pkg/proto/objectstorepb/v1" "google.golang.org/grpc" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" diff --git a/cmd/atelet/snapshot_transfer_test.go b/cmd/atelet/snapshot_transfer_test.go index b0c8bbaba6..e77712ebb5 100644 --- a/cmd/atelet/snapshot_transfer_test.go +++ b/cmd/atelet/snapshot_transfer_test.go @@ -27,16 +27,16 @@ import ( "testing" "time" - "github.com/agent-substrate/substrate/cmd/atelet/internal/snapshottracker" - "github.com/agent-substrate/substrate/internal/apierror" - "github.com/agent-substrate/substrate/internal/ateattr" - "github.com/agent-substrate/substrate/internal/ateinterceptors" - "github.com/agent-substrate/substrate/internal/objectstoreplugin" - "github.com/agent-substrate/substrate/internal/objectstoreplugin/objectstoreplugintest" - "github.com/agent-substrate/substrate/internal/pluginsocket" - "github.com/agent-substrate/substrate/internal/resources" - "github.com/agent-substrate/substrate/pkg/objectstorage" - objectstorev1 "github.com/agent-substrate/substrate/pkg/proto/objectstorepb/v1" + "go.ate.dev/substrate/cmd/atelet/internal/snapshottracker" + "go.ate.dev/substrate/internal/apierror" + "go.ate.dev/substrate/internal/ateattr" + "go.ate.dev/substrate/internal/ateinterceptors" + "go.ate.dev/substrate/internal/objectstoreplugin" + "go.ate.dev/substrate/internal/objectstoreplugin/objectstoreplugintest" + "go.ate.dev/substrate/internal/pluginsocket" + "go.ate.dev/substrate/internal/resources" + "go.ate.dev/substrate/pkg/objectstorage" + objectstorev1 "go.ate.dev/substrate/pkg/proto/objectstorepb/v1" "google.golang.org/grpc" "google.golang.org/grpc/codes" "google.golang.org/grpc/connectivity" diff --git a/cmd/atelet/snapshotlog.go b/cmd/atelet/snapshotlog.go index b4062adf09..2d201306b4 100644 --- a/cmd/atelet/snapshotlog.go +++ b/cmd/atelet/snapshotlog.go @@ -17,9 +17,9 @@ package main import ( "log/slog" - "github.com/agent-substrate/substrate/internal/apierror" - "github.com/agent-substrate/substrate/internal/ateattr" - "github.com/agent-substrate/substrate/internal/resources" + "go.ate.dev/substrate/internal/apierror" + "go.ate.dev/substrate/internal/ateattr" + "go.ate.dev/substrate/internal/resources" "google.golang.org/grpc/status" ) diff --git a/cmd/atelet/snapshotlog_test.go b/cmd/atelet/snapshotlog_test.go index cc51812e00..7afce5ed15 100644 --- a/cmd/atelet/snapshotlog_test.go +++ b/cmd/atelet/snapshotlog_test.go @@ -24,8 +24,8 @@ import ( "testing" "time" - "github.com/agent-substrate/substrate/internal/ateattr" - "github.com/agent-substrate/substrate/internal/resources" + "go.ate.dev/substrate/internal/ateattr" + "go.ate.dev/substrate/internal/resources" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" ) diff --git a/cmd/atelet/statspoller.go b/cmd/atelet/statspoller.go index 37de64876a..2a0530f101 100644 --- a/cmd/atelet/statspoller.go +++ b/cmd/atelet/statspoller.go @@ -36,10 +36,10 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes" - "github.com/agent-substrate/substrate/internal/ateattr" - "github.com/agent-substrate/substrate/internal/nodepath" - "github.com/agent-substrate/substrate/internal/proto/ateworkerpb" - "github.com/agent-substrate/substrate/internal/workerstats" + "go.ate.dev/substrate/internal/ateattr" + "go.ate.dev/substrate/internal/nodepath" + "go.ate.dev/substrate/internal/proto/ateworkerpb" + "go.ate.dev/substrate/internal/workerstats" ) // workerPoolLabel is the label the pool controller stamps on every worker pod diff --git a/cmd/atelet/statspoller_test.go b/cmd/atelet/statspoller_test.go index c17067b58e..8d9a685cdd 100644 --- a/cmd/atelet/statspoller_test.go +++ b/cmd/atelet/statspoller_test.go @@ -34,7 +34,7 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" k8sfake "k8s.io/client-go/kubernetes/fake" - "github.com/agent-substrate/substrate/internal/proto/ateworkerpb" + "go.ate.dev/substrate/internal/proto/ateworkerpb" ) // fakeStatsWorker answers GetActiveWorkloadStats with a canned response or diff --git a/cmd/atelet/systeminfovolume.go b/cmd/atelet/systeminfovolume.go index 9f30782148..76d6964220 100644 --- a/cmd/atelet/systeminfovolume.go +++ b/cmd/atelet/systeminfovolume.go @@ -26,11 +26,11 @@ import ( "sync" "time" - "github.com/agent-substrate/substrate/cmd/atelet/internal/ateletpath" - "github.com/agent-substrate/substrate/cmd/atelet/internal/trustbundle" - "github.com/agent-substrate/substrate/internal/proto/ateletpb" - "github.com/agent-substrate/substrate/internal/resources" - "github.com/agent-substrate/substrate/internal/volumepath" + "go.ate.dev/substrate/cmd/atelet/internal/ateletpath" + "go.ate.dev/substrate/cmd/atelet/internal/trustbundle" + "go.ate.dev/substrate/internal/proto/ateletpb" + "go.ate.dev/substrate/internal/resources" + "go.ate.dev/substrate/internal/volumepath" "k8s.io/apimachinery/pkg/api/meta" "k8s.io/apimachinery/pkg/util/wait" "k8s.io/client-go/tools/cache" diff --git a/cmd/atelet/systeminfovolume_test.go b/cmd/atelet/systeminfovolume_test.go index c5286c3b33..2a56ab9368 100644 --- a/cmd/atelet/systeminfovolume_test.go +++ b/cmd/atelet/systeminfovolume_test.go @@ -31,10 +31,10 @@ import ( "testing" "time" - "github.com/agent-substrate/substrate/cmd/atelet/internal/ateletpath" - "github.com/agent-substrate/substrate/cmd/atelet/internal/trustbundle" - "github.com/agent-substrate/substrate/internal/proto/ateletpb" - "github.com/agent-substrate/substrate/internal/resources" + "go.ate.dev/substrate/cmd/atelet/internal/ateletpath" + "go.ate.dev/substrate/cmd/atelet/internal/trustbundle" + "go.ate.dev/substrate/internal/proto/ateletpb" + "go.ate.dev/substrate/internal/resources" certsv1 "k8s.io/api/certificates/v1" certsv1beta1 "k8s.io/api/certificates/v1beta1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" diff --git a/cmd/atelet/volumes.go b/cmd/atelet/volumes.go index 149d89ac97..b9c573c939 100644 --- a/cmd/atelet/volumes.go +++ b/cmd/atelet/volumes.go @@ -21,10 +21,10 @@ import ( "log/slog" "os" - "github.com/agent-substrate/substrate/cmd/atelet/internal/ateletpath" - "github.com/agent-substrate/substrate/internal/proto/ateletpb" - "github.com/agent-substrate/substrate/internal/volume" - "github.com/agent-substrate/substrate/internal/volume/csi" + "go.ate.dev/substrate/cmd/atelet/internal/ateletpath" + "go.ate.dev/substrate/internal/proto/ateletpb" + "go.ate.dev/substrate/internal/volume" + "go.ate.dev/substrate/internal/volume/csi" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" ) diff --git a/cmd/atelet/volumes_test.go b/cmd/atelet/volumes_test.go index 7e57e36a2b..46dc30e80d 100644 --- a/cmd/atelet/volumes_test.go +++ b/cmd/atelet/volumes_test.go @@ -23,13 +23,13 @@ import ( "strings" "testing" - "github.com/agent-substrate/substrate/cmd/atelet/internal/ateletpath" - "github.com/agent-substrate/substrate/internal/nodepath" - "github.com/agent-substrate/substrate/internal/proto/ateletpb" - "github.com/agent-substrate/substrate/internal/volume" - "github.com/agent-substrate/substrate/pkg/api/v1alpha1" - "github.com/agent-substrate/substrate/pkg/client/clientset/versioned/fake" "github.com/google/go-cmp/cmp" + "go.ate.dev/substrate/cmd/atelet/internal/ateletpath" + "go.ate.dev/substrate/internal/nodepath" + "go.ate.dev/substrate/internal/proto/ateletpb" + "go.ate.dev/substrate/internal/volume" + "go.ate.dev/substrate/pkg/api/v1alpha1" + "go.ate.dev/substrate/pkg/client/clientset/versioned/fake" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) diff --git a/cmd/atelet/worker_sandbox_syncer.go b/cmd/atelet/worker_sandbox_syncer.go index 1378d08034..9880c40581 100644 --- a/cmd/atelet/worker_sandbox_syncer.go +++ b/cmd/atelet/worker_sandbox_syncer.go @@ -44,9 +44,9 @@ import ( "k8s.io/client-go/tools/cache" "k8s.io/client-go/util/workqueue" - "github.com/agent-substrate/substrate/internal/resources" - "github.com/agent-substrate/substrate/pkg/api/v1alpha1" - informersv1alpha1 "github.com/agent-substrate/substrate/pkg/client/informers/externalversions/api/v1alpha1" + "go.ate.dev/substrate/internal/resources" + "go.ate.dev/substrate/pkg/api/v1alpha1" + informersv1alpha1 "go.ate.dev/substrate/pkg/client/informers/externalversions/api/v1alpha1" ) // podUIDIndex indexes this node's worker pods by UID, the syncer's queue key. diff --git a/cmd/atelet/worker_sandbox_syncer_test.go b/cmd/atelet/worker_sandbox_syncer_test.go index 10557ca6f6..99c38368b0 100644 --- a/cmd/atelet/worker_sandbox_syncer_test.go +++ b/cmd/atelet/worker_sandbox_syncer_test.go @@ -33,11 +33,11 @@ import ( "k8s.io/client-go/tools/cache" "k8s.io/client-go/util/workqueue" - "github.com/agent-substrate/substrate/internal/sandboxversions" - "github.com/agent-substrate/substrate/pkg/api/v1alpha1" - atefake "github.com/agent-substrate/substrate/pkg/client/clientset/versioned/fake" - "github.com/agent-substrate/substrate/pkg/client/informers/externalversions" - listersv1alpha1 "github.com/agent-substrate/substrate/pkg/client/listers/api/v1alpha1" + "go.ate.dev/substrate/internal/sandboxversions" + "go.ate.dev/substrate/pkg/api/v1alpha1" + atefake "go.ate.dev/substrate/pkg/client/clientset/versioned/fake" + "go.ate.dev/substrate/pkg/client/informers/externalversions" + listersv1alpha1 "go.ate.dev/substrate/pkg/client/listers/api/v1alpha1" ) // workerPod is a worker pod on this node with the given UID, in pool when diff --git a/cmd/atelet/worker_sandbox_versions.go b/cmd/atelet/worker_sandbox_versions.go index f6037a7966..ad1358f17b 100644 --- a/cmd/atelet/worker_sandbox_versions.go +++ b/cmd/atelet/worker_sandbox_versions.go @@ -41,11 +41,11 @@ import ( "github.com/google/go-containerregistry/pkg/name" corev1 "k8s.io/api/core/v1" - "github.com/agent-substrate/substrate/internal/hardware" - "github.com/agent-substrate/substrate/internal/nodepath" - "github.com/agent-substrate/substrate/internal/sandboxversions" - "github.com/agent-substrate/substrate/pkg/api/v1alpha1" - listersv1alpha1 "github.com/agent-substrate/substrate/pkg/client/listers/api/v1alpha1" + "go.ate.dev/substrate/internal/hardware" + "go.ate.dev/substrate/internal/nodepath" + "go.ate.dev/substrate/internal/sandboxversions" + "go.ate.dev/substrate/pkg/api/v1alpha1" + listersv1alpha1 "go.ate.dev/substrate/pkg/client/listers/api/v1alpha1" ) // errPermanent marks a failure that retrying cannot fix, so the syncer drops diff --git a/cmd/atelet/worker_sandbox_versions_test.go b/cmd/atelet/worker_sandbox_versions_test.go index e64666dab6..e1f0fc7eb8 100644 --- a/cmd/atelet/worker_sandbox_versions_test.go +++ b/cmd/atelet/worker_sandbox_versions_test.go @@ -27,9 +27,9 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/utils/ptr" - "github.com/agent-substrate/substrate/internal/hardware" - "github.com/agent-substrate/substrate/internal/sandboxversions" - "github.com/agent-substrate/substrate/pkg/api/v1alpha1" + "go.ate.dev/substrate/internal/hardware" + "go.ate.dev/substrate/internal/sandboxversions" + "go.ate.dev/substrate/pkg/api/v1alpha1" ) // pauseDigest is the digest of the pause image of multiVersionConfig's diff --git a/cmd/atelet/workersupport.go b/cmd/atelet/workersupport.go index 0e05b7cc48..0aee16144d 100644 --- a/cmd/atelet/workersupport.go +++ b/cmd/atelet/workersupport.go @@ -20,11 +20,11 @@ import ( "fmt" "log/slog" - "github.com/agent-substrate/substrate/cmd/atelet/internal/apivalidation" - "github.com/agent-substrate/substrate/internal/proto/ateletpb" - "github.com/agent-substrate/substrate/internal/resources" - "github.com/agent-substrate/substrate/internal/substratex509" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "go.ate.dev/substrate/cmd/atelet/internal/apivalidation" + "go.ate.dev/substrate/internal/proto/ateletpb" + "go.ate.dev/substrate/internal/resources" + "go.ate.dev/substrate/internal/substratex509" + "go.ate.dev/substrate/pkg/proto/ateapipb" "google.golang.org/grpc/codes" "google.golang.org/grpc/credentials" "google.golang.org/grpc/peer" diff --git a/cmd/atelet/workersupport_test.go b/cmd/atelet/workersupport_test.go index 29c98e6c4d..6b9c4d5301 100644 --- a/cmd/atelet/workersupport_test.go +++ b/cmd/atelet/workersupport_test.go @@ -25,11 +25,11 @@ import ( "testing" "time" - "github.com/agent-substrate/substrate/internal/proto/ateletpb" - "github.com/agent-substrate/substrate/internal/resources" - "github.com/agent-substrate/substrate/internal/substratex509" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" "github.com/google/go-cmp/cmp" + "go.ate.dev/substrate/internal/proto/ateletpb" + "go.ate.dev/substrate/internal/resources" + "go.ate.dev/substrate/internal/substratex509" + "go.ate.dev/substrate/pkg/proto/ateapipb" "google.golang.org/grpc" "google.golang.org/grpc/codes" "google.golang.org/grpc/credentials" diff --git a/cmd/atenet/internal/root.go b/cmd/atenet/internal/root.go index d7d8046de8..6c703ebfe2 100644 --- a/cmd/atenet/internal/root.go +++ b/cmd/atenet/internal/root.go @@ -18,11 +18,11 @@ import ( "fmt" "os" - "github.com/agent-substrate/substrate/cmd/atenet/internal/router" - "github.com/agent-substrate/substrate/cmd/atenet/internal/sdsmint" - "github.com/agent-substrate/substrate/internal/preview" - "github.com/agent-substrate/substrate/internal/version" "github.com/spf13/cobra" + "go.ate.dev/substrate/cmd/atenet/internal/router" + "go.ate.dev/substrate/cmd/atenet/internal/sdsmint" + "go.ate.dev/substrate/internal/preview" + "go.ate.dev/substrate/internal/version" ) var previewFlags []string diff --git a/cmd/atenet/internal/router/cmd.go b/cmd/atenet/internal/router/cmd.go index bb412a545b..5a26645f6f 100644 --- a/cmd/atenet/internal/router/cmd.go +++ b/cmd/atenet/internal/router/cmd.go @@ -22,9 +22,9 @@ import ( "github.com/spf13/cobra" "github.com/spf13/pflag" - "github.com/agent-substrate/substrate/cmd/atenet/internal/router/egress" - "github.com/agent-substrate/substrate/cmd/atenet/internal/router/ingress" - "github.com/agent-substrate/substrate/internal/installdefaults" + "go.ate.dev/substrate/cmd/atenet/internal/router/egress" + "go.ate.dev/substrate/cmd/atenet/internal/router/ingress" + "go.ate.dev/substrate/internal/installdefaults" ) func NewRouterCmd() *cobra.Command { diff --git a/cmd/atenet/internal/router/config.go b/cmd/atenet/internal/router/config.go index 26143ccf22..84c43f4c6f 100644 --- a/cmd/atenet/internal/router/config.go +++ b/cmd/atenet/internal/router/config.go @@ -20,7 +20,7 @@ import ( "github.com/spiffe/go-spiffe/v2/spiffeid" - "github.com/agent-substrate/substrate/cmd/atenet/internal/router/ingress" + "go.ate.dev/substrate/cmd/atenet/internal/router/ingress" ) type atenetRouter string diff --git a/cmd/atenet/internal/router/config_test.go b/cmd/atenet/internal/router/config_test.go index 16ecb54bef..ece06c928b 100644 --- a/cmd/atenet/internal/router/config_test.go +++ b/cmd/atenet/internal/router/config_test.go @@ -21,7 +21,7 @@ import ( "github.com/spf13/pflag" - "github.com/agent-substrate/substrate/cmd/atenet/internal/router/ingress" + "go.ate.dev/substrate/cmd/atenet/internal/router/ingress" ) func TestRouterConfigValidate(t *testing.T) { diff --git a/cmd/atenet/internal/router/dataplane.go b/cmd/atenet/internal/router/dataplane.go index 06aae8cde1..352e3eca35 100644 --- a/cmd/atenet/internal/router/dataplane.go +++ b/cmd/atenet/internal/router/dataplane.go @@ -24,8 +24,8 @@ import ( "golang.org/x/sync/errgroup" - "github.com/agent-substrate/substrate/cmd/atenet/internal/router/ingress" - "github.com/agent-substrate/substrate/internal/credbundle" + "go.ate.dev/substrate/cmd/atenet/internal/router/ingress" + "go.ate.dev/substrate/internal/credbundle" ) type dataplaneHealthCheck struct { diff --git a/cmd/atenet/internal/router/drain.go b/cmd/atenet/internal/router/drain.go index 1c4ef913b1..3e94d44397 100644 --- a/cmd/atenet/internal/router/drain.go +++ b/cmd/atenet/internal/router/drain.go @@ -23,7 +23,7 @@ import ( "path/filepath" "time" - "github.com/agent-substrate/substrate/internal/serverboot" + "go.ate.dev/substrate/internal/serverboot" ) // defaultDrainCompleteFile is where the drain sequence leaves its completion diff --git a/cmd/atenet/internal/router/drain_test.go b/cmd/atenet/internal/router/drain_test.go index b4b7083afa..17bcb77658 100644 --- a/cmd/atenet/internal/router/drain_test.go +++ b/cmd/atenet/internal/router/drain_test.go @@ -26,7 +26,7 @@ import ( "testing" "time" - "github.com/agent-substrate/substrate/internal/serverboot" + "go.ate.dev/substrate/internal/serverboot" ) // drainFunc adapts a bare function to the dataplaneDrainer interface. diff --git a/cmd/atenet/internal/router/egress/actorjwt/actorjwt.go b/cmd/atenet/internal/router/egress/actorjwt/actorjwt.go index 4f79ca14a1..6ac39429ea 100644 --- a/cmd/atenet/internal/router/egress/actorjwt/actorjwt.go +++ b/cmd/atenet/internal/router/egress/actorjwt/actorjwt.go @@ -25,8 +25,8 @@ import ( "google.golang.org/protobuf/proto" "k8s.io/apimachinery/pkg/util/cache" - "github.com/agent-substrate/substrate/internal/resources" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "go.ate.dev/substrate/internal/resources" + "go.ate.dev/substrate/pkg/proto/ateapipb" ) // Minter mints actor JWTs and reuses each token until a third of its lifetime diff --git a/cmd/atenet/internal/router/egress/actorjwt/actorjwt_test.go b/cmd/atenet/internal/router/egress/actorjwt/actorjwt_test.go index f7194887a4..07a8a49f08 100644 --- a/cmd/atenet/internal/router/egress/actorjwt/actorjwt_test.go +++ b/cmd/atenet/internal/router/egress/actorjwt/actorjwt_test.go @@ -33,8 +33,8 @@ import ( "google.golang.org/protobuf/proto" "google.golang.org/protobuf/types/known/timestamppb" - "github.com/agent-substrate/substrate/internal/resources" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "go.ate.dev/substrate/internal/resources" + "go.ate.dev/substrate/pkg/proto/ateapipb" ) var testActor = resources.ActorRef{Atespace: "default", Name: "my-actor"} diff --git a/cmd/atenet/internal/router/egress/credentials.go b/cmd/atenet/internal/router/egress/credentials.go index 6ac51c3145..31f292df8f 100644 --- a/cmd/atenet/internal/router/egress/credentials.go +++ b/cmd/atenet/internal/router/egress/credentials.go @@ -25,11 +25,11 @@ import ( "google.golang.org/grpc/codes" "google.golang.org/grpc/status" - "github.com/agent-substrate/substrate/cmd/atenet/internal/router/extproc" - "github.com/agent-substrate/substrate/internal/egresspolicy" - "github.com/agent-substrate/substrate/internal/resources" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" - "github.com/agent-substrate/substrate/pkg/proto/credproviderpb" + "go.ate.dev/substrate/cmd/atenet/internal/router/extproc" + "go.ate.dev/substrate/internal/egresspolicy" + "go.ate.dev/substrate/internal/resources" + "go.ate.dev/substrate/pkg/proto/ateapipb" + "go.ate.dev/substrate/pkg/proto/credproviderpb" ) // mapCredentialProviderError converts a FetchSecret failure into a diff --git a/cmd/atenet/internal/router/egress/credentials_test.go b/cmd/atenet/internal/router/egress/credentials_test.go index 57fbcb09bd..e90d9aae93 100644 --- a/cmd/atenet/internal/router/egress/credentials_test.go +++ b/cmd/atenet/internal/router/egress/credentials_test.go @@ -27,9 +27,9 @@ import ( "google.golang.org/grpc/status" "google.golang.org/protobuf/proto" - "github.com/agent-substrate/substrate/cmd/atenet/internal/router/extproc" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" - "github.com/agent-substrate/substrate/pkg/proto/credproviderpb" + "go.ate.dev/substrate/cmd/atenet/internal/router/extproc" + "go.ate.dev/substrate/pkg/proto/ateapipb" + "go.ate.dev/substrate/pkg/proto/credproviderpb" ) // credentialInjectionPolicySample injects "authorization: Bearer " from diff --git a/cmd/atenet/internal/router/egress/egress.go b/cmd/atenet/internal/router/egress/egress.go index 0fcd11fd7c..1fd82c42dc 100644 --- a/cmd/atenet/internal/router/egress/egress.go +++ b/cmd/atenet/internal/router/egress/egress.go @@ -41,12 +41,12 @@ import ( "google.golang.org/grpc/status" "google.golang.org/protobuf/types/known/structpb" - "github.com/agent-substrate/substrate/cmd/atenet/internal/router/egress/actorjwt" - "github.com/agent-substrate/substrate/cmd/atenet/internal/router/extproc" - "github.com/agent-substrate/substrate/internal/egresspolicy" - "github.com/agent-substrate/substrate/internal/resources" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" - "github.com/agent-substrate/substrate/pkg/proto/credproviderpb" + "go.ate.dev/substrate/cmd/atenet/internal/router/egress/actorjwt" + "go.ate.dev/substrate/cmd/atenet/internal/router/extproc" + "go.ate.dev/substrate/internal/egresspolicy" + "go.ate.dev/substrate/internal/resources" + "go.ate.dev/substrate/pkg/proto/ateapipb" + "go.ate.dev/substrate/pkg/proto/credproviderpb" ) const ( diff --git a/cmd/atenet/internal/router/egress/egress_test.go b/cmd/atenet/internal/router/egress/egress_test.go index bc6df80dbc..8fe77bc535 100644 --- a/cmd/atenet/internal/router/egress/egress_test.go +++ b/cmd/atenet/internal/router/egress/egress_test.go @@ -42,10 +42,10 @@ import ( "google.golang.org/protobuf/types/known/structpb" "google.golang.org/protobuf/types/known/timestamppb" - "github.com/agent-substrate/substrate/cmd/atenet/internal/router/extproc" - "github.com/agent-substrate/substrate/internal/egresspolicy" - "github.com/agent-substrate/substrate/internal/resources" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "go.ate.dev/substrate/cmd/atenet/internal/router/extproc" + "go.ate.dev/substrate/internal/egresspolicy" + "go.ate.dev/substrate/internal/resources" + "go.ate.dev/substrate/pkg/proto/ateapipb" ) const ( diff --git a/cmd/atenet/internal/router/egress/policycache.go b/cmd/atenet/internal/router/egress/policycache.go index 954d91a174..28f6f29e02 100644 --- a/cmd/atenet/internal/router/egress/policycache.go +++ b/cmd/atenet/internal/router/egress/policycache.go @@ -26,9 +26,9 @@ import ( "google.golang.org/grpc/codes" "google.golang.org/grpc/status" - "github.com/agent-substrate/substrate/internal/egresspolicy" - "github.com/agent-substrate/substrate/internal/resources" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "go.ate.dev/substrate/internal/egresspolicy" + "go.ate.dev/substrate/internal/resources" + "go.ate.dev/substrate/pkg/proto/ateapipb" ) // DefaultPolicyCacheTTL is how long a fetched policy stays usable, and so how diff --git a/cmd/atenet/internal/router/egress/policycache_test.go b/cmd/atenet/internal/router/egress/policycache_test.go index 4a175d8ab8..b712a13ebf 100644 --- a/cmd/atenet/internal/router/egress/policycache_test.go +++ b/cmd/atenet/internal/router/egress/policycache_test.go @@ -26,8 +26,8 @@ import ( "google.golang.org/grpc/codes" "google.golang.org/grpc/status" - "github.com/agent-substrate/substrate/internal/egresspolicy" - "github.com/agent-substrate/substrate/internal/resources" + "go.ate.dev/substrate/internal/egresspolicy" + "go.ate.dev/substrate/internal/resources" ) var testActorRef = resources.ActorRef{Atespace: testEgressAtespace, Name: testEgressActor} diff --git a/cmd/atenet/internal/router/egress/provider.go b/cmd/atenet/internal/router/egress/provider.go index fce57bb1be..252e3ec237 100644 --- a/cmd/atenet/internal/router/egress/provider.go +++ b/cmd/atenet/internal/router/egress/provider.go @@ -33,7 +33,7 @@ import ( "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" - "github.com/agent-substrate/substrate/internal/ateapiauth" + "go.ate.dev/substrate/internal/ateapiauth" ) // credentialURIScheme is the only scheme a credential URI may carry. diff --git a/cmd/atenet/internal/router/egress/request.go b/cmd/atenet/internal/router/egress/request.go index 6313de4c58..e75ad8bd19 100644 --- a/cmd/atenet/internal/router/egress/request.go +++ b/cmd/atenet/internal/router/egress/request.go @@ -24,9 +24,9 @@ import ( extprocv3 "github.com/envoyproxy/go-control-plane/envoy/service/ext_proc/v3" envoy_type "github.com/envoyproxy/go-control-plane/envoy/type/v3" - "github.com/agent-substrate/substrate/cmd/atenet/internal/router/extproc" - "github.com/agent-substrate/substrate/internal/egresspolicy" - "github.com/agent-substrate/substrate/internal/resources" + "go.ate.dev/substrate/cmd/atenet/internal/router/extproc" + "go.ate.dev/substrate/internal/egresspolicy" + "go.ate.dev/substrate/internal/resources" ) const websocketDeniedBody = "WebSocket egress is not supported" diff --git a/cmd/atenet/internal/router/egress/request_test.go b/cmd/atenet/internal/router/egress/request_test.go index 67589c16b6..954d8dd32f 100644 --- a/cmd/atenet/internal/router/egress/request_test.go +++ b/cmd/atenet/internal/router/egress/request_test.go @@ -26,8 +26,8 @@ import ( "google.golang.org/grpc/status" "google.golang.org/protobuf/types/known/structpb" - "github.com/agent-substrate/substrate/cmd/atenet/internal/router/extproc" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "go.ate.dev/substrate/cmd/atenet/internal/router/extproc" + "go.ate.dev/substrate/pkg/proto/ateapipb" ) // testActorSPIFFEID is the identity filter state the CONNECT chain shares. diff --git a/cmd/atenet/internal/router/egresspolicymanifest_test.go b/cmd/atenet/internal/router/egresspolicymanifest_test.go index 5dc34d7c55..4aae9928fa 100644 --- a/cmd/atenet/internal/router/egresspolicymanifest_test.go +++ b/cmd/atenet/internal/router/egresspolicymanifest_test.go @@ -21,7 +21,7 @@ import ( "sigs.k8s.io/yaml" - "github.com/agent-substrate/substrate/cmd/atenet/internal/router/extproc" + "go.ate.dev/substrate/cmd/atenet/internal/router/extproc" ) // These tests pin the egress manifests to the Go constants the handler diff --git a/cmd/atenet/internal/router/extproc/egress_websocket_test.go b/cmd/atenet/internal/router/extproc/egress_websocket_test.go index f82116830b..dc25406859 100644 --- a/cmd/atenet/internal/router/extproc/egress_websocket_test.go +++ b/cmd/atenet/internal/router/extproc/egress_websocket_test.go @@ -23,8 +23,8 @@ import ( extprocv3 "github.com/envoyproxy/go-control-plane/envoy/service/ext_proc/v3" "google.golang.org/protobuf/types/known/structpb" - "github.com/agent-substrate/substrate/cmd/atenet/internal/router/egress" - "github.com/agent-substrate/substrate/cmd/atenet/internal/router/extproc" + "go.ate.dev/substrate/cmd/atenet/internal/router/egress" + "go.ate.dev/substrate/cmd/atenet/internal/router/extproc" ) const websocketDenialBody = "WebSocket egress is not supported" diff --git a/cmd/atenet/internal/router/extproc/handler.go b/cmd/atenet/internal/router/extproc/handler.go index 7105493d16..0309bf1acb 100644 --- a/cmd/atenet/internal/router/extproc/handler.go +++ b/cmd/atenet/internal/router/extproc/handler.go @@ -20,7 +20,7 @@ import ( extprocv3 "github.com/envoyproxy/go-control-plane/envoy/service/ext_proc/v3" "google.golang.org/protobuf/types/known/structpb" - "github.com/agent-substrate/substrate/internal/ateattr" + "go.ate.dev/substrate/internal/ateattr" ) // Handler applies one direction's policy to a request. Implementations live in diff --git a/cmd/atenet/internal/router/extproc/metadata_test.go b/cmd/atenet/internal/router/extproc/metadata_test.go index 09a4b2548b..e33ee4f04b 100644 --- a/cmd/atenet/internal/router/extproc/metadata_test.go +++ b/cmd/atenet/internal/router/extproc/metadata_test.go @@ -19,8 +19,8 @@ import ( "strings" "testing" - "github.com/agent-substrate/substrate/internal/atenet" corev3 "github.com/envoyproxy/go-control-plane/envoy/config/core/v3" + "go.ate.dev/substrate/internal/atenet" "google.golang.org/protobuf/types/known/structpb" ) diff --git a/cmd/atenet/internal/router/extproc/metrics.go b/cmd/atenet/internal/router/extproc/metrics.go index 0ff6972496..3f6a07ddb5 100644 --- a/cmd/atenet/internal/router/extproc/metrics.go +++ b/cmd/atenet/internal/router/extproc/metrics.go @@ -26,7 +26,7 @@ import ( "google.golang.org/grpc/codes" "google.golang.org/grpc/status" - "github.com/agent-substrate/substrate/internal/ateattr" + "go.ate.dev/substrate/internal/ateattr" ) // ServiceName is the OpenTelemetry service name and instrumentation scope diff --git a/cmd/atenet/internal/router/extproc/metrics_test.go b/cmd/atenet/internal/router/extproc/metrics_test.go index b02eadea01..2e081f9bfa 100644 --- a/cmd/atenet/internal/router/extproc/metrics_test.go +++ b/cmd/atenet/internal/router/extproc/metrics_test.go @@ -27,7 +27,7 @@ import ( "google.golang.org/grpc/codes" "google.golang.org/grpc/status" - "github.com/agent-substrate/substrate/internal/ateattr" + "go.ate.dev/substrate/internal/ateattr" ) func TestClassifyOutcome(t *testing.T) { diff --git a/cmd/atenet/internal/router/health.go b/cmd/atenet/internal/router/health.go index 19f7802196..335917945b 100644 --- a/cmd/atenet/internal/router/health.go +++ b/cmd/atenet/internal/router/health.go @@ -25,7 +25,7 @@ import ( "sync" "time" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "go.ate.dev/substrate/pkg/proto/ateapipb" "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" "k8s.io/apimachinery/pkg/version" "k8s.io/client-go/kubernetes" diff --git a/cmd/atenet/internal/router/health_test.go b/cmd/atenet/internal/router/health_test.go index a04892514b..ccac1a4c72 100644 --- a/cmd/atenet/internal/router/health_test.go +++ b/cmd/atenet/internal/router/health_test.go @@ -25,7 +25,7 @@ import ( "testing" "time" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "go.ate.dev/substrate/pkg/proto/ateapipb" "google.golang.org/grpc" "k8s.io/client-go/kubernetes" kubernetesfake "k8s.io/client-go/kubernetes/fake" diff --git a/cmd/atenet/internal/router/ingress/errors.go b/cmd/atenet/internal/router/ingress/errors.go index caaf6c7b32..6218d44dfa 100644 --- a/cmd/atenet/internal/router/ingress/errors.go +++ b/cmd/atenet/internal/router/ingress/errors.go @@ -23,9 +23,9 @@ import ( "google.golang.org/grpc/codes" "google.golang.org/grpc/status" - "github.com/agent-substrate/substrate/cmd/atenet/internal/router/extproc" - "github.com/agent-substrate/substrate/internal/ateattr" - "github.com/agent-substrate/substrate/internal/resources" + "go.ate.dev/substrate/cmd/atenet/internal/router/extproc" + "go.ate.dev/substrate/internal/ateattr" + "go.ate.dev/substrate/internal/resources" ) // actorNotFoundErr returns a 404 denial identifying the missing actor. diff --git a/cmd/atenet/internal/router/ingress/errors_test.go b/cmd/atenet/internal/router/ingress/errors_test.go index d366fa72e2..a95bf1bb86 100644 --- a/cmd/atenet/internal/router/ingress/errors_test.go +++ b/cmd/atenet/internal/router/ingress/errors_test.go @@ -23,8 +23,8 @@ import ( "google.golang.org/grpc/codes" "google.golang.org/grpc/status" - "github.com/agent-substrate/substrate/cmd/atenet/internal/router/extproc" - "github.com/agent-substrate/substrate/internal/resources" + "go.ate.dev/substrate/cmd/atenet/internal/router/extproc" + "go.ate.dev/substrate/internal/resources" ) func TestActorNotFoundErr(t *testing.T) { diff --git a/cmd/atenet/internal/router/ingress/flight.go b/cmd/atenet/internal/router/ingress/flight.go index c339d55e38..7afd444b01 100644 --- a/cmd/atenet/internal/router/ingress/flight.go +++ b/cmd/atenet/internal/router/ingress/flight.go @@ -17,7 +17,7 @@ package ingress import ( "sync/atomic" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "go.ate.dev/substrate/pkg/proto/ateapipb" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" ) diff --git a/cmd/atenet/internal/router/ingress/ingress.go b/cmd/atenet/internal/router/ingress/ingress.go index 334395f134..58dd9f5dec 100644 --- a/cmd/atenet/internal/router/ingress/ingress.go +++ b/cmd/atenet/internal/router/ingress/ingress.go @@ -36,10 +36,10 @@ import ( "go.opentelemetry.io/otel" "google.golang.org/protobuf/types/known/structpb" - "github.com/agent-substrate/substrate/cmd/atenet/internal/router/extproc" - "github.com/agent-substrate/substrate/internal/atenet" - atunnelingress "github.com/agent-substrate/substrate/internal/atunnel/ingress" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "go.ate.dev/substrate/cmd/atenet/internal/router/extproc" + "go.ate.dev/substrate/internal/atenet" + atunnelingress "go.ate.dev/substrate/internal/atunnel/ingress" + "go.ate.dev/substrate/pkg/proto/ateapipb" ) // defaultActorPort is the actor's port when a request names no other one. diff --git a/cmd/atenet/internal/router/ingress/ingress_test.go b/cmd/atenet/internal/router/ingress/ingress_test.go index b53ac89412..760b55f0b8 100644 --- a/cmd/atenet/internal/router/ingress/ingress_test.go +++ b/cmd/atenet/internal/router/ingress/ingress_test.go @@ -33,11 +33,11 @@ import ( "google.golang.org/grpc/status" "google.golang.org/protobuf/types/known/structpb" - "github.com/agent-substrate/substrate/cmd/atenet/internal/router/extproc" - "github.com/agent-substrate/substrate/internal/ateattr" - "github.com/agent-substrate/substrate/internal/atenet" - atunnelingress "github.com/agent-substrate/substrate/internal/atunnel/ingress" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "go.ate.dev/substrate/cmd/atenet/internal/router/extproc" + "go.ate.dev/substrate/internal/ateattr" + "go.ate.dev/substrate/internal/atenet" + atunnelingress "go.ate.dev/substrate/internal/atunnel/ingress" + "go.ate.dev/substrate/pkg/proto/ateapipb" ) type mockClient struct { diff --git a/cmd/atenet/internal/router/ingress/metrics.go b/cmd/atenet/internal/router/ingress/metrics.go index ee79c33559..23e3c7718a 100644 --- a/cmd/atenet/internal/router/ingress/metrics.go +++ b/cmd/atenet/internal/router/ingress/metrics.go @@ -23,8 +23,8 @@ import ( "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/metric" - "github.com/agent-substrate/substrate/cmd/atenet/internal/router/extproc" - "github.com/agent-substrate/substrate/internal/ateattr" + "go.ate.dev/substrate/cmd/atenet/internal/router/extproc" + "go.ate.dev/substrate/internal/ateattr" ) // Request-parking instruments. parking.active is the live count of parked diff --git a/cmd/atenet/internal/router/ingress/metrics_test.go b/cmd/atenet/internal/router/ingress/metrics_test.go index ad4db89556..f6c2acec0e 100644 --- a/cmd/atenet/internal/router/ingress/metrics_test.go +++ b/cmd/atenet/internal/router/ingress/metrics_test.go @@ -23,7 +23,7 @@ import ( sdkmetric "go.opentelemetry.io/otel/sdk/metric" "go.opentelemetry.io/otel/sdk/metric/metricdata" - "github.com/agent-substrate/substrate/internal/ateattr" + "go.ate.dev/substrate/internal/ateattr" ) // newTestParkingMetrics returns ParkingMetrics whose rejected counter reports diff --git a/cmd/atenet/internal/router/ingress/parking_test.go b/cmd/atenet/internal/router/ingress/parking_test.go index 332f4528a4..06c139225c 100644 --- a/cmd/atenet/internal/router/ingress/parking_test.go +++ b/cmd/atenet/internal/router/ingress/parking_test.go @@ -20,7 +20,7 @@ import ( "testing" "time" - "github.com/agent-substrate/substrate/internal/ateattr" + "go.ate.dev/substrate/internal/ateattr" ) func TestParkingLot_CapacityAndRelease(t *testing.T) { diff --git a/cmd/atenet/internal/router/ingress/resumer.go b/cmd/atenet/internal/router/ingress/resumer.go index 58ac0dded6..d522d3df1a 100644 --- a/cmd/atenet/internal/router/ingress/resumer.go +++ b/cmd/atenet/internal/router/ingress/resumer.go @@ -21,10 +21,10 @@ import ( "sync/atomic" "time" - "github.com/agent-substrate/substrate/cmd/atenet/internal/router/extproc" - "github.com/agent-substrate/substrate/internal/ateattr" - "github.com/agent-substrate/substrate/internal/resources" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "go.ate.dev/substrate/cmd/atenet/internal/router/extproc" + "go.ate.dev/substrate/internal/ateattr" + "go.ate.dev/substrate/internal/resources" + "go.ate.dev/substrate/pkg/proto/ateapipb" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/trace" "google.golang.org/grpc/codes" diff --git a/cmd/atenet/internal/router/ingress/resumer_test.go b/cmd/atenet/internal/router/ingress/resumer_test.go index 965bfa941f..3579ddf4dd 100644 --- a/cmd/atenet/internal/router/ingress/resumer_test.go +++ b/cmd/atenet/internal/router/ingress/resumer_test.go @@ -24,11 +24,11 @@ import ( "testing/synctest" "time" - "github.com/agent-substrate/substrate/cmd/atenet/internal/router/extproc" - "github.com/agent-substrate/substrate/internal/ateattr" - "github.com/agent-substrate/substrate/internal/resources" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" envoy_type "github.com/envoyproxy/go-control-plane/envoy/type/v3" + "go.ate.dev/substrate/cmd/atenet/internal/router/extproc" + "go.ate.dev/substrate/internal/ateattr" + "go.ate.dev/substrate/internal/resources" + "go.ate.dev/substrate/pkg/proto/ateapipb" "go.opentelemetry.io/otel/trace" "google.golang.org/grpc" "google.golang.org/grpc/codes" diff --git a/cmd/atenet/internal/router/router.go b/cmd/atenet/internal/router/router.go index 724ab979f9..5c84e1964a 100644 --- a/cmd/atenet/internal/router/router.go +++ b/cmd/atenet/internal/router/router.go @@ -36,15 +36,15 @@ import ( "k8s.io/client-go/tools/clientcmd" "sigs.k8s.io/controller-runtime/pkg/client/config" - "github.com/agent-substrate/substrate/cmd/atenet/internal/router/egress" - "github.com/agent-substrate/substrate/cmd/atenet/internal/router/extproc" - "github.com/agent-substrate/substrate/cmd/atenet/internal/router/ingress" - "github.com/agent-substrate/substrate/internal/ateapiauth" - "github.com/agent-substrate/substrate/internal/preview" - "github.com/agent-substrate/substrate/internal/serverboot" - "github.com/agent-substrate/substrate/internal/version" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" - "github.com/agent-substrate/substrate/pkg/proto/credproviderpb" + "go.ate.dev/substrate/cmd/atenet/internal/router/egress" + "go.ate.dev/substrate/cmd/atenet/internal/router/extproc" + "go.ate.dev/substrate/cmd/atenet/internal/router/ingress" + "go.ate.dev/substrate/internal/ateapiauth" + "go.ate.dev/substrate/internal/preview" + "go.ate.dev/substrate/internal/serverboot" + "go.ate.dev/substrate/internal/version" + "go.ate.dev/substrate/pkg/proto/ateapipb" + "go.ate.dev/substrate/pkg/proto/credproviderpb" ) // dataPlaneTraceRatio is the default root sampling fraction for parentless diff --git a/cmd/atenet/internal/router/status.go b/cmd/atenet/internal/router/status.go index 2afae673a1..1f9907dfdc 100644 --- a/cmd/atenet/internal/router/status.go +++ b/cmd/atenet/internal/router/status.go @@ -32,8 +32,8 @@ import ( "go.opentelemetry.io/otel" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "github.com/agent-substrate/substrate/cmd/atenet/internal/router/extproc" - "github.com/agent-substrate/substrate/cmd/atenet/internal/router/ingress" + "go.ate.dev/substrate/cmd/atenet/internal/router/extproc" + "go.ate.dev/substrate/cmd/atenet/internal/router/ingress" ) var BuildTag = "dev" diff --git a/cmd/atenet/internal/router/status_test.go b/cmd/atenet/internal/router/status_test.go index 02f3055a5c..43b973f044 100644 --- a/cmd/atenet/internal/router/status_test.go +++ b/cmd/atenet/internal/router/status_test.go @@ -36,8 +36,8 @@ import ( 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" + "go.ate.dev/substrate/cmd/atenet/internal/router/extproc" + "go.ate.dev/substrate/cmd/atenet/internal/router/ingress" ) func TestStatuszEndpoint(t *testing.T) { diff --git a/cmd/atenet/internal/router/xds.go b/cmd/atenet/internal/router/xds.go index 2d06f2c0b2..bca5cb0f52 100644 --- a/cmd/atenet/internal/router/xds.go +++ b/cmd/atenet/internal/router/xds.go @@ -34,8 +34,8 @@ import ( "google.golang.org/protobuf/types/known/durationpb" "google.golang.org/protobuf/types/known/wrapperspb" - "github.com/agent-substrate/substrate/internal/atenet" - "github.com/agent-substrate/substrate/internal/installdefaults" + "go.ate.dev/substrate/internal/atenet" + "go.ate.dev/substrate/internal/installdefaults" accesslogv3 "github.com/envoyproxy/go-control-plane/envoy/config/accesslog/v3" clusterv3 "github.com/envoyproxy/go-control-plane/envoy/config/cluster/v3" @@ -72,9 +72,9 @@ import ( resourcev3 "github.com/envoyproxy/go-control-plane/pkg/resource/v3" serverv3 "github.com/envoyproxy/go-control-plane/pkg/server/v3" - "github.com/agent-substrate/substrate/cmd/atenet/internal/router/extproc" - "github.com/agent-substrate/substrate/cmd/atenet/internal/router/ingress" - atunnelingress "github.com/agent-substrate/substrate/internal/atunnel/ingress" + "go.ate.dev/substrate/cmd/atenet/internal/router/extproc" + "go.ate.dev/substrate/cmd/atenet/internal/router/ingress" + atunnelingress "go.ate.dev/substrate/internal/atunnel/ingress" ) const ( diff --git a/cmd/atenet/internal/router/xds_test.go b/cmd/atenet/internal/router/xds_test.go index 8a62ff9b9e..7862ff559d 100644 --- a/cmd/atenet/internal/router/xds_test.go +++ b/cmd/atenet/internal/router/xds_test.go @@ -49,9 +49,9 @@ import ( 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/cmd/atenet/internal/router/ingress" - atunnelingress "github.com/agent-substrate/substrate/internal/atunnel/ingress" + "go.ate.dev/substrate/cmd/atenet/internal/router/extproc" + "go.ate.dev/substrate/cmd/atenet/internal/router/ingress" + atunnelingress "go.ate.dev/substrate/internal/atunnel/ingress" ) func TestActorRoutingFilterStateFilter(t *testing.T) { diff --git a/cmd/atenet/internal/router/xds_upstream_connect_test.go b/cmd/atenet/internal/router/xds_upstream_connect_test.go index 25f3796d86..d72c79f921 100644 --- a/cmd/atenet/internal/router/xds_upstream_connect_test.go +++ b/cmd/atenet/internal/router/xds_upstream_connect_test.go @@ -34,9 +34,9 @@ import ( resourcev3 "github.com/envoyproxy/go-control-plane/pkg/resource/v3" "github.com/spf13/pflag" - "github.com/agent-substrate/substrate/cmd/atenet/internal/router/extproc" - "github.com/agent-substrate/substrate/internal/atenet" - atunnelingress "github.com/agent-substrate/substrate/internal/atunnel/ingress" + "go.ate.dev/substrate/cmd/atenet/internal/router/extproc" + "go.ate.dev/substrate/internal/atenet" + atunnelingress "go.ate.dev/substrate/internal/atunnel/ingress" ) // upstreamConnectSnapshot also enables the CONNECT listener, so main_internal diff --git a/cmd/atenet/internal/sdsmint/minter.go b/cmd/atenet/internal/sdsmint/minter.go index 1ec60e30fe..c1d9bc2a4a 100644 --- a/cmd/atenet/internal/sdsmint/minter.go +++ b/cmd/atenet/internal/sdsmint/minter.go @@ -30,7 +30,7 @@ import ( "strings" "time" - "github.com/agent-substrate/substrate/internal/localca" + "go.ate.dev/substrate/internal/localca" ) // errHostNotAllowed is returned when a requested hostname will not be minted. diff --git a/cmd/atenet/internal/sdsmint/minter_test.go b/cmd/atenet/internal/sdsmint/minter_test.go index 4e84e5e55b..935c4a57cf 100644 --- a/cmd/atenet/internal/sdsmint/minter_test.go +++ b/cmd/atenet/internal/sdsmint/minter_test.go @@ -23,7 +23,7 @@ import ( "testing" "time" - "github.com/agent-substrate/substrate/internal/localca" + "go.ate.dev/substrate/internal/localca" ) func testMinter(t *testing.T, opts minterOptions) *minter { diff --git a/cmd/atenet/internal/sdsmint/run.go b/cmd/atenet/internal/sdsmint/run.go index 15d144e54b..884508523c 100644 --- a/cmd/atenet/internal/sdsmint/run.go +++ b/cmd/atenet/internal/sdsmint/run.go @@ -27,9 +27,9 @@ import ( "syscall" "time" - "github.com/agent-substrate/substrate/internal/localca" - "github.com/agent-substrate/substrate/internal/version" secretservice "github.com/envoyproxy/go-control-plane/envoy/service/secret/v3" + "go.ate.dev/substrate/internal/localca" + "go.ate.dev/substrate/internal/version" "google.golang.org/grpc" ) diff --git a/cmd/atenet/main.go b/cmd/atenet/main.go index 097f9617f9..bc0fa57a80 100644 --- a/cmd/atenet/main.go +++ b/cmd/atenet/main.go @@ -14,7 +14,7 @@ package main -import "github.com/agent-substrate/substrate/cmd/atenet/internal" +import "go.ate.dev/substrate/cmd/atenet/internal" func main() { internal.Execute() diff --git a/cmd/ateworker-gvisor/actorpath.go b/cmd/ateworker-gvisor/actorpath.go index eef2bd318e..32ed676d7a 100644 --- a/cmd/ateworker-gvisor/actorpath.go +++ b/cmd/ateworker-gvisor/actorpath.go @@ -23,8 +23,8 @@ import ( "os" "path/filepath" - "github.com/agent-substrate/substrate/internal/imagecache" - "github.com/agent-substrate/substrate/internal/proto/ateworkerpb" + "go.ate.dev/substrate/internal/imagecache" + "go.ate.dev/substrate/internal/proto/ateworkerpb" ) // ociBundlePath is the container's OCI bundle. diff --git a/cmd/ateworker-gvisor/actorpath_test.go b/cmd/ateworker-gvisor/actorpath_test.go index dba4708c1d..54c40b965e 100644 --- a/cmd/ateworker-gvisor/actorpath_test.go +++ b/cmd/ateworker-gvisor/actorpath_test.go @@ -23,8 +23,8 @@ import ( "golang.org/x/sys/unix" - "github.com/agent-substrate/substrate/internal/proto/ateworkerpb" - "github.com/agent-substrate/substrate/internal/roottest" + "go.ate.dev/substrate/internal/proto/ateworkerpb" + "go.ate.dev/substrate/internal/roottest" ) func TestResetRunscStateAndPidFileDirs(t *testing.T) { diff --git a/cmd/ateworker-gvisor/ckptcgroup.go b/cmd/ateworker-gvisor/ckptcgroup.go index 1a4663b43d..7660da6207 100644 --- a/cmd/ateworker-gvisor/ckptcgroup.go +++ b/cmd/ateworker-gvisor/ckptcgroup.go @@ -28,9 +28,9 @@ import ( "strconv" "strings" - "github.com/agent-substrate/substrate/internal/ocispec" - "github.com/agent-substrate/substrate/internal/proto/ateworkerpb" - "github.com/agent-substrate/substrate/internal/snapshotcache" + "go.ate.dev/substrate/internal/ocispec" + "go.ate.dev/substrate/internal/proto/ateworkerpb" + "go.ate.dev/substrate/internal/snapshotcache" "golang.org/x/sys/unix" ) diff --git a/cmd/ateworker-gvisor/ckptcgroup_test.go b/cmd/ateworker-gvisor/ckptcgroup_test.go index 970538fceb..adaa737838 100644 --- a/cmd/ateworker-gvisor/ckptcgroup_test.go +++ b/cmd/ateworker-gvisor/ckptcgroup_test.go @@ -27,9 +27,9 @@ import ( "testing" "time" - "github.com/agent-substrate/substrate/internal/ocispec" - "github.com/agent-substrate/substrate/internal/proto/ateworkerpb" - "github.com/agent-substrate/substrate/internal/snapshotcache" + "go.ate.dev/substrate/internal/ocispec" + "go.ate.dev/substrate/internal/proto/ateworkerpb" + "go.ate.dev/substrate/internal/snapshotcache" "golang.org/x/sys/unix" ) diff --git a/cmd/ateworker-gvisor/cleanup_test.go b/cmd/ateworker-gvisor/cleanup_test.go index abbb4667f1..655f765872 100644 --- a/cmd/ateworker-gvisor/cleanup_test.go +++ b/cmd/ateworker-gvisor/cleanup_test.go @@ -23,7 +23,7 @@ import ( "slices" "testing" - "github.com/agent-substrate/substrate/internal/proto/ateworkerpb" + "go.ate.dev/substrate/internal/proto/ateworkerpb" ) // fakeRunsc stands in for *runsc. containers is the set runsc has a record of; diff --git a/cmd/ateworker-gvisor/durable.go b/cmd/ateworker-gvisor/durable.go index dfe6051ef6..0dcb8b4e93 100644 --- a/cmd/ateworker-gvisor/durable.go +++ b/cmd/ateworker-gvisor/durable.go @@ -23,8 +23,8 @@ import ( "os" "path/filepath" - "github.com/agent-substrate/substrate/internal/proto/ateworkerpb" - "github.com/agent-substrate/substrate/internal/resources" + "go.ate.dev/substrate/internal/proto/ateworkerpb" + "go.ate.dev/substrate/internal/resources" ) const fsCheckpointManifest = "fscheckpoint.pb" diff --git a/cmd/ateworker-gvisor/durable_test.go b/cmd/ateworker-gvisor/durable_test.go index fc5303c15f..ee92c52184 100644 --- a/cmd/ateworker-gvisor/durable_test.go +++ b/cmd/ateworker-gvisor/durable_test.go @@ -22,7 +22,7 @@ import ( "slices" "testing" - "github.com/agent-substrate/substrate/internal/proto/ateworkerpb" + "go.ate.dev/substrate/internal/proto/ateworkerpb" ) func TestDurableFSCheckpointPaths(t *testing.T) { diff --git a/cmd/ateworker-gvisor/fidelity_test.go b/cmd/ateworker-gvisor/fidelity_test.go index e058e45966..3a68ec4a26 100644 --- a/cmd/ateworker-gvisor/fidelity_test.go +++ b/cmd/ateworker-gvisor/fidelity_test.go @@ -23,8 +23,8 @@ import ( "google.golang.org/grpc/codes" - "github.com/agent-substrate/substrate/internal/apierror" - "github.com/agent-substrate/substrate/internal/proto/ateworkerpb" + "go.ate.dev/substrate/internal/apierror" + "go.ate.dev/substrate/internal/proto/ateworkerpb" ) // TestValidateFidelity pins what this runtime serves. gVisor keeps rootfs diff --git a/cmd/ateworker-gvisor/hosted.go b/cmd/ateworker-gvisor/hosted.go index b219f498cd..0761de4ed8 100644 --- a/cmd/ateworker-gvisor/hosted.go +++ b/cmd/ateworker-gvisor/hosted.go @@ -24,12 +24,12 @@ import ( "sync/atomic" "time" - "github.com/agent-substrate/substrate/internal/apierror" - "github.com/agent-substrate/substrate/internal/ateworker/actornet" - "github.com/agent-substrate/substrate/internal/atunnel/ingress" - "github.com/agent-substrate/substrate/internal/proto/ateworkerpb" - "github.com/agent-substrate/substrate/internal/resources" - "github.com/agent-substrate/substrate/internal/workerstats" + "go.ate.dev/substrate/internal/apierror" + "go.ate.dev/substrate/internal/ateworker/actornet" + "go.ate.dev/substrate/internal/atunnel/ingress" + "go.ate.dev/substrate/internal/proto/ateworkerpb" + "go.ate.dev/substrate/internal/resources" + "go.ate.dev/substrate/internal/workerstats" ) // hostedActor holds one actor's attribution, network, and runtime state. diff --git a/cmd/ateworker-gvisor/hosted_test.go b/cmd/ateworker-gvisor/hosted_test.go index b70a76861f..e3aebb6594 100644 --- a/cmd/ateworker-gvisor/hosted_test.go +++ b/cmd/ateworker-gvisor/hosted_test.go @@ -25,9 +25,9 @@ import ( "google.golang.org/grpc/codes" "time" - "github.com/agent-substrate/substrate/internal/actorlock" - "github.com/agent-substrate/substrate/internal/apierror" - "github.com/agent-substrate/substrate/internal/resources" + "go.ate.dev/substrate/internal/actorlock" + "go.ate.dev/substrate/internal/apierror" + "go.ate.dev/substrate/internal/resources" ) func TestAdmitActorEnforcesTheCeilingConcurrently(t *testing.T) { diff --git a/cmd/ateworker-gvisor/internal/cdiinject/inject.go b/cmd/ateworker-gvisor/internal/cdiinject/inject.go index a1306d02de..d21e741067 100644 --- a/cmd/ateworker-gvisor/internal/cdiinject/inject.go +++ b/cmd/ateworker-gvisor/internal/cdiinject/inject.go @@ -35,7 +35,7 @@ import ( specs "github.com/opencontainers/runtime-spec/specs-go" "golang.org/x/sys/unix" - "github.com/agent-substrate/substrate/internal/cdi" + "go.ate.dev/substrate/internal/cdi" ) // Options tune what a merge is allowed to do. Everything vendor-specific lives diff --git a/cmd/ateworker-gvisor/internal/cdiinject/inject_test.go b/cmd/ateworker-gvisor/internal/cdiinject/inject_test.go index d5ca98f302..15457365d1 100644 --- a/cmd/ateworker-gvisor/internal/cdiinject/inject_test.go +++ b/cmd/ateworker-gvisor/internal/cdiinject/inject_test.go @@ -25,7 +25,7 @@ import ( specs "github.com/opencontainers/runtime-spec/specs-go" "golang.org/x/sys/unix" - "github.com/agent-substrate/substrate/internal/cdi" + "go.ate.dev/substrate/internal/cdi" ) // statDev returns a device node's numbers, so the expectation matches whatever diff --git a/cmd/ateworker-gvisor/main.go b/cmd/ateworker-gvisor/main.go index f0cc20cd47..968f8cd665 100644 --- a/cmd/ateworker-gvisor/main.go +++ b/cmd/ateworker-gvisor/main.go @@ -33,31 +33,31 @@ import ( "time" "cloud.google.com/go/compute/metadata" - "github.com/agent-substrate/substrate/cmd/ateworker-gvisor/internal/cgroupstats" - "github.com/agent-substrate/substrate/internal/actorlock" - "github.com/agent-substrate/substrate/internal/actorlog" - "github.com/agent-substrate/substrate/internal/apierror" - "github.com/agent-substrate/substrate/internal/ateinterceptors" - "github.com/agent-substrate/substrate/internal/ateworker/actornet" - "github.com/agent-substrate/substrate/internal/ateworker/cgroup" - "github.com/agent-substrate/substrate/internal/ateworker/phaselog" - "github.com/agent-substrate/substrate/internal/ateworker/pid1" - "github.com/agent-substrate/substrate/internal/ateworker/register" - "github.com/agent-substrate/substrate/internal/ateworker/tunnel" - "github.com/agent-substrate/substrate/internal/imagecache" - "github.com/agent-substrate/substrate/internal/nodepath" - "github.com/agent-substrate/substrate/internal/ocispec" - "github.com/agent-substrate/substrate/internal/otlprelay" - "github.com/agent-substrate/substrate/internal/preview" - "github.com/agent-substrate/substrate/internal/proto/ateworkerpb" - "github.com/agent-substrate/substrate/internal/resources" - "github.com/agent-substrate/substrate/internal/serverboot" - "github.com/agent-substrate/substrate/internal/sizing" - "github.com/agent-substrate/substrate/internal/version" - "github.com/agent-substrate/substrate/internal/wakeupprobe" - "github.com/agent-substrate/substrate/internal/workerstats" - atev1alpha1 "github.com/agent-substrate/substrate/pkg/api/v1alpha1" "github.com/spf13/pflag" + "go.ate.dev/substrate/cmd/ateworker-gvisor/internal/cgroupstats" + "go.ate.dev/substrate/internal/actorlock" + "go.ate.dev/substrate/internal/actorlog" + "go.ate.dev/substrate/internal/apierror" + "go.ate.dev/substrate/internal/ateinterceptors" + "go.ate.dev/substrate/internal/ateworker/actornet" + "go.ate.dev/substrate/internal/ateworker/cgroup" + "go.ate.dev/substrate/internal/ateworker/phaselog" + "go.ate.dev/substrate/internal/ateworker/pid1" + "go.ate.dev/substrate/internal/ateworker/register" + "go.ate.dev/substrate/internal/ateworker/tunnel" + "go.ate.dev/substrate/internal/imagecache" + "go.ate.dev/substrate/internal/nodepath" + "go.ate.dev/substrate/internal/ocispec" + "go.ate.dev/substrate/internal/otlprelay" + "go.ate.dev/substrate/internal/preview" + "go.ate.dev/substrate/internal/proto/ateworkerpb" + "go.ate.dev/substrate/internal/resources" + "go.ate.dev/substrate/internal/serverboot" + "go.ate.dev/substrate/internal/sizing" + "go.ate.dev/substrate/internal/version" + "go.ate.dev/substrate/internal/wakeupprobe" + "go.ate.dev/substrate/internal/workerstats" + atev1alpha1 "go.ate.dev/substrate/pkg/api/v1alpha1" "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc" "google.golang.org/grpc" "google.golang.org/grpc/reflection" diff --git a/cmd/ateworker-gvisor/main_test.go b/cmd/ateworker-gvisor/main_test.go index 8a937d7c40..5061629ca2 100644 --- a/cmd/ateworker-gvisor/main_test.go +++ b/cmd/ateworker-gvisor/main_test.go @@ -30,13 +30,13 @@ import ( "google.golang.org/grpc/codes" - "github.com/agent-substrate/substrate/internal/actorlog" - "github.com/agent-substrate/substrate/internal/apierror" - "github.com/agent-substrate/substrate/internal/ateattr" - "github.com/agent-substrate/substrate/internal/nodepath" - "github.com/agent-substrate/substrate/internal/ocispec" - "github.com/agent-substrate/substrate/internal/proto/ateworkerpb" - "github.com/agent-substrate/substrate/internal/resources" + "go.ate.dev/substrate/internal/actorlog" + "go.ate.dev/substrate/internal/apierror" + "go.ate.dev/substrate/internal/ateattr" + "go.ate.dev/substrate/internal/nodepath" + "go.ate.dev/substrate/internal/ocispec" + "go.ate.dev/substrate/internal/proto/ateworkerpb" + "go.ate.dev/substrate/internal/resources" ) // Every RPC rejects a request without ActorDirs before touching any state. diff --git a/cmd/ateworker-gvisor/phaselog.go b/cmd/ateworker-gvisor/phaselog.go index c7190d23dd..7d390bc1c3 100644 --- a/cmd/ateworker-gvisor/phaselog.go +++ b/cmd/ateworker-gvisor/phaselog.go @@ -19,8 +19,8 @@ package main import ( "time" - "github.com/agent-substrate/substrate/internal/ateattr" - "github.com/agent-substrate/substrate/internal/ateworker/phaselog" + "go.ate.dev/substrate/internal/ateattr" + "go.ate.dev/substrate/internal/ateworker/phaselog" ) // The phase names, suffixed onto the phaselog duration keys. Kept out of diff --git a/cmd/ateworker-gvisor/phaselog_test.go b/cmd/ateworker-gvisor/phaselog_test.go index 55d750159c..e647c4c26a 100644 --- a/cmd/ateworker-gvisor/phaselog_test.go +++ b/cmd/ateworker-gvisor/phaselog_test.go @@ -24,9 +24,9 @@ import ( "testing" "time" - "github.com/agent-substrate/substrate/internal/ateworker/phaselog" - "github.com/agent-substrate/substrate/internal/proto/ateworkerpb" - "github.com/agent-substrate/substrate/internal/resources" + "go.ate.dev/substrate/internal/ateworker/phaselog" + "go.ate.dev/substrate/internal/proto/ateworkerpb" + "go.ate.dev/substrate/internal/resources" ) // renderPhaseRecord logs attrs through a JSON handler, so the test checks the diff --git a/cmd/ateworker-gvisor/runsc.go b/cmd/ateworker-gvisor/runsc.go index 9157e65d20..7d53c37082 100644 --- a/cmd/ateworker-gvisor/runsc.go +++ b/cmd/ateworker-gvisor/runsc.go @@ -28,10 +28,10 @@ import ( "slices" "strings" - "github.com/agent-substrate/substrate/internal/ocispec" - "github.com/agent-substrate/substrate/internal/proto/ateworkerpb" - "github.com/agent-substrate/substrate/internal/resources" - "github.com/agent-substrate/substrate/internal/sizing" + "go.ate.dev/substrate/internal/ocispec" + "go.ate.dev/substrate/internal/proto/ateworkerpb" + "go.ate.dev/substrate/internal/resources" + "go.ate.dev/substrate/internal/sizing" ) type runsc struct { diff --git a/cmd/ateworker-gvisor/runsc_test.go b/cmd/ateworker-gvisor/runsc_test.go index 057cf7cb1f..553c7de701 100644 --- a/cmd/ateworker-gvisor/runsc_test.go +++ b/cmd/ateworker-gvisor/runsc_test.go @@ -28,10 +28,10 @@ import ( "github.com/opencontainers/runtime-spec/specs-go" "google.golang.org/grpc/codes" - "github.com/agent-substrate/substrate/internal/actorlock" - "github.com/agent-substrate/substrate/internal/apierror" - "github.com/agent-substrate/substrate/internal/ocispec" - "github.com/agent-substrate/substrate/internal/proto/ateworkerpb" + "go.ate.dev/substrate/internal/actorlock" + "go.ate.dev/substrate/internal/apierror" + "go.ate.dev/substrate/internal/ocispec" + "go.ate.dev/substrate/internal/proto/ateworkerpb" ) var testActorDirs = &ateworkerpb.ActorDirs{ diff --git a/cmd/ateworker-gvisor/sandboxexit.go b/cmd/ateworker-gvisor/sandboxexit.go index 1694a06b87..109e0cf221 100644 --- a/cmd/ateworker-gvisor/sandboxexit.go +++ b/cmd/ateworker-gvisor/sandboxexit.go @@ -25,8 +25,8 @@ import ( "golang.org/x/sys/unix" - "github.com/agent-substrate/substrate/internal/ocispec" - "github.com/agent-substrate/substrate/internal/proto/ateworkerpb" + "go.ate.dev/substrate/internal/ocispec" + "go.ate.dev/substrate/internal/proto/ateworkerpb" ) // sandboxExitTimeout bounds the wait for a checkpointed sandbox to go away. diff --git a/cmd/ateworker-gvisor/sandboxexit_test.go b/cmd/ateworker-gvisor/sandboxexit_test.go index 2ae50e2aa6..c7b477d8f6 100644 --- a/cmd/ateworker-gvisor/sandboxexit_test.go +++ b/cmd/ateworker-gvisor/sandboxexit_test.go @@ -24,8 +24,8 @@ import ( "testing" "time" - "github.com/agent-substrate/substrate/internal/ocispec" - "github.com/agent-substrate/substrate/internal/proto/ateworkerpb" + "go.ate.dev/substrate/internal/ocispec" + "go.ate.dev/substrate/internal/proto/ateworkerpb" ) // startFakeSandbox starts a process that runs until killed and records it as diff --git a/cmd/ateworker-gvisor/sandboxnet.go b/cmd/ateworker-gvisor/sandboxnet.go index 36d6b8f962..d1b368de54 100644 --- a/cmd/ateworker-gvisor/sandboxnet.go +++ b/cmd/ateworker-gvisor/sandboxnet.go @@ -25,8 +25,8 @@ import ( "os" "path/filepath" - "github.com/agent-substrate/substrate/internal/ateworker/actornet" - "github.com/agent-substrate/substrate/internal/proto/ateworkerpb" + "go.ate.dev/substrate/internal/ateworker/actornet" + "go.ate.dev/substrate/internal/proto/ateworkerpb" ) // actorResolvConf writes the resolver bind source outside the actor's rootfs. diff --git a/cmd/ateworker-gvisor/stats.go b/cmd/ateworker-gvisor/stats.go index 54ad824491..5a2a71b7ab 100644 --- a/cmd/ateworker-gvisor/stats.go +++ b/cmd/ateworker-gvisor/stats.go @@ -24,13 +24,13 @@ import ( "path/filepath" "time" - "github.com/agent-substrate/substrate/cmd/ateworker-gvisor/internal/cgroupstats" - "github.com/agent-substrate/substrate/internal/apierror" - "github.com/agent-substrate/substrate/internal/ateattr" - "github.com/agent-substrate/substrate/internal/ocispec" - "github.com/agent-substrate/substrate/internal/proto/ateworkerpb" - "github.com/agent-substrate/substrate/internal/resources" - atev1alpha1 "github.com/agent-substrate/substrate/pkg/api/v1alpha1" + "go.ate.dev/substrate/cmd/ateworker-gvisor/internal/cgroupstats" + "go.ate.dev/substrate/internal/apierror" + "go.ate.dev/substrate/internal/ateattr" + "go.ate.dev/substrate/internal/ocispec" + "go.ate.dev/substrate/internal/proto/ateworkerpb" + "go.ate.dev/substrate/internal/resources" + atev1alpha1 "go.ate.dev/substrate/pkg/api/v1alpha1" ) // defaultCgroupRoot is the worker pod's own cgroup scope. The worker runs in a diff --git a/cmd/ateworker-gvisor/stats_test.go b/cmd/ateworker-gvisor/stats_test.go index e03ca0c564..2b75483651 100644 --- a/cmd/ateworker-gvisor/stats_test.go +++ b/cmd/ateworker-gvisor/stats_test.go @@ -19,7 +19,7 @@ package main import ( "bytes" "context" - "github.com/agent-substrate/substrate/internal/actorlock" + "go.ate.dev/substrate/internal/actorlock" "log/slog" "os" "path/filepath" @@ -32,12 +32,12 @@ import ( "google.golang.org/grpc/codes" "google.golang.org/protobuf/testing/protocmp" - "github.com/agent-substrate/substrate/cmd/ateworker-gvisor/internal/cgroupstats" - "github.com/agent-substrate/substrate/internal/apierror" - "github.com/agent-substrate/substrate/internal/ocispec" - "github.com/agent-substrate/substrate/internal/proto/ateworkerpb" - "github.com/agent-substrate/substrate/internal/resources" - "github.com/agent-substrate/substrate/internal/workerstats" + "go.ate.dev/substrate/cmd/ateworker-gvisor/internal/cgroupstats" + "go.ate.dev/substrate/internal/apierror" + "go.ate.dev/substrate/internal/ocispec" + "go.ate.dev/substrate/internal/proto/ateworkerpb" + "go.ate.dev/substrate/internal/resources" + "go.ate.dev/substrate/internal/workerstats" ) // The lifecycle RPCs that host and unhost actors need netlink, runsc, and diff --git a/cmd/ateworker-gvisor/usage_test.go b/cmd/ateworker-gvisor/usage_test.go index e7f8d997f8..f9b059086e 100644 --- a/cmd/ateworker-gvisor/usage_test.go +++ b/cmd/ateworker-gvisor/usage_test.go @@ -24,12 +24,12 @@ import ( "testing" "time" - "github.com/agent-substrate/substrate/internal/actorlock" - "github.com/agent-substrate/substrate/internal/ateattr" - "github.com/agent-substrate/substrate/internal/ocispec" - "github.com/agent-substrate/substrate/internal/proto/ateworkerpb" - "github.com/agent-substrate/substrate/internal/workerstats" - "github.com/agent-substrate/substrate/internal/workerstats/workerstatstest" + "go.ate.dev/substrate/internal/actorlock" + "go.ate.dev/substrate/internal/ateattr" + "go.ate.dev/substrate/internal/ocispec" + "go.ate.dev/substrate/internal/proto/ateworkerpb" + "go.ate.dev/substrate/internal/workerstats" + "go.ate.dev/substrate/internal/workerstats/workerstatstest" ) func withUsageRecorder(s *AteWorkerService) *workerstatstest.Recorder { diff --git a/cmd/ateworker-microvm/actorpath.go b/cmd/ateworker-microvm/actorpath.go index 4d99fe30bb..a5f9c5c4b3 100644 --- a/cmd/ateworker-microvm/actorpath.go +++ b/cmd/ateworker-microvm/actorpath.go @@ -22,7 +22,7 @@ package main import ( "path/filepath" - "github.com/agent-substrate/substrate/internal/proto/ateworkerpb" + "go.ate.dev/substrate/internal/proto/ateworkerpb" ) // ociBundlePath is the container's OCI bundle. diff --git a/cmd/ateworker-microvm/checkpoint.go b/cmd/ateworker-microvm/checkpoint.go index 394a9dee5f..b4e80fa60f 100644 --- a/cmd/ateworker-microvm/checkpoint.go +++ b/cmd/ateworker-microvm/checkpoint.go @@ -25,15 +25,15 @@ import ( "path/filepath" "time" - "github.com/agent-substrate/substrate/internal/apierror" - "github.com/agent-substrate/substrate/internal/resources" - - "github.com/agent-substrate/substrate/cmd/ateworker-microvm/internal/ch" - "github.com/agent-substrate/substrate/cmd/ateworker-microvm/internal/kata" - "github.com/agent-substrate/substrate/internal/ateworker/phaselog" - "github.com/agent-substrate/substrate/internal/imagecache" - "github.com/agent-substrate/substrate/internal/proto/ateworkerpb" - "github.com/agent-substrate/substrate/internal/workerstats" + "go.ate.dev/substrate/internal/apierror" + "go.ate.dev/substrate/internal/resources" + + "go.ate.dev/substrate/cmd/ateworker-microvm/internal/ch" + "go.ate.dev/substrate/cmd/ateworker-microvm/internal/kata" + "go.ate.dev/substrate/internal/ateworker/phaselog" + "go.ate.dev/substrate/internal/imagecache" + "go.ate.dev/substrate/internal/proto/ateworkerpb" + "go.ate.dev/substrate/internal/workerstats" "golang.org/x/sync/errgroup" ) diff --git a/cmd/ateworker-microvm/csi.go b/cmd/ateworker-microvm/csi.go index 154d4928f9..6417ce0ad2 100644 --- a/cmd/ateworker-microvm/csi.go +++ b/cmd/ateworker-microvm/csi.go @@ -21,9 +21,9 @@ import ( "fmt" "os" - "github.com/agent-substrate/substrate/cmd/ateworker-microvm/internal/kata" - "github.com/agent-substrate/substrate/internal/ocispec" - "github.com/agent-substrate/substrate/internal/proto/ateworkerpb" + "go.ate.dev/substrate/cmd/ateworker-microvm/internal/kata" + "go.ate.dev/substrate/internal/ocispec" + "go.ate.dev/substrate/internal/proto/ateworkerpb" ) // hasCsiVolumes reports whether any container mounts a CSI volume. diff --git a/cmd/ateworker-microvm/csi_test.go b/cmd/ateworker-microvm/csi_test.go index aaa2c9bb02..f03cff8d5d 100644 --- a/cmd/ateworker-microvm/csi_test.go +++ b/cmd/ateworker-microvm/csi_test.go @@ -19,7 +19,7 @@ package main import ( "testing" - "github.com/agent-substrate/substrate/internal/proto/ateworkerpb" + "go.ate.dev/substrate/internal/proto/ateworkerpb" ) func TestHasCsiVolumes(t *testing.T) { diff --git a/cmd/ateworker-microvm/durable.go b/cmd/ateworker-microvm/durable.go index 830ac75a77..915ccc669d 100644 --- a/cmd/ateworker-microvm/durable.go +++ b/cmd/ateworker-microvm/durable.go @@ -45,10 +45,10 @@ import ( "slices" "strings" - "github.com/agent-substrate/substrate/cmd/ateworker-microvm/internal/kata" - "github.com/agent-substrate/substrate/internal/ocispec" - "github.com/agent-substrate/substrate/internal/proto/ateworkerpb" - "github.com/agent-substrate/substrate/internal/tarutil" + "go.ate.dev/substrate/cmd/ateworker-microvm/internal/kata" + "go.ate.dev/substrate/internal/ocispec" + "go.ate.dev/substrate/internal/proto/ateworkerpb" + "go.ate.dev/substrate/internal/tarutil" ) // durableTarFile is the snapshot file holding the tar of one durable-dir diff --git a/cmd/ateworker-microvm/durable_test.go b/cmd/ateworker-microvm/durable_test.go index 64d01ca1c5..ca848f9267 100644 --- a/cmd/ateworker-microvm/durable_test.go +++ b/cmd/ateworker-microvm/durable_test.go @@ -22,8 +22,8 @@ import ( "slices" "testing" - "github.com/agent-substrate/substrate/internal/proto/ateworkerpb" - "github.com/agent-substrate/substrate/internal/tarutil" + "go.ate.dev/substrate/internal/proto/ateworkerpb" + "go.ate.dev/substrate/internal/tarutil" ) func TestHasDurableVolumes(t *testing.T) { diff --git a/cmd/ateworker-microvm/fidelity.go b/cmd/ateworker-microvm/fidelity.go index de9a7049f7..244ce99f79 100644 --- a/cmd/ateworker-microvm/fidelity.go +++ b/cmd/ateworker-microvm/fidelity.go @@ -16,7 +16,7 @@ package main -import "github.com/agent-substrate/substrate/internal/proto/ateworkerpb" +import "go.ate.dev/substrate/internal/proto/ateworkerpb" // The fidelity ladder, lowest to highest, each level including the ones // below it: VOLUMES holds the durable volumes, ROOTFS adds each container's diff --git a/cmd/ateworker-microvm/fidelity_test.go b/cmd/ateworker-microvm/fidelity_test.go index 4634041d14..4c9eee1408 100644 --- a/cmd/ateworker-microvm/fidelity_test.go +++ b/cmd/ateworker-microvm/fidelity_test.go @@ -22,8 +22,8 @@ import ( "google.golang.org/grpc/codes" - "github.com/agent-substrate/substrate/internal/apierror" - "github.com/agent-substrate/substrate/internal/proto/ateworkerpb" + "go.ate.dev/substrate/internal/apierror" + "go.ate.dev/substrate/internal/proto/ateworkerpb" ) // TestFidelityLadder pins which layers each level carries: the checkpoint's diff --git a/cmd/ateworker-microvm/hosted.go b/cmd/ateworker-microvm/hosted.go index c6cb501584..61978a513e 100644 --- a/cmd/ateworker-microvm/hosted.go +++ b/cmd/ateworker-microvm/hosted.go @@ -23,15 +23,15 @@ import ( "net" "time" - "github.com/agent-substrate/substrate/cmd/ateworker-microvm/internal/kata" - "github.com/agent-substrate/substrate/internal/apierror" - "github.com/agent-substrate/substrate/internal/ateworker/actornet" - "github.com/agent-substrate/substrate/internal/ateworker/cgroup" - "github.com/agent-substrate/substrate/internal/atunnel/ingress" - "github.com/agent-substrate/substrate/internal/netns" - "github.com/agent-substrate/substrate/internal/resources" - "github.com/agent-substrate/substrate/internal/sizing" - "github.com/agent-substrate/substrate/internal/workerstats" + "go.ate.dev/substrate/cmd/ateworker-microvm/internal/kata" + "go.ate.dev/substrate/internal/apierror" + "go.ate.dev/substrate/internal/ateworker/actornet" + "go.ate.dev/substrate/internal/ateworker/cgroup" + "go.ate.dev/substrate/internal/atunnel/ingress" + "go.ate.dev/substrate/internal/netns" + "go.ate.dev/substrate/internal/resources" + "go.ate.dev/substrate/internal/sizing" + "go.ate.dev/substrate/internal/workerstats" ) // hostedActor holds one actor's attribution, network, and runtime state. diff --git a/cmd/ateworker-microvm/hosted_test.go b/cmd/ateworker-microvm/hosted_test.go index 9c60d5cff1..1ce50ec51b 100644 --- a/cmd/ateworker-microvm/hosted_test.go +++ b/cmd/ateworker-microvm/hosted_test.go @@ -21,8 +21,8 @@ import ( "google.golang.org/grpc/codes" - "github.com/agent-substrate/substrate/internal/apierror" - "github.com/agent-substrate/substrate/internal/resources" + "go.ate.dev/substrate/internal/apierror" + "go.ate.dev/substrate/internal/resources" ) // Re-admitting an actor that is already hosted keeps its slot, so it succeeds diff --git a/cmd/ateworker-microvm/internal/agentstats/agentstats.go b/cmd/ateworker-microvm/internal/agentstats/agentstats.go index ad9b83ed67..95b0f01c43 100644 --- a/cmd/ateworker-microvm/internal/agentstats/agentstats.go +++ b/cmd/ateworker-microvm/internal/agentstats/agentstats.go @@ -29,7 +29,7 @@ package agentstats import ( - "github.com/agent-substrate/substrate/cmd/ateworker-microvm/internal/third_party/kata/agentpb" + "go.ate.dev/substrate/cmd/ateworker-microvm/internal/third_party/kata/agentpb" ) // Sample is a point-in-time reading for one container, or the sum of several. diff --git a/cmd/ateworker-microvm/internal/agentstats/agentstats_test.go b/cmd/ateworker-microvm/internal/agentstats/agentstats_test.go index 3bcc79bc48..69f61e39ad 100644 --- a/cmd/ateworker-microvm/internal/agentstats/agentstats_test.go +++ b/cmd/ateworker-microvm/internal/agentstats/agentstats_test.go @@ -19,7 +19,7 @@ import ( "github.com/google/go-cmp/cmp" - "github.com/agent-substrate/substrate/cmd/ateworker-microvm/internal/third_party/kata/agentpb" + "go.ate.dev/substrate/cmd/ateworker-microvm/internal/third_party/kata/agentpb" ) // cgroupStats builds an agent reading: usage/max bytes, the memory.stat entries diff --git a/cmd/ateworker-microvm/internal/kata/agentclient.go b/cmd/ateworker-microvm/internal/kata/agentclient.go index ff39a0c991..4c00311fe0 100644 --- a/cmd/ateworker-microvm/internal/kata/agentclient.go +++ b/cmd/ateworker-microvm/internal/kata/agentclient.go @@ -25,8 +25,8 @@ import ( "strings" "time" - "github.com/agent-substrate/substrate/cmd/ateworker-microvm/internal/third_party/kata/agentpb" "github.com/containerd/ttrpc" + "go.ate.dev/substrate/cmd/ateworker-microvm/internal/third_party/kata/agentpb" "google.golang.org/protobuf/types/known/emptypb" ) diff --git a/cmd/ateworker-microvm/internal/kata/agentclient_linux_test.go b/cmd/ateworker-microvm/internal/kata/agentclient_linux_test.go index a27b40b397..d190b0175a 100644 --- a/cmd/ateworker-microvm/internal/kata/agentclient_linux_test.go +++ b/cmd/ateworker-microvm/internal/kata/agentclient_linux_test.go @@ -25,8 +25,8 @@ import ( "testing" "time" - "github.com/agent-substrate/substrate/cmd/ateworker-microvm/internal/third_party/kata/agentpb" "github.com/containerd/ttrpc" + "go.ate.dev/substrate/cmd/ateworker-microvm/internal/third_party/kata/agentpb" "google.golang.org/protobuf/types/known/emptypb" ) diff --git a/cmd/ateworker-microvm/internal/kata/cleanup_linux.go b/cmd/ateworker-microvm/internal/kata/cleanup_linux.go index 6f7fd6ae5c..0c2634275c 100644 --- a/cmd/ateworker-microvm/internal/kata/cleanup_linux.go +++ b/cmd/ateworker-microvm/internal/kata/cleanup_linux.go @@ -27,7 +27,7 @@ import ( "golang.org/x/sys/unix" - "github.com/agent-substrate/substrate/internal/nodepath" + "go.ate.dev/substrate/internal/nodepath" ) // CleanupSandboxState removes leftover host-side state for a sandbox id (the diff --git a/cmd/ateworker-microvm/internal/kata/cleanup_linux_test.go b/cmd/ateworker-microvm/internal/kata/cleanup_linux_test.go index 46220baf84..8180307ce8 100644 --- a/cmd/ateworker-microvm/internal/kata/cleanup_linux_test.go +++ b/cmd/ateworker-microvm/internal/kata/cleanup_linux_test.go @@ -20,7 +20,7 @@ import ( "strings" "testing" - "github.com/agent-substrate/substrate/internal/nodepath" + "go.ate.dev/substrate/internal/nodepath" ) func TestIsSandboxProcess(t *testing.T) { diff --git a/cmd/ateworker-microvm/internal/kata/overlay_linux.go b/cmd/ateworker-microvm/internal/kata/overlay_linux.go index 9e06d03e4b..d989644ba8 100644 --- a/cmd/ateworker-microvm/internal/kata/overlay_linux.go +++ b/cmd/ateworker-microvm/internal/kata/overlay_linux.go @@ -38,9 +38,9 @@ import ( "syscall" "time" - "github.com/agent-substrate/substrate/cmd/ateworker-microvm/internal/third_party/kata/agentpb" - "github.com/agent-substrate/substrate/internal/ocispec" specs "github.com/opencontainers/runtime-spec/specs-go" + "go.ate.dev/substrate/cmd/ateworker-microvm/internal/third_party/kata/agentpb" + "go.ate.dev/substrate/internal/ocispec" "golang.org/x/sys/unix" ) diff --git a/cmd/ateworker-microvm/internal/kata/overlay_linux_test.go b/cmd/ateworker-microvm/internal/kata/overlay_linux_test.go index be491697ce..fb9ca8269f 100644 --- a/cmd/ateworker-microvm/internal/kata/overlay_linux_test.go +++ b/cmd/ateworker-microvm/internal/kata/overlay_linux_test.go @@ -25,7 +25,7 @@ import ( "syscall" "testing" - "github.com/agent-substrate/substrate/internal/roottest" + "go.ate.dev/substrate/internal/roottest" "golang.org/x/sys/unix" ) diff --git a/cmd/ateworker-microvm/internal/kata/specconv.go b/cmd/ateworker-microvm/internal/kata/specconv.go index 87f0f2194c..5f143bb8a6 100644 --- a/cmd/ateworker-microvm/internal/kata/specconv.go +++ b/cmd/ateworker-microvm/internal/kata/specconv.go @@ -17,8 +17,8 @@ package kata import ( - "github.com/agent-substrate/substrate/cmd/ateworker-microvm/internal/third_party/kata/agentpb" specs "github.com/opencontainers/runtime-spec/specs-go" + "go.ate.dev/substrate/cmd/ateworker-microvm/internal/third_party/kata/agentpb" ) // DefaultCPUPeriodUS is the CFS period assumed when a spec carries a quota but diff --git a/cmd/ateworker-microvm/internal/third_party/kata/PROVENANCE.md b/cmd/ateworker-microvm/internal/third_party/kata/PROVENANCE.md index e9d8cdbd06..b884f7f647 100644 --- a/cmd/ateworker-microvm/internal/third_party/kata/PROVENANCE.md +++ b/cmd/ateworker-microvm/internal/third_party/kata/PROVENANCE.md @@ -50,5 +50,5 @@ ateworker drives a small subset of `AgentService`: 1. Check out the matching kata-containers tag and copy `src/libs/protocols/protos/{agent,oci,types,csi}.proto` into `agentpb/`. 2. Set each `option go_package` to - `github.com/agent-substrate/substrate/cmd/ateworker-microvm/internal/third_party/kata/agentpb;agentpb`. + `go.ate.dev/substrate/cmd/ateworker-microvm/internal/third_party/kata/agentpb;agentpb`. 3. Regenerate with `protoc --go_out=...` (protoc-gen-go), matching the versions above. diff --git a/cmd/ateworker-microvm/internal/third_party/kata/agentpb/agent.pb.go b/cmd/ateworker-microvm/internal/third_party/kata/agentpb/agent.pb.go index d1d5e570b4..ff93089850 100644 --- a/cmd/ateworker-microvm/internal/third_party/kata/agentpb/agent.pb.go +++ b/cmd/ateworker-microvm/internal/third_party/kata/agentpb/agent.pb.go @@ -4694,7 +4694,7 @@ const file_agent_proto_rawDesc = "" + "\vAddSwapPath\x12\x18.grpc.AddSwapPathRequest\x1a\x16.google.protobuf.Empty\x12E\n" + "\x0eGetVolumeStats\x12\x18.grpc.VolumeStatsRequest\x1a\x19.grpc.VolumeStatsResponse\x12A\n" + "\fResizeVolume\x12\x19.grpc.ResizeVolumeRequest\x1a\x16.google.protobuf.Empty\x12;\n" + - "\tSetPolicy\x12\x16.grpc.SetPolicyRequest\x1a\x16.google.protobuf.EmptyBfZdgithub.com/agent-substrate/substrate/cmd/ateworker-microvm/internal/third_party/kata/agentpb;agentpbb\x06proto3" + "\tSetPolicy\x12\x16.grpc.SetPolicyRequest\x1a\x16.google.protobuf.EmptyBVZTgo.ate.dev/substrate/cmd/ateworker-microvm/internal/third_party/kata/agentpb;agentpbb\x06proto3" var ( file_agent_proto_rawDescOnce sync.Once diff --git a/cmd/ateworker-microvm/internal/third_party/kata/agentpb/agent.proto b/cmd/ateworker-microvm/internal/third_party/kata/agentpb/agent.proto index eff52661e4..36bf01e91f 100644 --- a/cmd/ateworker-microvm/internal/third_party/kata/agentpb/agent.proto +++ b/cmd/ateworker-microvm/internal/third_party/kata/agentpb/agent.proto @@ -7,7 +7,7 @@ syntax = "proto3"; -option go_package = "github.com/agent-substrate/substrate/cmd/ateworker-microvm/internal/third_party/kata/agentpb;agentpb"; +option go_package = "go.ate.dev/substrate/cmd/ateworker-microvm/internal/third_party/kata/agentpb;agentpb"; package grpc; diff --git a/cmd/ateworker-microvm/internal/third_party/kata/agentpb/csi.pb.go b/cmd/ateworker-microvm/internal/third_party/kata/agentpb/csi.pb.go index d68026183e..3ff51b5961 100644 --- a/cmd/ateworker-microvm/internal/third_party/kata/agentpb/csi.pb.go +++ b/cmd/ateworker-microvm/internal/third_party/kata/agentpb/csi.pb.go @@ -286,7 +286,7 @@ const file_csi_proto_rawDesc = "" + "\x06INODES\x10\x02\"G\n" + "\x0fVolumeCondition\x12\x1a\n" + "\babnormal\x18\x01 \x01(\bR\babnormal\x12\x18\n" + - "\amessage\x18\x02 \x01(\tR\amessageBfZdgithub.com/agent-substrate/substrate/cmd/ateworker-microvm/internal/third_party/kata/agentpb;agentpbb\x06proto3" + "\amessage\x18\x02 \x01(\tR\amessageBVZTgo.ate.dev/substrate/cmd/ateworker-microvm/internal/third_party/kata/agentpb;agentpbb\x06proto3" var ( file_csi_proto_rawDescOnce sync.Once diff --git a/cmd/ateworker-microvm/internal/third_party/kata/agentpb/csi.proto b/cmd/ateworker-microvm/internal/third_party/kata/agentpb/csi.proto index 21d2f89c59..5978a713de 100644 --- a/cmd/ateworker-microvm/internal/third_party/kata/agentpb/csi.proto +++ b/cmd/ateworker-microvm/internal/third_party/kata/agentpb/csi.proto @@ -4,7 +4,7 @@ // syntax = "proto3"; -option go_package = "github.com/agent-substrate/substrate/cmd/ateworker-microvm/internal/third_party/kata/agentpb;agentpb"; +option go_package = "go.ate.dev/substrate/cmd/ateworker-microvm/internal/third_party/kata/agentpb;agentpb"; package grpc; diff --git a/cmd/ateworker-microvm/internal/third_party/kata/agentpb/oci.pb.go b/cmd/ateworker-microvm/internal/third_party/kata/agentpb/oci.pb.go index 7a704e0855..eeb482c3a1 100644 --- a/cmd/ateworker-microvm/internal/third_party/kata/agentpb/oci.pb.go +++ b/cmd/ateworker-microvm/internal/third_party/kata/agentpb/oci.pb.go @@ -2618,7 +2618,7 @@ const file_oci_proto_rawDesc = "" + "\n" + "\bErrnoRet\"5\n" + "\rLinuxIntelRdt\x12$\n" + - "\rL3CacheSchema\x18\x01 \x01(\tR\rL3CacheSchemaBfZdgithub.com/agent-substrate/substrate/cmd/ateworker-microvm/internal/third_party/kata/agentpb;agentpbb\x06proto3" + "\rL3CacheSchema\x18\x01 \x01(\tR\rL3CacheSchemaBVZTgo.ate.dev/substrate/cmd/ateworker-microvm/internal/third_party/kata/agentpb;agentpbb\x06proto3" var ( file_oci_proto_rawDescOnce sync.Once diff --git a/cmd/ateworker-microvm/internal/third_party/kata/agentpb/oci.proto b/cmd/ateworker-microvm/internal/third_party/kata/agentpb/oci.proto index f28cae63b0..84d70a7bde 100644 --- a/cmd/ateworker-microvm/internal/third_party/kata/agentpb/oci.proto +++ b/cmd/ateworker-microvm/internal/third_party/kata/agentpb/oci.proto @@ -6,7 +6,7 @@ // syntax = "proto3"; -option go_package = "github.com/agent-substrate/substrate/cmd/ateworker-microvm/internal/third_party/kata/agentpb;agentpb"; +option go_package = "go.ate.dev/substrate/cmd/ateworker-microvm/internal/third_party/kata/agentpb;agentpb"; package grpc; diff --git a/cmd/ateworker-microvm/internal/third_party/kata/agentpb/types.pb.go b/cmd/ateworker-microvm/internal/third_party/kata/agentpb/types.pb.go index 1a600ed555..0eee61126a 100644 --- a/cmd/ateworker-microvm/internal/third_party/kata/agentpb/types.pb.go +++ b/cmd/ateworker-microvm/internal/third_party/kata/agentpb/types.pb.go @@ -508,7 +508,7 @@ const file_types_proto_rawDesc = "" + "\x13FSGroupChangePolicy\x12\n" + "\n" + "\x06Always\x10\x00\x12\x12\n" + - "\x0eOnRootMismatch\x10\x01BfZdgithub.com/agent-substrate/substrate/cmd/ateworker-microvm/internal/third_party/kata/agentpb;agentpbb\x06proto3" + "\x0eOnRootMismatch\x10\x01BVZTgo.ate.dev/substrate/cmd/ateworker-microvm/internal/third_party/kata/agentpb;agentpbb\x06proto3" var ( file_types_proto_rawDescOnce sync.Once diff --git a/cmd/ateworker-microvm/internal/third_party/kata/agentpb/types.proto b/cmd/ateworker-microvm/internal/third_party/kata/agentpb/types.proto index 62c08fe7c2..baa8e5e773 100644 --- a/cmd/ateworker-microvm/internal/third_party/kata/agentpb/types.proto +++ b/cmd/ateworker-microvm/internal/third_party/kata/agentpb/types.proto @@ -7,7 +7,7 @@ syntax = "proto3"; -option go_package = "github.com/agent-substrate/substrate/cmd/ateworker-microvm/internal/third_party/kata/agentpb;agentpb"; +option go_package = "go.ate.dev/substrate/cmd/ateworker-microvm/internal/third_party/kata/agentpb;agentpb"; package types; diff --git a/cmd/ateworker-microvm/main.go b/cmd/ateworker-microvm/main.go index b577fc05d2..27d5b4cd53 100644 --- a/cmd/ateworker-microvm/main.go +++ b/cmd/ateworker-microvm/main.go @@ -37,25 +37,25 @@ import ( "time" "cloud.google.com/go/compute/metadata" - "github.com/agent-substrate/substrate/internal/actorlock" - "github.com/agent-substrate/substrate/internal/actorlog" - "github.com/agent-substrate/substrate/internal/apierror" - "github.com/agent-substrate/substrate/internal/ateinterceptors" - "github.com/agent-substrate/substrate/internal/ateworker/actornet" - "github.com/agent-substrate/substrate/internal/ateworker/cgroup" - "github.com/agent-substrate/substrate/internal/ateworker/pid1" - "github.com/agent-substrate/substrate/internal/ateworker/register" - "github.com/agent-substrate/substrate/internal/ateworker/tunnel" - "github.com/agent-substrate/substrate/internal/nodepath" - "github.com/agent-substrate/substrate/internal/otlprelay" - "github.com/agent-substrate/substrate/internal/preview" - "github.com/agent-substrate/substrate/internal/proto/ateworkerpb" - "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/workerstats" - atev1alpha1 "github.com/agent-substrate/substrate/pkg/api/v1alpha1" "github.com/spf13/pflag" + "go.ate.dev/substrate/internal/actorlock" + "go.ate.dev/substrate/internal/actorlog" + "go.ate.dev/substrate/internal/apierror" + "go.ate.dev/substrate/internal/ateinterceptors" + "go.ate.dev/substrate/internal/ateworker/actornet" + "go.ate.dev/substrate/internal/ateworker/cgroup" + "go.ate.dev/substrate/internal/ateworker/pid1" + "go.ate.dev/substrate/internal/ateworker/register" + "go.ate.dev/substrate/internal/ateworker/tunnel" + "go.ate.dev/substrate/internal/nodepath" + "go.ate.dev/substrate/internal/otlprelay" + "go.ate.dev/substrate/internal/preview" + "go.ate.dev/substrate/internal/proto/ateworkerpb" + "go.ate.dev/substrate/internal/resources" + "go.ate.dev/substrate/internal/serverboot" + "go.ate.dev/substrate/internal/version" + "go.ate.dev/substrate/internal/workerstats" + atev1alpha1 "go.ate.dev/substrate/pkg/api/v1alpha1" "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc" "golang.org/x/sys/unix" "google.golang.org/grpc" diff --git a/cmd/ateworker-microvm/net.go b/cmd/ateworker-microvm/net.go index ef4703492e..2e177a92fa 100644 --- a/cmd/ateworker-microvm/net.go +++ b/cmd/ateworker-microvm/net.go @@ -24,8 +24,8 @@ import ( "github.com/vishvananda/netlink" - "github.com/agent-substrate/substrate/internal/ateworker/actornet" - "github.com/agent-substrate/substrate/internal/netns" + "go.ate.dev/substrate/internal/ateworker/actornet" + "go.ate.dev/substrate/internal/netns" ) const ( diff --git a/cmd/ateworker-microvm/net_linux_test.go b/cmd/ateworker-microvm/net_linux_test.go index 46ec52695c..7831a7cd99 100644 --- a/cmd/ateworker-microvm/net_linux_test.go +++ b/cmd/ateworker-microvm/net_linux_test.go @@ -22,14 +22,14 @@ import ( "github.com/vishvananda/netlink" - "github.com/agent-substrate/substrate/internal/ateworker/actornet" - "github.com/agent-substrate/substrate/internal/ateworker/tunnel" - "github.com/agent-substrate/substrate/internal/atunnel/egress" - "github.com/agent-substrate/substrate/internal/linuxsys" - "github.com/agent-substrate/substrate/internal/netns" - "github.com/agent-substrate/substrate/internal/nodepath" - "github.com/agent-substrate/substrate/internal/resources" - "github.com/agent-substrate/substrate/internal/roottest" + "go.ate.dev/substrate/internal/ateworker/actornet" + "go.ate.dev/substrate/internal/ateworker/tunnel" + "go.ate.dev/substrate/internal/atunnel/egress" + "go.ate.dev/substrate/internal/linuxsys" + "go.ate.dev/substrate/internal/netns" + "go.ate.dev/substrate/internal/nodepath" + "go.ate.dev/substrate/internal/resources" + "go.ate.dev/substrate/internal/roottest" ) // Hosting an actor that is already hosted replaces its network rather than diff --git a/cmd/ateworker-microvm/phaselog.go b/cmd/ateworker-microvm/phaselog.go index f277b26cd5..e895371160 100644 --- a/cmd/ateworker-microvm/phaselog.go +++ b/cmd/ateworker-microvm/phaselog.go @@ -16,7 +16,7 @@ package main -import "github.com/agent-substrate/substrate/internal/ateattr" +import "go.ate.dev/substrate/internal/ateattr" // The phase names, suffixed onto the phaselog duration keys. Kept out of // ateattr on purpose: that package's SnapshotPhase* values are the diff --git a/cmd/ateworker-microvm/restore.go b/cmd/ateworker-microvm/restore.go index fa14efa91e..efa86e976e 100644 --- a/cmd/ateworker-microvm/restore.go +++ b/cmd/ateworker-microvm/restore.go @@ -27,19 +27,19 @@ import ( "strings" "time" - "github.com/agent-substrate/substrate/internal/apierror" - "github.com/agent-substrate/substrate/internal/ateworker/phaselog" - "github.com/agent-substrate/substrate/internal/workerstats" - - "github.com/agent-substrate/substrate/internal/ateworker/actornet" - - "github.com/agent-substrate/substrate/cmd/ateworker-microvm/internal/ch" - "github.com/agent-substrate/substrate/cmd/ateworker-microvm/internal/kata" - "github.com/agent-substrate/substrate/internal/imagecache" - "github.com/agent-substrate/substrate/internal/proto/ateworkerpb" - "github.com/agent-substrate/substrate/internal/resources" - "github.com/agent-substrate/substrate/internal/sizing" - "github.com/agent-substrate/substrate/internal/wakeupprobe" + "go.ate.dev/substrate/internal/apierror" + "go.ate.dev/substrate/internal/ateworker/phaselog" + "go.ate.dev/substrate/internal/workerstats" + + "go.ate.dev/substrate/internal/ateworker/actornet" + + "go.ate.dev/substrate/cmd/ateworker-microvm/internal/ch" + "go.ate.dev/substrate/cmd/ateworker-microvm/internal/kata" + "go.ate.dev/substrate/internal/imagecache" + "go.ate.dev/substrate/internal/proto/ateworkerpb" + "go.ate.dev/substrate/internal/resources" + "go.ate.dev/substrate/internal/sizing" + "go.ate.dev/substrate/internal/wakeupprobe" ) // restoreMemMode picks how cloud-hypervisor should load guest RAM, from what the VMM diff --git a/cmd/ateworker-microvm/restore_test.go b/cmd/ateworker-microvm/restore_test.go index a7118617e0..b95ddd2384 100644 --- a/cmd/ateworker-microvm/restore_test.go +++ b/cmd/ateworker-microvm/restore_test.go @@ -27,11 +27,11 @@ import ( "google.golang.org/grpc/codes" - "github.com/agent-substrate/substrate/cmd/ateworker-microvm/internal/ch" - "github.com/agent-substrate/substrate/cmd/ateworker-microvm/internal/kata" - "github.com/agent-substrate/substrate/internal/actorlock" - "github.com/agent-substrate/substrate/internal/apierror" - "github.com/agent-substrate/substrate/internal/proto/ateworkerpb" + "go.ate.dev/substrate/cmd/ateworker-microvm/internal/ch" + "go.ate.dev/substrate/cmd/ateworker-microvm/internal/kata" + "go.ate.dev/substrate/internal/actorlock" + "go.ate.dev/substrate/internal/apierror" + "go.ate.dev/substrate/internal/proto/ateworkerpb" ) // writeSnapshotConfig writes a config.json holding the given fs devices (plus the diff --git a/cmd/ateworker-microvm/rootfsupper.go b/cmd/ateworker-microvm/rootfsupper.go index c8984ebc66..5097135c21 100644 --- a/cmd/ateworker-microvm/rootfsupper.go +++ b/cmd/ateworker-microvm/rootfsupper.go @@ -53,9 +53,9 @@ import ( "path/filepath" "strings" - "github.com/agent-substrate/substrate/cmd/ateworker-microvm/internal/kata" - "github.com/agent-substrate/substrate/internal/proto/ateworkerpb" - "github.com/agent-substrate/substrate/internal/tarutil" + "go.ate.dev/substrate/cmd/ateworker-microvm/internal/kata" + "go.ate.dev/substrate/internal/proto/ateworkerpb" + "go.ate.dev/substrate/internal/tarutil" ) // rootfsUpperTarFile is the snapshot file holding the tar of one container's diff --git a/cmd/ateworker-microvm/rootfsupper_test.go b/cmd/ateworker-microvm/rootfsupper_test.go index 0181e1b251..3869055108 100644 --- a/cmd/ateworker-microvm/rootfsupper_test.go +++ b/cmd/ateworker-microvm/rootfsupper_test.go @@ -22,9 +22,9 @@ import ( "strings" "testing" - "github.com/agent-substrate/substrate/internal/tarutil" + "go.ate.dev/substrate/internal/tarutil" - "github.com/agent-substrate/substrate/internal/proto/ateworkerpb" + "go.ate.dev/substrate/internal/proto/ateworkerpb" ) // upperDirWith returns a rootfs upper directory laid out the way the host diff --git a/cmd/ateworker-microvm/run.go b/cmd/ateworker-microvm/run.go index ea076e6888..55748ff804 100644 --- a/cmd/ateworker-microvm/run.go +++ b/cmd/ateworker-microvm/run.go @@ -29,21 +29,21 @@ import ( "syscall" "time" - "github.com/agent-substrate/substrate/internal/apierror" - "github.com/agent-substrate/substrate/internal/workerstats" - - "github.com/agent-substrate/substrate/internal/ateworker/actornet" - - "github.com/agent-substrate/substrate/cmd/ateworker-microvm/internal/ch" - "github.com/agent-substrate/substrate/cmd/ateworker-microvm/internal/kata" - "github.com/agent-substrate/substrate/cmd/ateworker-microvm/internal/third_party/kata/agentpb" - "github.com/agent-substrate/substrate/internal/imagecache" - "github.com/agent-substrate/substrate/internal/ocispec" - "github.com/agent-substrate/substrate/internal/proto/ateworkerpb" - "github.com/agent-substrate/substrate/internal/resources" - "github.com/agent-substrate/substrate/internal/sizing" - "github.com/agent-substrate/substrate/internal/wakeupprobe" + "go.ate.dev/substrate/internal/apierror" + "go.ate.dev/substrate/internal/workerstats" + + "go.ate.dev/substrate/internal/ateworker/actornet" + specs "github.com/opencontainers/runtime-spec/specs-go" + "go.ate.dev/substrate/cmd/ateworker-microvm/internal/ch" + "go.ate.dev/substrate/cmd/ateworker-microvm/internal/kata" + "go.ate.dev/substrate/cmd/ateworker-microvm/internal/third_party/kata/agentpb" + "go.ate.dev/substrate/internal/imagecache" + "go.ate.dev/substrate/internal/ocispec" + "go.ate.dev/substrate/internal/proto/ateworkerpb" + "go.ate.dev/substrate/internal/resources" + "go.ate.dev/substrate/internal/sizing" + "go.ate.dev/substrate/internal/wakeupprobe" "golang.org/x/sys/unix" ) diff --git a/cmd/ateworker-microvm/run_test.go b/cmd/ateworker-microvm/run_test.go index bc23538f3e..34699b0f96 100644 --- a/cmd/ateworker-microvm/run_test.go +++ b/cmd/ateworker-microvm/run_test.go @@ -27,7 +27,7 @@ import ( "testing" "time" - "github.com/agent-substrate/substrate/cmd/ateworker-microvm/internal/kata" + "go.ate.dev/substrate/cmd/ateworker-microvm/internal/kata" ) // A vsock socket that has gone missing means cloud-hypervisor stopped the VM diff --git a/cmd/ateworker-microvm/sandboxnet.go b/cmd/ateworker-microvm/sandboxnet.go index a4595b7e13..298220f3d7 100644 --- a/cmd/ateworker-microvm/sandboxnet.go +++ b/cmd/ateworker-microvm/sandboxnet.go @@ -20,7 +20,7 @@ import ( "fmt" "os" - "github.com/agent-substrate/substrate/internal/ateworker/actornet" + "go.ate.dev/substrate/internal/ateworker/actornet" ) // writeActorResolvConf points the guest resolver at its fixed gateway address. diff --git a/cmd/ateworker-microvm/shutdown.go b/cmd/ateworker-microvm/shutdown.go index 2a4e0c427c..e91d5198c7 100644 --- a/cmd/ateworker-microvm/shutdown.go +++ b/cmd/ateworker-microvm/shutdown.go @@ -31,7 +31,7 @@ import ( "syscall" "time" - "github.com/agent-substrate/substrate/cmd/ateworker-microvm/internal/kata" + "go.ate.dev/substrate/cmd/ateworker-microvm/internal/kata" ) // workloadGracePeriod is the whole budget for draining the worker on shutdown: diff --git a/cmd/ateworker-microvm/snapshotcache.go b/cmd/ateworker-microvm/snapshotcache.go index 2f62457643..91ab2618ae 100644 --- a/cmd/ateworker-microvm/snapshotcache.go +++ b/cmd/ateworker-microvm/snapshotcache.go @@ -21,7 +21,7 @@ import ( "log/slog" "strings" - "github.com/agent-substrate/substrate/internal/snapshotcache" + "go.ate.dev/substrate/internal/snapshotcache" ) const memoryRangesFile = "memory-ranges" diff --git a/cmd/ateworker-microvm/snapshotcache_test.go b/cmd/ateworker-microvm/snapshotcache_test.go index f6ab7aaa8c..71f87c25e8 100644 --- a/cmd/ateworker-microvm/snapshotcache_test.go +++ b/cmd/ateworker-microvm/snapshotcache_test.go @@ -22,8 +22,8 @@ import ( "path/filepath" "testing" - "github.com/agent-substrate/substrate/cmd/ateworker-microvm/internal/ch" - "github.com/agent-substrate/substrate/internal/snapshotcache" + "go.ate.dev/substrate/cmd/ateworker-microvm/internal/ch" + "go.ate.dev/substrate/internal/snapshotcache" ) const ( diff --git a/cmd/ateworker-microvm/spec.go b/cmd/ateworker-microvm/spec.go index b0a2a88030..21eec6af87 100644 --- a/cmd/ateworker-microvm/spec.go +++ b/cmd/ateworker-microvm/spec.go @@ -22,8 +22,8 @@ import ( specs "github.com/opencontainers/runtime-spec/specs-go" - "github.com/agent-substrate/substrate/cmd/ateworker-microvm/internal/kata" - "github.com/agent-substrate/substrate/internal/apierror" + "go.ate.dev/substrate/cmd/ateworker-microvm/internal/kata" + "go.ate.dev/substrate/internal/apierror" ) // guestEnvelope is the ceiling an actor's container limits must fit inside. diff --git a/cmd/ateworker-microvm/spec_test.go b/cmd/ateworker-microvm/spec_test.go index 126dabe2b7..05ec03908d 100644 --- a/cmd/ateworker-microvm/spec_test.go +++ b/cmd/ateworker-microvm/spec_test.go @@ -26,9 +26,9 @@ import ( "google.golang.org/grpc/codes" "k8s.io/utils/ptr" - "github.com/agent-substrate/substrate/cmd/ateworker-microvm/internal/kata" - "github.com/agent-substrate/substrate/internal/apierror" - "github.com/agent-substrate/substrate/internal/ocispec" + "go.ate.dev/substrate/cmd/ateworker-microvm/internal/kata" + "go.ate.dev/substrate/internal/apierror" + "go.ate.dev/substrate/internal/ocispec" ) // Limits the guest can never satisfy must be rejected before the containers diff --git a/cmd/ateworker-microvm/stats.go b/cmd/ateworker-microvm/stats.go index d7e8c61d19..2bc2d5c2a4 100644 --- a/cmd/ateworker-microvm/stats.go +++ b/cmd/ateworker-microvm/stats.go @@ -25,13 +25,13 @@ import ( "sync" "time" - "github.com/agent-substrate/substrate/cmd/ateworker-microvm/internal/agentstats" - "github.com/agent-substrate/substrate/cmd/ateworker-microvm/internal/third_party/kata/agentpb" - "github.com/agent-substrate/substrate/internal/apierror" - "github.com/agent-substrate/substrate/internal/ateattr" - "github.com/agent-substrate/substrate/internal/proto/ateworkerpb" - "github.com/agent-substrate/substrate/internal/resources" - atev1alpha1 "github.com/agent-substrate/substrate/pkg/api/v1alpha1" + "go.ate.dev/substrate/cmd/ateworker-microvm/internal/agentstats" + "go.ate.dev/substrate/cmd/ateworker-microvm/internal/third_party/kata/agentpb" + "go.ate.dev/substrate/internal/apierror" + "go.ate.dev/substrate/internal/ateattr" + "go.ate.dev/substrate/internal/proto/ateworkerpb" + "go.ate.dev/substrate/internal/resources" + atev1alpha1 "go.ate.dev/substrate/pkg/api/v1alpha1" ) // statsCallTimeout bounds one container's guest-agent call. The RPC is polled diff --git a/cmd/ateworker-microvm/stats_test.go b/cmd/ateworker-microvm/stats_test.go index 6340144dd0..85977fa069 100644 --- a/cmd/ateworker-microvm/stats_test.go +++ b/cmd/ateworker-microvm/stats_test.go @@ -30,12 +30,12 @@ import ( "google.golang.org/grpc/codes" "google.golang.org/protobuf/testing/protocmp" - "github.com/agent-substrate/substrate/cmd/ateworker-microvm/internal/third_party/kata/agentpb" - "github.com/agent-substrate/substrate/internal/actorlock" - "github.com/agent-substrate/substrate/internal/apierror" - "github.com/agent-substrate/substrate/internal/proto/ateworkerpb" - "github.com/agent-substrate/substrate/internal/resources" - "github.com/agent-substrate/substrate/internal/workerstats" + "go.ate.dev/substrate/cmd/ateworker-microvm/internal/third_party/kata/agentpb" + "go.ate.dev/substrate/internal/actorlock" + "go.ate.dev/substrate/internal/apierror" + "go.ate.dev/substrate/internal/proto/ateworkerpb" + "go.ate.dev/substrate/internal/resources" + "go.ate.dev/substrate/internal/workerstats" ) // TestActorBootParamsAttribution pins the mapping from actorBootParams onto the diff --git a/cmd/ateworker-microvm/systeminfo.go b/cmd/ateworker-microvm/systeminfo.go index 32157e07b0..5e946d2f81 100644 --- a/cmd/ateworker-microvm/systeminfo.go +++ b/cmd/ateworker-microvm/systeminfo.go @@ -47,9 +47,9 @@ import ( "os" "path/filepath" - "github.com/agent-substrate/substrate/cmd/ateworker-microvm/internal/kata" - "github.com/agent-substrate/substrate/internal/ocispec" - "github.com/agent-substrate/substrate/internal/proto/ateworkerpb" + "go.ate.dev/substrate/cmd/ateworker-microvm/internal/kata" + "go.ate.dev/substrate/internal/ocispec" + "go.ate.dev/substrate/internal/proto/ateworkerpb" "golang.org/x/sys/unix" ) diff --git a/cmd/ateworker-microvm/usage_test.go b/cmd/ateworker-microvm/usage_test.go index b78f71482c..2b8a4e2cf4 100644 --- a/cmd/ateworker-microvm/usage_test.go +++ b/cmd/ateworker-microvm/usage_test.go @@ -23,11 +23,11 @@ import ( "testing" "time" - "github.com/agent-substrate/substrate/cmd/ateworker-microvm/internal/third_party/kata/agentpb" - "github.com/agent-substrate/substrate/internal/ateattr" - "github.com/agent-substrate/substrate/internal/proto/ateworkerpb" - "github.com/agent-substrate/substrate/internal/workerstats" - "github.com/agent-substrate/substrate/internal/workerstats/workerstatstest" + "go.ate.dev/substrate/cmd/ateworker-microvm/internal/third_party/kata/agentpb" + "go.ate.dev/substrate/internal/ateattr" + "go.ate.dev/substrate/internal/proto/ateworkerpb" + "go.ate.dev/substrate/internal/workerstats" + "go.ate.dev/substrate/internal/workerstats/workerstatstest" ) func withUsageRecorder(s *AteWorkerService) *workerstatstest.Recorder { diff --git a/cmd/benchmarking/boomer-worker/main.go b/cmd/benchmarking/boomer-worker/main.go index d4e1acbbf4..29b77a4a63 100644 --- a/cmd/benchmarking/boomer-worker/main.go +++ b/cmd/benchmarking/boomer-worker/main.go @@ -28,18 +28,18 @@ import ( "strings" "time" - "github.com/agent-substrate/substrate/internal/benchmarking/boomer/boomerutil" - "github.com/agent-substrate/substrate/internal/benchmarking/boomer/dynconfig" - bmetrics "github.com/agent-substrate/substrate/internal/benchmarking/boomer/metrics" - btrace "github.com/agent-substrate/substrate/internal/benchmarking/boomer/trace" - "github.com/agent-substrate/substrate/internal/benchmarking/boomer/userclass" "github.com/myzhan/boomer" + "go.ate.dev/substrate/internal/benchmarking/boomer/boomerutil" + "go.ate.dev/substrate/internal/benchmarking/boomer/dynconfig" + bmetrics "go.ate.dev/substrate/internal/benchmarking/boomer/metrics" + btrace "go.ate.dev/substrate/internal/benchmarking/boomer/trace" + "go.ate.dev/substrate/internal/benchmarking/boomer/userclass" // 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" + "go.ate.dev/substrate/internal/benchmarking/boomer/agentsession" + _ "go.ate.dev/substrate/internal/benchmarking/boomer/glutton" + _ "go.ate.dev/substrate/internal/benchmarking/boomer/resumepaused" + _ "go.ate.dev/substrate/internal/benchmarking/boomer/sweperf" ) func main() { diff --git a/cmd/benchmarking/glutton/main.go b/cmd/benchmarking/glutton/main.go index 8f5d8fd09c..60e3bebc2c 100644 --- a/cmd/benchmarking/glutton/main.go +++ b/cmd/benchmarking/glutton/main.go @@ -26,9 +26,9 @@ import ( "github.com/spf13/pflag" - "github.com/agent-substrate/substrate/internal/benchmarking/glutton" - "github.com/agent-substrate/substrate/internal/serverboot" - "github.com/agent-substrate/substrate/internal/version" + "go.ate.dev/substrate/internal/benchmarking/glutton" + "go.ate.dev/substrate/internal/serverboot" + "go.ate.dev/substrate/internal/version" ) var ( diff --git a/cmd/benchmarking/isolate/fake-atelet/herder.go b/cmd/benchmarking/isolate/fake-atelet/herder.go index 9006b6889d..1b58808abe 100644 --- a/cmd/benchmarking/isolate/fake-atelet/herder.go +++ b/cmd/benchmarking/isolate/fake-atelet/herder.go @@ -26,10 +26,10 @@ import ( "strings" "time" - "github.com/agent-substrate/substrate/internal/objectstore" - "github.com/agent-substrate/substrate/internal/proto/ateletpb" - "github.com/agent-substrate/substrate/internal/resources" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "go.ate.dev/substrate/internal/objectstore" + "go.ate.dev/substrate/internal/proto/ateletpb" + "go.ate.dev/substrate/internal/resources" + "go.ate.dev/substrate/pkg/proto/ateapipb" "google.golang.org/grpc" ) diff --git a/cmd/benchmarking/isolate/fake-atelet/herder_test.go b/cmd/benchmarking/isolate/fake-atelet/herder_test.go index f2e197fc58..a1b0a52bdf 100644 --- a/cmd/benchmarking/isolate/fake-atelet/herder_test.go +++ b/cmd/benchmarking/isolate/fake-atelet/herder_test.go @@ -27,8 +27,8 @@ import ( "testing" "time" - "github.com/agent-substrate/substrate/internal/proto/ateletpb" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "go.ate.dev/substrate/internal/proto/ateletpb" + "go.ate.dev/substrate/pkg/proto/ateapipb" "google.golang.org/grpc" "google.golang.org/protobuf/proto" ) diff --git a/cmd/benchmarking/isolate/fake-atelet/main.go b/cmd/benchmarking/isolate/fake-atelet/main.go index 3f3e7dc381..bcd956cbf7 100644 --- a/cmd/benchmarking/isolate/fake-atelet/main.go +++ b/cmd/benchmarking/isolate/fake-atelet/main.go @@ -37,19 +37,19 @@ import ( "syscall" "time" - "github.com/agent-substrate/substrate/cmd/benchmarking/isolate/internal/fakeworker" - "github.com/agent-substrate/substrate/internal/ateapiauth" - "github.com/agent-substrate/substrate/internal/atelet" - "github.com/agent-substrate/substrate/internal/credbundle" - "github.com/agent-substrate/substrate/internal/installdefaults" - "github.com/agent-substrate/substrate/internal/proto/ateletpb" - "github.com/agent-substrate/substrate/internal/serverboot" - "github.com/agent-substrate/substrate/internal/version" - "github.com/agent-substrate/substrate/pkg/objectstorage" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" "github.com/aws/aws-sdk-go-v2/config" "github.com/aws/aws-sdk-go-v2/service/s3" "github.com/spf13/pflag" + "go.ate.dev/substrate/cmd/benchmarking/isolate/internal/fakeworker" + "go.ate.dev/substrate/internal/ateapiauth" + "go.ate.dev/substrate/internal/atelet" + "go.ate.dev/substrate/internal/credbundle" + "go.ate.dev/substrate/internal/installdefaults" + "go.ate.dev/substrate/internal/proto/ateletpb" + "go.ate.dev/substrate/internal/serverboot" + "go.ate.dev/substrate/internal/version" + "go.ate.dev/substrate/pkg/objectstorage" + "go.ate.dev/substrate/pkg/proto/ateapipb" "google.golang.org/grpc" "google.golang.org/grpc/credentials" ) diff --git a/cmd/benchmarking/isolate/fake-atelet/relay.go b/cmd/benchmarking/isolate/fake-atelet/relay.go index c25d80a678..eed6d28c00 100644 --- a/cmd/benchmarking/isolate/fake-atelet/relay.go +++ b/cmd/benchmarking/isolate/fake-atelet/relay.go @@ -19,8 +19,8 @@ import ( "crypto/tls" "log/slog" - "github.com/agent-substrate/substrate/cmd/benchmarking/isolate/internal/fakeworker" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "go.ate.dev/substrate/cmd/benchmarking/isolate/internal/fakeworker" + "go.ate.dev/substrate/pkg/proto/ateapipb" "google.golang.org/grpc" ) diff --git a/cmd/benchmarking/isolate/fake-atelet/relay_test.go b/cmd/benchmarking/isolate/fake-atelet/relay_test.go index 88f70d7304..ba0d2c51ca 100644 --- a/cmd/benchmarking/isolate/fake-atelet/relay_test.go +++ b/cmd/benchmarking/isolate/fake-atelet/relay_test.go @@ -21,9 +21,9 @@ import ( "strings" "testing" - "github.com/agent-substrate/substrate/cmd/benchmarking/isolate/internal/fakeworker/fakeworkertest" - "github.com/agent-substrate/substrate/internal/credbundle" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "go.ate.dev/substrate/cmd/benchmarking/isolate/internal/fakeworker/fakeworkertest" + "go.ate.dev/substrate/internal/credbundle" + "go.ate.dev/substrate/pkg/proto/ateapipb" "google.golang.org/grpc" "google.golang.org/grpc/codes" "google.golang.org/grpc/credentials" diff --git a/cmd/benchmarking/isolate/fake-workersync/controller.go b/cmd/benchmarking/isolate/fake-workersync/controller.go index 320ecfc7cf..22a7d161dd 100644 --- a/cmd/benchmarking/isolate/fake-workersync/controller.go +++ b/cmd/benchmarking/isolate/fake-workersync/controller.go @@ -27,12 +27,12 @@ import ( "sync" "time" - "github.com/agent-substrate/substrate/cmd/benchmarking/isolate/internal/fakeworker" - "github.com/agent-substrate/substrate/internal/hardware" - "github.com/agent-substrate/substrate/internal/resources" - atev1alpha1 "github.com/agent-substrate/substrate/pkg/api/v1alpha1" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" "github.com/google/uuid" + "go.ate.dev/substrate/cmd/benchmarking/isolate/internal/fakeworker" + "go.ate.dev/substrate/internal/hardware" + "go.ate.dev/substrate/internal/resources" + atev1alpha1 "go.ate.dev/substrate/pkg/api/v1alpha1" + "go.ate.dev/substrate/pkg/proto/ateapipb" "golang.org/x/sync/errgroup" "google.golang.org/grpc" "google.golang.org/grpc/codes" diff --git a/cmd/benchmarking/isolate/fake-workersync/controller_test.go b/cmd/benchmarking/isolate/fake-workersync/controller_test.go index 43c4a24024..543f5d49f2 100644 --- a/cmd/benchmarking/isolate/fake-workersync/controller_test.go +++ b/cmd/benchmarking/isolate/fake-workersync/controller_test.go @@ -25,9 +25,9 @@ import ( "testing" "time" - "github.com/agent-substrate/substrate/cmd/benchmarking/isolate/internal/fakeworker" - atev1alpha1 "github.com/agent-substrate/substrate/pkg/api/v1alpha1" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "go.ate.dev/substrate/cmd/benchmarking/isolate/internal/fakeworker" + atev1alpha1 "go.ate.dev/substrate/pkg/api/v1alpha1" + "go.ate.dev/substrate/pkg/proto/ateapipb" "google.golang.org/grpc" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" diff --git a/cmd/benchmarking/isolate/fake-workersync/main.go b/cmd/benchmarking/isolate/fake-workersync/main.go index d5596f7a07..9a3829cae7 100644 --- a/cmd/benchmarking/isolate/fake-workersync/main.go +++ b/cmd/benchmarking/isolate/fake-workersync/main.go @@ -44,16 +44,16 @@ import ( "syscall" "time" - "github.com/agent-substrate/substrate/cmd/benchmarking/isolate/internal/fakeworker" - "github.com/agent-substrate/substrate/internal/benchmarking/boomer/boomerutil" - "github.com/agent-substrate/substrate/internal/installdefaults" - "github.com/agent-substrate/substrate/internal/serverboot" - "github.com/agent-substrate/substrate/internal/version" - atev1alpha1 "github.com/agent-substrate/substrate/pkg/api/v1alpha1" - "github.com/agent-substrate/substrate/pkg/client/clientset/versioned" - ateinformers "github.com/agent-substrate/substrate/pkg/client/informers/externalversions" - atelisters "github.com/agent-substrate/substrate/pkg/client/listers/api/v1alpha1" "github.com/spf13/pflag" + "go.ate.dev/substrate/cmd/benchmarking/isolate/internal/fakeworker" + "go.ate.dev/substrate/internal/benchmarking/boomer/boomerutil" + "go.ate.dev/substrate/internal/installdefaults" + "go.ate.dev/substrate/internal/serverboot" + "go.ate.dev/substrate/internal/version" + atev1alpha1 "go.ate.dev/substrate/pkg/api/v1alpha1" + "go.ate.dev/substrate/pkg/client/clientset/versioned" + ateinformers "go.ate.dev/substrate/pkg/client/informers/externalversions" + atelisters "go.ate.dev/substrate/pkg/client/listers/api/v1alpha1" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/labels" diff --git a/cmd/benchmarking/isolate/fake-workersync/relayclient.go b/cmd/benchmarking/isolate/fake-workersync/relayclient.go index 3b27c64d27..7d4fe48cad 100644 --- a/cmd/benchmarking/isolate/fake-workersync/relayclient.go +++ b/cmd/benchmarking/isolate/fake-workersync/relayclient.go @@ -23,9 +23,9 @@ import ( "sync" "time" - "github.com/agent-substrate/substrate/cmd/benchmarking/isolate/internal/fakeworker" - "github.com/agent-substrate/substrate/internal/credbundle" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "go.ate.dev/substrate/cmd/benchmarking/isolate/internal/fakeworker" + "go.ate.dev/substrate/internal/credbundle" + "go.ate.dev/substrate/pkg/proto/ateapipb" "google.golang.org/grpc" "google.golang.org/grpc/credentials" ) diff --git a/cmd/benchmarking/isolate/fake-workersync/relayclient_test.go b/cmd/benchmarking/isolate/fake-workersync/relayclient_test.go index f0bdffc756..0cc83ce898 100644 --- a/cmd/benchmarking/isolate/fake-workersync/relayclient_test.go +++ b/cmd/benchmarking/isolate/fake-workersync/relayclient_test.go @@ -26,10 +26,10 @@ import ( "sync" "testing" - "github.com/agent-substrate/substrate/cmd/benchmarking/isolate/internal/fakeworker" - "github.com/agent-substrate/substrate/cmd/benchmarking/isolate/internal/fakeworker/fakeworkertest" - "github.com/agent-substrate/substrate/internal/credbundle" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "go.ate.dev/substrate/cmd/benchmarking/isolate/internal/fakeworker" + "go.ate.dev/substrate/cmd/benchmarking/isolate/internal/fakeworker/fakeworkertest" + "go.ate.dev/substrate/internal/credbundle" + "go.ate.dev/substrate/pkg/proto/ateapipb" "google.golang.org/grpc" "google.golang.org/grpc/codes" "google.golang.org/grpc/credentials" diff --git a/cmd/benchmarking/isolate/internal/fakeworker/fakeworkertest/fakeworkertest.go b/cmd/benchmarking/isolate/internal/fakeworker/fakeworkertest/fakeworkertest.go index 6b1365bdd5..7339ebfaa7 100644 --- a/cmd/benchmarking/isolate/internal/fakeworker/fakeworkertest/fakeworkertest.go +++ b/cmd/benchmarking/isolate/internal/fakeworker/fakeworkertest/fakeworkertest.go @@ -33,7 +33,7 @@ import ( "testing" "time" - "github.com/agent-substrate/substrate/internal/localca" + "go.ate.dev/substrate/internal/localca" ) // CA is a throwaway signer whose files live in a test's temp dir. diff --git a/cmd/credential-provider/kubernetes-secrets/kubeprovider.go b/cmd/credential-provider/kubernetes-secrets/kubeprovider.go index d6741855fb..d6e84057bc 100644 --- a/cmd/credential-provider/kubernetes-secrets/kubeprovider.go +++ b/cmd/credential-provider/kubernetes-secrets/kubeprovider.go @@ -32,8 +32,8 @@ import ( "google.golang.org/grpc/codes" "google.golang.org/grpc/status" - "github.com/agent-substrate/substrate/internal/resources" - "github.com/agent-substrate/substrate/pkg/proto/credproviderpb" + "go.ate.dev/substrate/internal/resources" + "go.ate.dev/substrate/pkg/proto/credproviderpb" ) // ProviderName is the ate-secret:// URI host this backend serves. diff --git a/cmd/credential-provider/kubernetes-secrets/kubeprovider_test.go b/cmd/credential-provider/kubernetes-secrets/kubeprovider_test.go index b11715cdb8..57a67f8d09 100644 --- a/cmd/credential-provider/kubernetes-secrets/kubeprovider_test.go +++ b/cmd/credential-provider/kubernetes-secrets/kubeprovider_test.go @@ -28,7 +28,7 @@ import ( "google.golang.org/grpc/codes" "google.golang.org/grpc/status" - "github.com/agent-substrate/substrate/pkg/proto/credproviderpb" + "go.ate.dev/substrate/pkg/proto/credproviderpb" ) func TestParseURI(t *testing.T) { diff --git a/cmd/credential-provider/kubernetes-secrets/main.go b/cmd/credential-provider/kubernetes-secrets/main.go index d8b82667aa..544fff4ceb 100644 --- a/cmd/credential-provider/kubernetes-secrets/main.go +++ b/cmd/credential-provider/kubernetes-secrets/main.go @@ -39,12 +39,12 @@ import ( "k8s.io/client-go/kubernetes" "k8s.io/client-go/rest" - "github.com/agent-substrate/substrate/internal/credbundle" - "github.com/agent-substrate/substrate/internal/installdefaults" - "github.com/agent-substrate/substrate/internal/preview" - "github.com/agent-substrate/substrate/internal/serverboot" - "github.com/agent-substrate/substrate/internal/version" - "github.com/agent-substrate/substrate/pkg/proto/credproviderpb" + "go.ate.dev/substrate/internal/credbundle" + "go.ate.dev/substrate/internal/installdefaults" + "go.ate.dev/substrate/internal/preview" + "go.ate.dev/substrate/internal/serverboot" + "go.ate.dev/substrate/internal/version" + "go.ate.dev/substrate/pkg/proto/credproviderpb" ) const serviceName = "credprovider" diff --git a/cmd/credential-provider/kubernetes-secrets/main_test.go b/cmd/credential-provider/kubernetes-secrets/main_test.go index cb444305b2..fb1cae1c26 100644 --- a/cmd/credential-provider/kubernetes-secrets/main_test.go +++ b/cmd/credential-provider/kubernetes-secrets/main_test.go @@ -31,7 +31,7 @@ import ( "testing" "time" - "github.com/agent-substrate/substrate/internal/installdefaults" + "go.ate.dev/substrate/internal/installdefaults" "google.golang.org/grpc/credentials" ) diff --git a/cmd/credential-provider/kubernetes-secrets/statusz.go b/cmd/credential-provider/kubernetes-secrets/statusz.go index 45e2f52896..df586ff021 100644 --- a/cmd/credential-provider/kubernetes-secrets/statusz.go +++ b/cmd/credential-provider/kubernetes-secrets/statusz.go @@ -29,7 +29,7 @@ import ( "github.com/spf13/pflag" - "github.com/agent-substrate/substrate/internal/version" + "go.ate.dev/substrate/internal/version" ) // statusContext is the /statusz payload; the JSON form is the contract tests diff --git a/cmd/credential-provider/kubernetes-secrets/statusz_test.go b/cmd/credential-provider/kubernetes-secrets/statusz_test.go index 4155f4389d..184ec08e01 100644 --- a/cmd/credential-provider/kubernetes-secrets/statusz_test.go +++ b/cmd/credential-provider/kubernetes-secrets/statusz_test.go @@ -27,7 +27,7 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes/fake" - "github.com/agent-substrate/substrate/pkg/proto/credproviderpb" + "go.ate.dev/substrate/pkg/proto/credproviderpb" ) func TestGrants(t *testing.T) { diff --git a/cmd/kubectl-ate/internal/cmd/actor.go b/cmd/kubectl-ate/internal/cmd/actor.go index 20b59a3ad1..be7b57c711 100644 --- a/cmd/kubectl-ate/internal/cmd/actor.go +++ b/cmd/kubectl-ate/internal/cmd/actor.go @@ -27,12 +27,12 @@ import ( "sync" "time" - "github.com/agent-substrate/substrate/cmd/kubectl-ate/internal/printer" - "github.com/agent-substrate/substrate/internal/ateattr" - "github.com/agent-substrate/substrate/internal/ocispec" - "github.com/agent-substrate/substrate/internal/resources" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" "github.com/spf13/cobra" + "go.ate.dev/substrate/cmd/kubectl-ate/internal/printer" + "go.ate.dev/substrate/internal/ateattr" + "go.ate.dev/substrate/internal/ocispec" + "go.ate.dev/substrate/internal/resources" + "go.ate.dev/substrate/pkg/proto/ateapipb" "google.golang.org/grpc" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" diff --git a/cmd/kubectl-ate/internal/cmd/actor_test.go b/cmd/kubectl-ate/internal/cmd/actor_test.go index ce4c31bc2b..634369c2b8 100644 --- a/cmd/kubectl-ate/internal/cmd/actor_test.go +++ b/cmd/kubectl-ate/internal/cmd/actor_test.go @@ -24,9 +24,9 @@ import ( "testing" "time" - "github.com/agent-substrate/substrate/internal/resources" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" "github.com/google/go-cmp/cmp" + "go.ate.dev/substrate/internal/resources" + "go.ate.dev/substrate/pkg/proto/ateapipb" "google.golang.org/grpc" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" diff --git a/cmd/kubectl-ate/internal/cmd/actortemplate.go b/cmd/kubectl-ate/internal/cmd/actortemplate.go index a27d48fa47..8a73095942 100644 --- a/cmd/kubectl-ate/internal/cmd/actortemplate.go +++ b/cmd/kubectl-ate/internal/cmd/actortemplate.go @@ -19,9 +19,9 @@ import ( "io" "os" - "github.com/agent-substrate/substrate/cmd/kubectl-ate/internal/printer" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" "github.com/spf13/cobra" + "go.ate.dev/substrate/cmd/kubectl-ate/internal/printer" + "go.ate.dev/substrate/pkg/proto/ateapipb" "google.golang.org/protobuf/encoding/protojson" "sigs.k8s.io/yaml" ) diff --git a/cmd/kubectl-ate/internal/cmd/actortemplate_test.go b/cmd/kubectl-ate/internal/cmd/actortemplate_test.go index 6243b5a2b5..ec0033e2e9 100644 --- a/cmd/kubectl-ate/internal/cmd/actortemplate_test.go +++ b/cmd/kubectl-ate/internal/cmd/actortemplate_test.go @@ -20,8 +20,8 @@ import ( "strings" "testing" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" "github.com/google/go-cmp/cmp" + "go.ate.dev/substrate/pkg/proto/ateapipb" "google.golang.org/protobuf/testing/protocmp" ) @@ -41,7 +41,7 @@ workerSelector: workload: counter containers: - name: counter - image: ko://github.com/agent-substrate/substrate/demos/counter + image: ko://go.ate.dev/substrate/demos/counter command: ["/ko-app/counter", "--extra-port=9090"] wakeupProbe: httpGet: @@ -78,7 +78,7 @@ func TestActorTemplateFromManifest(t *testing.T) { WorkerSelector: &ateapipb.Selector{MatchLabels: map[string]string{"workload": "counter"}}, Containers: []*ateapipb.Container{{ Name: "counter", - Image: "ko://github.com/agent-substrate/substrate/demos/counter", + Image: "ko://go.ate.dev/substrate/demos/counter", Command: []string{"/ko-app/counter", "--extra-port=9090"}, WakeupProbe: &ateapipb.WakeupProbe{ HttpGet: &ateapipb.HttpGetAction{Path: "/readyz", Port: 80}, diff --git a/cmd/kubectl-ate/internal/cmd/admin.go b/cmd/kubectl-ate/internal/cmd/admin.go index 239b8531ee..8b73cd227b 100644 --- a/cmd/kubectl-ate/internal/cmd/admin.go +++ b/cmd/kubectl-ate/internal/cmd/admin.go @@ -21,9 +21,9 @@ import ( "text/tabwriter" "time" - "github.com/agent-substrate/substrate/internal/localca" - "github.com/agent-substrate/substrate/internal/localjwtauthority" "github.com/spf13/cobra" + "go.ate.dev/substrate/internal/localca" + "go.ate.dev/substrate/internal/localjwtauthority" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes" diff --git a/cmd/kubectl-ate/internal/cmd/admin_test.go b/cmd/kubectl-ate/internal/cmd/admin_test.go index 103aad9f11..2cab7ec11b 100644 --- a/cmd/kubectl-ate/internal/cmd/admin_test.go +++ b/cmd/kubectl-ate/internal/cmd/admin_test.go @@ -30,7 +30,7 @@ import ( "k8s.io/client-go/kubernetes/fake" k8stesting "k8s.io/client-go/testing" - "github.com/agent-substrate/substrate/internal/localjwtauthority" + "go.ate.dev/substrate/internal/localjwtauthority" ) func TestNewJWTPoolSecretWithFlagDefaults(t *testing.T) { diff --git a/cmd/kubectl-ate/internal/cmd/atespace.go b/cmd/kubectl-ate/internal/cmd/atespace.go index 8d208bdc69..48d673b08d 100644 --- a/cmd/kubectl-ate/internal/cmd/atespace.go +++ b/cmd/kubectl-ate/internal/cmd/atespace.go @@ -17,9 +17,9 @@ package cmd import ( "fmt" - "github.com/agent-substrate/substrate/cmd/kubectl-ate/internal/printer" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" "github.com/spf13/cobra" + "go.ate.dev/substrate/cmd/kubectl-ate/internal/printer" + "go.ate.dev/substrate/pkg/proto/ateapipb" ) var getAtespacesCmd = &cobra.Command{ diff --git a/cmd/kubectl-ate/internal/cmd/egress_policy.go b/cmd/kubectl-ate/internal/cmd/egress_policy.go index fb06708e8e..bc2c45ac46 100644 --- a/cmd/kubectl-ate/internal/cmd/egress_policy.go +++ b/cmd/kubectl-ate/internal/cmd/egress_policy.go @@ -21,10 +21,10 @@ import ( "fmt" "io" - "github.com/agent-substrate/substrate/cmd/kubectl-ate/internal/printer" - "github.com/agent-substrate/substrate/internal/resources" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" "github.com/spf13/cobra" + "go.ate.dev/substrate/cmd/kubectl-ate/internal/printer" + "go.ate.dev/substrate/internal/resources" + "go.ate.dev/substrate/pkg/proto/ateapipb" "google.golang.org/grpc" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" diff --git a/cmd/kubectl-ate/internal/cmd/egress_policy_test.go b/cmd/kubectl-ate/internal/cmd/egress_policy_test.go index b740d5724a..8faae33123 100644 --- a/cmd/kubectl-ate/internal/cmd/egress_policy_test.go +++ b/cmd/kubectl-ate/internal/cmd/egress_policy_test.go @@ -23,11 +23,11 @@ import ( "testing" "time" - "github.com/agent-substrate/substrate/cmd/kubectl-ate/internal/printer" - "github.com/agent-substrate/substrate/internal/resources" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" "github.com/google/go-cmp/cmp" "github.com/spf13/cobra" + "go.ate.dev/substrate/cmd/kubectl-ate/internal/printer" + "go.ate.dev/substrate/internal/resources" + "go.ate.dev/substrate/pkg/proto/ateapipb" "google.golang.org/grpc" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" diff --git a/cmd/kubectl-ate/internal/cmd/resourceref.go b/cmd/kubectl-ate/internal/cmd/resourceref.go index 27191be8ca..b2f43c59cf 100644 --- a/cmd/kubectl-ate/internal/cmd/resourceref.go +++ b/cmd/kubectl-ate/internal/cmd/resourceref.go @@ -18,8 +18,8 @@ import ( "fmt" "strings" - "github.com/agent-substrate/substrate/internal/resources" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "go.ate.dev/substrate/internal/resources" + "go.ate.dev/substrate/pkg/proto/ateapipb" ) // parseAtespacedName parses a reference to an atespace-scoped resource, diff --git a/cmd/kubectl-ate/internal/cmd/resourceref_test.go b/cmd/kubectl-ate/internal/cmd/resourceref_test.go index c269494079..8decc3ee9d 100644 --- a/cmd/kubectl-ate/internal/cmd/resourceref_test.go +++ b/cmd/kubectl-ate/internal/cmd/resourceref_test.go @@ -17,8 +17,8 @@ package cmd import ( "testing" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" "github.com/google/go-cmp/cmp" + "go.ate.dev/substrate/pkg/proto/ateapipb" "google.golang.org/protobuf/testing/protocmp" ) diff --git a/cmd/kubectl-ate/internal/cmd/revert_actor.go b/cmd/kubectl-ate/internal/cmd/revert_actor.go index d4890c9539..87ce5290c7 100644 --- a/cmd/kubectl-ate/internal/cmd/revert_actor.go +++ b/cmd/kubectl-ate/internal/cmd/revert_actor.go @@ -17,10 +17,10 @@ package cmd import ( "fmt" - "github.com/agent-substrate/substrate/cmd/kubectl-ate/internal/printer" - "github.com/agent-substrate/substrate/internal/resources" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" "github.com/spf13/cobra" + "go.ate.dev/substrate/cmd/kubectl-ate/internal/printer" + "go.ate.dev/substrate/internal/resources" + "go.ate.dev/substrate/pkg/proto/ateapipb" ) var revertAtespaceFlag string diff --git a/cmd/kubectl-ate/internal/cmd/root.go b/cmd/kubectl-ate/internal/cmd/root.go index e4d1ea1a07..14e8db801c 100644 --- a/cmd/kubectl-ate/internal/cmd/root.go +++ b/cmd/kubectl-ate/internal/cmd/root.go @@ -23,8 +23,8 @@ import ( "github.com/spf13/cobra" "google.golang.org/grpc/metadata" - "github.com/agent-substrate/substrate/internal/preview" - "github.com/agent-substrate/substrate/internal/version" + "go.ate.dev/substrate/internal/preview" + "go.ate.dev/substrate/internal/version" ) var ( diff --git a/cmd/kubectl-ate/internal/cmd/root_test.go b/cmd/kubectl-ate/internal/cmd/root_test.go index 61f5b86fd3..ae9c65a27b 100644 --- a/cmd/kubectl-ate/internal/cmd/root_test.go +++ b/cmd/kubectl-ate/internal/cmd/root_test.go @@ -20,9 +20,9 @@ import ( "strings" "testing" - "github.com/agent-substrate/substrate/internal/preview" "github.com/google/go-cmp/cmp" "github.com/spf13/cobra" + "go.ate.dev/substrate/internal/preview" "google.golang.org/grpc/metadata" ) diff --git a/cmd/kubectl-ate/internal/cmd/tag.go b/cmd/kubectl-ate/internal/cmd/tag.go index 9f34843457..a99355971a 100644 --- a/cmd/kubectl-ate/internal/cmd/tag.go +++ b/cmd/kubectl-ate/internal/cmd/tag.go @@ -19,9 +19,9 @@ import ( "fmt" "strings" - "github.com/agent-substrate/substrate/cmd/kubectl-ate/internal/printer" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" "github.com/spf13/cobra" + "go.ate.dev/substrate/cmd/kubectl-ate/internal/printer" + "go.ate.dev/substrate/pkg/proto/ateapipb" "google.golang.org/grpc" ) diff --git a/cmd/kubectl-ate/internal/cmd/tag_test.go b/cmd/kubectl-ate/internal/cmd/tag_test.go index fe63752ea5..52c3053e40 100644 --- a/cmd/kubectl-ate/internal/cmd/tag_test.go +++ b/cmd/kubectl-ate/internal/cmd/tag_test.go @@ -18,8 +18,8 @@ import ( "context" "testing" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" "github.com/google/go-cmp/cmp" + "go.ate.dev/substrate/pkg/proto/ateapipb" "google.golang.org/grpc" "google.golang.org/protobuf/proto" "google.golang.org/protobuf/testing/protocmp" diff --git a/cmd/kubectl-ate/internal/cmd/verbose.go b/cmd/kubectl-ate/internal/cmd/verbose.go index 0a718a6218..c75ec935b6 100644 --- a/cmd/kubectl-ate/internal/cmd/verbose.go +++ b/cmd/kubectl-ate/internal/cmd/verbose.go @@ -23,8 +23,8 @@ import ( "sync" "time" - "github.com/agent-substrate/substrate/internal/ateclient" "github.com/spf13/cobra" + "go.ate.dev/substrate/internal/ateclient" "google.golang.org/grpc" "google.golang.org/protobuf/encoding/protojson" "google.golang.org/protobuf/proto" diff --git a/cmd/kubectl-ate/internal/cmd/verbose_test.go b/cmd/kubectl-ate/internal/cmd/verbose_test.go index c036ae93fd..d161d40b9c 100644 --- a/cmd/kubectl-ate/internal/cmd/verbose_test.go +++ b/cmd/kubectl-ate/internal/cmd/verbose_test.go @@ -24,8 +24,8 @@ import ( "testing" "time" - "github.com/agent-substrate/substrate/internal/resources" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "go.ate.dev/substrate/internal/resources" + "go.ate.dev/substrate/pkg/proto/ateapipb" "google.golang.org/grpc" "google.golang.org/protobuf/proto" corev1 "k8s.io/api/core/v1" diff --git a/cmd/kubectl-ate/internal/cmd/worker.go b/cmd/kubectl-ate/internal/cmd/worker.go index cd499985dc..518ab4bb38 100644 --- a/cmd/kubectl-ate/internal/cmd/worker.go +++ b/cmd/kubectl-ate/internal/cmd/worker.go @@ -19,10 +19,10 @@ import ( "fmt" "io" - "github.com/agent-substrate/substrate/cmd/kubectl-ate/internal/printer" - "github.com/agent-substrate/substrate/internal/ateclient" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" "github.com/spf13/cobra" + "go.ate.dev/substrate/cmd/kubectl-ate/internal/printer" + "go.ate.dev/substrate/internal/ateclient" + "go.ate.dev/substrate/pkg/proto/ateapipb" "google.golang.org/grpc" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/resource" diff --git a/cmd/kubectl-ate/internal/cmd/worker_test.go b/cmd/kubectl-ate/internal/cmd/worker_test.go index 04a33d8bc3..adc5c39d8f 100644 --- a/cmd/kubectl-ate/internal/cmd/worker_test.go +++ b/cmd/kubectl-ate/internal/cmd/worker_test.go @@ -21,9 +21,9 @@ import ( "testing" "time" - "github.com/agent-substrate/substrate/cmd/kubectl-ate/internal/printer" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" "github.com/google/go-cmp/cmp" + "go.ate.dev/substrate/cmd/kubectl-ate/internal/printer" + "go.ate.dev/substrate/pkg/proto/ateapipb" "google.golang.org/grpc" "google.golang.org/protobuf/types/known/timestamppb" corev1 "k8s.io/api/core/v1" diff --git a/cmd/kubectl-ate/internal/printer/egress_policy.go b/cmd/kubectl-ate/internal/printer/egress_policy.go index 153b0cc77f..7af33d316e 100644 --- a/cmd/kubectl-ate/internal/printer/egress_policy.go +++ b/cmd/kubectl-ate/internal/printer/egress_policy.go @@ -19,7 +19,7 @@ import ( "io" "text/tabwriter" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "go.ate.dev/substrate/pkg/proto/ateapipb" ) // PrintEgressPolicyTo prints one actor's egress policy. json and yaml emit the diff --git a/cmd/kubectl-ate/internal/printer/egress_policy_test.go b/cmd/kubectl-ate/internal/printer/egress_policy_test.go index 58c1c2b97d..e1ee36cd67 100644 --- a/cmd/kubectl-ate/internal/printer/egress_policy_test.go +++ b/cmd/kubectl-ate/internal/printer/egress_policy_test.go @@ -19,8 +19,8 @@ import ( "testing" "time" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" "github.com/google/go-cmp/cmp" + "go.ate.dev/substrate/pkg/proto/ateapipb" "google.golang.org/protobuf/types/known/timestamppb" ) diff --git a/cmd/kubectl-ate/internal/printer/printer.go b/cmd/kubectl-ate/internal/printer/printer.go index f77cbcecf6..7b7cf83412 100644 --- a/cmd/kubectl-ate/internal/printer/printer.go +++ b/cmd/kubectl-ate/internal/printer/printer.go @@ -24,7 +24,7 @@ import ( "text/tabwriter" "time" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "go.ate.dev/substrate/pkg/proto/ateapipb" "google.golang.org/protobuf/encoding/protojson" "google.golang.org/protobuf/proto" "google.golang.org/protobuf/types/known/timestamppb" diff --git a/cmd/kubectl-ate/internal/printer/printer_test.go b/cmd/kubectl-ate/internal/printer/printer_test.go index 08c9b76366..fb78ec1261 100644 --- a/cmd/kubectl-ate/internal/printer/printer_test.go +++ b/cmd/kubectl-ate/internal/printer/printer_test.go @@ -19,8 +19,8 @@ import ( "testing" "time" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" "github.com/google/go-cmp/cmp" + "go.ate.dev/substrate/pkg/proto/ateapipb" "google.golang.org/protobuf/types/known/timestamppb" ) diff --git a/cmd/kubectl-ate/main.go b/cmd/kubectl-ate/main.go index 1adface558..3109c5e745 100644 --- a/cmd/kubectl-ate/main.go +++ b/cmd/kubectl-ate/main.go @@ -15,7 +15,7 @@ package main import ( - "github.com/agent-substrate/substrate/cmd/kubectl-ate/internal/cmd" + "go.ate.dev/substrate/cmd/kubectl-ate/internal/cmd" ) func main() { diff --git a/cmd/objectstore-plugin/main.go b/cmd/objectstore-plugin/main.go index 1da25a553c..6aa3469f5c 100644 --- a/cmd/objectstore-plugin/main.go +++ b/cmd/objectstore-plugin/main.go @@ -31,15 +31,15 @@ import ( "time" "cloud.google.com/go/storage" - "github.com/agent-substrate/substrate/internal/nodepath" - "github.com/agent-substrate/substrate/internal/objectstore" - "github.com/agent-substrate/substrate/internal/objectstoreplugin" - "github.com/agent-substrate/substrate/internal/pluginsocket" - "github.com/agent-substrate/substrate/internal/preview" - "github.com/agent-substrate/substrate/internal/serverboot" - "github.com/agent-substrate/substrate/pkg/objectstorage" - objectstorev1 "github.com/agent-substrate/substrate/pkg/proto/objectstorepb/v1" "github.com/spf13/pflag" + "go.ate.dev/substrate/internal/nodepath" + "go.ate.dev/substrate/internal/objectstore" + "go.ate.dev/substrate/internal/objectstoreplugin" + "go.ate.dev/substrate/internal/pluginsocket" + "go.ate.dev/substrate/internal/preview" + "go.ate.dev/substrate/internal/serverboot" + "go.ate.dev/substrate/pkg/objectstorage" + objectstorev1 "go.ate.dev/substrate/pkg/proto/objectstorepb/v1" "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc" "google.golang.org/grpc" "google.golang.org/grpc/health" diff --git a/cmd/podcertcontroller/internal/podidentitysigner/podidentitysigner.go b/cmd/podcertcontroller/internal/podidentitysigner/podidentitysigner.go index b253468558..50ab670afb 100644 --- a/cmd/podcertcontroller/internal/podidentitysigner/podidentitysigner.go +++ b/cmd/podcertcontroller/internal/podidentitysigner/podidentitysigner.go @@ -26,10 +26,10 @@ import ( "path" "time" - "github.com/agent-substrate/substrate/cmd/podcertcontroller/internal/podcertificate" - "github.com/agent-substrate/substrate/cmd/podcertcontroller/internal/signercontroller" - "github.com/agent-substrate/substrate/internal/localca" - "github.com/agent-substrate/substrate/internal/substratex509" + "go.ate.dev/substrate/cmd/podcertcontroller/internal/podcertificate" + "go.ate.dev/substrate/cmd/podcertcontroller/internal/signercontroller" + "go.ate.dev/substrate/internal/localca" + "go.ate.dev/substrate/internal/substratex509" certsv1 "k8s.io/api/certificates/v1" certsv1beta1 "k8s.io/api/certificates/v1beta1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" diff --git a/cmd/podcertcontroller/internal/podidentitysigner/podidentitysigner_test.go b/cmd/podcertcontroller/internal/podidentitysigner/podidentitysigner_test.go index cf14602149..5254c3c14a 100644 --- a/cmd/podcertcontroller/internal/podidentitysigner/podidentitysigner_test.go +++ b/cmd/podcertcontroller/internal/podidentitysigner/podidentitysigner_test.go @@ -27,9 +27,9 @@ import ( "testing/synctest" "time" - "github.com/agent-substrate/substrate/cmd/podcertcontroller/internal/podcertificate" - "github.com/agent-substrate/substrate/internal/localca" - "github.com/agent-substrate/substrate/internal/substratex509" + "go.ate.dev/substrate/cmd/podcertcontroller/internal/podcertificate" + "go.ate.dev/substrate/internal/localca" + "go.ate.dev/substrate/internal/substratex509" certsv1beta1 "k8s.io/api/certificates/v1beta1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" diff --git a/cmd/podcertcontroller/internal/postgressigner/postgressigner.go b/cmd/podcertcontroller/internal/postgressigner/postgressigner.go index a29675970d..cd9c2fb503 100644 --- a/cmd/podcertcontroller/internal/postgressigner/postgressigner.go +++ b/cmd/podcertcontroller/internal/postgressigner/postgressigner.go @@ -24,10 +24,10 @@ import ( "fmt" "time" - "github.com/agent-substrate/substrate/cmd/podcertcontroller/internal/podcertificate" - "github.com/agent-substrate/substrate/cmd/podcertcontroller/internal/signercontroller" - "github.com/agent-substrate/substrate/internal/localca" - "github.com/agent-substrate/substrate/pkg/postgressetup" + "go.ate.dev/substrate/cmd/podcertcontroller/internal/podcertificate" + "go.ate.dev/substrate/cmd/podcertcontroller/internal/signercontroller" + "go.ate.dev/substrate/internal/localca" + "go.ate.dev/substrate/pkg/postgressetup" certsv1 "k8s.io/api/certificates/v1" certsv1beta1 "k8s.io/api/certificates/v1beta1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" diff --git a/cmd/podcertcontroller/internal/postgressigner/postgressigner_test.go b/cmd/podcertcontroller/internal/postgressigner/postgressigner_test.go index 78e60e8119..979de34f16 100644 --- a/cmd/podcertcontroller/internal/postgressigner/postgressigner_test.go +++ b/cmd/podcertcontroller/internal/postgressigner/postgressigner_test.go @@ -25,10 +25,10 @@ import ( "testing" "time" - "github.com/agent-substrate/substrate/cmd/podcertcontroller/internal/podcertificate" - "github.com/agent-substrate/substrate/internal/installdefaults" - "github.com/agent-substrate/substrate/internal/localca" - "github.com/agent-substrate/substrate/pkg/postgressetup" + "go.ate.dev/substrate/cmd/podcertcontroller/internal/podcertificate" + "go.ate.dev/substrate/internal/installdefaults" + "go.ate.dev/substrate/internal/localca" + "go.ate.dev/substrate/pkg/postgressetup" certsv1beta1 "k8s.io/api/certificates/v1beta1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" diff --git a/cmd/podcertcontroller/internal/servicednssigner/servicednssigner.go b/cmd/podcertcontroller/internal/servicednssigner/servicednssigner.go index d482f3ed23..9e57142828 100644 --- a/cmd/podcertcontroller/internal/servicednssigner/servicednssigner.go +++ b/cmd/podcertcontroller/internal/servicednssigner/servicednssigner.go @@ -22,9 +22,9 @@ import ( "fmt" "time" - "github.com/agent-substrate/substrate/cmd/podcertcontroller/internal/podcertificate" - "github.com/agent-substrate/substrate/cmd/podcertcontroller/internal/signercontroller" - "github.com/agent-substrate/substrate/internal/localca" + "go.ate.dev/substrate/cmd/podcertcontroller/internal/podcertificate" + "go.ate.dev/substrate/cmd/podcertcontroller/internal/signercontroller" + "go.ate.dev/substrate/internal/localca" certsv1 "k8s.io/api/certificates/v1" certsv1beta1 "k8s.io/api/certificates/v1beta1" corev1 "k8s.io/api/core/v1" diff --git a/cmd/podcertcontroller/internal/signercontroller/signercontroller.go b/cmd/podcertcontroller/internal/signercontroller/signercontroller.go index b768086247..97cfa16dd1 100644 --- a/cmd/podcertcontroller/internal/signercontroller/signercontroller.go +++ b/cmd/podcertcontroller/internal/signercontroller/signercontroller.go @@ -21,9 +21,9 @@ import ( "log/slog" "time" - "github.com/agent-substrate/substrate/cmd/podcertcontroller/internal/podcertificate" - "github.com/agent-substrate/substrate/cmd/podcertcontroller/internal/rendezvous" - "github.com/agent-substrate/substrate/internal/clustertrustbundle" + "go.ate.dev/substrate/cmd/podcertcontroller/internal/podcertificate" + "go.ate.dev/substrate/cmd/podcertcontroller/internal/rendezvous" + "go.ate.dev/substrate/internal/clustertrustbundle" certsv1 "k8s.io/api/certificates/v1" certsv1beta1 "k8s.io/api/certificates/v1beta1" apiequality "k8s.io/apimachinery/pkg/api/equality" diff --git a/cmd/podcertcontroller/internal/signercontroller/signercontroller_test.go b/cmd/podcertcontroller/internal/signercontroller/signercontroller_test.go index 2b89cd7aa3..01490f45a0 100644 --- a/cmd/podcertcontroller/internal/signercontroller/signercontroller_test.go +++ b/cmd/podcertcontroller/internal/signercontroller/signercontroller_test.go @@ -18,7 +18,7 @@ import ( "context" "testing" - "github.com/agent-substrate/substrate/internal/clustertrustbundle" + "go.ate.dev/substrate/internal/clustertrustbundle" certsv1 "k8s.io/api/certificates/v1" certsv1beta1 "k8s.io/api/certificates/v1beta1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" diff --git a/cmd/podcertcontroller/main.go b/cmd/podcertcontroller/main.go index 6a240bae9a..cac621bdb1 100644 --- a/cmd/podcertcontroller/main.go +++ b/cmd/podcertcontroller/main.go @@ -31,18 +31,18 @@ import ( "path/filepath" "syscall" - "github.com/agent-substrate/substrate/cmd/podcertcontroller/internal/podcertificate" - "github.com/agent-substrate/substrate/cmd/podcertcontroller/internal/podidentitysigner" - "github.com/agent-substrate/substrate/cmd/podcertcontroller/internal/postgressigner" - "github.com/agent-substrate/substrate/cmd/podcertcontroller/internal/rendezvous" - "github.com/agent-substrate/substrate/cmd/podcertcontroller/internal/servicednssigner" - "github.com/agent-substrate/substrate/cmd/podcertcontroller/internal/signercontroller" - "github.com/agent-substrate/substrate/internal/clustertrustbundle" - "github.com/agent-substrate/substrate/internal/installdefaults" - "github.com/agent-substrate/substrate/internal/localca" - "github.com/agent-substrate/substrate/internal/preview" - "github.com/agent-substrate/substrate/internal/version" "github.com/spf13/pflag" + "go.ate.dev/substrate/cmd/podcertcontroller/internal/podcertificate" + "go.ate.dev/substrate/cmd/podcertcontroller/internal/podidentitysigner" + "go.ate.dev/substrate/cmd/podcertcontroller/internal/postgressigner" + "go.ate.dev/substrate/cmd/podcertcontroller/internal/rendezvous" + "go.ate.dev/substrate/cmd/podcertcontroller/internal/servicednssigner" + "go.ate.dev/substrate/cmd/podcertcontroller/internal/signercontroller" + "go.ate.dev/substrate/internal/clustertrustbundle" + "go.ate.dev/substrate/internal/installdefaults" + "go.ate.dev/substrate/internal/localca" + "go.ate.dev/substrate/internal/preview" + "go.ate.dev/substrate/internal/version" "k8s.io/apimachinery/pkg/types" "k8s.io/client-go/kubernetes" "k8s.io/client-go/rest" diff --git a/demos/autoscaled-workerpool/autoscaled-workerpool-template.yaml.tmpl b/demos/autoscaled-workerpool/autoscaled-workerpool-template.yaml.tmpl index d870abf04a..04ac32f934 100644 --- a/demos/autoscaled-workerpool/autoscaled-workerpool-template.yaml.tmpl +++ b/demos/autoscaled-workerpool/autoscaled-workerpool-template.yaml.tmpl @@ -25,7 +25,7 @@ workerSelector: workload: counter-autoscaled containers: - name: counter - image: ko://github.com/agent-substrate/substrate/demos/counter + image: ko://go.ate.dev/substrate/demos/counter command: - /ko-app/counter wakeupProbe: diff --git a/demos/autoscaled-workerpool/autoscaled-workerpool.yaml.tmpl b/demos/autoscaled-workerpool/autoscaled-workerpool.yaml.tmpl index 04aa6672ec..b60beb231f 100644 --- a/demos/autoscaled-workerpool/autoscaled-workerpool.yaml.tmpl +++ b/demos/autoscaled-workerpool/autoscaled-workerpool.yaml.tmpl @@ -37,6 +37,6 @@ metadata: workload: counter-autoscaled spec: replicas: 5 - workerImage: ko://github.com/agent-substrate/substrate/cmd/ateworker-gvisor + workerImage: ko://go.ate.dev/substrate/cmd/ateworker-gvisor sandboxClasses: - name: gvisor diff --git a/demos/claude-code-multiplex/claude-code-multiplex.yaml.tmpl b/demos/claude-code-multiplex/claude-code-multiplex.yaml.tmpl index 5267e91017..005a859c88 100644 --- a/demos/claude-code-multiplex/claude-code-multiplex.yaml.tmpl +++ b/demos/claude-code-multiplex/claude-code-multiplex.yaml.tmpl @@ -34,6 +34,6 @@ metadata: workload: claude-multiplex spec: replicas: 2 - workerImage: ko://github.com/agent-substrate/substrate/cmd/ateworker-gvisor + workerImage: ko://go.ate.dev/substrate/cmd/ateworker-gvisor sandboxClasses: - name: gvisor diff --git a/demos/claude-code-multiplex/ui/server.go b/demos/claude-code-multiplex/ui/server.go index c0881e9d2f..734f0d9e10 100644 --- a/demos/claude-code-multiplex/ui/server.go +++ b/demos/claude-code-multiplex/ui/server.go @@ -58,8 +58,8 @@ import ( "sync" "time" - "github.com/agent-substrate/substrate/internal/ateclient" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "go.ate.dev/substrate/internal/ateclient" + "go.ate.dev/substrate/pkg/proto/ateapipb" corev1 "k8s.io/api/core/v1" "k8s.io/client-go/kubernetes" "k8s.io/client-go/rest" diff --git a/demos/counter/counter-microvm-csi-test-template.yaml b/demos/counter/counter-microvm-csi-test-template.yaml index fd20354aae..293d0c407d 100644 --- a/demos/counter/counter-microvm-csi-test-template.yaml +++ b/demos/counter/counter-microvm-csi-test-template.yaml @@ -24,7 +24,7 @@ workerSelector: workload: counter-microvm-csi containers: - name: counter - image: ko://github.com/agent-substrate/substrate/demos/counter + image: ko://go.ate.dev/substrate/demos/counter wakeupProbe: httpGet: path: /readyz diff --git a/demos/counter/counter-microvm-csi-test.yaml b/demos/counter/counter-microvm-csi-test.yaml index 55bfbac1b9..06916b100c 100644 --- a/demos/counter/counter-microvm-csi-test.yaml +++ b/demos/counter/counter-microvm-csi-test.yaml @@ -34,4 +34,4 @@ spec: replicas: 1 sandboxClasses: - name: microvm - workerImage: ko://github.com/agent-substrate/substrate/cmd/ateworker-microvm + workerImage: ko://go.ate.dev/substrate/cmd/ateworker-microvm diff --git a/demos/counter/counter-microvm-template.yaml.tmpl b/demos/counter/counter-microvm-template.yaml.tmpl index 77d67fdcc5..e694e3d11d 100644 --- a/demos/counter/counter-microvm-template.yaml.tmpl +++ b/demos/counter/counter-microvm-template.yaml.tmpl @@ -24,7 +24,7 @@ workerSelector: workload: counter-microvm containers: - name: counter - image: ko://github.com/agent-substrate/substrate/demos/counter + image: ko://go.ate.dev/substrate/demos/counter command: - /ko-app/counter # Exercises atenet-router's arbitrary-port ingress support: a second diff --git a/demos/counter/counter-microvm.yaml.tmpl b/demos/counter/counter-microvm.yaml.tmpl index ba7b252886..a3b33e23c5 100644 --- a/demos/counter/counter-microvm.yaml.tmpl +++ b/demos/counter/counter-microvm.yaml.tmpl @@ -43,7 +43,7 @@ spec: - name: microvm configRef: name: microvm - workerImage: ko://github.com/agent-substrate/substrate/cmd/ateworker-microvm + workerImage: ko://go.ate.dev/substrate/cmd/ateworker-microvm template: # Per-worker resources: these size the worker POD and advertise its # scheduling capacity (the per-actor ceiling). The micro-VM sandbox itself is diff --git a/demos/counter/counter-template.yaml.tmpl b/demos/counter/counter-template.yaml.tmpl index 7ab64ca520..9aca738597 100644 --- a/demos/counter/counter-template.yaml.tmpl +++ b/demos/counter/counter-template.yaml.tmpl @@ -27,7 +27,7 @@ workerSelector: workload: counter containers: - name: counter - image: ko://github.com/agent-substrate/substrate/demos/counter + image: ko://go.ate.dev/substrate/demos/counter command: - /ko-app/counter # Exercises atenet-router's arbitrary-port ingress support: a second diff --git a/demos/counter/counter.yaml.tmpl b/demos/counter/counter.yaml.tmpl index 9919666b35..007016795e 100644 --- a/demos/counter/counter.yaml.tmpl +++ b/demos/counter/counter.yaml.tmpl @@ -37,7 +37,7 @@ spec: # pool that cannot place every replica is worse than a smaller one -- the # deploy blocks on a rollout that never completes. replicas: 3 - workerImage: ko://github.com/agent-substrate/substrate/cmd/ateworker-gvisor + workerImage: ko://go.ate.dev/substrate/cmd/ateworker-gvisor # No configRef: the pool follows the class default, the newest SandboxConfig # of the class annotated sandboxconfig.ate.dev/is-class-default=true # (gvisor-default as installed), and moves to a new default when one appears. diff --git a/demos/egress/egress-microvm-template.yaml.tmpl b/demos/egress/egress-microvm-template.yaml.tmpl index ad4400f831..dc0e70a9ff 100644 --- a/demos/egress/egress-microvm-template.yaml.tmpl +++ b/demos/egress/egress-microvm-template.yaml.tmpl @@ -32,7 +32,7 @@ workerSelector: workload: egress-microvm containers: - name: egress - image: ko://github.com/agent-substrate/substrate/demos/egress + image: ko://go.ate.dev/substrate/demos/egress command: ["/ko-app/egress"] # The demo fetches with a stock net/http client. SSL_CERT_FILE adds the # gateway CA, and Go still scans /etc/ssl/certs, so the image's public diff --git a/demos/egress/egress-microvm.yaml.tmpl b/demos/egress/egress-microvm.yaml.tmpl index 7f515c9b70..e45313d9b9 100644 --- a/demos/egress/egress-microvm.yaml.tmpl +++ b/demos/egress/egress-microvm.yaml.tmpl @@ -38,7 +38,7 @@ spec: replicas: 2 sandboxClasses: - name: microvm - workerImage: ko://github.com/agent-substrate/substrate/cmd/ateworker-microvm + workerImage: ko://go.ate.dev/substrate/cmd/ateworker-microvm # No template.resources, unlike the counter demo's micro-VM variant: an # unlimited worker container reports no capacity, which the scheduler reads as # unconstrained, and the pod requests nothing so it stays placeable next to diff --git a/demos/egress/egress-template.yaml.tmpl b/demos/egress/egress-template.yaml.tmpl index 81271c18af..09cf6b9775 100644 --- a/demos/egress/egress-template.yaml.tmpl +++ b/demos/egress/egress-template.yaml.tmpl @@ -30,7 +30,7 @@ workerSelector: workload: egress containers: - name: egress - image: ko://github.com/agent-substrate/substrate/demos/egress + image: ko://go.ate.dev/substrate/demos/egress command: ["/ko-app/egress"] env: - name: SSL_CERT_DIR diff --git a/demos/egress/egress.yaml.tmpl b/demos/egress/egress.yaml.tmpl index 09a798a4c9..234ad18330 100644 --- a/demos/egress/egress.yaml.tmpl +++ b/demos/egress/egress.yaml.tmpl @@ -33,6 +33,6 @@ metadata: workload: egress spec: replicas: 2 - workerImage: ko://github.com/agent-substrate/substrate/cmd/ateworker-gvisor + workerImage: ko://go.ate.dev/substrate/cmd/ateworker-gvisor sandboxClasses: - name: gvisor diff --git a/demos/egress/main.go b/demos/egress/main.go index da40483663..4e32cdedec 100644 --- a/demos/egress/main.go +++ b/demos/egress/main.go @@ -42,7 +42,7 @@ import ( "google.golang.org/grpc/credentials/insecure" grpcstatus "google.golang.org/grpc/status" - "github.com/agent-substrate/substrate/internal/proto/grpcechopb" + "go.ate.dev/substrate/internal/proto/grpcechopb" ) const ( diff --git a/demos/egress/main_test.go b/demos/egress/main_test.go index 82c1dc72d8..80778bbcdc 100644 --- a/demos/egress/main_test.go +++ b/demos/egress/main_test.go @@ -34,8 +34,8 @@ import ( "google.golang.org/grpc" "google.golang.org/grpc/codes" - "github.com/agent-substrate/substrate/internal/proto/grpcechopb" - "github.com/agent-substrate/substrate/internal/testcert" + "go.ate.dev/substrate/internal/proto/grpcechopb" + "go.ate.dev/substrate/internal/testcert" ) func TestFetch(t *testing.T) { diff --git a/demos/jupyter/jupyter.yaml.tmpl b/demos/jupyter/jupyter.yaml.tmpl index fb9fcaef2b..e8382d39e8 100644 --- a/demos/jupyter/jupyter.yaml.tmpl +++ b/demos/jupyter/jupyter.yaml.tmpl @@ -32,7 +32,7 @@ metadata: workload: jupyter spec: replicas: 1 - workerImage: ko://github.com/agent-substrate/substrate/cmd/ateworker-gvisor + workerImage: ko://go.ate.dev/substrate/cmd/ateworker-gvisor sandboxClasses: - name: gvisor diff --git a/demos/multi-template/counter-template.yaml.tmpl b/demos/multi-template/counter-template.yaml.tmpl index c37a608dc4..a84a2469fe 100644 --- a/demos/multi-template/counter-template.yaml.tmpl +++ b/demos/multi-template/counter-template.yaml.tmpl @@ -26,7 +26,7 @@ workerSelector: workload: multi-template-shared containers: - name: counter - image: ko://github.com/agent-substrate/substrate/demos/counter + image: ko://go.ate.dev/substrate/demos/counter # Sandbox size. The pool sets no worker limits, so the worker offers the # node's allocatable and these only need to fit the node. resources: diff --git a/demos/multi-template/fspersist-template.yaml.tmpl b/demos/multi-template/fspersist-template.yaml.tmpl index 8701ca002d..9ceb6881b2 100644 --- a/demos/multi-template/fspersist-template.yaml.tmpl +++ b/demos/multi-template/fspersist-template.yaml.tmpl @@ -26,7 +26,7 @@ workerSelector: workload: multi-template-shared containers: - name: fspersist - image: ko://github.com/agent-substrate/substrate/demos/multi-template/fspersist + image: ko://go.ate.dev/substrate/demos/multi-template/fspersist # Sandbox size. The pool sets no worker limits, so the worker offers the # node's allocatable and these only need to fit the node. resources: diff --git a/demos/multi-template/multi-template.yaml.tmpl b/demos/multi-template/multi-template.yaml.tmpl index d24aa04946..b4465c20f5 100644 --- a/demos/multi-template/multi-template.yaml.tmpl +++ b/demos/multi-template/multi-template.yaml.tmpl @@ -36,6 +36,6 @@ metadata: workload: multi-template-shared spec: replicas: 3 - workerImage: ko://github.com/agent-substrate/substrate/cmd/ateworker-gvisor + workerImage: ko://go.ate.dev/substrate/cmd/ateworker-gvisor sandboxClasses: - name: gvisor diff --git a/demos/parking/parking-template.yaml.tmpl b/demos/parking/parking-template.yaml.tmpl index e926fd9e21..eac6c76796 100644 --- a/demos/parking/parking-template.yaml.tmpl +++ b/demos/parking/parking-template.yaml.tmpl @@ -24,7 +24,7 @@ workerSelector: workload: parking containers: - name: counter - image: ko://github.com/agent-substrate/substrate/demos/counter + image: ko://go.ate.dev/substrate/demos/counter command: ["/ko-app/counter"] # Sandbox size. The pool sets no worker limits, so the worker offers the # node's allocatable and these only need to fit the node. diff --git a/demos/parking/parking.yaml.tmpl b/demos/parking/parking.yaml.tmpl index 9c908f093e..16a01db8ca 100644 --- a/demos/parking/parking.yaml.tmpl +++ b/demos/parking/parking.yaml.tmpl @@ -43,6 +43,6 @@ spec: # Intentionally small: 2 workers, so 3+ concurrently-active actors saturate # the pool and exercise the parking path. replicas: 2 - workerImage: ko://github.com/agent-substrate/substrate/cmd/ateworker-gvisor + workerImage: ko://go.ate.dev/substrate/cmd/ateworker-gvisor sandboxClasses: - name: gvisor diff --git a/demos/sandbox/client/main.go b/demos/sandbox/client/main.go index 65a8cfc953..05dfdf3ed5 100644 --- a/demos/sandbox/client/main.go +++ b/demos/sandbox/client/main.go @@ -28,10 +28,10 @@ import ( "strings" "syscall" - "github.com/agent-substrate/substrate/internal/atenet" - "github.com/agent-substrate/substrate/internal/resources" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" "github.com/spf13/pflag" + "go.ate.dev/substrate/internal/atenet" + "go.ate.dev/substrate/internal/resources" + "go.ate.dev/substrate/pkg/proto/ateapipb" "google.golang.org/grpc" "google.golang.org/grpc/credentials" ) diff --git a/demos/sandbox/manual-test-multi-template.yaml b/demos/sandbox/manual-test-multi-template.yaml index 3adaa45c92..3811c37684 100644 --- a/demos/sandbox/manual-test-multi-template.yaml +++ b/demos/sandbox/manual-test-multi-template.yaml @@ -24,7 +24,7 @@ workerSelector: workload: sandbox containers: - name: sandbox - image: ko://github.com/agent-substrate/substrate/demos/sandbox + image: ko://go.ate.dev/substrate/demos/sandbox command: ["/ko-app/sandbox"] env: - name: PORT diff --git a/demos/sandbox/manual-test-multi.yaml b/demos/sandbox/manual-test-multi.yaml index 3b19ff5213..5dbb69627b 100644 --- a/demos/sandbox/manual-test-multi.yaml +++ b/demos/sandbox/manual-test-multi.yaml @@ -30,6 +30,6 @@ metadata: workload: sandbox spec: replicas: 1 - workerImage: ko://github.com/agent-substrate/substrate/cmd/ateworker-gvisor + workerImage: ko://go.ate.dev/substrate/cmd/ateworker-gvisor sandboxClasses: - name: gvisor diff --git a/demos/sandbox/sandbox-template.yaml.tmpl b/demos/sandbox/sandbox-template.yaml.tmpl index 7ab19834d9..c638376bb7 100644 --- a/demos/sandbox/sandbox-template.yaml.tmpl +++ b/demos/sandbox/sandbox-template.yaml.tmpl @@ -24,7 +24,7 @@ workerSelector: workload: sandbox containers: - name: sandbox - image: ko://github.com/agent-substrate/substrate/demos/sandbox + image: ko://go.ate.dev/substrate/demos/sandbox env: - name: PORT value: "80" diff --git a/demos/sandbox/sandbox.yaml.tmpl b/demos/sandbox/sandbox.yaml.tmpl index 6b60b6a152..d51fe8ff9b 100644 --- a/demos/sandbox/sandbox.yaml.tmpl +++ b/demos/sandbox/sandbox.yaml.tmpl @@ -30,7 +30,7 @@ metadata: workload: sandbox spec: replicas: 2 - workerImage: ko://github.com/agent-substrate/substrate/cmd/ateworker-gvisor + workerImage: ko://go.ate.dev/substrate/cmd/ateworker-gvisor sandboxClasses: - name: gvisor template: diff --git a/docs/api-guide.md b/docs/api-guide.md index 70afb02311..9a37e48292 100644 --- a/docs/api-guide.md +++ b/docs/api-guide.md @@ -11,7 +11,7 @@ The `WorkerPool` defines the pool of physical "warm" compute capacity. It manage | Field | Type | Description | | :--- | :--- | :--- | | `replicas` | `int32` | **Required.** Number of physical standby pods to maintain in the cluster. | -| `workerImage` | `string` | **Required.** The container image for the `ateworker` herder process (e.g. `ko://github.com/agent-substrate/substrate/cmd/ateworker-gvisor`). | +| `workerImage` | `string` | **Required.** The container image for the `ateworker` herder process (e.g. `ko://go.ate.dev/substrate/cmd/ateworker-gvisor`). | | `sandboxClasses` | `[]WorkerPoolSandboxClass` | **Required.** The sandbox runtime families the pool runs; exactly one entry today. Each entry has `name` (**required**, `gvisor` or `microvm`, immutable), which drives the worker pod shape (e.g. KVM device mounts, node placement), and `configRef.name` (optional), which names a cluster-scoped [`SandboxConfig`](#3-sandboxconfig-the-sandbox-itself) of the same class. Without `configRef`, the pool uses the class default (see [Class default](#class-default)). The controller records the result in `status.sandboxClasses`. The sandbox binaries an actor boots with still come from the `SandboxConfig` its `ActorTemplate` selects. | | `template` | `WorkerPoolPodTemplate` | **Optional.** Metadata, scheduling, and resource settings for worker workloads. | @@ -57,7 +57,7 @@ metadata: workload: secret-agent spec: replicas: 10 - workerImage: ko://github.com/agent-substrate/substrate/cmd/ateworker-gvisor + workerImage: ko://go.ate.dev/substrate/cmd/ateworker-gvisor sandboxClasses: - name: gvisor configRef: diff --git a/docs/csi-volumes.md b/docs/csi-volumes.md index 59b77af85f..9997644c4e 100644 --- a/docs/csi-volumes.md +++ b/docs/csi-volumes.md @@ -161,7 +161,7 @@ metadata: workload: stateful-agent spec: replicas: 5 - workerImage: ko://github.com/agent-substrate/substrate/cmd/ateworker-gvisor + workerImage: ko://go.ate.dev/substrate/cmd/ateworker-gvisor sandboxClasses: - name: gvisor ``` diff --git a/docs/dev/best-practices/tracing.md b/docs/dev/best-practices/tracing.md index 3f9ffd9eee..a13aa2a9a9 100644 --- a/docs/dev/best-practices/tracing.md +++ b/docs/dev/best-practices/tracing.md @@ -82,7 +82,7 @@ address for whichever environment it is deployed to: ```yaml containers: - name: ateapi - image: ko://github.com/agent-substrate/substrate/cmd/ateapi + image: ko://go.ate.dev/substrate/cmd/ateapi ports: - containerPort: 443 # Supplies OTEL_EXPORTER_OTLP_ENDPOINT (and, on kind, the metric diff --git a/docs/dev/code-layout.md b/docs/dev/code-layout.md index a527f0aa7f..b06ce2f72e 100644 --- a/docs/dev/code-layout.md +++ b/docs/dev/code-layout.md @@ -26,12 +26,12 @@ substrate/ **`pkg/`** is for Go packages with an external API contract — code that users or third-party tools may import directly from -`github.com/agent-substrate/substrate/pkg/...`. Putting a package here signals +`go.ate.dev/substrate/pkg/...`. Putting a package here signals a commitment to backwards-compatibility and discoverability. **`internal/`** is for Go packages that are shared across multiple binaries within this module but are not part of any external API. The Go toolchain -enforces that nothing outside `github.com/agent-substrate/substrate` can import +enforces that nothing outside `go.ate.dev/substrate` can import these. The exception is [`internal/plugins/`](../../internal/plugins/README.md): self-contained plugins, each a Go module of its own, in a temporary location that nothing else in the repo should depend on or import. diff --git a/docs/observability.md b/docs/observability.md index 36b868560f..de0d219eb8 100644 --- a/docs/observability.md +++ b/docs/observability.md @@ -222,7 +222,7 @@ Three `event.name` values, which is the OTLP LogRecord's own field rather than a A crash is its own name because an event name promises a set of attributes and a crash has a different severity and shape. There is no name per state: `ate.actor.state` already says which transition happened, so a consumer still selects on that one attribute and needs no map from a name to a state. All three names are in [`docs/metrics/registry/events.yaml`](metrics/registry/events.yaml), which `hack/verify-all.sh` checks. -The attributes are the same flat `ate.*` keys as the stdout record, so they arrive as real log attributes with no transform in front of them. Trace context is not among them: it goes on the record's own `TraceId` and `SpanId` fields, where the stdout record's top-level `trace_id`/`span_id` would be mapped to anyway. The instrumentation scope is `github.com/agent-substrate/substrate/internal/actorevent` for every actor event. Select or drop a stream by `event.name`: the lifecycle events must never be sampled, the usage samples may be. +The attributes are the same flat `ate.*` keys as the stdout record, so they arrive as real log attributes with no transform in front of them. Trace context is not among them: it goes on the record's own `TraceId` and `SpanId` fields, where the stdout record's top-level `trace_id`/`span_id` would be mapped to anyway. The instrumentation scope is `go.ate.dev/substrate/internal/actorevent` for every actor event. Select or drop a stream by `event.name`: the lifecycle events must never be sampled, the usage samples may be. **Never sample or filter the lifecycle stream.** A consumer takes the last event for an actor's uid, so one dropped record reports a stale state with no sign that anything is missing. This is the one stream where a sampling policy is a correctness bug rather than a cost trade. diff --git a/go.mod b/go.mod index ec85d0fee5..5322e5a32c 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module github.com/agent-substrate/substrate +module go.ate.dev/substrate go 1.27.0 diff --git a/hack/run-root-tests.sh b/hack/run-root-tests.sh index f9b354f7e9..b8629a1654 100755 --- a/hack/run-root-tests.sh +++ b/hack/run-root-tests.sh @@ -24,7 +24,7 @@ set -o errexit -o nounset -o pipefail ROOT="$(git rev-parse --show-toplevel)" cd "${ROOT}" -MARKER="github.com/agent-substrate/substrate/internal/roottest" +MARKER="go.ate.dev/substrate/internal/roottest" # Look for the marker in both in-package (TestImports) and external _test # package (XTestImports) test files. diff --git a/hack/tools/code-generator/go.mod b/hack/tools/code-generator/go.mod index a6fa2a4ae5..a66da5b8ef 100644 --- a/hack/tools/code-generator/go.mod +++ b/hack/tools/code-generator/go.mod @@ -1,4 +1,4 @@ -module github.com/agent-substrate/substrate/hack/tools/code-generator +module go.ate.dev/substrate/hack/tools/code-generator go 1.27.0 diff --git a/hack/tools/controller-gen/go.mod b/hack/tools/controller-gen/go.mod index 977f2766bf..3c6ea3b272 100644 --- a/hack/tools/controller-gen/go.mod +++ b/hack/tools/controller-gen/go.mod @@ -1,4 +1,4 @@ -module github.com/agent-substrate/substrate/hack/tools/controller-gen +module go.ate.dev/substrate/hack/tools/controller-gen go 1.27.0 diff --git a/hack/tools/go-licenses/go.mod b/hack/tools/go-licenses/go.mod index 432b1753f3..7b23d3742d 100644 --- a/hack/tools/go-licenses/go.mod +++ b/hack/tools/go-licenses/go.mod @@ -1,4 +1,4 @@ -module github.com/agent-substrate/substrate/hack/tools/go-licenses +module go.ate.dev/substrate/hack/tools/go-licenses go 1.27.0 diff --git a/hack/tools/golangci-lint/go.mod b/hack/tools/golangci-lint/go.mod index 2593d4f0b0..8f19eb233c 100644 --- a/hack/tools/golangci-lint/go.mod +++ b/hack/tools/golangci-lint/go.mod @@ -1,4 +1,4 @@ -module github.com/agent-substrate/substrate/hack/tools/golangci-lint +module go.ate.dev/substrate/hack/tools/golangci-lint go 1.27.0 diff --git a/hack/tools/gotestsum/go.mod b/hack/tools/gotestsum/go.mod index cf0ca9cfd1..f32423efa8 100644 --- a/hack/tools/gotestsum/go.mod +++ b/hack/tools/gotestsum/go.mod @@ -1,4 +1,4 @@ -module github.com/agent-substrate/substrate/hack/tools/gotestsum +module go.ate.dev/substrate/hack/tools/gotestsum go 1.27.0 diff --git a/hack/tools/kind/go.mod b/hack/tools/kind/go.mod index 9100fa9ad0..5545361cdb 100644 --- a/hack/tools/kind/go.mod +++ b/hack/tools/kind/go.mod @@ -1,4 +1,4 @@ -module github.com/agent-substrate/substrate/hack/tools/kind +module go.ate.dev/substrate/hack/tools/kind go 1.27.0 diff --git a/hack/tools/ko/go.mod b/hack/tools/ko/go.mod index bc6d2f33ec..315dd667e7 100644 --- a/hack/tools/ko/go.mod +++ b/hack/tools/ko/go.mod @@ -1,4 +1,4 @@ -module github.com/agent-substrate/substrate/hack/tools/ko +module go.ate.dev/substrate/hack/tools/ko go 1.27.0 diff --git a/hack/tools/kube-api-linter/go.mod b/hack/tools/kube-api-linter/go.mod index 24e6a02533..e62bf3e968 100644 --- a/hack/tools/kube-api-linter/go.mod +++ b/hack/tools/kube-api-linter/go.mod @@ -1,4 +1,4 @@ -module github.com/agent-substrate/substrate/hack/tools/kube-api-linter +module go.ate.dev/substrate/hack/tools/kube-api-linter go 1.27.0 diff --git a/hack/tools/protoc-gen-go-grpc/go.mod b/hack/tools/protoc-gen-go-grpc/go.mod index 468f14b303..75ec153d5a 100644 --- a/hack/tools/protoc-gen-go-grpc/go.mod +++ b/hack/tools/protoc-gen-go-grpc/go.mod @@ -1,4 +1,4 @@ -module github.com/agent-substrate/substrate/hack/tools/protoc-gen-go-grpc +module go.ate.dev/substrate/hack/tools/protoc-gen-go-grpc go 1.27.0 diff --git a/hack/tools/protoc-gen-go/go.mod b/hack/tools/protoc-gen-go/go.mod index 0953874e21..6a2e087cf7 100644 --- a/hack/tools/protoc-gen-go/go.mod +++ b/hack/tools/protoc-gen-go/go.mod @@ -1,4 +1,4 @@ -module github.com/agent-substrate/substrate/hack/tools/protoc-gen-go +module go.ate.dev/substrate/hack/tools/protoc-gen-go go 1.27.0 diff --git a/hack/tools/setup-envtest/go.mod b/hack/tools/setup-envtest/go.mod index e80f2207cd..22eb248db2 100644 --- a/hack/tools/setup-envtest/go.mod +++ b/hack/tools/setup-envtest/go.mod @@ -1,4 +1,4 @@ -module github.com/agent-substrate/substrate/hack/tools/setup-envtest +module go.ate.dev/substrate/hack/tools/setup-envtest go 1.27.0 diff --git a/hack/update/licenses.sh b/hack/update/licenses.sh index 99e24d731b..95bc3cf4d2 100755 --- a/hack/update/licenses.sh +++ b/hack/update/licenses.sh @@ -73,7 +73,7 @@ for target in "${targets[@]}"; do } # Bug in go-licenses? Our repo gets included in a loop - rm -rf "${tmp_out}/github.com/agent-substrate/substrate" + rm -rf "${tmp_out}/go.ate.dev/substrate" # Merge the results into the main OUTDIR if [ "$(ls -A "${tmp_out}")" ]; then diff --git a/internal/actorevent/actorevent.go b/internal/actorevent/actorevent.go index b189583807..2a3bebbba4 100644 --- a/internal/actorevent/actorevent.go +++ b/internal/actorevent/actorevent.go @@ -39,12 +39,12 @@ import ( "go.opentelemetry.io/otel/log" "go.opentelemetry.io/otel/log/global" - "github.com/agent-substrate/substrate/internal/ateattr" + "go.ate.dev/substrate/internal/ateattr" ) // ScopeName is the instrumentation scope of every actor event. A consumer // selects a stream by event name. -const ScopeName = "github.com/agent-substrate/substrate/internal/actorevent" +const ScopeName = "go.ate.dev/substrate/internal/actorevent" // Event is one name in the closed vocabulary. Name is the LogRecord's own event // name field, not an attribute. Body and Severity live here rather than at a diff --git a/internal/actorevent/actorevent_test.go b/internal/actorevent/actorevent_test.go index d7ba9be5ac..66fc81ad9d 100644 --- a/internal/actorevent/actorevent_test.go +++ b/internal/actorevent/actorevent_test.go @@ -29,8 +29,8 @@ import ( sdklog "go.opentelemetry.io/otel/sdk/log" sdktrace "go.opentelemetry.io/otel/sdk/trace" - "github.com/agent-substrate/substrate/internal/ateattr" - "github.com/agent-substrate/substrate/internal/resources" + "go.ate.dev/substrate/internal/ateattr" + "go.ate.dev/substrate/internal/resources" ) const ( diff --git a/internal/actorlog/logger.go b/internal/actorlog/logger.go index ceddc3d254..7fdbaa77dc 100644 --- a/internal/actorlog/logger.go +++ b/internal/actorlog/logger.go @@ -38,8 +38,8 @@ import ( "go.opentelemetry.io/otel/trace" - "github.com/agent-substrate/substrate/internal/ateattr" - "github.com/agent-substrate/substrate/internal/resources" + "go.ate.dev/substrate/internal/ateattr" + "go.ate.dev/substrate/internal/resources" ) // SyncedWriter wraps an io.Writer and synchronizes writes across goroutines. diff --git a/internal/actorlog/logger_test.go b/internal/actorlog/logger_test.go index c9f374f387..c8339dc3cb 100644 --- a/internal/actorlog/logger_test.go +++ b/internal/actorlog/logger_test.go @@ -24,8 +24,8 @@ import ( "go.opentelemetry.io/otel/trace" - "github.com/agent-substrate/substrate/internal/ateattr" - "github.com/agent-substrate/substrate/internal/resources" + "go.ate.dev/substrate/internal/ateattr" + "go.ate.dev/substrate/internal/resources" ) const ( diff --git a/internal/ateapiauth/client.go b/internal/ateapiauth/client.go index 02d65f15d1..a6d8206cd6 100644 --- a/internal/ateapiauth/client.go +++ b/internal/ateapiauth/client.go @@ -20,8 +20,8 @@ import ( "errors" "fmt" - "github.com/agent-substrate/substrate/internal/credbundle" - "github.com/agent-substrate/substrate/internal/k8sresolver" + "go.ate.dev/substrate/internal/credbundle" + "go.ate.dev/substrate/internal/k8sresolver" "google.golang.org/grpc" "google.golang.org/grpc/credentials" "k8s.io/client-go/kubernetes" diff --git a/internal/ateattr/ateattr.go b/internal/ateattr/ateattr.go index 97aa5d730a..54e31b26e5 100644 --- a/internal/ateattr/ateattr.go +++ b/internal/ateattr/ateattr.go @@ -24,11 +24,11 @@ import ( "go.opentelemetry.io/otel/attribute" - "github.com/agent-substrate/substrate/internal/proto/ateletpb" - "github.com/agent-substrate/substrate/internal/resources" + "go.ate.dev/substrate/internal/proto/ateletpb" + "go.ate.dev/substrate/internal/resources" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" - "github.com/agent-substrate/substrate/pkg/sandboxclass" + "go.ate.dev/substrate/pkg/proto/ateapipb" + "go.ate.dev/substrate/pkg/sandboxclass" ) // Dotted ate.* matches the metric-instrument naming (atenet.*, atelet.*), not the diff --git a/internal/ateattr/ateattr_test.go b/internal/ateattr/ateattr_test.go index 8c1ec78826..64773d9f1f 100644 --- a/internal/ateattr/ateattr_test.go +++ b/internal/ateattr/ateattr_test.go @@ -21,11 +21,11 @@ import ( "go.opentelemetry.io/otel/attribute" - "github.com/agent-substrate/substrate/internal/proto/ateletpb" - "github.com/agent-substrate/substrate/internal/resources" + "go.ate.dev/substrate/internal/proto/ateletpb" + "go.ate.dev/substrate/internal/resources" - atev1alpha1 "github.com/agent-substrate/substrate/pkg/api/v1alpha1" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + atev1alpha1 "go.ate.dev/substrate/pkg/api/v1alpha1" + "go.ate.dev/substrate/pkg/proto/ateapipb" ) func toMap(kvs []attribute.KeyValue) map[attribute.Key]attribute.Value { diff --git a/internal/ateclient/builder.go b/internal/ateclient/builder.go index 45268c0dcc..8f9ac8cd8f 100644 --- a/internal/ateclient/builder.go +++ b/internal/ateclient/builder.go @@ -24,10 +24,10 @@ import ( "strings" "sync" - "github.com/agent-substrate/substrate/internal/clustertrustbundle" - "github.com/agent-substrate/substrate/internal/installdefaults" - "github.com/agent-substrate/substrate/internal/portforward" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "go.ate.dev/substrate/internal/clustertrustbundle" + "go.ate.dev/substrate/internal/installdefaults" + "go.ate.dev/substrate/internal/portforward" + "go.ate.dev/substrate/pkg/proto/ateapipb" "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/propagation" diff --git a/internal/ateclient/builder_test.go b/internal/ateclient/builder_test.go index 97aeaa52b6..955395d5ff 100644 --- a/internal/ateclient/builder_test.go +++ b/internal/ateclient/builder_test.go @@ -29,7 +29,7 @@ import ( "testing" "time" - "github.com/agent-substrate/substrate/internal/clustertrustbundle" + "go.ate.dev/substrate/internal/clustertrustbundle" certsv1 "k8s.io/api/certificates/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" diff --git a/internal/ateinterceptors/ateinterceptors.go b/internal/ateinterceptors/ateinterceptors.go index 679a724bc2..16717f59d5 100644 --- a/internal/ateinterceptors/ateinterceptors.go +++ b/internal/ateinterceptors/ateinterceptors.go @@ -20,8 +20,8 @@ import ( "strconv" "time" - "github.com/agent-substrate/substrate/internal/apierror" - "github.com/agent-substrate/substrate/internal/principal" + "go.ate.dev/substrate/internal/apierror" + "go.ate.dev/substrate/internal/principal" "google.golang.org/grpc" "google.golang.org/grpc/codes" "google.golang.org/grpc/metadata" diff --git a/internal/ateinterceptors/ateinterceptors_test.go b/internal/ateinterceptors/ateinterceptors_test.go index f19d8bb4c9..c62f2cd470 100644 --- a/internal/ateinterceptors/ateinterceptors_test.go +++ b/internal/ateinterceptors/ateinterceptors_test.go @@ -24,12 +24,12 @@ import ( "testing" "time" - "github.com/agent-substrate/substrate/internal/apierror" - "github.com/agent-substrate/substrate/internal/proto/ateletpb" - "github.com/agent-substrate/substrate/internal/protoredact" - "github.com/agent-substrate/substrate/internal/serverboot" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" - "github.com/agent-substrate/substrate/pkg/proto/credproviderpb" + "go.ate.dev/substrate/internal/apierror" + "go.ate.dev/substrate/internal/proto/ateletpb" + "go.ate.dev/substrate/internal/protoredact" + "go.ate.dev/substrate/internal/serverboot" + "go.ate.dev/substrate/pkg/proto/ateapipb" + "go.ate.dev/substrate/pkg/proto/credproviderpb" epb "google.golang.org/genproto/googleapis/rpc/errdetails" "google.golang.org/grpc" "google.golang.org/grpc/codes" diff --git a/internal/ateinterceptors/rejectunknown.go b/internal/ateinterceptors/rejectunknown.go index e930894cff..4a6fd94789 100644 --- a/internal/ateinterceptors/rejectunknown.go +++ b/internal/ateinterceptors/rejectunknown.go @@ -18,7 +18,7 @@ import ( "context" "strconv" - "github.com/agent-substrate/substrate/internal/apierror" + "go.ate.dev/substrate/internal/apierror" "google.golang.org/grpc" "google.golang.org/protobuf/encoding/protowire" "google.golang.org/protobuf/proto" diff --git a/internal/ateinterceptors/rejectunknown_test.go b/internal/ateinterceptors/rejectunknown_test.go index 30605dbf7b..87d1dcdb7e 100644 --- a/internal/ateinterceptors/rejectunknown_test.go +++ b/internal/ateinterceptors/rejectunknown_test.go @@ -24,9 +24,9 @@ import ( "google.golang.org/protobuf/proto" "k8s.io/apimachinery/pkg/util/validation/field" - "github.com/agent-substrate/substrate/internal/apierror" - "github.com/agent-substrate/substrate/internal/proto/ateletpb" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "go.ate.dev/substrate/internal/apierror" + "go.ate.dev/substrate/internal/proto/ateletpb" + "go.ate.dev/substrate/pkg/proto/ateapipb" ) // unknownField encodes a varint field that no descriptor in this binary diff --git a/internal/ateletdial/ateletdial.go b/internal/ateletdial/ateletdial.go index 7c79259342..4972b4447f 100644 --- a/internal/ateletdial/ateletdial.go +++ b/internal/ateletdial/ateletdial.go @@ -26,8 +26,8 @@ import ( "google.golang.org/grpc" "google.golang.org/grpc/credentials" - "github.com/agent-substrate/substrate/internal/credbundle" - "github.com/agent-substrate/substrate/internal/substratex509" + "go.ate.dev/substrate/internal/credbundle" + "go.ate.dev/substrate/internal/substratex509" ) // TLSConfig authenticates this worker to atelet with its Pod certificate, and diff --git a/internal/ateletdial/ateletdial_test.go b/internal/ateletdial/ateletdial_test.go index 2be4549988..36f2dd74e5 100644 --- a/internal/ateletdial/ateletdial_test.go +++ b/internal/ateletdial/ateletdial_test.go @@ -30,7 +30,7 @@ import ( "testing" "time" - "github.com/agent-substrate/substrate/internal/substratex509" + "go.ate.dev/substrate/internal/substratex509" ) const ateletSPIFFEID = "spiffe://cluster.local/ns/ate-system/sa/atelet" diff --git a/internal/atenet/headers.go b/internal/atenet/headers.go index ee1d640876..d43cc4d69f 100644 --- a/internal/atenet/headers.go +++ b/internal/atenet/headers.go @@ -19,7 +19,7 @@ import ( "fmt" "strings" - "github.com/agent-substrate/substrate/internal/resources" + "go.ate.dev/substrate/internal/resources" ) const ( diff --git a/internal/ateworker/actornet/sandbox.go b/internal/ateworker/actornet/sandbox.go index 60fa98c680..43161316c0 100644 --- a/internal/ateworker/actornet/sandbox.go +++ b/internal/ateworker/actornet/sandbox.go @@ -26,9 +26,9 @@ import ( "net/netip" "sync" - "github.com/agent-substrate/substrate/internal/atunnel/dns" - "github.com/agent-substrate/substrate/internal/netns" - "github.com/agent-substrate/substrate/internal/nodepath" + "go.ate.dev/substrate/internal/atunnel/dns" + "go.ate.dev/substrate/internal/netns" + "go.ate.dev/substrate/internal/nodepath" "github.com/google/nftables" "github.com/google/nftables/binaryutil" diff --git a/internal/ateworker/actornet/sandbox_atunnel_linux_test.go b/internal/ateworker/actornet/sandbox_atunnel_linux_test.go index 50a342fb5a..ef0c94bb14 100644 --- a/internal/ateworker/actornet/sandbox_atunnel_linux_test.go +++ b/internal/ateworker/actornet/sandbox_atunnel_linux_test.go @@ -25,10 +25,10 @@ import ( "testing" "time" - "github.com/agent-substrate/substrate/internal/ateworker/actornet" - "github.com/agent-substrate/substrate/internal/linuxsys" - "github.com/agent-substrate/substrate/internal/netns" - "github.com/agent-substrate/substrate/internal/roottest" + "go.ate.dev/substrate/internal/ateworker/actornet" + "go.ate.dev/substrate/internal/linuxsys" + "go.ate.dev/substrate/internal/netns" + "go.ate.dev/substrate/internal/roottest" ) // Verify redirection preserves the destination, including unconfigured ports. diff --git a/internal/ateworker/actornet/sandbox_linux_test.go b/internal/ateworker/actornet/sandbox_linux_test.go index 6308c39b5a..46283a2fe9 100644 --- a/internal/ateworker/actornet/sandbox_linux_test.go +++ b/internal/ateworker/actornet/sandbox_linux_test.go @@ -29,11 +29,11 @@ import ( "testing" "time" - "github.com/agent-substrate/substrate/internal/nodepath" "github.com/vishvananda/netlink" + "go.ate.dev/substrate/internal/nodepath" - "github.com/agent-substrate/substrate/internal/netns" - "github.com/agent-substrate/substrate/internal/roottest" + "go.ate.dev/substrate/internal/netns" + "go.ate.dev/substrate/internal/roottest" ) const testEgressPort = 15001 diff --git a/internal/ateworker/cgroup/actor_linux_test.go b/internal/ateworker/cgroup/actor_linux_test.go index 991470cf7e..a38ce75144 100644 --- a/internal/ateworker/cgroup/actor_linux_test.go +++ b/internal/ateworker/cgroup/actor_linux_test.go @@ -26,7 +26,7 @@ import ( "testing" "time" - "github.com/agent-substrate/substrate/internal/roottest" + "go.ate.dev/substrate/internal/roottest" ) // A process started with the leaf's SysProcAttr is born inside the leaf. diff --git a/internal/ateworker/phaselog/phaselog.go b/internal/ateworker/phaselog/phaselog.go index 85fe959047..aa520c354f 100644 --- a/internal/ateworker/phaselog/phaselog.go +++ b/internal/ateworker/phaselog/phaselog.go @@ -24,10 +24,10 @@ import ( "log/slog" "time" - "github.com/agent-substrate/substrate/internal/apierror" - "github.com/agent-substrate/substrate/internal/ateattr" - "github.com/agent-substrate/substrate/internal/proto/ateworkerpb" - "github.com/agent-substrate/substrate/internal/resources" + "go.ate.dev/substrate/internal/apierror" + "go.ate.dev/substrate/internal/ateattr" + "go.ate.dev/substrate/internal/proto/ateworkerpb" + "go.ate.dev/substrate/internal/resources" "google.golang.org/grpc/status" ) diff --git a/internal/ateworker/phaselog/phaselog_test.go b/internal/ateworker/phaselog/phaselog_test.go index 157fb821bd..d78c4dd54b 100644 --- a/internal/ateworker/phaselog/phaselog_test.go +++ b/internal/ateworker/phaselog/phaselog_test.go @@ -23,9 +23,9 @@ import ( "testing" "time" - "github.com/agent-substrate/substrate/internal/ateattr" - "github.com/agent-substrate/substrate/internal/proto/ateworkerpb" - "github.com/agent-substrate/substrate/internal/resources" + "go.ate.dev/substrate/internal/ateattr" + "go.ate.dev/substrate/internal/proto/ateworkerpb" + "go.ate.dev/substrate/internal/resources" ) func phaseLogAttribution() resources.ActorAttribution { diff --git a/internal/ateworker/register/register.go b/internal/ateworker/register/register.go index 3eb8cd0a7f..b9c6148032 100644 --- a/internal/ateworker/register/register.go +++ b/internal/ateworker/register/register.go @@ -29,13 +29,13 @@ import ( "strings" "time" - "github.com/agent-substrate/substrate/internal/ateletdial" - "github.com/agent-substrate/substrate/internal/hardware" - "github.com/agent-substrate/substrate/internal/resources" - "github.com/agent-substrate/substrate/internal/workercapacity" + "go.ate.dev/substrate/internal/ateletdial" + "go.ate.dev/substrate/internal/hardware" + "go.ate.dev/substrate/internal/resources" + "go.ate.dev/substrate/internal/workercapacity" "k8s.io/apimachinery/pkg/api/resource" - "github.com/agent-substrate/substrate/internal/proto/ateletpb" + "go.ate.dev/substrate/internal/proto/ateletpb" ) const ( diff --git a/internal/ateworker/register/register_test.go b/internal/ateworker/register/register_test.go index 43cb2690f8..c328e03feb 100644 --- a/internal/ateworker/register/register_test.go +++ b/internal/ateworker/register/register_test.go @@ -26,9 +26,9 @@ import ( "github.com/google/go-cmp/cmp" "google.golang.org/protobuf/testing/protocmp" - "github.com/agent-substrate/substrate/internal/hardware" - "github.com/agent-substrate/substrate/internal/proto/ateletpb" - "github.com/agent-substrate/substrate/internal/workercapacity" + "go.ate.dev/substrate/internal/hardware" + "go.ate.dev/substrate/internal/proto/ateletpb" + "go.ate.dev/substrate/internal/workercapacity" ) // testActors stands in for the worker's own ceiling, which is a flag in diff --git a/internal/ateworker/suspend/suspend.go b/internal/ateworker/suspend/suspend.go index f40bfd5960..08ac36f2c6 100644 --- a/internal/ateworker/suspend/suspend.go +++ b/internal/ateworker/suspend/suspend.go @@ -33,8 +33,8 @@ import ( "google.golang.org/grpc/status" "k8s.io/apimachinery/pkg/util/wait" - "github.com/agent-substrate/substrate/internal/ateletdial" - "github.com/agent-substrate/substrate/internal/proto/ateletpb" + "go.ate.dev/substrate/internal/ateletdial" + "go.ate.dev/substrate/internal/proto/ateletpb" ) // requestTimeout bounds one attempt. It is generous because the control plane diff --git a/internal/ateworker/suspend/suspend_test.go b/internal/ateworker/suspend/suspend_test.go index 9e5b9a7fbf..a8586a8eb5 100644 --- a/internal/ateworker/suspend/suspend_test.go +++ b/internal/ateworker/suspend/suspend_test.go @@ -32,7 +32,7 @@ import ( "google.golang.org/protobuf/testing/protocmp" "k8s.io/apimachinery/pkg/util/wait" - "github.com/agent-substrate/substrate/internal/proto/ateletpb" + "go.ate.dev/substrate/internal/proto/ateletpb" ) // capturingAtelet stands in for the node-local atelet. It records every diff --git a/internal/ateworker/tunnel/egresspolicy.go b/internal/ateworker/tunnel/egresspolicy.go index 7e62f133be..10d4c3ca9e 100644 --- a/internal/ateworker/tunnel/egresspolicy.go +++ b/internal/ateworker/tunnel/egresspolicy.go @@ -25,12 +25,12 @@ import ( "golang.org/x/sync/singleflight" - "github.com/agent-substrate/substrate/internal/ateattr" - "github.com/agent-substrate/substrate/internal/ateletdial" - "github.com/agent-substrate/substrate/internal/atunnel/dns" - "github.com/agent-substrate/substrate/internal/nodepath" - "github.com/agent-substrate/substrate/internal/proto/ateletpb" - "github.com/agent-substrate/substrate/internal/resources" + "go.ate.dev/substrate/internal/ateattr" + "go.ate.dev/substrate/internal/ateletdial" + "go.ate.dev/substrate/internal/atunnel/dns" + "go.ate.dev/substrate/internal/nodepath" + "go.ate.dev/substrate/internal/proto/ateletpb" + "go.ate.dev/substrate/internal/resources" ) const ( diff --git a/internal/ateworker/tunnel/egresspolicy_test.go b/internal/ateworker/tunnel/egresspolicy_test.go index 33b832989f..906e62c964 100644 --- a/internal/ateworker/tunnel/egresspolicy_test.go +++ b/internal/ateworker/tunnel/egresspolicy_test.go @@ -43,10 +43,10 @@ import ( "google.golang.org/grpc/status" "google.golang.org/protobuf/testing/protocmp" - "github.com/agent-substrate/substrate/internal/nodepath" - "github.com/agent-substrate/substrate/internal/proto/ateletpb" - "github.com/agent-substrate/substrate/internal/resources" - "github.com/agent-substrate/substrate/internal/substratex509" + "go.ate.dev/substrate/internal/nodepath" + "go.ate.dev/substrate/internal/proto/ateletpb" + "go.ate.dev/substrate/internal/resources" + "go.ate.dev/substrate/internal/substratex509" ) var testActor = resources.ActorAttribution{Ref: resources.ActorRef{Atespace: "space", Name: "actor"}, UID: "uid"} diff --git a/internal/ateworker/tunnel/tunnel.go b/internal/ateworker/tunnel/tunnel.go index 7cf52b17bb..142f044067 100644 --- a/internal/ateworker/tunnel/tunnel.go +++ b/internal/ateworker/tunnel/tunnel.go @@ -29,17 +29,17 @@ import ( "net/url" "time" - "github.com/agent-substrate/substrate/internal/ateletdial" - "github.com/agent-substrate/substrate/internal/atunnel/dns" - "github.com/agent-substrate/substrate/internal/atunnel/egress" - "github.com/agent-substrate/substrate/internal/atunnel/ingress" - "github.com/agent-substrate/substrate/internal/egresspolicy" - "github.com/agent-substrate/substrate/internal/installdefaults" - "github.com/agent-substrate/substrate/internal/linuxsys" - "github.com/agent-substrate/substrate/internal/nodepath" - "github.com/agent-substrate/substrate/internal/proto/ateworkerpb" - "github.com/agent-substrate/substrate/internal/resources" - "github.com/agent-substrate/substrate/internal/serverboot" + "go.ate.dev/substrate/internal/ateletdial" + "go.ate.dev/substrate/internal/atunnel/dns" + "go.ate.dev/substrate/internal/atunnel/egress" + "go.ate.dev/substrate/internal/atunnel/ingress" + "go.ate.dev/substrate/internal/egresspolicy" + "go.ate.dev/substrate/internal/installdefaults" + "go.ate.dev/substrate/internal/linuxsys" + "go.ate.dev/substrate/internal/nodepath" + "go.ate.dev/substrate/internal/proto/ateworkerpb" + "go.ate.dev/substrate/internal/resources" + "go.ate.dev/substrate/internal/serverboot" ) // resolvConfPath holds the worker pod's resolvers. Tests override it. diff --git a/internal/ateworker/tunnel/tunnel_test.go b/internal/ateworker/tunnel/tunnel_test.go index 4fbbe75f65..022ee9dc0e 100644 --- a/internal/ateworker/tunnel/tunnel_test.go +++ b/internal/ateworker/tunnel/tunnel_test.go @@ -33,11 +33,11 @@ import ( "testing" "time" - "github.com/agent-substrate/substrate/internal/egresspolicy" - "github.com/agent-substrate/substrate/internal/proto/ateworkerpb" - "github.com/agent-substrate/substrate/internal/resources" - "github.com/agent-substrate/substrate/internal/substratex509" "github.com/spf13/pflag" + "go.ate.dev/substrate/internal/egresspolicy" + "go.ate.dev/substrate/internal/proto/ateworkerpb" + "go.ate.dev/substrate/internal/resources" + "go.ate.dev/substrate/internal/substratex509" ) func TestRegisterFlags(t *testing.T) { diff --git a/internal/atunnel/dns/dnshandler.go b/internal/atunnel/dns/dnshandler.go index 33c6f3c689..c0d6178b50 100644 --- a/internal/atunnel/dns/dnshandler.go +++ b/internal/atunnel/dns/dnshandler.go @@ -21,7 +21,7 @@ import ( "net" "time" - "github.com/agent-substrate/substrate/internal/atunnel/dns/protocol" + "go.ate.dev/substrate/internal/atunnel/dns/protocol" "golang.org/x/net/dns/dnsmessage" ) diff --git a/internal/atunnel/dns/dnshandler_test.go b/internal/atunnel/dns/dnshandler_test.go index 09cbbd64d7..028d4010d6 100644 --- a/internal/atunnel/dns/dnshandler_test.go +++ b/internal/atunnel/dns/dnshandler_test.go @@ -20,7 +20,7 @@ import ( "net" "testing" - "github.com/agent-substrate/substrate/internal/atunnel/dns/protocol" + "go.ate.dev/substrate/internal/atunnel/dns/protocol" "golang.org/x/net/dns/dnsmessage" ) diff --git a/internal/atunnel/dns/evaluator.go b/internal/atunnel/dns/evaluator.go index e85acf5319..cd79a428ea 100644 --- a/internal/atunnel/dns/evaluator.go +++ b/internal/atunnel/dns/evaluator.go @@ -15,8 +15,8 @@ package dns import ( - "github.com/agent-substrate/substrate/internal/egresspolicy" - "github.com/agent-substrate/substrate/internal/proto/ateletpb" + "go.ate.dev/substrate/internal/egresspolicy" + "go.ate.dev/substrate/internal/proto/ateletpb" ) // evaluator decides whether a DNS hostname is allowed by an EgressPolicy's diff --git a/internal/atunnel/dns/evaluator_test.go b/internal/atunnel/dns/evaluator_test.go index d1e600ea3c..0b1604e2ea 100644 --- a/internal/atunnel/dns/evaluator_test.go +++ b/internal/atunnel/dns/evaluator_test.go @@ -17,7 +17,7 @@ package dns import ( "testing" - "github.com/agent-substrate/substrate/internal/proto/ateletpb" + "go.ate.dev/substrate/internal/proto/ateletpb" ) func TestEvaluator(t *testing.T) { diff --git a/internal/atunnel/dns/pendingreq.go b/internal/atunnel/dns/pendingreq.go index 801cead055..d120bc348d 100644 --- a/internal/atunnel/dns/pendingreq.go +++ b/internal/atunnel/dns/pendingreq.go @@ -21,8 +21,8 @@ import ( "sync" "time" - "github.com/agent-substrate/substrate/internal/atunnel/dns/freemap" - "github.com/agent-substrate/substrate/internal/atunnel/dns/protocol" + "go.ate.dev/substrate/internal/atunnel/dns/freemap" + "go.ate.dev/substrate/internal/atunnel/dns/protocol" "golang.org/x/net/dns/dnsmessage" ) diff --git a/internal/atunnel/dns/policy.go b/internal/atunnel/dns/policy.go index 39f2e3567b..6be8a9e643 100644 --- a/internal/atunnel/dns/policy.go +++ b/internal/atunnel/dns/policy.go @@ -21,7 +21,7 @@ import ( "golang.org/x/net/dns/dnsmessage" - "github.com/agent-substrate/substrate/internal/proto/ateletpb" + "go.ate.dev/substrate/internal/proto/ateletpb" ) // PolicySource supplies the egress policy an actor's DNS queries are checked diff --git a/internal/atunnel/dns/policy_test.go b/internal/atunnel/dns/policy_test.go index 3648853ec0..93164c2c13 100644 --- a/internal/atunnel/dns/policy_test.go +++ b/internal/atunnel/dns/policy_test.go @@ -25,8 +25,8 @@ import ( "github.com/google/go-cmp/cmp" "golang.org/x/net/dns/dnsmessage" - "github.com/agent-substrate/substrate/internal/atunnel/dns/protocol" - "github.com/agent-substrate/substrate/internal/proto/ateletpb" + "go.ate.dev/substrate/internal/atunnel/dns/protocol" + "go.ate.dev/substrate/internal/proto/ateletpb" ) // staticPolicy supplies policy, or fails with err. diff --git a/internal/atunnel/dns/relay_linux.go b/internal/atunnel/dns/relay_linux.go index 394bf88645..a85140d039 100644 --- a/internal/atunnel/dns/relay_linux.go +++ b/internal/atunnel/dns/relay_linux.go @@ -22,7 +22,7 @@ import ( "net" "strconv" - "github.com/agent-substrate/substrate/internal/netns" + "go.ate.dev/substrate/internal/netns" ) // Serve serves UDP and TCP DNS in the sandbox's local gateway namespace. diff --git a/internal/atunnel/dns/serve_linux_test.go b/internal/atunnel/dns/serve_linux_test.go index 995e3e3fbc..64958d055e 100644 --- a/internal/atunnel/dns/serve_linux_test.go +++ b/internal/atunnel/dns/serve_linux_test.go @@ -21,8 +21,8 @@ import ( "testing" "time" - "github.com/agent-substrate/substrate/internal/netns" - "github.com/agent-substrate/substrate/internal/roottest" + "go.ate.dev/substrate/internal/netns" + "go.ate.dev/substrate/internal/roottest" ) func TestClosingSandboxDNSStopsServing(t *testing.T) { diff --git a/internal/atunnel/dns/tcphandler.go b/internal/atunnel/dns/tcphandler.go index 109e5071b1..b7434b9b09 100644 --- a/internal/atunnel/dns/tcphandler.go +++ b/internal/atunnel/dns/tcphandler.go @@ -24,7 +24,7 @@ import ( "sync" "time" - "github.com/agent-substrate/substrate/internal/atunnel/dns/protocol" + "go.ate.dev/substrate/internal/atunnel/dns/protocol" ) const ( diff --git a/internal/atunnel/dns/tcphandler_test.go b/internal/atunnel/dns/tcphandler_test.go index 32bf69c582..5041831219 100644 --- a/internal/atunnel/dns/tcphandler_test.go +++ b/internal/atunnel/dns/tcphandler_test.go @@ -22,7 +22,7 @@ import ( "testing" "time" - "github.com/agent-substrate/substrate/internal/atunnel/dns/protocol" + "go.ate.dev/substrate/internal/atunnel/dns/protocol" "golang.org/x/net/dns/dnsmessage" ) diff --git a/internal/atunnel/egress/credential.go b/internal/atunnel/egress/credential.go index ed8cd1ab0b..5cf3a7b5be 100644 --- a/internal/atunnel/egress/credential.go +++ b/internal/atunnel/egress/credential.go @@ -26,8 +26,8 @@ import ( "sync" "time" - "github.com/agent-substrate/substrate/internal/ateletdial" - "github.com/agent-substrate/substrate/internal/proto/ateletpb" + "go.ate.dev/substrate/internal/ateletdial" + "go.ate.dev/substrate/internal/proto/ateletpb" ) // BrokerCertificateSource owns atunnel's actor private key and obtains the diff --git a/internal/atunnel/egress/credential_test.go b/internal/atunnel/egress/credential_test.go index fb201c14a0..22f96bc462 100644 --- a/internal/atunnel/egress/credential_test.go +++ b/internal/atunnel/egress/credential_test.go @@ -31,9 +31,9 @@ import ( "testing" "time" - "github.com/agent-substrate/substrate/internal/installdefaults" - "github.com/agent-substrate/substrate/internal/proto/ateletpb" - "github.com/agent-substrate/substrate/internal/substratex509" + "go.ate.dev/substrate/internal/installdefaults" + "go.ate.dev/substrate/internal/proto/ateletpb" + "go.ate.dev/substrate/internal/substratex509" "google.golang.org/grpc" "google.golang.org/grpc/codes" "google.golang.org/grpc/credentials" diff --git a/internal/atunnel/ingress/ingress.go b/internal/atunnel/ingress/ingress.go index 070c80a002..d291ded8e4 100644 --- a/internal/atunnel/ingress/ingress.go +++ b/internal/atunnel/ingress/ingress.go @@ -32,9 +32,9 @@ import ( "sync" "time" - "github.com/agent-substrate/substrate/internal/atenet" - "github.com/agent-substrate/substrate/internal/credbundle" - "github.com/agent-substrate/substrate/internal/resources" + "go.ate.dev/substrate/internal/atenet" + "go.ate.dev/substrate/internal/credbundle" + "go.ate.dev/substrate/internal/resources" ) const ( diff --git a/internal/atunnel/ingress/ingress_test.go b/internal/atunnel/ingress/ingress_test.go index 9c33faa85b..0e715cf3a0 100644 --- a/internal/atunnel/ingress/ingress_test.go +++ b/internal/atunnel/ingress/ingress_test.go @@ -40,9 +40,9 @@ import ( "testing" "time" - "github.com/agent-substrate/substrate/internal/resources" + "go.ate.dev/substrate/internal/resources" - "github.com/agent-substrate/substrate/internal/atenet" + "go.ate.dev/substrate/internal/atenet" ) func TestActivationDialerClosesLateConnection(t *testing.T) { diff --git a/internal/benchmarking/boomer/agentsession/agentsession.go b/internal/benchmarking/boomer/agentsession/agentsession.go index e91873e735..e727a2a335 100644 --- a/internal/benchmarking/boomer/agentsession/agentsession.go +++ b/internal/benchmarking/boomer/agentsession/agentsession.go @@ -40,17 +40,17 @@ import ( "sync" "time" - "github.com/agent-substrate/substrate/internal/ateinterceptors" - "github.com/agent-substrate/substrate/internal/atenet" - "github.com/agent-substrate/substrate/internal/benchmarking/boomer/boomerutil" - "github.com/agent-substrate/substrate/internal/benchmarking/boomer/dynconfig" - bmetrics "github.com/agent-substrate/substrate/internal/benchmarking/boomer/metrics" - "github.com/agent-substrate/substrate/internal/benchmarking/boomer/userclass" - "github.com/agent-substrate/substrate/internal/benchmarking/glutton" - gluttonpb "github.com/agent-substrate/substrate/internal/proto/glutton" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" "github.com/google/uuid" "github.com/prometheus/client_golang/prometheus" + "go.ate.dev/substrate/internal/ateinterceptors" + "go.ate.dev/substrate/internal/atenet" + "go.ate.dev/substrate/internal/benchmarking/boomer/boomerutil" + "go.ate.dev/substrate/internal/benchmarking/boomer/dynconfig" + bmetrics "go.ate.dev/substrate/internal/benchmarking/boomer/metrics" + "go.ate.dev/substrate/internal/benchmarking/boomer/userclass" + "go.ate.dev/substrate/internal/benchmarking/glutton" + gluttonpb "go.ate.dev/substrate/internal/proto/glutton" + "go.ate.dev/substrate/pkg/proto/ateapipb" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/propagation" "google.golang.org/grpc" diff --git a/internal/benchmarking/boomer/agentsession/agentsession_test.go b/internal/benchmarking/boomer/agentsession/agentsession_test.go index d1fc69144c..b6f9618d28 100644 --- a/internal/benchmarking/boomer/agentsession/agentsession_test.go +++ b/internal/benchmarking/boomer/agentsession/agentsession_test.go @@ -29,10 +29,10 @@ import ( "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/internal/benchmarking/glutton/fake" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "go.ate.dev/substrate/internal/benchmarking/boomer/dynconfig" + "go.ate.dev/substrate/internal/benchmarking/boomer/userclass" + "go.ate.dev/substrate/internal/benchmarking/glutton/fake" + "go.ate.dev/substrate/pkg/proto/ateapipb" "go.opentelemetry.io/otel" "google.golang.org/grpc" "google.golang.org/grpc/codes" diff --git a/internal/benchmarking/boomer/boomerutil/client.go b/internal/benchmarking/boomer/boomerutil/client.go index 01d6e04c33..a27e607b6f 100644 --- a/internal/benchmarking/boomer/boomerutil/client.go +++ b/internal/benchmarking/boomer/boomerutil/client.go @@ -17,9 +17,9 @@ package boomerutil import ( "fmt" - "github.com/agent-substrate/substrate/internal/ateapiauth" - _ "github.com/agent-substrate/substrate/internal/k8sresolver" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "go.ate.dev/substrate/internal/ateapiauth" + _ "go.ate.dev/substrate/internal/k8sresolver" + "go.ate.dev/substrate/pkg/proto/ateapipb" "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc" "google.golang.org/grpc" "k8s.io/client-go/kubernetes" diff --git a/internal/benchmarking/boomer/glutton/cpuload_test.go b/internal/benchmarking/boomer/glutton/cpuload_test.go index aacf4eaddd..162cc70164 100644 --- a/internal/benchmarking/boomer/glutton/cpuload_test.go +++ b/internal/benchmarking/boomer/glutton/cpuload_test.go @@ -19,8 +19,8 @@ import ( "net/http" "testing" - "github.com/agent-substrate/substrate/internal/benchmarking/boomer/dynconfig" - "github.com/agent-substrate/substrate/internal/benchmarking/glutton/fake" + "go.ate.dev/substrate/internal/benchmarking/boomer/dynconfig" + "go.ate.dev/substrate/internal/benchmarking/glutton/fake" ) func TestEnsureCPULoadRequestsConfiguredLoad(t *testing.T) { diff --git a/internal/benchmarking/boomer/glutton/durdir.go b/internal/benchmarking/boomer/glutton/durdir.go index ff9e072f8a..65f6bfa2b2 100644 --- a/internal/benchmarking/boomer/glutton/durdir.go +++ b/internal/benchmarking/boomer/glutton/durdir.go @@ -28,15 +28,15 @@ import ( "sync" "time" - "github.com/agent-substrate/substrate/internal/ateinterceptors" - "github.com/agent-substrate/substrate/internal/atenet" - "github.com/agent-substrate/substrate/internal/benchmarking/boomer/boomerutil" - "github.com/agent-substrate/substrate/internal/benchmarking/boomer/dynconfig" - bmetrics "github.com/agent-substrate/substrate/internal/benchmarking/boomer/metrics" - "github.com/agent-substrate/substrate/internal/benchmarking/boomer/userclass" - gluttonpb "github.com/agent-substrate/substrate/internal/proto/glutton" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" "github.com/google/uuid" + "go.ate.dev/substrate/internal/ateinterceptors" + "go.ate.dev/substrate/internal/atenet" + "go.ate.dev/substrate/internal/benchmarking/boomer/boomerutil" + "go.ate.dev/substrate/internal/benchmarking/boomer/dynconfig" + bmetrics "go.ate.dev/substrate/internal/benchmarking/boomer/metrics" + "go.ate.dev/substrate/internal/benchmarking/boomer/userclass" + gluttonpb "go.ate.dev/substrate/internal/proto/glutton" + "go.ate.dev/substrate/pkg/proto/ateapipb" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/propagation" diff --git a/internal/benchmarking/boomer/glutton/durdir_test.go b/internal/benchmarking/boomer/glutton/durdir_test.go index dd1b5d9410..bcc11330cc 100644 --- a/internal/benchmarking/boomer/glutton/durdir_test.go +++ b/internal/benchmarking/boomer/glutton/durdir_test.go @@ -24,11 +24,11 @@ import ( "slices" "testing" - "github.com/agent-substrate/substrate/internal/benchmarking/boomer/boomerutil" - "github.com/agent-substrate/substrate/internal/benchmarking/boomer/dynconfig" - "github.com/agent-substrate/substrate/internal/benchmarking/boomer/userclass" - "github.com/agent-substrate/substrate/internal/benchmarking/glutton/fake" - gluttonpb "github.com/agent-substrate/substrate/internal/proto/glutton" + "go.ate.dev/substrate/internal/benchmarking/boomer/boomerutil" + "go.ate.dev/substrate/internal/benchmarking/boomer/dynconfig" + "go.ate.dev/substrate/internal/benchmarking/boomer/userclass" + "go.ate.dev/substrate/internal/benchmarking/glutton/fake" + gluttonpb "go.ate.dev/substrate/internal/proto/glutton" ) func TestDurDirLoopSequence(t *testing.T) { diff --git a/internal/benchmarking/boomer/glutton/fixture_test.go b/internal/benchmarking/boomer/glutton/fixture_test.go index 170afe8cca..797aeae4d2 100644 --- a/internal/benchmarking/boomer/glutton/fixture_test.go +++ b/internal/benchmarking/boomer/glutton/fixture_test.go @@ -19,10 +19,10 @@ import ( "sync" "testing" - "github.com/agent-substrate/substrate/internal/benchmarking/boomer/dynconfig" - "github.com/agent-substrate/substrate/internal/benchmarking/boomer/userclass" - "github.com/agent-substrate/substrate/internal/benchmarking/glutton/fake" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "go.ate.dev/substrate/internal/benchmarking/boomer/dynconfig" + "go.ate.dev/substrate/internal/benchmarking/boomer/userclass" + "go.ate.dev/substrate/internal/benchmarking/glutton/fake" + "go.ate.dev/substrate/pkg/proto/ateapipb" "go.opentelemetry.io/otel" "google.golang.org/grpc" ) diff --git a/internal/benchmarking/boomer/glutton/lifecycle.go b/internal/benchmarking/boomer/glutton/lifecycle.go index 53c3f76105..e1e13e08b6 100644 --- a/internal/benchmarking/boomer/glutton/lifecycle.go +++ b/internal/benchmarking/boomer/glutton/lifecycle.go @@ -29,15 +29,15 @@ import ( "sync" "time" - "github.com/agent-substrate/substrate/internal/ateinterceptors" - "github.com/agent-substrate/substrate/internal/atenet" - "github.com/agent-substrate/substrate/internal/benchmarking/boomer/boomerutil" - "github.com/agent-substrate/substrate/internal/benchmarking/boomer/dynconfig" - bmetrics "github.com/agent-substrate/substrate/internal/benchmarking/boomer/metrics" - "github.com/agent-substrate/substrate/internal/benchmarking/boomer/userclass" - gluttonpb "github.com/agent-substrate/substrate/internal/proto/glutton" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" "github.com/google/uuid" + "go.ate.dev/substrate/internal/ateinterceptors" + "go.ate.dev/substrate/internal/atenet" + "go.ate.dev/substrate/internal/benchmarking/boomer/boomerutil" + "go.ate.dev/substrate/internal/benchmarking/boomer/dynconfig" + bmetrics "go.ate.dev/substrate/internal/benchmarking/boomer/metrics" + "go.ate.dev/substrate/internal/benchmarking/boomer/userclass" + gluttonpb "go.ate.dev/substrate/internal/proto/glutton" + "go.ate.dev/substrate/pkg/proto/ateapipb" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/propagation" diff --git a/internal/benchmarking/boomer/glutton/lifecycle_test.go b/internal/benchmarking/boomer/glutton/lifecycle_test.go index b1e637f38c..8673f5809f 100644 --- a/internal/benchmarking/boomer/glutton/lifecycle_test.go +++ b/internal/benchmarking/boomer/glutton/lifecycle_test.go @@ -19,10 +19,10 @@ import ( "slices" "testing" - "github.com/agent-substrate/substrate/internal/benchmarking/boomer/boomerutil" - "github.com/agent-substrate/substrate/internal/benchmarking/boomer/dynconfig" - "github.com/agent-substrate/substrate/internal/benchmarking/boomer/userclass" - "github.com/agent-substrate/substrate/internal/benchmarking/glutton/fake" + "go.ate.dev/substrate/internal/benchmarking/boomer/boomerutil" + "go.ate.dev/substrate/internal/benchmarking/boomer/dynconfig" + "go.ate.dev/substrate/internal/benchmarking/boomer/userclass" + "go.ate.dev/substrate/internal/benchmarking/glutton/fake" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" ) diff --git a/internal/benchmarking/boomer/glutton/memfill_test.go b/internal/benchmarking/boomer/glutton/memfill_test.go index 00485ae8ce..ac3499c028 100644 --- a/internal/benchmarking/boomer/glutton/memfill_test.go +++ b/internal/benchmarking/boomer/glutton/memfill_test.go @@ -18,10 +18,10 @@ import ( "context" "testing" - "github.com/agent-substrate/substrate/internal/benchmarking/boomer/dynconfig" - "github.com/agent-substrate/substrate/internal/benchmarking/boomer/userclass" - "github.com/agent-substrate/substrate/internal/benchmarking/glutton/fake" - gluttonpb "github.com/agent-substrate/substrate/internal/proto/glutton" + "go.ate.dev/substrate/internal/benchmarking/boomer/dynconfig" + "go.ate.dev/substrate/internal/benchmarking/boomer/userclass" + "go.ate.dev/substrate/internal/benchmarking/glutton/fake" + gluttonpb "go.ate.dev/substrate/internal/proto/glutton" ) func newTestGluttonActor(t *testing.T, srv *fake.Server, dyn dynconfig.Config) *gluttonActor { diff --git a/internal/benchmarking/boomer/glutton/resume_test.go b/internal/benchmarking/boomer/glutton/resume_test.go index 3cb808fda3..418a7c28bf 100644 --- a/internal/benchmarking/boomer/glutton/resume_test.go +++ b/internal/benchmarking/boomer/glutton/resume_test.go @@ -20,9 +20,9 @@ import ( "testing" "time" - "github.com/agent-substrate/substrate/internal/benchmarking/boomer/boomerutil" - "github.com/agent-substrate/substrate/internal/benchmarking/boomer/userclass" - "github.com/agent-substrate/substrate/internal/benchmarking/glutton/fake" + "go.ate.dev/substrate/internal/benchmarking/boomer/boomerutil" + "go.ate.dev/substrate/internal/benchmarking/boomer/userclass" + "go.ate.dev/substrate/internal/benchmarking/glutton/fake" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" ) diff --git a/internal/benchmarking/boomer/glutton/spawn.go b/internal/benchmarking/boomer/glutton/spawn.go index d9df58df7e..d14e0a74f8 100644 --- a/internal/benchmarking/boomer/glutton/spawn.go +++ b/internal/benchmarking/boomer/glutton/spawn.go @@ -27,14 +27,14 @@ import ( "sync/atomic" "time" - "github.com/agent-substrate/substrate/internal/ateinterceptors" - "github.com/agent-substrate/substrate/internal/atenet" - "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" - gluttonpb "github.com/agent-substrate/substrate/internal/proto/glutton" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" "github.com/google/uuid" + "go.ate.dev/substrate/internal/ateinterceptors" + "go.ate.dev/substrate/internal/atenet" + "go.ate.dev/substrate/internal/benchmarking/boomer/boomerutil" + bmetrics "go.ate.dev/substrate/internal/benchmarking/boomer/metrics" + "go.ate.dev/substrate/internal/benchmarking/boomer/userclass" + gluttonpb "go.ate.dev/substrate/internal/proto/glutton" + "go.ate.dev/substrate/pkg/proto/ateapipb" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/propagation" diff --git a/internal/benchmarking/boomer/glutton/spawn_test.go b/internal/benchmarking/boomer/glutton/spawn_test.go index 0199d8109b..9c33f73bdd 100644 --- a/internal/benchmarking/boomer/glutton/spawn_test.go +++ b/internal/benchmarking/boomer/glutton/spawn_test.go @@ -24,11 +24,11 @@ import ( "testing" "time" - "github.com/agent-substrate/substrate/internal/benchmarking/boomer/boomerutil" - "github.com/agent-substrate/substrate/internal/benchmarking/boomer/dynconfig" - "github.com/agent-substrate/substrate/internal/benchmarking/boomer/userclass" - gluttonpb "github.com/agent-substrate/substrate/internal/proto/glutton" "github.com/prometheus/client_golang/prometheus" + "go.ate.dev/substrate/internal/benchmarking/boomer/boomerutil" + "go.ate.dev/substrate/internal/benchmarking/boomer/dynconfig" + "go.ate.dev/substrate/internal/benchmarking/boomer/userclass" + gluttonpb "go.ate.dev/substrate/internal/proto/glutton" "go.opentelemetry.io/otel" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" diff --git a/internal/benchmarking/boomer/glutton/wait_test.go b/internal/benchmarking/boomer/glutton/wait_test.go index de9313de40..56aef7b17d 100644 --- a/internal/benchmarking/boomer/glutton/wait_test.go +++ b/internal/benchmarking/boomer/glutton/wait_test.go @@ -18,8 +18,8 @@ import ( "testing" "time" - "github.com/agent-substrate/substrate/internal/benchmarking/boomer/dynconfig" - "github.com/agent-substrate/substrate/internal/benchmarking/boomer/userclass" + "go.ate.dev/substrate/internal/benchmarking/boomer/dynconfig" + "go.ate.dev/substrate/internal/benchmarking/boomer/userclass" ) func TestUniformWaitStaysInRange(t *testing.T) { diff --git a/internal/benchmarking/boomer/resumepaused/resumepaused.go b/internal/benchmarking/boomer/resumepaused/resumepaused.go index 4967d63544..585edef2ef 100644 --- a/internal/benchmarking/boomer/resumepaused/resumepaused.go +++ b/internal/benchmarking/boomer/resumepaused/resumepaused.go @@ -35,11 +35,11 @@ import ( "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.ate.dev/substrate/internal/benchmarking/boomer/boomerutil" + bmetrics "go.ate.dev/substrate/internal/benchmarking/boomer/metrics" + "go.ate.dev/substrate/internal/benchmarking/boomer/userclass" + "go.ate.dev/substrate/pkg/proto/ateapipb" "go.opentelemetry.io/otel" "golang.org/x/time/rate" "google.golang.org/grpc/codes" diff --git a/internal/benchmarking/boomer/resumepaused/resumepaused_test.go b/internal/benchmarking/boomer/resumepaused/resumepaused_test.go index 7aa1572acf..64dc06759c 100644 --- a/internal/benchmarking/boomer/resumepaused/resumepaused_test.go +++ b/internal/benchmarking/boomer/resumepaused/resumepaused_test.go @@ -27,10 +27,10 @@ import ( "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.ate.dev/substrate/internal/benchmarking/boomer/dynconfig" + "go.ate.dev/substrate/internal/benchmarking/boomer/userclass" + "go.ate.dev/substrate/pkg/proto/ateapipb" "go.opentelemetry.io/otel" "google.golang.org/grpc" "google.golang.org/grpc/codes" diff --git a/internal/benchmarking/boomer/sweperf/sweperf.go b/internal/benchmarking/boomer/sweperf/sweperf.go index 0087c8857d..598b1523d3 100644 --- a/internal/benchmarking/boomer/sweperf/sweperf.go +++ b/internal/benchmarking/boomer/sweperf/sweperf.go @@ -36,13 +36,13 @@ import ( "sync/atomic" "time" - "github.com/agent-substrate/substrate/internal/ateinterceptors" - "github.com/agent-substrate/substrate/internal/atenet" - "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.ate.dev/substrate/internal/ateinterceptors" + "go.ate.dev/substrate/internal/atenet" + "go.ate.dev/substrate/internal/benchmarking/boomer/boomerutil" + bmetrics "go.ate.dev/substrate/internal/benchmarking/boomer/metrics" + "go.ate.dev/substrate/internal/benchmarking/boomer/userclass" + "go.ate.dev/substrate/pkg/proto/ateapipb" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/propagation" diff --git a/internal/benchmarking/boomer/sweperf/sweperf_test.go b/internal/benchmarking/boomer/sweperf/sweperf_test.go index 970921c0ef..98698991d8 100644 --- a/internal/benchmarking/boomer/sweperf/sweperf_test.go +++ b/internal/benchmarking/boomer/sweperf/sweperf_test.go @@ -29,13 +29,13 @@ import ( "testing" "time" - "github.com/agent-substrate/substrate/internal/ateinterceptors" - "github.com/agent-substrate/substrate/internal/atenet" - "github.com/agent-substrate/substrate/internal/benchmarking/boomer/boomerutil" - "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" + "go.ate.dev/substrate/internal/ateinterceptors" + "go.ate.dev/substrate/internal/atenet" + "go.ate.dev/substrate/internal/benchmarking/boomer/boomerutil" + "go.ate.dev/substrate/internal/benchmarking/boomer/dynconfig" + "go.ate.dev/substrate/internal/benchmarking/boomer/userclass" + "go.ate.dev/substrate/pkg/proto/ateapipb" "go.opentelemetry.io/otel" "google.golang.org/grpc" "google.golang.org/grpc/codes" diff --git a/internal/benchmarking/boomer/userclass/config.go b/internal/benchmarking/boomer/userclass/config.go index 6e73314a6b..28789ba3ec 100644 --- a/internal/benchmarking/boomer/userclass/config.go +++ b/internal/benchmarking/boomer/userclass/config.go @@ -18,8 +18,8 @@ import ( "net/http" "time" - "github.com/agent-substrate/substrate/internal/benchmarking/boomer/dynconfig" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "go.ate.dev/substrate/internal/benchmarking/boomer/dynconfig" + "go.ate.dev/substrate/pkg/proto/ateapipb" "go.opentelemetry.io/otel/trace" ) diff --git a/internal/benchmarking/glutton/cpu_load.go b/internal/benchmarking/glutton/cpu_load.go index 32833519f1..69174472f7 100644 --- a/internal/benchmarking/glutton/cpu_load.go +++ b/internal/benchmarking/glutton/cpu_load.go @@ -26,7 +26,7 @@ import ( "google.golang.org/grpc/codes" "google.golang.org/grpc/status" - gluttonpb "github.com/agent-substrate/substrate/internal/proto/glutton" + gluttonpb "go.ate.dev/substrate/internal/proto/glutton" ) // UseCPU replaces the currently-running CPU load pool. A num_cores of 0 diff --git a/internal/benchmarking/glutton/cpu_load_test.go b/internal/benchmarking/glutton/cpu_load_test.go index b9afa25cea..ca6a5ca1a3 100644 --- a/internal/benchmarking/glutton/cpu_load_test.go +++ b/internal/benchmarking/glutton/cpu_load_test.go @@ -24,7 +24,7 @@ import ( "google.golang.org/grpc/codes" "google.golang.org/grpc/status" - gluttonpb "github.com/agent-substrate/substrate/internal/proto/glutton" + gluttonpb "go.ate.dev/substrate/internal/proto/glutton" ) // TestUseCPULifecycle covers start, replace, and stop through a series of diff --git a/internal/benchmarking/glutton/fake/server.go b/internal/benchmarking/glutton/fake/server.go index 15bc983f93..49d9c19536 100644 --- a/internal/benchmarking/glutton/fake/server.go +++ b/internal/benchmarking/glutton/fake/server.go @@ -24,9 +24,9 @@ import ( "sync" "testing" - "github.com/agent-substrate/substrate/internal/ateinterceptors" - "github.com/agent-substrate/substrate/internal/benchmarking/glutton" - gluttonpb "github.com/agent-substrate/substrate/internal/proto/glutton" + "go.ate.dev/substrate/internal/ateinterceptors" + "go.ate.dev/substrate/internal/benchmarking/glutton" + gluttonpb "go.ate.dev/substrate/internal/proto/glutton" "google.golang.org/protobuf/proto" ) diff --git a/internal/benchmarking/glutton/gossip.go b/internal/benchmarking/glutton/gossip.go index 6af172e72a..588e02b3ea 100644 --- a/internal/benchmarking/glutton/gossip.go +++ b/internal/benchmarking/glutton/gossip.go @@ -29,7 +29,7 @@ import ( "google.golang.org/grpc/credentials/insecure" "google.golang.org/grpc/status" - gluttonpb "github.com/agent-substrate/substrate/internal/proto/glutton" + gluttonpb "go.ate.dev/substrate/internal/proto/glutton" ) type peerGossip struct { diff --git a/internal/benchmarking/glutton/ram_test.go b/internal/benchmarking/glutton/ram_test.go index 468f565b3f..cf6a35caeb 100644 --- a/internal/benchmarking/glutton/ram_test.go +++ b/internal/benchmarking/glutton/ram_test.go @@ -19,7 +19,7 @@ import ( "context" "testing" - gluttonpb "github.com/agent-substrate/substrate/internal/proto/glutton" + gluttonpb "go.ate.dev/substrate/internal/proto/glutton" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" ) diff --git a/internal/benchmarking/glutton/server.go b/internal/benchmarking/glutton/server.go index 17908ac50d..03f625d285 100644 --- a/internal/benchmarking/glutton/server.go +++ b/internal/benchmarking/glutton/server.go @@ -34,8 +34,8 @@ import ( "google.golang.org/grpc/status" "google.golang.org/protobuf/proto" - "github.com/agent-substrate/substrate/internal/ateinterceptors" - gluttonpb "github.com/agent-substrate/substrate/internal/proto/glutton" + "go.ate.dev/substrate/internal/ateinterceptors" + gluttonpb "go.ate.dev/substrate/internal/proto/glutton" ) // Handler builds the request handler for the given wire mode. ModeGRPC serves diff --git a/internal/benchmarking/glutton/server_test.go b/internal/benchmarking/glutton/server_test.go index 78d6cbea75..98b979a625 100644 --- a/internal/benchmarking/glutton/server_test.go +++ b/internal/benchmarking/glutton/server_test.go @@ -30,8 +30,8 @@ import ( "google.golang.org/grpc/credentials/insecure" "google.golang.org/protobuf/proto" - "github.com/agent-substrate/substrate/internal/ateinterceptors" - gluttonpb "github.com/agent-substrate/substrate/internal/proto/glutton" + "go.ate.dev/substrate/internal/ateinterceptors" + gluttonpb "go.ate.dev/substrate/internal/proto/glutton" ) // TestSplitGRPCServesReadyzAndGRPCOnOneListener starts the grpc-mode handler diff --git a/internal/benchmarking/glutton/service.go b/internal/benchmarking/glutton/service.go index ecf7f24d3b..bd765324ec 100644 --- a/internal/benchmarking/glutton/service.go +++ b/internal/benchmarking/glutton/service.go @@ -34,7 +34,7 @@ import ( "google.golang.org/grpc/codes" "google.golang.org/grpc/status" - gluttonpb "github.com/agent-substrate/substrate/internal/proto/glutton" + gluttonpb "go.ate.dev/substrate/internal/proto/glutton" ) // diskKeyRE rejects anything that could escape the data dir or hit a diff --git a/internal/benchmarking/glutton/service_test.go b/internal/benchmarking/glutton/service_test.go index f2e1fbc6de..d38aa5f991 100644 --- a/internal/benchmarking/glutton/service_test.go +++ b/internal/benchmarking/glutton/service_test.go @@ -25,7 +25,7 @@ import ( "google.golang.org/grpc/codes" "google.golang.org/grpc/status" - gluttonpb "github.com/agent-substrate/substrate/internal/proto/glutton" + gluttonpb "go.ate.dev/substrate/internal/proto/glutton" ) func TestWriteDiskReadDiskRoundTrip(t *testing.T) { diff --git a/internal/benchmarking/glutton/session_ops_test.go b/internal/benchmarking/glutton/session_ops_test.go index 6e89e59b4b..a4778c6049 100644 --- a/internal/benchmarking/glutton/session_ops_test.go +++ b/internal/benchmarking/glutton/session_ops_test.go @@ -26,7 +26,7 @@ import ( "google.golang.org/grpc/codes" "google.golang.org/grpc/status" - gluttonpb "github.com/agent-substrate/substrate/internal/proto/glutton" + gluttonpb "go.ate.dev/substrate/internal/proto/glutton" ) // newTestService builds a Service rooted in a per-test temp dir. diff --git a/internal/benchmarking/glutton/sizes_test.go b/internal/benchmarking/glutton/sizes_test.go index a7d0c1497d..5d8d8a1286 100644 --- a/internal/benchmarking/glutton/sizes_test.go +++ b/internal/benchmarking/glutton/sizes_test.go @@ -18,7 +18,7 @@ import ( "context" "testing" - gluttonpb "github.com/agent-substrate/substrate/internal/proto/glutton" + gluttonpb "go.ate.dev/substrate/internal/proto/glutton" ) func TestParseBytes(t *testing.T) { diff --git a/internal/contextlogging/contextlogging.go b/internal/contextlogging/contextlogging.go index af784b6229..2713537467 100644 --- a/internal/contextlogging/contextlogging.go +++ b/internal/contextlogging/contextlogging.go @@ -22,8 +22,8 @@ import ( "go.opentelemetry.io/otel/trace" "google.golang.org/protobuf/proto" - "github.com/agent-substrate/substrate/internal/ateattr" - "github.com/agent-substrate/substrate/internal/protoredact" + "go.ate.dev/substrate/internal/ateattr" + "go.ate.dev/substrate/internal/protoredact" ) type ContextHandler struct { diff --git a/internal/contextlogging/contextlogging_test.go b/internal/contextlogging/contextlogging_test.go index 29eb97b8d8..72c031e7bf 100644 --- a/internal/contextlogging/contextlogging_test.go +++ b/internal/contextlogging/contextlogging_test.go @@ -26,9 +26,9 @@ import ( "go.opentelemetry.io/otel/trace" - "github.com/agent-substrate/substrate/internal/ateattr" - "github.com/agent-substrate/substrate/internal/protoredact" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "go.ate.dev/substrate/internal/ateattr" + "go.ate.dev/substrate/internal/protoredact" + "go.ate.dev/substrate/pkg/proto/ateapipb" "google.golang.org/protobuf/reflect/protoreflect" ) diff --git a/internal/e2e/clients.go b/internal/e2e/clients.go index 4495812700..49927a5189 100644 --- a/internal/e2e/clients.go +++ b/internal/e2e/clients.go @@ -19,8 +19,8 @@ import ( "fmt" "time" - "github.com/agent-substrate/substrate/internal/ateclient" - "github.com/agent-substrate/substrate/pkg/client/clientset/versioned" + "go.ate.dev/substrate/internal/ateclient" + "go.ate.dev/substrate/pkg/client/clientset/versioned" apiextensionsclientset "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset" "k8s.io/client-go/kubernetes" ) diff --git a/internal/e2e/collector_metrics.go b/internal/e2e/collector_metrics.go index 218ead3a52..4ba3e0c7b4 100644 --- a/internal/e2e/collector_metrics.go +++ b/internal/e2e/collector_metrics.go @@ -23,8 +23,8 @@ import ( "strings" "time" - "github.com/agent-substrate/substrate/internal/ateclient" - "github.com/agent-substrate/substrate/internal/portforward" + "go.ate.dev/substrate/internal/ateclient" + "go.ate.dev/substrate/internal/portforward" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes" "k8s.io/client-go/rest" diff --git a/internal/e2e/credprovider.go b/internal/e2e/credprovider.go index e219182786..bd18c9d819 100644 --- a/internal/e2e/credprovider.go +++ b/internal/e2e/credprovider.go @@ -18,7 +18,7 @@ import ( "path/filepath" "testing" - "github.com/agent-substrate/substrate/internal/installdefaults" + "go.ate.dev/substrate/internal/installdefaults" ) // TODO(yufan-su): Move these helpers into an internal/e2e/credprovider package. diff --git a/internal/e2e/egresspolicy.go b/internal/e2e/egresspolicy.go index dae98f054b..dc8c98048b 100644 --- a/internal/e2e/egresspolicy.go +++ b/internal/e2e/egresspolicy.go @@ -21,8 +21,8 @@ import ( "google.golang.org/grpc/codes" "google.golang.org/grpc/status" - "github.com/agent-substrate/substrate/internal/resources" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "go.ate.dev/substrate/internal/resources" + "go.ate.dev/substrate/pkg/proto/ateapipb" ) // TODO(yufan-su): Move these constructors into an internal/e2e/egresspolicy package. diff --git a/internal/e2e/env.go b/internal/e2e/env.go index 40184026b6..0491442528 100644 --- a/internal/e2e/env.go +++ b/internal/e2e/env.go @@ -18,7 +18,7 @@ import ( "fmt" "os" - "github.com/agent-substrate/substrate/internal/installdefaults" + "go.ate.dev/substrate/internal/installdefaults" ) // CheckEnv checks the list of env vars exist and returns their value. diff --git a/internal/e2e/fixture.go b/internal/e2e/fixture.go index 00b567c066..699bb1cb80 100644 --- a/internal/e2e/fixture.go +++ b/internal/e2e/fixture.go @@ -20,7 +20,7 @@ import ( "testing" "time" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "go.ate.dev/substrate/pkg/proto/ateapipb" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" "google.golang.org/protobuf/encoding/protojson" diff --git a/internal/e2e/fixtures/capabilities/capabilities-templates.yaml.tmpl b/internal/e2e/fixtures/capabilities/capabilities-templates.yaml.tmpl index e783dce184..c0fe161dc2 100644 --- a/internal/e2e/fixtures/capabilities/capabilities-templates.yaml.tmpl +++ b/internal/e2e/fixtures/capabilities/capabilities-templates.yaml.tmpl @@ -29,7 +29,7 @@ workerSelector: workload: caps containers: - name: probe - image: ko://github.com/agent-substrate/substrate/internal/e2e/fixtures/probe + image: ko://go.ate.dev/substrate/internal/e2e/fixtures/probe command: ["/ko-app/probe"] wakeupProbe: httpGet: @@ -59,7 +59,7 @@ workerSelector: workload: caps containers: - name: probe - image: ko://github.com/agent-substrate/substrate/internal/e2e/fixtures/probe + image: ko://go.ate.dev/substrate/internal/e2e/fixtures/probe command: ["/ko-app/probe"] # The probe binds :80, which is exactly what NET_BIND_SERVICE permits, so # this template also proves the granted capability is usable and not merely diff --git a/internal/e2e/fixtures/probe/main_test.go b/internal/e2e/fixtures/probe/main_test.go index cf10505e29..94fff20588 100644 --- a/internal/e2e/fixtures/probe/main_test.go +++ b/internal/e2e/fixtures/probe/main_test.go @@ -27,7 +27,7 @@ import ( "github.com/google/go-cmp/cmp" - "github.com/agent-substrate/substrate/internal/testcert" + "go.ate.dev/substrate/internal/testcert" ) // The capabilities e2e assertions are only as trustworthy as this decoder and diff --git a/internal/e2e/fixtures/probe/probe-multiactor-template.yaml.tmpl b/internal/e2e/fixtures/probe/probe-multiactor-template.yaml.tmpl index 2cf48d5028..fdfa031a94 100644 --- a/internal/e2e/fixtures/probe/probe-multiactor-template.yaml.tmpl +++ b/internal/e2e/fixtures/probe/probe-multiactor-template.yaml.tmpl @@ -22,7 +22,7 @@ workerSelector: workload: probe-multiactor containers: - name: probe - image: ko://github.com/agent-substrate/substrate/internal/e2e/fixtures/probe + image: ko://go.ate.dev/substrate/internal/e2e/fixtures/probe command: ["/ko-app/probe"] volumeMounts: # Identity returned by /whoami. diff --git a/internal/e2e/fixtures/probe/probe-parking-template.yaml.tmpl b/internal/e2e/fixtures/probe/probe-parking-template.yaml.tmpl index c7ca64d87f..120513cfe0 100644 --- a/internal/e2e/fixtures/probe/probe-parking-template.yaml.tmpl +++ b/internal/e2e/fixtures/probe/probe-parking-template.yaml.tmpl @@ -25,7 +25,7 @@ workerSelector: workload: probe-parking containers: - name: probe - image: ko://github.com/agent-substrate/substrate/internal/e2e/fixtures/probe + image: ko://go.ate.dev/substrate/internal/e2e/fixtures/probe command: ["/ko-app/probe"] volumeMounts: - name: system-info diff --git a/internal/e2e/fixtures/probe/probe-sized-template.yaml.tmpl b/internal/e2e/fixtures/probe/probe-sized-template.yaml.tmpl index 1638fc4c9a..b1ed5d078f 100644 --- a/internal/e2e/fixtures/probe/probe-sized-template.yaml.tmpl +++ b/internal/e2e/fixtures/probe/probe-sized-template.yaml.tmpl @@ -25,7 +25,7 @@ workerSelector: workload: probe-sized containers: - name: probe - image: ko://github.com/agent-substrate/substrate/internal/e2e/fixtures/probe + image: ko://go.ate.dev/substrate/internal/e2e/fixtures/probe command: ["/ko-app/probe"] # Gate the golden snapshot on the probe actually serving, so the single # (un-retried) GET /resources the suite makes after resuming cannot race a diff --git a/internal/e2e/fixtures/probe/probe-template.yaml.tmpl b/internal/e2e/fixtures/probe/probe-template.yaml.tmpl index da362a14cb..a5e22aebab 100644 --- a/internal/e2e/fixtures/probe/probe-template.yaml.tmpl +++ b/internal/e2e/fixtures/probe/probe-template.yaml.tmpl @@ -26,7 +26,7 @@ workerSelector: workload: probe${FIXTURE_SUFFIX} containers: - name: probe - image: ko://github.com/agent-substrate/substrate/internal/e2e/fixtures/probe + image: ko://go.ate.dev/substrate/internal/e2e/fixtures/probe command: ["/ko-app/probe"] volumeMounts: # the probe reads actor metadata and trust bundles under /run/ate diff --git a/internal/e2e/fixtures/security/atespace-template.yaml.tmpl b/internal/e2e/fixtures/security/atespace-template.yaml.tmpl index f635ddcc5f..ae64bf69a2 100644 --- a/internal/e2e/fixtures/security/atespace-template.yaml.tmpl +++ b/internal/e2e/fixtures/security/atespace-template.yaml.tmpl @@ -37,7 +37,7 @@ workerSelector: workload: ate-e2e-security containers: - name: probe - image: ko://github.com/agent-substrate/substrate/internal/e2e/fixtures/probe + image: ko://go.ate.dev/substrate/internal/e2e/fixtures/probe command: ["/ko-app/probe"] volumeMounts: # The probe expects its identity files under /run/ate. The mount also makes diff --git a/internal/e2e/fixtures/testserver/common_test.go b/internal/e2e/fixtures/testserver/common_test.go index a64d46153c..9a5098903f 100644 --- a/internal/e2e/fixtures/testserver/common_test.go +++ b/internal/e2e/fixtures/testserver/common_test.go @@ -24,7 +24,7 @@ import ( "testing" "time" - "github.com/agent-substrate/substrate/internal/testcert" + "go.ate.dev/substrate/internal/testcert" ) type originCertificate struct { diff --git a/internal/e2e/fixtures/testserver/egressprobe.go b/internal/e2e/fixtures/testserver/egressprobe.go index 04aa43c631..535f0075bb 100644 --- a/internal/e2e/fixtures/testserver/egressprobe.go +++ b/internal/e2e/fixtures/testserver/egressprobe.go @@ -28,8 +28,8 @@ import ( "github.com/spf13/cobra" - "github.com/agent-substrate/substrate/internal/atunnel/egress" - "github.com/agent-substrate/substrate/internal/credbundle" + "go.ate.dev/substrate/internal/atunnel/egress" + "go.ate.dev/substrate/internal/credbundle" ) // tunnelDestination is the CONNECT authority. atunnel takes this from diff --git a/internal/e2e/fixtures/testserver/egressprobe.yaml.tmpl b/internal/e2e/fixtures/testserver/egressprobe.yaml.tmpl index 7dc0b2f676..9c4a973aae 100644 --- a/internal/e2e/fixtures/testserver/egressprobe.yaml.tmpl +++ b/internal/e2e/fixtures/testserver/egressprobe.yaml.tmpl @@ -30,7 +30,7 @@ spec: restartPolicy: Never containers: - name: egressprobe - image: ko://github.com/agent-substrate/substrate/internal/e2e/fixtures/testserver + image: ko://go.ate.dev/substrate/internal/e2e/fixtures/testserver args: - "egressprobe" - "--listen=:8080" diff --git a/internal/e2e/fixtures/testserver/grpc.go b/internal/e2e/fixtures/testserver/grpc.go index ce80f37d75..14cb6ca572 100644 --- a/internal/e2e/fixtures/testserver/grpc.go +++ b/internal/e2e/fixtures/testserver/grpc.go @@ -30,7 +30,7 @@ import ( healthpb "google.golang.org/grpc/health/grpc_health_v1" "google.golang.org/grpc/status" - "github.com/agent-substrate/substrate/internal/proto/grpcechopb" + "go.ate.dev/substrate/internal/proto/grpcechopb" ) // maxStreamCount bounds EchoStream. A test asks for a handful of messages; a diff --git a/internal/e2e/fixtures/testserver/grpc_test.go b/internal/e2e/fixtures/testserver/grpc_test.go index 71d6430f23..35d5b1c8c4 100644 --- a/internal/e2e/fixtures/testserver/grpc_test.go +++ b/internal/e2e/fixtures/testserver/grpc_test.go @@ -31,7 +31,7 @@ import ( healthpb "google.golang.org/grpc/health/grpc_health_v1" "google.golang.org/grpc/status" - "github.com/agent-substrate/substrate/internal/proto/grpcechopb" + "go.ate.dev/substrate/internal/proto/grpcechopb" ) // The e2e suite reads this fixture's answers as evidence about the egress diff --git a/internal/e2e/fixtures/testserver/grpcecho-template.yaml.tmpl b/internal/e2e/fixtures/testserver/grpcecho-template.yaml.tmpl index 8f81a7081d..e894e82c4e 100644 --- a/internal/e2e/fixtures/testserver/grpcecho-template.yaml.tmpl +++ b/internal/e2e/fixtures/testserver/grpcecho-template.yaml.tmpl @@ -28,7 +28,7 @@ workerSelector: workload: grpcecho${FIXTURE_SUFFIX} containers: - name: grpcecho - image: ko://github.com/agent-substrate/substrate/internal/e2e/fixtures/testserver + image: ko://go.ate.dev/substrate/internal/e2e/fixtures/testserver command: ["/ko-app/testserver"] # gRPC on the Actor's primary port, so the ingress test reaches it at the # Actor's DNS name with no CONNECT and no port juggling -- the plain path diff --git a/internal/e2e/fixtures/testserver/websocket-template.yaml.tmpl b/internal/e2e/fixtures/testserver/websocket-template.yaml.tmpl index 18617008f3..16e8d95f33 100644 --- a/internal/e2e/fixtures/testserver/websocket-template.yaml.tmpl +++ b/internal/e2e/fixtures/testserver/websocket-template.yaml.tmpl @@ -24,7 +24,7 @@ workerSelector: workload: websocket${FIXTURE_SUFFIX} containers: - name: websocket - image: ko://github.com/agent-substrate/substrate/internal/e2e/fixtures/testserver + image: ko://go.ate.dev/substrate/internal/e2e/fixtures/testserver args: ["websocket"] wakeupProbe: httpGet: diff --git a/internal/e2e/pod.go b/internal/e2e/pod.go index baf9bff29f..ed608e1511 100644 --- a/internal/e2e/pod.go +++ b/internal/e2e/pod.go @@ -24,7 +24,7 @@ import ( corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "github.com/agent-substrate/substrate/internal/portforward" + "go.ate.dev/substrate/internal/portforward" ) // WaitForPodReady blocks until the pod passes its readiness probe, and fails the diff --git a/internal/e2e/preflight.go b/internal/e2e/preflight.go index a7127b42f9..a3559eb2a7 100644 --- a/internal/e2e/preflight.go +++ b/internal/e2e/preflight.go @@ -21,7 +21,7 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "go.ate.dev/substrate/pkg/proto/ateapipb" ) // PreflightChecks checks that the test environment is ready for the test suite. diff --git a/internal/e2e/preview.go b/internal/e2e/preview.go index de8068af1a..fb997c64c2 100644 --- a/internal/e2e/preview.go +++ b/internal/e2e/preview.go @@ -22,7 +22,7 @@ import ( "google.golang.org/grpc" - "github.com/agent-substrate/substrate/internal/preview" + "go.ate.dev/substrate/internal/preview" "github.com/spf13/pflag" ) diff --git a/internal/e2e/probe.go b/internal/e2e/probe.go index f96707db4b..fbd99e339a 100644 --- a/internal/e2e/probe.go +++ b/internal/e2e/probe.go @@ -19,7 +19,7 @@ import ( "slices" "testing" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "go.ate.dev/substrate/pkg/proto/ateapipb" ) // ProbeName is the name of the probe fixture's WorkerPool and ActorTemplate, diff --git a/internal/e2e/probe_test.go b/internal/e2e/probe_test.go index 012cec6deb..66468bcf2d 100644 --- a/internal/e2e/probe_test.go +++ b/internal/e2e/probe_test.go @@ -20,7 +20,7 @@ import ( "strings" "testing" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "go.ate.dev/substrate/pkg/proto/ateapipb" ) // TestProbeTemplate_TrustBundle checks that the probe template projects diff --git a/internal/e2e/resume.go b/internal/e2e/resume.go index 2dd0f61526..c753e4b830 100644 --- a/internal/e2e/resume.go +++ b/internal/e2e/resume.go @@ -19,7 +19,7 @@ import ( "testing" "time" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "go.ate.dev/substrate/pkg/proto/ateapipb" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" ) diff --git a/internal/e2e/router_client.go b/internal/e2e/router_client.go index 2a461f24a7..b46ebd16e1 100644 --- a/internal/e2e/router_client.go +++ b/internal/e2e/router_client.go @@ -28,10 +28,10 @@ import ( "sync" "time" - "github.com/agent-substrate/substrate/internal/ateclient" - "github.com/agent-substrate/substrate/internal/atenet" - "github.com/agent-substrate/substrate/internal/portforward" - "github.com/agent-substrate/substrate/internal/resources" + "go.ate.dev/substrate/internal/ateclient" + "go.ate.dev/substrate/internal/atenet" + "go.ate.dev/substrate/internal/portforward" + "go.ate.dev/substrate/internal/resources" "k8s.io/client-go/kubernetes" "k8s.io/client-go/rest" ) diff --git a/internal/e2e/router_client_test.go b/internal/e2e/router_client_test.go index d8b60007fc..f01aa04fc4 100644 --- a/internal/e2e/router_client_test.go +++ b/internal/e2e/router_client_test.go @@ -21,8 +21,8 @@ import ( "strings" "testing" - "github.com/agent-substrate/substrate/internal/atenet" - "github.com/agent-substrate/substrate/internal/resources" + "go.ate.dev/substrate/internal/atenet" + "go.ate.dev/substrate/internal/resources" ) func TestRouterClientPostJSON(t *testing.T) { diff --git a/internal/e2e/sandbox.go b/internal/e2e/sandbox.go index 702b88629b..e8de901130 100644 --- a/internal/e2e/sandbox.go +++ b/internal/e2e/sandbox.go @@ -178,7 +178,7 @@ func RenderFixtureManifest(t *testing.T, relPath, bucket, name string) string { func fixtureSubstitutions(bucket, name string) (inline, blocks map[string]string) { inline = map[string]string{ "${BUCKET_NAME}": bucket, - "${WORKER_IMAGE}": "ko://github.com/agent-substrate/substrate/cmd/ateworker-gvisor", + "${WORKER_IMAGE}": "ko://go.ate.dev/substrate/cmd/ateworker-gvisor", // The manifest-side half of FixtureName: it suffixes the fixture's // namespace, and with it the snapshot prefix underneath. "${FIXTURE_SUFFIX}": "-" + name, @@ -193,7 +193,7 @@ func fixtureSubstitutions(bucket, name string) (inline, blocks map[string]string return inline, blocks } - inline["${WORKER_IMAGE}"] = "ko://github.com/agent-substrate/substrate/cmd/ateworker-microvm" + inline["${WORKER_IMAGE}"] = "ko://go.ate.dev/substrate/cmd/ateworker-microvm" inline["${FIXTURE_SUFFIX}"] = "-" + SandboxClassMicroVM + "-" + name // The micro-VM ActorTemplates name the cluster-wide SandboxConfig // hack/install-microvm-deps.sh installs (configName: microvm), so a diff --git a/internal/e2e/sandbox_test.go b/internal/e2e/sandbox_test.go index d04f1058cc..6e545dd52a 100644 --- a/internal/e2e/sandbox_test.go +++ b/internal/e2e/sandbox_test.go @@ -19,8 +19,8 @@ import ( "strings" "testing" - "github.com/agent-substrate/substrate/pkg/api/v1alpha1" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "go.ate.dev/substrate/pkg/api/v1alpha1" + "go.ate.dev/substrate/pkg/proto/ateapipb" "sigs.k8s.io/yaml" ) diff --git a/internal/e2e/serverpod_test.go b/internal/e2e/serverpod_test.go index df6ad7db9b..d61fcb9a34 100644 --- a/internal/e2e/serverpod_test.go +++ b/internal/e2e/serverpod_test.go @@ -80,14 +80,14 @@ func renderServerPodDocs(t *testing.T, spec ServerPod) (*corev1.Pod, *corev1.Ser func TestRenderServerPod_GRPCProbe(t *testing.T) { pod, service := renderServerPodDocs(t, ServerPod{ Name: "grpcecho", - ImportPath: "github.com/agent-substrate/substrate/internal/e2e/fixtures/testserver", + ImportPath: "go.ate.dev/substrate/internal/e2e/fixtures/testserver", Args: []string{"grpc"}, Port: 50051, GRPCProbe: true, }) container := pod.Spec.Containers[0] - if got, want := container.Image, "ko://github.com/agent-substrate/substrate/internal/e2e/fixtures/testserver"; got != want { + if got, want := container.Image, "ko://go.ate.dev/substrate/internal/e2e/fixtures/testserver"; got != want { t.Errorf("container image = %q, want %q", got, want) } // Args carry the subcommand ahead of the --listen the template appends. The @@ -137,7 +137,7 @@ func TestRenderServerPod_GRPCProbe(t *testing.T) { func TestRenderServerPod_HTTPProbe(t *testing.T) { pod, _ := renderServerPodDocs(t, ServerPod{ Name: "httporigin", - ImportPath: "github.com/agent-substrate/substrate/internal/e2e/fixtures/testserver", + ImportPath: "go.ate.dev/substrate/internal/e2e/fixtures/testserver", Args: []string{"http"}, Port: 8080, }) @@ -170,7 +170,7 @@ func TestRenderServerPod(t *testing.T) { name: "maps a privileged published port to an unprivileged listener", spec: ServerPod{ Name: "egresshttp", - ImportPath: "github.com/agent-substrate/substrate/internal/e2e/fixtures/testserver", + ImportPath: "go.ate.dev/substrate/internal/e2e/fixtures/testserver", Args: []string{"http"}, Port: 80, TargetPort: 8080, @@ -181,7 +181,7 @@ func TestRenderServerPod(t *testing.T) { name: "defaults the listener to Port when TargetPort is unset", spec: ServerPod{ Name: "httporigin", - ImportPath: "github.com/agent-substrate/substrate/internal/e2e/fixtures/testserver", + ImportPath: "go.ate.dev/substrate/internal/e2e/fixtures/testserver", Args: []string{"http"}, Port: 8080, }, @@ -227,7 +227,7 @@ func TestRenderServerPod(t *testing.T) { func TestRenderServerPod_Volumes(t *testing.T) { pod, _ := renderServerPodDocs(t, ServerPod{ Name: "egressprobe", - ImportPath: "github.com/agent-substrate/substrate/internal/e2e/fixtures/testserver", + ImportPath: "go.ate.dev/substrate/internal/e2e/fixtures/testserver", Args: []string{"egressprobe"}, Port: 8080, VolumeMounts: []corev1.VolumeMount{ @@ -319,7 +319,7 @@ func TestRenderServerPod_TLSPort(t *testing.T) { func TestServiceDNSTLS(t *testing.T) { base := ServerPod{ Name: "wss-origin", - ImportPath: "github.com/agent-substrate/substrate/internal/e2e/fixtures/testserver", + ImportPath: "go.ate.dev/substrate/internal/e2e/fixtures/testserver", Args: []string{"websocket", "--echo"}, HealthPath: "/readyz", Volumes: []corev1.Volume{{Name: "extra", VolumeSource: corev1.VolumeSource{EmptyDir: &corev1.EmptyDirVolumeSource{}}}}, diff --git a/internal/e2e/statusz.go b/internal/e2e/statusz.go index cae64166fd..c86b57efef 100644 --- a/internal/e2e/statusz.go +++ b/internal/e2e/statusz.go @@ -21,8 +21,8 @@ import ( "net/http" "time" - "github.com/agent-substrate/substrate/internal/ateclient" - "github.com/agent-substrate/substrate/internal/portforward" + "go.ate.dev/substrate/internal/ateclient" + "go.ate.dev/substrate/internal/portforward" "k8s.io/client-go/kubernetes" ) diff --git a/internal/e2e/suites/capabilities/capabilities_test.go b/internal/e2e/suites/capabilities/capabilities_test.go index 39339350af..0709fa87ea 100644 --- a/internal/e2e/suites/capabilities/capabilities_test.go +++ b/internal/e2e/suites/capabilities/capabilities_test.go @@ -22,9 +22,9 @@ import ( "slices" "testing" - "github.com/agent-substrate/substrate/internal/e2e" - "github.com/agent-substrate/substrate/internal/resources" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "go.ate.dev/substrate/internal/e2e" + "go.ate.dev/substrate/internal/resources" + "go.ate.dev/substrate/pkg/proto/ateapipb" ) // defaultCapabilities mirrors atelet's default set (cmd/atelet/oci.go). It is diff --git a/internal/e2e/suites/capabilities/testmain_test.go b/internal/e2e/suites/capabilities/testmain_test.go index 34b1c0f0a4..19b5579cd5 100644 --- a/internal/e2e/suites/capabilities/testmain_test.go +++ b/internal/e2e/suites/capabilities/testmain_test.go @@ -18,7 +18,7 @@ import ( "os" "testing" - "github.com/agent-substrate/substrate/internal/e2e" + "go.ate.dev/substrate/internal/e2e" ) func TestMain(m *testing.M) { os.Exit(e2e.RunTestMain(m)) } diff --git a/internal/e2e/suites/combinedvolumes/combinedvolumes_test.go b/internal/e2e/suites/combinedvolumes/combinedvolumes_test.go index 236ae0f6b7..c0cc7a39aa 100644 --- a/internal/e2e/suites/combinedvolumes/combinedvolumes_test.go +++ b/internal/e2e/suites/combinedvolumes/combinedvolumes_test.go @@ -30,10 +30,6 @@ import ( "testing" "time" - "github.com/agent-substrate/substrate/internal/e2e" - "github.com/agent-substrate/substrate/internal/preview" - "github.com/agent-substrate/substrate/internal/resources" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" "github.com/google/go-containerregistry/pkg/authn" "github.com/google/go-containerregistry/pkg/name" v1 "github.com/google/go-containerregistry/pkg/v1" @@ -41,6 +37,10 @@ import ( "github.com/google/go-containerregistry/pkg/v1/mutate" "github.com/google/go-containerregistry/pkg/v1/remote" "github.com/google/go-containerregistry/pkg/v1/tarball" + "go.ate.dev/substrate/internal/e2e" + "go.ate.dev/substrate/internal/preview" + "go.ate.dev/substrate/internal/resources" + "go.ate.dev/substrate/pkg/proto/ateapipb" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) diff --git a/internal/e2e/suites/combinedvolumes/testmain_test.go b/internal/e2e/suites/combinedvolumes/testmain_test.go index 0d283d557a..c4e46454a2 100644 --- a/internal/e2e/suites/combinedvolumes/testmain_test.go +++ b/internal/e2e/suites/combinedvolumes/testmain_test.go @@ -18,7 +18,7 @@ import ( "os" "testing" - "github.com/agent-substrate/substrate/internal/e2e" + "go.ate.dev/substrate/internal/e2e" ) func TestMain(m *testing.M) { os.Exit(e2e.RunTestMain(m)) } diff --git a/internal/e2e/suites/demo/demo_test.go b/internal/e2e/suites/demo/demo_test.go index d713ce5bd6..b57684cbc2 100644 --- a/internal/e2e/suites/demo/demo_test.go +++ b/internal/e2e/suites/demo/demo_test.go @@ -23,15 +23,15 @@ import ( "testing" "time" - "github.com/agent-substrate/substrate/internal/ateclient" - "github.com/agent-substrate/substrate/internal/atenet" - "github.com/agent-substrate/substrate/internal/e2e" - "github.com/agent-substrate/substrate/internal/hardware" - "github.com/agent-substrate/substrate/internal/preview" - "github.com/agent-substrate/substrate/internal/resources" - atev1alpha1 "github.com/agent-substrate/substrate/pkg/api/v1alpha1" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" "github.com/google/go-cmp/cmp" + "go.ate.dev/substrate/internal/ateclient" + "go.ate.dev/substrate/internal/atenet" + "go.ate.dev/substrate/internal/e2e" + "go.ate.dev/substrate/internal/hardware" + "go.ate.dev/substrate/internal/preview" + "go.ate.dev/substrate/internal/resources" + atev1alpha1 "go.ate.dev/substrate/pkg/api/v1alpha1" + "go.ate.dev/substrate/pkg/proto/ateapipb" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" "google.golang.org/protobuf/testing/protocmp" diff --git a/internal/e2e/suites/demo/termination_test.go b/internal/e2e/suites/demo/termination_test.go index 369c258eb0..e865ded0cc 100644 --- a/internal/e2e/suites/demo/termination_test.go +++ b/internal/e2e/suites/demo/termination_test.go @@ -19,9 +19,9 @@ import ( "testing" "time" - "github.com/agent-substrate/substrate/internal/e2e" - "github.com/agent-substrate/substrate/internal/resources" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "go.ate.dev/substrate/internal/e2e" + "go.ate.dev/substrate/internal/resources" + "go.ate.dev/substrate/pkg/proto/ateapipb" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" diff --git a/internal/e2e/suites/demo/testmain_test.go b/internal/e2e/suites/demo/testmain_test.go index d25a7d361c..7ecf69a5aa 100644 --- a/internal/e2e/suites/demo/testmain_test.go +++ b/internal/e2e/suites/demo/testmain_test.go @@ -18,7 +18,7 @@ import ( "os" "testing" - "github.com/agent-substrate/substrate/internal/e2e" + "go.ate.dev/substrate/internal/e2e" ) func TestMain(m *testing.M) { os.Exit(e2e.RunTestMain(m)) } diff --git a/internal/e2e/suites/demo/updatetemplate_test.go b/internal/e2e/suites/demo/updatetemplate_test.go index e7d8e954bf..38332b6557 100644 --- a/internal/e2e/suites/demo/updatetemplate_test.go +++ b/internal/e2e/suites/demo/updatetemplate_test.go @@ -20,9 +20,9 @@ import ( "testing" "time" - "github.com/agent-substrate/substrate/internal/e2e" - "github.com/agent-substrate/substrate/internal/resources" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "go.ate.dev/substrate/internal/e2e" + "go.ate.dev/substrate/internal/resources" + "go.ate.dev/substrate/pkg/proto/ateapipb" ) // TestUpdateTemplateLifecycle covers repointing a suspended actor at a diff --git a/internal/e2e/suites/egressauthz/actoridentity_test.go b/internal/e2e/suites/egressauthz/actoridentity_test.go index 5543ed931d..e05ea54658 100644 --- a/internal/e2e/suites/egressauthz/actoridentity_test.go +++ b/internal/e2e/suites/egressauthz/actoridentity_test.go @@ -30,8 +30,8 @@ import ( corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "github.com/agent-substrate/substrate/internal/e2e" - "github.com/agent-substrate/substrate/internal/localca" + "go.ate.dev/substrate/internal/e2e" + "go.ate.dev/substrate/internal/localca" ) // The gateway's front door requires a client certificate signed by the diff --git a/internal/e2e/suites/egressauthz/egressauthz_test.go b/internal/e2e/suites/egressauthz/egressauthz_test.go index e764add3ea..6940da9e28 100644 --- a/internal/e2e/suites/egressauthz/egressauthz_test.go +++ b/internal/e2e/suites/egressauthz/egressauthz_test.go @@ -48,9 +48,9 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes" - "github.com/agent-substrate/substrate/internal/ateclient" - "github.com/agent-substrate/substrate/internal/e2e" - "github.com/agent-substrate/substrate/internal/portforward" + "go.ate.dev/substrate/internal/ateclient" + "go.ate.dev/substrate/internal/e2e" + "go.ate.dev/substrate/internal/portforward" ) const ( diff --git a/internal/e2e/suites/egressauthz/testmain_test.go b/internal/e2e/suites/egressauthz/testmain_test.go index 8b36a55dc7..439325016e 100644 --- a/internal/e2e/suites/egressauthz/testmain_test.go +++ b/internal/e2e/suites/egressauthz/testmain_test.go @@ -18,7 +18,7 @@ import ( "os" "testing" - "github.com/agent-substrate/substrate/internal/e2e" + "go.ate.dev/substrate/internal/e2e" ) // Setup runs before the suite. The probe is created on first use, in the diff --git a/internal/e2e/suites/egresscredinject/egresscredinject_test.go b/internal/e2e/suites/egresscredinject/egresscredinject_test.go index f7ea9fec93..5d92f13c6d 100644 --- a/internal/e2e/suites/egresscredinject/egresscredinject_test.go +++ b/internal/e2e/suites/egresscredinject/egresscredinject_test.go @@ -31,9 +31,9 @@ import ( "testing" "time" - "github.com/agent-substrate/substrate/internal/e2e" - "github.com/agent-substrate/substrate/internal/resources" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "go.ate.dev/substrate/internal/e2e" + "go.ate.dev/substrate/internal/resources" + "go.ate.dev/substrate/pkg/proto/ateapipb" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" ) @@ -48,7 +48,7 @@ var probeNamespace string // the egress gateway trusts once hack/setup-e2e-egress-tls-kind.sh has run. var echoServer = e2e.ServiceDNSTLS(e2e.ServerPod{ Name: "echo", - ImportPath: "github.com/agent-substrate/substrate/internal/e2e/fixtures/testserver", + ImportPath: "go.ate.dev/substrate/internal/e2e/fixtures/testserver", Args: []string{"http"}, }) diff --git a/internal/e2e/suites/egresscredinject/testmain_test.go b/internal/e2e/suites/egresscredinject/testmain_test.go index 03e4c0e124..0ee27c2069 100644 --- a/internal/e2e/suites/egresscredinject/testmain_test.go +++ b/internal/e2e/suites/egresscredinject/testmain_test.go @@ -18,7 +18,7 @@ import ( "os" "testing" - "github.com/agent-substrate/substrate/internal/e2e" + "go.ate.dev/substrate/internal/e2e" ) func TestMain(m *testing.M) { os.Exit(e2e.RunTestMain(m)) } diff --git a/internal/e2e/suites/egressmitm/egressmitm_test.go b/internal/e2e/suites/egressmitm/egressmitm_test.go index 3a0258a325..d4db6dc182 100644 --- a/internal/e2e/suites/egressmitm/egressmitm_test.go +++ b/internal/e2e/suites/egressmitm/egressmitm_test.go @@ -31,9 +31,9 @@ import ( "testing" "time" - "github.com/agent-substrate/substrate/internal/e2e" - "github.com/agent-substrate/substrate/internal/resources" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "go.ate.dev/substrate/internal/e2e" + "go.ate.dev/substrate/internal/resources" + "go.ate.dev/substrate/pkg/proto/ateapipb" ) const probeTemplate = "probe" @@ -199,7 +199,7 @@ func TestActorEgressMITMTrust(t *testing.T) { func originSpec(name string) e2e.ServerPod { return e2e.ServiceDNSTLS(e2e.ServerPod{ Name: name, - ImportPath: "github.com/agent-substrate/substrate/internal/e2e/fixtures/testserver", + ImportPath: "go.ate.dev/substrate/internal/e2e/fixtures/testserver", Args: []string{"http"}, }) } diff --git a/internal/e2e/suites/egressmitm/testmain_test.go b/internal/e2e/suites/egressmitm/testmain_test.go index 0fd7958321..996e88beb6 100644 --- a/internal/e2e/suites/egressmitm/testmain_test.go +++ b/internal/e2e/suites/egressmitm/testmain_test.go @@ -18,7 +18,7 @@ import ( "os" "testing" - "github.com/agent-substrate/substrate/internal/e2e" + "go.ate.dev/substrate/internal/e2e" ) func TestMain(m *testing.M) { os.Exit(e2e.RunTestMain(m)) } diff --git a/internal/e2e/suites/example/testmain_test.go b/internal/e2e/suites/example/testmain_test.go index f221bd77a0..73413a924c 100644 --- a/internal/e2e/suites/example/testmain_test.go +++ b/internal/e2e/suites/example/testmain_test.go @@ -18,7 +18,7 @@ import ( "os" "testing" - "github.com/agent-substrate/substrate/internal/e2e" + "go.ate.dev/substrate/internal/e2e" ) func Setup() { diff --git a/internal/e2e/suites/identity/identity_test.go b/internal/e2e/suites/identity/identity_test.go index fd9dab28da..d19fd7af38 100644 --- a/internal/e2e/suites/identity/identity_test.go +++ b/internal/e2e/suites/identity/identity_test.go @@ -23,9 +23,9 @@ import ( "testing" "time" - "github.com/agent-substrate/substrate/internal/e2e" - "github.com/agent-substrate/substrate/internal/resources" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "go.ate.dev/substrate/internal/e2e" + "go.ate.dev/substrate/internal/resources" + "go.ate.dev/substrate/pkg/proto/ateapipb" ) const probeTemplate = "probe" diff --git a/internal/e2e/suites/identity/testmain_test.go b/internal/e2e/suites/identity/testmain_test.go index 2ccbd931b4..8a9bebc218 100644 --- a/internal/e2e/suites/identity/testmain_test.go +++ b/internal/e2e/suites/identity/testmain_test.go @@ -18,7 +18,7 @@ import ( "os" "testing" - "github.com/agent-substrate/substrate/internal/e2e" + "go.ate.dev/substrate/internal/e2e" ) func TestMain(m *testing.M) { os.Exit(e2e.RunTestMain(m)) } diff --git a/internal/e2e/suites/metrics/metrics_test.go b/internal/e2e/suites/metrics/metrics_test.go index ed15fbbd1d..590d19fef2 100644 --- a/internal/e2e/suites/metrics/metrics_test.go +++ b/internal/e2e/suites/metrics/metrics_test.go @@ -27,10 +27,10 @@ import ( "testing" "time" - "github.com/agent-substrate/substrate/internal/ateattr" - "github.com/agent-substrate/substrate/internal/e2e" - "github.com/agent-substrate/substrate/internal/resources" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "go.ate.dev/substrate/internal/ateattr" + "go.ate.dev/substrate/internal/e2e" + "go.ate.dev/substrate/internal/resources" + "go.ate.dev/substrate/pkg/proto/ateapipb" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) diff --git a/internal/e2e/suites/metrics/testmain_test.go b/internal/e2e/suites/metrics/testmain_test.go index ba011905f1..bad5ee4aab 100644 --- a/internal/e2e/suites/metrics/testmain_test.go +++ b/internal/e2e/suites/metrics/testmain_test.go @@ -18,7 +18,7 @@ import ( "os" "testing" - "github.com/agent-substrate/substrate/internal/e2e" + "go.ate.dev/substrate/internal/e2e" ) func TestMain(m *testing.M) { os.Exit(e2e.RunTestMain(m)) } diff --git a/internal/e2e/suites/multiactor/multiactor_test.go b/internal/e2e/suites/multiactor/multiactor_test.go index 53d8fb1d42..5daf2ccda4 100644 --- a/internal/e2e/suites/multiactor/multiactor_test.go +++ b/internal/e2e/suites/multiactor/multiactor_test.go @@ -22,9 +22,9 @@ import ( "testing" "time" - "github.com/agent-substrate/substrate/internal/e2e" - "github.com/agent-substrate/substrate/internal/resources" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "go.ate.dev/substrate/internal/e2e" + "go.ate.dev/substrate/internal/resources" + "go.ate.dev/substrate/pkg/proto/ateapipb" ) const multiactorTemplate = "probe-multiactor" diff --git a/internal/e2e/suites/multiactor/testmain_test.go b/internal/e2e/suites/multiactor/testmain_test.go index 16095fa959..f60ff8af5b 100644 --- a/internal/e2e/suites/multiactor/testmain_test.go +++ b/internal/e2e/suites/multiactor/testmain_test.go @@ -18,7 +18,7 @@ import ( "os" "testing" - "github.com/agent-substrate/substrate/internal/e2e" + "go.ate.dev/substrate/internal/e2e" ) func TestMain(m *testing.M) { diff --git a/internal/e2e/suites/networking/accesslog_test.go b/internal/e2e/suites/networking/accesslog_test.go index ae56fd064c..21c937428b 100644 --- a/internal/e2e/suites/networking/accesslog_test.go +++ b/internal/e2e/suites/networking/accesslog_test.go @@ -17,7 +17,7 @@ package networking import ( "testing" - "github.com/agent-substrate/substrate/internal/resources" + "go.ate.dev/substrate/internal/resources" ) func TestAccessLogField(t *testing.T) { diff --git a/internal/e2e/suites/networking/arbitraryport_test.go b/internal/e2e/suites/networking/arbitraryport_test.go index 1e7326c4df..d7dbc92c54 100644 --- a/internal/e2e/suites/networking/arbitraryport_test.go +++ b/internal/e2e/suites/networking/arbitraryport_test.go @@ -25,9 +25,9 @@ import ( "testing" "time" - "github.com/agent-substrate/substrate/internal/atenet" - "github.com/agent-substrate/substrate/internal/e2e" - "github.com/agent-substrate/substrate/internal/resources" + "go.ate.dev/substrate/internal/atenet" + "go.ate.dev/substrate/internal/e2e" + "go.ate.dev/substrate/internal/resources" ) // counterExtraPort is the counter demo's second listener (see diff --git a/internal/e2e/suites/networking/egresspolicy_test.go b/internal/e2e/suites/networking/egresspolicy_test.go index be2cc6e2c7..eb1b89a049 100644 --- a/internal/e2e/suites/networking/egresspolicy_test.go +++ b/internal/e2e/suites/networking/egresspolicy_test.go @@ -23,8 +23,8 @@ import ( "testing" "time" - "github.com/agent-substrate/substrate/internal/e2e" - "github.com/agent-substrate/substrate/internal/resources" + "go.ate.dev/substrate/internal/e2e" + "go.ate.dev/substrate/internal/resources" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -185,7 +185,7 @@ func TestActorEgressPassthroughPolicyRefresh(t *testing.T) { dataplane := e2e.CurrentAtenetDataplane() _, address, rootCA := prepareProtocolOrigin(t, ctx, e2e.ServerPod{ Name: "passthrough-origin", - ImportPath: "github.com/agent-substrate/substrate/internal/e2e/fixtures/testserver", + ImportPath: "go.ate.dev/substrate/internal/e2e/fixtures/testserver", Args: []string{"http"}, Port: 80, TargetPort: 8080, diff --git a/internal/e2e/suites/networking/grpcegress_test.go b/internal/e2e/suites/networking/grpcegress_test.go index 97ce9849f2..7c0aff3f50 100644 --- a/internal/e2e/suites/networking/grpcegress_test.go +++ b/internal/e2e/suites/networking/grpcegress_test.go @@ -25,15 +25,15 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "github.com/agent-substrate/substrate/internal/e2e" - "github.com/agent-substrate/substrate/internal/resources" + "go.ate.dev/substrate/internal/e2e" + "go.ate.dev/substrate/internal/resources" ) // grpcEcho is the origin this test deploys: a cleartext-HTTP/2 gRPC server, in // its own namespace, so nothing here depends on the internet. var grpcEcho = e2e.ServerPod{ Name: "grpcecho", - ImportPath: "github.com/agent-substrate/substrate/internal/e2e/fixtures/testserver", + ImportPath: "go.ate.dev/substrate/internal/e2e/fixtures/testserver", Args: []string{"grpc"}, Port: 50051, // A gRPC server answers an HTTP GET with a protocol error, so readiness has diff --git a/internal/e2e/suites/networking/grpcingress_test.go b/internal/e2e/suites/networking/grpcingress_test.go index 2cd02e8522..ce3f1127b5 100644 --- a/internal/e2e/suites/networking/grpcingress_test.go +++ b/internal/e2e/suites/networking/grpcingress_test.go @@ -28,12 +28,12 @@ import ( "google.golang.org/grpc/credentials/insecure" "google.golang.org/grpc/metadata" - "github.com/agent-substrate/substrate/internal/ateclient" - "github.com/agent-substrate/substrate/internal/atenet" - "github.com/agent-substrate/substrate/internal/e2e" - "github.com/agent-substrate/substrate/internal/portforward" - "github.com/agent-substrate/substrate/internal/proto/grpcechopb" - "github.com/agent-substrate/substrate/internal/resources" + "go.ate.dev/substrate/internal/ateclient" + "go.ate.dev/substrate/internal/atenet" + "go.ate.dev/substrate/internal/e2e" + "go.ate.dev/substrate/internal/portforward" + "go.ate.dev/substrate/internal/proto/grpcechopb" + "go.ate.dev/substrate/internal/resources" "k8s.io/client-go/kubernetes" ) diff --git a/internal/e2e/suites/networking/microvmclock_test.go b/internal/e2e/suites/networking/microvmclock_test.go index 43433498f0..cbbef4a26e 100644 --- a/internal/e2e/suites/networking/microvmclock_test.go +++ b/internal/e2e/suites/networking/microvmclock_test.go @@ -23,8 +23,8 @@ import ( "testing" "time" - "github.com/agent-substrate/substrate/internal/e2e" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "go.ate.dev/substrate/internal/e2e" + "go.ate.dev/substrate/pkg/proto/ateapipb" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -74,7 +74,7 @@ func TestMicroVMClockAfterDelayedRestore(t *testing.T) { t.Logf("clock evidence host_utc_after_delay=%s", time.Now().UTC().Format(time.RFC3339Nano)) target, address, rootCA := prepareProtocolOrigin(t, ctx, e2e.ServerPod{ - Name: "clock-origin", ImportPath: "github.com/agent-substrate/substrate/internal/e2e/fixtures/testserver", + Name: "clock-origin", ImportPath: "go.ate.dev/substrate/internal/e2e/fixtures/testserver", Args: []string{"http", "--body=delayed restore clock"}, Port: 8080, TLSPort: 8443, }, true) logOriginCertificateValidity(t, ctx, target, rootCA, suspendBoundary) diff --git a/internal/e2e/suites/networking/networking_test.go b/internal/e2e/suites/networking/networking_test.go index c173ef437e..7d0b94580b 100644 --- a/internal/e2e/suites/networking/networking_test.go +++ b/internal/e2e/suites/networking/networking_test.go @@ -27,12 +27,12 @@ import ( "testing" "time" - "github.com/agent-substrate/substrate/internal/atenet" - "github.com/agent-substrate/substrate/internal/e2e" - "github.com/agent-substrate/substrate/internal/localca" - "github.com/agent-substrate/substrate/internal/resources" - "github.com/agent-substrate/substrate/internal/testcert" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "go.ate.dev/substrate/internal/atenet" + "go.ate.dev/substrate/internal/e2e" + "go.ate.dev/substrate/internal/localca" + "go.ate.dev/substrate/internal/resources" + "go.ate.dev/substrate/internal/testcert" + "go.ate.dev/substrate/pkg/proto/ateapipb" corev1 "k8s.io/api/core/v1" discoveryv1 "k8s.io/api/discovery/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -83,7 +83,7 @@ func TestActorDirectAccess(t *testing.T) { func egressHTTPTarget() e2e.ServerPod { return e2e.ServerPod{ Name: "egresshttp", - ImportPath: "github.com/agent-substrate/substrate/internal/e2e/fixtures/testserver", + ImportPath: "go.ate.dev/substrate/internal/e2e/fixtures/testserver", Args: []string{"http"}, Port: 80, TargetPort: 8080, @@ -198,7 +198,7 @@ func TestActorEgressMultiplexing(t *testing.T) { func egressHTTPSTarget(name, body string) e2e.ServerPod { return e2e.ServiceDNSTLS(e2e.ServerPod{ Name: name, - ImportPath: "github.com/agent-substrate/substrate/internal/e2e/fixtures/testserver", + ImportPath: "go.ate.dev/substrate/internal/e2e/fixtures/testserver", Args: []string{"http", "--body=" + body}, }) } @@ -238,7 +238,7 @@ func TestActorEgressHTTPS(t *testing.T) { // serves nothing but /healthz, which is all this target is dialed for. var httpTarget = e2e.ServerPod{ Name: "httptarget", - ImportPath: "github.com/agent-substrate/substrate/internal/e2e/fixtures/testserver", + ImportPath: "go.ate.dev/substrate/internal/e2e/fixtures/testserver", Args: []string{"http"}, Port: 8080, } @@ -590,7 +590,7 @@ func TestActorEgressHTTPSNonStandardPort(t *testing.T) { const expectedBody = "https nonstandard origin" target, address, _ := prepareProtocolOrigin(t, ctx, e2e.ServerPod{ Name: "https-origin", - ImportPath: "github.com/agent-substrate/substrate/internal/e2e/fixtures/testserver", + ImportPath: "go.ate.dev/substrate/internal/e2e/fixtures/testserver", Args: []string{"http", "--body=" + expectedBody}, Port: 8080, TLSPort: 8443, diff --git a/internal/e2e/suites/networking/testmain_test.go b/internal/e2e/suites/networking/testmain_test.go index 2d31b9c9f5..9e9d471886 100644 --- a/internal/e2e/suites/networking/testmain_test.go +++ b/internal/e2e/suites/networking/testmain_test.go @@ -18,7 +18,7 @@ import ( "os" "testing" - "github.com/agent-substrate/substrate/internal/e2e" + "go.ate.dev/substrate/internal/e2e" ) func TestMain(m *testing.M) { os.Exit(e2e.RunTestMain(m)) } diff --git a/internal/e2e/suites/networking/websocketegress_test.go b/internal/e2e/suites/networking/websocketegress_test.go index f70198e2c6..4a7e683fc3 100644 --- a/internal/e2e/suites/networking/websocketegress_test.go +++ b/internal/e2e/suites/networking/websocketegress_test.go @@ -19,7 +19,7 @@ import ( "net/http" "testing" - "github.com/agent-substrate/substrate/internal/e2e" + "go.ate.dev/substrate/internal/e2e" ) // egressWebSocketResponse mirrors observations returned by the actor. The @@ -41,7 +41,7 @@ type egressWebSocketMessage struct { func TestActorEgressWebSocket(t *testing.T) { ctx := t.Context() _, address, _ := prepareProtocolOrigin(t, ctx, e2e.ServerPod{ - Name: "ws-origin", ImportPath: "github.com/agent-substrate/substrate/internal/e2e/fixtures/testserver", + Name: "ws-origin", ImportPath: "go.ate.dev/substrate/internal/e2e/fixtures/testserver", Args: []string{"websocket", "--echo"}, Port: 80, TargetPort: 8080, HealthPath: "/readyz", }, false) _, router, actorRef := prepareProtocolActor(t, ctx, "ws") @@ -69,7 +69,7 @@ func TestActorEgressWebSocket(t *testing.T) { func TestActorEgressSecureWebSocket(t *testing.T) { ctx := t.Context() _, address, _ := prepareProtocolOrigin(t, ctx, e2e.ServerPod{ - Name: "wss-origin", ImportPath: "github.com/agent-substrate/substrate/internal/e2e/fixtures/testserver", + Name: "wss-origin", ImportPath: "go.ate.dev/substrate/internal/e2e/fixtures/testserver", Args: []string{"websocket", "--echo"}, Port: 80, TargetPort: 8080, TLSPort: 443, HealthPath: "/readyz", }, true) _, router, actorRef := prepareProtocolActor(t, ctx, "wss") diff --git a/internal/e2e/suites/networking/websocketingress_test.go b/internal/e2e/suites/networking/websocketingress_test.go index d05153bb72..48dedc97d8 100644 --- a/internal/e2e/suites/networking/websocketingress_test.go +++ b/internal/e2e/suites/networking/websocketingress_test.go @@ -22,10 +22,10 @@ import ( "testing" "time" - "github.com/agent-substrate/substrate/internal/atenet" - "github.com/agent-substrate/substrate/internal/e2e" - "github.com/agent-substrate/substrate/internal/resources" "github.com/gorilla/websocket" + "go.ate.dev/substrate/internal/atenet" + "go.ate.dev/substrate/internal/e2e" + "go.ate.dev/substrate/internal/resources" ) // deployWebsocketFixture installs the websocket fixture for the sandbox class diff --git a/internal/e2e/suites/networkpolicy/networkpolicy_test.go b/internal/e2e/suites/networkpolicy/networkpolicy_test.go index 3f9efb1b6d..c357e81cce 100644 --- a/internal/e2e/suites/networkpolicy/networkpolicy_test.go +++ b/internal/e2e/suites/networkpolicy/networkpolicy_test.go @@ -33,12 +33,12 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes" - "github.com/agent-substrate/substrate/internal/ateclient" - "github.com/agent-substrate/substrate/internal/e2e" - "github.com/agent-substrate/substrate/internal/portforward" - "github.com/agent-substrate/substrate/internal/resources" - "github.com/agent-substrate/substrate/pkg/api/v1alpha1" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "go.ate.dev/substrate/internal/ateclient" + "go.ate.dev/substrate/internal/e2e" + "go.ate.dev/substrate/internal/portforward" + "go.ate.dev/substrate/internal/resources" + "go.ate.dev/substrate/pkg/api/v1alpha1" + "go.ate.dev/substrate/pkg/proto/ateapipb" ) const ( @@ -283,7 +283,7 @@ func testserverPod(name, namespace string) e2e.ServerPod { return e2e.ServerPod{ Name: name, Namespace: namespace, - ImportPath: "github.com/agent-substrate/substrate/internal/e2e/fixtures/testserver", + ImportPath: "go.ate.dev/substrate/internal/e2e/fixtures/testserver", Args: []string{"http"}, Port: testserverPort, } diff --git a/internal/e2e/suites/networkpolicy/testmain_test.go b/internal/e2e/suites/networkpolicy/testmain_test.go index 6a686e2edb..18a7e30e7b 100644 --- a/internal/e2e/suites/networkpolicy/testmain_test.go +++ b/internal/e2e/suites/networkpolicy/testmain_test.go @@ -18,7 +18,7 @@ import ( "os" "testing" - "github.com/agent-substrate/substrate/internal/e2e" + "go.ate.dev/substrate/internal/e2e" ) func run(m *testing.M) int { diff --git a/internal/e2e/suites/parking/parking_test.go b/internal/e2e/suites/parking/parking_test.go index 0df9c7972c..2cd23ab812 100644 --- a/internal/e2e/suites/parking/parking_test.go +++ b/internal/e2e/suites/parking/parking_test.go @@ -30,9 +30,9 @@ import ( "testing" "time" - "github.com/agent-substrate/substrate/internal/e2e" - "github.com/agent-substrate/substrate/internal/resources" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "go.ate.dev/substrate/internal/e2e" + "go.ate.dev/substrate/internal/resources" + "go.ate.dev/substrate/pkg/proto/ateapipb" ) // parkingAtespace is where deployParkingFixture applies the fixture, and the diff --git a/internal/e2e/suites/parking/testmain_test.go b/internal/e2e/suites/parking/testmain_test.go index 060577f537..d13fb98211 100644 --- a/internal/e2e/suites/parking/testmain_test.go +++ b/internal/e2e/suites/parking/testmain_test.go @@ -18,7 +18,7 @@ import ( "os" "testing" - "github.com/agent-substrate/substrate/internal/e2e" + "go.ate.dev/substrate/internal/e2e" ) func TestMain(m *testing.M) { os.Exit(e2e.RunTestMain(m)) } diff --git a/internal/e2e/suites/sandboxconfig/classdefault_test.go b/internal/e2e/suites/sandboxconfig/classdefault_test.go index 27bba75312..220cf0689c 100644 --- a/internal/e2e/suites/sandboxconfig/classdefault_test.go +++ b/internal/e2e/suites/sandboxconfig/classdefault_test.go @@ -36,8 +36,8 @@ import ( "testing" "time" - "github.com/agent-substrate/substrate/internal/e2e" - atev1alpha1 "github.com/agent-substrate/substrate/pkg/api/v1alpha1" + "go.ate.dev/substrate/internal/e2e" + atev1alpha1 "go.ate.dev/substrate/pkg/api/v1alpha1" corev1 "k8s.io/api/core/v1" eventsv1 "k8s.io/api/events/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" diff --git a/internal/e2e/suites/sandboxconfig/testmain_test.go b/internal/e2e/suites/sandboxconfig/testmain_test.go index e61c70574a..409c439196 100644 --- a/internal/e2e/suites/sandboxconfig/testmain_test.go +++ b/internal/e2e/suites/sandboxconfig/testmain_test.go @@ -18,7 +18,7 @@ import ( "os" "testing" - "github.com/agent-substrate/substrate/internal/e2e" + "go.ate.dev/substrate/internal/e2e" ) func TestMain(m *testing.M) { os.Exit(e2e.RunTestMain(m)) } diff --git a/internal/e2e/suites/security/crossatespace_test.go b/internal/e2e/suites/security/crossatespace_test.go index 0637f3cc33..5d5e1ca58f 100644 --- a/internal/e2e/suites/security/crossatespace_test.go +++ b/internal/e2e/suites/security/crossatespace_test.go @@ -37,10 +37,10 @@ import ( "testing" "time" - "github.com/agent-substrate/substrate/internal/e2e" - "github.com/agent-substrate/substrate/internal/nodepath" - "github.com/agent-substrate/substrate/internal/resources" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "go.ate.dev/substrate/internal/e2e" + "go.ate.dev/substrate/internal/nodepath" + "go.ate.dev/substrate/internal/resources" + "go.ate.dev/substrate/pkg/proto/ateapipb" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" ) diff --git a/internal/e2e/suites/security/testmain_test.go b/internal/e2e/suites/security/testmain_test.go index 3f30d513f4..2c1a5b0a14 100644 --- a/internal/e2e/suites/security/testmain_test.go +++ b/internal/e2e/suites/security/testmain_test.go @@ -18,7 +18,7 @@ import ( "os" "testing" - "github.com/agent-substrate/substrate/internal/e2e" + "go.ate.dev/substrate/internal/e2e" ) func TestMain(m *testing.M) { os.Exit(e2e.RunTestMain(m)) } diff --git a/internal/e2e/suites/sizing/sizing_test.go b/internal/e2e/suites/sizing/sizing_test.go index 6142f41321..7cc179b066 100644 --- a/internal/e2e/suites/sizing/sizing_test.go +++ b/internal/e2e/suites/sizing/sizing_test.go @@ -21,9 +21,9 @@ import ( "net/http" "testing" - "github.com/agent-substrate/substrate/internal/e2e" - "github.com/agent-substrate/substrate/internal/resources" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "go.ate.dev/substrate/internal/e2e" + "go.ate.dev/substrate/internal/resources" + "go.ate.dev/substrate/pkg/proto/ateapipb" ) const ( diff --git a/internal/e2e/suites/sizing/testmain_test.go b/internal/e2e/suites/sizing/testmain_test.go index 5ba57d803f..fc6380ad29 100644 --- a/internal/e2e/suites/sizing/testmain_test.go +++ b/internal/e2e/suites/sizing/testmain_test.go @@ -18,7 +18,7 @@ import ( "os" "testing" - "github.com/agent-substrate/substrate/internal/e2e" + "go.ate.dev/substrate/internal/e2e" ) func TestMain(m *testing.M) { diff --git a/internal/e2e/template.go b/internal/e2e/template.go index 5339e2b89c..102d49ce5e 100644 --- a/internal/e2e/template.go +++ b/internal/e2e/template.go @@ -19,8 +19,8 @@ import ( "testing" "time" - "github.com/agent-substrate/substrate/pkg/api/v1alpha1" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "go.ate.dev/substrate/pkg/api/v1alpha1" + "go.ate.dev/substrate/pkg/proto/ateapipb" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" diff --git a/internal/e2e/template_test.go b/internal/e2e/template_test.go index d870add40b..3211d14f34 100644 --- a/internal/e2e/template_test.go +++ b/internal/e2e/template_test.go @@ -18,8 +18,8 @@ import ( "context" "testing" - "github.com/agent-substrate/substrate/internal/ateclient" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "go.ate.dev/substrate/internal/ateclient" + "go.ate.dev/substrate/pkg/proto/ateapipb" "google.golang.org/grpc" ) diff --git a/internal/e2e/trustbundle.go b/internal/e2e/trustbundle.go index 11c8318453..01f0dbc781 100644 --- a/internal/e2e/trustbundle.go +++ b/internal/e2e/trustbundle.go @@ -23,8 +23,8 @@ import ( "testing" "time" - "github.com/agent-substrate/substrate/internal/clustertrustbundle" - "github.com/agent-substrate/substrate/internal/localca" + "go.ate.dev/substrate/internal/clustertrustbundle" + "go.ate.dev/substrate/internal/localca" corev1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" diff --git a/internal/e2e/trustbundle_test.go b/internal/e2e/trustbundle_test.go index 7b4fe5d0f1..0bcacff98d 100644 --- a/internal/e2e/trustbundle_test.go +++ b/internal/e2e/trustbundle_test.go @@ -24,8 +24,8 @@ import ( "testing" "time" - "github.com/agent-substrate/substrate/internal/localca" - "github.com/agent-substrate/substrate/internal/pemutil" + "go.ate.dev/substrate/internal/localca" + "go.ate.dev/substrate/internal/pemutil" ) // TestAddCA pins what lets the identity suite run alongside the suites doing diff --git a/internal/egresspolicy/egresspolicy.go b/internal/egresspolicy/egresspolicy.go index dd9ae88cf2..4d7e984da7 100644 --- a/internal/egresspolicy/egresspolicy.go +++ b/internal/egresspolicy/egresspolicy.go @@ -33,7 +33,7 @@ import ( "k8s.io/apimachinery/pkg/api/validate/content" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "go.ate.dev/substrate/pkg/proto/ateapipb" ) // Destination is what a request is going to, as far as the leg evaluating it diff --git a/internal/egresspolicy/egresspolicy_test.go b/internal/egresspolicy/egresspolicy_test.go index 9488d50e99..a4561754e0 100644 --- a/internal/egresspolicy/egresspolicy_test.go +++ b/internal/egresspolicy/egresspolicy_test.go @@ -19,7 +19,7 @@ import ( "slices" "testing" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "go.ate.dev/substrate/pkg/proto/ateapipb" ) func httpRule(patterns ...string) *ateapipb.EgressRule { diff --git a/internal/hardware/hardware.go b/internal/hardware/hardware.go index 827f59dd62..1de65f19a1 100644 --- a/internal/hardware/hardware.go +++ b/internal/hardware/hardware.go @@ -19,8 +19,8 @@ package hardware import ( "runtime" - "github.com/agent-substrate/substrate/internal/proto/ateletpb" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "go.ate.dev/substrate/internal/proto/ateletpb" + "go.ate.dev/substrate/pkg/proto/ateapipb" ) const ( diff --git a/internal/hardware/hardware_test.go b/internal/hardware/hardware_test.go index 652313c7ef..6ea10ae6d8 100644 --- a/internal/hardware/hardware_test.go +++ b/internal/hardware/hardware_test.go @@ -18,7 +18,7 @@ import ( "runtime" "testing" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "go.ate.dev/substrate/pkg/proto/ateapipb" ) func TestProbeHost(t *testing.T) { diff --git a/internal/imagecache/bundle_linux_test.go b/internal/imagecache/bundle_linux_test.go index 71c6c0b678..82901031c8 100644 --- a/internal/imagecache/bundle_linux_test.go +++ b/internal/imagecache/bundle_linux_test.go @@ -27,7 +27,7 @@ import ( "golang.org/x/sys/unix" - "github.com/agent-substrate/substrate/internal/roottest" + "go.ate.dev/substrate/internal/roottest" ) // writeLayer builds a layer dir (fs/ tree + whiteouts.json) as the store's diff --git a/internal/imagecache/imagecache.go b/internal/imagecache/imagecache.go index e076345820..a70ce0d21d 100644 --- a/internal/imagecache/imagecache.go +++ b/internal/imagecache/imagecache.go @@ -70,7 +70,7 @@ import ( "golang.org/x/sync/errgroup" "golang.org/x/sync/singleflight" - "github.com/agent-substrate/substrate/internal/ateattr" + "go.ate.dev/substrate/internal/ateattr" ) const ( diff --git a/internal/imagecache/metrics.go b/internal/imagecache/metrics.go index 6e014b4f77..4b51f85551 100644 --- a/internal/imagecache/metrics.go +++ b/internal/imagecache/metrics.go @@ -24,7 +24,7 @@ import ( "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/metric" - "github.com/agent-substrate/substrate/internal/ateattr" + "go.ate.dev/substrate/internal/ateattr" ) const requestsMetric = "ate.imagecache.requests" diff --git a/internal/imagecache/metrics_test.go b/internal/imagecache/metrics_test.go index 2280d5d586..45156586ce 100644 --- a/internal/imagecache/metrics_test.go +++ b/internal/imagecache/metrics_test.go @@ -29,7 +29,7 @@ import ( sdkmetric "go.opentelemetry.io/otel/sdk/metric" "go.opentelemetry.io/otel/sdk/metric/metricdata" - "github.com/agent-substrate/substrate/internal/ateattr" + "go.ate.dev/substrate/internal/ateattr" ) // newMeteredStore opens a store against a local ManualReader-backed provider, diff --git a/internal/linuxsys/original_dst_linux_test.go b/internal/linuxsys/original_dst_linux_test.go index 105095443d..a234b8a08f 100644 --- a/internal/linuxsys/original_dst_linux_test.go +++ b/internal/linuxsys/original_dst_linux_test.go @@ -33,8 +33,8 @@ import ( "github.com/vishvananda/netlink" "golang.org/x/sys/unix" - "github.com/agent-substrate/substrate/internal/netns" - "github.com/agent-substrate/substrate/internal/roottest" + "go.ate.dev/substrate/internal/netns" + "go.ate.dev/substrate/internal/roottest" ) var testNetNSSequence uint64 diff --git a/internal/localjwtauthority/jwks_filesource_test.go b/internal/localjwtauthority/jwks_filesource_test.go index 455e62cbac..66350d6dff 100644 --- a/internal/localjwtauthority/jwks_filesource_test.go +++ b/internal/localjwtauthority/jwks_filesource_test.go @@ -20,8 +20,8 @@ import ( "testing" "time" - "github.com/agent-substrate/substrate/internal/actoridjwt" - "github.com/agent-substrate/substrate/internal/oidcjwt" + "go.ate.dev/substrate/internal/actoridjwt" + "go.ate.dev/substrate/internal/oidcjwt" ) const fileSourceTestIssuer = "https://idp.ate-system.svc" diff --git a/internal/localjwtauthority/localjwtauthority.go b/internal/localjwtauthority/localjwtauthority.go index 6e3fc5adc2..d15ad84c7c 100644 --- a/internal/localjwtauthority/localjwtauthority.go +++ b/internal/localjwtauthority/localjwtauthority.go @@ -31,7 +31,7 @@ import ( "sync" "time" - "github.com/agent-substrate/substrate/internal/actoridjwt" + "go.ate.dev/substrate/internal/actoridjwt" ) // Pool is the interface for a JWT signing pool. diff --git a/internal/localjwtauthority/localjwtauthority_test.go b/internal/localjwtauthority/localjwtauthority_test.go index bc7f538a2f..937ad27608 100644 --- a/internal/localjwtauthority/localjwtauthority_test.go +++ b/internal/localjwtauthority/localjwtauthority_test.go @@ -29,7 +29,7 @@ import ( "github.com/google/go-cmp/cmp" - "github.com/agent-substrate/substrate/internal/actoridjwt" + "go.ate.dev/substrate/internal/actoridjwt" ) func TestRefreshingPool(t *testing.T) { diff --git a/internal/netns/netns_linux_test.go b/internal/netns/netns_linux_test.go index a2a38b5321..313a4bdd76 100644 --- a/internal/netns/netns_linux_test.go +++ b/internal/netns/netns_linux_test.go @@ -22,8 +22,8 @@ import ( "os" "testing" - "github.com/agent-substrate/substrate/internal/roottest" "github.com/vishvananda/netlink" + "go.ate.dev/substrate/internal/roottest" "golang.org/x/sys/unix" ) diff --git a/internal/objectstore/gcs_test.go b/internal/objectstore/gcs_test.go index b83107353e..dfd3e8a9d5 100644 --- a/internal/objectstore/gcs_test.go +++ b/internal/objectstore/gcs_test.go @@ -24,8 +24,8 @@ import ( "testing" "cloud.google.com/go/storage" - "github.com/agent-substrate/substrate/internal/objectstore" "github.com/google/go-cmp/cmp" + "go.ate.dev/substrate/internal/objectstore" "google.golang.org/api/option" ) diff --git a/internal/objectstore/objectstore.go b/internal/objectstore/objectstore.go index 59675e157b..490b730554 100644 --- a/internal/objectstore/objectstore.go +++ b/internal/objectstore/objectstore.go @@ -31,8 +31,8 @@ import ( "strings" "cloud.google.com/go/storage" - "github.com/agent-substrate/substrate/internal/resources" "github.com/aws/smithy-go" + "go.ate.dev/substrate/internal/resources" "golang.org/x/sync/errgroup" ) diff --git a/internal/objectstore/objectstore_test.go b/internal/objectstore/objectstore_test.go index f361741a50..75117b17d3 100644 --- a/internal/objectstore/objectstore_test.go +++ b/internal/objectstore/objectstore_test.go @@ -23,12 +23,12 @@ import ( "testing" "cloud.google.com/go/storage" - "github.com/agent-substrate/substrate/internal/objectstore" - "github.com/agent-substrate/substrate/internal/objectstore/objectstoretest" - "github.com/agent-substrate/substrate/internal/resources" "github.com/aws/aws-sdk-go-v2/service/s3/types" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" + "go.ate.dev/substrate/internal/objectstore" + "go.ate.dev/substrate/internal/objectstore/objectstoretest" + "go.ate.dev/substrate/internal/resources" ) const testLocation = "gs://bucket/root" diff --git a/internal/objectstore/objectstoretest/objectstoretest.go b/internal/objectstore/objectstoretest/objectstoretest.go index d0b5e371ce..6d8fde50ba 100644 --- a/internal/objectstore/objectstoretest/objectstoretest.go +++ b/internal/objectstore/objectstoretest/objectstoretest.go @@ -24,8 +24,8 @@ import ( "sync" "testing" - "github.com/agent-substrate/substrate/internal/objectstore" - "github.com/agent-substrate/substrate/internal/resources" + "go.ate.dev/substrate/internal/objectstore" + "go.ate.dev/substrate/internal/resources" ) // Fake is an in-memory object store. It is safe for concurrent use, as the diff --git a/internal/objectstore/s3_test.go b/internal/objectstore/s3_test.go index 7698a3ddb3..b8d7f93dae 100644 --- a/internal/objectstore/s3_test.go +++ b/internal/objectstore/s3_test.go @@ -25,12 +25,12 @@ import ( "sync" "testing" - "github.com/agent-substrate/substrate/internal/objectstore" "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/credentials" "github.com/aws/aws-sdk-go-v2/service/s3" "github.com/aws/smithy-go" "github.com/google/go-cmp/cmp" + "go.ate.dev/substrate/internal/objectstore" ) // s3Request is one call the SDK made, reduced to what the tests assert on. diff --git a/internal/objectstoreplugin/actorscope/actorscope.go b/internal/objectstoreplugin/actorscope/actorscope.go index 7fc57d2ee9..89e2e0a85d 100644 --- a/internal/objectstoreplugin/actorscope/actorscope.go +++ b/internal/objectstoreplugin/actorscope/actorscope.go @@ -22,8 +22,8 @@ import ( "fmt" "sort" - "github.com/agent-substrate/substrate/internal/resources" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "go.ate.dev/substrate/internal/resources" + "go.ate.dev/substrate/pkg/proto/ateapipb" ) // Access is what a grant permits under its prefix. diff --git a/internal/objectstoreplugin/actorscope/actorscope_test.go b/internal/objectstoreplugin/actorscope/actorscope_test.go index d049d6f446..021c053d22 100644 --- a/internal/objectstoreplugin/actorscope/actorscope_test.go +++ b/internal/objectstoreplugin/actorscope/actorscope_test.go @@ -19,8 +19,8 @@ import ( "github.com/google/go-cmp/cmp" - "github.com/agent-substrate/substrate/internal/resources" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "go.ate.dev/substrate/internal/resources" + "go.ate.dev/substrate/pkg/proto/ateapipb" ) const ( diff --git a/internal/objectstoreplugin/asset.go b/internal/objectstoreplugin/asset.go index 9be7eff19e..36bd0dc7de 100644 --- a/internal/objectstoreplugin/asset.go +++ b/internal/objectstoreplugin/asset.go @@ -20,9 +20,9 @@ import ( "io" "strings" - "github.com/agent-substrate/substrate/pkg/assetfetch" - "github.com/agent-substrate/substrate/pkg/objectstorage" - objectstorev1 "github.com/agent-substrate/substrate/pkg/proto/objectstorepb/v1" + "go.ate.dev/substrate/pkg/assetfetch" + "go.ate.dev/substrate/pkg/objectstorage" + objectstorev1 "go.ate.dev/substrate/pkg/proto/objectstorepb/v1" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" ) diff --git a/internal/objectstoreplugin/asset_test.go b/internal/objectstoreplugin/asset_test.go index 92778f8840..3ab28d6d84 100644 --- a/internal/objectstoreplugin/asset_test.go +++ b/internal/objectstoreplugin/asset_test.go @@ -26,9 +26,9 @@ import ( "sync/atomic" "testing" - "github.com/agent-substrate/substrate/internal/pluginsocket" - "github.com/agent-substrate/substrate/pkg/assetfetch" - objectstorev1 "github.com/agent-substrate/substrate/pkg/proto/objectstorepb/v1" + "go.ate.dev/substrate/internal/pluginsocket" + "go.ate.dev/substrate/pkg/assetfetch" + objectstorev1 "go.ate.dev/substrate/pkg/proto/objectstorepb/v1" "google.golang.org/grpc" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" diff --git a/internal/objectstoreplugin/control.go b/internal/objectstoreplugin/control.go index 69b46787f4..6541584e08 100644 --- a/internal/objectstoreplugin/control.go +++ b/internal/objectstoreplugin/control.go @@ -17,9 +17,9 @@ package objectstoreplugin import ( "context" - "github.com/agent-substrate/substrate/internal/objectstore" - "github.com/agent-substrate/substrate/internal/resources" - objectstorev1 "github.com/agent-substrate/substrate/pkg/proto/objectstorepb/v1" + "go.ate.dev/substrate/internal/objectstore" + "go.ate.dev/substrate/internal/resources" + objectstorev1 "go.ate.dev/substrate/pkg/proto/objectstorepb/v1" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" ) diff --git a/internal/objectstoreplugin/gcsboundary/boundary.go b/internal/objectstoreplugin/gcsboundary/boundary.go index 9e4e356889..cec0bc8600 100644 --- a/internal/objectstoreplugin/gcsboundary/boundary.go +++ b/internal/objectstoreplugin/gcsboundary/boundary.go @@ -23,9 +23,9 @@ import ( "golang.org/x/oauth2/google/downscope" - "github.com/agent-substrate/substrate/internal/objectstore" - "github.com/agent-substrate/substrate/internal/objectstoreplugin/actorscope" - "github.com/agent-substrate/substrate/internal/resources" + "go.ate.dev/substrate/internal/objectstore" + "go.ate.dev/substrate/internal/objectstoreplugin/actorscope" + "go.ate.dev/substrate/internal/resources" ) const ( diff --git a/internal/objectstoreplugin/gcsboundary/boundary_test.go b/internal/objectstoreplugin/gcsboundary/boundary_test.go index f3b718c028..d5cd3597eb 100644 --- a/internal/objectstoreplugin/gcsboundary/boundary_test.go +++ b/internal/objectstoreplugin/gcsboundary/boundary_test.go @@ -20,8 +20,8 @@ import ( "github.com/google/go-cmp/cmp" "golang.org/x/oauth2/google/downscope" - "github.com/agent-substrate/substrate/internal/objectstoreplugin/actorscope" - "github.com/agent-substrate/substrate/internal/resources" + "go.ate.dev/substrate/internal/objectstoreplugin/actorscope" + "go.ate.dev/substrate/internal/resources" ) func prefix(t *testing.T, uri string) resources.StoragePrefix { diff --git a/internal/objectstoreplugin/gcsissuer/issuer.go b/internal/objectstoreplugin/gcsissuer/issuer.go index 372b7f8f35..e48c88b909 100644 --- a/internal/objectstoreplugin/gcsissuer/issuer.go +++ b/internal/objectstoreplugin/gcsissuer/issuer.go @@ -37,8 +37,8 @@ import ( "golang.org/x/oauth2/google/downscope" "k8s.io/utils/lru" - "github.com/agent-substrate/substrate/internal/objectstoreplugin/actorscope" - "github.com/agent-substrate/substrate/internal/objectstoreplugin/gcsboundary" + "go.ate.dev/substrate/internal/objectstoreplugin/actorscope" + "go.ate.dev/substrate/internal/objectstoreplugin/gcsboundary" ) const ( diff --git a/internal/objectstoreplugin/gcsissuer/issuer_test.go b/internal/objectstoreplugin/gcsissuer/issuer_test.go index eb94f58be9..ea07e37baf 100644 --- a/internal/objectstoreplugin/gcsissuer/issuer_test.go +++ b/internal/objectstoreplugin/gcsissuer/issuer_test.go @@ -29,9 +29,9 @@ import ( "golang.org/x/oauth2" "golang.org/x/oauth2/google/downscope" - "github.com/agent-substrate/substrate/internal/objectstoreplugin/actorscope" - "github.com/agent-substrate/substrate/internal/objectstoreplugin/gcsboundary" - "github.com/agent-substrate/substrate/internal/resources" + "go.ate.dev/substrate/internal/objectstoreplugin/actorscope" + "go.ate.dev/substrate/internal/objectstoreplugin/gcsboundary" + "go.ate.dev/substrate/internal/resources" ) func prefix(uri string) resources.StoragePrefix { diff --git a/internal/objectstoreplugin/node.go b/internal/objectstoreplugin/node.go index c4180f65d1..712d3110e0 100644 --- a/internal/objectstoreplugin/node.go +++ b/internal/objectstoreplugin/node.go @@ -29,9 +29,9 @@ import ( "path/filepath" "slices" - "github.com/agent-substrate/substrate/internal/resources" - "github.com/agent-substrate/substrate/pkg/objectstorage" - objectstorev1 "github.com/agent-substrate/substrate/pkg/proto/objectstorepb/v1" + "go.ate.dev/substrate/internal/resources" + "go.ate.dev/substrate/pkg/objectstorage" + objectstorev1 "go.ate.dev/substrate/pkg/proto/objectstorepb/v1" "golang.org/x/sync/errgroup" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" diff --git a/internal/objectstoreplugin/objectstoreplugintest/objectstoreplugintest.go b/internal/objectstoreplugin/objectstoreplugintest/objectstoreplugintest.go index c7012105fb..fa7a019d64 100644 --- a/internal/objectstoreplugin/objectstoreplugintest/objectstoreplugintest.go +++ b/internal/objectstoreplugin/objectstoreplugintest/objectstoreplugintest.go @@ -20,9 +20,9 @@ package objectstoreplugintest import ( "context" - "github.com/agent-substrate/substrate/internal/objectstore" - "github.com/agent-substrate/substrate/internal/objectstoreplugin" - objectstorev1 "github.com/agent-substrate/substrate/pkg/proto/objectstorepb/v1" + "go.ate.dev/substrate/internal/objectstore" + "go.ate.dev/substrate/internal/objectstoreplugin" + objectstorev1 "go.ate.dev/substrate/pkg/proto/objectstorepb/v1" "google.golang.org/grpc" ) diff --git a/internal/objectstoreplugin/plugin_test.go b/internal/objectstoreplugin/plugin_test.go index 6793586348..855701e74b 100644 --- a/internal/objectstoreplugin/plugin_test.go +++ b/internal/objectstoreplugin/plugin_test.go @@ -27,9 +27,9 @@ import ( "testing" "time" - "github.com/agent-substrate/substrate/internal/pluginsocket" - "github.com/agent-substrate/substrate/pkg/objectstorage" - objectstorev1 "github.com/agent-substrate/substrate/pkg/proto/objectstorepb/v1" + "go.ate.dev/substrate/internal/pluginsocket" + "go.ate.dev/substrate/pkg/objectstorage" + objectstorev1 "go.ate.dev/substrate/pkg/proto/objectstorepb/v1" "google.golang.org/grpc" "google.golang.org/grpc/codes" "google.golang.org/grpc/health" diff --git a/internal/objectstoreplugin/status.go b/internal/objectstoreplugin/status.go index 472229fa60..60ba1382ac 100644 --- a/internal/objectstoreplugin/status.go +++ b/internal/objectstoreplugin/status.go @@ -20,8 +20,8 @@ import ( "fmt" "log/slog" - "github.com/agent-substrate/substrate/internal/objectstore" - "github.com/agent-substrate/substrate/pkg/objectstorage" + "go.ate.dev/substrate/internal/objectstore" + "go.ate.dev/substrate/pkg/objectstorage" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" ) diff --git a/internal/objectstoreplugin/status_test.go b/internal/objectstoreplugin/status_test.go index 6cf45397c7..e85c55c289 100644 --- a/internal/objectstoreplugin/status_test.go +++ b/internal/objectstoreplugin/status_test.go @@ -23,7 +23,7 @@ import ( "strings" "testing" - "github.com/agent-substrate/substrate/pkg/objectstorage" + "go.ate.dev/substrate/pkg/objectstorage" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" ) diff --git a/internal/objectstoreplugin/upload_test.go b/internal/objectstoreplugin/upload_test.go index 216530ac05..cbec1a68d8 100644 --- a/internal/objectstoreplugin/upload_test.go +++ b/internal/objectstoreplugin/upload_test.go @@ -26,7 +26,7 @@ import ( "testing" "testing/synctest" - objectstorev1 "github.com/agent-substrate/substrate/pkg/proto/objectstorepb/v1" + objectstorev1 "go.ate.dev/substrate/pkg/proto/objectstorepb/v1" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" ) diff --git a/internal/ocispec/gvisor.go b/internal/ocispec/gvisor.go index 135e44ae7e..cde27bccc8 100644 --- a/internal/ocispec/gvisor.go +++ b/internal/ocispec/gvisor.go @@ -18,9 +18,9 @@ import ( "path/filepath" "slices" - "github.com/agent-substrate/substrate/internal/proto/ateworkerpb" - "github.com/agent-substrate/substrate/internal/sizing" "github.com/opencontainers/runtime-spec/specs-go" + "go.ate.dev/substrate/internal/proto/ateworkerpb" + "go.ate.dev/substrate/internal/sizing" ) // PauseContainer is the name of the sandbox root container. The underscore diff --git a/internal/ocispec/microvm.go b/internal/ocispec/microvm.go index 33b307020c..5fcdb231c1 100644 --- a/internal/ocispec/microvm.go +++ b/internal/ocispec/microvm.go @@ -21,9 +21,9 @@ import ( "slices" "strings" - "github.com/agent-substrate/substrate/internal/imagecache" - "github.com/agent-substrate/substrate/internal/proto/ateworkerpb" "github.com/opencontainers/runtime-spec/specs-go" + "go.ate.dev/substrate/internal/imagecache" + "go.ate.dev/substrate/internal/proto/ateworkerpb" ) // Sub-share names inside the micro-VM virtio-fs share. diff --git a/internal/ocispec/microvm_test.go b/internal/ocispec/microvm_test.go index c791bbf25f..905a0f792e 100644 --- a/internal/ocispec/microvm_test.go +++ b/internal/ocispec/microvm_test.go @@ -17,8 +17,8 @@ package ocispec import ( "testing" - "github.com/agent-substrate/substrate/internal/proto/ateworkerpb" "github.com/opencontainers/runtime-spec/specs-go" + "go.ate.dev/substrate/internal/proto/ateworkerpb" ) // The device allowlist and CPU shares from guestResources are the proven-good diff --git a/internal/ocispec/ocispec.go b/internal/ocispec/ocispec.go index 7c40caa656..b6a7c18914 100644 --- a/internal/ocispec/ocispec.go +++ b/internal/ocispec/ocispec.go @@ -23,10 +23,10 @@ import ( "path" "path/filepath" - "github.com/agent-substrate/substrate/internal/imagecache" - "github.com/agent-substrate/substrate/internal/nodepath" - "github.com/agent-substrate/substrate/internal/proto/ateworkerpb" "github.com/opencontainers/runtime-spec/specs-go" + "go.ate.dev/substrate/internal/imagecache" + "go.ate.dev/substrate/internal/nodepath" + "go.ate.dev/substrate/internal/proto/ateworkerpb" ) // specFile is the OCI spec file name within a bundle. diff --git a/internal/ocispec/ocispec_test.go b/internal/ocispec/ocispec_test.go index 4fec74c022..8b945c4476 100644 --- a/internal/ocispec/ocispec_test.go +++ b/internal/ocispec/ocispec_test.go @@ -21,10 +21,10 @@ import ( "slices" "testing" - "github.com/agent-substrate/substrate/internal/imagecache" - "github.com/agent-substrate/substrate/internal/nodepath" - "github.com/agent-substrate/substrate/internal/proto/ateworkerpb" "github.com/opencontainers/runtime-spec/specs-go" + "go.ate.dev/substrate/internal/imagecache" + "go.ate.dev/substrate/internal/nodepath" + "go.ate.dev/substrate/internal/proto/ateworkerpb" ) // mountFor returns the spec's mount at destination dest, or fails. diff --git a/internal/ocispec/parity_test.go b/internal/ocispec/parity_test.go index 809c67ea03..e448625b07 100644 --- a/internal/ocispec/parity_test.go +++ b/internal/ocispec/parity_test.go @@ -20,9 +20,9 @@ import ( "strings" "testing" - "github.com/agent-substrate/substrate/internal/proto/ateworkerpb" - "github.com/agent-substrate/substrate/internal/sizing" "github.com/opencontainers/runtime-spec/specs-go" + "go.ate.dev/substrate/internal/proto/ateworkerpb" + "go.ate.dev/substrate/internal/sizing" ) const testActorUID = "actor_uid" diff --git a/internal/oidcjwt/oidcjwt.go b/internal/oidcjwt/oidcjwt.go index cd6ce5e8a4..fccd1788ea 100644 --- a/internal/oidcjwt/oidcjwt.go +++ b/internal/oidcjwt/oidcjwt.go @@ -36,7 +36,7 @@ import ( "sync" "time" - "github.com/agent-substrate/substrate/internal/actoridjwt" + "go.ate.dev/substrate/internal/actoridjwt" ) // KeyAndID wraps a crypto.PublicKey and its JWK key ID. diff --git a/internal/oidcjwt/oidcjwt_test.go b/internal/oidcjwt/oidcjwt_test.go index 91027b85d4..a7b9cf0684 100644 --- a/internal/oidcjwt/oidcjwt_test.go +++ b/internal/oidcjwt/oidcjwt_test.go @@ -36,7 +36,7 @@ import ( "testing" "time" - "github.com/agent-substrate/substrate/internal/actoridjwt" + "go.ate.dev/substrate/internal/actoridjwt" ) const testAudience = "ate-api" diff --git a/internal/otlprelay/e2e_test.go b/internal/otlprelay/e2e_test.go index b355b6c1be..da75f3b3ee 100644 --- a/internal/otlprelay/e2e_test.go +++ b/internal/otlprelay/e2e_test.go @@ -20,7 +20,7 @@ import ( "testing" "time" - "github.com/agent-substrate/substrate/internal/serverboot" + "go.ate.dev/substrate/internal/serverboot" ) // TestEndToEndThroughServerboot exercises the whole path a worker span actually diff --git a/internal/plugins/gcp-secret-manager/cmd/gsm-credential-provider/clientpool.go b/internal/plugins/gcp-secret-manager/cmd/gsm-credential-provider/clientpool.go index 0c965a13b7..efc383bf1c 100644 --- a/internal/plugins/gcp-secret-manager/cmd/gsm-credential-provider/clientpool.go +++ b/internal/plugins/gcp-secret-manager/cmd/gsm-credential-provider/clientpool.go @@ -18,7 +18,7 @@ import ( "errors" "sync" - "github.com/agent-substrate/substrate/internal/plugins/gcp-secret-manager/internal/provider" + "go.ate.dev/substrate/internal/plugins/gcp-secret-manager/internal/provider" ) // secretManagerClient is a closable SecretAccessor. diff --git a/internal/plugins/gcp-secret-manager/cmd/gsm-credential-provider/imports_test.go b/internal/plugins/gcp-secret-manager/cmd/gsm-credential-provider/imports_test.go index 1913be81fc..60b57818b7 100644 --- a/internal/plugins/gcp-secret-manager/cmd/gsm-credential-provider/imports_test.go +++ b/internal/plugins/gcp-secret-manager/cmd/gsm-credential-provider/imports_test.go @@ -26,7 +26,7 @@ import ( ) // substrateModule is the module this one is hosted in until it moves out. -const substrateModule = "github.com/agent-substrate/substrate" +const substrateModule = "go.ate.dev/substrate" // This module moves to a repository of its own by being copied, so it may // import only substrate's public packages. Go's internal rule goes by import diff --git a/internal/plugins/gcp-secret-manager/cmd/gsm-credential-provider/main.go b/internal/plugins/gcp-secret-manager/cmd/gsm-credential-provider/main.go index 5700ac64db..77304c459a 100644 --- a/internal/plugins/gcp-secret-manager/cmd/gsm-credential-provider/main.go +++ b/internal/plugins/gcp-secret-manager/cmd/gsm-credential-provider/main.go @@ -37,10 +37,10 @@ import ( "google.golang.org/api/option" "google.golang.org/grpc" - "github.com/agent-substrate/substrate/pkg/proto/credproviderpb" + "go.ate.dev/substrate/pkg/proto/credproviderpb" - "github.com/agent-substrate/substrate/internal/plugins/gcp-secret-manager/internal/mtls" - "github.com/agent-substrate/substrate/internal/plugins/gcp-secret-manager/internal/provider" + "go.ate.dev/substrate/internal/plugins/gcp-secret-manager/internal/mtls" + "go.ate.dev/substrate/internal/plugins/gcp-secret-manager/internal/provider" ) var ( diff --git a/internal/plugins/gcp-secret-manager/cmd/gsm-credential-provider/main_test.go b/internal/plugins/gcp-secret-manager/cmd/gsm-credential-provider/main_test.go index cec540dcbb..70102a1aa7 100644 --- a/internal/plugins/gcp-secret-manager/cmd/gsm-credential-provider/main_test.go +++ b/internal/plugins/gcp-secret-manager/cmd/gsm-credential-provider/main_test.go @@ -32,7 +32,7 @@ import ( "google.golang.org/grpc/credentials/insecure" "google.golang.org/grpc/test/bufconn" - "github.com/agent-substrate/substrate/pkg/proto/credproviderpb" + "go.ate.dev/substrate/pkg/proto/credproviderpb" ) // /healthz passes regardless of readiness; /readyz follows the ready flag. diff --git a/internal/plugins/gcp-secret-manager/config/deployment.yaml b/internal/plugins/gcp-secret-manager/config/deployment.yaml index 689521f19d..66838d737a 100644 --- a/internal/plugins/gcp-secret-manager/config/deployment.yaml +++ b/internal/plugins/gcp-secret-manager/config/deployment.yaml @@ -36,7 +36,7 @@ spec: runAsNonRoot: true containers: - name: gsm-credential-provider - image: ko://github.com/agent-substrate/substrate/internal/plugins/gcp-secret-manager/cmd/gsm-credential-provider + image: ko://go.ate.dev/substrate/internal/plugins/gcp-secret-manager/cmd/gsm-credential-provider args: - "--listen-address=:50051" - "--health-address=:9090" diff --git a/internal/plugins/gcp-secret-manager/go.mod b/internal/plugins/gcp-secret-manager/go.mod index feb77e0d4b..b45e90bcf4 100644 --- a/internal/plugins/gcp-secret-manager/go.mod +++ b/internal/plugins/gcp-secret-manager/go.mod @@ -1,11 +1,11 @@ -module github.com/agent-substrate/substrate/internal/plugins/gcp-secret-manager +module go.ate.dev/substrate/internal/plugins/gcp-secret-manager go 1.27.0 require ( cloud.google.com/go/secretmanager v1.16.0 - github.com/agent-substrate/substrate v0.2.0 github.com/googleapis/gax-go/v2 v2.21.0 + go.ate.dev/substrate v0.2.0 google.golang.org/api v0.274.0 google.golang.org/grpc v1.83.2 sigs.k8s.io/yaml v1.6.0 @@ -42,4 +42,4 @@ require ( google.golang.org/protobuf v1.36.12 // indirect ) -replace github.com/agent-substrate/substrate => ../../.. +replace go.ate.dev/substrate => ../../.. diff --git a/internal/plugins/gcp-secret-manager/internal/mtls/mtls.go b/internal/plugins/gcp-secret-manager/internal/mtls/mtls.go index a8c2265577..e146262381 100644 --- a/internal/plugins/gcp-secret-manager/internal/mtls/mtls.go +++ b/internal/plugins/gcp-secret-manager/internal/mtls/mtls.go @@ -25,7 +25,7 @@ import ( "google.golang.org/grpc/credentials" - "github.com/agent-substrate/substrate/internal/plugins/gcp-secret-manager/internal/credbundle" + "go.ate.dev/substrate/internal/plugins/gcp-secret-manager/internal/credbundle" ) // Config names the files and caller identity ServerCredentials serves with. diff --git a/internal/plugins/gcp-secret-manager/internal/provider/provider.go b/internal/plugins/gcp-secret-manager/internal/provider/provider.go index 86cccfd273..61fd97ac70 100644 --- a/internal/plugins/gcp-secret-manager/internal/provider/provider.go +++ b/internal/plugins/gcp-secret-manager/internal/provider/provider.go @@ -34,7 +34,7 @@ import ( "google.golang.org/grpc/codes" "google.golang.org/grpc/status" - "github.com/agent-substrate/substrate/pkg/proto/credproviderpb" + "go.ate.dev/substrate/pkg/proto/credproviderpb" ) // providerName is the ate-secret:// URI host this backend serves. diff --git a/internal/plugins/gcp-secret-manager/internal/provider/provider_test.go b/internal/plugins/gcp-secret-manager/internal/provider/provider_test.go index 6f191d7f5c..4488769214 100644 --- a/internal/plugins/gcp-secret-manager/internal/provider/provider_test.go +++ b/internal/plugins/gcp-secret-manager/internal/provider/provider_test.go @@ -28,7 +28,7 @@ import ( "google.golang.org/grpc/codes" "google.golang.org/grpc/status" - "github.com/agent-substrate/substrate/pkg/proto/credproviderpb" + "go.ate.dev/substrate/pkg/proto/credproviderpb" ) // fakeAccessor is a fake Secret Manager client serving payloads by resource diff --git a/internal/pluginsocket/calls_test.go b/internal/pluginsocket/calls_test.go index d9ae9cc717..f123a79ad4 100644 --- a/internal/pluginsocket/calls_test.go +++ b/internal/pluginsocket/calls_test.go @@ -18,7 +18,7 @@ import ( "errors" "testing" - "github.com/agent-substrate/substrate/internal/apierror" + "go.ate.dev/substrate/internal/apierror" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" ) diff --git a/internal/pluginsocket/pluginsocket.go b/internal/pluginsocket/pluginsocket.go index e6fedc41da..6d853f1356 100644 --- a/internal/pluginsocket/pluginsocket.go +++ b/internal/pluginsocket/pluginsocket.go @@ -27,7 +27,7 @@ import ( "path/filepath" "time" - "github.com/agent-substrate/substrate/internal/apierror" + "go.ate.dev/substrate/internal/apierror" "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc" "google.golang.org/grpc" "google.golang.org/grpc/backoff" diff --git a/internal/pluginsocket/pluginsocket_test.go b/internal/pluginsocket/pluginsocket_test.go index b26e83a490..6b2d085045 100644 --- a/internal/pluginsocket/pluginsocket_test.go +++ b/internal/pluginsocket/pluginsocket_test.go @@ -22,7 +22,7 @@ import ( "testing" "time" - objectstorev1 "github.com/agent-substrate/substrate/pkg/proto/objectstorepb/v1" + objectstorev1 "go.ate.dev/substrate/pkg/proto/objectstorepb/v1" "google.golang.org/grpc" "google.golang.org/grpc/codes" "google.golang.org/grpc/connectivity" diff --git a/internal/proto/ateletpb/atelet.pb.go b/internal/proto/ateletpb/atelet.pb.go index 885c0f06d9..0296359eb0 100644 --- a/internal/proto/ateletpb/atelet.pb.go +++ b/internal/proto/ateletpb/atelet.pb.go @@ -4370,7 +4370,7 @@ const file_atelet_proto_rawDesc = "" + "\aRestore\x12\x16.atelet.RestoreRequest\x1a\x17.atelet.RestoreResponse\"\x00\x12T\n" + "\x0fPromoteSnapshot\x12\x1e.atelet.PromoteSnapshotRequest\x1a\x1f.atelet.PromoteSnapshotResponse\"\x00\x12H\n" + "\vDetachActor\x12\x1a.atelet.DetachActorRequest\x1a\x1b.atelet.DetachActorResponse\"\x00\x12B\n" + - "\tTerminate\x12\x18.atelet.TerminateRequest\x1a\x19.atelet.TerminateResponse\"\x00B>Z\n" + "\aBurnCPU\x12\x17.glutton.BurnCPURequest\x1a\x18.glutton.BurnCPUResponse\"\x00\x12;\n" + "\x06Ingest\x12\x16.glutton.IngestRequest\x1a\x17.glutton.IngestResponse\"\x00\x12;\n" + - "\x06UseCPU\x12\x16.glutton.UseCPURequest\x1a\x17.glutton.UseCPUResponse\"\x00B=Z;github.com/agent-substrate/substrate/internal/proto/gluttonb\x06proto3" + "\x06UseCPU\x12\x16.glutton.UseCPURequest\x1a\x17.glutton.UseCPUResponse\"\x00B-Z+go.ate.dev/substrate/internal/proto/gluttonb\x06proto3" var ( file_glutton_proto_rawDescOnce sync.Once diff --git a/internal/proto/glutton/glutton.proto b/internal/proto/glutton/glutton.proto index 1e278574f3..e717abd4cd 100644 --- a/internal/proto/glutton/glutton.proto +++ b/internal/proto/glutton/glutton.proto @@ -16,7 +16,7 @@ syntax = "proto3"; package glutton; -option go_package = "github.com/agent-substrate/substrate/internal/proto/glutton"; +option go_package = "go.ate.dev/substrate/internal/proto/glutton"; // A glutton is a single small piece of a larger benchmarking suite. It is designed to be instantiated // multiple times, with each piece configured to do different units of work. diff --git a/internal/proto/grpcechopb/grpcecho.pb.go b/internal/proto/grpcechopb/grpcecho.pb.go index d0daaf535f..8ba5112683 100644 --- a/internal/proto/grpcechopb/grpcecho.pb.go +++ b/internal/proto/grpcechopb/grpcecho.pb.go @@ -206,7 +206,7 @@ const file_grpcecho_proto_rawDesc = "" + "\x04Echo\x12\x15.grpcecho.EchoRequest\x1a\x16.grpcecho.EchoResponse\"\x00\x12E\n" + "\n" + "EchoStream\x12\x1b.grpcecho.EchoStreamRequest\x1a\x16.grpcecho.EchoResponse\"\x000\x01\x12?\n" + - "\bEchoBidi\x12\x15.grpcecho.EchoRequest\x1a\x16.grpcecho.EchoResponse\"\x00(\x010\x01B@Z>github.com/agent-substrate/substrate/internal/proto/grpcechopbb\x06proto3" + "\bEchoBidi\x12\x15.grpcecho.EchoRequest\x1a\x16.grpcecho.EchoResponse\"\x00(\x010\x01B0Z.go.ate.dev/substrate/internal/proto/grpcechopbb\x06proto3" var ( file_grpcecho_proto_rawDescOnce sync.Once diff --git a/internal/proto/grpcechopb/grpcecho.proto b/internal/proto/grpcechopb/grpcecho.proto index 8e38ffe3db..437b881f1a 100644 --- a/internal/proto/grpcechopb/grpcecho.proto +++ b/internal/proto/grpcechopb/grpcecho.proto @@ -16,7 +16,7 @@ syntax = "proto3"; package grpcecho; -option go_package = "github.com/agent-substrate/substrate/internal/proto/grpcechopb"; +option go_package = "go.ate.dev/substrate/internal/proto/grpcechopb"; // Echo is the smallest service that can prove a gRPC request and its response // crossed a network path intact. It exists for the egress e2e coverage: the diff --git a/internal/protoredact/protoredact_test.go b/internal/protoredact/protoredact_test.go index 9750ddb4bf..28c9ef9d92 100644 --- a/internal/protoredact/protoredact_test.go +++ b/internal/protoredact/protoredact_test.go @@ -23,15 +23,15 @@ import ( "sync" "testing" - "github.com/agent-substrate/substrate/internal/proto/ateletpb" - "github.com/agent-substrate/substrate/internal/proto/ateworkerpb" - "github.com/agent-substrate/substrate/internal/proto/glutton" - "github.com/agent-substrate/substrate/internal/proto/grpcechopb" - "github.com/agent-substrate/substrate/internal/protoredact" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" - "github.com/agent-substrate/substrate/pkg/proto/ateconfigpb" - "github.com/agent-substrate/substrate/pkg/proto/credproviderpb" - objectstorev1 "github.com/agent-substrate/substrate/pkg/proto/objectstorepb/v1" + "go.ate.dev/substrate/internal/proto/ateletpb" + "go.ate.dev/substrate/internal/proto/ateworkerpb" + "go.ate.dev/substrate/internal/proto/glutton" + "go.ate.dev/substrate/internal/proto/grpcechopb" + "go.ate.dev/substrate/internal/protoredact" + "go.ate.dev/substrate/pkg/proto/ateapipb" + "go.ate.dev/substrate/pkg/proto/ateconfigpb" + "go.ate.dev/substrate/pkg/proto/credproviderpb" + objectstorev1 "go.ate.dev/substrate/pkg/proto/objectstorepb/v1" "google.golang.org/protobuf/proto" "google.golang.org/protobuf/reflect/protodesc" "google.golang.org/protobuf/reflect/protoreflect" diff --git a/internal/resources/actorattribution.go b/internal/resources/actorattribution.go index a1907690d5..4704bb9311 100644 --- a/internal/resources/actorattribution.go +++ b/internal/resources/actorattribution.go @@ -14,7 +14,7 @@ package resources -import "github.com/agent-substrate/substrate/pkg/proto/ateapipb" +import "go.ate.dev/substrate/pkg/proto/ateapipb" // ActorAttribution is what telemetry about an actor is attributed to: an // ActorRef plus the two things a ref does not carry, the server-assigned uid and diff --git a/internal/resources/quantities.go b/internal/resources/quantities.go index 2e4072215f..76c97362f7 100644 --- a/internal/resources/quantities.go +++ b/internal/resources/quantities.go @@ -18,7 +18,7 @@ import ( "fmt" "sort" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "go.ate.dev/substrate/pkg/proto/ateapipb" "k8s.io/apimachinery/pkg/api/resource" ) diff --git a/internal/resources/quantities_test.go b/internal/resources/quantities_test.go index 34aa1b2f24..c141566259 100644 --- a/internal/resources/quantities_test.go +++ b/internal/resources/quantities_test.go @@ -17,7 +17,7 @@ package resources import ( "testing" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "go.ate.dev/substrate/pkg/proto/ateapipb" "google.golang.org/protobuf/proto" "k8s.io/apimachinery/pkg/api/resource" ) diff --git a/internal/resources/resourceref.go b/internal/resources/resourceref.go index a14d079d75..3c27bd12f0 100644 --- a/internal/resources/resourceref.go +++ b/internal/resources/resourceref.go @@ -18,7 +18,7 @@ import ( "log/slog" "reflect" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "go.ate.dev/substrate/pkg/proto/ateapipb" ) // Resource is any Atespaced resource message carrying the common metadata. diff --git a/internal/resources/resourceref_test.go b/internal/resources/resourceref_test.go index d1c416a85a..47eeaa9b6e 100644 --- a/internal/resources/resourceref_test.go +++ b/internal/resources/resourceref_test.go @@ -19,7 +19,7 @@ import ( "reflect" "testing" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "go.ate.dev/substrate/pkg/proto/ateapipb" ) // The ref aliases must stay distinct types: the phantom ResourceRef kind is diff --git a/internal/resources/validate.go b/internal/resources/validate.go index 03002aad97..7376fb6b1b 100644 --- a/internal/resources/validate.go +++ b/internal/resources/validate.go @@ -27,11 +27,11 @@ import ( "strconv" "strings" - "github.com/agent-substrate/substrate/internal/apierror" - "github.com/agent-substrate/substrate/internal/proto/ateworkerpb" - "github.com/agent-substrate/substrate/internal/volumepath" - "github.com/agent-substrate/substrate/pkg/sandboxclass" "github.com/distribution/reference" + "go.ate.dev/substrate/internal/apierror" + "go.ate.dev/substrate/internal/proto/ateworkerpb" + "go.ate.dev/substrate/internal/volumepath" + "go.ate.dev/substrate/pkg/sandboxclass" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" "google.golang.org/protobuf/proto" diff --git a/internal/resources/validate_test.go b/internal/resources/validate_test.go index 0cf25d8261..d702097dd0 100644 --- a/internal/resources/validate_test.go +++ b/internal/resources/validate_test.go @@ -22,9 +22,9 @@ import ( "strings" "testing" - "github.com/agent-substrate/substrate/internal/apierror" - "github.com/agent-substrate/substrate/internal/proto/ateworkerpb" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "go.ate.dev/substrate/internal/apierror" + "go.ate.dev/substrate/internal/proto/ateworkerpb" + "go.ate.dev/substrate/pkg/proto/ateapipb" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" "k8s.io/apimachinery/pkg/util/validation/field" diff --git a/internal/resources/worker.go b/internal/resources/worker.go index 7ee068f8e4..2ba5dd0d4b 100644 --- a/internal/resources/worker.go +++ b/internal/resources/worker.go @@ -17,7 +17,7 @@ package resources import ( "fmt" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "go.ate.dev/substrate/pkg/proto/ateapipb" ) // AddToAllocated adjusts allocation by an assignment; sign is 1 or -1. diff --git a/internal/resources/worker_test.go b/internal/resources/worker_test.go index 68590df846..f56c309695 100644 --- a/internal/resources/worker_test.go +++ b/internal/resources/worker_test.go @@ -17,7 +17,7 @@ package resources import ( "testing" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "go.ate.dev/substrate/pkg/proto/ateapipb" "google.golang.org/protobuf/testing/protocmp" "github.com/google/go-cmp/cmp" diff --git a/internal/serverboot/logging.go b/internal/serverboot/logging.go index d50142d607..f9a7f9af57 100644 --- a/internal/serverboot/logging.go +++ b/internal/serverboot/logging.go @@ -24,7 +24,7 @@ import ( sdklog "go.opentelemetry.io/otel/sdk/log" "google.golang.org/grpc" - "github.com/agent-substrate/substrate/internal/actorevent" + "go.ate.dev/substrate/internal/actorevent" ) const logsExporterEnv = "OTEL_LOGS_EXPORTER" diff --git a/internal/serverboot/logging_test.go b/internal/serverboot/logging_test.go index 03c4286e4b..e41df9551a 100644 --- a/internal/serverboot/logging_test.go +++ b/internal/serverboot/logging_test.go @@ -27,10 +27,10 @@ import ( "go.opentelemetry.io/otel/sdk/resource" "google.golang.org/grpc" - "github.com/agent-substrate/substrate/internal/actorevent" - "github.com/agent-substrate/substrate/internal/ateattr" - "github.com/agent-substrate/substrate/internal/contextlogging" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "go.ate.dev/substrate/internal/actorevent" + "go.ate.dev/substrate/internal/ateattr" + "go.ate.dev/substrate/internal/contextlogging" + "go.ate.dev/substrate/pkg/proto/ateapipb" ) // TestResolveLogsExporter pins the logs names on top of the shared rules: otlp diff --git a/internal/serverboot/serverboot.go b/internal/serverboot/serverboot.go index cf52e4ef36..9b9779078c 100644 --- a/internal/serverboot/serverboot.go +++ b/internal/serverboot/serverboot.go @@ -32,11 +32,11 @@ import ( "sync/atomic" "time" - "github.com/agent-substrate/substrate/internal/ateattr" - "github.com/agent-substrate/substrate/internal/contextlogging" "github.com/google/uuid" promclient "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promhttp" + "go.ate.dev/substrate/internal/ateattr" + "go.ate.dev/substrate/internal/contextlogging" prombridge "go.opentelemetry.io/contrib/bridges/prometheus" "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc" "go.opentelemetry.io/otel" diff --git a/internal/serverboot/serverboot_test.go b/internal/serverboot/serverboot_test.go index 48763c8700..81c93e6702 100644 --- a/internal/serverboot/serverboot_test.go +++ b/internal/serverboot/serverboot_test.go @@ -43,7 +43,7 @@ import ( "google.golang.org/grpc/health" healthpb "google.golang.org/grpc/health/grpc_health_v1" - "github.com/agent-substrate/substrate/internal/ateattr" + "go.ate.dev/substrate/internal/ateattr" ) func resourceAttrs(res *resource.Resource) map[string]string { diff --git a/internal/tarutil/tarutil_test.go b/internal/tarutil/tarutil_test.go index a7ed7f6c0b..47572ae180 100644 --- a/internal/tarutil/tarutil_test.go +++ b/internal/tarutil/tarutil_test.go @@ -28,7 +28,7 @@ import ( "testing" "time" - "github.com/agent-substrate/substrate/internal/roottest" + "go.ate.dev/substrate/internal/roottest" "golang.org/x/sys/unix" ) diff --git a/internal/testcert/servertls.go b/internal/testcert/servertls.go index e5783c0371..a3d07e2f98 100644 --- a/internal/testcert/servertls.go +++ b/internal/testcert/servertls.go @@ -25,7 +25,7 @@ import ( "testing" "time" - "github.com/agent-substrate/substrate/internal/localca" + "go.ate.dev/substrate/internal/localca" ) // ServerTLS holds an origin key pair and the public CA trusted by its client. diff --git a/internal/testcert/servertls_test.go b/internal/testcert/servertls_test.go index fda0c12772..cde99d2a8f 100644 --- a/internal/testcert/servertls_test.go +++ b/internal/testcert/servertls_test.go @@ -21,7 +21,7 @@ import ( "testing" "time" - "github.com/agent-substrate/substrate/internal/localca" + "go.ate.dev/substrate/internal/localca" ) func TestNewServerTLS(t *testing.T) { diff --git a/internal/volume/csi/plugin.go b/internal/volume/csi/plugin.go index 326dc8617f..4b348680cb 100644 --- a/internal/volume/csi/plugin.go +++ b/internal/volume/csi/plugin.go @@ -24,11 +24,11 @@ import ( "path/filepath" "sync" - "github.com/agent-substrate/substrate/internal/credbundle" - "github.com/agent-substrate/substrate/internal/nodepath" - "github.com/agent-substrate/substrate/internal/volume" - v1alpha1 "github.com/agent-substrate/substrate/pkg/api/v1alpha1" "github.com/container-storage-interface/spec/lib/go/csi" + "go.ate.dev/substrate/internal/credbundle" + "go.ate.dev/substrate/internal/nodepath" + "go.ate.dev/substrate/internal/volume" + v1alpha1 "go.ate.dev/substrate/pkg/api/v1alpha1" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" "k8s.io/apimachinery/pkg/api/resource" diff --git a/internal/volume/csi/plugin_test.go b/internal/volume/csi/plugin_test.go index 5a96ae9067..908e971c1b 100644 --- a/internal/volume/csi/plugin_test.go +++ b/internal/volume/csi/plugin_test.go @@ -22,9 +22,9 @@ import ( "path/filepath" "testing" - "github.com/agent-substrate/substrate/internal/volume" "github.com/container-storage-interface/spec/lib/go/csi" "github.com/google/go-cmp/cmp" + "go.ate.dev/substrate/internal/volume" "google.golang.org/grpc" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" diff --git a/internal/volume/csi/tls_test.go b/internal/volume/csi/tls_test.go index 0bf4d6f2e7..5a4060b648 100644 --- a/internal/volume/csi/tls_test.go +++ b/internal/volume/csi/tls_test.go @@ -32,9 +32,9 @@ import ( "testing" "time" - "github.com/agent-substrate/substrate/pkg/api/v1alpha1" - listersv1alpha1 "github.com/agent-substrate/substrate/pkg/client/listers/api/v1alpha1" "github.com/container-storage-interface/spec/lib/go/csi" + "go.ate.dev/substrate/pkg/api/v1alpha1" + listersv1alpha1 "go.ate.dev/substrate/pkg/client/listers/api/v1alpha1" "google.golang.org/grpc" "google.golang.org/grpc/credentials" "k8s.io/apimachinery/pkg/labels" diff --git a/internal/volume/mock.go b/internal/volume/mock.go index b745e819c8..ad8e24758f 100644 --- a/internal/volume/mock.go +++ b/internal/volume/mock.go @@ -21,7 +21,7 @@ import ( "os" "path/filepath" - "github.com/agent-substrate/substrate/internal/nodepath" + "go.ate.dev/substrate/internal/nodepath" ) // Use a directory that is shared between atelet and worker but not cleaned up by atelet diff --git a/internal/wakeupprobe/wakeupprobe.go b/internal/wakeupprobe/wakeupprobe.go index 6995cc284a..86eb79dbca 100644 --- a/internal/wakeupprobe/wakeupprobe.go +++ b/internal/wakeupprobe/wakeupprobe.go @@ -31,7 +31,7 @@ import ( "strings" "time" - "github.com/agent-substrate/substrate/internal/proto/ateworkerpb" + "go.ate.dev/substrate/internal/proto/ateworkerpb" "golang.org/x/sync/errgroup" ) diff --git a/internal/wakeupprobe/wakeupprobe_test.go b/internal/wakeupprobe/wakeupprobe_test.go index fd43825824..a19f0e193e 100644 --- a/internal/wakeupprobe/wakeupprobe_test.go +++ b/internal/wakeupprobe/wakeupprobe_test.go @@ -26,7 +26,7 @@ import ( "testing" "time" - "github.com/agent-substrate/substrate/internal/proto/ateworkerpb" + "go.ate.dev/substrate/internal/proto/ateworkerpb" ) func TestURL(t *testing.T) { diff --git a/internal/workerstats/activation.go b/internal/workerstats/activation.go index f91e4a552f..68b647be30 100644 --- a/internal/workerstats/activation.go +++ b/internal/workerstats/activation.go @@ -21,7 +21,7 @@ import ( "sync" "time" - "github.com/agent-substrate/substrate/internal/proto/ateworkerpb" + "go.ate.dev/substrate/internal/proto/ateworkerpb" ) // Activation is the usage state of one activation of an actor: one Run or diff --git a/internal/workerstats/activation_test.go b/internal/workerstats/activation_test.go index 3a9366cb48..d078bf0b6e 100644 --- a/internal/workerstats/activation_test.go +++ b/internal/workerstats/activation_test.go @@ -24,7 +24,7 @@ import ( "testing" "time" - "github.com/agent-substrate/substrate/internal/proto/ateworkerpb" + "go.ate.dev/substrate/internal/proto/ateworkerpb" ) // reading is one read of counters by name; a single counter reads as "". diff --git a/internal/workerstats/actorattribution.go b/internal/workerstats/actorattribution.go index 332ec026c6..91fedb51ce 100644 --- a/internal/workerstats/actorattribution.go +++ b/internal/workerstats/actorattribution.go @@ -21,8 +21,8 @@ package workerstats import ( - "github.com/agent-substrate/substrate/internal/proto/ateworkerpb" - "github.com/agent-substrate/substrate/internal/resources" + "go.ate.dev/substrate/internal/proto/ateworkerpb" + "go.ate.dev/substrate/internal/resources" ) // attributionSource is the attribution-bearing subset of the worker requests diff --git a/internal/workerstats/actorattribution_test.go b/internal/workerstats/actorattribution_test.go index 51163f9bf4..7233aeee6a 100644 --- a/internal/workerstats/actorattribution_test.go +++ b/internal/workerstats/actorattribution_test.go @@ -19,8 +19,8 @@ import ( "github.com/google/go-cmp/cmp" - "github.com/agent-substrate/substrate/internal/proto/ateworkerpb" - "github.com/agent-substrate/substrate/internal/resources" + "go.ate.dev/substrate/internal/proto/ateworkerpb" + "go.ate.dev/substrate/internal/resources" ) // fullAttribution is what every fully-populated request below should produce. The diff --git a/internal/workerstats/usage.go b/internal/workerstats/usage.go index 7f20843aff..0c2e944cb0 100644 --- a/internal/workerstats/usage.go +++ b/internal/workerstats/usage.go @@ -24,11 +24,11 @@ import ( "go.opentelemetry.io/otel/log/noop" sdklog "go.opentelemetry.io/otel/sdk/log" - "github.com/agent-substrate/substrate/internal/actorevent" - "github.com/agent-substrate/substrate/internal/ateattr" - "github.com/agent-substrate/substrate/internal/contextlogging" - "github.com/agent-substrate/substrate/internal/proto/ateworkerpb" - "github.com/agent-substrate/substrate/internal/resources" + "go.ate.dev/substrate/internal/actorevent" + "go.ate.dev/substrate/internal/ateattr" + "go.ate.dev/substrate/internal/contextlogging" + "go.ate.dev/substrate/internal/proto/ateworkerpb" + "go.ate.dev/substrate/internal/resources" ) // Pool is the WorkerPool of the worker's pod. diff --git a/internal/workerstats/usage_test.go b/internal/workerstats/usage_test.go index 41aad1f8a3..bddcabe5b6 100644 --- a/internal/workerstats/usage_test.go +++ b/internal/workerstats/usage_test.go @@ -23,9 +23,9 @@ import ( "testing/synctest" "time" - "github.com/agent-substrate/substrate/internal/actorevent" - "github.com/agent-substrate/substrate/internal/ateattr" - "github.com/agent-substrate/substrate/internal/proto/ateworkerpb" + "go.ate.dev/substrate/internal/actorevent" + "go.ate.dev/substrate/internal/ateattr" + "go.ate.dev/substrate/internal/proto/ateworkerpb" ) var testPool = Pool{Namespace: "team-a", Name: "default"} diff --git a/internal/workerstats/workerstatstest/workerstatstest.go b/internal/workerstats/workerstatstest/workerstatstest.go index 55d4d3940d..92bb8f6499 100644 --- a/internal/workerstats/workerstatstest/workerstatstest.go +++ b/internal/workerstats/workerstatstest/workerstatstest.go @@ -20,8 +20,8 @@ import ( "log/slog" "sync" - "github.com/agent-substrate/substrate/internal/ateattr" - "github.com/agent-substrate/substrate/internal/workerstats" + "go.ate.dev/substrate/internal/ateattr" + "go.ate.dev/substrate/internal/workerstats" ) // Recorder is a stdout handler that keeps the kind and source of each record. diff --git a/manifests/ate-install/ate-api-server.yaml b/manifests/ate-install/ate-api-server.yaml index c2ddc394d6..b3fcaab851 100644 --- a/manifests/ate-install/ate-api-server.yaml +++ b/manifests/ate-install/ate-api-server.yaml @@ -89,7 +89,7 @@ spec: # starts only once the startup probe sees the plugin serving. initContainers: - name: objectstore-plugin - image: ko://github.com/agent-substrate/substrate/cmd/objectstore-plugin + image: ko://go.ate.dev/substrate/cmd/objectstore-plugin restartPolicy: Always args: - control @@ -126,7 +126,7 @@ spec: mountPath: /run/objectstore-plugin containers: - name: ate-api-server - image: ko://github.com/agent-substrate/substrate/cmd/ateapi + image: ko://go.ate.dev/substrate/cmd/ateapi args: - "--grpc-server-cred-bundle=/run/servicedns.podcert.ate.dev/credential-bundle.pem" - --config-file=/run/apiconfig.ate.dev/apiconfig.textproto diff --git a/manifests/ate-install/ate-controller.yaml b/manifests/ate-install/ate-controller.yaml index 7dfd673f6d..c1464e01a8 100644 --- a/manifests/ate-install/ate-controller.yaml +++ b/manifests/ate-install/ate-controller.yaml @@ -101,7 +101,7 @@ spec: serviceAccountName: ate-controller containers: - name: ate-controller - image: ko://github.com/agent-substrate/substrate/cmd/atecontroller + image: ko://go.ate.dev/substrate/cmd/atecontroller args: - --ateapi-ca-file=/run/servicedns-ca/trust-bundle.pem - --ateapi-client-cert=/run/podidentity.podcert.ate.dev/credential-bundle.pem diff --git a/manifests/ate-install/atelet.yaml b/manifests/ate-install/atelet.yaml index 4db08c5760..6e00986155 100644 --- a/manifests/ate-install/atelet.yaml +++ b/manifests/ate-install/atelet.yaml @@ -149,7 +149,7 @@ spec: # atelet starts only once the startup probe sees the plugin serving. initContainers: - name: objectstore-plugin - image: ko://github.com/agent-substrate/substrate/cmd/objectstore-plugin + image: ko://go.ate.dev/substrate/cmd/objectstore-plugin restartPolicy: Always args: - node @@ -207,7 +207,7 @@ spec: mountPath: /var/lib/objectstore-plugin/asset-staging containers: - name: atelet - image: ko://github.com/agent-substrate/substrate/cmd/atelet + image: ko://go.ate.dev/substrate/cmd/atelet args: # Pull credentials come from the node's own kubelet credential provider # plugin (mounted read-only below) rather than a cloud SDK compiled into diff --git a/manifests/ate-install/atenet-egress.yaml b/manifests/ate-install/atenet-egress.yaml index 2563b568bc..7b266df961 100644 --- a/manifests/ate-install/atenet-egress.yaml +++ b/manifests/ate-install/atenet-egress.yaml @@ -1276,7 +1276,7 @@ spec: # staying two CONTAINERS is what keeps the MITM signing key off the # data plane and gives the minter the separate memory limit the header # of this file depends on. - image: ko://github.com/agent-substrate/substrate/cmd/atenet + image: ko://go.ate.dev/substrate/cmd/atenet restartPolicy: Always args: - "sdsmint" @@ -1444,7 +1444,7 @@ spec: # Envoy calls it over localhost to # authenticate actor identity against the ate API on every CONNECT. - name: ext-proc - image: ko://github.com/agent-substrate/substrate/cmd/atenet + image: ko://go.ate.dev/substrate/cmd/atenet args: - router - --mode=egress diff --git a/manifests/ate-install/atenet-router.yaml b/manifests/ate-install/atenet-router.yaml index 852a26245d..638864efc2 100644 --- a/manifests/ate-install/atenet-router.yaml +++ b/manifests/ate-install/atenet-router.yaml @@ -201,7 +201,7 @@ spec: terminationGracePeriodSeconds: 60 containers: - name: atenet-router - image: ko://github.com/agent-substrate/substrate/cmd/atenet + image: ko://go.ate.dev/substrate/cmd/atenet args: - "router" # Serve the ingress direction only: the ingress ext_proc handler, plus diff --git a/manifests/ate-install/components/fake-data-plane/fake-workersync.yaml b/manifests/ate-install/components/fake-data-plane/fake-workersync.yaml index e87f9e614a..db503fd5bd 100644 --- a/manifests/ate-install/components/fake-data-plane/fake-workersync.yaml +++ b/manifests/ate-install/components/fake-data-plane/fake-workersync.yaml @@ -130,7 +130,7 @@ spec: serviceAccountName: fake-workersync containers: - name: fake-workersync - image: ko://github.com/agent-substrate/substrate/cmd/benchmarking/isolate/fake-workersync + image: ko://go.ate.dev/substrate/cmd/benchmarking/isolate/fake-workersync # Ready once it has adopted the registered fake Workers, which needs # ate-api-server serving. readinessProbe: diff --git a/manifests/ate-install/components/fake-data-plane/kustomization.yaml b/manifests/ate-install/components/fake-data-plane/kustomization.yaml index fa0691590b..dcd5f803eb 100644 --- a/manifests/ate-install/components/fake-data-plane/kustomization.yaml +++ b/manifests/ate-install/components/fake-data-plane/kustomization.yaml @@ -56,7 +56,7 @@ patches: path: /spec/template/spec/initContainers - op: replace path: /spec/template/spec/containers/0/image - value: ko://github.com/agent-substrate/substrate/cmd/benchmarking/isolate/fake-atelet + value: ko://go.ate.dev/substrate/cmd/benchmarking/isolate/fake-atelet # fake-atelet runs with its defaults. A benchmark tunes it through the # fake-atelet-tuning ConfigMap, which no install creates or resets. - op: remove diff --git a/manifests/ate-install/pod-certificate-controller.yaml b/manifests/ate-install/pod-certificate-controller.yaml index 4946fcaa2c..38e0770912 100644 --- a/manifests/ate-install/pod-certificate-controller.yaml +++ b/manifests/ate-install/pod-certificate-controller.yaml @@ -140,7 +140,7 @@ spec: spec: containers: - name: controller - image: ko://github.com/agent-substrate/substrate/cmd/podcertcontroller + image: ko://go.ate.dev/substrate/cmd/podcertcontroller args: - --in-cluster=true - --sharding-pod-namespace=$(POD_NAMESPACE) diff --git a/manifests/egress-credential-injection/k8s-credential-provider.yaml b/manifests/egress-credential-injection/k8s-credential-provider.yaml index 089983539a..90de2c24c7 100644 --- a/manifests/egress-credential-injection/k8s-credential-provider.yaml +++ b/manifests/egress-credential-injection/k8s-credential-provider.yaml @@ -71,7 +71,7 @@ spec: runAsNonRoot: true containers: - name: k8s-credential-provider - image: ko://github.com/agent-substrate/substrate/cmd/credential-provider/kubernetes-secrets + image: ko://go.ate.dev/substrate/cmd/credential-provider/kubernetes-secrets args: - "--listen-address=:50051" - "--metrics-address=:9090" diff --git a/pkg/api/v1alpha1/gen.go b/pkg/api/v1alpha1/gen.go index 87dfefe493..2dfe78a064 100644 --- a/pkg/api/v1alpha1/gen.go +++ b/pkg/api/v1alpha1/gen.go @@ -15,6 +15,6 @@ package v1alpha1 //go:generate bash ../../../hack/run-tool.sh controller-gen crd:headerFile=../../../hack/boilerplate/sh.txt object:headerFile=../../../hack/boilerplate/go.txt paths="./" output:crd:dir="../../../manifests/ate-install/generated/" -//go:generate bash ../../../hack/run-tool.sh client-gen --go-header-file=../../../hack/boilerplate/go.txt --clientset-name versioned --input-base github.com/agent-substrate/substrate --input pkg/api/v1alpha1 --output-pkg github.com/agent-substrate/substrate/pkg/client/clientset --output-dir ../../../pkg/client/clientset -//go:generate bash ../../../hack/run-tool.sh lister-gen --go-header-file=../../../hack/boilerplate/go.txt --output-pkg github.com/agent-substrate/substrate/pkg/client/listers --output-dir ../../../pkg/client/listers github.com/agent-substrate/substrate/pkg/api/v1alpha1 -//go:generate bash ../../../hack/run-tool.sh informer-gen --go-header-file=../../../hack/boilerplate/go.txt --versioned-clientset-package github.com/agent-substrate/substrate/pkg/client/clientset/versioned --listers-package github.com/agent-substrate/substrate/pkg/client/listers --output-pkg github.com/agent-substrate/substrate/pkg/client/informers --output-dir ../../../pkg/client/informers github.com/agent-substrate/substrate/pkg/api/v1alpha1 +//go:generate bash ../../../hack/run-tool.sh client-gen --go-header-file=../../../hack/boilerplate/go.txt --clientset-name versioned --input-base go.ate.dev/substrate --input pkg/api/v1alpha1 --output-pkg go.ate.dev/substrate/pkg/client/clientset --output-dir ../../../pkg/client/clientset +//go:generate bash ../../../hack/run-tool.sh lister-gen --go-header-file=../../../hack/boilerplate/go.txt --output-pkg go.ate.dev/substrate/pkg/client/listers --output-dir ../../../pkg/client/listers go.ate.dev/substrate/pkg/api/v1alpha1 +//go:generate bash ../../../hack/run-tool.sh informer-gen --go-header-file=../../../hack/boilerplate/go.txt --versioned-clientset-package go.ate.dev/substrate/pkg/client/clientset/versioned --listers-package go.ate.dev/substrate/pkg/client/listers --output-pkg go.ate.dev/substrate/pkg/client/informers --output-dir ../../../pkg/client/informers go.ate.dev/substrate/pkg/api/v1alpha1 diff --git a/pkg/api/v1alpha1/main_test.go b/pkg/api/v1alpha1/main_test.go index b4e2c6dab9..6a087530d5 100644 --- a/pkg/api/v1alpha1/main_test.go +++ b/pkg/api/v1alpha1/main_test.go @@ -19,7 +19,7 @@ import ( "os" "testing" - "github.com/agent-substrate/substrate/internal/testenv" + "go.ate.dev/substrate/internal/testenv" "k8s.io/apimachinery/pkg/runtime" utilruntime "k8s.io/apimachinery/pkg/util/runtime" clientgoscheme "k8s.io/client-go/kubernetes/scheme" diff --git a/pkg/api/v1alpha1/sandboxconfig_types.go b/pkg/api/v1alpha1/sandboxconfig_types.go index 25c8dd863c..8abbf96c8c 100644 --- a/pkg/api/v1alpha1/sandboxconfig_types.go +++ b/pkg/api/v1alpha1/sandboxconfig_types.go @@ -17,7 +17,7 @@ package v1alpha1 import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "github.com/agent-substrate/substrate/pkg/sandboxclass" + "go.ate.dev/substrate/pkg/sandboxclass" ) // SandboxClass selects the sandbox runtime family. It is shared by WorkerPool diff --git a/pkg/assetfetch/assetfetch.go b/pkg/assetfetch/assetfetch.go index 450a26917e..3480705ff7 100644 --- a/pkg/assetfetch/assetfetch.go +++ b/pkg/assetfetch/assetfetch.go @@ -42,7 +42,7 @@ import ( "regexp" "strings" - objectstorev1 "github.com/agent-substrate/substrate/pkg/proto/objectstorepb/v1" + objectstorev1 "go.ate.dev/substrate/pkg/proto/objectstorepb/v1" "golang.org/x/sync/semaphore" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" diff --git a/pkg/assetfetch/assetfetch_test.go b/pkg/assetfetch/assetfetch_test.go index 9b8ee6006b..c9b3a24a73 100644 --- a/pkg/assetfetch/assetfetch_test.go +++ b/pkg/assetfetch/assetfetch_test.go @@ -29,8 +29,8 @@ import ( "testing" "time" - "github.com/agent-substrate/substrate/internal/resources" - objectstorev1 "github.com/agent-substrate/substrate/pkg/proto/objectstorepb/v1" + "go.ate.dev/substrate/internal/resources" + objectstorev1 "go.ate.dev/substrate/pkg/proto/objectstorepb/v1" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" ) diff --git a/pkg/client/clientset/versioned/clientset.go b/pkg/client/clientset/versioned/clientset.go index fd34793756..1b037f499c 100644 --- a/pkg/client/clientset/versioned/clientset.go +++ b/pkg/client/clientset/versioned/clientset.go @@ -20,7 +20,7 @@ import ( fmt "fmt" http "net/http" - apiv1alpha1 "github.com/agent-substrate/substrate/pkg/client/clientset/versioned/typed/api/v1alpha1" + apiv1alpha1 "go.ate.dev/substrate/pkg/client/clientset/versioned/typed/api/v1alpha1" discovery "k8s.io/client-go/discovery" rest "k8s.io/client-go/rest" flowcontrol "k8s.io/client-go/util/flowcontrol" diff --git a/pkg/client/clientset/versioned/fake/clientset_generated.go b/pkg/client/clientset/versioned/fake/clientset_generated.go index f2650039d4..89549e4d95 100644 --- a/pkg/client/clientset/versioned/fake/clientset_generated.go +++ b/pkg/client/clientset/versioned/fake/clientset_generated.go @@ -17,9 +17,9 @@ package fake import ( - clientset "github.com/agent-substrate/substrate/pkg/client/clientset/versioned" - apiv1alpha1 "github.com/agent-substrate/substrate/pkg/client/clientset/versioned/typed/api/v1alpha1" - fakeapiv1alpha1 "github.com/agent-substrate/substrate/pkg/client/clientset/versioned/typed/api/v1alpha1/fake" + clientset "go.ate.dev/substrate/pkg/client/clientset/versioned" + apiv1alpha1 "go.ate.dev/substrate/pkg/client/clientset/versioned/typed/api/v1alpha1" + fakeapiv1alpha1 "go.ate.dev/substrate/pkg/client/clientset/versioned/typed/api/v1alpha1/fake" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/watch" diff --git a/pkg/client/clientset/versioned/fake/register.go b/pkg/client/clientset/versioned/fake/register.go index e72d8098c9..1677e7f9dc 100644 --- a/pkg/client/clientset/versioned/fake/register.go +++ b/pkg/client/clientset/versioned/fake/register.go @@ -17,7 +17,7 @@ package fake import ( - apiv1alpha1 "github.com/agent-substrate/substrate/pkg/api/v1alpha1" + apiv1alpha1 "go.ate.dev/substrate/pkg/api/v1alpha1" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" runtime "k8s.io/apimachinery/pkg/runtime" schema "k8s.io/apimachinery/pkg/runtime/schema" diff --git a/pkg/client/clientset/versioned/scheme/register.go b/pkg/client/clientset/versioned/scheme/register.go index 83ce5d1275..38ea0d1a46 100644 --- a/pkg/client/clientset/versioned/scheme/register.go +++ b/pkg/client/clientset/versioned/scheme/register.go @@ -17,7 +17,7 @@ package scheme import ( - apiv1alpha1 "github.com/agent-substrate/substrate/pkg/api/v1alpha1" + apiv1alpha1 "go.ate.dev/substrate/pkg/api/v1alpha1" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" runtime "k8s.io/apimachinery/pkg/runtime" schema "k8s.io/apimachinery/pkg/runtime/schema" diff --git a/pkg/client/clientset/versioned/typed/api/v1alpha1/api_client.go b/pkg/client/clientset/versioned/typed/api/v1alpha1/api_client.go index 500c137f09..16c190a986 100644 --- a/pkg/client/clientset/versioned/typed/api/v1alpha1/api_client.go +++ b/pkg/client/clientset/versioned/typed/api/v1alpha1/api_client.go @@ -19,8 +19,8 @@ package v1alpha1 import ( http "net/http" - apiv1alpha1 "github.com/agent-substrate/substrate/pkg/api/v1alpha1" - scheme "github.com/agent-substrate/substrate/pkg/client/clientset/versioned/scheme" + apiv1alpha1 "go.ate.dev/substrate/pkg/api/v1alpha1" + scheme "go.ate.dev/substrate/pkg/client/clientset/versioned/scheme" rest "k8s.io/client-go/rest" ) diff --git a/pkg/client/clientset/versioned/typed/api/v1alpha1/csidriverconfig.go b/pkg/client/clientset/versioned/typed/api/v1alpha1/csidriverconfig.go index 4abe3bb5b8..f03c92574f 100644 --- a/pkg/client/clientset/versioned/typed/api/v1alpha1/csidriverconfig.go +++ b/pkg/client/clientset/versioned/typed/api/v1alpha1/csidriverconfig.go @@ -19,8 +19,8 @@ package v1alpha1 import ( context "context" - apiv1alpha1 "github.com/agent-substrate/substrate/pkg/api/v1alpha1" - scheme "github.com/agent-substrate/substrate/pkg/client/clientset/versioned/scheme" + apiv1alpha1 "go.ate.dev/substrate/pkg/api/v1alpha1" + scheme "go.ate.dev/substrate/pkg/client/clientset/versioned/scheme" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" types "k8s.io/apimachinery/pkg/types" watch "k8s.io/apimachinery/pkg/watch" diff --git a/pkg/client/clientset/versioned/typed/api/v1alpha1/fake/fake_api_client.go b/pkg/client/clientset/versioned/typed/api/v1alpha1/fake/fake_api_client.go index b70615df1f..dd733289f5 100644 --- a/pkg/client/clientset/versioned/typed/api/v1alpha1/fake/fake_api_client.go +++ b/pkg/client/clientset/versioned/typed/api/v1alpha1/fake/fake_api_client.go @@ -17,7 +17,7 @@ package fake import ( - v1alpha1 "github.com/agent-substrate/substrate/pkg/client/clientset/versioned/typed/api/v1alpha1" + v1alpha1 "go.ate.dev/substrate/pkg/client/clientset/versioned/typed/api/v1alpha1" rest "k8s.io/client-go/rest" testing "k8s.io/client-go/testing" ) diff --git a/pkg/client/clientset/versioned/typed/api/v1alpha1/fake/fake_csidriverconfig.go b/pkg/client/clientset/versioned/typed/api/v1alpha1/fake/fake_csidriverconfig.go index e8bc825422..8a24f5f679 100644 --- a/pkg/client/clientset/versioned/typed/api/v1alpha1/fake/fake_csidriverconfig.go +++ b/pkg/client/clientset/versioned/typed/api/v1alpha1/fake/fake_csidriverconfig.go @@ -17,8 +17,8 @@ package fake import ( - v1alpha1 "github.com/agent-substrate/substrate/pkg/api/v1alpha1" - apiv1alpha1 "github.com/agent-substrate/substrate/pkg/client/clientset/versioned/typed/api/v1alpha1" + v1alpha1 "go.ate.dev/substrate/pkg/api/v1alpha1" + apiv1alpha1 "go.ate.dev/substrate/pkg/client/clientset/versioned/typed/api/v1alpha1" gentype "k8s.io/client-go/gentype" ) diff --git a/pkg/client/clientset/versioned/typed/api/v1alpha1/fake/fake_sandboxconfig.go b/pkg/client/clientset/versioned/typed/api/v1alpha1/fake/fake_sandboxconfig.go index a6a43e374b..dddc8ed559 100644 --- a/pkg/client/clientset/versioned/typed/api/v1alpha1/fake/fake_sandboxconfig.go +++ b/pkg/client/clientset/versioned/typed/api/v1alpha1/fake/fake_sandboxconfig.go @@ -17,8 +17,8 @@ package fake import ( - v1alpha1 "github.com/agent-substrate/substrate/pkg/api/v1alpha1" - apiv1alpha1 "github.com/agent-substrate/substrate/pkg/client/clientset/versioned/typed/api/v1alpha1" + v1alpha1 "go.ate.dev/substrate/pkg/api/v1alpha1" + apiv1alpha1 "go.ate.dev/substrate/pkg/client/clientset/versioned/typed/api/v1alpha1" gentype "k8s.io/client-go/gentype" ) diff --git a/pkg/client/clientset/versioned/typed/api/v1alpha1/fake/fake_workerpool.go b/pkg/client/clientset/versioned/typed/api/v1alpha1/fake/fake_workerpool.go index 187051e242..9e4da458c1 100644 --- a/pkg/client/clientset/versioned/typed/api/v1alpha1/fake/fake_workerpool.go +++ b/pkg/client/clientset/versioned/typed/api/v1alpha1/fake/fake_workerpool.go @@ -17,8 +17,8 @@ package fake import ( - v1alpha1 "github.com/agent-substrate/substrate/pkg/api/v1alpha1" - apiv1alpha1 "github.com/agent-substrate/substrate/pkg/client/clientset/versioned/typed/api/v1alpha1" + v1alpha1 "go.ate.dev/substrate/pkg/api/v1alpha1" + apiv1alpha1 "go.ate.dev/substrate/pkg/client/clientset/versioned/typed/api/v1alpha1" gentype "k8s.io/client-go/gentype" ) diff --git a/pkg/client/clientset/versioned/typed/api/v1alpha1/sandboxconfig.go b/pkg/client/clientset/versioned/typed/api/v1alpha1/sandboxconfig.go index 45c49226d0..6056bea5e8 100644 --- a/pkg/client/clientset/versioned/typed/api/v1alpha1/sandboxconfig.go +++ b/pkg/client/clientset/versioned/typed/api/v1alpha1/sandboxconfig.go @@ -19,8 +19,8 @@ package v1alpha1 import ( context "context" - apiv1alpha1 "github.com/agent-substrate/substrate/pkg/api/v1alpha1" - scheme "github.com/agent-substrate/substrate/pkg/client/clientset/versioned/scheme" + apiv1alpha1 "go.ate.dev/substrate/pkg/api/v1alpha1" + scheme "go.ate.dev/substrate/pkg/client/clientset/versioned/scheme" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" types "k8s.io/apimachinery/pkg/types" watch "k8s.io/apimachinery/pkg/watch" diff --git a/pkg/client/clientset/versioned/typed/api/v1alpha1/workerpool.go b/pkg/client/clientset/versioned/typed/api/v1alpha1/workerpool.go index c1c2256fac..539f2be3a1 100644 --- a/pkg/client/clientset/versioned/typed/api/v1alpha1/workerpool.go +++ b/pkg/client/clientset/versioned/typed/api/v1alpha1/workerpool.go @@ -19,8 +19,8 @@ package v1alpha1 import ( context "context" - apiv1alpha1 "github.com/agent-substrate/substrate/pkg/api/v1alpha1" - scheme "github.com/agent-substrate/substrate/pkg/client/clientset/versioned/scheme" + apiv1alpha1 "go.ate.dev/substrate/pkg/api/v1alpha1" + scheme "go.ate.dev/substrate/pkg/client/clientset/versioned/scheme" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" types "k8s.io/apimachinery/pkg/types" watch "k8s.io/apimachinery/pkg/watch" diff --git a/pkg/client/informers/externalversions/api/interface.go b/pkg/client/informers/externalversions/api/interface.go index d0a418b9ea..c994543342 100644 --- a/pkg/client/informers/externalversions/api/interface.go +++ b/pkg/client/informers/externalversions/api/interface.go @@ -17,8 +17,8 @@ package api import ( - v1alpha1 "github.com/agent-substrate/substrate/pkg/client/informers/externalversions/api/v1alpha1" - internalinterfaces "github.com/agent-substrate/substrate/pkg/client/informers/externalversions/internalinterfaces" + v1alpha1 "go.ate.dev/substrate/pkg/client/informers/externalversions/api/v1alpha1" + internalinterfaces "go.ate.dev/substrate/pkg/client/informers/externalversions/internalinterfaces" ) // Interface provides access to each of this group's versions. diff --git a/pkg/client/informers/externalversions/api/v1alpha1/csidriverconfig.go b/pkg/client/informers/externalversions/api/v1alpha1/csidriverconfig.go index d31546178e..ec26c674bc 100644 --- a/pkg/client/informers/externalversions/api/v1alpha1/csidriverconfig.go +++ b/pkg/client/informers/externalversions/api/v1alpha1/csidriverconfig.go @@ -20,10 +20,10 @@ import ( context "context" time "time" - pkgapiv1alpha1 "github.com/agent-substrate/substrate/pkg/api/v1alpha1" - versioned "github.com/agent-substrate/substrate/pkg/client/clientset/versioned" - internalinterfaces "github.com/agent-substrate/substrate/pkg/client/informers/externalversions/internalinterfaces" - apiv1alpha1 "github.com/agent-substrate/substrate/pkg/client/listers/api/v1alpha1" + pkgapiv1alpha1 "go.ate.dev/substrate/pkg/api/v1alpha1" + versioned "go.ate.dev/substrate/pkg/client/clientset/versioned" + internalinterfaces "go.ate.dev/substrate/pkg/client/informers/externalversions/internalinterfaces" + apiv1alpha1 "go.ate.dev/substrate/pkg/client/listers/api/v1alpha1" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" runtime "k8s.io/apimachinery/pkg/runtime" schema "k8s.io/apimachinery/pkg/runtime/schema" diff --git a/pkg/client/informers/externalversions/api/v1alpha1/interface.go b/pkg/client/informers/externalversions/api/v1alpha1/interface.go index 9bdd46f176..305eff420d 100644 --- a/pkg/client/informers/externalversions/api/v1alpha1/interface.go +++ b/pkg/client/informers/externalversions/api/v1alpha1/interface.go @@ -17,7 +17,7 @@ package v1alpha1 import ( - internalinterfaces "github.com/agent-substrate/substrate/pkg/client/informers/externalversions/internalinterfaces" + internalinterfaces "go.ate.dev/substrate/pkg/client/informers/externalversions/internalinterfaces" ) // Interface provides access to all the informers in this group version. diff --git a/pkg/client/informers/externalversions/api/v1alpha1/sandboxconfig.go b/pkg/client/informers/externalversions/api/v1alpha1/sandboxconfig.go index f41d4cd632..747ae5d1a1 100644 --- a/pkg/client/informers/externalversions/api/v1alpha1/sandboxconfig.go +++ b/pkg/client/informers/externalversions/api/v1alpha1/sandboxconfig.go @@ -20,10 +20,10 @@ import ( context "context" time "time" - pkgapiv1alpha1 "github.com/agent-substrate/substrate/pkg/api/v1alpha1" - versioned "github.com/agent-substrate/substrate/pkg/client/clientset/versioned" - internalinterfaces "github.com/agent-substrate/substrate/pkg/client/informers/externalversions/internalinterfaces" - apiv1alpha1 "github.com/agent-substrate/substrate/pkg/client/listers/api/v1alpha1" + pkgapiv1alpha1 "go.ate.dev/substrate/pkg/api/v1alpha1" + versioned "go.ate.dev/substrate/pkg/client/clientset/versioned" + internalinterfaces "go.ate.dev/substrate/pkg/client/informers/externalversions/internalinterfaces" + apiv1alpha1 "go.ate.dev/substrate/pkg/client/listers/api/v1alpha1" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" runtime "k8s.io/apimachinery/pkg/runtime" schema "k8s.io/apimachinery/pkg/runtime/schema" diff --git a/pkg/client/informers/externalversions/api/v1alpha1/workerpool.go b/pkg/client/informers/externalversions/api/v1alpha1/workerpool.go index 33357e1d18..27a9214c52 100644 --- a/pkg/client/informers/externalversions/api/v1alpha1/workerpool.go +++ b/pkg/client/informers/externalversions/api/v1alpha1/workerpool.go @@ -20,10 +20,10 @@ import ( context "context" time "time" - pkgapiv1alpha1 "github.com/agent-substrate/substrate/pkg/api/v1alpha1" - versioned "github.com/agent-substrate/substrate/pkg/client/clientset/versioned" - internalinterfaces "github.com/agent-substrate/substrate/pkg/client/informers/externalversions/internalinterfaces" - apiv1alpha1 "github.com/agent-substrate/substrate/pkg/client/listers/api/v1alpha1" + pkgapiv1alpha1 "go.ate.dev/substrate/pkg/api/v1alpha1" + versioned "go.ate.dev/substrate/pkg/client/clientset/versioned" + internalinterfaces "go.ate.dev/substrate/pkg/client/informers/externalversions/internalinterfaces" + apiv1alpha1 "go.ate.dev/substrate/pkg/client/listers/api/v1alpha1" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" runtime "k8s.io/apimachinery/pkg/runtime" schema "k8s.io/apimachinery/pkg/runtime/schema" diff --git a/pkg/client/informers/externalversions/factory.go b/pkg/client/informers/externalversions/factory.go index de9c7b410d..36d54285a2 100644 --- a/pkg/client/informers/externalversions/factory.go +++ b/pkg/client/informers/externalversions/factory.go @@ -22,9 +22,9 @@ import ( sync "sync" time "time" - versioned "github.com/agent-substrate/substrate/pkg/client/clientset/versioned" - api "github.com/agent-substrate/substrate/pkg/client/informers/externalversions/api" - internalinterfaces "github.com/agent-substrate/substrate/pkg/client/informers/externalversions/internalinterfaces" + versioned "go.ate.dev/substrate/pkg/client/clientset/versioned" + api "go.ate.dev/substrate/pkg/client/informers/externalversions/api" + internalinterfaces "go.ate.dev/substrate/pkg/client/informers/externalversions/internalinterfaces" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" runtime "k8s.io/apimachinery/pkg/runtime" schema "k8s.io/apimachinery/pkg/runtime/schema" diff --git a/pkg/client/informers/externalversions/generic.go b/pkg/client/informers/externalversions/generic.go index f46d04fd2a..0c14c958a4 100644 --- a/pkg/client/informers/externalversions/generic.go +++ b/pkg/client/informers/externalversions/generic.go @@ -19,7 +19,7 @@ package externalversions import ( fmt "fmt" - v1alpha1 "github.com/agent-substrate/substrate/pkg/api/v1alpha1" + v1alpha1 "go.ate.dev/substrate/pkg/api/v1alpha1" schema "k8s.io/apimachinery/pkg/runtime/schema" cache "k8s.io/client-go/tools/cache" ) diff --git a/pkg/client/informers/externalversions/internalinterfaces/factory_interfaces.go b/pkg/client/informers/externalversions/internalinterfaces/factory_interfaces.go index 4a02977dca..730a96413a 100644 --- a/pkg/client/informers/externalversions/internalinterfaces/factory_interfaces.go +++ b/pkg/client/informers/externalversions/internalinterfaces/factory_interfaces.go @@ -19,7 +19,7 @@ package internalinterfaces import ( time "time" - versioned "github.com/agent-substrate/substrate/pkg/client/clientset/versioned" + versioned "go.ate.dev/substrate/pkg/client/clientset/versioned" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" runtime "k8s.io/apimachinery/pkg/runtime" cache "k8s.io/client-go/tools/cache" diff --git a/pkg/client/listers/api/v1alpha1/csidriverconfig.go b/pkg/client/listers/api/v1alpha1/csidriverconfig.go index 5c92f456c8..2ce7f1f788 100644 --- a/pkg/client/listers/api/v1alpha1/csidriverconfig.go +++ b/pkg/client/listers/api/v1alpha1/csidriverconfig.go @@ -17,7 +17,7 @@ package v1alpha1 import ( - apiv1alpha1 "github.com/agent-substrate/substrate/pkg/api/v1alpha1" + apiv1alpha1 "go.ate.dev/substrate/pkg/api/v1alpha1" labels "k8s.io/apimachinery/pkg/labels" listers "k8s.io/client-go/listers" cache "k8s.io/client-go/tools/cache" diff --git a/pkg/client/listers/api/v1alpha1/sandboxconfig.go b/pkg/client/listers/api/v1alpha1/sandboxconfig.go index a198028b92..4b27c7230f 100644 --- a/pkg/client/listers/api/v1alpha1/sandboxconfig.go +++ b/pkg/client/listers/api/v1alpha1/sandboxconfig.go @@ -17,7 +17,7 @@ package v1alpha1 import ( - apiv1alpha1 "github.com/agent-substrate/substrate/pkg/api/v1alpha1" + apiv1alpha1 "go.ate.dev/substrate/pkg/api/v1alpha1" labels "k8s.io/apimachinery/pkg/labels" listers "k8s.io/client-go/listers" cache "k8s.io/client-go/tools/cache" diff --git a/pkg/client/listers/api/v1alpha1/workerpool.go b/pkg/client/listers/api/v1alpha1/workerpool.go index c3400f9131..32569334b1 100644 --- a/pkg/client/listers/api/v1alpha1/workerpool.go +++ b/pkg/client/listers/api/v1alpha1/workerpool.go @@ -17,7 +17,7 @@ package v1alpha1 import ( - apiv1alpha1 "github.com/agent-substrate/substrate/pkg/api/v1alpha1" + apiv1alpha1 "go.ate.dev/substrate/pkg/api/v1alpha1" labels "k8s.io/apimachinery/pkg/labels" listers "k8s.io/client-go/listers" cache "k8s.io/client-go/tools/cache" diff --git a/pkg/proto/ateapipb/ateapi.pb.go b/pkg/proto/ateapipb/ateapi.pb.go index 1af170a359..2ca325c45b 100644 --- a/pkg/proto/ateapipb/ateapi.pb.go +++ b/pkg/proto/ateapipb/ateapi.pb.go @@ -10020,7 +10020,7 @@ const file_ateapi_proto_rawDesc = "" + "\x13RequestActorSuspend\x12\".ateapi.RequestActorSuspendRequest\x1a#.ateapi.RequestActorSuspendResponse\x12m\n" + "\x18ReportActorSnapshotState\x12'.ateapi.ReportActorSnapshotStateRequest\x1a(.ateapi.ReportActorSnapshotStateResponse:Y\n" + "\bresource\x12\x1f.google.protobuf.MessageOptions\x18І\x03 \x01(\v2\x1a.ateapi.ResourceAnnotationR\bresource:R\n" + - "\x06method\x12\x1e.google.protobuf.MethodOptions\x18І\x03 \x01(\v2\x18.ateapi.MethodAnnotationR\x06methodB9Z7github.com/agent-substrate/substrate/pkg/proto/ateapipbb\x06proto3" + "\x06method\x12\x1e.google.protobuf.MethodOptions\x18І\x03 \x01(\v2\x18.ateapi.MethodAnnotationR\x06methodB)Z'go.ate.dev/substrate/pkg/proto/ateapipbb\x06proto3" var ( file_ateapi_proto_rawDescOnce sync.Once diff --git a/pkg/proto/ateapipb/ateapi.proto b/pkg/proto/ateapipb/ateapi.proto index 08196878a0..fe2a4cf73f 100644 --- a/pkg/proto/ateapipb/ateapi.proto +++ b/pkg/proto/ateapipb/ateapi.proto @@ -19,7 +19,7 @@ package ateapi; import "google/protobuf/descriptor.proto"; import "google/protobuf/timestamp.proto"; -option go_package = "github.com/agent-substrate/substrate/pkg/proto/ateapipb"; +option go_package = "go.ate.dev/substrate/pkg/proto/ateapipb"; // Control is the primary RPC interface for Agentic Substrate. service Control { diff --git a/pkg/proto/ateconfigpb/apiconfig.pb.go b/pkg/proto/ateconfigpb/apiconfig.pb.go index 3b56ebd0e0..0eb9400877 100644 --- a/pkg/proto/ateconfigpb/apiconfig.pb.go +++ b/pkg/proto/ateconfigpb/apiconfig.pb.go @@ -134,7 +134,7 @@ const file_apiconfig_proto_rawDesc = "" + "\alogging\x18\x01 \x01(\v2\x17.ateconfigpb.APILoggingR\alogging\"\"\n" + "\n" + "APILogging\x12\x14\n" + - "\x05level\x18\x01 \x01(\tR\x05levelB ../.. +replace go.ate.dev/substrate => ../.. require ( - github.com/agent-substrate/substrate v0.0.0-00010101000000-000000000000 + go.ate.dev/substrate v0.0.0-00010101000000-000000000000 github.com/bufbuild/protocompile v0.14.1 github.com/google/go-cmp v0.7.0 github.com/spf13/cobra v1.10.2 diff --git a/tools/apitool/internal/cmd/validate.go b/tools/apitool/internal/cmd/validate.go index 31e81cada2..443a97b393 100644 --- a/tools/apitool/internal/cmd/validate.go +++ b/tools/apitool/internal/cmd/validate.go @@ -20,9 +20,9 @@ import ( "github.com/spf13/cobra" - "github.com/agent-substrate/substrate/tools/apitool/internal/exemption" - "github.com/agent-substrate/substrate/tools/apitool/internal/lint" - "github.com/agent-substrate/substrate/tools/apitool/internal/validate" + "go.ate.dev/substrate/tools/apitool/internal/exemption" + "go.ate.dev/substrate/tools/apitool/internal/lint" + "go.ate.dev/substrate/tools/apitool/internal/validate" ) var validateCmd = &cobra.Command{ diff --git a/tools/apitool/internal/exemption/exemption_test.go b/tools/apitool/internal/exemption/exemption_test.go index 1ef0f65be1..bb59032ed3 100644 --- a/tools/apitool/internal/exemption/exemption_test.go +++ b/tools/apitool/internal/exemption/exemption_test.go @@ -19,7 +19,7 @@ import ( "reflect" "testing" - "github.com/agent-substrate/substrate/tools/apitool/internal/exemption" + "go.ate.dev/substrate/tools/apitool/internal/exemption" ) func TestLoadMissingFileIsEmpty(t *testing.T) { diff --git a/tools/apitool/internal/lint/documented.go b/tools/apitool/internal/lint/documented.go index 3d25d30199..65fae26715 100644 --- a/tools/apitool/internal/lint/documented.go +++ b/tools/apitool/internal/lint/documented.go @@ -17,7 +17,7 @@ package lint import ( "strings" - "github.com/agent-substrate/substrate/tools/apitool/internal/model" + "go.ate.dev/substrate/tools/apitool/internal/model" ) var Documented = Rule{ diff --git a/tools/apitool/internal/lint/documented_test.go b/tools/apitool/internal/lint/documented_test.go index 5896c171c0..75151c20ad 100644 --- a/tools/apitool/internal/lint/documented_test.go +++ b/tools/apitool/internal/lint/documented_test.go @@ -19,8 +19,8 @@ import ( "github.com/google/go-cmp/cmp" - "github.com/agent-substrate/substrate/tools/apitool/internal/lint" - "github.com/agent-substrate/substrate/tools/apitool/internal/model" + "go.ate.dev/substrate/tools/apitool/internal/lint" + "go.ate.dev/substrate/tools/apitool/internal/model" ) func TestDocumented(t *testing.T) { diff --git a/tools/apitool/internal/lint/fields.go b/tools/apitool/internal/lint/fields.go index 13b73b6040..0d6c5f75b6 100644 --- a/tools/apitool/internal/lint/fields.go +++ b/tools/apitool/internal/lint/fields.go @@ -21,7 +21,7 @@ import ( "google.golang.org/protobuf/reflect/protoreflect" - "github.com/agent-substrate/substrate/tools/apitool/internal/model" + "go.ate.dev/substrate/tools/apitool/internal/model" ) // typeMatcher reports whether a field has the wanted type, including diff --git a/tools/apitool/internal/lint/fields_test.go b/tools/apitool/internal/lint/fields_test.go index dc8fda8f54..fbc467750b 100644 --- a/tools/apitool/internal/lint/fields_test.go +++ b/tools/apitool/internal/lint/fields_test.go @@ -20,7 +20,7 @@ import ( "github.com/google/go-cmp/cmp" "google.golang.org/protobuf/reflect/protoreflect" - "github.com/agent-substrate/substrate/tools/apitool/internal/model" + "go.ate.dev/substrate/tools/apitool/internal/model" ) func TestCheckFields(t *testing.T) { diff --git a/tools/apitool/internal/lint/fixtures_test.go b/tools/apitool/internal/lint/fixtures_test.go index 8675de2e5f..08bd26a69c 100644 --- a/tools/apitool/internal/lint/fixtures_test.go +++ b/tools/apitool/internal/lint/fixtures_test.go @@ -19,8 +19,8 @@ package lint_test import ( "google.golang.org/protobuf/reflect/protoreflect" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" - "github.com/agent-substrate/substrate/tools/apitool/internal/model" + "go.ate.dev/substrate/pkg/proto/ateapipb" + "go.ate.dev/substrate/tools/apitool/internal/model" ) // methodAPI builds an API whose test.Control service has the one method diff --git a/tools/apitool/internal/lint/lint.go b/tools/apitool/internal/lint/lint.go index 9cf17d302f..133e41e1d7 100644 --- a/tools/apitool/internal/lint/lint.go +++ b/tools/apitool/internal/lint/lint.go @@ -17,7 +17,7 @@ package lint import ( - "github.com/agent-substrate/substrate/tools/apitool/internal/model" + "go.ate.dev/substrate/tools/apitool/internal/model" ) // Finding is one rule violation: which API entity it's about, and why. diff --git a/tools/apitool/internal/lint/method.go b/tools/apitool/internal/lint/method.go index 8ad7a2522a..5e9e96ecce 100644 --- a/tools/apitool/internal/lint/method.go +++ b/tools/apitool/internal/lint/method.go @@ -21,7 +21,7 @@ import ( "google.golang.org/protobuf/reflect/protoreflect" - "github.com/agent-substrate/substrate/tools/apitool/internal/model" + "go.ate.dev/substrate/tools/apitool/internal/model" ) // method is a standard method with everything a rule needs resolved: its diff --git a/tools/apitool/internal/lint/method_test.go b/tools/apitool/internal/lint/method_test.go index 5daab4ff6e..d3e57db3c3 100644 --- a/tools/apitool/internal/lint/method_test.go +++ b/tools/apitool/internal/lint/method_test.go @@ -20,7 +20,7 @@ import ( "github.com/google/go-cmp/cmp" - "github.com/agent-substrate/substrate/tools/apitool/internal/model" + "go.ate.dev/substrate/tools/apitool/internal/model" ) func TestAllOf(t *testing.T) { diff --git a/tools/apitool/internal/lint/naming.go b/tools/apitool/internal/lint/naming.go index c144f5f64f..d2e09f159f 100644 --- a/tools/apitool/internal/lint/naming.go +++ b/tools/apitool/internal/lint/naming.go @@ -22,7 +22,7 @@ import ( "google.golang.org/protobuf/reflect/protoreflect" - "github.com/agent-substrate/substrate/tools/apitool/internal/model" + "go.ate.dev/substrate/tools/apitool/internal/model" ) var RequestNameMatchesMethod = Rule{ diff --git a/tools/apitool/internal/lint/naming_test.go b/tools/apitool/internal/lint/naming_test.go index aa96ab65ec..b8f1040c38 100644 --- a/tools/apitool/internal/lint/naming_test.go +++ b/tools/apitool/internal/lint/naming_test.go @@ -19,8 +19,8 @@ import ( "github.com/google/go-cmp/cmp" - "github.com/agent-substrate/substrate/tools/apitool/internal/lint" - "github.com/agent-substrate/substrate/tools/apitool/internal/model" + "go.ate.dev/substrate/tools/apitool/internal/lint" + "go.ate.dev/substrate/tools/apitool/internal/model" ) func TestRequestNameMatchesMethod(t *testing.T) { diff --git a/tools/apitool/internal/lint/no_oneofs.go b/tools/apitool/internal/lint/no_oneofs.go index d9cae9c0b2..0bca0bf839 100644 --- a/tools/apitool/internal/lint/no_oneofs.go +++ b/tools/apitool/internal/lint/no_oneofs.go @@ -15,7 +15,7 @@ package lint import ( - "github.com/agent-substrate/substrate/tools/apitool/internal/model" + "go.ate.dev/substrate/tools/apitool/internal/model" ) var NoOneofs = Rule{ diff --git a/tools/apitool/internal/lint/no_oneofs_test.go b/tools/apitool/internal/lint/no_oneofs_test.go index d2bc11f030..a6cfd17abd 100644 --- a/tools/apitool/internal/lint/no_oneofs_test.go +++ b/tools/apitool/internal/lint/no_oneofs_test.go @@ -19,8 +19,8 @@ import ( "github.com/google/go-cmp/cmp" - "github.com/agent-substrate/substrate/tools/apitool/internal/lint" - "github.com/agent-substrate/substrate/tools/apitool/internal/model" + "go.ate.dev/substrate/tools/apitool/internal/lint" + "go.ate.dev/substrate/tools/apitool/internal/model" ) func TestNoOneofs(t *testing.T) { diff --git a/tools/apitool/internal/lint/resources.go b/tools/apitool/internal/lint/resources.go index 017801f7c9..6d16e4de4b 100644 --- a/tools/apitool/internal/lint/resources.go +++ b/tools/apitool/internal/lint/resources.go @@ -17,7 +17,7 @@ package lint import ( "google.golang.org/protobuf/reflect/protoreflect" - "github.com/agent-substrate/substrate/tools/apitool/internal/model" + "go.ate.dev/substrate/tools/apitool/internal/model" ) const resourceMetadataTypeFullName protoreflect.FullName = "ateapi.ResourceMetadata" diff --git a/tools/apitool/internal/lint/resources_test.go b/tools/apitool/internal/lint/resources_test.go index 1dce851caf..89a4f43ad6 100644 --- a/tools/apitool/internal/lint/resources_test.go +++ b/tools/apitool/internal/lint/resources_test.go @@ -20,8 +20,8 @@ import ( "github.com/google/go-cmp/cmp" "google.golang.org/protobuf/reflect/protoreflect" - "github.com/agent-substrate/substrate/tools/apitool/internal/lint" - "github.com/agent-substrate/substrate/tools/apitool/internal/model" + "go.ate.dev/substrate/tools/apitool/internal/lint" + "go.ate.dev/substrate/tools/apitool/internal/model" ) func TestResourceMetadata(t *testing.T) { diff --git a/tools/apitool/internal/lint/standard_methods.go b/tools/apitool/internal/lint/standard_methods.go index b2152d05a1..fd3605765a 100644 --- a/tools/apitool/internal/lint/standard_methods.go +++ b/tools/apitool/internal/lint/standard_methods.go @@ -17,8 +17,8 @@ package lint import ( "google.golang.org/protobuf/reflect/protoreflect" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" - "github.com/agent-substrate/substrate/tools/apitool/internal/model" + "go.ate.dev/substrate/pkg/proto/ateapipb" + "go.ate.dev/substrate/tools/apitool/internal/model" ) var StandardMethodReturnsResource = Rule{ diff --git a/tools/apitool/internal/lint/standard_methods_test.go b/tools/apitool/internal/lint/standard_methods_test.go index d61ef968d2..67e6f3d5e6 100644 --- a/tools/apitool/internal/lint/standard_methods_test.go +++ b/tools/apitool/internal/lint/standard_methods_test.go @@ -20,8 +20,8 @@ import ( "github.com/google/go-cmp/cmp" "google.golang.org/protobuf/reflect/protoreflect" - "github.com/agent-substrate/substrate/tools/apitool/internal/lint" - "github.com/agent-substrate/substrate/tools/apitool/internal/model" + "go.ate.dev/substrate/tools/apitool/internal/lint" + "go.ate.dev/substrate/tools/apitool/internal/model" ) func TestStandardMethodReturnsResource(t *testing.T) { diff --git a/tools/apitool/internal/model/model.go b/tools/apitool/internal/model/model.go index 38760d86d6..a40f551890 100644 --- a/tools/apitool/internal/model/model.go +++ b/tools/apitool/internal/model/model.go @@ -26,7 +26,7 @@ import ( "google.golang.org/protobuf/reflect/protoreflect" "google.golang.org/protobuf/reflect/protoregistry" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" + "go.ate.dev/substrate/pkg/proto/ateapipb" ) // sourceFileName is an arbitrary internal label: Build only ever compiles diff --git a/tools/apitool/internal/model/model_test.go b/tools/apitool/internal/model/model_test.go index 7071fd4628..8b3e9ab74a 100644 --- a/tools/apitool/internal/model/model_test.go +++ b/tools/apitool/internal/model/model_test.go @@ -21,8 +21,8 @@ import ( "github.com/google/go-cmp/cmp" "google.golang.org/protobuf/reflect/protoreflect" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" - "github.com/agent-substrate/substrate/tools/apitool/internal/model" + "go.ate.dev/substrate/pkg/proto/ateapipb" + "go.ate.dev/substrate/tools/apitool/internal/model" ) func buildAPI(t *testing.T, protoBody string) *model.API { diff --git a/tools/apitool/internal/validate/validate.go b/tools/apitool/internal/validate/validate.go index ff9daf291f..ae7b5f5555 100644 --- a/tools/apitool/internal/validate/validate.go +++ b/tools/apitool/internal/validate/validate.go @@ -25,10 +25,10 @@ import ( "path/filepath" "strings" - "github.com/agent-substrate/substrate/pkg/proto/ateapipb" - "github.com/agent-substrate/substrate/tools/apitool/internal/exemption" - "github.com/agent-substrate/substrate/tools/apitool/internal/lint" - "github.com/agent-substrate/substrate/tools/apitool/internal/model" + "go.ate.dev/substrate/pkg/proto/ateapipb" + "go.ate.dev/substrate/tools/apitool/internal/exemption" + "go.ate.dev/substrate/tools/apitool/internal/lint" + "go.ate.dev/substrate/tools/apitool/internal/model" ) // serviceName is the RPC service apitool validates: ateapi.proto's public, @@ -90,7 +90,7 @@ func DefaultExemptionsPath() (string, error) { return root + "/tools/apitool/exemptions.json", nil } -const rootModule = "module github.com/agent-substrate/substrate" +const rootModule = "module go.ate.dev/substrate" // repoRoot walks up from the working directory to find the repo root, i.e. // the directory containing the go.mod declaring rootModule. diff --git a/tools/apitool/internal/validate/validate_test.go b/tools/apitool/internal/validate/validate_test.go index 08911415b0..af4ac9c0a9 100644 --- a/tools/apitool/internal/validate/validate_test.go +++ b/tools/apitool/internal/validate/validate_test.go @@ -17,8 +17,8 @@ package validate_test import ( "testing" - "github.com/agent-substrate/substrate/tools/apitool/internal/exemption" - "github.com/agent-substrate/substrate/tools/apitool/internal/validate" + "go.ate.dev/substrate/tools/apitool/internal/exemption" + "go.ate.dev/substrate/tools/apitool/internal/validate" ) func TestExemptions(t *testing.T) { diff --git a/tools/apitool/main.go b/tools/apitool/main.go index 89ae6e0b6c..45cbedc7af 100644 --- a/tools/apitool/main.go +++ b/tools/apitool/main.go @@ -18,7 +18,7 @@ import ( "fmt" "os" - "github.com/agent-substrate/substrate/tools/apitool/internal/cmd" + "go.ate.dev/substrate/tools/apitool/internal/cmd" ) func main() { diff --git a/tools/junittool/go.mod b/tools/junittool/go.mod index 0ec975ac9a..57e3b7110a 100644 --- a/tools/junittool/go.mod +++ b/tools/junittool/go.mod @@ -1,3 +1,3 @@ -module github.com/agent-substrate/substrate/tools/junittool +module go.ate.dev/substrate/tools/junittool go 1.27.0 diff --git a/tools/setup-gcp/main.go b/tools/setup-gcp/main.go index 485e43c6c3..4aa5b679d0 100644 --- a/tools/setup-gcp/main.go +++ b/tools/setup-gcp/main.go @@ -18,7 +18,7 @@ import ( "fmt" "os" - "github.com/agent-substrate/substrate/tools/setup-gcp/cmd" + "go.ate.dev/substrate/tools/setup-gcp/cmd" ) func main() { diff --git a/tools/validate-image-cache/main.go b/tools/validate-image-cache/main.go index a5ae7f9de0..5ca3b8fe68 100644 --- a/tools/validate-image-cache/main.go +++ b/tools/validate-image-cache/main.go @@ -59,10 +59,10 @@ import ( "sync" "time" - "github.com/agent-substrate/substrate/internal/imagecache" - "github.com/agent-substrate/substrate/internal/nodepath" v1 "github.com/google/go-containerregistry/pkg/v1" googlecontainerauth "github.com/google/go-containerregistry/pkg/v1/google" + "go.ate.dev/substrate/internal/imagecache" + "go.ate.dev/substrate/internal/nodepath" "golang.org/x/sys/unix" ) From 603e006236f576d9435f46757e3fe2e556768ddf Mon Sep 17 00:00:00 2001 From: Benjamin Elder Date: Fri, 16 Oct 2026 04:46:17 +0000 Subject: [PATCH 29/31] skip device nodes when unpacking image layers (#2739) some base images include `/dev/*` device nodes in their layer tars. atelet runs without `CAP_MKNOD` and the runtime mounts `/dev` anyway, so skip char/block device entries when unpacking layers (like rootless containerd does) instead of failing the pull. while here: - if an entry replaces a directory that had subdirectories in the same layer, drop the removed paths from `dirModes` so the post-unpack chmod pass doesn't fail on them - include the tar entry path in `unhandled tar entry typeflag` errors and the image ref in `EnsureImage` errors - [x] Tests pass - [x] Appropriate changes to documentation are included in the PR --- cmd/atelet/oci.go | 7 ++-- internal/imagecache/README.md | 10 +++-- internal/imagecache/unpack.go | 67 ++++++++++++++++++++---------- internal/imagecache/unpack_test.go | 63 ++++++++++++++++++++-------- 4 files changed, 99 insertions(+), 48 deletions(-) diff --git a/cmd/atelet/oci.go b/cmd/atelet/oci.go index b9fcf4e903..32e4f39125 100644 --- a/cmd/atelet/oci.go +++ b/cmd/atelet/oci.go @@ -109,7 +109,7 @@ func prepareOCIDirectory(ctx context.Context, imageCache *imagecache.Store, acto g.Go(func() error { var err error if img, err = imageCache.EnsureImage(gctx, ref); err != nil { - return fmt.Errorf("in imageCache.EnsureImage: %w", err) + return fmt.Errorf("in imageCache.EnsureImage(%q): %w", ref, err) } return nil }) @@ -163,9 +163,10 @@ func resolveImageVolumes(ctx context.Context, imageCache *imagecache.Store, volu g, gctx := errgroup.WithContext(ctx) for i, vol := range wanted { g.Go(func() error { - img, err := imageCache.EnsureImage(gctx, vol.GetImage().GetReference()) + volRef := vol.GetImage().GetReference() + img, err := imageCache.EnsureImage(gctx, volRef) if err != nil { - return fmt.Errorf("in imageCache.EnsureImage for volume %q: %w", vol.GetName(), err) + return fmt.Errorf("in imageCache.EnsureImage(%q) for volume %q: %w", volRef, vol.GetName(), err) } out[i] = imagecache.ImageVolumeOverlay{ Name: vol.GetName(), diff --git a/internal/imagecache/README.md b/internal/imagecache/README.md index 4d9e5cc8db..e366534174 100644 --- a/internal/imagecache/README.md +++ b/internal/imagecache/README.md @@ -99,10 +99,12 @@ layer diffIDs in order — layers shared by N images exist once. confinement (path traversal and symlink/hardlink escapes are refused), "later entry wins" within a layer, read-only-dir handling that works without `CAP_DAC_OVERRIDE`, and creation of parent directories that the - layer tar omits (they may exist only in lower layers). Whiteout entries - (`.wh.*`) are **not** written into the tree — overlayfs whiteouts are - char devices atelet cannot create — they are recorded in - `whiteouts.json` for the consumer to materialize. + layer tar omits (they may exist only in lower layers). Character and + block device nodes are skipped (atelet runs without `CAP_MKNOD` and the + runtime mounts `/dev`). Whiteout entries (`.wh.*`) are **not** written + into the tree — overlayfs whiteouts are char devices atelet cannot + create — they are recorded in `whiteouts.json` for the consumer to + materialize. 5. **Record**: the image config + diffID list is written under the requested digest (and the per-platform child digest for multi-arch refs). diff --git a/internal/imagecache/unpack.go b/internal/imagecache/unpack.go index 9b95ccd462..7d9b94ecd1 100644 --- a/internal/imagecache/unpack.go +++ b/internal/imagecache/unpack.go @@ -127,6 +127,27 @@ func unpackLayer(ctx context.Context, tarData io.Reader, root *os.Root) (*whiteo } } + // replaceExisting removes whatever is at name ("later entry wins" within a + // layer) and drops any deferred dir mode/mtime restore for name and its + // subtree so the restore pass does not chmod a removed directory. + replaceExisting := func(name, before string) error { + if _, err := root.Lstat(name); err == nil { + if err := root.RemoveAll(name); err != nil { + return fmt.Errorf("while replacing existing path at %q before %s: %w", name, before, err) + } + prefix := name + "/" + for d := range dirModes { + if d == name || strings.HasPrefix(d, prefix) { + delete(dirModes, d) + delete(dirModTimes, d) + } + } + } else if !errors.Is(err, os.ErrNotExist) { + return fmt.Errorf("while checking existing path at %q before %s: %w", name, before, err) + } + return nil + } + tarReader := tar.NewReader(tarData) for { hdr, err := tarReader.Next() @@ -160,6 +181,16 @@ func unpackLayer(ctx context.Context, tarData io.Reader, root *os.Root) (*whiteo continue } + // Skip device nodes like rootless containerd does: an image must never + // grant device access, and the runtime provides /dev. + if hdr.Typeflag == tar.TypeChar || hdr.Typeflag == tar.TypeBlock { + if err := replaceExisting(name, "device node"); err != nil { + return nil, err + } + slog.DebugContext(ctx, "Skipping device node in image layer", slog.String("name", name)) + continue + } + mode := hdr.FileInfo().Mode().Perm() // A layer tar routinely omits entries for parent directories that @@ -181,12 +212,8 @@ func unpackLayer(ctx context.Context, tarData io.Reader, root *os.Root) (*whiteo // 1. If it's a symlink, we don't write through it (security vulnerability / incorrectness). // 2. If it's a hardlink, we unlink it instead of truncating the shared inode. // 3. If it's a directory, we recursively remove it so we can write the file. - if _, err := root.Lstat(name); err == nil { - if err := root.RemoveAll(name); err != nil { - return nil, fmt.Errorf("while replacing existing path at %q before regular file: %w", name, err) - } - } else if !errors.Is(err, os.ErrNotExist) { - return nil, fmt.Errorf("while checking existing path at %q before regular file: %w", name, err) + if err := replaceExisting(name, "regular file"); err != nil { + return nil, err } // Stream directly from tarReader to target file to avoid buffering in memory. @@ -236,15 +263,13 @@ func unpackLayer(ctx context.Context, tarData io.Reader, root *os.Root) (*whiteo continue } } - // Root.RemoveAll removes the symlink entry itself; it does NOT - // traverse and remove the directory the symlink points to. - // That's the desired semantic here — replace this path's - // entry without touching whatever the prior symlink targeted. - if err := root.RemoveAll(name); err != nil { - return nil, fmt.Errorf("while replacing existing path at %q before symlink: %w", name, err) - } - } else if !errors.Is(err, os.ErrNotExist) { - return nil, fmt.Errorf("while checking existing path at %q before symlink: %w", name, err) + } + // Root.RemoveAll removes the symlink entry itself; it does NOT + // traverse and remove the directory the symlink points to. + // That's the desired semantic here — replace this path's + // entry without touching whatever the prior symlink targeted. + if err := replaceExisting(name, "symlink"); err != nil { + return nil, err } if err := root.Symlink(hdr.Linkname, name); err != nil { return nil, fmt.Errorf("while creating symlink src=%q target=%q: %w", name, hdr.Linkname, err) @@ -261,12 +286,8 @@ func unpackLayer(ctx context.Context, tarData io.Reader, root *os.Root) (*whiteo return nil, fmt.Errorf("invalid hardlink target for %q: empty", name) } // Same "later entry wins" handling as TypeSymlink: replace existing entry. - if _, err := root.Lstat(name); err == nil { - if err := root.RemoveAll(name); err != nil { - return nil, fmt.Errorf("while replacing existing path at %q before hardlink: %w", name, err) - } - } else if !errors.Is(err, os.ErrNotExist) { - return nil, fmt.Errorf("while checking existing path at %q before hardlink: %w", name, err) + if err := replaceExisting(name, "hardlink"); err != nil { + return nil, err } if err := root.Link(linkname, name); err != nil { return nil, fmt.Errorf("while creating hardlink src=%q target=%q: %w", name, linkname, err) @@ -276,8 +297,8 @@ func unpackLayer(ctx context.Context, tarData io.Reader, root *os.Root) (*whiteo default: tfStr := string([]byte{hdr.Typeflag}) - slog.ErrorContext(ctx, "Unhandled tar entry typeflag", slog.String("typeflag", tfStr), slog.Any("hdr", hdr)) - return nil, fmt.Errorf("unhandled tar entry typeflag %q", tfStr) + slog.ErrorContext(ctx, "Unhandled tar entry typeflag", slog.String("typeflag", tfStr), slog.String("name", name), slog.Any("hdr", hdr)) + return nil, fmt.Errorf("unhandled tar entry typeflag %q at %q", tfStr, name) } } diff --git a/internal/imagecache/unpack_test.go b/internal/imagecache/unpack_test.go index d6f8c375a9..746d4d328b 100644 --- a/internal/imagecache/unpack_test.go +++ b/internal/imagecache/unpack_test.go @@ -18,6 +18,7 @@ import ( "archive/tar" "bytes" "context" + "errors" "os" "path/filepath" "slices" @@ -33,6 +34,8 @@ type tarEntry struct { body string linkname string modTime time.Time + devmajor int64 + devminor int64 } func defaultMode(typeflag byte) int64 { @@ -62,6 +65,8 @@ func buildTar(t *testing.T, entries []tarEntry) []byte { Size: int64(len(e.body)), Linkname: e.linkname, ModTime: e.modTime, + Devmajor: e.devmajor, + Devminor: e.devminor, } if err := tw.WriteHeader(hdr); err != nil { t.Fatalf("tar.WriteHeader(%+v): %v", hdr, err) @@ -627,25 +632,47 @@ func TestUnpackLayer_HardlinkEscape(t *testing.T) { } } -func TestUnpackLayer_RejectSpecialFiles(t *testing.T) { - tests := []struct { - name string - typeflag byte - }{ - {name: "char device", typeflag: tar.TypeChar}, - {name: "block device", typeflag: tar.TypeBlock}, - {name: "fifo", typeflag: tar.TypeFifo}, +func TestUnpackLayer_SkipsDeviceNodes(t *testing.T) { + dir, wh, err := runUnpack(t, []tarEntry{ + {name: "dev/", typeflag: tar.TypeDir}, + {name: "dev/console", typeflag: tar.TypeChar, devmajor: 5, devminor: 1}, + {name: "dev/sda", typeflag: tar.TypeBlock, devmajor: 8}, + {name: "dev/zero", typeflag: tar.TypeChar}, + // Undeclared parent of a skipped device node must not be created or + // recorded in ImplicitDirs. + {name: "undeclared/tty0", typeflag: tar.TypeChar, devmajor: 4}, + // A device node at an existing path removes the prior entry (including + // a declared directory subtree) without breaking the mode-restore pass. + {name: "dev/replaced", typeflag: tar.TypeReg, body: "stale"}, + {name: "dev/replaced", typeflag: tar.TypeChar, devmajor: 1, devminor: 3}, + {name: "dev/subdir/", typeflag: tar.TypeDir}, + {name: "dev/subdir/nested/", typeflag: tar.TypeDir}, + {name: "dev/subdir", typeflag: tar.TypeChar, devmajor: 1, devminor: 5}, + {name: "etc/hostname", typeflag: tar.TypeReg, body: "x"}, + }) + if err != nil { + t.Fatalf("unpackLayer: %v", err) } - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - _, _, err := runUnpack(t, []tarEntry{{name: "weird", typeflag: tc.typeflag}}) - if err == nil { - t.Fatalf("unpackLayer succeeded, want unhandled-typeflag error") - } - if !strings.Contains(err.Error(), "unhandled tar entry typeflag") { - t.Errorf("error = %q, want it to mention 'unhandled tar entry typeflag'", err.Error()) - } - }) + for _, name := range []string{"dev/console", "dev/sda", "dev/zero", "undeclared", "undeclared/tty0", "dev/replaced", "dev/subdir"} { + if _, err := os.Lstat(filepath.Join(dir, name)); !errors.Is(err, os.ErrNotExist) { + t.Errorf("Lstat(%q) = %v, want not exist", name, err) + } + } + if want := []string{"etc"}; !slices.Equal(wh.ImplicitDirs, want) { + t.Errorf("ImplicitDirs = %v, want %v", wh.ImplicitDirs, want) + } + if _, err := os.Stat(filepath.Join(dir, "etc/hostname")); err != nil { + t.Errorf("entry after device nodes not extracted: %v", err) + } +} + +func TestUnpackLayer_RejectFifo(t *testing.T) { + _, _, err := runUnpack(t, []tarEntry{{name: "weird", typeflag: tar.TypeFifo}}) + if err == nil { + t.Fatalf("unpackLayer succeeded, want unhandled-typeflag error") + } + if want := `unhandled tar entry typeflag "6" at "weird"`; !strings.Contains(err.Error(), want) { + t.Errorf("error = %q, want it to contain %q", err.Error(), want) } } From 36197017a520152df85edd95bd0d6ab442ad34ce Mon Sep 17 00:00:00 2001 From: shrutiyam-glitch Date: Fri, 16 Oct 2026 05:34:41 +0000 Subject: [PATCH 30/31] atelet, ateworker: probe CPU features and watch sandbox-runtimes.json for dynamic worker registration (#2660) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Part of #587. Follows #2521. #2521 added the initial `atelet` syncer that wrote `sandbox_runtime_hash` per enabled `SandboxConfig` version to each local worker pod's directory. This PR completes the runtime and hardware compatibility pipeline from `atelet` through the worker into `WorkerService.RegisterWorker`: 1. **Renames `sandbox-versions.json` to `sandbox-runtimes.json` (`internal/sandboxruntimes`)**: `atelet` now writes the complete set of `VersionedSandboxCompat` attributes for `defaultRuntime` and `restorableRuntimes` (`architecture`, `sandbox_runtime_hash`, `cpu_features` for `gvisor`, and `cpu_vendor` + `cpu_model` for `microvm`). 2. **Probes CPU identity and gVisor `cpu_features` in `atelet`**: `atelet` owns all hardware and runtime probing on the node — reading `/proc/cpuinfo` for microVM CPU vendor/model and running ` cpu-features` (memoized by asset SHA-256) for each enabled gVisor version, encoded as a canonical lowercase hexadecimal bitmask whose bit numbers are gVisor's own `Feature` constants (`internal/hardware`). 3. **Watches `sandbox-runtimes.json` in the worker**: `ateworker-gvisor` and `ateworker-microvm` watch `/var/lib/ate/workers//sandbox-runtimes.json` via `register.WatchFile` and keep `default_runtime` and `restorable_runtimes` registered through `WorkerSupport.RegisterWorker` (`internal/ateworker/register`). ## End-to-End Flow ```mermaid flowchart LR subgraph Node["Kubernetes Node"] direction TB SC["SandboxConfig CRD"] --> Atelet["atelet (workerSandboxSyncer)"] CPU["Host /proc/cpuinfo\n(microVM vendor & model)"] --> Atelet Runsc["Cached runsc binary\n( cpu-features)"] --> Atelet Atelet -->|"Atomic rename"| File["/var/lib/ate/workers//\nsandbox-runtimes.json"] File -->|"register.WatchFile (fsnotify)"| Worker["ateworker-gvisor / ateworker-microvm"] Worker -->|"WorkerSupport.RegisterWorker"| Atelet end Atelet -->|"WorkerService.RegisterWorker"| AteAPI["ateapi (Control Plane)"] ``` ## How it works ```mermaid sequenceDiagram participant Atelet as atelet participant FS as /var/lib/ate/workers// participant Watcher as register.WatchFile (fsnotify / poll fallback) participant Reconciler as reconciler.run / sync participant Support as WorkerSupport.RegisterWorker Note over Watcher,FS: Watches parent dir for Create/Write on sandbox-runtimes.json Reconciler->>FS: Initial sync(): Read sandbox-runtimes.json alt File not written yet FS-->>Reconciler: ErrNotExist (wait for watch event or 5m resync) end Atelet->>FS: Probe hardware/runsc & atomic rename -> sandbox-runtimes.json FS-->>Watcher: fsnotify Create/Write event Watcher->>Reconciler: signal on changes channel Reconciler->>FS: Read sandbox-runtimes.json Reconciler->>Reconciler: proto.Equal(req, registered)? alt Changed (or first registration) Reconciler->>Support: RegisterWorker(capacity, default_runtime, restorable_runtimes) alt RPC succeeds Support-->>Reconciler: OK -> record registered = req else InvalidArgument (malformed runtime attributes) Support-->>Reconciler: Log Error, do not retry until file changes else Transient RPC error (e.g. Worker record not in ateapi yet) Support-->>Reconciler: Retry with exponential backoff (500ms..30s) end end ``` - **`atelet` owns all probing**: - For **`gvisor`** workers, `atelet` fetches the version's `gvisor` asset via `ensureSandboxAssets`, executes ` cpu-features` (memoized in-memory by the asset's SHA-256 digest), and encodes the reported names via `hardware.EncodeCPUFeatures` into a compact lowercase hex bitmask alongside `architecture` and `sandbox_runtime_hash`. - For **`microvm`** workers, `atelet` reads `/proc/cpuinfo` once (memoized after the first successful read) to record `cpu_vendor` and `cpu_model` alongside `architecture` and `sandbox_runtime_hash`. - **`cpu_features` bit layout**: - A feature's bit is the value of its gVisor `Feature` constant: on amd64, `32*block + bit` in that block's CPUID register (`X86FeatureAVX2 = 2*32 + 5 = 69`); on arm64, the value gVisor declares. Intel/AMD never reassign a CPUID bit, so a bit never moves when gVisor adds, reorders, or renames a feature, and a bit with no entry is one the hardware reserves. - `internal/hardware/cpufeatures_table.go` is a committed table, produced once from gVisor's `pkg/cpuid` sources at `gvisor.dev/gvisor@v0.0.0-20261014212059-ddc37c698141`: for every `Feature` constant with an entry in `allFeatures`, the constant's value keyed to the display name `runsc cpu-features` prints. Each entry's comment names the constant and, on amd64, its block and bit. The file header records this and how to add features from a newer gVisor by hand; because a bit is the hardware's, existing entries never change. - **This mapping is temporary.** Once `runsc` can print its feature set as this bitmask directly, `atelet` records that value as printed and the table, `EncodeCPUFeatures`, and `DecodeCPUFeatures` go away (TODO in the table header). The mask format and subset matching stay. No gVisor sources or generator are vendored for it. - **Subset matching**: `hardware.MatchesCompat` decodes `cpu_features` into a stack-allocated `[6]uint64` mask (384 bits; amd64 bits reach 261 today, with room for gVisor blocks through 11) and checks bitwise subset inclusion ($S_{\text{snapshot}} \subseteq S_{\text{worker}}$) while requiring exact equality on all other attributes. `DecodeCPUFeatures` rejects a mask that sets a bit with no name. - **Resilient watching & registration in the worker**: - `register.WatchFile` watches the parent directory for `Create`/`Write` events on the target file (reused by both `register.Report` and `ateworker-gvisor`'s `flagStore`), and falls back to periodic polling (`5s`) with a warning if `fsnotify` initialization fails (e.g. when `fs.inotify.max_user_instances` is exhausted on dense nodes). - `reconciler` runs a `5m` periodic resync tick alongside watch events, deduplicates consecutive registrations via `proto.Equal`, retries transient read/RPC failures with capped exponential backoff (`500ms..30s`), and drops permanent `InvalidArgument` rejections until the file changes. ## Compatibility `cpu_features` values recorded before this PR used a different bit numbering and a narrower mask. They do not compare meaningfully against workers built from this PR: any dev-cluster snapshot carrying one should be retaken rather than restored. No compatibility is guaranteed before v1.0.0, so there is no migration. - [x] Tests pass (`go test -race ./internal/hardware/... ./internal/sandboxruntimes/... ./internal/ateworker/register/... ./cmd/atelet/... ./cmd/ateworker-gvisor/... ./cmd/ateworker-microvm/...`) - [x] Appropriate changes to documentation are included in the PR --- .../internal/controlapi/workflow_resume.go | 43 +- .../controlapi/workflow_resume_test.go | 7 +- cmd/ateapi/internal/scheduling/scheduling.go | 11 +- .../internal/scheduling/scheduling_test.go | 13 +- cmd/atelet/main.go | 7 +- cmd/atelet/sandbox_assets.go | 12 +- cmd/atelet/worker_sandbox_syncer.go | 114 ++-- cmd/atelet/worker_sandbox_syncer_test.go | 146 ++++- cmd/atelet/worker_sandbox_versions.go | 276 +++++++-- cmd/atelet/worker_sandbox_versions_test.go | 402 +++++++++++-- cmd/atelet/workersupport.go | 8 +- cmd/ateworker-gvisor/flags.go | 55 +- cmd/ateworker-gvisor/flags_test.go | 13 + cmd/ateworker-gvisor/main.go | 16 +- cmd/ateworker-microvm/main.go | 16 +- docs/api-guide.md | 4 +- internal/ateworker/register/register.go | 198 ++++-- internal/ateworker/register/register_test.go | 568 ++++++++++++++++-- internal/ateworker/register/watch.go | 123 ++++ internal/hardware/cpufeatures.go | 244 ++++++++ internal/hardware/cpufeatures_table.go | 244 ++++++++ internal/hardware/cpufeatures_test.go | 333 ++++++++++ internal/hardware/hardware.go | 200 ++++-- internal/hardware/hardware_test.go | 103 +++- internal/nodepath/nodepath.go | 19 +- internal/proto/ateletpb/atelet.pb.go | 14 +- internal/proto/ateletpb/atelet.proto | 14 +- .../sandboxruntimes.go} | 15 +- .../sandboxruntimes_test.go} | 20 +- manifests/ate-install/atelet.yaml | 4 +- pkg/proto/ateapipb/ateapi.pb.go | 17 +- pkg/proto/ateapipb/ateapi.proto | 17 +- 32 files changed, 2846 insertions(+), 430 deletions(-) create mode 100644 internal/ateworker/register/watch.go create mode 100644 internal/hardware/cpufeatures.go create mode 100644 internal/hardware/cpufeatures_table.go create mode 100644 internal/hardware/cpufeatures_test.go rename internal/{sandboxversions/sandboxversions.go => sandboxruntimes/sandboxruntimes.go} (86%) rename internal/{sandboxversions/sandboxversions_test.go => sandboxruntimes/sandboxruntimes_test.go} (91%) diff --git a/cmd/ateapi/internal/controlapi/workflow_resume.go b/cmd/ateapi/internal/controlapi/workflow_resume.go index ffe448b47a..a28938a7be 100644 --- a/cmd/ateapi/internal/controlapi/workflow_resume.go +++ b/cmd/ateapi/internal/controlapi/workflow_resume.go @@ -508,7 +508,7 @@ func (w *ActorWorkflow) assignWorkerAttempt(ctx context.Context, actorRef resour if assignedWorker == nil { pickedWorker, err := w.scheduler.Schedule(ctx, constraints) if errors.Is(err, scheduling.ErrNoCapacity) && relaxable { - constraints.RequiredRuntime = nil + constraints.RequiredRuntime = hardware.Requirement{} pickedWorker, err = w.scheduler.Schedule(ctx, constraints) } if err != nil { @@ -521,10 +521,10 @@ func (w *ActorWorkflow) assignWorkerAttempt(ctx context.Context, actorRef resour assignedWorker = pickedWorker slog.InfoContext(ctx, "Picked worker", slog.Any("worker", pickedWorker.String())) - } else if relaxable && !hardware.WorkerMatches(assignedWorker.GetStatus(), snapRuntime) { + } else if relaxable && !snapRuntime.WorkerMatches(assignedWorker.GetStatus()) { // The stale claim only passed the relaxed check, so bind under it: // admit asks the same question again under the row lock. - constraints.RequiredRuntime = nil + constraints.RequiredRuntime = hardware.Requirement{} } assignment := &ateapipb.ActorAssignment{ @@ -601,7 +601,7 @@ func (w *ActorWorkflow) assignWorkerAttempt(ctx context.Context, actorRef resour // attempt re-runs scheduling. var errWorkerFilledUp = errors.New("picked worker no longer has room") -func workerAssignmentFrom(w *ateapipb.Worker, snapRuntime *ateapipb.SandboxRuntime) *ateapipb.WorkerAssignment { +func workerAssignmentFrom(w *ateapipb.Worker, snapRuntime hardware.Requirement) *ateapipb.WorkerAssignment { return &ateapipb.WorkerAssignment{ Worker: &ateapipb.ObjectRef{Name: w.GetMetadata().GetName()}, WorkerNamespace: w.GetWorkerNamespace(), @@ -615,15 +615,16 @@ func workerAssignmentFrom(w *ateapipb.Worker, snapRuntime *ateapipb.SandboxRunti // during this assignment are stamped with it, so a worker that later // changes its default does not relabel state captured on the old one. // - // TODO: A restorable_runtimes match records the old runtime here, but - // resolveSandboxAssets still ships the SandboxConfig's default version - // and atelet restores with the assets in the request, so the restore - // runs on the new version: it fails, or the next checkpoint is stamped - // with a runtime that did not produce it. Resolve the assets for the - // matched version once SandboxConfig can name them, or treat a - // restorable-only match as incompatible until it can. Latent for now: - // worker reports no restorable_runtimes (internal/ateworker/register.go). - SandboxRuntime: proto.CloneOf(hardware.RuntimeFor(w.GetStatus(), snapRuntime)), + // TODO: Workers report restorable_runtimes, so a match on one records + // that older runtime here, but resolveSandboxAssets still ships the + // template's SandboxConfig default version and atelet restores with + // the assets in the request: the restore runs on the newer version and + // fails, or its next checkpoint is stamped with a runtime that did not + // produce it. A follow-up replaces the assets in Run and Restore with + // this sandbox_runtime, which atelet resolves to the matching version + // of the worker pool's SandboxConfig; until it lands, keep a config's + // default version the only enabled one. + SandboxRuntime: proto.CloneOf(snapRuntime.RuntimeFor(w.GetStatus())), } } @@ -660,7 +661,7 @@ func schedulingConstraints(actor *ateapipb.Actor, tmpl *ateapipb.ActorTemplate) } c := scheduling.Constraints{ SandboxClass: tmpl.GetSandboxConfig().GetSandboxClass(), - RequiredRuntime: requiredSnapshotRuntime(actor, tmpl), + RequiredRuntime: hardware.NewRequirement(requiredSnapshotRuntime(actor, tmpl)), ActorSelector: labels.SelectorFromSet(labels.Set(actor.GetWorkerSelector().GetMatchLabels())), RequiredNodes: requiredNodes, Limits: limits.Proto(), @@ -674,6 +675,10 @@ func schedulingConstraints(actor *ateapipb.Actor, tmpl *ateapipb.ActorTemplate) // requiredSnapshotRuntime returns the SandboxRuntime stamped on the snapshot // that ResumeActor will restore guest state from, or nil when the resume does // not restore guest state (cold boot, VOLUMES fidelity, or replaced template). +// +// TODO: Once gVisor can restore ROOTFS snapshots, return nil for ROOTFS as +// well: a rootfs upper is files tied to the image, not to the runtime, so only +// MEMORY needs a runtime match. Until then ROOTFS is pinned like MEMORY. func requiredSnapshotRuntime(actor *ateapipb.Actor, tmpl *ateapipb.ActorTemplate) *ateapipb.SandboxRuntime { if localSnap := findLatestLocalSnapshot(actor.GetStatus()); localSnap != nil { if localSnap.GetFidelity() == ateapipb.SnapshotFidelity_SNAPSHOT_FIDELITY_VOLUMES { @@ -701,7 +706,7 @@ func requiredSnapshotRuntime(actor *ateapipb.Actor, tmpl *ateapipb.ActorTemplate // fidelity downgrade, and nothing invalidates the checkpoint files if the actor // boots elsewhere. Suspending the actor uploads it and lifts the pin. func canRelaxRuntime(actor *ateapipb.Actor, constraints scheduling.Constraints) bool { - if constraints.RequiredRuntime == nil { + if constraints.RequiredRuntime.Runtime() == nil { return false } return findLatestLocalSnapshot(actor.GetStatus()) == nil @@ -711,11 +716,11 @@ func canRelaxRuntime(actor *ateapipb.Actor, constraints scheduling.Constraints) // relaxable — satisfies constraints with RequiredRuntime relaxed. func (w *ActorWorkflow) appliesWithFallback(worker *ateapipb.Worker, constraints scheduling.Constraints, relaxable bool) bool { relaxed := constraints - relaxed.RequiredRuntime = nil + relaxed.RequiredRuntime = hardware.Requirement{} if !w.scheduler.Applies(worker, relaxed) { return false } - return relaxable || hardware.WorkerMatches(worker.GetStatus(), constraints.RequiredRuntime) + return relaxable || constraints.RequiredRuntime.WorkerMatches(worker.GetStatus()) } // ensureVolumesAttached attaches the actor's mounted external volumes to the @@ -833,6 +838,10 @@ func (w *ActorWorkflow) ensureAteletRestored(ctx context.Context, actorRef resou if runtimeIncompatible { logRuntimeFallback(ctx, actorRef, assignment, src.RequiredRuntime) } + // TODO: Once gVisor can restore ROOTFS snapshots, drop an incompatible + // runtime to ROOTFS, not VOLUMES: only the memory layer is invalid. A + // replaced template still drops to VOLUMES, as the rootfs upper is + // only valid over the image it was written on. scope = ateletpb.SnapshotFidelity_SNAPSHOT_FIDELITY_VOLUMES } tele.WireFidelity = ateattr.SnapshotFidelityValue(scope) diff --git a/cmd/ateapi/internal/controlapi/workflow_resume_test.go b/cmd/ateapi/internal/controlapi/workflow_resume_test.go index 31777f5adf..c70b0faf8c 100644 --- a/cmd/ateapi/internal/controlapi/workflow_resume_test.go +++ b/cmd/ateapi/internal/controlapi/workflow_resume_test.go @@ -30,6 +30,7 @@ import ( "go.ate.dev/substrate/cmd/ateapi/internal/workercache" "go.ate.dev/substrate/internal/apierror" "go.ate.dev/substrate/internal/ateattr" + "go.ate.dev/substrate/internal/hardware" "go.ate.dev/substrate/internal/installdefaults" "go.ate.dev/substrate/internal/preview" "go.ate.dev/substrate/internal/proto/ateletpb" @@ -2367,7 +2368,7 @@ type relaxingTestScheduler struct { } func (s *relaxingTestScheduler) Schedule(_ context.Context, c scheduling.Constraints) (*ateapipb.Worker, error) { - if c.RequiredRuntime != nil { + if c.RequiredRuntime.Runtime() != nil { return nil, scheduling.ErrNoCapacity } return s.relaxedWorker, nil @@ -2683,7 +2684,7 @@ func TestCanRelaxRuntime(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { actor := &ateapipb.Actor{Status: &ateapipb.ActorStatus{Snapshots: tc.snapshots}} - if got := canRelaxRuntime(actor, scheduling.Constraints{RequiredRuntime: tc.required}); got != tc.want { + if got := canRelaxRuntime(actor, scheduling.Constraints{RequiredRuntime: hardware.NewRequirement(tc.required)}); got != tc.want { t.Errorf("canRelaxRuntime = %v, want %v", got, tc.want) } }) @@ -2694,7 +2695,7 @@ func TestCanRelaxRuntime(t *testing.T) { // so the worker's has to reach it. func TestWorkerAssignmentFromCopiesSPIFFEID(t *testing.T) { const id = "spiffe://cluster.local/ns/workers/sa/substrate-worker" - got := workerAssignmentFrom(&ateapipb.Worker{WorkerSpiffeId: id}, nil) + got := workerAssignmentFrom(&ateapipb.Worker{WorkerSpiffeId: id}, hardware.Requirement{}) if got.GetWorkerSpiffeId() != id { t.Errorf("worker SPIFFE ID = %q, want %q", got.GetWorkerSpiffeId(), id) } diff --git a/cmd/ateapi/internal/scheduling/scheduling.go b/cmd/ateapi/internal/scheduling/scheduling.go index 58c4d04bc9..070d2bceb2 100644 --- a/cmd/ateapi/internal/scheduling/scheduling.go +++ b/cmd/ateapi/internal/scheduling/scheduling.go @@ -34,10 +34,11 @@ type Constraints struct { // portable across sandbox classes, so this is never relaxed. SandboxClass string - // RequiredRuntime, when non-nil, restricts placement to workers whose - // DefaultRuntime or RestorableRuntimes can restore a snapshot stamped with - // this runtime. - RequiredRuntime *ateapipb.SandboxRuntime + // RequiredRuntime, when it names a runtime, restricts placement to + // workers whose DefaultRuntime or RestorableRuntimes can restore a + // snapshot stamped with it. Prepared once per Schedule, since Applies + // asks it of every candidate. + RequiredRuntime hardware.Requirement // TemplateSelector and ActorSelector must both match the worker's labels. TemplateSelector labels.Selector @@ -232,7 +233,7 @@ func (s *scheduler) Applies(worker *ateapipb.Worker, constraints Constraints) bo return false } - if !hardware.WorkerMatches(worker.GetStatus(), constraints.RequiredRuntime) { + if !constraints.RequiredRuntime.WorkerMatches(worker.GetStatus()) { return false } diff --git a/cmd/ateapi/internal/scheduling/scheduling_test.go b/cmd/ateapi/internal/scheduling/scheduling_test.go index bf296d6a6b..d94e50d5f9 100644 --- a/cmd/ateapi/internal/scheduling/scheduling_test.go +++ b/cmd/ateapi/internal/scheduling/scheduling_test.go @@ -19,6 +19,7 @@ import ( "errors" "testing" + "go.ate.dev/substrate/internal/hardware" "go.ate.dev/substrate/internal/resources" "go.ate.dev/substrate/pkg/proto/ateapipb" "k8s.io/apimachinery/pkg/labels" @@ -331,7 +332,7 @@ func TestSchedule(t *testing.T) { worker("w-arm", "gvisor", "node-a", tierTwo, withRuntimes(sandboxRuntime("gvisor", "arm64"))), worker("w-amd", "gvisor", "node-b", tierTwo, withRuntimes(sandboxRuntime("gvisor", "amd64"))), }, - constraints: Constraints{SandboxClass: "gvisor", RequiredRuntime: sandboxRuntime("gvisor", "amd64")}, + constraints: Constraints{SandboxClass: "gvisor", RequiredRuntime: hardware.NewRequirement(sandboxRuntime("gvisor", "amd64"))}, wantPod: "w-amd", }, { @@ -340,7 +341,7 @@ func TestSchedule(t *testing.T) { worker("w-upgraded", "gvisor", "node-a", tierTwo, withRuntimes(sandboxRuntime("gvisor", "amd64", "new"), sandboxRuntime("gvisor", "amd64", "old"))), }, - constraints: Constraints{SandboxClass: "gvisor", RequiredRuntime: sandboxRuntime("gvisor", "amd64", "old")}, + constraints: Constraints{SandboxClass: "gvisor", RequiredRuntime: hardware.NewRequirement(sandboxRuntime("gvisor", "amd64", "old"))}, wantPod: "w-upgraded", }, { @@ -348,7 +349,7 @@ func TestSchedule(t *testing.T) { fleet: fleet{ worker("w-arm", "gvisor", "node-a", tierTwo, withRuntimes(sandboxRuntime("gvisor", "arm64"))), }, - constraints: Constraints{SandboxClass: "gvisor", RequiredRuntime: sandboxRuntime("gvisor", "amd64")}, + constraints: Constraints{SandboxClass: "gvisor", RequiredRuntime: hardware.NewRequirement(sandboxRuntime("gvisor", "amd64"))}, }, } @@ -458,20 +459,20 @@ func TestApplies(t *testing.T) { { name: "matches required runtime via default runtime", worker: worker("w", "gvisor", "node-a", nil, withRuntimes(sandboxRuntime("gvisor", "amd64", "old"))), - constraints: Constraints{SandboxClass: "gvisor", RequiredRuntime: sandboxRuntime("gvisor", "amd64", "old")}, + constraints: Constraints{SandboxClass: "gvisor", RequiredRuntime: hardware.NewRequirement(sandboxRuntime("gvisor", "amd64", "old"))}, want: true, }, { name: "matches required runtime via restorable runtimes", worker: worker("w", "gvisor", "node-a", nil, withRuntimes(sandboxRuntime("gvisor", "amd64", "new"), sandboxRuntime("gvisor", "amd64", "old"))), - constraints: Constraints{SandboxClass: "gvisor", RequiredRuntime: sandboxRuntime("gvisor", "amd64", "old")}, + constraints: Constraints{SandboxClass: "gvisor", RequiredRuntime: hardware.NewRequirement(sandboxRuntime("gvisor", "amd64", "old"))}, want: true, }, { name: "skips worker when required runtime does not match", worker: worker("w", "gvisor", "node-a", nil, withRuntimes(sandboxRuntime("gvisor", "arm64"))), - constraints: Constraints{SandboxClass: "gvisor", RequiredRuntime: sandboxRuntime("gvisor", "amd64")}, + constraints: Constraints{SandboxClass: "gvisor", RequiredRuntime: hardware.NewRequirement(sandboxRuntime("gvisor", "amd64"))}, want: false, }, } diff --git a/cmd/atelet/main.go b/cmd/atelet/main.go index 7c1adecddf..8e5ad30235 100644 --- a/cmd/atelet/main.go +++ b/cmd/atelet/main.go @@ -394,15 +394,16 @@ func main() { if _, err := startSandboxAssetPrewarm(ctx, sandboxConfigInformer, wmService, imageCache, microvmNodeCapable(hostDevRoot)); err != nil { slog.ErrorContext(ctx, "Sandbox asset prewarm disabled", slog.Any("err", err)) } - // Keep a sandbox versions file in each local worker's directory: the - // runtime hashes of the enabled versions of its pool's SandboxConfig, + // Keep a sandbox runtimes file in each local worker's directory: the + // runtimes of the enabled versions of its pool's SandboxConfig, // rewritten when the config changes. if _, err := startWorkerSandboxSyncer(ctx, workerPodFactory.Core().V1().Pods(), ateFactory.Api().V1alpha1().WorkerPools(), ateFactory.Api().V1alpha1().SandboxConfigs(), + wmService, nodepath.WorkersDir()); err != nil { - slog.ErrorContext(ctx, "Worker sandbox versions syncer disabled", slog.Any("err", err)) + slog.ErrorContext(ctx, "Worker sandbox runtimes syncer disabled", slog.Any("err", err)) } // Start after the informers are registered: a factory only runs // informers that exist when Start is called. diff --git a/cmd/atelet/sandbox_assets.go b/cmd/atelet/sandbox_assets.go index ed3945217e..12b177a398 100644 --- a/cmd/atelet/sandbox_assets.go +++ b/cmd/atelet/sandbox_assets.go @@ -184,13 +184,19 @@ func (s *Atelet) ensureSandboxAssets(ctx context.Context, rec *sandboxAssetsReco return paths, nil } +// runscAssetNames are the assets that carry the gVisor `runsc` binary, most +// preferred first. +var runscAssetNames = []string{gvisorAssetName, runscAssetName} + // runscPathFor returns the local path of the gVisor `runsc` binary from a // fetched asset-path map, or "" if the runtime has none (e.g. micro-VM). func runscPathFor(paths map[string]string) string { - if p := paths[gvisorAssetName]; p != "" { - return p + for _, name := range runscAssetNames { + if p := paths[name]; p != "" { + return p + } } - return paths[runscAssetName] + return "" } // fetchAsset downloads one content-addressed asset (verifying its sha256) into diff --git a/cmd/atelet/worker_sandbox_syncer.go b/cmd/atelet/worker_sandbox_syncer.go index 9880c40581..0d25863ff9 100644 --- a/cmd/atelet/worker_sandbox_syncer.go +++ b/cmd/atelet/worker_sandbox_syncer.go @@ -21,10 +21,10 @@ package main // random delay when a pool or config change reaches every node at once // - process: for each UID, sync the pod, or remove its worker directory // once the pod is gone -// - finish: forget the UID, or retry it with backoff up to -// syncMaxRetries times +// - finish: forget the UID, or retry it with backoff, capped at +// syncMaxRetries while the pool's SandboxConfig is unresolved // -// worker_sandbox_versions.go builds the sandbox config version file for each +// worker_sandbox_versions.go builds the sandbox runtimes file for each // worker pod. import ( @@ -35,6 +35,7 @@ import ( "math/rand/v2" "os" "path/filepath" + "sync" "time" corev1 "k8s.io/api/core/v1" @@ -44,6 +45,7 @@ import ( "k8s.io/client-go/tools/cache" "k8s.io/client-go/util/workqueue" + "go.ate.dev/substrate/internal/hardware" "go.ate.dev/substrate/internal/resources" "go.ate.dev/substrate/pkg/api/v1alpha1" informersv1alpha1 "go.ate.dev/substrate/pkg/client/informers/externalversions/api/v1alpha1" @@ -70,40 +72,70 @@ const orphanMinAge = time.Minute // for the file. const syncMaxJitter = 30 * time.Second -// syncMaxRetries bounds how often one pod's failed sync is retried before it -// is dropped. The pod is synced again only when its pool switches to another -// SandboxConfig, the config's spec changes, or atelet restarts. The backoff -// doubles from one second, so this covers a few minutes of informer lag or an -// unresolved pool without retrying forever. +// syncMaxRetries bounds how often one pod's errUnresolved sync is retried +// before it is dropped. The backoff doubles from one second, so this covers a +// few minutes of informer lag or an unresolved pool without retrying forever; +// the informer events that end those also enqueue the pod again. Every other +// failure is exempt and keeps retrying at the rate limiter's longest backoff: +// an asset fetch or probe that fails for the default version, one that leaves +// a restorable version out (errIncomplete), or a write to the worker +// directory, since nothing enqueues the pod when those recover. const syncMaxRetries = 8 -// workerSandboxSyncer keeps a sandbox versions file in each local worker's +// syncWorkers is how many goroutines drain the sync queue in run. More than +// one keeps a slow asset fetch on one pod from blocking directory cleanup and +// cached syncs for other pods on the node. +const syncWorkers = 2 + +// workerSandboxSyncer keeps a sandbox runtimes file in each local worker's // directory under workersDir: the runtimes of the enabled versions of the // SandboxConfig the worker's pool uses. It creates a worker's directory once // it writes the pod's file and removes it when the pod is gone. type workerSandboxSyncer struct { resolver *workerSandboxResolver // pods holds this node's worker pods, indexed by podUIDIndex. - pods cache.Indexer - // queue carries pod UIDs. One worker drains it, so a pod's file is never - // written and its directory removed at the same time. + pods cache.Indexer + assets sandboxAssetFetcher + // queue carries pod UIDs. A workqueue never hands the same key to two + // workers at once, so a pod's file is never written and its directory + // removed at the same time. queue workqueue.TypedRateLimitingInterface[string] workersDir string // nodepath.WorkersDir(); a temp dir in tests minAge time.Duration maxJitter time.Duration + + // probeCPUIdentity is hardware.ProbeCPUIdentity. Overridable in tests. + probeCPUIdentity func() (vendor, model string, err error) + // runCPUFeatures is runRunscCPUFeatures. Overridable in tests. + runCPUFeatures func(ctx context.Context, runscPath string) ([]string, error) + // mu guards hostCPU and cpuFeatures across the syncWorkers in run: + // hostCPU is the probed host CPU identity, nil until a probe succeeds, + // and cpuFeatures the encoded cpu_features of each runsc binary by its + // gVisor asset's SHA256 digest. A binary is fetched and run once per + // digest, except that two workers meeting a digest before either has + // memoized it both probe it; the duplicate costs a fetch from the asset + // cache and a run of runsc, and both store the same mask. + mu sync.Mutex + hostCPU *cpuIdentity + cpuFeatures map[string]string } -func newWorkerSandboxSyncer(resolver *workerSandboxResolver, pods cache.Indexer, workersDir string) *workerSandboxSyncer { +func newWorkerSandboxSyncer(resolver *workerSandboxResolver, pods cache.Indexer, assets sandboxAssetFetcher, workersDir string) *workerSandboxSyncer { return &workerSandboxSyncer{ resolver: resolver, pods: pods, - // Failures are informer lag and unresolved pools, both on the scale - // of seconds to minutes. + assets: assets, + // Backoff doubles from 1s for informer lag and unresolved pools, and + // caps at 5m where uncapped retries for transient asset fetch or probe + // failures stay until they recover. queue: workqueue.NewTypedRateLimitingQueue( workqueue.NewTypedItemExponentialFailureRateLimiter[string](time.Second, 5*time.Minute)), - workersDir: workersDir, - minAge: orphanMinAge, - maxJitter: syncMaxJitter, + workersDir: workersDir, + minAge: orphanMinAge, + maxJitter: syncMaxJitter, + probeCPUIdentity: hardware.ProbeCPUIdentity, + runCPUFeatures: runRunscCPUFeatures, + cpuFeatures: make(map[string]string), } } @@ -115,7 +147,7 @@ func newWorkerSandboxSyncer(resolver *workerSandboxResolver, pods cache.Indexer, // The syncer never holds up atelet startup: until the informers sync, or // while a list/watch fails (e.g. Forbidden while an RBAC rollout lags the // binary), files wait and existing ones stay as they are. -func startWorkerSandboxSyncer(ctx context.Context, pods coreinformers.PodInformer, workerPools informersv1alpha1.WorkerPoolInformer, configs informersv1alpha1.SandboxConfigInformer, workersDir string) (*workerSandboxSyncer, error) { +func startWorkerSandboxSyncer(ctx context.Context, pods coreinformers.PodInformer, workerPools informersv1alpha1.WorkerPoolInformer, configs informersv1alpha1.SandboxConfigInformer, assets sandboxAssetFetcher, workersDir string) (*workerSandboxSyncer, error) { if err := pods.Informer().AddIndexers(cache.Indexers{podUIDIndex: func(obj any) ([]string, error) { pod, ok := obj.(*corev1.Pod) if !ok { @@ -129,7 +161,7 @@ func startWorkerSandboxSyncer(ctx context.Context, pods coreinformers.PodInforme workerPools: workerPools.Lister(), configs: configs.Lister(), } - s := newWorkerSandboxSyncer(resolver, pods.Informer().GetIndexer(), workersDir) + s := newWorkerSandboxSyncer(resolver, pods.Informer().GetIndexer(), assets, workersDir) if err := watch(ctx, "worker pod", pods.Informer(), s.podHandler()); err != nil { return nil, err } @@ -140,14 +172,14 @@ func startWorkerSandboxSyncer(ctx context.Context, pods coreinformers.PodInforme return nil, err } go s.run(ctx, pods.Informer().HasSynced, workerPools.Informer().HasSynced, configs.Informer().HasSynced) - slog.InfoContext(ctx, "Worker sandbox versions syncer started") + slog.InfoContext(ctx, "Worker sandbox runtimes syncer started") return s, nil } // watch registers handler on inf and logs inf's list/watch failures. func watch(ctx context.Context, kind string, inf cache.SharedIndexInformer, handler cache.ResourceEventHandler) error { if err := inf.SetWatchErrorHandler(func(_ *cache.Reflector, err error) { - slog.WarnContext(ctx, kind+" list/watch failed; workers on this node cannot update their sandbox versions until it recovers", slog.Any("err", err)) + slog.WarnContext(ctx, kind+" list/watch failed; workers on this node cannot update their sandbox runtimes until it recovers", slog.Any("err", err)) }); err != nil { // Fails only on an already-started informer, where the default // reflector logging applies. @@ -282,13 +314,21 @@ func (s *workerSandboxSyncer) run(ctx context.Context, synced ...cache.InformerS return } s.enqueueExistingDirs(ctx) - for { - uid, shutdown := s.queue.Get() - if shutdown { - return - } - s.process(ctx, uid) + var wg sync.WaitGroup + for range syncWorkers { + wg.Add(1) + go func() { + defer wg.Done() + for { + uid, shutdown := s.queue.Get() + if shutdown { + return + } + s.process(ctx, uid) + } + }() } + wg.Wait() } // process brings the directory of the pod with the given UID up to date: @@ -326,24 +366,28 @@ func (s *workerSandboxSyncer) podByUID(uid string) (*corev1.Pod, error) { return pod, nil } -// finish forgets uid on success or errPermanent, and otherwise retries it with -// backoff up to syncMaxRetries times. A sync that fails before it writes -// leaves the existing file in place. +// finish forgets uid on success or errPermanent, caps errUnresolved retries at +// syncMaxRetries, and otherwise requeues with backoff without a retry cap. A +// sync that fails before it writes leaves the existing file in place. func (s *workerSandboxSyncer) finish(ctx context.Context, uid string, err error) { switch { case err == nil: s.queue.Forget(uid) case errors.Is(err, errPermanent): - slog.WarnContext(ctx, "Cannot sync worker sandbox versions; not retrying until the pool's SandboxConfig changes or atelet restarts", slog.String("pod_uid", uid), slog.Any("err", err)) + slog.WarnContext(ctx, "Cannot sync worker sandbox runtimes; not retrying until the pool's SandboxConfig changes or atelet restarts", slog.String("pod_uid", uid), slog.Any("err", err)) s.queue.Forget(uid) case ctx.Err() != nil: // Shutting down, not a sync failure. - case s.queue.NumRequeues(uid) < syncMaxRetries: - slog.WarnContext(ctx, "Could not sync worker sandbox versions; will retry", + case errors.Is(err, errIncomplete): + slog.WarnContext(ctx, "Worker sandbox runtimes file is missing a restorable version; will retry", + slog.String("pod_uid", uid), slog.Int("retries", s.queue.NumRequeues(uid)), slog.Any("err", err)) + s.queue.AddRateLimited(uid) + case s.queue.NumRequeues(uid) < syncMaxRetries || !errors.Is(err, errUnresolved): + slog.WarnContext(ctx, "Could not sync worker sandbox runtimes; will retry", slog.String("pod_uid", uid), slog.Int("retries", s.queue.NumRequeues(uid)), slog.Any("err", err)) s.queue.AddRateLimited(uid) default: - slog.WarnContext(ctx, "Could not sync worker sandbox versions; giving up until the pool's SandboxConfig changes or atelet restarts", + slog.WarnContext(ctx, "Could not sync worker sandbox runtimes; giving up until the pool's SandboxConfig changes or atelet restarts", slog.String("pod_uid", uid), slog.Int("retries", syncMaxRetries), slog.Any("err", err)) s.queue.Forget(uid) } diff --git a/cmd/atelet/worker_sandbox_syncer_test.go b/cmd/atelet/worker_sandbox_syncer_test.go index 99c38368b0..d335d2e473 100644 --- a/cmd/atelet/worker_sandbox_syncer_test.go +++ b/cmd/atelet/worker_sandbox_syncer_test.go @@ -19,7 +19,9 @@ import ( "errors" "os" "path/filepath" + "runtime" "slices" + "strings" "testing" "time" @@ -33,13 +35,73 @@ import ( "k8s.io/client-go/tools/cache" "k8s.io/client-go/util/workqueue" - "go.ate.dev/substrate/internal/sandboxversions" + "go.ate.dev/substrate/internal/sandboxruntimes" "go.ate.dev/substrate/pkg/api/v1alpha1" atefake "go.ate.dev/substrate/pkg/client/clientset/versioned/fake" "go.ate.dev/substrate/pkg/client/informers/externalversions" listersv1alpha1 "go.ate.dev/substrate/pkg/client/listers/api/v1alpha1" ) +const ( + testCPUVendor = "GenuineIntel" + testCPUModel = "6:143" +) + +// testCPUFeatures are the first two CPU feature names for runtime.GOARCH, +// which encode to bit 0 | bit 1 = "3" on both amd64 and arm64. +func testCPUFeatures() []string { + switch runtime.GOARCH { + case "arm64": + return []string{"fp", "asimd"} + default: + return []string{"pni", "pclmulqdq"} + } +} + +type fakeAssetFetcher struct { + fn func(ctx context.Context, rec *sandboxAssetsRecord) (map[string]string, error) +} + +func (f *fakeAssetFetcher) ensureSandboxAssets(ctx context.Context, rec *sandboxAssetsRecord) (map[string]string, error) { + return f.fn(ctx, rec) +} + +// newTestAssetFetcher returns a sandboxAssetFetcher that maps every asset of +// a record to a path under its digest without fetching anything, for syncers +// whose runsc probe is faked. +func newTestAssetFetcher() sandboxAssetFetcher { + return &fakeAssetFetcher{ + fn: func(_ context.Context, rec *sandboxAssetsRecord) (map[string]string, error) { + paths := make(map[string]string, len(rec.Assets)) + for name, a := range rec.Assets { + paths[name] = "/cache/" + a.SHA256 + "/" + name + } + return paths, nil + }, + } +} + +// newScriptAssetFetcher returns a sandboxAssetFetcher that backs every asset +// with an executable script printing testCPUFeatures(), for a syncer that +// runs the real runsc probe. +func newScriptAssetFetcher(t *testing.T) sandboxAssetFetcher { + t.Helper() + script := filepath.Join(t.TempDir(), "runsc") + body := "#!/bin/sh\necho " + strings.Join(testCPUFeatures(), ",") + "\n" + if err := os.WriteFile(script, []byte(body), 0o755); err != nil { + t.Fatal(err) + } + return &fakeAssetFetcher{ + fn: func(_ context.Context, rec *sandboxAssetsRecord) (map[string]string, error) { + paths := make(map[string]string, len(rec.Assets)) + for name := range rec.Assets { + paths[name] = script + } + return paths, nil + }, + } +} + // workerPod is a worker pod on this node with the given UID, in pool when // pool is not "". func workerPod(uid, pool string) *corev1.Pod { @@ -53,18 +115,26 @@ func workerPod(uid, pool string) *corev1.Pod { // resolvedPool is a gVisor WorkerPool whose class resolved to config, or has // not resolved when config is "". func resolvedPool(name, config string) *v1alpha1.WorkerPool { + return resolvedPoolOf(v1alpha1.SandboxClassGvisor, name, config) +} + +// resolvedPoolOf is a WorkerPool of the given sandbox class whose class +// resolved to config, or has not resolved when config is "". +func resolvedPoolOf(class v1alpha1.SandboxClass, name, config string) *v1alpha1.WorkerPool { pool := &v1alpha1.WorkerPool{ObjectMeta: metav1.ObjectMeta{Namespace: "workers", Name: name}} - pool.Spec.SandboxClasses = []v1alpha1.WorkerPoolSandboxClass{{Name: v1alpha1.SandboxClassGvisor}} - class := v1alpha1.WorkerPoolSandboxClassStatus{Name: v1alpha1.SandboxClassGvisor} + pool.Spec.SandboxClasses = []v1alpha1.WorkerPoolSandboxClass{{Name: class}} + status := v1alpha1.WorkerPoolSandboxClassStatus{Name: class} if config != "" { - class.ConfigRef = &v1alpha1.SandboxConfigReference{Name: config} + status.ConfigRef = &v1alpha1.SandboxConfigReference{Name: config} } - pool.Status.SandboxClasses = []v1alpha1.WorkerPoolSandboxClassStatus{class} + pool.Status.SandboxClasses = []v1alpha1.WorkerPoolSandboxClassStatus{status} return pool } // newTestSyncer serves objs from synced caches and keeps worker directories -// under a temp dir. +// under a temp dir. Its probes are faked: the host CPU is +// testCPUVendor/testCPUModel and every runsc reports testCPUFeatures(); +// TestRunRunscCPUFeatures covers the real probe. func newTestSyncer(t *testing.T, objs ...any) *workerSandboxSyncer { t.Helper() newIndexer := func() cache.Indexer { @@ -94,7 +164,9 @@ func newTestSyncer(t *testing.T, objs ...any) *workerSandboxSyncer { s := newWorkerSandboxSyncer(&workerSandboxResolver{ workerPools: listersv1alpha1.NewWorkerPoolLister(pools), configs: listersv1alpha1.NewSandboxConfigLister(configs), - }, pods, t.TempDir()) + }, pods, newTestAssetFetcher(), t.TempDir()) + s.probeCPUIdentity = func() (string, string, error) { return testCPUVendor, testCPUModel, nil } + s.runCPUFeatures = func(context.Context, string) ([]string, error) { return testCPUFeatures(), nil } // Tests that check the jitter set it. s.maxJitter = 0 return s @@ -297,11 +369,13 @@ func TestWorkerSandboxSyncerProcess(t *testing.T) { name string objs []any // existing is written to pod-a's file before processing, when set. - existing *sandboxversions.File + existing *sandboxruntimes.File + // assets overrides s.assets when set. + assets sandboxAssetFetcher // priorRetries is how often pod-a has been retried before. priorRetries int // want is the file afterwards; nil means no file. - want *sandboxversions.File + want *sandboxruntimes.File wantDir bool // wantUntouched is whether the existing file must not be rewritten. wantUntouched bool @@ -354,6 +428,40 @@ func TestWorkerSandboxSyncerProcess(t *testing.T) { name: "unresolved pool gives up after syncMaxRetries", objs: []any{workerPod("pod-a", "pool"), multiVersionConfig()}, priorRetries: syncMaxRetries, + }, { + // A transient failure on the default version leaves the existing file + // in place and requeues without the syncMaxRetries cap, since no + // informer event enqueues the pod when an asset fetch recovers. + name: "transient failure on default version keeps existing file and requeues past syncMaxRetries", + objs: []any{workerPod("pod-a", "pool"), resolvedPool("pool", "gvisor-release"), multiVersionConfig()}, + existing: stale, + assets: &fakeAssetFetcher{ + fn: func(context.Context, *sandboxAssetsRecord) (map[string]string, error) { + return nil, errors.New("temporary download failure on default version") + }, + }, + priorRetries: syncMaxRetries, + want: stale, + wantDir: true, + wantUntouched: true, + wantRetries: syncMaxRetries + 1, + }, { + // A transient failure on a restorable version writes the default + // runtime right away and requeues without the syncMaxRetries cap. + name: "incomplete restorable version writes partial file and requeues past syncMaxRetries", + objs: []any{workerPod("pod-a", "pool"), resolvedPool("pool", "gvisor-release"), multiVersionConfig()}, + assets: &fakeAssetFetcher{ + fn: func(ctx context.Context, rec *sandboxAssetsRecord) (map[string]string, error) { + if rec.Assets[gvisorAssetName].SHA256 != "sha-v2" { + return nil, errors.New("temporary download failure on restorable version") + } + return newTestAssetFetcher().ensureSandboxAssets(ctx, rec) + }, + }, + priorRetries: syncMaxRetries, + want: &sandboxruntimes.File{DefaultRuntime: wantRuntime("v2")}, + wantDir: true, + wantRetries: syncMaxRetries + 1, }, { name: "pod of no pool is not retried", objs: []any{workerPod("pod-a", ""), resolvedPool("pool", "gvisor-release"), multiVersionConfig()}, @@ -361,6 +469,9 @@ func TestWorkerSandboxSyncerProcess(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { s := newTestSyncer(t, tt.objs...) + if tt.assets != nil { + s.assets = tt.assets + } limiter := workqueue.NewTypedItemExponentialFailureRateLimiter[string](time.Second, 5*time.Minute) for range tt.priorRetries { limiter.When("pod-a") @@ -371,13 +482,13 @@ func TestWorkerSandboxSyncerProcess(t *testing.T) { if err := os.MkdirAll(dir, 0o700); err != nil { t.Fatal(err) } - if _, err := sandboxversions.Write(s.versionsFile("pod-a"), tt.existing); err != nil { + if _, err := sandboxruntimes.Write(s.runtimesFile("pod-a"), tt.existing); err != nil { t.Fatal(err) } } var fileBefore time.Time if _, err := os.Stat(dir); err == nil { - file, err := os.Stat(s.versionsFile("pod-a")) + file, err := os.Stat(s.runtimesFile("pod-a")) if err != nil { t.Fatal(err) } @@ -393,7 +504,7 @@ func TestWorkerSandboxSyncerProcess(t *testing.T) { if _, err := os.Stat(dir); (err == nil) != tt.wantDir { t.Errorf("directory exists = %v (%v), want %v", err == nil, err, tt.wantDir) } - got, err := sandboxversions.Read(s.versionsFile("pod-a")) + got, err := sandboxruntimes.Read(s.runtimesFile("pod-a")) if err != nil && !errors.Is(err, os.ErrNotExist) { t.Fatal(err) } @@ -401,7 +512,7 @@ func TestWorkerSandboxSyncerProcess(t *testing.T) { t.Errorf("file mismatch (-want +got):\n%s", diff) } if tt.wantUntouched { - if after, err := os.Stat(s.versionsFile("pod-a")); err != nil || !after.ModTime().Equal(fileBefore) { + if after, err := os.Stat(s.runtimesFile("pod-a")); err != nil || !after.ModTime().Equal(fileBefore) { t.Errorf("an equal file was rewritten (mtime %v -> %v, %v)", fileBefore, after.ModTime(), err) } } @@ -559,7 +670,7 @@ func TestWorkerSandboxSyncerRun(t *testing.T) { // A pod Add event queued before the caches sync. s.queue.Add("pod-live") matches := func() bool { - return exists(s.podDir("pod-gone")) == tt.wantOrphan && exists(s.versionsFile("pod-live")) == tt.wantFile + return exists(s.podDir("pod-gone")) == tt.wantOrphan && exists(s.runtimesFile("pod-live")) == tt.wantFile } ctx, cancel := context.WithCancel(context.Background()) @@ -576,7 +687,7 @@ func TestWorkerSandboxSyncerRun(t *testing.T) { time.Sleep(300 * time.Millisecond) if !matches() { t.Errorf("before every cache synced: pod-gone directory exists = %v, pod-live file exists = %v; want %v, %v", - exists(s.podDir("pod-gone")), exists(s.versionsFile("pod-live")), tt.wantOrphan, tt.wantFile) + exists(s.podDir("pod-gone")), exists(s.runtimesFile("pod-live")), tt.wantOrphan, tt.wantFile) } } @@ -654,6 +765,7 @@ func TestStartWorkerSandboxSyncer(t *testing.T) { podFactory.Core().V1().Pods(), ateFactory.Api().V1alpha1().WorkerPools(), ateFactory.Api().V1alpha1().SandboxConfigs(), + newScriptAssetFetcher(t), dir) if err != nil { t.Fatalf("startWorkerSandboxSyncer() error = %v", err) @@ -662,7 +774,7 @@ func TestStartWorkerSandboxSyncer(t *testing.T) { podFactory.Start(ctx.Done()) for _, pod := range tt.pods { - waitFor(t, "the file of "+string(pod.UID), func() bool { return exists(s.versionsFile(string(pod.UID))) }) + waitFor(t, "the file of "+string(pod.UID), func() bool { return exists(s.runtimesFile(string(pod.UID))) }) } if tt.change != nil { if err := tt.change(ctx, kube); err != nil { @@ -670,7 +782,7 @@ func TestStartWorkerSandboxSyncer(t *testing.T) { } } for _, uid := range tt.wantFiles { - waitFor(t, "the file of "+uid, func() bool { return exists(s.versionsFile(uid)) }) + waitFor(t, "the file of "+uid, func() bool { return exists(s.runtimesFile(uid)) }) } for _, uid := range tt.wantNoDirs { waitFor(t, "the directory of "+uid+" to be removed", func() bool { return !exists(s.podDir(uid)) }) diff --git a/cmd/atelet/worker_sandbox_versions.go b/cmd/atelet/worker_sandbox_versions.go index ad1358f17b..76c4c2d7fe 100644 --- a/cmd/atelet/worker_sandbox_versions.go +++ b/cmd/atelet/worker_sandbox_versions.go @@ -14,12 +14,14 @@ package main -// This file decides how one worker pod's sandbox versions file is built, +// This file decides how one worker pod's sandbox runtimes file is built, // in the steps syncPod runs: // - resolve: find the SandboxConfig of the pod's WorkerPool // - plan: pick the config's versions this node can run // - build: hash each version's assets and pause image digests into its -// sandbox_runtime_hash +// sandbox_runtime_hash, and probe what a snapshot taken on the runtime +// must find again: the host CPU identity for microVM, the runsc +// binary's CPU features for gVisor // - write: replace the file in the worker directory if its content changed // // worker_sandbox_syncer.go decides when each pod is synced, and creates and @@ -33,17 +35,19 @@ import ( "fmt" "log/slog" "os" + "os/exec" "path/filepath" "runtime" "slices" "strings" + "time" "github.com/google/go-containerregistry/pkg/name" corev1 "k8s.io/api/core/v1" "go.ate.dev/substrate/internal/hardware" "go.ate.dev/substrate/internal/nodepath" - "go.ate.dev/substrate/internal/sandboxversions" + "go.ate.dev/substrate/internal/sandboxruntimes" "go.ate.dev/substrate/pkg/api/v1alpha1" listersv1alpha1 "go.ate.dev/substrate/pkg/client/listers/api/v1alpha1" ) @@ -53,12 +57,31 @@ import ( // - the pod has no worker pool label // - the config's default version is missing, has no assets for this // node's architecture, or has a pause image not pinned by digest +// - the default version's runsc runs but fails, prints no features, or +// prints one this atelet does not know; the binary is content-addressed, +// so running it again gives the same answer +// +// The same failures on a restorable version leave it out of the file instead. var errPermanent = errors.New("permanent failure") -// sandboxPlan is what one worker's sandbox versions file is built from. +// errIncomplete marks a sync that wrote the worker's file with the default +// version and the restorable versions that resolved, while at least one +// restorable version had a transient failure. finish requeues the pod +// without the syncMaxRetries cap so the remaining restorable versions are +// added once the transient failure clears. +var errIncomplete = errors.New("incomplete sandbox runtimes") + +// errUnresolved marks a resolve failure caused by informer lag or a WorkerPool +// that has not resolved a SandboxConfig yet. Because the informer events that +// end those states also enqueue the pod again, finish caps errUnresolved +// retries at syncMaxRetries. +var errUnresolved = errors.New("unresolved sandbox config") + +// sandboxPlan is what one worker's sandbox runtimes file is built from. type sandboxPlan struct { - config string - versions []versionPlan // the default first + config string + sandboxClass v1alpha1.SandboxClass // the config's; decides which attributes its runtimes report + versions []versionPlan // the default first } // versionPlan is one SandboxConfig version projected onto this node's @@ -90,32 +113,32 @@ func poolConfigName(pool *v1alpha1.WorkerPool) string { return "" } -// resolve returns the sandbox class and SandboxConfig of a worker pod on this -// node. Apart from a pod in no pool, every failure can succeed once an -// informer catches up or the atecontroller resolves the pool. -func (r *workerSandboxResolver) resolve(pod *corev1.Pod) (v1alpha1.SandboxClass, *v1alpha1.SandboxConfig, error) { +// resolve returns the SandboxConfig of a worker pod on this node. Apart from +// a pod in no pool, every failure can succeed once an informer catches up or +// the atecontroller resolves the pool. +func (r *workerSandboxResolver) resolve(pod *corev1.Pod) (*v1alpha1.SandboxConfig, error) { poolName := pod.Labels[workerPoolLabel] if poolName == "" { - return "", nil, fmt.Errorf("%w: worker pod %s/%s has no %s label", errPermanent, pod.Namespace, pod.Name, workerPoolLabel) + return nil, fmt.Errorf("%w: worker pod %s/%s has no %s label", errPermanent, pod.Namespace, pod.Name, workerPoolLabel) } pool, err := r.workerPools.WorkerPools(pod.Namespace).Get(poolName) if err != nil { - return "", nil, fmt.Errorf("while getting WorkerPool %s/%s: %w", pod.Namespace, poolName, err) + return nil, fmt.Errorf("%w: while getting WorkerPool %s/%s: %w", errUnresolved, pod.Namespace, poolName, err) } configName := poolConfigName(pool) if configName == "" { - return "", nil, fmt.Errorf("WorkerPool %s/%s has not resolved a SandboxConfig for sandbox class %q", pod.Namespace, poolName, pool.Spec.DefaultSandboxClass()) + return nil, fmt.Errorf("%w: WorkerPool %s/%s has not resolved a SandboxConfig for sandbox class %q", errUnresolved, pod.Namespace, poolName, pool.Spec.DefaultSandboxClass()) } cfg, err := r.configs.Get(configName) if err != nil { - return "", nil, fmt.Errorf("while getting SandboxConfig %q: %w", configName, err) + return nil, fmt.Errorf("%w: while getting SandboxConfig %q: %w", errUnresolved, configName, err) } - return pool.Spec.DefaultSandboxClass(), cfg, nil + return cfg, nil } // plan resolves pod's SandboxConfig and the versions of it this node can run. func (r *workerSandboxResolver) plan(pod *corev1.Pod) (*sandboxPlan, error) { - _, cfg, err := r.resolve(pod) + cfg, err := r.resolve(pod) if err != nil { return nil, err } @@ -123,7 +146,7 @@ func (r *workerSandboxResolver) plan(pod *corev1.Pod) (*sandboxPlan, error) { if err != nil { return nil, err } - return &sandboxPlan{config: cfg.Name, versions: vers}, nil + return &sandboxPlan{config: cfg.Name, sandboxClass: cfg.Spec.SandboxClass, versions: vers}, nil } // planVersions returns the versions of cfg a worker on this node may use: the @@ -173,54 +196,207 @@ func recordForArch(cfg *v1alpha1.SandboxConfig, ver *v1alpha1.SandboxVersionConf } // buildFile returns the file content naming the runtimes of the versions in -// plan. The hashes come from the SandboxConfig spec alone, so it does no IO. -// A version whose runtime hash equals an earlier one's is left out, so the -// file lists each runtime once. +// plan. A version whose runtime hash equals an earlier one's is left out, so +// the file lists each runtime once. The default version's failure fails the +// sync, which leaves the existing file in place until a retry succeeds. A +// restorable version whose attributes cannot be built is left out; when the +// failure is transient the file is returned with an error wrapping +// errIncomplete, so the caller writes the runtimes that resolved right away +// and retries for the rest. // -// The file does not promise a runtime's assets are on the node. The -// prewarmer fetches each config's default version, and Run/Restore fetch any -// other version on demand. +// TODO: write the file as soon as the default version's attributes are known +// and add restorable runtimes on a later pass. On a fresh node the first +// worker cannot register until every restorable version's runsc has been +// fetched and probed, serially. // -// TODO: fetch the default version's assets and pause image through -// cmd/atelet/internal/filecache here, and remove sandbox_prewarm.go. Other -// versions stay fetched on demand at Restore. -func buildFile(ctx context.Context, plan *sandboxPlan) (*sandboxversions.File, error) { - out := &sandboxversions.File{} +// Listing a runtime does not promise its assets are on the node: probing +// fetches a gVisor version's runsc the first time this atelet sees it, but +// not its pause image, and nothing of a microVM version. The prewarmer +// fetches each config's default version, and Run/Restore fetch any other +// version on demand. +func (s *workerSandboxSyncer) buildFile(ctx context.Context, plan *sandboxPlan) (*sandboxruntimes.File, error) { + out := &sandboxruntimes.File{} seen := make(map[string]bool, len(plan.versions)) + var incomplete error for _, v := range plan.versions { h, err := versionRuntimeHash(v.rec) + if err == nil && seen[h] { + continue + } + var attrs sandboxruntimes.Attributes + if err == nil { + attrs, err = s.versionAttrs(ctx, plan.sandboxClass, v.rec, h) + } if err != nil { err = fmt.Errorf("SandboxConfig %q, version %q: %w", plan.config, v.name, err) if v.isDefault { - // No worker of the pool could start actors on this node. - return nil, fmt.Errorf("%w: %w", errPermanent, err) + // Without the default no worker of the pool could start + // actors on this node. + return nil, err + } + if errors.Is(err, errPermanent) { + slog.WarnContext(ctx, "Leaving a sandbox version out of the worker sandbox runtimes file", slog.Any("err", err)) + } else { + // Logged once by finish, with every version this names. + incomplete = errors.Join(incomplete, err) } - slog.WarnContext(ctx, "Leaving a sandbox version out of the worker sandbox versions file", slog.Any("err", err)) - continue - } - if seen[h] { continue } seen[h] = true - attrs := sandboxversions.Attributes{hardware.AttrSandboxRuntimeHash: h} if v.isDefault { out.DefaultRuntime = attrs } else { out.RestorableRuntimes = append(out.RestorableRuntimes, attrs) } } + if incomplete != nil { + return out, fmt.Errorf("%w: %w", errIncomplete, incomplete) + } return out, nil } +// versionAttrs returns the attributes of one version's runtime, whose +// sandbox_runtime_hash is hash: architecture and the hash for every class, +// the host CPU identity for a microVM runtime, and the runsc binary's +// cpu_features for a gVisor one. +func (s *workerSandboxSyncer) versionAttrs(ctx context.Context, class v1alpha1.SandboxClass, rec *sandboxAssetsRecord, hash string) (sandboxruntimes.Attributes, error) { + attrs := sandboxruntimes.Attributes{ + hardware.AttrArchitecture: runtime.GOARCH, + hardware.AttrSandboxRuntimeHash: hash, + } + switch class { + case v1alpha1.SandboxClassMicroVM: + vendor, model, err := s.hostCPUIdentity() + if err != nil { + return nil, fmt.Errorf("while probing host CPU identity: %w", err) + } + attrs[hardware.AttrCPUVendor], attrs[hardware.AttrCPUModel] = vendor, model + case v1alpha1.SandboxClassGvisor: + mask, err := s.gvisorCPUFeatures(ctx, rec) + if err != nil { + return nil, err + } + attrs[hardware.AttrCPUFeatures] = mask + default: + return nil, fmt.Errorf("%w: unsupported sandbox class %q", errPermanent, class) + } + return attrs, nil +} + +// cpuIdentity is the host CPU identity microVM runtimes report. +type cpuIdentity struct { + vendor, model string +} + +// hostCPUIdentity returns the host CPU's vendor and model, probing them the +// first time. A failed probe is tried again on the next sync. +func (s *workerSandboxSyncer) hostCPUIdentity() (vendor, model string, err error) { + s.mu.Lock() + defer s.mu.Unlock() + if s.hostCPU == nil { + if vendor, model, err = s.probeCPUIdentity(); err != nil { + return "", "", err + } + s.hostCPU = &cpuIdentity{vendor: vendor, model: model} + } + return s.hostCPU.vendor, s.hostCPU.model, nil +} + +// gvisorCPUFeatures returns the encoded cpu_features of rec's runsc binary. +// The first time this atelet sees the binary's digest it fetches the binary +// into the local asset cache and runs ` cpu-features`. +func (s *workerSandboxSyncer) gvisorCPUFeatures(ctx context.Context, rec *sandboxAssetsRecord) (string, error) { + asset, ok := runscAssetOf(rec) + if !ok { + return "", fmt.Errorf("%w: gvisor version has no %q or %q asset", errPermanent, gvisorAssetName, runscAssetName) + } + digest := rec.Assets[asset].SHA256 + s.mu.Lock() + mask, ok := s.cpuFeatures[digest] + s.mu.Unlock() + if ok { + return mask, nil + } + // A download that hangs would hold a sync worker; prewarmTimeout bounds + // the fetch for the same reason it bounds prewarming. + ctx, cancel := context.WithTimeout(ctx, prewarmTimeout) + defer cancel() + paths, err := s.assets.ensureSandboxAssets(ctx, rec) + if err != nil { + return "", err + } + names, err := s.runCPUFeatures(ctx, paths[asset]) + if err != nil { + return "", err + } + mask, err = hardware.EncodeCPUFeatures(runtime.GOARCH, names) + if err != nil { + return "", fmt.Errorf("%w: %w", errPermanent, err) + } + s.mu.Lock() + s.cpuFeatures[digest] = mask + s.mu.Unlock() + return mask, nil +} + +// runscAssetOf returns the name of the asset in rec that carries runsc, the +// most preferred of runscAssetNames that is present with a digest. +func runscAssetOf(rec *sandboxAssetsRecord) (string, bool) { + for _, name := range runscAssetNames { + if a, ok := rec.Assets[name]; ok && a.SHA256 != "" { + return name, true + } + } + return "", false +} + +// cpuFeaturesTimeout bounds one `runsc cpu-features` run, which takes +// milliseconds. A runsc that hangs would hold a sync worker. A var so tests +// can shorten it. +var cpuFeaturesTimeout = 30 * time.Second + +// runRunscCPUFeatures executes ` cpu-features` and returns the +// comma-separated feature names it prints. The binary is content-addressed, +// so one that runs and fails, or prints nothing, would do so again and fails +// permanently; one that cannot be started, or is cut off by ctx, is retried. +func runRunscCPUFeatures(ctx context.Context, runscPath string) ([]string, error) { + ctx, cancel := context.WithTimeout(ctx, cpuFeaturesTimeout) + defer cancel() + cmd := exec.CommandContext(ctx, runscPath, "cpu-features") + cmd.Env = []string{} // the probe needs nothing from atelet's environment + out, err := cmd.Output() + var exitErr *exec.ExitError + switch { + case err == nil: + case ctx.Err() != nil: + // Before the ExitError case: a process killed at the deadline exits + // with one too. + return nil, fmt.Errorf("while running %s cpu-features: %w", runscPath, ctx.Err()) + case errors.As(err, &exitErr): + if stderr := strings.TrimSpace(string(exitErr.Stderr)); stderr != "" { + return nil, fmt.Errorf("%w: %s cpu-features: %w (%s)", errPermanent, runscPath, err, stderr) + } + return nil, fmt.Errorf("%w: %s cpu-features: %w", errPermanent, runscPath, err) + default: + return nil, fmt.Errorf("while starting %s cpu-features: %w", runscPath, err) + } + printed := strings.TrimSpace(string(out)) + if printed == "" { + return nil, fmt.Errorf("%w: %s cpu-features printed no features", errPermanent, runscPath) + } + return strings.Split(printed, ","), nil +} + // versionRuntimeHash returns the runtime hash of one version. The CRD // requires every pause image to be pinned by digest, which is the digest the -// image cache resolves the ref to, so the hash needs no pull. +// image cache resolves the ref to, so the hash needs no pull. An unpinned one +// is errPermanent: only a spec change fixes it. func versionRuntimeHash(rec *sandboxAssetsRecord) (string, error) { var pauseDigest string if rec.PauseImage != "" { ref, err := name.NewDigest(rec.PauseImage) if err != nil { - return "", fmt.Errorf("pause image %q is not pinned by digest: %w", rec.PauseImage, err) + return "", fmt.Errorf("%w: pause image %q is not pinned by digest: %w", errPermanent, rec.PauseImage, err) } pauseDigest = ref.DigestStr() } @@ -243,32 +419,38 @@ func runtimeHash(rec *sandboxAssetsRecord, pauseDigest string) string { return "sha256:" + hex.EncodeToString(sum[:]) } -// syncPod writes pod's sandbox versions file: +// syncPod writes pod's sandbox runtimes file: // - plan: resolve the pool's config and its versions for this node -// - build: hash each version's runtime +// - build: hash and probe each version's runtime // - write: replace the file if its content changed +// +// A file missing restorable runtimes for a transient reason is written all +// the same, and the errIncomplete from build is returned so finish retries. func (s *workerSandboxSyncer) syncPod(ctx context.Context, pod *corev1.Pod) error { plan, err := s.resolver.plan(pod) if err != nil { return err } - want, err := buildFile(ctx, plan) - if err != nil { + want, buildErr := s.buildFile(ctx, plan) + if buildErr != nil && !errors.Is(buildErr, errIncomplete) { + return buildErr + } + if err := s.write(ctx, string(pod.UID), plan.config, want); err != nil { return err } - return s.write(ctx, string(pod.UID), plan.config, want) + return buildErr } -func (s *workerSandboxSyncer) write(ctx context.Context, uid, config string, want *sandboxversions.File) error { +func (s *workerSandboxSyncer) write(ctx context.Context, uid, config string, want *sandboxruntimes.File) error { if err := os.MkdirAll(s.podDir(uid), 0o700); err != nil { return fmt.Errorf("while creating worker directory: %w", err) } - wrote, err := sandboxversions.Write(s.versionsFile(uid), want) + wrote, err := sandboxruntimes.Write(s.runtimesFile(uid), want) if err != nil { return err } if wrote { - slog.InfoContext(ctx, "Wrote worker sandbox versions", + slog.InfoContext(ctx, "Wrote worker sandbox runtimes", slog.String("pod_uid", uid), slog.String("sandbox_config", config), slog.String("default_runtime_hash", want.DefaultRuntime[hardware.AttrSandboxRuntimeHash]), @@ -277,6 +459,6 @@ func (s *workerSandboxSyncer) write(ctx context.Context, uid, config string, wan return nil } -func (s *workerSandboxSyncer) versionsFile(uid string) string { - return filepath.Join(s.podDir(uid), nodepath.SandboxVersionsFileName) +func (s *workerSandboxSyncer) runtimesFile(uid string) string { + return filepath.Join(s.podDir(uid), nodepath.SandboxRuntimesFileName) } diff --git a/cmd/atelet/worker_sandbox_versions_test.go b/cmd/atelet/worker_sandbox_versions_test.go index e1f0fc7eb8..ad593f8858 100644 --- a/cmd/atelet/worker_sandbox_versions_test.go +++ b/cmd/atelet/worker_sandbox_versions_test.go @@ -19,8 +19,12 @@ import ( "crypto/sha256" "errors" "fmt" + "os" + "path/filepath" "runtime" + "strings" "testing" + "time" "github.com/google/go-cmp/cmp" corev1 "k8s.io/api/core/v1" @@ -28,7 +32,7 @@ import ( "k8s.io/utils/ptr" "go.ate.dev/substrate/internal/hardware" - "go.ate.dev/substrate/internal/sandboxversions" + "go.ate.dev/substrate/internal/sandboxruntimes" "go.ate.dev/substrate/pkg/api/v1alpha1" ) @@ -190,12 +194,16 @@ func TestWorkerSandboxResolverResolve(t *testing.T) { cfg := gvisorConfig("gvisor-default", "gs://bucket/runsc", "abc") tests := []struct { - name string - objs []any - pod *corev1.Pod - wantConfig string - wantErr bool - wantPermanent bool + name string + objs []any + pod *corev1.Pod + wantConfig string + wantErr bool + // wantPermanent and wantUnresolved are the error's class, which + // decides whether finish drops it, caps its retries, or retries it + // without a cap. + wantPermanent bool + wantUnresolved bool }{{ name: "the config the pool resolved for the pool's class", objs: []any{resolvedPool("pool", "gvisor-default"), cfg}, @@ -208,15 +216,17 @@ func TestWorkerSandboxResolverResolve(t *testing.T) { wantErr: true, wantPermanent: true, }, { - name: "pool not found", - objs: []any{cfg}, - pod: workerPod("pod-a", "pool"), - wantErr: true, + name: "pool not found", + objs: []any{cfg}, + pod: workerPod("pod-a", "pool"), + wantErr: true, + wantUnresolved: true, }, { - name: "pool has not resolved a config", - objs: []any{resolvedPool("pool", ""), cfg}, - pod: workerPod("pod-a", "pool"), - wantErr: true, + name: "pool has not resolved a config", + objs: []any{resolvedPool("pool", ""), cfg}, + pod: workerPod("pod-a", "pool"), + wantErr: true, + wantUnresolved: true, }, { // A pending config is one the atecontroller has claimed but not yet // confirmed exists and is not being deleted. @@ -226,8 +236,9 @@ func TestWorkerSandboxResolverResolve(t *testing.T) { pool.Status.SandboxClasses[0].PendingConfigRef = &v1alpha1.SandboxConfigReference{Name: "gvisor-default"} return pool }(), cfg}, - pod: workerPod("pod-a", "pool"), - wantErr: true, + pod: workerPod("pod-a", "pool"), + wantErr: true, + wantUnresolved: true, }, { // The class comes from the pool's spec, not from the status entry. name: "status resolved another class than the pool's", @@ -236,29 +247,34 @@ func TestWorkerSandboxResolverResolve(t *testing.T) { pool.Spec.SandboxClasses[0].Name = v1alpha1.SandboxClassMicroVM return pool }(), cfg}, - pod: workerPod("pod-a", "pool"), - wantErr: true, + pod: workerPod("pod-a", "pool"), + wantErr: true, + wantUnresolved: true, }, { - name: "config not found", - objs: []any{resolvedPool("pool", "gvisor-missing"), cfg}, - pod: workerPod("pod-a", "pool"), - wantErr: true, + name: "config not found", + objs: []any{resolvedPool("pool", "gvisor-missing"), cfg}, + pod: workerPod("pod-a", "pool"), + wantErr: true, + wantUnresolved: true, }} for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { s := newTestSyncer(t, tt.objs...) - class, got, err := s.resolver.resolve(tt.pod) + got, err := s.resolver.resolve(tt.pod) if (err != nil) != tt.wantErr { t.Fatalf("resolve() error = %v, want error %v", err, tt.wantErr) } if errors.Is(err, errPermanent) != tt.wantPermanent { t.Errorf("resolve() error = %v, want permanent %v", err, tt.wantPermanent) } + if errors.Is(err, errUnresolved) != tt.wantUnresolved { + t.Errorf("resolve() error = %v, want unresolved %v", err, tt.wantUnresolved) + } if err != nil { return } - if got.Name != tt.wantConfig || class != v1alpha1.SandboxClassGvisor { - t.Errorf("resolve() = %q, %q, want %q, %q", class, got.Name, v1alpha1.SandboxClassGvisor, tt.wantConfig) + if got.Name != tt.wantConfig { + t.Errorf("resolve() = %q, want %q", got.Name, tt.wantConfig) } }) } @@ -296,6 +312,9 @@ func TestVersionRuntimeHash(t *testing.T) { if (err != nil) != tt.wantErr { t.Fatalf("versionRuntimeHash() error = %v, want error %v", err, tt.wantErr) } + if err != nil && !errors.Is(err, errPermanent) { + t.Errorf("versionRuntimeHash() error = %v, want errPermanent", err) + } if got != tt.want { t.Errorf("versionRuntimeHash() = %q, want %q", got, tt.want) } @@ -304,34 +323,42 @@ func TestVersionRuntimeHash(t *testing.T) { } // wantRuntime is the runtime buildFile builds from multiVersionConfig's -// version with the given name. -func wantRuntime(name string) sandboxversions.Attributes { +// version with the given name when runsc reports testCPUFeatures() ("3"). +func wantRuntime(name string) sandboxruntimes.Attributes { + return wantRuntimeWithFeatures(name, "3") +} + +func wantRuntimeWithFeatures(name, cpuFeatures string) sandboxruntimes.Attributes { rec := &sandboxAssetsRecord{Assets: map[string]assetEntry{gvisorAssetName: {SHA256: "sha-" + name}}} - return sandboxversions.Attributes{hardware.AttrSandboxRuntimeHash: runtimeHash(rec, pauseDigest(name))} + return sandboxruntimes.Attributes{ + hardware.AttrArchitecture: runtime.GOARCH, + hardware.AttrCPUFeatures: cpuFeatures, + hardware.AttrSandboxRuntimeHash: runtimeHash(rec, pauseDigest(name)), + } } // wantVersions is the file content for multiVersionConfig. -func wantVersions() *sandboxversions.File { - return &sandboxversions.File{ +func wantVersions() *sandboxruntimes.File { + return &sandboxruntimes.File{ DefaultRuntime: wantRuntime("v2"), - RestorableRuntimes: []sandboxversions.Attributes{wantRuntime("v1")}, + RestorableRuntimes: []sandboxruntimes.Attributes{wantRuntime("v1")}, } } // planAndBuild runs the plan and build steps of syncPod for pod. -func planAndBuild(s *workerSandboxSyncer, pod *corev1.Pod) (*sandboxversions.File, error) { +func planAndBuild(s *workerSandboxSyncer, pod *corev1.Pod) (*sandboxruntimes.File, error) { plan, err := s.resolver.plan(pod) if err != nil { return nil, err } - return buildFile(context.Background(), plan) + return s.buildFile(context.Background(), plan) } func TestPlanAndBuild(t *testing.T) { defaultNoArch := multiVersionConfig() defaultNoArch.Spec.Versions[3].Assets = map[string]map[string]v1alpha1.AssetFile{"other-arch": defaultNoArch.Spec.Versions[3].Assets[runtime.GOARCH]} - onlyDefault := &sandboxversions.File{DefaultRuntime: wantRuntime("v2")} + onlyDefault := &sandboxruntimes.File{DefaultRuntime: wantRuntime("v2")} // sameAsDefault has v1 run the default version's binaries. sameAsDefault := multiVersionConfig() @@ -351,10 +378,18 @@ func TestPlanAndBuild(t *testing.T) { return cfg } + unsupportedClass := multiVersionConfig() + unsupportedClass.Spec.SandboxClass = "unknown" + + noRunscAsset := multiVersionConfig() + noRunscAsset.Spec.Versions[3].Assets = map[string]map[string]v1alpha1.AssetFile{ + runtime.GOARCH: {"other": {URL: "gs://bucket/other", SHA256: "sha-other"}}, + } + tests := []struct { name string cfg *v1alpha1.SandboxConfig - want *sandboxversions.File + want *sandboxruntimes.File wantErr bool wantPermanent bool }{{ @@ -387,6 +422,16 @@ func TestPlanAndBuild(t *testing.T) { name: "non-default version pause image not pinned by digest is left out", cfg: tagged(1), want: onlyDefault, + }, { + name: "unsupported sandbox class", + cfg: unsupportedClass, + wantErr: true, + wantPermanent: true, + }, { + name: "gvisor version has no gvisor or runsc asset", + cfg: noRunscAsset, + wantErr: true, + wantPermanent: true, }, { name: "surfaces an unresolved config", wantErr: true, @@ -412,3 +457,288 @@ func TestPlanAndBuild(t *testing.T) { }) } } + +func TestPlanAndBuildMicroVM(t *testing.T) { + pool := resolvedPoolOf(v1alpha1.SandboxClassMicroVM, "microvm-pool", "microvm-release") + version := func(name, sha string) v1alpha1.SandboxVersionConfig { + return v1alpha1.SandboxVersionConfig{ + Name: name, + Assets: map[string]map[string]v1alpha1.AssetFile{ + runtime.GOARCH: {"kernel": {URL: "gs://bucket/kernel-" + name, SHA256: sha}}, + }, + } + } + cfg := &v1alpha1.SandboxConfig{ + ObjectMeta: metav1.ObjectMeta{Name: "microvm-release"}, + Spec: v1alpha1.SandboxConfigSpec{ + SandboxClass: v1alpha1.SandboxClassMicroVM, + DefaultVersion: "v2", + Versions: []v1alpha1.SandboxVersionConfig{ + version("v1", "sha-v1"), + version("v2", "sha-v2"), + }, + }, + } + wantMicroVM := func(sha string) sandboxruntimes.Attributes { + rec := &sandboxAssetsRecord{Assets: map[string]assetEntry{"kernel": {SHA256: sha}}} + return sandboxruntimes.Attributes{ + hardware.AttrArchitecture: runtime.GOARCH, + hardware.AttrCPUVendor: testCPUVendor, + hardware.AttrCPUModel: testCPUModel, + hardware.AttrSandboxRuntimeHash: runtimeHash(rec, ""), + } + } + + s := newTestSyncer(t, pool, cfg) + s.assets = &fakeAssetFetcher{ + fn: func(context.Context, *sandboxAssetsRecord) (map[string]string, error) { + t.Fatal("microVM buildFile must not fetch sandbox assets") + return nil, nil + }, + } + probeCalls := 0 + probeErr := errors.New("broken /proc/cpuinfo") + s.probeCPUIdentity = func() (string, string, error) { + probeCalls++ + if probeErr != nil { + return "", "", probeErr + } + return testCPUVendor, testCPUModel, nil + } + + // A failed probe is not cached: the sync fails with a retryable error and + // the next one probes again. + if _, err := planAndBuild(s, workerPod("pod-a", "microvm-pool")); !errors.Is(err, probeErr) || errors.Is(err, errPermanent) { + t.Fatalf("planAndBuild() on probe failure = %v, want %v and not permanent", err, probeErr) + } + + // Once a probe succeeds, every later sync reuses it. + probeErr = nil + want := &sandboxruntimes.File{ + DefaultRuntime: wantMicroVM("sha-v2"), + RestorableRuntimes: []sandboxruntimes.Attributes{wantMicroVM("sha-v1")}, + } + for _, uid := range []string{"pod-a", "pod-b"} { + got, err := planAndBuild(s, workerPod(uid, "microvm-pool")) + if err != nil { + t.Fatalf("planAndBuild(%s) error = %v", uid, err) + } + if diff := cmp.Diff(want, got); diff != "" { + t.Errorf("planAndBuild(%s) mismatch (-want +got):\n%s", uid, diff) + } + } + if probeCalls != 2 { + t.Errorf("probe calls = %d, want 2: one that failed, one that succeeded and was reused", probeCalls) + } +} + +func TestGVisorCPUFeaturesMemoizationAndErrors(t *testing.T) { + feats := testCPUFeatures() + + t.Run("a version with an earlier version's binaries in another bucket is neither fetched nor probed", func(t *testing.T) { + // runtimeHash leaves URLs out, so v3 is v1's runtime. The syncer must + // see that from the spec alone, before touching v3's bucket. + cfg := multiVersionConfig() + v3 := *cfg.Spec.Versions[1].DeepCopy() + v3.Name = "v3" + v3.Assets[runtime.GOARCH][gvisorAssetName] = v1alpha1.AssetFile{URL: "gs://other-bucket/v1", SHA256: "sha-v1"} + cfg.Spec.Versions = append(cfg.Spec.Versions, v3) + + s := newTestSyncer(t, resolvedPool("pool", "gvisor-release"), cfg) + s.assets = &fakeAssetFetcher{ + fn: func(ctx context.Context, rec *sandboxAssetsRecord) (map[string]string, error) { + if url := rec.Assets[gvisorAssetName].URL; strings.HasPrefix(url, "gs://other-bucket/") { + return nil, fmt.Errorf("fetched a duplicate runtime from %s", url) + } + return newTestAssetFetcher().ensureSandboxAssets(ctx, rec) + }, + } + got, err := planAndBuild(s, workerPod("pod-a", "pool")) + if err != nil { + t.Fatalf("planAndBuild() error = %v", err) + } + if diff := cmp.Diff(wantVersions(), got); diff != "" { + t.Errorf("planAndBuild() mismatch (-want +got):\n%s", diff) + } + }) + + t.Run("memoizes runsc cpu-features by asset digest across versions and syncs", func(t *testing.T) { + cfg := multiVersionConfig() + // Add a third version "v3" that shares v1's gvisor asset digest ("sha-v1") + // but has its own pause image digest, so it has a distinct runtime hash + // while reusing the same runsc binary. + v3 := *cfg.Spec.Versions[1].DeepCopy() + v3.Name = "v3" + v3.PauseImage = "registry.k8s.io/pause@" + pauseDigest("v3") + cfg.Spec.Versions = append(cfg.Spec.Versions, v3) + + s := newTestSyncer(t, resolvedPool("pool", "gvisor-release"), cfg) + s.assets = &fakeAssetFetcher{ + fn: func(_ context.Context, rec *sandboxAssetsRecord) (map[string]string, error) { + return map[string]string{gvisorAssetName: "/cache/" + rec.Assets[gvisorAssetName].SHA256 + "/runsc"}, nil + }, + } + execCalls := make(map[string]int) + s.runCPUFeatures = func(_ context.Context, runscPath string) ([]string, error) { + execCalls[runscPath]++ + if runscPath == "/cache/sha-v2/runsc" { + return feats, nil // bits 0 and 1 -> "3" + } + return feats[:1], nil // bit 0 only -> "1" + } + + got, err := planAndBuild(s, workerPod("pod-a", "pool")) + if err != nil { + t.Fatalf("first planAndBuild() error = %v", err) + } + v3Rec := &sandboxAssetsRecord{Assets: map[string]assetEntry{gvisorAssetName: {SHA256: "sha-v1"}}} + want := &sandboxruntimes.File{ + DefaultRuntime: wantRuntimeWithFeatures("v2", "3"), + RestorableRuntimes: []sandboxruntimes.Attributes{ + wantRuntimeWithFeatures("v1", "1"), + { + hardware.AttrArchitecture: runtime.GOARCH, + hardware.AttrCPUFeatures: "1", + hardware.AttrSandboxRuntimeHash: runtimeHash(v3Rec, pauseDigest("v3")), + }, + }, + } + if diff := cmp.Diff(want, got); diff != "" { + t.Errorf("planAndBuild() mismatch (-want +got):\n%s", diff) + } + + // Syncing again must answer from the memo alone: no fetch, no runsc. + s.assets = &fakeAssetFetcher{ + fn: func(context.Context, *sandboxAssetsRecord) (map[string]string, error) { + return nil, errors.New("fetched a runsc whose cpu_features are memoized") + }, + } + if _, err := planAndBuild(s, workerPod("pod-b", "pool")); err != nil { + t.Fatalf("second planAndBuild() error = %v", err) + } + wantCalls := map[string]int{ + "/cache/sha-v2/runsc": 1, + "/cache/sha-v1/runsc": 1, + } + if diff := cmp.Diff(wantCalls, execCalls); diff != "" { + t.Errorf("runCPUFeatures call counts mismatch (-want +got):\n%s", diff) + } + }) + + t.Run("transient asset fetch or runsc execution error is retriable", func(t *testing.T) { + s := newTestSyncer(t, resolvedPool("pool", "gvisor-release"), multiVersionConfig()) + s.assets = &fakeAssetFetcher{ + fn: func(context.Context, *sandboxAssetsRecord) (map[string]string, error) { + return nil, errors.New("temporary download failure") + }, + } + if _, err := planAndBuild(s, workerPod("pod-a", "pool")); err == nil || errors.Is(err, errPermanent) { + t.Errorf("planAndBuild() on asset fetch error = %v, want non-permanent error", err) + } + + s = newTestSyncer(t, resolvedPool("pool", "gvisor-release"), multiVersionConfig()) + s.runCPUFeatures = func(context.Context, string) ([]string, error) { + return nil, errors.New("runsc failed") + } + if _, err := planAndBuild(s, workerPod("pod-a", "pool")); err == nil || errors.Is(err, errPermanent) { + t.Errorf("planAndBuild() on runCPUFeatures error = %v, want non-permanent error", err) + } + + // On a restorable version alone, buildFile returns the default runtime + // alongside errIncomplete so syncPod writes the partial file right away + // and requeues for the rest. + s = newTestSyncer(t, resolvedPool("pool", "gvisor-release"), multiVersionConfig()) + s.assets = &fakeAssetFetcher{ + fn: func(_ context.Context, rec *sandboxAssetsRecord) (map[string]string, error) { + if sha := rec.Assets[gvisorAssetName].SHA256; sha != "sha-v2" { + return nil, fmt.Errorf("temporary download failure for %s", sha) + } + return map[string]string{gvisorAssetName: "sha-v2"}, nil + }, + } + s.runCPUFeatures = func(context.Context, string) ([]string, error) { return feats, nil } + got, err := planAndBuild(s, workerPod("pod-a", "pool")) + if !errors.Is(err, errIncomplete) || errors.Is(err, errPermanent) { + t.Errorf("planAndBuild() on a restorable version's fetch error = %v, want errIncomplete and not errPermanent", err) + } + want := &sandboxruntimes.File{DefaultRuntime: wantRuntime("v2")} + if diff := cmp.Diff(want, got); diff != "" { + t.Errorf("planAndBuild() partial file mismatch (-want +got):\n%s", diff) + } + }) + + t.Run("unknown CPU feature on default version is permanent, on non-default is skipped", func(t *testing.T) { + s := newTestSyncer(t, resolvedPool("pool", "gvisor-release"), multiVersionConfig()) + s.runCPUFeatures = func(context.Context, string) ([]string, error) { + return []string{"not_a_known_feature"}, nil + } + if _, err := planAndBuild(s, workerPod("pod-a", "pool")); !errors.Is(err, errPermanent) { + t.Errorf("planAndBuild() with unknown feature on default = %v, want errPermanent", err) + } + + s = newTestSyncer(t, resolvedPool("pool", "gvisor-release"), multiVersionConfig()) + s.assets = &fakeAssetFetcher{ + fn: func(_ context.Context, rec *sandboxAssetsRecord) (map[string]string, error) { + return map[string]string{gvisorAssetName: rec.Assets[gvisorAssetName].SHA256}, nil + }, + } + s.runCPUFeatures = func(_ context.Context, runscPath string) ([]string, error) { + if runscPath == "sha-v2" { + return feats, nil + } + return []string{"future_unknown_feature"}, nil + } + got, err := planAndBuild(s, workerPod("pod-a", "pool")) + if err != nil { + t.Fatalf("planAndBuild() with unknown feature on non-default error = %v", err) + } + want := &sandboxruntimes.File{DefaultRuntime: wantRuntime("v2")} + if diff := cmp.Diff(want, got); diff != "" { + t.Errorf("planAndBuild() mismatch (-want +got):\n%s", diff) + } + }) +} + +func TestRunRunscCPUFeatures(t *testing.T) { + ctx := context.Background() + dir := t.TempDir() + script := func(name, body string) string { + t.Helper() + p := filepath.Join(dir, name) + if err := os.WriteFile(p, []byte("#!/bin/sh\n"+body), 0o755); err != nil { + t.Fatal(err) + } + return p + } + + got, err := runRunscCPUFeatures(ctx, script("ok", "echo pni,avx2\n")) + if err != nil { + t.Fatalf("runRunscCPUFeatures() error = %v", err) + } + if diff := cmp.Diff([]string{"pni", "avx2"}, got); diff != "" { + t.Errorf("runRunscCPUFeatures() mismatch (-want +got):\n%s", diff) + } + + // A binary that runs and fails, or prints nothing, will do so again: + // permanent, with its stderr in the error. + _, err = runRunscCPUFeatures(ctx, script("fail", "echo boom >&2\nexit 1\n")) + if !errors.Is(err, errPermanent) || !strings.Contains(err.Error(), "boom") { + t.Errorf("runRunscCPUFeatures() on a failing binary = %v, want permanent error quoting boom", err) + } + if _, err := runRunscCPUFeatures(ctx, script("empty", "exit 0\n")); !errors.Is(err, errPermanent) { + t.Errorf("runRunscCPUFeatures() on empty output = %v, want permanent error", err) + } + + // A binary that cannot be started, or is cut off, is retried. + if _, err := runRunscCPUFeatures(ctx, filepath.Join(dir, "missing")); err == nil || errors.Is(err, errPermanent) { + t.Errorf("runRunscCPUFeatures() on a missing binary = %v, want non-permanent error", err) + } + origTimeout := cpuFeaturesTimeout + cpuFeaturesTimeout = 50 * time.Millisecond + t.Cleanup(func() { cpuFeaturesTimeout = origTimeout }) + // exec, so the kill reaches the process holding stdout. + _, err = runRunscCPUFeatures(ctx, script("hang", "exec sleep 10\n")) + if !errors.Is(err, context.DeadlineExceeded) || errors.Is(err, errPermanent) { + t.Errorf("runRunscCPUFeatures() on a hanging binary = %v, want context.DeadlineExceeded", err) + } +} diff --git a/cmd/atelet/workersupport.go b/cmd/atelet/workersupport.go index 0aee16144d..e899ca52e6 100644 --- a/cmd/atelet/workersupport.go +++ b/cmd/atelet/workersupport.go @@ -106,10 +106,10 @@ func verifyClientOnSameNode(node *substratex509.PodIdentity) func(tls.Connection // worker says its capacity and sandbox runtimes are, in one // WorkerService.RegisterWorker call so capacity and runtimes land atomically. // -// It returns the control plane's error unwrapped so the caller retries: a -// worker reports once, so an accepted call is the only thing that puts -// capacity and runtimes on the Worker, and a Worker record the syncer has not -// created yet is the ordinary reason for a first attempt to fail. +// It returns the control plane's error unwrapped so the caller retries: an +// accepted call is the only thing that puts capacity and the current runtimes +// on the Worker, and a Worker record the syncer has not created yet is the +// ordinary reason for a first attempt to fail. func (s *workerSupportServer) RegisterWorker(ctx context.Context, req *ateletpb.RegisterWorkerRequest) (*ateletpb.RegisterWorkerResponse, error) { // Identity comes only from the mTLS certificate, never from the request: // a worker can report its own capacity and runtimes and no one else's. diff --git a/cmd/ateworker-gvisor/flags.go b/cmd/ateworker-gvisor/flags.go index 58073f577e..3c673e9e89 100644 --- a/cmd/ateworker-gvisor/flags.go +++ b/cmd/ateworker-gvisor/flags.go @@ -26,9 +26,10 @@ import ( "log/slog" "os" "path/filepath" + "reflect" "sync" - "github.com/fsnotify/fsnotify" + "go.ate.dev/substrate/internal/ateworker/register" ) // commandFlags holds per-command flags for a single command. @@ -126,24 +127,12 @@ func newFlagStore(ctx context.Context, path string) (*flagStore, error) { return nil, err } - watcher, err := fsnotify.NewWatcher() - if err != nil { - return nil, fmt.Errorf("while creating fsnotify watcher for %q: %w", cleanPath, err) - } - - // Watch the parent directory so both in-place writes and atomic file - // replacements (renames) are detected. - dir := filepath.Dir(cleanPath) - if err := watcher.Add(dir); err != nil { - _ = watcher.Close() - return nil, fmt.Errorf("while watching directory %q for %q: %w", dir, cleanPath, err) - } - + changes := register.WatchFilePath(ctx, cleanPath) store := &flagStore{ path: cleanPath, cfg: cfg, } - go store.watchLoop(ctx, watcher) + go store.watchLoop(ctx, changes) return store, nil } @@ -155,41 +144,20 @@ func (s *flagStore) get() *flagConfig { return cfg } -func (s *flagStore) watchLoop(ctx context.Context, watcher *fsnotify.Watcher) { - defer func() { - if err := watcher.Close(); err != nil { - slog.Warn("Failed to close flags config watcher", slog.String("path", s.path), slog.Any("err", err)) - } - }() +func (s *flagStore) watchLoop(ctx context.Context, changes <-chan struct{}) { for { select { case <-ctx.Done(): return - case event, ok := <-watcher.Events: + case _, ok := <-changes: if !ok { return } - if s.isConfigEvent(event) { - s.reload() - } - case err, ok := <-watcher.Errors: - if !ok { - return - } - slog.Error("Error watching flags configuration file", - slog.String("path", s.path), - slog.Any("err", err)) + s.reload() } } } -func (s *flagStore) isConfigEvent(event fsnotify.Event) bool { - if filepath.Clean(event.Name) != s.path { - return false - } - return event.Has(fsnotify.Write) || event.Has(fsnotify.Create) -} - func (s *flagStore) reload() { defer func() { if s.onReload != nil { @@ -206,7 +174,12 @@ func (s *flagStore) reload() { } s.mu.Lock() - s.cfg = cfg + unchanged := reflect.DeepEqual(s.cfg, cfg) + if !unchanged { + s.cfg = cfg + } s.mu.Unlock() - slog.Info("Reloaded flags configuration", slog.String("path", s.path)) + if !unchanged { + slog.Info("Reloaded flags configuration", slog.String("path", s.path)) + } } diff --git a/cmd/ateworker-gvisor/flags_test.go b/cmd/ateworker-gvisor/flags_test.go index 0d291d90eb..a794ba2c57 100644 --- a/cmd/ateworker-gvisor/flags_test.go +++ b/cmd/ateworker-gvisor/flags_test.go @@ -214,4 +214,17 @@ func TestFlagStoreWatchAndReload(t *testing.T) { if !reflect.DeepEqual(gotAfterBad, wantUpdated) { t.Fatalf("after invalid reload buildArgs(create) = %v, want previous valid config %v", gotAfterBad, wantUpdated) } + + // Reloading when the file content is unchanged (e.g. on a polling tick) + // must be a no-op and keep the existing *flagConfig pointer. + if err := os.WriteFile(cfgPath, []byte(updatedJSON), 0o600); err != nil { + t.Fatalf("failed to rewrite config: %v", err) + } + waitForReload(t, reloadCh) + beforePtr := store.get() + store.reload() + waitForReload(t, reloadCh) + if afterPtr := store.get(); afterPtr != beforePtr { + t.Errorf("reload() with unchanged content replaced *flagConfig (%p -> %p), want no-op", beforePtr, afterPtr) + } } diff --git a/cmd/ateworker-gvisor/main.go b/cmd/ateworker-gvisor/main.go index 968f8cd665..31d0ab15c8 100644 --- a/cmd/ateworker-gvisor/main.go +++ b/cmd/ateworker-gvisor/main.go @@ -194,7 +194,7 @@ func do(ctx context.Context) error { if err := resources.ValidateWorkerPodUID(*podUID); err != nil { return fmt.Errorf("in resources.ValidateWorkerPodUID: %w", err) } - // atelet owns the directory and the sandbox versions file in it; only the + // atelet owns the directory and the sandbox runtimes file in it; only the // socket below is this process's. Both outlive a container restart. if err := os.MkdirAll(ateworkerDir, 0o700); err != nil { return fmt.Errorf("in os.MkdirAll(%q): %w", ateworkerDir, err) @@ -272,11 +272,12 @@ func do(ctx context.Context) error { svr.GracefulStop() }() - // Report what this worker can supply. Nothing else tells the control plane, - // which places no Actor here until it lands, so a worker that cannot report - // is one that will sit idle forever. Report retries every failure it can - // outlast, including the window before the Worker record exists; anything - // that reaches here is a misconfiguration no restart-in-place will fix. + // Report what this worker can supply and the sandbox runtimes it can run. + // Nothing else tells the control plane, which places no Actor here until it + // lands, so a worker that cannot report is one that will sit idle forever. + // Report retries every failure it can outlast, including the window before + // the Worker record exists; anything that reaches here is a misconfiguration + // no restart-in-place will fix. go func() { err := register.Report(ctx, register.ReportConfig{ SocketPath: nodepath.WorkerSupportSocket, @@ -285,9 +286,10 @@ func do(ctx context.Context) error { AteletSPIFFEID: tunnelConfig.BrokerIdentity, Actors: *maxActors, SandboxClass: string(atev1alpha1.SandboxClassGvisor), + RuntimesFile: nodepath.WorkerSandboxRuntimesPath(*podUID), }) if err != nil && ctx.Err() == nil { - serverboot.Fatal(ctx, "Failed to report worker capacity", err) + serverboot.Fatal(ctx, "Failed to register worker", err) } }() diff --git a/cmd/ateworker-microvm/main.go b/cmd/ateworker-microvm/main.go index 27d5b4cd53..6f1a34ed6c 100644 --- a/cmd/ateworker-microvm/main.go +++ b/cmd/ateworker-microvm/main.go @@ -182,7 +182,7 @@ func do(ctx context.Context) error { if err := resources.ValidateWorkerPodUID(*podUID); err != nil { return fmt.Errorf("in resources.ValidateWorkerPodUID: %w", err) } - // atelet owns the directory and the sandbox versions file in it; only the + // atelet owns the directory and the sandbox runtimes file in it; only the // socket below is this process's. Both outlive a container restart. if err := os.MkdirAll(ateworkerDir, 0o700); err != nil { return fmt.Errorf("in os.MkdirAll(%q): %w", ateworkerDir, err) @@ -272,11 +272,12 @@ func do(ctx context.Context) error { svr.GracefulStop() }() - // Report what this worker can supply. Nothing else tells the control plane, - // which places no Actor here until it lands, so a worker that cannot report - // is one that will sit idle forever. Report retries every failure it can - // outlast, including the window before the Worker record exists; anything - // that reaches here is a misconfiguration no restart-in-place will fix. + // Report what this worker can supply and the sandbox runtimes it can run. + // Nothing else tells the control plane, which places no Actor here until it + // lands, so a worker that cannot report is one that will sit idle forever. + // Report retries every failure it can outlast, including the window before + // the Worker record exists; anything that reaches here is a misconfiguration + // no restart-in-place will fix. go func() { err := register.Report(ctx, register.ReportConfig{ SocketPath: nodepath.WorkerSupportSocket, @@ -285,9 +286,10 @@ func do(ctx context.Context) error { AteletSPIFFEID: tunnelConfig.BrokerIdentity, Actors: *maxActors, SandboxClass: string(atev1alpha1.SandboxClassMicroVM), + RuntimesFile: nodepath.WorkerSandboxRuntimesPath(*podUID), }) if err != nil && ctx.Err() == nil { - serverboot.Fatal(ctx, "Failed to report worker capacity", err) + serverboot.Fatal(ctx, "Failed to register worker", err) } }() diff --git a/docs/api-guide.md b/docs/api-guide.md index 9a37e48292..075bfa045b 100644 --- a/docs/api-guide.md +++ b/docs/api-guide.md @@ -402,7 +402,9 @@ a `Snapshot` with the following fields: an eligible worker whose `defaultRuntime` or `restorableRuntimes` is compatible with `sandboxRuntime` (same sandbox class, same schema version, and every attribute recorded on the snapshot present on the worker with the same - value; attributes only the worker reports are ignored). When no compatible worker has + value, except `cpu_features`, whose hexadecimal bitmask on the snapshot must be + a bitwise subset of the worker's; attributes only the worker reports are + ignored). When no compatible worker has capacity: - A durable snapshot — the actor's own, a `sourceTag` clone, or the template's golden snapshot a new actor starts from — falls back to any eligible diff --git a/internal/ateworker/register/register.go b/internal/ateworker/register/register.go index b9c6148032..582a0bac57 100644 --- a/internal/ateworker/register/register.go +++ b/internal/ateworker/register/register.go @@ -13,8 +13,8 @@ // limitations under the License. // Package register registers a worker with the control plane through the -// node-local atelet, reporting its compute capacity and the sandbox runtime -// its actors start under. +// node-local atelet, reporting its compute capacity and the sandbox runtimes +// of its pool, and provides file-watching helpers shared by worker binaries. package register import ( @@ -22,26 +22,33 @@ import ( "crypto/tls" "errors" "fmt" + "io/fs" "log/slog" + "maps" "os" "path/filepath" + "slices" "strconv" "strings" "time" "go.ate.dev/substrate/internal/ateletdial" "go.ate.dev/substrate/internal/hardware" + "go.ate.dev/substrate/internal/proto/ateletpb" "go.ate.dev/substrate/internal/resources" + "go.ate.dev/substrate/internal/sandboxruntimes" "go.ate.dev/substrate/internal/workercapacity" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/proto" "k8s.io/apimachinery/pkg/api/resource" - - "go.ate.dev/substrate/internal/proto/ateletpb" ) const ( reportTimeout = 10 * time.Second initialReportBackoff = 500 * time.Millisecond maxReportBackoff = 30 * time.Second + resyncInterval = 5 * time.Minute ) // FromFiles combines the downward API compute limits with the worker's own @@ -68,17 +75,36 @@ func fromDir(dir string, actors int) *ateletpb.WorkerResources { } } -// defaultRuntime is the runtime actors hosted by this worker start under: its -// sandbox class on this host's hardware. -// -// TODO: Report the pool's SandboxConfig so atelet can complete the identity -// with the digests of the default version's assets and list its other -// enabled versions as restorable_runtimes. -func defaultRuntime(sandboxClass string) *ateletpb.SandboxRuntime { +// buildRuntime converts the compatibility attributes atelet wrote for one +// SandboxConfig version into a SandboxRuntime of sandboxClass. +func buildRuntime(sandboxClass string, attrs sandboxruntimes.Attributes) *ateletpb.SandboxRuntime { + entries := make([]*ateletpb.AttributeEntry, 0, len(attrs)) + for _, k := range slices.Sorted(maps.Keys(attrs)) { + entries = append(entries, &ateletpb.AttributeEntry{ + Key: k, + Value: attrs[k], + }) + } return &ateletpb.SandboxRuntime{ SandboxClass: sandboxClass, - Version: hardware.ProbeHost(), + Version: &ateletpb.VersionedSandboxCompat{ + SchemaVersion: hardware.SchemaVersionV1, + Attributes: entries, + }, + } +} + +// buildRequest builds the RegisterWorkerRequest for capacity and the runtimes +// in f under sandboxClass. +func buildRequest(capacity *ateletpb.WorkerResources, sandboxClass string, f *sandboxruntimes.File) *ateletpb.RegisterWorkerRequest { + req := &ateletpb.RegisterWorkerRequest{ + Capacity: capacity, + DefaultRuntime: buildRuntime(sandboxClass, f.DefaultRuntime), + } + for _, attrs := range f.RestorableRuntimes { + req.RestorableRuntimes = append(req.RestorableRuntimes, buildRuntime(sandboxClass, attrs)) } + return req } // cpuMemory is the Resources for those two dimensions. A zero dimension is @@ -113,7 +139,7 @@ func readLimit(path string) int64 { } // ReportConfig configures how a worker reaches the node-local atelet and what -// capacity and sandbox runtime class it registers with. +// it registers with. type ReportConfig struct { SocketPath string CredentialBundlePath string @@ -126,60 +152,146 @@ type ReportConfig struct { Actors int // SandboxClass is the class this worker implements ("gvisor", "microvm"). SandboxClass string + // RuntimesFile is the sandbox runtimes file atelet keeps for this worker + // (nodepath.WorkerSandboxRuntimesPath). + RuntimesFile string } -// Report tells the node-local atelet what this worker can supply and the -// runtime its actors start under, retrying until it is accepted or ctx ends. +// Report keeps the worker registered with the node-local atelet until ctx +// ends: its capacity, and the runtimes in its sandbox runtimes file, which it +// reads at startup and again whenever atelet rewrites it. It returns early +// only on a configuration error, which no retry would fix. // -// Retrying is what makes a single report durable: atelet only accepts once the +// Retrying is what makes a report durable: atelet only accepts once the // control plane has recorded it, and the Worker record may not exist yet when // a worker first comes up. Nothing else reports this, so giving up would leave // the Worker holding no capacity and hosting nothing. func Report(ctx context.Context, cfg ReportConfig) error { if cfg.SandboxClass == "" { - // The control plane refuses a runtime without a class, and retrying - // would not change that. - return errors.New("capacity report: SandboxClass is required") + // The control plane refuses a runtime without a class. + return errors.New("register worker: SandboxClass is required") + } + if cfg.RuntimesFile == "" { + return errors.New("register worker: RuntimesFile is required") } tlsConfig, err := ateletdial.TLSConfig(cfg.CredentialBundlePath, cfg.TrustBundlePath, cfg.AteletSPIFFEID) if err != nil { - return fmt.Errorf("capacity report: %w", err) - } - req := &ateletpb.RegisterWorkerRequest{ - Capacity: FromFiles(cfg.Actors), - DefaultRuntime: defaultRuntime(cfg.SandboxClass), + return fmt.Errorf("register worker: %w", err) } - err = retryReport(ctx, func() error { - return reportOnce(ctx, cfg.SocketPath, tlsConfig, req) - }, initialReportBackoff) - if err != nil { - return err + changes := WatchFilePath(ctx, cfg.RuntimesFile) + r := newReconciler( + cfg, + FromFiles(cfg.Actors), + func() (*sandboxruntimes.File, error) { return sandboxruntimes.Read(cfg.RuntimesFile) }, + func(ctx context.Context, req *ateletpb.RegisterWorkerRequest) error { + return reportOnce(ctx, cfg.SocketPath, tlsConfig, req) + }, + ) + return r.run(ctx, changes) +} + +// reconciler registers the worker with the runtimes in its sandbox runtimes +// file whenever they differ from the ones it last registered. +type reconciler struct { + cfg ReportConfig + capacity *ateletpb.WorkerResources + read func() (*sandboxruntimes.File, error) + register func(context.Context, *ateletpb.RegisterWorkerRequest) error + initialBackoff time.Duration + maxBackoff time.Duration + resync time.Duration + after func(time.Duration) <-chan time.Time + // registered is the last request accepted by atelet, nil until the first + // registration succeeds. + registered *ateletpb.RegisterWorkerRequest +} + +func newReconciler( + cfg ReportConfig, + capacity *ateletpb.WorkerResources, + read func() (*sandboxruntimes.File, error), + register func(context.Context, *ateletpb.RegisterWorkerRequest) error, +) *reconciler { + return &reconciler{ + cfg: cfg, + capacity: capacity, + read: read, + register: register, + initialBackoff: initialReportBackoff, + maxBackoff: maxReportBackoff, + resync: resyncInterval, + after: time.After, } - slog.InfoContext(ctx, "Registered worker capacity and sandbox runtime", - slog.Any("capacity", req.GetCapacity()), - slog.Any("default_runtime", req.GetDefaultRuntime())) - return nil } -// retryReport calls send until it succeeds or ctx ends, backing off between -// attempts. send is a parameter so the loop can be exercised without a socket -// or certificates. -func retryReport(ctx context.Context, send func() error, backoff time.Duration) error { +// run syncs at startup, on every change to the file, with backoff after a +// transient failure, and periodically at resyncInterval, until ctx ends. +// Change signals are ignored while backing off so periodic polling or rapid +// writes cannot bypass the backoff delay when atelet or ateapi is down. +func (r *reconciler) run(ctx context.Context, changes <-chan struct{}) error { + backoff := r.initialBackoff for { - err := send() - if err == nil { - return nil + wait := r.resync + onChange := changes + if r.sync(ctx) { + wait = backoff + backoff = min(backoff*2, r.maxBackoff) + onChange = nil + } else { + backoff = r.initialBackoff } - slog.WarnContext(ctx, "Retrying worker capacity report", slog.Duration("in", backoff), slog.Any("err", err)) select { case <-ctx.Done(): return ctx.Err() - case <-time.After(backoff): + case _, ok := <-onChange: + if !ok { + changes = nil + } + case <-r.after(wait): + } + } +} + +// sync reads the file and registers if needed. It reports whether the caller +// should retry with backoff. +func (r *reconciler) sync(ctx context.Context) (retry bool) { + desired, err := r.read() + if errors.Is(err, fs.ErrNotExist) { + if r.registered == nil { + slog.InfoContext(ctx, "Waiting for the sandbox runtimes file, which atelet writes; see its logs if this persists", + slog.String("path", r.cfg.RuntimesFile)) } - backoff = min(backoff*2, maxReportBackoff) + return false } + if err != nil { + slog.WarnContext(ctx, "Ignoring an unreadable sandbox runtimes file", slog.String("path", r.cfg.RuntimesFile), slog.Any("err", err)) + return false + } + if len(desired.DefaultRuntime) == 0 { + slog.WarnContext(ctx, "Ignoring a sandbox runtimes file with no default runtime", slog.String("path", r.cfg.RuntimesFile)) + return false + } + req := buildRequest(r.capacity, r.cfg.SandboxClass, desired) + if proto.Equal(req, r.registered) { + return false + } + if err := r.register(ctx, req); err != nil { + if status.Code(err) == codes.InvalidArgument { + slog.ErrorContext(ctx, "Worker registration rejected as invalid; waiting for sandbox runtimes file to change", slog.Any("err", err)) + return false + } + slog.WarnContext(ctx, "Could not register worker; will retry", slog.Any("err", err)) + return true + } + r.registered = req + slog.InfoContext(ctx, "Registered worker capacity and sandbox runtimes", + slog.Any("capacity", req.GetCapacity()), + slog.Any("default_runtime", req.GetDefaultRuntime()), + slog.Any("restorable_runtimes", req.GetRestorableRuntimes())) + return false } +// reportOnce dials atelet and sends one RegisterWorker. func reportOnce(ctx context.Context, socketPath string, tlsConfig *tls.Config, req *ateletpb.RegisterWorkerRequest) error { conn, err := ateletdial.Dial(socketPath, tlsConfig) if err != nil { diff --git a/internal/ateworker/register/register_test.go b/internal/ateworker/register/register_test.go index c328e03feb..ab63b289cb 100644 --- a/internal/ateworker/register/register_test.go +++ b/internal/ateworker/register/register_test.go @@ -23,11 +23,16 @@ import ( "testing" "time" + "github.com/fsnotify/fsnotify" "github.com/google/go-cmp/cmp" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" "google.golang.org/protobuf/testing/protocmp" "go.ate.dev/substrate/internal/hardware" + "go.ate.dev/substrate/internal/nodepath" "go.ate.dev/substrate/internal/proto/ateletpb" + "go.ate.dev/substrate/internal/sandboxruntimes" "go.ate.dev/substrate/internal/workercapacity" ) @@ -73,53 +78,521 @@ func TestFromFilesMissing(t *testing.T) { } } -func TestDefaultRuntime(t *testing.T) { - got := defaultRuntime("gvisor") - // The class is the worker's own; the compat identity is the host's, under - // the probe's schema version. +func TestBuildRuntime(t *testing.T) { + got := buildRuntime("gvisor", sandboxruntimes.Attributes{ + hardware.AttrSandboxRuntimeHash: "sha256:abc", + hardware.AttrCPUFeatures: "3f", + hardware.AttrArchitecture: runtime.GOARCH, + }) want := &ateletpb.SandboxRuntime{ SandboxClass: "gvisor", Version: &ateletpb.VersionedSandboxCompat{ SchemaVersion: hardware.SchemaVersionV1, - Attributes: []*ateletpb.AttributeEntry{{Key: hardware.AttrArchitecture, Value: runtime.GOARCH}}, + Attributes: []*ateletpb.AttributeEntry{ + {Key: hardware.AttrArchitecture, Value: runtime.GOARCH}, + {Key: hardware.AttrCPUFeatures, Value: "3f"}, + {Key: hardware.AttrSandboxRuntimeHash, Value: "sha256:abc"}, + }, }, } if diff := cmp.Diff(want, got, protocmp.Transform()); diff != "" { - t.Errorf("defaultRuntime() mismatch (-want +got):\n%s", diff) + t.Errorf("buildRuntime() mismatch (-want +got):\n%s", diff) } } -// reportSeam swaps the one-shot call out so the retry loop can be exercised -// without a socket or certificates. -func TestReportRetriesUntilAccepted(t *testing.T) { - attempts := 0 - send := func() error { - attempts++ - if attempts < 3 { - return errors.New("worker record does not exist yet") +func TestReconcilerSync(t *testing.T) { + capacity := &ateletpb.WorkerResources{Actors: testActors} + v1 := &sandboxruntimes.File{ + DefaultRuntime: sandboxruntimes.Attributes{ + hardware.AttrArchitecture: runtime.GOARCH, + hardware.AttrCPUFeatures: "3", + hardware.AttrSandboxRuntimeHash: "sha256:v1", + }, + } + v2 := &sandboxruntimes.File{ + DefaultRuntime: sandboxruntimes.Attributes{ + hardware.AttrArchitecture: runtime.GOARCH, + hardware.AttrCPUFeatures: "7", + hardware.AttrSandboxRuntimeHash: "sha256:v2", + }, + RestorableRuntimes: []sandboxruntimes.Attributes{v1.DefaultRuntime}, + } + reqV1 := buildRequest(capacity, "gvisor", v1) + reqV2 := buildRequest(capacity, "gvisor", v2) + + tests := []struct { + name string + setupFile func(t *testing.T, path string) + registered *ateletpb.RegisterWorkerRequest + registerErr error + wantRetry bool + wantCalls []*ateletpb.RegisterWorkerRequest + wantRegistered *ateletpb.RegisterWorkerRequest + }{ + { + // File not written yet: waits for fsnotify rather than spinning in backoff. + name: "file missing", + setupFile: func(*testing.T, string) {}, + }, + { + name: "corrupt JSON", + setupFile: func(t *testing.T, path string) { + if err := os.WriteFile(path, []byte("{not json"), 0o600); err != nil { + t.Fatal(err) + } + }, + }, + { + name: "empty default runtime", + setupFile: func(t *testing.T, path string) { + if _, err := sandboxruntimes.Write(path, &sandboxruntimes.File{}); err != nil { + t.Fatal(err) + } + }, + }, + { + name: "register error asks for retry and keeps prior registered state", + setupFile: func(t *testing.T, path string) { + if _, err := sandboxruntimes.Write(path, v1); err != nil { + t.Fatal(err) + } + }, + registerErr: errors.New("worker record does not exist yet"), + wantRetry: true, + }, + { + name: "InvalidArgument from register is not retried", + setupFile: func(t *testing.T, path string) { + if _, err := sandboxruntimes.Write(path, v1); err != nil { + t.Fatal(err) + } + }, + registerErr: status.Error(codes.InvalidArgument, "invalid attribute key"), + wantRetry: false, + }, + { + name: "first success registers and records request", + setupFile: func(t *testing.T, path string) { + if _, err := sandboxruntimes.Write(path, v1); err != nil { + t.Fatal(err) + } + }, + wantCalls: []*ateletpb.RegisterWorkerRequest{reqV1}, + wantRegistered: reqV1, + }, + { + name: "unchanged file is a no-op", + setupFile: func(t *testing.T, path string) { + if _, err := sandboxruntimes.Write(path, v1); err != nil { + t.Fatal(err) + } + }, + registered: reqV1, + wantRegistered: reqV1, + }, + { + name: "updated file registers new default and restorable runtimes", + setupFile: func(t *testing.T, path string) { + if _, err := sandboxruntimes.Write(path, v2); err != nil { + t.Fatal(err) + } + }, + registered: reqV1, + wantCalls: []*ateletpb.RegisterWorkerRequest{reqV2}, + wantRegistered: reqV2, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + path := filepath.Join(t.TempDir(), nodepath.SandboxRuntimesFileName) + tc.setupFile(t, path) + + var calls []*ateletpb.RegisterWorkerRequest + r := newReconciler( + ReportConfig{SandboxClass: "gvisor", RuntimesFile: path}, + capacity, + func() (*sandboxruntimes.File, error) { return sandboxruntimes.Read(path) }, + func(_ context.Context, req *ateletpb.RegisterWorkerRequest) error { + if tc.registerErr != nil { + return tc.registerErr + } + calls = append(calls, req) + return nil + }, + ) + r.registered = tc.registered + + gotRetry := r.sync(context.Background()) + if gotRetry != tc.wantRetry { + t.Errorf("sync() retry = %v, want %v", gotRetry, tc.wantRetry) + } + if diff := cmp.Diff(tc.wantCalls, calls, protocmp.Transform()); diff != "" { + t.Errorf("register calls mismatch (-want +got):\n%s", diff) + } + if diff := cmp.Diff(tc.wantRegistered, r.registered, protocmp.Transform()); diff != "" { + t.Errorf("registered state mismatch (-want +got):\n%s", diff) + } + }) + } +} + +func TestReconcilerRunBackoff(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + file := &sandboxruntimes.File{ + DefaultRuntime: sandboxruntimes.Attributes{hardware.AttrSandboxRuntimeHash: "sha256:v1"}, + } + changes := make(chan struct{}, 1) + + const ( + initial = 10 * time.Millisecond + maxB = 35 * time.Millisecond + resync = 5 * time.Minute + ) + + var ( + waits []time.Duration + attempts int + ) + r := newReconciler( + ReportConfig{SandboxClass: "gvisor", RuntimesFile: "sandbox-runtimes.json"}, + &ateletpb.WorkerResources{Actors: testActors}, + func() (*sandboxruntimes.File, error) { return file, nil }, + func(_ context.Context, _ *ateletpb.RegisterWorkerRequest) error { + attempts++ + switch attempts { + case 1, 2, 3: + // Fail three times: waits should be 10ms, 20ms, 35ms (capped at maxBackoff). + return errors.New("transient failure") + case 4: + // Succeed on attempt 4, then trigger a file update on changes. + file = &sandboxruntimes.File{ + DefaultRuntime: sandboxruntimes.Attributes{hardware.AttrSandboxRuntimeHash: "sha256:v2"}, + } + changes <- struct{}{} + return nil + case 5: + // Fail once on the updated file: backoff must have reset to initial (10ms). + return errors.New("transient failure after reset") + default: + cancel() + return nil + } + }, + ) + r.initialBackoff = initial + r.maxBackoff = maxB + r.resync = resync + r.after = func(d time.Duration) <-chan time.Time { + waits = append(waits, d) + if d == resync { + // Resync timer is armed on success; let changes wake the select instead. + return nil } - return nil + ch := make(chan time.Time, 1) + ch <- time.Now() + return ch } - if err := retryReport(context.Background(), send, time.Millisecond); err != nil { - t.Fatalf("retryReport() failed: %v", err) + + if err := r.run(ctx, changes); !errors.Is(err, context.Canceled) { + t.Fatalf("run() = %v, want context.Canceled", err) + } + wantWaits := []time.Duration{ + initial, // after attempt 1 + 2 * initial, // after attempt 2 (doubled) + maxB, // after attempt 3 (capped at maxBackoff) + resync, // after attempt 4 (succeeded; armed resync timer while changes fired) + initial, // after attempt 5 (reset following attempt 4's success) + resync, // after attempt 6 (succeeded before cancel) } - if attempts != 3 { - t.Errorf("gave up after %d attempts, want 3", attempts) + if diff := cmp.Diff(wantWaits, waits); diff != "" { + t.Errorf("timer durations mismatch (-want +got):\n%s", diff) } } -func TestReportStopsWhenContextEnds(t *testing.T) { +// A change signal that lands while run is backing off after a failed +// registration must not cut the backoff short: with ateapi down, every +// worker on the node would otherwise retry on every signal in lockstep. The +// signal is still acted on once the backoff has elapsed. +func TestReconcilerIgnoresChangesDuringBackoff(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + file := &sandboxruntimes.File{ + DefaultRuntime: sandboxruntimes.Attributes{hardware.AttrSandboxRuntimeHash: "sha256:v1"}, + } + changes := make(chan struct{}, 1) + // started reports each registration attempt as it begins; release lets + // the second one return. + started := make(chan int, 4) + release := make(chan struct{}) attempts := 0 - send := func() error { - attempts++ - if attempts == 2 { - cancel() + r := newReconciler( + ReportConfig{SandboxClass: "gvisor", RuntimesFile: "sandbox-runtimes.json"}, + &ateletpb.WorkerResources{Actors: testActors}, + func() (*sandboxruntimes.File, error) { return file, nil }, + func(context.Context, *ateletpb.RegisterWorkerRequest) error { + attempts++ + started <- attempts + if attempts == 1 { + // The change is pending before run reaches its select. + changes <- struct{}{} + return errors.New("transient failure") + } + <-release + return nil + }, + ) + // Unbuffered, so a send succeeds only while run is waiting on the timer. + backoffTimer := make(chan time.Time) + armed := make(chan time.Duration, 4) + r.after = func(d time.Duration) <-chan time.Time { + armed <- d + if d == r.initialBackoff { + return backoffTimer + } + return make(chan time.Time) // the resync timer never fires here + } + + done := make(chan error, 1) + go func() { + done <- r.run(ctx, changes) + }() + + if n := <-started; n != 1 { + t.Fatalf("first attempt = %d, want 1", n) + } + if d := <-armed; d != r.initialBackoff { + t.Fatalf("armed %v after the failed attempt, want the initial backoff %v", d, r.initialBackoff) + } + // Were run listening to changes during the backoff, it would be inside + // the second attempt now rather than receiving the timer. + select { + case n := <-started: + t.Fatalf("attempt %d started before the backoff elapsed: the change signal bypassed the backoff", n) + case backoffTimer <- time.Now(): + } + if n := <-started; n != 2 { + t.Fatalf("attempt after the backoff = %d, want 2", n) + } + close(release) + + // The pending change is consumed once the retry succeeds; the file it + // announces is what was just registered, so no third attempt follows. + for _, want := range []time.Duration{r.resync, r.resync} { + if d := <-armed; d != want { + t.Fatalf("armed %v after a successful sync, want the resync interval %v", d, want) } - return errors.New("still failing") } - if err := retryReport(ctx, send, time.Millisecond); !errors.Is(err, context.Canceled) { - t.Errorf("retryReport() = %v, want context.Canceled", err) + cancel() + if err := <-done; !errors.Is(err, context.Canceled) { + t.Fatalf("run() = %v, want context.Canceled", err) + } + if attempts != 2 { + t.Errorf("attempts = %d, want 2", attempts) + } +} + +func TestReconcilerRunResync(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + var reads int + r := newReconciler( + ReportConfig{SandboxClass: "gvisor", RuntimesFile: "sandbox-runtimes.json"}, + &ateletpb.WorkerResources{Actors: testActors}, + func() (*sandboxruntimes.File, error) { + reads++ + if reads >= 2 { + cancel() + } + return &sandboxruntimes.File{ + DefaultRuntime: sandboxruntimes.Attributes{hardware.AttrSandboxRuntimeHash: "sha256:v1"}, + }, nil + }, + func(context.Context, *ateletpb.RegisterWorkerRequest) error { return nil }, + ) + var armed []time.Duration + r.after = func(d time.Duration) <-chan time.Time { + armed = append(armed, d) + if ctx.Err() != nil { + return nil // once canceled, let ctx.Done win the select + } + ch := make(chan time.Time, 1) + ch <- time.Now() + return ch + } + + // Pass a nil changes channel: only the resync timer can trigger the second read. + if err := r.run(ctx, nil); !errors.Is(err, context.Canceled) { + t.Fatalf("run() = %v, want context.Canceled", err) + } + if reads != 2 { + t.Errorf("read calls = %d, want 2 (initial sync + resync tick)", reads) + } + if len(armed) == 0 || armed[0] != resyncInterval { + t.Errorf("armed timers = %v, want first timer to be resyncInterval (%v)", armed, resyncInterval) + } +} + +func TestWatchFileFallbackToPolling(t *testing.T) { + origInterval := watchPollInterval + watchPollInterval = 5 * time.Millisecond + t.Cleanup(func() { watchPollInterval = origInterval }) + + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) + defer cancel() + + // Point WatchFilePath at a path whose parent directory does not exist so w.Add + // fails naturally and triggers the polling fallback. + missingPath := filepath.Join(t.TempDir(), "missing-parent", nodepath.SandboxRuntimesFileName) + changes := WatchFilePath(ctx, missingPath) + + select { + case _, ok := <-changes: + if !ok { + t.Fatal("changes channel closed before emitting a polling signal") + } + case <-ctx.Done(): + t.Fatal("timed out waiting for polling fallback signal") + } + + cancel() + for range changes { + } +} + +func TestWatchFileFilterAndErrors(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + dir := nodepath.WorkerPath("pod-uid") + target := nodepath.WorkerSandboxRuntimesPath("pod-uid") + events := make(chan fsnotify.Event) + errs := make(chan error) + changes := make(chan struct{}, 1) + + done := make(chan struct{}) + go func() { + defer close(done) + watchFileLoop(ctx, target, events, errs, changes) + }() + + assertNoSignal := func(t *testing.T, desc string) { + t.Helper() + select { + case <-changes: + t.Fatalf("unexpected signal after %s", desc) + case <-time.After(10 * time.Millisecond): + } + } + assertSignal := func(t *testing.T, desc string) { + t.Helper() + select { + case <-changes: + case <-time.After(2 * time.Second): + t.Fatalf("timed out waiting for signal after %s", desc) + } + } + + // Unrelated files in the same worker directory must be ignored. + events <- fsnotify.Event{Name: nodepath.WorkerSocketPath("pod-uid"), Op: fsnotify.Create} + events <- fsnotify.Event{Name: filepath.Join(dir, ".sandbox-runtimes-123456"), Op: fsnotify.Create | fsnotify.Write} + assertNoSignal(t, "events on worker.sock and temp file") + + // Non-Create/Write operations on the target file must be ignored. + events <- fsnotify.Event{Name: target, Op: fsnotify.Chmod} + events <- fsnotify.Event{Name: target, Op: fsnotify.Remove} + events <- fsnotify.Event{Name: target, Op: fsnotify.Rename} + assertNoSignal(t, "Chmod/Remove/Rename on target file") + + // Create and Write on the target file must signal. + events <- fsnotify.Event{Name: target, Op: fsnotify.Create} + assertSignal(t, "Create on target file") + events <- fsnotify.Event{Name: target, Op: fsnotify.Write} + assertSignal(t, "Write on target file") + + // An error on the watcher's Errors channel must also signal so the caller re-reads. + errs <- errors.New("fsnotify queue overflow") + assertSignal(t, "watcher error") + + cancel() + <-done +} + +func TestReconcilerRunWatchesFile(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + path := filepath.Join(t.TempDir(), nodepath.SandboxRuntimesFileName) + got := make(chan *ateletpb.RegisterWorkerRequest, 4) + r := newReconciler( + ReportConfig{SandboxClass: "gvisor", RuntimesFile: path}, + &ateletpb.WorkerResources{Actors: testActors}, + func() (*sandboxruntimes.File, error) { return sandboxruntimes.Read(path) }, + func(_ context.Context, req *ateletpb.RegisterWorkerRequest) error { + got <- req + return nil + }, + ) + + changes := WatchFilePath(ctx, path) + done := make(chan error, 1) + go func() { + done <- r.run(ctx, changes) + }() + + // Write the file after WatchFilePath has started, as happens when a worker comes up + // before atelet has reconciled the pod. + v1 := &sandboxruntimes.File{ + DefaultRuntime: sandboxruntimes.Attributes{ + hardware.AttrArchitecture: runtime.GOARCH, + hardware.AttrCPUFeatures: "3", + hardware.AttrSandboxRuntimeHash: "sha256:v1", + }, + } + if _, err := sandboxruntimes.Write(path, v1); err != nil { + t.Fatalf("writing v1: %v", err) + } + select { + case req := <-got: + if diff := cmp.Diff(buildRuntime("gvisor", v1.DefaultRuntime), req.GetDefaultRuntime(), protocmp.Transform()); diff != "" { + t.Errorf("initial registration default_runtime mismatch (-want +got):\n%s", diff) + } + case <-ctx.Done(): + t.Fatal("timed out waiting for initial registration after file creation") + } + + // Rewrite the file via atomic rename and verify the watcher triggers a + // second registration. + v2 := &sandboxruntimes.File{ + DefaultRuntime: sandboxruntimes.Attributes{ + hardware.AttrArchitecture: runtime.GOARCH, + hardware.AttrCPUFeatures: "7", + hardware.AttrSandboxRuntimeHash: "sha256:v2", + }, + RestorableRuntimes: []sandboxruntimes.Attributes{v1.DefaultRuntime}, + } + if _, err := sandboxruntimes.Write(path, v2); err != nil { + t.Fatalf("writing v2: %v", err) + } + select { + case req := <-got: + if diff := cmp.Diff(buildRuntime("gvisor", v2.DefaultRuntime), req.GetDefaultRuntime(), protocmp.Transform()); diff != "" { + t.Errorf("updated registration default_runtime mismatch (-want +got):\n%s", diff) + } + if len(req.GetRestorableRuntimes()) != 1 { + t.Fatalf("got %d restorable runtimes, want 1", len(req.GetRestorableRuntimes())) + } + case <-ctx.Done(): + t.Fatal("timed out waiting for registration after file update") + } + + cancel() + if err := <-done; !errors.Is(err, context.Canceled) { + t.Errorf("run() = %v, want context.Canceled", err) } } @@ -135,6 +608,7 @@ func TestReportFailsFastOnBadCredentials(t *testing.T) { CredentialBundlePath: filepath.Join(t.TempDir(), "does-not-exist.pem"), TrustBundlePath: filepath.Join(t.TempDir(), "also-missing.pem"), SandboxClass: "gvisor", + RuntimesFile: filepath.Join(t.TempDir(), nodepath.SandboxRuntimesFileName), }) if err == nil { t.Fatal("Report() with unreadable credentials succeeded, want an error the caller can exit on") @@ -144,17 +618,39 @@ func TestReportFailsFastOnBadCredentials(t *testing.T) { } } -// A runtime without a class is refused by the control plane on every attempt, -// so a worker that names none must exit rather than retry. -func TestReportFailsFastWithoutSandboxClass(t *testing.T) { +// A runtime without a class or runtimes file path is a configuration bug, so +// Report must fail immediately rather than spin. +func TestReportFailsFastOnMissingConfig(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() - err := Report(ctx, ReportConfig{SocketPath: filepath.Join(t.TempDir(), "atelet.sock")}) - if err == nil { - t.Fatal("Report() without a SandboxClass succeeded, want an error the caller can exit on") - } - if errors.Is(err, context.DeadlineExceeded) { - t.Errorf("Report() retried a permanent failure until the deadline: %v", err) + for _, tc := range []struct { + name string + cfg ReportConfig + }{ + { + name: "missing SandboxClass", + cfg: ReportConfig{ + SocketPath: filepath.Join(t.TempDir(), "atelet.sock"), + RuntimesFile: filepath.Join(t.TempDir(), nodepath.SandboxRuntimesFileName), + }, + }, + { + name: "missing RuntimesFile", + cfg: ReportConfig{ + SocketPath: filepath.Join(t.TempDir(), "atelet.sock"), + SandboxClass: "gvisor", + }, + }, + } { + t.Run(tc.name, func(t *testing.T) { + err := Report(ctx, tc.cfg) + if err == nil { + t.Fatal("Report() succeeded, want an error the caller can exit on") + } + if errors.Is(err, context.DeadlineExceeded) { + t.Errorf("Report() retried a permanent failure until the deadline: %v", err) + } + }) } } diff --git a/internal/ateworker/register/watch.go b/internal/ateworker/register/watch.go new file mode 100644 index 0000000000..31d8df67ca --- /dev/null +++ b/internal/ateworker/register/watch.go @@ -0,0 +1,123 @@ +// 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 register + +import ( + "context" + "log/slog" + "path/filepath" + "time" + + "github.com/fsnotify/fsnotify" +) + +// watchPollInterval is how often WatchFilePath signals when fsnotify is +// unavailable (for example, when the node's inotify instance budget is +// exhausted). A var so tests can shorten it. +var watchPollInterval = 5 * time.Second + +// WatchFilePath signals on the returned channel whenever the file at path is +// created, written, or replaced, or when the underlying watcher reports an +// error (such as an event queue overflow) so the caller can re-read the file. +// It watches path's parent directory, which must exist, so atomic file +// replacements via rename are detected even when the file itself does not +// exist yet at startup. +// +// If fsnotify cannot watch the parent directory (for example, when the host +// UID's fs.inotify.max_user_instances budget is exhausted), WatchFilePath logs a +// warning and falls back to signaling periodically at watchPollInterval until +// ctx ends. The returned channel is closed when ctx ends. +func WatchFilePath(ctx context.Context, path string) <-chan struct{} { + cleanPath := filepath.Clean(path) + changes := make(chan struct{}, 1) + w, err := fsnotify.NewWatcher() + if err != nil { + slog.WarnContext(ctx, "Could not create fsnotify watcher; falling back to polling", + slog.String("path", cleanPath), slog.Duration("interval", watchPollInterval), slog.Any("err", err)) + go pollFileLoop(ctx, watchPollInterval, changes) + return changes + } + dir := filepath.Dir(cleanPath) + if err := w.Add(dir); err != nil { + _ = w.Close() + slog.WarnContext(ctx, "Could not watch directory with fsnotify; falling back to polling", + slog.String("dir", dir), slog.String("path", cleanPath), slog.Duration("interval", watchPollInterval), slog.Any("err", err)) + go pollFileLoop(ctx, watchPollInterval, changes) + return changes + } + go func() { + defer close(changes) + defer func() { + if err := w.Close(); err != nil { + slog.WarnContext(ctx, "Failed to close file watcher", slog.String("path", cleanPath), slog.Any("err", err)) + } + }() + watchFileLoop(ctx, cleanPath, w.Events, w.Errors, changes) + }() + return changes +} + +func pollFileLoop(ctx context.Context, interval time.Duration, changes chan<- struct{}) { + defer close(changes) + ticker := time.NewTicker(interval) + defer ticker.Stop() + for { + select { + case <-ctx.Done(): + return + case <-ticker.C: + select { + case changes <- struct{}{}: + default: + } + } + } +} + +func watchFileLoop(ctx context.Context, cleanPath string, events <-chan fsnotify.Event, errs <-chan error, changes chan<- struct{}) { + for { + select { + case <-ctx.Done(): + return + case ev, ok := <-events: + if !ok { + return + } + if !isFileModifyEvent(cleanPath, ev) { + continue + } + select { + case changes <- struct{}{}: + default: // a signal is already pending + } + case err, ok := <-errs: + if !ok { + return + } + slog.WarnContext(ctx, "File watch error; signaling re-read", slog.String("path", cleanPath), slog.Any("err", err)) + select { + case changes <- struct{}{}: + default: + } + } + } +} + +func isFileModifyEvent(cleanPath string, ev fsnotify.Event) bool { + if filepath.Clean(ev.Name) != cleanPath { + return false + } + return ev.Has(fsnotify.Create) || ev.Has(fsnotify.Write) +} diff --git a/internal/hardware/cpufeatures.go b/internal/hardware/cpufeatures.go new file mode 100644 index 0000000000..075f81b78a --- /dev/null +++ b/internal/hardware/cpufeatures.go @@ -0,0 +1,244 @@ +// 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 hardware + +import ( + "bufio" + "fmt" + "io" + "maps" + "math/bits" + "os" + "runtime" + "slices" + "strconv" + "strings" +) + +// cpuFeatureTables maps each supported architecture to its CPU feature table +// from cpufeatures_table.go, whose header says where the tables come from +// and what a bit means. +var cpuFeatureTables = map[string]featureTable{ + "amd64": newFeatureTable(amd64CPUFeatures), + "arm64": newFeatureTable(arm64CPUFeatures), +} + +// featureTable is one architecture's CPU feature names by bit, with the +// reverse lookup EncodeCPUFeatures uses and the set of bits that have a +// name, which DecodeCPUFeatures validates against. +type featureTable struct { + names map[int]string + bits []int // the keys of names, ascending + index map[string]int + defined featureMask +} + +// newFeatureTable indexes a table. A bit outside featureMask is a table +// error, so it is a panic rather than a dropped name. +func newFeatureTable(names map[int]string) featureTable { + t := featureTable{ + names: names, + bits: slices.Sorted(maps.Keys(names)), + index: make(map[string]int, len(names)), + } + for bit, name := range names { + if bit < 0 || bit >= featureMaskWords*64 { + panic(fmt.Sprintf("hardware: CPU feature %q has bit %d outside a %d-bit featureMask", name, bit, featureMaskWords*64)) + } + t.defined.set(bit) + t.index[name] = bit + } + return t +} + +// featureMaskWords is the width of a cpu_features bitmask in 64-bit words. +// gVisor's amd64 blocks 0-8 use bits up to 261, and 6 words leave room for +// blocks through 11 within SchemaVersionV1. +const featureMaskWords = 6 + +// featureMask is a decoded cpu_features value: bit i set means the feature at +// bit i of the architecture's table is present. It is a fixed-size array so +// matching on the scheduler's hot path allocates nothing. +type featureMask [featureMaskWords]uint64 + +func (m *featureMask) set(bit int) { + m[bit/64] |= 1 << (bit % 64) +} + +func (m featureMask) has(bit int) bool { + return m[bit/64]&(1<<(bit%64)) != 0 +} + +// subsetOf reports whether every bit set in m is also set in w. +func (m featureMask) subsetOf(w featureMask) bool { + for i := range m { + if m[i]&^w[i] != 0 { + return false + } + } + return true +} + +// firstNotIn is the lowest bit set in m but not in w, or -1 if m is a +// subset of w. +func (m featureMask) firstNotIn(w featureMask) int { + for i := range m { + if extra := m[i] &^ w[i]; extra != 0 { + return i*64 + bits.TrailingZeros64(extra) + } + } + return -1 +} + +// String is the canonical encoding: lowercase hex, no prefix, no leading +// zeros, "0" for the empty mask. Equal sets encode to equal strings, which +// is what proto equality on a recorded runtime compares. +func (m featureMask) String() string { + top := featureMaskWords - 1 + for top > 0 && m[top] == 0 { + top-- + } + var sb strings.Builder + sb.WriteString(strconv.FormatUint(m[top], 16)) + for i := top - 1; i >= 0; i-- { + fmt.Fprintf(&sb, "%016x", m[i]) + } + return sb.String() +} + +// parseFeatureMask parses the canonical encoding and rejects anything else. +func parseFeatureMask(s string) (featureMask, bool) { + var m featureMask + if s == "" || len(s) > featureMaskWords*16 || (len(s) > 1 && s[0] == '0') { + return m, false + } + for i := 0; i < len(s); i++ { + c := s[len(s)-1-i] + var v uint64 + switch { + case '0' <= c && c <= '9': + v = uint64(c - '0') + case 'a' <= c && c <= 'f': + v = uint64(c-'a') + 10 + default: + return featureMask{}, false + } + m[i/16] |= v << (4 * (i % 16)) + } + return m, true +} + +// EncodeCPUFeatures encodes the feature names `runsc cpu-features` printed +// for arch as a cpu_features value. Blank names are skipped; duplicates and +// order do not matter. An unknown name is an error rather than a dropped +// bit: a snapshot encoded without a feature its sandbox used would match +// workers that lack it. +func EncodeCPUFeatures(arch string, features []string) (string, error) { + table, ok := cpuFeatureTables[arch] + if !ok { + return "", fmt.Errorf("unsupported architecture %q for cpu_features", arch) + } + var m featureMask + for _, raw := range features { + name := strings.TrimSpace(raw) + if name == "" { + continue + } + bit, known := table.index[name] + if !known { + return "", fmt.Errorf("unknown %s CPU feature %q", arch, name) + } + m.set(bit) + } + return m.String(), nil +} + +// DecodeCPUFeatures returns the feature names a cpu_features value encodes +// for arch, in bit order. A set bit with no name is an error: the hardware +// reserves it, so no encoder of this schema could have produced the value. +func DecodeCPUFeatures(arch, value string) ([]string, error) { + table, ok := cpuFeatureTables[arch] + if !ok { + return nil, fmt.Errorf("unsupported architecture %q for cpu_features", arch) + } + m, ok := parseFeatureMask(value) + if !ok { + return nil, fmt.Errorf("invalid cpu_features value %q", value) + } + if bit := m.firstNotIn(table.defined); bit >= 0 { + return nil, fmt.Errorf("cpu_features value %q sets bit %d, which is not a known %s feature", value, bit, arch) + } + var out []string + for _, bit := range table.bits { + if m.has(bit) { + out = append(out, table.names[bit]) + } + } + return out, nil +} + +// ProbeCPUIdentity reads /proc/cpuinfo for the host CPU's vendor and model: +// the AttrCPUVendor and AttrCPUModel values of a microVM runtime. +func ProbeCPUIdentity() (vendor, model string, err error) { + f, err := os.Open("/proc/cpuinfo") + if err != nil { + return "", "", fmt.Errorf("while opening /proc/cpuinfo: %w", err) + } + defer f.Close() + return parseCPUInfo(runtime.GOARCH, f) +} + +// parseCPUInfo extracts the vendor and model for arch from /proc/cpuinfo +// content, from the first processor that lists them: +// - amd64: vendor_id (e.g. "GenuineIntel") and ":" +// (e.g. "6:143"). +// - arm64: CPU implementer (e.g. "0x41") and +// "::" (e.g. "0x0:0xd40:1"). +func parseCPUInfo(arch string, r io.Reader) (vendor, model string, err error) { + // The vendor key first, then the keys the model is composed of. + var keys []string + switch arch { + case "amd64": + keys = []string{"vendor_id", "cpu family", "model"} + case "arm64": + keys = []string{"CPU implementer", "CPU variant", "CPU part", "CPU revision"} + default: + return "", "", fmt.Errorf("unsupported architecture %q for CPU identity", arch) + } + fields := make(map[string]string, len(keys)) + scanner := bufio.NewScanner(r) + for len(fields) < len(keys) && scanner.Scan() { + k, v, ok := strings.Cut(scanner.Text(), ":") + if !ok { + continue + } + k = strings.TrimSpace(k) + if _, seen := fields[k]; !seen && slices.Contains(keys, k) { + fields[k] = strings.TrimSpace(v) + } + } + if err := scanner.Err(); err != nil { + return "", "", fmt.Errorf("while reading /proc/cpuinfo: %w", err) + } + values := make([]string, 0, len(keys)) + for _, k := range keys { + v, ok := fields[k] + if !ok || v == "" { + return "", "", fmt.Errorf("missing %q in /proc/cpuinfo", k) + } + values = append(values, v) + } + return values[0], strings.Join(values[1:], ":"), nil +} diff --git a/internal/hardware/cpufeatures_table.go b/internal/hardware/cpufeatures_table.go new file mode 100644 index 0000000000..7fc97ad7f4 --- /dev/null +++ b/internal/hardware/cpufeatures_table.go @@ -0,0 +1,244 @@ +// 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 hardware + +// CPU feature tables for the cpu_features compatibility attribute. +// +// `runsc cpu-features` prints the features a gVisor sandbox exposes by name, +// and the control plane needs them as a bitmask it can compare by subset. +// Each table maps a bit to the name gVisor prints for it. A bit is the value +// of the gVisor Feature constant named in the entry's comment: on amd64, +// 32*block + the feature's bit in that block's CPUID register, which the +// hardware fixes; on arm64, the value gVisor declares. A bit therefore never +// moves between gVisor versions, and a bit with no entry is one the hardware +// reserves. +// +// The tables were produced once from gVisor's pkg/cpuid sources at +// gvisor.dev/gvisor@v0.0.0-20261014212059-ddc37c698141: for every constant of +// type Feature in features_amd64.go and features_arm64.go with an entry in +// that file's allFeatures map, the constant's value (evaluated with go/types) +// keyed to the entry's display name. To take features from a newer gVisor, +// add its new constants the same way; existing entries do not change. +// +// TODO: Remove the tables, EncodeCPUFeatures, and DecodeCPUFeatures once +// runsc can print its feature set as this bitmask itself, so atelet records +// the value directly instead of mapping names. The mask format and the +// subset matching stay. +var ( + amd64CPUFeatures = map[int]string{ + 0: "pni", // X86FeatureSSE3 (block 0, bit 0) + 1: "pclmulqdq", // X86FeaturePCLMULDQ (block 0, bit 1) + 2: "dtes64", // X86FeatureDTES64 (block 0, bit 2) + 3: "monitor", // X86FeatureMONITOR (block 0, bit 3) + 4: "ds_cpl", // X86FeatureDSCPL (block 0, bit 4) + 5: "vmx", // X86FeatureVMX (block 0, bit 5) + 6: "smx", // X86FeatureSMX (block 0, bit 6) + 7: "est", // X86FeatureEST (block 0, bit 7) + 8: "tm2", // X86FeatureTM2 (block 0, bit 8) + 9: "ssse3", // X86FeatureSSSE3 (block 0, bit 9) + 10: "cid", // X86FeatureCNXTID (block 0, bit 10) + 11: "sdbg", // X86FeatureSDBG (block 0, bit 11) + 12: "fma", // X86FeatureFMA (block 0, bit 12) + 13: "cx16", // X86FeatureCX16 (block 0, bit 13) + 14: "xtpr", // X86FeatureXTPR (block 0, bit 14) + 15: "pdcm", // X86FeaturePDCM (block 0, bit 15) + 17: "pcid", // X86FeaturePCID (block 0, bit 17) + 18: "dca", // X86FeatureDCA (block 0, bit 18) + 19: "sse4_1", // X86FeatureSSE4_1 (block 0, bit 19) + 20: "sse4_2", // X86FeatureSSE4_2 (block 0, bit 20) + 21: "x2apic", // X86FeatureX2APIC (block 0, bit 21) + 22: "movbe", // X86FeatureMOVBE (block 0, bit 22) + 23: "popcnt", // X86FeaturePOPCNT (block 0, bit 23) + 24: "tsc_deadline_timer", // X86FeatureTSCD (block 0, bit 24) + 25: "aes", // X86FeatureAES (block 0, bit 25) + 26: "xsave", // X86FeatureXSAVE (block 0, bit 26) + 27: "osxsave", // X86FeatureOSXSAVE (block 0, bit 27) + 28: "avx", // X86FeatureAVX (block 0, bit 28) + 29: "f16c", // X86FeatureF16C (block 0, bit 29) + 30: "rdrand", // X86FeatureRDRAND (block 0, bit 30) + 31: "hypervisor", // X86FeatureHypervisor (block 0, bit 31) + 32: "fpu", // X86FeatureFPU (block 1, bit 0) + 33: "vme", // X86FeatureVME (block 1, bit 1) + 34: "de", // X86FeatureDE (block 1, bit 2) + 35: "pse", // X86FeaturePSE (block 1, bit 3) + 36: "tsc", // X86FeatureTSC (block 1, bit 4) + 37: "msr", // X86FeatureMSR (block 1, bit 5) + 38: "pae", // X86FeaturePAE (block 1, bit 6) + 39: "mce", // X86FeatureMCE (block 1, bit 7) + 40: "cx8", // X86FeatureCX8 (block 1, bit 8) + 41: "apic", // X86FeatureAPIC (block 1, bit 9) + 43: "sep", // X86FeatureSEP (block 1, bit 11) + 44: "mtrr", // X86FeatureMTRR (block 1, bit 12) + 45: "pge", // X86FeaturePGE (block 1, bit 13) + 46: "mca", // X86FeatureMCA (block 1, bit 14) + 47: "cmov", // X86FeatureCMOV (block 1, bit 15) + 48: "pat", // X86FeaturePAT (block 1, bit 16) + 49: "pse36", // X86FeaturePSE36 (block 1, bit 17) + 50: "pn", // X86FeaturePSN (block 1, bit 18) + 51: "clflush", // X86FeatureCLFSH (block 1, bit 19) + 53: "dts", // X86FeatureDS (block 1, bit 21) + 54: "acpi", // X86FeatureACPI (block 1, bit 22) + 55: "mmx", // X86FeatureMMX (block 1, bit 23) + 56: "fxsr", // X86FeatureFXSR (block 1, bit 24) + 57: "sse", // X86FeatureSSE (block 1, bit 25) + 58: "sse2", // X86FeatureSSE2 (block 1, bit 26) + 59: "ss", // X86FeatureSS (block 1, bit 27) + 60: "ht", // X86FeatureHTT (block 1, bit 28) + 61: "tm", // X86FeatureTM (block 1, bit 29) + 62: "ia64", // X86FeatureIA64 (block 1, bit 30) + 63: "pbe", // X86FeaturePBE (block 1, bit 31) + 64: "fsgsbase", // X86FeatureFSGSBase (block 2, bit 0) + 65: "tsc_adjust", // X86FeatureTSC_ADJUST (block 2, bit 1) + 67: "bmi1", // X86FeatureBMI1 (block 2, bit 3) + 68: "hle", // X86FeatureHLE (block 2, bit 4) + 69: "avx2", // X86FeatureAVX2 (block 2, bit 5) + 70: "fdp_excptn_only", // X86FeatureFDP_EXCPTN_ONLY (block 2, bit 6) + 71: "smep", // X86FeatureSMEP (block 2, bit 7) + 72: "bmi2", // X86FeatureBMI2 (block 2, bit 8) + 73: "erms", // X86FeatureERMS (block 2, bit 9) + 74: "invpcid", // X86FeatureINVPCID (block 2, bit 10) + 75: "rtm", // X86FeatureRTM (block 2, bit 11) + 76: "cqm", // X86FeatureCQM (block 2, bit 12) + 77: "fpcsds", // X86FeatureFPCSDS (block 2, bit 13) + 78: "mpx", // X86FeatureMPX (block 2, bit 14) + 79: "rdt_a", // X86FeatureRDT (block 2, bit 15) + 80: "avx512f", // X86FeatureAVX512F (block 2, bit 16) + 81: "avx512dq", // X86FeatureAVX512DQ (block 2, bit 17) + 82: "rdseed", // X86FeatureRDSEED (block 2, bit 18) + 83: "adx", // X86FeatureADX (block 2, bit 19) + 84: "smap", // X86FeatureSMAP (block 2, bit 20) + 87: "clfushopt", // X86FeatureCLFLUSHOPT (block 2, bit 23) + 88: "clwb", // X86FeatureCLWB (block 2, bit 24) + 89: "ipt", // X86FeatureIPT (block 2, bit 25) + 90: "avx512pf", // X86FeatureAVX512PF (block 2, bit 26) + 91: "avx512er", // X86FeatureAVX512ER (block 2, bit 27) + 92: "avx512cd", // X86FeatureAVX512CD (block 2, bit 28) + 93: "sha_ni", // X86FeatureSHA (block 2, bit 29) + 94: "avx512bw", // X86FeatureAVX512BW (block 2, bit 30) + 95: "avx512vl", // X86FeatureAVX512VL (block 2, bit 31) + 96: "prefetchwt1", // X86FeaturePREFETCHWT1 (block 3, bit 0) + 97: "avx512vbmi", // X86FeatureAVX512VBMI (block 3, bit 1) + 98: "umip", // X86FeatureUMIP (block 3, bit 2) + 99: "pku", // X86FeaturePKU (block 3, bit 3) + 100: "ospke", // X86FeatureOSPKE (block 3, bit 4) + 101: "waitpkg", // X86FeatureWAITPKG (block 3, bit 5) + 102: "avx512_vbmi2", // X86FeatureAVX512_VBMI2 (block 3, bit 6) + 103: "cet_ss", // X86FeatureCET_SS (block 3, bit 7) + 104: "gfni", // X86FeatureGFNI (block 3, bit 8) + 105: "vaes", // X86FeatureVAES (block 3, bit 9) + 106: "vpclmulqdq", // X86FeatureVPCLMULQDQ (block 3, bit 10) + 107: "avx512_vnni", // X86FeatureAVX512_VNNI (block 3, bit 11) + 108: "avx512_bitalg", // X86FeatureAVX512_BITALG (block 3, bit 12) + 109: "tme", // X86FeatureTME (block 3, bit 13) + 110: "avx512_vpopcntdq", // X86FeatureAVX512_VPOPCNTDQ (block 3, bit 14) + 112: "la57", // X86FeatureLA57 (block 3, bit 16) + 118: "rdpid", // X86FeatureRDPID (block 3, bit 22) + 121: "cldemote", // X86FeatureCLDEMOTE (block 3, bit 25) + 123: "movdiri", // X86FeatureMOVDIRI (block 3, bit 27) + 124: "movdir64b", // X86FeatureMOVDIR64B (block 3, bit 28) + 128: "xsaveopt", // X86FeatureXSAVEOPT (block 4, bit 0) + 129: "xsavec", // X86FeatureXSAVEC (block 4, bit 1) + 130: "xgetbv1", // X86FeatureXGETBV1 (block 4, bit 2) + 131: "xsaves", // X86FeatureXSAVES (block 4, bit 3) + 160: "lahf_lm", // X86FeatureLAHF64 (block 5, bit 0) + 161: "cmp_legacy", // X86FeatureCMP_LEGACY (block 5, bit 1) + 162: "svm", // X86FeatureSVM (block 5, bit 2) + 163: "extapic", // X86FeatureEXTAPIC (block 5, bit 3) + 164: "cr8_legacy", // X86FeatureCR8_LEGACY (block 5, bit 4) + 165: "abm", // X86FeatureLZCNT (block 5, bit 5) + 166: "sse4a", // X86FeatureSSE4A (block 5, bit 6) + 167: "misalignsse", // X86FeatureMISALIGNSSE (block 5, bit 7) + 168: "3dnowprefetch", // X86FeaturePREFETCHW (block 5, bit 8) + 169: "osvw", // X86FeatureOSVW (block 5, bit 9) + 170: "ibs", // X86FeatureIBS (block 5, bit 10) + 171: "xop", // X86FeatureXOP (block 5, bit 11) + 172: "skinit", // X86FeatureSKINIT (block 5, bit 12) + 173: "wdt", // X86FeatureWDT (block 5, bit 13) + 175: "lwp", // X86FeatureLWP (block 5, bit 15) + 176: "fma4", // X86FeatureFMA4 (block 5, bit 16) + 177: "tce", // X86FeatureTCE (block 5, bit 17) + 181: "tbm", // X86FeatureTBM (block 5, bit 21) + 182: "topoext", // X86FeatureTOPOLOGY (block 5, bit 22) + 183: "perfctr_core", // X86FeaturePERFCTR_CORE (block 5, bit 23) + 184: "perfctr_nb", // X86FeaturePERFCTR_NB (block 5, bit 24) + 186: "bpext", // X86FeatureBPEXT (block 5, bit 26) + 187: "ptsc", // X86FeaturePERFCTR_TSC (block 5, bit 27) + 188: "perfctr_llc", // X86FeaturePERFCTR_LLC (block 5, bit 28) + 189: "mwaitx", // X86FeatureMWAITX (block 5, bit 29) + 190: "ad_mask_extn", // X86FeatureADMSKEXTN (block 5, bit 30) + 203: "syscall", // X86FeatureSYSCALL (block 6, bit 11) + 212: "nx", // X86FeatureNX (block 6, bit 20) + 214: "mmxext", // X86FeatureMMXEXT (block 6, bit 22) + 217: "fxsr_opt", // X86FeatureFXSR_OPT (block 6, bit 25) + 218: "pdpe1gb", // X86FeatureGBPAGES (block 6, bit 26) + 219: "rdtscp", // X86FeatureRDTSCP (block 6, bit 27) + 221: "lm", // X86FeatureLM (block 6, bit 29) + 222: "3dnowext", // X86Feature3DNOWEXT (block 6, bit 30) + 223: "3dnow", // X86Feature3DNOW (block 6, bit 31) + 226: "avx512_4vnniw", // X86FeatureAVX512_4VNNIW (block 7, bit 2) + 227: "avx512_4fmaps", // X86FeatureAVX512_4FMAPS (block 7, bit 3) + 228: "fsrm", // X86FeatureFSRM (block 7, bit 4) + 232: "avx512_vp2intersect", // X86FeatureAVX512_VP2INTERSECT (block 7, bit 8) + 233: "srbds_ctrl", // X86FeatureSRBDS_CTRL (block 7, bit 9) + 234: "md_clear", // X86FeatureMD_CLEAR (block 7, bit 10) + 235: "rtm_always_abort", // X86FeatureRTM_ALWAYS_ABORT (block 7, bit 11) + 237: "tsx_force_abort", // X86FeatureTSX_FORCE_ABORT (block 7, bit 13) + 238: "serialize", // X86FeatureSERIALIZE (block 7, bit 14) + 239: "hybrid_cpu", // X86FeatureHYBRID_CPU (block 7, bit 15) + 240: "tsxldtrk", // X86FeatureTSXLDTRK (block 7, bit 16) + 242: "pconfig", // X86FeaturePCONFIG (block 7, bit 18) + 243: "arch_lbr", // X86FeatureARCH_LBR (block 7, bit 19) + 244: "ibt", // X86FeatureIBT (block 7, bit 20) + 246: "amx_bf16", // X86FeatureAMX_BF16 (block 7, bit 22) + 247: "avx512_fp16", // X86FeatureAVX512_FP16 (block 7, bit 23) + 248: "amx_tile", // X86FeatureAMX_TILE (block 7, bit 24) + 249: "amx_int8", // X86FeatureAMX_INT8 (block 7, bit 25) + 250: "spec_ctrl", // X86FeatureSPEC_CTRL (block 7, bit 26) + 251: "intel_stibp", // X86FeatureINTEL_STIBP (block 7, bit 27) + 252: "flush_l1d", // X86FeatureFLUSH_L1D (block 7, bit 28) + 253: "arch_capabilities", // X86FeatureARCH_CAPABILITIES (block 7, bit 29) + 254: "core_capabilities", // X86FeatureCORE_CAPABILITIES (block 7, bit 30) + 255: "spec_ctrl_ssbd", // X86FeatureSPEC_CTRL_SSBD (block 7, bit 31) + 260: "avx_vnni", // X86FeatureAVX_VNNI (block 8, bit 4) + 261: "avx512_bf16", // X86FeatureAVX512_BF16 (block 8, bit 5) + } + + arm64CPUFeatures = map[int]string{ + 0: "fp", // ARM64FeatureFP + 1: "asimd", // ARM64FeatureASIMD + 2: "evtstrm", // ARM64FeatureEVTSTRM + 3: "aes", // ARM64FeatureAES + 4: "pmull", // ARM64FeaturePMULL + 5: "sha1", // ARM64FeatureSHA1 + 6: "sha2", // ARM64FeatureSHA2 + 7: "crc32", // ARM64FeatureCRC32 + 8: "atomics", // ARM64FeatureATOMICS + 9: "fphp", // ARM64FeatureFPHP + 10: "asimdhp", // ARM64FeatureASIMDHP + 11: "cpuid", // ARM64FeatureCPUID + 12: "asimdrdm", // ARM64FeatureASIMDRDM + 13: "jscvt", // ARM64FeatureJSCVT + 14: "fcma", // ARM64FeatureFCMA + 15: "lrcpc", // ARM64FeatureLRCPC + 16: "dcpop", // ARM64FeatureDCPOP + 17: "sha3", // ARM64FeatureSHA3 + 18: "sm3", // ARM64FeatureSM3 + 19: "sm4", // ARM64FeatureSM4 + 20: "asimddp", // ARM64FeatureASIMDDP + 21: "sha512", // ARM64FeatureSHA512 + 22: "sve", // ARM64FeatureSVE + 23: "asimdfhm", // ARM64FeatureASIMDFHM + } +) diff --git a/internal/hardware/cpufeatures_test.go b/internal/hardware/cpufeatures_test.go new file mode 100644 index 0000000000..51b2ad6fd4 --- /dev/null +++ b/internal/hardware/cpufeatures_test.go @@ -0,0 +1,333 @@ +// 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 hardware + +import ( + "slices" + "strconv" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" +) + +func TestEncodeDecodeCPUFeatures(t *testing.T) { + for _, arch := range []string{"amd64", "arm64"} { + t.Run(arch+"/all_features_roundtrip", func(t *testing.T) { + table := cpuFeatureTables[arch] + all := make([]string, 0, len(table.bits)) + for _, bit := range table.bits { + all = append(all, table.names[bit]) + } + mask, err := EncodeCPUFeatures(arch, all) + if err != nil { + t.Fatalf("EncodeCPUFeatures(%q, all) error: %v", arch, err) + } + got, err := DecodeCPUFeatures(arch, mask) + if err != nil { + t.Fatalf("DecodeCPUFeatures(%q, %q) error: %v", arch, mask, err) + } + if diff := cmp.Diff(all, got); diff != "" { + t.Errorf("round-trip mismatch (-want +got):\n%s", diff) + } + }) + + t.Run(arch+"/empty_roundtrip", func(t *testing.T) { + mask, err := EncodeCPUFeatures(arch, nil) + if err != nil { + t.Fatalf("EncodeCPUFeatures(%q, nil) error: %v", arch, err) + } + if mask != "0" { + t.Errorf("EncodeCPUFeatures(%q, nil) = %q, want \"0\"", arch, mask) + } + got, err := DecodeCPUFeatures(arch, mask) + if err != nil { + t.Fatalf("DecodeCPUFeatures(%q, %q) error: %v", arch, mask, err) + } + if len(got) != 0 { + t.Errorf("DecodeCPUFeatures(%q, \"0\") = %v, want empty", arch, got) + } + }) + } + + t.Run("duplicates_and_order_normalized", func(t *testing.T) { + m1, err := EncodeCPUFeatures("amd64", []string{"avx2", "pni", "avx2", ""}) + if err != nil { + t.Fatal(err) + } + m2, err := EncodeCPUFeatures("amd64", []string{"pni", "avx2"}) + if err != nil { + t.Fatal(err) + } + if m1 != m2 { + t.Errorf("EncodeCPUFeatures with duplicates/reordering = %q, want %q", m1, m2) + } + got, err := DecodeCPUFeatures("amd64", m1) + if err != nil { + t.Fatal(err) + } + if diff := cmp.Diff([]string{"pni", "avx2"}, got); diff != "" { + t.Errorf("decoded features mismatch (-want +got):\n%s", diff) + } + }) + + t.Run("errors", func(t *testing.T) { + if _, err := EncodeCPUFeatures("riscv64", []string{"pni"}); err == nil { + t.Error("EncodeCPUFeatures with unsupported arch succeeded") + } + if _, err := EncodeCPUFeatures("amd64", []string{"not_a_real_feature"}); err == nil { + t.Error("EncodeCPUFeatures with unknown feature succeeded") + } + if _, err := DecodeCPUFeatures("riscv64", "0"); err == nil { + t.Error("DecodeCPUFeatures with unsupported arch succeeded") + } + for _, bad := range []string{"", "0x1f", "1F", "07", "xyz", strings.Repeat("f", featureMaskWords*16+1)} { + if _, err := DecodeCPUFeatures("amd64", bad); err == nil { + t.Errorf("DecodeCPUFeatures(%q) succeeded, want a non-canonical value rejected", bad) + } + } + // Bit 16 is CPUID.1:ECX bit 16, which the hardware reserves, so it + // sits inside the table's range with no name; and one past the + // highest named bit has none either. Decoding must name the bit. + amd64 := cpuFeatureTables["amd64"] + lastBit := amd64.bits[len(amd64.bits)-1] + for _, bit := range []int{16, lastBit + 1} { + if _, named := amd64.names[bit]; named { + t.Fatalf("test assumes amd64 bit %d has no name, but it is %q", bit, amd64.names[bit]) + } + var m featureMask + m.set(0) + m.set(bit) + _, err := DecodeCPUFeatures("amd64", m.String()) + if err == nil || !strings.Contains(err.Error(), strconv.Itoa(bit)) { + t.Errorf("DecodeCPUFeatures with unnamed bit %d set = %v, want an error naming the bit", bit, err) + } + } + }) + + t.Run("canonical_hex", func(t *testing.T) { + // Bits 0, 1, 2 and 64: one word boundary crossed, low word zero-padded. + var m featureMask + for _, bit := range []int{0, 1, 2, 64} { + m.set(bit) + } + if got, want := m.String(), "1"+strings.Repeat("0", 15)+"7"; got != want { + t.Errorf("featureMask.String() = %q, want %q", got, want) + } + back, ok := parseFeatureMask(m.String()) + if !ok || back != m { + t.Errorf("parseFeatureMask(%q) = %v, %v; want the original mask", m.String(), back, ok) + } + }) +} + +// TestCPUFeatureTables checks structural invariants of each architecture's +// feature table and pins anchor bits to the values of gVisor's Feature +// constants (32*block + CPUID bit on amd64). A moved anchor means +// cpufeatures_table.go no longer follows the hardware's numbering and stored +// snapshots would decode differently than they were encoded. +func TestCPUFeatureTables(t *testing.T) { + for _, tc := range []struct { + arch, name string + bit int + }{ + {"amd64", "pni", 0}, + {"amd64", "pcid", 17}, // after the reserved bit 16 + {"amd64", "osxsave", 27}, // listed after bit 31 in gVisor's map + {"amd64", "fpu", 32}, // block 1, bit 0 + {"amd64", "avx2", 2*32 + 5}, // block 2 + {"amd64", "syscall", 6*32 + 11}, // sparse block 6 + {"amd64", "avx512_bf16", 8*32 + 5}, + {"arm64", "fp", 0}, + {"arm64", "asimdfhm", 23}, + } { + if got, ok := cpuFeatureTables[tc.arch].index[tc.name]; !ok || got != tc.bit { + t.Errorf("%s %q at bit %d (present=%v), want %d", tc.arch, tc.name, got, ok, tc.bit) + } + } + for arch, table := range cpuFeatureTables { + t.Run(arch, func(t *testing.T) { + if len(table.bits) == 0 { + t.Fatalf("%s table is empty", arch) + } + if !slices.IsSorted(table.bits) || len(table.bits) != len(table.names) { + t.Errorf("%s table bits %v are not the sorted keys of its %d names", arch, table.bits, len(table.names)) + } + var defined featureMask + seen := make(map[string]int, len(table.names)) + for bit, name := range table.names { + defined.set(bit) // newFeatureTable panics at init on a bit outside the mask + if other, dup := seen[name]; dup { + t.Errorf("%s table lists %q at both %d and %d", arch, name, other, bit) + } + seen[name] = bit + if name != strings.TrimSpace(name) || name == "" { + t.Errorf("%s table bit %d is %q, want a trimmed non-empty name", arch, bit, name) + } + if got := table.index[name]; got != bit { + t.Errorf("%s index[%q] = %d, want %d", arch, name, got, bit) + } + } + if defined != table.defined { + t.Errorf("%s table's defined mask %v does not match its names", arch, table.defined) + } + for name, bit := range table.index { + if _, ok := table.names[bit]; !ok { + t.Errorf("%s index maps %q to bit %d, which has no name", arch, name, bit) + } + } + }) + } +} + +// A table entry the mask cannot hold is a panic at package init rather than a +// dropped name. +func TestNewFeatureTableRejectsBitOutsideMask(t *testing.T) { + defer func() { + if recover() == nil { + t.Error("newFeatureTable with a bit outside the mask did not panic") + } + }() + newFeatureTable(map[int]string{featureMaskWords * 64: "beyond"}) +} + +func TestParseCPUInfo(t *testing.T) { + tests := []struct { + name string + arch string + cpuinfo string + wantVendor string + wantModel string + wantErr bool + }{ + { + name: "amd64 Intel", + arch: "amd64", + cpuinfo: `processor : 0 +vendor_id : GenuineIntel +cpu family : 6 +model : 143 +model name : Intel(R) Xeon(R) Platinum 8481C +`, + wantVendor: "GenuineIntel", + wantModel: "6:143", + }, + { + name: "arm64 Neoverse", + arch: "arm64", + cpuinfo: `processor : 0 +BogoMIPS : 2000.00 +Features : fp asimd evtstrm aes pmull sha1 sha2 crc32 atomics +CPU implementer : 0x41 +CPU architecture: 8 +CPU variant : 0x0 +CPU part : 0xd40 +CPU revision : 1 +`, + wantVendor: "0x41", + wantModel: "0x0:0xd40:1", + }, + { + name: "missing fields", + arch: "amd64", + cpuinfo: "vendor_id : GenuineIntel\n", + wantErr: true, + }, + { + name: "unsupported arch", + arch: "riscv64", + cpuinfo: "vendor_id : test\n", + wantErr: true, + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + vendor, model, err := parseCPUInfo(tc.arch, strings.NewReader(tc.cpuinfo)) + if (err != nil) != tc.wantErr { + t.Fatalf("parseCPUInfo() error = %v, wantErr %v", err, tc.wantErr) + } + if vendor != tc.wantVendor || model != tc.wantModel { + t.Errorf("parseCPUInfo() = (%q, %q), want (%q, %q)", vendor, model, tc.wantVendor, tc.wantModel) + } + }) + } +} + +// FuzzCPUFeatures checks, from real `runsc cpu-features` output and whatever +// the fuzzer derives from it, that a feature list round-trips through +// EncodeCPUFeatures and DecodeCPUFeatures, that the encoding is canonical, +// and that subsetOf agrees with set inclusion on the names. +func FuzzCPUFeatures(f *testing.F) { + const ( + broadwellCSV = "fpu,vme,de,pse,tsc,msr,pae,mce,cx8,apic,sep,mtrr,pge,mca,cmov,pat,pse36,clflush,mmx,fxsr,sse,sse2,ss,ht,syscall,nx,pdpe1gb,rdtscp,lm,pni,pclmulqdq,ssse3,fma,cx16,pcid,sse4_1,sse4_2,x2apic,movbe,popcnt,aes,xsave,osxsave,avx,f16c,rdrand,hypervisor,lahf_lm,abm,3dnowprefetch,fsgsbase,tsc_adjust,bmi1,hle,avx2,smep,bmi2,erms,invpcid,rtm,rdseed,adx,smap,xsaveopt,md_clear,arch_capabilities" + sapphireRapidsCSV = "fpu,vme,de,pse,tsc,msr,pae,mce,cx8,apic,sep,mtrr,pge,mca,cmov,pat,pse36,clflush,mmx,fxsr,sse,sse2,ss,ht,syscall,nx,pdpe1gb,rdtscp,lm,pni,pclmulqdq,ssse3,fma,cx16,pcid,sse4_1,sse4_2,x2apic,movbe,popcnt,tsc_deadline_timer,aes,xsave,osxsave,avx,f16c,rdrand,hypervisor,lahf_lm,abm,3dnowprefetch,fsgsbase,tsc_adjust,bmi1,hle,avx2,smep,bmi2,erms,invpcid,rtm,avx512f,avx512dq,rdseed,adx,smap,clfushopt,clwb,avx512cd,sha_ni,avx512bw,avx512vl,xsaveopt,xsavec,xgetbv1,xsaves,avx512vbmi,umip,pku,ospke,waitpkg,avx512_vbmi2,gfni,vaes,vpclmulqdq,avx512_vnni,avx512_bitalg,avx512_vpopcntdq,la57,rdpid,cldemote,movdiri,movdir64b,fsrm,md_clear,serialize,tsxldtrk,amx_bf16,avx512_fp16,amx_tile,amx_int8,spec_ctrl,intel_stibp,flush_l1d,arch_capabilities,spec_ctrl_ssbd,avx_vnni,avx512_bf16" + amdMilanCSV = "fpu,vme,de,pse,tsc,msr,pae,mce,cx8,apic,sep,mtrr,pge,mca,cmov,pat,pse36,clflush,mmx,fxsr,sse,sse2,ht,syscall,nx,mmxext,fxsr_opt,pdpe1gb,rdtscp,lm,pni,pclmulqdq,ssse3,fma,cx16,pcid,sse4_1,sse4_2,x2apic,movbe,popcnt,aes,xsave,osxsave,avx,f16c,rdrand,hypervisor,lahf_lm,cmp_legacy,svm,cr8_legacy,abm,sse4a,misalignsse,3dnowprefetch,osvw,topoext,perfctr_core,fsgsbase,tsc_adjust,bmi1,avx2,smep,bmi2,erms,invpcid,rdseed,adx,smap,clfushopt,clwb,sha_ni,xsaveopt,xsavec,xgetbv1,xsaves,umip,pku,ospke,vaes,vpclmulqdq,rdpid,fsrm" + neoverseN1CSV = "fp,asimd,evtstrm,aes,pmull,sha1,sha2,crc32,atomics,fphp,asimdhp,cpuid,asimdrdm,lrcpc,dcpop,asimddp" + neoverseV2CSV = "fp,asimd,evtstrm,aes,pmull,sha1,sha2,crc32,atomics,fphp,asimdhp,cpuid,asimdrdm,jscvt,fcma,lrcpc,dcpop,sha3,sm3,sm4,asimddp,sha512,sve,asimdfhm" + ) + // A seed the tables cannot encode would only exercise the early return + // below, so a table change that drops one of these names fails here + // rather than quietly weakening the corpus. + for _, seed := range []struct{ arch, csv string }{ + {"amd64", broadwellCSV}, {"amd64", sapphireRapidsCSV}, {"amd64", amdMilanCSV}, + {"arm64", neoverseN1CSV}, {"arm64", neoverseV2CSV}, + } { + if _, err := EncodeCPUFeatures(seed.arch, strings.Split(seed.csv, ",")); err != nil { + f.Fatalf("seed does not encode: %v", err) + } + } + f.Add("amd64", broadwellCSV, sapphireRapidsCSV) + f.Add("amd64", sapphireRapidsCSV, broadwellCSV) + f.Add("amd64", broadwellCSV, amdMilanCSV) + f.Add("arm64", neoverseN1CSV, neoverseV2CSV) + f.Add("arm64", neoverseV2CSV, neoverseN1CSV) + // Linux-synthesized flags runsc never prints; encoding rejects them. + f.Add("amd64", "fpu,sse2,constant_tsc,rep_good,nopl", broadwellCSV) + + f.Fuzz(func(t *testing.T, arch, snapCSV, workerCSV string) { + snapRaw, workerRaw := strings.Split(snapCSV, ","), strings.Split(workerCSV, ",") + snapMask, snapErr := EncodeCPUFeatures(arch, snapRaw) + workerMask, workerErr := EncodeCPUFeatures(arch, workerRaw) + if snapErr != nil || workerErr != nil { + return + } + decoded, err := DecodeCPUFeatures(arch, snapMask) + if err != nil { + t.Fatalf("DecodeCPUFeatures(%q, %q) failed: %v", arch, snapMask, err) + } + if reencoded, err := EncodeCPUFeatures(arch, decoded); err != nil || reencoded != snapMask { + t.Fatalf("round-trip EncodeCPUFeatures(%q, %v) = (%q, %v), want %q", arch, decoded, reencoded, err, snapMask) + } + workerDecoded, err := DecodeCPUFeatures(arch, workerMask) + if err != nil { + t.Fatalf("DecodeCPUFeatures(%q, %q) failed: %v", arch, workerMask, err) + } + wantSubset := true + for _, feat := range decoded { + if !slices.Contains(workerDecoded, feat) { + wantSubset = false + break + } + } + mSnap, okSnap := parseFeatureMask(snapMask) + mWorker, okWorker := parseFeatureMask(workerMask) + if !okSnap || !okWorker || mSnap.String() != snapMask || mWorker.String() != workerMask { + t.Fatalf("non-canonical mask produced: snap=%q worker=%q", snapMask, workerMask) + } + if got := mSnap.subsetOf(mWorker); got != wantSubset { + t.Fatalf("subsetOf(%q, %q) = %v, want %v", snapMask, workerMask, got, wantSubset) + } + }) +} diff --git a/internal/hardware/hardware.go b/internal/hardware/hardware.go index 1de65f19a1..4511b90707 100644 --- a/internal/hardware/hardware.go +++ b/internal/hardware/hardware.go @@ -35,15 +35,26 @@ const ( // digest, computed by atelet. AttrSandboxRuntimeHash = "sandbox_runtime_hash" - // TODO: Add AttrCPUFeatures ("cpu_features") and other compatibility - // attributes as snapshot compatibility expands. + // AttrCPUFeatures is the set of CPU features `runsc cpu-features` reports + // for a gVisor runtime, encoded by EncodeCPUFeatures as a hex bitmask over + // the architecture's feature table. A snapshot's set must be a subset of + // the worker's. + AttrCPUFeatures = "cpu_features" + + // AttrCPUVendor identifies the host CPU vendor (e.g. "GenuineIntel", + // "AuthenticAMD", or arm64 implementer "0x41") reported by microVM + // runtimes. + AttrCPUVendor = "cpu_vendor" + + // AttrCPUModel identifies the host CPU family/model (e.g. "6:143" on amd64 + // or "0x0:0xd40:1" on arm64) reported by microVM runtimes. + AttrCPUModel = "cpu_model" ) -// ProbeHost inspects the current host and returns its v1 VersionedSandboxCompat -// in atelet wire format. -// -// TODO: Probe and populate cpu_features and other host hardware attributes -// (e.g. via CPUID on amd64 and MIDR_EL1 on arm64). +// ProbeHost returns a minimal v1 VersionedSandboxCompat with the current +// host's architecture in atelet wire format, for synthetic workers in tests +// and benchmarks. Production workers report the attributes atelet writes to +// sandbox-runtimes.json instead. func ProbeHost() *ateletpb.VersionedSandboxCompat { return &ateletpb.VersionedSandboxCompat{ SchemaVersion: SchemaVersionV1, @@ -56,47 +67,92 @@ func ProbeHost() *ateletpb.VersionedSandboxCompat { } } -// Matches reports whether a worker's SandboxRuntime can restore a snapshot -// stamped with snap. A nil snapshot SandboxRuntime imposes no constraint. -// Otherwise the sandbox classes must be equal and MatchesCompat must hold. -func Matches(worker, snap *ateapipb.SandboxRuntime) bool { - if snap == nil { +// Requirement is what a worker must run to restore a snapshot: the +// SandboxRuntime the snapshot was stamped with, prepared for matching against +// many workers. The scheduler asks it once per candidate, so the snapshot's +// AttrCPUFeatures is parsed here, once, rather than on every comparison. The +// zero Requirement, like NewRequirement(nil), requires nothing. +type Requirement struct { + snap *ateapipb.SandboxRuntime + compat *ateapipb.VersionedSandboxCompat // snap's version + // cpuFeatures is compat's AttrCPUFeatures parsed, when it has one. + // badCPUFeatures records one that is not a canonical mask, which no + // worker satisfies. + cpuFeatures featureMask + badCPUFeatures bool +} + +// NewRequirement prepares snap for matching. A nil snap requires nothing. +func NewRequirement(snap *ateapipb.SandboxRuntime) Requirement { + r := compatRequirement(snap.GetVersion()) + r.snap = snap + return r +} + +func compatRequirement(compat *ateapipb.VersionedSandboxCompat) Requirement { + r := Requirement{compat: compat} + for _, a := range compat.GetAttributes() { + if a.GetKey() == AttrCPUFeatures { + var ok bool + r.cpuFeatures, ok = parseFeatureMask(a.GetValue()) + r.badCPUFeatures = !ok + break + } + } + return r +} + +// Runtime returns the SandboxRuntime the requirement was prepared from, or +// nil when it requires nothing. +func (r Requirement) Runtime() *ateapipb.SandboxRuntime { + return r.snap +} + +// Matches reports whether a worker's SandboxRuntime can restore the +// snapshot. A requirement of nothing is met by any worker. Otherwise the +// sandbox classes must be equal and the compat must match. +func (r Requirement) Matches(worker *ateapipb.SandboxRuntime) bool { + if r.snap == nil { return true } - if worker == nil || worker.GetSandboxClass() != snap.GetSandboxClass() { + if worker == nil || worker.GetSandboxClass() != r.snap.GetSandboxClass() { return false } - return MatchesCompat(worker.GetVersion(), snap.GetVersion()) + return r.matchesCompat(worker.GetVersion()) } -// MatchesCompat reports whether worker can restore a snapshot stamped with -// snap: the same non-empty schema version, and every attribute on snap -// present on worker with an equal value. Keys only the worker has are -// ignored, so a worker can start reporting a new key under the same schema -// version without invalidating the snapshots taken before it; removing or -// recomputing a key is a new schema version, under which older snapshots no -// longer match. -func MatchesCompat(worker, snap *ateapipb.VersionedSandboxCompat) bool { - if worker == nil || snap == nil { +// matchesCompat reports whether worker can restore the snapshot: the same +// non-empty schema version, and every attribute on the snapshot satisfied by +// worker. AttrCPUFeatures is satisfied when the snapshot's feature bitmask is +// a subset of the worker's; every other attribute must be present on worker +// with an equal value. Keys only the worker has are ignored, so a worker can +// start reporting a new key under the same schema version without +// invalidating the snapshots taken before it; removing or recomputing a key +// is a new schema version, under which older snapshots no longer match. +func (r Requirement) matchesCompat(worker *ateapipb.VersionedSandboxCompat) bool { + if worker == nil || r.compat == nil { return false } - if worker.GetSchemaVersion() == "" || worker.GetSchemaVersion() != snap.GetSchemaVersion() { + if worker.GetSchemaVersion() == "" || worker.GetSchemaVersion() != r.compat.GetSchemaVersion() { + return false + } + if r.badCPUFeatures { return false } wAttrs := worker.GetAttributes() - sAttrs := snap.GetAttributes() // API validation bounds attributes to at most 32 entries with unique keys, // so the nested scan is small and avoids map allocation on the hot path. - for _, sa := range sAttrs { + for _, sa := range r.compat.GetAttributes() { found := false for _, wa := range wAttrs { - if wa.GetKey() == sa.GetKey() { - if wa.GetValue() != sa.GetValue() { - return false - } - found = true - break + if wa.GetKey() != sa.GetKey() { + continue + } + if !r.attributeSatisfied(sa.GetKey(), wa.GetValue(), sa.GetValue()) { + return false } + found = true + break } if !found { return false @@ -105,42 +161,78 @@ func MatchesCompat(worker, snap *ateapipb.VersionedSandboxCompat) bool { return true } -// MatchingRuntime returns the SandboxRuntime from status that can restore a -// snapshot stamped with snap (checking DefaultRuntime first, then -// RestorableRuntimes in order), or nil if none matches. When snap is nil, -// DefaultRuntime is returned. -func MatchingRuntime(status *ateapipb.WorkerStatus, snap *ateapipb.SandboxRuntime) *ateapipb.SandboxRuntime { - if snap == nil { - return status.GetDefaultRuntime() +// attributeSatisfied reports whether a worker's value for key satisfies the +// snapshot's. AttrCPUFeatures is a set the worker's must include, compared +// against the mask parsed by NewRequirement; every other attribute must be +// equal. +func (r Requirement) attributeSatisfied(key, workerVal, snapVal string) bool { + if key == AttrCPUFeatures { + w, ok := parseFeatureMask(workerVal) + return ok && r.cpuFeatures.subsetOf(w) } - if def := status.GetDefaultRuntime(); Matches(def, snap) { + return workerVal == snapVal +} + +// MatchingRuntime returns the SandboxRuntime from status that can restore +// the snapshot (checking DefaultRuntime first, then RestorableRuntimes in +// order), or nil if none matches. A requirement of nothing returns +// DefaultRuntime. +func (r Requirement) MatchingRuntime(status *ateapipb.WorkerStatus) *ateapipb.SandboxRuntime { + if def := status.GetDefaultRuntime(); r.snap == nil || r.Matches(def) { return def } for _, rt := range status.GetRestorableRuntimes() { - if Matches(rt, snap) { + if r.Matches(rt) { return rt } } return nil } -// WorkerMatches reports whether a worker with status can restore a snapshot -// stamped with snap. A nil snapshot SandboxRuntime imposes no constraint. -func WorkerMatches(status *ateapipb.WorkerStatus, snap *ateapipb.SandboxRuntime) bool { - if snap == nil { - return true - } - return MatchingRuntime(status, snap) != nil +// WorkerMatches reports whether a worker with status can restore the +// snapshot. A requirement of nothing is met by any worker. +func (r Requirement) WorkerMatches(status *ateapipb.WorkerStatus) bool { + return r.snap == nil || r.MatchingRuntime(status) != nil } // RuntimeFor returns the runtime a worker with status runs an actor under -// whose snapshot is stamped with snap: the matching runtime when it can -// restore the snapshot, else the DefaultRuntime the guest boots fresh under. -// This is what an assignment records and what the actor's next checkpoint -// is stamped with. -func RuntimeFor(status *ateapipb.WorkerStatus, snap *ateapipb.SandboxRuntime) *ateapipb.SandboxRuntime { - if rt := MatchingRuntime(status, snap); rt != nil { +// whose snapshot is stamped with the required runtime: the matching runtime +// when it can restore the snapshot, else the DefaultRuntime the guest boots +// fresh under. This is what an assignment records and what the actor's next +// checkpoint is stamped with. +func (r Requirement) RuntimeFor(status *ateapipb.WorkerStatus) *ateapipb.SandboxRuntime { + if rt := r.MatchingRuntime(status); rt != nil { return rt } return status.GetDefaultRuntime() } + +// Matches reports whether a worker's SandboxRuntime can restore a snapshot +// stamped with snap, for one comparison; a caller holding one snap against +// many workers should prepare a Requirement once. A nil snap imposes no +// constraint. +func Matches(worker, snap *ateapipb.SandboxRuntime) bool { + return NewRequirement(snap).Matches(worker) +} + +// MatchesCompat reports whether worker can restore a snapshot stamped with +// snap, for one comparison. Unlike Matches, a nil snap is a stamp missing its +// identity and matches nothing. +func MatchesCompat(worker, snap *ateapipb.VersionedSandboxCompat) bool { + return compatRequirement(snap).matchesCompat(worker) +} + +// MatchingRuntime is NewRequirement(snap).MatchingRuntime(status). +func MatchingRuntime(status *ateapipb.WorkerStatus, snap *ateapipb.SandboxRuntime) *ateapipb.SandboxRuntime { + return NewRequirement(snap).MatchingRuntime(status) +} + +// WorkerMatches is NewRequirement(snap).WorkerMatches(status). +func WorkerMatches(status *ateapipb.WorkerStatus, snap *ateapipb.SandboxRuntime) bool { + return NewRequirement(snap).WorkerMatches(status) +} + +// RuntimeFor is NewRequirement(snap).RuntimeFor(status). +func RuntimeFor(status *ateapipb.WorkerStatus, snap *ateapipb.SandboxRuntime) *ateapipb.SandboxRuntime { + return NewRequirement(snap).RuntimeFor(status) +} diff --git a/internal/hardware/hardware_test.go b/internal/hardware/hardware_test.go index 6ea10ae6d8..ee50f280dc 100644 --- a/internal/hardware/hardware_test.go +++ b/internal/hardware/hardware_test.go @@ -44,28 +44,28 @@ func TestMatches(t *testing.T) { } amd64GVisor := runtimeWith("gvisor", "v1", &ateapipb.AttributeEntry{Key: AttrArchitecture, Value: "amd64"}, - &ateapipb.AttributeEntry{Key: "cpu_features", Value: "GenuineIntel"}, + &ateapipb.AttributeEntry{Key: AttrCPUFeatures, Value: "7"}, ) amd64GVisorReordered := runtimeWith("gvisor", "v1", - &ateapipb.AttributeEntry{Key: "cpu_features", Value: "GenuineIntel"}, + &ateapipb.AttributeEntry{Key: AttrCPUFeatures, Value: "7"}, &ateapipb.AttributeEntry{Key: AttrArchitecture, Value: "amd64"}, ) arm64GVisor := runtimeWith("gvisor", "v1", &ateapipb.AttributeEntry{Key: AttrArchitecture, Value: "arm64"}, - &ateapipb.AttributeEntry{Key: "cpu_features", Value: "GenuineIntel"}, + &ateapipb.AttributeEntry{Key: AttrCPUFeatures, Value: "7"}, ) amd64MicroVM := runtimeWith("microvm", "v1", &ateapipb.AttributeEntry{Key: AttrArchitecture, Value: "amd64"}, - &ateapipb.AttributeEntry{Key: "cpu_features", Value: "GenuineIntel"}, + &ateapipb.AttributeEntry{Key: AttrCPUFeatures, Value: "7"}, ) amd64GVisorV2 := runtimeWith("gvisor", "v2", &ateapipb.AttributeEntry{Key: AttrArchitecture, Value: "amd64"}, - &ateapipb.AttributeEntry{Key: "cpu_features", Value: "GenuineIntel"}, + &ateapipb.AttributeEntry{Key: AttrCPUFeatures, Value: "7"}, ) amd64GVisorExtraAttr := runtimeWith("gvisor", "v1", &ateapipb.AttributeEntry{Key: AttrArchitecture, Value: "amd64"}, - &ateapipb.AttributeEntry{Key: "cpu_features", Value: "GenuineIntel"}, - &ateapipb.AttributeEntry{Key: "gvisor_asset_hash", Value: "abc"}, + &ateapipb.AttributeEntry{Key: AttrCPUFeatures, Value: "7"}, + &ateapipb.AttributeEntry{Key: AttrSandboxRuntimeHash, Value: "sha256:abc"}, ) tests := []struct { @@ -100,8 +100,24 @@ func TestMatchesCompat(t *testing.T) { } amd64 := &ateapipb.AttributeEntry{Key: AttrArchitecture, Value: "amd64"} arm64 := &ateapipb.AttributeEntry{Key: AttrArchitecture, Value: "arm64"} - cpu := &ateapipb.AttributeEntry{Key: "cpu_features", Value: "GenuineIntel"} - hash := &ateapipb.AttributeEntry{Key: "gvisor_asset_hash", Value: "abc"} + hash := &ateapipb.AttributeEntry{Key: AttrSandboxRuntimeHash, Value: "sha256:abc"} + cpuBits := func(val string) *ateapipb.AttributeEntry { + return &ateapipb.AttributeEntry{Key: AttrCPUFeatures, Value: val} + } + + // Build >64-bit masks so the subset check spans mask words. + highSnap, err := EncodeCPUFeatures("amd64", []string{"pni", "avx2", "avx512_bf16"}) + if err != nil { + t.Fatal(err) + } + highWorkerSuperset, err := EncodeCPUFeatures("amd64", []string{"pni", "sse2", "avx2", "avx_vnni", "avx512_bf16"}) + if err != nil { + t.Fatal(err) + } + highWorkerMissingOne, err := EncodeCPUFeatures("amd64", []string{"pni", "sse2", "avx2", "avx_vnni"}) + if err != nil { + t.Fatal(err) + } tests := []struct { name string @@ -109,10 +125,19 @@ func TestMatchesCompat(t *testing.T) { want bool }{ {"equal", compat("v1", amd64), compat("v1", amd64), true}, - {"key only the worker has is ignored", compat("v1", amd64, cpu), compat("v1", amd64), true}, - {"key only the snapshot has fails", compat("v1", amd64), compat("v1", amd64, cpu), false}, - {"key only the snapshot has fails even at equal count", compat("v1", amd64, cpu), compat("v1", amd64, hash), false}, + {"key only the worker has is ignored", compat("v1", amd64, cpuBits("7")), compat("v1", amd64), true}, + {"key only the snapshot has fails", compat("v1", amd64), compat("v1", amd64, cpuBits("7")), false}, + {"key only the snapshot has fails even at equal count", compat("v1", amd64, cpuBits("7")), compat("v1", amd64, hash), false}, {"value differs", compat("v1", arm64), compat("v1", amd64), false}, + {"cpu_features exact match", compat("v1", amd64, cpuBits(highSnap)), compat("v1", amd64, cpuBits(highSnap)), true}, + {"cpu_features worker is superset of snapshot", compat("v1", amd64, cpuBits(highWorkerSuperset)), compat("v1", amd64, cpuBits(highSnap)), true}, + {"cpu_features worker missing a snapshot feature bit", compat("v1", amd64, cpuBits(highWorkerMissingOne)), compat("v1", amd64, cpuBits(highSnap)), false}, + {"cpu_features empty snapshot set matches any worker", compat("v1", amd64, cpuBits("7")), compat("v1", amd64, cpuBits("0")), true}, + {"cpu_features empty worker set matches nothing", compat("v1", amd64, cpuBits("0")), compat("v1", amd64, cpuBits("7")), false}, + {"cpu_features invalid worker bitmask fails", compat("v1", amd64, cpuBits("not-a-number")), compat("v1", amd64, cpuBits("7")), false}, + {"cpu_features invalid snapshot bitmask fails", compat("v1", amd64, cpuBits("7")), compat("v1", amd64, cpuBits("-1")), false}, + {"cpu_features non-canonical bitmask fails", compat("v1", amd64, cpuBits("07")), compat("v1", amd64, cpuBits("7")), false}, + {"cpu_features uppercase hex fails", compat("v1", amd64, cpuBits("1F")), compat("v1", amd64, cpuBits("7")), false}, {"schema version differs", compat("v2", amd64), compat("v1", amd64), false}, {"both schema versions empty", compat("", amd64), compat("", amd64), false}, {"no attributes on either", compat("v1"), compat("v1"), true}, @@ -131,6 +156,60 @@ func TestMatchesCompat(t *testing.T) { } } +// A Requirement is prepared once and asked of many workers, so what it keeps +// of the snapshot must hold across comparisons, and the zero value must +// require nothing. +func TestRequirement(t *testing.T) { + rt := func(cpu string) *ateapipb.SandboxRuntime { + return &ateapipb.SandboxRuntime{ + SandboxClass: "gvisor", + Version: &ateapipb.VersionedSandboxCompat{ + SchemaVersion: SchemaVersionV1, + Attributes: []*ateapipb.AttributeEntry{ + {Key: AttrArchitecture, Value: "amd64"}, + {Key: AttrCPUFeatures, Value: cpu}, + }, + }, + } + } + snap := rt("7") + req := NewRequirement(snap) + if req.Runtime() != snap { + t.Errorf("Runtime() = %v, want the prepared runtime", req.Runtime()) + } + for _, tc := range []struct { + name string + worker *ateapipb.SandboxRuntime + want bool + }{ + {"worker superset", rt("f"), true}, + {"worker missing a bit", rt("3"), false}, + {"worker mask invalid", rt("zz"), false}, + {"worker superset after misses", rt("7"), true}, + } { + if got := req.Matches(tc.worker); got != tc.want { + t.Errorf("%s: Matches() = %v, want %v", tc.name, got, tc.want) + } + } + status := &ateapipb.WorkerStatus{DefaultRuntime: rt("3"), RestorableRuntimes: []*ateapipb.SandboxRuntime{rt("f")}} + if got := req.MatchingRuntime(status); got != status.RestorableRuntimes[0] { + t.Errorf("MatchingRuntime() = %v, want the restorable superset", got) + } + + // A snapshot mask that does not parse is remembered as matching nothing. + if NewRequirement(rt("zz")).Matches(rt("f")) { + t.Error("Matches() with an invalid snapshot mask = true, want false") + } + + var none Requirement + if none.Runtime() != nil || !none.Matches(nil) || !none.WorkerMatches(nil) { + t.Error("the zero Requirement must require nothing") + } + if got := none.RuntimeFor(status); got != status.DefaultRuntime { + t.Errorf("zero Requirement RuntimeFor() = %v, want DefaultRuntime", got) + } +} + func TestMatchingRuntimeAndWorkerMatches(t *testing.T) { rt := func(class, arch, hash string) *ateapipb.SandboxRuntime { attrs := []*ateapipb.AttributeEntry{{Key: AttrArchitecture, Value: arch}} diff --git a/internal/nodepath/nodepath.go b/internal/nodepath/nodepath.go index 5743f40310..2eb3431cbd 100644 --- a/internal/nodepath/nodepath.go +++ b/internal/nodepath/nodepath.go @@ -58,7 +58,7 @@ func AteletOTLPSocketPath() string { // WorkersDir is the parent of every per-worker directory, so listing it is how // a scraper with no prior knowledge discovers the node's workers. atelet -// creates WorkerPath(podUID) once it writes the pod's sandbox versions file and +// creates WorkerPath(podUID) once it writes the pod's sandbox runtimes file and // removes it when the pod is deleted; the worker also MkdirAlls it at boot. A // directory may exist before its worker listens on its socket. func WorkersDir() string { @@ -76,11 +76,20 @@ func WorkerSocketPath(podUID string) string { ) } -// SandboxVersionsFileName is the base name of the file in WorkerPath(podUID) -// where atelet keeps the sandbox versions of a worker's pool, a -// sandboxversions.File. A runtime it lists may not have its assets on the +// SandboxRuntimesFileName is the base name of the file in WorkerPath(podUID) +// where atelet keeps the sandbox runtimes of a worker's pool, a +// sandboxruntimes.File. A runtime it lists may not have its assets on the // node yet. -const SandboxVersionsFileName = "sandbox-versions.json" +const SandboxRuntimesFileName = "sandbox-runtimes.json" + +// WorkerSandboxRuntimesPath is the path of the sandbox runtimes file atelet +// writes for the worker pod with the given UID. +func WorkerSandboxRuntimesPath(podUID string) string { + return filepath.Join( + WorkerPath(podUID), + SandboxRuntimesFileName, + ) +} // ActorNetNSName names an actor's sandbox network namespace. func ActorNetNSName(actorUID string) string { diff --git a/internal/proto/ateletpb/atelet.pb.go b/internal/proto/ateletpb/atelet.pb.go index 0296359eb0..4bef1e0548 100644 --- a/internal/proto/ateletpb/atelet.pb.go +++ b/internal/proto/ateletpb/atelet.pb.go @@ -366,12 +366,14 @@ type VersionedSandboxCompat struct { // +k8s:maxLength=64 SchemaVersion string `protobuf:"bytes,1,opt,name=schema_version,json=schemaVersion,proto3" json:"schema_version,omitempty"` // Key-value compatibility attributes defined by this schema version, covering - // both runtime assets (e.g. "gvisor_asset_hash") and effective guest hardware - // features after masking (e.g. "architecture", "cpu_features"). + // both runtime assets (e.g. "sandbox_runtime_hash") and effective guest + // hardware features after masking (e.g. "architecture", "cpu_features"). // Matched against a snapshot under the same schema_version: every attribute - // on the snapshot must be present on the worker with an equal value; keys - // only the worker has are ignored. Bump schema_version when a key's meaning - // changes or older snapshots must no longer match. + // on the snapshot must be present on the worker with an equal value (except + // "cpu_features", where the snapshot's hexadecimal bitmask must be a bitwise + // subset of the worker's); keys only the worker has are ignored. Bump + // schema_version when a key's meaning changes or older snapshots must no + // longer match. // Order as sent does not matter: ate-api-server sorts attributes by key // before recording them. // @@ -431,7 +433,7 @@ func (x *VersionedSandboxCompat) GetAttributes() []*AttributeEntry { // AttributeEntry is a single key-value compatibility attribute. type AttributeEntry struct { state protoimpl.MessageState `protogen:"open.v1"` - // Attribute name (e.g. "architecture", "cpu_features", "gvisor_asset_hash"). + // Attribute name (e.g. "architecture", "cpu_features", "sandbox_runtime_hash"). // // +k8s:required // +k8s:maxLength=128 diff --git a/internal/proto/ateletpb/atelet.proto b/internal/proto/ateletpb/atelet.proto index 717875e9bd..94fbbdefbd 100644 --- a/internal/proto/ateletpb/atelet.proto +++ b/internal/proto/ateletpb/atelet.proto @@ -110,12 +110,14 @@ message VersionedSandboxCompat { string schema_version = 1; // Key-value compatibility attributes defined by this schema version, covering - // both runtime assets (e.g. "gvisor_asset_hash") and effective guest hardware - // features after masking (e.g. "architecture", "cpu_features"). + // both runtime assets (e.g. "sandbox_runtime_hash") and effective guest + // hardware features after masking (e.g. "architecture", "cpu_features"). // Matched against a snapshot under the same schema_version: every attribute - // on the snapshot must be present on the worker with an equal value; keys - // only the worker has are ignored. Bump schema_version when a key's meaning - // changes or older snapshots must no longer match. + // on the snapshot must be present on the worker with an equal value (except + // "cpu_features", where the snapshot's hexadecimal bitmask must be a bitwise + // subset of the worker's); keys only the worker has are ignored. Bump + // schema_version when a key's meaning changes or older snapshots must no + // longer match. // Order as sent does not matter: ate-api-server sorts attributes by key // before recording them. // @@ -128,7 +130,7 @@ message VersionedSandboxCompat { // AttributeEntry is a single key-value compatibility attribute. message AttributeEntry { - // Attribute name (e.g. "architecture", "cpu_features", "gvisor_asset_hash"). + // Attribute name (e.g. "architecture", "cpu_features", "sandbox_runtime_hash"). // // +k8s:required // +k8s:maxLength=128 diff --git a/internal/sandboxversions/sandboxversions.go b/internal/sandboxruntimes/sandboxruntimes.go similarity index 86% rename from internal/sandboxversions/sandboxversions.go rename to internal/sandboxruntimes/sandboxruntimes.go index 3ae871c9f0..34c6ca8c6b 100644 --- a/internal/sandboxversions/sandboxversions.go +++ b/internal/sandboxruntimes/sandboxruntimes.go @@ -12,10 +12,10 @@ // See the License for the specific language governing permissions and // limitations under the License. -// Package sandboxversions reads and writes a worker's sandbox versions file, +// Package sandboxruntimes reads and writes a worker's sandbox runtimes file, // which atelet keeps in the worker's directory (nodepath.WorkerPath) as -// nodepath.SandboxVersionsFileName. -package sandboxversions +// nodepath.SandboxRuntimesFileName. +package sandboxruntimes import ( "bytes" @@ -25,11 +25,10 @@ import ( "path/filepath" ) -// File is the content of a worker's sandbox versions file: the sandbox +// File is the content of a worker's sandbox runtimes file: the sandbox // runtimes the worker can run, as the compatibility attributes atelet computed -// for them. The worker adds each runtime's attributes to the ones it probes -// itself and registers the result as RegisterWorkerRequest's default_runtime -// and restorable_runtimes. +// for them. The worker registers DefaultRuntime and RestorableRuntimes as +// RegisterWorkerRequest's default_runtime and restorable_runtimes. type File struct { // DefaultRuntime is the runtime new and cold-booted actors start under, // from the default version of the pool's SandboxConfig. @@ -46,7 +45,7 @@ type Attributes map[string]string // tempPrefix starts the name of every temporary file Write renames into // place. It is dot-prefixed so a directory listing does not show one. -const tempPrefix = ".sandbox-versions-" +const tempPrefix = ".sandbox-runtimes-" // Write atomically replaces the file at path with v, unless it already holds // the same content. It reports whether it wrote. diff --git a/internal/sandboxversions/sandboxversions_test.go b/internal/sandboxruntimes/sandboxruntimes_test.go similarity index 91% rename from internal/sandboxversions/sandboxversions_test.go rename to internal/sandboxruntimes/sandboxruntimes_test.go index 5665d6e6c2..0562caa464 100644 --- a/internal/sandboxversions/sandboxversions_test.go +++ b/internal/sandboxruntimes/sandboxruntimes_test.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package sandboxversions +package sandboxruntimes import ( "errors" @@ -26,7 +26,7 @@ import ( "github.com/google/go-cmp/cmp" ) -func testVersions(defaultHash string) *File { +func testRuntimes(defaultHash string) *File { return &File{ DefaultRuntime: Attributes{"sandbox_runtime_hash": defaultHash}, RestorableRuntimes: []Attributes{{"sandbox_runtime_hash": "sha256:41ab"}}, @@ -44,22 +44,22 @@ func TestWrite(t *testing.T) { wantErr bool }{{ name: "new file", - v: testVersions("sha256:7c2e"), + v: testRuntimes("sha256:7c2e"), wantWrote: true, }, { name: "equal content is not rewritten", - existing: testVersions("sha256:7c2e"), - v: testVersions("sha256:7c2e"), + existing: testRuntimes("sha256:7c2e"), + v: testRuntimes("sha256:7c2e"), wantWrote: false, }, { name: "different content replaces the file", - existing: testVersions("sha256:7c2e"), - v: testVersions("sha256:9d01"), + existing: testRuntimes("sha256:7c2e"), + v: testRuntimes("sha256:9d01"), wantWrote: true, }, { name: "missing directory", noDir: true, - v: testVersions("sha256:7c2e"), + v: testRuntimes("sha256:7c2e"), wantErr: true, }} for _, tt := range tests { @@ -68,7 +68,7 @@ func TestWrite(t *testing.T) { if tt.noDir { dir = filepath.Join(dir, "missing") } - path := filepath.Join(dir, "sandbox-versions.json") + path := filepath.Join(dir, "sandbox-runtimes.json") var before time.Time if tt.existing != nil { if _, err := Write(path, tt.existing); err != nil { @@ -156,7 +156,7 @@ func TestRead(t *testing.T) { }} for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - path := filepath.Join(t.TempDir(), "sandbox-versions.json") + path := filepath.Join(t.TempDir(), "sandbox-runtimes.json") if tt.contents != "" { if err := os.WriteFile(path, []byte(tt.contents), 0o600); err != nil { t.Fatal(err) diff --git a/manifests/ate-install/atelet.yaml b/manifests/ate-install/atelet.yaml index 6e00986155..7ff7371dd3 100644 --- a/manifests/ate-install/atelet.yaml +++ b/manifests/ate-install/atelet.yaml @@ -34,12 +34,12 @@ rules: # SandboxConfigs are watched to pre-download sandbox assets into the node's # cache before the first actor needs them (see cmd/atelet/sandbox_prewarm.go), # and to write the runtimes of the configs local workers use to each worker's -# sandbox versions file (see cmd/atelet/worker_sandbox_syncer.go). +# sandbox runtimes file (see cmd/atelet/worker_sandbox_syncer.go). - apiGroups: ["ate.dev"] resources: ["sandboxconfigs"] verbs: ["get", "list", "watch"] # WorkerPools are watched to find the SandboxConfig each local worker's pool -# uses, whose versions atelet writes to the worker's sandbox versions file +# uses, whose versions atelet writes to the worker's sandbox runtimes file # (see cmd/atelet/worker_sandbox_syncer.go). - apiGroups: ["ate.dev"] resources: ["workerpools"] diff --git a/pkg/proto/ateapipb/ateapi.pb.go b/pkg/proto/ateapipb/ateapi.pb.go index 2ca325c45b..f2263209df 100644 --- a/pkg/proto/ateapipb/ateapi.pb.go +++ b/pkg/proto/ateapipb/ateapi.pb.go @@ -7563,14 +7563,15 @@ type VersionedSandboxCompat struct { // +k8s:maxLength=64 SchemaVersion string `protobuf:"bytes,1,opt,name=schema_version,json=schemaVersion,proto3" json:"schema_version,omitempty"` // Key-value compatibility attributes defined by this schema version, covering - // both runtime assets (e.g. "gvisor_asset_hash") and effective guest hardware - // features after masking (e.g. "architecture", "cpu_features"). + // both runtime assets (e.g. "sandbox_runtime_hash") and effective guest + // hardware features after masking (e.g. "architecture", "cpu_features"). // Matched against a snapshot under the same schema_version: every attribute - // on the snapshot must be present on the worker with an equal value; keys - // only the worker has are ignored, so a worker can gain keys under one - // schema_version without invalidating older snapshots. Removing or - // recomputing a key is a new schema_version, under which older snapshots - // no longer match. + // on the snapshot must be present on the worker with an equal value (except + // "cpu_features", where the snapshot's hexadecimal bitmask must be a bitwise + // subset of the worker's); keys only the worker has are ignored, so a worker + // can gain keys under one schema_version without invalidating older + // snapshots. Removing or recomputing a key is a new schema_version, under + // which older snapshots no longer match. // Order as sent does not matter: ate-api-server sorts attributes by key // before recording them. // @@ -7630,7 +7631,7 @@ func (x *VersionedSandboxCompat) GetAttributes() []*AttributeEntry { // AttributeEntry is a single key-value compatibility attribute. type AttributeEntry struct { state protoimpl.MessageState `protogen:"open.v1"` - // Attribute name (e.g. "architecture", "cpu_features", "gvisor_asset_hash"). + // Attribute name (e.g. "architecture", "cpu_features", "sandbox_runtime_hash"). // // +k8s:required // +k8s:maxLength=128 diff --git a/pkg/proto/ateapipb/ateapi.proto b/pkg/proto/ateapipb/ateapi.proto index fe2a4cf73f..0ba0d4a3be 100644 --- a/pkg/proto/ateapipb/ateapi.proto +++ b/pkg/proto/ateapipb/ateapi.proto @@ -2435,14 +2435,15 @@ message VersionedSandboxCompat { string schema_version = 1; // Key-value compatibility attributes defined by this schema version, covering - // both runtime assets (e.g. "gvisor_asset_hash") and effective guest hardware - // features after masking (e.g. "architecture", "cpu_features"). + // both runtime assets (e.g. "sandbox_runtime_hash") and effective guest + // hardware features after masking (e.g. "architecture", "cpu_features"). // Matched against a snapshot under the same schema_version: every attribute - // on the snapshot must be present on the worker with an equal value; keys - // only the worker has are ignored, so a worker can gain keys under one - // schema_version without invalidating older snapshots. Removing or - // recomputing a key is a new schema_version, under which older snapshots - // no longer match. + // on the snapshot must be present on the worker with an equal value (except + // "cpu_features", where the snapshot's hexadecimal bitmask must be a bitwise + // subset of the worker's); keys only the worker has are ignored, so a worker + // can gain keys under one schema_version without invalidating older + // snapshots. Removing or recomputing a key is a new schema_version, under + // which older snapshots no longer match. // Order as sent does not matter: ate-api-server sorts attributes by key // before recording them. // @@ -2455,7 +2456,7 @@ message VersionedSandboxCompat { // AttributeEntry is a single key-value compatibility attribute. message AttributeEntry { - // Attribute name (e.g. "architecture", "cpu_features", "gvisor_asset_hash"). + // Attribute name (e.g. "architecture", "cpu_features", "sandbox_runtime_hash"). // // +k8s:required // +k8s:maxLength=128 From c4518a13759a0b94fbad808afa10cd3ff5408b98 Mon Sep 17 00:00:00 2001 From: Shruti Nair Date: Fri, 16 Oct 2026 05:36:29 +0000 Subject: [PATCH 31/31] ate-setup: enforce ateapi authorization by default and manage access with kubectl ate (#2591) Part of #1563. Fixes https://github.com/agent-substrate/substrate/issues/2103, https://github.com/agent-substrate/substrate/issues/2106, https://github.com/agent-substrate/substrate/issues/2186 ## What - **Authz on by default** in ate-setup, through two settings: `ateapi.authz.enabled` and `ateapi.authz.bootstrapOwners`. The bootstrap owner defaults to the shared `ate-client` ServiceAccount, so demos, benchmarks and CI keep working. ate-setup and `kubectl ate` warn about it, and the docs tell production installs to name their own owners. - **`kubectl ate access-policy`** get/create/update/delete, for an atespace (`-a`) or the global scope (`--global`). - **Tests:** an authz e2e suite (basic flow plus security regressions), `model_test.fga.yaml` run in CI, and Postgres tests that fail rather than skip when CI requires Docker. - **Benchmarks:** the nightly run adds `benchmark-runner` as a bootstrap owner. - **Docs:** new `docs/authorization.md`, covering roles, the checks every RPC makes, managing access and limitations. Follow-up: replace the `ate-client` default with per-caller identities (token options, a `kubectl ate` credential plugin, issuer-qualified principals), then require explicit owners. ## Testing Unit tests and `hack/verify-all.sh` pass. The authz e2e suite runs in the CI e2e lanes. --- AGENTS.md | 1 + benchmarking/README.md | 13 + benchmarking/automation/README.md | 4 +- benchmarking/automation/orchestrator.py | 15 +- benchmarking/automation/test_orchestrator.py | 24 ++ cmd/ate-setup/internal/config/config.go | 7 + cmd/ate-setup/internal/config/config_test.go | 64 +++ cmd/ate-setup/internal/config/load.go | 37 +- cmd/ate-setup/internal/config/setting.go | 11 + cmd/ate-setup/internal/steps/apienv.go | 83 ++++ cmd/ate-setup/internal/steps/apienv_test.go | 140 +++++++ cmd/ateapi/internal/authz/authz_test.go | 11 +- cmd/ateapi/internal/authz/authz_util.go | 3 + cmd/ateapi/internal/authz/authz_util_test.go | 4 + cmd/ateapi/internal/authz/interceptor_test.go | 13 + cmd/ateapi/internal/authz/model.fga | 12 +- cmd/ateapi/internal/authz/model_fga_test.go | 134 ++++++ cmd/ateapi/internal/authz/model_test.fga.yaml | 32 ++ cmd/ateapi/internal/authz/registry.go | 7 +- .../internal/store/atepg/common_test.go | 1 + .../internal/store/storetest/storetest.go | 1 + cmd/ateapi/main.go | 28 +- cmd/ateapi/main_test.go | 7 - cmd/kubectl-ate/internal/cmd/access_policy.go | 389 ++++++++++++++++++ .../internal/cmd/access_policy_test.go | 331 +++++++++++++++ cmd/kubectl-ate/internal/cmd/verbose.go | 13 + cmd/kubectl-ate/internal/cmd/verbose_test.go | 20 + .../internal/printer/access_policy.go | 50 +++ .../internal/printer/access_policy_test.go | 79 ++++ docs/architecture.md | 6 +- docs/authentication.md | 9 +- docs/authorization.md | 347 ++++++++++++++++ docs/operator-install.md | 4 + hack/install-ate.sh | 7 + internal/ateclient/builder.go | 18 + internal/ateclient/builder_test.go | 21 + internal/e2e/suites/authz/authz_test.go | 46 +++ internal/e2e/suites/authz/flow_test.go | 78 ++++ internal/e2e/suites/authz/helpers_test.go | 362 ++++++++++++++++ .../suites/authz/security_regression_test.go | 318 ++++++++++++++ internal/e2e/suites/authz/testmain_test.go | 24 ++ internal/installdefaults/installdefaults.go | 7 + .../installdefaults/installdefaults_test.go | 8 + 43 files changed, 2741 insertions(+), 48 deletions(-) create mode 100644 cmd/ateapi/internal/authz/model_fga_test.go create mode 100644 cmd/kubectl-ate/internal/cmd/access_policy.go create mode 100644 cmd/kubectl-ate/internal/cmd/access_policy_test.go create mode 100644 cmd/kubectl-ate/internal/printer/access_policy.go create mode 100644 cmd/kubectl-ate/internal/printer/access_policy_test.go create mode 100644 docs/authorization.md create mode 100644 internal/e2e/suites/authz/authz_test.go create mode 100644 internal/e2e/suites/authz/flow_test.go create mode 100644 internal/e2e/suites/authz/helpers_test.go create mode 100644 internal/e2e/suites/authz/security_regression_test.go create mode 100644 internal/e2e/suites/authz/testmain_test.go diff --git a/AGENTS.md b/AGENTS.md index ab3eaf7953..59a2e4a269 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -104,5 +104,6 @@ Keep this up to date when updating AGENTS.md. - **Workload Isolation**: The project uses `gVisor` (`runsc`) for sandboxing and security isolation of workloads on pods. - **JWTs and JWKs**: Signing (`internal/localjwtauthority`), verification (`internal/oidcjwt`), and JWK set publishing (`internal/oidcdiscovery`) are built on the standard library's crypto packages. Extend those packages rather than adding go-jose or another JOSE library, so the code that decides whether a token is valid stays small and supports only the algorithms Substrate uses. - **Ingress Authentication**: `atenet-router --ingress-auth-mode=static-mtls` requires clients to present a SPIFFE certificate signed by `--ingress-client-ca-file` whose ID is in `--ingress-allowed-spiffe-ids`; that list is the whole authorization policy, so any listed client may reach any Actor. The default, `deprecated-insecure`, checks a certificate only when a client presents one, and also serves plaintext listeners that accept any client. Install with it via `ate-setup --ingress-auth-mode=static-mtls` (`manifests/ate-install/components/router-static-mtls`); the allowlist lives in `manifests/ate-install/atenet-router.yaml`. +- **Control API Authorization**: `ate-api-server` checks every Control RPC against an embedded OpenFGA model (`cmd/ateapi/internal/authz/model.fga`), on by default. Every served unary RPC needs an entry in `defaultRPCPermissions` (`registry.go`): with enforcement on, an RPC without one is denied, and a test fails the build. Access is granted per global scope and atespace through AccessPolicy (`kubectl ate access-policy`); the install's bootstrap owner is the shared `ate-client` ServiceAccount that `kubectl ate` uses. See `docs/authorization.md` for roles and known gaps. For future plans for security, reference `docs/roadmap.md`. diff --git a/benchmarking/README.md b/benchmarking/README.md index 5d5b449d02..d64187df9c 100644 --- a/benchmarking/README.md +++ b/benchmarking/README.md @@ -20,6 +20,19 @@ scenario ladder, read [observability.md](observability.md). > export BUCKET_NAME="ate-snapshots" > ``` +> [!IMPORTANT] +> The ate-api-server checks every call against +> [authorization](../docs/authorization.md), and Locust calls it as the +> `benchmarking/default` ServiceAccount. Install Substrate with that identity +> as a bootstrap owner, alongside the default `ate-client` owner, or every +> benchmark call is denied: +> ```bash +> ATE_API_AUTHZ_BOOTSTRAP_OWNERS=system:serviceaccount:ate-system:ate-client,spiffe://cluster.local/ns/benchmarking/sa/default \ +> ./hack/install-ate.sh --deploy-ate-system +> ``` +> The nightly automation sets this itself; see +> [automation/README.md](automation/README.md). + Note that deploying the benchmarks does not run them. You must visit Locust's web UI to start a test. diff --git a/benchmarking/automation/README.md b/benchmarking/automation/README.md index f5b64c9fe8..3849e6ce53 100644 --- a/benchmarking/automation/README.md +++ b/benchmarking/automation/README.md @@ -23,7 +23,9 @@ the router capacity benchmark — see `ateArgs`) + `benchmarking/workloads/deploy.sh --deploy --sandbox-class ` (these build & push substrate / workload images via `ko` as part of their deploy steps — there's no separate - image build step). For a `microvm` test the orchestrator also + image build step). The install sets `ATE_API_AUTHZ_BOOTSTRAP_OWNERS` to + the `ate-client` and `benchmarking/benchmark-runner` ServiceAccounts so the + runner's calls pass [authorization](../../docs/authorization.md). For a `microvm` test the orchestrator also runs `hack/install-microvm-deps.sh --install` between the two, which stages kata + cloud-hypervisor + virtiofsd assets to the cluster's object store bucket and applies the cluster-wide `microvm` SandboxConfig. diff --git a/benchmarking/automation/orchestrator.py b/benchmarking/automation/orchestrator.py index a9fac23d77..f6ed296e92 100644 --- a/benchmarking/automation/orchestrator.py +++ b/benchmarking/automation/orchestrator.py @@ -397,8 +397,21 @@ def substrate_args(test: dict[str, Any]) -> list[str]: return [*test.get("ateArgs", []), *isolate_install_args(test)] +# ate-api-server enforces authorization, and the runner Job calls it with its +# pod certificate as BENCHMARK_RUNNER_PRINCIPAL. Each test reinstalls +# Substrate, so making the runner a bootstrap owner on every install grants it +# global owner for exactly the life of that install. kubectl-ate's principal +# stays an owner for the workload and teardown steps. +BENCHMARK_RUNNER_PRINCIPAL = "spiffe://cluster.local/ns/benchmarking/sa/benchmark-runner" +CLIENT_PRINCIPAL = "system:serviceaccount:ate-system:ate-client" + + def deploy_substrate(ate_args: Iterable[str] = ()) -> None: - run(["hack/install-ate.sh", "--deploy-ate-system", *(str(a) for a in ate_args)]) + env = { + **os.environ, + "ATE_API_AUTHZ_BOOTSTRAP_OWNERS": f"{CLIENT_PRINCIPAL},{BENCHMARK_RUNNER_PRINCIPAL}", + } + run(["hack/install-ate.sh", "--deploy-ate-system", *(str(a) for a in ate_args)], env=env) def teardown_substrate() -> None: diff --git a/benchmarking/automation/test_orchestrator.py b/benchmarking/automation/test_orchestrator.py index 569b69388e..004785f011 100644 --- a/benchmarking/automation/test_orchestrator.py +++ b/benchmarking/automation/test_orchestrator.py @@ -60,6 +60,30 @@ def test_bare_isolate_key(self): self.assertEqual(orchestrator.substrate_args({"isolate": None}), ["--fake-data-plane"]) +class DeploySubstrateTest(unittest.TestCase): + # The runner Job authenticates with its pod certificate, so ate-api-server + # denies it unless the install makes it an owner. + @mock.patch("orchestrator.run") + def test_runner_and_client_are_bootstrap_owners(self, run): + orchestrator.deploy_substrate(["--credential-provider={}"]) + cmd = run.call_args.args[0] + self.assertEqual(cmd, ["hack/install-ate.sh", "--deploy-ate-system", "--credential-provider={}"]) + owners = run.call_args.kwargs["env"]["ATE_API_AUTHZ_BOOTSTRAP_OWNERS"].split(",") + self.assertEqual( + owners, + [ + "system:serviceaccount:ate-system:ate-client", + "spiffe://cluster.local/ns/benchmarking/sa/benchmark-runner", + ], + ) + + @mock.patch("orchestrator.run") + def test_keeps_the_rest_of_the_environment(self, run): + with mock.patch.dict(os.environ, {"KO_DOCKER_REPO": "example"}): + orchestrator.deploy_substrate() + self.assertEqual(run.call_args.kwargs["env"]["KO_DOCKER_REPO"], "example") + + class DeployWorkloadsTest(unittest.TestCase): @mock.patch("orchestrator.run") def test_defaults_omit_optional_flags(self, run): diff --git a/cmd/ate-setup/internal/config/config.go b/cmd/ate-setup/internal/config/config.go index f687bab32b..126ff83c2e 100644 --- a/cmd/ate-setup/internal/config/config.go +++ b/cmd/ate-setup/internal/config/config.go @@ -122,6 +122,13 @@ type Config struct { // follows neither form. ExpectedJWTIssuer string + // APIExperimentalEnableAuthz turns on ate-api-server's runtime + // authorization (ATE_API_EXPERIMENTAL_ENABLE_AUTHZ). + APIExperimentalEnableAuthz bool + // APIAuthzBootstrapOwners is the comma-separated principal list granted + // owner on global:root (ATE_API_AUTHZ_BOOTSTRAP_OWNERS). + APIAuthzBootstrapOwners string + // ActorJWTAlgorithm is the signing algorithm of the key in a new actor JWT // pool (ACTOR_JWT_ALGORITHM): ES256 or RS256. ActorJWTAlgorithm string diff --git a/cmd/ate-setup/internal/config/config_test.go b/cmd/ate-setup/internal/config/config_test.go index 387c0313c8..e9d8c79dd4 100644 --- a/cmd/ate-setup/internal/config/config_test.go +++ b/cmd/ate-setup/internal/config/config_test.go @@ -46,6 +46,8 @@ func loadEnv(t *testing.T) { "ACTOR_JWT_ISSUER", "ANTHROPIC_API_KEY", "ATE_ADDITIONAL_EGRESS_EXTPROC_SERVICE", + "ATE_API_AUTHZ_BOOTSTRAP_OWNERS", + "ATE_API_EXPERIMENTAL_ENABLE_AUTHZ", "ATE_API_LOG_LEVEL", "ATE_API_POSTGRES_CLOUDSQL_GSA", "ATE_API_POSTGRES_CLOUDSQL_IAM_AUTH", @@ -601,6 +603,68 @@ func TestLoadAPILogLevel(t *testing.T) { } } +func TestLoadAPIAuthz(t *testing.T) { + const ( + clientOwner = "system:serviceaccount:ate-system:ate-client" + owners = "alice@example.com,bob@example.com" + ) + for _, tc := range []struct { + name string + env map[string]string + wantEnabled bool + wantOwners string + wantErr bool + }{ + { + name: "on by default, owned by the client ServiceAccount", + wantEnabled: true, + wantOwners: clientOwner, + }, + { + name: "default owner follows the namespace", + env: map[string]string{"ATE_NAMESPACE": "custom"}, + wantEnabled: true, + wantOwners: "system:serviceaccount:custom:ate-client", + }, + { + name: "explicit owners replace the default", + env: map[string]string{"ATE_API_AUTHZ_BOOTSTRAP_OWNERS": owners}, + wantEnabled: true, + wantOwners: owners, + }, + { + name: "turned off", + env: map[string]string{"ATE_API_EXPERIMENTAL_ENABLE_AUTHZ": "false"}, + wantOwners: clientOwner, + }, + { + name: "enabled with only separators", + env: map[string]string{"ATE_API_AUTHZ_BOOTSTRAP_OWNERS": " , "}, + wantErr: true, + }, + } { + t.Run(tc.name, func(t *testing.T) { + loadEnv(t) + for name, value := range tc.env { + t.Setenv(name, value) + } + cfg, err := Load(Options{}) + if tc.wantErr { + if err == nil || !strings.Contains(err.Error(), "bootstrap owner") { + t.Fatalf("Load() error = %v, want the missing bootstrap owner error", err) + } + return + } + if err != nil { + t.Fatalf("Load() error = %v", err) + } + if cfg.APIExperimentalEnableAuthz != tc.wantEnabled || cfg.APIAuthzBootstrapOwners != tc.wantOwners { + t.Errorf("authz = %v, %q, want %v, %q", cfg.APIExperimentalEnableAuthz, cfg.APIAuthzBootstrapOwners, tc.wantEnabled, tc.wantOwners) + } + }) + } +} + func TestLoadActorJWTAlgorithm(t *testing.T) { for _, tt := range []struct { name string diff --git a/cmd/ate-setup/internal/config/load.go b/cmd/ate-setup/internal/config/load.go index 301f89a9b3..13b2ad9f29 100644 --- a/cmd/ate-setup/internal/config/load.go +++ b/cmd/ate-setup/internal/config/load.go @@ -209,17 +209,29 @@ func buildConfig(root string, env map[string]string, r *Resolved) (*Config, erro if actorJWTIssuer == "" { actorJWTIssuer = installdefaults.ActorJWTIssuer(r.String("namespace")) } + // kubectl-ate, ate-setup's demo steps and the e2e clients fall back to + // tokens minted from the client ServiceAccount, so making it the default + // bootstrap owner keeps them working with authorization on. The deploy + // warns whenever it is a bootstrap owner. + bootstrapOwners := r.String("ateapi.authz.bootstrapOwners") + if bootstrapOwners == "" { + bootstrapOwners = installdefaults.ClientPrincipal(r.String("namespace")) + } cfg := &Config{ - Root: root, - Kind: r.Bool("kindCluster.enabled"), - Namespace: r.String("namespace"), - Kubeconfig: kubeconfig, - Context: r.String("context"), - ProjectID: r.String("gcp.projectID"), - ClusterName: r.String("gcp.clusterName"), - ClusterLocation: r.String("gcp.clusterLocation"), - ExpectedJWTIssuer: r.String("ateapi.expectedJWTIssuer"), + Root: root, + Kind: r.Bool("kindCluster.enabled"), + Namespace: r.String("namespace"), + Kubeconfig: kubeconfig, + Context: r.String("context"), + ProjectID: r.String("gcp.projectID"), + ClusterName: r.String("gcp.clusterName"), + ClusterLocation: r.String("gcp.clusterLocation"), + ExpectedJWTIssuer: r.String("ateapi.expectedJWTIssuer"), + + APIExperimentalEnableAuthz: r.Bool("ateapi.authz.enabled"), + APIAuthzBootstrapOwners: bootstrapOwners, + BucketName: r.String("storage.bucketName"), KODockerRepo: r.String("ko.dockerRepo"), KODefaultPlatforms: r.String("ko.defaultPlatforms"), @@ -449,5 +461,12 @@ func validateResolved(cfg *Config, r *Resolved) error { return fmt.Errorf("%w (from %s)", err, provider.From.Describe(provider.Setting)) } } + // ate-api-server exits on this combination, so catch it before deploying. + if cfg.APIExperimentalEnableAuthz && strings.Trim(cfg.APIAuthzBootstrapOwners, " ,") == "" { + enabled, _ := r.Value("ateapi.authz.enabled") + owners, _ := r.Value("ateapi.authz.bootstrapOwners") + return &ConflictError{A: enabled, B: owners, + Why: "enforced authorization needs at least one bootstrap owner"} + } return nil } diff --git a/cmd/ate-setup/internal/config/setting.go b/cmd/ate-setup/internal/config/setting.go index 664a40967f..f6d829bc73 100644 --- a/cmd/ate-setup/internal/config/setting.go +++ b/cmd/ate-setup/internal/config/setting.go @@ -173,6 +173,17 @@ var Registry = []Setting{ Kind: KindString, Usage: "Service account token issuer ate-api-server trusts, overriding derivation and discovery", }, + { + Key: "ateapi.authz.enabled", Env: "ATE_API_EXPERIMENTAL_ENABLE_AUTHZ", + Flag: "ateapi-experimental-enable-authz", Kind: KindBool, Default: "true", + Usage: "Enforce ate-api-server's runtime authorization", + }, + { + Key: "ateapi.authz.bootstrapOwners", Env: "ATE_API_AUTHZ_BOOTSTRAP_OWNERS", + Flag: "ateapi-authz-bootstrap-owners", Kind: KindString, + Usage: "Comma-separated principals that always hold owner on global:root when authorization is enforced. " + + "Unset means kubectl-ate's ServiceAccount, system:serviceaccount::ate-client", + }, { // The egress gateway's credential provider, as a JSON object. Upstream // takes it as one value on one flag; it is carried the same way here diff --git a/cmd/ate-setup/internal/steps/apienv.go b/cmd/ate-setup/internal/steps/apienv.go index 96f194174b..f59b560d1b 100644 --- a/cmd/ate-setup/internal/steps/apienv.go +++ b/cmd/ate-setup/internal/steps/apienv.go @@ -22,11 +22,13 @@ import ( "maps" "os" "slices" + "strings" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "go.ate.dev/substrate/cmd/ate-setup/internal/kube" "go.ate.dev/substrate/cmd/ate-setup/internal/log" + "go.ate.dev/substrate/internal/installdefaults" ) // envHashAnnotation carries a digest of the apiserver's environment on the pod @@ -147,6 +149,27 @@ func (e *Env) UpdateAPIServerEnvVars(ctx context.Context) error { return e.annotateAPIServerEnvHash(ctx) } +// sharedBootstrapOwnerWarning returns a warning when enforced authorization +// makes the shared client ServiceAccount a bootstrap owner, and "" otherwise. +// It is the default so demos, benchmarks and e2e work, but everyone who can +// mint its token, including every kubectl-ate user without --token-file, is +// then a global owner. +func sharedBootstrapOwnerWarning(enabled bool, bootstrapOwners, namespace string) string { + if !enabled { + return "" + } + shared := installdefaults.ClientPrincipal(namespace) + for owner := range strings.SplitSeq(bootstrapOwners, ",") { + if strings.TrimSpace(owner) == shared { + return fmt.Sprintf("%s is a bootstrap owner, so anyone who can mint a token for it is a global owner of this install. "+ + "It is the default so the demos, benchmarks and e2e tests work; for a shared or production install, "+ + "set --ateapi-authz-bootstrap-owners (ATE_API_AUTHZ_BOOTSTRAP_OWNERS) to your operators' own principals. "+ + "See docs/authorization.md.", shared) + } + } + return "" +} + // buildAPIServerEnvVars is the Secret payload. ate-api-server takes both // connection strings and the schema from it, and exits on an empty schema; an // unrecognized key here reaches the container as a stray environment variable. @@ -292,9 +315,69 @@ func (e *Env) applyWithAPIServerEnvHash(ctx context.Context, manifest []byte) er if err := stampAPIServerEnvHash(objs, hash); err != nil { return err } + if err := setAPIServerAuthzArgs(objs, e.Cfg.APIExperimentalEnableAuthz, e.Cfg.APIAuthzBootstrapOwners); err != nil { + return err + } + if msg := sharedBootstrapOwnerWarning(e.Cfg.APIExperimentalEnableAuthz, e.Cfg.APIAuthzBootstrapOwners, e.Namespace()); msg != "" { + log.Warnf("%s", msg) + } return e.Kube.Apply(ctx, objs) } +// apiServerAuthzFlags are the ate-api-server flags setAPIServerAuthzArgs owns. +var apiServerAuthzFlags = []string{"--experimental-enable-authz", "--authz-bootstrap-owners"} + +// setAPIServerAuthzArgs sets ate-api-server's authorization flags on its +// container in objs, replacing any the manifest already carries. A changed +// flag changes the pod template, so it rolls the Deployment. +// +// The bootstrap owners are passed even with enforcement off: access policy +// RPCs are checked either way, so without them nobody could set up policies +// before turning enforcement on. +func setAPIServerAuthzArgs(objs []*unstructured.Unstructured, enabled bool, bootstrapOwners string) error { + var dep *unstructured.Unstructured + for _, obj := range objs { + if obj.GetKind() == "Deployment" && obj.GetName() == "ate-api-server" { + dep = obj + break + } + } + if dep == nil { + return fmt.Errorf("the manifest has no deployment/ate-api-server to set authorization flags on") + } + containers, found, err := unstructured.NestedSlice(dep.Object, "spec", "template", "spec", "containers") + if err != nil || !found { + return fmt.Errorf("%s has no containers", kube.Describe(dep)) + } + for i, c := range containers { + container, ok := c.(map[string]any) + if !ok || container["name"] != "ate-api-server" { + continue + } + args, _, err := unstructured.NestedStringSlice(container, "args") + if err != nil { + return fmt.Errorf("%s: %w", kube.Describe(dep), err) + } + args = slices.DeleteFunc(args, func(arg string) bool { + return slices.ContainsFunc(apiServerAuthzFlags, func(flag string) bool { + return arg == flag || strings.HasPrefix(arg, flag+"=") + }) + }) + if enabled { + args = append(args, "--experimental-enable-authz=true") + } + if strings.Trim(bootstrapOwners, " ,") != "" { + args = append(args, "--authz-bootstrap-owners="+bootstrapOwners) + } + if err := unstructured.SetNestedStringSlice(container, args, "args"); err != nil { + return fmt.Errorf("%s: %w", kube.Describe(dep), err) + } + containers[i] = container + return unstructured.SetNestedSlice(dep.Object, containers, "spec", "template", "spec", "containers") + } + return fmt.Errorf("%s has no ate-api-server container", kube.Describe(dep)) +} + // stampAPIServerEnvHash sets the digest on the ate-api-server Deployment's pod // template and leaves every other object alone. A manifest without that // Deployment is an error: applying it unstamped would stop a changed diff --git a/cmd/ate-setup/internal/steps/apienv_test.go b/cmd/ate-setup/internal/steps/apienv_test.go index 1fae0650de..2371e48dc5 100644 --- a/cmd/ate-setup/internal/steps/apienv_test.go +++ b/cmd/ate-setup/internal/steps/apienv_test.go @@ -18,6 +18,7 @@ import ( "strings" "testing" + "github.com/google/go-cmp/cmp" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -610,3 +611,142 @@ spec: t.Errorf("stampAPIServerEnvHash() without ate-api-server error = nil, want an error") } } + +// Authorization reaches ate-api-server as flags on its container, so a change +// rolls the Deployment, and the envvars ConfigMap carries none of it. +func TestSetAPIServerAuthzArgs(t *testing.T) { + const manifest = `apiVersion: apps/v1 +kind: Deployment +metadata: + name: ate-api-server + namespace: ate-system +spec: + template: + spec: + containers: + - name: objectstore-plugin + args: ["--keep"] + - name: ate-api-server + args: + - --config-file=/run/apiconfig.ate.dev/apiconfig.textproto + - --authz-bootstrap-owners=stale + - --experimental-enable-authz +` + for _, tc := range []struct { + name string + enabled bool + owners string + want []string + }{ + { + name: "enforced", + enabled: true, + owners: "alice,bob", + want: []string{ + "--config-file=/run/apiconfig.ate.dev/apiconfig.textproto", + "--experimental-enable-authz=true", + "--authz-bootstrap-owners=alice,bob", + }, + }, + { + // Owners stay, so access policies can be set up before enforcement is on. + name: "off keeps the owners", + owners: "alice", + want: []string{ + "--config-file=/run/apiconfig.ate.dev/apiconfig.textproto", + "--authz-bootstrap-owners=alice", + }, + }, + { + name: "off with no owners", + want: []string{"--config-file=/run/apiconfig.ate.dev/apiconfig.textproto"}, + }, + } { + t.Run(tc.name, func(t *testing.T) { + objs, err := kube.DecodeManifestBytes([]byte(manifest)) + if err != nil { + t.Fatalf("DecodeManifestBytes() error = %v", err) + } + if err := setAPIServerAuthzArgs(objs, tc.enabled, tc.owners); err != nil { + t.Fatalf("setAPIServerAuthzArgs() error = %v", err) + } + containers, _, _ := unstructured.NestedSlice(objs[0].Object, "spec", "template", "spec", "containers") + argsOf := func(i int) []string { + args, _, _ := unstructured.NestedStringSlice(containers[i].(map[string]any), "args") + return args + } + if diff := cmp.Diff(tc.want, argsOf(1)); diff != "" { + t.Errorf("ate-api-server args (-want +got):\n%s", diff) + } + if diff := cmp.Diff([]string{"--keep"}, argsOf(0)); diff != "" { + t.Errorf("other container's args changed (-want +got):\n%s", diff) + } + }) + } + + objs, err := kube.DecodeManifestBytes([]byte("apiVersion: apps/v1\nkind: Deployment\nmetadata:\n name: ate-controller\n")) + if err != nil { + t.Fatalf("DecodeManifestBytes() error = %v", err) + } + if err := setAPIServerAuthzArgs(objs, true, "alice"); err == nil { + t.Error("setAPIServerAuthzArgs() without deployment/ate-api-server error = nil, want an error") + } +} + +func TestCreateAPIServerEnvVarsOmitsAuthz(t *testing.T) { + e := &Env{Cfg: &config.Config{ + PostgresReadWriteRole: config.DefaultPostgresReadWriteRole, + PostgresOwnerRole: config.DefaultPostgresOwnerRole, + APIExperimentalEnableAuthz: true, + APIAuthzBootstrapOwners: "system:serviceaccount:ate-system:ate-client", + }, Kube: fakeKube(t, + &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: NamespaceAteSystem}}, + apiServerEnvVarsConfigMap(map[string]string{}), + &corev1.Secret{ObjectMeta: metav1.ObjectMeta{Namespace: NamespaceAteSystem, Name: SecretAPIEnvVars}}, + )} + if err := e.CreateAPIServerEnvVars(t.Context()); err != nil { + t.Fatalf("CreateAPIServerEnvVars() error = %v", err) + } + cm, err := e.Kube.GetConfigMap(t.Context(), NamespaceAteSystem, ConfigMapAPIEnvVars) + if err != nil || cm == nil { + t.Fatalf("GetConfigMap() = %v, %v", cm, err) + } + for _, key := range []string{"ATE_API_EXPERIMENTAL_ENABLE_AUTHZ", "ATE_API_AUTHZ_BOOTSTRAP_OWNERS"} { + if v, ok := cm.Data[key]; ok { + t.Errorf("ConfigMap carries %s=%q; authorization is passed as flags", key, v) + } + } +} + +// The shared client ServiceAccount as a bootstrap owner makes everyone who can +// mint its token a global owner, so the install says so whenever it is listed. +func TestSharedBootstrapOwnerWarning(t *testing.T) { + const shared = "system:serviceaccount:ate-system:ate-client" + for _, tc := range []struct { + name string + enabled bool + owners string + warn bool + }{ + {name: "default owner", enabled: true, owners: shared, warn: true}, + {name: "listed among others", enabled: true, owners: "alice, " + shared + " ,bob", warn: true}, + {name: "own principals only", enabled: true, owners: "alice,bob", warn: false}, + {name: "another namespace's client", enabled: true, owners: "system:serviceaccount:other:ate-client", warn: false}, + {name: "prefix is not a match", enabled: true, owners: shared + "-x", warn: false}, + {name: "authorization off", enabled: false, owners: shared, warn: false}, + } { + t.Run(tc.name, func(t *testing.T) { + got := sharedBootstrapOwnerWarning(tc.enabled, tc.owners, "ate-system") + if (got != "") != tc.warn { + t.Fatalf("sharedBootstrapOwnerWarning(%v, %q) = %q, want warning: %v", tc.enabled, tc.owners, got, tc.warn) + } + if tc.warn { + for _, want := range []string{shared, "--ateapi-authz-bootstrap-owners", "ATE_API_AUTHZ_BOOTSTRAP_OWNERS"} { + if !strings.Contains(got, want) { + t.Errorf("warning %q does not name %q", got, want) + } + } + } + }) + } +} diff --git a/cmd/ateapi/internal/authz/authz_test.go b/cmd/ateapi/internal/authz/authz_test.go index 7dec76c6e2..c391ffb48d 100644 --- a/cmd/ateapi/internal/authz/authz_test.go +++ b/cmd/ateapi/internal/authz/authz_test.go @@ -33,6 +33,7 @@ import ( "github.com/testcontainers/testcontainers-go/modules/postgres" "google.golang.org/grpc/codes" + "go.ate.dev/substrate/cmd/ateapi/internal/store/dockerenv" "go.ate.dev/substrate/internal/apierror" "go.ate.dev/substrate/internal/principal" ) @@ -63,8 +64,14 @@ func configureDockerEnv(ctx context.Context) error { func startPostgres(t *testing.T) *pgxpool.Pool { t.Helper() ctx := context.Background() + // Without Docker the tests skip locally, but must fail in CI, where a + // skip would pass the package without running it. + unavailable := t.Skipf + if dockerenv.Required() { + unavailable = t.Fatalf + } if err := configureDockerEnv(ctx); err != nil { - t.Skipf("skipping test; docker is unavailable: %v", err) + unavailable("docker is unavailable: %v", err) } pgContainer, err := postgres.Run(ctx, @@ -75,7 +82,7 @@ func startPostgres(t *testing.T) *pgxpool.Pool { postgres.BasicWaitStrategies(), ) if err != nil { - t.Skipf("skipping test; failed to start postgres container: %v", err) + unavailable("failed to start postgres container: %v", err) } t.Cleanup(func() { _ = testcontainers.TerminateContainer(pgContainer) diff --git a/cmd/ateapi/internal/authz/authz_util.go b/cmd/ateapi/internal/authz/authz_util.go index 608d660469..e4c86cc91e 100644 --- a/cmd/ateapi/internal/authz/authz_util.go +++ b/cmd/ateapi/internal/authz/authz_util.go @@ -47,6 +47,7 @@ const ( RoleController = "controller" RoleEgressGateway = "egress_gateway" RoleIngressRouter = "ingress_router" + RoleAtelet = "atelet" RelationCanCreateAtespace = "can_create_atespace" RelationCanListAtespaces = "can_list_atespaces" @@ -234,6 +235,7 @@ type SystemPrincipals struct { ControllerSPIFFEID string EgressGatewaySPIFFEID string IngressRouterSPIFFEID string + AteletSPIFFEID string } // New provisions the default OpenFGA store and authorization model via @@ -293,6 +295,7 @@ func parseSystemPrincipals(system SystemPrincipals) (map[string][]string, error) {role: RoleController, spiffeID: system.ControllerSPIFFEID}, {role: RoleEgressGateway, spiffeID: system.EgressGatewaySPIFFEID}, {role: RoleIngressRouter, spiffeID: system.IngressRouterSPIFFEID}, + {role: RoleAtelet, spiffeID: system.AteletSPIFFEID}, } out := make(map[string][]string, len(bindings)) for _, b := range bindings { diff --git a/cmd/ateapi/internal/authz/authz_util_test.go b/cmd/ateapi/internal/authz/authz_util_test.go index 66d165789d..7007d23675 100644 --- a/cmd/ateapi/internal/authz/authz_util_test.go +++ b/cmd/ateapi/internal/authz/authz_util_test.go @@ -28,6 +28,7 @@ func testSystemPrincipals() SystemPrincipals { ControllerSPIFFEID: installdefaults.ControllerSPIFFEID(installdefaults.SystemNamespace), EgressGatewaySPIFFEID: installdefaults.EgressSPIFFEID(installdefaults.SystemNamespace), IngressRouterSPIFFEID: installdefaults.RouterSPIFFEID(installdefaults.SystemNamespace), + AteletSPIFFEID: installdefaults.AteletSPIFFEID(installdefaults.SystemNamespace), } } @@ -96,6 +97,7 @@ func TestParseSystemPrincipals(t *testing.T) { ControllerSPIFFEID: "spiffe://cluster.local/ns/ate-system/sa/ate-controller", EgressGatewaySPIFFEID: router, IngressRouterSPIFFEID: router, + AteletSPIFFEID: "spiffe://cluster.local/ns/ate-system/sa/atelet", }) if err != nil { t.Fatalf("parseSystemPrincipals failed: %v", err) @@ -103,6 +105,7 @@ func TestParseSystemPrincipals(t *testing.T) { want := map[string][]string{ "user:spiffe%3A//cluster.local/ns/ate-system/sa/ate-controller": {RoleController}, "user:spiffe%3A//cluster.local/ns/ate-system/sa/atenet-router": {RoleEgressGateway, RoleIngressRouter}, + "user:spiffe%3A//cluster.local/ns/ate-system/sa/atelet": {RoleAtelet}, } if diff := cmp.Diff(want, got); diff != "" { t.Errorf("parseSystemPrincipals (-want +got):\n%s", diff) @@ -119,6 +122,7 @@ func TestNewRejectsInvalidSystemPrincipals(t *testing.T) { {"empty controller SPIFFE ID", func(p *SystemPrincipals) { p.ControllerSPIFFEID = "" }, "invalid controller SPIFFE ID"}, {"empty egress gateway SPIFFE ID", func(p *SystemPrincipals) { p.EgressGatewaySPIFFEID = "" }, "invalid egress_gateway SPIFFE ID"}, {"empty ingress router SPIFFE ID", func(p *SystemPrincipals) { p.IngressRouterSPIFFEID = "" }, "invalid ingress_router SPIFFE ID"}, + {"empty atelet SPIFFE ID", func(p *SystemPrincipals) { p.AteletSPIFFEID = "" }, "invalid atelet SPIFFE ID"}, {"bare ServiceAccount name", func(p *SystemPrincipals) { p.ControllerSPIFFEID = "ate-controller" }, "invalid controller SPIFFE ID"}, {"non-spiffe URI", func(p *SystemPrincipals) { p.EgressGatewaySPIFFEID = "https://cluster.local/ns/ate-system/sa/atenet-egress" diff --git a/cmd/ateapi/internal/authz/interceptor_test.go b/cmd/ateapi/internal/authz/interceptor_test.go index 350f947570..4bab436a23 100644 --- a/cmd/ateapi/internal/authz/interceptor_test.go +++ b/cmd/ateapi/internal/authz/interceptor_test.go @@ -688,12 +688,14 @@ func TestUnaryServerInterceptor_SystemPrincipals(t *testing.T) { controllerID = "spiffe://cluster.local/ns/ate-system/sa/ate-controller" egressID = "spiffe://cluster.local/ns/ate-system/sa/atenet-egress" routerID = "spiffe://cluster.local/ns/ate-system/sa/atenet-router" + ateletID = "spiffe://cluster.local/ns/ate-system/sa/atelet" otherID = "spiffe://cluster.local/ns/ate-system/sa/other" ) authorizer, _, err := New(ctx, pool, fgaServer, nil, SystemPrincipals{ ControllerSPIFFEID: controllerID, EgressGatewaySPIFFEID: egressID, IngressRouterSPIFFEID: routerID, + AteletSPIFFEID: ateletID, }) if err != nil { t.Fatalf("New failed: %v", err) @@ -779,11 +781,22 @@ func TestUnaryServerInterceptor_SystemPrincipals(t *testing.T) { {"ingress router cannot mint actor JWT", mtls(routerID), mintJWT, codes.PermissionDenied}, {"controller cannot mint actor certificate", mtls(controllerID), mintCert, codes.PermissionDenied}, + // atelet reads egress policies for the worker that enforces them, and + // nothing else. + {"atelet gets egress policy", mtls(ateletID), getEgress, codes.OK}, + {"atelet cannot update egress policy", mtls(ateletID), updateEgress, codes.PermissionDenied}, + {"atelet cannot get actor", mtls(ateletID), getActor, codes.PermissionDenied}, + {"atelet cannot list actors", mtls(ateletID), listAllActors, codes.PermissionDenied}, + {"atelet cannot resume actor", mtls(ateletID), resume, codes.PermissionDenied}, + {"atelet cannot mint actor JWT", mtls(ateletID), mintJWT, codes.PermissionDenied}, + {"atelet cannot get worker", mtls(ateletID), getWorker, codes.PermissionDenied}, + // A bearer token whose subject is a granted SPIFFE ID gets nothing. {"jwt with controller ID cannot create worker", jwt(controllerID), createWorker, codes.PermissionDenied}, {"jwt with egress ID cannot get actor", jwt(egressID), getActor, codes.PermissionDenied}, {"jwt with egress ID cannot mint actor JWT", jwt(egressID), mintJWT, codes.PermissionDenied}, {"jwt with router ID cannot resume actor", jwt(routerID), resume, codes.PermissionDenied}, + {"jwt with atelet ID cannot get egress policy", jwt(ateletID), getEgress, codes.PermissionDenied}, // Other SPIFFE IDs get nothing. {"other mTLS identity cannot resume actor", mtls(otherID), resume, codes.PermissionDenied}, diff --git a/cmd/ateapi/internal/authz/model.fga b/cmd/ateapi/internal/authz/model.fga index ccd8242108..ea41388938 100644 --- a/cmd/ateapi/internal/authz/model.fga +++ b/cmd/ateapi/internal/authz/model.fga @@ -50,6 +50,7 @@ type global define controller: [user] define egress_gateway: [user] define ingress_router: [user] + define atelet: [user] define can_create_atespace: owner define can_list_atespaces: viewer @@ -81,6 +82,7 @@ type atespace # System component roles, inherited from global only. define egress_gateway: egress_gateway from parent_global define ingress_router: ingress_router from parent_global + define atelet: atelet from parent_global # --- 2. Policy Governance --- define can_create_access_policy: owner @@ -143,7 +145,15 @@ type actor define can_delete: editor from parent_atespace # Egress policy: one per actor, so its permissions live on the actor. - define can_get_egress_policy: viewer from parent_atespace or egress_gateway from parent_atespace + # atelet reads it on behalf of the worker hosting the actor, which + # enforces it. + # + # TODO(#1564): Every node's atelet shares one SPIFFE ID, so any atelet can read + # any actor's egress policy, and atelet does not check that the worker + # asking hosts the actor. Serve the read from WorkerService instead, + # authorizing atelet on the node hosting the actor as MintWorkerActorJwt + # does, have atelet check the requesting worker, and drop atelet here. + define can_get_egress_policy: viewer from parent_atespace or egress_gateway from parent_atespace or atelet from parent_atespace define can_create_egress_policy: editor from parent_atespace define can_update_egress_policy: editor from parent_atespace define can_delete_egress_policy: editor from parent_atespace diff --git a/cmd/ateapi/internal/authz/model_fga_test.go b/cmd/ateapi/internal/authz/model_fga_test.go new file mode 100644 index 0000000000..5524349846 --- /dev/null +++ b/cmd/ateapi/internal/authz/model_fga_test.go @@ -0,0 +1,134 @@ +// 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 authz + +import ( + "context" + "os" + "slices" + "testing" + + openfgav1 "github.com/openfga/api/proto/openfga/v1" + "sigs.k8s.io/yaml" +) + +// modelTestFile is in the format `fga model test` reads, so the same file +// also runs with the OpenFGA CLI. +const modelTestFile = "model_test.fga.yaml" + +// modelTests is the subset of the `fga model test` format the file uses. +// Parsing is strict, so a test using any other part of the format (contextual +// tuples, list_objects, conditions, ...) fails here rather than being +// silently skipped. +type modelTests struct { + Name string `json:"name"` + ModelFile string `json:"model_file"` + Tuples []modelTuple `json:"tuples"` + Tests []struct { + Name string `json:"name"` + Check []struct { + User string `json:"user"` + Object string `json:"object"` + Assertions map[string]bool `json:"assertions"` + } `json:"check"` + } `json:"tests"` +} + +type modelTuple struct { + User string `json:"user"` + Relation string `json:"relation"` + Object string `json:"object"` +} + +// TestModelFGATests runs the checks in model_test.fga.yaml against model.fga +// as ate-api-server loads it. +func TestModelFGATests(t *testing.T) { + data, err := os.ReadFile(modelTestFile) + if err != nil { + t.Fatalf("reading %s: %v", modelTestFile, err) + } + var mt modelTests + if err := yaml.UnmarshalStrict(data, &mt); err != nil { + t.Fatalf("parsing %s: %v", modelTestFile, err) + } + // The checks must run against the model ate-api-server embeds. + if mt.ModelFile != "./model.fga" { + t.Fatalf("%s: model_file = %q, want %q", modelTestFile, mt.ModelFile, "./model.fga") + } + if len(mt.Tests) == 0 { + t.Fatalf("%s has no tests", modelTestFile) + } + + ctx := context.Background() + pool := startPostgres(t) + fgaSrv, storeID, modelID := newTestAuthz(t, ctx, pool) + + // OpenFGA caps the tuples one Write may carry. + const maxTuplesPerWrite = 100 + for chunk := range slices.Chunk(mt.Tuples, maxTuplesPerWrite) { + keys := make([]*openfgav1.TupleKey, 0, len(chunk)) + for _, tk := range chunk { + keys = append(keys, &openfgav1.TupleKey{User: tk.User, Relation: tk.Relation, Object: tk.Object}) + } + tx, err := pool.Begin(ctx) + if err != nil { + t.Fatalf("pool.Begin failed: %v", err) + } + if _, err := fgaSrv.Write(ContextWithTx(ctx, tx), &openfgav1.WriteRequest{ + StoreId: storeID, + AuthorizationModelId: modelID, + Writes: &openfgav1.WriteRequestWrites{TupleKeys: keys}, + }); err != nil { + _ = tx.Rollback(ctx) + t.Fatalf("writing tuples: %v", err) + } + if err := tx.Commit(ctx); err != nil { + t.Fatalf("tx.Commit failed: %v", err) + } + } + + for _, tc := range mt.Tests { + t.Run(tc.Name, func(t *testing.T) { + if len(tc.Check) == 0 { + t.Fatal("test has no checks") + } + for _, c := range tc.Check { + relations := make([]string, 0, len(c.Assertions)) + for r := range c.Assertions { + relations = append(relations, r) + } + slices.Sort(relations) + for _, relation := range relations { + resp, err := fgaSrv.Check(ctx, &openfgav1.CheckRequest{ + StoreId: storeID, + AuthorizationModelId: modelID, + TupleKey: &openfgav1.CheckRequestTupleKey{ + User: c.User, + Relation: relation, + Object: c.Object, + }, + }) + if err != nil { + t.Errorf("Check(user=%s, relation=%s, object=%s): %v", c.User, relation, c.Object, err) + continue + } + if want := c.Assertions[relation]; resp.GetAllowed() != want { + t.Errorf("Check(user=%s, relation=%s, object=%s) = %v, want %v", c.User, relation, c.Object, resp.GetAllowed(), want) + } + } + } + }) + } +} diff --git a/cmd/ateapi/internal/authz/model_test.fga.yaml b/cmd/ateapi/internal/authz/model_test.fga.yaml index 0464b336cd..e2f945c27a 100644 --- a/cmd/ateapi/internal/authz/model_test.fga.yaml +++ b/cmd/ateapi/internal/authz/model_test.fga.yaml @@ -87,6 +87,9 @@ tuples: - user: user:sys-router relation: ingress_router object: global:root + - user: user:sys-atelet + relation: atelet + object: global:root tests: - name: 1. Global Superadmin & Viewer Propagation @@ -420,6 +423,33 @@ tests: viewer: false can_list_actors: false can_get: false + - user: user:sys-atelet # atelet reads egress policies for the worker that enforces them + object: actor:runner-1 + assertions: + can_get_egress_policy: true + can_get: false + can_update: false + can_delete: false + can_create_egress_policy: false + can_update_egress_policy: false + can_delete_egress_policy: false + can_mint_actor_jwt: false + can_mint_actor_certificate: false + can_resume: false + can_suspend: false + - user: user:sys-atelet + object: atespace:space-1 + assertions: + viewer: false + can_get: false + can_list_actors: false + can_create_actor: false + - user: user:sys-atelet + object: global:root + assertions: + can_list_actors: false + can_list_workers: false + can_create_worker: false - user: user:sys-router # atenet-router resumes actors and lists them object: actor:runner-1 assertions: @@ -445,12 +475,14 @@ tests: assertions: egress_gateway: false ingress_router: false + atelet: false - user: user:carol # Global owner does not hold system roles object: global:root assertions: controller: false egress_gateway: false ingress_router: false + atelet: false - name: 8. Tag Operations (Inherited from Atespace) check: diff --git a/cmd/ateapi/internal/authz/registry.go b/cmd/ateapi/internal/authz/registry.go index 12d7401b08..c22d81a810 100644 --- a/cmd/ateapi/internal/authz/registry.go +++ b/cmd/ateapi/internal/authz/registry.go @@ -338,10 +338,9 @@ var defaultRPCPermissions = map[string]rpcRule{ // WorkerService is served only to atelet, which each handler // authenticates by its mTLS SPIFFE ID. - ateapipb.WorkerService_RegisterWorker_FullMethodName: inHandler("ateletauth: caller is atelet on the Worker's node"), - ateapipb.WorkerService_RequestActorSuspend_FullMethodName: inHandler("ateletauth: caller is atelet on the node of the Worker hosting the Actor"), - // TODO: also require that the Actor runs on the caller's node. - ateapipb.WorkerService_MintWorkerActorCertificate_FullMethodName: inHandler("ateletauth: caller is atelet"), + ateapipb.WorkerService_RegisterWorker_FullMethodName: inHandler("ateletauth: caller is atelet on the Worker's node"), + ateapipb.WorkerService_RequestActorSuspend_FullMethodName: inHandler("ateletauth: caller is atelet on the node of the Worker hosting the Actor"), + ateapipb.WorkerService_MintWorkerActorCertificate_FullMethodName: inHandler("ateletauth: caller is atelet on the node of the Worker the Actor is assigned to"), ateapipb.WorkerService_MintWorkerActorJwt_FullMethodName: inHandler("ateletauth: caller is atelet on the node hosting the Actor"), // TODO(#2570): not served yet; the handler must authenticate atelet on the // node of the Worker hosting the Actor when it lands. diff --git a/cmd/ateapi/internal/store/atepg/common_test.go b/cmd/ateapi/internal/store/atepg/common_test.go index 16a171524f..accb18c49f 100644 --- a/cmd/ateapi/internal/store/atepg/common_test.go +++ b/cmd/ateapi/internal/store/atepg/common_test.go @@ -59,6 +59,7 @@ func setTestPolicyManager(t *testing.T, p *Persistence) { ControllerSPIFFEID: installdefaults.ControllerSPIFFEID(installdefaults.SystemNamespace), EgressGatewaySPIFFEID: installdefaults.EgressSPIFFEID(installdefaults.SystemNamespace), IngressRouterSPIFFEID: installdefaults.RouterSPIFFEID(installdefaults.SystemNamespace), + AteletSPIFFEID: installdefaults.AteletSPIFFEID(installdefaults.SystemNamespace), }) if err != nil { t.Fatalf("authz.New failed: %v", err) diff --git a/cmd/ateapi/internal/store/storetest/storetest.go b/cmd/ateapi/internal/store/storetest/storetest.go index 9976457da9..3385bb4112 100644 --- a/cmd/ateapi/internal/store/storetest/storetest.go +++ b/cmd/ateapi/internal/store/storetest/storetest.go @@ -103,6 +103,7 @@ func DefaultSystemPrincipals() authz.SystemPrincipals { ControllerSPIFFEID: installdefaults.ControllerSPIFFEID(installdefaults.SystemNamespace), EgressGatewaySPIFFEID: installdefaults.EgressSPIFFEID(installdefaults.SystemNamespace), IngressRouterSPIFFEID: installdefaults.RouterSPIFFEID(installdefaults.SystemNamespace), + AteletSPIFFEID: installdefaults.AteletSPIFFEID(installdefaults.SystemNamespace), } } diff --git a/cmd/ateapi/main.go b/cmd/ateapi/main.go index 6eef1e149f..7262f731a3 100644 --- a/cmd/ateapi/main.go +++ b/cmd/ateapi/main.go @@ -231,6 +231,18 @@ func main() { // AccessPolicy, so the enforced API would be unusable. serverboot.Fatal(ctx, "Invalid flags", fmt.Errorf("--authz-bootstrap-owners must list at least one principal when --experimental-enable-authz is set")) } + // atelet shares ateapi's namespace in every supported deployment topology, + // so we read it from Kubernetes' downward API rather than expose a flag. + ateletNamespace := installdefaults.NamespaceFromPodEnv() + // An empty ServiceAccount would not fail here: path.Join drops the empty + // segment, yielding an identity that parses but matches nothing, so every + // atelet dial would be rejected with no hint at the cause. + if *ateletServiceAccount == "" { + serverboot.Fatal(ctx, "Invalid flags", fmt.Errorf("--atelet-service-account must not be empty")) + } + ateletSPIFFEID := installdefaults.SPIFFEID(ateletNamespace, *ateletServiceAccount) + slog.InfoContext(ctx, "Resolved atelet namespace", slog.String("atelet-namespace", ateletNamespace), slog.String("atelet-spiffe-id", ateletSPIFFEID)) + fgaServer, err := authz.NewOpenFGAServer(pool) if err != nil { serverboot.Fatal(ctx, "Failed to create OpenFGA server", err) @@ -240,6 +252,7 @@ func main() { ControllerSPIFFEID: *controllerSPIFFEID, EgressGatewaySPIFFEID: *egressGatewaySPIFFEID, IngressRouterSPIFFEID: *ingressRouterSPIFFEID, + AteletSPIFFEID: ateletSPIFFEID, }) if err != nil { serverboot.Fatal(ctx, "Failed to initialize OpenFGA authz", err) @@ -266,18 +279,6 @@ func main() { sandboxConfigLister := ateFactory.Api().V1alpha1().SandboxConfigs().Lister() csiDriverConfigLister := ateFactory.Api().V1alpha1().CSIDriverConfigs().Lister() - // atelet shares ateapi's namespace in every supported deployment topology, - // so we read it from Kubernetes' downward API rather than expose a flag. - ateletNamespace := installdefaults.NamespaceFromPodEnv() - // An empty ServiceAccount would not fail here: path.Join drops the empty - // segment, yielding an identity that parses but matches nothing, so every - // atelet dial would be rejected with no hint at the cause. - if *ateletServiceAccount == "" { - serverboot.Fatal(ctx, "Invalid flags", fmt.Errorf("--atelet-service-account must not be empty")) - } - ateletSPIFFEID := installdefaults.SPIFFEID(ateletNamespace, *ateletServiceAccount) - slog.InfoContext(ctx, "Resolved atelet namespace", slog.String("atelet-namespace", ateletNamespace), slog.String("atelet-spiffe-id", ateletSPIFFEID)) - ateletPodInformerFactory, ateletPodInformer := controlapi.AteletInformer(clientset, ateletNamespace) scInformerFactory := informers.NewSharedInformerFactory(clientset, 0) storageClassLister := scInformerFactory.Storage().V1().StorageClasses().Lister() @@ -466,9 +467,6 @@ func loadFlagsFromEnv() error { *postgresPoolMaxConns = int32(value) } } - if v := os.Getenv("ATE_API_EXPERIMENTAL_ENABLE_AUTHZ"); v != "" && !pflag.CommandLine.Changed("experimental-enable-authz") { - *experimentalEnableAuthz = (v == "true" || v == "1") - } return nil } diff --git a/cmd/ateapi/main_test.go b/cmd/ateapi/main_test.go index 54546caae9..d16278467e 100644 --- a/cmd/ateapi/main_test.go +++ b/cmd/ateapi/main_test.go @@ -49,24 +49,20 @@ func TestConnectStoreRequiresPostgresReadWriteConnectionString(t *testing.T) { func TestLoadFlagsFromEnvResolvesPostgresSourcesOnce(t *testing.T) { oldRuntime, oldDDL := *postgresReadWriteConnectionString, *postgresOwnerConnectionString oldRuntimeRole, oldDDLRole := *postgresReadWriteRole, *postgresOwnerRole - oldAuthz := *experimentalEnableAuthz t.Cleanup(func() { *postgresReadWriteConnectionString = oldRuntime *postgresOwnerConnectionString = oldDDL *postgresReadWriteRole = oldRuntimeRole *postgresOwnerRole = oldDDLRole - *experimentalEnableAuthz = oldAuthz }) *postgresReadWriteConnectionString = "@env" *postgresOwnerConnectionString = "@env" *postgresReadWriteRole = "@env" *postgresOwnerRole = "@env" - *experimentalEnableAuthz = false t.Setenv("ATE_API_POSTGRES_READ_WRITE_CONNECTION_STRING", "runtime-a") t.Setenv("ATE_API_POSTGRES_OWNER_CONNECTION_STRING", "ddl-a") t.Setenv("ATE_API_POSTGRES_READ_WRITE_ROLE", "runtime-role") t.Setenv("ATE_API_POSTGRES_OWNER_ROLE", "ddl-role") - t.Setenv("ATE_API_EXPERIMENTAL_ENABLE_AUTHZ", "true") if err := loadFlagsFromEnv(); err != nil { t.Fatal(err) @@ -75,9 +71,6 @@ func TestLoadFlagsFromEnvResolvesPostgresSourcesOnce(t *testing.T) { *postgresReadWriteRole != "runtime-role" || *postgresOwnerRole != "ddl-role" { t.Fatalf("resolved values = %q, %q, %q, %q", *postgresReadWriteConnectionString, *postgresOwnerConnectionString, *postgresReadWriteRole, *postgresOwnerRole) } - if !*experimentalEnableAuthz { - t.Fatal("authorization environment flag was not resolved alongside PostgreSQL settings") - } t.Setenv("ATE_API_POSTGRES_READ_WRITE_CONNECTION_STRING", "runtime-b") t.Setenv("ATE_API_POSTGRES_OWNER_CONNECTION_STRING", "ddl-b") if err := loadFlagsFromEnv(); err != nil { diff --git a/cmd/kubectl-ate/internal/cmd/access_policy.go b/cmd/kubectl-ate/internal/cmd/access_policy.go new file mode 100644 index 0000000000..092fd8fd56 --- /dev/null +++ b/cmd/kubectl-ate/internal/cmd/access_policy.go @@ -0,0 +1,389 @@ +// 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 cmd + +import ( + "context" + "errors" + "fmt" + "io" + + "github.com/spf13/cobra" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/encoding/protojson" + + "go.ate.dev/substrate/cmd/kubectl-ate/internal/printer" + "go.ate.dev/substrate/internal/resources" + "go.ate.dev/substrate/pkg/proto/ateapipb" +) + +var accessPolicyFlags struct { + atespace string + global bool + filename string + uid string + version int64 +} + +const accessPolicyManifestHelp = `The manifest is a YAML or JSON AccessPolicy, as printed by +"kubectl ate get access-policy -o yaml". Its metadata may be omitted. Members +are principals with a "user:" prefix, e.g. +"user:system:serviceaccount:team-a:alice". + + bindings: + - role: owner + members: + - user:system:serviceaccount:team-a:alice` + +var getAccessPolicyCmd = &cobra.Command{ + Use: "access-policy (-a | --global)", + Aliases: []string{"access-policies"}, + Short: "Get the access policy of an atespace or the global access policy", + Args: cobra.NoArgs, + RunE: runGetAccessPolicy, +} + +var createAccessPolicyCmd = &cobra.Command{ + Use: "access-policy (-a | --global) -f ", + Aliases: []string{"access-policies"}, + Short: "Create an access policy from a manifest", + Long: "Create the access policy of an atespace, or the global access policy.\n\n" + accessPolicyManifestHelp, + Args: cobra.NoArgs, + RunE: runCreateAccessPolicy, +} + +var updateAccessPolicyCmd = &cobra.Command{ + Use: "access-policy (-a | --global) -f ", + Aliases: []string{"access-policies"}, + Short: "Replace an access policy from a manifest", + Long: "Replace the access policy of an atespace, or the global access policy.\n\n" + accessPolicyManifestHelp + ` + +Its metadata.uid and metadata.version are required preconditions; the server +rejects the update when they no longer match those of the stored policy.`, + Args: cobra.NoArgs, + RunE: runUpdateAccessPolicy, +} + +var deleteAccessPolicyCmd = &cobra.Command{ + Use: "access-policy -a ", + Aliases: []string{"access-policies"}, + Short: "Delete the access policy of an atespace", + Long: `Delete the access policy of an atespace. The global access policy cannot be +deleted; update it with no bindings instead.`, + Args: cobra.NoArgs, + RunE: runDeleteAccessPolicy, +} + +// accessPolicyScope is the parent of an access policy: one atespace, or the +// global singleton when atespace is empty. +type accessPolicyScope struct { + atespace string +} + +func (s accessPolicyScope) global() bool { return s.atespace == "" } + +func (s accessPolicyScope) String() string { + if s.global() { + return "global" + } + return fmt.Sprintf("atespace %q", s.atespace) +} + +func (s accessPolicyScope) ref() *ateapipb.ObjectRef { + return &ateapipb.ObjectRef{Name: s.atespace} +} + +// scopeFromFlags requires exactly one of --atespace and --global. +func scopeFromFlags(atespace string, global bool) (accessPolicyScope, error) { + switch { + case atespace != "" && global: + return accessPolicyScope{}, errors.New("--atespace and --global are mutually exclusive") + case atespace == "" && !global: + return accessPolicyScope{}, errors.New("one of --atespace or --global is required") + } + return accessPolicyScope{atespace: atespace}, nil +} + +// accessPolicyFromManifest parses a single protojson-shaped YAML or JSON +// document into an AccessPolicy. Parsing is strict: unknown fields are an +// error. +func accessPolicyFromManifest(data []byte) (*ateapipb.AccessPolicy, error) { + jsonData, err := manifestToJSON(data) + if err != nil { + return nil, err + } + policy := &ateapipb.AccessPolicy{} + if err := protojson.Unmarshal(jsonData, policy); err != nil { + return nil, fmt.Errorf("invalid AccessPolicy: %w", err) + } + return policy, nil +} + +// loadAccessPolicyManifest reads a manifest from filename, or from in when +// filename is "-", parses it, and checks its metadata. An access policy's +// metadata.atespace is always empty, whatever its scope, and its name is +// always "default"; omitted metadata is filled in. +func loadAccessPolicyManifest(in io.Reader, filename string) (*ateapipb.AccessPolicy, error) { + data, err := readFileOrStdin(in, filename) + if err != nil { + return nil, err + } + policy, err := accessPolicyFromManifest(data) + if err != nil { + return nil, fmt.Errorf("failed to parse access policy manifest %q: %w", filename, err) + } + if policy.Metadata == nil { + policy.Metadata = &ateapipb.ResourceMetadata{} + } + if policy.Metadata.Name == "" { + policy.Metadata.Name = resources.SingletonName + } + if policy.Metadata.Name != resources.SingletonName { + return nil, fmt.Errorf("manifest metadata.name %q must be %q", policy.Metadata.Name, resources.SingletonName) + } + if policy.Metadata.Atespace != "" { + return nil, fmt.Errorf("manifest metadata.atespace must be empty, got %q; choose the atespace with --atespace", policy.Metadata.Atespace) + } + return policy, nil +} + +// atespaceGetter abstracts the atespace read that tells a missing atespace +// from a missing access policy. +type atespaceGetter interface { + GetAtespace(ctx context.Context, req *ateapipb.GetAtespaceRequest, opts ...grpc.CallOption) (*ateapipb.Atespace, error) +} + +// requireScope settles a NotFound from an access policy RPC. For an atespace +// the server answers NotFound for a missing atespace and for an atespace +// without a policy alike, so it reads the atespace. It returns nil when the +// scope exists, which means the policy is what is missing. +func requireScope(ctx context.Context, getter atespaceGetter, scope accessPolicyScope) error { + if scope.global() { + return nil + } + if _, err := getter.GetAtespace(ctx, &ateapipb.GetAtespaceRequest{Atespace: scope.ref()}); err != nil { + if status.Code(err) == codes.NotFound { + return fmt.Errorf("atespace %q not found", scope.atespace) + } + return fmt.Errorf("failed to get atespace %q: %w", scope.atespace, err) + } + return nil +} + +// accessPolicyClient is the subset of the Control API the access policy +// commands call. +type accessPolicyClient interface { + GetGlobalAccessPolicy(ctx context.Context, req *ateapipb.GetGlobalAccessPolicyRequest, opts ...grpc.CallOption) (*ateapipb.AccessPolicy, error) + CreateGlobalAccessPolicy(ctx context.Context, req *ateapipb.CreateGlobalAccessPolicyRequest, opts ...grpc.CallOption) (*ateapipb.AccessPolicy, error) + UpdateGlobalAccessPolicy(ctx context.Context, req *ateapipb.UpdateGlobalAccessPolicyRequest, opts ...grpc.CallOption) (*ateapipb.AccessPolicy, error) + GetAtespaceAccessPolicy(ctx context.Context, req *ateapipb.GetAtespaceAccessPolicyRequest, opts ...grpc.CallOption) (*ateapipb.AccessPolicy, error) + CreateAtespaceAccessPolicy(ctx context.Context, req *ateapipb.CreateAtespaceAccessPolicyRequest, opts ...grpc.CallOption) (*ateapipb.AccessPolicy, error) + UpdateAtespaceAccessPolicy(ctx context.Context, req *ateapipb.UpdateAtespaceAccessPolicyRequest, opts ...grpc.CallOption) (*ateapipb.AccessPolicy, error) + DeleteAtespaceAccessPolicy(ctx context.Context, req *ateapipb.DeleteAtespaceAccessPolicyRequest, opts ...grpc.CallOption) (*ateapipb.AccessPolicy, error) + atespaceGetter +} + +// accessPolicyRunner executes the access policy commands. +type accessPolicyRunner struct { + client accessPolicyClient + scope accessPolicyScope + outputFmt string + stdout io.Writer + stderr io.Writer +} + +func (r *accessPolicyRunner) get(ctx context.Context) error { + var policy *ateapipb.AccessPolicy + var err error + if r.scope.global() { + policy, err = r.client.GetGlobalAccessPolicy(ctx, &ateapipb.GetGlobalAccessPolicyRequest{Name: resources.SingletonName}) + } else { + policy, err = r.client.GetAtespaceAccessPolicy(ctx, &ateapipb.GetAtespaceAccessPolicyRequest{Atespace: r.scope.ref(), Name: resources.SingletonName}) + } + if status.Code(err) == codes.NotFound { + if err := requireScope(ctx, r.client, r.scope); err != nil { + return err + } + // No policy is a valid state, not a failure: it grants nothing. + fmt.Fprintf(r.stderr, "%s has no access policy\n", r.scope) + return nil + } + if err != nil { + return fmt.Errorf("failed to get %s access policy: %w", r.scope, err) + } + return printer.PrintAccessPolicyTo(r.stdout, r.scope.atespace, policy, r.outputFmt) +} + +func (r *accessPolicyRunner) create(ctx context.Context, policy *ateapipb.AccessPolicy) error { + var created *ateapipb.AccessPolicy + var err error + if r.scope.global() { + created, err = r.client.CreateGlobalAccessPolicy(ctx, &ateapipb.CreateGlobalAccessPolicyRequest{AccessPolicy: policy}) + } else { + created, err = r.client.CreateAtespaceAccessPolicy(ctx, &ateapipb.CreateAtespaceAccessPolicyRequest{Atespace: r.scope.ref(), AccessPolicy: policy}) + } + if status.Code(err) == codes.AlreadyExists { + return fmt.Errorf(`%s already has an access policy; change it with "kubectl ate update access-policy": %w`, r.scope, err) + } + if err != nil { + return fmt.Errorf("failed to create %s access policy: %w", r.scope, err) + } + return printer.PrintAccessPolicyTo(r.stdout, r.scope.atespace, created, r.outputFmt) +} + +func (r *accessPolicyRunner) update(ctx context.Context, policy *ateapipb.AccessPolicy) error { + var updated *ateapipb.AccessPolicy + var err error + if r.scope.global() { + updated, err = r.client.UpdateGlobalAccessPolicy(ctx, &ateapipb.UpdateGlobalAccessPolicyRequest{AccessPolicy: policy}) + } else { + updated, err = r.client.UpdateAtespaceAccessPolicy(ctx, &ateapipb.UpdateAtespaceAccessPolicyRequest{Atespace: r.scope.ref(), AccessPolicy: policy}) + } + if status.Code(err) == codes.NotFound { + if err := requireScope(ctx, r.client, r.scope); err != nil { + return err + } + return fmt.Errorf(`%s has no access policy to update; create it with "kubectl ate create access-policy"`, r.scope) + } + if status.Code(err) == codes.Aborted { + return fmt.Errorf(`manifest's uid and version preconditions do not match those of the stored %s access policy (changed since it was read, or read from another scope); re-run "get access-policy -o yaml" and reapply the edit: %w`, r.scope, err) + } + if err != nil { + return fmt.Errorf("failed to update %s access policy: %w", r.scope, err) + } + return printer.PrintAccessPolicyTo(r.stdout, r.scope.atespace, updated, r.outputFmt) +} + +func (r *accessPolicyRunner) delete(ctx context.Context, options *ateapipb.DeleteOptions) error { + _, err := r.client.DeleteAtespaceAccessPolicy(ctx, &ateapipb.DeleteAtespaceAccessPolicyRequest{Atespace: r.scope.ref(), Name: resources.SingletonName, Options: options}) + if status.Code(err) == codes.NotFound { + if err := requireScope(ctx, r.client, r.scope); err != nil { + return err + } + return fmt.Errorf("%s has no access policy", r.scope) + } + if err != nil { + return fmt.Errorf("failed to delete %s access policy: %w", r.scope, err) + } + _, err = fmt.Fprintf(r.stdout, "access policy for %s deleted\n", r.scope) + return err +} + +// newAccessPolicyRunner connects to ate-api-server. The returned close func +// releases the connection. +func newAccessPolicyRunner(cmd *cobra.Command, scope accessPolicyScope) (*accessPolicyRunner, func(), error) { + apiClient, err := newAPIClient(cmd) + if err != nil { + return nil, nil, fmt.Errorf("failed to connect to ate-api-server: %w", err) + } + return &accessPolicyRunner{ + client: apiClient, + scope: scope, + outputFmt: outputFmt, + stdout: cmd.OutOrStdout(), + stderr: cmd.ErrOrStderr(), + }, apiClient.Close, nil +} + +func runGetAccessPolicy(cmd *cobra.Command, _ []string) error { + scope, err := scopeFromFlags(accessPolicyFlags.atespace, accessPolicyFlags.global) + if err != nil { + return err + } + runner, closeFn, err := newAccessPolicyRunner(cmd, scope) + if err != nil { + return err + } + defer closeFn() + return runner.get(cmd.Context()) +} + +func runCreateAccessPolicy(cmd *cobra.Command, _ []string) error { + scope, err := scopeFromFlags(accessPolicyFlags.atespace, accessPolicyFlags.global) + if err != nil { + return err + } + policy, err := loadAccessPolicyManifest(cmd.InOrStdin(), accessPolicyFlags.filename) + if err != nil { + return err + } + runner, closeFn, err := newAccessPolicyRunner(cmd, scope) + if err != nil { + return err + } + defer closeFn() + return runner.create(cmd.Context(), policy) +} + +func runUpdateAccessPolicy(cmd *cobra.Command, _ []string) error { + scope, err := scopeFromFlags(accessPolicyFlags.atespace, accessPolicyFlags.global) + if err != nil { + return err + } + policy, err := loadAccessPolicyManifest(cmd.InOrStdin(), accessPolicyFlags.filename) + if err != nil { + return err + } + // Checked before dialing, so an unusable manifest costs no connection. + if policy.GetMetadata().GetUid() == "" || policy.GetMetadata().GetVersion() == 0 { + return fmt.Errorf(`manifest for the %s access policy lacks the metadata.uid and metadata.version preconditions that "get access-policy -o yaml" prints`, scope) + } + runner, closeFn, err := newAccessPolicyRunner(cmd, scope) + if err != nil { + return err + } + defer closeFn() + return runner.update(cmd.Context(), policy) +} + +func runDeleteAccessPolicy(cmd *cobra.Command, _ []string) error { + options, err := deleteOptionsFromFlags(deleteGuardFlags{ + uidSet: cmd.Flags().Changed("uid"), + uid: accessPolicyFlags.uid, + versionSet: cmd.Flags().Changed("version"), + version: accessPolicyFlags.version, + }) + if err != nil { + return err + } + runner, closeFn, err := newAccessPolicyRunner(cmd, accessPolicyScope{atespace: accessPolicyFlags.atespace}) + if err != nil { + return err + } + defer closeFn() + return runner.delete(cmd.Context(), options) +} + +func init() { + for _, c := range []*cobra.Command{getAccessPolicyCmd, createAccessPolicyCmd, updateAccessPolicyCmd} { + c.Flags().StringVarP(&accessPolicyFlags.atespace, "atespace", "a", "", "Atespace whose access policy to use") + c.Flags().BoolVar(&accessPolicyFlags.global, "global", false, "Use the global access policy instead of an atespace's") + } + for _, c := range []*cobra.Command{createAccessPolicyCmd, updateAccessPolicyCmd} { + c.Flags().StringVarP(&accessPolicyFlags.filename, "filename", "f", "", "Manifest file holding one AccessPolicy; use - for stdin (required)") + _ = c.MarkFlagRequired("filename") + } + getCmd.AddCommand(getAccessPolicyCmd) + createCmd.AddCommand(createAccessPolicyCmd) + updateCmd.AddCommand(updateAccessPolicyCmd) + + deleteAccessPolicyCmd.Flags().StringVarP(&accessPolicyFlags.atespace, "atespace", "a", "", "Atespace whose access policy to delete (required)") + _ = deleteAccessPolicyCmd.MarkFlagRequired("atespace") + deleteAccessPolicyCmd.Flags().StringVar(&accessPolicyFlags.uid, "uid", "", "Delete only if the policy's metadata.uid matches; from get -o yaml") + deleteAccessPolicyCmd.Flags().Int64Var(&accessPolicyFlags.version, "version", 0, "Delete only if the policy's metadata.version matches; from get -o yaml") + deleteCmd.AddCommand(deleteAccessPolicyCmd) +} diff --git a/cmd/kubectl-ate/internal/cmd/access_policy_test.go b/cmd/kubectl-ate/internal/cmd/access_policy_test.go new file mode 100644 index 0000000000..9288cac71c --- /dev/null +++ b/cmd/kubectl-ate/internal/cmd/access_policy_test.go @@ -0,0 +1,331 @@ +// 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 cmd + +import ( + "bytes" + "context" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/testing/protocmp" + + "go.ate.dev/substrate/internal/resources" + "go.ate.dev/substrate/pkg/proto/ateapipb" +) + +func TestScopeFromFlags(t *testing.T) { + t.Parallel() + tests := []struct { + name string + atespace string + global bool + want accessPolicyScope + wantErr string + }{ + {name: "atespace", atespace: "team-a", want: accessPolicyScope{atespace: "team-a"}}, + {name: "global", global: true, want: accessPolicyScope{}}, + {name: "neither", wantErr: "one of --atespace or --global is required"}, + {name: "both", atespace: "team-a", global: true, wantErr: "mutually exclusive"}, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + got, err := scopeFromFlags(tc.atespace, tc.global) + if tc.wantErr != "" { + if err == nil || !strings.Contains(err.Error(), tc.wantErr) { + t.Fatalf("scopeFromFlags() error = %v, want %q", err, tc.wantErr) + } + return + } + if err != nil { + t.Fatalf("scopeFromFlags() error = %v", err) + } + if got != tc.want { + t.Errorf("scopeFromFlags() = %+v, want %+v", got, tc.want) + } + }) + } +} + +func TestLoadAccessPolicyManifest(t *testing.T) { + t.Parallel() + ownerAlice := []*ateapipb.Binding{{Role: "owner", Members: []string{"user:alice"}}} + tests := []struct { + name string + manifest string + want *ateapipb.AccessPolicy + wantErr string + }{ + { + name: "metadata omitted is filled in", + manifest: "bindings:\n- role: owner\n members: [\"user:alice\"]\n", + want: &ateapipb.AccessPolicy{Metadata: &ateapipb.ResourceMetadata{Name: "default"}, Bindings: ownerAlice}, + }, + { + name: "preconditions preserved", + manifest: `metadata: {name: default, uid: u-1, version: "3"} +bindings: [{role: owner, members: ["user:alice"]}] +`, + want: &ateapipb.AccessPolicy{Metadata: &ateapipb.ResourceMetadata{Name: "default", Uid: "u-1", Version: 3}, Bindings: ownerAlice}, + }, + { + name: "no bindings", + manifest: "metadata: {name: default}\n", + want: &ateapipb.AccessPolicy{Metadata: &ateapipb.ResourceMetadata{Name: "default"}}, + }, + {name: "wrong name", manifest: "metadata: {name: other}\n", wantErr: `must be "default"`}, + {name: "atespace set", manifest: "metadata: {atespace: team-a}\n", wantErr: "metadata.atespace must be empty"}, + {name: "unknown field", manifest: "bindingz: []\n", wantErr: "invalid AccessPolicy"}, + {name: "empty", manifest: "", wantErr: "manifest is empty"}, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + got, err := loadAccessPolicyManifest(strings.NewReader(tc.manifest), "-") + if tc.wantErr != "" { + if err == nil || !strings.Contains(err.Error(), tc.wantErr) { + t.Fatalf("loadAccessPolicyManifest() error = %v, want %q", err, tc.wantErr) + } + return + } + if err != nil { + t.Fatalf("loadAccessPolicyManifest() error = %v", err) + } + if diff := cmp.Diff(tc.want, got, protocmp.Transform()); diff != "" { + t.Errorf("loadAccessPolicyManifest() (-want +got):\n%s", diff) + } + }) + } +} + +// fakeAccessPolicyClient records the last RPC and answers with policy or err. +type fakeAccessPolicyClient struct { + policy *ateapipb.AccessPolicy + err error + atespaceErr error + + method string + req any +} + +func (f *fakeAccessPolicyClient) answer(method string, req any) (*ateapipb.AccessPolicy, error) { + f.method, f.req = method, req + return f.policy, f.err +} + +func (f *fakeAccessPolicyClient) GetGlobalAccessPolicy(_ context.Context, req *ateapipb.GetGlobalAccessPolicyRequest, _ ...grpc.CallOption) (*ateapipb.AccessPolicy, error) { + return f.answer("GetGlobalAccessPolicy", req) +} + +func (f *fakeAccessPolicyClient) CreateGlobalAccessPolicy(_ context.Context, req *ateapipb.CreateGlobalAccessPolicyRequest, _ ...grpc.CallOption) (*ateapipb.AccessPolicy, error) { + return f.answer("CreateGlobalAccessPolicy", req) +} + +func (f *fakeAccessPolicyClient) UpdateGlobalAccessPolicy(_ context.Context, req *ateapipb.UpdateGlobalAccessPolicyRequest, _ ...grpc.CallOption) (*ateapipb.AccessPolicy, error) { + return f.answer("UpdateGlobalAccessPolicy", req) +} + +func (f *fakeAccessPolicyClient) GetAtespaceAccessPolicy(_ context.Context, req *ateapipb.GetAtespaceAccessPolicyRequest, _ ...grpc.CallOption) (*ateapipb.AccessPolicy, error) { + return f.answer("GetAtespaceAccessPolicy", req) +} + +func (f *fakeAccessPolicyClient) CreateAtespaceAccessPolicy(_ context.Context, req *ateapipb.CreateAtespaceAccessPolicyRequest, _ ...grpc.CallOption) (*ateapipb.AccessPolicy, error) { + return f.answer("CreateAtespaceAccessPolicy", req) +} + +func (f *fakeAccessPolicyClient) UpdateAtespaceAccessPolicy(_ context.Context, req *ateapipb.UpdateAtespaceAccessPolicyRequest, _ ...grpc.CallOption) (*ateapipb.AccessPolicy, error) { + return f.answer("UpdateAtespaceAccessPolicy", req) +} + +func (f *fakeAccessPolicyClient) DeleteAtespaceAccessPolicy(_ context.Context, req *ateapipb.DeleteAtespaceAccessPolicyRequest, _ ...grpc.CallOption) (*ateapipb.AccessPolicy, error) { + return f.answer("DeleteAtespaceAccessPolicy", req) +} + +func (f *fakeAccessPolicyClient) GetAtespace(_ context.Context, req *ateapipb.GetAtespaceRequest, _ ...grpc.CallOption) (*ateapipb.Atespace, error) { + if f.atespaceErr != nil { + return nil, f.atespaceErr + } + return &ateapipb.Atespace{Metadata: &ateapipb.ResourceMetadata{Name: req.GetAtespace().GetName()}}, nil +} + +func TestAccessPolicyRunner(t *testing.T) { + t.Parallel() + stored := &ateapipb.AccessPolicy{ + Metadata: &ateapipb.ResourceMetadata{Name: "default", Uid: "u-1", Version: 2}, + Bindings: []*ateapipb.Binding{{Role: "owner", Members: []string{"user:alice"}}}, + } + edit := &ateapipb.AccessPolicy{Metadata: &ateapipb.ResourceMetadata{Name: "default", Uid: "u-1", Version: 2}} + teamA := accessPolicyScope{atespace: "team-a"} + global := accessPolicyScope{} + notFound := status.Error(codes.NotFound, "not found") + + tests := []struct { + name string + scope accessPolicyScope + client *fakeAccessPolicyClient + run func(context.Context, *accessPolicyRunner) error + wantMethod string + wantReq any + wantStdout string + wantStderr string + wantErr string + wantNoCalls bool + }{ + { + name: "get atespace", + scope: teamA, + client: &fakeAccessPolicyClient{policy: stored}, + run: func(ctx context.Context, r *accessPolicyRunner) error { return r.get(ctx) }, + wantMethod: "GetAtespaceAccessPolicy", + wantReq: &ateapipb.GetAtespaceAccessPolicyRequest{Atespace: &ateapipb.ObjectRef{Name: "team-a"}, Name: resources.SingletonName}, + wantStdout: "team-a owner user:alice", + }, + { + name: "get global", + scope: global, + client: &fakeAccessPolicyClient{policy: stored}, + run: func(ctx context.Context, r *accessPolicyRunner) error { return r.get(ctx) }, + wantMethod: "GetGlobalAccessPolicy", + wantReq: &ateapipb.GetGlobalAccessPolicyRequest{Name: resources.SingletonName}, + wantStdout: " owner user:alice", + }, + { + name: "get atespace without a policy", + scope: teamA, + client: &fakeAccessPolicyClient{err: notFound}, + run: func(ctx context.Context, r *accessPolicyRunner) error { return r.get(ctx) }, + wantMethod: "GetAtespaceAccessPolicy", + wantStderr: `atespace "team-a" has no access policy`, + }, + { + name: "get missing atespace", + scope: teamA, + client: &fakeAccessPolicyClient{err: notFound, atespaceErr: notFound}, + run: func(ctx context.Context, r *accessPolicyRunner) error { return r.get(ctx) }, + wantMethod: "GetAtespaceAccessPolicy", + wantErr: `atespace "team-a" not found`, + }, + { + name: "get permission denied", + scope: teamA, + client: &fakeAccessPolicyClient{err: status.Error(codes.PermissionDenied, "denied")}, + run: func(ctx context.Context, r *accessPolicyRunner) error { return r.get(ctx) }, + wantMethod: "GetAtespaceAccessPolicy", + wantErr: "PermissionDenied", + }, + { + name: "create atespace", + scope: teamA, + client: &fakeAccessPolicyClient{policy: stored}, + run: func(ctx context.Context, r *accessPolicyRunner) error { return r.create(ctx, edit) }, + wantMethod: "CreateAtespaceAccessPolicy", + wantReq: &ateapipb.CreateAtespaceAccessPolicyRequest{Atespace: &ateapipb.ObjectRef{Name: "team-a"}, AccessPolicy: edit}, + wantStdout: "team-a owner user:alice", + }, + { + name: "create global", + scope: global, + client: &fakeAccessPolicyClient{policy: stored}, + run: func(ctx context.Context, r *accessPolicyRunner) error { return r.create(ctx, edit) }, + wantMethod: "CreateGlobalAccessPolicy", + wantReq: &ateapipb.CreateGlobalAccessPolicyRequest{AccessPolicy: edit}, + }, + { + name: "create existing", + scope: teamA, + client: &fakeAccessPolicyClient{err: status.Error(codes.AlreadyExists, "exists")}, + run: func(ctx context.Context, r *accessPolicyRunner) error { return r.create(ctx, edit) }, + wantMethod: "CreateAtespaceAccessPolicy", + wantErr: "kubectl ate update access-policy", + }, + { + name: "update global", + scope: global, + client: &fakeAccessPolicyClient{policy: stored}, + run: func(ctx context.Context, r *accessPolicyRunner) error { return r.update(ctx, edit) }, + wantMethod: "UpdateGlobalAccessPolicy", + wantReq: &ateapipb.UpdateGlobalAccessPolicyRequest{AccessPolicy: edit}, + }, + { + name: "update stale", + scope: teamA, + client: &fakeAccessPolicyClient{err: status.Error(codes.Aborted, "version mismatch")}, + run: func(ctx context.Context, r *accessPolicyRunner) error { return r.update(ctx, edit) }, + wantMethod: "UpdateAtespaceAccessPolicy", + wantErr: "re-run", + }, + { + name: "update without a policy", + scope: teamA, + client: &fakeAccessPolicyClient{err: notFound}, + run: func(ctx context.Context, r *accessPolicyRunner) error { return r.update(ctx, edit) }, + wantMethod: "UpdateAtespaceAccessPolicy", + wantErr: "kubectl ate create access-policy", + }, + { + name: "delete", + scope: teamA, + client: &fakeAccessPolicyClient{policy: stored}, + run: func(ctx context.Context, r *accessPolicyRunner) error { + return r.delete(ctx, &ateapipb.DeleteOptions{Version: 2}) + }, + wantMethod: "DeleteAtespaceAccessPolicy", + wantReq: &ateapipb.DeleteAtespaceAccessPolicyRequest{Atespace: &ateapipb.ObjectRef{Name: "team-a"}, Name: resources.SingletonName, Options: &ateapipb.DeleteOptions{Version: 2}}, + wantStdout: `access policy for atespace "team-a" deleted`, + }, + { + name: "delete without a policy", + scope: teamA, + client: &fakeAccessPolicyClient{err: notFound}, + run: func(ctx context.Context, r *accessPolicyRunner) error { return r.delete(ctx, nil) }, + wantMethod: "DeleteAtespaceAccessPolicy", + wantErr: `atespace "team-a" has no access policy`, + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + var stdout, stderr bytes.Buffer + r := &accessPolicyRunner{client: tc.client, scope: tc.scope, outputFmt: "table", stdout: &stdout, stderr: &stderr} + err := tc.run(t.Context(), r) + if tc.wantErr != "" { + if err == nil || !strings.Contains(err.Error(), tc.wantErr) { + t.Fatalf("error = %v, want it to contain %q", err, tc.wantErr) + } + } else if err != nil { + t.Fatalf("error = %v", err) + } + if tc.client.method != tc.wantMethod { + t.Errorf("called %q, want %q", tc.client.method, tc.wantMethod) + } + if tc.wantReq != nil { + if diff := cmp.Diff(tc.wantReq, tc.client.req, protocmp.Transform()); diff != "" { + t.Errorf("request (-want +got):\n%s", diff) + } + } + if !strings.Contains(stdout.String(), tc.wantStdout) { + t.Errorf("stdout = %q, want it to contain %q", stdout.String(), tc.wantStdout) + } + if !strings.Contains(stderr.String(), tc.wantStderr) { + t.Errorf("stderr = %q, want it to contain %q", stderr.String(), tc.wantStderr) + } + }) + } +} diff --git a/cmd/kubectl-ate/internal/cmd/verbose.go b/cmd/kubectl-ate/internal/cmd/verbose.go index c75ec935b6..c6ef24a6e9 100644 --- a/cmd/kubectl-ate/internal/cmd/verbose.go +++ b/cmd/kubectl-ate/internal/cmd/verbose.go @@ -75,6 +75,7 @@ func loadKubeConfig(errOut io.Writer, kubeconfigPath, contextName string) (*rest func newAPIClient(cmd *cobra.Command) (*ateclient.Client, error) { ctx := cmd.Context() errOut := cmd.ErrOrStderr() + warnSharedIdentity(errOut, tokenFile) if verbosity >= vFlow { vlogf(errOut, vFlow, "Connecting to ate-api-server (endpoint=%q, kubeconfig=%q, context=%q, token-file=%q, trace=%t)", @@ -105,6 +106,18 @@ func newAPIClient(cmd *cobra.Command) (*ateclient.Client, error) { return cli, nil } +// warnSharedIdentity warns on errOut when the command will act as the shared +// fallback ServiceAccount rather than as the user. Everyone relying on the +// fallback is the same principal, holding whatever that principal is granted: +// global owner wherever the install names it a bootstrap owner. +func warnSharedIdentity(errOut io.Writer, tokenFile string) { + if tokenFile != "" { + return + } + fmt.Fprintf(errOut, "Warning: no --token-file given; acting as the shared identity %s rather than as yourself. Pass --token-file to act as yourself.\n", + ateclient.FallbackPrincipal()) +} + func formatProtoCompact(v any) string { msg, ok := v.(proto.Message) if !ok || msg == nil { diff --git a/cmd/kubectl-ate/internal/cmd/verbose_test.go b/cmd/kubectl-ate/internal/cmd/verbose_test.go index d161d40b9c..ad36577387 100644 --- a/cmd/kubectl-ate/internal/cmd/verbose_test.go +++ b/cmd/kubectl-ate/internal/cmd/verbose_test.go @@ -287,3 +287,23 @@ func TestTopWorkersRunner_VerboseSurfacesMetricsIssues(t *testing.T) { } }) } + +func TestWarnSharedIdentity(t *testing.T) { + t.Run("warns without a token file", func(t *testing.T) { + var buf bytes.Buffer + warnSharedIdentity(&buf, "") + got := buf.String() + for _, want := range []string{"Warning:", "ate-client", "shared identity", "--token-file"} { + if !strings.Contains(got, want) { + t.Errorf("warning %q does not mention %q", got, want) + } + } + }) + t.Run("silent with a token file", func(t *testing.T) { + var buf bytes.Buffer + warnSharedIdentity(&buf, "/path/to/token") + if got := buf.String(); got != "" { + t.Errorf("warning = %q, want none", got) + } + }) +} diff --git a/cmd/kubectl-ate/internal/printer/access_policy.go b/cmd/kubectl-ate/internal/printer/access_policy.go new file mode 100644 index 0000000000..a260e02897 --- /dev/null +++ b/cmd/kubectl-ate/internal/printer/access_policy.go @@ -0,0 +1,50 @@ +// 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 printer + +import ( + "fmt" + "io" + "strings" + "text/tabwriter" + + "go.ate.dev/substrate/pkg/proto/ateapipb" +) + +// PrintAccessPolicyTo prints an access policy, one table row per binding. +// json and yaml emit the bare AccessPolicy. The policy's metadata carries no +// atespace, so the caller passes it; empty means the global policy. +func PrintAccessPolicyTo(out io.Writer, atespace string, policy *ateapipb.AccessPolicy, format string) error { + switch format { + case "json", "yaml": + return printProto(out, policy, format) + case "table": + scope := atespace + if scope == "" { + scope = "" + } + w := tabwriter.NewWriter(out, 0, 0, 3, ' ', 0) + fmt.Fprintln(w, "SCOPE\tROLE\tMEMBERS") + if len(policy.GetBindings()) == 0 { + fmt.Fprintf(w, "%s\t\t\n", scope) + } + for _, b := range policy.GetBindings() { + fmt.Fprintf(w, "%s\t%s\t%s\n", scope, b.GetRole(), strings.Join(b.GetMembers(), ",")) + } + return w.Flush() + default: + return fmt.Errorf("unsupported format %q", format) + } +} diff --git a/cmd/kubectl-ate/internal/printer/access_policy_test.go b/cmd/kubectl-ate/internal/printer/access_policy_test.go new file mode 100644 index 0000000000..e068c025ce --- /dev/null +++ b/cmd/kubectl-ate/internal/printer/access_policy_test.go @@ -0,0 +1,79 @@ +// 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 printer + +import ( + "bytes" + "strings" + "testing" + + "go.ate.dev/substrate/pkg/proto/ateapipb" +) + +func TestPrintAccessPolicyTo(t *testing.T) { + policy := &ateapipb.AccessPolicy{ + Metadata: &ateapipb.ResourceMetadata{Name: "default", Uid: "u-1", Version: 2}, + Bindings: []*ateapipb.Binding{ + {Role: "owner", Members: []string{"user:alice", "user:bob"}}, + {Role: "viewer", Members: []string{"user:carol"}}, + }, + } + tests := []struct { + name string + atespace string + policy *ateapipb.AccessPolicy + format string + want []string + wantErr bool + }{ + { + name: "table", atespace: "team-a", policy: policy, format: "table", + want: []string{"SCOPE", "team-a owner user:alice,user:bob", "team-a viewer user:carol"}, + }, + { + name: "global table", policy: policy, format: "table", + want: []string{" owner"}, + }, + { + name: "no bindings", atespace: "team-a", policy: &ateapipb.AccessPolicy{}, format: "table", + want: []string{"team-a "}, + }, + { + name: "yaml carries preconditions", atespace: "team-a", policy: policy, format: "yaml", + want: []string{"uid: u-1", `version: "2"`, "role: owner"}, + }, + {name: "unknown format", policy: policy, format: "xml", wantErr: true}, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + var out bytes.Buffer + err := PrintAccessPolicyTo(&out, tc.atespace, tc.policy, tc.format) + if tc.wantErr { + if err == nil { + t.Fatal("PrintAccessPolicyTo() succeeded, want an error") + } + return + } + if err != nil { + t.Fatalf("PrintAccessPolicyTo() error = %v", err) + } + for _, w := range tc.want { + if !strings.Contains(out.String(), w) { + t.Errorf("output = %q, want it to contain %q", out.String(), w) + } + } + }) + } +} diff --git a/docs/architecture.md b/docs/architecture.md index 9b57095e8f..a0f6f193bd 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -256,9 +256,9 @@ and Performance: SandboxConfigs) as Kubernetes objects allows platform teams to apply familiar RBAC, auditing, and policy enforcement to the underlying infrastructure, while workload definitions (ActorTemplates) are managed - through the substrate API instead, which authenticates callers itself - but does not yet implement authorization (see - [authentication.md](authentication.md)). + through the substrate API instead, which authenticates and authorizes + callers itself (see [authentication.md](authentication.md) and + [authorization.md](authorization.md)). ### Resource Model diff --git a/docs/authentication.md b/docs/authentication.md index 4ebc6ad08d..e7c6c0fd56 100644 --- a/docs/authentication.md +++ b/docs/authentication.md @@ -24,11 +24,10 @@ matches. `certificateAuthorityFile` and `discoveryTokenFile` are optional and are needed for OIDC discovery against some private Kubernetes API servers. `actorIdentityJWTProvider` identifies the provider allowed to call -`ActorIdentity.MintJWT`. Other authenticated providers can call every RPC. -Authorization and RBAC are not implemented yet, so only configure providers -whose users should have full control of the entire control plane: every -atespace, actor, actor template, egress policy, snapshot and worker in the -cluster. +`ActorIdentity.MintJWT`. What an authenticated caller may do is decided by +authorization; see [authorization.md](authorization.md). Its principal ID does +not record which provider issued the token, so only configure providers you +trust not to issue a token whose subject is another principal's ID. ## Google Cloud CLI tokens diff --git a/docs/authorization.md b/docs/authorization.md new file mode 100644 index 0000000000..6943ada4c5 --- /dev/null +++ b/docs/authorization.md @@ -0,0 +1,347 @@ +# ate-api Authorization + +> [!WARNING] +> **Do not keep `ate-client` as the bootstrap owner outside development.** +> `ate-setup` makes the shared `ate-client` ServiceAccount the default +> bootstrap owner so the demos, benchmarks and e2e tests work. Anyone who can +> mint a token for it, including every `kubectl ate` user who doesn't pass +> `--token-file`, is then a global owner. For a shared or production install, +> set [bootstrap owners](#bootstrap-owners) to your operators' own principals. + +`ate-api-server` checks every API call against an +[OpenFGA](https://openfga.dev) model before running it. OpenFGA is embedded in +`ate-api-server` and stores its data in the same PostgreSQL database. For how +callers authenticate, see [authentication.md](authentication.md). + +## How a call is checked + +1. **Authentication** turns the credential into a principal ID, or rejects it + with `Unauthenticated`. +2. **Authorization** allows the call if the principal is a bootstrap owner, + holds a role from an access policy that permits it, or is a Substrate + component with the system role it needs. Otherwise, or if the RPC has no + rule, it returns `PermissionDenied`. + +## Settings + +These are `ate-setup` settings, so the environment variable is read by +`ate-setup`. It passes them to `ate-api-server` as the +`--experimental-enable-authz` and `--authz-bootstrap-owners` flags on its +container; an install that doesn't use `ate-setup` sets those flags in its +manifest. A change rolls `ate-api-server`. + +| Setting | Environment variable | Example | +|---|---|---| +| `ateapi.authz.enabled` | `ATE_API_EXPERIMENTAL_ENABLE_AUTHZ` | `true` (the default) | +| `ateapi.authz.bootstrapOwners` | `ATE_API_AUTHZ_BOOTSTRAP_OWNERS` | `alice@example.com,bob@example.com` | + +If you don't set `ateapi.authz.bootstrapOwners`, `ate-setup` uses the shared +`ate-client` ServiceAccount (`system:serviceaccount::ate-client`), +so the demos, benchmarks and e2e tests work. See the warning at the top of +this page. + +```sh +ATE_API_EXPERIMENTAL_ENABLE_AUTHZ=false hack/install-ate.sh --deploy-ate-system # turn it off +``` + +With authorization off, access policy RPCs are still checked, so nobody can +grant themselves access while it is off and keep it once it is on. Bootstrap +owners still apply, so they can set up policies before turning it on. + +## Principals + +A principal is the ID authentication produces. Access policies name it as +`user:`; bootstrap owners list the bare ``. + +| Caller | `` | +|---|---| +| Kubernetes ServiceAccount token | `system:serviceaccount::` | +| Other JWT provider, such as Google | The token's `sub` (for Google, a numeric account ID, not the email) | +| Pod certificate (mTLS) | Its SPIFFE ID, e.g. `spiffe://cluster.local/ns/benchmarking/sa/default` | + +A denied call names the principal in the error with `:` percent-encoded +(`user:system%3Aserviceaccount%3A...`); decode it before using it. + +## Bootstrap owners + +Bootstrap owners always hold `owner` on the global scope, whatever the access +policies say. They write an install's first access policies and recover access +if those break. + +- They come from configuration, not the API. `ate-api-server` reads them at + startup and exits if authorization is on and the list is empty. Restarting + it with a new list takes effect immediately. +- Any principal `ate-api-server` can authenticate can be one. +- `ate-setup` warns whenever `ate-client` is one. +- Keep the list short. Grant day-to-day admins `owner` in the global access + policy instead, where it can be revoked through the API. + +For example, with a JWT provider whose tokens carry the user's email as `sub`: + +```sh +ATE_API_AUTHZ_BOOTSTRAP_OWNERS=alice@example.com,bob@example.com hack/install-ate.sh --deploy-ate-system +``` + +To make your Google account a bootstrap owner, use its numeric `sub`: + +```sh +gcloud auth print-identity-token | cut -d. -f2 | base64 -d 2>/dev/null | jq -r .sub # your sub +ATE_API_AUTHZ_BOOTSTRAP_OWNERS=,system:serviceaccount:ate-system:ate-client \ + hack/install-ate.sh --deploy-ate-system # keep ate-client for demos and benchmarks +``` + +Then add the Google provider to the `ate-api-authentication` ConfigMap and +restart `ate-api-server` +([authentication.md](authentication.md#google-cloud-cli-tokens)), and act as +yourself: + +```sh +gcloud auth print-identity-token > me.token +kubectl ate --token-file me.token get atespaces +``` + +## `kubectl ate` and `ate-setup` identities + +`kubectl ate` sends the token from `--token-file` (`-` for stdin). Without +one, it uses your kubeconfig to mint a token for `ate-client` (or +`ATE_CLIENT_SERVICE_ACCOUNT`) and warns that it is acting as a shared +identity. It still needs Kubernetes rights to port-forward to `ate-api-server` +unless you pass `--endpoint`. + +`ate-setup` calls the API only to deploy and delete demos and benchmarks, as +`ate-client`. If you leave `ate-client` out of the bootstrap owners, grant it +access in a policy or those steps are denied. + +## Roles + +Roles are granted on the **global** scope, which covers every atespace, or on +one **atespace**. + +| Permission | Global owner | Global viewer | Atespace owner | Atespace editor | Atespace viewer | +|---|:-:|:-:|:-:|:-:|:-:| +| Create atespaces; list all atespaces | ✅ | list only | | | | +| Get the atespace | ✅ | ✅ | ✅ | ✅ | ✅ | +| Delete the atespace | ✅ | | ✅ | | | +| Get access policies | ✅ | ✅ | ✅ (its own) | ✅ (its own) | ✅ (its own) | +| Create, update, delete access policies | ✅ | | ✅ (its own) | | | +| Get and list actors, actor templates, tags, egress policies | ✅ | ✅ | ✅ | ✅ | ✅ | +| Create, update, delete actors and actor templates | ✅ | | ✅ | ✅ | | +| Pause, resume, suspend, revert actors | ✅ | | ✅ | ✅ | | +| Create, update, delete egress policies | ✅ | | ✅ | ✅ | | +| Create, publish, delete tags | ✅ | | ✅ | ✅ | | +| Create an actor from a tag (`sourceTag`) | ✅ | | ✅ | ✅ | | +| Workers: get, list, list assignments | ✅ | ✅ | | | | +| Workers: create, update, delete, drain | ✅ | | | | | + +### System roles + +Substrate's components hold roles that access policies cannot grant, and only +through their pod certificates. `ate-api-server`'s `--controller-spiffe-id`, +`--egress-gateway-spiffe-id` and `--ingress-router-spiffe-id` flags bind them, +defaulting to the install's; `atelet`'s comes from `--atelet-service-account`. + +| Role | Held by | Allows | +|---|---|---| +| `controller` | `ate-controller` | Create, get, list, update, delete and drain workers | +| `egress_gateway` | `atenet-egress` | Get actors and their egress policies; list actors across all atespaces; mint actor JWTs and certificates | +| `ingress_router` | `atenet-router` | Resume actors; list actors across all atespaces | +| `atelet` | `atelet` | Get actors' egress policies, for the worker that enforces them | + +Only `egress_gateway` can mint actor credentials: not even a global owner can, +since a credential lets its holder act as the actor. + +## Checks per RPC + +A gRPC interceptor makes every check before the handler runs +([`registry.go`](../cmd/ateapi/internal/authz/registry.go), +[`model.fga`](../cmd/ateapi/internal/authz/model.fga)). When an RPC lists +several checks, all must pass. + +Roles inherit: an owner is also an editor and a viewer, an editor is also a +viewer, and a global role holds the same role in every atespace. So +"atespace editor" below also means atespace owner, global owner, and so on. + +### Atespaces and access policies + +| RPC | Permission, on | Allowed for | +|---|---|---| +| `CreateAtespace` | `can_create_atespace`, global | Global owner | +| `ListAtespaces` | `can_list_atespaces`, global | Global viewer | +| `GetAtespace` | `can_get`, the atespace | Atespace viewer | +| `DeleteAtespace` | `can_delete`, the atespace | Atespace owner | +| `GetGlobalAccessPolicy` | `can_get_access_policy`, global | Global viewer | +| `CreateGlobalAccessPolicy`, `UpdateGlobalAccessPolicy` | `can_create_access_policy` / `can_update_access_policy`, global | Global owner | +| `GetAtespaceAccessPolicy` | `can_get_access_policy`, the atespace | Atespace viewer | +| `CreateAtespaceAccessPolicy`, `UpdateAtespaceAccessPolicy`, `DeleteAtespaceAccessPolicy` | `can_create_access_policy` / `can_update_access_policy` / `can_delete_access_policy`, the atespace | Atespace owner | + +The access policy RPCs are checked even with authorization off. + +### Actor templates and actors + +| RPC | Permission, on | Allowed for | +|---|---|---| +| `CreateActorTemplate` | `can_create_actor_template`, `metadata.atespace` | Atespace editor | +| `GetActorTemplate` | `can_get`, the template | Viewer of its atespace | +| `DeleteActorTemplate` | `can_delete`, the template | Editor of its atespace | +| `ListActorTemplates` | `can_list_actor_templates`, the atespace, or global if none is given | Atespace viewer; global viewer for all atespaces | +| `CreateActor` | 1. `can_create_actor`, the actor's atespace
2. `can_use_template`, the actor template
3. `can_use_tag`, the `sourceTag`, if one is given | 1. Atespace editor
2. Viewer of the template's atespace, which may be another one
3. Editor of the tag's atespace. Golden tags skip this check: the handler only accepts one built from the actor's own template. | +| `UpdateActor` | 1. `can_update`, the actor
2. `can_use_template`, the template in the request | 1. Editor of its atespace
2. Viewer of the template's atespace, so an actor can't be switched to a template its editor can't use | +| `GetActor` | `can_get`, the actor | Viewer of its atespace; `atenet-egress` | +| `DeleteActor` | `can_delete`, the actor | Editor of its atespace | +| `ListActors` | `can_list_actors`, the atespace, or global if none is given | Atespace viewer; for all atespaces, global viewer, `atenet-router` or `atenet-egress` | +| `PauseActor`, `SuspendActor`, `RevertActor` | `can_pause` / `can_suspend` / `can_revert`, the actor | Editor of its atespace | +| `ResumeActor` | `can_resume`, the actor | Editor of its atespace; `atenet-router` | +| `GetActorEgressPolicy` | `can_get_egress_policy`, the actor | Viewer of its atespace; `atenet-egress`; `atelet` | +| `CreateActorEgressPolicy`, `UpdateActorEgressPolicy`, `DeleteActorEgressPolicy` | `can_create_egress_policy` / `can_update_egress_policy` / `can_delete_egress_policy`, the actor | Editor of its atespace | +| `MintActorJwt`, `MintActorCertificate` | `can_mint_actor_jwt` / `can_mint_actor_certificate`, the actor | `atenet-egress` only | + +### Tags + +| RPC | Permission, on | Allowed for | +|---|---|---| +| `CreateTag` | `can_create_tag`, the source actor's atespace, where the tag is stored | Atespace editor | +| `GetTag` | `can_get`, the tag | Viewer of its atespace | +| `UpdateTag` (including publishing) | `can_update`, the tag | Editor of its atespace | +| `DeleteTag` | `can_delete`, the tag | Editor of its atespace | +| `ListTags` | `can_list_tags`, the atespace, or global if none is given | Atespace viewer; global viewer for all atespaces | + +### Workers + +Workers have no atespace; their permissions come from the global scope. + +| RPC | Permission, on | Allowed for | +|---|---|---| +| `CreateWorker` | `can_create_worker`, global | Global owner; `ate-controller` | +| `ListWorkers` | `can_list_workers`, global | Global viewer; `ate-controller` | +| `GetWorker` | `can_get`, the worker | Global viewer; `ate-controller` | +| `UpdateWorker`, `MarkWorkerAvailable`, `MarkWorkerUnavailable` | `can_update`, the worker | Global owner; `ate-controller` | +| `DeleteWorker` | `can_delete`, the worker | Global owner; `ate-controller` | +| `DrainWorker` | `can_drain`, the worker | Global owner; `ate-controller` | +| `ListWorkerActorAssignments` | `can_list_actor_assignments`, the worker | Global viewer | + +### WorkerService + +The interceptor skips these; each handler requires `atelet`'s pod +certificate. + +| RPC | The caller must be | +|---|---| +| `RegisterWorker` | `atelet` on the worker's node | +| `RequestActorSuspend` | `atelet` on the node of the worker hosting the actor | +| `MintWorkerActorJwt` | `atelet` on the node hosting the actor | +| `MintWorkerActorCertificate` | `atelet` on the node of the worker the actor is assigned to | + +`ReportActorSnapshotState` has a rule but is not served yet. + +### Every RPC + +With authorization on: + +- An RPC with no rule is denied, and a test fails the build if a served RPC + has none. +- An unauthenticated caller gets `Unauthenticated` before its request is read. +- A missing or invalid resource name returns `InvalidArgument`; no check is + skipped. + +## Managing access + +Each scope has one access policy, named `default`, managed with `kubectl ate` +using `--global` or `-a `: + +```yaml +# policy.yaml +metadata: + name: default +bindings: +- role: editor + members: + - user:system:serviceaccount:team-a:alice +- role: viewer + members: + - user:system:serviceaccount:team-a:bob +``` + +```sh +kubectl ate --token-file me.token create access-policy -a team-a -f policy.yaml +kubectl ate --token-file me.token get access-policy -a team-a -o yaml > current.yaml +kubectl ate --token-file me.token update access-policy -a team-a -f current.yaml +kubectl ate --token-file me.token delete access-policy -a team-a +``` + +- Global policies accept `owner` and `viewer`; atespace policies also accept + `editor`. +- `update` replaces the whole policy and needs the `uid` and `version` from + `get -o yaml`. It fails if the policy changed, or was deleted and recreated, + since. +- The global policy can't be deleted; update it with no bindings instead. +- Deleting an atespace deletes its policy, so a recreated atespace starts + with none. +- Changes take effect on the next call. + +## Recovering access + +If no principal anyone can authenticate as holds `owner` (for example, a +Google account listed by email instead of its numeric `sub`), anyone who can +redeploy `ate-api-server` can recover: redeploy with a bootstrap owner they +can use, fix the policies, then redeploy with the intended owners. + +```sh +ATE_API_AUTHZ_BOOTSTRAP_OWNERS=alice@example.com hack/install-ate.sh --deploy-ate-system +``` + +## Production readiness + +For a shared cluster, beyond replacing the default bootstrap owner: + +- **Give people their own identities** in access policies, and have them pass + `--token-file` with a token from a trusted JWT provider or their own + ServiceAccount. To let a user mint tokens only for their own ServiceAccount: + + ```yaml + apiVersion: rbac.authorization.k8s.io/v1 + kind: Role + metadata: {name: mint-alice-token, namespace: team-a} + rules: + - apiGroups: [""] + resources: ["serviceaccounts/token"] + resourceNames: ["alice"] + verbs: ["create"] + ``` + + ```sh + kubectl create token -n team-a alice --audience api.ate-system.svc > alice.token + kubectl ate --token-file alice.token get actors -a team-a + ``` + +- **Limit who can act as a ServiceAccount bootstrap owner.** Anyone who can + create `serviceaccounts/token`, pods or Secrets in its namespace, or change + RBAC there, can get its token. A + `kubernetes.io/service-account-token` Secret gets one that never expires. + Check who can, and alert on the audit events: + + ```sh + kubectl auth can-i create serviceaccounts/ate-client --subresource=token -n ate-system --as alice@example.com + ``` + +### Not yet supported + +- **Signing in.** `kubectl ate` has no login flow or credential plugin; people + obtain tokens themselves. +- **JWT providers at install time.** `ate-setup` configures only the cluster's + ServiceAccount issuer; add others to the ConfigMap afterwards. +- **Groups.** Policy members are individual principals. + +## Known limitations + +- **Tags can copy an actor's data.** A tag is a durable copy of an actor's + memory and disk, including secrets, and outlives the actor. Any atespace + editor can tag, publish and clone any actor in the atespace. +- **Principals are not qualified by issuer.** If `ate-api-server` trusts more + than one JWT provider, any of them that can issue a token whose `sub` is + another principal's ID, a bootstrap owner's included, acts as that principal. + System roles are unaffected: they need the component's pod certificate. +- **The atespace is the only boundary.** There are no per-actor or + per-template grants. +- **Any atelet can read any actor's egress policy.** Every node's atelet + shares one identity, so the `atelet` role isn't limited to actors on its + node ([#1564](https://github.com/agent-substrate/substrate/issues/1564)). diff --git a/docs/operator-install.md b/docs/operator-install.md index 9d64e3411c..08bfc93d68 100644 --- a/docs/operator-install.md +++ b/docs/operator-install.md @@ -263,6 +263,8 @@ changes it. Defaults are also shown by `ate-setup --help` and by `--help` on the owning command. A setting with no default is unset unless something supplies it. +The `ateapi.authz.*` settings are described in +[authorization.md](authorization.md). Settings for the demos, benchmarks, and e2e tests are listed with them: the [counter](../demos/counter/README.md#settings) and @@ -274,6 +276,8 @@ demos, the [benchmarks](../benchmarking/README.md#settings), and the |----------------------------------------------|-------------------------------------------------|----------------------------------------------------|-----------------------|------------------------------------------| | `actorJWT.algorithm` | `ACTOR_JWT_ALGORITHM` | `--actor-jwt-algorithm` | `ES256` | (global) | | `actorJWT.issuer` | `ACTOR_JWT_ISSUER` | `--actor-jwt-issuer` | — | (global) | +| `ateapi.authz.bootstrapOwners` | `ATE_API_AUTHZ_BOOTSTRAP_OWNERS` | `--ateapi-authz-bootstrap-owners` | `system:serviceaccount::ate-client` | (global) | +| `ateapi.authz.enabled` | `ATE_API_EXPERIMENTAL_ENABLE_AUTHZ` | `--ateapi-experimental-enable-authz` | `true` | (global) | | `ateapi.expectedJWTIssuer` | `EXPECTED_JWT_ISSUER` | `--ateapi-expected-jwt-issuer` | — | (global) | | `ateapi.logLevel` | `ATE_API_LOG_LEVEL` | `--ateapi-log-level` | — | (global) | | `ateapi.postgres.cloudsql.gsa` | `ATE_API_POSTGRES_CLOUDSQL_GSA` | `--ateapi-postgres-cloudsql-gsa` | — | (global) | diff --git a/hack/install-ate.sh b/hack/install-ate.sh index 2eae504550..2d0c3c4635 100755 --- a/hack/install-ate.sh +++ b/hack/install-ate.sh @@ -181,6 +181,13 @@ usage() { echo " ACTOR_JWT_ISSUER iss claim of actor JWTs, verbatim. Relying parties fetch /.well-known/openid-configuration." echo " Default: https://idp..svc" echo "" + echo "Authorization configuration:" + echo "" + echo " ATE_API_EXPERIMENTAL_ENABLE_AUTHZ false turns off ate-api-server's runtime authorization. Default: true" + echo " ATE_API_AUTHZ_BOOTSTRAP_OWNERS Comma-separated principals that are always global owners." + echo " Default: kubectl-ate's, system:serviceaccount::ate-client" + echo " See docs/authorization.md." + echo "" echo "Benchmarks (see benchmarking/README.md for details and customization):" echo "" echo " --deploy-benchmarks Deploy workloads + locust load test stack" diff --git a/internal/ateclient/builder.go b/internal/ateclient/builder.go index 8f9ac8cd8f..aaba019966 100644 --- a/internal/ateclient/builder.go +++ b/internal/ateclient/builder.go @@ -74,6 +74,13 @@ func clientServiceAccount() string { return installdefaults.ClientServiceAccount } +// FallbackPrincipal is the principal a client authenticates as when it is +// given no token file: the ServiceAccount it mints a token from. Every caller +// relying on the fallback is this same principal. +func FallbackPrincipal() string { + return fmt.Sprintf("system:serviceaccount:%s:%s", systemNamespace(), clientServiceAccount()) +} + // systemNamespace is the namespace the client expects ateapi to be running in. func systemNamespace() string { if ns := os.Getenv(NamespaceEnv); ns != "" { @@ -126,6 +133,13 @@ func (c *Client) Close() { } } +// WorkerService returns a WorkerService client on the same connection. Only +// atelet is permitted to call WorkerService; this exists so tests can check +// that other callers are rejected. +func (c *Client) WorkerService() ateapipb.WorkerServiceClient { + return ateapipb.NewWorkerServiceClient(c.conn) +} + // NewClient creates a new Ate API client. If endpoint is empty, it automatically // port-forwards to the ate-api-server pod in substrate's system namespace. // extraOpts are appended to the options used to dial the server. @@ -302,6 +316,10 @@ func serverTLSConfig(ctx context.Context, clientset kubernetes.Interface) (*tls. // bearerTokenDialOption attaches the configured token, or mints an ate-client // ServiceAccount token when tokenFile is empty. +// +// The minted token authenticates as FallbackPrincipal, not as whoever runs +// the client: anyone Kubernetes RBAC lets create a token for that +// ServiceAccount gets the same identity, and whatever it is granted. func bearerTokenDialOption(ctx context.Context, clientset *kubernetes.Clientset, tokenFile string) (grpc.DialOption, error) { if tokenFile == "-" { creds, err := readBearerToken(os.Stdin) diff --git a/internal/ateclient/builder_test.go b/internal/ateclient/builder_test.go index 955395d5ff..e6d4cff25f 100644 --- a/internal/ateclient/builder_test.go +++ b/internal/ateclient/builder_test.go @@ -210,3 +210,24 @@ func TestServerTLSConfigErrors(t *testing.T) { }) } } + +func TestFallbackPrincipal(t *testing.T) { + tests := []struct { + name string + namespace string + sa string + want string + }{ + {"defaults", "", "", "system:serviceaccount:ate-system:ate-client"}, + {"overridden", "custom-ns", "custom-client", "system:serviceaccount:custom-ns:custom-client"}, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + t.Setenv(NamespaceEnv, tc.namespace) + t.Setenv(ClientServiceAccountEnv, tc.sa) + if got := FallbackPrincipal(); got != tc.want { + t.Errorf("FallbackPrincipal() = %q, want %q", got, tc.want) + } + }) + } +} diff --git a/internal/e2e/suites/authz/authz_test.go b/internal/e2e/suites/authz/authz_test.go new file mode 100644 index 0000000000..f6c9709833 --- /dev/null +++ b/internal/e2e/suites/authz/authz_test.go @@ -0,0 +1,46 @@ +// 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 authz runs against an install that enforces ateapi's runtime +// authorization with the ate-client ServiceAccount, the identity the e2e +// clients use, among its bootstrap owners, as the default install does. +// +// The unit tests in cmd/ateapi/internal/authz cover which role may make +// which call. This suite covers what only an installed system shows: a user +// granted access through an AccessPolicy using Substrate end to end, and +// the security boundaries that depend on the install, real credentials, +// stored state, and policy changes reaching every ateapi replica. +package authz + +import ( + "context" + "testing" + + "go.ate.dev/substrate/internal/e2e" +) + +// TestAuthz deploys one fixture, the expensive part, and runs every check +// against it. The subtests run in order, not in parallel: each sets the +// fixture atespace's AccessPolicy for its own principals. +func TestAuthz(t *testing.T) { + env, err := e2e.CheckEnv("BUCKET_NAME", "KO_DOCKER_REPO") + if err != nil { + t.Fatalf("CheckEnv failed: %v", err) + } + ctx := context.Background() + f := deployFixture(t, ctx, env["BUCKET_NAME"]) + + t.Run("BasicFlow", func(t *testing.T) { testBasicFlow(t, ctx, f) }) + t.Run("SecurityRegression", func(t *testing.T) { testSecurityRegression(t, ctx, f) }) +} diff --git a/internal/e2e/suites/authz/flow_test.go b/internal/e2e/suites/authz/flow_test.go new file mode 100644 index 0000000000..891879abb1 --- /dev/null +++ b/internal/e2e/suites/authz/flow_test.go @@ -0,0 +1,78 @@ +// 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 authz + +import ( + "context" + "testing" + + "go.ate.dev/substrate/internal/e2e" + "go.ate.dev/substrate/pkg/proto/ateapipb" +) + +// testBasicFlow runs an actor's whole lifecycle as users granted access +// through an AccessPolicy, not as the bootstrap owner every other suite runs +// as. Alice, an editor, creates, resumes, suspends and deletes an actor; Bob, +// a viewer, can watch it but not change it. Each step needs the one before. +func testBasicFlow(t *testing.T, ctx context.Context, f *fixture) { + alice := newPrincipal(t, ctx, f.clients, f.atespace, "flow-alice") + bob := newPrincipal(t, ctx, f.clients, f.atespace, "flow-bob") + setAtespacePolicy(t, ctx, f.clients, f.atespace, binding("editor", alice), binding("viewer", bob)) + cleanupAtespacePolicy(t, f.clients, f.atespace) + + // Wait for the grants to reach every ateapi replica. + expectAll(t, ctx, []call{ + {"GetActor", alice, getActor(f.actor), allowed}, + {"GetActor", bob, getActor(f.actor), allowed}, + }) + if t.Failed() { + return + } + + actor := &ateapipb.ObjectRef{Atespace: f.atespace, Name: "flow-actor"} + if _, err := alice.api.CreateActor(ctx, &ateapipb.CreateActorRequest{Actor: &ateapipb.Actor{ + Metadata: &ateapipb.ResourceMetadata{Atespace: actor.GetAtespace(), Name: actor.GetName()}, + ActorTemplate: f.template, + }}); err != nil { + t.Fatalf("CreateActor as alice: %v", err) + } + t.Cleanup(func() { + // Best effort, in case a step below failed partway. + _, _ = f.clients.SubstrateAPI.SuspendActor(context.Background(), &ateapipb.SuspendActorRequest{Actor: actor}) + _, _ = f.clients.SubstrateAPI.DeleteActor(context.Background(), &ateapipb.DeleteActorRequest{Actor: actor}) + }) + + if _, err := e2e.ResumeActorAwaitCapacity(t, ctx, &e2e.Clients{SubstrateAPI: alice.api}, &ateapipb.ResumeActorRequest{Actor: actor}); err != nil { + t.Fatalf("ResumeActor as alice: %v", err) + } + + // Bob sees the running actor but can't suspend it. + expectAll(t, ctx, []call{ + {"GetActor", bob, getActor(actor), allowed}, + {"ListActors", bob, listActors(f.atespace), allowed}, + {"SuspendActor", bob, suspendActor(actor), denied}, + }) + if t.Failed() { + return + } + + if _, err := alice.api.SuspendActor(ctx, &ateapipb.SuspendActorRequest{Actor: actor}); err != nil { + t.Fatalf("SuspendActor as alice: %v", err) + } + waitForActorState(t, ctx, f.clients, actor, ateapipb.ActorState_ACTOR_STATE_SUSPENDED) + if _, err := alice.api.DeleteActor(ctx, &ateapipb.DeleteActorRequest{Actor: actor}); err != nil { + t.Fatalf("DeleteActor as alice: %v", err) + } +} diff --git a/internal/e2e/suites/authz/helpers_test.go b/internal/e2e/suites/authz/helpers_test.go new file mode 100644 index 0000000000..cad437f5a0 --- /dev/null +++ b/internal/e2e/suites/authz/helpers_test.go @@ -0,0 +1,362 @@ +// 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 authz + +import ( + "context" + "fmt" + "os" + "path/filepath" + "testing" + "time" + + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + authv1 "k8s.io/api/authentication/v1" + corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "go.ate.dev/substrate/internal/ateclient" + "go.ate.dev/substrate/internal/e2e" + "go.ate.dev/substrate/internal/installdefaults" + "go.ate.dev/substrate/internal/resources" + "go.ate.dev/substrate/pkg/proto/ateapipb" +) + +const ( + actorName = "authz-actor" + // propagationTimeout bounds how long a policy change may take to reach + // every ateapi replica's checks. + propagationTimeout = 30 * time.Second +) + +// fixture is what every test shares: an atespace holding the probe template +// and one suspended actor from it, all created by the bootstrap owner. +type fixture struct { + clients *e2e.Clients + bootstrap *principal + atespace string + template *ateapipb.ObjectRef + actor *ateapipb.ObjectRef +} + +// outcome is the set of status codes a call may return to pass. +type outcome struct { + name string + ok func(codes.Code) bool +} + +func exactly(c codes.Code) outcome { + return outcome{name: c.String(), ok: func(got codes.Code) bool { return got == c }} +} + +var ( + allowed = exactly(codes.OK) + denied = exactly(codes.PermissionDenied) + // passedCheck is any outcome but a permission or authentication failure. + // The permission check runs before the handler validates or looks + // anything up, so a request the handler then rejects (an invalid + // request, a missing object) shows the caller got past the check + // without changing anything. + // + // The check also rejects a malformed identifier with InvalidArgument, so + // every request that expects passedCheck is paired with a caller that + // expects denied on the same request: were its identifiers malformed, + // that caller would get InvalidArgument and fail. + passedCheck = outcome{name: "anything but PermissionDenied or Unauthenticated", ok: func(c codes.Code) bool { + return c != codes.PermissionDenied && c != codes.Unauthenticated + }} + // rejected is a caller refused for who it is, whether by the permission + // check or by a handler that authenticates the caller itself. + rejected = outcome{name: "PermissionDenied or Unauthenticated", ok: func(c codes.Code) bool { + return c == codes.PermissionDenied || c == codes.Unauthenticated + }} +) + +// rpc is one request, made with whichever client a call supplies. +type rpc func(ctx context.Context, api *ateclient.Client) error + +// call is an rpc made as who, and the outcome it must have. +type call struct { + name string + who *principal + do rpc + want outcome +} + +// expectAll polls calls until each has its outcome, since a policy change is +// not guaranteed to reach every ateapi replica at once. The calls share one +// deadline, so a step costs at most propagationTimeout however many it has. +func expectAll(t *testing.T, ctx context.Context, calls []call) { + t.Helper() + deadline := time.Now().Add(propagationTimeout) + pending := calls + for { + lastErr := map[int]error{} + var still []call + for _, c := range pending { + err := c.do(ctx, c.who.api) + if !c.want.ok(status.Code(err)) { + lastErr[len(still)] = err + still = append(still, c) + } + } + if len(still) == 0 { + return + } + if time.Now().After(deadline) { + for i, c := range still { + t.Errorf("%s %s: got %v, want %s", c.who.name, c.name, lastErr[i], c.want.name) + } + return + } + pending = still + time.Sleep(time.Second) + } +} + +// principal is an identity the tests make calls as. +type principal struct { + name string + member string // AccessPolicy member: "user:" + the token subject + api *ateclient.Client +} + +// newPrincipal creates a ServiceAccount in namespace and returns a principal +// that authenticates with its token. +func newPrincipal(t *testing.T, ctx context.Context, clients *e2e.Clients, namespace, name string) *principal { + t.Helper() + sa := &corev1.ServiceAccount{ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: namespace}} + if _, err := clients.K8s.CoreV1().ServiceAccounts(namespace).Create(ctx, sa, metav1.CreateOptions{}); err != nil { + t.Fatalf("creating ServiceAccount %s/%s: %v", namespace, name, err) + } + return principalFor(t, ctx, clients, namespace, name) +} + +// principalFor returns a principal that authenticates with a token minted +// for the existing ServiceAccount namespace/name. +func principalFor(t *testing.T, ctx context.Context, clients *e2e.Clients, namespace, name string) *principal { + t.Helper() + expiration := int64(3600) + token, err := clients.K8s.CoreV1().ServiceAccounts(namespace).CreateToken(ctx, name, &authv1.TokenRequest{ + Spec: authv1.TokenRequestSpec{ + Audiences: []string{installdefaults.APIServiceName + "." + installdefaults.SystemNamespace + ".svc"}, + ExpirationSeconds: &expiration, + }, + }, metav1.CreateOptions{}) + if err != nil { + t.Fatalf("minting a token for %s/%s: %v", namespace, name, err) + } + return &principal{ + name: name, + member: fmt.Sprintf("user:system:serviceaccount:%s:%s", namespace, name), + api: clientWithToken(t, ctx, token.Status.Token), + } +} + +// clientWithToken returns an ateapi client that sends token as its bearer +// token. +func clientWithToken(t *testing.T, ctx context.Context, token string) *ateclient.Client { + t.Helper() + tokenFile := filepath.Join(t.TempDir(), "token") + if err := os.WriteFile(tokenFile, []byte(token), 0o600); err != nil { + t.Fatalf("writing token: %v", err) + } + connectCtx, cancel := context.WithTimeout(ctx, 30*time.Second) + defer cancel() + api, err := ateclient.NewClient(connectCtx, e2e.KubeConfig, e2e.KubeContext, "", tokenFile, false) + if err != nil { + t.Fatalf("ateclient.NewClient: %v", err) + } + t.Cleanup(api.Close) + return api +} + +// serviceAccountExists reports whether namespace/name exists, failing the +// test on any other error. +func serviceAccountExists(t *testing.T, ctx context.Context, clients *e2e.Clients, namespace, name string) bool { + t.Helper() + _, err := clients.K8s.CoreV1().ServiceAccounts(namespace).Get(ctx, name, metav1.GetOptions{}) + if apierrors.IsNotFound(err) { + return false + } + if err != nil { + t.Fatalf("getting ServiceAccount %s/%s: %v", namespace, name, err) + } + return true +} + +func binding(role string, ps ...*principal) *ateapipb.Binding { + b := &ateapipb.Binding{Role: role} + for _, p := range ps { + b.Members = append(b.Members, p.member) + } + return b +} + +// setAtespacePolicy replaces atespace's AccessPolicy bindings, creating the +// policy if it doesn't exist yet. The caller deletes it, with +// cleanupAtespacePolicy at the scope the policy must last for. +func setAtespacePolicy(t *testing.T, ctx context.Context, clients *e2e.Clients, atespace string, bindings ...*ateapipb.Binding) { + t.Helper() + ref := &ateapipb.ObjectRef{Name: atespace} + policy := &ateapipb.AccessPolicy{Metadata: &ateapipb.ResourceMetadata{Name: resources.SingletonName}, Bindings: bindings} + current, err := clients.SubstrateAPI.GetAtespaceAccessPolicy(ctx, &ateapipb.GetAtespaceAccessPolicyRequest{Atespace: ref, Name: resources.SingletonName}) + switch { + case status.Code(err) == codes.NotFound: + _, err = clients.SubstrateAPI.CreateAtespaceAccessPolicy(ctx, &ateapipb.CreateAtespaceAccessPolicyRequest{Atespace: ref, AccessPolicy: policy}) + case err == nil: + policy.Metadata = current.GetMetadata() + _, err = clients.SubstrateAPI.UpdateAtespaceAccessPolicy(ctx, &ateapipb.UpdateAtespaceAccessPolicyRequest{Atespace: ref, AccessPolicy: policy}) + } + if err != nil { + t.Fatalf("setting the AccessPolicy of atespace %q: %v", atespace, err) + } +} + +// cleanupAtespacePolicy deletes atespace's AccessPolicy when t ends. +func cleanupAtespacePolicy(t *testing.T, clients *e2e.Clients, atespace string) { + t.Cleanup(func() { + if err := deleteAtespacePolicy(context.Background(), clients, atespace); err != nil { + t.Logf("cleanup: deleting the AccessPolicy of atespace %q: %v", atespace, err) + } + }) +} + +// deleteAtespacePolicy deletes atespace's AccessPolicy; one already gone is +// not an error. +func deleteAtespacePolicy(ctx context.Context, clients *e2e.Clients, atespace string) error { + _, err := clients.SubstrateAPI.DeleteAtespaceAccessPolicy(ctx, &ateapipb.DeleteAtespaceAccessPolicyRequest{ + Atespace: &ateapipb.ObjectRef{Name: atespace}, + Name: resources.SingletonName, + }) + if status.Code(err) == codes.NotFound { + return nil + } + return err +} + +// createAtespace creates an empty atespace as the bootstrap owner and deletes +// it when t ends. Deleting it also deletes its AccessPolicy. +func createAtespace(t *testing.T, ctx context.Context, clients *e2e.Clients, name string) { + t.Helper() + if _, err := clients.SubstrateAPI.CreateAtespace(ctx, &ateapipb.CreateAtespaceRequest{ + Atespace: &ateapipb.Atespace{Metadata: &ateapipb.ResourceMetadata{Name: name}}, + }); err != nil { + t.Fatalf("CreateAtespace %q: %v", name, err) + } + t.Cleanup(func() { + if err := deleteAtespace(context.Background(), clients, name); err != nil { + t.Logf("cleanup: DeleteAtespace %q: %v", name, err) + } + }) +} + +// deleteAtespace deletes an atespace; one already gone is not an error. +func deleteAtespace(ctx context.Context, clients *e2e.Clients, name string) error { + _, err := clients.SubstrateAPI.DeleteAtespace(ctx, &ateapipb.DeleteAtespaceRequest{Atespace: &ateapipb.ObjectRef{Name: name}}) + if status.Code(err) == codes.NotFound { + return nil + } + return err +} + +// deployFixture deploys the probe template into a new atespace and creates +// one actor from it, all as the bootstrap owner. The actor is never resumed: +// the tests only need an actor that exists. +func deployFixture(t *testing.T, ctx context.Context, bucket string) *fixture { + t.Helper() + clients := e2e.GetClients() + atespace, _ := e2e.DeployProbe(t, bucket, "authz") + actor := &ateapipb.ObjectRef{Atespace: atespace, Name: actorName} + template := &ateapipb.ObjectRef{Atespace: atespace, Name: e2e.ProbeName} + if _, err := clients.SubstrateAPI.CreateActor(ctx, &ateapipb.CreateActorRequest{Actor: &ateapipb.Actor{ + Metadata: &ateapipb.ResourceMetadata{Atespace: atespace, Name: actorName}, + ActorTemplate: template, + }}); err != nil { + t.Fatalf("CreateActor %s/%s: %v", atespace, actorName, err) + } + t.Cleanup(func() { + if _, err := clients.SubstrateAPI.DeleteActor(context.Background(), &ateapipb.DeleteActorRequest{Actor: actor}); err != nil { + t.Logf("cleanup: DeleteActor %s/%s: %v", atespace, actorName, err) + } + }) + return &fixture{ + clients: clients, + bootstrap: &principal{name: "bootstrap-owner", api: clients.SubstrateAPI}, + atespace: atespace, + template: template, + actor: actor, + } +} + +// waitForActorState polls actor, as the bootstrap owner, until it is in want. +func waitForActorState(t *testing.T, ctx context.Context, clients *e2e.Clients, actor *ateapipb.ObjectRef, want ateapipb.ActorState) { + t.Helper() + deadline := time.Now().Add(60 * time.Second) + for time.Now().Before(deadline) { + resp, err := clients.SubstrateAPI.GetActor(ctx, &ateapipb.GetActorRequest{Actor: actor}) + if err == nil && resp.GetStatus().GetState() == want { + return + } + time.Sleep(time.Second) + } + t.Fatalf("timed out waiting for actor %s/%s to reach %v", actor.GetAtespace(), actor.GetName(), want) +} + +// The requests the tests make, shared so that each test pairs callers on +// the same request. + +func getAtespace(name string) rpc { + return func(ctx context.Context, api *ateclient.Client) error { + _, err := api.GetAtespace(ctx, &ateapipb.GetAtespaceRequest{Atespace: &ateapipb.ObjectRef{Name: name}}) + return err + } +} + +func getActor(ref *ateapipb.ObjectRef) rpc { + return func(ctx context.Context, api *ateclient.Client) error { + _, err := api.GetActor(ctx, &ateapipb.GetActorRequest{Actor: ref}) + return err + } +} + +func listActors(atespace string) rpc { + return func(ctx context.Context, api *ateclient.Client) error { + _, err := api.ListActors(ctx, &ateapipb.ListActorsRequest{Atespace: atespace}) + return err + } +} + +func suspendActor(ref *ateapipb.ObjectRef) rpc { + return func(ctx context.Context, api *ateclient.Client) error { + _, err := api.SuspendActor(ctx, &ateapipb.SuspendActorRequest{Actor: ref}) + return err + } +} + +// createActorUnnamed asks for an actor with no name: the check reads only +// its atespace and template, and validation then rejects it, so nothing is +// created. +func createActorUnnamed(atespace string, template *ateapipb.ObjectRef) rpc { + return func(ctx context.Context, api *ateclient.Client) error { + _, err := api.CreateActor(ctx, &ateapipb.CreateActorRequest{Actor: &ateapipb.Actor{ + Metadata: &ateapipb.ResourceMetadata{Atespace: atespace}, + ActorTemplate: template, + }}) + return err + } +} diff --git a/internal/e2e/suites/authz/security_regression_test.go b/internal/e2e/suites/authz/security_regression_test.go new file mode 100644 index 0000000000..7d80979e4a --- /dev/null +++ b/internal/e2e/suites/authz/security_regression_test.go @@ -0,0 +1,318 @@ +// 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 authz + +import ( + "context" + "testing" + + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + + "go.ate.dev/substrate/internal/ateclient" + "go.ate.dev/substrate/internal/e2e" + "go.ate.dev/substrate/internal/installdefaults" + "go.ate.dev/substrate/internal/resources" + "go.ate.dev/substrate/pkg/proto/ateapipb" +) + +// testSecurityRegression checks one case per threat, each in a subtest +// named for it. Every negative check has a positive control showing the +// request could have succeeded, because a denial that stops exercising its +// boundary keeps passing. +func testSecurityRegression(t *testing.T, ctx context.Context, f *fixture) { + t.Run("authorization is enforced by default", func(t *testing.T) { testEnforcedByDefault(t, ctx, f) }) + t.Run("invalid credentials are rejected", func(t *testing.T) { testInvalidCredentials(t, ctx, f) }) + t.Run("no access across atespaces", func(t *testing.T) { testTenantCrossing(t, ctx, f) }) + t.Run("editor cannot grant itself owner", func(t *testing.T) { testPrivilegeEscalation(t, ctx, f) }) + t.Run("revocation takes effect", func(t *testing.T) { testRevocation(t, ctx, f) }) + t.Run("recreated atespace drops old grants", func(t *testing.T) { testAtespaceRecreated(t, ctx, f) }) + t.Run("recreated AccessPolicy rejects stale updates", func(t *testing.T) { testAccessPolicyRecreated(t, ctx, f) }) + t.Run("WorkerService accepts only atelet", func(t *testing.T) { testWorkerServiceRequiresAtelet(t, ctx, f) }) +} + +// testEnforcedByDefault checks that a default install enforces +// authorization: a ServiceAccount no policy names gets nothing. An install +// that left it off would pass every unit test. +func testEnforcedByDefault(t *testing.T, ctx context.Context, f *fixture) { + nobody := newPrincipal(t, ctx, f.clients, f.atespace, "boundary-nobody") + expectAll(t, ctx, []call{ + {"GetActor", f.bootstrap, getActor(f.actor), allowed}, + {"GetActor", nobody, getActor(f.actor), denied}, + {"CreateActor", nobody, createActorUnnamed(f.atespace, f.template), denied}, + }) +} + +// testInvalidCredentials checks that a bearer token ateapi can't verify is +// refused rather than treated as some caller. +func testInvalidCredentials(t *testing.T, ctx context.Context, f *fixture) { + forged := &principal{name: "invalid-token", api: clientWithToken(t, ctx, "not-a-token")} + expectAll(t, ctx, []call{ + {"GetActor", f.bootstrap, getActor(f.actor), allowed}, + {"GetActor", forged, getActor(f.actor), rejected}, + }) +} + +// testTenantCrossing checks that an editor of one atespace can't reach into +// another. +func testTenantCrossing(t *testing.T, ctx context.Context, f *fixture) { + other := f.atespace + "-other" + createAtespace(t, ctx, f.clients, other) + grace := newPrincipal(t, ctx, f.clients, f.atespace, "boundary-grace") + setAtespacePolicy(t, ctx, f.clients, other, binding("editor", grace)) + + expectAll(t, ctx, []call{ + // Control: grace's grant on her own atespace works. + {"ListActors in her atespace", grace, listActors(other), allowed}, + {"GetActor in another atespace", grace, getActor(f.actor), denied}, + {"CreateActor in another atespace", grace, createActorUnnamed(f.atespace, f.template), denied}, + }) +} + +// testPrivilegeEscalation checks that an editor can't rewrite the +// AccessPolicy to make itself owner, while an owner can update it. +func testPrivilegeEscalation(t *testing.T, ctx context.Context, f *fixture) { + owner := newPrincipal(t, ctx, f.clients, f.atespace, "escalate-owner") + editor := newPrincipal(t, ctx, f.clients, f.atespace, "escalate-editor") + current := []*ateapipb.Binding{binding("owner", owner), binding("editor", editor)} + setAtespacePolicy(t, ctx, f.clients, f.atespace, current...) + cleanupAtespacePolicy(t, f.clients, f.atespace) + + // updatePolicy rewrites the policy's bindings, guarded by the uid and + // version the bootstrap owner reads just before, so the update is + // otherwise valid. + ref := &ateapipb.ObjectRef{Name: f.atespace} + updatePolicy := func(bindings ...*ateapipb.Binding) rpc { + return func(ctx context.Context, api *ateclient.Client) error { + cur, err := f.clients.SubstrateAPI.GetAtespaceAccessPolicy(ctx, &ateapipb.GetAtespaceAccessPolicyRequest{Atespace: ref, Name: resources.SingletonName}) + if err != nil { + return err + } + _, err = api.UpdateAtespaceAccessPolicy(ctx, &ateapipb.UpdateAtespaceAccessPolicyRequest{ + Atespace: ref, + AccessPolicy: &ateapipb.AccessPolicy{Metadata: cur.GetMetadata(), Bindings: bindings}, + }) + return err + } + } + + // Wait for both grants to take effect, so the editor's denial below is + // for its role and not for having none yet. + expectAll(t, ctx, []call{ + {"GetActor", owner, getActor(f.actor), allowed}, + {"GetActor", editor, getActor(f.actor), allowed}, + }) + if t.Failed() { + return + } + expectAll(t, ctx, []call{ + {"UpdateAtespaceAccessPolicy to make itself owner", editor, updatePolicy(binding("owner", owner, editor)), denied}, + // Control: the same request with the bindings unchanged, as owner. + {"UpdateAtespaceAccessPolicy unchanged", owner, updatePolicy(current...), allowed}, + }) +} + +// testRevocation checks that removing a binding, downgrading a role and +// deleting the AccessPolicy each take effect on every ateapi replica, with +// no cached grant outliving the change. Steps run in order. +func testRevocation(t *testing.T, ctx context.Context, f *fixture) { + carol := newPrincipal(t, ctx, f.clients, f.atespace, "revoke-carol") + dan := newPrincipal(t, ctx, f.clients, f.atespace, "revoke-dan") + erin := newPrincipal(t, ctx, f.clients, f.atespace, "revoke-erin") + cleanupAtespacePolicy(t, f.clients, f.atespace) + createActor := createActorUnnamed(f.atespace, f.template) + + for _, step := range []struct { + name string + change func(t *testing.T) + expect []call + }{ + { + name: "granted", + change: func(t *testing.T) { + setAtespacePolicy(t, ctx, f.clients, f.atespace, binding("editor", carol, dan), binding("viewer", erin)) + }, + expect: []call{ + {"CreateActor", carol, createActor, passedCheck}, + {"CreateActor", dan, createActor, passedCheck}, + {"CreateActor", erin, createActor, denied}, + }, + }, + { + name: "binding removed", + change: func(t *testing.T) { + setAtespacePolicy(t, ctx, f.clients, f.atespace, binding("editor", dan), binding("viewer", erin)) + }, + expect: []call{ + {"CreateActor", carol, createActor, denied}, + {"GetActor", carol, getActor(f.actor), denied}, + {"CreateActor", dan, createActor, passedCheck}, + }, + }, + { + name: "editor downgraded to viewer", + change: func(t *testing.T) { + setAtespacePolicy(t, ctx, f.clients, f.atespace, binding("viewer", dan, erin)) + }, + expect: []call{ + {"CreateActor", dan, createActor, denied}, + {"GetActor", dan, getActor(f.actor), allowed}, + }, + }, + { + name: "AccessPolicy deleted", + change: func(t *testing.T) { + if err := deleteAtespacePolicy(ctx, f.clients, f.atespace); err != nil { + t.Fatalf("DeleteAtespaceAccessPolicy: %v", err) + } + }, + expect: []call{ + {"GetActor", dan, getActor(f.actor), denied}, + {"GetActor", erin, getActor(f.actor), denied}, + {"GetActor", f.bootstrap, getActor(f.actor), allowed}, + }, + }, + } { + if !t.Run(step.name, func(t *testing.T) { + step.change(t) + expectAll(t, ctx, step.expect) + }) { + // Later steps assume this one's change took effect. + t.FailNow() + } + } +} + +// testAtespaceRecreated checks that grants on an atespace go with it: +// deleting an atespace and creating another with the same name doesn't +// hand the new one's access to the old one's members. +func testAtespaceRecreated(t *testing.T, ctx context.Context, f *fixture) { + name := f.atespace + "-aba" + frank := newPrincipal(t, ctx, f.clients, f.atespace, "aba-frank") + createAtespace(t, ctx, f.clients, name) + setAtespacePolicy(t, ctx, f.clients, name, binding("owner", frank)) + expectAll(t, ctx, []call{{"GetAtespace", frank, getAtespace(name), allowed}}) + if t.Failed() { + return + } + + if err := deleteAtespace(ctx, f.clients, name); err != nil { + t.Fatalf("DeleteAtespace %q: %v", name, err) + } + createAtespace(t, ctx, f.clients, name) + expectAll(t, ctx, []call{ + {"GetAtespace", frank, getAtespace(name), denied}, + {"GetAtespace", f.bootstrap, getAtespace(name), allowed}, + }) +} + +// testAccessPolicyRecreated checks that a deleted and recreated AccessPolicy +// is a new object: it carries none of the old one's bindings, and an update +// guarded by the old one's uid is rejected rather than applied to it. +func testAccessPolicyRecreated(t *testing.T, ctx context.Context, f *fixture) { + gina := newPrincipal(t, ctx, f.clients, f.atespace, "aba-gina") + setAtespacePolicy(t, ctx, f.clients, f.atespace, binding("viewer", gina)) + cleanupAtespacePolicy(t, f.clients, f.atespace) + expectAll(t, ctx, []call{{"GetActor", gina, getActor(f.actor), allowed}}) + if t.Failed() { + return + } + + ref := &ateapipb.ObjectRef{Name: f.atespace} + old, err := f.clients.SubstrateAPI.GetAtespaceAccessPolicy(ctx, &ateapipb.GetAtespaceAccessPolicyRequest{Atespace: ref, Name: resources.SingletonName}) + if err != nil { + t.Fatalf("GetAtespaceAccessPolicy: %v", err) + } + if err := deleteAtespacePolicy(ctx, f.clients, f.atespace); err != nil { + t.Fatalf("DeleteAtespaceAccessPolicy: %v", err) + } + if _, err := f.clients.SubstrateAPI.CreateAtespaceAccessPolicy(ctx, &ateapipb.CreateAtespaceAccessPolicyRequest{ + Atespace: ref, + AccessPolicy: &ateapipb.AccessPolicy{Metadata: &ateapipb.ResourceMetadata{Name: resources.SingletonName}}, + }); err != nil { + t.Fatalf("CreateAtespaceAccessPolicy: %v", err) + } + + // The new policy has none of the old one's bindings. + expectAll(t, ctx, []call{{"GetActor", gina, getActor(f.actor), denied}}) + + // An update guarded by the old policy's uid and version must not apply + // to the new one. + _, err = f.clients.SubstrateAPI.UpdateAtespaceAccessPolicy(ctx, &ateapipb.UpdateAtespaceAccessPolicyRequest{ + Atespace: ref, + AccessPolicy: &ateapipb.AccessPolicy{Metadata: old.GetMetadata(), Bindings: []*ateapipb.Binding{binding("owner", gina)}}, + }) + if status.Code(err) != codes.Aborted { + t.Errorf("UpdateAtespaceAccessPolicy with the deleted policy's uid: got %v, want %v", err, codes.Aborted) + } + cur, err := f.clients.SubstrateAPI.GetAtespaceAccessPolicy(ctx, &ateapipb.GetAtespaceAccessPolicyRequest{Atespace: ref, Name: resources.SingletonName}) + if err != nil { + t.Fatalf("GetAtespaceAccessPolicy: %v", err) + } + if len(cur.GetBindings()) != 0 { + t.Errorf("recreated AccessPolicy bindings = %v, want none", cur.GetBindings()) + } +} + +// testWorkerServiceRequiresAtelet checks that WorkerService rejects every +// caller but atelet. Its RPCs skip the permission checks; each handler +// authenticates atelet by its mTLS certificate, so that is all that guards +// them. A bearer token is rejected even when it is atelet's own +// ServiceAccount's or the bootstrap owner's. +func testWorkerServiceRequiresAtelet(t *testing.T, ctx context.Context, f *fixture) { + ns := e2e.SystemNamespace() + sa := installdefaults.AteletServiceAccount + // Every install creates atelet's ServiceAccount, so a missing one fails + // the test rather than skipping these checks. + if !serviceAccountExists(t, ctx, f.clients, ns, sa) { + t.Fatalf("ServiceAccount %s/%s not found", ns, sa) + } + ateletToken := principalFor(t, ctx, f.clients, ns, sa) + + // The worker named is never created: a handler that let the caller + // through would answer NotFound, not reject it. + worker := &ateapipb.ObjectRef{Name: "authz-missing"} + registerWorker := func(ctx context.Context, api *ateclient.Client) error { + _, err := api.WorkerService().RegisterWorker(ctx, &ateapipb.RegisterWorkerRequest{Worker: worker}) + return err + } + requestSuspend := func(ctx context.Context, api *ateclient.Client) error { + _, err := api.WorkerService().RequestActorSuspend(ctx, &ateapipb.RequestActorSuspendRequest{Worker: worker, Actor: f.actor}) + return err + } + // This handler validates the request before authenticating the caller, + // so the request must be valid. The CSR is parsed only after + // authentication, so it needn't be a real one. + mintWorkerCert := func(ctx context.Context, api *ateclient.Client) error { + _, err := api.WorkerService().MintWorkerActorCertificate(ctx, &ateapipb.MintWorkerActorCertificateRequest{ + Worker: worker, + Actor: f.actor, + ActorUid: "00000000-0000-4000-8000-000000000000", + CertificateSigningRequest: []byte("not a CSR"), + }) + return err + } + // TODO(#2570): also check ReportActorSnapshotState once it is served. Until + // then every caller gets UNIMPLEMENTED, before anything authenticates it. + + expectAll(t, ctx, []call{ + {"RegisterWorker", f.bootstrap, registerWorker, rejected}, + {"RegisterWorker", ateletToken, registerWorker, rejected}, + {"RequestActorSuspend", f.bootstrap, requestSuspend, rejected}, + {"RequestActorSuspend", ateletToken, requestSuspend, rejected}, + {"MintWorkerActorCertificate", f.bootstrap, mintWorkerCert, rejected}, + {"MintWorkerActorCertificate", ateletToken, mintWorkerCert, rejected}, + }) +} diff --git a/internal/e2e/suites/authz/testmain_test.go b/internal/e2e/suites/authz/testmain_test.go new file mode 100644 index 0000000000..61b1896273 --- /dev/null +++ b/internal/e2e/suites/authz/testmain_test.go @@ -0,0 +1,24 @@ +// 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 authz + +import ( + "os" + "testing" + + "go.ate.dev/substrate/internal/e2e" +) + +func TestMain(m *testing.M) { os.Exit(e2e.RunTestMain(m)) } diff --git a/internal/installdefaults/installdefaults.go b/internal/installdefaults/installdefaults.go index 3147aad5ef..3ebe646191 100644 --- a/internal/installdefaults/installdefaults.go +++ b/internal/installdefaults/installdefaults.go @@ -80,6 +80,13 @@ func ActorJWTIssuer(namespace string) string { }).String() } +// ClientPrincipal returns the principal ID ate-api-server sees for a bearer +// token minted from ClientServiceAccount in namespace: the Kubernetes +// ServiceAccount username in the token's subject. +func ClientPrincipal(namespace string) string { + return "system:serviceaccount:" + namespace + ":" + ClientServiceAccount +} + // SPIFFEID returns the SPIFFE ID that Pod certificates for serviceAccount in // namespace carry. Peers authenticate by comparing against this exact string. func SPIFFEID(namespace, serviceAccount string) string { diff --git a/internal/installdefaults/installdefaults_test.go b/internal/installdefaults/installdefaults_test.go index 165265abc2..e15cc7f128 100644 --- a/internal/installdefaults/installdefaults_test.go +++ b/internal/installdefaults/installdefaults_test.go @@ -16,6 +16,14 @@ package installdefaults import "testing" +// ate-api-server matches bootstrap owners on this exact string, which is the +// username Kubernetes puts in a ServiceAccount token's subject. +func TestClientPrincipal(t *testing.T) { + if got, want := ClientPrincipal("ate-system"), "system:serviceaccount:ate-system:ate-client"; got != want { + t.Errorf("ClientPrincipal(ate-system) = %q, want %q", got, want) + } +} + func TestAteletSPIFFEID(t *testing.T) { tests := []struct { name string