package main import ( "context" "encoding/json" "fmt" "log" "net/http" "net/url" "os" "github.com/carlosframework/platform/internal/host" ) // carlos system box role and carlos system group role // (region HA spec 2 ยง7, A11-1). A box's role says whether // placement may choose it: legacy and platform boxes take no new // instances and keep the ones they have. A group's role is every box it // has enrolled and every box it enrolls later. Operators only, and only on // a console with region HA on. // rolePost is the one console call a role change makes. type rolePost func(ctx context.Context, path string, in any) (int, []byte, error) // boxRoleLine is what the CLI says once a box has its role. func boxRoleLine(box, role string) string { return fmt.Sprintf("%s now has the %s role", box, role) } // groupRoleLine is what it says once a group has its role. func groupRoleLine(region, role string) string { return fmt.Sprintf("the group in %s now has the %s role", region, role) } // setBoxRole asks the console to give box the role, and returns the line // to print. func setBoxRole(ctx context.Context, post rolePost, box, role string) (string, error) { status, body, err := post(ctx, "/api/cli/system/boxes/"+url.PathEscape(box)+"/role", map[string]string{"role": role}) if err != nil { return "", err } if status != http.StatusOK { return "", apiErrorFromBody(status, body) } var out struct { Box string `json:"box"` Role string `json:"role"` } if err := json.Unmarshal(body, &out); err != nil { return "", fmt.Errorf("unreadable response: %w", err) } return boxRoleLine(out.Box, out.Role), nil } // setGroupRole asks the console to give region's group the role, and // returns the lines to print: the group's, then one per box it set. func setGroupRole(ctx context.Context, post rolePost, region, role string) ([]string, error) { status, body, err := post(ctx, "/api/cli/system/groups/"+url.PathEscape(region)+"/role", map[string]string{"role": role}) if err != nil { return nil, err } if status != http.StatusOK { return nil, apiErrorFromBody(status, body) } var out struct { Region string `json:"region"` Role string `json:"role"` Boxes []string `json:"boxes"` } if err := json.Unmarshal(body, &out); err != nil { return nil, fmt.Errorf("unreadable response: %w", err) } lines := []string{groupRoleLine(out.Region, out.Role)} for _, b := range out.Boxes { lines = append(lines, boxRoleLine(b, out.Role)) } return lines, nil } // roleArgs reads `role ` with an optional --console flag // after them, and exits with the usage on anything else. func roleArgs(rest []string) (name, role, console string) { if len(rest) < 3 || rest[0] != "role" { systemUsage() os.Exit(2) } name, role = rest[1], rest[2] tail := rest[3:] switch { case len(tail) == 0: case len(tail) == 2 && (tail[0] == "--console" || tail[0] == "-console"): console = tail[1] default: systemUsage() os.Exit(2) } return name, role, console } func runSystemBoxVerb(rest []string) { if len(rest) > 0 && rest[0] == "publish-record" { // On the box itself, not through the console (internal/host // boxpublish.go). host.RunSystemBoxPublishRecord(rest[1:]) return } box, role, consoleFlag := roleArgs(rest) sess := requireDeployAPISession(consoleFlag) printUsing(stdout, sess) line, err := setBoxRole(context.Background(), sess.postJSON, box, role) if err != nil { log.Fatalf("carlos system box role: %v", err) } fmt.Fprintln(stdout, line) } func runSystemGroupVerb(rest []string) { region, role, consoleFlag := roleArgs(rest) sess := requireDeployAPISession(consoleFlag) printUsing(stdout, sess) lines, err := setGroupRole(context.Background(), sess.postJSON, region, role) if err != nil { log.Fatalf("carlos system group role: %v", err) } for _, l := range lines { fmt.Fprintln(stdout, l) } }