| 1 | package provision |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "errors" |
| 6 | "testing" |
| 7 | |
| 8 | "github.com/carlosframework/platform/internal/registry" |
| 9 | ) |
| 10 | |
| 11 | // Issue #213: a box mints an instance's record only if it can provide that |
| 12 | // instance's config. ReconcileConfig.ConfigGate is the seam; the host agent |
| 13 | // wires it to the same guard the activator asks before taking a lease. |
| 14 | |
| 15 | type gateLog struct { |
| 16 | asked []registry.Route |
| 17 | refuse map[string]error // host -> refusal |
| 18 | } |
| 19 | |
| 20 | func (g *gateLog) gate(_ context.Context, rt registry.Route) error { |
| 21 | g.asked = append(g.asked, rt) |
| 22 | return g.refuse[rt.Host] |
| 23 | } |
| 24 | |
| 25 | func TestReconcileDoesNotMintWhatTheGateRefuses(t *testing.T) { |
| 26 | s, sqid := setup(t) |
| 27 | if err := CreateRecord(ctx, s, acmeRecord(sqid)); err != nil { |
| 28 | t.Fatal(err) |
| 29 | } |
| 30 | reg := regWith(t) |
| 31 | g := &gateLog{refuse: map[string]error{"acme.go.tito.io": errors.New("no key")}} |
| 32 | cfg := ReconcileConfig{Region: "ie", Pools: []string{"tito"}, DataDir: "/data", ConfigGate: g.gate} |
| 33 | |
| 34 | if err := Reconcile(ctx, s, reg, cfg); err != nil { |
| 35 | t.Fatal(err) |
| 36 | } |
| 37 | noRoute(t, reg, "acme.go.tito.io", "a refused record was minted") |
| 38 | if len(g.asked) != 1 { |
| 39 | t.Fatalf("gate asked %d times, want 1", len(g.asked)) |
| 40 | } |
| 41 | // The gate sees the route that WOULD be minted: the activator's own gate |
| 42 | // reads App, Account, Environment off it. |
| 43 | if got := g.asked[0]; got.App != "tito" || got.Account != sqid || got.Kind != "instance" || got.Socket == "" { |
| 44 | t.Errorf("gate saw %+v, want the would-be minted socket-backed instance route", got) |
| 45 | } |
| 46 | |
| 47 | // Retried next pass: once the box can provide the config, it mints. |
| 48 | delete(g.refuse, "acme.go.tito.io") |
| 49 | if err := Reconcile(ctx, s, reg, cfg); err != nil { |
| 50 | t.Fatal(err) |
| 51 | } |
| 52 | hasRoute(t, reg, "acme.go.tito.io", "not minted once the gate allowed it") |
| 53 | } |
| 54 | |
| 55 | // A nil gate is today's behaviour exactly. |
| 56 | func TestReconcileNilGateMintsAsBefore(t *testing.T) { |
| 57 | s, sqid := setup(t) |
| 58 | if err := CreateRecord(ctx, s, acmeRecord(sqid)); err != nil { |
| 59 | t.Fatal(err) |
| 60 | } |
| 61 | reg := regWith(t) |
| 62 | if err := Reconcile(ctx, s, reg, ReconcileConfig{Region: "ie", Pools: []string{"tito"}, DataDir: "/data"}); err != nil { |
| 63 | t.Fatal(err) |
| 64 | } |
| 65 | hasRoute(t, reg, "acme.go.tito.io", "nil gate stopped a mint") |
| 66 | } |
| 67 | |
| 68 | // The gate is asked about every minted shape and decides itself which need |
| 69 | // config; a refusal of one record leaves the others minted, and an existing |
| 70 | // route is never unminted by a refusal (the wake gate is what protects it). |
| 71 | func TestReconcileGateRefusesOnlyTheRecordItNames(t *testing.T) { |
| 72 | s, sqid := setup(t) |
| 73 | a := acmeRecord(sqid) |
| 74 | b := acmeRecord(sqid) |
| 75 | b.Host, b.Slug = "beta.go.tito.io", "beta" |
| 76 | for _, r := range []Record{a, b} { |
| 77 | if err := CreateRecord(ctx, s, r); err != nil { |
| 78 | t.Fatal(err) |
| 79 | } |
| 80 | } |
| 81 | reg := regWith(t) |
| 82 | g := &gateLog{refuse: map[string]error{"beta.go.tito.io": errors.New("cannot open")}} |
| 83 | cfg := ReconcileConfig{Region: "ie", Pools: []string{"tito"}, DataDir: "/data", ConfigGate: g.gate} |
| 84 | if err := Reconcile(ctx, s, reg, cfg); err != nil { |
| 85 | t.Fatal(err) |
| 86 | } |
| 87 | hasRoute(t, reg, "acme.go.tito.io", "an allowed record was held back by its neighbour's refusal") |
| 88 | noRoute(t, reg, "beta.go.tito.io", "a refused record was minted") |
| 89 | |
| 90 | // Already minted, then refused: the route stays (removal is only for a |
| 91 | // retired record). |
| 92 | g.refuse["acme.go.tito.io"] = errors.New("now cannot open") |
| 93 | if err := Reconcile(ctx, s, reg, cfg); err != nil { |
| 94 | t.Fatal(err) |
| 95 | } |
| 96 | hasRoute(t, reg, "acme.go.tito.io", "a refusal unminted an existing route") |
| 97 | } |
| 98 | |
| 99 | func TestSidecarReconcileDoesNotMintWhatTheGateRefuses(t *testing.T) { |
| 100 | f := sidecarSetup(t) |
| 101 | f.putManifest(t, "eleven", SidecarManifest{ |
| 102 | Schema: 1, App: "eleven", Args: []string{"sidecar", "run"}, Channel: "stable"}) |
| 103 | f.putRecord(t, SidecarRecord{Schema: 1, Name: "bot", App: "eleven", Channel: "stable"}) |
| 104 | host := f.host("bot", "eleven") |
| 105 | g := &gateLog{refuse: map[string]error{host: errors.New("no key")}} |
| 106 | |
| 107 | f.reconcile(t, ReconcileConfig{ConfigGate: g.gate}) |
| 108 | noRoute(t, f.reg, host, "a refused sidecar was minted") |
| 109 | if len(g.asked) != 1 || g.asked[0].Kind != "sidecar" || g.asked[0].App != "eleven" { |
| 110 | t.Fatalf("gate asked %+v, want the would-be sidecar route", g.asked) |
| 111 | } |
| 112 | |
| 113 | delete(g.refuse, host) |
| 114 | f.reconcile(t, ReconcileConfig{ConfigGate: g.gate}) |
| 115 | hasRoute(t, f.reg, host, "sidecar not minted once the gate allowed it") |
| 116 | } |
| 117 | |