瀏覽代碼

Implement compose ps on ACI

Signed-off-by: Guillaume Tardif <[email protected]>
Guillaume Tardif 5 年之前
父節點
當前提交
b0c50ed6dd
共有 7 個文件被更改,包括 144 次插入32 次删除
  1. 36 11
      aci/backend.go
  2. 18 0
      aci/convert/convert.go
  3. 40 0
      aci/convert/convert_test.go
  4. 4 2
      cli/cmd/ps.go
  5. 34 7
      tests/aci-e2e/e2e-aci_test.go
  6. 3 3
      utils/formatter/container.go
  7. 9 9
      utils/formatter/container_test.go

+ 36 - 11
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 {

+ 18 - 0
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.

+ 40 - 0
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{

+ 4 - 2
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()

+ 34 - 7
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

+ 3 - 3
cli/formatter/container.go → 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 {

+ 9 - 9
cli/formatter/container_test.go → 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)
 		})
 	}
 }