This commit is contained in:
christophe-duc 2026-01-07 15:05:07 -04:00
parent 39a8e864b9
commit 1ec809ec8a
14 changed files with 152 additions and 241 deletions

View file

@ -6,7 +6,7 @@ test
.goreleaser.yml
*.md
coverage.txt
Dockerfile
Containerfile
LICENSE
test.sh
.git

1
.gitignore vendored
View file

@ -1,4 +1,5 @@
lazydocker*
lazypodman*
TODO.md
Lazydocker.code-workspace
.vscode

View file

@ -120,7 +120,7 @@ brews:
# # apps:
#
# # # The name of the app must be the same name as the binary built or the snapcraft name.
# # lazydocker:
# # lazypodman:
#
# # # If your app requires extra permissions to work outside of its default
# # # confined space, declare them here.

118
CLAUDE.md
View file

@ -2,13 +2,13 @@
## Project Overview
This is a fork of **lazydocker** being converted to **lazypodman** - a terminal UI for managing Podman containers.
This is a fork of **lazydocker** converted to **lazypodman** - a terminal UI for managing Podman containers.
**Goals:**
1. Support native libpod (Podman's Go library) instead of Docker SDK
2. Enable socket-less operation for Podman commands (no daemon required)
**Current State:** The codebase uses Docker Go SDK (`github.com/docker/docker` v28.5.2) and requires conversion to libpod.
**Current State:** Conversion complete. The codebase now uses Podman bindings (`github.com/containers/podman/v5` v5.7.1) with a hybrid runtime architecture supporting both socket mode (REST API) and socket-less mode (direct libpod).
## Quick Start
@ -26,7 +26,7 @@ go build -mod=vendor
./lazypodman -d
# Run with specific compose files
./lazypodman -f docker-compose.yml -f docker-compose.override.yml
./lazypodman -f podman-compose.yml -f podman-compose.override.yml
```
## Architecture
@ -48,78 +48,83 @@ pkg/
└── cheatsheet/ # Keybinding reference
```
### Key Files for Podman Integration
### Key Files
**Primary targets for libpod conversion:**
- `pkg/commands/docker.go` - Main client connection and initialization
- `pkg/commands/container.go` - Container operations
- `pkg/commands/image.go` - Image operations
- `pkg/commands/volume.go` - Volume operations
- `pkg/commands/network.go` - Network operations
**Runtime abstraction layer:**
- `pkg/commands/runtime.go` - `ContainerRuntime` interface abstracting all container operations
- `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_stub.go` - Stub for non-Linux platforms
- `pkg/commands/runtime_types.go` - Custom types (ContainerSummary, ImageSummary, etc.)
**Podman integration:**
- `pkg/commands/podman.go` - Main client connection, auto-detection, and initialization
- `pkg/commands/container.go` - Container wrapper operations
- `pkg/commands/image.go` - Image wrapper operations
- `pkg/commands/volume.go` - Volume wrapper operations
- `pkg/commands/network.go` - Network wrapper operations
- `pkg/commands/service.go` - Compose service operations
- `pkg/commands/docker_host_unix.go` - Unix socket detection
- `pkg/commands/docker_host_windows.go` - Windows pipe detection
- `pkg/commands/podman_host_unix.go` - Unix socket detection
- `pkg/commands/podman_host_windows.go` - Windows pipe detection
**Application entry:**
- `main.go` - Entry point, CLI flags
- `pkg/app/app.go` - App struct, initialization flow
## Current Docker Integration
## Runtime Architecture
### Hybrid Runtime System
The codebase uses a `ContainerRuntime` interface with two implementations:
1. **Socket Mode** (`runtime_socket.go`)
- Uses Podman REST API via `pkg/bindings`
- Connects to local or remote Podman instances
- Supports SSH tunneling for remote hosts
- Real event streaming
2. **Libpod Mode** (`runtime_libpod.go`)
- Direct libpod library calls (no socket required)
- Linux + CGO only
- Event polling (2-second intervals)
- Fallback when socket unavailable
### Connection Flow
1. `NewDockerCommand()` in `docker.go` initializes client
2. Docker host determined from `DOCKER_HOST` env or platform defaults
3. Uses `github.com/docker/docker/client` SDK for API calls
4. SSH tunneling supported for remote hosts
1. `NewPodmanCommand()` in `podman.go` initializes runtime
2. Host determined from `CONTAINER_HOST` env or platform defaults
3. Tries socket mode first, falls back to libpod if unavailable
4. Auto-detects compose tool: `podman-compose`, `podman compose`, or `docker-compose`
### API Methods Used
### Runtime Interface Methods
```go
// Container operations
Client.ContainerList()
Client.ContainerInspect()
Client.ContainerStats() // Streaming
Client.ContainerStart/Stop/Pause/Unpause/Restart/Remove()
Client.ContainersPrune()
ListContainers() / InspectContainer() / ContainerStats()
StartContainer() / StopContainer() / PauseContainer() / UnpauseContainer()
RestartContainer() / RemoveContainer() / PruneContainers() / ContainerTop()
// Image operations
Client.ImageList()
Client.ImagesPrune()
ListImages() / InspectImage() / ImageHistory() / RemoveImage() / PruneImages()
// Volume/Network
Client.VolumeList/VolumesPrune()
Client.NetworkList/NetworksPrune()
ListVolumes() / RemoveVolume() / PruneVolumes()
ListNetworks() / RemoveNetwork() / PruneNetworks()
// Events
GetEvents() - Streaming (socket) or polling (libpod)
```
### Command Execution Patterns
1. **SDK calls** - For most container/image operations
1. **Runtime interface calls** - For most container/image operations
2. **Shell commands** - For interactive operations (attach, logs) and compose
3. **Template-based** - Configurable command templates in config
## Libpod Integration Path
## Platform Support
### Required Changes
1. **Replace Docker SDK with libpod bindings**
- Add `github.com/containers/podman/v5/pkg/bindings` dependency
- Replace `*client.Client` with libpod connection
2. **Update type mappings**
- Docker `container.Summary` -> Podman equivalent
- Docker `image.Summary` -> Podman equivalent
- All API response types need mapping
3. **Socket-less mode**
- Implement direct libpod calls without socket
- Use `bindings.NewConnection()` with appropriate URI
4. **Compose support**
- Detect `podman-compose` vs `docker-compose`
- Update command templates
### Files NOT requiring changes
- `pkg/gui/*` - UI layer is container-runtime agnostic
- `pkg/config/*` - Configuration structure remains same
- `pkg/i18n/*` - Translations unaffected
| Platform | Socket Mode | Libpod Mode |
|----------|-------------|-------------|
| Linux | ✅ | ✅ (requires CGO) |
| macOS | ✅ | ❌ (stub) |
| Windows | ✅ | ❌ (stub) |
## Development Guidelines
@ -153,4 +158,11 @@ go test -mod=vendor ./pkg/gui/...
## Module Info
- Module: `github.com/christophe-duc/lazypodman`
- Go: 1.22+ (toolchain 1.23.6)
- Key deps: gocui, docker/docker, logrus
- Key deps: gocui, containers/podman/v5, logrus
## Known Limitations
- **Network Prune**: Libpod mode has a TODO for manual pruning implementation
- **Event Streaming**: Libpod mode uses polling (2-second intervals) instead of true event streaming
- **Libpod Availability**: Socket-less mode requires Linux + CGO compilation
- **Docker SDK**: Present in go.mod as indirect transitive dependency (pulled by Podman itself, not used by application code)

53
Containerfile Normal file
View file

@ -0,0 +1,53 @@
ARG BASE_IMAGE_BUILDER=golang
ARG ALPINE_VERSION=3.20
ARG GO_VERSION=1.23
FROM ${BASE_IMAGE_BUILDER}:${GO_VERSION}-alpine${ALPINE_VERSION} AS builder
ARG GOARCH=amd64
ARG GOARM
ARG VERSION
ARG VCS_REF
WORKDIR /tmp/gobuild
COPY ./ .
RUN CGO_ENABLED=0 GOOS=linux GOARCH=${GOARCH} GOARM=${GOARM} \
go build -a -mod=vendor \
-ldflags="-s -w \
-X main.commit=${VCS_REF} \
-X main.version=${VERSION} \
-X main.buildSource=Podman"
FROM ${BASE_IMAGE_BUILDER}:${GO_VERSION}-alpine${ALPINE_VERSION} AS podman-builder
ARG GOARCH=amd64
ARG GOARM
ARG PODMAN_VERSION=v5.3.1
RUN apk add -U -q --progress --no-cache curl tar gzip
WORKDIR /tmp/podman
# Download pre-built podman-remote binary from GitHub releases
RUN ARCH=$(case "${GOARCH}" in \
amd64) echo "amd64" ;; \
arm64) echo "arm64" ;; \
arm) echo "arm" ;; \
*) echo "amd64" ;; \
esac) && \
curl -fsSL "https://github.com/containers/podman/releases/download/${PODMAN_VERSION}/podman-remote-static-linux_${ARCH}.tar.gz" | \
tar -xzf - && \
mv bin/podman-remote-static-linux_${ARCH} /usr/local/bin/podman && \
chmod +x /usr/local/bin/podman
FROM scratch
ARG BUILD_DATE
ARG VCS_REF
ARG VERSION
LABEL \
org.opencontainers.image.authors="christophe-duc" \
org.opencontainers.image.created=$BUILD_DATE \
org.opencontainers.image.version=$VERSION \
org.opencontainers.image.revision=$VCS_REF \
org.opencontainers.image.url="https://github.com/christophe-duc/lazypodman" \
org.opencontainers.image.documentation="https://github.com/christophe-duc/lazypodman" \
org.opencontainers.image.source="https://github.com/christophe-duc/lazypodman" \
org.opencontainers.image.title="lazypodman" \
org.opencontainers.image.description="The lazier way to manage everything podman"
ENTRYPOINT [ "/bin/lazypodman" ]
COPY --from=podman-builder /usr/local/bin/podman /bin/podman
COPY --from=builder /tmp/gobuild/lazypodman /bin/lazypodman

