// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"). You may // not use this file except in compliance with the License. A copy of the // License is located at // // http://aws.amazon.com/apache2.0/ // // or in the "license" file accompanying this file. This file is distributed // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either // express or implied. See the License for the specific language governing // permissions and limitations under the License. package api import ( "fmt" "strconv" "time" apicontainer "github.com/aws/amazon-ecs-agent/agent/api/container" apicontainerstatus "github.com/aws/amazon-ecs-agent/agent/api/container/status" apitask "github.com/aws/amazon-ecs-agent/agent/api/task" apitaskstatus "github.com/aws/amazon-ecs-agent/agent/api/task/status" "github.com/aws/amazon-ecs-agent/agent/statechange" apieni "github.com/aws/amazon-ecs-agent/ecs-agent/api/eni" "github.com/aws/amazon-ecs-agent/ecs-agent/logger" "github.com/pkg/errors" "github.com/aws/aws-sdk-go/aws" ) // ContainerStateChange represents a state change that needs to be sent to the // SubmitContainerStateChange API type ContainerStateChange struct { // TaskArn is the unique identifier for the task TaskArn string // RuntimeID is the dockerID of the container RuntimeID string // ContainerName is the name of the container ContainerName string // Status is the status to send Status apicontainerstatus.ContainerStatus // ImageDigest is the sha-256 digest of the container image as pulled from the repository ImageDigest string // Reason may contain details of why the container stopped Reason string // ExitCode is the exit code of the container, if available ExitCode *int // PortBindings are the details of the host ports picked for the specified // container ports PortBindings []apicontainer.PortBinding // Container is a pointer to the container involved in the state change that gives the event handler a hook into // storing what status was sent. This is used to ensure the same event is handled only once. Container *apicontainer.Container } type ManagedAgentStateChange struct { // TaskArn is the unique identifier for the task TaskArn string // Name is the name of the managed agent Name string // Container is a pointer to the container involved in the state change that gives the event handler a hook into // storing what status was sent. This is used to ensure the same event is handled only once. Container *apicontainer.Container // Status is the status of the managed agent Status apicontainerstatus.ManagedAgentStatus // Reason indicates an error in a managed agent state chage Reason string } // TaskStateChange represents a state change that needs to be sent to the // SubmitTaskStateChange API type TaskStateChange struct { // Attachment is the eni attachment object to send Attachment *apieni.ENIAttachment // TaskArn is the unique identifier for the task TaskARN string // Status is the status to send Status apitaskstatus.TaskStatus // Reason may contain details of why the task stopped Reason string // Containers holds the events generated by containers owned by this task Containers []ContainerStateChange // ManagedAgents contain the name and status of Agents running inside the container ManagedAgents []ManagedAgentStateChange // PullStartedAt is the timestamp when the task start pulling PullStartedAt *time.Time // PullStoppedAt is the timestamp when the task finished pulling PullStoppedAt *time.Time // ExecutionStoppedAt is the timestamp when the essential container stopped ExecutionStoppedAt *time.Time // Task is a pointer to the task involved in the state change that gives the event handler a hook into storing // what status was sent. This is used to ensure the same event is handled only once. Task *apitask.Task } // AttachmentStateChange represents a state change that needs to be sent to the // SubmitAttachmentStateChanges API type AttachmentStateChange struct { // Attachment is the eni attachment object to send Attachment *apieni.ENIAttachment } type ErrShouldNotSendEvent struct { resourceId string } func (e ErrShouldNotSendEvent) Error() string { return fmt.Sprintf("should not send events for internal tasks or containers: %s", e.resourceId) } // NewTaskStateChangeEvent creates a new task state change event // returns error if the state change doesn't need to be sent to the ECS backend. func NewTaskStateChangeEvent(task *apitask.Task, reason string) (TaskStateChange, error) { var event TaskStateChange if task.IsInternal { return event, ErrShouldNotSendEvent{task.Arn} } taskKnownStatus := task.GetKnownStatus() if !taskKnownStatus.BackendRecognized() { return event, errors.Errorf( "create task state change event api: status not recognized by ECS: %v", taskKnownStatus) } if task.GetSentStatus() >= taskKnownStatus { return event, errors.Errorf( "create task state change event api: status [%s] already sent", taskKnownStatus.String()) } event = TaskStateChange{ TaskARN: task.Arn, Status: taskKnownStatus, Reason: reason, Task: task, } event.SetTaskTimestamps() return event, nil } // NewContainerStateChangeEvent creates a new container state change event // returns error if the state change doesn't need to be sent to the ECS backend. func NewContainerStateChangeEvent(task *apitask.Task, cont *apicontainer.Container, reason string) (ContainerStateChange, error) { event, err := newUncheckedContainerStateChangeEvent(task, cont, reason) if err != nil { return event, err } contKnownStatus := cont.GetKnownStatus() if !contKnownStatus.ShouldReportToBackend(cont.GetSteadyStateStatus()) { return event, ErrShouldNotSendEvent{fmt.Sprintf( "create container state change event api: status not recognized by ECS: %v", contKnownStatus)} } if cont.GetSentStatus() >= contKnownStatus { return event, ErrShouldNotSendEvent{fmt.Sprintf( "create container state change event api: status [%s] already sent for container %s, task %s", contKnownStatus.String(), cont.Name, task.Arn)} } if reason == "" && cont.ApplyingError != nil { reason = cont.ApplyingError.Error() event.Reason = reason } return event, nil } func newUncheckedContainerStateChangeEvent(task *apitask.Task, cont *apicontainer.Container, reason string) (ContainerStateChange, error) { var event ContainerStateChange if cont.IsInternal() { return event, ErrShouldNotSendEvent{cont.Name} } portBindings := cont.GetKnownPortBindings() if task.IsServiceConnectEnabled() && task.IsNetworkModeBridge() { pauseCont, err := task.GetBridgeModePauseContainerForTaskContainer(cont) if err != nil { return event, fmt.Errorf("error resolving pause container for bridge mode SC container: %s", cont.Name) } portBindings = pauseCont.GetKnownPortBindings() } contKnownStatus := cont.GetKnownStatus() event = ContainerStateChange{ TaskArn: task.Arn, ContainerName: cont.Name, RuntimeID: cont.GetRuntimeID(), Status: contKnownStatus.BackendStatus(cont.GetSteadyStateStatus()), ExitCode: cont.GetKnownExitCode(), PortBindings: portBindings, ImageDigest: cont.GetImageDigest(), Reason: reason, Container: cont, } return event, nil } // NewManagedAgentChangeEvent creates a new managedAgent change event to convey managed agent state changes // returns error if the state change doesn't need to be sent to the ECS backend. func NewManagedAgentChangeEvent(task *apitask.Task, cont *apicontainer.Container, managedAgentName string, reason string) (ManagedAgentStateChange, error) { var event = ManagedAgentStateChange{} managedAgent, ok := cont.GetManagedAgentByName(managedAgentName) if !ok { return event, errors.Errorf("No ExecuteCommandAgent available in container: %v", cont.Name) } if !managedAgent.Status.ShouldReportToBackend() { return event, errors.Errorf("create managed agent state change event: status not recognized by ECS: %v", managedAgent.Status) } event = ManagedAgentStateChange{ TaskArn: task.Arn, Name: managedAgent.Name, Container: cont, Status: managedAgent.Status, Reason: reason, } return event, nil } // NewAttachmentStateChangeEvent creates a new attachment state change event func NewAttachmentStateChangeEvent(eniAttachment *apieni.ENIAttachment) AttachmentStateChange { return AttachmentStateChange{ Attachment: eniAttachment, } } func (c *ContainerStateChange) ToFields() logger.Fields { return logger.Fields{ "eventType": "ContainerStateChange", "taskArn": c.TaskArn, "containerName": c.ContainerName, "containerStatus": c.Status.String(), "exitCode": strconv.Itoa(*c.ExitCode), "reason": c.Reason, "portBindings": c.PortBindings, } } // String returns a human readable string representation of this object func (c *ContainerStateChange) String() string { res := fmt.Sprintf("containerName=%s containerStatus=%s", c.ContainerName, c.Status.String()) if c.ExitCode != nil { res += " containerExitCode=" + strconv.Itoa(*c.ExitCode) } if c.Reason != "" { res += " containerReason=" + c.Reason } if len(c.PortBindings) != 0 { res += fmt.Sprintf(" containerPortBindings=%v", c.PortBindings) } if c.Container != nil { res += fmt.Sprintf(" containerKnownSentStatus=%s containerRuntimeID=%s containerIsEssential=%v", c.Container.GetSentStatus().String(), c.Container.GetRuntimeID(), c.Container.IsEssential()) } return res } // String returns a human readable string representation of ManagedAgentStateChange func (m *ManagedAgentStateChange) String() string { res := fmt.Sprintf("containerName=%s managedAgentName=%s managedAgentStatus=%s", m.Container.Name, m.Name, m.Status.String()) if m.Reason != "" { res += " managedAgentReason=" + m.Reason } return res } // SetTaskTimestamps adds the timestamp information of task into the event // to be sent by SubmitTaskStateChange func (change *TaskStateChange) SetTaskTimestamps() { if change.Task == nil { return } // Send the task timestamp if set if timestamp := change.Task.GetPullStartedAt(); !timestamp.IsZero() { change.PullStartedAt = aws.Time(timestamp.UTC()) } if timestamp := change.Task.GetPullStoppedAt(); !timestamp.IsZero() { change.PullStoppedAt = aws.Time(timestamp.UTC()) } if timestamp := change.Task.GetExecutionStoppedAt(); !timestamp.IsZero() { change.ExecutionStoppedAt = aws.Time(timestamp.UTC()) } } // ShouldBeReported checks if the statechange should be reported to backend func (change *TaskStateChange) ShouldBeReported() bool { // Events that should be reported: // 1. Normal task state change: RUNNING/STOPPED // 2. Container state change, with task status in CREATED/RUNNING/STOPPED // The task timestamp will be sent in both of the event type // TODO Move the Attachment statechange check into this method if change.Status == apitaskstatus.TaskRunning || change.Status == apitaskstatus.TaskStopped { return true } if len(change.Containers) != 0 { return true } return false } func (change *TaskStateChange) ToFields() logger.Fields { fields := logger.Fields{ "eventType": "TaskStateChange", "taskArn": change.TaskARN, "taskStatus": change.Status.String(), "taskReason": change.Reason, } if change.Task != nil { fields["taskKnownSentStatus"] = change.Task.GetSentStatus().String() fields["taskPullStartedAt"] = change.Task.GetPullStartedAt().UTC().Format(time.RFC3339) fields["taskPullStoppedAt"] = change.Task.GetPullStoppedAt().UTC().Format(time.RFC3339) fields["taskExecutionStoppedAt"] = change.Task.GetExecutionStoppedAt().UTC().Format(time.RFC3339) } if change.Attachment != nil { fields["eniAttachment"] = change.Attachment.String() } for i, containerChange := range change.Containers { fields["containerChange-"+strconv.Itoa(i)] = containerChange.String() } for i, managedAgentChange := range change.ManagedAgents { fields["managedAgentChange-"+strconv.Itoa(i)] = managedAgentChange.String() } return fields } // String returns a human readable string representation of this object func (change *TaskStateChange) String() string { res := fmt.Sprintf("%s -> %s", change.TaskARN, change.Status.String()) if change.Task != nil { res += fmt.Sprintf(", Known Sent: %s, PullStartedAt: %s, PullStoppedAt: %s, ExecutionStoppedAt: %s", change.Task.GetSentStatus().String(), change.Task.GetPullStartedAt(), change.Task.GetPullStoppedAt(), change.Task.GetExecutionStoppedAt()) } if change.Attachment != nil { res += ", " + change.Attachment.String() } for _, containerChange := range change.Containers { res += ", container change: " + containerChange.String() } for _, managedAgentChange := range change.ManagedAgents { res += ", managed agent: " + managedAgentChange.String() } return res } // String returns a human readable string representation of this object func (change *AttachmentStateChange) String() string { if change.Attachment != nil { return fmt.Sprintf("%s -> %s, %s", change.Attachment.AttachmentARN, change.Attachment.Status.String(), change.Attachment.String()) } return "" } // GetEventType returns an enum identifying the event type func (ContainerStateChange) GetEventType() statechange.EventType { return statechange.ContainerEvent } func (ms ManagedAgentStateChange) GetEventType() statechange.EventType { return statechange.ManagedAgentEvent } // GetEventType returns an enum identifying the event type func (ts TaskStateChange) GetEventType() statechange.EventType { return statechange.TaskEvent } // GetEventType returns an enum identifying the event type func (AttachmentStateChange) GetEventType() statechange.EventType { return statechange.AttachmentEvent }