carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package instancectl
2
3import (
4 "crypto/sha256"
5 "encoding/hex"
6 "errors"
7 "fmt"
8 "io"
9 "net/http"
10 "os"
11 "path/filepath"
12 "strings"
13
14 "github.com/carlosframework/platform/internal/instancefs"
15 "github.com/carlosframework/platform/internal/provision"
16 "github.com/carlosframework/platform/internal/registry"
17 "github.com/carlosframework/platform/internal/release"
18)
19
20// StagedExe is a nested generation's executable as the agent names it: the
21// staged build of the attempt, its version and the sha256 the attempt
22// verified (zero-downtime spec §7.5 step 3).
23type StagedExe struct {
24 Path string
25 Version string
26 Digest string
27}
28
29// ErrStagedExe is root refusing to mount a staged executable it cannot
30// vouch for. The agent abandons the swap; nothing is mounted.
31var ErrStagedExe = errors.New("instancectl: staged executable refused")
32
33// validateStagedExe is root's check before it mounts a staged build into a
34// tenant's namespace. Root mounts only what is provably that host's own
35// app's staged build:
36// - The path must be exactly release.VersionPath for the host's account
37// and app, as its registry row records them, at the attempt's version.
38// Another app's build, another account's, or any other path is refused.
39// - The file is opened beneath the version store with every component
40// resolved without a symlink (instancefs.OpenBeneath).
41// - Its sha256 must be the attempt's digest.
42//
43// The version store is written by root alone, so what is checked here is
44// what the bind will find.
45func (s *Server) validateStagedExe(host string, x StagedExe) error {
46 if s.HostApp == nil || s.OptDir == "" {
47 return fmt.Errorf("%w: this service has no registry view", ErrStagedExe)
48 }
49 if !release.ValidVersion(x.Version) || len(x.Digest) != 2*sha256.Size {
50 return fmt.Errorf("%w: version %q, digest %q", ErrStagedExe, x.Version, x.Digest)
51 }
52 acct, app, err := s.HostApp(host)
53 if err != nil || app == "" {
54 return fmt.Errorf("%w: %s has no app on this box (%v)", ErrStagedExe, host, err)
55 }
56 want := release.VersionPath(s.OptDir, release.RouteAccount(registry.Route{Account: acct}), x.Version, app)
57 if x.Path != want {
58 return fmt.Errorf("%w: %q is not %s's staged %s (%q)", ErrStagedExe, x.Path, host, x.Version, want)
59 }
60 versions := filepath.Join(s.OptDir, "versions")
61 rel, err := filepath.Rel(versions, want)
62 if err != nil {
63 return fmt.Errorf("%w: %v", ErrStagedExe, err)
64 }
65 f, err := instancefs.OpenBeneath(versions, rel, os.O_RDONLY, 0)
66 if err != nil {
67 return fmt.Errorf("%w: %v", ErrStagedExe, err)
68 }
69 defer f.Close()
70 h := sha256.New()
71 if _, err := io.Copy(h, f); err != nil {
72 return fmt.Errorf("%w: %v", ErrStagedExe, err)
73 }
74 if got := hex.EncodeToString(h.Sum(nil)); got != strings.ToLower(x.Digest) {
75 return fmt.Errorf("%w: %s has digest %s, the attempt verified %s", ErrStagedExe, want, got, x.Digest)
76 }
77 return nil
78}
79
80// ValidateStagedExe applies the same host-app check to root claim launches.
81func (s *Server) ValidateStagedExe(host string, x StagedExe) error {
82 return s.validateStagedExe(host, x)
83}
84
85// refuseStagedExe answers 403 for a start whose staged executable root
86// cannot vouch for, and reports whether it did. A request that names none
87// is not refused here. It is the one place every path that binds a staged
88// build validates (a generation's start; the original run, seed and
89// sidecar units), before anything is laid out.
90func (s *Server) refuseStagedExe(w http.ResponseWriter, req request, unit string) bool {
91 if req.exe.Path == "" {
92 return false
93 }
94 if err := s.validateStagedExe(req.host, req.exe); err != nil {
95 s.logf("instance-control: %s: not starting %s: %v", req.host, unit, err)
96 http.Error(w, "not starting "+unit+": "+err.Error(), http.StatusForbidden)
97 return true
98 }
99 return false
100}
101
102// ensureGenExeAt lays out generation n's executable mount. It points at a
103// validated staged build when x names one, and at the live link otherwise
104// (plan 2's ensureGenExe).
105func (s *Server) ensureGenExeAt(host string, n uint64, x StagedExe) error {
106 if x.Path == "" {
107 return s.ensureGenExe(host, n)
108 }
109 return ensureExe(s.ExeRoot, GenKey(host, n), x.Path, host)
110}
111
112// ensureHostExeAt lays out the host's own executable mount (the one the
113// original templates bind: <ExeRoot>/<id>/app). It points at a validated
114// staged build when x names one (a legacy sidecar's start, or a run or seed
115// unit running the build its cold wake recorded), and at the live link
116// otherwise (ensureHostExe). A run's command has a mount of its own
117// (ensureCmdExeAt), so none of these starts can repoint the source a
118// command is about to bind.
119func (s *Server) ensureHostExeAt(host string, x StagedExe) error {
120 if x.Path == "" {
121 return s.ensureHostExe(host)
122 }
123 return ensureExe(s.ExeRoot, provision.InstanceID(host), x.Path, host)
124}
125
126// ensureCmdExeAt lays out a run's executable mount, apart from the host's
127// (merge ruling M4): <ExeRoot>/<id>/cmd-app and cmd-live/<host>, which only
128// the cmd template binds. A command and a sidecar starting on different
129// builds around a swap therefore never overwrite the source the other is
130// about to bind. It points at a validated staged build when x names one,
131// and at the live link otherwise.
132func (s *Server) ensureCmdExeAt(host string, x StagedExe) error {
133 target := x.Path
134 if target == "" {
135 target = filepath.Join(LiveDir, host)
136 }
137 return ensureExeAs(s.ExeRoot, provision.InstanceID(host), target, host, CmdExeApp, CmdExeLive)
138}
139
140// removeCmdExe removes a run's executable mount sources, once its unit is
141// stopped: they name a build the instance may no longer serve. Best-effort,
142// as removeGenFiles is: a leftover is two links in /run, rewritten by the
143// next run's start.
144func (s *Server) removeCmdExe(iid string) {
145 if s.ExeRoot == "" || iid == "" || strings.ContainsAny(iid, "/\x00") || iid == "." || iid == ".." {
146 return
147 }
148 dir := filepath.Join(s.ExeRoot, iid)
149 _ = os.Remove(filepath.Join(dir, CmdExeApp))
150 _ = os.RemoveAll(filepath.Join(dir, CmdExeLive))
151}
152