View file

@ -1,49 +0,0 @@
ARG BASE_IMAGE_BUILDER=golang
ARG ALPINE_VERSION=3.20
ARG GO_VERSION=1.23
FROM ${BASE_IMAGE_BUILDER}:${GO_VERSION}-alpine${ALPINE_VERSION} AS builder
ARG GOARCH=amd64
ARG GOARM
ARG VERSION
ARG VCS_REF
WORKDIR /tmp/gobuild
COPY ./ .
RUN CGO_ENABLED=0 GOOS=linux GOARCH=${GOARCH} GOARM=${GOARM} \
go build -a -mod=vendor \
-ldflags="-s -w \
-X main.commit=${VCS_REF} \
-X main.version=${VERSION} \
-X main.buildSource=Docker"
FROM ${BASE_IMAGE_BUILDER}:${GO_VERSION}-alpine${ALPINE_VERSION} AS docker-builder
ARG GOARCH=amd64
ARG GOARM
ARG DOCKER_VERSION=v27.0.3
RUN apk add -U -q --progress --no-cache git bash coreutils gcc musl-dev
WORKDIR /go/src/github.com/docker/cli
RUN git clone --branch ${DOCKER_VERSION} --single-branch --depth 1 https://github.com/docker/cli.git . > /dev/null 2>&1
ENV CGO_ENABLED=0 \
GOARCH=${GOARCH} \
GOARM=${GOARM} \
DISABLE_WARN_OUTSIDE_CONTAINER=1
RUN ./scripts/build/binary
RUN rm build/docker && mv build/docker-linux-* build/docker
FROM scratch
ARG BUILD_DATE
ARG VCS_REF
ARG VERSION
LABEL \
org.opencontainers.image.authors="jessedduffield@gmail.com" \
org.opencontainers.image.created=$BUILD_DATE \
org.opencontainers.image.version=$VERSION \
org.opencontainers.image.revision=$VCS_REF \
org.opencontainers.image.url="https://github.com/jesseduffield/lazydocker" \
org.opencontainers.image.documentation="https://github.com/jesseduffield/lazydocker" \
org.opencontainers.image.source="https://github.com/jesseduffield/lazydocker" \
org.opencontainers.image.title="lazydocker" \
org.opencontainers.image.description="The lazier way to manage everything docker"
ENTRYPOINT [ "/bin/lazydocker" ]
COPY --from=docker-builder /go/src/github.com/docker/cli/build/docker /bin/docker
COPY --from=builder /tmp/gobuild/lazydocker /bin/lazydocker

