// canary_create_e2e_test.go package main import ( "context" "errors" "os" "strings" "testing" "time" "github.com/carlosframework/platform/internal/console" "github.com/carlosframework/platform/internal/provision" "github.com/carlosframework/platform/internal/release" ) // Discussion 207: `carlos canary` has not been able to CREATE a canary since // #384, because canaryHost built .. and the create door // takes one label before the domain. These drive the real CLI against a real // console, which is what the canary tests until now never did: every one of // them either refused before the create or found the record already there. // // The run itself ends in a watch timeout, since nothing serves the host in // a test; what is asserted is the record the create wrote. func TestCanaryCreateMakesAOneLabelHost(t *testing.T) { for _, region := range []string{"", "ie"} { name := "no region flag" if region != "" { name = "region flag" } t.Run(name, func(t *testing.T) { e := regionE2EConsole(t, "") ctx := context.Background() const host = "probe-titogo.bab.oncarlos.test" args := []string{"canary", "deploy", "--app", "titogo", "--name", "probe", "--console", e.url, "--version", "abc1234", "--timeout", "1s"} if region != "" { args = append(args, "--region", region) } out, _ := e.run(t, append(args, artifact(t))...) if strings.Contains(out, "invalid host") { t.Fatalf("the create door refused the canary's host:\n%s", out) } if n := e.creates.Load(); n != 1 { t.Fatalf("%d create request(s), want 1:\n%s", n, out) } rec, err := provision.GetRecord(ctx, e.store, release.OpsAccount, "titogo", host) if err != nil || rec.Channel != "canary/probe" { t.Fatalf("record at %s = %+v %v, want it on canary/probe\n%s", host, rec, err, out) } if _, err := provision.GetRecord(ctx, e.store, release.OpsAccount, "titogo", "probe.titogo.bab.oncarlos.test"); !errors.Is(err, os.ErrNotExist) { t.Fatalf("a two-label record exists: %v", err) } if !strings.Contains(out, "canary live at") && !strings.Contains(out, host) { t.Fatalf("the output never names the canary host %s:\n%s", host, out) } // A second run finds it by that host: nothing to create. // The re-run fails only on the watch, like the first: it reaches // the end, finds the record, and says so, instead of dying early // before a create it was never going to make. out, err = e.run(t, append(args, artifact(t))...) if n := e.creates.Load(); n != 1 { t.Fatalf("the re-run created again (%d creates):\n%s", n, out) } if !strings.Contains(out, "canary host "+host+" already follows canary/probe") || !strings.Contains(out, "promoted abc1234 to canary/probe") { t.Fatalf("the re-run did not reuse the host and promote (err=%v):\n%s", err, out) } // ls reads it back, and rm removes it by name. out, err = e.run(t, "canary", "ls", "--app", "titogo", "--console", e.url) if err != nil || !strings.Contains(out, host) || !strings.Contains(out, "canary/probe") { t.Fatalf("canary ls = %v\n%s, want the one-label host", err, out) } out, err = e.run(t, "canary", "rm", "--app", "titogo", "--name", "probe", "--console", e.url) if err != nil || !strings.Contains(out, "removed "+host) { t.Fatalf("canary rm = %v\n%s, want it to remove %s", err, out, host) } if _, err := provision.GetRecord(ctx, e.store, release.OpsAccount, "titogo", host); !errors.Is(err, os.ErrNotExist) { t.Fatalf("the record survived rm: %v", err) } }) } } // The canary name and the app share one 63-byte label now. A name that fits // on its own but not beside the app is refused before any write, with the // numbers in it. func TestCanaryTooLongForOneLabelRefusesBeforeAnyWrite(t *testing.T) { e := regionE2EConsole(t, "") long := strings.Repeat("a", 58) // 58 + "-" + "titogo" = 65 want := "canary: " + canaryLabelTooLong(long, "titogo") for run := 0; run < 2; run++ { out, err := e.run(t, "canary", "deploy", "--app", "titogo", "--name", long, "--console", e.url, "--version", "abc1234", "--timeout", "1s", artifact(t)) if err == nil || !hasRefusalLine(out, want) { t.Fatalf("run %d: err=%v out=%s, want the whole line %q", run, err, out, want) } } if e.writes.Load() != 0 || e.releases(t) != 0 { t.Fatalf("writes %d, releases %d; want nothing written", e.writes.Load(), e.releases(t)) } // The limit itself fits: 56 + 1 + 6 = 63. fits := strings.Repeat("a", 56) out, _ := e.run(t, "canary", "deploy", "--app", "titogo", "--name", fits, "--console", e.url, "--version", "abc1234", "--timeout", "1s", artifact(t)) if _, err := provision.GetRecord(context.Background(), e.store, release.OpsAccount, "titogo", fits+"-titogo.bab.oncarlos.test"); err != nil { t.Fatalf("a 63-byte canary label was not created: %v\n%s", err, out) } } // Flattening puts a canary where an ordinary instance of the same app can // already be: app "titogo" with a customer instance "probe-titogo". Before // the flattening a canary host was always one label deeper than any // instance, so reusing "the record on this host" could only ever reuse a // canary. Now it could hand a customer's instance to the canary channel, so // a record that is not on a canary/* channel is refused and left as it is. func TestCanaryDoesNotTakeOverAnInstanceAtItsHost(t *testing.T) { e := regionE2EConsole(t, "") ctx := context.Background() const host = "probe-titogo.bab.oncarlos.test" rec := provision.Record{Schema: 1, Host: host, App: "titogo", Account: release.OpsAccount, Channel: "stable", CreatedAt: time.Now().UTC()} if err := provision.CreateRecord(ctx, e.store, rec); err != nil { t.Fatal(err) } want := "canary: " + canaryHostTaken(host, "stable") out, err := e.run(t, "canary", "deploy", "--app", "titogo", "--name", "probe", "--console", e.url, "--version", "abc1234", "--timeout", "1s", artifact(t)) if err == nil || !hasRefusalLine(out, want) { t.Fatalf("err=%v out=%s, want the whole line %q", err, out, want) } got, err := provision.GetRecord(ctx, e.store, release.OpsAccount, "titogo", host) if err != nil || got.Channel != "stable" { t.Fatalf("the instance = %+v %v, want it still on stable", got, err) } if e.releases(t) != 0 { t.Fatal("a refused canary shipped a release") } } // App names may end in a dash and hostname labels may not, so an app called // "jam-" has no canary host for ANY canary name. It is refused before any // write, saying why, rather than failing at the create door with an // "invalid host" no other --name can fix. func TestCanaryRefusesAnAppWhoseNameEndsInADash(t *testing.T) { e := regionE2EConsole(t, "") if _, err := e.cst.CreateAppIn(console.OpsAccountID, "jam-", "paul@keymail.dev", time.Now()); err != nil { t.Fatal(err) } e.writes.Store(0) want := "canary: " + canaryAppEndsInDash("jam-") for run := 0; run < 2; run++ { out, err := e.run(t, "canary", "deploy", "--app", "jam-", "--name", "probe", "--console", e.url, "--version", "abc1234", "--timeout", "1s", artifact(t)) if err == nil || !hasRefusalLine(out, want) { t.Fatalf("run %d: err=%v out=%s, want the whole line %q", run, err, out, want) } } if e.writes.Load() != 0 || e.creates.Load() != 0 { t.Fatalf("writes %d, creates %d; want nothing written", e.writes.Load(), e.creates.Load()) } }