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
+38 -8
View File
@@ -10,11 +10,11 @@ import (
"charm.land/lipgloss/v2"
"github.com/docker/docker/api/types/container"
"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/tui"
"github.com/psviderski/uncloud/pkg/api"
"github.com/psviderski/uncloud/pkg/client"
"github.com/spf13/cobra"
)
const (
@@ -66,12 +66,13 @@ type containerInfo struct {
serviceName string
machineName string
id string
name string
image string
status string
highlight containerHighlight
created time.Time
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 {
@@ -132,7 +133,21 @@ func runPs(ctx context.Context, uncli *cli.CLI, opts psOptions) error {
func printContainers(containers []containerInfo) error {
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 {
id := ctr.id
@@ -154,10 +169,21 @@ func printContainers(containers []containerInfo) error {
statusStyle = lipgloss.NewStyle() // Default
}
if hasHooks {
t.Row(
ctr.serviceName,
id,
tui.FormatImage(ctr.image, tui.NoStyle),
created,
statusStyle.Render(ctr.status),
ctr.hook,
ctr.ip,
ctr.machineName,
)
} else {
t.Row(
ctr.serviceName,
id,
ctr.name,
tui.FormatImage(ctr.image, tui.NoStyle),
created,
statusStyle.Render(ctr.status),
@@ -165,6 +191,7 @@ func printContainers(containers []containerInfo) error {
ctr.machineName,
)
}
}
fmt.Println(t)
return nil
@@ -220,7 +247,7 @@ func collectContainers(ctx context.Context, cli *client.Client) ([]containerInfo
continue
}
for _, ctr := range msc.Containers {
for _, ctr := range append(msc.Containers, msc.HookContainers...) {
if ctr.Container.State == nil || ctr.Container.Config == nil {
continue
}
@@ -242,6 +269,9 @@ func collectContainers(ctx context.Context, cli *client.Client) ([]containerInfo
highlight = highlightSuccess
} else if ctr.Container.State.Status == "running" {
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
highlight = highlightWarning
}
@@ -259,12 +289,12 @@ func collectContainers(ctx context.Context, cli *client.Client) ([]containerInfo
serviceName: ctr.ServiceName(),
machineName: machineName,
id: ctr.Container.ID,
name: ctr.Container.Name,
image: ctr.Container.Config.Image,
status: status,
highlight: highlight,
created: created,
ip: ipStr,
hook: ctr.Config.Labels[api.LabelHook],
}
containers = append(containers, info)
}
-1
View File
@@ -111,7 +111,6 @@ func TestCollectContainers_NilMetadata(t *testing.T) {
if len(containers) > 0 {
c := containers[0]
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")
}
}
+26 -4
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.Println()
// Combine regular and hook containers.
allContainers := append(svc.Containers, svc.HookContainers...)
// Parse created times for sorting and display.
createdTimes := make(map[string]time.Time, len(svc.Containers))
for _, ctr := range svc.Containers {
createdTimes := make(map[string]time.Time, len(allContainers))
for _, ctr := range allContainers {
createdTimes[ctr.Container.ID], _ = time.Parse(time.RFC3339Nano, ctr.Container.Created)
}
// 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])
})
// 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()
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()
for _, ctr := range svc.Containers {
for _, ctr := range allContainers {
created := units.HumanDuration(now.Sub(createdTimes[ctr.Container.ID])) + " ago"
machine := machinesNamesByID[ctr.MachineID]
@@ -95,6 +105,17 @@ func inspect(ctx context.Context, uncli *cli.CLI, opts inspectOptions) error {
ipStr = ip.String()
}
if hasHooks {
t.Row(
stringid.TruncateID(ctr.Container.ID),
tui.FormatImage(ctr.Container.Config.Image, tui.NoStyle),
created,
state,
ctr.Container.Config.Labels[api.LabelHook],
ipStr,
machine,
)
} else {
t.Row(
stringid.TruncateID(ctr.Container.ID),
tui.FormatImage(ctr.Container.Config.Image, tui.NoStyle),
@@ -104,6 +125,7 @@ func inspect(ctx context.Context, uncli *cli.CLI, opts inspectOptions) error {
machine,
)
}
}
fmt.Println(t)
return nil