feat(pre-deploy): list hook containers in 'uc ps' and 'uc inspect'

This commit is contained in:
Pasha Sviderski
2026-04-08 19:16:42 +10:00
parent a844cc6f67
commit 1930bb5766
3 changed files with 82 additions and 31 deletions
+47 -17
View File
@@ -10,11 +10,11 @@ import (
"charm.land/lipgloss/v2" "charm.land/lipgloss/v2"
"github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/container"
"github.com/docker/go-units" "github.com/docker/go-units"
"github.com/psviderski/uncloud/internal/cli/tui"
"github.com/spf13/cobra"
"github.com/psviderski/uncloud/internal/cli" "github.com/psviderski/uncloud/internal/cli"
"github.com/psviderski/uncloud/internal/cli/tui"
"github.com/psviderski/uncloud/pkg/api"
"github.com/psviderski/uncloud/pkg/client" "github.com/psviderski/uncloud/pkg/client"
"github.com/spf13/cobra"
) )
const ( const (
@@ -66,12 +66,13 @@ type containerInfo struct {
serviceName string serviceName string
machineName string machineName string
id string id string
name string
image string image string
status string status string
highlight containerHighlight highlight containerHighlight
created time.Time created time.Time
ip string ip string
// Hook type (e.g., "pre-deploy"), empty for regular containers.
hook string
} }
func runPs(ctx context.Context, uncli *cli.CLI, opts psOptions) error { func runPs(ctx context.Context, uncli *cli.CLI, opts psOptions) error {
@@ -132,7 +133,21 @@ func runPs(ctx context.Context, uncli *cli.CLI, opts psOptions) error {
func printContainers(containers []containerInfo) error { func printContainers(containers []containerInfo) error {
t := tui.NewTable() t := tui.NewTable()
t.Headers("SERVICE", "CONTAINER ID", "CONTAINER NAME", "IMAGE", "CREATED", "STATUS", "IP ADDRESS", "MACHINE")
// Show HOOK column only when hook containers are present.
hasHooks := false
for _, ctr := range containers {
if ctr.hook != "" {
hasHooks = true
break
}
}
if hasHooks {
t.Headers("SERVICE", "CONTAINER ID", "IMAGE", "CREATED", "STATUS", "HOOK", "IP ADDRESS", "MACHINE")
} else {
t.Headers("SERVICE", "CONTAINER ID", "IMAGE", "CREATED", "STATUS", "IP ADDRESS", "MACHINE")
}
for _, ctr := range containers { for _, ctr := range containers {
id := ctr.id id := ctr.id
@@ -154,16 +169,28 @@ func printContainers(containers []containerInfo) error {
statusStyle = lipgloss.NewStyle() // Default statusStyle = lipgloss.NewStyle() // Default
} }
t.Row( if hasHooks {
ctr.serviceName, t.Row(
id, ctr.serviceName,
ctr.name, id,
tui.FormatImage(ctr.image, tui.NoStyle), tui.FormatImage(ctr.image, tui.NoStyle),
created, created,
statusStyle.Render(ctr.status), statusStyle.Render(ctr.status),
ctr.ip, ctr.hook,
ctr.machineName, ctr.ip,
) ctr.machineName,
)
} else {
t.Row(
ctr.serviceName,
id,
tui.FormatImage(ctr.image, tui.NoStyle),
created,
statusStyle.Render(ctr.status),
ctr.ip,
ctr.machineName,
)
}
} }
fmt.Println(t) fmt.Println(t)
@@ -220,7 +247,7 @@ func collectContainers(ctx context.Context, cli *client.Client) ([]containerInfo
continue continue
} }
for _, ctr := range msc.Containers { for _, ctr := range append(msc.Containers, msc.HookContainers...) {
if ctr.Container.State == nil || ctr.Container.Config == nil { if ctr.Container.State == nil || ctr.Container.Config == nil {
continue continue
} }
@@ -242,6 +269,9 @@ func collectContainers(ctx context.Context, cli *client.Client) ([]containerInfo
highlight = highlightSuccess highlight = highlightSuccess
} else if ctr.Container.State.Status == "running" { } else if ctr.Container.State.Status == "running" {
highlight = highlightNormal highlight = highlightNormal
} else if ctr.IsHook() && ctr.Container.State.Status == "exited" && ctr.Container.State.ExitCode == 0 {
// Hook containers (e.g., pre-deploy) are expected to exit successfully.
highlight = highlightNormal
} else { // Other non-critical but noteworthy states } else { // Other non-critical but noteworthy states
highlight = highlightWarning highlight = highlightWarning
} }
@@ -259,12 +289,12 @@ func collectContainers(ctx context.Context, cli *client.Client) ([]containerInfo
serviceName: ctr.ServiceName(), serviceName: ctr.ServiceName(),
machineName: machineName, machineName: machineName,
id: ctr.Container.ID, id: ctr.Container.ID,
name: ctr.Container.Name,
image: ctr.Container.Config.Image, image: ctr.Container.Config.Image,
status: status, status: status,
highlight: highlight, highlight: highlight,
created: created, created: created,
ip: ipStr, ip: ipStr,
hook: ctr.Config.Labels[api.LabelHook],
} }
containers = append(containers, info) containers = append(containers, info)
} }
-1
View File
@@ -111,7 +111,6 @@ func TestCollectContainers_NilMetadata(t *testing.T) {
if len(containers) > 0 { if len(containers) > 0 {
c := containers[0] c := containers[0]
assert.Equal(t, "container1", c.id) assert.Equal(t, "container1", c.id)
assert.Equal(t, "test-container", c.name)
assert.Equal(t, "machine-1", c.machineName, "Should fall back to the single machine name when metadata is nil") assert.Equal(t, "machine-1", c.machineName, "Should fall back to the single machine name when metadata is nil")
} }
} }
+35 -13
View File
@@ -60,23 +60,33 @@ func inspect(ctx context.Context, uncli *cli.CLI, opts inspectOptions) error {
fmt.Printf("Mode: %s\n", svc.Mode) fmt.Printf("Mode: %s\n", svc.Mode)
fmt.Println() fmt.Println()
// Combine regular and hook containers.
allContainers := append(svc.Containers, svc.HookContainers...)
// Parse created times for sorting and display. // Parse created times for sorting and display.
createdTimes := make(map[string]time.Time, len(svc.Containers)) createdTimes := make(map[string]time.Time, len(allContainers))
for _, ctr := range svc.Containers { for _, ctr := range allContainers {
createdTimes[ctr.Container.ID], _ = time.Parse(time.RFC3339Nano, ctr.Container.Created) createdTimes[ctr.Container.ID], _ = time.Parse(time.RFC3339Nano, ctr.Container.Created)
} }
// Sort containers by created time (newest first). // Sort containers by created time (newest first).
slices.SortFunc(svc.Containers, func(a, b api.MachineServiceContainer) int { slices.SortFunc(allContainers, func(a, b api.MachineServiceContainer) int {
return createdTimes[b.Container.ID].Compare(createdTimes[a.Container.ID]) return createdTimes[b.Container.ID].Compare(createdTimes[a.Container.ID])
}) })
// Print the list of containers in a table format. // Print the list of containers in a table format.
// Show HOOK column only when hook containers are present.
hasHooks := len(svc.HookContainers) > 0
t := tui.NewTable() t := tui.NewTable()
t.Headers("CONTAINER ID", "IMAGE", "CREATED", "STATUS", "IP ADDRESS", "MACHINE") if hasHooks {
t.Headers("CONTAINER ID", "IMAGE", "CREATED", "STATUS", "HOOK", "IP ADDRESS", "MACHINE")
} else {
t.Headers("CONTAINER ID", "IMAGE", "CREATED", "STATUS", "IP ADDRESS", "MACHINE")
}
now := time.Now().UTC() now := time.Now().UTC()
for _, ctr := range svc.Containers { for _, ctr := range allContainers {
created := units.HumanDuration(now.Sub(createdTimes[ctr.Container.ID])) + " ago" created := units.HumanDuration(now.Sub(createdTimes[ctr.Container.ID])) + " ago"
machine := machinesNamesByID[ctr.MachineID] machine := machinesNamesByID[ctr.MachineID]
@@ -95,14 +105,26 @@ func inspect(ctx context.Context, uncli *cli.CLI, opts inspectOptions) error {
ipStr = ip.String() ipStr = ip.String()
} }
t.Row( if hasHooks {
stringid.TruncateID(ctr.Container.ID), t.Row(
tui.FormatImage(ctr.Container.Config.Image, tui.NoStyle), stringid.TruncateID(ctr.Container.ID),
created, tui.FormatImage(ctr.Container.Config.Image, tui.NoStyle),
state, created,
ipStr, state,
machine, ctr.Container.Config.Labels[api.LabelHook],
) ipStr,
machine,
)
} else {
t.Row(
stringid.TruncateID(ctr.Container.ID),
tui.FormatImage(ctr.Container.Config.Image, tui.NoStyle),
created,
state,
ipStr,
machine,
)
}
} }
fmt.Println(t) fmt.Println(t)