| 1 | package main |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "encoding/json" |
| 6 | "fmt" |
| 7 | "log" |
| 8 | "net/http" |
| 9 | "net/url" |
| 10 | "os" |
| 11 | |
| 12 | "github.com/carlosframework/platform/internal/host" |
| 13 | ) |
| 14 | |
| 15 | // carlos system box role <box> <role> and carlos system group role |
| 16 | // <region> <role> (region HA spec 2 §7, A11-1). A box's role says whether |
| 17 | // placement may choose it: legacy and platform boxes take no new |
| 18 | // instances and keep the ones they have. A group's role is every box it |
| 19 | // has enrolled and every box it enrolls later. Operators only, and only on |
| 20 | // a console with region HA on. |
| 21 | |
| 22 | // rolePost is the one console call a role change makes. |
| 23 | type rolePost func(ctx context.Context, path string, in any) (int, []byte, error) |
| 24 | |
| 25 | // boxRoleLine is what the CLI says once a box has its role. |
| 26 | func boxRoleLine(box, role string) string { return fmt.Sprintf("%s now has the %s role", box, role) } |
| 27 | |
| 28 | // groupRoleLine is what it says once a group has its role. |
| 29 | func groupRoleLine(region, role string) string { |
| 30 | return fmt.Sprintf("the group in %s now has the %s role", region, role) |
| 31 | } |
| 32 | |
| 33 | // setBoxRole asks the console to give box the role, and returns the line |
| 34 | // to print. |
| 35 | func setBoxRole(ctx context.Context, post rolePost, box, role string) (string, error) { |
| 36 | status, body, err := post(ctx, "/api/cli/system/boxes/"+url.PathEscape(box)+"/role", map[string]string{"role": role}) |
| 37 | if err != nil { |
| 38 | return "", err |
| 39 | } |
| 40 | if status != http.StatusOK { |
| 41 | return "", apiErrorFromBody(status, body) |
| 42 | } |
| 43 | var out struct { |
| 44 | Box string `json:"box"` |
| 45 | Role string `json:"role"` |
| 46 | } |
| 47 | if err := json.Unmarshal(body, &out); err != nil { |
| 48 | return "", fmt.Errorf("unreadable response: %w", err) |
| 49 | } |
| 50 | return boxRoleLine(out.Box, out.Role), nil |
| 51 | } |
| 52 | |
| 53 | // setGroupRole asks the console to give region's group the role, and |
| 54 | // returns the lines to print: the group's, then one per box it set. |
| 55 | func setGroupRole(ctx context.Context, post rolePost, region, role string) ([]string, error) { |
| 56 | status, body, err := post(ctx, "/api/cli/system/groups/"+url.PathEscape(region)+"/role", map[string]string{"role": role}) |
| 57 | if err != nil { |
| 58 | return nil, err |
| 59 | } |
| 60 | if status != http.StatusOK { |
| 61 | return nil, apiErrorFromBody(status, body) |
| 62 | } |
| 63 | var out struct { |
| 64 | Region string `json:"region"` |
| 65 | Role string `json:"role"` |
| 66 | Boxes []string `json:"boxes"` |
| 67 | } |
| 68 | if err := json.Unmarshal(body, &out); err != nil { |
| 69 | return nil, fmt.Errorf("unreadable response: %w", err) |
| 70 | } |
| 71 | lines := []string{groupRoleLine(out.Region, out.Role)} |
| 72 | for _, b := range out.Boxes { |
| 73 | lines = append(lines, boxRoleLine(b, out.Role)) |
| 74 | } |
| 75 | return lines, nil |
| 76 | } |
| 77 | |
| 78 | // roleArgs reads `role <name> <role>` with an optional --console flag |
| 79 | // after them, and exits with the usage on anything else. |
| 80 | func roleArgs(rest []string) (name, role, console string) { |
| 81 | if len(rest) < 3 || rest[0] != "role" { |
| 82 | systemUsage() |
| 83 | os.Exit(2) |
| 84 | } |
| 85 | name, role = rest[1], rest[2] |
| 86 | tail := rest[3:] |
| 87 | switch { |
| 88 | case len(tail) == 0: |
| 89 | case len(tail) == 2 && (tail[0] == "--console" || tail[0] == "-console"): |
| 90 | console = tail[1] |
| 91 | default: |
| 92 | systemUsage() |
| 93 | os.Exit(2) |
| 94 | } |
| 95 | return name, role, console |
| 96 | } |
| 97 | |
| 98 | func runSystemBoxVerb(rest []string) { |
| 99 | if len(rest) > 0 && rest[0] == "publish-record" { |
| 100 | // On the box itself, not through the console (internal/host |
| 101 | // boxpublish.go). |
| 102 | host.RunSystemBoxPublishRecord(rest[1:]) |
| 103 | return |
| 104 | } |
| 105 | box, role, consoleFlag := roleArgs(rest) |
| 106 | sess := requireDeployAPISession(consoleFlag) |
| 107 | printUsing(stdout, sess) |
| 108 | line, err := setBoxRole(context.Background(), sess.postJSON, box, role) |
| 109 | if err != nil { |
| 110 | log.Fatalf("carlos system box role: %v", err) |
| 111 | } |
| 112 | fmt.Fprintln(stdout, line) |
| 113 | } |
| 114 | |
| 115 | func runSystemGroupVerb(rest []string) { |
| 116 | region, role, consoleFlag := roleArgs(rest) |
| 117 | sess := requireDeployAPISession(consoleFlag) |
| 118 | printUsing(stdout, sess) |
| 119 | lines, err := setGroupRole(context.Background(), sess.postJSON, region, role) |
| 120 | if err != nil { |
| 121 | log.Fatalf("carlos system group role: %v", err) |
| 122 | } |
| 123 | for _, l := range lines { |
| 124 | fmt.Fprintln(stdout, l) |
| 125 | } |
| 126 | } |
| 127 | |