carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1package console
2
3import (
4 "context"
5 "net/http"
6 "strings"
7 "testing"
8 "time"
9
10 "github.com/carlosframework/platform/internal/domains"
11 "github.com/carlosframework/platform/internal/provision"
12 "github.com/carlosframework/platform/internal/release"
13)
14
15// canaryDoorFixture is a console with the platform apps domain set, and two
16// apps in the ops account ("bar" and "foo-bar") with instances enabled on
17// the account's platform subdomain, the way `carlos instances enable` pins
18// them. It returns the fixture, a member token and the manifest domain.
19func canaryDoorFixture(t *testing.T, code string, apps ...string) (*consoleFixture, string, string) {
20 t.Helper()
21 fx, member := instanceFixture(t, code)
22 fx.srv.cfg.AppsDomain = "oncarlos.test"
23 now := time.Now()
24 for _, a := range apps {
25 if _, err := fx.st.CreateAppIn(OpsAccountID, a, "paul@keymail.dev", now); err != nil {
26 t.Fatal(err)
27 }
28 }
29 for _, a := range append([]string{"titogo"}, apps...) {
30 if status, _, out := rawReq(t, http.MethodPut, enableURL(fx, "bab", a), member, `{}`); status != http.StatusOK {
31 t.Fatalf("enable %s = %d %s", a, status, out)
32 }
33 }
34 return fx, member, fx.sqid + ".oncarlos.test"
35}
36
37// The flat canary host is <canary>-<app>.<domain>: one label, so the create
38// door that insists on one label accepts it. This is the door half of
39// discussion 207: the host `carlos canary` builds must pass ValidHost.
40func TestAPIInstanceCreateAcceptsAFlatCanaryHost(t *testing.T) {
41 fx, member, domain := canaryDoorFixture(t, "code-canary-flat")
42 status, _, out := rawReq(t, http.MethodPost, instancesURL(fx, "bab", "titogo"), member,
43 `{"host":"new-queue-titogo.`+domain+`","channel":"canary/new-queue"}`)
44 if status != http.StatusCreated {
45 t.Fatalf("flat canary host create = %d %s, want 201", status, out)
46 }
47 // The two-label form stays refused: ValidHost is a tenancy boundary and
48 // is not loosened for canaries.
49 status, _, out = rawReq(t, http.MethodPost, instancesURL(fx, "bab", "titogo"), member,
50 `{"host":"other.titogo.`+domain+`","channel":"canary/other"}`)
51 if status != http.StatusBadRequest || !strings.Contains(out, "invalid host") {
52 t.Fatalf("two-label host create = %d %s, want 400 invalid host", status, out)
53 }
54}
55
56// A canary named "foo" on app "bar" lands on foo-bar.<domain>, which is also
57// the platform apex of an app called "foo-bar". Flattening makes that
58// reachable, so both orders are pinned: the canary cannot take a live or
59// trashed app's apex, and an app cannot be created over a canary that
60// already sits on its apex.
61func TestFlatCanaryHostCannotTakeAnotherAppsApex(t *testing.T) {
62 fx, member, domain := canaryDoorFixture(t, "code-canary-apex", "bar", "foo-bar")
63
64 // canary "foo" of app "bar": host foo-bar, app foo-bar's apex.
65 status, _, out := rawReq(t, http.MethodPost, instancesURL(fx, "bab", "bar"), member,
66 `{"host":"foo-bar.`+domain+`","channel":"canary/foo"}`)
67 if status != http.StatusConflict || !strings.Contains(out, "foo-bar") || !strings.Contains(out, "platform apex") {
68 t.Fatalf("canary onto a live app's apex = %d %s, want 409 naming the apex", status, out)
69 }
70 if err := fx.st.TrashAppRow(OpsAccountID, "foo-bar", time.Now()); err != nil {
71 t.Fatal(err)
72 }
73 status, _, out = rawReq(t, http.MethodPost, instancesURL(fx, "bab", "bar"), member,
74 `{"host":"foo-bar.`+domain+`","channel":"canary/foo"}`)
75 if status != http.StatusConflict || !strings.Contains(out, "trash") {
76 t.Fatalf("canary onto a trashed app's apex = %d %s, want 409 naming the trash", status, out)
77 }
78 if err := fx.st.RestoreAppRow(OpsAccountID, "foo-bar"); err != nil {
79 t.Fatal(err)
80 }
81 if _, err := provision.GetRecord(context.Background(), fx.rel, release.OpsAccount, "bar", "foo-bar."+domain); err == nil {
82 t.Fatal("a refused canary left a record behind")
83 }
84
85 // The other order: the canary exists while no app owns the name, and an
86 // app arrives under it afterwards.
87 status, _, out = rawReq(t, http.MethodPost, instancesURL(fx, "bab", "bar"), member,
88 `{"host":"qux-bar.`+domain+`","channel":"canary/qux"}`)
89 if status != http.StatusCreated {
90 t.Fatalf("canary on a free label = %d %s, want 201", status, out)
91 }
92 if status, out := postJSON(t, fx.ts.URL+"/api/cli/apps/"+fx.sqid+"/new", map[string]string{"name": "qux-bar"}, member); status != http.StatusConflict {
93 t.Fatalf("creating an app over a canary's host = %d %v, want 409", status, out)
94 }
95}
96
97// Two canaries of two apps can flatten to the same label: canary "a" of app
98// "b-c" and canary "a-b" of app "c" are both a-b-c. Instance records are
99// keyed per app, so nothing about the key stops the second one, and the
100// provisioning pass would quietly keep whichever record it lists first. The
101// door refuses a host another app of the account already holds, whatever
102// its channel, and in both directions.
103func TestFlatCanaryHostsOfTwoAppsCannotCollide(t *testing.T) {
104 fx, member, domain := canaryDoorFixture(t, "code-canary-collide", "b-c", "c")
105 const host = "a-b-c."
106
107 status, _, out := rawReq(t, http.MethodPost, instancesURL(fx, "bab", "b-c"), member,
108 `{"host":"`+host+domain+`","channel":"canary/a"}`)
109 if status != http.StatusCreated {
110 t.Fatalf("first canary = %d %s, want 201", status, out)
111 }
112 status, _, out = rawReq(t, http.MethodPost, instancesURL(fx, "bab", "c"), member,
113 `{"host":"`+host+domain+`","channel":"canary/a-b"}`)
114 if status != http.StatusConflict || !strings.Contains(out, "b-c") {
115 t.Fatalf("colliding canary of another app = %d %s, want 409 naming app b-c", status, out)
116 }
117 if _, err := provision.GetRecord(context.Background(), fx.rel, release.OpsAccount, "c", host+domain); err == nil {
118 t.Fatal("the refused canary left a record under app c")
119 }
120
121 // An ordinary instance of another app at a host a canary holds is the
122 // same collision, as is the reverse.
123 status, _, out = rawReq(t, http.MethodPost, instancesURL(fx, "bab", "titogo"), member,
124 `{"host":"`+host+domain+`"}`)
125 if status != http.StatusConflict {
126 t.Fatalf("ordinary instance on a canary's host = %d %s, want 409", status, out)
127 }
128
129 // The holder itself is told "already exists", as ever: this is not a
130 // sibling's host.
131 status, _, out = rawReq(t, http.MethodPost, instancesURL(fx, "bab", "b-c"), member,
132 `{"host":"`+host+domain+`","channel":"canary/a"}`)
133 if status != http.StatusConflict || !strings.Contains(out, "already exists") {
134 t.Fatalf("re-create by the holder = %d %s, want 409 already exists", status, out)
135 }
136}
137
138// An attached alias is a hostname claim (domains.ClaimKey) with no instance
139// record, and no route until the domains pass derives one, so the holder
140// check cannot see it; a route minted at that host would shadow the alias at
141// the edge. The create door reads the claim itself: another app's claim, in
142// this account or another, refuses the create (the other account's without
143// saying whose), this app's own claim does not, and a claim that cannot be
144// read fails closed.
145func TestInstanceCreateRefusesAHostAnotherAppHasClaimed(t *testing.T) {
146 fx, member, domain := canaryDoorFixture(t, "code-canary-claim", "other")
147 ctx := context.Background()
148 claim := func(host, acct, app string) {
149 t.Helper()
150 if _, _, err := domains.AttachClaim(ctx, fx.rel, domains.Claim{Schema: 1, Host: host, Account: acct, App: app,
151 CreatedAt: time.Now().UTC().Format(time.RFC3339)}); err != nil {
152 t.Fatal(err)
153 }
154 }
155 create := func(label string) (int, string) {
156 t.Helper()
157 status, _, out := rawReq(t, http.MethodPost, instancesURL(fx, "bab", "titogo"), member,
158 `{"host":"`+label+`.`+domain+`","channel":"canary/`+label+`"}`)
159 return status, out
160 }
161
162 claim("probe-titogo."+domain, "bab", "other")
163 if status, out := create("probe-titogo"); status != http.StatusConflict || !strings.Contains(out, "other") {
164 t.Fatalf("create over a sibling's claim = %d %s, want 409 naming app other", status, out)
165 }
166
167 claim("away-titogo."+domain, "zzzz", "other")
168 status, out := create("away-titogo")
169 if status != http.StatusConflict || strings.Contains(out, "zzzz") || strings.Contains(out, "other") {
170 t.Fatalf("create over another account's claim = %d %s, want 409 that names neither", status, out)
171 }
172
173 claim("mine-titogo."+domain, "bab", "titogo")
174 if status, out := create("mine-titogo"); status != http.StatusCreated {
175 t.Fatalf("create over this app's own claim = %d %s, want 201", status, out)
176 }
177
178 if _, err := fx.rel.Put(ctx, domains.ClaimKey("junk-titogo."+domain), []byte("not json"), ""); err != nil {
179 t.Fatal(err)
180 }
181 if status, out := create("junk-titogo"); status != http.StatusInternalServerError {
182 t.Fatalf("create with an unreadable claim = %d %s, want 500 (fail closed)", status, out)
183 }
184 if _, err := provision.GetRecord(ctx, fx.rel, release.OpsAccount, "titogo", "probe-titogo."+domain); err == nil {
185 t.Fatal("a refused create left a record behind")
186 }
187}
188