diff --git a/aci/convert/convert.go b/aci/convert/convert.go index d0e2ba231..31bee1a12 100644 --- a/aci/convert/convert.go +++ b/aci/convert/convert.go @@ -43,8 +43,10 @@ const ( StatusRunning = "Running" // ComposeDNSSidecarName name of the dns sidecar container ComposeDNSSidecarName = "aci--dns--sidecar" - dnsSidecarImage = "busybox:1.31.1" + // ExtensionDomainName compose extension to set ACI DNS label name + ExtensionDomainName = "x-aci-domain-name" + dnsSidecarImage = "busybox:1.31.1" azureFileDriverName = "azure_file" volumeDriveroptsShareNameKey = "share_name" volumeDriveroptsAccountNameKey = "storage_account_name" @@ -103,26 +105,16 @@ func ToContainerGroup(ctx context.Context, aciContext store.AciContext, p types. return containerinstance.ContainerGroup{}, errors.New("ACI integration does not support labels in compose applications") } if service.Ports != nil { - var containerPorts []containerinstance.ContainerPort - for _, portConfig := range service.Ports { - if portConfig.Published != 0 && portConfig.Published != portConfig.Target { - msg := fmt.Sprintf("Port mapping is not supported with ACI, cannot map port %d to %d for container %s", - portConfig.Published, portConfig.Target, service.Name) - return groupDefinition, errors.New(msg) - } - portNumber := int32(portConfig.Target) - containerPorts = append(containerPorts, containerinstance.ContainerPort{ - Port: to.Int32Ptr(portNumber), - }) - groupPorts = append(groupPorts, containerinstance.Port{ - Port: to.Int32Ptr(portNumber), - Protocol: containerinstance.TCP, - }) + containerPorts, serviceGroupPorts, dnsLabelName, err := convertPortsToAci(service, p) + if err != nil { + return groupDefinition, err } containerDefinition.ContainerProperties.Ports = &containerPorts + groupPorts = append(groupPorts, serviceGroupPorts...) groupDefinition.ContainerGroupProperties.IPAddress = &containerinstance.IPAddress{ - Type: containerinstance.Public, - Ports: &groupPorts, + Type: containerinstance.Public, + Ports: &groupPorts, + DNSNameLabel: dnsLabelName, } } @@ -137,6 +129,35 @@ func ToContainerGroup(ctx context.Context, aciContext store.AciContext, p types. return groupDefinition, nil } +func convertPortsToAci(service serviceConfigAciHelper, p types.Project) ([]containerinstance.ContainerPort, []containerinstance.Port, *string, error) { + var groupPorts []containerinstance.Port + var containerPorts []containerinstance.ContainerPort + for _, portConfig := range service.Ports { + if portConfig.Published != 0 && portConfig.Published != portConfig.Target { + msg := fmt.Sprintf("Port mapping is not supported with ACI, cannot map port %d to %d for container %s", + portConfig.Published, portConfig.Target, service.Name) + return nil, nil, nil, errors.New(msg) + } + portNumber := int32(portConfig.Target) + containerPorts = append(containerPorts, containerinstance.ContainerPort{ + Port: to.Int32Ptr(portNumber), + }) + groupPorts = append(groupPorts, containerinstance.Port{ + Port: to.Int32Ptr(portNumber), + Protocol: containerinstance.TCP, + }) + } + var dnsLabelName *string = nil + if extension, ok := p.Extensions[ExtensionDomainName]; ok { + domain, ok := extension.(string) + if !ok { + return nil, nil, nil, fmt.Errorf("could not read %s compose extension as string", ExtensionDomainName) + } + dnsLabelName = &domain + } + return containerPorts, groupPorts, dnsLabelName, nil +} + func getDNSSidecar(containers []containerinstance.Container) containerinstance.Container { var commands []string for _, container := range containers { diff --git a/cli/cmd/compose/compose.go b/cli/cmd/compose/compose.go index e58243d64..8e8211208 100644 --- a/cli/cmd/compose/compose.go +++ b/cli/cmd/compose/compose.go @@ -29,6 +29,7 @@ import ( type composeOptions struct { Name string + DomainName string WorkingDir string ConfigPaths []string Environment []string @@ -60,7 +61,7 @@ func (o *composeOptions) toProjectOptions() (*cli.ProjectOptions, error) { } // Command returns the compose command with its child commands -func Command() *cobra.Command { +func Command(contextType string) *cobra.Command { command := &cobra.Command{ Short: "Docker Compose", Use: "compose", @@ -70,7 +71,7 @@ func Command() *cobra.Command { } command.AddCommand( - upCommand(), + upCommand(contextType), downCommand(), psCommand(), listCommand(), diff --git a/cli/cmd/compose/up.go b/cli/cmd/compose/up.go index 3c5ec616f..957d809b5 100644 --- a/cli/cmd/compose/up.go +++ b/cli/cmd/compose/up.go @@ -20,14 +20,15 @@ import ( "context" "github.com/compose-spec/compose-go/cli" - "github.com/spf13/cobra" + aciconvert "github.com/docker/compose-cli/aci/convert" "github.com/docker/compose-cli/api/client" + "github.com/docker/compose-cli/context/store" "github.com/docker/compose-cli/progress" ) -func upCommand() *cobra.Command { +func upCommand(contextType string) *cobra.Command { opts := composeOptions{} upCmd := &cobra.Command{ Use: "up", @@ -40,6 +41,11 @@ func upCommand() *cobra.Command { upCmd.Flags().StringArrayVarP(&opts.ConfigPaths, "file", "f", []string{}, "Compose configuration files") upCmd.Flags().StringArrayVarP(&opts.Environment, "environment", "e", []string{}, "Environment variables") upCmd.Flags().BoolP("detach", "d", true, " Detached mode: Run containers in the background") + + if contextType == store.AciContextType { + upCmd.Flags().StringVar(&opts.DomainName, "domainname", "", "Container NIS domain name") + } + return upCmd } @@ -55,6 +61,9 @@ func runUp(ctx context.Context, opts composeOptions) error { return "", err } project, err := cli.ProjectFromOptions(options) + if opts.DomainName != "" { + project.Extensions = map[string]interface{}{aciconvert.ExtensionDomainName: opts.DomainName} + } if err != nil { return "", err } diff --git a/cli/main.go b/cli/main.go index ead230b1a..eb8a64f15 100644 --- a/cli/main.go +++ b/cli/main.go @@ -126,7 +126,6 @@ func main() { cmd.StopCommand(), cmd.KillCommand(), cmd.SecretCommand(), - compose.Command(), // Place holders cmd.EcsCommand(), @@ -179,7 +178,10 @@ func main() { ctype = cc.Type() } - root.AddCommand(run.Command(ctype)) + root.AddCommand( + run.Command(ctype), + compose.Command(ctype), + ) if ctype == store.AciContextType { // we can also pass ctype as a parameter to the volume command and customize subcommands, flags, etc. when we have other backend implementations diff --git a/tests/aci-e2e/e2e-aci_test.go b/tests/aci-e2e/e2e-aci_test.go index b136f6c36..3456ffc6e 100644 --- a/tests/aci-e2e/e2e-aci_test.go +++ b/tests/aci-e2e/e2e-aci_test.go @@ -453,7 +453,7 @@ func TestContainerRunAttached(t *testing.T) { func TestComposeUpUpdate(t *testing.T) { c := NewParallelE2eCLI(t, binDir) - _, _ = setupTestResourceGroup(t, c) + _, groupID := setupTestResourceGroup(t, c) const ( composeFile = "../composefiles/aci-demo/aci_demo_port.yaml" @@ -465,8 +465,11 @@ func TestComposeUpUpdate(t *testing.T) { ) t.Run("compose up", func(t *testing.T) { + dnsLabelName := "nginx-" + groupID + fqdn := dnsLabelName + "." + location + ".azurecontainer.io" // Name of Compose project is taken from current folder "acie2e" - c.RunDockerCmd("compose", "up", "-f", composeFile) + c.RunDockerCmd("compose", "up", "-f", composeFile, "--domainname", dnsLabelName) + res := c.RunDockerCmd("ps") out := lines(res.Stdout()) // Check three containers are running @@ -493,6 +496,11 @@ func TestComposeUpUpdate(t *testing.T) { b, err := ioutil.ReadAll(r.Body) assert.NilError(t, err) assert.Assert(t, strings.Contains(string(b), `"word":`)) + + endpoint = fmt.Sprintf("http://%s:%d", fqdn, containerInspect.Ports[0].HostPort) + r, err = HTTPGetWithRetry(endpoint+"/words/noun", 3) + assert.NilError(t, err) + assert.Equal(t, r.StatusCode, http.StatusOK) }) t.Run("compose ps", func(t *testing.T) {