Skip to content

Commit 2f7a14d

Browse files
committed
Add registry-creds cmd and creds_input_reader, tests
1 parent e80c533 commit 2f7a14d

File tree

5 files changed

+328
-0
lines changed

5 files changed

+328
-0
lines changed

ecs-cli/main.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import (
2424
"github.com/aws/amazon-ecs-cli/ecs-cli/modules/commands/image"
2525
"github.com/aws/amazon-ecs-cli/ecs-cli/modules/commands/license"
2626
"github.com/aws/amazon-ecs-cli/ecs-cli/modules/commands/log"
27+
"github.com/aws/amazon-ecs-cli/ecs-cli/modules/commands/regcreds"
2728
"github.com/aws/amazon-ecs-cli/ecs-cli/modules/utils/logger"
2829
"github.com/aws/amazon-ecs-cli/ecs-cli/modules/version"
2930
"github.com/cihub/seelog"
@@ -56,6 +57,7 @@ func main() {
5657
licenseCommand.LicenseCommand(),
5758
composeCommand.ComposeCommand(composeFactory),
5859
logsCommand.LogCommand(),
60+
regcredsCommand.RegistryCredsCommand(),
5961
}
6062

6163
app.Flags = []cli.Flag{
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
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+
"github.com/aws/amazon-ecs-cli/ecs-cli/modules/utils/regcreds"
18+
log "github.com/sirupsen/logrus"
19+
"github.com/urfave/cli"
20+
)
21+
22+
// Up creates or updates registry credential secrets and an ECS task execution role needed to use them in a task def
23+
func Up(c *cli.Context) {
24+
args := c.Args()
25+
26+
if len(args) != 1 {
27+
log.Fatal("Exactly 1 credential file is required. Found: ", len(args))
28+
}
29+
30+
credsInput, err := readers.ReadCredsInput(args[0])
31+
if err != nil {
32+
log.Fatal("Error executing 'up': ", err)
33+
}
34+
log.Infof("Read creds input: %+v", credsInput) // remove after SDK calls added
35+
36+
//TODO: create secrets, create role, produce output
37+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
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 regcredsCommand
15+
16+
import (
17+
ecscli "github.com/aws/amazon-ecs-cli/ecs-cli/modules"
18+
"github.com/aws/amazon-ecs-cli/ecs-cli/modules/cli/regcreds"
19+
"github.com/aws/amazon-ecs-cli/ecs-cli/modules/commands/flags"
20+
"github.com/urfave/cli"
21+
)
22+
23+
// RegistryCredsCommand provides a list of commands that facilitate use of private registry credentials with ECS.
24+
func RegistryCredsCommand() cli.Command {
25+
return cli.Command{
26+
Name: "registry-creds",
27+
Usage: "Facilitates the creation and use of private registry credentials within ECS.",
28+
Before: ecscli.BeforeApp,
29+
Flags: flags.OptionalRegionAndProfileFlags(),
30+
Subcommands: []cli.Command{
31+
upCommand(),
32+
},
33+
}
34+
}
35+
36+
func upCommand() cli.Command {
37+
return cli.Command{
38+
Name: "up",
39+
Usage: "Generates AWS Secrets Manager secrets and an IAM Task Execution Role for use in an ECS Task Definition.",
40+
Action: regcreds.Up,
41+
Flags: nil, //TODO: add flags as funtionality is implemented
42+
OnUsageError: flags.UsageErrorFactory("up"),
43+
}
44+
}
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
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 readers
15+
16+
import (
17+
"io/ioutil"
18+
"os"
19+
"strings"
20+
21+
"github.com/pkg/errors"
22+
"gopkg.in/yaml.v2"
23+
)
24+
25+
// ECSRegCredsInput contains registry cred entries for creation and/or use in a task execution role
26+
type ECSRegCredsInput struct {
27+
Version string
28+
RegistryCredentials RegistryCreds `yaml:"registry_credentials"`
29+
}
30+
31+
// RegistryCreds is a map of registry names to RegCredEntry structs
32+
type RegistryCreds map[string]RegistryCredEntry
33+
34+
// RegistryCredEntry contains info needed to create an AWS Secrets Manager secret and match it to an ECS container(s)
35+
type RegistryCredEntry struct {
36+
SecretManagerARN string `yaml:"secret_manager_arn"`
37+
Username string `yaml:"username"`
38+
Password string `yaml:"password"`
39+
KmdKeyID string `yaml:"kms_key_id"`
40+
ContainerNames []string `yaml:"container_names"`
41+
}
42+
43+
// ReadCredsInput parses 'registry-creds up' input into an ECSRegCredsInput struct
44+
func ReadCredsInput(filename string) (*ECSRegCredsInput, error) {
45+
46+
rawCredsInput, err := ioutil.ReadFile(filename)
47+
if err != nil {
48+
return nil, errors.Wrapf(err, "Error reading file '%v'", filename)
49+
}
50+
credsInput := &ECSRegCredsInput{}
51+
52+
if err = yaml.Unmarshal([]byte(rawCredsInput), &credsInput); err != nil {
53+
return nil, errors.Wrapf(err, "Error unmarshalling yaml data from credential input file: %s", filename)
54+
}
55+
56+
expandedCredsInput := RegistryCreds{}
57+
for regName, credEntry := range credsInput.RegistryCredentials {
58+
expandedCredEntry := expandCredEntry(credEntry)
59+
expandedCredsInput[regName] = expandedCredEntry
60+
}
61+
62+
credsInput.RegistryCredentials = expandedCredsInput
63+
64+
return credsInput, nil
65+
}
66+
67+
// expandCredEntry checks if individual fields are env vars and if so, retrieves & sets that value
68+
func expandCredEntry(credEntry RegistryCredEntry) RegistryCredEntry {
69+
expandedSecretARN := getValueOrEnvVar(credEntry.SecretManagerARN)
70+
expandedUsername := getValueOrEnvVar(credEntry.Username)
71+
expandedPassword := getValueOrEnvVar(credEntry.Password)
72+
expandedKmsKeyID := getValueOrEnvVar(credEntry.KmdKeyID)
73+
//TODO: look for env vars in container names?
74+
75+
expandedCredEntry := RegistryCredEntry{
76+
SecretManagerARN: expandedSecretARN,
77+
Username: expandedUsername,
78+
Password: expandedPassword,
79+
KmdKeyID: expandedKmsKeyID,
80+
ContainerNames: credEntry.ContainerNames,
81+
}
82+
return expandedCredEntry
83+
}
84+
85+
// selectively runs ExpandEnv() to avoid indescriminant replacement of substrings with '$'
86+
// e.g., password='c00l$tuff2018' -> return same; password='${MY_PASSWORD}' -> return env value.
87+
func getValueOrEnvVar(s string) string {
88+
if strings.HasPrefix(s, "${") && strings.HasSuffix(s, "}") {
89+
return os.ExpandEnv(s)
90+
}
91+
return s
92+
}
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
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 readers
15+
16+
import (
17+
"io/ioutil"
18+
"os"
19+
"testing"
20+
21+
"github.com/stretchr/testify/assert"
22+
)
23+
24+
func TestReadCredsInput(t *testing.T) {
25+
credsInputString := `version: 1
26+
registry_credentials:
27+
registry.io:
28+
username: some_user_name
29+
password: myl337p4$$w0rd!<bz*
30+
kms_key_id: aws:arn:kms:key/iuytre-jhgfd
31+
container_names:
32+
- nginx-custom
33+
- logging
34+
other-registry.net:
35+
secret_manager_arn: aws:arn:secretsmanager:secret/repocreds-776ytg
36+
container_names:
37+
- metrics`
38+
39+
tmpfile, err := ioutil.TempFile("", "test")
40+
assert.NoError(t, err, "Unexpected error in creating test file")
41+
defer os.Remove(tmpfile.Name())
42+
43+
_, err = tmpfile.Write([]byte(credsInputString))
44+
assert.NoError(t, err, "Unexpected error writing file")
45+
err = tmpfile.Close()
46+
assert.NoError(t, err, "Unexpected error closing file")
47+
48+
credsResult, err := ReadCredsInput(tmpfile.Name())
49+
assert.NoError(t, err, "Unexpected error reading file")
50+
51+
// assert expected values match
52+
assert.Equal(t, "1", credsResult.Version)
53+
assert.Equal(t, 2, len(credsResult.RegistryCredentials))
54+
55+
firstRegResult := credsResult.RegistryCredentials["registry.io"]
56+
assert.NotEmpty(t, firstRegResult)
57+
assert.Equal(t, "some_user_name", firstRegResult.Username)
58+
assert.Equal(t, "myl337p4$$w0rd!<bz*", firstRegResult.Password)
59+
assert.Equal(t, "aws:arn:kms:key/iuytre-jhgfd", firstRegResult.KmdKeyID)
60+
assert.Equal(t, 2, len(firstRegResult.ContainerNames))
61+
62+
otherRegResult := credsResult.RegistryCredentials["other-registry.net"]
63+
assert.NotEmpty(t, otherRegResult)
64+
assert.Equal(t, "aws:arn:secretsmanager:secret/repocreds-776ytg", otherRegResult.SecretManagerARN)
65+
assert.Equal(t, 1, len(otherRegResult.ContainerNames))
66+
}
67+
68+
func TestReadCredsInputWithEnvVarsFromShell(t *testing.T) {
69+
// setup test env vars
70+
secretEnvKey := "MY_SECRET_ARN"
71+
secretEnvVal := "aws:arn:secretmanager:secret/regsecret-1"
72+
73+
usrnameEnvKey := "MY_REG_USRNAME"
74+
usrnameEnvVal := "myname@example.net"
75+
76+
passwrdEnvKey := "MY_REG_PASSWORD"
77+
passwrdEnvVal := "ne4t04905e867uyrdtoilfgkj"
78+
79+
kmsEnvKey := "MY_KEY_ARN"
80+
kmsEnvVal := "aws:arn:kms:key/iuytre-yhe4"
81+
82+
os.Setenv(usrnameEnvKey, usrnameEnvVal)
83+
os.Setenv(passwrdEnvKey, passwrdEnvVal)
84+
os.Setenv(kmsEnvKey, kmsEnvVal)
85+
os.Setenv(secretEnvKey, secretEnvVal)
86+
defer func() {
87+
os.Unsetenv(usrnameEnvKey)
88+
os.Unsetenv(passwrdEnvKey)
89+
os.Unsetenv(kmsEnvKey)
90+
os.Unsetenv(secretEnvKey)
91+
}()
92+
93+
inputFileString := `version: 1
94+
registry_credentials:
95+
myrepo.someregistry.io:
96+
secret_manager_arn: ${MY_SECRET_ARN}
97+
username: ${MY_REG_USRNAME}
98+
password: ${MY_REG_PASSWORD}
99+
kms_key_id: ${MY_KEY_ARN}
100+
container_names:
101+
- test`
102+
103+
tmpfile, err := ioutil.TempFile("", "test")
104+
assert.NoError(t, err, "Unexpected error in creating test file")
105+
defer os.Remove(tmpfile.Name())
106+
107+
_, err = tmpfile.Write([]byte(inputFileString))
108+
assert.NoError(t, err, "Unexpected error writing file")
109+
err = tmpfile.Close()
110+
assert.NoError(t, err, "Unexpected error closing file")
111+
112+
credsResult, err := ReadCredsInput(tmpfile.Name())
113+
assert.NoError(t, err, "Unexpected error reading file")
114+
115+
// assert expected values match
116+
assert.Equal(t, "1", credsResult.Version)
117+
assert.Equal(t, 1, len(credsResult.RegistryCredentials))
118+
119+
credEntry := credsResult.RegistryCredentials["myrepo.someregistry.io"]
120+
assert.NotEmpty(t, credEntry)
121+
assert.Equal(t, usrnameEnvVal, credEntry.Username)
122+
assert.Equal(t, passwrdEnvVal, credEntry.Password)
123+
assert.Equal(t, kmsEnvVal, credEntry.KmdKeyID)
124+
assert.Equal(t, secretEnvVal, credEntry.SecretManagerARN)
125+
assert.Equal(t, 1, len(credEntry.ContainerNames))
126+
}
127+
128+
func TestReadCredsInput_ErrorFileNotFound(t *testing.T) {
129+
var fakeFileName = "/missingFile"
130+
_, err := ReadCredsInput(fakeFileName)
131+
assert.Error(t, err, "Expected error on missing file")
132+
}
133+
134+
func TestReadCredsInput_ErrorBadYaml(t *testing.T) {
135+
badCredEntryFileString := `version: 1
136+
registry_credentials:
137+
myrepo.someregistry.io:
138+
secret_manager_arn: arn:aws:secretmanager:some-secret
139+
container_names:
140+
- test`
141+
142+
tmpfile, err := ioutil.TempFile("", "test")
143+
assert.NoError(t, err, "Unexpected error in creating test file")
144+
defer os.Remove(tmpfile.Name())
145+
146+
_, err = tmpfile.Write([]byte(badCredEntryFileString))
147+
assert.NoError(t, err, "Unexpected error writing file")
148+
err = tmpfile.Close()
149+
assert.NoError(t, err, "Unexpected error closing file")
150+
151+
_, err = ReadCredsInput(tmpfile.Name())
152+
assert.Error(t, err, "Expected error on bad file YAML")
153+
}

0 commit comments

Comments
 (0)