carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1// Package vet checks a shipped release against the platform contract before
2// anyone promotes it: the manifest must exist and be valid, every artifact's
3// stored bytes must match their manifested sha256+size, and — for binary
4// releases, on request — the binary must actually boot.
5//
6// The app contract vet's boot check enforces (the same contract the
7// activator's exec backend and `carlos agent` rely on): a binary release
8// must accept a `-socket <path>` flag, listen for HTTP on that unix socket,
9// and answer GET /healthz with 200 within 10 seconds of exec.
10package vet
11
12import (
13 "bytes"
14 "context"
15 "crypto/ed25519"
16 "crypto/sha256"
17 "encoding/hex"
18 "flag"
19 "fmt"
20 "io"
21 "log"
22 "net"
23 "net/http"
24 "os"
25 "os/exec"
26 "path/filepath"
27 "runtime"
28 "sort"
29 "strings"
30 "time"
31
32 "github.com/carlosframework/platform/internal/cliflag"
33 "github.com/carlosframework/platform/internal/release"
34)
35
36// bootTimeout is how long a booted binary has to answer /healthz. A var so
37// the failure-path test doesn't sit through the full production window.
38var bootTimeout = 10 * time.Second
39
40// Check vets app@version in s and returns the list of problems found (empty
41// = clean). pub is the deployment's release public key — unused today
42// because release manifests are content-addressed rather than signed (only
43// channel pointers are), but part of the signature so pointer-level checks
44// can land without touching every caller. With boot set and kind "binary",
45// the platform-matching artifact is extracted to a temp dir, exec'd with
46// `-socket <tmp>`, and polled for a 200 on GET /healthz for up to 10s; when
47// the environment can't listen on unix sockets (sandboxes), the boot check
48// is skipped with a log line rather than reported as a failure.
49func Check(ctx context.Context, s release.Store, pub ed25519.PublicKey, acct, app, version string, boot bool) []string {
50 _ = pub // reserved: manifests are content-addressed, nothing to verify yet
51 m, err := release.GetManifest(ctx, s, acct, app, version)
52 if err != nil {
53 return []string{fmt.Sprintf("manifest: %v", err)}
54 }
55
56 // Deterministic problem order: artifacts by map key.
57 keys := make([]string, 0, len(m.Artifacts))
58 for k := range m.Artifacts {
59 keys = append(keys, k)
60 }
61 sort.Strings(keys)
62
63 var problems []string
64 var bootBytes []byte // verified bytes of the artifact this platform runs
65 platform := runtime.GOOS + "-" + runtime.GOARCH
66 for _, k := range keys {
67 a := m.Artifacts[k]
68 b, _, err := release.GetQualified(ctx, s, release.ArtifactKey(acct, app, version, a.File))
69 if err != nil {
70 problems = append(problems, fmt.Sprintf("artifact %s (%s): %v", k, a.File, err))
71 continue
72 }
73 sum := sha256.Sum256(b)
74 if got := hex.EncodeToString(sum[:]); got != a.SHA256 {
75 problems = append(problems, fmt.Sprintf("artifact %s (%s): sha256 mismatch (manifest %s, stored %s)", k, a.File, a.SHA256, got))
76 continue
77 }
78 if int64(len(b)) != a.Size {
79 problems = append(problems, fmt.Sprintf("artifact %s (%s): size mismatch (manifest %d, stored %d)", k, a.File, a.Size, len(b)))
80 continue
81 }
82 if k == platform || len(m.Artifacts) == 1 {
83 bootBytes = b
84 }
85 }
86
87 if boot && m.Kind == "binary" && len(problems) == 0 {
88 switch {
89 case !canListenUnix():
90 log.Printf("vet: unix sockets unavailable in this environment — skipping boot check")
91 case bootBytes == nil:
92 problems = append(problems, fmt.Sprintf("boot: no artifact for this platform (%s)", platform))
93 default:
94 if err := bootHealthz(ctx, bootBytes); err != nil {
95 problems = append(problems, fmt.Sprintf("boot: %v", err))
96 }
97 }
98 }
99 return problems
100}
101
102// canListenUnix probes whether this environment permits unix-socket
103// listeners (sandboxes commonly block them with EPERM).
104func canListenUnix() bool {
105 dir, err := os.MkdirTemp("", "carlos-vet-probe-")
106 if err != nil {
107 return false
108 }
109 defer os.RemoveAll(dir)
110 ln, err := net.Listen("unix", filepath.Join(dir, "probe.sock"))
111 if err != nil {
112 return false
113 }
114 ln.Close()
115 return true
116}
117
118// bootHealthz writes bin to a temp dir, execs it with `-socket <tmp>` (the
119// app contract), and polls GET /healthz over that socket until 200 or
120// bootTimeout. The child is killed on the way out no matter what.
121func bootHealthz(ctx context.Context, bin []byte) error {
122 dir, err := os.MkdirTemp("", "carlos-vet-")
123 if err != nil {
124 return err
125 }
126 defer os.RemoveAll(dir)
127 exe := filepath.Join(dir, "app")
128 if err := os.WriteFile(exe, bin, 0o755); err != nil {
129 return err
130 }
131 sock := filepath.Join(dir, "app.sock")
132
133 ctx, cancel := context.WithTimeout(ctx, bootTimeout+2*time.Second)
134 defer cancel()
135 cmd := exec.CommandContext(ctx, exe, "-socket", sock)
136 var out bytes.Buffer
137 cmd.Stdout, cmd.Stderr = &out, &out
138 if err := cmd.Start(); err != nil {
139 return fmt.Errorf("exec: %v", err)
140 }
141 defer func() {
142 _ = cmd.Process.Kill()
143 _ = cmd.Wait()
144 }()
145
146 client := &http.Client{
147 Timeout: 2 * time.Second,
148 Transport: &http.Transport{
149 DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) {
150 return (&net.Dialer{}).DialContext(ctx, "unix", sock)
151 },
152 },
153 }
154 deadline := time.Now().Add(bootTimeout)
155 var last error
156 for time.Now().Before(deadline) {
157 resp, err := client.Get("http://vet/healthz")
158 if err == nil {
159 _, _ = io.Copy(io.Discard, resp.Body)
160 resp.Body.Close()
161 if resp.StatusCode == http.StatusOK {
162 return nil
163 }
164 last = fmt.Errorf("/healthz answered %d", resp.StatusCode)
165 } else {
166 last = err
167 }
168 select {
169 case <-ctx.Done():
170 return ctx.Err()
171 case <-time.After(150 * time.Millisecond):
172 }
173 }
174 msg := strings.TrimSpace(out.String())
175 if msg != "" {
176 return fmt.Errorf("no 200 from /healthz within %s: %v (app output: %s)", bootTimeout, last, msg)
177 }
178 return fmt.Errorf("no 200 from /healthz within %s: %v", bootTimeout, last)
179}
180
181const vetUsage = "usage: carlos vet --app <a> --version <v> [--account <sqid>] [--boot]"
182
183// Run is the `carlos vet` verb: vet a shipped release against the store the
184// environment names.
185func Run(args []string) {
186 fs := flag.NewFlagSet("vet", flag.ExitOnError)
187 app := cliflag.String(fs, "app", "", "app name")
188 // Sqid only, deliberately: vet has no -registry flag to resolve a name
189 // against, and guessing an account would vet the wrong app's bytes.
190 acct := fs.String("account", release.OpsAccount, "account sqid the app lives under")
191 ver := cliflag.String(fs, "version", "", "shipped version to vet")
192 boot := fs.Bool("boot", false, "also boot the binary with -socket and poll /healthz (binary kind)")
193 fs.Usage = cliflag.Usage(fs, vetUsage)
194 _ = fs.Parse(args)
195 if *app == "" || *ver == "" {
196 log.Fatal(vetUsage)
197 }
198 if !release.ValidAccount(*acct) {
199 log.Fatalf("vet: invalid account sqid %q", *acct)
200 }
201 s := openStore()
202 var pub ed25519.PublicKey
203 if b := os.Getenv("CARLOS_RELEASE_PUBKEY"); b != "" {
204 p, err := release.ParsePublicKey(b, "CARLOS_RELEASE_PUBKEY")
205 if err != nil {
206 log.Fatal(err)
207 }
208 pub = p
209 }
210 problems := Check(context.Background(), s, pub, *acct, *app, *ver, *boot)
211 if len(problems) == 0 {
212 fmt.Printf("vet: %s %s OK\n", *app, *ver)
213 return
214 }
215 for _, p := range problems {
216 fmt.Fprintf(os.Stderr, "vet: %s %s: %s\n", *app, *ver, p)
217 }
218 os.Exit(1)
219}
220
221// openStore duplicates the CLI's ten-line env->store helper (an internal
222// package can't import package main): S3 when CARLOS_DEPLOYMENT_BUCKET is
223// set, else a directory store at CARLOS_DEPLOYMENT_DIR.
224func openStore() release.Store {
225 if b := os.Getenv("CARLOS_DEPLOYMENT_BUCKET"); b != "" {
226 s, err := release.NewDeploymentS3Store(context.Background(), b)
227 if err != nil {
228 log.Fatal(err)
229 }
230 return s
231 }
232 if d := os.Getenv("CARLOS_DEPLOYMENT_DIR"); d != "" {
233 ds := release.NewDirStore(d)
234 ds.EnableChangeIndex()
235 return ds
236 }
237 log.Fatal("set CARLOS_DEPLOYMENT_BUCKET (S3) or CARLOS_DEPLOYMENT_DIR (local)")
238 return nil
239}
240