carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package runs
2
3import (
4 "context"
5 "crypto/sha256"
6 "encoding/hex"
7 "errors"
8 "fmt"
9 "io"
10 "os"
11 "path/filepath"
12
13 "github.com/carlosframework/platform/internal/instancefs"
14 "github.com/carlosframework/platform/internal/release"
15)
16
17// StageFiles makes <instanceDir>/.carlos-run/<id>/ and downloads the run's
18// files into it (run spec §5.2 step 3). Both directories are created
19// beneath the instance's own directory, 0700, without following a symlink.
20// A directory that is already there is accepted as it is: the tenant shares
21// the agent's uid so its mode and owner prove nothing, the run id is
22// console-minted and unguessable, and a collision only denies the tenant's
23// own run. Anything else at either name (a symlink, a file, a FIFO) is a
24// refusal. Each file is created O_EXCL 0600 through instancefs, so a name
25// the tenant planted first is a refusal, never a write through it, and is
26// streamed from the store through a limit of its recorded size plus one
27// byte, so the read is bounded however large the object is; its length and
28// sha256 are checked against request.json as it is written, and its name
29// against PlainName. The directory exists even when there are no files: it
30// is CARLOS_RUN_DIR.
31//
32// When a file fails after the run directory is in place, the directory is
33// removed again (no symlink followed), so a refused staging leaves nothing
34// half-written; a failure to remove it is joined onto the error returned.
35func StageFiles(ctx context.Context, s release.Store, instanceDir string, req Request) (string, error) {
36 if !ValidID(req.ID) {
37 return "", fmt.Errorf("runs: %q is not a run id", req.ID)
38 }
39 root := filepath.Join(instanceDir, DirName)
40 if _, err := instancefs.EnsureDirBeneath(instanceDir, DirName, 0o700); err != nil {
41 return "", err
42 }
43 if _, err := instancefs.EnsureDirBeneath(root, req.ID, 0o700); err != nil {
44 return "", err
45 }
46 dir := filepath.Join(root, req.ID)
47 if err := stageEach(ctx, s, dir, req); err != nil {
48 if rerr := instancefs.RemoveAllBeneath(root, req.ID); rerr != nil {
49 err = errors.Join(err, rerr)
50 }
51 return "", err
52 }
53 return dir, nil
54}
55
56// stageOne streams one input into dir/f.Name: created first, so a planted
57// name is refused before anything is fetched, then copied through a limit
58// of f.Size+1 into the file and a hasher at once. A length or hash that is
59// not the recorded one is an error; the caller removes the run directory.
60func stageOne(ctx context.Context, s release.Store, dir string, req Request, f File) error {
61 out, err := instancefs.OpenBeneath(dir, f.Name, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0o600)
62 if err != nil {
63 return err
64 }
65 defer out.Close()
66 rc, _, err := release.OpenObject(ctx, s, InputKey(req.Account, req.App, req.ID, f.Name))
67 if err != nil {
68 return fmt.Errorf("runs: read %s: %w", f.Name, err)
69 }
70 defer rc.Close()
71 h := sha256.New()
72 n, err := io.Copy(io.MultiWriter(out, h), io.LimitReader(rc, f.Size+1))
73 if err != nil {
74 return fmt.Errorf("runs: read %s: %w", f.Name, err)
75 }
76 if n != f.Size || hex.EncodeToString(h.Sum(nil)) != f.SHA256 {
77 return fmt.Errorf("runs: %s does not match its size and sha256", f.Name)
78 }
79 return out.Close()
80}
81
82func stageEach(ctx context.Context, s release.Store, dir string, req Request) error {
83 for _, f := range req.Files {
84 if !PlainName(f.Name) {
85 return fmt.Errorf("runs: %q is not a plain file name", f.Name)
86 }
87 if err := stageOne(ctx, s, dir, req, f); err != nil {
88 return err
89 }
90 }
91 return nil
92}
93
94// RemoveRunDir deletes one run's directory, following no symlink. A tenant
95// can make a tree uncleanable (by depth or mode): the caller logs the error
96// and carries on, it never fails a run.
97func RemoveRunDir(instanceDir, id string) error {
98 if !ValidID(id) {
99 return fmt.Errorf("runs: %q is not a run id", id)
100 }
101 return instancefs.RemoveAllBeneath(filepath.Join(instanceDir, DirName), id)
102}
103
104// SweepRunDirs deletes every instance's .carlos-run directory (run spec
105// §5.8, the box sweep). It runs at agent start, before any run is picked:
106// the edge's stop took every cmd unit with it (BindsTo=carlos-edge.service),
107// so no run on this box is open. One instance it cannot clean (a tenant can
108// make a tree uncleanable) does not stop it: it moves on to the next and
109// returns every failure joined, for the caller to log.
110func SweepRunDirs(dataDir string) error {
111 base := filepath.Join(dataDir, "instances")
112 ents, err := os.ReadDir(base)
113 if errors.Is(err, os.ErrNotExist) {
114 return nil
115 }
116 if err != nil {
117 return err
118 }
119 var errs []error
120 for _, e := range ents {
121 if !e.IsDir() {
122 continue
123 }
124 if err := instancefs.RemoveAllBeneath(filepath.Join(base, e.Name()), DirName); err != nil {
125 errs = append(errs, err)
126 }
127 }
128 return errors.Join(errs...)
129}
130