Selaa lähdekoodia

use an initContainer to inject secrets as /run/secrets/xx

Signed-off-by: Nicolas De Loof <[email protected]>
Nicolas De Loof 5 vuotta sitten
vanhempi
sitoutus
85b3cbd6ea

+ 4 - 2
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
 		}),

+ 2 - 12
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=

+ 27 - 0
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,

+ 6 - 0
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) {

+ 113 - 42
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,
-					},
+	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",
 				},
-				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,
 			},
-		},
+			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
 }
 

+ 45 - 0
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
+}

+ 4 - 2
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
 	}

+ 1 - 0
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"
 )

+ 8 - 0
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"]

+ 85 - 0
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
+}