| 1 | package console |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "errors" |
| 6 | "net/http" |
| 7 | "os" |
| 8 | "strings" |
| 9 | "testing" |
| 10 | "time" |
| 11 | |
| 12 | "github.com/carlosframework/platform/internal/hostclaim" |
| 13 | "github.com/carlosframework/platform/internal/provision" |
| 14 | ) |
| 15 | |
| 16 | // mc19 is M-C19 for the fixture's host. |
| 17 | const mc19 = "acme.go.tito.io is being moved. Delete it after the move finishes, or abort the move first." |
| 18 | |
| 19 | func recordExists(t *testing.T, fx *consoleFixture) bool { |
| 20 | t.Helper() |
| 21 | _, err := provision.GetRecord(context.Background(), fx.rel, "bab", "titogo", "acme.go.tito.io") |
| 22 | if errors.Is(err, os.ErrNotExist) { |
| 23 | return false |
| 24 | } |
| 25 | if err != nil { |
| 26 | t.Fatal(err) |
| 27 | } |
| 28 | return true |
| 29 | } |
| 30 | |
| 31 | // Task 2b (preflight L6): with the switch on, a console without the host |
| 32 | // claim refuses to delete an instance whose record carries a live move, |
| 33 | // with M-C19, and deletes nothing. Once the move has closed the delete is |
| 34 | // main's. |
| 35 | func TestMoveDeleteRefusesDuringAMove(t *testing.T) { |
| 36 | fx, token, moveURL := moveDoorWithClaim(t, "code-inst-move-del") |
| 37 | instURL := strings.TrimSuffix(moveURL, "/move") |
| 38 | readMoveAnswer(t, http.MethodPost, moveURL, token, `{"region":"uk","cut":"manual"}`, http.StatusAccepted) |
| 39 | for _, state := range []string{provision.MovePreparing, provision.MoveCommitted, provision.MoveStuck} { |
| 40 | setMove(t, fx, func(rec *provision.Record) { rec.Move.State = state }) |
| 41 | if code, _, out := rawReq(t, http.MethodDelete, instURL, token, ""); code != http.StatusConflict || errorText(out) != mc19 { |
| 42 | t.Fatalf("delete during a move in %s = %d %s, want M-C19", state, code, out) |
| 43 | } |
| 44 | if !recordExists(t, fx) { |
| 45 | t.Fatalf("a refused delete in %s removed the record", state) |
| 46 | } |
| 47 | } |
| 48 | mv := recordMove(t, fx) |
| 49 | setMove(t, fx, func(rec *provision.Record) { rec.Move.State = provision.MoveAborting }) |
| 50 | if _, err := provision.SetRecordMove(context.Background(), fx.rel, "bab", "titogo", "acme.go.tito.io", |
| 51 | provision.MoveGuardedAbortComplete(provision.MoveGuard{ID: mv.ID, Incarnation: mv.Incarnation}, time.Now())); err != nil { |
| 52 | t.Fatal(err) |
| 53 | } |
| 54 | if code, _, out := rawReq(t, http.MethodDelete, instURL, token, ""); code != http.StatusOK { |
| 55 | t.Fatalf("delete after the move closed = %d %s", code, out) |
| 56 | } |
| 57 | if recordExists(t, fx) { |
| 58 | t.Fatal("the delete after the move closed left the record") |
| 59 | } |
| 60 | } |
| 61 | |
| 62 | // With the switch off the delete door is main's: a record with a move is |
| 63 | // deleted as before. |
| 64 | func TestMoveDeleteSwitchOffIsMains(t *testing.T) { |
| 65 | fx, token, moveURL := moveDoorWithClaim(t, "code-inst-move-del-off") |
| 66 | instURL := strings.TrimSuffix(moveURL, "/move") |
| 67 | setMove(t, fx, func(rec *provision.Record) { |
| 68 | rec.Move = &provision.MoveState{To: "uk", State: provision.MovePreparing, RequestedAt: time.Now().UTC()} |
| 69 | }) |
| 70 | fx.srv.cfg.MovesEnabled = false |
| 71 | if code, _, out := rawReq(t, http.MethodDelete, instURL, token, ""); code != http.StatusOK { |
| 72 | t.Fatalf("switch-off delete during a legacy move = %d %s", code, out) |
| 73 | } |
| 74 | if recordExists(t, fx) { |
| 75 | t.Fatal("the switch-off delete left the record") |
| 76 | } |
| 77 | } |
| 78 | |
| 79 | // On a host-claim console a delete during a move meets the claim's pending |
| 80 | // refusal; the door says M-C19 for it, not the generic busy sentence, when |
| 81 | // the claim's operation is the move's. |
| 82 | func TestMoveDeleteUnderHostClaimSaysItIsMoving(t *testing.T) { |
| 83 | fx, token, moveURL, _ := j17StartedMove(t, "code-inst-move-del-claim") |
| 84 | instURL := strings.TrimSuffix(moveURL, "/move") |
| 85 | code, _, out := rawReq(t, http.MethodDelete, instURL, token, "") |
| 86 | if code != http.StatusConflict || errorText(out) != mc19 { |
| 87 | t.Fatalf("delete under the host claim during a move = %d %s, want M-C19", code, out) |
| 88 | } |
| 89 | if !recordExists(t, fx) { |
| 90 | t.Fatal("a refused delete removed the record") |
| 91 | } |
| 92 | } |
| 93 | |
| 94 | // A host-claim console says M-C19 only for a move's claim operation: a |
| 95 | // serving claim, and a relocation with no move journaled on the record (a |
| 96 | // region HA relocation of its own), go on to the claim's retire. |
| 97 | func TestMoveDeleteUnderHostClaimOnlyForAMove(t *testing.T) { |
| 98 | fx, token, moveURL := moveDoorWithClaim(t, "code-inst-move-del-claim2") |
| 99 | armClaimMoves(t, fx, true, true, nil) |
| 100 | instURL := strings.TrimSuffix(moveURL, "/move") |
| 101 | if code, _, out := rawReq(t, http.MethodDelete, instURL, token, ""); errorText(out) == mc19 { |
| 102 | t.Fatalf("delete of a serving host under the host claim = %d %s", code, out) |
| 103 | } |
| 104 | rewriteClaim(t, fx, "acme.go.tito.io", func(c *hostclaim.Claim) { |
| 105 | c.Op = hostclaim.Op{Kind: hostclaim.KindRelocating, ID: "op-ha", Phase: "pinning", Exec: "none", Intent: map[string]string{"region": "ie", "source_box": "ie-box"}} |
| 106 | }) |
| 107 | if code, _, out := rawReq(t, http.MethodDelete, instURL, token, ""); errorText(out) == mc19 { |
| 108 | t.Fatalf("delete during a relocation that is not a move = %d %s", code, out) |
| 109 | } |
| 110 | } |
| 111 | |