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:
Christophe 2026-01-09 14:52:41 -04:00 committed by GitHub
commit 439a4422a9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 42427 additions and 243 deletions

View file

@ -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:

View file

@ -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

File diff suppressed because it is too large Load diff

View file

@ -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) {

View 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")
}