carlos / platform Public

seeds: the legacy route owners reader (Plan 7 Task 2)

Fails closed: a present file that is unreadable, malformed, of an unknown
version or carries an incomplete entry is an error, never an empty set.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev c9754d6e21a32a50b3433b45a78959cfad55e805 parent 6955bba
2 files changed, +211 −0
  • internal/seeds/legacyowners.go +80 −0
  • internal/seeds/legacyowners_test.go +131 −0

CI passed — run details

diff --git a/internal/seeds/legacyowners.go b/internal/seeds/legacyowners.go
new file mode 100644
index 000000000..764c079d7
--- /dev/null
+++ b/internal/seeds/legacyowners.go
@@ -0,0 +1,80 @@
+package seeds
+
+import (
+ "bytes"
+ "context"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "io"
+ "os"
+
+ "github.com/carlosframework/platform/internal/release"
+)
+
+// LegacyOwnersKey is the operator assignment of legacy route names
+// (seeded-instances spec §6.4, codex 9): which account and app a route
+// filed under the operator account belongs to.
+const LegacyOwnersKey = "control/legacy-route-owners.json"
+
+// legacyOwnersVersion is the only file version this reader understands.
+const legacyOwnersVersion = 1
+
+// LegacyOwner is the account and app a legacy route name is assigned to.
+type LegacyOwner struct {
+ Account string `json:"account"`
+ App string `json:"app"`
+}
+
+// LegacyOwners maps a route name, as filed under the operator account, to
+// the one account and app it belongs to.
+type LegacyOwners struct {
+ V int `json:"v"`
+ Names map[string]LegacyOwner `json:"names"`
+}
+
+// ReadLegacyOwners reads the assignments. It is an authority check, so it
+// fails closed: an absent file is an empty set (nothing is assigned), but a
+// file that is present and unreadable, malformed, of an unknown version or
+// carrying an incomplete entry is an error and no owners, never "no
+// assignment". The etag is "" for an absent file.
+func ReadLegacyOwners(ctx context.Context, s release.Store) (*LegacyOwners, string, error) {
+ body, etag, err := s.Get(ctx, LegacyOwnersKey)
+ if errors.Is(err, os.ErrNotExist) {
+ return &LegacyOwners{V: legacyOwnersVersion, Names: map[string]LegacyOwner{}}, "", nil
+ }
+ if err != nil {
+ return nil, "", fmt.Errorf("seeds: read legacy route owners: %w", err)
+ }
+ dec := json.NewDecoder(bytes.NewReader(body))
+ dec.DisallowUnknownFields()
+ var o LegacyOwners
+ if err := dec.Decode(&o); err != nil {
+ return nil, "", fmt.Errorf("seeds: parse legacy route owners: %w", err)
+ }
+ if _, err := dec.Token(); err != io.EOF {
+ return nil, "", errors.New("seeds: parse legacy route owners: data after the object")
+ }
+ if o.V != legacyOwnersVersion {
+ return nil, "", fmt.Errorf("seeds: legacy route owners: unknown version %d", o.V)
+ }
+ if o.Names == nil {
+ o.Names = map[string]LegacyOwner{}
+ }
+ for name, ow := range o.Names {
+ if name == "" || ow.Account == "" || ow.App == "" {
+ return nil, "", fmt.Errorf("seeds: legacy route owners: incomplete entry %q", name)
+ }
+ }
+ return &o, etag, nil
+}
+
+// Lookup returns the owner assigned to name. A name that is not in the set
+// is not assigned.
+func (o *LegacyOwners) Lookup(name string) (LegacyOwner, bool) {
+ if o == nil {
+ return LegacyOwner{}, false
+ }
+ v, ok := o.Names[name]
+ return v, ok
+}
diff --git a/internal/seeds/legacyowners_test.go b/internal/seeds/legacyowners_test.go
new file mode 100644
index 000000000..0d7bfef29
--- /dev/null
+++ b/internal/seeds/legacyowners_test.go
@@ -0,0 +1,131 @@
+package seeds
+
+import (
+ "context"
+ "errors"
+ "os"
+ "strings"
+ "testing"
+
+ "github.com/carlosframework/platform/internal/release"
+)
+
+func putOwners(t *testing.T, s release.Store, body, ifMatch string) string {
+ t.Helper()
+ etag, err := s.Put(context.Background(), LegacyOwnersKey, []byte(body), ifMatch)
+ if err != nil {
+ t.Fatal(err)
+ }
+ return etag
+}
+
+func TestLegacyOwners(t *testing.T) {
+ ctx := context.Background()
+ s := release.NewDirStore(t.TempDir())
+ o, etag, err := ReadLegacyOwners(ctx, s)
+ if err != nil || etag != "" || o == nil || len(o.Names) != 0 {
+ t.Fatalf("absent = %+v %q %v", o, etag, err)
+ }
+ if _, ok := o.Lookup("shop"); ok {
+ t.Fatal("a name resolved from an absent file")
+ }
+ etag = putOwners(t, s, `{"v":1,"names":{"shop":{"account":"abc","app":"shop"}}}`, "")
+ o, got, err := ReadLegacyOwners(ctx, s)
+ if err != nil || got == "" || got != etag {
+ t.Fatalf("read = %q (put %q) %v", got, etag, err)
+ }
+ if got, ok := o.Lookup("shop"); !ok || got != (LegacyOwner{Account: "abc", App: "shop"}) {
+ t.Fatalf("lookup = %+v %v", got, ok)
+ }
+ if _, ok := o.Lookup("other"); ok {
+ t.Fatal("an unassigned name resolved")
+ }
+ if _, _, err := ReadLegacyOwners(ctx, s); err != nil {
+ t.Fatal(err)
+ }
+ putOwners(t, s, `{garbage`, etag)
+ if _, _, err := ReadLegacyOwners(ctx, s); err == nil {
+ t.Fatal("garbage read as no assignments")
+ }
+}
+
+func TestLegacyOwnersEmptySet(t *testing.T) {
+ s := release.NewDirStore(t.TempDir())
+ putOwners(t, s, `{"v":1}`, "")
+ o, etag, err := ReadLegacyOwners(context.Background(), s)
+ if err != nil || etag == "" || o == nil || o.Names == nil || len(o.Names) != 0 {
+ t.Fatalf("empty v1 file = %+v %q %v", o, etag, err)
+ }
+ if _, ok := o.Lookup("shop"); ok {
+ t.Fatal("a name resolved from an empty set")
+ }
+}
+
+// Every present-but-unusable file is an error and no owners, never an empty
+// set: the swap treats "no assignment" as a refusal it can explain, and a
+// read it cannot trust as a failure of its own.
+func TestLegacyOwnersFailClosed(t *testing.T) {
+ good := `"names":{"shop":{"account":"abc","app":"shop"}}`
+ cases := map[string]string{
+ "garbage": `{garbage`,
+ "empty body": ``,
+ "not an object": `[]`,
+ "null": `null`,
+ "version missing": `{` + good + `}`,
+ "version zero": `{"v":0,` + good + `}`,
+ "version unknown": `{"v":2,` + good + `}`,
+ "version negative": `{"v":-1,` + good + `}`,
+ "version a string": `{"v":"1",` + good + `}`,
+ "names a list": `{"v":1,"names":[]}`,
+ "entry a string": `{"v":1,"names":{"shop":"abc/shop"}}`,
+ "entry no account": `{"v":1,"names":{"shop":{"app":"shop"}}}`,
+ "entry no app": `{"v":1,"names":{"shop":{"account":"abc"}}}`,
+ "entry null": `{"v":1,"names":{"shop":null}}`,
+ "empty name": `{"v":1,"names":{"":{"account":"abc","app":"shop"}}}`,
+ "unknown field": `{"v":1,"extra":true,` + good + `}`,
+ "unknown entry key": `{"v":1,"names":{"shop":{"account":"abc","app":"shop","x":1}}}`,
+ "trailing object": `{"v":1,` + good + `}{"v":1}`,
+ "trailing garbage": `{"v":1,` + good + `}x`,
+ }
+ for name, body := range cases {
+ t.Run(name, func(t *testing.T) {
+ s := release.NewDirStore(t.TempDir())
+ putOwners(t, s, body, "")
+ o, etag, err := ReadLegacyOwners(context.Background(), s)
+ if err == nil {
+ t.Fatalf("read %q = %+v, want an error", body, o)
+ }
+ if o != nil || etag != "" {
+ t.Fatalf("an error came with owners %+v / etag %q", o, etag)
+ }
+ if errors.Is(err, os.ErrNotExist) {
+ t.Fatalf("a bad file reads as absent: %v", err)
+ }
+ if !strings.Contains(err.Error(), "legacy route owners") {
+ t.Fatalf("error does not say what failed: %v", err)
+ }
+ })
+ }
+}
+
+// A store error other than not-found is an error too.
+func TestLegacyOwnersReadError(t *testing.T) {
+ boom := errors.New("bucket unreachable")
+ o, etag, err := ReadLegacyOwners(context.Background(), failingStore{Store: release.NewDirStore(t.TempDir()), err: boom})
+ if !errors.Is(err, boom) || o != nil || etag != "" {
+ t.Fatalf("read error = %+v %q %v", o, etag, err)
+ }
+ var nilOwners *LegacyOwners
+ if _, ok := nilOwners.Lookup("shop"); ok {
+ t.Fatal("a nil set resolved a name")
+ }
+}
+
+type failingStore struct {
+ release.Store
+ err error
+}
+
+func (f failingStore) Get(context.Context, string) ([]byte, string, error) {
+ return nil, "", f.err
+}