From b0c50ed6dd1b29c27a5d6983c4342fc14ccc1b8a Mon Sep 17 00:00:00 2001 From: Guillaume Tardif Date: Fri, 28 Aug 2020 14:38:51 +0200 Subject: [PATCH] Implement compose ps on ACI Signed-off-by: Guillaume Tardif --- aci/backend.go | 47 +++++++++++++++++----- aci/convert/convert.go | 18 +++++++++ aci/convert/convert_test.go | 40 ++++++++++++++++++ cli/cmd/ps.go | 6 ++- tests/aci-e2e/e2e-aci_test.go | 41 +++++++++++++++---- {cli => utils}/formatter/container.go | 6 +-- {cli => utils}/formatter/container_test.go | 18 ++++----- 7 files changed, 144 insertions(+), 32 deletions(-) rename {cli => utils}/formatter/container.go (95%) rename {cli => utils}/formatter/container_test.go (77%) diff --git a/aci/backend.go b/aci/backend.go index 40ec0f427..fe86d990f 100644 --- a/aci/backend.go +++ b/aci/backend.go @@ -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 { diff --git a/aci/convert/convert.go b/aci/convert/convert.go index 0d0ea4255..a88cc315d 100644 --- a/aci/convert/convert.go +++ b/aci/convert/convert.go @@ -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. diff --git a/aci/convert/convert_test.go b/aci/convert/convert_test.go index e648bb1b7..dfc60a099 100644 --- a/aci/convert/convert_test.go +++ b/aci/convert/convert_test.go @@ -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{ diff --git a/cli/cmd/ps.go b/cli/cmd/ps.go index ddd2e325e..400fd1358 100644 --- a/cli/cmd/ps.go +++ b/cli/cmd/ps.go @@ -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() diff --git a/tests/aci-e2e/e2e-aci_test.go b/tests/aci-e2e/e2e-aci_test.go index 93a450134..e2e84aff9 100644 --- a/tests/aci-e2e/e2e-aci_test.go +++ b/tests/aci-e2e/e2e-aci_test.go @@ -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 diff --git a/cli/formatter/container.go b/utils/formatter/container.go similarity index 95% rename from cli/formatter/container.go rename to utils/formatter/container.go index 58734bfdb..072c03e22 100644 --- a/cli/formatter/container.go +++ b/utils/formatter/container.go @@ -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 { diff --git a/cli/formatter/container_test.go b/utils/formatter/container_test.go similarity index 77% rename from cli/formatter/container_test.go rename to utils/formatter/container_test.go index 0da9eaa39..771c497bf 100644 --- a/cli/formatter/container_test.go +++ b/utils/formatter/container_test.go @@ -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) }) } }