agent/taskresource/asmauth/asmauth.go (301 lines of code) (raw):
// 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 asmauth
import (
"encoding/json"
"sync"
"time"
apicontainer "github.com/aws/amazon-ecs-agent/agent/api/container"
"github.com/aws/amazon-ecs-agent/agent/asm"
"github.com/aws/amazon-ecs-agent/agent/asm/factory"
"github.com/aws/amazon-ecs-agent/agent/taskresource"
resourcestatus "github.com/aws/amazon-ecs-agent/agent/taskresource/status"
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/aws/amazon-ecs-agent/ecs-agent/credentials"
"github.com/docker/docker/api/types/registry"
"github.com/cihub/seelog"
"github.com/pkg/errors"
)
const (
// ResourceName is the name of the ASM auth resource
ResourceName = "asm-auth"
)
// ASMAuthResource represents private registry credentials as a task resource.
// These credentials are stored in AWS Secrets Manager
type ASMAuthResource struct {
taskARN string
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
resourceStatusToTransitionFunction map[resourcestatus.ResourceStatus]func() error
credentialsManager credentials.Manager
executionCredentialsID string
// required for asm private registry auth
requiredASMResources []*apicontainer.ASMAuthData
dockerAuthData map[string]registry.AuthConfig
// asmClientCreator is a factory interface that creates new ASM clients. This is
// needed mostly for testing as we're creating an asm client per every item in
// the requiredASMResources list. Each of these items could be from different
// regions.
// TODO: Refactor this struct so that each ASMAuthData gets associated with
// exactly one ASMAuthResource object
asmClientCreator factory.ClientCreator
// terminalReason should be set for resource creation failures. This ensures
// the resource object carries some context for why provisoning failed.
terminalReason string
terminalReasonOnce sync.Once
// lock is used for fields that are accessed and updated concurrently
lock sync.RWMutex
}
// NewASMAuthResource creates a new ASMAuthResource object
func NewASMAuthResource(taskARN string,
asmRequirements []*apicontainer.ASMAuthData,
executionCredentialsID string,
credentialsManager credentials.Manager,
asmClientCreator factory.ClientCreator) *ASMAuthResource {
c := &ASMAuthResource{
taskARN: taskARN,
requiredASMResources: asmRequirements,
credentialsManager: credentialsManager,
executionCredentialsID: executionCredentialsID,
asmClientCreator: asmClientCreator,
}
c.initStatusToTransition()
return c
}
func (auth *ASMAuthResource) initStatusToTransition() {
resourceStatusToTransitionFunction := map[resourcestatus.ResourceStatus]func() error{
resourcestatus.ResourceStatus(ASMAuthStatusCreated): auth.Create,
}
auth.resourceStatusToTransitionFunction = resourceStatusToTransitionFunction
}
func (auth *ASMAuthResource) setTerminalReason(reason string) {
auth.terminalReasonOnce.Do(func() {
seelog.Infof("ASM Auth: setting terminal reason for asm auth resource in task: [%s]", auth.taskARN)
auth.terminalReason = reason
})
}
// GetTerminalReason returns an error string to propagate up through to task
// state change messages
func (auth *ASMAuthResource) GetTerminalReason() string {
return auth.terminalReason
}
// SetDesiredStatus safely sets the desired status of the resource
func (auth *ASMAuthResource) SetDesiredStatus(status resourcestatus.ResourceStatus) {
auth.lock.Lock()
defer auth.lock.Unlock()
auth.desiredStatusUnsafe = status
}
// GetDesiredStatus safely returns the desired status of the task
func (auth *ASMAuthResource) GetDesiredStatus() resourcestatus.ResourceStatus {
auth.lock.RLock()
defer auth.lock.RUnlock()
return auth.desiredStatusUnsafe
}
// GetName safely returns the name of the resource
func (auth *ASMAuthResource) GetName() string {
auth.lock.RLock()
defer auth.lock.RUnlock()
return ResourceName
}
// DesiredTerminal returns true if the cgroup's desired status is REMOVED
func (auth *ASMAuthResource) DesiredTerminal() bool {
auth.lock.RLock()
defer auth.lock.RUnlock()
return auth.desiredStatusUnsafe == resourcestatus.ResourceStatus(ASMAuthStatusRemoved)
}
// KnownCreated returns true if the cgroup's known status is CREATED
func (auth *ASMAuthResource) KnownCreated() bool {
auth.lock.RLock()
defer auth.lock.RUnlock()
return auth.knownStatusUnsafe == resourcestatus.ResourceStatus(ASMAuthStatusCreated)
}
// TerminalStatus returns the last transition state of cgroup
func (auth *ASMAuthResource) TerminalStatus() resourcestatus.ResourceStatus {
return resourcestatus.ResourceStatus(ASMAuthStatusRemoved)
}
// NextKnownState returns the state that the resource should
// progress to based on its `KnownState`.
func (auth *ASMAuthResource) NextKnownState() resourcestatus.ResourceStatus {
return auth.GetKnownStatus() + 1
}
// ApplyTransition calls the function required to move to the specified status
func (auth *ASMAuthResource) ApplyTransition(nextState resourcestatus.ResourceStatus) error {
transitionFunc, ok := auth.resourceStatusToTransitionFunction[nextState]
if !ok {
return errors.Errorf("resource [%s]: transition to %s impossible", auth.GetName(),
auth.StatusString(nextState))
}
return transitionFunc()
}
// SteadyState returns the transition state of the resource defined as "ready"
func (auth *ASMAuthResource) SteadyState() resourcestatus.ResourceStatus {
return resourcestatus.ResourceStatus(ASMAuthStatusCreated)
}
// SetKnownStatus safely sets the currently known status of the resource
func (auth *ASMAuthResource) SetKnownStatus(status resourcestatus.ResourceStatus) {
auth.lock.Lock()
defer auth.lock.Unlock()
auth.knownStatusUnsafe = status
auth.updateAppliedStatusUnsafe(status)
}
// updateAppliedStatusUnsafe updates the resource transitioning status
func (auth *ASMAuthResource) updateAppliedStatusUnsafe(knownStatus resourcestatus.ResourceStatus) {
if auth.appliedStatus == resourcestatus.ResourceStatus(ASMAuthStatusNone) {
return
}
// Check if the resource transition has already finished
if auth.appliedStatus <= knownStatus {
auth.appliedStatus = resourcestatus.ResourceStatus(ASMAuthStatusNone)
}
}
// SetAppliedStatus sets the applied status of resource and returns whether
// the resource is already in a transition
func (auth *ASMAuthResource) SetAppliedStatus(status resourcestatus.ResourceStatus) bool {
auth.lock.Lock()
defer auth.lock.Unlock()
if auth.appliedStatus != resourcestatus.ResourceStatus(ASMAuthStatusNone) {
// return false to indicate the set operation failed
return false
}
auth.appliedStatus = status
return true
}
// GetKnownStatus safely returns the currently known status of the task
func (auth *ASMAuthResource) GetKnownStatus() resourcestatus.ResourceStatus {
auth.lock.RLock()
defer auth.lock.RUnlock()
return auth.knownStatusUnsafe
}
// StatusString returns the string of the cgroup resource status
func (auth *ASMAuthResource) StatusString(status resourcestatus.ResourceStatus) string {
return ASMAuthStatus(status).String()
}
// SetCreatedAt sets the timestamp for resource's creation time
func (auth *ASMAuthResource) SetCreatedAt(createdAt time.Time) {
if createdAt.IsZero() {
return
}
auth.lock.Lock()
defer auth.lock.Unlock()
auth.createdAt = createdAt
}
// GetCreatedAt sets the timestamp for resource's creation time
func (auth *ASMAuthResource) GetCreatedAt() time.Time {
auth.lock.RLock()
defer auth.lock.RUnlock()
return auth.createdAt
}
// Create fetches credentials from ASM
func (auth *ASMAuthResource) Create() error {
seelog.Infof("ASM Auth: Retrieving credentials for containers in task: [%s]", auth.taskARN)
if auth.dockerAuthData == nil {
auth.dockerAuthData = make(map[string]registry.AuthConfig)
}
for _, a := range auth.GetRequiredASMResources() {
err := auth.retrieveASMDockerAuthData(a)
if err != nil {
auth.setTerminalReason(err.Error())
return err
}
}
return nil
}
func (auth *ASMAuthResource) retrieveASMDockerAuthData(asmAuthData *apicontainer.ASMAuthData) error {
secretID := asmAuthData.CredentialsParameter
if _, ok := auth.GetASMDockerAuthConfig(secretID); ok {
// resource for this secretID already retrieved
return nil
}
executionCredentials, ok := auth.credentialsManager.GetTaskCredentials(auth.GetExecutionCredentialsID())
if !ok {
// No need to log here. managedTask.applyResourceState already does that
return errors.New("asm resource: unable to find execution role credentials")
}
iamCredentials := executionCredentials.GetIAMRoleCredentials()
asmClient, err := auth.asmClientCreator.NewASMClient(asmAuthData.Region, iamCredentials)
if err != nil {
return errors.Errorf("unable to create ASM client: %v", err)
}
seelog.Debugf("ASM Auth: Retrieving resource with ID [%s] in task: [%s]", secretID, auth.taskARN)
dac, err := asm.GetDockerAuthFromASM(secretID, asmClient)
if err != nil {
return err
}
auth.lock.Lock()
defer auth.lock.Unlock()
// put retrieved dac in dockerAuthMap
auth.dockerAuthData[secretID] = dac
return nil
}
// GetRequiredASMResources returns the list of ASMAuthData that has to be
// retrieved from AWS Secrets Manager
func (auth *ASMAuthResource) GetRequiredASMResources() []*apicontainer.ASMAuthData {
auth.lock.RLock()
defer auth.lock.RUnlock()
return auth.requiredASMResources
}
// GetExecutionCredentialsID returns the execution role's credential ID
func (auth *ASMAuthResource) GetExecutionCredentialsID() string {
auth.lock.RLock()
defer auth.lock.RUnlock()
return auth.executionCredentialsID
}
// Cleanup removes the asm auth resource created for the task
func (auth *ASMAuthResource) Cleanup() error {
auth.clearASMDockerAuthConfig()
return nil
}
// clearASMDockerAuthConfig cycles through the collection of docker private
// registry auth data and removes them from the task
func (auth *ASMAuthResource) clearASMDockerAuthConfig() {
auth.lock.Lock()
defer auth.lock.Unlock()
for k := range auth.dockerAuthData {
delete(auth.dockerAuthData, k)
}
}
// GetASMDockerAuthConfig retrieves the docker private registry auth data from
// the task
func (auth *ASMAuthResource) GetASMDockerAuthConfig(secretID string) (registry.AuthConfig, bool) {
auth.lock.RLock()
defer auth.lock.RUnlock()
d, ok := auth.dockerAuthData[secretID]
return d, ok
}
// Stores provided docker auth config against the provided secret ID.
func (auth *ASMAuthResource) PutASMDockerAuthConfig(secretID string, authCfg registry.AuthConfig) {
auth.lock.Lock()
defer auth.lock.Unlock()
if auth.dockerAuthData == nil {
auth.dockerAuthData = make(map[string]registry.AuthConfig)
}
auth.dockerAuthData[secretID] = authCfg
}
func (auth *ASMAuthResource) Initialize(resourceFields *taskresource.ResourceFields,
taskKnownStatus status.TaskStatus,
taskDesiredStatus status.TaskStatus) {
auth.initStatusToTransition()
auth.credentialsManager = resourceFields.CredentialsManager
auth.asmClientCreator = resourceFields.ASMClientCreator
if taskKnownStatus < status.TaskPulled && // Containers in the task need to be pulled
taskDesiredStatus <= status.TaskRunning { // and the task is not terminal.
// Reset the ASM resource's known status as None so that the NONE -> CREATED
// transition gets triggered
auth.SetKnownStatus(resourcestatus.ResourceStatusNone)
}
}
type asmAuthResourceJSON struct {
TaskARN string `json:"taskARN"`
CreatedAt *time.Time `json:"createdAt,omitempty"`
DesiredStatus *ASMAuthStatus `json:"desiredStatus"`
KnownStatus *ASMAuthStatus `json:"knownStatus"`
RequiredASMResources []*apicontainer.ASMAuthData `json:"asmResources"`
ExecutionCredentialsID string `json:"executionCredentialsID"`
}
// MarshalJSON serialises the ASMAuthResource struct to JSON
func (auth *ASMAuthResource) MarshalJSON() ([]byte, error) {
if auth == nil {
return nil, errors.New("asm-auth resource is nil")
}
createdAt := auth.GetCreatedAt()
return json.Marshal(asmAuthResourceJSON{
TaskARN: auth.taskARN,
CreatedAt: &createdAt,
DesiredStatus: func() *ASMAuthStatus {
desiredState := auth.GetDesiredStatus()
status := ASMAuthStatus(desiredState)
return &status
}(),
KnownStatus: func() *ASMAuthStatus {
knownState := auth.GetKnownStatus()
status := ASMAuthStatus(knownState)
return &status
}(),
RequiredASMResources: auth.GetRequiredASMResources(),
ExecutionCredentialsID: auth.GetExecutionCredentialsID(),
})
}
// UnmarshalJSON deserialises the raw JSON to a ASMAuthResource struct
func (auth *ASMAuthResource) UnmarshalJSON(b []byte) error {
temp := asmAuthResourceJSON{}
if err := json.Unmarshal(b, &temp); err != nil {
return err
}
if temp.DesiredStatus != nil {
auth.SetDesiredStatus(resourcestatus.ResourceStatus(*temp.DesiredStatus))
}
if temp.KnownStatus != nil {
auth.SetKnownStatus(resourcestatus.ResourceStatus(*temp.KnownStatus))
}
if temp.CreatedAt != nil && !temp.CreatedAt.IsZero() {
auth.SetCreatedAt(*temp.CreatedAt)
}
if temp.RequiredASMResources != nil {
auth.requiredASMResources = temp.RequiredASMResources
}
auth.taskARN = temp.TaskARN
auth.executionCredentialsID = temp.ExecutionCredentialsID
return nil
}
// GetAppliedStatus safely returns the currently applied status of the resource
func (auth *ASMAuthResource) GetAppliedStatus() resourcestatus.ResourceStatus {
auth.lock.RLock()
defer auth.lock.RUnlock()
return auth.appliedStatus
}
func (auth *ASMAuthResource) DependOnTaskNetwork() bool {
return false
}
func (auth *ASMAuthResource) BuildContainerDependency(containerName string, satisfied apicontainerstatus.ContainerStatus,
dependent resourcestatus.ResourceStatus) {
}
func (auth *ASMAuthResource) GetContainerDependencies(dependent resourcestatus.ResourceStatus) []apicontainer.ContainerDependency {
return nil
}