Skip to content

Commit

Permalink
Show Health Status events
Browse files Browse the repository at this point in the history
Previously, health status events were not being generated at all. Both
the API and `podman events` will generate health_status events.

```
{"status":"health_status","id":"ae498ac3aa6c63db8b69a37583a6eae1a9cefbdbdbeeadcf8e1d66d745f0df63","from":"localhost/healthcheck-demo:latest","Type":"container","Action":"health_status","Actor":{"ID":"ae498ac3aa6c63db8b69a37583a6eae1a9cefbdbdbeeadcf8e1d66d745f0df63","Attributes":{"containerExitCode":"0","image":"localhost/healthcheck-demo:latest","io.buildah.version":"1.26.1","maintainer":"NGINX Docker Maintainers \[email protected]\u003e","name":"healthcheck-demo"}},"scope":"local","time":1656082205,"timeNano":1656082205882271276,"HealthStatus":"healthy"}
```
```
2022-06-24 11:06:04.886238493 -0400 EDT container health_status ae498ac3aa6c63db8b69a37583a6eae1a9cefbdbdbeeadcf8e1d66d745f0df63 (image=localhost/healthcheck-demo:latest, name=healthcheck-demo, health_status=healthy, io.buildah.version=1.26.1, maintainer=NGINX Docker Maintainers <[email protected]>)
```

Signed-off-by: Jake Correnti <[email protected]>
  • Loading branch information
Jake Correnti committed Jun 27, 2022
1 parent b34cba6 commit 0c1a3b7
Show file tree
Hide file tree
Showing 9 changed files with 81 additions and 10 deletions.
21 changes: 17 additions & 4 deletions libpod/container_exec.go
Original file line number Diff line number Diff line change
Expand Up @@ -277,9 +277,13 @@ func (c *Container) ExecStart(sessionID string) error {
return c.save()
}

func (c *Container) ExecStartAndAttach(sessionID string, streams *define.AttachStreams, newSize *define.TerminalSize) error {
return c.execStartAndAttach(sessionID, streams, newSize, false)
}

// ExecStartAndAttach starts and attaches to an exec session in a container.
// newSize resizes the tty to this size before the process is started, must be nil if the exec session has no tty
func (c *Container) ExecStartAndAttach(sessionID string, streams *define.AttachStreams, newSize *define.TerminalSize) error {
func (c *Container) execStartAndAttach(sessionID string, streams *define.AttachStreams, newSize *define.TerminalSize, isHealthcheck bool) error {
if !c.batched {
c.lock.Lock()
defer c.lock.Unlock()
Expand Down Expand Up @@ -315,7 +319,12 @@ func (c *Container) ExecStartAndAttach(sessionID string, streams *define.AttachS
return err
}

c.newContainerEvent(events.Exec)
if isHealthcheck {
c.newContainerEvent(events.HealthStatus)
} else {
c.newContainerEvent(events.Exec)
}

logrus.Debugf("Successfully started exec session %s in container %s", session.ID(), c.ID())

var lastErr error
Expand Down Expand Up @@ -743,10 +752,14 @@ func (c *Container) ExecResize(sessionID string, newSize define.TerminalSize) er
return c.ociRuntime.ExecAttachResize(c, sessionID, newSize)
}

func (c *Container) Exec(config *ExecConfig, streams *define.AttachStreams, resize <-chan define.TerminalSize) (int, error) {
return c.exec(config, streams, resize, false)
}

// Exec emulates the old Libpod exec API, providing a single call to create,
// run, and remove an exec session. Returns exit code and error. Exit code is
// not guaranteed to be set sanely if error is not nil.
func (c *Container) Exec(config *ExecConfig, streams *define.AttachStreams, resize <-chan define.TerminalSize) (int, error) {
func (c *Container) exec(config *ExecConfig, streams *define.AttachStreams, resize <-chan define.TerminalSize, isHealthcheck bool) (int, error) {
sessionID, err := c.ExecCreate(config)
if err != nil {
return -1, err
Expand Down Expand Up @@ -780,7 +793,7 @@ func (c *Container) Exec(config *ExecConfig, streams *define.AttachStreams, resi
}()
}

if err := c.ExecStartAndAttach(sessionID, streams, size); err != nil {
if err := c.execStartAndAttach(sessionID, streams, size, isHealthcheck); err != nil {
return -1, err
}

Expand Down
10 changes: 10 additions & 0 deletions libpod/events.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,16 @@ func (c *Container) newContainerEvent(status events.Status) {
Attributes: c.Labels(),
}

// if the current event is a HealthStatus event, we need to get the current
// status of the container to pass to the event
if status == events.HealthStatus {
containerHealthStatus, err := c.healthCheckStatus()
if err != nil {
e.HealthStatus = fmt.Sprintf("%v", err)
}
e.HealthStatus = containerHealthStatus
}

if err := c.runtime.eventer.Write(e); err != nil {
logrus.Errorf("Unable to write pod event: %q", err)
}
Expand Down
4 changes: 4 additions & 0 deletions libpod/events/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ type Event struct {
Time time.Time
// Type of event that occurred
Type Type
// Health status of the current container
HealthStatus string `json:"health_status,omitempty"`

Details
}
Expand Down Expand Up @@ -141,6 +143,8 @@ const (
Exited Status = "died"
// Export ...
Export Status = "export"
// HealthStatus ...
HealthStatus Status = "health_status"
// History ...
History Status = "history"
// Import ...
Expand Down
4 changes: 3 additions & 1 deletion libpod/events/events.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ func (e *Event) ToHumanReadable(truncate bool) string {
}
switch e.Type {
case Container, Pod:
humanFormat = fmt.Sprintf("%s %s %s %s (image=%s, name=%s", e.Time, e.Type, e.Status, id, e.Image, e.Name)
humanFormat = fmt.Sprintf("%s %s %s %s (image=%s, name=%s, health_status=%s", e.Time, e.Type, e.Status, id, e.Image, e.Name, e.HealthStatus)
// check if the container has labels and add it to the output
if len(e.Attributes) > 0 {
for k, v := range e.Attributes {
Expand Down Expand Up @@ -168,6 +168,8 @@ func StringToStatus(name string) (Status, error) {
return Exited, nil
case Export.String():
return Export, nil
case HealthStatus.String():
return HealthStatus, nil
case History.String():
return History, nil
case Import.String():
Expand Down
2 changes: 2 additions & 0 deletions libpod/events/journal_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ func (e EventJournalD) Write(ee Event) error {
}
m["PODMAN_LABELS"] = string(b)
}
m["PODMAN_HEALTH_STATUS"] = ee.HealthStatus
case Network:
m["PODMAN_ID"] = ee.ID
m["PODMAN_NETWORK_NAME"] = ee.Network
Expand Down Expand Up @@ -214,6 +215,7 @@ func newEventFromJournalEntry(entry *sdjournal.JournalEntry) (*Event, error) { /
newEvent.Details = Details{Attributes: labels}
}
}
newEvent.HealthStatus = entry.Fields["PODMAN_HEALTH_STATUS"]
case Network:
newEvent.ID = entry.Fields["PODMAN_ID"]
newEvent.Network = entry.Fields["PODMAN_NETWORK_NAME"]
Expand Down
15 changes: 12 additions & 3 deletions libpod/healthcheck.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ func (c *Container) runHealthCheck() (define.HealthCheckStatus, error) {
hcResult := define.HealthCheckSuccess
config := new(ExecConfig)
config.Command = newCommand
exitCode, hcErr := c.Exec(config, streams, nil)
exitCode, hcErr := c.exec(config, streams, nil, true)
if hcErr != nil {
errCause := errors.Cause(hcErr)
hcResult = define.HealthCheckFailure
Expand Down Expand Up @@ -232,18 +232,27 @@ func (c *Container) getHealthCheckLog() (define.HealthCheckResults, error) {

// HealthCheckStatus returns the current state of a container with a healthcheck
func (c *Container) HealthCheckStatus() (string, error) {
c.lock.Lock()
defer c.lock.Unlock()
return c.healthCheckStatus()
}

// Internal function to return the current state of a container with a healthcheck.
// This function does not lock the container.
func (c *Container) healthCheckStatus() (string, error) {
if !c.HasHealthCheck() {
return "", errors.Errorf("container %s has no defined healthcheck", c.ID())
}
c.lock.Lock()
defer c.lock.Unlock()

if err := c.syncContainer(); err != nil {
return "", err
}

results, err := c.getHealthCheckLog()
if err != nil {
return "", errors.Wrapf(err, "unable to get healthcheck log for %s", c.ID())
}

return results.Status, nil
}

Expand Down
10 changes: 8 additions & 2 deletions pkg/domain/entities/events.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ type Event struct {
// TODO: it would be nice to have full control over the types at some
// point and fork such Docker types.
dockerEvents.Message
HealthStatus string
}

// ConvertToLibpodEvent converts an entities event to a libpod one.
Expand Down Expand Up @@ -44,6 +45,7 @@ func ConvertToLibpodEvent(e Event) *libpodEvents.Event {
Status: status,
Time: time.Unix(0, e.TimeNano),
Type: t,
HealthStatus: e.HealthStatus,
Details: libpodEvents.Details{
Attributes: details,
},
Expand All @@ -59,7 +61,7 @@ func ConvertToEntitiesEvent(e libpodEvents.Event) *Event {
attributes["image"] = e.Image
attributes["name"] = e.Name
attributes["containerExitCode"] = strconv.Itoa(e.ContainerExitCode)
return &Event{dockerEvents.Message{
message := dockerEvents.Message{
// Compatibility with clients that still look for deprecated API elements
Status: e.Status.String(),
ID: e.ID,
Expand All @@ -73,5 +75,9 @@ func ConvertToEntitiesEvent(e libpodEvents.Event) *Event {
Scope: "local",
Time: e.Time.Unix(),
TimeNano: e.Time.UnixNano(),
}}
}
return &Event{
message,
e.HealthStatus,
}
}
4 changes: 4 additions & 0 deletions test/apiv2/27-containersEvents.at
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ t GET "libpod/events?stream=false&since=$START" 200 \
'select(.status | contains("died")).Action=died' \
'select(.status | contains("died")).Actor.Attributes.containerExitCode=1'

t GET "libpod/events?stream=false&since=$START" 200 \
'select(.status | contains("start")).Action=start' \
'select(.status | contains("start")).HealthStatus='\

# compat api, uses status=die (#12643)
t GET "events?stream=false&since=$START" 200 \
'select(.status | contains("start")).Action=start' \
Expand Down
21 changes: 21 additions & 0 deletions test/e2e/events_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -216,4 +216,25 @@ var _ = Describe("Podman events", func() {
Expect(result.OutputToString()).To(ContainSubstring("create"))
})

It("podman events health_status generated", func() {
session := podmanTest.Podman([]string{"run", "--name", "test-hc", "-dt", "--health-cmd", "echo working", "busybox"})
session.WaitWithDefaultTimeout()
Expect(session).Should(Exit(0))

for i := 0; i < 5; i++ {
hc := podmanTest.Podman([]string{"healthcheck", "run", "test-hc"})
hc.WaitWithDefaultTimeout()
exitCode := hc.ExitCode()
if exitCode == 0 || i == 4 {
break
}
time.Sleep(1 * time.Second)
}

result := podmanTest.Podman([]string{"events", "--stream=false", "--filter", "event=health_status"})
result.WaitWithDefaultTimeout()
Expect(result).Should(Exit(0))
Expect(len(result.OutputToStringArray())).To(BeNumerically(">=", 1), "Number of health_status events")
})

})

0 comments on commit 0c1a3b7

Please sign in to comment.