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 "bufio"
5 "context"
6 "flag"
7 "fmt"
8 "io"
9 "log"
10 "os"
11 "strings"
12 "time"
13
14 "github.com/carlosframework/platform/internal/release"
15)
16
17// The strings below are the copy Paul approved for pointer digests (#253),
18// recorded in docs/superpowers/plans/2026-10-07-instance-settings-1-copy.json
19// and pinned by approved_copy_digests_test.go.
20const (
21 textResignIntro = "Re-sign %s %s with these manifest digests:"
22 textResignRow = " %s %s %s"
23 textResignComputed = "from the bucket now: check it"
24 textResignKept = "already signed"
25 textResignMissing = "no manifest: left without a digest"
26 textResignPrompt = "Sign this pointer? Type yes to continue: "
27 textResignDone = "re-signed %s %s with digests"
28 textResignAborted = "nothing signed"
29 textChannelsHeader = "CHANNEL\tVERSION\tPROMOTED\tTAG\tDIGESTS"
30 textStateYes = "yes"
31 textStateNo = "no"
32 textStateStraggler = "no (written after digests were turned on: an old writer may still be in use)"
33 textStatusOn = "pointer digests: on for %s since %s"
34 textStatusOff = "pointer digests: off for %s"
35 textStatusHeader = "ACCOUNT\tAPP\tCHANNEL\tVERSION\tDIGESTS"
36 textStatusNoneLeft = "every channel's pointer carries digests: CARLOS_REQUIRE_POINTER_DIGEST can be turned on"
37 textTurnedOn = "pointer digests on for %s. Upgraded writers sign digests from now on"
38 textTurnedOff = "pointer digests off for %s. Boxes that have already seen digests keep refusing pointers without them"
39 textAllAccounts = "every account"
40 textStatusError = "pointer digests: unknown for %s, the report could not be read: %s"
41 textUnknownAccount = "no account %s on this console, so nothing was changed for it"
42)
43
44// resignInput is where resign reads its confirmation; tests swap it.
45var resignInput io.Reader = os.Stdin
46
47func digestCell(head, straggler bool) string {
48 switch {
49 case head:
50 return textStateYes
51 case straggler:
52 return textStateStraggler
53 }
54 return textStateNo
55}
56
57// runChannelsResign is `carlos channels resign [--app <a>] <channel>` (also
58// `<app> <channel>`, as the spec writes it). It shows every digest it would
59// sign and signs only on "yes" (or --yes).
60func runChannelsResign(args []string) {
61 fs := flag.NewFlagSet("channels resign", flag.ExitOnError)
62 app := appFlag(fs, "")
63 acctRaw, acctIDRaw, acctOf := accountFlags(fs)
64 yes := fs.Bool("yes", false, "sign without asking")
65 consoleFlag := consoleOption(fs, "console origin to resign through (API mode only; default: the only logged-in one)")
66 fs.Usage = flagUsage(fs, "")
67 fs.Parse(args)
68 usage := "usage: carlos channels resign [--app <a>] [--account <sqid-or-name>] [--yes] <channel>"
69 pos := fs.Args()
70 if len(pos) == 2 && *app == "" {
71 *app, pos = pos[0], pos[1:]
72 }
73 if len(pos) != 1 {
74 log.Fatal(usage)
75 }
76 channel := pos[0]
77 confirm := func(version, digest string, history, hd []string, computed []bool) bool {
78 fmt.Fprintf(stdout, textResignIntro+"\n", *app, channel)
79 row := func(v, d string, c bool) {
80 src := textResignKept
81 switch {
82 case d == "":
83 src = textResignMissing
84 case c:
85 src = textResignComputed
86 }
87 fmt.Fprintf(stdout, textResignRow+"\n", v, d, src)
88 }
89 row(version, digest, computed[0])
90 for i, v := range history {
91 row(v, hd[i], computed[i+1])
92 }
93 if *yes {
94 return true
95 }
96 fmt.Fprint(stdout, textResignPrompt)
97 line, _ := bufio.NewReader(resignInput).ReadString('\n')
98 if strings.TrimSpace(line) != "yes" {
99 fmt.Fprintln(stdout, textResignAborted)
100 return false
101 }
102 return true
103 }
104
105 sess, err := resolveMode(*consoleFlag)
106 if err != nil {
107 log.Fatal(err)
108 }
109 ctx := context.Background()
110 if sess != nil {
111 printUsing(stdout, sess)
112 acct := resolvedAccount(sess, acctRaw, acctIDRaw)
113 *app = resolvedApp(sess, acct, *app, usage)
114 plan, err := sess.resignPlan(ctx, acct, *app, channel)
115 if err != nil {
116 log.Fatal(err)
117 }
118 if !confirm(plan.Version, plan.Digest, plan.History, plan.HistoryDigests, plan.Computed) {
119 return
120 }
121 if err := sess.resignSign(ctx, acct, *app, channel, plan.Digest, plan.HistoryDigests); err != nil {
122 log.Fatal(err)
123 }
124 fmt.Fprintf(stdout, textResignDone+"\n", *app, channel)
125 return
126 }
127 if *app == "" {
128 log.Fatal(usage)
129 }
130 sg := releaseSigner()
131 s, acct := openStore(), acctOf()
132 plan, err := sg.PlanResign(ctx, s, acct, *app, channel)
133 if err != nil {
134 log.Fatal(err)
135 }
136 if !confirm(plan.Version, plan.Digest, plan.History, plan.HistoryDigests, plan.Computed) {
137 return
138 }
139 if err := sg.Resign(ctx, s, plan, promoteActor()); err != nil {
140 log.Fatal(err)
141 }
142 fmt.Fprintf(stdout, textResignDone+"\n", *app, channel)
143}
144
145// runChannelsDigests is `carlos channels digests [on|off] [--account-id <sqid>]...`.
146// Bare, it reports; on and off write or remove the emission marker. Through a
147// console these are platform-operator doors; direct-store they need the root
148// key in CARLOS_RELEASE_KEY and name each account.
149func runChannelsDigests(args []string) {
150 fs := flag.NewFlagSet("channels digests", flag.ExitOnError)
151 var ids multiFlag
152 fs.Var(&ids, "account-id", "an account sqid; repeat for several (direct-store: required)")
153 consoleFlag := consoleOption(fs, "console origin (API mode only; default: the only logged-in one)")
154 fs.Usage = flagUsage(fs, "")
155 usage := "usage: carlos channels digests [on|off] [--account-id <sqid>]..."
156 // The verb comes first (`digests on --account-id bab`, the form the
157 // refusal texts print), and the flag package stops at the first
158 // non-flag, so it is peeled off before the flags are parsed.
159 verb := ""
160 if len(args) > 0 && (args[0] == "on" || args[0] == "off") {
161 verb, args = args[0], args[1:]
162 }
163 fs.Parse(args)
164 if fs.NArg() != 0 {
165 log.Fatal(usage)
166 }
167 ctx := context.Background()
168 sess, err := resolveMode(*consoleFlag)
169 if err != nil {
170 log.Fatal(err)
171 }
172 var rep apiPointerDigests
173 if sess != nil {
174 printUsing(stdout, sess)
175 rep, err = sess.pointerDigests(ctx, verb, ids)
176 } else {
177 if len(ids) == 0 {
178 log.Fatal(usage)
179 }
180 rep, err = localPointerDigests(ctx, openStore(), verb, ids)
181 }
182 if err != nil {
183 log.Fatal(err)
184 }
185 if sess != nil && verb != "" {
186 if err := checkNamedAccounts(rep, ids); err != nil {
187 log.Fatal(err)
188 }
189 }
190 names := strings.Join(ids, ", ")
191 if names == "" {
192 names = textAllAccounts
193 }
194 switch verb {
195 case "on":
196 fmt.Fprintf(stdout, textTurnedOn+"\n", names)
197 case "off":
198 fmt.Fprintf(stdout, textTurnedOff+"\n", names)
199 }
200 // An account whose report could not be read is "unknown", never "off",
201 // and an incomplete report never says the switch can be turned on.
202 failed := false
203 for _, a := range rep.Accounts {
204 switch {
205 case a.Error != "":
206 failed = true
207 fmt.Fprintf(stdout, textStatusError+"\n", a.Account, a.Error)
208 case a.On:
209 fmt.Fprintf(stdout, textStatusOn+"\n", a.Account, a.Since)
210 default:
211 fmt.Fprintf(stdout, textStatusOff+"\n", a.Account)
212 }
213 }
214 if len(rep.Channels) == 0 {
215 if failed {
216 return
217 }
218 fmt.Fprintln(stdout, textStatusNoneLeft)
219 return
220 }
221 w := newTable(stdout)
222 fmt.Fprintln(w, textStatusHeader)
223 for _, c := range rep.Channels {
224 fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\n", c.Account, c.App, c.Channel, c.Version, digestCell(false, c.Straggler))
225 }
226 w.Flush()
227}
228
229// checkNamedAccounts is the guard for an operator who named accounts: the
230// console's system door skips an account id it does not know and still
231// answers 200, which must not read as success.
232func checkNamedAccounts(rep apiPointerDigests, ids []string) error {
233 for _, id := range ids {
234 known := false
235 for _, a := range rep.Accounts {
236 known = known || a.Account == id
237 }
238 if !known {
239 return fmt.Errorf(textUnknownAccount, id)
240 }
241 }
242 return nil
243}
244
245// localPointerDigests is the direct-store form: the root key writes or
246// removes each named account's marker, and the bucket is listed for the
247// report.
248func localPointerDigests(ctx context.Context, s release.Store, verb string, ids []string) (apiPointerDigests, error) {
249 pub := publicKey()
250 var rep apiPointerDigests
251 for _, acct := range ids {
252 var err error
253 switch verb {
254 case "on":
255 err = release.WriteDigestMarker(ctx, s, releaseSigner(), acct, promoteActor())
256 case "off":
257 err = release.RemoveDigestMarker(ctx, s, acct)
258 }
259 if err != nil {
260 return rep, err
261 }
262 row := apiDigestAccount{Account: acct}
263 // A marker or listing that cannot be read marks this account's row,
264 // as the console's report does; it does not hide the other accounts.
265 marker, err := release.ReadDigestMarker(ctx, s, pub, acct)
266 if err != nil {
267 row.Error = err.Error()
268 rep.Accounts = append(rep.Accounts, row)
269 continue
270 }
271 if marker != nil {
272 row.On, row.Since = true, marker.At.UTC().Format(time.RFC3339)
273 }
274 dirs, err := s.ListDirs(ctx, "apps/"+acct+"/")
275 if err != nil {
276 row.Error = err.Error()
277 rep.Accounts = append(rep.Accounts, row)
278 continue
279 }
280 for _, d := range dirs {
281 app := strings.TrimSuffix(strings.TrimPrefix(d, "apps/"+acct+"/"), "/")
282 if !release.ValidApp(app) {
283 continue
284 }
285 states, _, err := release.DigestReport(ctx, s, pub, acct, app)
286 if err != nil {
287 row.Error = err.Error()
288 break
289 }
290 for _, st := range states {
291 switch {
292 case st.Err != "":
293 // A channel whose pointer could not be read leaves the
294 // account unknown, as the console's report does.
295 if row.Error == "" {
296 row.Error = st.Err
297 }
298 case !st.HeadDigest:
299 rep.Channels = append(rep.Channels, apiDigestChannel{Account: acct, App: app, Channel: st.Channel, Version: st.Version, Straggler: st.Straggler})
300 }
301 }
302 }
303 rep.Accounts = append(rep.Accounts, row)
304 }
305 return rep, nil
306}
307