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 <nicolas.deloof@gmail.com>
This commit is contained in:
Nicolas De Loof 2020-08-10 19:02:27 +02:00
parent 4bfab35007
commit d74796aca2
No known key found for this signature in database
GPG Key ID: 9858809D6F8F6E7E
6 changed files with 155 additions and 168 deletions

View File

@ -1,12 +1,15 @@
package backend package backend
import ( import (
"encoding/json"
"fmt" "fmt"
"sort" "sort"
"strconv" "strconv"
"strings" "strings"
"time" "time"
"github.com/docker/ecs-plugin/secrets"
"github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws"
ecsapi "github.com/aws/aws-sdk-go/service/ecs" ecsapi "github.com/aws/aws-sdk-go/service/ecs"
"github.com/awslabs/goformation/v4/cloudformation" "github.com/awslabs/goformation/v4/cloudformation"
@ -55,6 +58,7 @@ func Convert(project *types.Project, service types.ServiceConfig) (*ecs.TaskDefi
initContainers []ecs.TaskDefinition_ContainerDependency initContainers []ecs.TaskDefinition_ContainerDependency
) )
if len(service.Secrets) > 0 { if len(service.Secrets) > 0 {
initContainerName := fmt.Sprintf("%s_Secrets_InitContainer", normalizeResourceName(service.Name))
volumes = append(volumes, ecs.TaskDefinition_Volume{ volumes = append(volumes, ecs.TaskDefinition_Volume{
Name: "secrets", Name: "secrets",
}) })
@ -65,25 +69,24 @@ func Convert(project *types.Project, service types.ServiceConfig) (*ecs.TaskDefi
}) })
initContainers = append(initContainers, ecs.TaskDefinition_ContainerDependency{ initContainers = append(initContainers, ecs.TaskDefinition_ContainerDependency{
Condition: ecsapi.ContainerConditionSuccess, Condition: ecsapi.ContainerConditionSuccess,
ContainerName: "Secrets_InitContainer", ContainerName: initContainerName,
}) })
var ( var (
names []string args []secrets.Secret
secrets []ecs.TaskDefinition_Secret taskSecrets []ecs.TaskDefinition_Secret
) )
for _, s := range service.Secrets { for _, s := range service.Secrets {
secretConfig := project.Secrets[s.Source] secretConfig := project.Secrets[s.Source]
if s.Target == "" { if s.Target == "" {
s.Target = s.Source s.Target = s.Source
} }
secrets = append(secrets, ecs.TaskDefinition_Secret{ taskSecrets = append(taskSecrets, ecs.TaskDefinition_Secret{
Name: s.Target, Name: s.Target,
ValueFrom: secretConfig.Name, ValueFrom: secretConfig.Name,
}) })
name := s.Target var keys []string
if ext, ok := secretConfig.Extensions[compose.ExtensionKeys]; ok { if ext, ok := secretConfig.Extensions[compose.ExtensionKeys]; ok {
var keys []string
if key, ok := ext.(string); ok { if key, ok := ext.(string); ok {
keys = append(keys, key) keys = append(keys, key)
} else { } else {
@ -91,14 +94,20 @@ func Convert(project *types.Project, service types.ServiceConfig) (*ecs.TaskDefi
keys = append(keys, k.(string)) 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{ containers = append(containers, ecs.TaskDefinition_ContainerDefinition{
Name: fmt.Sprintf("%s_Secrets_InitContainer", normalizeResourceName(service.Name)), Name: initContainerName,
Image: secretsInitContainerImage, 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 Essential: false, // FIXME this will be ignored, see https://github.com/awslabs/goformation/issues/61#issuecomment-625139607
LogConfiguration: logConfiguration, LogConfiguration: logConfiguration,
MountPoints: []ecs.TaskDefinition_MountPoint{ MountPoints: []ecs.TaskDefinition_MountPoint{
@ -108,7 +117,7 @@ func Convert(project *types.Project, service types.ServiceConfig) (*ecs.TaskDefi
SourceVolume: "secrets", SourceVolume: "secrets",
}, },
}, },
Secrets: secrets, Secrets: taskSecrets,
}) })
} }

View File

@ -1,7 +1,7 @@
FROM golang:1.14.4-alpine AS builder 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 . . 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 FROM scratch
COPY --from=builder /go/bin/secrets /secrets COPY --from=builder /go/bin/secrets /secrets

87
ecs/secrets/init.go Normal file
View File

@ -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
}

View File

@ -1,4 +1,4 @@
package main package secrets
import ( import (
"io/ioutil" "io/ioutil"
@ -10,34 +10,14 @@ import (
"gotest.tools/v3/fs" "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) { func TestRawSecret(t *testing.T) {
dir := fs.NewDir(t, "secrets").Path() dir := fs.NewDir(t, "secrets").Path()
os.Setenv("raw", "something_secret") os.Setenv("raw", "something_secret")
defer os.Unsetenv("raw") defer os.Unsetenv("raw")
err := createSecretFiles(secret{ err := CreateSecretFiles(Secret{
name: "raw", Name: "raw",
keys: nil, Keys: nil,
}, dir) }, dir)
assert.NilError(t, err) assert.NilError(t, err)
file, err := ioutil.ReadFile(filepath.Join(dir, "raw")) file, err := ioutil.ReadFile(filepath.Join(dir, "raw"))
@ -55,9 +35,9 @@ func TestSelectedKeysSecret(t *testing.T) {
}`) }`)
defer os.Unsetenv("json") defer os.Unsetenv("json")
err := createSecretFiles(secret{ err := CreateSecretFiles(Secret{
name: "json", Name: "json",
keys: []string{"foo"}, Keys: []string{"foo"},
}, dir) }, dir)
assert.NilError(t, err) assert.NilError(t, err)
file, err := ioutil.ReadFile(filepath.Join(dir, "json", "foo")) file, err := ioutil.ReadFile(filepath.Join(dir, "json", "foo"))
@ -78,9 +58,9 @@ func TestAllKeysSecret(t *testing.T) {
}`) }`)
defer os.Unsetenv("json") defer os.Unsetenv("json")
err := createSecretFiles(secret{ err := CreateSecretFiles(Secret{
name: "json", Name: "json",
keys: []string{"*"}, Keys: []string{"*"},
}, dir) }, dir)
assert.NilError(t, err) assert.NilError(t, err)
file, err := ioutil.ReadFile(filepath.Join(dir, "json", "foo")) file, err := ioutil.ReadFile(filepath.Join(dir, "json", "foo"))
@ -97,9 +77,9 @@ func TestAllKeysSecret(t *testing.T) {
func TestUnknownSecret(t *testing.T) { func TestUnknownSecret(t *testing.T) {
dir := fs.NewDir(t, "secrets").Path() dir := fs.NewDir(t, "secrets").Path()
err := createSecretFiles(secret{ err := CreateSecretFiles(Secret{
name: "not_set", Name: "not_set",
keys: nil, Keys: nil,
}, dir) }, dir)
assert.Check(t, err != nil) assert.Check(t, err != nil)
} }

View File

@ -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
}

33
ecs/secrets/main/main.go Normal file
View File

@ -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 <json encoded []Secret>")
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)
}
}
}