carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1package main
2
3import (
4 "os"
5 "os/exec"
6 "path/filepath"
7 "strings"
8 "testing"
9
10 "github.com/carlosframework/platform/internal/release"
11)
12
13// carlosTestEnv is os.Environ() with the operator's real login and console
14// state masked: CARLOS_CREDENTIALS points into the test's own tempdir where
15// no credential exists, HOME is a scratch dir (the binary reads
16// ~/.carlos/config for the default console), and CARLOS_CONSOLE is cleared.
17// Every subprocess that runs the carlos binary MUST build its env through
18// this AND set cmd.Dir to a scratch dir — the binary walks cwd ancestors
19// for .carlos/config, so a subprocess running from the real checkout reads
20// the developer's real config the moment one exists. A test that inherits
21// the real ~/.carlos is one `go test ./...` on a logged-in laptop away from
22// writing to a production console — which is exactly how 40+ test accounts
23// got minted on the flagship (2026-08-04).
24//
25// Duplicated from e2e_test.go rather than shared: that file is package
26// main_test (an external test package with no access to this package's
27// unexported symbols), this one is package main, and the two cannot share
28// an unexported helper across the package boundary.
29func carlosTestEnv(t *testing.T, extra ...string) []string {
30 t.Helper()
31 scratch := t.TempDir()
32 // An inherited CARLOS_SERVICE_TOKEN (a CI shell that exports one) would
33 // make any child that also sets CARLOS_DEPLOYMENT_DIR fail resolveMode's
34 // token-plus-store refusal, so it is dropped here. It is filtered rather
35 // than blanked: a test that passes its own token in extra still wins.
36 var env []string
37 for _, kv := range os.Environ() {
38 if !strings.HasPrefix(kv, "CARLOS_SERVICE_TOKEN=") {
39 env = append(env, kv)
40 }
41 }
42 env = append(env,
43 "CARLOS_CREDENTIALS="+filepath.Join(scratch, "credentials"),
44 "HOME="+scratch,
45 "CARLOS_CONSOLE=",
46 "CARLOS_DESTINATION=",
47 )
48 return append(env, extra...)
49}
50
51// The scoped signing key as an operator meets it: two modes of one verb, and
52// a promote that signs with whatever CARLOS_RELEASE_KEY holds. Driven through
53// the real binary, because the whole feature is about which secret is sitting
54// in which variable on which machine — a thing the functions underneath
55// cannot be wrong about on their own.
56func TestScopedKeysCLI(t *testing.T) {
57 bin := buildCarlos(t)
58 bucket := t.TempDir()
59 scratch := t.TempDir()
60
61 run := func(env []string, args ...string) (string, error) {
62 t.Helper()
63 cmd := exec.Command(bin, args...)
64 cmd.Env = carlosTestEnv(t,
65 append([]string{
66 "CARLOS_DEPLOYMENT_DIR=" + bucket,
67 "CARLOS_DATA_DIR=" + t.TempDir(),
68 }, env...)...)
69 // pick() walks cwd ancestors for .carlos/config, so the subprocess must not run from the real checkout.
70 cmd.Dir = scratch
71 out, err := cmd.CombinedOutput()
72 return string(out), err
73 }
74
75 // -- root keygen, unchanged ------------------------------------------
76 //
77 // Mutation-mindedness: any change to the no-flag output breaks every
78 // deployment's bootstrap instructions, so it is asserted key by key.
79 out, err := run(nil, "release-keygen")
80 if err != nil {
81 t.Fatalf("release-keygen: %v\n%s", err, out)
82 }
83 rootKey := envValue(t, out, "CARLOS_RELEASE_KEY")
84 rootPub := envValue(t, out, "CARLOS_RELEASE_PUBKEY")
85 if release.IsScopedSeed(rootKey) {
86 t.Fatal("the root key came out tagged as scoped")
87 }
88 rootEnv := []string{"CARLOS_RELEASE_KEY=" + rootKey, "CARLOS_RELEASE_PUBKEY=" + rootPub}
89
90 // -name/-expires without -scope are refused rather than silently
91 // ignored: an operator who typed them meant to mint a scoped key, and
92 // walking away with a ROOT key they think is scoped is the worst
93 // possible outcome of this verb.
94 if out, err := run(rootEnv, "release-keygen", "-name", "edge-builder"); err == nil {
95 t.Fatalf("release-keygen --name without --scope minted a root key:\n%s", out)
96 }
97
98 // -- scoped keygen ---------------------------------------------------
99 out, err = run(rootEnv, "release-keygen", "-scope", "canary,edge", "-name", "edge-builder")
100 if err != nil {
101 t.Fatalf("release-keygen --scope: %v\n%s", err, out)
102 }
103 scopedKey := envValue(t, out, "CARLOS_RELEASE_KEY")
104 grant := envValue(t, out, "CARLOS_RELEASE_GRANT")
105 if !release.IsScopedSeed(scopedKey) {
106 t.Fatalf("scoped keygen printed an untagged key %q", scopedKey)
107 }
108 // The fleet's anchor is not reprinted as an assignment. A second
109 // copy-pasteable CARLOS_RELEASE_PUBKEY= line is an invitation to install
110 // it on a box, which would give the deployment two trust anchors and
111 // defeat the entire scheme. (The prose guidance names the variable to say
112 // it does NOT change; that is the opposite of a footgun.)
113 for _, line := range strings.Split(out, "\n") {
114 if strings.HasPrefix(strings.TrimSpace(line), "CARLOS_RELEASE_PUBKEY=") {
115 t.Fatalf("scoped keygen printed a public key assignment:\n%s", out)
116 }
117 }
118 if !strings.Contains(out, "edge-builder") {
119 t.Fatalf("scoped keygen does not name the key it minted:\n%s", out)
120 }
121
122 // stable is refused at mint, by the verb, naming stable.
123 if out, err := run(rootEnv, "release-keygen", "-scope", "edge,stable", "-name", "bad"); err == nil {
124 t.Fatalf("release-keygen --scope edge,stable succeeded:\n%s", out)
125 } else if !strings.Contains(out, "stable") {
126 t.Fatalf("refusal does not name stable:\n%s", out)
127 }
128
129 // Minting needs the ROOT key: a scoped key cannot widen itself.
130 scopedOnly := []string{"CARLOS_RELEASE_KEY=" + scopedKey, "CARLOS_RELEASE_PUBKEY=" + rootPub}
131 if out, err := run(scopedOnly, "release-keygen", "-scope", "beta", "-name", "wider"); err == nil {
132 t.Fatalf("a scoped key minted a grant:\n%s", out)
133 }
134
135 // -- promoting with the scoped key -----------------------------------
136 art := filepath.Join(t.TempDir(), "kass")
137 if err := os.WriteFile(art, []byte("build one"), 0o755); err != nil {
138 t.Fatal(err)
139 }
140 if out, err := run(rootEnv, "ship", "-app", "kass", "-version", "9f3c1ad", art); err != nil {
141 t.Fatalf("ship: %v\n%s", err, out)
142 }
143
144 scopedEnv := append(append([]string{}, scopedOnly...), "CARLOS_RELEASE_GRANT="+grant)
145 if out, err := run(scopedEnv, "promote", "-app", "kass", "9f3c1ad", "edge"); err != nil {
146 t.Fatalf("scoped promote to edge: %v\n%s", err, out)
147 }
148 // It verifies for a reader holding only the ROOT public key — which is
149 // every box in the fleet.
150 out, err = run(rootEnv, "channels", "-app", "kass")
151 if err != nil {
152 t.Fatalf("channels: %v\n%s", err, out)
153 }
154 if !strings.Contains(out, "edge") || !strings.Contains(out, "9f3c1ad") {
155 t.Fatalf("channels does not show the scope-signed pointer:\n%s", out)
156 }
157
158 // -- what the scoped key may not do ----------------------------------
159 //
160 // stable is refused locally and nothing is written. The bucket check is
161 // the load-bearing half: an error message is easy, a channel pointer that
162 // never appeared is the property.
163 out, err = run(scopedEnv, "promote", "-app", "kass", "9f3c1ad", "stable")
164 if err == nil {
165 t.Fatalf("the edge key promoted to stable:\n%s", out)
166 }
167 for _, want := range []string{"edge-builder", "stable"} {
168 if !strings.Contains(out, want) {
169 t.Errorf("refusal does not name %q:\n%s", want, out)
170 }
171 }
172 if _, err := os.Stat(filepath.Join(bucket, "apps", release.OpsAccount, "kass", "channels", "stable.json")); !os.IsNotExist(err) {
173 t.Fatalf("a stable pointer exists after a refused promote (stat err = %v)", err)
174 }
175
176 // -hotfix bypasses the ladder, never the grant.
177 if out, err := run(scopedEnv, "promote", "-app", "kass", "-hotfix", "9f3c1ad", "stable"); err == nil {
178 t.Fatalf("--hotfix let the edge key reach stable:\n%s", out)
179 }
180
181 // A scoped key with no grant beside it fails HERE, naming the grant,
182 // rather than signing something the fleet will silently refuse later.
183 out, err = run(scopedOnly, "promote", "-app", "kass", "9f3c1ad", "edge")
184 if err == nil {
185 t.Fatalf("a scoped key promoted with no grant:\n%s", out)
186 }
187 if !strings.Contains(out, "CARLOS_RELEASE_GRANT") {
188 t.Fatalf("refusal does not name the missing grant:\n%s", out)
189 }
190
191 // -- and the root key is still unrestricted --------------------------
192 if out, err := run(rootEnv, "promote", "-app", "kass", "9f3c1ad", "beta"); err != nil {
193 t.Fatalf("root promote to beta: %v\n%s", err, out)
194 }
195 if out, err := run(rootEnv, "promote", "-app", "kass", "9f3c1ad", "stable"); err != nil {
196 t.Fatalf("root promote to stable: %v\n%s", err, out)
197 }
198}
199
200// envValue pulls VAR=value out of a verb's stdout.
201func envValue(t *testing.T, out, key string) string {
202 t.Helper()
203 for _, line := range strings.Split(out, "\n") {
204 if v, ok := strings.CutPrefix(strings.TrimSpace(line), key+"="); ok {
205 return v
206 }
207 }
208 t.Fatalf("no %s in output:\n%s", key, out)
209 return ""
210}
211