Merge pull request #1 from christophe-duc/feature/add_pod_support

feature: add pod support in the container pane
This commit is contained in:
Christophe 2026-01-07 18:41:08 -04:00 committed by GitHub
commit 0730120daa
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
35 changed files with 2277 additions and 625 deletions

View file

@ -55,11 +55,13 @@ pkg/
- `pkg/commands/runtime_socket.go` - Socket mode implementation using Podman REST API bindings - `pkg/commands/runtime_socket.go` - Socket mode implementation using Podman REST API bindings
- `pkg/commands/runtime_libpod.go` - Direct libpod implementation (Linux+CGO only) - `pkg/commands/runtime_libpod.go` - Direct libpod implementation (Linux+CGO only)
- `pkg/commands/runtime_libpod_stub.go` - Stub for non-Linux platforms - `pkg/commands/runtime_libpod_stub.go` - Stub for non-Linux platforms
- `pkg/commands/runtime_types.go` - Custom types (ContainerSummary, ImageSummary, etc.) - `pkg/commands/runtime_types.go` - Custom types (ContainerSummary, ImageSummary, PodSummary, etc.)
**Podman integration:** **Podman integration:**
- `pkg/commands/podman.go` - Main client connection, auto-detection, and initialization - `pkg/commands/podman.go` - Main client connection, auto-detection, and initialization
- `pkg/commands/container.go` - Container wrapper operations - `pkg/commands/container.go` - Container wrapper operations
- `pkg/commands/pod.go` - Pod wrapper with state and container count methods
- `pkg/commands/container_list_item.go` - Unified wrapper for pods/containers in list view
- `pkg/commands/image.go` - Image wrapper operations - `pkg/commands/image.go` - Image wrapper operations
- `pkg/commands/volume.go` - Volume wrapper operations - `pkg/commands/volume.go` - Volume wrapper operations
- `pkg/commands/network.go` - Network wrapper operations - `pkg/commands/network.go` - Network wrapper operations
@ -102,6 +104,9 @@ ListContainers() / InspectContainer() / ContainerStats()
StartContainer() / StopContainer() / PauseContainer() / UnpauseContainer() StartContainer() / StopContainer() / PauseContainer() / UnpauseContainer()
RestartContainer() / RemoveContainer() / PruneContainers() / ContainerTop() RestartContainer() / RemoveContainer() / PruneContainers() / ContainerTop()
// Pod operations
ListPods() - Returns all pods with their metadata
// Image operations // Image operations
ListImages() / InspectImage() / ImageHistory() / RemoveImage() / PruneImages() ListImages() / InspectImage() / ImageHistory() / RemoveImage() / PruneImages()

File diff suppressed because it is too large Load diff

4
go.mod
View file

