Implement compose ps on ACI

Signed-off-by: Guillaume Tardif <guillaume.tardif@docker.com>
This commit is contained in:
Guillaume Tardif 2020-08-28 14:38:51 +02:00
parent f80e90caca
commit b0c50ed6dd
7 changed files with 144 additions and 32 deletions

View File

@ -164,24 +164,28 @@ func (cs *aciContainerService) List(ctx context.Context, all bool) ([]containers
}
for _, container := range *group.Containers {
// don't list sidecar container
if *container.Name == convert.ComposeDNSSidecarName {
if isContainerVisible(container, group, all) {
continue
}
if !all && convert.GetStatus(container, group) != statusRunning {
continue
}
containerID := *containerGroup.Name + composeContainerSeparator + *container.Name
if _, ok := group.Tags[singleContainerTag]; ok {
containerID = *containerGroup.Name
}
c := convert.ContainerGroupToContainer(containerID, group, container)
c := convert.ContainerGroupToContainer(getContainerID(group, container), group, container)
res = append(res, c)
}
}
return res, nil
}
func getContainerID(group containerinstance.ContainerGroup, container containerinstance.Container) string {
containerID := *group.Name + composeContainerSeparator + *container.Name
if _, ok := group.Tags[singleContainerTag]; ok {
containerID = *group.Name
}
return containerID
}
func isContainerVisible(container containerinstance.Container, group containerinstance.ContainerGroup, showAll bool) bool {
return *container.Name == convert.ComposeDNSSidecarName || (!showAll && convert.GetStatus(container, group) != statusRunning)
}
func (cs *aciContainerService) Run(ctx context.Context, r containers.ContainerConfig) error {
if strings.Contains(r.ID, composeContainerSeparator) {
return errors.New(fmt.Sprintf("invalid container name. ACI container name cannot include %q", composeContainerSeparator))
@ -411,7 +415,28 @@ func (cs *aciComposeService) Down(ctx context.Context, project string) error {
}
func (cs *aciComposeService) Ps(ctx context.Context, project string) ([]compose.ServiceStatus, error) {
return nil, errdefs.ErrNotImplemented
groupsClient, err := login.NewContainerGroupsClient(cs.ctx.SubscriptionID)
if err != nil {
return nil, err
}
group, err := groupsClient.Get(ctx, cs.ctx.ResourceGroup, project)
if err != nil {
return []compose.ServiceStatus{}, err
}
if group.Containers == nil || len(*group.Containers) < 1 {
return []compose.ServiceStatus{}, fmt.Errorf("no containers found in ACI container group %s", project)
}
res := []compose.ServiceStatus{}
for _, container := range *group.Containers {
if isContainerVisible(container, group, false) {
continue
}
res = append(res, convert.ContainerGroupToServiceStatus(getContainerID(group, container), group, container))
}
return res, nil
}
func (cs *aciComposeService) Logs(ctx context.Context, project string, w io.Writer) error {

View File

@ -26,6 +26,9 @@ import (
"strconv"
"strings"
"github.com/docker/compose-cli/compose"
"github.com/docker/compose-cli/utils/formatter"
"github.com/Azure/azure-sdk-for-go/services/containerinstance/mgmt/2018-10-01/containerinstance"
"github.com/Azure/go-autorest/autorest/to"
"github.com/compose-spec/compose-go/types"
@ -385,6 +388,21 @@ func bytesToGb(b types.UnitBytes) float64 {
return math.Round(f*100) / 100
}
// ContainerGroupToServiceStatus convert from an ACI container definition to service status
func ContainerGroupToServiceStatus(containerID string, group containerinstance.ContainerGroup, container containerinstance.Container) compose.ServiceStatus {
var replicas = 1
if GetStatus(container, group) != "Running" {
replicas = 0
}
return compose.ServiceStatus{
ID: containerID,
Name: *container.Name,
Ports: formatter.PortsToStrings(ToPorts(group.IPAddress, *container.Ports)),
Replicas: replicas,
Desired: 1,
}
}
// ContainerGroupToContainer composes a Container from an ACI container definition
func ContainerGroupToContainer(containerID string, cg containerinstance.ContainerGroup, cc containerinstance.Container) containers.Container {
memLimits := 0.

View File

@ -21,6 +21,8 @@ import (
"os"
"testing"
"github.com/docker/compose-cli/compose"
"github.com/Azure/azure-sdk-for-go/profiles/latest/containerinstance/mgmt/containerinstance"
"github.com/Azure/go-autorest/autorest/to"
"github.com/compose-spec/compose-go/types"
@ -103,6 +105,44 @@ func TestContainerGroupToContainer(t *testing.T) {
assert.DeepEqual(t, container, expectedContainer)
}
func TestContainerGroupToServiceStatus(t *testing.T) {
myContainerGroup := containerinstance.ContainerGroup{
ContainerGroupProperties: &containerinstance.ContainerGroupProperties{
IPAddress: &containerinstance.IPAddress{
Ports: &[]containerinstance.Port{{
Port: to.Int32Ptr(80),
}},
IP: to.StringPtr("42.42.42.42"),
},
},
}
myContainer := containerinstance.Container{
Name: to.StringPtr("myContainerID"),
ContainerProperties: &containerinstance.ContainerProperties{
Ports: &[]containerinstance.ContainerPort{{
Port: to.Int32Ptr(80),
}},
InstanceView: &containerinstance.ContainerPropertiesInstanceView{
RestartCount: nil,
CurrentState: &containerinstance.ContainerState{
State: to.StringPtr("Running"),
},
},
},
}
var expectedService = compose.ServiceStatus{
ID: "myContainerID",
Name: "myContainerID",
Ports: []string{"42.42.42.42:80->80/tcp"},
Replicas: 1,
Desired: 1,
}
container := ContainerGroupToServiceStatus("myContainerID", myContainerGroup, myContainer)
assert.DeepEqual(t, container, expectedService)
}
func TestComposeContainerGroupToContainerWithDnsSideCarSide(t *testing.T) {
project := types.Project{
Services: []types.ServiceConfig{

View File

@ -20,12 +20,14 @@ import (
"context"
"fmt"
"os"
"strings"
"text/tabwriter"
"github.com/docker/compose-cli/utils/formatter"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"github.com/docker/compose-cli/cli/formatter"
"github.com/docker/compose-cli/client"
formatter2 "github.com/docker/compose-cli/formatter"
)
@ -97,7 +99,7 @@ func runPs(ctx context.Context, opts psOpts) error {
fmt.Fprintf(w, "CONTAINER ID\tIMAGE\tCOMMAND\tSTATUS\tPORTS\n")
format := "%s\t%s\t%s\t%s\t%s\n"
for _, c := range containers {
fmt.Fprintf(w, format, c.ID, c.Image, c.Command, c.Status, formatter.PortsString(c.Ports))
fmt.Fprintf(w, format, c.ID, c.Image, c.Command, c.Status, strings.Join(formatter.PortsToStrings(c.Ports), ", "))
}
return w.Flush()

View File

@ -81,10 +81,10 @@ func TestLoginLogout(t *testing.T) {
t.Run("create context", func(t *testing.T) {
sID := getSubscriptionID(t)
err := createResourceGroup(sID, rg)
err := createResourceGroup(t, sID, rg)
assert.Check(t, is.Nil(err))
t.Cleanup(func() {
_ = deleteResourceGroup(rg)
_ = deleteResourceGroup(t, rg)
})
c.RunDockerCmd("context", "create", "aci", contextName, "--subscription-id", sID, "--resource-group", rg, "--location", location)
@ -388,7 +388,7 @@ func TestContainerRunAttached(t *testing.T) {
})
}
func TestCompose(t *testing.T) {
func TestComposeUpUpdate(t *testing.T) {
c := NewParallelE2eCLI(t, binDir)
_, _ = setupTestResourceGroup(t, c)
@ -398,6 +398,7 @@ func TestCompose(t *testing.T) {
composeProjectName = "acidemo"
serverContainer = composeProjectName + "_web"
wordsContainer = composeProjectName + "_words"
dbContainer = composeProjectName + "_db"
)
t.Run("compose up", func(t *testing.T) {
@ -431,6 +432,30 @@ func TestCompose(t *testing.T) {
assert.Assert(t, strings.Contains(string(b), `"word":`))
})
t.Run("compose ps", func(t *testing.T) {
res := c.RunDockerCmd("compose", "ps", "--project-name", composeProjectName)
lines := strings.Split(strings.TrimSpace(res.Stdout()), "\n")
assert.Assert(t, is.Len(lines, 4))
var wordsDisplayed, webDisplayed, dbDisplayed bool
for _, line := range lines {
fields := strings.Fields(line)
containerID := fields[0]
switch containerID {
case wordsContainer:
wordsDisplayed = true
assert.DeepEqual(t, fields, []string{containerID, "words", "1/1"})
case dbContainer:
dbDisplayed = true
assert.DeepEqual(t, fields, []string{containerID, "db", "1/1"})
case serverContainer:
webDisplayed = true
assert.Equal(t, fields[1], "web")
assert.Check(t, strings.Contains(fields[3], ":80->80/tcp"))
}
}
assert.Check(t, webDisplayed && wordsDisplayed && dbDisplayed, "\n%s\n", res.Stdout())
})
t.Run("logs web", func(t *testing.T) {
res := c.RunDockerCmd("logs", serverContainer)
res.Assert(t, icmd.Expected{Out: "Listening on port 80"})
@ -527,10 +552,10 @@ func setupTestResourceGroup(t *testing.T, c *E2eCLI) (string, string) {
rg := "E2E-" + t.Name() + "-" + startTime
azureLogin(t, c)
sID := getSubscriptionID(t)
err := createResourceGroup(sID, rg)
err := createResourceGroup(t, sID, rg)
assert.Check(t, is.Nil(err))
t.Cleanup(func() {
if err := deleteResourceGroup(rg); err != nil {
if err := deleteResourceGroup(t, rg); err != nil {
t.Error(err)
}
})
@ -541,7 +566,8 @@ func setupTestResourceGroup(t *testing.T, c *E2eCLI) (string, string) {
return sID, rg
}
func deleteResourceGroup(rgName string) error {
func deleteResourceGroup(t *testing.T, rgName string) error {
fmt.Printf(" [%s] deleting resource group %s\n", t.Name(), rgName)
ctx := context.TODO()
helper := aci.NewACIResourceGroupHelper()
models, err := helper.GetSubscriptionIDs(ctx)
@ -574,7 +600,8 @@ func getSubscriptionID(t *testing.T) string {
return *models[0].SubscriptionID
}
func createResourceGroup(sID, rgName string) error {
func createResourceGroup(t *testing.T, sID, rgName string) error {
fmt.Printf(" [%s] creating resource group %s\n", t.Name(), rgName)
helper := aci.NewACIResourceGroupHelper()
_, err := helper.CreateOrUpdate(context.TODO(), sID, rgName, resources.Group{Location: to.StringPtr(location)})
return err

View File

@ -30,8 +30,8 @@ type portGroup struct {
last uint32
}
// PortsString returns a human readable published ports
func PortsString(ports []containers.Port) string {
// PortsToStrings returns a human readable published ports
func PortsToStrings(ports []containers.Port) []string {
groupMap := make(map[string]*portGroup)
var (
result []string
@ -82,7 +82,7 @@ func PortsString(ports []containers.Port) string {
result = append(result, hostMappings...)
return strings.Join(result, ", ")
return result
}
func formGroup(key string, start uint32, last uint32) string {

View File

@ -28,37 +28,37 @@ func TestDisplayPorts(t *testing.T) {
testCases := []struct {
name string
in []string
expected string
expected []string
}{
{
name: "simple",
in: []string{"80"},
expected: "0.0.0.0:80->80/tcp",
expected: []string{"0.0.0.0:80->80/tcp"},
},
{
name: "different ports",
in: []string{"80:90"},
expected: "0.0.0.0:80->90/tcp",
expected: []string{"0.0.0.0:80->90/tcp"},
},
{
name: "host ip",
in: []string{"192.168.0.1:80:90"},
expected: "192.168.0.1:80->90/tcp",
expected: []string{"192.168.0.1:80->90/tcp"},
},
{
name: "port range",
in: []string{"80-90:80-90"},
expected: "0.0.0.0:80-90->80-90/tcp",
expected: []string{"0.0.0.0:80-90->80-90/tcp"},
},
{
name: "grouping",
in: []string{"80:80", "81:81"},
expected: "0.0.0.0:80-81->80-81/tcp",
expected: []string{"0.0.0.0:80-81->80-81/tcp"},
},
{
name: "groups",
in: []string{"80:80", "82:82"},
expected: "0.0.0.0:80->80/tcp, 0.0.0.0:82->82/tcp",
expected: []string{"0.0.0.0:80->80/tcp", "0.0.0.0:82->82/tcp"},
},
}
@ -70,8 +70,8 @@ func TestDisplayPorts(t *testing.T) {
containerConfig, err := runOpts.ToContainerConfig("test")
assert.NilError(t, err)
out := PortsString(containerConfig.Ports)
assert.Equal(t, testCase.expected, out)
out := PortsToStrings(containerConfig.Ports)
assert.DeepEqual(t, testCase.expected, out)
})
}
}