agent/taskresource/cgroup/cgroup.go (285 lines of code) (raw):
//go:build linux
// +build linux
// 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 cgroup
import (
"encoding/json"
"errors"
"fmt"
"os"
"path/filepath"
"strconv"
"sync"
"time"
apicontainer "github.com/aws/amazon-ecs-agent/agent/api/container"
"github.com/aws/amazon-ecs-agent/agent/config"
"github.com/aws/amazon-ecs-agent/agent/taskresource"
"github.com/aws/amazon-ecs-agent/agent/taskresource/cgroup/control"
resourcestatus "github.com/aws/amazon-ecs-agent/agent/taskresource/status"
"github.com/aws/amazon-ecs-agent/agent/utils/ioutilwrapper"
apicontainerstatus "github.com/aws/amazon-ecs-agent/ecs-agent/api/container/status"
"github.com/aws/amazon-ecs-agent/ecs-agent/api/task/status"
"github.com/cihub/seelog"
cgroups "github.com/containerd/cgroups/v3/cgroup1"
"github.com/opencontainers/runtime-spec/specs-go"
)
const (
memorySubsystem = "/memory"
memoryUseHierarchy = "memory.use_hierarchy"
rootReadOnlyPermissions = os.FileMode(400)
resourceName = "cgroup"
resourceProvisioningError = "CgroupError: Agent could not create task's platform resources"
)
var (
enableMemoryHierarchy = []byte(strconv.Itoa(1))
)
// CgroupResource represents Cgroup resource
type CgroupResource struct {
taskARN string
control control.Control
cgroupRoot string
cgroupMountPath string
resourceSpec specs.LinuxResources
ioutil ioutilwrapper.IOUtil
createdAt time.Time
desiredStatusUnsafe resourcestatus.ResourceStatus
knownStatusUnsafe resourcestatus.ResourceStatus
// appliedStatus is the status that has been "applied" (e.g., we've called some
// operation such as 'Create' on the resource) but we don't yet know that the
// application was successful, which may then change the known status. This is
// used while progressing resource states in progressTask() of task manager
appliedStatus resourcestatus.ResourceStatus
statusToTransitions map[resourcestatus.ResourceStatus]func() error
// lock is used for fields that are accessed and updated concurrently
lock sync.RWMutex
}
// NewCgroupResource is used to return an object that implements the Resource interface
func NewCgroupResource(
taskARN string,
control control.Control,
ioutil ioutilwrapper.IOUtil,
cgroupRoot string,
cgroupMountPath string,
resourceSpec specs.LinuxResources,
) *CgroupResource {
c := &CgroupResource{
taskARN: taskARN,
control: control,
ioutil: ioutil,
cgroupRoot: cgroupRoot,
cgroupMountPath: cgroupMountPath,
resourceSpec: resourceSpec,
}
c.initializeResourceStatusToTransitionFunction()
return c
}
// GetTerminalReason returns an error string to propagate up through to task
// state change messages
func (cgroup *CgroupResource) GetTerminalReason() string {
// for cgroups we can send up a static string because this is an
// implementation detail and unrelated to customer resources
return resourceProvisioningError
}
func (cgroup *CgroupResource) initializeResourceStatusToTransitionFunction() {
resourceStatusToTransitionFunction := map[resourcestatus.ResourceStatus]func() error{
resourcestatus.ResourceStatus(CgroupCreated): cgroup.Create,
}
cgroup.statusToTransitions = resourceStatusToTransitionFunction
}
func (cgroup *CgroupResource) SetIOUtil(ioutil ioutilwrapper.IOUtil) {
cgroup.ioutil = ioutil
}
// SetDesiredStatus safely sets the desired status of the resource
func (cgroup *CgroupResource) SetDesiredStatus(status resourcestatus.ResourceStatus) {
cgroup.lock.Lock()
defer cgroup.lock.Unlock()
cgroup.desiredStatusUnsafe = status
}
// GetDesiredStatus safely returns the desired status of the task
func (cgroup *CgroupResource) GetDesiredStatus() resourcestatus.ResourceStatus {
cgroup.lock.RLock()
defer cgroup.lock.RUnlock()
return cgroup.desiredStatusUnsafe
}
// GetName safely returns the name of the resource
func (cgroup *CgroupResource) GetName() string {
cgroup.lock.RLock()
defer cgroup.lock.RUnlock()
return resourceName
}
// DesiredTerminal returns true if the cgroup's desired status is REMOVED
func (cgroup *CgroupResource) DesiredTerminal() bool {
cgroup.lock.RLock()
defer cgroup.lock.RUnlock()
return cgroup.desiredStatusUnsafe == resourcestatus.ResourceStatus(CgroupRemoved)
}
// KnownCreated returns true if the cgroup's known status is CREATED
func (cgroup *CgroupResource) KnownCreated() bool {
cgroup.lock.RLock()
defer cgroup.lock.RUnlock()
return cgroup.knownStatusUnsafe == resourcestatus.ResourceStatus(CgroupCreated)
}
// TerminalStatus returns the last transition state of cgroup
func (cgroup *CgroupResource) TerminalStatus() resourcestatus.ResourceStatus {
return resourcestatus.ResourceStatus(CgroupRemoved)
}
// NextKnownState returns the state that the resource should
// progress to based on its `KnownState`.
func (cgroup *CgroupResource) NextKnownState() resourcestatus.ResourceStatus {
return cgroup.GetKnownStatus() + 1
}
// ApplyTransition calls the function required to move to the specified status
func (cgroup *CgroupResource) ApplyTransition(nextState resourcestatus.ResourceStatus) error {
transitionFunc, ok := cgroup.statusToTransitions[nextState]
if !ok {
seelog.Errorf("Cgroup Resource [%s]: unsupported desired state transition [%s]: %s",
cgroup.taskARN, cgroup.GetName(), cgroup.StatusString(nextState))
return fmt.Errorf("resource [%s]: transition to %s impossible", cgroup.GetName(),
cgroup.StatusString(nextState))
}
return transitionFunc()
}
// SteadyState returns the transition state of the resource defined as "ready"
func (cgroup *CgroupResource) SteadyState() resourcestatus.ResourceStatus {
return resourcestatus.ResourceStatus(CgroupCreated)
}
// SetKnownStatus safely sets the currently known status of the resource
func (cgroup *CgroupResource) SetKnownStatus(status resourcestatus.ResourceStatus) {
cgroup.lock.Lock()
defer cgroup.lock.Unlock()
cgroup.knownStatusUnsafe = status
cgroup.updateAppliedStatusUnsafe(status)
}
// updateAppliedStatusUnsafe updates the resource transitioning status
func (cgroup *CgroupResource) updateAppliedStatusUnsafe(knownStatus resourcestatus.ResourceStatus) {
if cgroup.appliedStatus == resourcestatus.ResourceStatus(CgroupStatusNone) {
return
}
// Check if the resource transition has already finished
if cgroup.appliedStatus <= knownStatus {
cgroup.appliedStatus = resourcestatus.ResourceStatus(CgroupStatusNone)
}
}
// SetAppliedStatus sets the applied status of resource and returns whether
// the resource is already in a transition
func (cgroup *CgroupResource) SetAppliedStatus(status resourcestatus.ResourceStatus) bool {
cgroup.lock.Lock()
defer cgroup.lock.Unlock()
if cgroup.appliedStatus != resourcestatus.ResourceStatus(CgroupStatusNone) {
// return false to indicate the set operation failed
return false
}
cgroup.appliedStatus = status
return true
}
// GetAppliedStatus safely returns the currently applied status of the resource
func (cgroup *CgroupResource) GetAppliedStatus() resourcestatus.ResourceStatus {
cgroup.lock.RLock()
defer cgroup.lock.RUnlock()
return cgroup.appliedStatus
}
// GetKnownStatus safely returns the currently known status of the task
func (cgroup *CgroupResource) GetKnownStatus() resourcestatus.ResourceStatus {
cgroup.lock.RLock()
defer cgroup.lock.RUnlock()
return cgroup.knownStatusUnsafe
}
// StatusString returns the string of the cgroup resource status
func (cgroup *CgroupResource) StatusString(status resourcestatus.ResourceStatus) string {
return CgroupStatus(status).String()
}
// SetCreatedAt sets the timestamp for resource's creation time
func (cgroup *CgroupResource) SetCreatedAt(createdAt time.Time) {
if createdAt.IsZero() {
return
}
cgroup.lock.Lock()
defer cgroup.lock.Unlock()
cgroup.createdAt = createdAt
}
// GetCreatedAt sets the timestamp for resource's creation time
func (cgroup *CgroupResource) GetCreatedAt() time.Time {
cgroup.lock.RLock()
defer cgroup.lock.RUnlock()
return cgroup.createdAt
}
// Create creates cgroup root for the task
func (cgroup *CgroupResource) Create() error {
err := cgroup.setupTaskCgroup()
if err != nil {
// this error is already formatted in setupTaskCgroup function
return err
}
return nil
}
func (cgroup *CgroupResource) setupTaskCgroup() error {
cgroupRoot := cgroup.cgroupRoot
if cgroup.control.Exists(cgroupRoot) {
seelog.Debugf("Cgroup already exists, skipping creation taskARN=%s cgroupPath=%s cgroupV2=%v", cgroup.taskARN, cgroupRoot, config.CgroupV2)
return nil
}
cgroupSpec := control.Spec{
Root: cgroupRoot,
Specs: &cgroup.resourceSpec,
}
seelog.Infof("Creating task cgroup taskARN=%s cgroupPath=%s cgroupV2=%v", cgroup.taskARN, cgroupRoot, config.CgroupV2)
err := cgroup.control.Create(&cgroupSpec)
if err != nil {
return fmt.Errorf("cgroup resource: setup cgroup: unable to create cgroup taskARN=%s cgroupPath=%s cgroupV2=%v err=%s", cgroup.taskARN, cgroupRoot, config.CgroupV2, err)
}
if !config.CgroupV2 {
// enabling cgroup memory hierarchy by doing 'echo 1 > memory.use_hierarchy'
memoryHierarchyPath := filepath.Join(cgroup.cgroupMountPath, memorySubsystem, cgroupRoot, memoryUseHierarchy)
err = cgroup.ioutil.WriteFile(memoryHierarchyPath, enableMemoryHierarchy, rootReadOnlyPermissions)
if err != nil {
return fmt.Errorf("cgroup resource: setup cgroup: unable to set use hierarchy flag taskARN=%s cgroupPath=%s cgroupV2=%v err=%s", cgroup.taskARN, cgroupRoot, config.CgroupV2, err)
}
}
return nil
}
// Cleanup removes the cgroup root created for the task
func (cgroup *CgroupResource) Cleanup() error {
err := cgroup.control.Remove(cgroup.cgroupRoot)
// Explicitly handle cgroup deleted error
if err != nil {
if errors.Is(err, cgroups.ErrCgroupDeleted) {
seelog.Warnf("Cgroup at %s has already been removed: %v", cgroup.cgroupRoot, err)
return nil
}
return fmt.Errorf("resource: cleanup cgroup: unable to remove cgroup at %s: %w", cgroup.cgroupRoot, err)
}
return nil
}
// cgroupResourceJSON duplicates CgroupResource fields, only for marshalling and unmarshalling purposes
type cgroupResourceJSON struct {
CgroupRoot string `json:"cgroupRoot"`
CgroupMountPath string `json:"cgroupMountPath"`
CreatedAt time.Time `json:"createdAt,omitempty"`
DesiredStatus *CgroupStatus `json:"desiredStatus"`
KnownStatus *CgroupStatus `json:"knownStatus"`
LinuxSpec specs.LinuxResources `json:"resourceSpec"`
}
// MarshalJSON marshals CgroupResource object using duplicate struct CgroupResourceJSON
func (cgroup *CgroupResource) MarshalJSON() ([]byte, error) {
if cgroup == nil {
return nil, errors.New("cgroup resource is nil")
}
return json.Marshal(cgroupResourceJSON{
cgroup.cgroupRoot,
cgroup.cgroupMountPath,
cgroup.GetCreatedAt(),
func() *CgroupStatus {
desiredState := cgroup.GetDesiredStatus()
status := CgroupStatus(desiredState)
return &status
}(),
func() *CgroupStatus {
knownState := cgroup.GetKnownStatus()
status := CgroupStatus(knownState)
return &status
}(),
cgroup.resourceSpec,
})
}
// UnmarshalJSON unmarshals CgroupResource object using duplicate struct CgroupResourceJSON
func (cgroup *CgroupResource) UnmarshalJSON(b []byte) error {
temp := cgroupResourceJSON{}
if err := json.Unmarshal(b, &temp); err != nil {
return err
}
cgroup.cgroupRoot = temp.CgroupRoot
cgroup.cgroupMountPath = temp.CgroupMountPath
cgroup.resourceSpec = temp.LinuxSpec
if temp.DesiredStatus != nil {
cgroup.SetDesiredStatus(resourcestatus.ResourceStatus(*temp.DesiredStatus))
}
if temp.KnownStatus != nil {
cgroup.SetKnownStatus(resourcestatus.ResourceStatus(*temp.KnownStatus))
}
return nil
}
// GetCgroupRoot returns cgroup root of the resource
func (cgroup *CgroupResource) GetCgroupRoot() string {
cgroup.lock.RLock()
defer cgroup.lock.RUnlock()
return cgroup.cgroupRoot
}
// GetCgroupMountPath returns cgroup mount path of the resource
func (cgroup *CgroupResource) GetCgroupMountPath() string {
cgroup.lock.RLock()
defer cgroup.lock.RUnlock()
return cgroup.cgroupMountPath
}
// Initialize initializes the resource fileds in cgroup
func (cgroup *CgroupResource) Initialize(resourceFields *taskresource.ResourceFields,
taskKnownStatus status.TaskStatus,
taskDesiredStatus status.TaskStatus) {
cgroup.lock.Lock()
defer cgroup.lock.Unlock()
cgroup.initializeResourceStatusToTransitionFunction()
cgroup.ioutil = resourceFields.IOUtil
cgroup.control = resourceFields.Control
}
func (cgroup *CgroupResource) DependOnTaskNetwork() bool {
return false
}
func (cgroup *CgroupResource) BuildContainerDependency(containerName string, satisfied apicontainerstatus.ContainerStatus,
dependent resourcestatus.ResourceStatus) {
}
func (cgroup *CgroupResource) GetContainerDependencies(dependent resourcestatus.ResourceStatus) []apicontainer.ContainerDependency {
return nil
}