carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1// Package instanceexec holds the three programs that run INSIDE an isolated
2// instance's namespace, as carlos, launched by the template units of the
3// isolated-exec design: the trampoline (instance-exec, §6.2), the advisory
4// exit-record writer (instance-exit, §6.5) and the readiness probe
5// (instance-probe, §8.5). None of them is for operators; each is an
6// ExecStart= or ExecStopPost= of a unit.
7package instanceexec
8
9import (
10 "encoding/json"
11 "errors"
12 "flag"
13 "fmt"
14 "log"
15 "os"
16 "path/filepath"
17 "strings"
18
19 "github.com/carlosframework/platform/internal/cliflag"
20 "github.com/carlosframework/platform/internal/instancectl"
21 "github.com/carlosframework/platform/internal/instancefs"
22 "github.com/carlosframework/platform/internal/provision"
23)
24
25// LiveDir is where the adoption pass keeps each host's live symlink; the
26// trampoline execs nothing that is not directly under it.
27const LiveDir = "/opt/carlos/live"
28
29var errSymlinkOrEscape = errors.New("instance-exec: launch file refused")
30
31// dataDir is the agent's own resolver (host/agent.go), repeated here because
32// this program runs in a namespace where only the instance's directory is
33// bound, and it must agree with the agent on where that directory is.
34func dataDir() string {
35 if d := os.Getenv("CARLOS_DATA_DIR"); d != "" {
36 return d
37 }
38 return "/data"
39}
40
41// RunExec is `carlos instance-exec <role> <iid>`.
42func RunExec(args []string) {
43 fs := flag.NewFlagSet("instance-exec", flag.ExitOnError)
44 fs.Usage = cliflag.Usage(fs, "usage: carlos instance-exec <run|seed|sidecar|cmd> <instance-id>")
45 claimSnapshot := fs.String("claim-reservation", "", "root launch reservation")
46 _ = fs.Parse(args)
47 if fs.NArg() != 2 {
48 fs.Usage()
49 os.Exit(2)
50 }
51 role, ok := instancectl.ParseRole(fs.Arg(0))
52 if !ok {
53 log.Fatalf("instance-exec: role %q is not run, seed, sidecar or cmd", fs.Arg(0))
54 }
55 // The tenant is scoped (#178, see ScopeAbstractUnix) and runs under
56 // the trampoline as its init (#126, see init_linux.go).
57 launch := scopedInit
58 if *claimSnapshot != "" {
59 if *claimSnapshot != instancectl.ClaimExecSnapshot || role != instancectl.RoleRun || strings.Contains(fs.Arg(1), ":") {
60 log.Fatal("instance-exec: claim boundary refused")
61 }
62 launch = func(argv0 string, argv, env []string) error { return scopedClaimInit(argv0, argv, env, *claimSnapshot) }
63 }
64 if err := execLaunch(dataDir(), role, fs.Arg(1), launch); err != nil {
65 log.Fatalf("instance-exec: %v", err)
66 }
67}
68
69// execLaunch reads launch.<role>.json from the instance directory through
70// instancefs (never through a symlink), validates it, and launches. It
71// returns only on refusal; a successful launch exits with the tenant.
72//
73// The validation is defence in depth: the agent wrote the file, but the
74// directory is the instance's own, so the instance can rewrite it. What
75// the checks guarantee is that whatever is executed is directly under
76// LiveDir (the platform's live links, root-written) and named for the host
77// in the file. The file's host must also match the instance the unit was
78// started for, so a rewritten file can execute only what the tenant could
79// execute anyway: its own binary with an argv and env it chose.
80func execLaunch(dataDir string, role instancectl.Role, inst string, execFn func(argv0 string, argv, env []string) error) error {
81 iid, launchName, err := launchTarget(role, inst)
82 if err != nil {
83 return err
84 }
85 dir := filepath.Join(dataDir, "instances", iid)
86 raw, err := instancefs.ReadBeneath(dir, launchName)
87 if err != nil {
88 if errors.Is(err, instancefs.ErrSymlink) || errors.Is(err, instancefs.ErrEscapes) {
89 return fmt.Errorf("%w: %w", errSymlinkOrEscape, err)
90 }
91 return err
92 }
93 var l instancectl.Launch
94 if err := json.Unmarshal(raw, &l); err != nil {
95 return fmt.Errorf("launch file: %v", err)
96 }
97 if l.Schema != instancectl.LaunchSchema {
98 return fmt.Errorf("launch file schema %d is not %d", l.Schema, instancectl.LaunchSchema)
99 }
100 if !provision.ValidHostShape(l.Host) {
101 return fmt.Errorf("launch file host %q is not a host", l.Host)
102 }
103 if provision.InstanceID(l.Host) != iid {
104 return fmt.Errorf("launch file host %q is not instance %s", l.Host, iid)
105 }
106 if len(l.Argv) == 0 {
107 return errors.New("launch file has no argv")
108 }
109 argv0 := l.Argv[0]
110 if !filepath.IsAbs(argv0) || filepath.Clean(argv0) != argv0 || filepath.Dir(argv0) != LiveDir {
111 return fmt.Errorf("argv[0] %q is not directly under %s", argv0, LiveDir)
112 }
113 if filepath.Base(argv0) != l.Host {
114 return fmt.Errorf("argv[0] %q is not the live link of host %s", argv0, l.Host)
115 }
116 for _, kv := range l.Env {
117 if k, _, ok := strings.Cut(kv, "="); !ok || k == "" {
118 return fmt.Errorf("launch file env entry %q is not KEY=value", kv)
119 }
120 }
121 return execFn(argv0, l.Argv, l.Env)
122}
123
124// launchTarget is the trampoline's split of the unit's instance string
125// (zero-downtime spec §7.6): "<iid>" for the three original templates,
126// "<iid>:<n>" for the generation template, whose launch file is keyed by
127// generation. Only a run unit has generations.
128func launchTarget(role instancectl.Role, inst string) (iid, name string, err error) {
129 if !strings.Contains(inst, ":") {
130 return inst, instancectl.LaunchFileName(role), nil
131 }
132 iid, n, err := splitGeneration(role, inst)
133 if err != nil {
134 return "", "", err
135 }
136 return iid, instancectl.GenLaunchFileName(n), nil
137}
138
139// recordTarget is launchTarget for the exit record.
140func recordTarget(role instancectl.Role, inst, invocation string) (iid, name string, err error) {
141 if !strings.Contains(inst, ":") {
142 return inst, instancectl.ExitRecordName(role, invocation), nil
143 }
144 iid, n, err := splitGeneration(role, inst)
145 if err != nil {
146 return "", "", err
147 }
148 return iid, instancectl.GenExitRecordName(n, invocation), nil
149}
150
151func splitGeneration(role instancectl.Role, inst string) (string, uint64, error) {
152 if role != instancectl.RoleRun {
153 return "", 0, fmt.Errorf("instance %q: only a run unit has generations", inst)
154 }
155 iid, n, ok := instancectl.ParseGenInstance(inst)
156 if !ok {
157 return "", 0, fmt.Errorf("instance %q is not <instance-id>:<generation>", inst)
158 }
159 return iid, n, nil
160}
161