mirror of
https://github.com/docker/compose.git
synced 2025-07-26 07:04:32 +02:00
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:
parent
4bfab35007
commit
d74796aca2
@ -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,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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
87
ecs/secrets/init.go
Normal 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
|
||||||
|
}
|
@ -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)
|
||||||
}
|
}
|
@ -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
33
ecs/secrets/main/main.go
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user