117
README.md

File diff suppressed because one or more lines are too long

View file

@ -1,16 +0,0 @@
version: '3'
services:
lazypodman:
build:
context: https://github.com/christophe-duc/lazypodman.git
args:
BASE_IMAGE_BUILDER: golang
GOARCH: amd64
GOARM:
image: christophe-duc/lazypodman
container_name: lazypodman
stdin_open: true
tty: true
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- ./config:/.config/christophe-duc/lazypodman

View file

@ -5,7 +5,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
## Proje
<pre>
<kbd>e</kbd>: lazzydocker ayarlarını düzenle
<kbd>e</kbd>: lazypodman ayarlarını düzenle
<kbd>o</kbd>: lazypodman ayarlarını
<kbd>m</kbd>: kayıt defterini görüntüle
<kbd>enter</kbd>: ana panele odaklan

View file

@ -1,6 +1,7 @@
#!/bin/bash
docker build --build-arg BUILD_DATE=`date -u +"%Y-%m-%dT%H:%M:%SZ"` \
podman build -f Containerfile \
--build-arg BUILD_DATE=`date -u +"%Y-%m-%dT%H:%M:%SZ"` \
--build-arg VCS_REF=`git rev-parse --short HEAD` \
--build-arg VERSION=`git describe --abbrev=0 --tag` \
-t $IMAGE_NAME .

