| 1 | package hostrelocate |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "crypto/sha256" |
| 6 | "encoding/hex" |
| 7 | "encoding/json" |
| 8 | "errors" |
| 9 | "fmt" |
| 10 | "testing" |
| 11 | "time" |
| 12 | |
| 13 | "github.com/carlosframework/platform/internal/hostclaim" |
| 14 | "github.com/carlosframework/platform/internal/hostfiles" |
| 15 | "github.com/carlosframework/platform/internal/instancekey" |
| 16 | "github.com/carlosframework/platform/internal/release" |
| 17 | ) |
| 18 | |
| 19 | // O2' (instance-key plan rev 3, replacing stage B's O2 and ruling 55's |
| 20 | // KeyEvidence reader): a nonempty database's pinned manifest evidences its |
| 21 | // key by (a) instance.key, or (b') an adoption record bound to the pinned |
| 22 | // writer for the key the target will deliver. Presence of a key, in the env |
| 23 | // or in the configuration, is never evidence (Astra rounds 1 and 2). |
| 24 | func TestStorePinnerKeyEvidence(t *testing.T) { |
| 25 | ctx := context.Background() |
| 26 | at := time.Date(2026, 10, 7, 9, 0, 0, 0, time.UTC) |
| 27 | const key = "q+/Zk3vJ0aW1Yx9sR7tUe2oPl4mN8bC6dF5gH0iJ1kA=" |
| 28 | writer := instancekey.Binding{Host: host, Incarnation: "inc-1", Session: 3, Generation: "g3"} |
| 29 | earlier := instancekey.Binding{Host: host, Incarnation: "inc-1", Session: 2, Generation: "g2"} |
| 30 | envWithKey := "CARLOS_ADMIN_TOKEN=t\nCARLOS_INSTANCE_KEY=" + key + "\n" |
| 31 | rec := func(b instancekey.Binding, source, k string) string { |
| 32 | return string(instancekey.AdoptionRecord(b, source, instancekey.Digest(k))) |
| 33 | } |
| 34 | for _, tc := range []struct { |
| 35 | name string |
| 36 | appKey bool // (a) instance.key in the manifest |
| 37 | env, adoption string // "" is no item |
| 38 | unreadable string // a named item whose object is missing |
| 39 | holder func(*hostclaim.Holder) |
| 40 | want error |
| 41 | pinned *KeyPin |
| 42 | }{ |
| 43 | {name: "(a) the app's own instance.key", appKey: true, env: envWithKey, want: nil}, |
| 44 | {name: "(b') the platform key, adopted by the pinned writer", env: envWithKey, adoption: rec(writer, "platform", key), pinned: &KeyPin{Source: "platform", SHA256: instancekey.Digest(key)}}, |
| 45 | {name: "(b') a configured key, adopted by the pinned writer", env: "CARLOS_ADMIN_TOKEN=t\n", adoption: rec(writer, "config", "configured-A"), pinned: &KeyPin{Source: "config", SHA256: instancekey.Digest("configured-A")}}, |
| 46 | {name: "(b') a configured key under an empty override", env: "CARLOS_ADMIN_TOKEN=t\nCARLOS_INSTANCE_KEY=\n", adoption: rec(writer, "config", "configured-A"), pinned: &KeyPin{Source: "config", SHA256: instancekey.Digest("configured-A")}}, |
| 47 | {name: "a backed-up platform key, the app's own key missing", env: envWithKey, want: ErrFilesUnevidenced}, |
| 48 | {name: "configured A, the app's own unpublished B: no report, no record", env: "CARLOS_ADMIN_TOKEN=t\n", want: ErrFilesUnevidenced}, |
| 49 | {name: "a stale record from a previous release", env: envWithKey, adoption: rec(earlier, "platform", key), want: ErrFilesUnevidenced}, |
| 50 | {name: "a rollback: a new session, the old session's record", env: envWithKey, adoption: rec(writer, "platform", key), |
| 51 | holder: func(h *hostclaim.Holder) { |
| 52 | h.Session, h.Generation = 4, "g4" |
| 53 | h.Admissions = append(h.Admissions, hostclaim.AdmissionRecord{Generation: "g4", Predecessor: "g3", Session: 4}) |
| 54 | }, want: ErrFilesUnevidenced}, |
| 55 | {name: "a record from another installation", env: envWithKey, adoption: rec(instancekey.Binding{Host: host, Incarnation: "inc-0", Session: 3, Generation: "g3"}, "platform", key), want: ErrFilesUnevidenced}, |
| 56 | {name: "a platform record for another key", env: envWithKey, adoption: rec(writer, "platform", "another-key"), want: ErrFilesUnevidenced}, |
| 57 | {name: "a config record while the env's own key overrides it", env: envWithKey, adoption: rec(writer, "config", key), want: ErrFilesUnevidenced}, |
| 58 | {name: "a platform record without the platform key", env: "CARLOS_ADMIN_TOKEN=t\n", adoption: rec(writer, "platform", key), want: ErrFilesUnevidenced}, |
| 59 | {name: "an exported key line is not what the app receives", env: "export CARLOS_INSTANCE_KEY=" + key + "\n", adoption: rec(writer, "platform", key), want: ErrFilesUnevidenced}, |
| 60 | {name: "a malformed record", env: envWithKey, adoption: `{"v":1}`, want: ErrFilesUnevidenced}, |
| 61 | {name: "no admitted writer for the pinned generation", env: envWithKey, adoption: rec(writer, "platform", key), holder: func(h *hostclaim.Holder) { h.Incarnation = "" }, want: ErrFilesUnevidenced}, |
| 62 | {name: "a named adoption item that cannot be read", env: envWithKey, unreadable: instancekey.AdoptionItem, want: errUnreadable}, |
| 63 | {name: "a named env item that cannot be read", adoption: rec(writer, "config", "configured-A"), unreadable: hostfiles.EnvItem, want: errUnreadable}, |
| 64 | } { |
| 65 | t.Run(tc.name, func(t *testing.T) { |
| 66 | claims, data := release.NewDirStore(t.TempDir()), release.NewDirStore(t.TempDir()) |
| 67 | obs := pinClaim() |
| 68 | obs.Claim.Holder.Incarnation = "inc-1" |
| 69 | gen := "g3" |
| 70 | if tc.holder != nil { |
| 71 | tc.holder(obs.Claim.Holder) |
| 72 | gen = obs.Claim.Holder.Generation |
| 73 | } |
| 74 | m := withItem(hostfiles.Manifest{V: hostfiles.ManifestVersion, Seq: 4, Entries: map[string]hostfiles.Entry{}, Platform: map[string]hostfiles.Entry{}}, hostfiles.WakeItem, "d") |
| 75 | if tc.appKey { |
| 76 | m = keyed(m) |
| 77 | } |
| 78 | item := func(name, body string, put bool) { |
| 79 | sum := sha256.Sum256([]byte(body)) |
| 80 | sha := hex.EncodeToString(sum[:]) |
| 81 | obj := pinLocation + "/files/g-" + gen + "/objects/" + fmt.Sprintf("%016d-%s", 4, sha) |
| 82 | if put { |
| 83 | if _, err := data.Put(ctx, obj, []byte(body), ""); err != nil { |
| 84 | t.Fatal(err) |
| 85 | } |
| 86 | } |
| 87 | m.Platform[name] = hostfiles.Entry{Object: obj, SHA256: sha, Size: int64(len(body)), Mode: 0o600, Seq: 4} |
| 88 | } |
| 89 | if tc.env != "" { |
| 90 | item(hostfiles.EnvItem, tc.env, true) |
| 91 | } |
| 92 | if tc.adoption != "" { |
| 93 | item(instancekey.AdoptionItem, tc.adoption, true) |
| 94 | } |
| 95 | if tc.unreadable != "" { |
| 96 | item(tc.unreadable, "never stored\n", false) |
| 97 | } |
| 98 | putManifest(t, data, gen, m) |
| 99 | p := StorePinner{Claims: claims, Data: data, Release: routeRelease, Bucket: "data", Region: "ie", SelectDB: selectAt(41)} |
| 100 | ref, err := p.Pin(ctx, obs, at) |
| 101 | switch { |
| 102 | case tc.want == errUnreadable: |
| 103 | if err == nil || errors.Is(err, ErrFilesUnevidenced) { |
| 104 | t.Fatalf("got %v, want a read error, never a refusal for no evidence", err) |
| 105 | } |
| 106 | return |
| 107 | case !errors.Is(err, tc.want): |
| 108 | t.Fatalf("got %v, want %v", err, tc.want) |
| 109 | } |
| 110 | if err != nil { |
| 111 | return |
| 112 | } |
| 113 | raw, _, err := claims.Get(ctx, ref.Key) |
| 114 | if err != nil { |
| 115 | t.Fatal(err) |
| 116 | } |
| 117 | var obj PinObject |
| 118 | if err := json.Unmarshal(raw, &obj); err != nil { |
| 119 | t.Fatal(err) |
| 120 | } |
| 121 | if (obj.InstanceKey == nil) != (tc.pinned == nil) || (tc.pinned != nil && *obj.InstanceKey != *tc.pinned) { |
| 122 | t.Fatalf("pinned key %+v, want %+v", obj.InstanceKey, tc.pinned) |
| 123 | } |
| 124 | }) |
| 125 | } |
| 126 | } |
| 127 | |
| 128 | var errUnreadable = errors.New("sentinel: any read error") |
| 129 | |
| 130 | // A configured key's record vouches only beside the env item that shows |
| 131 | // the instance's own file supplies no key. A manifest without host.env is |
| 132 | // not evidence that nothing overrides the configuration at launch (absence |
| 133 | // is never evidence): the pin refuses, and the same record beside an env |
| 134 | // item with no key of its own pins. |
| 135 | func TestStorePinnerConfigRecordNeedsTheEnvItem(t *testing.T) { |
| 136 | ctx := context.Background() |
| 137 | at := time.Date(2026, 10, 7, 9, 0, 0, 0, time.UTC) |
| 138 | writer := instancekey.Binding{Host: host, Incarnation: "inc-1", Session: 3, Generation: "g3"} |
| 139 | record := string(instancekey.AdoptionRecord(writer, instancekey.SourceConfig, instancekey.Digest("configured-A"))) |
| 140 | for _, tc := range []struct { |
| 141 | name string |
| 142 | env string // "" or "item" |
| 143 | want error |
| 144 | }{ |
| 145 | {"an env item never published", "", ErrFilesUnevidenced}, |
| 146 | {"an env item with no key of its own", "item", nil}, |
| 147 | } { |
| 148 | t.Run(tc.name, func(t *testing.T) { |
| 149 | claims, data := release.NewDirStore(t.TempDir()), release.NewDirStore(t.TempDir()) |
| 150 | obs := pinClaim() |
| 151 | obs.Claim.Holder.Incarnation = "inc-1" |
| 152 | m := withItem(hostfiles.Manifest{V: hostfiles.ManifestVersion, Seq: 4, Entries: map[string]hostfiles.Entry{}, Platform: map[string]hostfiles.Entry{}}, hostfiles.WakeItem, "d") |
| 153 | item := func(name, body string) { |
| 154 | sum := sha256.Sum256([]byte(body)) |
| 155 | sha := hex.EncodeToString(sum[:]) |
| 156 | obj := pinLocation + "/files/g-g3/objects/" + fmt.Sprintf("%016d-%s", 4, sha) |
| 157 | if _, err := data.Put(ctx, obj, []byte(body), ""); err != nil { |
| 158 | t.Fatal(err) |
| 159 | } |
| 160 | m.Platform[name] = hostfiles.Entry{Object: obj, SHA256: sha, Size: int64(len(body)), Mode: 0o600, Seq: 4} |
| 161 | } |
| 162 | item(instancekey.AdoptionItem, record) |
| 163 | if tc.env == "item" { |
| 164 | item(hostfiles.EnvItem, "CARLOS_ADMIN_TOKEN=t\n") |
| 165 | } |
| 166 | putManifest(t, data, "g3", m) |
| 167 | p := StorePinner{Claims: claims, Data: data, Release: routeRelease, Bucket: "data", Region: "ie", SelectDB: selectAt(41)} |
| 168 | ref, err := p.Pin(ctx, obs, at) |
| 169 | if tc.want != nil { |
| 170 | if !errors.Is(err, tc.want) { |
| 171 | t.Fatalf("got %v, want %v", err, tc.want) |
| 172 | } |
| 173 | return |
| 174 | } |
| 175 | if err != nil { |
| 176 | t.Fatal(err) |
| 177 | } |
| 178 | obj, err := ReadPin(ctx, claims, ref) |
| 179 | if err != nil { |
| 180 | t.Fatal(err) |
| 181 | } |
| 182 | want := KeyPin{Source: instancekey.SourceConfig, SHA256: instancekey.Digest("configured-A")} |
| 183 | if obj.InstanceKey == nil || *obj.InstanceKey != want { |
| 184 | t.Fatalf("pinned key %+v, want %+v", obj.InstanceKey, want) |
| 185 | } |
| 186 | }) |
| 187 | } |
| 188 | } |
| 189 | |