From d74796aca22d7ec9d97a620576f6c133998f9ead Mon Sep 17 00:00:00 2001 From: Nicolas De Loof Date: Mon, 10 Aug 2020 19:02:27 +0200 Subject: [PATCH] Pass secret definition to init container as json struct this avoid yet another new micro-formats that is poorly documented Signed-off-by: Nicolas De Loof --- ecs/pkg/amazon/backend/convert.go | 31 ++++-- ecs/secrets/Dockerfile | 4 +- ecs/secrets/init.go | 87 +++++++++++++++ ecs/secrets/{main_test.go => init_test.go} | 46 +++----- ecs/secrets/main.go | 122 --------------------- ecs/secrets/main/main.go | 33 ++++++ 6 files changed, 155 insertions(+), 168 deletions(-) create mode 100644 ecs/secrets/init.go rename ecs/secrets/{main_test.go => init_test.go} (64%) delete mode 100644 ecs/secrets/main.go create mode 100644 ecs/secrets/main/main.go diff --git a/ecs/pkg/amazon/backend/convert.go b/ecs/pkg/amazon/backend/convert.go index e024bde26..3eddde174 100644 --- a/ecs/pkg/amazon/backend/convert.go +++ b/ecs/pkg/amazon/backend/convert.go @@ -1,12 +1,15 @@ package backend import ( + "encoding/json" "fmt" "sort" "strconv" "strings" "time" + "github.com/docker/ecs-plugin/secrets" + "github.com/aws/aws-sdk-go/aws" ecsapi "github.com/aws/aws-sdk-go/service/ecs" "github.com/awslabs/goformation/v4/cloudformation" @@ -55,6 +58,7 @@ func Convert(project *types.Project, service types.ServiceConfig) (*ecs.TaskDefi initContainers []ecs.TaskDefinition_ContainerDependency ) if len(service.Secrets) > 0 { + initContainerName := fmt.Sprintf("%s_Secrets_InitContainer", normalizeResourceName(service.Name)) volumes = append(volumes, ecs.TaskDefinition_Volume{ Name: "secrets", }) @@ -65,25 +69,24 @@ func Convert(project *types.Project, service types.ServiceConfig) (*ecs.TaskDefi }) initContainers = append(initContainers, ecs.TaskDefinition_ContainerDependency{ Condition: ecsapi.ContainerConditionSuccess, - ContainerName: "Secrets_InitContainer", + ContainerName: initContainerName, }) var ( - names []string - secrets []ecs.TaskDefinition_Secret + args []secrets.Secret + taskSecrets []ecs.TaskDefinition_Secret ) for _, s := range service.Secrets { secretConfig := project.Secrets[s.Source] if s.Target == "" { s.Target = s.Source } - secrets = append(secrets, ecs.TaskDefinition_Secret{ + taskSecrets = append(taskSecrets, ecs.TaskDefinition_Secret{ Name: s.Target, ValueFrom: secretConfig.Name, }) - name := s.Target + var keys []string if ext, ok := secretConfig.Extensions[compose.ExtensionKeys]; ok { - var keys []string if key, ok := ext.(string); ok { keys = append(keys, key) } else { @@ -91,14 +94,20 @@ func Convert(project *types.Project, service types.ServiceConfig) (*ecs.TaskDefi keys = append(keys, k.(string)) } } - name = fmt.Sprintf("%s:%s", s.Target, strings.Join(keys, ",")) } - names = append(names, name) + args = append(args, secrets.Secret{ + Name: s.Target, + Keys: keys, + }) + } + command, err := json.Marshal(args) + if err != nil { + return nil, err } containers = append(containers, ecs.TaskDefinition_ContainerDefinition{ - Name: fmt.Sprintf("%s_Secrets_InitContainer", normalizeResourceName(service.Name)), + Name: initContainerName, Image: secretsInitContainerImage, - Command: names, + Command: []string{string(command)}, Essential: false, // FIXME this will be ignored, see https://github.com/awslabs/goformation/issues/61#issuecomment-625139607 LogConfiguration: logConfiguration, MountPoints: []ecs.TaskDefinition_MountPoint{ @@ -108,7 +117,7 @@ func Convert(project *types.Project, service types.ServiceConfig) (*ecs.TaskDefi SourceVolume: "secrets", }, }, - Secrets: secrets, + Secrets: taskSecrets, }) } diff --git a/ecs/secrets/Dockerfile b/ecs/secrets/Dockerfile index 7395d1250..638e113b4 100644 --- a/ecs/secrets/Dockerfile +++ b/ecs/secrets/Dockerfile @@ -1,7 +1,7 @@ FROM golang:1.14.4-alpine AS builder -WORKDIR $GOPATH/src/github.com/docker/ecs-secrets +WORKDIR $GOPATH/src/github.com/docker/ecs-plugin/secrets COPY . . -RUN GOOS=linux GOARCH=amd64 go build -ldflags="-w -s" -o /go/bin/secrets +RUN GOOS=linux GOARCH=amd64 go build -ldflags="-w -s" -o /go/bin/secrets main/main.go FROM scratch COPY --from=builder /go/bin/secrets /secrets diff --git a/ecs/secrets/init.go b/ecs/secrets/init.go new file mode 100644 index 000000000..1903d3cc9 --- /dev/null +++ b/ecs/secrets/init.go @@ -0,0 +1,87 @@ +package secrets + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "os" + "path/filepath" +) + +type Secret struct { + Name string + Keys []string +} + +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 fmt.Errorf("%q Secret is not a valid JSON document: %w", secret.Name, err) + } + + dict, ok := unmarshalled.(map[string]interface{}) + if !ok { + return fmt.Errorf("%q Secret is not a JSON dictionary: %w", secret.Name, err) + } + 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 +} + +func contains(keys []string, s string) bool { + for _, k := range keys { + if k == s { + return true + } + } + return false +} diff --git a/ecs/secrets/main_test.go b/ecs/secrets/init_test.go similarity index 64% rename from ecs/secrets/main_test.go rename to ecs/secrets/init_test.go index ff6f630f1..dd068f5af 100644 --- a/ecs/secrets/main_test.go +++ b/ecs/secrets/init_test.go @@ -1,4 +1,4 @@ -package main +package secrets import ( "io/ioutil" @@ -10,34 +10,14 @@ import ( "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, + err := CreateSecretFiles(Secret{ + Name: "raw", + Keys: nil, }, dir) assert.NilError(t, err) file, err := ioutil.ReadFile(filepath.Join(dir, "raw")) @@ -55,9 +35,9 @@ func TestSelectedKeysSecret(t *testing.T) { }`) defer os.Unsetenv("json") - err := createSecretFiles(secret{ - name: "json", - keys: []string{"foo"}, + err := CreateSecretFiles(Secret{ + Name: "json", + Keys: []string{"foo"}, }, dir) assert.NilError(t, err) file, err := ioutil.ReadFile(filepath.Join(dir, "json", "foo")) @@ -78,9 +58,9 @@ func TestAllKeysSecret(t *testing.T) { }`) defer os.Unsetenv("json") - err := createSecretFiles(secret{ - name: "json", - keys: []string{"*"}, + err := CreateSecretFiles(Secret{ + Name: "json", + Keys: []string{"*"}, }, dir) assert.NilError(t, err) file, err := ioutil.ReadFile(filepath.Join(dir, "json", "foo")) @@ -97,9 +77,9 @@ func TestAllKeysSecret(t *testing.T) { func TestUnknownSecret(t *testing.T) { dir := fs.NewDir(t, "secrets").Path() - err := createSecretFiles(secret{ - name: "not_set", - keys: nil, + err := CreateSecretFiles(Secret{ + Name: "not_set", + Keys: nil, }, dir) assert.Check(t, err != nil) } diff --git a/ecs/secrets/main.go b/ecs/secrets/main.go deleted file mode 100644 index eeaa992e6..000000000 --- a/ecs/secrets/main.go +++ /dev/null @@ -1,122 +0,0 @@ -package main - -import ( - "encoding/json" - "fmt" - "io/ioutil" - "os" - "path/filepath" - "strings" - - "github.com/pkg/errors" -) - -type secret struct { - name string - keys []string -} - -const secretsFolder = "/run/secrets" - -func main() { - 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] - } - secrets = append(secrets, secret{ - name: name, - keys: keys, - }) - } - return secrets -} - -func contains(keys []string, s string) bool { - for _, k := range keys { - if k == s { - return true - } - } - return false -} diff --git a/ecs/secrets/main/main.go b/ecs/secrets/main/main.go new file mode 100644 index 000000000..703f88f8e --- /dev/null +++ b/ecs/secrets/main/main.go @@ -0,0 +1,33 @@ +package main + +import ( + "encoding/json" + "fmt" + "os" + + "github.com/docker/ecs-plugin/secrets" +) + +const secretsFolder = "/run/secrets" + +func main() { + if len(os.Args) != 2 { + fmt.Fprintf(os.Stderr, "usage: secrets ") + os.Exit(1) + } + + var input []secrets.Secret + err := json.Unmarshal([]byte(os.Args[1]), &input) + if err != nil { + fmt.Fprintf(os.Stderr, err.Error()) + os.Exit(1) + } + + for _, secret := range input { + err := secrets.CreateSecretFiles(secret, secretsFolder) + if err != nil { + fmt.Fprintf(os.Stderr, err.Error()) + os.Exit(1) + } + } +}