mirror of
https://github.com/jesseduffield/lazydocker.git
synced 2026-07-25 08:31:03 +00:00
feature: add pod support in the container pane
This commit is contained in:
parent
71c53a5c5e
commit
e5c209de93
16 changed files with 1460 additions and 622 deletions
1284
coverage.txt
1284
coverage.txt
File diff suppressed because it is too large
Load diff
66
pkg/commands/container_list_item.go
Normal file
66
pkg/commands/container_list_item.go
Normal file
|
|
@ -0,0 +1,66 @@
|
||||||
|
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 {
|
||||||
|
return c.Pod.ID
|
||||||
|
}
|
||||||
|
return c.Container.ID
|
||||||
|
}
|
||||||
|
|
||||||
|
// Name returns the display name for the item.
|
||||||
|
func (c *ContainerListItem) Name() string {
|
||||||
|
if c.IsPod {
|
||||||
|
return c.Pod.Name
|
||||||
|
}
|
||||||
|
return c.Container.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
// State returns the state for the item.
|
||||||
|
func (c *ContainerListItem) State() string {
|
||||||
|
if c.IsPod {
|
||||||
|
return c.Pod.State()
|
||||||
|
}
|
||||||
|
return c.Container.Summary.State
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetContainers returns the containers if this is a pod, nil otherwise.
|
||||||
|
func (c *ContainerListItem) GetContainers() []*Container {
|
||||||
|
if c.IsPod {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
return c.Container.Summary.Pod != ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// PodID returns the pod ID if this container is in a pod, empty string otherwise.
|
||||||
|
func (c *ContainerListItem) PodID() string {
|
||||||
|
if c.IsPod {
|
||||||
|
return c.Pod.ID
|
||||||
|
}
|
||||||
|
return c.Container.Summary.Pod
|
||||||
|
}
|
||||||
|
|
||||||
|
// PodName returns the pod name if this container is in a pod, empty string otherwise.
|
||||||
|
func (c *ContainerListItem) PodName() string {
|
||||||
|
if c.IsPod {
|
||||||
|
return c.Pod.Name
|
||||||
|
}
|
||||||
|
return c.Container.Summary.PodName
|
||||||
|
}
|
||||||
36
pkg/commands/pod.go
Normal file
36
pkg/commands/pod.go
Normal 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
|
||||||
|
}
|
||||||
|
|
@ -347,15 +347,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 +387,88 @@ 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add pods and their containers
|
||||||
|
for podID, ps := range podMap {
|
||||||
|
// 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) {
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 := pod.Config()
|
||||||
|
infraID := ""
|
||||||
|
if 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
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ package commands
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"os/exec"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
|
@ -275,6 +276,45 @@ func (r *SocketRuntime) PruneNetworks(ctx context.Context) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// podPsJSON represents the JSON output of `podman pod ps --format json`
|
||||||
|
type podPsJSON struct {
|
||||||
|
ID string `json:"Id"`
|
||||||
|
Name string `json:"Name"`
|
||||||
|
Status string `json:"Status"`
|
||||||
|
Created string `json:"Created"`
|
||||||
|
InfraID string `json:"InfraId"`
|
||||||
|
Labels map[string]string `json:"Labels"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListPods returns all pods.
|
||||||
|
func (r *SocketRuntime) ListPods(ctx context.Context) ([]PodSummary, error) {
|
||||||
|
// Use podman CLI since there's no pods binding package
|
||||||
|
cmd := exec.CommandContext(ctx, "podman", "pod", "ps", "--format", "json")
|
||||||
|
output, err := cmd.Output()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var pods []podPsJSON
|
||||||
|
if err := json.Unmarshal(output, &pods); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
result := make([]PodSummary, len(pods))
|
||||||
|
for i, p := range pods {
|
||||||
|
created, _ := time.Parse(time.RFC3339, p.Created)
|
||||||
|
result[i] = PodSummary{
|
||||||
|
ID: p.ID,
|
||||||
|
Name: p.Name,
|
||||||
|
Status: p.Status,
|
||||||
|
Created: 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 +386,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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -142,3 +142,74 @@ 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
|
||||||
|
args := []string{"pod", "logs", "--follow", "--color"}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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("Pod operations not yet supported")
|
||||||
|
}
|
||||||
|
|
||||||
|
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("Pod operations not yet supported")
|
||||||
|
}
|
||||||
|
|
||||||
|
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("Pod operations not yet supported")
|
||||||
|
}
|
||||||
|
|
||||||
|
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("Pod operations not yet supported")
|
||||||
|
}
|
||||||
|
|
||||||
|
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("Pod operations not yet supported")
|
||||||
|
}
|
||||||
|
|
||||||
|
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("Pod logs to stdout not yet supported")
|
||||||
|
}
|
||||||
|
|
||||||
|
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("Cannot exec into a pod. 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")
|
||||||
|
}
|
||||||
|
|
||||||
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,9 +643,11 @@ 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 {
|
||||||
gui.Log.Error(err)
|
if err := item.Container.Stop(); err != nil {
|
||||||
|
gui.Log.Error(err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -487,9 +659,11 @@ 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 {
|
||||||
gui.Log.Error(err)
|
if err := item.Container.Remove(true, false); err != nil {
|
||||||
|
gui.Log.Error(err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,91 @@ 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 {
|
||||||
|
return GetPodDisplayStrings(guiConfig, item.Pod)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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:")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -109,18 +109,17 @@ 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
|
ComposeConfigTitle string
|
||||||
ComposeConfigTitle string
|
StatsTitle string
|
||||||
StatsTitle string
|
CreditsTitle string
|
||||||
CreditsTitle string
|
ContainerConfigTitle string
|
||||||
ContainerConfigTitle string
|
ContainerEnvTitle string
|
||||||
ContainerEnvTitle string
|
NothingToDisplay string
|
||||||
NothingToDisplay string
|
NoContainerForService string
|
||||||
NoContainerForService string
|
CannotDisplayEnvVariables string
|
||||||
CannotDisplayEnvVariables string
|
|
||||||
|
|
||||||
No string
|
No string
|
||||||
Yes string
|
Yes string
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue