From 4bfab35007abee926201cb4791546f245bb194c4 Mon Sep 17 00:00:00 2001 From: Nicolas De Loof Date: Mon, 10 Aug 2020 16:15:46 +0200 Subject: [PATCH] TestCase for the secrets init container Signed-off-by: Nicolas De Loof --- ecs/cmd/commands/compose.go | 9 +- ecs/go.mod | 1 + ecs/pkg/amazon/backend/cloudformation.go | 3 +- ecs/secrets/main.go | 149 ++++++++++++++--------- ecs/secrets/main_test.go | 105 ++++++++++++++++ 5 files changed, 203 insertions(+), 64 deletions(-) create mode 100644 ecs/secrets/main_test.go diff --git a/ecs/cmd/commands/compose.go b/ecs/cmd/commands/compose.go index f4e6d4e91..d0f1a224c 100644 --- a/ecs/cmd/commands/compose.go +++ b/ecs/cmd/commands/compose.go @@ -7,11 +7,10 @@ import ( "os" "strings" - "github.com/docker/ecs-plugin/pkg/amazon/cloudformation" - "github.com/compose-spec/compose-go/cli" "github.com/docker/cli/cli/command" amazon "github.com/docker/ecs-plugin/pkg/amazon/backend" + "github.com/docker/ecs-plugin/pkg/amazon/cloudformation" "github.com/docker/ecs-plugin/pkg/docker" "github.com/spf13/cobra" ) @@ -60,13 +59,11 @@ func ConvertCommand(dockerCli command.Cli, options *composeOptions) *cobra.Comma if err != nil { return err } - json, err := cloudformation.Marshall(template) if err != nil { - fmt.Printf("Failed to generate JSON: %s\n", err) - } else { - fmt.Printf("%s\n", string(json)) + return err } + fmt.Printf("%s\n", string(json)) return nil }), } diff --git a/ecs/go.mod b/ecs/go.mod index f3e2949d5..c7f1ad67a 100644 --- a/ecs/go.mod +++ b/ecs/go.mod @@ -39,6 +39,7 @@ require ( github.com/morikuni/aec v1.0.0 // indirect github.com/onsi/ginkgo v1.11.0 // indirect github.com/opencontainers/image-spec v1.0.1 // indirect + github.com/pkg/errors v0.9.1 github.com/sirupsen/logrus v1.6.0 github.com/smartystreets/goconvey v1.6.4 // indirect github.com/spf13/cobra v0.0.5 diff --git a/ecs/pkg/amazon/backend/cloudformation.go b/ecs/pkg/amazon/backend/cloudformation.go index a47e1ad6c..e54108588 100644 --- a/ecs/pkg/amazon/backend/cloudformation.go +++ b/ecs/pkg/amazon/backend/cloudformation.go @@ -6,8 +6,6 @@ import ( "regexp" "strings" - "github.com/awslabs/goformation/v4/cloudformation/secretsmanager" - ecsapi "github.com/aws/aws-sdk-go/service/ecs" "github.com/aws/aws-sdk-go/service/elbv2" cloudmapapi "github.com/aws/aws-sdk-go/service/servicediscovery" @@ -17,6 +15,7 @@ import ( "github.com/awslabs/goformation/v4/cloudformation/elasticloadbalancingv2" "github.com/awslabs/goformation/v4/cloudformation/iam" "github.com/awslabs/goformation/v4/cloudformation/logs" + "github.com/awslabs/goformation/v4/cloudformation/secretsmanager" cloudmap "github.com/awslabs/goformation/v4/cloudformation/servicediscovery" "github.com/awslabs/goformation/v4/cloudformation/tags" "github.com/compose-spec/compose-go/compatibility" diff --git a/ecs/secrets/main.go b/ecs/secrets/main.go index cc37325a5..eeaa992e6 100644 --- a/ecs/secrets/main.go +++ b/ecs/secrets/main.go @@ -7,72 +7,109 @@ import ( "os" "path/filepath" "strings" + + "github.com/pkg/errors" ) -// return codes: -// 1: failed to read secret from env -// 2: failed to parse hierarchical secret -// 3: failed to write secret content into file +type secret struct { + name string + keys []string +} + +const secretsFolder = "/run/secrets" + func main() { - for _, name := range os.Args[1:] { + secrets := parseInput(os.Args[1:]) + + for _, secret := range secrets { + err := createSecretFiles(secret, secretsFolder) + if err != nil { + fmt.Fprintf(os.Stderr, err.Error()) + os.Exit(1) + } + } +} + +func createSecretFiles(secret secret, path string) error { + value, ok := os.LookupEnv(secret.name) + if !ok { + return fmt.Errorf("%q variable not set", secret.name) + } + + secrets := filepath.Join(path, secret.name) + + if len(secret.keys) == 0 { + // raw secret + fmt.Printf("inject secret %q info %s\n", secret.name, secrets) + return ioutil.WriteFile(secrets, []byte(value), 0444) + } + + var unmarshalled interface{} + err := json.Unmarshal([]byte(value), &unmarshalled) + if err != nil { + return errors.Wrapf(err, "%q secret is not a valid JSON document", secret.name) + } + + dict, ok := unmarshalled.(map[string]interface{}) + if !ok { + return errors.Wrapf(err, "%q secret is not a JSON dictionary", secret.name) + } + err = os.MkdirAll(secrets, 0755) + if err != nil { + return err + } + + if contains(secret.keys, "*") { + var keys []string + for k := range dict { + keys = append(keys, k) + } + secret.keys = keys + } + + for _, k := range secret.keys { + path := filepath.Join(secrets, k) + fmt.Printf("inject secret %q info %s\n", k, path) + + v, ok := dict[k] + if !ok { + return fmt.Errorf("%q secret has no %q key", secret.name, k) + } + + var raw []byte + if s, ok := v.(string); ok { + raw = []byte(s) + } else { + raw, err = json.Marshal(v) + if err != nil { + return err + } + } + + err = ioutil.WriteFile(path, raw, 0444) + if err != nil { + return err + } + } + return nil +} + +// parseInput parse secret to be dumped into secret files with syntax `VARIABLE_NAME[:COMA_SEPARATED_KEYS]` +func parseInput(input []string) []secret { + var secrets []secret + for _, name := range input { i := strings.Index(name, ":") var keys []string if i > 0 { keys = strings.Split(name[i+1:], ",") name = name[:i] } - value, ok := os.LookupEnv(name) - if !ok { - fmt.Fprintf(os.Stderr, "%q variable not set", name) - os.Exit(1) - } - - secrets := filepath.Join("/run/secrets", name) - - if len(keys) == 0 { - // raw secret - fmt.Printf("inject secret %q info %s\n", name, secrets) - err := ioutil.WriteFile(secrets, []byte(value), 0444) - if err != nil { - fmt.Fprintf(os.Stderr, err.Error()) - os.Exit(3) - } - os.Exit(0) - } - - var unmarshalled interface{} - err := json.Unmarshal([]byte(value), &unmarshalled) - if err == nil { - if dict, ok := unmarshalled.(map[string]interface{}); ok { - os.MkdirAll(secrets, 0555) - for k, v := range dict { - if !contains(keys, k) && !contains(keys, "*") { - continue - } - path := filepath.Join(secrets, k) - fmt.Printf("inject secret %q info %s\n", k, path) - - var raw []byte - if s, ok := v.(string); ok { - raw = []byte(s) - } else { - raw, err = json.Marshal(v) - if err != nil { - fmt.Fprintf(os.Stderr, err.Error()) - os.Exit(2) - } - } - - err = ioutil.WriteFile(path, raw, 0444) - if err != nil { - fmt.Fprintf(os.Stderr, err.Error()) - os.Exit(3) - } - } - os.Exit(0) - } - } + secrets = append(secrets, secret{ + name: name, + keys: keys, + }) } + return secrets } func contains(keys []string, s string) bool { diff --git a/ecs/secrets/main_test.go b/ecs/secrets/main_test.go new file mode 100644 index 000000000..ff6f630f1 --- /dev/null +++ b/ecs/secrets/main_test.go @@ -0,0 +1,105 @@ +package main + +import ( + "io/ioutil" + "os" + "path/filepath" + "testing" + + "gotest.tools/v3/assert" + "gotest.tools/v3/fs" +) + +func TestParseSecrets(t *testing.T) { + secrets := parseInput([]string{ + "foo", + "bar:*", + "zot:key0,key1", + }) + assert.Check(t, len(secrets) == 3) + assert.Check(t, secrets[0].name == "foo") + assert.Check(t, secrets[0].keys == nil) + + assert.Check(t, secrets[1].name == "bar") + assert.Check(t, len(secrets[1].keys) == 1) + assert.Check(t, secrets[1].keys[0] == "*") + + assert.Check(t, secrets[2].name == "zot") + assert.Check(t, len(secrets[2].keys) == 2) + assert.Check(t, secrets[2].keys[0] == "key0") + assert.Check(t, secrets[2].keys[1] == "key1") +} + +func TestRawSecret(t *testing.T) { + dir := fs.NewDir(t, "secrets").Path() + os.Setenv("raw", "something_secret") + defer os.Unsetenv("raw") + + err := createSecretFiles(secret{ + name: "raw", + keys: nil, + }, dir) + assert.NilError(t, err) + file, err := ioutil.ReadFile(filepath.Join(dir, "raw")) + assert.NilError(t, err) + content := string(file) + assert.Equal(t, content, "something_secret") +} + +func TestSelectedKeysSecret(t *testing.T) { + dir := fs.NewDir(t, "secrets").Path() + os.Setenv("json", ` +{ + "foo": "bar", + "zot": "qix" +}`) + defer os.Unsetenv("json") + + err := createSecretFiles(secret{ + name: "json", + keys: []string{"foo"}, + }, dir) + assert.NilError(t, err) + file, err := ioutil.ReadFile(filepath.Join(dir, "json", "foo")) + assert.NilError(t, err) + content := string(file) + assert.Equal(t, content, "bar") + + _, err = os.Stat(filepath.Join(dir, "json", "zot")) + assert.Check(t, os.IsNotExist(err)) +} + +func TestAllKeysSecret(t *testing.T) { + dir := fs.NewDir(t, "secrets").Path() + os.Setenv("json", ` +{ + "foo": "bar", + "zot": "qix" +}`) + defer os.Unsetenv("json") + + err := createSecretFiles(secret{ + name: "json", + keys: []string{"*"}, + }, dir) + assert.NilError(t, err) + file, err := ioutil.ReadFile(filepath.Join(dir, "json", "foo")) + assert.NilError(t, err) + content := string(file) + assert.Equal(t, content, "bar") + + file, err = ioutil.ReadFile(filepath.Join(dir, "json", "zot")) + assert.NilError(t, err) + content = string(file) + assert.Equal(t, content, "qix") +} + +func TestUnknownSecret(t *testing.T) { + dir := fs.NewDir(t, "secrets").Path() + + err := createSecretFiles(secret{ + name: "not_set", + keys: nil, + }, dir) + assert.Check(t, err != nil) +}