From 85b3cbd6ea8f7d6dc88751dd8dbf572d968941bc Mon Sep 17 00:00:00 2001 From: Nicolas De Loof Date: Tue, 4 Aug 2020 18:04:06 +0200 Subject: [PATCH] use an initContainer to inject secrets as /run/secrets/xx Signed-off-by: Nicolas De Loof --- ecs/cmd/commands/compose.go | 6 +- ecs/go.sum | 14 +- ecs/pkg/amazon/backend/cloudformation.go | 27 ++++ ecs/pkg/amazon/backend/compatibility.go | 6 + ecs/pkg/amazon/backend/convert.go | 157 ++++++++++++++++------ ecs/pkg/amazon/cloudformation/marshall.go | 45 +++++++ ecs/pkg/amazon/sdk/sdk.go | 6 +- ecs/pkg/compose/x.go | 1 + ecs/secrets/Dockerfile | 8 ++ ecs/secrets/main.go | 85 ++++++++++++ 10 files changed, 296 insertions(+), 59 deletions(-) create mode 100644 ecs/pkg/amazon/cloudformation/marshall.go create mode 100644 ecs/secrets/Dockerfile create mode 100644 ecs/secrets/main.go diff --git a/ecs/cmd/commands/compose.go b/ecs/cmd/commands/compose.go index f4262fd4e..f4e6d4e91 100644 --- a/ecs/cmd/commands/compose.go +++ b/ecs/cmd/commands/compose.go @@ -7,6 +7,8 @@ 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" @@ -59,11 +61,11 @@ func ConvertCommand(dockerCli command.Cli, options *composeOptions) *cobra.Comma return err } - j, err := template.JSON() + json, err := cloudformation.Marshall(template) if err != nil { fmt.Printf("Failed to generate JSON: %s\n", err) } else { - fmt.Printf("%s\n", string(j)) + fmt.Printf("%s\n", string(json)) } return nil }), diff --git a/ecs/go.sum b/ecs/go.sum index 296309a32..964066997 100644 --- a/ecs/go.sum +++ b/ecs/go.sum @@ -19,12 +19,8 @@ github.com/akavel/rsrc v0.8.0/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkK github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= -github.com/aws/aws-sdk-go v1.30.22 h1:wImJ8jQrplgmxaTeUY7FrJFn4te/VtWq+mmmJ1TnWAg= -github.com/aws/aws-sdk-go v1.30.22/go.mod h1:5zCpMtNQVjRREroY7sYe8lOMRSxkhG6MZveU8YkpAk0= github.com/aws/aws-sdk-go v1.33.18 h1:Ccy1SV2SsgJU3rfrD+SOhQ0jvuzfrFuja/oKI86ruPw= github.com/aws/aws-sdk-go v1.33.18/go.mod h1:5zCpMtNQVjRREroY7sYe8lOMRSxkhG6MZveU8YkpAk0= -github.com/awslabs/goformation/v4 v4.8.0 h1:UiUhyokRy3suEqBXTnipvY8klqY3Eyl4GCH17brraEc= -github.com/awslabs/goformation/v4 v4.8.0/go.mod h1:GcJULxCJfloT+3pbqCluXftdEK2AD/UqpS3hkaaBntg= github.com/awslabs/goformation/v4 v4.14.0 h1:E2Pet9eIqA4qzt3dzzzE4YN83V4Kyfbcio0VokBC9TA= github.com/awslabs/goformation/v4 v4.14.0/go.mod h1:GcJULxCJfloT+3pbqCluXftdEK2AD/UqpS3hkaaBntg= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= @@ -58,14 +54,6 @@ github.com/cloudflare/cfssl v1.4.1 h1:vScfU2DrIUI9VPHBVeeAQ0q5A+9yshO1Gz+3QoUQiK github.com/cloudflare/cfssl v1.4.1/go.mod h1:KManx/OJPb5QY+y0+o/898AMcM128sF0bURvoVUSjTo= github.com/cloudflare/go-metrics v0.0.0-20151117154305-6a9aea36fb41/go.mod h1:eaZPlJWD+G9wseg1BuRXlHnjntPMrywMsyxf+LTOdP4= github.com/cloudflare/redoctober v0.0.0-20171127175943-746a508df14c/go.mod h1:6Se34jNoqrd8bTxrmJB2Bg2aoZ2CdSXonils9NsiNgo= -github.com/compose-spec/compose-go v0.0.0-20200624120600-614475470cd8 h1:sVvKsoXizFOuJNc8dM91IeET2/zDNFj3hwHgk437iJ8= -github.com/compose-spec/compose-go v0.0.0-20200624120600-614475470cd8/go.mod h1:ih9anT8po+49hrb+1j3ldIJ/YRAaBH52ErlQLTKE2Yo= -github.com/compose-spec/compose-go v0.0.0-20200707124823-710ff8e60ad9 h1:WkFqc6UpRqxROso9KC+ceaTiXx/VWpeO1x+NV0d4d+o= -github.com/compose-spec/compose-go v0.0.0-20200707124823-710ff8e60ad9/go.mod h1:ArodJ6gsEB7iWKrbV3fSHZ08LlBvSVB0Oqg04fX86t4= -github.com/compose-spec/compose-go v0.0.0-20200709084333-492a50989a5a h1:pIiSz5jML7rQ1aupg/KHlTqCxhyXvIgeDMf4kDTzIg8= -github.com/compose-spec/compose-go v0.0.0-20200709084333-492a50989a5a/go.mod h1:ArodJ6gsEB7iWKrbV3fSHZ08LlBvSVB0Oqg04fX86t4= -github.com/compose-spec/compose-go v0.0.0-20200710075715-6fcc35384ee1 h1:F+YIkKDMHdgZBacawhFY1P9RAIgO+6uv2te6hjsjzF0= -github.com/compose-spec/compose-go v0.0.0-20200710075715-6fcc35384ee1/go.mod h1:ArodJ6gsEB7iWKrbV3fSHZ08LlBvSVB0Oqg04fX86t4= github.com/compose-spec/compose-go v0.0.0-20200716130117-e87e4f7839e3 h1:+ntlMTrEcScJjlnEOP8P1IIrusJaR93Eazr66YgUueA= github.com/compose-spec/compose-go v0.0.0-20200716130117-e87e4f7839e3/go.mod h1:ArodJ6gsEB7iWKrbV3fSHZ08LlBvSVB0Oqg04fX86t4= github.com/containerd/cgroups v0.0.0-20190919134610-bf292b21730f/go.mod h1:OApqhQ4XNSNC13gXIwDjhOQxjWa/NxkwZXJ1EvqT0ko= @@ -80,6 +68,7 @@ github.com/containerd/fifo v0.0.0-20190226154929-a9fb20d87448/go.mod h1:ODA38xgv github.com/containerd/go-runc v0.0.0-20180907222934-5a6d9f37cfa3/go.mod h1:IV7qH3hrUgRmyYrtgEeGWJfWbgcHL9CSRruz2Vqcph0= github.com/containerd/ttrpc v0.0.0-20190828154514-0e0f228740de/go.mod h1:PvCDdDGpgqzQIzDW1TphrGLssLDZp2GuS+X5DkEJB8o= github.com/containerd/typeurl v0.0.0-20180627222232-a93fcdb778cd/go.mod h1:Cm3kwCdlkCfMSHURc+r6fwoGH6/F1hH3S4sg0rLFWPc= +github.com/coreos/etcd v3.3.10+incompatible h1:jFneRYjIvLMLhDLCzuTuU4rSJUjRplcJQ7pD7MnhC04= github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= @@ -475,6 +464,7 @@ gotest.tools/v3 v3.0.2 h1:kG1BFyqVHuQoVQiR1bWGnfz/fmHvvuiSPIV7rvl360E= gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +k8s.io/kubernetes v1.13.0 h1:qTfB+u5M92k2fCCCVP2iuhgwwSOv1EkAkvQY1tQODD8= k8s.io/kubernetes v1.13.0/go.mod h1:ocZa8+6APFNC2tX1DZASIbocyYT5jHzqFVsY5aoB7Jk= vbom.ml/util v0.0.0-20180919145318-efcd4e0f9787 h1:O69FD9pJA4WUZlEwYatBEEkRWKQ5cKodWpdKTrCS/iQ= vbom.ml/util v0.0.0-20180919145318-efcd4e0f9787/go.mod h1:so/NYdZXCz+E3ZpW0uAoCj6uzU2+8OWDFv/HxUSs7kI= diff --git a/ecs/pkg/amazon/backend/cloudformation.go b/ecs/pkg/amazon/backend/cloudformation.go index 54cebb20f..a47e1ad6c 100644 --- a/ecs/pkg/amazon/backend/cloudformation.go +++ b/ecs/pkg/amazon/backend/cloudformation.go @@ -2,9 +2,12 @@ package backend import ( "fmt" + "io/ioutil" "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" @@ -93,6 +96,30 @@ func (b Backend) Convert(project *types.Project) (*cloudformation.Template, erro networks[net.Name] = convertNetwork(project, net, cloudformation.Ref(ParameterVPCId), template) } + for i, s := range project.Secrets { + if s.External.External { + continue + } + secret, err := ioutil.ReadFile(s.File) + if err != nil { + return nil, err + } + + name := fmt.Sprintf("%sSecret", normalizeResourceName(s.Name)) + template.Resources[name] = &secretsmanager.Secret{ + Description: "", + SecretString: string(secret), + Tags: []tags.Tag{ + { + Key: compose.ProjectTag, + Value: project.Name, + }, + }, + } + s.Name = cloudformation.Ref(name) + project.Secrets[i] = s + } + logGroup := fmt.Sprintf("/docker-compose/%s", project.Name) template.Resources["LogGroup"] = &logs.LogGroup{ LogGroupName: logGroup, diff --git a/ecs/pkg/amazon/backend/compatibility.go b/ecs/pkg/amazon/backend/compatibility.go index 93806e96c..ebf915889 100644 --- a/ecs/pkg/amazon/backend/compatibility.go +++ b/ecs/pkg/amazon/backend/compatibility.go @@ -37,8 +37,14 @@ var compatibleComposeAttributes = []string{ "services.ports.mode", "services.ports.target", "services.ports.protocol", + "services.secrets", + "services.secrets.source", + "services.secrets.target", "services.user", "services.working_dir", + "secrets.external", + "secrets.name", + "secrets.file", } func (c *FargateCompatibilityChecker) CheckImage(service *types.ServiceConfig) { diff --git a/ecs/pkg/amazon/backend/convert.go b/ecs/pkg/amazon/backend/convert.go index b4ae15ae0..e024bde26 100644 --- a/ecs/pkg/amazon/backend/convert.go +++ b/ecs/pkg/amazon/backend/convert.go @@ -17,6 +17,8 @@ import ( "github.com/docker/ecs-plugin/pkg/compose" ) +const secretsInitContainerImage = "docker/ecs-secrets-sidecar" + func Convert(project *types.Project, service types.ServiceConfig) (*ecs.TaskDefinition, error) { cpu, mem, err := toLimits(service) if err != nil { @@ -37,50 +39,118 @@ func Convert(project *types.Project, service types.ServiceConfig) (*ecs.TaskDefi fmt.Sprintf(" %s.local", project.Name), })) - return &ecs.TaskDefinition{ - ContainerDefinitions: []ecs.TaskDefinition_ContainerDefinition{ - { - Command: service.Command, - DisableNetworking: service.NetworkMode == "none", - DnsSearchDomains: service.DNSSearch, - DnsServers: service.DNS, - DockerSecurityOptions: service.SecurityOpt, - EntryPoint: service.Entrypoint, - Environment: toKeyValuePair(service.Environment), - Essential: true, - ExtraHosts: toHostEntryPtr(service.ExtraHosts), - FirelensConfiguration: nil, - HealthCheck: toHealthCheck(service.HealthCheck), - Hostname: service.Hostname, - Image: service.Image, - Interactive: false, - Links: nil, - LinuxParameters: toLinuxParameters(service), - LogConfiguration: &ecs.TaskDefinition_LogConfiguration{ - LogDriver: ecsapi.LogDriverAwslogs, - Options: map[string]string{ - "awslogs-region": cloudformation.Ref("AWS::Region"), - "awslogs-group": cloudformation.Ref("LogGroup"), - "awslogs-stream-prefix": project.Name, - }, - }, - MemoryReservation: memReservation, - Name: service.Name, - PortMappings: toPortMappings(service.Ports), - Privileged: service.Privileged, - PseudoTerminal: service.Tty, - ReadonlyRootFilesystem: service.ReadOnly, - RepositoryCredentials: credential, - ResourceRequirements: nil, - StartTimeout: 0, - StopTimeout: durationToInt(service.StopGracePeriod), - SystemControls: toSystemControls(service.Sysctls), - Ulimits: toUlimits(service.Ulimits), - User: service.User, - VolumesFrom: nil, - WorkingDirectory: service.WorkingDir, - }, + logConfiguration := &ecs.TaskDefinition_LogConfiguration{ + LogDriver: ecsapi.LogDriverAwslogs, + Options: map[string]string{ + "awslogs-region": cloudformation.Ref("AWS::Region"), + "awslogs-group": cloudformation.Ref("LogGroup"), + "awslogs-stream-prefix": project.Name, }, + } + + var ( + containers []ecs.TaskDefinition_ContainerDefinition + volumes []ecs.TaskDefinition_Volume + mounts []ecs.TaskDefinition_MountPoint + initContainers []ecs.TaskDefinition_ContainerDependency + ) + if len(service.Secrets) > 0 { + volumes = append(volumes, ecs.TaskDefinition_Volume{ + Name: "secrets", + }) + mounts = append(mounts, ecs.TaskDefinition_MountPoint{ + ContainerPath: "/run/secrets/", + ReadOnly: true, + SourceVolume: "secrets", + }) + initContainers = append(initContainers, ecs.TaskDefinition_ContainerDependency{ + Condition: ecsapi.ContainerConditionSuccess, + ContainerName: "Secrets_InitContainer", + }) + + var ( + names []string + secrets []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{ + Name: s.Target, + ValueFrom: secretConfig.Name, + }) + name := s.Target + if ext, ok := secretConfig.Extensions[compose.ExtensionKeys]; ok { + var keys []string + if key, ok := ext.(string); ok { + keys = append(keys, key) + } else { + for _, k := range ext.([]interface{}) { + keys = append(keys, k.(string)) + } + } + name = fmt.Sprintf("%s:%s", s.Target, strings.Join(keys, ",")) + } + names = append(names, name) + } + containers = append(containers, ecs.TaskDefinition_ContainerDefinition{ + Name: fmt.Sprintf("%s_Secrets_InitContainer", normalizeResourceName(service.Name)), + Image: secretsInitContainerImage, + Command: names, + Essential: false, // FIXME this will be ignored, see https://github.com/awslabs/goformation/issues/61#issuecomment-625139607 + LogConfiguration: logConfiguration, + MountPoints: []ecs.TaskDefinition_MountPoint{ + { + ContainerPath: "/run/secrets/", + ReadOnly: false, + SourceVolume: "secrets", + }, + }, + Secrets: secrets, + }) + } + + containers = append(containers, ecs.TaskDefinition_ContainerDefinition{ + Command: service.Command, + DisableNetworking: service.NetworkMode == "none", + DependsOnProp: initContainers, + DnsSearchDomains: service.DNSSearch, + DnsServers: service.DNS, + DockerSecurityOptions: service.SecurityOpt, + EntryPoint: service.Entrypoint, + Environment: toKeyValuePair(service.Environment), + Essential: true, + ExtraHosts: toHostEntryPtr(service.ExtraHosts), + FirelensConfiguration: nil, + HealthCheck: toHealthCheck(service.HealthCheck), + Hostname: service.Hostname, + Image: service.Image, + Interactive: false, + Links: nil, + LinuxParameters: toLinuxParameters(service), + LogConfiguration: logConfiguration, + MemoryReservation: memReservation, + MountPoints: mounts, + Name: service.Name, + PortMappings: toPortMappings(service.Ports), + Privileged: service.Privileged, + PseudoTerminal: service.Tty, + ReadonlyRootFilesystem: service.ReadOnly, + RepositoryCredentials: credential, + ResourceRequirements: nil, + StartTimeout: 0, + StopTimeout: durationToInt(service.StopGracePeriod), + SystemControls: toSystemControls(service.Sysctls), + Ulimits: toUlimits(service.Ulimits), + User: service.User, + VolumesFrom: nil, + WorkingDirectory: service.WorkingDir, + }) + + return &ecs.TaskDefinition{ + ContainerDefinitions: containers, Cpu: cpu, Family: fmt.Sprintf("%s-%s", project.Name, service.Name), IpcMode: service.Ipc, @@ -90,6 +160,7 @@ func Convert(project *types.Project, service types.ServiceConfig) (*ecs.TaskDefi PlacementConstraints: toPlacementConstraints(service.Deploy), ProxyConfiguration: nil, RequiresCompatibilities: []string{ecsapi.LaunchTypeFargate}, + Volumes: volumes, }, nil } diff --git a/ecs/pkg/amazon/cloudformation/marshall.go b/ecs/pkg/amazon/cloudformation/marshall.go new file mode 100644 index 000000000..034bf8097 --- /dev/null +++ b/ecs/pkg/amazon/cloudformation/marshall.go @@ -0,0 +1,45 @@ +package cloudformation + +import ( + "encoding/json" + "fmt" + "strings" + + "github.com/awslabs/goformation/v4/cloudformation" +) + +func Marshall(template *cloudformation.Template) ([]byte, error) { + raw, err := template.JSON() + if err != nil { + return nil, err + } + + var unmarshalled interface{} + if err := json.Unmarshal(raw, &unmarshalled); err != nil { + return nil, fmt.Errorf("invalid JSON: %s", err) + } + + if input, ok := unmarshalled.(map[string]interface{}); ok { + if resources, ok := input["Resources"]; ok { + for _, uresource := range resources.(map[string]interface{}) { + if resource, ok := uresource.(map[string]interface{}); ok { + if resource["Type"] == "AWS::ECS::TaskDefinition" { + properties := resource["Properties"].(map[string]interface{}) + for _, def := range properties["ContainerDefinitions"].([]interface{}) { + containerDefinition := def.(map[string]interface{}) + if strings.HasSuffix(containerDefinition["Name"].(string), "_InitContainer") { + containerDefinition["Essential"] = "false" + } + } + } + } + } + } + } + + raw, err = json.MarshalIndent(unmarshalled, "", " ") + if err != nil { + return nil, fmt.Errorf("invalid JSON: %s", err) + } + return raw, err +} diff --git a/ecs/pkg/amazon/sdk/sdk.go b/ecs/pkg/amazon/sdk/sdk.go index da40fa524..057e9fffb 100644 --- a/ecs/pkg/amazon/sdk/sdk.go +++ b/ecs/pkg/amazon/sdk/sdk.go @@ -6,6 +6,8 @@ import ( "strings" "time" + cloudformation2 "github.com/docker/ecs-plugin/pkg/amazon/cloudformation" + "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/request" "github.com/aws/aws-sdk-go/aws/session" @@ -164,7 +166,7 @@ func (s sdk) StackExists(ctx context.Context, name string) (bool, error) { func (s sdk) CreateStack(ctx context.Context, name string, template *cf.Template, parameters map[string]string) error { logrus.Debug("Create CloudFormation stack") - json, err := template.JSON() + json, err := cloudformation2.Marshall(template) if err != nil { return err } @@ -192,7 +194,7 @@ func (s sdk) CreateStack(ctx context.Context, name string, template *cf.Template func (s sdk) CreateChangeSet(ctx context.Context, name string, template *cf.Template, parameters map[string]string) (string, error) { logrus.Debug("Create CloudFormation Changeset") - json, err := template.JSON() + json, err := cloudformation2.Marshall(template) if err != nil { return "", err } diff --git a/ecs/pkg/compose/x.go b/ecs/pkg/compose/x.go index 8e52a368f..777987afa 100644 --- a/ecs/pkg/compose/x.go +++ b/ecs/pkg/compose/x.go @@ -6,4 +6,5 @@ const ( ExtensionPullCredentials = "x-aws-pull_credentials" ExtensionLB = "x-aws-loadbalancer" ExtensionCluster = "x-aws-cluster" + ExtensionKeys = "x-aws-keys" ) diff --git a/ecs/secrets/Dockerfile b/ecs/secrets/Dockerfile new file mode 100644 index 000000000..7395d1250 --- /dev/null +++ b/ecs/secrets/Dockerfile @@ -0,0 +1,8 @@ +FROM golang:1.14.4-alpine AS builder +WORKDIR $GOPATH/src/github.com/docker/ecs-secrets +COPY . . +RUN GOOS=linux GOARCH=amd64 go build -ldflags="-w -s" -o /go/bin/secrets + +FROM scratch +COPY --from=builder /go/bin/secrets /secrets +ENTRYPOINT ["/secrets"] diff --git a/ecs/secrets/main.go b/ecs/secrets/main.go new file mode 100644 index 000000000..cc37325a5 --- /dev/null +++ b/ecs/secrets/main.go @@ -0,0 +1,85 @@ +package main + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "os" + "path/filepath" + "strings" +) + +// return codes: +// 1: failed to read secret from env +// 2: failed to parse hierarchical secret +// 3: failed to write secret content into file +func main() { + for _, name := range os.Args[1:] { + 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) + } + } + } +} + +func contains(keys []string, s string) bool { + for _, k := range keys { + if k == s { + return true + } + } + return false +}