Skip to content

Commit 9667b09

Browse files
committed
Add secret creation to registry-creds cmd
1 parent 2f7a14d commit 9667b09

File tree

8 files changed

+318
-8
lines changed

8 files changed

+318
-8
lines changed

ecs-cli/modules/cli/regcreds/regcreds_app.go

Lines changed: 182 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,26 @@
1414
package regcreds
1515

1616
import (
17+
"fmt"
18+
19+
secretsClient "github.com/aws/amazon-ecs-cli/ecs-cli/modules/clients/aws/secretsmanager"
20+
"github.com/aws/amazon-ecs-cli/ecs-cli/modules/commands/flags"
21+
"github.com/aws/amazon-ecs-cli/ecs-cli/modules/config"
1722
"github.com/aws/amazon-ecs-cli/ecs-cli/modules/utils/regcreds"
23+
"github.com/aws/aws-sdk-go/aws"
24+
"github.com/aws/aws-sdk-go/service/secretsmanager"
25+
"github.com/pkg/errors"
1826
log "github.com/sirupsen/logrus"
1927
"github.com/urfave/cli"
2028
)
2129

30+
// CredsOutputEntry contains the credential ARN and associated container names
31+
// TODO: use & move to output_reader once implemented?
32+
type CredsOutputEntry struct {
33+
CredentialARN string
34+
ContainerNames []string
35+
}
36+
2237
// Up creates or updates registry credential secrets and an ECS task execution role needed to use them in a task def
2338
func Up(c *cli.Context) {
2439
args := c.Args()
@@ -31,7 +46,172 @@ func Up(c *cli.Context) {
3146
if err != nil {
3247
log.Fatal("Error executing 'up': ", err)
3348
}
34-
log.Infof("Read creds input: %+v", credsInput) // remove after SDK calls added
3549

36-
//TODO: create secrets, create role, produce output
50+
err = validateCredsInput(*credsInput)
51+
if err != nil {
52+
log.Fatal("Error executing 'up': ", err)
53+
}
54+
55+
commandConfig := getNewCommandConfig(c)
56+
57+
_, err = getOrCreateRegistryCredentials(credsInput.RegistryCredentials, commandConfig, c)
58+
if err != nil {
59+
log.Fatal("Error executing 'up': ", err)
60+
}
61+
62+
//TODO: create role, produce output
63+
}
64+
65+
func getOrCreateRegistryCredentials(entryMap readers.RegistryCreds, cmdConfig *config.CommandConfig, c *cli.Context) (*map[string]CredsOutputEntry, error) {
66+
registryResults := make(map[string]CredsOutputEntry)
67+
updateAllowed := c.Bool(flags.UpdateExistingSecretsFlag)
68+
69+
smClient := secretsClient.NewSecretsManagerClient(cmdConfig)
70+
71+
for registryName, credentialEntry := range entryMap {
72+
hasCredPair := hasCredPair(credentialEntry)
73+
hasSecretARN := false
74+
if credentialEntry.SecretManagerARN != "" {
75+
hasSecretARN = true
76+
}
77+
78+
log.Infof("Processing credentials for registry %s...", registryName)
79+
80+
if hasCredPair && hasSecretARN {
81+
arn, err := updateOrWarnForExistingSecret(credentialEntry, updateAllowed, smClient)
82+
if err != nil {
83+
return nil, err
84+
}
85+
registryResults[registryName] = buildOutputEntry(arn, credentialEntry.ContainerNames)
86+
87+
} else if hasSecretARN {
88+
registryResults[registryName] = buildOutputEntry(credentialEntry.SecretManagerARN, credentialEntry.ContainerNames)
89+
log.Infof("Using existing secret %s.", registryName)
90+
91+
} else {
92+
arn, err := createNewRegistrySecret(registryName, credentialEntry, smClient)
93+
if err != nil {
94+
return nil, err
95+
}
96+
registryResults[registryName] = buildOutputEntry(arn, credentialEntry.ContainerNames)
97+
}
98+
}
99+
100+
log.Infof("\n up results: %v", registryResults)
101+
102+
return &registryResults, nil
103+
}
104+
105+
func createNewRegistrySecret(registryName string, credEntry readers.RegistryCredEntry, smClient secretsClient.SMClient) (string, error) {
106+
107+
secretName := generateSecretName(registryName)
108+
109+
existingSecret, _ := smClient.DescribeSecret(secretName)
110+
if existingSecret != nil {
111+
log.Infof("Existing credential secret found, using %s", *existingSecret.ARN)
112+
113+
return *existingSecret.ARN, nil
114+
}
115+
116+
secretString := generateSecretString(credEntry.Username, credEntry.Password)
117+
118+
createSecretRequest := secretsmanager.CreateSecretInput{
119+
Name: aws.String(secretName),
120+
SecretString: aws.String(secretString),
121+
Description: aws.String(fmt.Sprintf("Created with the ECS CLI for use with registry %s", registryName)),
122+
}
123+
if credEntry.KmsKeyID != "" {
124+
createSecretRequest.SetKmsKeyId(credEntry.KmsKeyID)
125+
}
126+
127+
output, err := smClient.CreateSecret(createSecretRequest)
128+
if err != nil {
129+
return "", err
130+
}
131+
log.Infof("New credential secret created: %s", *output.ARN)
132+
133+
return *output.ARN, nil
134+
}
135+
136+
func updateOrWarnForExistingSecret(credEntry readers.RegistryCredEntry, updateAllowed bool, smClient secretsClient.SMClient) (string, error) {
137+
secretArn := credEntry.SecretManagerARN
138+
139+
if updateAllowed {
140+
updatedSecretString := generateSecretString(credEntry.Username, credEntry.Password)
141+
putSecretValueRequest := secretsmanager.PutSecretValueInput{
142+
SecretId: aws.String(secretArn),
143+
SecretString: aws.String(updatedSecretString),
144+
}
145+
146+
_, err := smClient.PutSecretValue(putSecretValueRequest)
147+
if err != nil {
148+
return "", err
149+
}
150+
151+
log.Infof("Updated existing secret %s with new value", secretArn)
152+
153+
} else {
154+
log.Warnf("'username' and 'password' found but ignored for existing secret %s. To update existing secrets with new values, use '--update-existing-secrets' flag.", secretArn)
155+
}
156+
157+
return secretArn, nil
158+
}
159+
160+
func validateCredsInput(input readers.ECSRegCredsInput) error {
161+
// TODO: validate version?
162+
163+
inputRegCreds := input.RegistryCredentials
164+
165+
if len(inputRegCreds) == 0 {
166+
return errors.New("provided credentials must contain at least one registry")
167+
}
168+
169+
for registryName, credentialEntry := range inputRegCreds {
170+
if !hasRequiredFields(credentialEntry) {
171+
return fmt.Errorf("missing required field(s) for registry %s; registry credentials should contain existing secret ARN or username + password", registryName)
172+
}
173+
}
174+
return nil
175+
}
176+
177+
func hasRequiredFields(entry readers.RegistryCredEntry) bool {
178+
if (entry.SecretManagerARN != "") || hasCredPair(entry) {
179+
return true
180+
}
181+
return false
182+
}
183+
184+
func hasCredPair(entry readers.RegistryCredEntry) bool {
185+
if entry.Username != "" && entry.Password != "" {
186+
return true
187+
}
188+
return false
189+
}
190+
191+
func getNewCommandConfig(c *cli.Context) *config.CommandConfig {
192+
rdwr, err := config.NewReadWriter()
193+
if err != nil {
194+
log.Fatal("Error executing 'up': ", err)
195+
}
196+
commandConfig, err := config.NewCommandConfig(c, rdwr)
197+
if err != nil {
198+
log.Fatal("Error executing 'up': ", err)
199+
}
200+
201+
return commandConfig
202+
}
203+
204+
func generateSecretName(regName string) string {
205+
return "amazon-ecs-cli-setup-" + regName
206+
}
207+
208+
func generateSecretString(username, password string) string {
209+
return `{"username":"` + username + `"},{"password":"` + password + `"}`
210+
}
211+
212+
func buildOutputEntry(arn string, containers []string) CredsOutputEntry {
213+
return CredsOutputEntry{
214+
CredentialARN: arn,
215+
ContainerNames: containers,
216+
}
37217
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
// Copyright 2015-2018 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License"). You may
4+
// not use this file except in compliance with the License. A copy of the
5+
// License is located at
6+
//
7+
// http://aws.amazon.com/apache2.0/
8+
//
9+
// or in the "license" file accompanying this file. This file is distributed
10+
// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
11+
// express or implied. See the License for the specific language governing
12+
// permissions and limitations under the License.
13+
14+
package regcreds
15+
16+
import (
17+
"io/ioutil"
18+
"os"
19+
"testing"
20+
21+
"github.com/aws/amazon-ecs-cli/ecs-cli/modules/utils/regcreds"
22+
"github.com/stretchr/testify/assert"
23+
)
24+
25+
func TestRegCregsUp(t *testing.T) {
26+
testFileString := `version: 1
27+
registry_credentials:
28+
myrepo.someregistry.io:
29+
username: some_user_name
30+
password: myl337p4$$w0rd!<bz*
31+
container_names:
32+
- test`
33+
34+
tmpfile, err := ioutil.TempFile("", "test")
35+
assert.NoError(t, err, "Unexpected error in creating test file")
36+
defer os.Remove(tmpfile.Name())
37+
38+
_, err = tmpfile.Write([]byte(testFileString))
39+
assert.NoError(t, err, "Unexpected error writing file")
40+
err = tmpfile.Close()
41+
assert.NoError(t, err, "Unexpected error closing file")
42+
43+
//todo: add client and call mocks when added
44+
}
45+
46+
func TestValidateCredsInput_ErrorEmptyCreds(t *testing.T) {
47+
emptyCredMap := make(map[string]readers.RegistryCredEntry)
48+
emptyCredsInput := readers.ECSRegCredsInput{
49+
Version: "1",
50+
RegistryCredentials: emptyCredMap,
51+
}
52+
53+
err := validateCredsInput(emptyCredsInput)
54+
assert.Error(t, err, "Expected empty creds to return error")
55+
}
56+
57+
func TestValidateCredsInput_ErrorOnMissingReqFields(t *testing.T) {
58+
mapWithEmptyCredEntry := make(map[string]readers.RegistryCredEntry)
59+
mapWithEmptyCredEntry["example.com"] = readers.RegistryCredEntry{}
60+
61+
testCredsInput := readers.ECSRegCredsInput{
62+
Version: "1",
63+
RegistryCredentials: mapWithEmptyCredEntry,
64+
}
65+
66+
err := validateCredsInput(testCredsInput)
67+
assert.Error(t, err, "Expected creds with empty entry to return error")
68+
}

ecs-cli/modules/clients/aws/secretsmanager/client.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,9 @@ import (
2323
// SMClient defines methods for interacting with the SecretsManagerAPI interface
2424
type SMClient interface {
2525
CreateSecret(secretsmanager.CreateSecretInput) (*secretsmanager.CreateSecretOutput, error)
26+
DescribeSecret(secretID string) (*secretsmanager.DescribeSecretOutput, error)
2627
ListSecrets(*string) (*secretsmanager.ListSecretsOutput, error)
28+
PutSecretValue(input secretsmanager.PutSecretValueInput) (*secretsmanager.PutSecretValueOutput, error)
2729
}
2830

2931
type secretsManagerClient struct {
@@ -54,6 +56,18 @@ func (c *secretsManagerClient) CreateSecret(input secretsmanager.CreateSecretInp
5456
return output, nil
5557
}
5658

59+
func (c *secretsManagerClient) DescribeSecret(secretID string) (*secretsmanager.DescribeSecretOutput, error) {
60+
request := secretsmanager.DescribeSecretInput{}
61+
request.SetSecretId(secretID)
62+
63+
output, err := c.client.DescribeSecret(&request)
64+
if err != nil {
65+
return nil, err
66+
}
67+
68+
return output, nil
69+
}
70+
5771
func (c *secretsManagerClient) ListSecrets(nextToken *string) (*secretsmanager.ListSecretsOutput, error) {
5872
request := secretsmanager.ListSecretsInput{
5973
NextToken: nextToken,
@@ -66,3 +80,13 @@ func (c *secretsManagerClient) ListSecrets(nextToken *string) (*secretsmanager.L
6680

6781
return output, nil
6882
}
83+
84+
func (c *secretsManagerClient) PutSecretValue(input secretsmanager.PutSecretValueInput) (*secretsmanager.PutSecretValueOutput, error) {
85+
output, err := c.client.PutSecretValue(&input)
86+
87+
if err != nil {
88+
return nil, err
89+
}
90+
91+
return output, nil
92+
}

ecs-cli/modules/clients/aws/secretsmanager/mock/client.go

Lines changed: 26 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

ecs-cli/modules/commands/flags/flags.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,9 @@ const (
127127
RoleFlag = "role"
128128
ComposeServiceTimeOutFlag = "timeout"
129129
ForceDeploymentFlag = "force-deployment"
130+
131+
// Registry Creds
132+
UpdateExistingSecretsFlag = "update-existing-secrets"
130133
)
131134

132135
// OptionalRegionAndProfileFlags provides these flags:

ecs-cli/modules/commands/regcreds/regcreds_command.go

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,16 @@ func upCommand() cli.Command {
3838
Name: "up",
3939
Usage: "Generates AWS Secrets Manager secrets and an IAM Task Execution Role for use in an ECS Task Definition.",
4040
Action: regcreds.Up,
41-
Flags: nil, //TODO: add flags as funtionality is implemented
41+
Flags: regcredsUpFlags(),
4242
OnUsageError: flags.UsageErrorFactory("up"),
4343
}
4444
}
45+
46+
func regcredsUpFlags() []cli.Flag {
47+
return []cli.Flag{
48+
cli.BoolFlag{
49+
Name: flags.UpdateExistingSecretsFlag,
50+
Usage: "[Optional] Specifies whether existing secrets should be updated with new credential input.",
51+
},
52+
}
53+
}

0 commit comments

Comments
 (0)