carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package console
2
3import (
4 "context"
5 "crypto/ed25519"
6 "encoding/base64"
7 "encoding/json"
8 "errors"
9 "net/http"
10 "os"
11 "strings"
12 "testing"
13 "time"
14
15 "github.com/carlosframework/platform/internal/release"
16 "github.com/carlosframework/platform/internal/svcrole"
17)
18
19// deployFixture is a console with app hello in bab, v1 and v2 shipped by
20// paul, and a deploy credential ci over hello with rungs canary and edge.
21func deployFixture(t *testing.T) (fx *consoleFixture, person, deploy string) {
22 t.Helper()
23 fx = newConsoleFixture(t)
24 if _, err := fx.st.CreateAppIn(OpsAccountID, "hello", "paul@keymail.dev", time.Now()); err != nil {
25 t.Fatal(err)
26 }
27 person = cliToken(t, fx, "paul@keymail.dev", "code-deploy-sign")
28 liftShipBudget(fx)
29 for _, v := range []string{"v1", "v2"} {
30 status, out := postShip(t, fx.ts.URL+"/api/cli/apps/bab/hello/releases", person,
31 []shipPart{metaPart(t, v, "binary"), {name: "linux-arm64", body: []byte("bin " + v)}})
32 if status != http.StatusCreated {
33 t.Fatalf("ship %s = %d %v", v, status, out)
34 }
35 }
36 tok, err := fx.st.NewServicePrincipalScoped(OpsAccountID, "ci", "hello", svcrole.Deploy, []string{"canary", "edge"}, nil, "paul@keymail.dev", time.Now())
37 if err != nil {
38 t.Fatal(err)
39 }
40 return fx, person, tok
41}
42
43// pointerGrant returns the decoded payload of the grant carried by a raw
44// channel pointer, or nil when the pointer is root-signed (no grant field).
45func pointerGrant(t *testing.T, fx *consoleFixture, channel string) map[string]any {
46 t.Helper()
47 raw, _, err := release.GetQualified(context.Background(), fx.rel, release.ChannelKey("bab", "hello", channel))
48 if err != nil {
49 t.Fatalf("read %s pointer: %v", channel, err)
50 }
51 var wrap struct {
52 Grant *struct {
53 Payload string `json:"payload"`
54 Sig string `json:"sig"`
55 } `json:"grant"`
56 }
57 if err := json.Unmarshal(raw, &wrap); err != nil {
58 t.Fatalf("pointer is not JSON: %v", err)
59 }
60 if wrap.Grant == nil {
61 return nil
62 }
63 pl, err := base64.StdEncoding.DecodeString(wrap.Grant.Payload)
64 if err != nil {
65 pl, err = base64.RawURLEncoding.DecodeString(wrap.Grant.Payload)
66 if err != nil {
67 t.Fatalf("grant payload is not base64: %v", err)
68 }
69 }
70 var g map[string]any
71 if err := json.Unmarshal(pl, &g); err != nil {
72 t.Fatalf("grant payload is not JSON: %v", err)
73 }
74 return g
75}
76
77func grantRungs(t *testing.T, g map[string]any) []string {
78 t.Helper()
79 var out []string
80 rs, _ := g["rungs"].([]any)
81 for _, r := range rs {
82 s, _ := r.(string)
83 out = append(out, s)
84 }
85 return out
86}
87
88func serialNow(t *testing.T, fx *consoleFixture) int64 {
89 t.Helper()
90 doc, _, err := release.ReadSerial(context.Background(), fx.rel)
91 if err != nil {
92 t.Fatal(err)
93 }
94 return doc.Serial
95}
96
97func TestDeployPromoteIsSignedByAScopedGrant(t *testing.T) {
98 fx, _, deploy := deployFixture(t)
99 before := serialNow(t, fx)
100 status, out := postJSON(t, fx.ts.URL+"/api/cli/apps/bab/hello/promote",
101 map[string]any{"version": "v1", "channel": "edge"}, deploy)
102 if status != http.StatusOK || str(t, out, "result") != "promoted" {
103 t.Fatalf("deploy promote = %d %v", status, out)
104 }
105 if after := serialNow(t, fx); after != before+1 {
106 t.Fatalf("serial %d -> %d, want +1", before, after)
107 }
108 evs, err := fx.st.Events(OpsAccountID, "hello", 10)
109 if err != nil {
110 t.Fatal(err)
111 }
112 found := false
113 for _, e := range evs {
114 if e.Action == "promote" {
115 found = true
116 }
117 }
118 if !found {
119 t.Fatalf("no promote event in %v", evs)
120 }
121 cp, err := release.Channel(context.Background(), fx.rel, fx.pub, "bab", "hello", "edge")
122 if err != nil {
123 t.Fatalf("edge does not verify: %v", err)
124 }
125 if cp.Version != "v1" {
126 t.Fatalf("edge = %q, want v1", cp.Version)
127 }
128 g := pointerGrant(t, fx, "edge")
129 if g == nil {
130 t.Fatal("edge pointer carries no grant")
131 }
132 if got := strings.Join(grantRungs(t, g), ","); got != "canary,edge" {
133 t.Fatalf("grant rungs = %q, want canary,edge", got)
134 }
135 if g["name"] != "deploy.ci" {
136 t.Fatalf("grant name = %v, want deploy.ci", g["name"])
137 }
138}
139
140func TestDeployRepeatIsANoOp(t *testing.T) {
141 fx, _, deploy := deployFixture(t)
142 body := map[string]any{"version": "v1", "channel": "edge"}
143 if status, out := postJSON(t, fx.ts.URL+"/api/cli/apps/bab/hello/promote", body, deploy); status != http.StatusOK || str(t, out, "result") != "promoted" {
144 t.Fatalf("first promote = %d %v", status, out)
145 }
146 before := serialNow(t, fx)
147 status, out := postJSON(t, fx.ts.URL+"/api/cli/apps/bab/hello/promote", body, deploy)
148 if status != http.StatusOK || str(t, out, "result") != "noop" {
149 t.Fatalf("repeat promote = %d %v, want 200 noop", status, out)
150 }
151 if after := serialNow(t, fx); after != before {
152 t.Fatalf("serial %d -> %d on a no-op", before, after)
153 }
154}
155
156func TestDeploySignerRefusesWithoutTheScopeFunction(t *testing.T) {
157 fx, _, _ := deployFixture(t)
158 sg, err := fx.srv.deploySigner(Principal{Kind: PrincipalService, Role: svcrole.Deploy, Name: "ci", App: "hello", Rungs: []string{"edge"}})
159 if err != nil {
160 t.Fatal(err)
161 }
162 ctx := context.Background()
163 for _, ch := range []string{"beta", "stable"} {
164 if _, err := sg.PromoteOnLadder(ctx, fx.rel, "bab", "hello", ch, "v1", release.PromoteOptions{Hotfix: true}); err == nil {
165 t.Errorf("signer promoted %s with no Authorize hook", ch)
166 }
167 }
168}
169
170func TestDeploySignerFailureFailsClosed(t *testing.T) {
171 fx, _, deploy := deployFixture(t)
172 pub, _, err := ed25519.GenerateKey(nil)
173 if err != nil {
174 t.Fatal(err)
175 }
176 fx.srv.cfg.ReleasePub = pub
177 status, out := postJSON(t, fx.ts.URL+"/api/cli/apps/bab/hello/promote",
178 map[string]any{"version": "v1", "channel": "edge"}, deploy)
179 if status != http.StatusServiceUnavailable {
180 t.Fatalf("promote = %d %v, want 503", status, out)
181 }
182 if got := str(t, out, "error"); got != "could not prepare a signing key for this deploy credential. Nothing was promoted." {
183 t.Fatalf("error = %q", got)
184 }
185 if _, _, err := release.GetQualified(context.Background(), fx.rel, release.ChannelKey("bab", "hello", "edge")); !errors.Is(err, os.ErrNotExist) {
186 t.Fatalf("edge pointer after a failed signer: err = %v, want ErrNotExist", err)
187 }
188}
189
190func TestPersonPromoteUnchanged(t *testing.T) {
191 fx, person, _ := deployFixture(t)
192 status, out := postJSON(t, fx.ts.URL+"/api/cli/apps/bab/hello/promote",
193 map[string]any{"version": "v1", "channel": "edge"}, person)
194 if status != http.StatusOK {
195 t.Fatalf("person promote = %d %v", status, out)
196 }
197 if g := pointerGrant(t, fx, "edge"); g != nil {
198 t.Fatalf("person-signed pointer carries a grant: %v", g)
199 }
200}
201
202func TestDeployRollback(t *testing.T) {
203 fx, person, deploy := deployFixture(t)
204 for _, v := range []string{"v1", "v2"} {
205 if status, out := postJSON(t, fx.ts.URL+"/api/cli/apps/bab/hello/promote",
206 map[string]any{"version": v, "channel": "edge"}, person); status != http.StatusOK {
207 t.Fatalf("person promote %s = %d %v", v, status, out)
208 }
209 }
210 status, out := postJSON(t, fx.ts.URL+"/api/cli/apps/bab/hello/rollback",
211 map[string]any{"channel": "edge"}, deploy)
212 if status != http.StatusOK || str(t, out, "result") != "rolled back" {
213 t.Fatalf("deploy rollback = %d %v", status, out)
214 }
215 cp, err := release.Channel(context.Background(), fx.rel, fx.pub, "bab", "hello", "edge")
216 if err != nil {
217 t.Fatal(err)
218 }
219 if cp.Version != "v1" {
220 t.Fatalf("edge = %q, want v1", cp.Version)
221 }
222 g := pointerGrant(t, fx, "edge")
223 if g == nil || g["name"] != "deploy.ci" {
224 t.Fatalf("rolled-back pointer grant = %v, want name deploy.ci", g)
225 }
226}
227
228// TestDeployPromoteCannotFastTrack pins spec §12: the console's promote door
229// has no fast-track input, so a deploy credential cannot waive the bake
230// window by sending one, under either spelling. Whatever status the request
231// gets, a pointer it wrote must not carry fast_track.
232func TestDeployPromoteCannotFastTrack(t *testing.T) {
233 for _, key := range []string{"fast_track", "fastTrack"} {
234 t.Run(key, func(t *testing.T) {
235 fx, _, deploy := deployFixture(t)
236 status, out := postJSON(t, fx.ts.URL+"/api/cli/apps/bab/hello/promote",
237 map[string]any{"version": "v1", "channel": "edge", key: true}, deploy)
238 t.Logf("promote with %s = %d %v", key, status, out)
239 cp, err := release.Channel(context.Background(), fx.rel, fx.pub, "bab", "hello", "edge")
240 if err != nil {
241 if status == http.StatusOK {
242 t.Fatalf("promote answered 200 but edge does not verify: %v", err)
243 }
244 return // refused and nothing written: no pointer to carry the flag
245 }
246 if cp.FastTrack {
247 t.Fatalf("edge pointer payload carries fast_track=true: %+v", cp)
248 }
249 })
250 }
251}
252
253// TestDeployRollbackSignerFailureSaysRolledBack: when the request-local
254// signer cannot be built on a rollback, the 503 says what did not happen —
255// a rollback (approved copy D26r), not D26's "Nothing was promoted." The
256// signer is made to fail with a credential name the grant format refuses;
257// only the store's own non-empty check stands between such a row and the
258// door, and the mint doors never write one.
259func TestDeployRollbackSignerFailureSaysRolledBack(t *testing.T) {
260 fx, person, _ := deployFixture(t)
261 for _, v := range []string{"v1", "v2"} {
262 if status, out := postJSON(t, fx.ts.URL+"/api/cli/apps/bab/hello/promote",
263 map[string]any{"version": v, "channel": "edge"}, person); status != http.StatusOK {
264 t.Fatalf("person promote %s = %d %v", v, status, out)
265 }
266 }
267 bad, err := fx.st.NewServicePrincipalScoped(OpsAccountID, "bad name!", "hello", svcrole.Deploy,
268 []string{"canary", "edge"}, nil, "paul@keymail.dev", time.Now())
269 if err != nil {
270 t.Fatal(err)
271 }
272 status, out := postJSON(t, fx.ts.URL+"/api/cli/apps/bab/hello/rollback",
273 map[string]any{"channel": "edge"}, bad)
274 const want = "could not prepare a signing key for this deploy credential. Nothing was rolled back."
275 if status != http.StatusServiceUnavailable || str(t, out, "error") != want {
276 t.Fatalf("rollback with a failing signer = %d %v, want 503 %q", status, out, want)
277 }
278 cp, err := release.Channel(context.Background(), fx.rel, fx.pub, "bab", "hello", "edge")
279 if err != nil {
280 t.Fatal(err)
281 }
282 if cp.Version != "v2" {
283 t.Fatalf("edge = %q after a refused rollback, want v2", cp.Version)
284 }
285}
286