mirror of
https://github.com/jesseduffield/lazydocker.git
synced 2026-07-25 08:31:03 +00:00
Merge pull request #12 from christophe-duc/feature/add_streaming_event_support_for_libpod
Feature/add streaming event support for libpod
This commit is contained in:
commit
439a4422a9
5 changed files with 42427 additions and 243 deletions
4
.github/workflows/cd.yml
vendored
4
.github/workflows/cd.yml
vendored
|
|
@ -19,6 +19,10 @@ jobs:
|
|||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.25'
|
||||
- name: Install build dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y gcc-aarch64-linux-gnu libc6-dev-arm64-cross
|
||||
- name: Run goreleaser
|
||||
uses: goreleaser/goreleaser-action@v6
|
||||
with:
|
||||
|
|
|
|||
|
|
@ -1,19 +1,36 @@
|
|||
# This is an example goreleaser.yaml file with some sane defaults.
|
||||
# Make sure to check the documentation at http://goreleaser.com
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
|
||||
builds:
|
||||
- id: binary
|
||||
# macOS builds - CGO disabled (libpod not supported)
|
||||
- id: darwin
|
||||
tags:
|
||||
- containers_image_openpgp
|
||||
- exclude_graphdriver_btrfs
|
||||
goos:
|
||||
- darwin
|
||||
goarch:
|
||||
- amd64
|
||||
- arm64
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
ldflags:
|
||||
- -s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.Date}} -X main.buildSource=binaryRelease
|
||||
|
||||
# Linux builds - CGO enabled (libpod support)
|
||||
- id: linux
|
||||
tags:
|
||||
- containers_image_openpgp
|
||||
- exclude_graphdriver_btrfs
|
||||
goos:
|
||||
- linux
|
||||
goarch:
|
||||
- amd64
|
||||
- arm64
|
||||
env:
|
||||
- CGO_ENABLED=1
|
||||
- >-
|
||||
{{- if eq .Arch "arm64" }}CC=aarch64-linux-gnu-gcc{{- end }}
|
||||
ldflags:
|
||||
- -s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.Date}} -X main.buildSource=binaryRelease
|
||||
- id: snap
|
||||
|
|
@ -30,7 +47,8 @@ builds:
|
|||
|
||||
archives:
|
||||
- builds:
|
||||
- binary
|
||||
- darwin
|
||||
- linux
|
||||
name_template: >-
|
||||
{{ .ProjectName }}_
|
||||
{{- if eq .Os "darwin" }}Darwin
|
||||
|
|
|
|||
42277
coverage.txt
42277
coverage.txt
File diff suppressed because it is too large
Load diff
|
|
@ -4,10 +4,12 @@ package commands
|
|||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/containers/podman/v5/libpod"
|
||||
"github.com/containers/podman/v5/libpod/define"
|
||||
"github.com/containers/podman/v5/libpod/events"
|
||||
"go.podman.io/common/libimage"
|
||||
nettypes "go.podman.io/common/libnetwork/types"
|
||||
)
|
||||
|
|
@ -503,8 +505,9 @@ func (r *LibpodRuntime) RemovePod(ctx context.Context, id string, force bool) er
|
|||
return err
|
||||
}
|
||||
|
||||
// Events streams container runtime events.
|
||||
// For libpod, we use a polling approach since direct event streaming requires more setup.
|
||||
// Events streams container runtime events using native libpod event streaming.
|
||||
// This provides real-time event delivery with <100ms latency for actual events,
|
||||
// plus an initial synthetic event to trigger UI load and periodic heartbeat refreshes.
|
||||
func (r *LibpodRuntime) Events(ctx context.Context) (<-chan Event, <-chan error) {
|
||||
eventChan := make(chan Event)
|
||||
errChan := make(chan error, 1)
|
||||
|
|
@ -513,27 +516,92 @@ func (r *LibpodRuntime) Events(ctx context.Context) (<-chan Event, <-chan error)
|
|||
defer close(eventChan)
|
||||
defer close(errChan)
|
||||
|
||||
// Libpod events require more complex setup; for now use periodic polling
|
||||
// by sending empty events that trigger refreshes
|
||||
ticker := time.NewTicker(2 * time.Second)
|
||||
// Send initial synthetic event immediately to trigger UI load
|
||||
// This ensures the UI displays containers/pods even before any real events occur
|
||||
select {
|
||||
case eventChan <- Event{
|
||||
Type: "system",
|
||||
Action: "refresh",
|
||||
Time: time.Now().Unix(),
|
||||
}:
|
||||
case <-ctx.Done():
|
||||
return
|
||||
}
|
||||
|
||||
// Create libpod event channel (buffered to prevent blocking during bursts)
|
||||
libpodEventChan := make(chan events.ReadResult, 10)
|
||||
|
||||
// Configure event streaming
|
||||
opts := events.ReadOptions{
|
||||
EventChannel: libpodEventChan,
|
||||
Stream: true, // Follow new events (tail -f mode)
|
||||
FromStart: false, // Don't replay historical events
|
||||
Filters: []string{}, // Empty = all event types
|
||||
}
|
||||
|
||||
// Start libpod event reader in background goroutine
|
||||
go func() {
|
||||
if err := r.runtime.Events(ctx, opts); err != nil {
|
||||
// Only report error if context wasn't cancelled
|
||||
if ctx.Err() == nil {
|
||||
select {
|
||||
case errChan <- fmt.Errorf("libpod events error: %w", err):
|
||||
case <-ctx.Done():
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// Periodic heartbeat ticker for UI refresh when no events occur
|
||||
// This is much less frequent than the old 2-second polling (now 10 seconds)
|
||||
// because real events provide immediate updates
|
||||
ticker := time.NewTicker(10 * time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
// Convert libpod events to ContainerRuntime Event format
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
|
||||
case <-ticker.C:
|
||||
// Send a synthetic event to trigger refresh
|
||||
event := Event{
|
||||
Type: "refresh",
|
||||
Action: "poll",
|
||||
Time: time.Now().Unix(),
|
||||
}
|
||||
// Send periodic heartbeat to ensure UI updates even without real events
|
||||
// This handles edge cases like containers dying without generating events
|
||||
select {
|
||||
case eventChan <- event:
|
||||
case eventChan <- Event{
|
||||
Type: "system",
|
||||
Action: "refresh",
|
||||
Time: time.Now().Unix(),
|
||||
}:
|
||||
case <-ctx.Done():
|
||||
return
|
||||
}
|
||||
|
||||
case result, ok := <-libpodEventChan:
|
||||
if !ok {
|
||||
// Channel closed, exit
|
||||
return
|
||||
}
|
||||
|
||||
// Handle errors
|
||||
if result.Error != nil {
|
||||
select {
|
||||
case errChan <- result.Error:
|
||||
case <-ctx.Done():
|
||||
return
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// Convert and forward event
|
||||
if result.Event != nil {
|
||||
event := convertLibpodEvent(result.Event)
|
||||
select {
|
||||
case eventChan <- event:
|
||||
case <-ctx.Done():
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
|
@ -541,6 +609,19 @@ func (r *LibpodRuntime) Events(ctx context.Context) (<-chan Event, <-chan error)
|
|||
return eventChan, errChan
|
||||
}
|
||||
|
||||
// convertLibpodEvent converts native libpod event to ContainerRuntime Event format.
|
||||
func convertLibpodEvent(e *events.Event) Event {
|
||||
return Event{
|
||||
Type: string(e.Type), // "Container", "Pod", "Image", "Volume", "Network"
|
||||
Action: string(e.Status), // "start", "stop", "create", "remove", etc.
|
||||
Actor: EventActor{
|
||||
ID: e.ID,
|
||||
Attributes: e.Attributes,
|
||||
},
|
||||
Time: e.Time.Unix(),
|
||||
}
|
||||
}
|
||||
|
||||
// Conversion functions
|
||||
|
||||
func convertLibpodContainerList(ctrs []*libpod.Container) ([]ContainerSummary, error) {
|
||||
|
|
|
|||
258
pkg/commands/runtime_libpod_test.go
Normal file
258
pkg/commands/runtime_libpod_test.go
Normal file
|
|
@ -0,0 +1,258 @@
|
|||
//go:build linux && cgo
|
||||
|
||||
package commands
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/containers/podman/v5/libpod/events"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestConvertLibpodEvent(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input *events.Event
|
||||
expected Event
|
||||
}{
|
||||
{
|
||||
name: "container start event",
|
||||
input: &events.Event{
|
||||
Type: events.Container,
|
||||
Status: events.Start,
|
||||
ID: "abc123",
|
||||
Name: "test-container",
|
||||
Time: time.Unix(1234567890, 0),
|
||||
Details: events.Details{
|
||||
Attributes: map[string]string{"image": "test:latest"},
|
||||
},
|
||||
},
|
||||
expected: Event{
|
||||
Type: "container",
|
||||
Action: "start",
|
||||
Actor: EventActor{
|
||||
ID: "abc123",
|
||||
Attributes: map[string]string{"image": "test:latest"},
|
||||
},
|
||||
Time: 1234567890,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "pod stop event",
|
||||
input: &events.Event{
|
||||
Type: events.Pod,
|
||||
Status: events.Stop,
|
||||
ID: "pod456",
|
||||
Name: "test-pod",
|
||||
Time: time.Unix(1234567891, 0),
|
||||
Details: events.Details{
|
||||
Attributes: nil,
|
||||
},
|
||||
},
|
||||
expected: Event{
|
||||
Type: "pod",
|
||||
Action: "stop",
|
||||
Actor: EventActor{
|
||||
ID: "pod456",
|
||||
Attributes: nil,
|
||||
},
|
||||
Time: 1234567891,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "image pull event",
|
||||
input: &events.Event{
|
||||
Type: events.Image,
|
||||
Status: events.Pull,
|
||||
ID: "img789",
|
||||
Name: "nginx:latest",
|
||||
Time: time.Unix(1234567892, 0),
|
||||
Details: events.Details{
|
||||
Attributes: map[string]string{},
|
||||
},
|
||||
},
|
||||
expected: Event{
|
||||
Type: "image",
|
||||
Action: "pull",
|
||||
Actor: EventActor{
|
||||
ID: "img789",
|
||||
Attributes: map[string]string{},
|
||||
},
|
||||
Time: 1234567892,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "volume create event",
|
||||
input: &events.Event{
|
||||
Type: events.Volume,
|
||||
Status: events.Create,
|
||||
ID: "vol999",
|
||||
Name: "test-volume",
|
||||
Time: time.Unix(1234567893, 0),
|
||||
},
|
||||
expected: Event{
|
||||
Type: "volume",
|
||||
Action: "create",
|
||||
Actor: EventActor{
|
||||
ID: "vol999",
|
||||
Attributes: nil,
|
||||
},
|
||||
Time: 1234567893,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "network remove event",
|
||||
input: &events.Event{
|
||||
Type: events.Network,
|
||||
Status: events.Remove,
|
||||
ID: "net111",
|
||||
Name: "test-network",
|
||||
Time: time.Unix(1234567894, 0),
|
||||
},
|
||||
expected: Event{
|
||||
Type: "network",
|
||||
Action: "remove",
|
||||
Actor: EventActor{
|
||||
ID: "net111",
|
||||
Attributes: nil,
|
||||
},
|
||||
Time: 1234567894,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "container die event with attributes",
|
||||
input: &events.Event{
|
||||
Type: events.Container,
|
||||
Status: events.Exited,
|
||||
ID: "ctr222",
|
||||
Name: "dying-container",
|
||||
Time: time.Unix(1234567895, 0),
|
||||
Details: events.Details{
|
||||
Attributes: map[string]string{
|
||||
"exitCode": "1",
|
||||
"image": "alpine:latest",
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: Event{
|
||||
Type: "container",
|
||||
Action: "died",
|
||||
Actor: EventActor{
|
||||
ID: "ctr222",
|
||||
Attributes: map[string]string{
|
||||
"exitCode": "1",
|
||||
"image": "alpine:latest",
|
||||
},
|
||||
},
|
||||
Time: 1234567895,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := convertLibpodEvent(tt.input)
|
||||
assert.Equal(t, tt.expected.Type, result.Type, "Event type mismatch")
|
||||
assert.Equal(t, tt.expected.Action, result.Action, "Event action mismatch")
|
||||
assert.Equal(t, tt.expected.Actor.ID, result.Actor.ID, "Actor ID mismatch")
|
||||
assert.Equal(t, tt.expected.Time, result.Time, "Event time mismatch")
|
||||
assert.Equal(t, tt.expected.Actor.Attributes, result.Actor.Attributes, "Attributes mismatch")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertLibpodEvent_AllEventTypes(t *testing.T) {
|
||||
// Test that all major event types are correctly converted
|
||||
eventTypes := []events.Type{
|
||||
events.Container,
|
||||
events.Pod,
|
||||
events.Image,
|
||||
events.Volume,
|
||||
events.Network,
|
||||
events.System,
|
||||
}
|
||||
|
||||
for _, eventType := range eventTypes {
|
||||
t.Run(string(eventType), func(t *testing.T) {
|
||||
input := &events.Event{
|
||||
Type: eventType,
|
||||
Status: events.Create,
|
||||
ID: "test-id",
|
||||
Time: time.Unix(1234567890, 0),
|
||||
}
|
||||
result := convertLibpodEvent(input)
|
||||
assert.Equal(t, string(eventType), result.Type)
|
||||
assert.Equal(t, "create", result.Action)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertLibpodEvent_AllEventStatuses(t *testing.T) {
|
||||
// Test that common event statuses are correctly converted
|
||||
statuses := []events.Status{
|
||||
events.Start,
|
||||
events.Stop,
|
||||
events.Create,
|
||||
events.Remove,
|
||||
events.Pause,
|
||||
events.Unpause,
|
||||
events.Kill,
|
||||
events.Exited,
|
||||
events.Pull,
|
||||
events.Push,
|
||||
events.Restart,
|
||||
}
|
||||
|
||||
for _, status := range statuses {
|
||||
t.Run(string(status), func(t *testing.T) {
|
||||
input := &events.Event{
|
||||
Type: events.Container,
|
||||
Status: status,
|
||||
ID: "test-id",
|
||||
Time: time.Unix(1234567890, 0),
|
||||
}
|
||||
result := convertLibpodEvent(input)
|
||||
assert.Equal(t, string(status), result.Action)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertLibpodEvent_EmptyAttributes(t *testing.T) {
|
||||
// Test that nil and empty attributes are handled correctly
|
||||
testCases := []struct {
|
||||
name string
|
||||
attributes map[string]string
|
||||
}{
|
||||
{"nil attributes", nil},
|
||||
{"empty attributes", map[string]string{}},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
input := &events.Event{
|
||||
Type: events.Container,
|
||||
Status: events.Start,
|
||||
ID: "test-id",
|
||||
Time: time.Unix(1234567890, 0),
|
||||
Details: events.Details{
|
||||
Attributes: tc.attributes,
|
||||
},
|
||||
}
|
||||
result := convertLibpodEvent(input)
|
||||
assert.Equal(t, tc.attributes, result.Actor.Attributes)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLibpodRuntimeEvents_ContextCancellation(t *testing.T) {
|
||||
// This test verifies that Events() respects context cancellation
|
||||
// Mock implementation would be needed for full testing
|
||||
t.Skip("Requires mock libpod runtime for full integration test")
|
||||
}
|
||||
|
||||
func TestLibpodRuntimeEvents_ErrorHandling(t *testing.T) {
|
||||
// This test verifies error handling in event streaming
|
||||
// Mock implementation would be needed for full testing
|
||||
t.Skip("Requires mock libpod runtime for full integration test")
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue