diff --git a/ecs/cloudformation.go b/ecs/cloudformation.go index b9f7c61c0..1da5e4089 100644 --- a/ecs/cloudformation.go +++ b/ecs/cloudformation.go @@ -93,7 +93,7 @@ func (b *ecsAPIService) convert(project *types.Project, resources awsResources) taskExecutionRole := b.createTaskExecutionRole(project, service, template) taskRole := b.createTaskRole(project, service, template) - definition, err := b.createTaskExecution(project, service) + definition, err := b.createTaskDefinition(project, service) if err != nil { return nil, err } diff --git a/ecs/cloudformation_test.go b/ecs/cloudformation_test.go index 87491a419..1929ba8cf 100644 --- a/ecs/cloudformation_test.go +++ b/ecs/cloudformation_test.go @@ -56,8 +56,12 @@ services: x-aws-logs_retention: 10 `) def := template.Resources["FooTaskDefinition"].(*ecs.TaskDefinition) - logging := def.ContainerDefinitions[0].LogConfiguration - assert.Equal(t, logging.Options["awslogs-datetime-pattern"], "FOO") + logging := getMainContainer(def, t).LogConfiguration + if logging != nil { + assert.Equal(t, logging.Options["awslogs-datetime-pattern"], "FOO") + } else { + t.Fatal("Logging not configured") + } logGroup := template.Resources["LogGroup"].(*logs.LogGroup) assert.Equal(t, logGroup.RetentionInDays, 10) @@ -72,7 +76,7 @@ services: - testdata/input/envfile `) def := template.Resources["FooTaskDefinition"].(*ecs.TaskDefinition) - env := def.ContainerDefinitions[0].Environment + env := getMainContainer(def, t).Environment var found bool for _, pair := range env { if pair.Name == "FOO" { @@ -94,7 +98,7 @@ services: - "FOO=ZOT" `) def := template.Resources["FooTaskDefinition"].(*ecs.TaskDefinition) - env := def.ContainerDefinitions[0].Environment + env := getMainContainer(def, t).Environment var found bool for _, pair := range env { if pair.Name == "FOO" { @@ -358,7 +362,7 @@ services: working_dir: "working_dir" `) def := template.Resources["TestTaskDefinition"].(*ecs.TaskDefinition) - container := def.ContainerDefinitions[0] + container := getMainContainer(def, t) assert.Equal(t, container.Image, "image") assert.Equal(t, container.Command[0], "command") assert.Equal(t, container.EntryPoint[0], "entrypoint") @@ -446,3 +450,13 @@ func loadConfig(t *testing.T, yaml string) *types.Project { assert.NilError(t, err) return model } + +func getMainContainer(def *ecs.TaskDefinition, t *testing.T) ecs.TaskDefinition_ContainerDefinition { + for _, c := range def.ContainerDefinitions { + if c.Essential { + return c + } + } + t.Fail() + return def.ContainerDefinitions[0] +} diff --git a/ecs/convert.go b/ecs/convert.go index 3b0636fb7..8537a0a15 100644 --- a/ecs/convert.go +++ b/ecs/convert.go @@ -26,7 +26,7 @@ import ( "strings" "time" - "github.com/aws/aws-sdk-go/aws" + "github.com/docker/compose-cli/ecs/secrets" ecsapi "github.com/aws/aws-sdk-go/service/ecs" "github.com/awslabs/goformation/v4/cloudformation" @@ -34,13 +34,12 @@ import ( "github.com/compose-spec/compose-go/types" "github.com/docker/cli/opts" "github.com/joho/godotenv" - - "github.com/docker/compose-cli/ecs/secrets" ) const secretsInitContainerImage = "docker/ecs-secrets-sidecar" +const searchDomainInitContainerImage = "docker/ecs-searchdomain-sidecar" -func (b *ecsAPIService) createTaskExecution(project *types.Project, service types.ServiceConfig) (*ecs.TaskDefinition, error) { +func (b *ecsAPIService) createTaskDefinition(project *types.Project, service types.ServiceConfig) (*ecs.TaskDefinition, error) { cpu, mem, err := toLimits(service) if err != nil { return nil, err @@ -48,15 +47,6 @@ func (b *ecsAPIService) createTaskExecution(project *types.Project, service type _, memReservation := toContainerReservation(service) credential := getRepoCredentials(service) - // override resolve.conf search directive to also search .local - // TODO remove once ECS support hostname-only service discovery - service.Environment["LOCALDOMAIN"] = aws.String( - cloudformation.Join("", []string{ - cloudformation.Ref("AWS::Region"), - ".compute.internal", - fmt.Sprintf(" %s.local", project.Name), - })) - logConfiguration := getLogConfiguration(service, project) var ( @@ -74,6 +64,14 @@ func (b *ecsAPIService) createTaskExecution(project *types.Project, service type mounts = append(mounts, secretsMount) } + initContainers = append(initContainers, ecs.TaskDefinition_ContainerDefinition{ + Name: fmt.Sprintf("%s_ResolvConf_InitContainer", normalizeResourceName(service.Name)), + Image: searchDomainInitContainerImage, + Essential: false, + Command: []string{b.Region + ".compute.internal", project.Name + ".local"}, + LogConfiguration: logConfiguration, + }) + var dependencies []ecs.TaskDefinition_ContainerDependency for _, c := range initContainers { dependencies = append(dependencies, ecs.TaskDefinition_ContainerDependency{ diff --git a/ecs/resolv/Dockerfile b/ecs/resolv/Dockerfile new file mode 100644 index 000000000..679c4a909 --- /dev/null +++ b/ecs/resolv/Dockerfile @@ -0,0 +1,23 @@ +# Copyright 2020 Docker Compose CLI authors + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +FROM golang:1.14.4-alpine AS builder +WORKDIR $GOPATH/src/github.com/docker/compose-cli/ecs/resolv +COPY . . +RUN GOOS=linux GOARCH=amd64 go build -ldflags="-w -s" -o /go/bin/resolv main/main.go +RUN chmod +x /go/bin/resolv + +FROM scratch +COPY --from=builder /go/bin/resolv /resolv +ENTRYPOINT ["/resolv"] diff --git a/ecs/resolv/main/main.go b/ecs/resolv/main/main.go new file mode 100644 index 000000000..56663c925 --- /dev/null +++ b/ecs/resolv/main/main.go @@ -0,0 +1,39 @@ +/* + Copyright 2020 Docker Compose CLI authors + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package main + +import ( + "fmt" + "os" + + "github.com/docker/compose-cli/ecs/resolv" +) + +const resolvconf = "/etc/resolv.conf" + +func main() { + if len(os.Args) < 2 { + fmt.Fprint(os.Stderr, "usage: resolv DOMAIN [DOMAIN]") + os.Exit(1) + } + + err := resolv.SetSearchDomains(resolvconf, os.Args[1:]...) + if err != nil { + fmt.Fprint(os.Stderr, err.Error()) + os.Exit(1) + } +} diff --git a/ecs/resolv/resolv.go b/ecs/resolv/resolv.go new file mode 100644 index 000000000..f516405a0 --- /dev/null +++ b/ecs/resolv/resolv.go @@ -0,0 +1,35 @@ +/* + Copyright 2020 Docker Compose CLI authors + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package resolv + +import ( + "os" + "strings" +) + +// SetSearchDomains appends a `search` directive to resolv.conf file for domains +func SetSearchDomains(file string, domains ...string) error { + search := strings.Join(domains, " ") + + f, err := os.OpenFile(file, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) + if err != nil { + return err + } + defer f.Close() //nolint:errcheck + _, err = f.WriteString("\nsearch " + search) + return err +} diff --git a/ecs/resolv/resolv_test.go b/ecs/resolv/resolv_test.go new file mode 100644 index 000000000..2dd37e320 --- /dev/null +++ b/ecs/resolv/resolv_test.go @@ -0,0 +1,48 @@ +/* + Copyright 2020 Docker Compose CLI authors + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package resolv + +import ( + "io/ioutil" + "os" + "path/filepath" + "testing" + + "gotest.tools/v3/assert" + "gotest.tools/v3/fs" + "gotest.tools/v3/golden" +) + +func TestSetDomain(t *testing.T) { + dir := fs.NewDir(t, "resolv").Path() + f := filepath.Join(dir, "resolv.conf") + touch(t, f) + + err := SetSearchDomains(f, "foo", "bar", "zot") + assert.NilError(t, err) + + got, err := ioutil.ReadFile(f) + assert.NilError(t, err) + golden.Assert(t, string(got), "resolv.conf.golden") +} + +func touch(t *testing.T, f string) { + file, err := os.Create(f) + assert.NilError(t, err) + err = file.Close() + assert.NilError(t, err) +} diff --git a/ecs/resolv/testdata/resolv.conf.golden b/ecs/resolv/testdata/resolv.conf.golden new file mode 100644 index 000000000..ed0e2453a --- /dev/null +++ b/ecs/resolv/testdata/resolv.conf.golden @@ -0,0 +1,2 @@ + +search foo bar zot \ No newline at end of file diff --git a/ecs/testdata/simple/simple-cloudformation-conversion.golden b/ecs/testdata/simple/simple-cloudformation-conversion.golden index 07200659c..a5833d43e 100644 --- a/ecs/testdata/simple/simple-cloudformation-conversion.golden +++ b/ecs/testdata/simple/simple-cloudformation-conversion.golden @@ -226,21 +226,31 @@ "Properties": { "ContainerDefinitions": [ { - "Environment": [ + "Command": [ + ".compute.internal", + "TestSimpleConvert.local" + ], + "Essential": "false", + "Image": "docker/ecs-searchdomain-sidecar", + "LogConfiguration": { + "LogDriver": "awslogs", + "Options": { + "awslogs-group": { + "Ref": "LogGroup" + }, + "awslogs-region": { + "Ref": "AWS::Region" + }, + "awslogs-stream-prefix": "TestSimpleConvert" + } + }, + "Name": "Simple_ResolvConf_InitContainer" + }, + { + "DependsOn": [ { - "Name": "LOCALDOMAIN", - "Value": { - "Fn::Join": [ - "", - [ - { - "Ref": "AWS::Region" - }, - ".compute.internal", - " TestSimpleConvert.local" - ] - ] - } + "Condition": "SUCCESS", + "ContainerName": "Simple_ResolvConf_InitContainer" } ], "Essential": true,