Merge pull request #631 from docker/aci_domainname

ACI: allow users to set DNSLabelName and deploy containers with fqdn like `myapp.eastus.azurecontainers.io`
This commit is contained in:
Guillaume Tardif 2020-09-22 15:00:28 +02:00 committed by GitHub
commit d42a931d67
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 264 additions and 65 deletions

View File

@ -89,7 +89,7 @@ func (cs *aciComposeService) Ps(ctx context.Context, project string) ([]compose.
if isContainerVisible(container, group, false) { if isContainerVisible(container, group, false) {
continue continue
} }
res = append(res, convert.ContainerGroupToServiceStatus(getContainerID(group, container), group, container)) res = append(res, convert.ContainerGroupToServiceStatus(getContainerID(group, container), group, container, cs.ctx.Location))
} }
return res, nil return res, nil
} }

View File

@ -63,7 +63,7 @@ func (cs *aciContainerService) List(ctx context.Context, all bool) ([]containers
if isContainerVisible(container, group, all) { if isContainerVisible(container, group, all) {
continue continue
} }
c := convert.ContainerGroupToContainer(getContainerID(group, container), group, container) c := convert.ContainerGroupToContainer(getContainerID(group, container), group, container, cs.ctx.Location)
res = append(res, c) res = append(res, c)
} }
} }
@ -257,5 +257,5 @@ func (cs *aciContainerService) Inspect(ctx context.Context, containerID string)
return containers.Container{}, errdefs.ErrNotFound return containers.Container{}, errdefs.ErrNotFound
} }
return convert.ContainerGroupToContainer(containerID, cg, cc), nil return convert.ContainerGroupToContainer(containerID, cg, cc, cs.ctx.Location), nil
} }

View File