Binary file not shown.

View file

@ -126,7 +126,7 @@ func addBinding(title string, bindingSections []*bindingSection, binding *gui.Bi
}
func formatSections(mApp *app.App, bindingSections []*bindingSection) string {
content := fmt.Sprintf("# Lazydocker %s\n", mApp.Tr.Menu)
content := fmt.Sprintf("# Lazypodman %s\n", mApp.Tr.Menu)
for _, section := range bindingSections {
content += formatTitle(section.title)

View file

@ -84,7 +84,7 @@ func (c *OSCommand) RunExecutable(cmd *exec.Cmd) error {
return err
}
// ExecutableFromString takes a string like `docker ps -a` and returns an executable command for it
// ExecutableFromString takes a string like `podman ps -a` and returns an executable command for it
func (c *OSCommand) ExecutableFromString(commandStr string) *exec.Cmd {
splitCmd := str.ToArgv(commandStr)
return c.NewCmd(splitCmd[0], splitCmd[1:]...)
@ -369,7 +369,7 @@ func (c *OSCommand) Kill(cmd *exec.Cmd) error {
return kill.Kill(cmd)
}
// PrepareForChildren sets Setpgid to true on the cmd, so that when we run it as a subprocess, we can kill its group rather than the process itself. This is because some commands, like `docker-compose logs` spawn multiple children processes, and killing the parent process isn't sufficient for killing those child processes. We set the group id here, and then in subprocess.go we check if the group id is set and if so, we kill the whole group rather than just the one process.
// PrepareForChildren sets Setpgid to true on the cmd, so that when we run it as a subprocess, we can kill its group rather than the process itself. This is because some commands, like `podman-compose logs` spawn multiple children processes, and killing the parent process isn't sufficient for killing those child processes. We set the group id here, and then in subprocess.go we check if the group id is set and if so, we kill the whole group rather than just the one process.
func (c *OSCommand) PrepareForChildren(cmd *exec.Cmd) {
kill.PrepareForChildren(cmd)
}

24
podman-compose.yml Normal file
View file

@ -0,0 +1,24 @@
version: '3'
services:
lazypodman:
build:
context: https://github.com/christophe-duc/lazypodman.git
dockerfile: Containerfile
args:
BASE_IMAGE_BUILDER: golang
GOARCH: amd64
GOARM:
image: christophe-duc/lazypodman
container_name: lazypodman
stdin_open: true
tty: true
volumes:
# Rootless Podman socket (default for most users)
# For rootful: use /run/podman/podman.sock instead
- ${XDG_RUNTIME_DIR:-/run/user/1000}/podman/podman.sock:/run/podman/podman.sock:ro
# Config directory
- ./config:/.config/lazypodman
environment:
- CONTAINER_HOST=unix:///run/podman/podman.sock
security_opt:
- label=disable