Skip to content

Add join-token CLI command #69

@NavarrePratt

Description

@NavarrePratt

Summary

Add a cwsandbox join-token command that creates a Tower join token via the ATC API. This replaces the manual curl workflow documented in the Sandbox Tower Helm chart installation guide, giving operators a single command to generate tokens for new Tower deployments.

Context: Join tokens are currently created by manually calling the ATC REST API (POST /v1beta1/towers/tokens). Brandon identified the need for a CLI helper in [Slack discussion] after manually distributing tokens to staging towers. This is a natural fit for the cwsandbox CLI.

Blocked by: #10 / PR #60 (base CLI with ls and exec commands)

Motivation

When deploying the Sandbox Tower Helm chart to a new cluster, operators need a join token. Today this requires:

  1. Constructing a curl POST request with the correct endpoint, headers, and JSON body
  2. Extracting the token from the JSON response
  3. Figuring out the right kubectl and helm commands to apply it

A CLI command makes this workflow self-documenting and reduces mistakes during Tower onboarding.

Proposed UX

cwsandbox join-token \
  --tower-id my-cluster-east \
  --tower-group-id production \
  --ttl 3600 \
  --description "Join token for my-cluster-east"

Default output prints the token value and the follow-up commands needed to use it:

Join token created successfully.

  Token:     <token-value>
  Token ID:  a1b2c3d4-...
  Tower:     my-cluster-east
  Group:     production
  Expires:   2026-02-28T15:02:21Z

To use this token, run:

  kubectl create namespace sandbox-system

  kubectl create secret generic sandbox-tower-join-token \
    --namespace sandbox-system \
    --from-literal=token='<token-value>'

Then install the Helm chart:

  helm install sandbox-tower coreweave/sandbox-tower \
    --namespace sandbox-system \
    -f sandbox-tower-values.yaml

With --json flag, output the raw API response for scripting:

{
  "token": "...",
  "tokenId": "a1b2c3d4-...",
  "towerId": "my-cluster-east",
  "towerGroupId": "production",
  "expiresAt": "2026-02-28T15:02:21Z",
  "organizationId": "org-..."
}

Flags

Flag Required Default Description
--tower-id Yes - Unique identifier for the Tower (e.g. cluster name). Must be unique per org.
--tower-group-id No "default" Group identifier for organizing related Towers
--ttl No 3600 (1h) Token TTL in seconds
--description No Auto-generated from tower-id Human-readable description
--label No - Key=value label, repeatable (e.g. --label env=prod --label team=infra)
--json No false Output raw JSON response

API Reference

Source: aviato/proto/coreweave/aviato/v1beta1/tower_join.proto

POST /v1beta1/towers/tokens - Create

Request:

tower_id        string              REQUIRED, must be unique per org
tower_group_id  string              optional, defaults to "default"
ttl_seconds     int32               optional, defaults to 3600 (1 hour)
description     string              optional
labels          map<string, string> optional

Response:

token           string              the join token (single-use, treat as secret)
token_id        string              UUID for tracking
tower_id        string
tower_group_id  string
expires_at      Timestamp
organization_id string

Behavior:

  • One active token per tower per org. Creating a new token auto-revokes the previous active token for that tower.
  • Token is single-use: marked "used" after the Tower exchanges it for mTLS certs via POST /v1beta1/towers/join.

Token lifecycle

States: active -> used | revoked | expired

  • active: newly created, not yet exchanged
  • used: Tower exchanged the token for mTLS certs
  • revoked: explicitly revoked by user
  • expired: TTL elapsed (evaluated lazily at query time)

Implementation Notes

  • This is an HTTP REST call, not gRPC. The existing SDK uses gRPC for sandbox operations, but the join token endpoint is REST. Use httpx (already a dependency) or requests.
  • Authentication should reuse the existing CWSANDBOX_API_KEY path from _auth.py. W&B auth is not relevant for this admin operation.
  • The command is an admin/operator action, distinct from the sandbox lifecycle commands (ls, exec). Bare cwsandbox join-token is sufficient for now; if list/revoke are added later, make bare invocation an alias for join-token create so existing usage doesn't break.
  • The token value is a secret: consider warning if outputting to a terminal.

Future Work

  • cwsandbox join-token list - list tokens with status filtering (GET /v1beta1/towers/tokens)
  • cwsandbox join-token revoke <token-id> - revoke active tokens (DELETE /v1beta1/towers/tokens/{token_id})

The API already supports both operations.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions