Files
portainer/api/containerautomation/notify.go
T
agent_coder 2f939203f7 fix(#28 review): restore stack-name producer for container notifications (F3)
F3: removing the stack-redeploy path dropped every producer of Event.StackName,
leaving the webhook stack-notification branch + 3 formatter tests dead and silently
regressing recreated stack members to 'Container [x]' instead of 'Stack [name]'
(#19 behaviour). Thread the compose-project label (consts.ComposeStackNameLabel)
into Event.StackName at every member event: updateStandalone (recreate success +
failure), rollback (its 3 events, via a new stackName param), and autoheal
(EventHealRestarted). Empty label -> StackName='' -> formatter falls back to the
Container branch, so non-stack containers are unregressed. The recreate regression
test now asserts StackName is populated end-to-end (live producer, not hand-set).
F-doc: reworded 3 stale redeploy comments (notify.go, service.go, seams.go).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 22:05:56 +03:00

117 lines
4.3 KiB
Go

package containerautomation
import "github.com/rs/zerolog/log"
// EventKind enumerates the container-automation events surfaced to a Notifier.
// The set is intentionally small: it is the seam future milestones extend with
// real senders (Slack/email/webhook) without touching the daemon call sites.
type EventKind string
const (
// EventUpdated is emitted after a container/stack image was updated.
EventUpdated EventKind = "updated"
// EventRollback is emitted after a health-gated rollback to the previous image.
EventRollback EventKind = "rollback"
// EventUpdateFailed is emitted when an update (or its rollback) could not be applied.
EventUpdateFailed EventKind = "update-failed"
// EventHealRestarted is emitted after an unhealthy container was restarted.
EventHealRestarted EventKind = "heal-restarted"
)
// Event is a structured container-automation notification. Optional fields are
// left zero when not applicable to the event (e.g. StackName for a standalone
// container that is not a compose stack member, or OldDigest/NewDigest when the
// pre/post image identity could not be resolved). Every event is per-container:
// the update path recreates each container individually, so there is no
// whole-stack redeploy event.
type Event struct {
Kind EventKind
EndpointID int
ContainerID string
// ContainerName is the human-readable container name (no leading slash), used
// by the webhook message. It may be empty for events keyed only by ID.
ContainerName string
StackID int
// StackName is the compose project (stack) name a container belongs to, sourced
// from its com.docker.compose.project label at detection time. It is set on a
// per-container update event for a stack member so the webhook can print a
// "Stack [name]" line without a StackID/Stack().Read round-trip; empty for
// standalone containers.
StackName string
Image string
// OldDigest and NewDigest carry the pre/post image identities for an update
// (image IDs, e.g. "sha256:59b9..."). They are threaded from the update call
// site where they are known and left empty otherwise; the webhook notifier
// short-forms them into the "old → new" part of the message.
OldDigest string
NewDigest string
Message string
// Err carries the underlying error for failure events; nil otherwise.
Err error
}
// Notifier receives container-automation events. CE has no generic notification
// subsystem, so the only implementation is logNotifier; this interface is the
// seam external senders plug into later.
type Notifier interface {
Notify(event Event)
}
// logNotifier is the default Notifier: it emits each event as a structured log
// line. It never blocks and never errors, so it is safe to call from the daemon
// hot path.
type logNotifier struct{}
// Notify logs the event with its kind and context fields. Failure events are
// logged at warn (with the error), the rest at info.
func (logNotifier) Notify(event Event) {
entry := log.Info()
if event.Kind == EventUpdateFailed {
entry = log.Warn()
if event.Err != nil {
entry = entry.Err(event.Err)
}
}
entry = entry.Str("event", string(event.Kind)).Int("endpoint_id", event.EndpointID)
if event.ContainerID != "" {
entry = entry.Str("container_id", event.ContainerID)
}
if event.StackID != 0 {
entry = entry.Int("stack_id", event.StackID)
}
if event.Image != "" {
entry = entry.Str("image", event.Image)
}
message := event.Message
if message == "" {
message = "container automation event"
}
entry.Msg("container automation: " + message)
}
// multiNotifier fans an event out to several notifiers in order. It is how the
// service composes the always-on logNotifier with the optional webhookNotifier
// without either implementation having to know about the other. Each notifier is
// itself non-blocking, so multiNotifier stays safe on the daemon hot path.
type multiNotifier []Notifier
// Notify forwards the event to every wrapped notifier. Each call is isolated by
// a recover() so one misbehaving notifier can neither abort the others nor let a
// panic reach the daemon hot path; logNotifier is kept first and unchanged.
func (m multiNotifier) Notify(event Event) {
for _, n := range m {
func() {
defer func() {
if r := recover(); r != nil {
log.Warn().Interface("panic", r).Msg("container automation: recovered from panic in notifier")
}
}()
n.Notify(event)
}()
}
}