agent/asm/asm.go (113 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 asm
import (
"context"
"encoding/json"
"fmt"
"github.com/aws/amazon-ecs-agent/ecs-agent/logger"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/secretsmanager"
"github.com/aws/aws-sdk-go-v2/service/secretsmanager/types"
"github.com/cihub/seelog"
"github.com/docker/docker/api/types/registry"
"github.com/pkg/errors"
)
// For mocking purpose
type SecretsManagerAPI interface {
GetSecretValue(ctx context.Context, params *secretsmanager.GetSecretValueInput, optFns ...func(*secretsmanager.Options)) (*secretsmanager.GetSecretValueOutput, error)
}
// AuthDataValue is the schema for
// the SecretStringValue returned by ASM
type AuthDataValue struct {
Username *string
Password *string
}
func resourceInitializationErrMsg(secretID string) string {
return fmt.Sprintf(
`ResourceNotFoundException: The task can't retrieve the secret with ARN %sfrom AWS Secrets Manager. Check whether the secret exists in the specified Region`,
secretID)
}
// Augment error message with extra details for most common exceptions:
func augmentErrMsg(secretID string, err error) string {
if secretID == "" {
logger.Warn("augmentErrMsg: SecretID is empty (which is unexpected)")
}
var rnfe *types.ResourceNotFoundException
if errors.As(err, &rnfe) {
secretID = "'" + secretID + "' "
return resourceInitializationErrMsg(secretID)
} else {
return fmt.Sprintf("secret %s: %s", secretID, err.Error())
}
}
// GetDockerAuthFromASM makes the api call to the AWS Secrets Manager service to
// retrieve the docker auth data
func GetDockerAuthFromASM(secretID string, client SecretsManagerAPI) (registry.AuthConfig, error) {
in := &secretsmanager.GetSecretValueInput{
SecretId: aws.String(secretID),
}
out, err := client.GetSecretValue(context.TODO(), in)
if err != nil {
return registry.AuthConfig{}, errors.Wrapf(err,
"asm fetching secret from the service for %s", secretID)
}
return extractASMValue(out)
}
func extractASMValue(out *secretsmanager.GetSecretValueOutput) (registry.AuthConfig, error) {
if out == nil {
return registry.AuthConfig{}, errors.New(
"asm fetching authorization data: empty response")
}
secretValue := aws.ToString(out.SecretString)
if secretValue == "" {
return registry.AuthConfig{}, errors.New(
"asm fetching authorization data: empty secrets value")
}
authDataValue := AuthDataValue{}
err := json.Unmarshal([]byte(secretValue), &authDataValue)
if err != nil {
// could not unmarshal, incorrect secret value schema
return registry.AuthConfig{}, errors.New(
"asm fetching authorization data: unable to unmarshal secret value, invalid schema")
}
username := aws.ToString(authDataValue.Username)
password := aws.ToString(authDataValue.Password)
if username == "" {
return registry.AuthConfig{}, errors.New(
"asm fetching username: AuthorizationData is malformed, empty field")
}
if password == "" {
return registry.AuthConfig{}, errors.New(
"asm fetching password: AuthorizationData is malformed, empty field")
}
dac := registry.AuthConfig{
Username: username,
Password: password,
}
return dac, nil
}
func GetSecretFromASMWithInput(input *secretsmanager.GetSecretValueInput,
client SecretsManagerAPI, jsonKey string) (string, error) {
secretID := *input.SecretId
out, err := client.GetSecretValue(context.TODO(), input)
if err != nil {
return "", errors.Wrap(err, augmentErrMsg(secretID, err))
}
if jsonKey == "" {
return aws.ToString(out.SecretString), nil
}
secretMap := make(map[string]interface{})
jsonErr := json.Unmarshal([]byte(*out.SecretString), &secretMap)
if jsonErr != nil {
seelog.Warnf("Error when treating retrieved secret value with secret id %s as JSON and calling unmarshal.", *input.SecretId)
return "", jsonErr
}
secretValue, ok := secretMap[jsonKey]
if !ok {
err = errors.New(fmt.Sprintf("retrieved secret from Secrets Manager did not contain json key %s", jsonKey))
return "", err
}
return fmt.Sprintf("%v", secretValue), nil
}
// GetSecretFromASM makes the api call to the AWS Secrets Manager service to
// retrieve the secret value
func GetSecretFromASM(secretID string, client SecretsManagerAPI) (string, error) {
in := &secretsmanager.GetSecretValueInput{
SecretId: aws.String(secretID),
}
out, err := client.GetSecretValue(context.TODO(), in)
if err != nil {
return "", errors.Wrapf(err, "secret %s", secretID)
}
return aws.ToString(out.SecretString), nil
}