package console // claim_invariant_probes_world_test.go stages the worlds the probes need and // reads the world back, so a probe proves what a handler DID rather than what // it answered. Everything here writes below the console (raw bucket, store // handle, registry) or drives a door the way a client does. import ( "bytes" "context" "crypto/sha256" "encoding/json" "fmt" "io" "mime/multipart" "net/http" "net/url" "testing" "time" "github.com/carlosframework/platform/internal/dbdoor" "github.com/carlosframework/platform/internal/hostactors" "github.com/carlosframework/platform/internal/hostclaim" "github.com/carlosframework/platform/internal/hostfence" "github.com/carlosframework/platform/internal/hostresolve" "github.com/carlosframework/platform/internal/objstore" "github.com/carlosframework/platform/internal/registry" "github.com/carlosframework/platform/internal/release" "github.com/carlosframework/platform/internal/status" ) // bucketJSON decodes the object at key from the raw bucket into v. It answers // false when the object is absent or is not that shape. func (cx *claimCase) bucketJSON(key string, v any) bool { raw, _, err := cx.rel.Get(context.Background(), key) if err != nil { return false } return json.Unmarshal(raw, v) == nil } // fleetBox reads one fleet box straight from the registry file. func (cx *claimCase) fleetBox(t *testing.T, fleet, label string) (registry.FleetBox, bool) { t.Helper() reg, err := registry.Open(cx.regPath) if err != nil { t.Fatal(err) } boxes, err := reg.ListFleetBoxes(fleet) if err != nil { t.Fatal(err) } for _, b := range boxes { if b.Label == label { return b, true } } return registry.FleetBox{}, false } // fleetNamed reports whether the registry holds a fleet of that name. func (cx *claimCase) fleetNamed(t *testing.T, name string) bool { t.Helper() reg, err := registry.Open(cx.regPath) if err != nil { t.Fatal(err) } fleets, err := reg.ListFleets() if err != nil { t.Fatal(err) } for _, f := range fleets { if f.Name == name { return true } } return false } // fleetBoxWorld is fleet pi with one active box, pi-1; the box's token hash is // kept in vars so a rotation can be seen to change it. func (cx *claimCase) fleetBoxWorld(t *testing.T) { t.Helper() cx.fleet(t) cx.mustAPI(t, http.MethodPost, "/api/cli/fleets/bab/pi/boxes", `{"label":"pi-1"}`, http.StatusOK) box, ok := cx.fleetBox(t, "pi", "pi-1") if !ok { t.Fatal("stage fleet box: pi/pi-1 is not in the registry") } cx.vars["boxhash"] = box.TokenHash } // entitlement reads bab's storage entitlement from the bucket. func (cx *claimCase) entitlement(t *testing.T) *objstore.Entitlement { t.Helper() e, _, err := objstore.GetEntitlement(context.Background(), cx.rel, "bab") if err != nil { t.Fatal(err) } return e } // accountNamed reports whether address belongs to an account of that name. func (cx *claimCase) accountNamed(t *testing.T, address, name string) bool { t.Helper() accts, err := cx.st.AccountsFor(address) if err != nil { t.Fatal(err) } for _, a := range accts { if a.Name == name { return true } } return false } // mintedAccount is an empty account the owner owns, minted the way the console // mints one (the bucket manifest and the owner's membership), and its sqid. func (cx *claimCase) mintedAccount(t *testing.T, name string) string { t.Helper() a, err := cx.srv.mintAccount(context.Background(), name, claimFxOwner) if err != nil { t.Fatalf("mint account %s: %v", name, err) } return a.Sqid } // cookieOf reads one cookie a browser holds for the console. func (cx *claimCase) cookieOf(c *http.Client, name string) string { u, err := url.Parse(cx.ts.URL) if err != nil { return "" } for _, ck := range c.Jar.Cookies(u) { if ck.Name == name { return ck.Value } } return "" } // signedInAs answers whether browser c holds a live console session for // address. func (cx *claimCase) signedInAs(c *http.Client, address string) bool { raw := cx.cookieOf(c, sessionCookie) if raw == "" { return false } got, ok := cx.st.SessionAddress(raw, time.Now()) return ok && got == address } // signInReply walks the OAuth ceremony on browser c and answers the CALLBACK, // the request under test (signUp does the same walk and asserts instead). func (cx *claimCase) signInReply(t *testing.T, c *http.Client, code string) claimReply { t.Helper() status, loc, _ := get(t, c, cx.ts.URL+"/auth/signin") if status != http.StatusSeeOther { t.Fatalf("the sign-in start answered %d, want 303", status) } u, err := url.Parse(loc) if err != nil { t.Fatal(err) } q := u.Query() cx.fk.mu.Lock() cx.fk.challenge = q.Get("code_challenge") cx.fk.expectCode = code cx.fk.mu.Unlock() status, loc, body := get(t, c, cx.ts.URL+"/auth/callback?code="+url.QueryEscape(code)+"&state="+url.QueryEscape(q.Get("state"))) return cx.browserReply(c, status, loc, body) } // ---- the platform system door --------------------------------------------------- // systemUploadReply parks one local build (version, linux-arm64) through the // upload door. func (cx *claimCase) systemUploadReply(t *testing.T, version string) claimReply { t.Helper() var buf bytes.Buffer mw := multipart.NewWriter(&buf) meta, err := mw.CreateFormField("meta") if err != nil { t.Fatal(err) } if _, err := meta.Write([]byte(`{"version":"` + version + `","platform":"linux-arm64"}`)); err != nil { t.Fatal(err) } file, err := mw.CreateFormFile("file", "carlos-linux-arm64") if err != nil { t.Fatal(err) } if _, err := file.Write([]byte("ELF " + version)); err != nil { t.Fatal(err) } if err := mw.Close(); err != nil { t.Fatal(err) } req, err := http.NewRequest(http.MethodPut, cx.ts.URL+"/api/cli/system/upload", &buf) if err != nil { t.Fatal(err) } req.Header.Set("Content-Type", mw.FormDataContentType()) req.Header.Set("Authorization", "Bearer "+cx.bearer) res, err := http.DefaultClient.Do(req) if err != nil { t.Fatal(err) } defer res.Body.Close() out, err := io.ReadAll(res.Body) if err != nil { t.Fatal(err) } return claimReply{Status: res.StatusCode, Body: string(out)} } // systemJob polls one platform job until it is done and answers its record. func (cx *claimCase) systemJob(t *testing.T, id string) map[string]any { t.Helper() for i := 0; i < 200; i++ { r := cx.apiReply(t, http.MethodGet, "/api/cli/system/jobs/"+id, "") if r.Status != http.StatusOK { t.Fatalf("job %s answered %s", id, r) } var job map[string]any if err := json.Unmarshal([]byte(r.Body), &job); err != nil { t.Fatalf("job %s: %v", id, err) } if job["done"] == true { return job } time.Sleep(20 * time.Millisecond) } t.Fatalf("job %s never finished", id) return nil } // systemRelease promotes one uploaded local build to a channel, through the // upload and update doors, and waits for the job. func (cx *claimCase) systemRelease(t *testing.T, channel, version string) string { t.Helper() cx.stageSystemCandidate(t, version) up := cx.systemUploadReply(t, version) if up.Status != http.StatusOK { t.Fatalf("stage upload %s: %s", version, up) } handle := claimReplyField(t, up.Body, "upload") out := cx.mustAPI(t, http.MethodPost, "/api/cli/system/update", fmt.Sprintf(`{"channel":%q,"upload":%q}`, channel, handle), http.StatusOK) id := claimReplyField(t, out, "job") if job := cx.systemJob(t, id); job["error"] != "" { t.Fatalf("stage update %s: job failed: %v", version, job) } return id } // systemVersion is the version a channel's pointer names now, "" when it names // none. func (cx *claimCase) systemVersion(channel string) string { p, _, err := release.GetSystemPointer(context.Background(), cx.rel, cx.pub, release.SystemComponentRouter, channel) if err != nil || p == nil { return "" } return p.Current.Version } // ---- the replica door ------------------------------------------------------------ const ( claimDBHost = "acme.go.tito.io" claimDBBucket = "data-claim" ) // dbWorld is the `carlos db` door's world on an enabled console: a real // instance with its host claim, this console serving its region, a data bucket // and one level-0 LTX file in the instance's replica location. func (cx *claimCase) dbWorld(t *testing.T) { t.Helper() cx.movableInstance(t) cx.srv.cfg.Region = "ie" cx.srv.cfg.HibernateBucket = claimDBBucket cx.versions.AddBucket(claimDBBucket) obs, err := hostclaim.Read(context.Background(), cx.rel, claimDBHost) if err != nil || obs.Absent || obs.Claim == nil || obs.Claim.Holder == nil || obs.Claim.Holder.Location == "" { t.Fatalf("stage the db world: %s has no claim with a replica location: %+v %v", claimDBHost, obs, err) } cx.versions.Put(claimDBBucket, dbVersionKey(obs.Claim.Holder.Location, 0, 1, 1), doorLTXHeader(t, 1, 1)) } // dbHandle lists the replica as the owner and answers the signed handle of the // level-0 file, which is what a file download must present. func (cx *claimCase) dbHandle(t *testing.T) string { t.Helper() r := cx.apiReply(t, http.MethodGet, dbdoor.ListPath("bab", "titogo", claimDBHost, 0), "") if r.Status != http.StatusOK { t.Fatalf("stage the db handle: the listing answered %s", r) } var list dbdoor.ListResponse if err := json.Unmarshal([]byte(r.Body), &list); err != nil { t.Fatalf("stage the db handle: %v: %s", err, r) } for _, f := range list.Files { if f.Level == 0 && f.MinTXID == 1 && f.MaxTXID == 1 && f.Handle != "" { return f.Handle } } t.Fatalf("stage the db handle: the listing names no level-0 file 1-1: %s", r) return "" } // apiReplyWith sends one CLI request as the owner with extra headers. func (cx *claimCase) apiReplyWith(t *testing.T, method, path string, header map[string]string) claimReply { t.Helper() req, err := http.NewRequest(method, cx.ts.URL+cx.expand(path), nil) if err != nil { t.Fatal(err) } req.Header.Set("Authorization", "Bearer "+cx.bearer) for k, v := range header { req.Header.Set(k, v) } res, err := http.DefaultClient.Do(req) if err != nil { t.Fatal(err) } defer res.Body.Close() out, err := io.ReadAll(res.Body) if err != nil { t.Fatal(err) } return claimReply{Status: res.StatusCode, Loc: res.Header.Get("Location"), Body: string(out)} } // ---- host loss ------------------------------------------------------------------ const claimLossHost = "shop.bab.oncarlos.com" // claimLossBox is the roster box the operator declares lost (it is the world's // own tito box, so the roster needs no edit). const claimLossBox = "box-ie-tito" // hostLossWorld stages one serving claim on claimLossHost whose other box is // claimLossBox: the shape a lost box leaves behind. It writes the raw bucket, // below every boundary, as the actor simulator does. func (cx *claimCase) hostLossWorld(t *testing.T) { t.Helper() ctx := context.Background() obs, err := hostclaim.Read(ctx, cx.rel, claimLossHost) if err != nil { t.Fatal(err) } n := 0 nh := hostclaim.NewHolder{Family: "instance", Key: "apps/bab/jam/instances/" + claimLossHost + ".json", Account: "bab", App: "jam", Host: claimLossHost, Region: "ie", RecordRegion: "ie"} if _, err := hostclaim.Update(ctx, cx.rel, obs, hostclaim.Adopt(nh, "box-ie-default", nil, []string{claimLossBox}, "discovery", func() string { n++; return fmt.Sprintf("claim-loss-%d", n) })); err != nil { t.Fatalf("stage the serving claim of %s: %v", claimLossHost, err) } } // lossInspection asks the inspect door for the challenge and decodes it. func (cx *claimCase) lossInspection(t *testing.T) hostresolve.Inspection { t.Helper() r := cx.apiReply(t, http.MethodPost, "/api/cli/ops/hosts/"+claimLossHost+"/resolve/inspect", `{"action":"declare-lost","box":"`+claimLossBox+`"}`) if r.Status != http.StatusOK { t.Fatalf("inspect %s: %s", claimLossHost, r) } var in hostresolve.Inspection if err := json.Unmarshal([]byte(r.Body), &in); err != nil || len(in.Challenge) == 0 || in.Details.ID == "" { t.Fatalf("inspect %s answered no challenge: %v %s", claimLossHost, err, r) } return in } // lossSubmission is the operator's side of a declaration: the cloud // observation the challenge asks for and the attestations it names. func claimLossSubmission(in hostresolve.Inspection) hostfence.Submission { return hostfence.Submission{ Challenge: in.Challenge, Confirmation: in.Details.ID, Observation: hostfence.CloudObservation{ ObservedAt: in.Details.IssuedAt.Add(time.Millisecond), Account: in.Details.Cloud.Account, Region: in.Details.Cloud.Region, InstanceID: in.Details.Cloud.InstanceID, State: "stopped", RequestID: "claim-fixture-request"}, Attestation: hostfence.OperatorAttestation{MaintainComputeFence: true, ColdQuarantineStartOnly: true, Disks: in.Details.Consequences.DestroyedStorageAssertion}, } } // rosterBox reads one roster box through the deployment directory. func (cx *claimCase) rosterBox(t *testing.T, id string) hostactors.Box { t.Helper() d := hostactors.Directory{Store: cx.rel, PublicKey: cx.pub, Bucket: claimFxBucket} o, err := d.Read(context.Background()) if err != nil { t.Fatal(err) } return o.Roster.Boxes[id] } // An enabled update requires a release-workflow identity and fresh floor- // capable box reports. All boxes follow stable; these canary updates have // no pending box acknowledgements, and still run the real approval checks. func (cx *claimCase) stageSystemCandidate(t *testing.T, version string) { t.Helper() ctx := context.Background() tag := "v1.1.0" if version == "77698ab" { tag = "v1.2.0" } sum := sha256.Sum256([]byte("ELF " + version)) rel := release.SystemRelease{Version: version, ReleaseTag: tag, Source: "release:" + tag, MovesProtocol: 1, Artifacts: map[string]release.SystemArtifact{"linux-arm64": {Key: release.SystemArtifactKey(version, "linux-arm64"), SHA256: fmt.Sprintf("%x", sum)}}} st := release.NewDirStore(t.TempDir()) sg := release.RootSigner(cx.priv) if _, err := release.PromoteSystem(ctx, st, sg, "router", "canary", rel, release.SystemPromoteOptions{}); err != nil { t.Fatal(err) } wire, _, err := st.Get(ctx, release.SystemPointerKey("router", "canary")) if err != nil { t.Fatal(err) } if err = release.PublishSystemIdentity(ctx, cx.rel, cx.pub, wire); err != nil { t.Fatal(err) } d := cx.claimFixtureDirectory() o, err := d.Read(ctx) if err != nil { t.Fatal(err) } for ref, b := range o.Roster.Boxes { report, err := cx.srv.cfg.ClaimAuthority.Evidence.BoxReport(ctx, ref, b) if err != nil { t.Fatal(err) } box := status.Box{GeneratedAt: report.GeneratedAt, Metrics: &status.BoxMetrics{Enrollment: report.Enrollment, InstanceID: report.InstanceID, Release: report.Release, ReleaseTag: report.ReleaseTag, Platform: report.Platform, BinarySHA256: report.BinarySHA256, MovesProtocol: report.MovesProtocol, BootID: report.BootID, FloorSHA256: report.FloorSHA256, FloorGuardReady: true, HostClaimEnabled: true, SystemChannel: "stable", Layout: &status.LayoutReport{Mode: "flat", Rows: map[string]status.RowLayout{}}}} body, err := json.Marshal(box) if err != nil { t.Fatal(err) } key := "status/boxes/" + ref + ".json" _, etag, _ := cx.rel.Get(ctx, key) if _, err = cx.rel.Put(ctx, key, body, etag); err != nil { t.Fatal(err) } } }