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
2
3package host
4
5import (
6 "context"
7 "errors"
8 "fmt"
9 "net"
10 "net/http"
11 "os"
12 "os/exec"
13 "path/filepath"
14 "sync/atomic"
15 "testing"
16 "time"
17
18 "github.com/carlosframework/platform/internal/hostbox"
19 "github.com/carlosframework/platform/internal/hostclaim"
20 "github.com/carlosframework/platform/internal/lease/leasetest"
21 "github.com/carlosframework/platform/internal/provision"
22 "github.com/carlosframework/platform/internal/registry"
23 "github.com/carlosframework/platform/internal/release"
24)
25
26func TestClaimServingAssemblyWithoutMoves(t *testing.T) {
27 fixtureWithoutRootMigrationCensus(t)
28 t.Setenv("CARLOS_HOST_CLAIM", "1")
29 t.Setenv("CARLOS_HOST_CLAIM_ORIGINS", "1")
30 t.Setenv("CARLOS_MOVES_PHASE1", "")
31 for _, ha := range []string{"", "1"} {
32 t.Run("region-ha="+ha, func(t *testing.T) {
33 t.Setenv("CARLOS_REGION_HA", ha)
34 store, resources, owners, _ := fix4JournalFixture(t)
35 ctx, err := claimEntryContext(t.Context(), os.Getenv, func(context.Context) (release.Store, error) { return store, nil })
36 if err != nil {
37 t.Fatal(err)
38 }
39 actor, err := owners.Identity.Verify(ctx)
40 if err != nil {
41 t.Fatal(err)
42 }
43 data := leasetest.NewVersionedBucket()
44 data.AddBucket(actor.Bucket)
45 rt := registry.Route{Host: filesHost, Kind: "instance", Hibernate: true, DBPath: provision.NestedDBPath(resources.DataDir, filesHost), Socket: provision.NestedSocket(filesHost)}
46 dir := filepath.Dir(rt.DBPath)
47 if err := os.MkdirAll(dir, 0700); err != nil {
48 t.Fatal(err)
49 }
50 if err := os.WriteFile(filepath.Join(dir, filesHost+".env"), []byte("K=1\n"), 0600); err != nil {
51 t.Fatal(err)
52 }
53 source := &movesFilesSource{DataDir: resources.DataDir, Routes: testClaimRoutes{rows: map[string]registry.Route{filesHost: rt}}, newFiles: func(context.Context, string, string) (release.Store, error) {
54 return casBucketStore{b: data, bucket: actor.Bucket}, nil
55 }}
56 files, err := source.Open(ctx, store, data, actor)
57 if err != nil {
58 t.Fatal(err)
59 }
60 d5 := plan9D5Fixture(t)
61 owners.Data = data
62 owners.Serving = d5.owners.Sources
63 owners.Serving.Files = files
64 owners.Serving.Markers = plan9D5Markers{data}
65 assembly, err := OpenProductionClaimAssembly(ctx, store, resources, owners)
66 if err != nil {
67 t.Fatal(err)
68 }
69 defer assembly.Close(ctx)
70 if assembly.wakeUnavailable != nil {
71 t.Fatal(assembly.wakeUnavailable)
72 }
73 dispatch, ok := assembly.Wake.Inputs.(ClaimOwnerDispatch)
74 if !ok || dispatch.Moves != nil || dispatch.Relocation != nil || assembly.Wake.Operations != nil || assembly.Wake.ProbeReady != nil {
75 t.Fatalf("serving-only assembly: %#v", assembly.Wake)
76 }
77 if digest, err := assembly.Wake.Retries.Inputs(ctx, rt); err != nil || digest == "" {
78 t.Fatalf("digest %q: %v", digest, err)
79 }
80 o, err := hostclaim.Read(ctx, store, filesHost)
81 if err != nil {
82 t.Fatal(err)
83 }
84 n := hostclaim.NewHolder{Family: "instance", Key: "apps/bab/jam/instances/" + filesHost + ".json", Account: "bab", App: "jam", Host: filesHost, Region: actor.Region, CreatedAt: filesCreated}
85 o, err = hostclaim.Update(ctx, store, o, hostclaim.BeginCreate(n, []byte(`{"host":"shop.example.com","account":"bab","app":"jam","created_at":"2026-10-01T00:00:00Z"}`), "create", "console", time.Now()))
86 if err != nil {
87 t.Fatal(err)
88 }
89 o, err = hostclaim.Update(ctx, store, o, hostclaim.CompleteCreate("create", actor.Box))
90 if err != nil {
91 t.Fatal(err)
92 }
93 in, err := dispatch.Select(ctx, rt, o, recoveryPolicy)
94 if err != nil || !in.Empty {
95 t.Fatalf("empty selection: %+v %v", in, err)
96 }
97 writer := hostbox.Writer{Claims: store, Box: actor.Box, Region: actor.Region, AdmissionEvidence: assembly.AdmissionEvidence}
98 admitted, err := writer.Admit(ctx, o, in.Admission, in.Baseline)
99 if err != nil {
100 t.Fatal(err)
101 }
102 work := &filesFixture{t: t}
103 restore := hostbox.ColdRestore{Generation: in.Admission.NewGeneration, Work: work, Identity: in.Identity}
104 if err := restore.InstallEmpty(ctx, rt.DBPath, func(ctx context.Context) error { return dispatch.EmptyAuthority(ctx, admitted, in) }); err != nil {
105 t.Fatal(err)
106 }
107 if err := dispatch.Materialize(ctx, admitted, in, work); err != nil {
108 t.Fatal(err)
109 }
110 key := movesFilesPrefix(admitted.Claim.Holder.Location, in.Admission.NewGeneration) + "manifest.json"
111 if data.LatestID(actor.Bucket, key) == "" {
112 t.Fatal("production C8 did not commit")
113 }
114 if assembly.Wake.BeforeLegacyInstall == nil || assembly.Wake.RecoverArtifacts == nil || assembly.Wake.BeforeInstallLocked == nil || assembly.Wake.Retries.BeforeReserve == nil {
115 t.Fatal("serving retirement or capacity not wired")
116 }
117 rootKey := fmt.Sprintf("%s/g-%s/0000/0000000000000001-0000000000000001.ltx", admitted.Claim.Holder.Location, in.Admission.NewGeneration)
118 syncs := 0
119 publication := ClaimPublication{Sync: func(context.Context) error { syncs++; data.Put(actor.Bucket, rootKey, plan9D5RootBody(t)); return nil }, Check: func(context.Context) error { return nil }}
120 receipt, err := assembly.Wake.Publication.PublishFenced(ctx, admitted, in, publication)
121 if err != nil || receipt.MarkerVersion == "" || receipt.Root.Key != rootKey || syncs != 1 {
122 t.Fatal("moves-off publication", receipt, err, syncs)
123 }
124 writer.PublicationEvidence = assembly.PublicationEvidence
125 writer.NewID = func() string { return "serving-published" }
126 published, err := writer.Published(ctx, admitted)
127 if err != nil || published.Claim.Holder.Generation != in.Admission.NewGeneration {
128 t.Fatal("moves-off publication evidence", err)
129 }
130 sockdir, err := os.MkdirTemp(os.Getenv("TMPDIR"), "seed-ready-")
131 if err != nil {
132 t.Fatal(err)
133 }
134 defer os.RemoveAll(sockdir)
135 rt.Socket = filepath.Join(sockdir, "ready.sock")
136 listener, err := net.Listen("unix", rt.Socket)
137 if err != nil {
138 t.Fatal(err)
139 }
140 var requests atomic.Int32
141 server := &http.Server{Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { requests.Add(1); w.WriteHeader(http.StatusOK) })}
142 done := make(chan struct{})
143 go func() { defer close(done); _ = server.Serve(listener) }()
144 defer func() { server.Close(); <-done }()
145 process, err := os.FindProcess(os.Getpid())
146 if err != nil {
147 t.Fatal(err)
148 }
149 readyCtx, cancel := context.WithTimeout(ctx, time.Second)
150 err = assembly.Wake.Ready(readyCtx, &exec.Cmd{Process: process}, rt, in)
151 cancel()
152 if err != nil || requests.Load() != 1 {
153 t.Fatal("moves-off readiness", err, requests.Load())
154 }
155 wrong, _ := os.FindProcess(os.Getpid() + 1000000)
156 readyCtx, cancel = context.WithTimeout(ctx, 40*time.Millisecond)
157 err = assembly.Wake.Ready(readyCtx, &exec.Cmd{Process: wrong}, rt, in)
158 cancel()
159 if err == nil || requests.Load() != 1 {
160 t.Fatal("readiness accepted another process", err, requests.Load())
161 }
162
163 for _, kind := range []string{hostclaim.KindMoving, hostclaim.KindLanding, hostclaim.KindReverting, hostclaim.KindRestoring, hostclaim.KindRelocating} {
164 if err := plan9D5Call(dispatch, plan9D5Kind(kind)); err == nil {
165 t.Fatalf("moves-off accepted %s", kind)
166 }
167 }
168 })
169 }
170}
171
172func TestClaimServingMissingSourcesRefusesBeforeSelection(t *testing.T) {
173 for _, field := range []string{"launch", "markers", "files"} {
174 t.Run(field, func(t *testing.T) {
175 f := plan9D5Fixture(t)
176 switch field {
177 case "launch":
178 f.owners.Sources.Launch = nil
179 case "markers":
180 f.owners.Sources.Markers = nil
181 case "files":
182 f.owners.Sources.Files = nil
183 }
184 var dependency *ClaimDependencyError
185 if err := checkClaimServingSources(f.owners.Sources); !errors.As(err, &dependency) {
186 t.Fatalf("missing %s: %v", field, err)
187 }
188 _, err := f.owners.Select(t.Context(), registry.Route{}, plan9D5Observed(hostclaim.Holder{}), recoveryPolicy)
189 if !errors.As(err, &dependency) {
190 t.Fatalf("selection with missing %s: %v", field, err)
191 }
192 if len(f.files) != 0 || f.seq != 0 {
193 t.Fatal("missing dependency had effects")
194 }
195 })
196 }
197}
198
199func TestClaimServingAssemblyFlagMatrix(t *testing.T) {
200 fixtureWithoutRootMigrationCensus(t)
201 for _, claims := range []string{"", "1"} {
202 for _, moves := range []string{"", "1"} {
203 for _, partial := range []bool{false, true} {
204 name := "claims=" + claims + "/moves=" + moves
205 if partial {
206 name += "/partial"
207 }
208 t.Run(name, func(t *testing.T) {
209 t.Setenv("CARLOS_HOST_CLAIM", claims)
210 t.Setenv("CARLOS_HOST_CLAIM_ORIGINS", "")
211 t.Setenv("CARLOS_MOVES_PHASE1", moves)
212 t.Setenv("CARLOS_REGION_HA", "1")
213 store, resources, owners, _ := fix4JournalFixture(t)
214 ctx, err := claimEntryContext(t.Context(), os.Getenv, func(context.Context) (release.Store, error) { return store, nil })
215 if err != nil {
216 t.Fatal(err)
217 }
218 f := plan9D5Fixture(t)
219 owners.Serving = f.owners.Sources
220 var calls []string
221 owners.Moves = ClaimMovesOwners{ClaimKindOwners: plan9D5Owner{"moves", &calls}.owners(), Operations: &plan9D2Attempts{}}
222 if partial {
223 owners.Moves.PublicationEvidence = nil
224 }
225 assembly, err := OpenProductionClaimAssembly(ctx, store, resources, owners)
226 if err != nil {
227 t.Fatal(err)
228 }
229 defer assembly.Close(ctx)
230 dispatch, bound := assembly.Wake.Inputs.(ClaimOwnerDispatch)
231 if bound != (claims != "") {
232 t.Fatalf("serving binding %v, claims %q", bound, claims)
233 }
234 wantMoves := claims != "" && moves != "" && !partial
235 if bound && (dispatch.Moves != nil) != wantMoves {
236 t.Fatalf("moves binding %v, want %v", dispatch.Moves != nil, wantMoves)
237 }
238 if (assembly.Wake.Operations != nil) != wantMoves {
239 t.Fatal("operation gate changed")
240 }
241 wantMissing := claims != "" && moves != "" && partial
242 if (assembly.wakeUnavailable != nil) != wantMissing {
243 t.Fatalf("partial owner refusal %v, want %v", assembly.wakeUnavailable, wantMissing)
244 }
245 if bound && dispatch.Relocation != nil {
246 t.Fatal("region HA bypassed absent relocation owners")
247 }
248 if len(calls) != 0 || len(f.files) != 0 || f.seq != 0 {
249 t.Fatal("assembly ran a host operation")
250 }
251 })
252 }
253 }
254 }
255}
256