carlos / platform Public

host: the end-to-end world's two boxes are built by newTwoBoxWorld, outside the test

newE2EWorld keeps its console and builds the deployment and both boxes
through newTwoBoxWorld in e2e_bridge.go (the environment and buckets, the
keys, the account, the record, the arming, the source at rest), with the
nested source's stand-in as its prepare hook. The publish and tick helpers
move from e2eWorld to landWorld, which e2eWorld embeds. Each box gains two
identity hooks, unset in this package's worlds (an instance id read as
moveCfg.InstanceIDOf, and the stage path's answer), and an agent restart
that arms it as the world first armed it.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev a6ac2c357c388b1225dc9632e82096e211bce974 parent ae84e1a
2 files changed, +299 −189
  • internal/host/e2e_bridge.go +265 −4
  • internal/host/movesoffline_e2e_test.go +34 −185
diff --git a/internal/host/e2e_bridge.go b/internal/host/e2e_bridge.go
index b4e684992..c8e3614a6 100644
--- a/internal/host/e2e_bridge.go
+++ b/internal/host/e2e_bridge.go
@@ -21,21 +21,25 @@ import (
"crypto/ecdh"
"crypto/ed25519"
"database/sql"
+ "encoding/base64"
"encoding/json"
"errors"
"fmt"
"net"
"net/http"
+ "net/http/httptest"
"os"
"path/filepath"
"strings"
"sync"
+ "sync/atomic"
"testing"
"time"
"github.com/carlosframework/platform/internal/activator"
"github.com/carlosframework/platform/internal/appconfig"
"github.com/carlosframework/platform/internal/cgroup"
+ "github.com/carlosframework/platform/internal/domains"
"github.com/carlosframework/platform/internal/hostwrite"
"github.com/carlosframework/platform/internal/instancekey"
"github.com/carlosframework/platform/internal/lease"
@@ -44,6 +48,7 @@ import (
"github.com/carlosframework/platform/internal/provision"
"github.com/carlosframework/platform/internal/registry"
"github.com/carlosframework/platform/internal/release"
+ "github.com/carlosframework/platform/internal/status"
)
// offHost is the one instance every offline world moves.
@@ -93,6 +98,21 @@ type offBox struct {
w *movesWorker
nested bool
last []byte
+ // The bridge's identity hooks (E2E): the cloud instance id the box
+ // reports, read on every use as moveCfg.InstanceIDOf, and the stage
+ // path's answer. Both unset in this package's own worlds, which then
+ // arm exactly as before: an empty id, and armOffline's stagePathActive.
+ instanceID atomic.Pointer[string]
+ stagePath func() *bool
+}
+
+// instanceIDOf is moveCfg.InstanceIDOf for the box: "" until the bridge
+// names an instance id.
+func (b *offBox) instanceIDOf() string {
+ if id := b.instanceID.Load(); id != nil {
+ return *id
+ }
+ return ""
}
func (wd *offWorld) box(name, region string, env map[string]string) *offBox {
@@ -122,7 +142,8 @@ func (b *offBox) getenv(k string) string {
// worker over the same registry and data directory.
func (b *offBox) restart() {
b.svc = activator.New(activator.Config{Bucket: "data", Idle: time.Hour}, &fakeWakeBackend{}, b.reg)
- cfg := moveCfg{Judge: b.wd.judge, Region: b.region, Box: b.name, DataDir: b.dd, OptDir: b.opt, Quiesced: b.svc.Quiesced, Route: b.reg.Get, Now: b.wd.clock}
+ cfg := moveCfg{Judge: b.wd.judge, Region: b.region, Box: b.name, DataDir: b.dd, OptDir: b.opt, Quiesced: b.svc.Quiesced, Route: b.reg.Get, Now: b.wd.clock,
+ InstanceIDOf: b.instanceIDOf, StagePathArmed: b.stagePath}
armOffline(&cfg, b.dd, b.reg, b.svc, nil, hostwrite.Config{}, b.getenv)
// The stage-requested marker is root's business, and its throttled
// timer would outlive the test's directories.
@@ -135,15 +156,21 @@ func (b *offBox) restart() {
func (b *offBox) reconcile() {
b.wd.t.Helper()
+ if err := b.reconcileErr(context.Background()); err != nil {
+ b.wd.t.Fatal(err)
+ }
+}
+
+// reconcileErr is the box's reconcile pass (reconcileSerializedWith over
+// its own worker), returning the pass's error.
+func (b *offBox) reconcileErr(ctx context.Context) error {
cfg := provision.ReconcileConfig{Region: b.region, DataDir: b.dd, Judge: b.wd.judge}
if b.nested {
cfg.Layout = func(context.Context, release.Store) (string, error) { return release.LayoutModeNested, nil }
} else {
cfg.Layout = func(context.Context, release.Store) (string, error) { return release.LayoutModeFlat, nil }
}
- if err := reconcileSerializedWith(context.Background(), b.wd.s, b.reg, cfg, b.w); err != nil {
- b.wd.t.Fatal(err)
- }
+ return reconcileSerializedWith(ctx, b.wd.s, b.reg, cfg, b.w)
}
func (b *offBox) edges() {
@@ -388,6 +415,9 @@ type landWorld struct {
opts landOpts
// The source's files directory, env file and config file.
srcFiles, srcEnv, srcConfig string
+ // rearm, set by newTwoBoxWorld, is an agent restart of a box that arms
+ // it exactly as the world first armed it.
+ rearm map[*offBox]func()
}
// arm wires the target box as armMovesPass would, over its own data bucket.
@@ -395,6 +425,7 @@ func (lw *landWorld) arm(b *offBox) {
bucket, region := b.getenv("CARLOS_HIBERNATE_BUCKET"), b.getenv("CARLOS_HIBERNATE_REGION")
b.svc = activator.New(activator.Config{Bucket: bucket, Idle: time.Hour}, &fakeWakeBackend{}, b.reg)
cfg := moveCfg{Judge: b.wd.judge, Region: b.region, Box: b.name, DataDir: b.dd, OptDir: b.opt, Quiesced: b.svc.Quiesced, Route: b.reg.Get, Now: b.wd.clock,
+ InstanceIDOf: b.instanceIDOf, StagePathArmed: b.stagePath,
OwnBucket: bucket, OwnBucketRegion: region, OwnPrefix: sealPrefix,
Mirror: func(ctx context.Context, dst string) (lease.MirrorStore, error) {
return lease.NewS3Mirror(ctx, region, dst)
@@ -658,3 +689,233 @@ func mustJSON(t testing.TB, v any) []byte {
}
return b
}
+
+// --- the two-box world's construction ----------------------------------------
+
+// e2eRows is how many rows the source's database holds once the world is
+// built: 0..24 replicated in two sessions, 25..29 a tail the stop never
+// checkpointed.
+const e2eRows = 30
+
+// newE2EEnv sets the process environment both boxes share and serves the
+// data buckets data-ie and data-uk over the S3 wire protocol on loopback,
+// returning the buckets' model.
+func newE2EEnv(t testing.TB) *leasetest.VersionedBucket {
+ t.Helper()
+ t.Setenv("CARLOS_HOST_CLAIM", "")
+ // The test process's own values of the agent variables, so each box's
+ // swap is visibly its own and the process value is restored after.
+ for k := range e2eIEAgentEnv {
+ t.Setenv(k, "test-process")
+ }
+ fake := leasetest.NewVersionedBucket()
+ fake.AddBucket(sealIE)
+ fake.AddBucket(sealUK)
+ h, _ := leasetest.NewS3Server(fake)
+ s3srv := httptest.NewServer(h) // 127.0.0.1 only
+ t.Cleanup(s3srv.Close)
+ t.Setenv("AWS_ACCESS_KEY_ID", "test-key")
+ t.Setenv("AWS_SECRET_ACCESS_KEY", "test-secret")
+ t.Setenv("AWS_SESSION_TOKEN", "")
+ t.Setenv("AWS_EC2_METADATA_DISABLED", "true")
+ t.Setenv("AWS_CONFIG_FILE", filepath.Join(t.TempDir(), "absent"))
+ t.Setenv("AWS_SHARED_CREDENTIALS_FILE", filepath.Join(t.TempDir(), "absent"))
+ t.Setenv("CARLOS_S3_ENDPOINT", s3srv.URL)
+ return fake
+}
+
+// e2eKeys are the deployment's keys: the secrets key (each box holds the
+// private half, the console seals to the public half) and the release key
+// (the console signs, the target box verifies).
+type e2eKeys struct {
+ keyB64 string
+ priv *ecdh.PrivateKey
+ relPriv ed25519.PrivateKey
+ relPub ed25519.PublicKey
+ pubB64 string // relPub as CARLOS_RELEASE_PUBKEY carries it
+}
+
+func newE2EKeys(t testing.TB) e2eKeys {
+ t.Helper()
+ var k e2eKeys
+ var err error
+ if k.keyB64, _, err = appconfig.GenerateKey(); err != nil {
+ t.Fatal(err)
+ }
+ if k.priv, err = appconfig.ParsePrivateKey(k.keyB64, "test key"); err != nil {
+ t.Fatal(err)
+ }
+ seed, pubB64, err := release.GenerateKey()
+ if err != nil {
+ t.Fatal(err)
+ }
+ if k.relPriv, err = release.ParseSigningKey(seed, "test"); err != nil {
+ t.Fatal(err)
+ }
+ if k.relPub, err = release.ParsePublicKey(pubB64, "test"); err != nil {
+ t.Fatal(err)
+ }
+ k.pubB64 = pubB64
+ return k
+}
+
+// secretsPub is the secrets key's public half as console.Config.SecretsPub
+// takes it.
+func (k e2eKeys) secretsPub() string {
+ return base64.StdEncoding.EncodeToString(k.priv.PublicKey().Bytes())
+}
+
+// e2eAccount is the account that owns app: its sqid, its console id and
+// name, as its manifest in the deployment bucket carries them.
+type e2eAccount struct {
+ sqid string
+ id int64
+ name string
+}
+
+// newTwoBoxWorld is offHost at home on ie-box (data-ie) with uk-box
+// (data-uk) in uk, over the deployment store s: the account's manifest
+// (written only when absent), app's manifest and the instance's record at
+// home as opts says; both boxes armed as armMovesPass arms them, the
+// source's production build-state carry and each box's launch composer
+// under its own agent environment; two shipped versions, the first
+// promoted, and the app's config bundle; and the source at rest, as a box
+// that has served it. prepare, when set, runs once the source is at rest
+// and before both boxes publish their edges files and status snapshots.
+func newTwoBoxWorld(t testing.TB, s release.Store, fake *leasetest.VersionedBucket, keys e2eKeys, acct e2eAccount, now time.Time, opts landOpts, prepare func(*landWorld)) *landWorld {
+ t.Helper()
+ ctx := context.Background()
+ wd := &offWorld{t: t, s: s, acct: acct.sqid, now: now}
+ if opts.judged || opts.lifetime {
+ wd.judge = provision.NewLifetimeJudge()
+ }
+ if _, _, err := s.Get(ctx, domains.AccountManifestKey(wd.acct)); errors.Is(err, os.ErrNotExist) {
+ manifest, _ := json.Marshal(map[string]any{"id": acct.id, "sqid": wd.acct, "name": acct.name, "created_at": now})
+ if _, err := s.Put(ctx, domains.AccountManifestKey(wd.acct), manifest, ""); err != nil {
+ t.Fatal(err)
+ }
+ }
+ if err := provision.PutManifest(ctx, s, wd.acct, "app", provision.Manifest{Schema: 1, App: "app", Domain: "example.com"}); err != nil {
+ t.Fatal(err)
+ }
+ switch {
+ case opts.lifetime:
+ wd.createLifetime(offHost, 1)
+ case opts.incarnation > 0:
+ wd.createAt(offHost, opts.incarnation)
+ default:
+ wd.create(offHost)
+ }
+
+ // The boxes. ie-box is the home's one box; uk-box is uk's.
+ sw := &sealWorld{offWorld: wd, fake: fake, keyB64: keys.keyB64, priv: keys.priv}
+ sw.ie = sw.sourceBox("ie-box")
+ sw.ie.env["CARLOS_HIBERNATE_REGION"] = sealIERegion
+ sw.uk = sw.box("uk-box", "uk", map[string]string{"CARLOS_HIBERNATE_BUCKET": sealUK, "CARLOS_SECRETS_KEY": keys.keyB64,
+ "CARLOS_HIBERNATE_REGION": sealUKRegion, "CARLOS_RELEASE_PUBKEY": keys.pubB64})
+ lw := &landWorld{t: t, sw: sw, wd: wd, s3: &landS3{f: fake}, ie: sw.ie, uk: sw.uk, opts: opts, relPriv: keys.relPriv}
+ shipVersion(t, s, wd.acct, "app", sealVersion)
+ shipVersion(t, s, wd.acct, "app", "v2")
+ lw.promote(sealVersion)
+ lw.bundle(landStore)
+ lw.ukEB = &activator.ExecBackend{DataDir: lw.uk.dd, OptDir: lw.uk.opt, Region: "uk"}
+ lw.arm(lw.uk)
+
+ // The source box's production build-state carry, and each box's launch
+ // composer under its own agent environment. rearm is an agent restart
+ // that arms the box exactly so again.
+ ie := lw.ie
+ ieAgent := func() {
+ bs, bsErr := newBuildStates(&activator.Config{Bucket: sealIE, Region: sealIERegion, Prefix: sealPrefix}, ie.reg, ie.opt)
+ ie.w.cfg.CarryRequest = moveCarry(bs, bsErr)
+ ie.w.cfg.LaunchLayers = withAgentEnv(e2eIEAgentEnv, ie.w.cfg.LaunchLayers)
+ }
+ ukAgent := func() { lw.uk.w.cfg.LaunchLayers = withAgentEnv(e2eUKAgentEnv, lw.ukEB.LaunchLayers) }
+ ieAgent()
+ ukAgent()
+ lw.rearm = map[*offBox]func(){
+ ie: func() { ie.restart(); sw.arm(ie); ieAgent() },
+ lw.uk: func() { lw.arm(lw.uk); ukAgent() },
+ }
+
+ // The source at rest, as a box that has served it: rows replicated in
+ // two sessions, then a tail the stop never checkpointed.
+ sw.instance(ie) // rows 0..19, replicated
+ db := sw.dbPath(ie)
+ sw.writeRows(ie, db, 20, 25, true)
+ sw.writeRows(ie, db, 25, e2eRows, false)
+ lw.srcFiles, lw.srcEnv, lw.srcConfig = filepath.Join(ie.dd, offHost), filepath.Join(ie.dd, offHost+".env"), filepath.Join(ie.dd, offHost+".config.env")
+ if prepare != nil {
+ prepare(lw)
+ }
+
+ // Both boxes published as their loops publish them, and their status
+ // snapshots.
+ lw.tickAll()
+ for _, b := range []*offBox{ie, lw.uk} {
+ if err := status.WriteBox(ctx, s, status.Box{Label: b.name, GeneratedAt: time.Now(), Metrics: &status.BoxMetrics{}}); err != nil {
+ t.Fatal(err)
+ }
+ }
+ return lw
+}
+
+// publish is one pass of edgesFileLoop on b: the identity it builds at arm
+// from the box's own environment, the capability from the box's armed
+// worker, through the production writer, with the box's lifetimes switch
+// read as runAgent reads it (lifetimes := boxJudge != nil): a box judging
+// lifetimes publishes each row's (incarnation, serving_seq).
+func (lw *landWorld) publish(b *offBox) {
+ lw.t.Helper()
+ if err := lw.publishErr(context.Background(), b); err != nil {
+ lw.t.Fatal(err)
+ }
+}
+
+func (lw *landWorld) publishErr(ctx context.Context, b *offBox) error {
+ sharedPool, inbound := edgesFileEligibility(b.getenv)
+ aws := sealIERegion
+ if b.region == "uk" {
+ aws = sealUKRegion
+ }
+ ident := edgesFile{
+ Box: b.name, BoxID: b.name, Region: aws, Placement: b.region,
+ DataBucket: strings.TrimSpace(b.getenv("CARLOS_HIBERNATE_BUCKET")),
+ DataBucketRegion: strings.TrimSpace(b.getenv("CARLOS_HIBERNATE_REGION")),
+ HibernationConfigured: strings.TrimSpace(b.getenv("CARLOS_HIBERNATE_BUCKET")) != "",
+ ServesSharedPool: sharedPool,
+ AcceptsInbound: inbound,
+ SecretsKeyFP: edgesFileSecretsFP(b.getenv),
+ Capabilities: withOfflineCapability(edgesFileCapabilities(b.getenv), b.w.cfg.OfflineCapable && b.w.cfg.Offline != nil),
+ }
+ return edgesFileWriteClaims(ctx, lw.wd.s, b.reg, ident, "192.0.2.1", &b.last, false, nil, b.w.cfg.Judge != nil)
+}
+
+// tick is the agent's order on one box: reconcile, the edges file, the
+// moves pass. The boxes' clock is the wall clock, which the doors judge
+// report freshness by.
+func (lw *landWorld) tick(b *offBox) {
+ lw.t.Helper()
+ lw.tickAt(b, time.Now())
+}
+
+// tickAt is tick with the boxes' clock reading at.
+func (lw *landWorld) tickAt(b *offBox, at time.Time) {
+ lw.t.Helper()
+ lw.setBoxClock(at)
+ b.reconcile()
+ lw.publish(b)
+ b.pass()
+}
+
+func (lw *landWorld) setBoxClock(at time.Time) {
+ lw.wd.mu.Lock()
+ lw.wd.now = at
+ lw.wd.mu.Unlock()
+}
+
+func (lw *landWorld) tickAll() {
+ lw.t.Helper()
+ lw.tick(lw.ie)
+ lw.tick(lw.uk)
+}
diff --git a/internal/host/movesoffline_e2e_test.go b/internal/host/movesoffline_e2e_test.go
index 06f0d724a..b8b0d17b4 100644
--- a/internal/host/movesoffline_e2e_test.go
+++ b/internal/host/movesoffline_e2e_test.go
@@ -5,7 +5,6 @@ package host
import (
"bytes"
"context"
- "encoding/base64"
"encoding/json"
"errors"
"fmt"
@@ -23,7 +22,6 @@ import (
"time"
"github.com/carlosframework/platform/internal/activator"
- "github.com/carlosframework/platform/internal/appconfig"
"github.com/carlosframework/platform/internal/console"
"github.com/carlosframework/platform/internal/domains"
"github.com/carlosframework/platform/internal/hostops"
@@ -31,13 +29,11 @@ import (
"github.com/carlosframework/platform/internal/instancekey"
"github.com/carlosframework/platform/internal/layout"
"github.com/carlosframework/platform/internal/lease"
- "github.com/carlosframework/platform/internal/lease/leasetest"
"github.com/carlosframework/platform/internal/moves"
"github.com/carlosframework/platform/internal/provision"
"github.com/carlosframework/platform/internal/registry"
"github.com/carlosframework/platform/internal/release"
"github.com/carlosframework/platform/internal/schedule"
- "github.com/carlosframework/platform/internal/status"
)
// The offline operator move end to end (Task 8 of docs/superpowers/plans/
@@ -159,54 +155,16 @@ type e2eWorld struct {
func newE2EWorld(t *testing.T, opts landOpts) *e2eWorld {
t.Helper()
- ctx := context.Background()
- t.Setenv("CARLOS_HOST_CLAIM", "")
- // The test process's own values of the agent variables, so each box's
- // swap is visibly its own and the process value is restored after.
- for k := range e2eIEAgentEnv {
- t.Setenv(k, "test-process")
- }
-
- // The data buckets over the S3 wire protocol, on loopback.
- fake := leasetest.NewVersionedBucket()
- fake.AddBucket(sealIE)
- fake.AddBucket(sealUK)
- h, _ := leasetest.NewS3Server(fake)
- s3srv := httptest.NewServer(h)
- t.Cleanup(s3srv.Close)
- t.Setenv("AWS_ACCESS_KEY_ID", "test-key")
- t.Setenv("AWS_SECRET_ACCESS_KEY", "test-secret")
- t.Setenv("AWS_SESSION_TOKEN", "")
- t.Setenv("AWS_EC2_METADATA_DISABLED", "true")
- t.Setenv("AWS_CONFIG_FILE", filepath.Join(t.TempDir(), "absent"))
- t.Setenv("AWS_SHARED_CREDENTIALS_FILE", filepath.Join(t.TempDir(), "absent"))
- t.Setenv("CARLOS_S3_ENDPOINT", s3srv.URL)
+ // The process environment and the data buckets over the S3 wire
+ // protocol, on loopback.
+ fake := newE2EEnv(t)
// One deployment store for everyone, recording every write.
rs := &recordingStore{DirStore: release.NewDirStore(t.TempDir())}
// The keys: the deployment's secrets key (the console's public half,
// each box's private half) and the release key.
- keyB64, _, err := appconfig.GenerateKey()
- if err != nil {
- t.Fatal(err)
- }
- priv, err := appconfig.ParsePrivateKey(keyB64, "test key")
- if err != nil {
- t.Fatal(err)
- }
- seed, pubB64, err := release.GenerateKey()
- if err != nil {
- t.Fatal(err)
- }
- relPriv, err := release.ParseSigningKey(seed, "test")
- if err != nil {
- t.Fatal(err)
- }
- relPub, err := release.ParsePublicKey(pubB64, "test")
- if err != nil {
- t.Fatal(err)
- }
+ keys := newE2EKeys(t)
// The console: an operator, its CLI token, the ops account owning app.
dir := t.TempDir()
@@ -219,8 +177,8 @@ func newE2EWorld(t *testing.T, opts landOpts) *e2eWorld {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { srv.ServeHTTP(w, r) }))
t.Cleanup(ts.Close)
if srv, err = console.NewServer(console.Config{Origin: ts.URL, Keymail: "http://keymail.invalid",
- Registry: filepath.Join(dir, "registry.db"), Releases: rs, ReleasePub: relPub, Key: relPriv, AppScheme: "http",
- HomeRegion: "ie", SecretsPub: base64.StdEncoding.EncodeToString(priv.PublicKey().Bytes())}, cst); err != nil {
+ Registry: filepath.Join(dir, "registry.db"), Releases: rs, ReleasePub: keys.relPub, Key: keys.relPriv, AppScheme: "http",
+ HomeRegion: "ie", SecretsPub: keys.secretsPub()}, cst); err != nil {
t.Fatal(err)
}
now := time.Now()
@@ -245,152 +203,43 @@ func newE2EWorld(t *testing.T, opts landOpts) *e2eWorld {
t.Fatalf("no CLI token: %v", err)
}
- // The deployment: the ops account, app's manifest and the instance's
- // record at home.
- wd := &offWorld{t: t, s: rs, acct: release.OpsAccount, now: now}
- if opts.judged || opts.lifetime {
- wd.judge = provision.NewLifetimeJudge()
- }
- if _, _, err := rs.Get(ctx, domains.AccountManifestKey(wd.acct)); errors.Is(err, os.ErrNotExist) {
- manifest, _ := json.Marshal(map[string]any{"id": console.OpsAccountID, "sqid": wd.acct, "name": "ops", "created_at": now})
- if _, err := rs.Put(ctx, domains.AccountManifestKey(wd.acct), manifest, ""); err != nil {
- t.Fatal(err)
+ // The deployment (the ops account, app's manifest and the instance's
+ // record at home) and the two boxes, the source at rest. For the
+ // nested source, T5's raw-directory stand-in for the bind mount, before
+ // the boxes publish.
+ acct := e2eAccount{sqid: release.OpsAccount, id: console.OpsAccountID, name: "ops"}
+ lw := newTwoBoxWorld(t, rs, fake, keys, acct, now, opts, func(lw *landWorld) {
+ ie := lw.ie
+ if opts.nested {
+ raw := lw.sw.nest(ie)
+ lw.srcFiles, lw.srcEnv, lw.srcConfig = filepath.Join(raw, offHost), filepath.Join(raw, offHost+".env"), filepath.Join(raw, offHost+".config.env")
+ for _, name := range []string{"launch.run.json", "launch.seed.json", "launch.gen.7.json"} {
+ writeFile(t, filepath.Join(raw, name), "{}")
+ }
+ // T5's stand-in for the bind mount: the same two files, composed by
+ // the same code, from the raw instance directory.
+ ie.w.cfg.LaunchLayers = withAgentEnv(e2eIEAgentEnv, func(rt registry.Route) (map[string]string, map[string]bool, []string, error) {
+ rt.DBPath, rt.Socket = provision.DefaultDBPath(raw, rt.Host), filepath.Join(provision.SocketDir, rt.Host+".sock")
+ return (&activator.ExecBackend{DataDir: raw}).LaunchLayers(rt)
+ })
}
- }
- if err := provision.PutManifest(ctx, rs, wd.acct, "app", provision.Manifest{Schema: 1, App: "app", Domain: "example.com"}); err != nil {
- t.Fatal(err)
- }
- switch {
- case opts.lifetime:
- wd.createLifetime(offHost, 1)
- case opts.incarnation > 0:
- wd.createAt(offHost, opts.incarnation)
- default:
- wd.create(offHost)
- }
-
- // The boxes. ie-box is the home's one box; uk-box is uk's.
- sw := &sealWorld{offWorld: wd, fake: fake, keyB64: keyB64, priv: priv}
- sw.ie = sw.sourceBox("ie-box")
- sw.ie.env["CARLOS_HIBERNATE_REGION"] = sealIERegion
- sw.uk = sw.box("uk-box", "uk", map[string]string{"CARLOS_HIBERNATE_BUCKET": sealUK, "CARLOS_SECRETS_KEY": keyB64,
- "CARLOS_HIBERNATE_REGION": sealUKRegion, "CARLOS_RELEASE_PUBKEY": pubB64})
- lw := &landWorld{t: t, sw: sw, wd: wd, s3: &landS3{f: fake}, ie: sw.ie, uk: sw.uk, opts: opts, relPriv: relPriv}
- shipVersion(t, rs, wd.acct, "app", sealVersion)
- shipVersion(t, rs, wd.acct, "app", "v2")
- lw.promote(sealVersion)
- lw.bundle(landStore)
- lw.ukEB = &activator.ExecBackend{DataDir: lw.uk.dd, OptDir: lw.uk.opt, Region: "uk"}
- lw.arm(lw.uk)
- w := &e2eWorld{landWorld: lw, t: t, rs: rs, base: ts.URL + "/api/cli/apps/" + wd.acct + "/app/instances/" + offHost + "/move/offline", token: token}
-
- // The source box's production build-state carry, and each box's launch
- // composer under its own agent environment.
+ if opts.noFiles {
+ if err := os.RemoveAll(lw.srcFiles); err != nil {
+ t.Fatal(err)
+ }
+ }
+ })
+ w := &e2eWorld{landWorld: lw, t: t, rs: rs, base: ts.URL + "/api/cli/apps/" + lw.wd.acct + "/app/instances/" + offHost + "/move/offline", token: token}
ie := lw.ie
- bs, bsErr := newBuildStates(&activator.Config{Bucket: sealIE, Region: sealIERegion, Prefix: sealPrefix}, ie.reg, ie.opt)
- ie.w.cfg.CarryRequest = moveCarry(bs, bsErr)
- ie.w.cfg.LaunchLayers = withAgentEnv(e2eIEAgentEnv, ie.w.cfg.LaunchLayers)
- lw.uk.w.cfg.LaunchLayers = withAgentEnv(e2eUKAgentEnv, lw.ukEB.LaunchLayers)
w.ieEB = &activator.ExecBackend{Cfg: activator.Config{Bucket: sealIE, Region: sealIERegion, Prefix: sealPrefix}, DataDir: ie.dd, OptDir: ie.opt}
-
- // The source at rest, as a box that has served it: rows replicated in
- // two sessions, then a tail the stop never checkpointed.
- sw.instance(ie) // rows 0..19, replicated
- db := sw.dbPath(ie)
- sw.writeRows(ie, db, 20, 25, true)
- sw.writeRows(ie, db, 25, 30, false)
- w.rows = 30
- w.srcFiles, w.srcEnv, w.cfg = filepath.Join(ie.dd, offHost), filepath.Join(ie.dd, offHost+".env"), filepath.Join(ie.dd, offHost+".config.env")
- if opts.nested {
- raw := sw.nest(ie)
- w.srcFiles, w.srcEnv, w.cfg = filepath.Join(raw, offHost), filepath.Join(raw, offHost+".env"), filepath.Join(raw, offHost+".config.env")
- for _, name := range []string{"launch.run.json", "launch.seed.json", "launch.gen.7.json"} {
- writeFile(t, filepath.Join(raw, name), "{}")
- }
- // T5's stand-in for the bind mount: the same two files, composed by
- // the same code, from the raw instance directory.
- ie.w.cfg.LaunchLayers = withAgentEnv(e2eIEAgentEnv, func(rt registry.Route) (map[string]string, map[string]bool, []string, error) {
- rt.DBPath, rt.Socket = provision.DefaultDBPath(raw, rt.Host), filepath.Join(provision.SocketDir, rt.Host+".sock")
- return (&activator.ExecBackend{DataDir: raw}).LaunchLayers(rt)
- })
- }
- if opts.noFiles {
- if err := os.RemoveAll(w.srcFiles); err != nil {
- t.Fatal(err)
- }
- }
- lw.srcFiles, lw.srcEnv, lw.srcConfig = w.srcFiles, w.srcEnv, w.cfg
-
- // Both boxes published as their loops publish them, and their status
- // snapshots.
- w.tickAll()
- for _, b := range []*offBox{ie, lw.uk} {
- if err := status.WriteBox(ctx, rs, status.Box{Label: b.name, GeneratedAt: time.Now(), Metrics: &status.BoxMetrics{}}); err != nil {
- t.Fatal(err)
- }
- }
+ w.rows = e2eRows
+ w.srcFiles, w.srcEnv, w.cfg = lw.srcFiles, lw.srcEnv, lw.srcConfig
if !ie.lists(offHost) || lw.uk.lists(offHost) {
t.Fatal("fixture: ie-box must serve the host and uk-box must not")
}
return w
}
-// publish is one pass of edgesFileLoop on b: the identity it builds at arm
-// from the box's own environment, the capability from the box's armed
-// worker, through the production writer, with the box's lifetimes switch
-// read as runAgent reads it (lifetimes := boxJudge != nil): a box judging
-// lifetimes publishes each row's (incarnation, serving_seq).
-func (w *e2eWorld) publish(b *offBox) {
- w.t.Helper()
- sharedPool, inbound := edgesFileEligibility(b.getenv)
- aws := sealIERegion
- if b.region == "uk" {
- aws = sealUKRegion
- }
- ident := edgesFile{
- Box: b.name, BoxID: b.name, Region: aws, Placement: b.region,
- DataBucket: strings.TrimSpace(b.getenv("CARLOS_HIBERNATE_BUCKET")),
- DataBucketRegion: strings.TrimSpace(b.getenv("CARLOS_HIBERNATE_REGION")),
- HibernationConfigured: strings.TrimSpace(b.getenv("CARLOS_HIBERNATE_BUCKET")) != "",
- ServesSharedPool: sharedPool,
- AcceptsInbound: inbound,
- SecretsKeyFP: edgesFileSecretsFP(b.getenv),
- Capabilities: withOfflineCapability(edgesFileCapabilities(b.getenv), b.w.cfg.OfflineCapable && b.w.cfg.Offline != nil),
- }
- if err := edgesFileWriteClaims(context.Background(), w.wd.s, b.reg, ident, "192.0.2.1", &b.last, false, nil, b.w.cfg.Judge != nil); err != nil {
- w.t.Fatal(err)
- }
-}
-
-// tick is the agent's order on one box: reconcile, the edges file, the
-// moves pass. The boxes' clock is the wall clock, which the doors judge
-// report freshness by.
-func (w *e2eWorld) tick(b *offBox) {
- w.t.Helper()
- w.tickAt(b, time.Now())
-}
-
-// tickAt is tick with the boxes' clock reading at.
-func (w *e2eWorld) tickAt(b *offBox, at time.Time) {
- w.t.Helper()
- w.setBoxClock(at)
- b.reconcile()
- w.publish(b)
- b.pass()
-}
-
-func (w *e2eWorld) setBoxClock(at time.Time) {
- w.wd.mu.Lock()
- w.wd.now = at
- w.wd.mu.Unlock()
-}
-
-func (w *e2eWorld) tickAll() {
- w.t.Helper()
- w.tick(w.ie)
- w.tick(w.uk)
-}
-
// door posts to one of the doors (verb "" is start).
func (w *e2eWorld) door(verb, body string) (int, string) {
w.t.Helper()