| 1 | //go:build linux |
| 2 | |
| 3 | package host |
| 4 | |
| 5 | import ( |
| 6 | "context" |
| 7 | "errors" |
| 8 | "fmt" |
| 9 | "io" |
| 10 | "os" |
| 11 | "path/filepath" |
| 12 | "reflect" |
| 13 | "testing" |
| 14 | "time" |
| 15 | |
| 16 | "github.com/carlosframework/platform/internal/hostclaim" |
| 17 | "github.com/carlosframework/platform/internal/hostfiles" |
| 18 | "github.com/carlosframework/platform/internal/lease" |
| 19 | "github.com/carlosframework/platform/internal/lease/leasetest" |
| 20 | "github.com/carlosframework/platform/internal/provision" |
| 21 | "github.com/carlosframework/platform/internal/registry" |
| 22 | ) |
| 23 | |
| 24 | type filesEvidenceMetadata struct { |
| 25 | lease.VersionedStore |
| 26 | lists, reads int |
| 27 | } |
| 28 | |
| 29 | func (s *filesEvidenceMetadata) ListVersions(ctx context.Context, b, k string) ([]lease.ObjectVersion, error) { |
| 30 | s.lists++ |
| 31 | return s.VersionedStore.ListVersions(ctx, b, k) |
| 32 | } |
| 33 | func (s *filesEvidenceMetadata) GetVersion(context.Context, string, string, string) ([]byte, error) { |
| 34 | s.reads++ |
| 35 | return nil, hostfiles.ErrNoEvidence |
| 36 | } |
| 37 | func (s *filesEvidenceMetadata) OpenVersion(context.Context, string, string, string) (io.ReadCloser, error) { |
| 38 | s.reads++ |
| 39 | return nil, hostfiles.ErrNoEvidence |
| 40 | } |
| 41 | |
| 42 | func TestUnseededServingRequestsSpendNoRetry(t *testing.T) { |
| 43 | for _, state := range []string{"none", "attempting", "open", "failed"} { |
| 44 | t.Run(state, func(t *testing.T) { |
| 45 | f := newServingRootFixture(t, true) |
| 46 | count := 0 |
| 47 | if state != "none" { |
| 48 | count = 1 |
| 49 | } |
| 50 | if state == "failed" { |
| 51 | count = 5 |
| 52 | } |
| 53 | for i := 0; i < count; i++ { |
| 54 | o := f.f.read() |
| 55 | attempt := hostclaim.WakeRetryAttempt{ID: fmt.Sprint("attempt-", i), Box: f.actor.Box, Enrollment: f.actor.Enrollment, BootID: "old-boot", ControllerID: "old-controller", InvocationID: fmt.Sprint("invocation-", i), LaunchInputs: "inputs"} |
| 56 | reserved, err := hostclaim.Update(t.Context(), f.f.claims, o, hostclaim.ReserveWakeRetryAttempt(hostclaim.WakeRetryReservation{HolderIncarnation: o.Claim.Holder.Incarnation, EpisodeID: "episode", Attempt: attempt, Now: f.f.now})) |
| 57 | if err != nil { |
| 58 | t.Fatal(err) |
| 59 | } |
| 60 | if state != "attempting" { |
| 61 | if _, err := hostclaim.Update(t.Context(), f.f.claims, reserved, hostclaim.FailWakeRetryAttempt(hostclaim.WakeRetryResult{HolderIncarnation: o.Claim.Holder.Incarnation, EpisodeID: "episode", Attempt: attempt, Now: f.f.now})); err != nil { |
| 62 | t.Fatal(err) |
| 63 | } |
| 64 | f.f.now = f.f.now.Add(time.Minute) |
| 65 | } |
| 66 | } |
| 67 | bucket := leasetest.NewVersionedBucket() |
| 68 | bucket.AddBucket("data") |
| 69 | data := &filesEvidenceMetadata{VersionedStore: bucket} |
| 70 | provider := &movesFiles{Data: data, Bucket: "data", Box: f.actor.Box, Region: f.actor.Region, DataDir: f.f.guard.DataDir, EnvFile: func(host, db string) (claimEnvFile, error) { |
| 71 | return claimEnvFile{Dir: filepath.Dir(db), Name: host + ".env", Nested: true}, nil |
| 72 | }} |
| 73 | p := newServingPreparation(t, f, "boot", "controller", func(w *ClaimWakeAssembly) { w.ServingPrerequisites = servingFilesPrerequisites(f.f.claims, provider) }) |
| 74 | before := f.f.read() |
| 75 | disk := servingBootSnapshot(t, f.disk.dir) |
| 76 | roots := servingBootSnapshot(t, f.rootDisk.dir) |
| 77 | for i := 0; i < 12; i++ { |
| 78 | f.f.now = f.f.now.Add(time.Minute) |
| 79 | err := p.runtime.Prerequisites(t.Context(), registry.Route{Host: before.Host, DBPath: provision.NestedDBPath(f.f.guard.DataDir, before.Host)}, before, recoveryPolicy) |
| 80 | if !errors.Is(err, hostfiles.ErrNoEvidence) { |
| 81 | t.Fatal(err) |
| 82 | } |
| 83 | } |
| 84 | after := f.f.read() |
| 85 | if !reflect.DeepEqual(before, after) || !reflect.DeepEqual(disk, servingBootSnapshot(t, f.disk.dir)) || !reflect.DeepEqual(roots, servingBootSnapshot(t, f.rootDisk.dir)) || len(p.events) != 0 { |
| 86 | t.Fatal("missing evidence spent retries or changed root state", p.events) |
| 87 | } |
| 88 | if data.reads != 0 || data.lists < 12 { |
| 89 | t.Fatal("guard did not use metadata only", data.lists, data.reads) |
| 90 | } |
| 91 | }) |
| 92 | } |
| 93 | } |
| 94 | |
| 95 | func TestServingEmptyBootstrapNeedsNoExistingInstanceDirectory(t *testing.T) { |
| 96 | f := newFilesSeedFixture(t) |
| 97 | if err := os.RemoveAll(filepath.Dir(f.route.DBPath)); err != nil { |
| 98 | t.Fatal(err) |
| 99 | } |
| 100 | o := f.observed |
| 101 | o.Claim.Holder.Generation = "" |
| 102 | o.Claim.Holder.Session = 0 |
| 103 | o.Claim.Holder.Admissions = nil |
| 104 | o.Claim.Holder.WriterBox = "" |
| 105 | // Match the production empty-authority predicate, not absence of a manifest. |
| 106 | if !claimEmptyBootstrap(o.Claim.Holder) { |
| 107 | t.Fatal("fixture not bootstrap") |
| 108 | } |
| 109 | if err := f.files.checkServingFilesEvidence(t.Context(), o, f.route); err != nil { |
| 110 | t.Fatal(err) |
| 111 | } |
| 112 | } |
| 113 | |