chore: add CREATED column to 'uc ps', fallback sorting by CREATED

This commit is contained in:
Pasha Sviderski
2025-12-12 20:01:49 +10:00
parent 596bc561b8
commit bc65c73c51
+22 -17
View File
@@ -4,11 +4,13 @@ import (
"context" "context"
"fmt" "fmt"
"sort" "sort"
"time"
"github.com/charmbracelet/huh/spinner" "github.com/charmbracelet/huh/spinner"
"github.com/charmbracelet/lipgloss" "github.com/charmbracelet/lipgloss"
"github.com/charmbracelet/lipgloss/table" "github.com/charmbracelet/lipgloss/table"
"github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/container"
"github.com/docker/go-units"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/psviderski/uncloud/internal/cli" "github.com/psviderski/uncloud/internal/cli"
@@ -44,16 +46,19 @@ func NewPsCommand() *cobra.Command {
This command provides a comprehensive overview of all running containers that are part of a service, This command provides a comprehensive overview of all running containers that are part of a service,
making it easy to see the distribution and status of containers across the cluster.`, making it easy to see the distribution and status of containers across the cluster.`,
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
uncli := cmd.Context().Value("cli").(*cli.CLI)
if opts.sortBy != sortByService && opts.sortBy != sortByMachine && opts.sortBy != sortByHealth { if opts.sortBy != sortByService && opts.sortBy != sortByMachine && opts.sortBy != sortByHealth {
return fmt.Errorf("invalid value for --sort: %q, must be one of '%s', '%s' or '%s'", opts.sortBy, return fmt.Errorf("invalid value for --sort: %q, must be one of '%s', '%s' or '%s'", opts.sortBy,
sortByService, sortByMachine, sortByHealth) sortByService, sortByMachine, sortByHealth)
} }
return runPs(cmd, opts)
return runPs(cmd.Context(), uncli, opts)
}, },
GroupID: "service", GroupID: "service",
} }
cmd.Flags().StringVarP(&opts.sortBy, "sort", "s", sortByService, cmd.Flags().StringVarP(&opts.sortBy, "sort", "s", sortByService,
"Sort containers by 'service', 'machine' or 'health'") "Sort containers by 'service', 'machine', or 'health'.")
return cmd return cmd
} }
@@ -65,15 +70,15 @@ type containerInfo struct {
image string image string
status string status string
highlight containerHighlight highlight containerHighlight
created time.Time
} }
func runPs(cmd *cobra.Command, opts psOptions) error { func runPs(ctx context.Context, uncli *cli.CLI, opts psOptions) error {
uncli := cmd.Context().Value("cli").(*cli.CLI) clusterClient, err := uncli.ConnectCluster(ctx)
client, err := uncli.ConnectCluster(cmd.Context())
if err != nil { if err != nil {
return fmt.Errorf("connect to cluster: %w", err) return fmt.Errorf("connect to cluster: %w", err)
} }
defer client.Close() defer clusterClient.Close()
var containers []containerInfo var containers []containerInfo
err = spinner.New(). err = spinner.New().
@@ -81,7 +86,7 @@ func runPs(cmd *cobra.Command, opts psOptions) error {
Type(spinner.MiniDot). Type(spinner.MiniDot).
Style(lipgloss.NewStyle().Foreground(lipgloss.Color("3"))). Style(lipgloss.NewStyle().Foreground(lipgloss.Color("3"))).
ActionWithErr(func(ctx context.Context) error { ActionWithErr(func(ctx context.Context) error {
containers, err = collectContainers(ctx, client) containers, err = collectContainers(ctx, clusterClient)
return err return err
}). }).
Run() Run()
@@ -89,7 +94,7 @@ func runPs(cmd *cobra.Command, opts psOptions) error {
return fmt.Errorf("collect containers: %w", err) return fmt.Errorf("collect containers: %w", err)
} }
// Sort the containers based on the sorting option // Sort the containers based on the sorting option.
sort.SliceStable(containers, func(i, j int) bool { sort.SliceStable(containers, func(i, j int) bool {
a, b := containers[i], containers[j] a, b := containers[i], containers[j]
switch opts.sortBy { switch opts.sortBy {
@@ -100,9 +105,6 @@ func runPs(cmd *cobra.Command, opts psOptions) error {
if a.serviceName != b.serviceName { if a.serviceName != b.serviceName {
return a.serviceName < b.serviceName return a.serviceName < b.serviceName
} }
if a.machineName != b.machineName {
return a.machineName < b.machineName
}
case sortByMachine: case sortByMachine:
if a.machineName != b.machineName { if a.machineName != b.machineName {
return a.machineName < b.machineName return a.machineName < b.machineName
@@ -114,12 +116,9 @@ func runPs(cmd *cobra.Command, opts psOptions) error {
if a.serviceName != b.serviceName { if a.serviceName != b.serviceName {
return a.serviceName < b.serviceName return a.serviceName < b.serviceName
} }
if a.machineName != b.machineName {
return a.machineName < b.machineName
} }
} // Fallback to creation time (newest first).
// Final tie-breaker return a.created.After(b.created)
return a.name < b.name
}) })
return printContainers(containers) return printContainers(containers)
@@ -143,7 +142,7 @@ func printContainers(containers []containerInfo) error {
return lipgloss.NewStyle().PaddingRight(3) return lipgloss.NewStyle().PaddingRight(3)
}) })
t.Headers("SERVICE", "CONTAINER ID", "NAME", "IMAGE", "STATUS", "MACHINE") t.Headers("SERVICE", "CONTAINER ID", "CONTAINER NAME", "IMAGE", "CREATED", "STATUS", "MACHINE")
for _, ctr := range containers { for _, ctr := range containers {
id := ctr.id id := ctr.id
@@ -151,6 +150,8 @@ func printContainers(containers []containerInfo) error {
id = id[:12] id = id[:12]
} }
created := units.HumanDuration(time.Now().UTC().Sub(ctr.created)) + " ago"
var statusStyle lipgloss.Style var statusStyle lipgloss.Style
switch ctr.highlight { switch ctr.highlight {
case highlightSuccess: case highlightSuccess:
@@ -168,6 +169,7 @@ func printContainers(containers []containerInfo) error {
id, id,
ctr.name, ctr.name,
ctr.image, ctr.image,
created,
statusStyle.Render(ctr.status), statusStyle.Render(ctr.status),
ctr.machineName, ctr.machineName,
) )
@@ -253,6 +255,8 @@ func collectContainers(ctx context.Context, cli *client.Client) ([]containerInfo
highlight = highlightWarning highlight = highlightWarning
} }
created, _ := time.Parse(time.RFC3339Nano, ctr.Container.Created)
info := containerInfo{ info := containerInfo{
serviceName: ctr.ServiceName(), serviceName: ctr.ServiceName(),
machineName: machineName, machineName: machineName,
@@ -261,6 +265,7 @@ func collectContainers(ctx context.Context, cli *client.Client) ([]containerInfo
image: ctr.Container.Config.Image, image: ctr.Container.Config.Image,
status: status, status: status,
highlight: highlight, highlight: highlight,
created: created,
} }
containers = append(containers, info) containers = append(containers, info)
} }