per-environment object storage: the CLI
create, status, grant and rotate take --environment, and act on that store alone. Without it they act on the app's own store, exactly as before — an app with no environment stores sees no change at all, in behaviour or output. The flag is declared once (storeEnvironmentFlag) so four verbs cannot drift on its spelling, and it is the same word `carlos env --environment` and `carlos instances create --environment` already use: one named bundle, named the same way by every plane that touches it. It rides to the console as the ?environment= query parameter every store door reads, built in storeEnvTail so apiPath keeps deciding how these paths are punctuated. `carlos store status --app a` now also names every environment this app has a store for, and points at how to read one. Teardown is explicit by ruling, so an abandoned environment store keeps its bucket and keeps billing; this is the line where someone sees it. Both printers gained lines rather than reworded ones: store_test.go asserts on their output by substring, and an environment store should read as the app's own store plus a fact, not as a different screen. docs/site/cli.md gains "A store for one environment" — the worked shape of an instance getting a bucket of its own by being the only instance bound to its environment. The docsite gate checks that page against main.go's dispatch, so this is part of the change rather than after it. Spec: docs/superpowers/specs/2026-09-19-per-environment-store-design.md §5 Discussion: amadan carlos/platform #159
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
4 files changed,
+131
−40
apimode.go+35 −11docs/site/cli.md+29 −0store.go+54 −16store_test.go+13 −13
diff --git a/apimode.go b/apimode.go| index 7e8d0cd6f..737cbe2e7 100644 |
| --- a/apimode.go |
| +++ b/apimode.go |
| @@ -422,6 +422,21 @@ func apiPath(a apiAcct, app, tail string) string { |
| return p |
| } |
| +// storeEnvTail names the environment a store verb is aimed at, as the query |
| +// parameter every store door reads. "" is the app's own store and adds |
| +// nothing at all, so this CLI's request to an older console is byte-identical |
| +// to the one it has always sent. |
| +// |
| +// It goes in the TAIL rather than after apiPath, because apiPath appends |
| +// `exact=sqid` with its own ?/& check — building the query here keeps one |
| +// place deciding how these paths are punctuated. |
| +func storeEnvTail(tail, env string) string { |
| + if env == "" { |
| + return tail |
| + } |
| + return tail + "?environment=" + url.QueryEscape(env) |
| +} |
| + |
| func apiAccountStorePath(a apiAcct, tail string) string { |
| p := "/api/cli/accounts/" + url.PathEscape(a.Seg) + "/store/" + tail |
| if a.Exact { |
| @@ -1644,7 +1659,16 @@ type apiStoreProvisioned struct { |
| } |
| type apiStoreReply struct { |
| - App string `json:"app"` |
| + App string `json:"app"` |
| + // Environment is the config environment this store belongs to, "" for the |
| + // app's own. Always sent by a console that knows about environment |
| + // stores; absent from an older one's reply, which decodes as "" — the |
| + // only store such a console has. |
| + Environment string `json:"environment"` |
| + // Environments names every environment this app has a store for, and is |
| + // sent ONLY on a reply about the app's own store. A reply aimed at one |
| + // environment answers about that environment and lists nothing. |
| + Environments []string `json:"environments,omitempty"` |
| Backend string `json:"backend"` |
| Declared *apiStoreDeclared `json:"declared"` |
| Grant *apiStoreGrantBlock `json:"grant"` |
| @@ -1948,11 +1972,11 @@ func (s *apiSession) scanEnforce(ctx context.Context, a apiAcct, app string, on |
| // is "", so a bare `carlos store create --app x` sends the literal `{}` body |
| // handleAPIStoreCreate documents as its own default case — never a |
| // zero-length body, which that door's readJSON would refuse as malformed. |
| -func (s *apiSession) storeCreate(ctx context.Context, a apiAcct, app, envPrefix string) (apiStoreReply, error) { |
| +func (s *apiSession) storeCreate(ctx context.Context, a apiAcct, app, env, envPrefix string) (apiStoreReply, error) { |
| in := struct { |
| EnvPrefix string `json:"env_prefix,omitempty"` |
| }{envPrefix} |
| - status, body, err := s.postJSON(ctx, apiPath(a, app, "/store"), in) |
| + status, body, err := s.postJSON(ctx, apiPath(a, app, storeEnvTail("/store", env)), in) |
| if err != nil { |
| return apiStoreReply{}, err |
| } |
| @@ -1970,7 +1994,7 @@ func (s *apiSession) storeCreate(ctx context.Context, a apiAcct, app, envPrefix |
| // spend its full 60-second storeGrantTimeout on backend work, so this one JSON |
| // door gets a scoped client with a slightly longer deadline; mutating the |
| // session's ordinary 30-second client would silently widen every later call. |
| -func (s *apiSession) storeProvision(ctx context.Context, a apiAcct, app string) (apiStoreReply, error) { |
| +func (s *apiSession) storeProvision(ctx context.Context, a apiAcct, app, env string) (apiStoreReply, error) { |
| clone := *s |
| client := *s.c |
| httpc := *s.c.httpc |
| @@ -1978,7 +2002,7 @@ func (s *apiSession) storeProvision(ctx context.Context, a apiAcct, app string) |
| client.httpc = &httpc |
| clone.c = &client |
| - status, body, err := clone.postJSON(ctx, apiPath(a, app, "/store/provision"), struct{}{}) |
| + status, body, err := clone.postJSON(ctx, apiPath(a, app, storeEnvTail("/store/provision", env)), struct{}{}) |
| if err != nil { |
| return apiStoreReply{}, err |
| } |
| @@ -1998,8 +2022,8 @@ func (s *apiSession) storeProvision(ctx context.Context, a apiAcct, app string) |
| // {"error": "no store is declared..."} body once membership is already |
| // proven, so apiErrorFromBody's json-first decode surfaces it verbatim like |
| // any other server diagnosis; no special case is needed here. |
| -func (s *apiSession) storeGet(ctx context.Context, a apiAcct, app string) (apiStoreReply, error) { |
| - status, body, err := s.getJSON(ctx, apiPath(a, app, "/store")) |
| +func (s *apiSession) storeGet(ctx context.Context, a apiAcct, app, env string) (apiStoreReply, error) { |
| + status, body, err := s.getJSON(ctx, apiPath(a, app, storeEnvTail("/store", env))) |
| if err != nil { |
| return apiStoreReply{}, err |
| } |
| @@ -2023,11 +2047,11 @@ func (s *apiSession) storeGet(ctx context.Context, a apiAcct, app string) (apiSt |
| // bucket == "" mints a fresh bucket; a non-empty bucket asks the walk to |
| // ADOPT that existing one instead. Same omitempty shape as storeCreate's |
| // prefix: a bare `--app x` with no `--bucket` sends `{}`. |
| -func (s *apiSession) storeGrant(ctx context.Context, a apiAcct, app, bucket string) (apiStoreReply, error) { |
| +func (s *apiSession) storeGrant(ctx context.Context, a apiAcct, app, env, bucket string) (apiStoreReply, error) { |
| in := struct { |
| Bucket string `json:"bucket,omitempty"` |
| }{bucket} |
| - status, body, err := s.postJSON(ctx, apiPath(a, app, "/store/grant"), in) |
| + status, body, err := s.postJSON(ctx, apiPath(a, app, storeEnvTail("/store/grant", env)), in) |
| if err != nil { |
| return apiStoreReply{}, err |
| } |
| @@ -2049,12 +2073,12 @@ func (s *apiSession) storeGrant(ctx context.Context, a apiAcct, app, bucket stri |
| // rather than by a body field, matching the server's own route table |
| // (internal/console/authcli.go) and restart's "no body, the whole request is |
| // which door you POST to" shape (handleAPIStoreRotate's doc comment). |
| -func (s *apiSession) storeRotate(ctx context.Context, a apiAcct, app string, finish bool) (apiStoreReply, error) { |
| +func (s *apiSession) storeRotate(ctx context.Context, a apiAcct, app, env string, finish bool) (apiStoreReply, error) { |
| tail := "/store/rotate" |
| if finish { |
| tail = "/store/rotate/finish" |
| } |
| - status, body, err := s.postJSON(ctx, apiPath(a, app, tail), struct{}{}) |
| + status, body, err := s.postJSON(ctx, apiPath(a, app, storeEnvTail(tail, env)), struct{}{}) |
| if err != nil { |
| return apiStoreReply{}, err |
| } |
diff --git a/docs/site/cli.md b/docs/site/cli.md| index 60d098f31..95ef33dc4 100644 |
| --- a/docs/site/cli.md |
| +++ b/docs/site/cli.md |
| @@ -837,6 +837,35 @@ it was measured and can be up to a day old. Sizing a bucket means listing |
| every object in it, and `status` does not do that on each call. A fresh store |
| reads "not measured yet" until the next sweep. |
| +#### A store for one environment |
| + |
| +A store normally belongs to the app: one bucket, one credential, delivered |
| +into the app's default config bundle, and every instance of the app reads it. |
| +`--environment` declares a store that belongs to one named config bundle |
| +instead — its own bucket, its own credential, delivered into that bundle, so |
| +only the instances bound to it can reach it. An instance gets a bucket of its |
| +own by being the only instance bound to its environment. |
| + |
| +```sh |
| +carlos env --environment acme set APP_ORIGIN=https://acme.example.com |
| +carlos store create --app hello --environment acme |
| +carlos instances create --app hello --host acme.example.com --environment acme |
| +carlos store status --app hello --environment acme |
| +``` |
| + |
| +`create`, `status`, `grant` and `rotate` all take `--environment`, and each |
| +acts on that store alone. Without it they act on the app's own store, exactly |
| +as they always have — nothing about an app with no environment stores changes. |
| + |
| +`carlos store status --app hello` also names every environment this app has a |
| +store for. Tearing a store down is always an explicit act, so an environment |
| +nobody uses keeps its bucket and keeps billing until someone destroys it; |
| +this line is where you see one. |
| + |
| +Two instances bound to the same environment share its store, the same way |
| +they share its config. That is visible rather than refused: `status` names |
| +the environment a store belongs to. |
| + |
| `carlos store rotate` mints a fresh credential and leaves the old one live |
| until you run it again with `--finish`, so the app has a window to pick the |
| new one up. Rotating, declaring and reading status are member verbs and want |
diff --git a/store.go b/store.go| index cae6a08a1..684676aaa 100644 |
| --- a/store.go |
| +++ b/store.go |
| @@ -48,14 +48,19 @@ import ( |
| const storeUsage = `usage: carlos store <create|status|apply|approvals|approve|decline|grant|rotate|scan> [flags] |
| - create --app <a> [--env-prefix <PREFIX>] |
| + create --app <a> [--env-prefix <PREFIX>] [--environment <e>] |
| declare that this app wants object storage, under |
| an env prefix (default CARLOS_STORE). For an |
| approved account, this also provisions the store. |
| - status --app <a> show what's declared, granted and provisioned, |
| + --environment declares a store belonging to one |
| + named config bundle, with its own bucket and its |
| + own credential, delivered into that bundle. |
| + status --app <a> [--environment <e>] |
| + show what's declared, granted and provisioned, |
| which delivered variables have not reached the |
| app's config yet, and whether a rotation is only |
| - half finished. |
| + half finished. Without --environment it also names |
| + every environment this app has a store for. |
| apply [--note <text>] apply for object storage for an account you own. |
| approvals list pending account applications (operator only). |
| approve --account <sqid> |
| @@ -64,13 +69,13 @@ const storeUsage = `usage: carlos store <create|status|apply|approvals|approve|d |
| decline --account <sqid> --reason <text> |
| decline an account application (operator only; |
| an explicit account sqid and reason are required). |
| - grant --app <a> [--bucket <name>] |
| + grant --app <a> [--bucket <name>] [--environment <e>] |
| provision the declared store and deliver its |
| credentials. Needs a deployment operator |
| (CARLOS_STORE_OPERATORS) — a 403 for anyone else, |
| member or not. --bucket adopts an existing bucket |
| instead of minting one. |
| - rotate --app <a> [--finish] |
| + rotate --app <a> [--finish] [--environment <e>] |
| mint a fresh credential, leaving the old one live |
| until --finish deletes it. A member verb: spends |
| nothing new, so there is no operator gate here. |
| @@ -164,12 +169,13 @@ func runStoreCreate(args []string) { |
| fs := flag.NewFlagSet("store create", flag.ExitOnError) |
| app := appFlag(fs, "app to declare a store for") |
| envPrefix := fs.String("env-prefix", "", "env var prefix to deliver under (default CARLOS_STORE)") |
| + environment := storeEnvironmentFlag(fs, "declare a store for this config environment instead of the app itself") |
| acctRaw, acctIDRaw, _ := accountFlags(fs) |
| consoleFlag := consoleOption(fs, "console origin to declare through (API mode only; default: the only logged-in one)") |
| fs.Usage = flagUsage(fs, "") |
| fs.Parse(args) |
| name := strings.TrimSpace(*app) |
| - usage := "usage: carlos store create --app <a> [--env-prefix <PREFIX>] [--account <sqid-or-name>] [--account-id <sqid>] [--console <origin>]" |
| + usage := "usage: carlos store create --app <a> [--env-prefix <PREFIX>] [--environment <e>] [--account <sqid-or-name>] [--account-id <sqid>] [--console <origin>]" |
| // Client-side gate before the server's, the same one every write verb in |
| // this file applies: a name the console is certain to refuse should cost |
| // a typo nothing. |
| @@ -183,13 +189,25 @@ func runStoreCreate(args []string) { |
| printUsing(stdout, sess) |
| acct := resolvedAccount(sess, acctRaw, acctIDRaw) |
| name = resolvedApp(sess, acct, name, usage) |
| - if err := storeCreateAndProvision(context.Background(), sess, acct, name, strings.TrimSpace(*envPrefix), stdout); err != nil { |
| + if err := storeCreateAndProvision(context.Background(), sess, acct, name, |
| + strings.TrimSpace(*environment), strings.TrimSpace(*envPrefix), stdout); err != nil { |
| log.Fatalf("store create: %v", err) |
| } |
| } |
| -func storeCreateAndProvision(ctx context.Context, sess *apiSession, acct apiAcct, app, envPrefix string, out io.Writer) error { |
| - res, err := sess.storeCreate(ctx, acct, app, envPrefix) |
| +// storeEnvironmentFlag is the --environment flag every store verb takes: the |
| +// config environment whose store to act on, or none for the app's own. |
| +// |
| +// Declared in one place so the four verbs cannot drift on its spelling or on |
| +// what it means. The name matches `carlos env --environment` and |
| +// `carlos instances create --environment` deliberately: it is the same named |
| +// bundle in all three, and a store delivers INTO that bundle. |
| +func storeEnvironmentFlag(fs *flag.FlagSet, usage string) *string { |
| + return fs.String("environment", "", usage) |
| +} |
| + |
| +func storeCreateAndProvision(ctx context.Context, sess *apiSession, acct apiAcct, app, env, envPrefix string, out io.Writer) error { |
| + res, err := sess.storeCreate(ctx, acct, app, env, envPrefix) |
| if err != nil { |
| return err |
| } |
| @@ -197,7 +215,7 @@ func storeCreateAndProvision(ctx context.Context, sess *apiSession, acct apiAcct |
| if res.Backend == "" || !res.Entitlement.Approved() { |
| return nil |
| } |
| - res, err = sess.storeProvision(ctx, acct, app) |
| + res, err = sess.storeProvision(ctx, acct, app, env) |
| if err != nil { |
| return err |
| } |
| @@ -208,12 +226,13 @@ func storeCreateAndProvision(ctx context.Context, sess *apiSession, acct apiAcct |
| func runStoreStatus(args []string) { |
| fs := flag.NewFlagSet("store status", flag.ExitOnError) |
| app := appFlag(fs, "app whose store to show") |
| + environment := storeEnvironmentFlag(fs, "show this config environment's store instead of the app's own") |
| acctRaw, acctIDRaw, _ := accountFlags(fs) |
| consoleFlag := consoleOption(fs, "console origin to read through (API mode only; default: the only logged-in one)") |
| fs.Usage = flagUsage(fs, "") |
| fs.Parse(args) |
| name := strings.TrimSpace(*app) |
| - usage := "usage: carlos store status --app <a> [--account <sqid-or-name>] [--account-id <sqid>] [--console <origin>]" |
| + usage := "usage: carlos store status --app <a> [--environment <e>] [--account <sqid-or-name>] [--account-id <sqid>] [--console <origin>]" |
| if name != "" { |
| if err := validAppName(name); err != nil { |
| log.Fatalf("store status: %v", err) |
| @@ -224,7 +243,7 @@ func runStoreStatus(args []string) { |
| printUsing(stdout, sess) |
| acct := resolvedAccount(sess, acctRaw, acctIDRaw) |
| name = resolvedApp(sess, acct, name, usage) |
| - res, err := sess.storeGet(context.Background(), acct, name) |
| + res, err := sess.storeGet(context.Background(), acct, name, strings.TrimSpace(*environment)) |
| if err != nil { |
| log.Fatalf("store status: %v", err) |
| } |
| @@ -235,12 +254,13 @@ func runStoreGrant(args []string) { |
| fs := flag.NewFlagSet("store grant", flag.ExitOnError) |
| app := appFlag(fs, "app to grant a store for") |
| bucket := fs.String("bucket", "", "adopt this existing bucket instead of minting one") |
| + environment := storeEnvironmentFlag(fs, "grant this config environment's store instead of the app's own") |
| acctRaw, acctIDRaw, _ := accountFlags(fs) |
| consoleFlag := consoleOption(fs, "console origin to grant through (API mode only; default: the only logged-in one)") |
| fs.Usage = flagUsage(fs, "") |
| fs.Parse(args) |
| name := strings.TrimSpace(*app) |
| - usage := "usage: carlos store grant --app <a> [--bucket <name>] [--account <sqid-or-name>] [--account-id <sqid>] [--console <origin>]" |
| + usage := "usage: carlos store grant --app <a> [--bucket <name>] [--environment <e>] [--account <sqid-or-name>] [--account-id <sqid>] [--console <origin>]" |
| if name != "" { |
| if err := validAppName(name); err != nil { |
| log.Fatalf("store grant: %v", err) |
| @@ -251,7 +271,7 @@ func runStoreGrant(args []string) { |
| printUsing(stdout, sess) |
| acct := resolvedAccount(sess, acctRaw, acctIDRaw) |
| name = resolvedApp(sess, acct, name, usage) |
| - res, err := sess.storeGrant(context.Background(), acct, name, strings.TrimSpace(*bucket)) |
| + res, err := sess.storeGrant(context.Background(), acct, name, strings.TrimSpace(*environment), strings.TrimSpace(*bucket)) |
| if err != nil { |
| log.Fatalf("store grant: %v", err) |
| } |
| @@ -262,12 +282,13 @@ func runStoreRotate(args []string) { |
| fs := flag.NewFlagSet("store rotate", flag.ExitOnError) |
| app := appFlag(fs, "app whose store credential to rotate") |
| finish := fs.Bool("finish", false, "delete the previous key instead of minting a new one") |
| + environment := storeEnvironmentFlag(fs, "rotate this config environment's store instead of the app's own") |
| acctRaw, acctIDRaw, _ := accountFlags(fs) |
| consoleFlag := consoleOption(fs, "console origin to rotate through (API mode only; default: the only logged-in one)") |
| fs.Usage = flagUsage(fs, "") |
| fs.Parse(args) |
| name := strings.TrimSpace(*app) |
| - usage := "usage: carlos store rotate --app <a> [--finish] [--account <sqid-or-name>] [--account-id <sqid>] [--console <origin>]" |
| + usage := "usage: carlos store rotate --app <a> [--finish] [--environment <e>] [--account <sqid-or-name>] [--account-id <sqid>] [--console <origin>]" |
| if name != "" { |
| if err := validAppName(name); err != nil { |
| log.Fatalf("store rotate: %v", err) |
| @@ -278,7 +299,7 @@ func runStoreRotate(args []string) { |
| printUsing(stdout, sess) |
| acct := resolvedAccount(sess, acctRaw, acctIDRaw) |
| name = resolvedApp(sess, acct, name, usage) |
| - res, err := sess.storeRotate(context.Background(), acct, name, *finish) |
| + res, err := sess.storeRotate(context.Background(), acct, name, strings.TrimSpace(*environment), *finish) |
| if err != nil { |
| log.Fatalf("store rotate: %v", err) |
| } |
| @@ -420,6 +441,12 @@ func printStoreCreate(w io.Writer, res apiStoreReply) { |
| } |
| fmt.Fprintf(w, "%s %s under prefix %s — delivers %s\n", |
| c.ok("declared a store for"), res.App, prefix, strings.Join(res.EnvNames, ", ")) |
| + if res.Environment != "" { |
| + // Named on its own line rather than folded into the sentence above: |
| + // the variables are delivered into THAT bundle, so only the |
| + // instances bound to it read them. |
| + fmt.Fprintf(w, " environment: %s — delivered into that bundle, not the app's default one\n", res.Environment) |
| + } |
| if res.Provisioned != nil { |
| p := res.Provisioned |
| fmt.Fprintf(w, " provisioned: bucket %s in %s, user %s\n", p.Bucket, p.Region, p.IAMUser) |
| @@ -517,6 +544,9 @@ func printStoreSize(w io.Writer, sz *apiStoreSize) { |
| func printStoreStatus(w io.Writer, res apiStoreReply) { |
| c := colorFor(w) |
| fmt.Fprintf(w, "%s %s\n", c.head("store for"), res.App) |
| + if res.Environment != "" { |
| + fmt.Fprintf(w, " environment: %s\n", res.Environment) |
| + } |
| if res.Backend == "" { |
| warnf(w, "this deployment has no object storage backend configured yet (the operator sets CARLOS_STORE_BACKEND)") |
| } |
| @@ -558,6 +588,14 @@ func printStoreStatus(w io.Writer, res apiStoreReply) { |
| if res.PendingFinish { |
| warnf(w, "a rotation is pending finish — the previous key is still live: carlos store rotate --finish --app %s", res.App) |
| } |
| + // Every OTHER store this app has. Only an app-store status carries these, |
| + // and each one is a bucket of its own with a credential of its own — a |
| + // store nobody remembers is a bill nobody remembers, and teardown is |
| + // explicit, so this line is where an abandoned one is visible. |
| + if len(res.Environments) > 0 { |
| + fmt.Fprintf(w, " environments with their own store: %s\n", strings.Join(res.Environments, ", ")) |
| + mutedf(w, " read one with carlos store status --app %s --environment %s", res.App, res.Environments[0]) |
| + } |
| // Scanning, once it exists. Deliberately three distinct states rather |
| // than a boolean: "requested" is a member waiting on an operator, |
| // "enabled" is an engine looking at objects, and "enforcing" is infected |
diff --git a/store_test.go b/store_test.go| index f2172ca07..72e92216e 100644 |
| --- a/store_test.go |
| +++ b/store_test.go |
| @@ -60,7 +60,7 @@ func TestAPISessionStoreCreateWireShape(t *testing.T) { |
| })) |
| defer ts.Close() |
| - res, err := newTestSession(ts.URL).storeCreate(context.Background(), apiAcct{Seg: "keymail-team"}, "keymail", "") |
| + res, err := newTestSession(ts.URL).storeCreate(context.Background(), apiAcct{Seg: "keymail-team"}, "keymail", "", "") |
| if err != nil { |
| t.Fatalf("storeCreate: %v", err) |
| } |
| @@ -103,7 +103,7 @@ func TestAPISessionStoreCreateSendsChosenPrefix(t *testing.T) { |
| })) |
| defer ts.Close() |
| - if _, err := newTestSession(ts.URL).storeCreate(context.Background(), apiAcct{Seg: "acct"}, "keymail", "TITO_S3"); err != nil { |
| + if _, err := newTestSession(ts.URL).storeCreate(context.Background(), apiAcct{Seg: "acct"}, "keymail", "", "TITO_S3"); err != nil { |
| t.Fatalf("storeCreate: %v", err) |
| } |
| var sent struct { |
| @@ -138,7 +138,7 @@ func TestStoreCreateAndProvisionPrintsDeclarationBeforeProvisionFailure(t *testi |
| })) |
| defer ts.Close() |
| - err := storeCreateAndProvision(context.Background(), newTestSession(ts.URL), apiAcct{Seg: "bac"}, "keymail", "", &out) |
| + err := storeCreateAndProvision(context.Background(), newTestSession(ts.URL), apiAcct{Seg: "bac"}, "keymail", "", "", &out) |
| if err == nil || !strings.Contains(err.Error(), "provisioning failed") { |
| t.Fatalf("error = %v, want provision failure", err) |
| } |
| @@ -172,7 +172,7 @@ func TestStoreCreateAndProvisionBranchesOnReply(t *testing.T) { |
| json.NewEncoder(w).Encode(apiStoreReply{App: "keymail", Backend: tt.backend, Entitlement: tt.entitlement, Declared: &apiStoreDeclared{EnvPrefix: "CARLOS_STORE"}}) |
| })) |
| defer ts.Close() |
| - if err := storeCreateAndProvision(context.Background(), newTestSession(ts.URL), apiAcct{Seg: "bac"}, "keymail", "", new(bytes.Buffer)); err != nil { |
| + if err := storeCreateAndProvision(context.Background(), newTestSession(ts.URL), apiAcct{Seg: "bac"}, "keymail", "", "", new(bytes.Buffer)); err != nil { |
| t.Fatal(err) |
| } |
| if calls != tt.wantCalls { |
| @@ -190,7 +190,7 @@ func TestStoreProvisionUsesLongWalkClient(t *testing.T) { |
| defer ts.Close() |
| s := newTestSession(ts.URL) |
| s.c.httpc.Timeout = time.Millisecond |
| - if _, err := s.storeProvision(context.Background(), apiAcct{Seg: "bac"}, "keymail"); err != nil { |
| + if _, err := s.storeProvision(context.Background(), apiAcct{Seg: "bac"}, "keymail", ""); err != nil { |
| t.Fatalf("storeProvision inherited ordinary JSON timeout: %v", err) |
| } |
| if s.c.httpc.Timeout != time.Millisecond { |
| @@ -333,7 +333,7 @@ func TestAPISessionStoreGetWireShape(t *testing.T) { |
| })) |
| defer ts.Close() |
| - res, err := newTestSession(ts.URL).storeGet(context.Background(), apiAcct{Seg: "acct"}, "keymail") |
| + res, err := newTestSession(ts.URL).storeGet(context.Background(), apiAcct{Seg: "acct"}, "keymail", "") |
| if err != nil { |
| t.Fatalf("storeGet: %v", err) |
| } |
| @@ -364,7 +364,7 @@ func TestAPISessionStoreGetNotDeclaredSurfacesServerText(t *testing.T) { |
| })) |
| defer ts.Close() |
| - _, err := newTestSession(ts.URL).storeGet(context.Background(), apiAcct{Seg: "acct"}, "keymail") |
| + _, err := newTestSession(ts.URL).storeGet(context.Background(), apiAcct{Seg: "acct"}, "keymail", "") |
| if err == nil || !strings.Contains(err.Error(), "no store is declared") { |
| t.Fatalf("storeGet 404 = %v, want the server's own text", err) |
| } |
| @@ -390,7 +390,7 @@ func TestAPISessionStoreGrantWireShape(t *testing.T) { |
| })) |
| defer ts.Close() |
| - res, err := newTestSession(ts.URL).storeGrant(context.Background(), apiAcct{Seg: "acct"}, "keymail", "") |
| + res, err := newTestSession(ts.URL).storeGrant(context.Background(), apiAcct{Seg: "acct"}, "keymail", "", "") |
| if err != nil { |
| t.Fatalf("storeGrant: %v", err) |
| } |
| @@ -417,7 +417,7 @@ func TestAPISessionStoreGrantSendsChosenBucket(t *testing.T) { |
| })) |
| defer ts.Close() |
| - if _, err := newTestSession(ts.URL).storeGrant(context.Background(), apiAcct{Seg: "acct"}, "keymail", "my-existing-bucket"); err != nil { |
| + if _, err := newTestSession(ts.URL).storeGrant(context.Background(), apiAcct{Seg: "acct"}, "keymail", "", "my-existing-bucket"); err != nil { |
| t.Fatalf("storeGrant: %v", err) |
| } |
| var sent struct { |
| @@ -439,7 +439,7 @@ func TestAPISessionStoreGrant403SurfacesVerbatim(t *testing.T) { |
| })) |
| defer ts.Close() |
| - _, err := newTestSession(ts.URL).storeGrant(context.Background(), apiAcct{Seg: "acct"}, "keymail", "") |
| + _, err := newTestSession(ts.URL).storeGrant(context.Background(), apiAcct{Seg: "acct"}, "keymail", "", "") |
| if err == nil || err.Error() != "store grants need a deployment operator (CARLOS_STORE_OPERATORS)" { |
| t.Fatalf("storeGrant 403 = %v, want the console's own text byte-for-byte", err) |
| } |
| @@ -464,7 +464,7 @@ func TestAPISessionStoreRotateWireShape(t *testing.T) { |
| })) |
| defer ts.Close() |
| - res, err := newTestSession(ts.URL).storeRotate(context.Background(), apiAcct{Seg: "acct"}, "keymail", false) |
| + res, err := newTestSession(ts.URL).storeRotate(context.Background(), apiAcct{Seg: "acct"}, "keymail", "", false) |
| if err != nil { |
| t.Fatalf("storeRotate: %v", err) |
| } |
| @@ -496,7 +496,7 @@ func TestAPISessionStoreRotateFinishHitsFinishDoor(t *testing.T) { |
| })) |
| defer ts.Close() |
| - res, err := newTestSession(ts.URL).storeRotate(context.Background(), apiAcct{Seg: "acct"}, "keymail", true) |
| + res, err := newTestSession(ts.URL).storeRotate(context.Background(), apiAcct{Seg: "acct"}, "keymail", "", true) |
| if err != nil { |
| t.Fatalf("storeRotate(finish=true): %v", err) |
| } |
| @@ -517,7 +517,7 @@ func TestAPISessionStoreRotateConflictSurfacesVerbatim(t *testing.T) { |
| })) |
| defer ts.Close() |
| - _, err := newTestSession(ts.URL).storeRotate(context.Background(), apiAcct{Seg: "acct"}, "keymail", false) |
| + _, err := newTestSession(ts.URL).storeRotate(context.Background(), apiAcct{Seg: "acct"}, "keymail", "", false) |
| if err == nil || !strings.Contains(err.Error(), "grant one first") { |
| t.Fatalf("storeRotate 409 = %v, want the console's own text", err) |
| } |