@ -7,7 +7,6 @@ require (
github.com/boz/go-throttle v0.0.0-20160922054636-fdc4eab740c1 github.com/boz/go-throttle v0.0.0-20160922054636-fdc4eab740c1
github.com/cloudfoundry/jibber_jabber v0.0.0-20151120183258-bcc4c8345a21 github.com/cloudfoundry/jibber_jabber v0.0.0-20151120183258-bcc4c8345a21
github.com/containers/podman/v5 v5.7.1 github.com/containers/podman/v5 v5.7.1
github.com/docker/docker v28.5.1+incompatible
github.com/fatih/color v1.15.0 github.com/fatih/color v1.15.0
github.com/go-errors/errors v1.5.1 github.com/go-errors/errors v1.5.1
github.com/gookit/color v1.5.0 github.com/gookit/color v1.5.0
@ -27,6 +26,7 @@ require (
github.com/sirupsen/logrus v1.9.3 github.com/sirupsen/logrus v1.9.3
github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad
github.com/stretchr/testify v1.11.1 github.com/stretchr/testify v1.11.1
go.podman.io/common v0.66.1
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1
) )
@ -63,6 +63,7 @@ require (
github.com/disiqueira/gotree/v3 v3.0.2 // indirect github.com/disiqueira/gotree/v3 v3.0.2 // indirect
github.com/distribution/reference v0.6.0 // indirect github.com/distribution/reference v0.6.0 // indirect
github.com/docker/distribution v2.8.3+incompatible // indirect github.com/docker/distribution v2.8.3+incompatible // indirect
github.com/docker/docker v28.5.1+incompatible // indirect
github.com/docker/docker-credential-helpers v0.9.4 // indirect github.com/docker/docker-credential-helpers v0.9.4 // indirect
github.com/docker/go-connections v0.6.0 // indirect github.com/docker/go-connections v0.6.0 // indirect
github.com/docker/go-plugins-helpers v0.0.0-20240701071450-45e2431495c8 // indirect github.com/docker/go-plugins-helpers v0.0.0-20240701071450-45e2431495c8 // indirect
@ -155,7 +156,6 @@ require (
go.opentelemetry.io/otel v1.36.0 // indirect go.opentelemetry.io/otel v1.36.0 // indirect
go.opentelemetry.io/otel/metric v1.36.0 // indirect go.opentelemetry.io/otel/metric v1.36.0 // indirect
go.opentelemetry.io/otel/trace v1.36.0 // indirect go.opentelemetry.io/otel/trace v1.36.0 // indirect
go.podman.io/common v0.66.1 // indirect
go.podman.io/image/v5 v5.38.0 // indirect go.podman.io/image/v5 v5.38.0 // indirect
go.podman.io/storage v1.61.0 // indirect go.podman.io/storage v1.61.0 // indirect
go.yaml.in/yaml/v2 v2.4.2 // indirect go.yaml.in/yaml/v2 v2.4.2 // indirect

View file

@ -0,0 +1,84 @@
package commands
// ContainerListItem represents either a pod or a container in the unified list view.
// This allows the container panel to display both pods and containers.
type ContainerListItem struct {
IsPod bool
Pod *Pod // Set if IsPod is true
Container *Container // Set if IsPod is false
Indent int // 0 for pods/standalone containers, 2 for containers in pods
}
// ID returns the unique ID for the item.
func (c *ContainerListItem) ID() string {
if c.IsPod && c.Pod != nil {
return c.Pod.ID
}
if c.Container != nil {
return c.Container.ID
}
return ""
}
// Name returns the display name for the item.
func (c *ContainerListItem) Name() string {
if c.IsPod && c.Pod != nil {
return c.Pod.Name
}
if c.Container != nil {
return c.Container.Name
}
return ""
}
// State returns the state for the item.
func (c *ContainerListItem) State() string {
if c.IsPod && c.Pod != nil {
return c.Pod.State()
}
if c.Container != nil {
return c.Container.Summary.State
}
return ""
}
// GetContainers returns the containers if this is a pod, nil otherwise.
func (c *ContainerListItem) GetContainers() []*Container {
if c.IsPod && c.Pod != nil {
return c.Pod.Containers
}
return nil
}
// IsInPod returns true if this is a container that belongs to a pod.
func (c *ContainerListItem) IsInPod() bool {
if c.IsPod {
return false
}
if c.Container != nil {
return c.Container.Summary.Pod != ""
}
return false
}
// PodID returns the pod ID if this container is in a pod, empty string otherwise.
func (c *ContainerListItem) PodID() string {
if c.IsPod && c.Pod != nil {
return c.Pod.ID
}
if c.Container != nil {
return c.Container.Summary.Pod
}
return ""
}
// PodName returns the pod name if this container is in a pod, empty string otherwise.
func (c *ContainerListItem) PodName() string {
if c.IsPod && c.Pod != nil {
return c.Pod.Name
}
if c.Container != nil {
return c.Container.Summary.PodName
}
return ""
}

36
pkg/commands/pod.go Normal file
View file

@ -0,0 +1,36 @@
package commands
import (
"github.com/sirupsen/logrus"
)
// Pod represents a Podman pod with its containers.
type Pod struct {
ID string
Name string
Summary PodSummary
Containers []*Container // Non-infra containers in this pod
OSCommand *OSCommand
Log *logrus.Entry
}
// State returns the pod state.
func (p *Pod) State() string {
return p.Summary.Status
}
// HasContainers returns true if the pod has non-infra containers.
func (p *Pod) HasContainers() bool {
return len(p.Containers) > 0
}
// GetRunningContainerCount returns the number of running containers in the pod.
func (p *Pod) GetRunningContainerCount() int {
count := 0
for _, c := range p.Containers {
if c.Summary.State == "running" {
count++
}
}
return count
}

View file

@ -7,6 +7,7 @@ import (
ogLog "log" ogLog "log"
"os" "os"
"os/exec" "os/exec"
"sort"
"strings" "strings"
"sync" "sync"
"time" "time"
@ -347,15 +348,35 @@ func calculateMemoryPercentageFromEntry(stats ContainerStatsEntry) float64 {
return 0.0 return 0.0
} }
func (c *PodmanCommand) RefreshContainersAndServices(currentServices []*Service, currentContainers []*Container) ([]*Container, []*Service, error) { func (c *PodmanCommand) RefreshContainersAndServices(currentServices []*Service, currentItems []*ContainerListItem) ([]*ContainerListItem, []*Service, error) {
c.ServiceMutex.Lock() c.ServiceMutex.Lock()
defer c.ServiceMutex.Unlock() defer c.ServiceMutex.Unlock()
// Extract existing containers from current items
var currentContainers []*Container
for _, item := range currentItems {
if !item.IsPod && item.Container != nil {
currentContainers = append(currentContainers, item.Container)
}
}
containers, err := c.GetContainers(currentContainers) containers, err := c.GetContainers(currentContainers)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
// Get pods
ctx := context.Background()
podSummaries, err := c.Runtime.ListPods(ctx)
if err != nil {
// Don't fail if pods can't be listed, just log and continue without pods
c.Log.Warnf("Failed to list pods: %v", err)
podSummaries = nil
}
// Build the unified list
items := c.buildContainerListItems(containers, podSummaries)
var services []*Service var services []*Service
// we only need to get these services once because they won't change in the runtime of the program // we only need to get these services once because they won't change in the runtime of the program
if currentServices != nil { if currentServices != nil {
@ -367,9 +388,98 @@ func (c *PodmanCommand) RefreshContainersAndServices(currentServices []*Service,
} }
} }
c.assignContainersToServices(containers, services) // Extract just containers for service assignment
var containersForServices []*Container
for _, item := range items {
if !item.IsPod && item.Container != nil {
containersForServices = append(containersForServices, item.Container)
}
}
c.assignContainersToServices(containersForServices, services)
return containers, services, nil return items, services, nil
}
// buildContainerListItems creates a unified list of pods and containers
func (c *PodmanCommand) buildContainerListItems(containers []*Container, podSummaries []PodSummary) []*ContainerListItem {
var items []*ContainerListItem
// Create a map of pod ID -> pod summary
podMap := make(map[string]PodSummary)
for _, ps := range podSummaries {
podMap[ps.ID] = ps
}
// Create a map of pod ID -> containers in that pod
podContainers := make(map[string][]*Container)
var standaloneContainers []*Container
for _, ctr := range containers {
// Skip infra containers
if ctr.Summary.IsInfra {
continue
}
if ctr.Summary.Pod != "" {
podContainers[ctr.Summary.Pod] = append(podContainers[ctr.Summary.Pod], ctr)
} else {
standaloneContainers = append(standaloneContainers, ctr)
}
}
// Sort pod IDs for deterministic ordering
podIDs := make([]string, 0, len(podMap))
for podID := range podMap {
podIDs = append(podIDs, podID)
}
sort.Slice(podIDs, func(i, j int) bool {
return podMap[podIDs[i]].Name < podMap[podIDs[j]].Name
})
// Add pods and their containers
for _, podID := range podIDs {
ps := podMap[podID]
// Create pod object
pod := &Pod{
ID: ps.ID,
Name: ps.Name,
Summary: ps,
Containers: podContainers[podID],
OSCommand: c.OSCommand,
Log: c.Log,
}
// Add pod item
items = append(items, &ContainerListItem{
IsPod: true,
Pod: pod,
Indent: 0,
})
// Add containers in this pod with indent
for _, ctr := range podContainers[podID] {
// Set pod name on container if not already set
if ctr.Summary.PodName == "" {
ctr.Summary.PodName = ps.Name
}
items = append(items, &ContainerListItem{
IsPod: false,
Container: ctr,
Indent: 2,
})
}
}
// Add standalone containers
for _, ctr := range standaloneContainers {
items = append(items, &ContainerListItem{
IsPod: false,
Container: ctr,
Indent: 0,
})
}
return items
} }
func (c *PodmanCommand) assignContainersToServices(containers []*Container, services []*Service) { func (c *PodmanCommand) assignContainersToServices(containers []*Container, services []*Service) {

View file

@ -37,6 +37,9 @@ type ContainerRuntime interface {
RemoveNetwork(ctx context.Context, name string) error RemoveNetwork(ctx context.Context, name string) error
PruneNetworks(ctx context.Context) error PruneNetworks(ctx context.Context) error
// Pod operations
ListPods(ctx context.Context) ([]PodSummary, error)
// Events streams container/image/volume/network events // Events streams container/image/volume/network events
Events(ctx context.Context) (<-chan Event, <-chan error) Events(ctx context.Context) (<-chan Event, <-chan error)

View file

@ -146,7 +146,7 @@ func (r *LibpodRuntime) ContainerTop(ctx context.Context, id string) ([]string,
// Parse the result: first line is headers, rest are process rows // Parse the result: first line is headers, rest are process rows
// Each line is space-separated fields // Each line is space-separated fields
headers := splitFields(result[0]) headers := splitFields(result[0])
var processes [][]string processes := make([][]string, 0, len(result)-1)
for _, line := range result[1:] { for _, line := range result[1:] {
processes = append(processes, splitFields(line)) processes = append(processes, splitFields(line))
} }
@ -309,6 +309,37 @@ func (r *LibpodRuntime) PruneNetworks(ctx context.Context) error {
return nil return nil
} }
// Pod operations
// ListPods returns all pods.
func (r *LibpodRuntime) ListPods(ctx context.Context) ([]PodSummary, error) {
pods, err := r.runtime.GetAllPods()
if err != nil {
return nil, err
}
result := make([]PodSummary, len(pods))
for i, pod := range pods {
status, err := pod.GetPodStatus()
if err != nil {
status = "unknown"
}
config, configErr := pod.Config()
infraID := ""
if configErr == nil && config != nil {
infraID = config.InfraContainerID
}
result[i] = PodSummary{
ID: pod.ID(),
Name: pod.Name(),
Status: status,
Created: pod.CreatedTime(),
InfraID: infraID,
Labels: pod.Labels(),
}
}
return result, nil
}
// Events streams container runtime events. // Events streams container runtime events.
// For libpod, we use a polling approach since direct event streaming requires more setup. // For libpod, we use a polling approach since direct event streaming requires more setup.
func (r *LibpodRuntime) Events(ctx context.Context) (<-chan Event, <-chan error) { func (r *LibpodRuntime) Events(ctx context.Context) (<-chan Event, <-chan error) {
@ -361,6 +392,14 @@ func convertLibpodContainerList(ctrs []*libpod.Container) ([]ContainerSummary, e
if len(config.Command) > 0 { if len(config.Command) > 0 {
command = config.Command[0] command = config.Command[0]
} }
// Get pod name if container is in a pod
podName := ""
if podID := ctr.PodID(); podID != "" {
// Try to get pod name from config labels or leave empty
if pn, ok := config.Labels["io.kubernetes.pod.name"]; ok {
podName = pn
}
}
result[i] = ContainerSummary{ result[i] = ContainerSummary{
ID: ctr.ID(), ID: ctr.ID(),
Names: []string{ctr.Name()}, Names: []string{ctr.Name()},
@ -372,6 +411,8 @@ func convertLibpodContainerList(ctrs []*libpod.Container) ([]ContainerSummary, e
Status: state.String(), Status: state.String(),
Labels: config.Labels, Labels: config.Labels,
Pod: ctr.PodID(), Pod: ctr.PodID(),
PodName: podName,
IsInfra: ctr.IsInfra(),
} }
} }
return result, nil return result, nil

View file

@ -128,6 +128,12 @@ func (r *LibpodRuntime) PruneNetworks(ctx context.Context) error {
return ErrLibpodNotAvailable return ErrLibpodNotAvailable
} }
// Pod operations - all return ErrLibpodNotAvailable
func (r *LibpodRuntime) ListPods(ctx context.Context) ([]PodSummary, error) {
return nil, ErrLibpodNotAvailable
}
// Events returns an error channel on non-Linux platforms. // Events returns an error channel on non-Linux platforms.
func (r *LibpodRuntime) Events(ctx context.Context) (<-chan Event, <-chan error) { func (r *LibpodRuntime) Events(ctx context.Context) (<-chan Event, <-chan error) {
errChan := make(chan error, 1) errChan := make(chan error, 1)

View file

@ -39,6 +39,9 @@ type MockRuntime struct {
RemoveNetworkFunc func(ctx context.Context, name string) error RemoveNetworkFunc func(ctx context.Context, name string) error
PruneNetworksFunc func(ctx context.Context) error PruneNetworksFunc func(ctx context.Context) error
// Pod operation mocks
ListPodsFunc func(ctx context.Context) ([]PodSummary, error)
// Event mock // Event mock
EventsFunc func(ctx context.Context) (<-chan Event, <-chan error) EventsFunc func(ctx context.Context) (<-chan Event, <-chan error)
@ -251,6 +254,16 @@ func (m *MockRuntime) PruneNetworks(ctx context.Context) error {
return ErrMockNotImplemented return ErrMockNotImplemented
} }
// Pod operations
func (m *MockRuntime) ListPods(ctx context.Context) ([]PodSummary, error) {
m.recordCall("ListPods")
if m.ListPodsFunc != nil {
return m.ListPodsFunc(ctx)
}
return nil, ErrMockNotImplemented
}
// Events // Events
func (m *MockRuntime) Events(ctx context.Context) (<-chan Event, <-chan error) { func (m *MockRuntime) Events(ctx context.Context) (<-chan Event, <-chan error) {

View file

@ -11,6 +11,7 @@ import (
"github.com/containers/podman/v5/pkg/bindings/containers" "github.com/containers/podman/v5/pkg/bindings/containers"
"github.com/containers/podman/v5/pkg/bindings/images" "github.com/containers/podman/v5/pkg/bindings/images"
"github.com/containers/podman/v5/pkg/bindings/network" "github.com/containers/podman/v5/pkg/bindings/network"
"github.com/containers/podman/v5/pkg/bindings/pods"
"github.com/containers/podman/v5/pkg/bindings/system" "github.com/containers/podman/v5/pkg/bindings/system"
"github.com/containers/podman/v5/pkg/bindings/volumes" "github.com/containers/podman/v5/pkg/bindings/volumes"
"github.com/containers/podman/v5/pkg/domain/entities" "github.com/containers/podman/v5/pkg/domain/entities"
@ -275,6 +276,27 @@ func (r *SocketRuntime) PruneNetworks(ctx context.Context) error {
return err return err
} }
// ListPods returns all pods using the pods bindings API.
func (r *SocketRuntime) ListPods(ctx context.Context) ([]PodSummary, error) {
podList, err := pods.List(r.conn, nil)
if err != nil {
return nil, err
}
result := make([]PodSummary, len(podList))
for i, p := range podList {
result[i] = PodSummary{
ID: p.Id,
Name: p.Name,
Status: p.Status,
Created: p.Created,
InfraID: p.InfraId,
Labels: p.Labels,
}
}
return result, nil
}
// Events streams container runtime events. // Events streams container runtime events.
func (r *SocketRuntime) Events(ctx context.Context) (<-chan Event, <-chan error) { func (r *SocketRuntime) Events(ctx context.Context) (<-chan Event, <-chan error) {
eventChan := make(chan Event) eventChan := make(chan Event)
@ -346,6 +368,7 @@ func convertPodmanContainerList(podmanContainers []entities.ListContainer) []Con
SizeRootFs: sizeRootFs, SizeRootFs: sizeRootFs,
Pod: c.Pod, Pod: c.Pod,
PodName: c.PodName, PodName: c.PodName,
IsInfra: c.IsInfra,
} }
} }
return result return result

View file

@ -20,6 +20,7 @@ type ContainerSummary struct {
// Podman-specific fields // Podman-specific fields
Pod string Pod string
PodName string PodName string
IsInfra bool
} }
// PortMapping represents a container port mapping. // PortMapping represents a container port mapping.
@ -424,3 +425,13 @@ type EventActor struct {
ID string ID string
Attributes map[string]string Attributes map[string]string
} }
// PodSummary provides runtime-agnostic pod information.
type PodSummary struct {
ID string
Name string
Status string
Created time.Time
InfraID string
Labels map[string]string
}

View file

@ -142,3 +142,75 @@ func (gui *Gui) writeContainerLogs(ctr *commands.Container, ctx context.Context,
return err return err
} }
} }
// Pod logs rendering
func (gui *Gui) renderPodLogsToMain(pod *commands.Pod) tasks.TaskFunc {
return gui.NewTickerTask(TickerTaskOpts{
Func: func(ctx context.Context, notifyStopped chan struct{}) {
gui.renderPodLogsToMainAux(pod, ctx, notifyStopped)
},
Duration: time.Millisecond * 200,
Before: func(ctx context.Context) { gui.clearMainView() },
Wrap: gui.Config.UserConfig.Gui.WrapMainPanel,
Autoscroll: true,
})
}
func (gui *Gui) renderPodLogsToMainAux(pod *commands.Pod, ctx context.Context, notifyStopped chan struct{}) {
gui.clearMainView()
defer func() {
notifyStopped <- struct{}{}
}()
mainView := gui.Views.Main
if err := gui.writePodLogs(pod, ctx, mainView); err != nil {
gui.Log.Error(err)
}
// Wait for context cancellation
<-ctx.Done()
}
func (gui *Gui) writePodLogs(pod *commands.Pod, ctx context.Context, writer io.Writer) error {
// Build podman pod logs command
// Note: --color is used to distinguish output from different containers in the pod
args := []string{"pod", "logs", "--follow"}
if gui.Config.UserConfig.Logs.Timestamps {
args = append(args, "--timestamps")
}
if gui.Config.UserConfig.Logs.Since != "" {
args = append(args, "--since", gui.Config.UserConfig.Logs.Since)
}
if gui.Config.UserConfig.Logs.Tail != "" {
args = append(args, "--tail", gui.Config.UserConfig.Logs.Tail)
}
args = append(args, pod.Name)
cmd := pod.OSCommand.NewCmd("podman", args...)
cmd.Stdout = writer
cmd.Stderr = writer
if err := cmd.Start(); err != nil {
gui.Log.Error(err)
return err
}
// Wait for context cancellation or command completion
done := make(chan error, 1)
go func() {
done <- cmd.Wait()
}()
select {
case <-ctx.Done():
if err := pod.OSCommand.Kill(cmd); err != nil {
gui.Log.Warn(err)
}
return nil
case err := <-done:
return err
}
}

View file

@ -18,7 +18,7 @@ import (
"github.com/samber/lo" "github.com/samber/lo"
) )
func (gui *Gui) getContainersPanel() *panels.SideListPanel[*commands.Container] { func (gui *Gui) getContainersPanel() *panels.SideListPanel[*commands.ContainerListItem] {
// Standalone containers are containers which are either one-off containers, or whose service is not part of this docker-compose context. // Standalone containers are containers which are either one-off containers, or whose service is not part of this docker-compose context.
isStandaloneContainer := func(container *commands.Container) bool { isStandaloneContainer := func(container *commands.Container) bool {
if container.OneOff || container.ServiceName == "" { if container.OneOff || container.ServiceName == "" {
@ -30,58 +30,60 @@ func (gui *Gui) getContainersPanel() *panels.SideListPanel[*commands.Container]
}) })
} }
return &panels.SideListPanel[*commands.Container]{ return &panels.SideListPanel[*commands.ContainerListItem]{
ContextState: &panels.ContextState[*commands.Container]{ ContextState: &panels.ContextState[*commands.ContainerListItem]{
GetMainTabs: func() []panels.MainTab[*commands.Container] { GetMainTabs: func() []panels.MainTab[*commands.ContainerListItem] {
return []panels.MainTab[*commands.Container]{ return []panels.MainTab[*commands.ContainerListItem]{
{ {
Key: "logs", Key: "logs",
Title: gui.Tr.LogsTitle, Title: gui.Tr.LogsTitle,
Render: gui.renderContainerLogsToMain, Render: gui.renderContainerListItemLogs,
}, },
{ {
Key: "stats", Key: "stats",
Title: gui.Tr.StatsTitle, Title: gui.Tr.StatsTitle,
Render: gui.renderContainerStats, Render: gui.renderContainerListItemStats,
}, },
{ {
Key: "env", Key: "env",
Title: gui.Tr.EnvTitle, Title: gui.Tr.EnvTitle,
Render: gui.renderContainerEnv, Render: gui.renderContainerListItemEnv,
}, },
{ {
Key: "config", Key: "config",
Title: gui.Tr.ConfigTitle, Title: gui.Tr.ConfigTitle,
Render: gui.renderContainerConfig, Render: gui.renderContainerListItemConfig,
}, },
{ {
Key: "top", Key: "top",
Title: gui.Tr.TopTitle, Title: gui.Tr.TopTitle,
Render: gui.renderContainerTop, Render: gui.renderContainerListItemTop,
}, },
} }
}, },
GetItemContextCacheKey: func(container *commands.Container) string { GetItemContextCacheKey: func(item *commands.ContainerListItem) string {
// Including the container state in the cache key so that if the container // Including the state in the cache key so that if the container/pod
// restarts we re-read the logs. In the past we've had some glitchiness // restarts we re-read the logs.
// where a container restarts but the new logs don't get read. return "containers-" + item.ID() + "-" + item.State()
// Note that this might be jarring if we have a lot of logs and the container
// restarts a lot, so let's keep an eye on it.
return "containers-" + container.ID + "-" + container.Summary.State
}, },
}, },
ListPanel: panels.ListPanel[*commands.Container]{ ListPanel: panels.ListPanel[*commands.ContainerListItem]{
List: panels.NewFilteredList[*commands.Container](), List: panels.NewFilteredList[*commands.ContainerListItem](),
View: gui.Views.Containers, View: gui.Views.Containers,
}, },
NoItemsMessage: gui.Tr.NoContainers, NoItemsMessage: gui.Tr.NoContainers,
Gui: gui.intoInterface(), Gui: gui.intoInterface(),
// sortedContainers returns containers sorted by state if c.SortContainersByState is true (follows 1- running, 2- exited, 3- created) // Sort items: pods first (with their containers grouped), then standalone containers
// and sorted by name if c.SortContainersByState is false Sort: func(a *commands.ContainerListItem, b *commands.ContainerListItem) bool {
Sort: func(a *commands.Container, b *commands.Container) bool { return sortContainerListItems(a, b, gui.Config.UserConfig.Gui.LegacySortContainers)
return sortContainers(a, b, gui.Config.UserConfig.Gui.LegacySortContainers)
}, },
Filter: func(container *commands.Container) bool { Filter: func(item *commands.ContainerListItem) bool {
// Pods are always shown
if item.IsPod {
return true
}
container := item.Container
// Note that this is O(N*M) time complexity where N is the number of services // Note that this is O(N*M) time complexity where N is the number of services
// and M is the number of containers. We expect N to be small but M may be large, // and M is the number of containers. We expect N to be small but M may be large,
// so we will need to keep an eye on this. // so we will need to keep an eye on this.
@ -95,8 +97,8 @@ func (gui *Gui) getContainersPanel() *panels.SideListPanel[*commands.Container]
return true return true
}, },
GetTableCells: func(container *commands.Container) []string { GetTableCells: func(item *commands.ContainerListItem) []string {
return presentation.GetContainerDisplayStrings(&gui.Config.UserConfig.Gui, container) return presentation.GetContainerListItemDisplayStrings(&gui.Config.UserConfig.Gui, item)
}, },
} }
} }
@ -121,6 +123,137 @@ func sortContainers(a *commands.Container, b *commands.Container, legacySort boo
return containerStates[a.Summary.State] < containerStates[b.Summary.State] return containerStates[a.Summary.State] < containerStates[b.Summary.State]
} }
// sortContainerListItems sorts items to group pods with their containers.
// Order: pods first (sorted by state/name), then their containers indented,
// then standalone containers (sorted by state/name).
func sortContainerListItems(a *commands.ContainerListItem, b *commands.ContainerListItem, legacySort bool) bool {
// If both are in the same pod, sort by indent (pod first) then by name
if a.PodID() != "" && a.PodID() == b.PodID() {
// Pod comes before its containers
if a.IsPod && !b.IsPod {
return true
}
if !a.IsPod && b.IsPod {
return false
}
// Both are containers in the same pod, sort by name
return a.Name() < b.Name()
}
// Get the effective sort key (pod name for items in pods, own name for standalone)
aKey := a.Name()
bKey := b.Name()
if a.PodID() != "" && !a.IsPod {
aKey = a.PodName() + "\x00" + a.Name() // Sort after the pod
}
if b.PodID() != "" && !b.IsPod {
bKey = b.PodName() + "\x00" + b.Name()
}
if a.IsPod {
aKey = a.Name() + "\x00" // Pod sorts before its containers
}
if b.IsPod {
bKey = b.Name() + "\x00"
}
// Pods and their containers sort together, standalone containers at the end
aInPod := a.IsPod || a.PodID() != ""
bInPod := b.IsPod || b.PodID() != ""
if aInPod && !bInPod {
return true // Pods/pod containers before standalone
}
if !aInPod && bInPod {
return false
}
// Both in same category (pod-related or standalone)
if legacySort {
return aKey < bKey
}
// Sort by state, then by key
stateA := containerStates[a.State()]
stateB := containerStates[b.State()]
if stateA == stateB {
return aKey < bKey
}
return stateA < stateB
}
// Wrapper functions that delegate to container or pod rendering
func (gui *Gui) renderContainerListItemLogs(item *commands.ContainerListItem) tasks.TaskFunc {
if item.IsPod {
return gui.renderPodLogsToMain(item.Pod)
}
return gui.renderContainerLogsToMain(item.Container)
}
func (gui *Gui) renderContainerListItemStats(item *commands.ContainerListItem) tasks.TaskFunc {
if item.IsPod {
return gui.NewSimpleRenderStringTask(func() string {
return fmt.Sprintf("Pod: %s\nStatus: %s\nContainers: %d\n\nStats not available for pods. Select a container to view stats.",
item.Pod.Name, item.Pod.State(), len(item.Pod.Containers))
})
}
return gui.renderContainerStats(item.Container)
}
func (gui *Gui) renderContainerListItemEnv(item *commands.ContainerListItem) tasks.TaskFunc {
if item.IsPod {
return gui.NewSimpleRenderStringTask(func() string {
return "Environment variables not available for pods. Select a container to view environment."
})
}
return gui.renderContainerEnv(item.Container)
}
func (gui *Gui) renderContainerListItemConfig(item *commands.ContainerListItem) tasks.TaskFunc {
if item.IsPod {
return gui.renderPodConfig(item.Pod)
}
return gui.renderContainerConfig(item.Container)
}
func (gui *Gui) renderContainerListItemTop(item *commands.ContainerListItem) tasks.TaskFunc {
if item.IsPod {
return gui.NewSimpleRenderStringTask(func() string {
return "Process list not available for pods. Select a container to view processes."
})
}
return gui.renderContainerTop(item.Container)
}
func (gui *Gui) renderPodConfig(pod *commands.Pod) tasks.TaskFunc {
return gui.NewSimpleRenderStringTask(func() string {
padding := 10
output := ""
output += utils.ColoredString("Pod Information\n\n", color.FgCyan)
output += utils.WithPadding("ID: ", padding) + pod.ID + "\n"
output += utils.WithPadding("Name: ", padding) + pod.Name + "\n"
output += utils.WithPadding("Status: ", padding) + pod.State() + "\n"
output += utils.WithPadding("Created: ", padding) + pod.Summary.Created.Format(time.RFC3339) + "\n"
output += fmt.Sprintf("%s%d\n", utils.WithPadding("Containers: ", padding), len(pod.Containers))
if len(pod.Containers) > 0 {
output += "\n" + utils.ColoredString("Containers in this pod:\n", color.FgYellow)
for _, c := range pod.Containers {
stateColor := color.FgWhite
switch c.Summary.State {
case "running":
stateColor = color.FgGreen
case "exited":
stateColor = color.FgRed
}
output += fmt.Sprintf(" - %s (%s)\n", c.Name, utils.ColoredString(c.Summary.State, stateColor))
}
}
return output
})
}
func (gui *Gui) renderContainerEnv(container *commands.Container) tasks.TaskFunc { func (gui *Gui) renderContainerEnv(container *commands.Container) tasks.TaskFunc {
return gui.NewSimpleRenderStringTask(func() string { return gui.containerEnv(container) }) return gui.NewSimpleRenderStringTask(func() string { return gui.containerEnv(container) })
} }
@ -179,9 +312,9 @@ func (gui *Gui) containerConfigStr(container *commands.Container) string {
output += "\n" output += "\n"
for _, mount := range container.Details.Mounts { for _, mount := range container.Details.Mounts {
if mount.Type == "volume" { if mount.Type == "volume" {
output += fmt.Sprintf("%s%s %s\n", strings.Repeat(" ", padding), utils.ColoredString(string(mount.Type)+":", color.FgYellow), mount.Name) output += fmt.Sprintf("%s%s %s\n", strings.Repeat(" ", padding), utils.ColoredString(mount.Type+":", color.FgYellow), mount.Name)
} else { } else {
output += fmt.Sprintf("%s%s %s:%s\n", strings.Repeat(" ", padding), utils.ColoredString(string(mount.Type)+":", color.FgYellow), mount.Source, mount.Destination) output += fmt.Sprintf("%s%s %s:%s\n", strings.Repeat(" ", padding), utils.ColoredString(mount.Type+":", color.FgYellow), mount.Source, mount.Destination)
} }
} }
} else { } else {
@ -302,11 +435,16 @@ func (gui *Gui) handleHideStoppedContainers(g *gocui.Gui, v *gocui.View) error {
} }
func (gui *Gui) handleContainersRemoveMenu(g *gocui.Gui, v *gocui.View) error { func (gui *Gui) handleContainersRemoveMenu(g *gocui.Gui, v *gocui.View) error {
ctr, err := gui.Panels.Containers.GetSelectedItem() item, err := gui.Panels.Containers.GetSelectedItem()
if err != nil { if err != nil {
return nil return nil
} }
if item.IsPod {
return gui.createErrorPanel("Remove not yet supported for pods")
}
ctr := item.Container
handleMenuPress := func(force bool, removeVolumes bool) error { handleMenuPress := func(force bool, removeVolumes bool) error {
return gui.WithWaitingStatus(gui.Tr.RemovingStatus, func() error { return gui.WithWaitingStatus(gui.Tr.RemovingStatus, func() error {
if err := ctr.Remove(force, removeVolumes); err != nil { if err := ctr.Remove(force, removeVolumes); err != nil {
@ -357,20 +495,29 @@ func (gui *Gui) PauseContainer(container *commands.Container) error {
} }
func (gui *Gui) handleContainerPause(g *gocui.Gui, v *gocui.View) error { func (gui *Gui) handleContainerPause(g *gocui.Gui, v *gocui.View) error {
ctr, err := gui.Panels.Containers.GetSelectedItem() item, err := gui.Panels.Containers.GetSelectedItem()
if err != nil { if err != nil {
return nil return nil
} }
return gui.PauseContainer(ctr) if item.IsPod {
return gui.createErrorPanel("Pause not yet supported for pods")
}
return gui.PauseContainer(item.Container)
} }
func (gui *Gui) handleContainerStop(g *gocui.Gui, v *gocui.View) error { func (gui *Gui) handleContainerStop(g *gocui.Gui, v *gocui.View) error {
ctr, err := gui.Panels.Containers.GetSelectedItem() item, err := gui.Panels.Containers.GetSelectedItem()
if err != nil { if err != nil {
return nil return nil
} }
if item.IsPod {
return gui.createErrorPanel("Stop not yet supported for pods")
}
ctr := item.Container
return gui.createConfirmationPanel(gui.Tr.Confirm, gui.Tr.StopContainer, func(g *gocui.Gui, v *gocui.View) error { return gui.createConfirmationPanel(gui.Tr.Confirm, gui.Tr.StopContainer, func(g *gocui.Gui, v *gocui.View) error {
return gui.WithWaitingStatus(gui.Tr.StoppingStatus, func() error { return gui.WithWaitingStatus(gui.Tr.StoppingStatus, func() error {
if err := ctr.Stop(); err != nil { if err := ctr.Stop(); err != nil {
@ -383,11 +530,16 @@ func (gui *Gui) handleContainerStop(g *gocui.Gui, v *gocui.View) error {
} }
func (gui *Gui) handleContainerRestart(g *gocui.Gui, v *gocui.View) error { func (gui *Gui) handleContainerRestart(g *gocui.Gui, v *gocui.View) error {
ctr, err := gui.Panels.Containers.GetSelectedItem() item, err := gui.Panels.Containers.GetSelectedItem()
if err != nil { if err != nil {
return nil return nil
} }
if item.IsPod {
return gui.createErrorPanel("Restart not yet supported for pods")
}
ctr := item.Container
return gui.WithWaitingStatus(gui.Tr.RestartingStatus, func() error { return gui.WithWaitingStatus(gui.Tr.RestartingStatus, func() error {
if err := ctr.Restart(); err != nil { if err := ctr.Restart(); err != nil {
return gui.createErrorPanel(err.Error()) return gui.createErrorPanel(err.Error())
@ -398,11 +550,16 @@ func (gui *Gui) handleContainerRestart(g *gocui.Gui, v *gocui.View) error {
} }
func (gui *Gui) handleContainerAttach(g *gocui.Gui, v *gocui.View) error { func (gui *Gui) handleContainerAttach(g *gocui.Gui, v *gocui.View) error {
ctr, err := gui.Panels.Containers.GetSelectedItem() item, err := gui.Panels.Containers.GetSelectedItem()
if err != nil { if err != nil {
return nil return nil
} }
if item.IsPod {
return gui.createErrorPanel("Attach not yet supported for pods")
}
ctr := item.Container
c, err := ctr.Attach() c, err := ctr.Attach()
if err != nil { if err != nil {
return gui.createErrorPanel(err.Error()) return gui.createErrorPanel(err.Error())
@ -424,23 +581,32 @@ func (gui *Gui) handlePruneContainers() error {
} }
func (gui *Gui) handleContainerViewLogs(g *gocui.Gui, v *gocui.View) error { func (gui *Gui) handleContainerViewLogs(g *gocui.Gui, v *gocui.View) error {
ctr, err := gui.Panels.Containers.GetSelectedItem() item, err := gui.Panels.Containers.GetSelectedItem()
if err != nil { if err != nil {
return nil return nil
} }
gui.renderLogsToStdout(ctr) if item.IsPod {
// TODO: implement pod logs to stdout
return gui.createErrorPanel("View logs (stdout) not yet supported for pods")
}
gui.renderLogsToStdout(item.Container)
return nil return nil
} }
func (gui *Gui) handleContainersExecShell(g *gocui.Gui, v *gocui.View) error { func (gui *Gui) handleContainersExecShell(g *gocui.Gui, v *gocui.View) error {
ctr, err := gui.Panels.Containers.GetSelectedItem() item, err := gui.Panels.Containers.GetSelectedItem()
if err != nil { if err != nil {
return nil return nil
} }
return gui.containerExecShell(ctr) if item.IsPod {
return gui.createErrorPanel("Exec shell not yet supported for pods. Select a container instead.")
}
return gui.containerExecShell(item.Container)
} }
func (gui *Gui) containerExecShell(container *commands.Container) error { func (gui *Gui) containerExecShell(container *commands.Container) error {
@ -456,13 +622,17 @@ func (gui *Gui) containerExecShell(container *commands.Container) error {
} }
func (gui *Gui) handleContainersCustomCommand(g *gocui.Gui, v *gocui.View) error { func (gui *Gui) handleContainersCustomCommand(g *gocui.Gui, v *gocui.View) error {
ctr, err := gui.Panels.Containers.GetSelectedItem() item, err := gui.Panels.Containers.GetSelectedItem()
if err != nil { if err != nil {
return nil return nil
} }
if item.IsPod {
return gui.createErrorPanel("Custom commands not yet supported for pods. Select a container instead.")
}
commandObject := gui.PodmanCommand.NewCommandObject(commands.CommandObject{ commandObject := gui.PodmanCommand.NewCommandObject(commands.CommandObject{
Container: ctr, Container: item.Container,
}) })
customCommands := gui.Config.UserConfig.CustomCommands.Containers customCommands := gui.Config.UserConfig.CustomCommands.Containers
@ -473,11 +643,13 @@ func (gui *Gui) handleContainersCustomCommand(g *gocui.Gui, v *gocui.View) error
func (gui *Gui) handleStopContainers() error { func (gui *Gui) handleStopContainers() error {
return gui.createConfirmationPanel(gui.Tr.Confirm, gui.Tr.ConfirmStopContainers, func(g *gocui.Gui, v *gocui.View) error { return gui.createConfirmationPanel(gui.Tr.Confirm, gui.Tr.ConfirmStopContainers, func(g *gocui.Gui, v *gocui.View) error {
return gui.WithWaitingStatus(gui.Tr.StoppingStatus, func() error { return gui.WithWaitingStatus(gui.Tr.StoppingStatus, func() error {
for _, ctr := range gui.Panels.Containers.List.GetAllItems() { for _, item := range gui.Panels.Containers.List.GetAllItems() {
if err := ctr.Stop(); err != nil { if !item.IsPod && item.Container != nil {
if err := item.Container.Stop(); err != nil {
gui.Log.Error(err) gui.Log.Error(err)
} }
} }
}
return nil return nil
}) })
@ -487,11 +659,13 @@ func (gui *Gui) handleStopContainers() error {
func (gui *Gui) handleRemoveContainers() error { func (gui *Gui) handleRemoveContainers() error {
return gui.createConfirmationPanel(gui.Tr.Confirm, gui.Tr.ConfirmRemoveContainers, func(g *gocui.Gui, v *gocui.View) error { return gui.createConfirmationPanel(gui.Tr.Confirm, gui.Tr.ConfirmRemoveContainers, func(g *gocui.Gui, v *gocui.View) error {
return gui.WithWaitingStatus(gui.Tr.RemovingStatus, func() error { return gui.WithWaitingStatus(gui.Tr.RemovingStatus, func() error {
for _, ctr := range gui.Panels.Containers.List.GetAllItems() { for _, item := range gui.Panels.Containers.List.GetAllItems() {
if err := ctr.Remove(true, false); err != nil { if !item.IsPod && item.Container != nil {
if err := item.Container.Remove(true, false); err != nil {
gui.Log.Error(err) gui.Log.Error(err)
} }
} }
}
return nil return nil
}) })
@ -522,12 +696,16 @@ func (gui *Gui) handleContainersBulkCommand(g *gocui.Gui, v *gocui.View) error {
// Open first port in browser // Open first port in browser
func (gui *Gui) handleContainersOpenInBrowserCommand(g *gocui.Gui, v *gocui.View) error { func (gui *Gui) handleContainersOpenInBrowserCommand(g *gocui.Gui, v *gocui.View) error {
ctr, err := gui.Panels.Containers.GetSelectedItem() item, err := gui.Panels.Containers.GetSelectedItem()
if err != nil { if err != nil {
return nil return nil
} }
return gui.openContainerInBrowser(ctr) if item.IsPod {
return gui.createErrorPanel("Cannot open pod in browser. Select a container instead.")
}
return gui.openContainerInBrowser(item.Container)
} }
func (gui *Gui) openContainerInBrowser(ctr *commands.Container) error { func (gui *Gui) openContainerInBrowser(ctr *commands.Container) error {

View file

@ -48,7 +48,7 @@ type Gui struct {
type Panels struct { type Panels struct {
Projects *panels.SideListPanel[*commands.Project] Projects *panels.SideListPanel[*commands.Project]
Services *panels.SideListPanel[*commands.Service] Services *panels.SideListPanel[*commands.Service]
Containers *panels.SideListPanel[*commands.Container] Containers *panels.SideListPanel[*commands.ContainerListItem]
Images *panels.SideListPanel[*commands.Image] Images *panels.SideListPanel[*commands.Image]
Volumes *panels.SideListPanel[*commands.Volume] Volumes *panels.SideListPanel[*commands.Volume]
Networks *panels.SideListPanel[*commands.Network] Networks *panels.SideListPanel[*commands.Network]
@ -290,7 +290,14 @@ func (gui *Gui) setPanels() {
} }
func (gui *Gui) updateContainerDetails() error { func (gui *Gui) updateContainerDetails() error {
return gui.PodmanCommand.RefreshContainerDetails(gui.Panels.Containers.List.GetAllItems()) // Extract containers from ContainerListItems
var containers []*commands.Container
for _, item := range gui.Panels.Containers.List.GetAllItems() {
if !item.IsPod && item.Container != nil {
containers = append(containers, item.Container)
}
}
return gui.PodmanCommand.RefreshContainerDetails(containers)
} }
func (gui *Gui) refresh() { func (gui *Gui) refresh() {
@ -469,9 +476,9 @@ func (gui *Gui) monitorContainerStats(ctx context.Context) {
case <-ctx.Done(): case <-ctx.Done():
return return
case <-ticker.C: case <-ticker.C:
for _, container := range gui.Panels.Containers.List.GetAllItems() { for _, item := range gui.Panels.Containers.List.GetAllItems() {
if !container.MonitoringStats { if !item.IsPod && item.Container != nil && !item.Container.MonitoringStats {
go gui.PodmanCommand.CreateClientStatMonitor(container) go gui.PodmanCommand.CreateClientStatMonitor(item.Container)
} }
} }
} }

View file

@ -8,11 +8,11 @@ import (
"strings" "strings"
"time" "time"
"github.com/fatih/color"
"github.com/jesseduffield/asciigraph"
"github.com/christophe-duc/lazypodman/pkg/commands" "github.com/christophe-duc/lazypodman/pkg/commands"
"github.com/christophe-duc/lazypodman/pkg/config" "github.com/christophe-duc/lazypodman/pkg/config"
"github.com/christophe-duc/lazypodman/pkg/utils" "github.com/christophe-duc/lazypodman/pkg/utils"
"github.com/fatih/color"
"github.com/jesseduffield/asciigraph"
"github.com/mcuadros/go-lookup" "github.com/mcuadros/go-lookup"
"github.com/samber/lo" "github.com/samber/lo"
) )

View file

@ -24,6 +24,96 @@ func GetContainerDisplayStrings(guiConfig *config.GuiConfig, container *commands
} }
} }
// GetContainerListItemDisplayStrings returns display strings for a ContainerListItem (pod or container)
func GetContainerListItemDisplayStrings(guiConfig *config.GuiConfig, item *commands.ContainerListItem) []string {
if item.IsPod && item.Pod != nil {
return GetPodDisplayStrings(guiConfig, item.Pod)
}
if item.Container == nil {
// Return an empty row with the expected number of columns when container data is missing.
return []string{"", "", "", "", "", ""}
}
// Add indentation for containers in pods
strings := GetContainerDisplayStrings(guiConfig, item.Container)
if item.Indent > 0 {
strings[2] = fmt.Sprintf("%s%s", createIndent(item.Indent), strings[2])
}
return strings
}
// GetPodDisplayStrings returns display strings for a pod
func GetPodDisplayStrings(guiConfig *config.GuiConfig, pod *commands.Pod) []string {
return []string{
getPodDisplayStatus(guiConfig, pod),
"", // No substatus for pods
utils.ColoredString(pod.Name, color.FgCyan),
"", // No CPU% for pods
"", // No ports for pods
utils.ColoredString(fmt.Sprintf("(%d containers)", len(pod.Containers)), color.FgMagenta),
}
}
func getPodDisplayStatus(guiConfig *config.GuiConfig, pod *commands.Pod) string {
shortStatusMap := map[string]string{
"Running": "R",
"Degraded": "D",
"Exited": "X",
"Dead": "!",
"Created": "C",
}
iconStatusMap := map[string]rune{
"Running": '▶',
"Degraded": '◐',
"Exited": '',
"Dead": '!',
"Created": '+',
}
var podState string
switch guiConfig.ContainerStatusHealthStyle {
case "short":
if s, ok := shortStatusMap[pod.State()]; ok {
podState = s
} else {
podState = "?"
}
case "icon":
if r, ok := iconStatusMap[pod.State()]; ok {
podState = string(r)
} else {
podState = "?"
}
default:
podState = pod.State()
}
return utils.ColoredString(podState, getPodColor(pod))
}
func getPodColor(pod *commands.Pod) color.Attribute {
switch pod.State() {
case "Running":
return color.FgGreen
case "Degraded":
return color.FgYellow
case "Exited":
return color.FgRed
case "Dead":
return color.FgRed
case "Created":
return color.FgCyan
default:
return color.FgWhite
}
}
func createIndent(spaces int) string {
return fmt.Sprintf("%*s", spaces, "")
}
func displayContainerImage(container *commands.Container) string { func displayContainerImage(container *commands.Container) string {
return strings.TrimPrefix(container.Summary.Image, "sha256:") return strings.TrimPrefix(container.Summary.Image, "sha256:")
} }

View file

@ -109,7 +109,6 @@ type TranslationSet struct {
FilterList string FilterList string
OpenInBrowser string OpenInBrowser string
SortContainersByState string SortContainersByState string
LogsTitle string LogsTitle string
ConfigTitle string ConfigTitle string
EnvTitle string EnvTitle string

View file

@ -0,0 +1,342 @@
package pods
import (
"context"
"net/http"
"net/url"
"strings"
"github.com/containers/podman/v5/pkg/api/handlers"
"github.com/containers/podman/v5/pkg/bindings"
entitiesTypes "github.com/containers/podman/v5/pkg/domain/entities/types"
"github.com/containers/podman/v5/pkg/errorhandling"
jsoniter "github.com/json-iterator/go"
)
func CreatePodFromSpec(ctx context.Context, spec *entitiesTypes.PodSpec) (*entitiesTypes.PodCreateReport, error) {
var pcr entitiesTypes.PodCreateReport
if spec == nil {
spec = new(entitiesTypes.PodSpec)
}
conn, err := bindings.GetClient(ctx)
if err != nil {
return nil, err
}
specString, err := jsoniter.MarshalToString(spec.PodSpecGen)
if err != nil {
return nil, err
}
stringReader := strings.NewReader(specString)
response, err := conn.DoRequest(ctx, stringReader, http.MethodPost, "/pods/create", nil, nil)
if err != nil {
return nil, err
}
defer response.Body.Close()
return &pcr, response.Process(&pcr)
}
// Exists is a lightweight method to determine if a pod exists in local storage
func Exists(ctx context.Context, nameOrID string, _ *ExistsOptions) (bool, error) {
conn, err := bindings.GetClient(ctx)
if err != nil {
return false, err
}
response, err := conn.DoRequest(ctx, nil, http.MethodGet, "/pods/%s/exists", nil, nil, nameOrID)
if err != nil {
return false, err
}
defer response.Body.Close()
return response.IsSuccess(), nil
}
// Inspect returns low-level information about the given pod.
func Inspect(ctx context.Context, nameOrID string, options *InspectOptions) (*entitiesTypes.PodInspectReport, error) {
var report entitiesTypes.PodInspectReport
if options == nil {
options = new(InspectOptions)
}
_ = options
conn, err := bindings.GetClient(ctx)
if err != nil {
return nil, err
}
response, err := conn.DoRequest(ctx, nil, http.MethodGet, "/pods/%s/json", nil, nil, nameOrID)
if err != nil {
return nil, err
}
defer response.Body.Close()
return &report, response.Process(&report)
}
// Kill sends a SIGTERM to all the containers in a pod. The optional signal parameter
// can be used to override SIGTERM.
func Kill(ctx context.Context, nameOrID string, options *KillOptions) (*entitiesTypes.PodKillReport, error) {
var report entitiesTypes.PodKillReport
if options == nil {
options = new(KillOptions)
}
conn, err := bindings.GetClient(ctx)
if err != nil {
return nil, err
}
params, err := options.ToParams()
if err != nil {
return nil, err
}
response, err := conn.DoRequest(ctx, nil, http.MethodPost, "/pods/%s/kill", params, nil, nameOrID)
if err != nil {
return nil, err
}
defer response.Body.Close()
return &report, response.ProcessWithError(&report, &errorhandling.PodConflictErrorModel{})
}
// Pause pauses all running containers in a given pod.
func Pause(ctx context.Context, nameOrID string, options *PauseOptions) (*entitiesTypes.PodPauseReport, error) {
var report entitiesTypes.PodPauseReport
if options == nil {
options = new(PauseOptions)
}
_ = options
conn, err := bindings.GetClient(ctx)
if err != nil {
return nil, err
}
response, err := conn.DoRequest(ctx, nil, http.MethodPost, "/pods/%s/pause", nil, nil, nameOrID)
if err != nil {
return nil, err
}
defer response.Body.Close()
return &report, response.ProcessWithError(&report, &errorhandling.PodConflictErrorModel{})
}
// Prune by default removes all non-running pods in local storage.
// And with force set true removes all pods.
func Prune(ctx context.Context, options *PruneOptions) ([]*entitiesTypes.PodPruneReport, error) {
var reports []*entitiesTypes.PodPruneReport
if options == nil {
options = new(PruneOptions)
}
_ = options
conn, err := bindings.GetClient(ctx)
if err != nil {
return nil, err
}
response, err := conn.DoRequest(ctx, nil, http.MethodPost, "/pods/prune", nil, nil)
if err != nil {
return nil, err
}
defer response.Body.Close()
return reports, response.Process(&reports)
}
// List returns all pods in local storage. The optional filters parameter can
// be used to refine which pods should be listed.
func List(ctx context.Context, options *ListOptions) ([]*entitiesTypes.ListPodsReport, error) {
var podsReports []*entitiesTypes.ListPodsReport
if options == nil {
options = new(ListOptions)
}
conn, err := bindings.GetClient(ctx)
if err != nil {
return nil, err
}
params, err := options.ToParams()
if err != nil {
return nil, err
}
response, err := conn.DoRequest(ctx, nil, http.MethodGet, "/pods/json", params, nil)
if err != nil {
return podsReports, err
}
defer response.Body.Close()
return podsReports, response.Process(&podsReports)
}
// Restart restarts all containers in a pod.
func Restart(ctx context.Context, nameOrID string, options *RestartOptions) (*entitiesTypes.PodRestartReport, error) {
var report entitiesTypes.PodRestartReport
if options == nil {
options = new(RestartOptions)
}
_ = options
conn, err := bindings.GetClient(ctx)
if err != nil {
return nil, err
}
response, err := conn.DoRequest(ctx, nil, http.MethodPost, "/pods/%s/restart", nil, nil, nameOrID)
if err != nil {
return nil, err
}
defer response.Body.Close()
return &report, response.ProcessWithError(&report, &errorhandling.PodConflictErrorModel{})
}
// Remove deletes a Pod from local storage. The optional force parameter denotes
// that the Pod can be removed even if in a running state.
func Remove(ctx context.Context, nameOrID string, options *RemoveOptions) (*entitiesTypes.PodRmReport, error) {
var report entitiesTypes.PodRmReport
if options == nil {
options = new(RemoveOptions)
}
conn, err := bindings.GetClient(ctx)
if err != nil {
return nil, err
}
params, err := options.ToParams()
if err != nil {
return nil, err
}
response, err := conn.DoRequest(ctx, nil, http.MethodDelete, "/pods/%s", params, nil, nameOrID)
if err != nil {
return nil, err
}
defer response.Body.Close()
return &report, response.Process(&report)
}
// Start starts all containers in a pod.
func Start(ctx context.Context, nameOrID string, options *StartOptions) (*entitiesTypes.PodStartReport, error) {
var report entitiesTypes.PodStartReport
if options == nil {
options = new(StartOptions)
}
_ = options
conn, err := bindings.GetClient(ctx)
if err != nil {
return nil, err
}
response, err := conn.DoRequest(ctx, nil, http.MethodPost, "/pods/%s/start", nil, nil, nameOrID)
if err != nil {
return nil, err
}
defer response.Body.Close()
if response.StatusCode == http.StatusNotModified {
report.Id = nameOrID
report.RawInput = nameOrID
return &report, nil
}
return &report, response.ProcessWithError(&report, &errorhandling.PodConflictErrorModel{})
}
// Stop stops all containers in a Pod. The optional timeout parameter can be
// used to override the timeout before the container is killed.
func Stop(ctx context.Context, nameOrID string, options *StopOptions) (*entitiesTypes.PodStopReport, error) {
var report entitiesTypes.PodStopReport
if options == nil {
options = new(StopOptions)
}
conn, err := bindings.GetClient(ctx)
if err != nil {
return nil, err
}
params, err := options.ToParams()
if err != nil {
return nil, err
}
response, err := conn.DoRequest(ctx, nil, http.MethodPost, "/pods/%s/stop", params, nil, nameOrID)
if err != nil {
return nil, err
}
defer response.Body.Close()
if response.StatusCode == http.StatusNotModified {
report.Id = nameOrID
return &report, nil
}
return &report, response.ProcessWithError(&report, &errorhandling.PodConflictErrorModel{})
}
// Top gathers statistics about the running processes in a pod. The nameOrID can be a pod name
// or a partial/full ID. The descriptors allow for specifying which data to collect from each process.
func Top(ctx context.Context, nameOrID string, options *TopOptions) ([]string, error) {
if options == nil {
options = new(TopOptions)
}
conn, err := bindings.GetClient(ctx)
if err != nil {
return nil, err
}
params := url.Values{}
if descriptors := options.GetDescriptors(); len(descriptors) > 0 {
params.Set("ps_args", strings.Join(descriptors, ","))
}
response, err := conn.DoRequest(ctx, nil, http.MethodGet, "/pods/%s/top", params, nil, nameOrID)
if err != nil {
return nil, err
}
defer response.Body.Close()
body := handlers.PodTopOKBody{}
if err = response.Process(&body); err != nil {
return nil, err
}
// handlers.PodTopOKBody{} returns a slice of slices where each cell in the top table is an item.
// In libpod land, we're just using a slice with cells being split by tabs, which allows for an idiomatic
// usage of the tabwriter.
topOutput := []string{strings.Join(body.Titles, "\t")}
for _, out := range body.Processes {
topOutput = append(topOutput, strings.Join(out, "\t"))
}
return topOutput, err
}
// Unpause unpauses all paused containers in a Pod.
func Unpause(ctx context.Context, nameOrID string, options *UnpauseOptions) (*entitiesTypes.PodUnpauseReport, error) {
if options == nil {
options = new(UnpauseOptions)
}
_ = options
var report entitiesTypes.PodUnpauseReport
conn, err := bindings.GetClient(ctx)
if err != nil {
return nil, err
}
response, err := conn.DoRequest(ctx, nil, http.MethodPost, "/pods/%s/unpause", nil, nil, nameOrID)
if err != nil {
return nil, err
}
defer response.Body.Close()
return &report, response.ProcessWithError(&report, &errorhandling.PodConflictErrorModel{})
}
// Stats display resource-usage statistics of one or more pods.
func Stats(ctx context.Context, namesOrIDs []string, options *StatsOptions) ([]*entitiesTypes.PodStatsReport, error) {
if options == nil {
options = new(StatsOptions)
}
conn, err := bindings.GetClient(ctx)
if err != nil {
return nil, err
}
params, err := options.ToParams()
if err != nil {
return nil, err
}
for _, i := range namesOrIDs {
params.Add("namesOrIDs", i)
}
var reports []*entitiesTypes.PodStatsReport
response, err := conn.DoRequest(ctx, nil, http.MethodGet, "/pods/stats", params, nil)
if err != nil {
return nil, err
}
defer response.Body.Close()
return reports, response.Process(&reports)
}

View file

@ -0,0 +1,92 @@
package pods
// CreateOptions are optional options for creating pods
//
//go:generate go run ../generator/generator.go CreateOptions
type CreateOptions struct {
}
// InspectOptions are optional options for inspecting pods
//
//go:generate go run ../generator/generator.go InspectOptions
type InspectOptions struct {
}
// KillOptions are optional options for killing pods
//
//go:generate go run ../generator/generator.go KillOptions
type KillOptions struct {
Signal *string
}
// PauseOptions are optional options for pausing pods
//
//go:generate go run ../generator/generator.go PauseOptions
type PauseOptions struct {
}
// PruneOptions are optional options for pruning pods
//
//go:generate go run ../generator/generator.go PruneOptions
type PruneOptions struct {
}
// ListOptions are optional options for listing pods
//
//go:generate go run ../generator/generator.go ListOptions
type ListOptions struct {
Filters map[string][]string
}
// RestartOptions are optional options for restarting pods
//
//go:generate go run ../generator/generator.go RestartOptions
type RestartOptions struct {
}
// StartOptions are optional options for starting pods
//
//go:generate go run ../generator/generator.go StartOptions
type StartOptions struct {
}
// StopOptions are optional options for stopping pods
//
//go:generate go run ../generator/generator.go StopOptions
type StopOptions struct {
Timeout *int
}
// TopOptions are optional options for getting top on pods
//
//go:generate go run ../generator/generator.go TopOptions
type TopOptions struct {
Descriptors []string
}
// UnpauseOptions are optional options for unpausinging pods
//
//go:generate go run ../generator/generator.go UnpauseOptions
type UnpauseOptions struct {
}
// StatsOptions are optional options for getting stats of pods
//
//go:generate go run ../generator/generator.go StatsOptions
type StatsOptions struct {
All *bool
}
// RemoveOptions are optional options for removing pods
//
//go:generate go run ../generator/generator.go RemoveOptions
type RemoveOptions struct {
Force *bool
Timeout *uint
}
// ExistsOptions are optional options for checking if a pod exists
//
//go:generate go run ../generator/generator.go ExistsOptions
type ExistsOptions struct {
}

View file

@ -0,0 +1,18 @@
// Code generated by go generate; DO NOT EDIT.
package pods
import (
"net/url"
"github.com/containers/podman/v5/pkg/bindings/internal/util"
)
// Changed returns true if named field has been set
func (o *CreateOptions) Changed(fieldName string) bool {
return util.Changed(o, fieldName)
}
// ToParams formats struct fields to be passed to API service
func (o *CreateOptions) ToParams() (url.Values, error) {
return util.ToParams(o)
}

View file

@ -0,0 +1,18 @@
// Code generated by go generate; DO NOT EDIT.
package pods
import (
"net/url"
"github.com/containers/podman/v5/pkg/bindings/internal/util"
)
// Changed returns true if named field has been set
func (o *ExistsOptions) Changed(fieldName string) bool {
return util.Changed(o, fieldName)
}
// ToParams formats struct fields to be passed to API service
func (o *ExistsOptions) ToParams() (url.Values, error) {
return util.ToParams(o)
}

View file

@ -0,0 +1,18 @@
// Code generated by go generate; DO NOT EDIT.
package pods
import (
"net/url"
"github.com/containers/podman/v5/pkg/bindings/internal/util"
)
// Changed returns true if named field has been set
func (o *InspectOptions) Changed(fieldName string) bool {
return util.Changed(o, fieldName)
}
// ToParams formats struct fields to be passed to API service
func (o *InspectOptions) ToParams() (url.Values, error) {
return util.ToParams(o)
}

View file

@ -0,0 +1,33 @@
// Code generated by go generate; DO NOT EDIT.
package pods
import (
"net/url"
"github.com/containers/podman/v5/pkg/bindings/internal/util"
)
// Changed returns true if named field has been set
func (o *KillOptions) Changed(fieldName string) bool {
return util.Changed(o, fieldName)
}
// ToParams formats struct fields to be passed to API service
func (o *KillOptions) ToParams() (url.Values, error) {
return util.ToParams(o)
}
// WithSignal set field Signal to given value
func (o *KillOptions) WithSignal(value string) *KillOptions {
o.Signal = &value
return o
}
// GetSignal returns value of field Signal
func (o *KillOptions) GetSignal() string {
if o.Signal == nil {
var z string
return z
}
return *o.Signal
}

View file

@ -0,0 +1,33 @@
// Code generated by go generate; DO NOT EDIT.
package pods
import (
"net/url"
"github.com/containers/podman/v5/pkg/bindings/internal/util"
)
// Changed returns true if named field has been set
func (o *ListOptions) Changed(fieldName string) bool {
return util.Changed(o, fieldName)
}
// ToParams formats struct fields to be passed to API service
func (o *ListOptions) ToParams() (url.Values, error) {
return util.ToParams(o)
}
// WithFilters set field Filters to given value
func (o *ListOptions) WithFilters(value map[string][]string) *ListOptions {
o.Filters = value
return o
}
// GetFilters returns value of field Filters
func (o *ListOptions) GetFilters() map[string][]string {
if o.Filters == nil {
var z map[string][]string
return z
}
return o.Filters
}

View file

@ -0,0 +1,18 @@
// Code generated by go generate; DO NOT EDIT.
package pods
import (
"net/url"
"github.com/containers/podman/v5/pkg/bindings/internal/util"
)
// Changed returns true if named field has been set
func (o *PauseOptions) Changed(fieldName string) bool {
return util.Changed(o, fieldName)
}
// ToParams formats struct fields to be passed to API service
func (o *PauseOptions) ToParams() (url.Values, error) {
return util.ToParams(o)
}

View file

@ -0,0 +1,18 @@
// Code generated by go generate; DO NOT EDIT.
package pods
import (
"net/url"
"github.com/containers/podman/v5/pkg/bindings/internal/util"
)
// Changed returns true if named field has been set
func (o *PruneOptions) Changed(fieldName string) bool {
return util.Changed(o, fieldName)
}
// ToParams formats struct fields to be passed to API service
func (o *PruneOptions) ToParams() (url.Values, error) {
return util.ToParams(o)
}

View file

@ -0,0 +1,48 @@
// Code generated by go generate; DO NOT EDIT.
package pods
import (
"net/url"
"github.com/containers/podman/v5/pkg/bindings/internal/util"
)
// Changed returns true if named field has been set
func (o *RemoveOptions) Changed(fieldName string) bool {
return util.Changed(o, fieldName)
}
// ToParams formats struct fields to be passed to API service
func (o *RemoveOptions) ToParams() (url.Values, error) {
return util.ToParams(o)
}
// WithForce set field Force to given value
func (o *RemoveOptions) WithForce(value bool) *RemoveOptions {
o.Force = &value
return o
}
// GetForce returns value of field Force
func (o *RemoveOptions) GetForce() bool {
if o.Force == nil {
var z bool
return z
}
return *o.Force
}
// WithTimeout set field Timeout to given value
func (o *RemoveOptions) WithTimeout(value uint) *RemoveOptions {
o.Timeout = &value
return o
}
// GetTimeout returns value of field Timeout
func (o *RemoveOptions) GetTimeout() uint {
if o.Timeout == nil {
var z uint
return z
}
return *o.Timeout
}

View file

@ -0,0 +1,18 @@
// Code generated by go generate; DO NOT EDIT.
package pods
import (
"net/url"
"github.com/containers/podman/v5/pkg/bindings/internal/util"
)
// Changed returns true if named field has been set
func (o *RestartOptions) Changed(fieldName string) bool {
return util.Changed(o, fieldName)
}
// ToParams formats struct fields to be passed to API service
func (o *RestartOptions) ToParams() (url.Values, error) {
return util.ToParams(o)
}

View file

@ -0,0 +1,18 @@
// Code generated by go generate; DO NOT EDIT.
package pods
import (
"net/url"
"github.com/containers/podman/v5/pkg/bindings/internal/util"
)
// Changed returns true if named field has been set
func (o *StartOptions) Changed(fieldName string) bool {
return util.Changed(o, fieldName)
}
// ToParams formats struct fields to be passed to API service
func (o *StartOptions) ToParams() (url.Values, error) {
return util.ToParams(o)
}

View file

@ -0,0 +1,33 @@
// Code generated by go generate; DO NOT EDIT.
package pods
import (
"net/url"
"github.com/containers/podman/v5/pkg/bindings/internal/util"
)
// Changed returns true if named field has been set
func (o *StatsOptions) Changed(fieldName string) bool {
return util.Changed(o, fieldName)
}
// ToParams formats struct fields to be passed to API service
func (o *StatsOptions) ToParams() (url.Values, error) {
return util.ToParams(o)
}
// WithAll set field All to given value
func (o *StatsOptions) WithAll(value bool) *StatsOptions {
o.All = &value
return o
}
// GetAll returns value of field All
func (o *StatsOptions) GetAll() bool {
if o.All == nil {
var z bool
return z
}
return *o.All
}

View file

@ -0,0 +1,33 @@
// Code generated by go generate; DO NOT EDIT.
package pods
import (
"net/url"
"github.com/containers/podman/v5/pkg/bindings/internal/util"
)
// Changed returns true if named field has been set
func (o *StopOptions) Changed(fieldName string) bool {
return util.Changed(o, fieldName)
}
// ToParams formats struct fields to be passed to API service
func (o *StopOptions) ToParams() (url.Values, error) {
return util.ToParams(o)
}
// WithTimeout set field Timeout to given value
func (o *StopOptions) WithTimeout(value int) *StopOptions {
o.Timeout = &value
return o
}
// GetTimeout returns value of field Timeout
func (o *StopOptions) GetTimeout() int {
if o.Timeout == nil {
var z int
return z
}
return *o.Timeout
}

View file

@ -0,0 +1,33 @@
// Code generated by go generate; DO NOT EDIT.
package pods
import (
"net/url"
"github.com/containers/podman/v5/pkg/bindings/internal/util"
)
// Changed returns true if named field has been set
func (o *TopOptions) Changed(fieldName string) bool {
return util.Changed(o, fieldName)
}
// ToParams formats struct fields to be passed to API service
func (o *TopOptions) ToParams() (url.Values, error) {
return util.ToParams(o)
}
// WithDescriptors set field Descriptors to given value
func (o *TopOptions) WithDescriptors(value []string) *TopOptions {
o.Descriptors = value
return o
}
// GetDescriptors returns value of field Descriptors
func (o *TopOptions) GetDescriptors() []string {
if o.Descriptors == nil {
var z []string
return z
}
return o.Descriptors
}

View file

@ -0,0 +1,18 @@
// Code generated by go generate; DO NOT EDIT.
package pods
import (
"net/url"
"github.com/containers/podman/v5/pkg/bindings/internal/util"
)
// Changed returns true if named field has been set
func (o *UnpauseOptions) Changed(fieldName string) bool {
return util.Changed(o, fieldName)
}
// ToParams formats struct fields to be passed to API service
func (o *UnpauseOptions) ToParams() (url.Values, error) {
return util.ToParams(o)
}

1
vendor/modules.txt vendored
View file

@ -165,6 +165,7 @@ github.com/containers/podman/v5/pkg/bindings/containers
github.com/containers/podman/v5/pkg/bindings/images github.com/containers/podman/v5/pkg/bindings/images
github.com/containers/podman/v5/pkg/bindings/internal/util github.com/containers/podman/v5/pkg/bindings/internal/util
github.com/containers/podman/v5/pkg/bindings/network github.com/containers/podman/v5/pkg/bindings/network
github.com/containers/podman/v5/pkg/bindings/pods
github.com/containers/podman/v5/pkg/bindings/system github.com/containers/podman/v5/pkg/bindings/system
github.com/containers/podman/v5/pkg/bindings/volumes github.com/containers/podman/v5/pkg/bindings/volumes
github.com/containers/podman/v5/pkg/checkpoint/crutils github.com/containers/podman/v5/pkg/checkpoint/crutils