fix: fall back from coordinator pool list
This commit is contained in:
parent
93a9e64998
commit
7884b1d71f
@ -18,6 +18,7 @@
|
||||
- Fixed Islo provider sync so `crabbox run --provider islo` uploads the local workspace, uses the correct `/workspace/<workdir>`, and falls back to chunked exec upload while the archive API returns server errors.
|
||||
- Fixed Code and WebVNC bridge websocket auth so upgraded brokers receive short-lived bridge tickets in the `Authorization` header instead of logging them in URL query strings, while preserving query fallback for older brokers.
|
||||
- Fixed managed AWS macOS desktop leases so readiness and WebVNC use a writable `ec2-user` work root, call `crabbox-ready` by absolute path, and read the generated Screen Sharing password via sudo.
|
||||
- Fixed coordinator-backed `crabbox list` so a stale admin token no longer blocks normal logged-in users; the CLI now falls back to active user-visible leases instead of failing with `401 unauthorized`.
|
||||
|
||||
## 0.6.0 - 2026-05-07
|
||||
|
||||
|
||||
@ -575,6 +575,25 @@ func (c *CoordinatorClient) Pool(ctx context.Context, cfg Config) ([]Coordinator
|
||||
return res.Machines, err
|
||||
}
|
||||
|
||||
func (c *CoordinatorClient) Leases(ctx context.Context, state string, limit int) ([]CoordinatorLease, error) {
|
||||
var res struct {
|
||||
Leases []CoordinatorLease `json:"leases"`
|
||||
}
|
||||
values := url.Values{}
|
||||
if state != "" {
|
||||
values.Set("state", state)
|
||||
}
|
||||
if limit > 0 {
|
||||
values.Set("limit", strconv.Itoa(limit))
|
||||
}
|
||||
path := "/v1/leases"
|
||||
if encoded := values.Encode(); encoded != "" {
|
||||
path += "?" + encoded
|
||||
}
|
||||
err := c.do(ctx, http.MethodGet, path, nil, &res)
|
||||
return res.Leases, err
|
||||
}
|
||||
|
||||
func (c *CoordinatorClient) Usage(ctx context.Context, scope, owner, org, month string) (CoordinatorUsageResponse, error) {
|
||||
var res CoordinatorUsageResponse
|
||||
values := url.Values{}
|
||||
|
||||
@ -39,6 +39,16 @@ func (a App) list(ctx context.Context, args []string) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if *jsonOut {
|
||||
if jsonBackend, ok := backend.(JSONListBackend); ok {
|
||||
view, err := jsonBackend.ListJSON(ctx, ListRequest{Options: leaseOptionsFromConfig(cfg)})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
a.syncExternalRunnersBestEffort(ctx, cfg, backend)
|
||||
return json.NewEncoder(a.Stdout).Encode(view)
|
||||
}
|
||||
}
|
||||
var servers []Server
|
||||
switch b := backend.(type) {
|
||||
case SSHLeaseBackend:
|
||||
@ -53,13 +63,6 @@ func (a App) list(ctx context.Context, args []string) error {
|
||||
}
|
||||
a.syncExternalRunnersBestEffort(ctx, cfg, backend)
|
||||
if *jsonOut {
|
||||
if jsonBackend, ok := backend.(JSONListBackend); ok {
|
||||
view, err := jsonBackend.ListJSON(ctx, ListRequest{Options: leaseOptionsFromConfig(cfg)})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return json.NewEncoder(a.Stdout).Encode(view)
|
||||
}
|
||||
return json.NewEncoder(a.Stdout).Encode(servers)
|
||||
}
|
||||
renderServerList(a.Stdout, servers)
|
||||
|
||||
@ -132,7 +132,11 @@ func (b *coordinatorLeaseBackend) Status(ctx context.Context, req StatusRequest)
|
||||
func (b *coordinatorLeaseBackend) List(ctx context.Context, req ListRequest) ([]Server, error) {
|
||||
machines, activeLeaseIDs, err := b.listMachines(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
leases, fallbackErr := b.listLeasesFallback(ctx, err)
|
||||
if fallbackErr != nil {
|
||||
return nil, fallbackErr
|
||||
}
|
||||
return coordinatorLeasesToServers(leases, b.cfg), nil
|
||||
}
|
||||
return coordinatorMachinesToServers(machines, activeLeaseIDs), nil
|
||||
}
|
||||
@ -141,7 +145,7 @@ func (b *coordinatorLeaseBackend) ListJSON(ctx context.Context, req ListRequest)
|
||||
_ = req
|
||||
machines, _, err := b.listMachines(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return b.listLeasesFallback(ctx, err)
|
||||
}
|
||||
return machines, nil
|
||||
}
|
||||
@ -168,6 +172,51 @@ func (b *coordinatorLeaseBackend) listMachines(ctx context.Context) ([]Coordinat
|
||||
return machines, activeCoordinatorLeaseIDs(activeLeases), nil
|
||||
}
|
||||
|
||||
func (b *coordinatorLeaseBackend) listLeasesFallback(ctx context.Context, adminErr error) ([]CoordinatorLease, error) {
|
||||
if b.cfg.CoordToken == "" {
|
||||
return nil, adminErr
|
||||
}
|
||||
if adminErr != nil && isCoordinatorUnauthorized(adminErr) {
|
||||
fmt.Fprintf(b.rt.Stderr, "warning: coordinator admin pool list unauthorized; falling back to user-visible leases\n")
|
||||
} else if adminErr != nil && b.cfg.CoordAdminToken == "" {
|
||||
fmt.Fprintf(b.rt.Stderr, "warning: coordinator admin pool list unavailable; falling back to user-visible leases\n")
|
||||
} else if adminErr != nil {
|
||||
return nil, adminErr
|
||||
}
|
||||
leases, err := b.coord.Leases(ctx, "active", 1000)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return filterCoordinatorLeasesForProvider(leases, b.cfg.Provider), nil
|
||||
}
|
||||
|
||||
func coordinatorLeasesToServers(leases []CoordinatorLease, cfg Config) []Server {
|
||||
servers := make([]Server, 0, len(leases))
|
||||
for _, lease := range leases {
|
||||
server, _, _ := leaseToServerTarget(lease, cfg)
|
||||
servers = append(servers, server)
|
||||
}
|
||||
return servers
|
||||
}
|
||||
|
||||
func filterCoordinatorLeasesForProvider(leases []CoordinatorLease, provider string) []CoordinatorLease {
|
||||
provider = strings.TrimSpace(provider)
|
||||
if provider == "" {
|
||||
return leases
|
||||
}
|
||||
out := make([]CoordinatorLease, 0, len(leases))
|
||||
for _, lease := range leases {
|
||||
if strings.EqualFold(strings.TrimSpace(lease.Provider), provider) {
|
||||
out = append(out, lease)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func isCoordinatorUnauthorized(err error) bool {
|
||||
return err != nil && strings.Contains(err.Error(), "http 401")
|
||||
}
|
||||
|
||||
func (b *coordinatorLeaseBackend) ReleaseLease(ctx context.Context, req ReleaseLeaseRequest) error {
|
||||
if req.Lease.LeaseID == "" {
|
||||
return exit(2, "missing coordinator lease id")
|
||||
|
||||
115
internal/cli/provider_coordinator_test.go
Normal file
115
internal/cli/provider_coordinator_test.go
Normal file
@ -0,0 +1,115 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestCoordinatorListFallsBackToUserLeasesWhenAdminTokenUnauthorized(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.URL.Path {
|
||||
case "/v1/pool":
|
||||
if got := r.Header.Get("Authorization"); got != "Bearer stale-admin-token" {
|
||||
t.Fatalf("pool auth=%q", got)
|
||||
}
|
||||
http.Error(w, `{"error":"unauthorized"}`, http.StatusUnauthorized)
|
||||
case "/v1/leases":
|
||||
if got := r.URL.Query().Get("state"); got != "active" {
|
||||
t.Fatalf("leases state=%q", got)
|
||||
}
|
||||
if got := r.Header.Get("Authorization"); got != "Bearer user-token" {
|
||||
t.Fatalf("leases auth=%q", got)
|
||||
}
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{"leases": []CoordinatorLease{
|
||||
{
|
||||
ID: "cbx_123",
|
||||
Slug: "blue-lobster",
|
||||
Provider: "aws",
|
||||
TargetOS: targetLinux,
|
||||
ServerID: 42,
|
||||
CloudID: "i-123",
|
||||
ServerName: "crabbox-blue-lobster",
|
||||
Host: "203.0.113.10",
|
||||
SSHUser: "crabbox",
|
||||
SSHPort: "2222",
|
||||
ServerType: "c7a.48xlarge",
|
||||
State: "active",
|
||||
Keep: true,
|
||||
ExpiresAt: "2026-05-07T15:00:00Z",
|
||||
IdleTimeoutSeconds: 1800,
|
||||
},
|
||||
{ID: "cbx_other", Provider: "hetzner", State: "active"},
|
||||
}})
|
||||
default:
|
||||
http.NotFound(w, r)
|
||||
}
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
var stderr bytes.Buffer
|
||||
cfg := Config{
|
||||
Provider: "aws",
|
||||
TargetOS: targetLinux,
|
||||
Coordinator: server.URL,
|
||||
CoordToken: "user-token",
|
||||
CoordAdminToken: "stale-admin-token",
|
||||
}
|
||||
coord, _, err := newCoordinatorClient(cfg)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
backend := &coordinatorLeaseBackend{cfg: cfg, coord: coord, rt: Runtime{Stderr: &stderr}}
|
||||
|
||||
servers, err := backend.List(context.Background(), ListRequest{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(servers) != 1 {
|
||||
t.Fatalf("servers=%d, want 1: %#v", len(servers), servers)
|
||||
}
|
||||
if servers[0].Labels["lease"] != "cbx_123" || servers[0].Labels["slug"] != "blue-lobster" {
|
||||
t.Fatalf("server labels=%#v", servers[0].Labels)
|
||||
}
|
||||
if !strings.Contains(stderr.String(), "falling back to user-visible leases") {
|
||||
t.Fatalf("missing fallback warning: %q", stderr.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestCoordinatorListJSONFallsBackWhenAdminTokenMissing(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/v1/leases" {
|
||||
t.Fatalf("unexpected path %s", r.URL.Path)
|
||||
}
|
||||
if got := r.URL.Query().Get("state"); got != "active" {
|
||||
t.Fatalf("leases state=%q", got)
|
||||
}
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{"leases": []CoordinatorLease{
|
||||
{ID: "cbx_123", Provider: "aws", State: "active"},
|
||||
}})
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
cfg := Config{Provider: "aws", TargetOS: targetLinux, Coordinator: server.URL, CoordToken: "user-token"}
|
||||
coord, _, err := newCoordinatorClient(cfg)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
backend := &coordinatorLeaseBackend{cfg: cfg, coord: coord, rt: Runtime{Stderr: &bytes.Buffer{}}}
|
||||
|
||||
view, err := backend.ListJSON(context.Background(), ListRequest{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
leases, ok := view.([]CoordinatorLease)
|
||||
if !ok {
|
||||
t.Fatalf("view=%T, want []CoordinatorLease", view)
|
||||
}
|
||||
if len(leases) != 1 || leases[0].ID != "cbx_123" {
|
||||
t.Fatalf("leases=%#v", leases)
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user