@ -49,6 +49,7 @@ func ContainerToComposeProject(r containers.ContainerConfig) (types.Project, err
Ports: ports, Ports: ports,
Labels: r.Labels, Labels: r.Labels,
Volumes: serviceConfigVolumes, Volumes: serviceConfigVolumes,
DomainName: r.DomainName,
Environment: toComposeEnvs(r.Environment), Environment: toComposeEnvs(r.Environment),
Deploy: &types.DeployConfig{ Deploy: &types.DeployConfig{
Resources: types.Resources{ Resources: types.Resources{

View File

@ -54,6 +54,18 @@ func TestConvertRestartPolicy(t *testing.T) {
assert.Equal(t, service1.Deploy.RestartPolicy.Condition, "none") assert.Equal(t, service1.Deploy.RestartPolicy.Condition, "none")
} }
func TestConvertDomainName(t *testing.T) {
container := containers.ContainerConfig{
ID: "container1",
DomainName: "myapp",
}
project, err := ContainerToComposeProject(container)
assert.NilError(t, err)
service1 := project.Services[0]
assert.Equal(t, service1.Name, container.ID)
assert.Equal(t, service1.DomainName, "myapp")
}
func TestConvertEnvVariables(t *testing.T) { func TestConvertEnvVariables(t *testing.T) {
container := containers.ContainerConfig{ container := containers.ContainerConfig{
ID: "container1", ID: "container1",

View File

@ -43,8 +43,8 @@ const (
StatusRunning = "Running" StatusRunning = "Running"
// ComposeDNSSidecarName name of the dns sidecar container // ComposeDNSSidecarName name of the dns sidecar container
ComposeDNSSidecarName = "aci--dns--sidecar" ComposeDNSSidecarName = "aci--dns--sidecar"
dnsSidecarImage = "busybox:1.31.1"
dnsSidecarImage = "busybox:1.31.1"
azureFileDriverName = "azure_file" azureFileDriverName = "azure_file"
volumeDriveroptsShareNameKey = "share_name" volumeDriveroptsShareNameKey = "share_name"
volumeDriveroptsAccountNameKey = "storage_account_name" volumeDriveroptsAccountNameKey = "storage_account_name"
@ -93,6 +93,7 @@ func ToContainerGroup(ctx context.Context, aciContext store.AciContext, p types.
} }
var groupPorts []containerinstance.Port var groupPorts []containerinstance.Port
var dnsLabelName *string
for _, s := range project.Services { for _, s := range project.Services {
service := serviceConfigAciHelper(s) service := serviceConfigAciHelper(s)
containerDefinition, err := service.getAciContainer(volumesCache) containerDefinition, err := service.getAciContainer(volumesCache)
@ -102,32 +103,29 @@ func ToContainerGroup(ctx context.Context, aciContext store.AciContext, p types.
if service.Labels != nil && len(service.Labels) > 0 { if service.Labels != nil && len(service.Labels) > 0 {
return containerinstance.ContainerGroup{}, errors.New("ACI integration does not support labels in compose applications") return containerinstance.ContainerGroup{}, errors.New("ACI integration does not support labels in compose applications")
} }
if service.Ports != nil {
var containerPorts []containerinstance.ContainerPort containerPorts, serviceGroupPorts, serviceDomainName, err := convertPortsToAci(service)
for _, portConfig := range service.Ports { if err != nil {
if portConfig.Published != 0 && portConfig.Published != portConfig.Target { return groupDefinition, err
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) containerDefinition.ContainerProperties.Ports = &containerPorts
return groupDefinition, errors.New(msg) groupPorts = append(groupPorts, serviceGroupPorts...)
} if serviceDomainName != nil {
portNumber := int32(portConfig.Target) if dnsLabelName != nil && *serviceDomainName != *dnsLabelName {
containerPorts = append(containerPorts, containerinstance.ContainerPort{ return containerinstance.ContainerGroup{}, fmt.Errorf("ACI integration does not support specifying different domain names on services in the same compose application")
Port: to.Int32Ptr(portNumber),
})
groupPorts = append(groupPorts, containerinstance.Port{
Port: to.Int32Ptr(portNumber),
Protocol: containerinstance.TCP,
})
}
containerDefinition.ContainerProperties.Ports = &containerPorts
groupDefinition.ContainerGroupProperties.IPAddress = &containerinstance.IPAddress{
Type: containerinstance.Public,
Ports: &groupPorts,
} }
dnsLabelName = serviceDomainName
} }
containers = append(containers, containerDefinition) containers = append(containers, containerDefinition)
} }
if len(groupPorts) > 0 {
groupDefinition.ContainerGroupProperties.IPAddress = &containerinstance.IPAddress{
Type: containerinstance.Public,
Ports: &groupPorts,
DNSNameLabel: dnsLabelName,
}
}
if len(containers) > 1 { if len(containers) > 1 {
dnsSideCar := getDNSSidecar(containers) dnsSideCar := getDNSSidecar(containers)
containers = append(containers, dnsSideCar) containers = append(containers, dnsSideCar)
@ -137,6 +135,31 @@ func ToContainerGroup(ctx context.Context, aciContext store.AciContext, p types.
return groupDefinition, nil return groupDefinition, nil
} }
func convertPortsToAci(service serviceConfigAciHelper) ([]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 service.DomainName != "" {
dnsLabelName = &service.DomainName
}
return containerPorts, groupPorts, dnsLabelName, nil
}
func getDNSSidecar(containers []containerinstance.Container) containerinstance.Container { func getDNSSidecar(containers []containerinstance.Container) containerinstance.Container {
var commands []string var commands []string
for _, container := range containers { for _, container := range containers {
@ -249,7 +272,7 @@ func (p projectAciHelper) getRestartPolicy() (containerinstance.ContainerGroupRe
restartPolicyCondition = toAciRestartPolicy(service.Deploy.RestartPolicy.Condition) restartPolicyCondition = toAciRestartPolicy(service.Deploy.RestartPolicy.Condition)
} }
if alreadySpecified && restartPolicyCondition != toAciRestartPolicy(service.Deploy.RestartPolicy.Condition) { if alreadySpecified && restartPolicyCondition != toAciRestartPolicy(service.Deploy.RestartPolicy.Condition) {
return "", errors.New("ACI integration does not support specifying different restart policies on containers in the same compose application") return "", errors.New("ACI integration does not support specifying different restart policies on services in the same compose application")
} }
} }
@ -390,7 +413,7 @@ func bytesToGb(b types.UnitBytes) float64 {
} }
// ContainerGroupToServiceStatus convert from an ACI container definition to service status // ContainerGroupToServiceStatus convert from an ACI container definition to service status
func ContainerGroupToServiceStatus(containerID string, group containerinstance.ContainerGroup, container containerinstance.Container) compose.ServiceStatus { func ContainerGroupToServiceStatus(containerID string, group containerinstance.ContainerGroup, container containerinstance.Container, region string) compose.ServiceStatus {
var replicas = 1 var replicas = 1
if GetStatus(container, group) != StatusRunning { if GetStatus(container, group) != StatusRunning {
replicas = 0 replicas = 0
@ -398,14 +421,22 @@ func ContainerGroupToServiceStatus(containerID string, group containerinstance.C
return compose.ServiceStatus{ return compose.ServiceStatus{
ID: containerID, ID: containerID,
Name: *container.Name, Name: *container.Name,
Ports: formatter.PortsToStrings(ToPorts(group.IPAddress, *container.Ports)), Ports: formatter.PortsToStrings(ToPorts(group.IPAddress, *container.Ports), fqdn(group, region)),
Replicas: replicas, Replicas: replicas,
Desired: 1, Desired: 1,
} }
} }
func fqdn(group containerinstance.ContainerGroup, region string) string {
fqdn := ""
if group.IPAddress != nil && group.IPAddress.DNSNameLabel != nil && *group.IPAddress.DNSNameLabel != "" {
fqdn = *group.IPAddress.DNSNameLabel + "." + region + ".azurecontainer.io"
}
return fqdn
}
// ContainerGroupToContainer composes a Container from an ACI container definition // ContainerGroupToContainer composes a Container from an ACI container definition
func ContainerGroupToContainer(containerID string, cg containerinstance.ContainerGroup, cc containerinstance.Container) containers.Container { func ContainerGroupToContainer(containerID string, cg containerinstance.ContainerGroup, cc containerinstance.Container, region string) containers.Container {
memLimits := 0. memLimits := 0.
if cc.Resources != nil && if cc.Resources != nil &&
cc.Resources.Limits != nil && cc.Resources.Limits != nil &&
@ -436,9 +467,9 @@ func ContainerGroupToContainer(containerID string, cg containerinstance.Containe
} }
} }
var config *containers.RuntimeConfig = nil var config *containers.RuntimeConfig = &containers.RuntimeConfig{FQDN: fqdn(cg, region)}
if envVars != nil { if envVars != nil {
config = &containers.RuntimeConfig{Env: envVars} config.Env = envVars
} }
c := containers.Container{ c := containers.Container{
ID: containerID, ID: containerID,

View File

@ -59,7 +59,8 @@ func TestContainerGroupToContainer(t *testing.T) {
Ports: &[]containerinstance.Port{{ Ports: &[]containerinstance.Port{{
Port: to.Int32Ptr(80), Port: to.Int32Ptr(80),
}}, }},
IP: to.StringPtr("42.42.42.42"), IP: to.StringPtr("42.42.42.42"),
DNSNameLabel: to.StringPtr("myapp"),
}, },
OsType: "Linux", OsType: "Linux",
}, },
@ -102,10 +103,13 @@ func TestContainerGroupToContainer(t *testing.T) {
Protocol: "tcp", Protocol: "tcp",
HostIP: "42.42.42.42", HostIP: "42.42.42.42",
}}, }},
Config: &containers.RuntimeConfig{
FQDN: "myapp.eastus.azurecontainer.io",
},
RestartPolicyCondition: "any", RestartPolicyCondition: "any",
} }
container := ContainerGroupToContainer("myContainerID", myContainerGroup, myContainer) container := ContainerGroupToContainer("myContainerID", myContainerGroup, myContainer, "eastus")
assert.DeepEqual(t, container, expectedContainer) assert.DeepEqual(t, container, expectedContainer)
} }
@ -143,7 +147,7 @@ func TestContainerGroupToServiceStatus(t *testing.T) {
Desired: 1, Desired: 1,
} }
container := ContainerGroupToServiceStatus("myContainerID", myContainerGroup, myContainer) container := ContainerGroupToServiceStatus("myContainerID", myContainerGroup, myContainer, "eastus")
assert.DeepEqual(t, container, expectedService) assert.DeepEqual(t, container, expectedService)
} }
@ -366,7 +370,7 @@ func TestComposeInconsistentMultiContainerRestartPolicy(t *testing.T) {
} }
_, err := ToContainerGroup(context.TODO(), convertCtx, project, mockStorageHelper) _, err := ToContainerGroup(context.TODO(), convertCtx, project, mockStorageHelper)
assert.Error(t, err, "ACI integration does not support specifying different restart policies on containers in the same compose application") assert.Error(t, err, "ACI integration does not support specifying different restart policies on services in the same compose application")
} }
func TestLabelsErrorMessage(t *testing.T) { func TestLabelsErrorMessage(t *testing.T) {
@ -448,6 +452,88 @@ func TestComposeContainerGroupToContainerMultiplePorts(t *testing.T) {
assert.Assert(t, is.Len(groupPorts, 2)) assert.Assert(t, is.Len(groupPorts, 2))
assert.Equal(t, *groupPorts[0].Port, int32(80)) assert.Equal(t, *groupPorts[0].Port, int32(80))
assert.Equal(t, *groupPorts[1].Port, int32(8080)) assert.Equal(t, *groupPorts[1].Port, int32(8080))
assert.Assert(t, group.IPAddress.DNSNameLabel == nil)
}
func TestComposeContainerGroupToContainerWithDomainName(t *testing.T) {
project := types.Project{
Services: []types.ServiceConfig{
{
Name: "service1",
Image: "image1",
Ports: []types.ServicePortConfig{
{
Published: 80,
Target: 80,
},
},
DomainName: "myApp",
},
{
Name: "service2",
Image: "image2",
Ports: []types.ServicePortConfig{
{
Published: 8080,
Target: 8080,
},
},
},
},
}
group, err := ToContainerGroup(context.TODO(), convertCtx, project, mockStorageHelper)
assert.NilError(t, err)
assert.Assert(t, is.Len(*group.Containers, 3))
groupPorts := *group.IPAddress.Ports
assert.Assert(t, is.Len(groupPorts, 2))
assert.Equal(t, *groupPorts[0].Port, int32(80))
assert.Equal(t, *groupPorts[1].Port, int32(8080))
assert.Equal(t, *group.IPAddress.DNSNameLabel, "myApp")
}
func TestComposeContainerGroupToContainerErrorWhenSeveralDomainNames(t *testing.T) {
project := types.Project{
Services: []types.ServiceConfig{
{
Name: "service1",
Image: "image1",
DomainName: "myApp",
},
{
Name: "service2",
Image: "image2",
DomainName: "myApp2",
},
},
}
_, err := ToContainerGroup(context.TODO(), convertCtx, project, mockStorageHelper)
assert.Error(t, err, "ACI integration does not support specifying different domain names on services in the same compose application")
}
// ACI fails if group definition IPAddress has no ports
func TestComposeContainerGroupToContainerIgnoreDomainNameWithoutPorts(t *testing.T) {
project := types.Project{
Services: []types.ServiceConfig{
{
Name: "service1",
Image: "image1",
DomainName: "myApp",
},
{
Name: "service2",
Image: "image2",
DomainName: "myApp",
},
},
}
group, err := ToContainerGroup(context.TODO(), convertCtx, project, mockStorageHelper)
assert.NilError(t, err)
assert.Assert(t, is.Len(*group.Containers, 3))
assert.Assert(t, group.IPAddress == nil)
} }
func TestComposeContainerGroupToContainerResourceLimits(t *testing.T) { func TestComposeContainerGroupToContainerResourceLimits(t *testing.T) {

View File

@ -57,6 +57,8 @@ type Container struct {
// RuntimeConfig config of a created container // RuntimeConfig config of a created container
type RuntimeConfig struct { type RuntimeConfig struct {
Env map[string]string `json:",omitempty"` Env map[string]string `json:",omitempty"`
// FQDN is the fqdn to use
FQDN string `json:"fqdn,omitempty"`
} }
// Port represents a published port of a container // Port represents a published port of a container
@ -91,6 +93,8 @@ type ContainerConfig struct {
Environment []string Environment []string
// Restart policy condition // Restart policy condition
RestartPolicyCondition string RestartPolicyCondition string
// DomainName Container NIS domain name
DomainName string
} }
// ExecRequest contaiens configuration about an exec request // ExecRequest contaiens configuration about an exec request

View File

@ -29,6 +29,7 @@ import (
type composeOptions struct { type composeOptions struct {
Name string Name string
DomainName string
WorkingDir string WorkingDir string
ConfigPaths []string ConfigPaths []string
Environment []string Environment []string
@ -60,7 +61,7 @@ func (o *composeOptions) toProjectOptions() (*cli.ProjectOptions, error) {
} }
// Command returns the compose command with its child commands // Command returns the compose command with its child commands
func Command() *cobra.Command { func Command(contextType string) *cobra.Command {
command := &cobra.Command{ command := &cobra.Command{
Short: "Docker Compose", Short: "Docker Compose",
Use: "compose", Use: "compose",
@ -70,7 +71,7 @@ func Command() *cobra.Command {
} }
command.AddCommand( command.AddCommand(
upCommand(), upCommand(contextType),
downCommand(), downCommand(),
psCommand(), psCommand(),
listCommand(), listCommand(),

View File

@ -19,15 +19,16 @@ package compose
import ( import (
"context" "context"
"github.com/compose-spec/compose-go/cli"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/compose-spec/compose-go/cli"
"github.com/docker/compose-cli/api/client" "github.com/docker/compose-cli/api/client"
"github.com/docker/compose-cli/context/store"
"github.com/docker/compose-cli/progress" "github.com/docker/compose-cli/progress"
) )
func upCommand() *cobra.Command { func upCommand(contextType string) *cobra.Command {
opts := composeOptions{} opts := composeOptions{}
upCmd := &cobra.Command{ upCmd := &cobra.Command{
Use: "up", 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.ConfigPaths, "file", "f", []string{}, "Compose configuration files")
upCmd.Flags().StringArrayVarP(&opts.Environment, "environment", "e", []string{}, "Environment variables") upCmd.Flags().StringArrayVarP(&opts.Environment, "environment", "e", []string{}, "Environment variables")
upCmd.Flags().BoolP("detach", "d", true, " Detached mode: Run containers in the background") 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 return upCmd
} }
@ -55,6 +61,10 @@ func runUp(ctx context.Context, opts composeOptions) error {
return "", err return "", err
} }
project, err := cli.ProjectFromOptions(options) project, err := cli.ProjectFromOptions(options)
if opts.DomainName != "" {
//arbitrarily set the domain name on the first service ; ACI backend will expose the entire project
project.Services[0].DomainName = opts.DomainName
}
if err != nil { if err != nil {
return "", err return "", err
} }

View File

@ -23,13 +23,13 @@ import (
"strings" "strings"
"text/tabwriter" "text/tabwriter"
"github.com/docker/compose-cli/utils/formatter"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/docker/compose-cli/api/client" "github.com/docker/compose-cli/api/client"
"github.com/docker/compose-cli/api/containers"
formatter2 "github.com/docker/compose-cli/formatter" formatter2 "github.com/docker/compose-cli/formatter"
"github.com/docker/compose-cli/utils/formatter"
) )
type psOpts struct { type psOpts struct {
@ -98,9 +98,17 @@ func runPs(ctx context.Context, opts psOpts) error {
w := tabwriter.NewWriter(os.Stdout, 20, 1, 3, ' ', 0) w := tabwriter.NewWriter(os.Stdout, 20, 1, 3, ' ', 0)
fmt.Fprintf(w, "CONTAINER ID\tIMAGE\tCOMMAND\tSTATUS\tPORTS\n") fmt.Fprintf(w, "CONTAINER ID\tIMAGE\tCOMMAND\tSTATUS\tPORTS\n")
format := "%s\t%s\t%s\t%s\t%s\n" format := "%s\t%s\t%s\t%s\t%s\n"
for _, c := range containers { for _, container := range containers {
fmt.Fprintf(w, format, c.ID, c.Image, c.Command, c.Status, strings.Join(formatter.PortsToStrings(c.Ports), ", ")) fmt.Fprintf(w, format, container.ID, container.Image, container.Command, container.Status, strings.Join(formatter.PortsToStrings(container.Ports, fqdn(container)), ", "))
} }
return w.Flush() return w.Flush()
} }
func fqdn(container containers.Container) string {
fqdn := ""
if container.Config != nil {
fqdn = container.Config.FQDN
}
return fqdn
}

View File

@ -25,15 +25,15 @@ import (
"github.com/containerd/console" "github.com/containerd/console"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/docker/compose-cli/api/containers"
"github.com/docker/compose-cli/api/client" "github.com/docker/compose-cli/api/client"
"github.com/docker/compose-cli/api/containers"
"github.com/docker/compose-cli/cli/options/run" "github.com/docker/compose-cli/cli/options/run"
"github.com/docker/compose-cli/context/store"
"github.com/docker/compose-cli/progress" "github.com/docker/compose-cli/progress"
) )
// Command runs a container // Command runs a container
func Command() *cobra.Command { func Command(contextType string) *cobra.Command {
var opts run.Opts var opts run.Opts
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "run", Use: "run",
@ -54,6 +54,10 @@ func Command() *cobra.Command {
cmd.Flags().StringArrayVarP(&opts.Environment, "env", "e", []string{}, "Set environment variables") cmd.Flags().StringArrayVarP(&opts.Environment, "env", "e", []string{}, "Set environment variables")
cmd.Flags().StringVarP(&opts.RestartPolicyCondition, "restart", "", containers.RestartPolicyNone, "Restart policy to apply when a container exits") cmd.Flags().StringVarP(&opts.RestartPolicyCondition, "restart", "", containers.RestartPolicyNone, "Restart policy to apply when a container exits")
if contextType == store.AciContextType {
cmd.Flags().StringVar(&opts.DomainName, "domainname", "", "Container NIS domain name")
}
return cmd return cmd
} }

View File

@ -18,15 +18,25 @@ package run
import ( import (
"bytes" "bytes"
"strings"
"testing" "testing"
"gotest.tools/v3/assert"
"gotest.tools/v3/golden" "gotest.tools/v3/golden"
) )
func TestHelp(t *testing.T) { func TestHelp(t *testing.T) {
var b bytes.Buffer var b bytes.Buffer
c := Command() c := Command("aci")
c.SetOutput(&b) c.SetOutput(&b)
_ = c.Help() _ = c.Help()
golden.Assert(t, b.String(), "run-help.golden") golden.Assert(t, b.String(), "run-help.golden")
} }
func TestHelpNoDomainFlag(t *testing.T) {
var b bytes.Buffer
c := Command("default")
c.SetOutput(&b)
_ = c.Help()
assert.Assert(t, !strings.Contains(b.String(), "domainname"))
}

View File

@ -6,6 +6,7 @@ Usage:
Flags: Flags:
--cpus float Number of CPUs (default 1) --cpus float Number of CPUs (default 1)
-d, --detach Run container in background and print container ID -d, --detach Run container in background and print container ID
--domainname string Container NIS domain name
-e, --env stringArray Set environment variables -e, --env stringArray Set environment variables
-l, --label stringArray Set meta data on a container -l, --label stringArray Set meta data on a container
-m, --memory bytes Memory limit -m, --memory bytes Memory limit

View File

@ -115,7 +115,6 @@ func main() {
contextcmd.Command(), contextcmd.Command(),
cmd.PsCommand(), cmd.PsCommand(),
cmd.ServeCommand(), cmd.ServeCommand(),
run.Command(),
cmd.ExecCommand(), cmd.ExecCommand(),
cmd.LogsCommand(), cmd.LogsCommand(),
cmd.RmCommand(), cmd.RmCommand(),
@ -127,7 +126,6 @@ func main() {
cmd.StopCommand(), cmd.StopCommand(),
cmd.KillCommand(), cmd.KillCommand(),
cmd.SecretCommand(), cmd.SecretCommand(),
compose.Command(),
// Place holders // Place holders
cmd.EcsCommand(), cmd.EcsCommand(),
@ -180,6 +178,11 @@ func main() {
ctype = cc.Type() ctype = cc.Type()
} }
root.AddCommand(
run.Command(ctype),
compose.Command(ctype),
)
if ctype == store.AciContextType { 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 // we can also pass ctype as a parameter to the volume command and customize subcommands, flags, etc. when we have other backend implementations
root.AddCommand(volume.ACICommand()) root.AddCommand(volume.ACICommand())

View File

@ -64,7 +64,7 @@ func TestCheckOwnCommand(t *testing.T) {
assert.Assert(t, isContextAgnosticCommand(login.Command())) assert.Assert(t, isContextAgnosticCommand(login.Command()))
assert.Assert(t, isContextAgnosticCommand(context.Command())) assert.Assert(t, isContextAgnosticCommand(context.Command()))
assert.Assert(t, isContextAgnosticCommand(cmd.ServeCommand())) assert.Assert(t, isContextAgnosticCommand(cmd.ServeCommand()))
assert.Assert(t, !isContextAgnosticCommand(run.Command())) assert.Assert(t, !isContextAgnosticCommand(run.Command("default")))
assert.Assert(t, !isContextAgnosticCommand(cmd.ExecCommand())) assert.Assert(t, !isContextAgnosticCommand(cmd.ExecCommand()))
assert.Assert(t, !isContextAgnosticCommand(cmd.LogsCommand())) assert.Assert(t, !isContextAgnosticCommand(cmd.LogsCommand()))
assert.Assert(t, !isContextAgnosticCommand(cmd.PsCommand())) assert.Assert(t, !isContextAgnosticCommand(cmd.PsCommand()))

View File

@ -41,6 +41,7 @@ type Opts struct {
Detach bool Detach bool
Environment []string Environment []string
RestartPolicyCondition string RestartPolicyCondition string
DomainName string
} }
// ToContainerConfig convert run options to a container configuration // ToContainerConfig convert run options to a container configuration
@ -74,6 +75,7 @@ func (r *Opts) ToContainerConfig(image string) (containers.ContainerConfig, erro
CPULimit: r.Cpus, CPULimit: r.Cpus,
Environment: r.Environment, Environment: r.Environment,
RestartPolicyCondition: restartPolicy, RestartPolicyCondition: restartPolicy,
DomainName: r.DomainName,
}, nil }, nil
} }

View File

@ -335,18 +335,19 @@ func lines(output string) []string {
func TestContainerRunAttached(t *testing.T) { func TestContainerRunAttached(t *testing.T) {
c := NewParallelE2eCLI(t, binDir) c := NewParallelE2eCLI(t, binDir)
_, _ = setupTestResourceGroup(t, c) _, groupID := setupTestResourceGroup(t, c)
// Used in subtests // Used in subtests
var ( var (
container string container string = "test-container"
endpoint string endpoint string
followLogsProcess *icmd.Result
) )
container = "test-container"
var followLogsProcess *icmd.Result
t.Run("run attached limits", func(t *testing.T) { t.Run("run attached limits", func(t *testing.T) {
dnsLabelName := "nginx-" + groupID
fqdn := dnsLabelName + "." + location + ".azurecontainer.io"
cmd := c.NewDockerCmd( cmd := c.NewDockerCmd(
"run", "run",
"--name", container, "--name", container,
@ -354,15 +355,17 @@ func TestContainerRunAttached(t *testing.T) {
"--memory", "0.1G", "--cpus", "0.1", "--memory", "0.1G", "--cpus", "0.1",
"-p", "80:80", "-p", "80:80",
"nginx", "nginx",
"--domainname",
dnsLabelName,
) )
followLogsProcess = icmd.StartCmd(cmd) followLogsProcess = icmd.StartCmd(cmd)
checkRunning := func(t poll.LogT) poll.Result { checkRunning := func(t poll.LogT) poll.Result {
res := c.RunDockerOrExitError("inspect", container) res := c.RunDockerOrExitError("inspect", container)
if res.ExitCode == 0 { if res.ExitCode == 0 && strings.Contains(res.Stdout(), `"Status": "Running"`) {
return poll.Success() return poll.Success()
} }
return poll.Continue("waiting for container to be running") return poll.Continue("waiting for container to be running, current inspect result: \n%s", res.Combined())
} }
poll.WaitOn(t, checkRunning, poll.WithDelay(5*time.Second), poll.WithTimeout(60*time.Second)) poll.WaitOn(t, checkRunning, poll.WithDelay(5*time.Second), poll.WithTimeout(60*time.Second))
@ -380,7 +383,8 @@ func TestContainerRunAttached(t *testing.T) {
assert.Assert(t, len(port.HostIP) > 0) assert.Assert(t, len(port.HostIP) > 0)
assert.Equal(t, port.ContainerPort, uint32(80)) assert.Equal(t, port.ContainerPort, uint32(80))
assert.Equal(t, port.HostPort, uint32(80)) assert.Equal(t, port.HostPort, uint32(80))
endpoint = fmt.Sprintf("http://%s:%d", port.HostIP, port.HostPort) assert.Equal(t, containerInspect.Config.FQDN, fqdn)
endpoint = fmt.Sprintf("http://%s:%d", fqdn, port.HostPort)
assert.Assert(t, !strings.Contains(followLogsProcess.Stdout(), "/test")) assert.Assert(t, !strings.Contains(followLogsProcess.Stdout(), "/test"))
checkRequest := func(t poll.LogT) poll.Result { checkRequest := func(t poll.LogT) poll.Result {
@ -449,7 +453,7 @@ func TestContainerRunAttached(t *testing.T) {
func TestComposeUpUpdate(t *testing.T) { func TestComposeUpUpdate(t *testing.T) {
c := NewParallelE2eCLI(t, binDir) c := NewParallelE2eCLI(t, binDir)
_, _ = setupTestResourceGroup(t, c) _, groupID := setupTestResourceGroup(t, c)
const ( const (
composeFile = "../composefiles/aci-demo/aci_demo_port.yaml" composeFile = "../composefiles/aci-demo/aci_demo_port.yaml"
@ -461,8 +465,11 @@ func TestComposeUpUpdate(t *testing.T) {
) )
t.Run("compose up", func(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" // 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") res := c.RunDockerCmd("ps")
out := lines(res.Stdout()) out := lines(res.Stdout())
// Check three containers are running // Check three containers are running
@ -489,6 +496,11 @@ func TestComposeUpUpdate(t *testing.T) {
b, err := ioutil.ReadAll(r.Body) b, err := ioutil.ReadAll(r.Body)
assert.NilError(t, err) assert.NilError(t, err)
assert.Assert(t, strings.Contains(string(b), `"word":`)) 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) { t.Run("compose ps", func(t *testing.T) {

View File

@ -31,7 +31,7 @@ type portGroup struct {
} }
// PortsToStrings returns a human readable published ports // PortsToStrings returns a human readable published ports
func PortsToStrings(ports []containers.Port) []string { func PortsToStrings(ports []containers.Port, fqdn string) []string {
groupMap := make(map[string]*portGroup) groupMap := make(map[string]*portGroup)
var ( var (
result []string result []string
@ -49,6 +49,9 @@ func PortsToStrings(ports []containers.Port) []string {
if port.HostIP != "" { if port.HostIP != "" {
hostIP = port.HostIP hostIP = port.HostIP
} }
if fqdn != "" {
hostIP = fqdn
}
if port.HostPort != port.ContainerPort { if port.HostPort != port.ContainerPort {
hostMappings = append(hostMappings, fmt.Sprintf("%s:%d->%d/%s", hostIP, port.HostPort, port.ContainerPort, port.Protocol)) hostMappings = append(hostMappings, fmt.Sprintf("%s:%d->%d/%s", hostIP, port.HostPort, port.ContainerPort, port.Protocol))

View File

@ -24,7 +24,7 @@ import (
"github.com/docker/compose-cli/cli/options/run" "github.com/docker/compose-cli/cli/options/run"
) )
func TestDisplayPorts(t *testing.T) { func TestDisplayPortsNoDomainname(t *testing.T) {
testCases := []struct { testCases := []struct {
name string name string
in []string in []string
@ -70,8 +70,19 @@ func TestDisplayPorts(t *testing.T) {
containerConfig, err := runOpts.ToContainerConfig("test") containerConfig, err := runOpts.ToContainerConfig("test")
assert.NilError(t, err) assert.NilError(t, err)
out := PortsToStrings(containerConfig.Ports) out := PortsToStrings(containerConfig.Ports, "")
assert.DeepEqual(t, testCase.expected, out) assert.DeepEqual(t, testCase.expected, out)
}) })
} }
} }
func TestDisplayPortsWithDomainname(t *testing.T) {
runOpts := run.Opts{
Publish: []string{"80"},
}
containerConfig, err := runOpts.ToContainerConfig("test")
assert.NilError(t, err)
out := PortsToStrings(containerConfig.Ports, "mydomain.westus.azurecontainner.io")
assert.DeepEqual(t, []string{"mydomain.westus.azurecontainner.io:80->80/tcp"}, out)
}