diff --git a/cmd/podman/common/completion.go b/cmd/podman/common/completion.go index 676e67fcf0f..d0dfa2fc2a9 100644 --- a/cmd/podman/common/completion.go +++ b/cmd/podman/common/completion.go @@ -1580,7 +1580,7 @@ func getMethodNames(f reflect.Value, prefix string) []formatSuggestion { } // AutocompleteEventFilter - Autocomplete event filter flag options. -// -> "container=", "event=", "image=", "pod=", "volume=", "type=" +// -> "container=", "event=", "image=", "pod=", "volume=", "type=", "artifact=" func AutocompleteEventFilter(cmd *cobra.Command, _ []string, toComplete string) ([]string, cobra.ShellCompDirective) { event := func(_ string) ([]string, cobra.ShellCompDirective) { return []string{ @@ -1599,11 +1599,13 @@ func AutocompleteEventFilter(cmd *cobra.Command, _ []string, toComplete string) return []string{ events.Container.String(), events.Image.String(), events.Network.String(), events.Pod.String(), events.System.String(), events.Volume.String(), events.Secret.String(), + events.Artifact.String(), }, cobra.ShellCompDirectiveNoFileComp } kv := keyValueCompletion{ "container=": func(s string) ([]string, cobra.ShellCompDirective) { return getContainers(cmd, s, completeDefault) }, "image=": func(s string) ([]string, cobra.ShellCompDirective) { return getImages(cmd, s) }, + "artifact=": func(s string) ([]string, cobra.ShellCompDirective) { return getArtifacts(cmd, s) }, "pod=": func(s string) ([]string, cobra.ShellCompDirective) { return getPods(cmd, s, completeDefault) }, "volume=": func(s string) ([]string, cobra.ShellCompDirective) { return getVolumes(cmd, s) }, "event=": event, diff --git a/docs/source/markdown/podman-events.1.md b/docs/source/markdown/podman-events.1.md index 681e43d3a08..c4c22164642 100644 --- a/docs/source/markdown/podman-events.1.md +++ b/docs/source/markdown/podman-events.1.md @@ -70,6 +70,12 @@ The *image* event type reports the following statuses: * unmount * untag +The *artifact* event type reports the following statuses: + * create + * pull + * push + * remove + The *system* type reports the following statuses: * refresh * renumber @@ -102,6 +108,7 @@ filters are supported: | **Filter** | **Description** | |------------|-------------------------------------| +| artifact | [Name or ID] Artifact name or ID | | container | [Name or ID] Container's name or ID | | event | event_status (described above) | | image | [Name or ID] Image name or ID | @@ -167,8 +174,8 @@ The journald events-backend of Podman uses the following journald identifiers. | PODMAN_EVENT | The event status as described above | | PODMAN_TYPE | The event type as described above | | PODMAN_TIME | The time stamp when the event was written | -| PODMAN_NAME | Name of the event object (e.g., container, image) | -| PODMAN_ID | ID of the event object (e.g., container, image) | +| PODMAN_NAME | Name of the event object (e.g., container, image, artifact) | +| PODMAN_ID | ID of the event object (e.g., container, image, artifact) | | PODMAN_EXIT_CODE | Exit code of the container | | PODMAN_POD_ID | Pod ID of the container | | PODMAN_LABELS | Labels of the container | diff --git a/libpod/events.go b/libpod/events.go index a4ff9197be2..157c5c497d2 100644 --- a/libpod/events.go +++ b/libpod/events.go @@ -6,6 +6,7 @@ import ( "context" "fmt" "path/filepath" + "time" "github.com/sirupsen/logrus" "go.podman.io/podman/v6/libpod/define" @@ -257,3 +258,32 @@ func (r *Runtime) GetExecDiedEvent(ctx context.Context, nameOrID, execSessionID } return containerEvents[len(containerEvents)-1], nil } + +// spawnEventForwarder starts a goroutine that reads events from the given channel, transforms and forwards them to the eventer. +func spawnEventForwarder[T any](eventer events.Eventer, toLibpodEvent func(libEvent T) events.Event, eventChannel <-chan T, shutdownChan chan bool) { + go func() { + sawShutdown := false + for { + // Make sure to read and write all events before + // shutting down. + for len(eventChannel) > 0 { + libEvent := <-eventChannel + libpodEvent := toLibpodEvent(libEvent) + if err := eventer.Write(libpodEvent); err != nil { + logrus.Errorf("Unable to write event of type %T: %q", libEvent, err) + } + } + + if sawShutdown { + close(shutdownChan) + return + } + + select { + case <-shutdownChan: + sawShutdown = true + case <-time.After(100 * time.Millisecond): + } + } + }() +} diff --git a/libpod/events/config.go b/libpod/events/config.go index f73854dd32e..09da743520d 100644 --- a/libpod/events/config.go +++ b/libpod/events/config.go @@ -122,6 +122,8 @@ const ( Container Type = "container" // Image - event is related to images Image Type = "image" + // Artifact - event is related to artifacts + Artifact Type = "artifact" // Network - event is related to networks Network Type = "network" // Pod - event is related to pods diff --git a/libpod/events/events.go b/libpod/events/events.go index 1638ffb62f5..5ae86491e97 100644 --- a/libpod/events/events.go +++ b/libpod/events/events.go @@ -79,7 +79,7 @@ func (e *Event) ToHumanReadable(truncate bool) string { } else { humanFormat = fmt.Sprintf("%s %s %s %s (container=%s, name=%s)", e.Time, e.Type, e.Status, id, id, e.Network) } - case Image: + case Image, Artifact: humanFormat = fmt.Sprintf("%s %s %s %s %s", e.Time, e.Type, e.Status, id, e.Name) if e.Error != "" { humanFormat += " " + e.Error @@ -111,6 +111,8 @@ func (s Status) String() string { // StringToType converts string to an EventType func StringToType(name string) (Type, error) { switch name { + case Artifact.String(): + return Artifact, nil case Container.String(): return Container, nil case Image.String(): diff --git a/libpod/events/filters.go b/libpod/events/filters.go index fab5fe3631d..4581c92c86f 100644 --- a/libpod/events/filters.go +++ b/libpod/events/filters.go @@ -39,6 +39,16 @@ func generateEventFilter(filter, filterValue string) (func(e *Event) bool, error } return strings.HasPrefix(e.ID, filterValue) }, nil + case "ARTIFACT": + return func(e *Event) bool { + if e.Type != Artifact { + return false + } + if e.Name == filterValue { + return true + } + return strings.HasPrefix(e.ID, filterValue) + }, nil case "POD": return func(e *Event) bool { if e.Type != Pod { diff --git a/libpod/events/journal_linux.go b/libpod/events/journal_linux.go index 8f9f2a6f32e..f596c870bff 100644 --- a/libpod/events/journal_linux.go +++ b/libpod/events/journal_linux.go @@ -37,7 +37,7 @@ func (e EventJournalD) Write(ee Event) error { // Add specialized information based on the podman type switch ee.Type { - case Image: + case Image, Artifact: m["PODMAN_NAME"] = ee.Name m["PODMAN_ID"] = ee.ID if ee.Error != "" { @@ -281,7 +281,7 @@ func newEventFromJournalEntry(entry *sdjournal.JournalEntry) (*Event, error) { if err := getLabelsFromJournal(entry, &newEvent); err != nil { return nil, err } - case Image: + case Image, Artifact: newEvent.ID = entry.Fields["PODMAN_ID"] if val, ok := entry.Fields["ERROR"]; ok { newEvent.Error = val diff --git a/libpod/events/logfile.go b/libpod/events/logfile.go index b7ce336abb7..5cb58c722a5 100644 --- a/libpod/events/logfile.go +++ b/libpod/events/logfile.go @@ -174,7 +174,7 @@ func (e EventLogFile) Read(ctx context.Context, options ReadOptions) error { continue } switch event.Type { - case Image, Volume, Pod, Container, Network, Secret: + case Image, Volume, Pod, Container, Network, Secret, Artifact: // no-op case System: begin, end, err := e.readRotateEvent(event) diff --git a/libpod/runtime.go b/libpod/runtime.go index 0aa82d25aa0..2f4f199d74f 100644 --- a/libpod/runtime.go +++ b/libpod/runtime.go @@ -14,7 +14,6 @@ import ( "strings" "sync" "syscall" - "time" "github.com/hashicorp/go-multierror" jsoniter "github.com/json-iterator/go" @@ -69,21 +68,23 @@ type Runtime struct { storageConfig storage.StoreOptions storageSet storageSet - state State - store storage.Store - storageService *storageService - imageContext types.SystemContext - defaultOCIRuntime OCIRuntime - ociRuntimes map[string]OCIRuntime - runtimeFlags []string - network nettypes.ContainerNetwork - conmonPath string - libimageRuntime *libimage.Runtime - libimageEventsShutdown chan bool - lockManager lock.Manager + state State + store storage.Store + storageService *storageService + imageContext types.SystemContext + defaultOCIRuntime OCIRuntime + ociRuntimes map[string]OCIRuntime + runtimeFlags []string + network nettypes.ContainerNetwork + conmonPath string + libimageRuntime *libimage.Runtime + libimageEventsShutdown chan bool + libartifactEventsShutdown chan bool + lockManager lock.Manager // ArtifactStore returns the artifact store created from the runtime. - ArtifactStore func() (*artStore.ArtifactStore, error) + ArtifactStore func() (*artStore.ArtifactStore, error) + shutdownArtifactStore func() // Worker workerChannel chan func() @@ -452,6 +453,16 @@ func makeRuntime(ctx context.Context, runtime *Runtime) (retErr error) { return fmt.Errorf("namespaces are not supported by this version of Libpod, please unset the `namespace` field in containers.conf: %w", define.ErrNotImplemented) } + // Set up the eventer + // WARN: This must be done synchronously before spawning any event listeners + // (e.g., libimageEvents, libartifactEvents) to prevent race conditions. + // TODO: Remove this temporal coupling. + eventer, err := runtime.newEventer() + if err != nil { + return err + } + runtime.eventer = eventer + needsUserns := os.Geteuid() != 0 if !needsUserns { hasCapSysAdmin, err := unshare.HasCapSysAdmin() @@ -485,13 +496,6 @@ func makeRuntime(ctx context.Context, runtime *Runtime) (retErr error) { } }() - // Set up the eventer - eventer, err := runtime.newEventer() - if err != nil { - return err - } - runtime.eventer = eventer - // Set up containers/image if runtime.imageContext.BigFilesTemporaryDir == "" { runtime.imageContext.BigFilesTemporaryDir = parse.GetTempDir() @@ -558,7 +562,15 @@ func makeRuntime(ctx context.Context, runtime *Runtime) (retErr error) { // Using sync once value to only init the store exactly once and only when it will be actually be used. runtime.ArtifactStore = sync.OnceValues(func() (*artStore.ArtifactStore, error) { - return artStore.NewArtifactStore(filepath.Join(runtime.storageConfig.GraphRoot, "artifacts"), runtime.SystemContext()) + artifactStore, err := artStore.NewArtifactStore(filepath.Join(runtime.storageConfig.GraphRoot, "artifacts"), runtime.SystemContext()) + if err != nil { + return nil, err + } + runtime.libartifactEvents(artifactStore) + runtime.shutdownArtifactStore = func() { + artifactStore.CloseEventChannel() + } + return artifactStore, nil }) } @@ -743,50 +755,59 @@ var libimageEventsMap = map[libimage.EventType]events.Status{ // when the main() exists. func (r *Runtime) libimageEvents() { r.libimageEventsShutdown = make(chan bool) - - toLibpodEventStatus := func(e *libimage.Event) events.Status { - status, found := libimageEventsMap[e.Type] + eventChannel := r.libimageRuntime.EventChannel() + toLibpodEventFunc := func(libimageEvent *libimage.Event) events.Event { + status, found := libimageEventsMap[libimageEvent.Type] if !found { - return "Unknown" + status = "Unknown" + } + event := events.Event{ + ID: libimageEvent.ID, + Name: libimageEvent.Name, + Status: status, + Time: libimageEvent.Time, + Type: events.Image, } - return status + if libimageEvent.Error != nil { + event.Error = libimageEvent.Error.Error() + } + return event } + spawnEventForwarder(r.eventer, toLibpodEventFunc, eventChannel, r.libimageEventsShutdown) +} - eventChannel := r.libimageRuntime.EventChannel() - go func() { - sawShutdown := false - for { - // Make sure to read and write all events before - // shutting down. - for len(eventChannel) > 0 { - libimageEvent := <-eventChannel - e := events.Event{ - ID: libimageEvent.ID, - Name: libimageEvent.Name, - Status: toLibpodEventStatus(libimageEvent), - Time: libimageEvent.Time, - Type: events.Image, - } - if libimageEvent.Error != nil { - e.Error = libimageEvent.Error.Error() - } - if err := r.eventer.Write(e); err != nil { - logrus.Errorf("Unable to write image event: %q", err) - } - } - - if sawShutdown { - close(r.libimageEventsShutdown) - return - } +// libartifactEventsMap translates a libartifact event type to a libpod event status. +var libartifactEventsMap = map[artStore.EventType]events.Status{ + artStore.EventTypeArtifactPull: events.Pull, + artStore.EventTypeArtifactPush: events.Push, + artStore.EventTypeArtifactRemove: events.Remove, + artStore.EventTypeArtifactAdd: events.Create, +} - select { - case <-r.libimageEventsShutdown: - sawShutdown = true - case <-time.After(100 * time.Millisecond): - } +// libartifactEvents spawns a goroutine which will listen for events on +// the artStore.ArtifactStore. The goroutine will be cleaned up implicitly +// when the main() exists. +func (r *Runtime) libartifactEvents(store *artStore.ArtifactStore) { + r.libartifactEventsShutdown = make(chan bool) + eventChannel := store.EventChannel() + toLibpodEventFunc := func(artStoreEvent *artStore.Event) events.Event { + status, found := libartifactEventsMap[artStoreEvent.Type] + if !found { + status = "Unknown" } - }() + event := events.Event{ + ID: artStoreEvent.ID, + Name: artStoreEvent.Name, + Status: status, + Time: artStoreEvent.Time, + Type: events.Artifact, + } + if artStoreEvent.Error != nil { + event.Error = artStoreEvent.Error.Error() + } + return event + } + spawnEventForwarder(r.eventer, toLibpodEventFunc, eventChannel, r.libartifactEventsShutdown) } // DeferredShutdown shuts down the runtime without exposing any @@ -843,6 +864,18 @@ func (r *Runtime) Shutdown(force bool) error { lastError = fmt.Errorf("shutting down container storage: %w", err) } } + + // Shutdown the artifact store if it exists + if r.libartifactEventsShutdown != nil { + // Tell loop to shutdown + r.libartifactEventsShutdown <- true + // Wait for close to signal shutdown + <-r.libartifactEventsShutdown + } + if r.shutdownArtifactStore != nil { + r.shutdownArtifactStore() + } + if err := r.state.Close(); err != nil { if lastError != nil { logrus.Error(lastError) diff --git a/test/e2e/events_test.go b/test/e2e/events_test.go index fe95867bfdd..9813e01fa83 100644 --- a/test/e2e/events_test.go +++ b/test/e2e/events_test.go @@ -307,4 +307,47 @@ var _ = Describe("Podman events", func() { Expect(result).Should(ExitCleanly()) Expect(result.OutputToStringArray()).ToNot(BeEmpty(), "Number of health_status events") }) + + It("podman events for artifacts", func() { + artifactFile, err := createArtifactFile(1024) + Expect(err).ToNot(HaveOccurred()) + + lock, port, err := setupRegistry(nil) + if err == nil { + defer lock.Unlock() + } + Expect(err).ToNot(HaveOccurred()) + + artifactName := fmt.Sprintf("localhost:%s/test/events-artifact-remote:latest", port) + add := podmanTest.Podman([]string{"artifact", "add", artifactName, artifactFile}) + add.WaitWithDefaultTimeout() + Expect(add).Should(ExitCleanly()) + + push := podmanTest.Podman([]string{"artifact", "push", "-q", "--tls-verify=false", artifactName}) + push.WaitWithDefaultTimeout() + Expect(push).Should(ExitCleanly()) + + rm := podmanTest.Podman([]string{"artifact", "rm", artifactName}) + rm.WaitWithDefaultTimeout() + Expect(rm).Should(ExitCleanly()) + + pull := podmanTest.Podman([]string{"artifact", "pull", "-q", "--tls-verify=false", artifactName}) + pull.WaitWithDefaultTimeout() + Expect(pull).Should(ExitCleanly()) + + var events []string + Eventually(func() int { + result := podmanTest.Podman([]string{"events", "--stream=false", "--filter", "type=artifact", "--filter", "artifact=" + artifactName}) + result.WaitWithDefaultTimeout() + Expect(result).Should(ExitCleanly()) + events = result.OutputToStringArray() + return len(events) + }, defaultWaitTimeout, 2).Should(BeNumerically("==", 4), "number of artifact events") + + Expect(events).To(HaveLen(4), "number of artifact events") + Expect(events[0]).To(And(ContainSubstring("artifact create"), ContainSubstring(artifactName)), "event log includes artifact create") + Expect(events[1]).To(And(ContainSubstring("artifact push"), ContainSubstring(artifactName)), "event log includes artifact push") + Expect(events[2]).To(And(ContainSubstring("artifact remove"), ContainSubstring(artifactName)), "event log includes artifact remove") + Expect(events[3]).To(And(ContainSubstring("artifact pull"), ContainSubstring(artifactName)), "event log includes artifact pull") + }) })