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 "encoding/json"
8 "io"
9 "net"
10 "os"
11 "path/filepath"
12 "strings"
13 "testing"
14 "time"
15
16 "github.com/carlosframework/platform/internal/activator"
17 "github.com/carlosframework/platform/internal/hostclaim"
18 "github.com/carlosframework/platform/internal/hostfiles"
19 "github.com/carlosframework/platform/internal/registry"
20)
21
22func TestRootFilesSeedSocketDisconnectExecutor(t *testing.T) {
23 runRootFilesSeedRealOriginPreparation(t, true)
24}
25func TestRootFilesSeedSocketShutdownExecutor(t *testing.T) {
26 runRootFilesSeedRealOriginPreparation(t, true)
27}
28func TestRootFilesSeedSocketCompleteExecutor(t *testing.T) {
29 runRootFilesSeedRealOriginPreparation(t, true)
30}
31
32func rootFilesSeedLifetime(t *testing.T, p *servingPreparation, e *filesSeedExecutor) {
33 blocked := !strings.Contains(t.Name(), "Complete")
34 entered := make(chan struct{})
35 factory := e.assembly.Wake.seedFactory
36 e.assembly.Wake.seedFactory = func(ctx context.Context, op *filesSeedOperation, token *activator.SeedExclusion) (*activator.ClaimRuntime, *ClaimWakeAssembly, error) {
37 runtime, wake, err := factory(ctx, op, token)
38 if err != nil {
39 return runtime, wake, err
40 }
41 prepare := runtime.Prepare
42 runtime.Prepare = func(ctx context.Context, rt registry.Route, o hostclaim.Observed, policy activator.StartPolicy) (activator.ClaimStart, error) {
43 start, err := prepare(ctx, rt, o, policy)
44 if err == nil && blocked {
45 start.Materialize = func(ctx context.Context) error { close(entered); <-ctx.Done(); return ctx.Err() }
46 }
47 return start, err
48 }
49 return runtime, wake, nil
50 }
51 ready := make(chan struct{})
52 close(ready)
53 path := filepath.Join(t.TempDir(), "seed.sock")
54 if err := os.Chmod(filepath.Dir(path), 0700); err != nil {
55 t.Fatal(err)
56 }
57 // Peer and delegated service authentication have their own installed witness.
58 // Here the real executor is attached to the production connection lifetime.
59 open := func() *filesSeedServer {
60 s, err := startFilesSeedSocket(t.Context(), ready, path, func(context.Context, *net.UnixConn) error { return nil }, e.run)
61 if err != nil {
62 t.Fatal(err)
63 }
64 s.finalize = e.close
65 return s
66 }
67 s := open()
68 shutdown := func() {
69 ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
70 defer cancel()
71 if err := s.close(ctx); err != nil {
72 t.Fatal("seed server did not join executor", err)
73 }
74 }
75 defer func() {
76 ctx, cancel := context.WithTimeout(context.Background(), time.Second)
77 defer cancel()
78 s.close(ctx)
79 }()
80 request := func() net.Conn {
81 c, err := net.DialTimeout("unix", path, time.Second)
82 if err != nil {
83 t.Fatal(err)
84 }
85 c.SetDeadline(time.Now().Add(10 * time.Second))
86 if err := json.NewEncoder(c).Encode(filesSeedRequest{Verb: "seed", Host: recoveryHost}); err != nil {
87 t.Fatal(err)
88 }
89 return c
90 }
91 c := request()
92 defer c.Close()
93 if blocked {
94 select {
95 case <-entered:
96 case <-time.After(10 * time.Second):
97 t.Fatal("seed did not reach precommit")
98 }
99 if err := e.run(t.Context(), recoveryHost, io.Discard); err != activator.ErrFenceHeld {
100 t.Fatal("second executor escaped exclusion", err)
101 }
102 if strings.Contains(t.Name(), "Disconnect") {
103 c.Close()
104 deadline := time.Now().Add(5 * time.Second)
105 for {
106 e.mu.Lock()
107 running := e.running
108 e.mu.Unlock()
109 if !running {
110 break
111 }
112 if time.Now().After(deadline) {
113 t.Fatal("executor survived client disconnect")
114 }
115 time.Sleep(10 * time.Millisecond)
116 }
117 }
118 shutdown()
119 e.mu.Lock()
120 retained, running := len(e.pending), e.running
121 e.mu.Unlock()
122 if retained != 0 || running {
123 t.Fatal("executor outlived server", retained, running)
124 }
125 o := p.f.f.read()
126 if o.Claim.Holder.WakeRetry == nil || o.Claim.Holder.WakeRetry.State != "open" || o.Claim.Holder.WakeRetry.AttemptsReserved != 1 {
127 t.Fatal("cancellation lost settlement", o)
128 }
129 files, err := e.source.Open(t.Context(), e.assembly.Claims, e.assembly.Data, p.f.actor)
130 if err != nil {
131 t.Fatal(err)
132 }
133 if _, found, err := hostfiles.Load(t.Context(), files.(*movesFiles).Files, movesFilesPrefix(o.Claim.Holder.Location, o.Claim.Holder.Generation)); err != nil || found {
134 t.Fatal("canceled seed committed", found, err)
135 }
136 blocked = false
137 p.f.f.now = p.f.f.now.Add(time.Minute)
138 s = open()
139 c = request()
140 defer c.Close()
141 }
142 decoder := json.NewDecoder(c)
143 var text strings.Builder
144 for {
145 var reply filesSeedReply
146 if err := decoder.Decode(&reply); err != nil {
147 t.Fatal("seed reply", err, text.String())
148 }
149 text.WriteString(reply.Text)
150 if reply.Code != nil {
151 if *reply.Code != 0 {
152 t.Fatal("seed failed", *reply.Code, text.String())
153 }
154 break
155 }
156 }
157 shutdown()
158 o := p.f.f.read()
159 files, err := e.source.Open(t.Context(), e.assembly.Claims, e.assembly.Data, p.f.actor)
160 if err != nil {
161 t.Fatal(err)
162 }
163 manifest, found, err := hostfiles.Load(t.Context(), files.(*movesFiles).Files, movesFilesPrefix(o.Claim.Holder.Location, o.Claim.Holder.Generation))
164 if err != nil || !found || manifest.Entries["payload"].SHA256 == "" {
165 t.Fatal("seed did not publish", found, err)
166 }
167 if !strings.Contains(text.String(), "seeded.") {
168 t.Fatal(text.String())
169 }
170 if _, err := os.Lstat(path); !os.IsNotExist(err) {
171 t.Fatal("socket outlived server", err)
172 }
173}
174
175func TestFilesSeedExecutorLifetime(t *testing.T) {
176 // The runner's /tmp is a small tmpfs. Keep the real start-floor check,
177 // and place this direct fixture on its disk, as the installed /data tests do.
178 if os.Geteuid() == 0 {
179 t.Setenv("TMPDIR", "/var/tmp")
180 }
181 for _, mode := range []string{"Complete", "Disconnect", "Shutdown"} {
182 t.Run(mode, func(t *testing.T) {
183 runFilesSeedPreparationWithLive(t, "real-seed", "same-controller", nil, []filesSeedLiveWitness{{seed: rootFilesSeedLifetime}})
184 })
185 }
186}
187