crabbox/internal/cli/status.go
2026-05-06 09:03:19 +01:00

260 lines
9.4 KiB
Go

package cli
import (
"context"
"encoding/json"
"fmt"
"time"
)
func (a App) status(ctx context.Context, args []string) error {
defaults := defaultConfig()
fs := newFlagSet("status", a.Stderr)
provider := fs.String("provider", defaults.Provider, providerHelpAll())
id := fs.String("id", "", "lease id or slug")
wait := fs.Bool("wait", false, "wait until ready")
waitTimeout := fs.Duration("wait-timeout", 5*time.Minute, "maximum wait duration")
jsonOut := fs.Bool("json", false, "print JSON")
providerFlags := registerProviderFlags(fs, defaults)
targetFlags := registerTargetFlags(fs, defaults)
networkFlags := registerNetworkModeFlag(fs, defaults)
if err := parseFlags(fs, args); err != nil {
return err
}
setIDFromFirstArg(fs, id)
cfg, err := loadLeaseTargetConfig(fs, *provider, targetFlags, networkFlags, leaseTargetConfigOptions{})
if err != nil {
return err
}
if err := applyProviderFlags(&cfg, fs, providerFlags); err != nil {
return err
}
if err := requireLeaseID(*id, "crabbox status --id <lease-id-or-slug>", cfg); err != nil {
return err
}
backend, err := loadBackend(cfg, runtimeForApp(a))
if err != nil {
return err
}
statusBackend, isStatus := backend.(interface {
Status(context.Context, StatusRequest) (statusView, error)
})
delegated, isDelegated := backend.(DelegatedRunBackend)
sshBackend, isSSH := backend.(SSHLeaseBackend)
deadline := time.Now().Add(*waitTimeout)
for {
var state statusView
var err error
if isStatus {
state, err = statusBackend.Status(ctx, StatusRequest{Options: leaseOptionsFromConfig(cfg), ID: *id, Wait: *wait, WaitTimeout: *waitTimeout})
} else if isDelegated {
state, err = delegated.Status(ctx, StatusRequest{Options: leaseOptionsFromConfig(cfg), ID: *id, Wait: *wait, WaitTimeout: *waitTimeout})
} else if isSSH {
var lease LeaseTarget
lease, err = sshBackend.Resolve(ctx, ResolveRequest{Options: leaseOptionsFromConfig(cfg), ID: *id})
if err == nil {
state, err = statusViewFromLeaseTarget(ctx, cfg, lease)
if err == nil && *wait {
_, touchErr := sshBackend.Touch(ctx, TouchRequest{Lease: lease, State: state.State, IdleTimeout: cfg.IdleTimeout})
if touchErr != nil {
fmt.Fprintf(a.Stderr, "warning: touch failed for %s: %v\n", lease.LeaseID, touchErr)
}
}
}
} else {
state, err = a.leaseStatus(ctx, cfg, *id)
}
if err != nil {
return err
}
if *jsonOut {
if !*wait || state.Ready {
return json.NewEncoder(a.Stdout).Encode(state)
}
} else {
tailscale := ""
if state.Tailscale != nil && state.Tailscale.Enabled {
tailscale = fmt.Sprintf(" tailscale=%s", blank(tailscaleTargetHost(*state.Tailscale), blank(state.Tailscale.State, "requested")))
}
telemetry := leaseTelemetryStatusSummary(state.Telemetry)
if telemetry != "" {
telemetry = " " + telemetry
}
fmt.Fprintf(a.Stdout, "%s slug=%s provider=%s target=%s windows_mode=%s state=%s type=%s host=%s network=%s%s ready=%t has_host=%t idle_for=%s idle_timeout=%s expires=%s%s\n", state.ID, blank(state.Slug, "-"), state.Provider, state.TargetOS, blank(state.WindowsMode, "-"), state.State, state.ServerType, state.Host, state.Network, tailscale, state.Ready, state.HasHost, blank(state.IdleFor, "-"), blank(state.IdleTimeout, "-"), blank(state.ExpiresAt, "-"), telemetry)
}
if !*wait || state.Ready {
return nil
}
if time.Now().After(deadline) {
return exit(5, "timed out waiting for %s to become ready", *id)
}
time.Sleep(5 * time.Second)
}
}
func statusViewFromLeaseTarget(ctx context.Context, cfg Config, lease LeaseTarget) (statusView, error) {
server := lease.Server
target := lease.SSH
hasHost := server.PublicNet.IPv4.IP != ""
if target.NetworkKind == NetworkPublic && target.Host != "" {
hasHost = true
}
if (cfg.Provider == "daytona" || server.Provider == "daytona") && target.Host != "" {
hasHost = true
}
resolved, err := resolveNetworkTarget(ctx, cfg, server, target)
if err != nil {
return statusView{}, err
}
target = resolved.Target
state := blank(server.Labels["state"], server.Status)
ready := hasHost && leaseStatusStateCanBeReady(lease, state) && probeSSHReady(ctx, &target, 4*time.Second)
meta := serverTailscaleMetadata(server)
var tailscale *TailscaleMetadata
if meta.Enabled {
tailscale = &meta
}
return statusView{
ID: lease.LeaseID,
Slug: serverSlug(server),
Provider: blank(server.Provider, cfg.Provider),
TargetOS: blank(server.Labels["target"], cfg.TargetOS),
WindowsMode: blank(server.Labels["windows_mode"], cfg.WindowsMode),
State: state,
ServerID: server.DisplayID(),
ServerType: server.ServerType.Name,
Host: server.PublicNet.IPv4.IP,
Network: resolved.Network,
Tailscale: tailscale,
SSHHost: target.Host,
SSHUser: redactedSSHUser(cfg, server, target),
SSHPort: target.Port,
SSHFallbackPorts: target.FallbackPorts,
SSHKey: target.Key,
LastTouchedAt: blank(leaseLabelTimeDisplay(server.Labels["last_touched_at"]), server.Labels["last_touched_at"]),
IdleFor: idleForString(server.Labels["last_touched_at"], time.Now()),
IdleTimeout: leaseLabelDurationDisplay(server.Labels["idle_timeout_secs"], server.Labels["idle_timeout"]),
ExpiresAt: blank(leaseLabelTimeDisplay(server.Labels["expires_at"]), server.Labels["expires_at"]),
Labels: server.Labels,
HasHost: hasHost,
Ready: ready,
}, nil
}
func leaseStatusStateCanBeReady(lease LeaseTarget, state string) bool {
if lease.Coordinator != nil {
return state == "active"
}
return state != "provisioning"
}
type StatusView struct {
ID string `json:"id"`
Slug string `json:"slug,omitempty"`
Provider string `json:"provider"`
TargetOS string `json:"target"`
WindowsMode string `json:"windowsMode,omitempty"`
State string `json:"state"`
ServerID string `json:"serverId"`
ServerType string `json:"serverType"`
Host string `json:"host"`
Network NetworkMode `json:"network"`
Tailscale *TailscaleMetadata `json:"tailscale,omitempty"`
SSHHost string `json:"sshHost"`
SSHUser string `json:"sshUser"`
SSHPort string `json:"sshPort"`
SSHFallbackPorts []string `json:"sshFallbackPorts,omitempty"`
SSHKey string `json:"sshKey"`
LastTouchedAt string `json:"lastTouchedAt,omitempty"`
IdleFor string `json:"idleFor,omitempty"`
IdleTimeout string `json:"idleTimeout,omitempty"`
ExpiresAt string `json:"expiresAt,omitempty"`
Labels map[string]string `json:"labels,omitempty"`
HasHost bool `json:"hasHost"`
Ready bool `json:"ready"`
Telemetry *LeaseTelemetry `json:"telemetry,omitempty"`
TelemetryHistory []*LeaseTelemetry `json:"telemetryHistory,omitempty"`
}
type statusView = StatusView
func (a App) leaseStatus(ctx context.Context, cfg Config, id string) (statusView, error) {
backend, err := loadBackend(cfg, runtimeForApp(a))
if err != nil {
return statusView{}, err
}
if statusBackend, ok := backend.(interface {
Status(context.Context, StatusRequest) (statusView, error)
}); ok {
return statusBackend.Status(ctx, StatusRequest{Options: leaseOptionsFromConfig(cfg), ID: id})
}
if delegated, ok := backend.(DelegatedRunBackend); ok {
return delegated.Status(ctx, StatusRequest{Options: leaseOptionsFromConfig(cfg), ID: id})
}
sshBackend, ok := backend.(SSHLeaseBackend)
if !ok {
return statusView{}, exit(2, "provider=%s does not support status", backend.Spec().Name)
}
lease, err := sshBackend.Resolve(ctx, ResolveRequest{Options: leaseOptionsFromConfig(cfg), ID: id})
if err != nil {
return statusView{}, err
}
return statusViewFromLeaseTarget(ctx, cfg, lease)
}
func (a App) resolveLeaseTarget(ctx context.Context, cfg Config, id string) (Server, SSHTarget, string, error) {
backend, err := loadBackend(cfg, runtimeForApp(a))
if err != nil {
return Server{}, SSHTarget{}, "", err
}
sshBackend, ok := backend.(SSHLeaseBackend)
if !ok {
return Server{}, SSHTarget{}, "", exit(2, "provider=%s does not expose an SSH target", backend.Spec().Name)
}
lease, err := sshBackend.Resolve(ctx, ResolveRequest{Options: leaseOptionsFromConfig(cfg), ID: id})
if err != nil {
return Server{}, SSHTarget{}, "", err
}
return lease.Server, lease.SSH, lease.LeaseID, nil
}
func idleForString(value string, now time.Time) string {
if value == "" {
return ""
}
touched, ok := parseLeaseLabelTime(value)
if !ok || touched.After(now) {
return ""
}
return now.Sub(touched).Round(time.Second).String()
}
func IdleForString(value string, now time.Time) string {
return idleForString(value, now)
}
func redactedSSHUser(cfg Config, server Server, target SSHTarget) string {
if target.AuthSecret {
return "<token>"
}
if cfg.Provider == "daytona" || server.Provider == "daytona" {
return "<token>"
}
return target.User
}
func formatSecondsDuration(seconds int) string {
if seconds <= 0 {
return ""
}
return (time.Duration(seconds) * time.Second).String()
}
func formatSecondsDurationString(value string) string {
duration, ok := parseDurationSecondsLabel(value)
if !ok {
return ""
}
return duration.String()
}