carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1//go:build linux && rootrunner
2
3package host
4
5import (
6 "context"
7 "crypto/ed25519"
8 "crypto/sha256"
9 "encoding/hex"
10 "net"
11 "net/http"
12 "os"
13 "path/filepath"
14 "strings"
15 "testing"
16 "time"
17
18 "github.com/carlosframework/platform/internal/hostbox"
19 "github.com/carlosframework/platform/internal/hostclaim"
20 execlease "github.com/carlosframework/platform/internal/hostexec"
21 "github.com/carlosframework/platform/internal/instancectl"
22 "github.com/carlosframework/platform/internal/provision"
23 "github.com/carlosframework/platform/internal/release"
24 "github.com/carlosframework/platform/internal/rootstate"
25)
26
27func plan14BoundSibling(t *testing.T, body []byte, ctl func(context.Context, ...string) (string, error), uid, gid int, lock *release.ReleaseExclusion) func() {
28 t.Helper()
29 f, disk, rec := generationLaunchFixtureHost(t, "sibling.example")
30 host := rec.Host
31 for _, dir := range []string{provision.InstanceDir("/data", host), provision.InstanceRunDir(host)} {
32 if err := os.MkdirAll(dir, 0755); err != nil {
33 t.Fatal(err)
34 }
35 if err := os.Chown(dir, uid, gid); err != nil {
36 t.Fatal(err)
37 }
38 }
39 template, err := unitsFS.ReadFile("units/carlos-instance@.service")
40 if err != nil {
41 t.Fatal(err)
42 }
43 // Match the named test's dependency and output adapters.
44 lines := plan14RunnerUnitLines(template)
45 path := "/run/systemd/system/carlos-instance@.service"
46 if err := os.WriteFile(path, []byte(lines), 0644); err != nil {
47 t.Fatal(err)
48 }
49 t.Cleanup(func() {
50 ctl(context.Background(), "stop", instancectl.UnitName(instancectl.RoleRun, host))
51 ctl(context.Background(), "stop", instancectl.SliceName(host))
52 os.Remove(path)
53 os.RemoveAll(filepath.Join("/run/systemd/system", instancectl.UnitName(instancectl.RoleRun, host)+".d"))
54 os.RemoveAll(filepath.Join("/run/systemd/system", instancectl.SliceName(host)+".d"))
55 ctl(context.Background(), "daemon-reload")
56 })
57 if _, err := ctl(t.Context(), "daemon-reload"); err != nil {
58 t.Fatal(err)
59 }
60 store := release.NewDirStore(t.TempDir())
61 rec.Schema = 1
62 plan14PutRecord(t, store, rec)
63 pub, priv, err := ed25519.GenerateKey(nil)
64 if err != nil {
65 t.Fatal(err)
66 }
67 if err := release.StageArtifact("/opt/carlos", rec.Account, "sibling", rec.App, body); err != nil {
68 t.Fatal(err)
69 }
70 m := release.Manifest{App: rec.App, Version: "sibling", Kind: "binary", Artifacts: map[string]release.Artifact{release.AdoptPlatform(): {}}}
71 if err := release.Ship(t.Context(), store, rec.Account, m, map[string][]byte{release.AdoptPlatform(): body}); err != nil {
72 t.Fatal(err)
73 }
74 if err := release.Promote(t.Context(), store, priv, rec.Account, rec.App, "canary/console", "sibling"); err != nil {
75 t.Fatal(err)
76 }
77 exe := release.VersionPath("/opt/carlos", rec.Account, "sibling", rec.App)
78 file, err := rootstate.OpenTrustedFile(exe)
79 if err != nil {
80 t.Fatal(err)
81 }
82 id, err := rootstate.Identity(file)
83 file.Close()
84 if err != nil {
85 t.Fatal(err)
86 }
87 sum := sha256.Sum256(body)
88 f.req.Executable, f.req.Spec.Executable, f.req.Spec.Digest = exe, exe, hex.EncodeToString(sum[:])
89 f.req.Reservation.Executable = hostbox.LaunchFile{Device: id.Device, Inode: id.Inode}
90 socket := filepath.Join(provision.InstanceRunDir(host), "app.sock")
91 f.req.Spec.Args = []string{"--socket", socket, "--db", provision.NestedDBPath("/data", host)}
92 f.req.Spec.Env = []string{"CARLOS_ADMIN_TOKEN=test", "PLAN14_TENANT_SOCKET=" + socket}
93 inputs, err := f.req.Spec.Hash()
94 if err != nil {
95 t.Fatal(err)
96 }
97 f.req.Reservation.Inputs = inputs
98 f.claims.set(func(c *hostclaim.Claim) { c.Holder.WakeRetry.Attempt.LaunchInputs = inputs })
99 f.root.Clock, f.root.DataRoot = execlease.SystemClock{}, "/data"
100 f.root.ReadyBudget = 10 * time.Second
101 f.root.Units = instancectl.ClaimUnits{Systemctl: ctl, CgroupRoot: "/sys/fs/cgroup", ExeRoot: "/run/carlos-control/exe", UnitRoot: "/run/systemd/system", DataRoot: "/data"}
102 f.root.Kernel = linuxClaimLaunchKernel{OptRoot: "/opt/carlos", CgroupRoot: "/sys/fs/cgroup", ProcRoot: "/proc"}
103 f.root.generations = &rootGenerationSource{store: store, optDir: "/opt/carlos", pub: func() (ed25519.PublicKey, error) { return pub, nil }, open: func() (rootstate.Store, error) { return disk, nil }, openFile: rootstate.OpenTrustedFile}
104 var started hostbox.RoleLaunchResponse
105 if err := lock.With(t.Context(), func(ctx context.Context) error {
106 var err error
107 started, err = f.root.Serve(ctx, f.peer, f.req)
108 return err
109 }); err != nil {
110 t.Fatal("sibling launch", err)
111 }
112 pin := readGenerationLaunch(t, disk)
113 t.Cleanup(func() {
114 q := f.req
115 q.Op = hostbox.RoleLaunchStop
116 q.Spec, q.Executable, q.MemoryMax = nil, "", 0
117 if _, err := f.root.Serve(context.Background(), f.peer, q); err != nil {
118 t.Error("sibling stop", err)
119 }
120 })
121 return func() {
122 t.Helper()
123 if got := readGenerationLaunch(t, disk); got != pin {
124 t.Fatal("sibling pin changed")
125 }
126 g := started.Grant
127 if err := f.root.Units.Same(t.Context(), instancectl.ClaimUnitStarted{Unit: g.Unit, InvocationID: g.UnitInvocationID, MainPID: g.PID, ControlGroup: g.Group.RelativePath}); err != nil {
128 t.Fatal("sibling invocation changed", err)
129 }
130 transport := &http.Transport{DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) {
131 return (&net.Dialer{}).DialContext(ctx, "unix", socket)
132 }}
133 defer transport.CloseIdleConnections()
134 resp, err := (&http.Client{Transport: transport}).Get("http://sibling/")
135 if err != nil {
136 t.Fatal("sibling stopped serving", err)
137 }
138 resp.Body.Close()
139 if resp.StatusCode != 200 {
140 t.Fatal(resp.Status)
141 }
142 }
143}
144
145func plan14RunnerUnitLines(template []byte) string {
146 var lines []string
147 for _, line := range strings.Split(string(template), "\n") {
148 key, _, _ := strings.Cut(line, "=")
149 switch key {
150 case "BindsTo", "Requires", "After":
151 continue
152 case "StandardOutput":
153 line = "StandardOutput=journal"
154 }
155 lines = append(lines, line)
156 }
157 return strings.Join(lines, "\n")
158}
159