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 "bytes"
5 "context"
6 "crypto/ed25519"
7 "encoding/json"
8 "errors"
9 "os"
10 "os/exec"
11 "path/filepath"
12 "runtime"
13 "strings"
14 "testing"
15 "time"
16
17 "github.com/carlosframework/platform/internal/historyreadback"
18 "github.com/carlosframework/platform/internal/hostactors"
19 "github.com/carlosframework/platform/internal/release"
20)
21
22func TestPlan14HistoryCommandResume(t *testing.T) {
23 for _, boundary := range []string{"timeout", "save failure"} {
24 t.Run(boundary, func(t *testing.T) {
25 pub, key, _ := ed25519.GenerateKey(nil)
26 _, boxKey, _ := ed25519.GenerateKey(nil)
27 signer := release.RootSigner(key)
28 d := hostactors.Directory{Store: release.NewDirStore(t.TempDir()), PublicKey: pub, Bucket: "bucket"}
29 enrollment := strings.Repeat("a", 32)
30 roster := hostactors.Roster{V: 1, DeploymentBucket: "bucket", Revision: 1, Boxes: map[string]hostactors.Box{"box": {Enrollment: enrollment, Namespace: "platform", BoxID: "box", Label: "box", Region: "ie", Pool: "default", Cloud: hostactors.Cloud{Provider: "aws", Account: "123", Region: "eu-west-1", InstanceID: "i-box"}, EvidenceSHA256: strings.Repeat("b", 64), State: "active", CensusPublicKey: boxKey.Public().(ed25519.PublicKey)}}, Consoles: map[string]hostactors.Console{}, Holds: map[string]hostactors.Reservation{}, Activations: map[string]hostactors.Reservation{}}
31 wire, err := release.SignRootEnvelope(signer, roster)
32 if err != nil {
33 t.Fatal(err)
34 }
35 if _, err = d.Store.Put(t.Context(), hostactors.Key, wire, ""); err != nil {
36 t.Fatal(err)
37 }
38 state := filepath.Join(t.TempDir(), "operation.json")
39 connect := func(bucket string, resume bool) (hostactors.Directory, release.Signer, error) {
40 if bucket != "bucket" {
41 t.Fatal(bucket)
42 }
43 if resume {
44 return d, release.Signer{}, nil
45 }
46 return d, signer, nil
47 }
48 args := []string{"status", "--bucket", "bucket", "--box", "box", "--enrollment", enrollment, "--account", "abc", "--app", "web", "--host", "web.example", "--state", state, "--wait", "1ms"}
49 var out bytes.Buffer
50 save := historySave
51 if boundary == "save failure" {
52 save = func(path string, op historyreadback.Operation) error {
53 if err := historySave(path, op); err != nil {
54 return err
55 }
56 return os.ErrPermission
57 }
58 }
59 if err := historyCommandWithSave(t.Context(), args, connect, &out, save); err == nil {
60 t.Fatal("missing receipt succeeded")
61 }
62 raw, err := os.ReadFile(state)
63 if err != nil {
64 t.Fatal(err)
65 }
66 var op historyreadback.Operation
67 if err = json.Unmarshal(raw, &op); err != nil {
68 t.Fatal(err)
69 }
70 if boundary == "save failure" {
71 scope := historyreadback.Scope{Bucket: "bucket", Box: "box", Enrollment: enrollment, Binding: release.HistoryBinding{Account: "abc", App: "web", Host: "web.example"}}
72 if _, _, err := d.Store.Get(t.Context(), historyreadback.RequestKey(scope)); !errors.Is(err, os.ErrNotExist) {
73 t.Fatal("published after save failure", err)
74 }
75 if err := op.Deliver(t.Context(), d, time.Now()); err != nil {
76 t.Fatal(err)
77 }
78 }
79 var q historyreadback.Request
80 if err = release.VerifyRootEnvelope(pub, op.Request, &q); err != nil {
81 t.Fatal(err)
82 }
83 status := release.HistoryMigrationStatus{Schema: 1, Account: "abc", App: "web", HistoryReconstructionStatus: release.HistoryReconstructionStatus{Identity: strings.Repeat("c", 64), Revision: 1}, Hosts: []release.HistoryHostStatus{{HistoryReconstructionHost: release.HistoryReconstructionHost{Host: "web.example", Source: "canonical"}}}}
84 if err = historyreadback.Respond(t.Context(), d, q.Scope, boxKey, time.Now(), func(context.Context) (release.HistoryMigrationStatus, error) { return status, nil }); err != nil {
85 t.Fatal(err)
86 }
87 out.Reset()
88 if err = historyCommand(t.Context(), []string{"resume", "--bucket", "bucket", "--state", state, "--wait", "1ms"}, connect, &out); err != nil {
89 t.Fatal(err)
90 }
91 var got historyreadback.Receipt
92 if err = json.Unmarshal(out.Bytes(), &got); err != nil {
93 t.Fatal(err)
94 }
95 if got.Status.AppComplete {
96 t.Fatal("invented migration completion")
97 }
98 again, _ := os.ReadFile(state)
99 if !bytes.Equal(raw, again) {
100 t.Fatal("resume changed saved operation")
101 }
102 if err = historyCommand(t.Context(), args, connect, &out); err == nil {
103 t.Fatal("overwrote existing operation")
104 }
105
106 })
107 }
108}
109
110func TestPlan14HistoryCommandInputRefusals(t *testing.T) {
111 for _, args := range [][]string{{}, {"wat"}, {"resume"}, {"status", "--state", "x", "--wait", "0s"}, {"resume", "--state", "x", "--input", "other"}, {"status", "--state", "x", "--unknown"}} {
112 connect := func(string, bool) (hostactors.Directory, release.Signer, error) {
113 t.Fatal("invalid arguments connected")
114 return hostactors.Directory{}, release.Signer{}, nil
115 }
116 if err := historyCommand(t.Context(), args, connect, &bytes.Buffer{}); err == nil {
117 t.Fatal("accepted", args)
118 }
119 }
120}
121
122func TestPlan14HistoryCLIDispatch(t *testing.T) {
123 bin := buildCarlos(t)
124 cmd := exec.CommandContext(t.Context(), bin, "proof", "history", "status", "--help")
125 cmd.Env = carlosTestEnv(t)
126 cmd.Dir = t.TempDir()
127 out, err := cmd.CombinedOutput()
128 if err != nil || !bytes.Contains(out, []byte("save the signed operation here before delivery")) {
129 t.Fatalf("dispatch: %v\n%s", err, out)
130 }
131}
132
133func TestPlan14HistorySavedFiles(t *testing.T) {
134 dir := t.TempDir()
135 path := filepath.Join(dir, "state.json")
136 op := historyreadback.Operation{Request: json.RawMessage(`{"saved":"request"}`)}
137 if err := historySave(path, op); err != nil {
138 t.Fatal(err)
139 }
140 before, err := os.ReadFile(path)
141 if err != nil {
142 t.Fatal(err)
143 }
144 if err := historySave(path, historyreadback.Operation{Request: json.RawMessage(`{"other":"request"}`)}); err == nil {
145 t.Fatal("replaced saved operation")
146 }
147 var got historyreadback.Operation
148 if err := historyReadFile(path, &got, true); err != nil {
149 t.Fatal(err)
150 }
151 after, _ := os.ReadFile(path)
152 if !bytes.Equal(before, after) {
153 t.Fatal("repair changed state")
154 }
155 for _, raw := range []string{`{"request":{}`, `{"request":{},"unknown":true}`, `{"request":{}} {}`, strings.Repeat("x", 3*release.MaxRootEnvelopeBytes+1)} {
156 bad := filepath.Join(t.TempDir(), "bad.json")
157 if err := os.WriteFile(bad, []byte(raw), 0600); err != nil {
158 t.Fatal(err)
159 }
160 if err := historyReadFile(bad, &got, true); err == nil {
161 t.Fatal("accepted incomplete state")
162 }
163 }
164 if runtime.GOOS == "linux" {
165 alias := filepath.Join(dir, "alias")
166 if err := os.Symlink(path, alias); err != nil {
167 t.Fatal(err)
168 }
169 if err := historyReadFile(alias, &got, true); err == nil {
170 t.Fatal("followed state symlink")
171 }
172 }
173}
174