refactor: route cli commands through kong
This commit is contained in:
parent
5c19a4d39a
commit
352a6e1618
@ -4,6 +4,7 @@
|
||||
|
||||
### Added
|
||||
|
||||
- Added generated command help for grouped commands so `crabbox actions --help`, `crabbox cache --help`, `crabbox desktop --help`, and similar entrypoints exit cleanly.
|
||||
- Added optional Tailscale reachability for managed Linux leases with `--tailscale`, `--network auto|tailscale|public`, brokered OAuth auth-key minting, and non-secret tailnet metadata in status/inspect output.
|
||||
- Added managed AWS Windows desktop leases with OpenSSH, Git for Windows, loopback TightVNC, per-lease VNC passwords, and `crabbox vnc`.
|
||||
- Added AWS macOS desktop lease plumbing for EC2 Mac Dedicated Hosts, including Screen Sharing setup and per-lease credentials.
|
||||
@ -17,9 +18,14 @@
|
||||
- Added generated Windows console login details and auto-logon for managed AWS Windows desktop leases.
|
||||
- Added a minimal XFCE desktop profile with panel/window manager for managed VNC leases.
|
||||
|
||||
### Changed
|
||||
|
||||
- Switched top-level CLI routing to Kong while preserving existing per-command flags, passthrough remote commands, aliases, and exit-code behavior.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed `crabbox run --junit` so all-passing JUnit files record results instead of leaving the coordinator run stuck when the failure list is empty.
|
||||
- Fixed `crabbox desktop launch --browser` on freshly warmed desktop leases by creating the remote workdir before launching the app.
|
||||
- Fixed failed Blacksmith Testbox warmups so printed or newly listed `tbx_...` boxes are stopped instead of being left queued after an upstream workflow error.
|
||||
- Fixed Worker deploy smoke to prefer the Crabbox-scoped Cloudflare token when it is present in the environment or local profile.
|
||||
- Fixed brokered Tailscale requests on coordinators without OAuth secrets so they fail as disabled instead of entering the auth-key minting path.
|
||||
|
||||
@ -10,7 +10,7 @@ This page maps user-facing behavior back to implementation files. Keep docs desc
|
||||
|
||||
## CLI Surface
|
||||
|
||||
- Command router and top-level help: `internal/cli/app.go`
|
||||
- Kong command router and top-level help: `internal/cli/cli_kong.go`, `internal/cli/app.go`
|
||||
- Shared flag parsing and exit helpers: `internal/cli/flags.go`, `internal/cli/errors.go`
|
||||
- Config defaults, YAML keys, env overrides, target selection, and class maps: `internal/cli/config.go`, `internal/cli/target.go`, `worker/src/config.ts`
|
||||
- Network target resolution and Tailscale metadata: `internal/cli/network.go`
|
||||
|
||||
1
go.mod
1
go.mod
@ -5,6 +5,7 @@ go 1.26
|
||||
toolchain go1.26.2
|
||||
|
||||
require (
|
||||
github.com/alecthomas/kong v1.15.0
|
||||
github.com/aws/aws-sdk-go-v2 v1.41.7
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.17
|
||||
github.com/aws/aws-sdk-go-v2/service/ec2 v1.299.1
|
||||
|
||||
8
go.sum
8
go.sum
@ -1,3 +1,9 @@
|
||||
github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=
|
||||
github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
|
||||
github.com/alecthomas/kong v1.15.0 h1:BVJstKbpO73zKpmIu+m/aLRrNmWwxXPIGTNin9VmLVI=
|
||||
github.com/alecthomas/kong v1.15.0/go.mod h1:wrlbXem1CWqUV5Vbmss5ISYhsVPkBb1Yo7YKJghju2I=
|
||||
github.com/alecthomas/repr v0.5.2 h1:SU73FTI9D1P5UNtvseffFSGmdNci/O6RsqzeXJtP0Qs=
|
||||
github.com/alecthomas/repr v0.5.2/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
|
||||
github.com/aws/aws-sdk-go-v2 v1.41.7 h1:DWpAJt66FmnnaRIOT/8ASTucrvuDPZASqhhLey6tLY8=
|
||||
github.com/aws/aws-sdk-go-v2 v1.41.7/go.mod h1:4LAfZOPHNVNQEckOACQx60Y8pSRjIkNZQz1w92xpMJc=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.17 h1:FpL4/758/diKwqbytU0prpuiu60fgXKUWCpDJtApclU=
|
||||
@ -29,6 +35,8 @@ github.com/aws/aws-sdk-go-v2/service/sts v1.42.1/go.mod h1:mTNxImtovCOEEuD65mKW7
|
||||
github.com/aws/smithy-go v1.25.1 h1:J8ERsGSU7d+aCmdQur5Txg6bVoYelvQJgtZehD12GkI=
|
||||
github.com/aws/smithy-go v1.25.1/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
|
||||
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
|
||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
|
||||
@ -27,68 +27,6 @@ func (r GitHubRepo) Slug() string {
|
||||
return r.Owner + "/" + r.Name
|
||||
}
|
||||
|
||||
func (a App) actions(ctx context.Context, args []string) error {
|
||||
if len(args) == 0 {
|
||||
a.printActionsHelp()
|
||||
return exit(2, "missing actions subcommand")
|
||||
}
|
||||
if wantsHelp(args) {
|
||||
a.printActionsHelp()
|
||||
return nil
|
||||
}
|
||||
switch args[0] {
|
||||
case "hydrate":
|
||||
return a.actionsHydrate(ctx, args[1:])
|
||||
case "register":
|
||||
return a.actionsRegister(ctx, args[1:])
|
||||
case "dispatch":
|
||||
return a.actionsDispatch(ctx, args[1:])
|
||||
default:
|
||||
return exit(2, "unknown actions command %q", args[0])
|
||||
}
|
||||
}
|
||||
|
||||
func (a App) printActionsHelp() {
|
||||
fmt.Fprintln(a.Stdout, `Usage:
|
||||
crabbox actions hydrate --id <lease-id-or-slug> [flags]
|
||||
crabbox actions register --id <lease-id-or-slug> [flags]
|
||||
crabbox actions dispatch [flags]
|
||||
|
||||
Subcommands:
|
||||
hydrate Register a runner, dispatch the hydrate workflow, wait for readiness
|
||||
register Register an existing Linux lease as a GitHub Actions runner
|
||||
dispatch Dispatch the configured GitHub Actions workflow
|
||||
|
||||
Hydrate Flags:
|
||||
--id <lease-id-or-slug>
|
||||
--repo <owner/name>
|
||||
--workflow <file|name|id>
|
||||
--ref <ref>
|
||||
--wait-timeout <duration>
|
||||
--keep-alive-minutes <n>
|
||||
--reclaim
|
||||
--timing-json
|
||||
-f, --field <key=value>
|
||||
|
||||
Register Flags:
|
||||
--id <lease-id-or-slug>
|
||||
--repo <owner/name>
|
||||
--name <runner-name>
|
||||
--labels <csv>
|
||||
--version <version|latest>
|
||||
--ephemeral
|
||||
--reclaim
|
||||
|
||||
Dispatch Flags:
|
||||
--repo <owner/name>
|
||||
--workflow <file|name|id>
|
||||
--ref <ref>
|
||||
-f, --field <key=value>
|
||||
|
||||
Docs:
|
||||
docs/commands/actions.md`)
|
||||
}
|
||||
|
||||
func (a App) actionsHydrate(ctx context.Context, args []string) error {
|
||||
started := time.Now()
|
||||
fs := newFlagSet("actions hydrate", a.Stderr)
|
||||
|
||||
@ -6,47 +6,6 @@ import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
func (a App) admin(ctx context.Context, args []string) error {
|
||||
if len(args) == 0 {
|
||||
a.printAdminHelp()
|
||||
return exit(2, "missing admin subcommand")
|
||||
}
|
||||
if wantsHelp(args) {
|
||||
a.printAdminHelp()
|
||||
return nil
|
||||
}
|
||||
switch args[0] {
|
||||
case "leases":
|
||||
return a.adminLeases(ctx, args[1:])
|
||||
case "release":
|
||||
return a.adminRelease(ctx, args[1:])
|
||||
case "delete":
|
||||
return a.adminDelete(ctx, args[1:])
|
||||
default:
|
||||
return exit(2, "unknown admin command %q", args[0])
|
||||
}
|
||||
}
|
||||
|
||||
func (a App) printAdminHelp() {
|
||||
fmt.Fprintln(a.Stdout, `Usage:
|
||||
crabbox admin leases [flags]
|
||||
crabbox admin release <lease-id-or-slug> [flags]
|
||||
crabbox admin delete <lease-id-or-slug> --force [flags]
|
||||
|
||||
Subcommands:
|
||||
leases List coordinator lease records
|
||||
release Mark a lease released, optionally deleting the backing server
|
||||
delete Delete the backing server and mark the lease released
|
||||
|
||||
Flags:
|
||||
leases: --state <state> --owner <email> --org <name> --limit <n> --json
|
||||
release: --id <lease-id-or-slug> --delete --json
|
||||
delete: --id <lease-id-or-slug> --force --json
|
||||
|
||||
Docs:
|
||||
docs/commands/admin.md`)
|
||||
}
|
||||
|
||||
func (a App) adminLeases(ctx context.Context, args []string) error {
|
||||
fs := newFlagSet("admin leases", a.Stderr)
|
||||
state := fs.String("state", "", "filter by state")
|
||||
|
||||
@ -31,80 +31,13 @@ func (a App) Run(ctx context.Context, args []string) error {
|
||||
return nil
|
||||
case "help":
|
||||
if len(args) > 1 {
|
||||
next := append([]string{}, args[1:]...)
|
||||
next = append(next, "--help")
|
||||
return a.Run(ctx, next)
|
||||
return a.runKong(ctx, args)
|
||||
}
|
||||
a.printHelp()
|
||||
return nil
|
||||
case "-v", "--version", "version":
|
||||
fmt.Fprintln(a.Stdout, version)
|
||||
return nil
|
||||
case "doctor":
|
||||
return a.doctor(ctx, args[1:])
|
||||
case "login":
|
||||
return a.login(ctx, args[1:])
|
||||
case "logout":
|
||||
return a.logout(ctx, args[1:])
|
||||
case "whoami":
|
||||
return a.whoami(ctx, args[1:])
|
||||
case "admin":
|
||||
return a.admin(ctx, args[1:])
|
||||
case "history":
|
||||
return a.history(ctx, args[1:])
|
||||
case "logs":
|
||||
return a.logs(ctx, args[1:])
|
||||
case "events":
|
||||
return a.events(ctx, args[1:])
|
||||
case "attach":
|
||||
return a.attach(ctx, args[1:])
|
||||
case "results":
|
||||
return a.results(ctx, args[1:])
|
||||
case "cache":
|
||||
return a.cache(ctx, args[1:])
|
||||
case "config":
|
||||
return a.config(ctx, args[1:])
|
||||
case "init":
|
||||
return a.initProject(ctx, args[1:])
|
||||
case "image":
|
||||
return a.image(ctx, args[1:])
|
||||
case "pool":
|
||||
return a.pool(ctx, args[1:])
|
||||
case "machine":
|
||||
return a.machine(ctx, args[1:])
|
||||
case "list":
|
||||
return a.list(ctx, args[1:])
|
||||
case "usage":
|
||||
return a.usage(ctx, args[1:])
|
||||
case "actions":
|
||||
return a.actions(ctx, args[1:])
|
||||
case "cleanup":
|
||||
return a.cleanup(ctx, args[1:])
|
||||
case "warmup":
|
||||
return a.warmup(ctx, args[1:])
|
||||
case "run":
|
||||
return a.runCommand(ctx, args[1:])
|
||||
case "desktop":
|
||||
return a.desktop(ctx, args[1:])
|
||||
case "sync-plan":
|
||||
return a.syncPlan(ctx, args[1:])
|
||||
case "status":
|
||||
return a.status(ctx, args[1:])
|
||||
case "ssh":
|
||||
return a.ssh(ctx, args[1:])
|
||||
case "vnc":
|
||||
return a.vnc(ctx, args[1:])
|
||||
case "webvnc":
|
||||
return a.webvnc(ctx, args[1:])
|
||||
case "screenshot":
|
||||
return a.screenshot(ctx, args[1:])
|
||||
case "inspect":
|
||||
return a.inspect(ctx, args[1:])
|
||||
case "stop", "release":
|
||||
return a.stop(ctx, args[1:])
|
||||
default:
|
||||
return exit(2, "unknown command %q", args[0])
|
||||
}
|
||||
|
||||
return a.runKong(ctx, args)
|
||||
}
|
||||
|
||||
func (a App) printHelp() {
|
||||
@ -246,10 +179,3 @@ func parseFlags(fs *flag.FlagSet, args []string) error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func wantsHelp(args []string) bool {
|
||||
if len(args) == 0 {
|
||||
return false
|
||||
}
|
||||
return args[0] == "-h" || args[0] == "--help" || args[0] == "help"
|
||||
}
|
||||
|
||||
@ -14,48 +14,6 @@ type cacheEntry struct {
|
||||
Note string `json:"note,omitempty"`
|
||||
}
|
||||
|
||||
func (a App) cache(ctx context.Context, args []string) error {
|
||||
if len(args) == 0 {
|
||||
a.printCacheHelp()
|
||||
return exit(2, "missing cache subcommand")
|
||||
}
|
||||
if wantsHelp(args) {
|
||||
a.printCacheHelp()
|
||||
return nil
|
||||
}
|
||||
switch args[0] {
|
||||
case "list", "stats":
|
||||
return a.cacheStats(ctx, args[1:])
|
||||
case "purge":
|
||||
return a.cachePurge(ctx, args[1:])
|
||||
case "warm":
|
||||
return a.cacheWarm(ctx, args[1:])
|
||||
default:
|
||||
return exit(2, "unknown cache command %q", args[0])
|
||||
}
|
||||
}
|
||||
|
||||
func (a App) printCacheHelp() {
|
||||
fmt.Fprintln(a.Stdout, `Usage:
|
||||
crabbox cache stats --id <lease-id-or-slug> [flags]
|
||||
crabbox cache list --id <lease-id-or-slug> [flags]
|
||||
crabbox cache purge --id <lease-id-or-slug> --kind <kind> --force [flags]
|
||||
crabbox cache warm --id <lease-id-or-slug> -- <command...>
|
||||
|
||||
Subcommands:
|
||||
list, stats Show remote cache usage
|
||||
purge Remove selected cache content
|
||||
warm Run a command that populates caches
|
||||
|
||||
Flags:
|
||||
stats/list: --id <lease-id-or-slug> --reclaim --json
|
||||
purge: --id <lease-id-or-slug> --kind pnpm|npm|docker|git|all --force --reclaim
|
||||
warm: --id <lease-id-or-slug> --reclaim
|
||||
|
||||
Docs:
|
||||
docs/commands/cache.md`)
|
||||
}
|
||||
|
||||
func (a App) cacheStats(ctx context.Context, args []string) error {
|
||||
fs := newFlagSet("cache stats", a.Stderr)
|
||||
id := fs.String("id", "", "lease id or slug")
|
||||
|
||||
367
internal/cli/cli_kong.go
Normal file
367
internal/cli/cli_kong.go
Normal file
@ -0,0 +1,367 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/alecthomas/kong"
|
||||
)
|
||||
|
||||
type crabboxKongCLI struct {
|
||||
Version kong.VersionFlag `name:"version" short:"v" help:"Print version."`
|
||||
|
||||
VersionCmd versionKongCmd `cmd:"" name:"version" help:"Print version."`
|
||||
Init initKongCmd `cmd:"" passthrough:"" help:"Onboard the current repo for Crabbox."`
|
||||
Login loginKongCmd `cmd:"" passthrough:"" help:"Open GitHub login, store broker credentials, verify access."`
|
||||
Logout logoutKongCmd `cmd:"" passthrough:"" help:"Remove the stored broker token."`
|
||||
Whoami whoamiKongCmd `cmd:"" passthrough:"" help:"Show broker identity."`
|
||||
Doctor doctorKongCmd `cmd:"" passthrough:"" help:"Check local and broker/provider readiness."`
|
||||
Warmup warmupKongCmd `cmd:"" passthrough:"" help:"Lease a box and wait until it is ready."`
|
||||
Run runKongCmd `cmd:"" passthrough:"" help:"Sync the repo, run a remote command, stream output."`
|
||||
Desktop desktopKongCmd `cmd:"" help:"Launch apps into a visible desktop session."`
|
||||
SyncPlan syncPlanKongCmd `cmd:"" name:"sync-plan" passthrough:"" help:"Show local sync manifest size hotspots."`
|
||||
History historyKongCmd `cmd:"" passthrough:"" help:"List recorded remote runs."`
|
||||
Logs logsKongCmd `cmd:"" passthrough:"" help:"Print recorded run logs."`
|
||||
Events eventsKongCmd `cmd:"" passthrough:"" help:"Print recorded run events."`
|
||||
Attach attachKongCmd `cmd:"" passthrough:"" help:"Follow recorded events for an active run."`
|
||||
Results resultsKongCmd `cmd:"" passthrough:"" help:"Show recorded test result summaries."`
|
||||
Cache cacheKongCmd `cmd:"" help:"Inspect, purge, or warm remote caches."`
|
||||
Status statusKongCmd `cmd:"" passthrough:"" help:"Show lease state; add --wait to block until ready."`
|
||||
List listKongCmd `cmd:"" passthrough:"" help:"List Crabbox machines."`
|
||||
Image imageKongCmd `cmd:"" help:"Create or promote brokered AWS runner images."`
|
||||
Usage usageKongCmd `cmd:"" passthrough:"" help:"Show cost and usage estimates by user, org, or fleet."`
|
||||
Admin adminKongCmd `cmd:"" help:"Lease admin controls for trusted operators."`
|
||||
Actions actionsKongCmd `cmd:"" help:"Register GitHub Actions runners or dispatch workflows."`
|
||||
Ssh sshKongCmd `cmd:"" name:"ssh" passthrough:"" help:"Print the SSH command for a lease."`
|
||||
Vnc vncKongCmd `cmd:"" name:"vnc" passthrough:"" help:"Print or open VNC connection details for a desktop lease."`
|
||||
Webvnc webvncKongCmd `cmd:"" name:"webvnc" passthrough:"" help:"Bridge a desktop lease into the authenticated web portal."`
|
||||
Screenshot screenshotKongCmd `cmd:"" passthrough:"" help:"Capture a PNG from a desktop lease."`
|
||||
Inspect inspectKongCmd `cmd:"" passthrough:"" help:"Print lease/provider details; add --json for scripts."`
|
||||
Stop stopKongCmd `cmd:"" passthrough:"" help:"Release a lease or delete a direct-provider machine."`
|
||||
Release releaseKongCmd `cmd:"" passthrough:"" help:"Alias for stop."`
|
||||
Cleanup cleanupKongCmd `cmd:"" passthrough:"" help:"Sweep expired direct-provider machines."`
|
||||
Config configKongCmd `cmd:"" help:"Show or update user config."`
|
||||
Pool poolKongCmd `cmd:"" help:"Alias commands for machine pools."`
|
||||
Machine machineKongCmd `cmd:"" help:"Alias commands for direct-provider machines."`
|
||||
}
|
||||
|
||||
type kongExit struct {
|
||||
code int
|
||||
}
|
||||
|
||||
func (a App) runKong(ctx context.Context, args []string) (err error) {
|
||||
args = normalizeKongHelpArgs(args)
|
||||
var cli crabboxKongCLI
|
||||
parser, err := kong.New(&cli,
|
||||
kong.Name("crabbox"),
|
||||
kong.Description("Crabbox leases remote test boxes, syncs your dirty checkout, runs commands, and cleans up."),
|
||||
kong.Vars{"version": version},
|
||||
kong.Writers(a.Stdout, a.Stderr),
|
||||
kong.Exit(func(code int) {
|
||||
panic(kongExit{code: code})
|
||||
}),
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
recovered := recover()
|
||||
if recovered == nil {
|
||||
return
|
||||
}
|
||||
if exit, ok := recovered.(kongExit); ok {
|
||||
if exit.code == 0 {
|
||||
err = nil
|
||||
} else {
|
||||
err = ExitError{Code: exit.code}
|
||||
}
|
||||
return
|
||||
}
|
||||
panic(recovered)
|
||||
}()
|
||||
kctx, err := parser.Parse(args)
|
||||
if err != nil {
|
||||
var parseErr *kong.ParseError
|
||||
if errors.As(err, &parseErr) {
|
||||
return exit(2, "%v", parseErr)
|
||||
}
|
||||
return err
|
||||
}
|
||||
kctx.BindTo(ctx, (*context.Context)(nil))
|
||||
return kctx.Run(a)
|
||||
}
|
||||
|
||||
func normalizeKongHelpArgs(args []string) []string {
|
||||
if len(args) > 1 && args[0] == "help" {
|
||||
next := append([]string{}, args[1:]...)
|
||||
next = append(next, "--help")
|
||||
return next
|
||||
}
|
||||
return args
|
||||
}
|
||||
|
||||
type initKongCmd struct {
|
||||
Args []string `arg:"" optional:""`
|
||||
}
|
||||
type loginKongCmd struct {
|
||||
Args []string `arg:"" optional:""`
|
||||
}
|
||||
type logoutKongCmd struct {
|
||||
Args []string `arg:"" optional:""`
|
||||
}
|
||||
type whoamiKongCmd struct {
|
||||
Args []string `arg:"" optional:""`
|
||||
}
|
||||
type doctorKongCmd struct {
|
||||
Args []string `arg:"" optional:""`
|
||||
}
|
||||
type warmupKongCmd struct {
|
||||
Args []string `arg:"" optional:""`
|
||||
}
|
||||
type runKongCmd struct {
|
||||
Args []string `arg:"" optional:""`
|
||||
}
|
||||
type syncPlanKongCmd struct {
|
||||
Args []string `arg:"" optional:""`
|
||||
}
|
||||
type historyKongCmd struct {
|
||||
Args []string `arg:"" optional:""`
|
||||
}
|
||||
type logsKongCmd struct {
|
||||
Args []string `arg:"" optional:""`
|
||||
}
|
||||
type eventsKongCmd struct {
|
||||
Args []string `arg:"" optional:""`
|
||||
}
|
||||
type attachKongCmd struct {
|
||||
Args []string `arg:"" optional:""`
|
||||
}
|
||||
type resultsKongCmd struct {
|
||||
Args []string `arg:"" optional:""`
|
||||
}
|
||||
type statusKongCmd struct {
|
||||
Args []string `arg:"" optional:""`
|
||||
}
|
||||
type listKongCmd struct {
|
||||
Args []string `arg:"" optional:""`
|
||||
}
|
||||
type usageKongCmd struct {
|
||||
Args []string `arg:"" optional:""`
|
||||
}
|
||||
type sshKongCmd struct {
|
||||
Args []string `arg:"" optional:""`
|
||||
}
|
||||
type vncKongCmd struct {
|
||||
Args []string `arg:"" optional:""`
|
||||
}
|
||||
type webvncKongCmd struct {
|
||||
Args []string `arg:"" optional:""`
|
||||
}
|
||||
type screenshotKongCmd struct {
|
||||
Args []string `arg:"" optional:""`
|
||||
}
|
||||
type inspectKongCmd struct {
|
||||
Args []string `arg:"" optional:""`
|
||||
}
|
||||
type stopKongCmd struct {
|
||||
Args []string `arg:"" optional:""`
|
||||
}
|
||||
type releaseKongCmd struct {
|
||||
Args []string `arg:"" optional:""`
|
||||
}
|
||||
type cleanupKongCmd struct {
|
||||
Args []string `arg:"" optional:""`
|
||||
}
|
||||
|
||||
type desktopKongCmd struct {
|
||||
Launch desktopLaunchKongCmd `cmd:"" passthrough:"" help:"Start an app inside a desktop lease."`
|
||||
}
|
||||
type desktopLaunchKongCmd struct {
|
||||
Args []string `arg:"" optional:""`
|
||||
}
|
||||
|
||||
type cacheKongCmd struct {
|
||||
List cacheListKongCmd `cmd:"" passthrough:"" help:"Show remote cache usage."`
|
||||
Stats cacheStatsKongCmd `cmd:"" passthrough:"" help:"Show remote cache usage."`
|
||||
Purge cachePurgeKongCmd `cmd:"" passthrough:"" help:"Remove selected cache content."`
|
||||
Warm cacheWarmKongCmd `cmd:"" passthrough:"" help:"Run a command that populates caches."`
|
||||
}
|
||||
type cacheListKongCmd struct {
|
||||
Args []string `arg:"" optional:""`
|
||||
}
|
||||
type cacheStatsKongCmd struct {
|
||||
Args []string `arg:"" optional:""`
|
||||
}
|
||||
type cachePurgeKongCmd struct {
|
||||
Args []string `arg:"" optional:""`
|
||||
}
|
||||
type cacheWarmKongCmd struct {
|
||||
Args []string `arg:"" optional:""`
|
||||
}
|
||||
|
||||
type imageKongCmd struct {
|
||||
Create imageCreateKongCmd `cmd:"" passthrough:"" help:"Create an AMI from a brokered AWS lease."`
|
||||
Promote imagePromoteKongCmd `cmd:"" passthrough:"" help:"Promote an AMI for brokered AWS runners."`
|
||||
}
|
||||
type imageCreateKongCmd struct {
|
||||
Args []string `arg:"" optional:""`
|
||||
}
|
||||
type imagePromoteKongCmd struct {
|
||||
Args []string `arg:"" optional:""`
|
||||
}
|
||||
|
||||
type adminKongCmd struct {
|
||||
Leases adminLeasesKongCmd `cmd:"" passthrough:"" help:"List coordinator lease records."`
|
||||
Release adminReleaseKongCmd `cmd:"" passthrough:"" help:"Mark a lease released."`
|
||||
Delete adminDeleteKongCmd `cmd:"" passthrough:"" help:"Delete the backing server and mark the lease released."`
|
||||
}
|
||||
type adminLeasesKongCmd struct {
|
||||
Args []string `arg:"" optional:""`
|
||||
}
|
||||
type adminReleaseKongCmd struct {
|
||||
Args []string `arg:"" optional:""`
|
||||
}
|
||||
type adminDeleteKongCmd struct {
|
||||
Args []string `arg:"" optional:""`
|
||||
}
|
||||
|
||||
type actionsKongCmd struct {
|
||||
Hydrate actionsHydrateKongCmd `cmd:"" passthrough:"" help:"Register a runner, dispatch the hydrate workflow, wait for readiness."`
|
||||
Register actionsRegisterKongCmd `cmd:"" passthrough:"" help:"Register an existing Linux lease as a GitHub Actions runner."`
|
||||
Dispatch actionsDispatchKongCmd `cmd:"" passthrough:"" help:"Dispatch the configured GitHub Actions workflow."`
|
||||
}
|
||||
type actionsHydrateKongCmd struct {
|
||||
Args []string `arg:"" optional:""`
|
||||
}
|
||||
type actionsRegisterKongCmd struct {
|
||||
Args []string `arg:"" optional:""`
|
||||
}
|
||||
type actionsDispatchKongCmd struct {
|
||||
Args []string `arg:"" optional:""`
|
||||
}
|
||||
|
||||
type configKongCmd struct {
|
||||
Path configPathKongCmd `cmd:"" help:"Print the user config path."`
|
||||
Show configShowKongCmd `cmd:"" passthrough:"" help:"Print merged config without secret values."`
|
||||
SetBroker configSetBrokerKongCmd `cmd:"" name:"set-broker" passthrough:"" help:"Store broker URL and optional tokens in user config."`
|
||||
}
|
||||
type configPathKongCmd struct{}
|
||||
type configShowKongCmd struct {
|
||||
Args []string `arg:"" optional:""`
|
||||
}
|
||||
type configSetBrokerKongCmd struct {
|
||||
Args []string `arg:"" optional:""`
|
||||
}
|
||||
|
||||
type poolKongCmd struct {
|
||||
List poolListKongCmd `cmd:"" passthrough:"" help:"Alias for list."`
|
||||
}
|
||||
type poolListKongCmd struct {
|
||||
Args []string `arg:"" optional:""`
|
||||
}
|
||||
|
||||
type machineKongCmd struct {
|
||||
Cleanup machineCleanupKongCmd `cmd:"" passthrough:"" help:"Alias for cleanup."`
|
||||
}
|
||||
type machineCleanupKongCmd struct {
|
||||
Args []string `arg:"" optional:""`
|
||||
}
|
||||
|
||||
type versionKongCmd struct{}
|
||||
|
||||
func (c *initKongCmd) Run(ctx context.Context, app App) error { return app.initProject(ctx, c.Args) }
|
||||
func (c *loginKongCmd) Run(ctx context.Context, app App) error { return app.login(ctx, c.Args) }
|
||||
func (c *logoutKongCmd) Run(ctx context.Context, app App) error { return app.logout(ctx, c.Args) }
|
||||
func (c *whoamiKongCmd) Run(ctx context.Context, app App) error { return app.whoami(ctx, c.Args) }
|
||||
func (c *doctorKongCmd) Run(ctx context.Context, app App) error { return app.doctor(ctx, c.Args) }
|
||||
func (c *warmupKongCmd) Run(ctx context.Context, app App) error { return app.warmup(ctx, c.Args) }
|
||||
func (c *runKongCmd) Run(ctx context.Context, app App) error { return app.runCommand(ctx, c.Args) }
|
||||
func (c *syncPlanKongCmd) Run(ctx context.Context, app App) error { return app.syncPlan(ctx, c.Args) }
|
||||
func (c *historyKongCmd) Run(ctx context.Context, app App) error { return app.history(ctx, c.Args) }
|
||||
func (c *logsKongCmd) Run(ctx context.Context, app App) error { return app.logs(ctx, c.Args) }
|
||||
func (c *eventsKongCmd) Run(ctx context.Context, app App) error { return app.events(ctx, c.Args) }
|
||||
func (c *attachKongCmd) Run(ctx context.Context, app App) error { return app.attach(ctx, c.Args) }
|
||||
func (c *resultsKongCmd) Run(ctx context.Context, app App) error { return app.results(ctx, c.Args) }
|
||||
func (c *statusKongCmd) Run(ctx context.Context, app App) error { return app.status(ctx, c.Args) }
|
||||
func (c *listKongCmd) Run(ctx context.Context, app App) error { return app.list(ctx, c.Args) }
|
||||
func (c *usageKongCmd) Run(ctx context.Context, app App) error { return app.usage(ctx, c.Args) }
|
||||
func (c *sshKongCmd) Run(ctx context.Context, app App) error { return app.ssh(ctx, c.Args) }
|
||||
func (c *vncKongCmd) Run(ctx context.Context, app App) error { return app.vnc(ctx, c.Args) }
|
||||
func (c *webvncKongCmd) Run(ctx context.Context, app App) error { return app.webvnc(ctx, c.Args) }
|
||||
func (c *screenshotKongCmd) Run(ctx context.Context, app App) error {
|
||||
return app.screenshot(ctx, c.Args)
|
||||
}
|
||||
func (c *inspectKongCmd) Run(ctx context.Context, app App) error { return app.inspect(ctx, c.Args) }
|
||||
func (c *stopKongCmd) Run(ctx context.Context, app App) error { return app.stop(ctx, c.Args) }
|
||||
func (c *releaseKongCmd) Run(ctx context.Context, app App) error { return app.stop(ctx, c.Args) }
|
||||
func (c *cleanupKongCmd) Run(ctx context.Context, app App) error { return app.cleanup(ctx, c.Args) }
|
||||
|
||||
func (c *desktopLaunchKongCmd) Run(ctx context.Context, app App) error {
|
||||
return app.desktopLaunch(ctx, c.Args)
|
||||
}
|
||||
|
||||
func (c *cacheListKongCmd) Run(ctx context.Context, app App) error {
|
||||
return app.cacheStats(ctx, c.Args)
|
||||
}
|
||||
func (c *cacheStatsKongCmd) Run(ctx context.Context, app App) error {
|
||||
return app.cacheStats(ctx, c.Args)
|
||||
}
|
||||
func (c *cachePurgeKongCmd) Run(ctx context.Context, app App) error {
|
||||
return app.cachePurge(ctx, c.Args)
|
||||
}
|
||||
func (c *cacheWarmKongCmd) Run(ctx context.Context, app App) error { return app.cacheWarm(ctx, c.Args) }
|
||||
|
||||
func (c *imageCreateKongCmd) Run(ctx context.Context, app App) error {
|
||||
return app.imageCreate(ctx, c.Args)
|
||||
}
|
||||
func (c *imagePromoteKongCmd) Run(ctx context.Context, app App) error {
|
||||
return app.imagePromote(ctx, c.Args)
|
||||
}
|
||||
|
||||
func (c *adminLeasesKongCmd) Run(ctx context.Context, app App) error {
|
||||
return app.adminLeases(ctx, c.Args)
|
||||
}
|
||||
func (c *adminReleaseKongCmd) Run(ctx context.Context, app App) error {
|
||||
return app.adminRelease(ctx, c.Args)
|
||||
}
|
||||
func (c *adminDeleteKongCmd) Run(ctx context.Context, app App) error {
|
||||
return app.adminDelete(ctx, c.Args)
|
||||
}
|
||||
|
||||
func (c *actionsHydrateKongCmd) Run(ctx context.Context, app App) error {
|
||||
return app.actionsHydrate(ctx, c.Args)
|
||||
}
|
||||
func (c *actionsRegisterKongCmd) Run(ctx context.Context, app App) error {
|
||||
return app.actionsRegister(ctx, c.Args)
|
||||
}
|
||||
func (c *actionsDispatchKongCmd) Run(ctx context.Context, app App) error {
|
||||
return app.actionsDispatch(ctx, c.Args)
|
||||
}
|
||||
|
||||
func (c *configPathKongCmd) Run(ctx context.Context, app App) error {
|
||||
path := userConfigPath()
|
||||
if path == "" {
|
||||
return exit(2, "user config directory is unavailable")
|
||||
}
|
||||
fmt.Fprintln(app.Stdout, path)
|
||||
return nil
|
||||
}
|
||||
func (c *configShowKongCmd) Run(app App) error {
|
||||
return app.configShow(c.Args)
|
||||
}
|
||||
func (c *configSetBrokerKongCmd) Run(app App) error {
|
||||
return app.configSetBroker(c.Args)
|
||||
}
|
||||
|
||||
func (c *poolListKongCmd) Run(ctx context.Context, app App) error {
|
||||
return app.list(ctx, c.Args)
|
||||
}
|
||||
func (c *machineCleanupKongCmd) Run(ctx context.Context, app App) error {
|
||||
return app.cleanup(ctx, c.Args)
|
||||
}
|
||||
|
||||
func (c *versionKongCmd) Run(app App) error {
|
||||
fmt.Fprintln(app.Stdout, version)
|
||||
return nil
|
||||
}
|
||||
@ -1,7 +1,6 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
@ -9,51 +8,6 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
func (a App) config(_ context.Context, args []string) error {
|
||||
if len(args) == 0 {
|
||||
a.printConfigHelp()
|
||||
return exit(2, "missing config subcommand")
|
||||
}
|
||||
if wantsHelp(args) {
|
||||
a.printConfigHelp()
|
||||
return nil
|
||||
}
|
||||
switch args[0] {
|
||||
case "path":
|
||||
path := userConfigPath()
|
||||
if path == "" {
|
||||
return exit(2, "user config directory is unavailable")
|
||||
}
|
||||
fmt.Fprintln(a.Stdout, path)
|
||||
return nil
|
||||
case "show":
|
||||
return a.configShow(args[1:])
|
||||
case "set-broker":
|
||||
return a.configSetBroker(args[1:])
|
||||
default:
|
||||
return exit(2, "unknown config command %q", args[0])
|
||||
}
|
||||
}
|
||||
|
||||
func (a App) printConfigHelp() {
|
||||
fmt.Fprintln(a.Stdout, `Usage:
|
||||
crabbox config path
|
||||
crabbox config show [--json]
|
||||
crabbox config set-broker --url <url> [flags]
|
||||
|
||||
Subcommands:
|
||||
path Print the user config path
|
||||
show Print merged config without secret values
|
||||
set-broker Store broker URL and optional tokens in user config
|
||||
|
||||
Flags:
|
||||
show: --json
|
||||
set-broker: --url <url> --provider hetzner|aws --token-stdin --admin-token-stdin
|
||||
|
||||
Docs:
|
||||
docs/commands/config.md`)
|
||||
}
|
||||
|
||||
func (a App) configShow(args []string) error {
|
||||
fs := newFlagSet("config show", a.Stderr)
|
||||
jsonOut := fs.Bool("json", false, "print JSON")
|
||||
|
||||
@ -7,48 +7,6 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
func (a App) desktop(ctx context.Context, args []string) error {
|
||||
if len(args) == 0 {
|
||||
a.printDesktopHelp()
|
||||
return exit(2, "missing desktop subcommand")
|
||||
}
|
||||
if wantsHelp(args) {
|
||||
a.printDesktopHelp()
|
||||
return nil
|
||||
}
|
||||
switch args[0] {
|
||||
case "launch":
|
||||
return a.desktopLaunch(ctx, args[1:])
|
||||
default:
|
||||
return exit(2, "unknown desktop command %q", args[0])
|
||||
}
|
||||
}
|
||||
|
||||
func (a App) printDesktopHelp() {
|
||||
fmt.Fprintln(a.Stdout, `Usage:
|
||||
crabbox desktop launch --id <lease-id-or-slug> [flags] -- <command...>
|
||||
crabbox desktop launch --id <lease-id-or-slug> --browser [--url <url>]
|
||||
|
||||
Subcommands:
|
||||
launch Start an app inside a desktop lease
|
||||
|
||||
Flags:
|
||||
--id <lease-id-or-slug>
|
||||
--provider hetzner|aws|ssh
|
||||
--target linux|macos|windows
|
||||
--windows-mode normal|wsl2
|
||||
--static-host <host>
|
||||
--static-user <user>
|
||||
--static-port <port>
|
||||
--static-work-root <path>
|
||||
--browser
|
||||
--url <url>
|
||||
--reclaim
|
||||
|
||||
Docs:
|
||||
docs/commands/desktop.md`)
|
||||
}
|
||||
|
||||
func (a App) desktopLaunch(ctx context.Context, args []string) error {
|
||||
defaults := defaultConfig()
|
||||
fs := newFlagSet("desktop launch", a.Stderr)
|
||||
@ -146,6 +104,7 @@ func posixDesktopLaunchRemoteCommand(workdir string, env map[string]string, comm
|
||||
var b bytes.Buffer
|
||||
b.WriteString("set -eu\n")
|
||||
if workdir != "" {
|
||||
b.WriteString("mkdir -p " + shellQuote(workdir) + "\n")
|
||||
b.WriteString("cd " + shellQuote(workdir) + "\n")
|
||||
}
|
||||
for key, value := range env {
|
||||
@ -205,6 +164,7 @@ func windowsDesktopLaunchScript(workdir string, env map[string]string, command [
|
||||
var b bytes.Buffer
|
||||
b.WriteString("$ErrorActionPreference = \"Stop\"\n")
|
||||
if workdir != "" {
|
||||
b.WriteString("New-Item -ItemType Directory -Force -Path " + psQuote(workdir) + " | Out-Null\n")
|
||||
b.WriteString("Set-Location -LiteralPath " + psQuote(workdir) + "\n")
|
||||
}
|
||||
for key, value := range env {
|
||||
|
||||
@ -13,6 +13,7 @@ func TestDesktopLaunchRemoteCommandUsesDetachedPOSIXSession(t *testing.T) {
|
||||
[]string{"/usr/bin/chromium", "https://example.com"},
|
||||
)
|
||||
for _, want := range []string{
|
||||
"mkdir -p '/work/crabbox/cbx_1/repo'",
|
||||
"cd '/work/crabbox/cbx_1/repo'",
|
||||
"DISPLAY=':99'",
|
||||
"BROWSER='/usr/bin/chromium'",
|
||||
@ -52,6 +53,7 @@ func TestWindowsDesktopLaunchScriptStartsAndForegroundsProcess(t *testing.T) {
|
||||
[]string{`C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe`, "https://example.com"},
|
||||
)
|
||||
for _, want := range []string{
|
||||
`New-Item -ItemType Directory -Force -Path 'C:\crabbox\cbx_1\repo'`,
|
||||
`Set-Location -LiteralPath 'C:\crabbox\cbx_1\repo'`,
|
||||
`$env:BROWSER = 'C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe'`,
|
||||
"Shell.Application",
|
||||
|
||||
@ -8,25 +8,6 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
func (a App) image(ctx context.Context, args []string) error {
|
||||
if len(args) == 0 {
|
||||
return exit(2, "usage: crabbox image <create|promote> [flags]")
|
||||
}
|
||||
switch args[0] {
|
||||
case "-h", "--help", "help":
|
||||
fmt.Fprintln(a.Stdout, `Usage:
|
||||
crabbox image create --id <cbx_id> --name <ami-name> [--wait]
|
||||
crabbox image promote <ami-id>`)
|
||||
return nil
|
||||
case "create":
|
||||
return a.imageCreate(ctx, args[1:])
|
||||
case "promote":
|
||||
return a.imagePromote(ctx, args[1:])
|
||||
default:
|
||||
return exit(2, "unknown image command %q", args[0])
|
||||
}
|
||||
}
|
||||
|
||||
func (a App) imageCreate(ctx context.Context, args []string) error {
|
||||
fs := newFlagSet("image create", a.Stderr)
|
||||
id := fs.String("id", "", "AWS lease id to image")
|
||||
|
||||
@ -128,3 +128,20 @@ func TestTopLevelHelpIsWorkflowFirst(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestKongRouterPreservesVersionAndUsageExitCodes(t *testing.T) {
|
||||
var stdout bytes.Buffer
|
||||
app := App{Stdout: &stdout, Stderr: &bytes.Buffer{}}
|
||||
if err := app.Run(context.Background(), []string{"--version"}); err != nil {
|
||||
t.Fatalf("--version error: %v", err)
|
||||
}
|
||||
if strings.TrimSpace(stdout.String()) != version {
|
||||
t.Fatalf("--version output=%q, want %q", stdout.String(), version)
|
||||
}
|
||||
|
||||
err := app.Run(context.Background(), []string{"nope"})
|
||||
var exitErr ExitError
|
||||
if !AsExitError(err, &exitErr) || exitErr.Code != 2 {
|
||||
t.Fatalf("unknown command error=%v, want exit 2", err)
|
||||
}
|
||||
}
|
||||
|
||||
@ -8,21 +8,6 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
func (a App) pool(ctx context.Context, args []string) error {
|
||||
if wantsHelp(args) {
|
||||
fmt.Fprintln(a.Stdout, `Usage:
|
||||
crabbox pool list [--json]
|
||||
|
||||
Alias:
|
||||
crabbox pool list is an alias for crabbox list`)
|
||||
return nil
|
||||
}
|
||||
if len(args) == 0 || args[0] != "list" {
|
||||
return exit(2, "usage: crabbox pool list [--json]")
|
||||
}
|
||||
return a.list(ctx, args[1:])
|
||||
}
|
||||
|
||||
func (a App) list(ctx context.Context, args []string) error {
|
||||
fs := newFlagSet("list", a.Stderr)
|
||||
provider := fs.String("provider", defaultConfig().Provider, "provider: hetzner, aws, ssh, or blacksmith-testbox")
|
||||
@ -147,31 +132,6 @@ func coordinatorMachineOrphanField(labels map[string]string, activeLeaseIDs map[
|
||||
return ""
|
||||
}
|
||||
|
||||
func (a App) machine(ctx context.Context, args []string) error {
|
||||
if len(args) == 0 {
|
||||
fmt.Fprintln(a.Stdout, `Usage:
|
||||
crabbox machine cleanup [--dry-run]
|
||||
|
||||
Alias:
|
||||
crabbox machine cleanup is an alias for crabbox cleanup`)
|
||||
return exit(2, "missing machine subcommand")
|
||||
}
|
||||
if wantsHelp(args) {
|
||||
fmt.Fprintln(a.Stdout, `Usage:
|
||||
crabbox machine cleanup [--dry-run]
|
||||
|
||||
Alias:
|
||||
crabbox machine cleanup is an alias for crabbox cleanup`)
|
||||
return nil
|
||||
}
|
||||
switch args[0] {
|
||||
case "cleanup":
|
||||
return a.cleanup(ctx, args[1:])
|
||||
default:
|
||||
return exit(2, "unknown machine command %q", args[0])
|
||||
}
|
||||
}
|
||||
|
||||
func (a App) cleanup(ctx context.Context, args []string) error {
|
||||
fs := newFlagSet("machine cleanup", a.Stderr)
|
||||
provider := fs.String("provider", defaultConfig().Provider, "provider: hetzner or aws")
|
||||
|
||||
Loading…
Reference in New Issue
Block a user