From ee8f8d2f5f28d69c952a89fef625a7c1f74a7090 Mon Sep 17 00:00:00 2001 From: Mario Rodriguez Date: Fri, 29 Jul 2022 16:15:24 +0200 Subject: [PATCH 01/28] Set initial stack tatus command --- cmd/stack.go | 47 ++++++++++++++- internal/compose/compose.go | 111 ++++++++++++++++++++++++++++++++++++ internal/docker/docker.go | 4 ++ internal/stack/compose.go | 21 +++++++ internal/stack/status.go | 20 +++++++ 5 files changed, 202 insertions(+), 1 deletion(-) create mode 100644 internal/stack/status.go diff --git a/cmd/stack.go b/cmd/stack.go index 3a5fc0a476..c24d544fd0 100644 --- a/cmd/stack.go +++ b/cmd/stack.go @@ -6,13 +6,16 @@ package cmd import ( "fmt" + "strconv" "strings" + "github.com/jedib0t/go-pretty/table" "github.com/pkg/errors" "github.com/spf13/cobra" "github.com/elastic/elastic-package/internal/cobraext" "github.com/elastic/elastic-package/internal/common" + "github.com/elastic/elastic-package/internal/compose" "github.com/elastic/elastic-package/internal/install" "github.com/elastic/elastic-package/internal/profile" "github.com/elastic/elastic-package/internal/stack" @@ -246,6 +249,32 @@ func setupStackCommand() *cobraext.Command { } dumpCommand.Flags().StringP(cobraext.StackDumpOutputFlagName, "", "elastic-stack-dump", cobraext.StackDumpOutputFlagDescription) + statusCommand := &cobra.Command{ + Use: "status", + Short: "Show status of the stack services", + RunE: func(cmd *cobra.Command, args []string) error { + profileName, err := cmd.Flags().GetString(cobraext.ProfileFlagName) + if err != nil { + return cobraext.FlagParsingError(err, cobraext.ProfileFlagName) + } + profile, err := profile.LoadProfile(profileName) + if err != nil { + return errors.Wrap(err, "error loading profile") + } + + servicesStatus, err := stack.Status(stack.Options{ + Profile: profile, + }) + if err != nil { + return errors.Wrap(err, "failed updating the stack images") + } + + cmd.Println("Status of Elastic stack services:") + printStatus(cmd, servicesStatus) + return nil + }, + } + cmd := &cobra.Command{ Use: "stack", Short: "Manage the Elastic stack", @@ -257,7 +286,8 @@ func setupStackCommand() *cobraext.Command { downCommand, updateCommand, shellInitCommand, - dumpCommand) + dumpCommand, + statusCommand) return cobraext.NewCommand(cmd, cobraext.ContextGlobal) } @@ -300,3 +330,18 @@ func printInitConfig(cmd *cobra.Command, profile *profile.Profile) error { cmd.Printf("Password: %s\n", initConfig.ElasticsearchPassword) return nil } + +func printStatus(cmd *cobra.Command, servicesStatus []compose.ServiceStatus) { + t := table.NewWriter() + t.AppendHeader(table.Row{"Service", "Version", "Status", "Health", "Exit Code (if any)"}) + + for _, service := range servicesStatus { + exitCode := strconv.Itoa(service.ExitCode) + if service.ExitCode == 0 { + exitCode = "" + } + t.AppendRow(table.Row{service.Name, service.Version, service.Status, service.Health, exitCode}) + } + t.SetStyle(table.StyleRounded) + cmd.Println(t.Render()) +} diff --git a/internal/compose/compose.go b/internal/compose/compose.go index 03c821d1bb..5779ede4ff 100644 --- a/internal/compose/compose.go +++ b/internal/compose/compose.go @@ -10,6 +10,7 @@ import ( "io" "os" "os/exec" + "regexp" "strconv" "strings" "time" @@ -42,11 +43,22 @@ type Project struct { type Config struct { Services map[string]service } + type service struct { Ports []portMapping Environment map[string]string } +type ServiceStatus struct { + ExitCode int + Health string + ID string + Name string + Running bool + Status string + Version string +} + type portMapping struct { ExternalIP string ExternalPort int @@ -284,6 +296,97 @@ func (p *Project) Logs(opts CommandOptions) ([]byte, error) { return b.Bytes(), nil } +// Status returns status services for the selected service in the Docker Compose project. +func (p *Project) Status(opts CommandOptions) ([]ServiceStatus, error) { + args := p.baseArgs() + args = append(args, "ps") + args = append(args, "-q") + + logger.Debugf("Services to check: %v", opts.Services) + var services []ServiceStatus + var b bytes.Buffer + + args = append(args, opts.Services...) + + if err := p.runDockerComposeCmd(dockerComposeOptions{args: args, env: opts.Env, stdout: &b}); err != nil { + return nil, err + } + + logger.Debugf("%v\n", b.String()) + containerIDs := strings.Fields(b.String()) + + containerDescriptions, err := docker.InspectContainers(containerIDs...) + if err != nil { + return nil, err + } + + var serviceNameRegex = regexp.MustCompile(fmt.Sprintf("^/%v_(.*)_\\d+$", p.name)) + for _, containerDescription := range containerDescriptions { + logger.Debugf("Image container: \"%v\"", containerDescription.Config.Image) + matches := serviceNameRegex.FindStringSubmatch(containerDescription.Name) + if matches == nil || len(matches) == 0 { + return nil, fmt.Errorf("Unknown container %v", containerDescription.Name) + } + logger.Debugf("Matches: %v", matches) + service := ServiceStatus{ + ID: containerDescription.ID, + Name: matches[1], + Running: containerDescription.State.Status == "running", + Status: containerDescription.State.Status, + Version: getVersionFromDockerImage(containerDescription.Config.Image), + } + if containerDescription.State.Status != "running" { + service.ExitCode = containerDescription.State.ExitCode + } + if containerDescription.State.Health != nil { + service.Health = containerDescription.State.Health.Status + } + services = append(services, service) + } + + return services, nil + + // for _, serviceName := range opts.Services { + // var b bytes.Buffer + // logger.Debugf("Getting status for %v\n", serviceName) + // commandArgs := append(args, serviceName) + + // if err := p.runDockerComposeCmd(dockerComposeOptions{args: commandArgs, env: opts.Env, stdout: &b}); err != nil { + // return nil, err + // } + // containerID := strings.TrimSpace(b.String()) + // logger.Debugf("Checking containerID \"%s\" for %v", containerID, serviceName) + // containerDescriptions, err := docker.InspectContainers(containerID) + // if err != nil { + // return nil, err + // } + + // containerDescription := containerDescriptions[0] + + // service := ServiceStatus{ + // ID: containerDescription.ID, + // Status: containerDescription.State.Status, + // ExitCode: containerDescription.State.ExitCode, + // Name: serviceName, + // } + // if containerDescription.State.Health != nil { + // service.Health = containerDescription.State.Health.Status + // } + // services = append(services, service) + // } + + // return services, nil +} + +func getVersionFromDockerImage(dockerImage string) string { + fields := strings.Split(dockerImage, ":") + if len(fields) == 2 { + return fields[1] + } + return "latest" + +} + // WaitForHealthy method waits until all containers are healthy. func (p *Project) WaitForHealthy(opts CommandOptions) error { // Read container IDs @@ -414,3 +517,11 @@ func (p *Project) ContainerName(serviceName string) string { } return fmt.Sprintf("%s-%s-1", p.name, serviceName) } + +// ContainerStatus method returns the status for the given service. +func (p *Project) ContainerStatus(serviceName string) string { + if p.dockerComposeV1 { + return fmt.Sprintf("%s_%s_1", p.name, serviceName) + } + return fmt.Sprintf("%s-%s-1", p.name, serviceName) +} diff --git a/internal/docker/docker.go b/internal/docker/docker.go index f780c890c2..c0077964e2 100644 --- a/internal/docker/docker.go +++ b/internal/docker/docker.go @@ -27,7 +27,11 @@ type NetworkDescription struct { // ContainerDescription describes the Docker container. type ContainerDescription struct { + Config struct { + Image string + } ID string + Name string State struct { Status string ExitCode int diff --git a/internal/stack/compose.go b/internal/stack/compose.go index 2d26510441..7eebe9d789 100644 --- a/internal/stack/compose.go +++ b/internal/stack/compose.go @@ -146,6 +146,27 @@ func dockerComposeDown(options Options) error { return nil } +func dockerComposeStatus(options Options) ([]compose.ServiceStatus, error) { + p, err := compose.NewProject(DockerComposeProjectName, options.Profile.FetchPath(profile.SnapshotFile)) + if err != nil { + return nil, errors.Wrap(err, "could not create docker compose project") + } + + statusOptions := compose.CommandOptions{ + Env: newEnvBuilder(). + withEnv(stackVariantAsEnv(options.StackVersion)). + withEnvs(options.Profile.ComposeEnvVars()). + build(), + Services: options.Services, + } + + statusServices, err := p.Status(statusOptions) + if err != nil { + return nil, errors.Wrap(err, "running command failed") + } + return statusServices, nil +} + func withDependentServices(services []string) []string { for _, aService := range services { if aService == "elastic-agent" { diff --git a/internal/stack/status.go b/internal/stack/status.go new file mode 100644 index 0000000000..3c5203fda8 --- /dev/null +++ b/internal/stack/status.go @@ -0,0 +1,20 @@ +package stack + +import ( + "github.com/pkg/errors" + + "github.com/elastic/elastic-package/internal/compose" +) + +// Status shows the status for each service +func Status(options Options) ([]compose.ServiceStatus, error) { + opts := options + opts.Services = observedServices + + statusServices, err := dockerComposeStatus(opts) + if err != nil { + return nil, errors.Wrap(err, "stack status failed") + } + + return statusServices, nil +} From 859da259a6069e29e507d894be2b6aad448b2f22 Mon Sep 17 00:00:00 2001 From: Mario Rodriguez Date: Fri, 29 Jul 2022 17:32:33 +0200 Subject: [PATCH 02/28] Removed health and exit code columns --- cmd/stack.go | 9 +--- internal/compose/compose.go | 90 +++++++++++++------------------------ 2 files changed, 32 insertions(+), 67 deletions(-) diff --git a/cmd/stack.go b/cmd/stack.go index c24d544fd0..2c64706ed5 100644 --- a/cmd/stack.go +++ b/cmd/stack.go @@ -6,7 +6,6 @@ package cmd import ( "fmt" - "strconv" "strings" "github.com/jedib0t/go-pretty/table" @@ -333,14 +332,10 @@ func printInitConfig(cmd *cobra.Command, profile *profile.Profile) error { func printStatus(cmd *cobra.Command, servicesStatus []compose.ServiceStatus) { t := table.NewWriter() - t.AppendHeader(table.Row{"Service", "Version", "Status", "Health", "Exit Code (if any)"}) + t.AppendHeader(table.Row{"Service", "Version", "Status"}) for _, service := range servicesStatus { - exitCode := strconv.Itoa(service.ExitCode) - if service.ExitCode == 0 { - exitCode = "" - } - t.AppendRow(table.Row{service.Name, service.Version, service.Status, service.Health, exitCode}) + t.AppendRow(table.Row{service.Name, service.Version, service.Status}) } t.SetStyle(table.StyleRounded) cmd.Println(t.Render()) diff --git a/internal/compose/compose.go b/internal/compose/compose.go index 5779ede4ff..b82c94a38c 100644 --- a/internal/compose/compose.go +++ b/internal/compose/compose.go @@ -50,13 +50,11 @@ type service struct { } type ServiceStatus struct { - ExitCode int - Health string - ID string - Name string - Running bool - Status string - Version string + ID string + Name string + Running bool + Status string + Version string } type portMapping struct { @@ -301,18 +299,16 @@ func (p *Project) Status(opts CommandOptions) ([]ServiceStatus, error) { args := p.baseArgs() args = append(args, "ps") args = append(args, "-q") + args = append(args, opts.Services...) logger.Debugf("Services to check: %v", opts.Services) var services []ServiceStatus var b bytes.Buffer - args = append(args, opts.Services...) - if err := p.runDockerComposeCmd(dockerComposeOptions{args: args, env: opts.Env, stdout: &b}); err != nil { return nil, err } - logger.Debugf("%v\n", b.String()) containerIDs := strings.Fields(b.String()) containerDescriptions, err := docker.InspectContainers(containerIDs...) @@ -322,60 +318,35 @@ func (p *Project) Status(opts CommandOptions) ([]ServiceStatus, error) { var serviceNameRegex = regexp.MustCompile(fmt.Sprintf("^/%v_(.*)_\\d+$", p.name)) for _, containerDescription := range containerDescriptions { - logger.Debugf("Image container: \"%v\"", containerDescription.Config.Image) - matches := serviceNameRegex.FindStringSubmatch(containerDescription.Name) - if matches == nil || len(matches) == 0 { - return nil, fmt.Errorf("Unknown container %v", containerDescription.Name) - } - logger.Debugf("Matches: %v", matches) - service := ServiceStatus{ - ID: containerDescription.ID, - Name: matches[1], - Running: containerDescription.State.Status == "running", - Status: containerDescription.State.Status, - Version: getVersionFromDockerImage(containerDescription.Config.Image), - } - if containerDescription.State.Status != "running" { - service.ExitCode = containerDescription.State.ExitCode - } - if containerDescription.State.Health != nil { - service.Health = containerDescription.State.Health.Status + service, err := newServiceStatus(&containerDescription, serviceNameRegex) + if err != nil { + return nil, err } - services = append(services, service) + services = append(services, *service) } return services, nil +} + +func newServiceStatus(description *docker.ContainerDescription, regex *regexp.Regexp) (*ServiceStatus, error) { + logger.Debugf("Image container: \"%v\"", description.Config.Image) + matches := regex.FindStringSubmatch(description.Name) + if matches == nil || len(matches) == 0 { + return nil, fmt.Errorf("Unknown container %v", description.Name) + } - // for _, serviceName := range opts.Services { - // var b bytes.Buffer - // logger.Debugf("Getting status for %v\n", serviceName) - // commandArgs := append(args, serviceName) - - // if err := p.runDockerComposeCmd(dockerComposeOptions{args: commandArgs, env: opts.Env, stdout: &b}); err != nil { - // return nil, err - // } - // containerID := strings.TrimSpace(b.String()) - // logger.Debugf("Checking containerID \"%s\" for %v", containerID, serviceName) - // containerDescriptions, err := docker.InspectContainers(containerID) - // if err != nil { - // return nil, err - // } - - // containerDescription := containerDescriptions[0] - - // service := ServiceStatus{ - // ID: containerDescription.ID, - // Status: containerDescription.State.Status, - // ExitCode: containerDescription.State.ExitCode, - // Name: serviceName, - // } - // if containerDescription.State.Health != nil { - // service.Health = containerDescription.State.Health.Status - // } - // services = append(services, service) - // } - - // return services, nil + logger.Debugf("Matches: %v", matches) + service := ServiceStatus{ + ID: description.ID, + Name: matches[1], + Status: description.State.Status, + Version: getVersionFromDockerImage(description.Config.Image), + } + if description.State.Health != nil { + service.Status = fmt.Sprintf("%v (%v)", service.Status, description.State.Health.Status) + } + + return &service, nil } func getVersionFromDockerImage(dockerImage string) string { @@ -384,7 +355,6 @@ func getVersionFromDockerImage(dockerImage string) string { return fields[1] } return "latest" - } // WaitForHealthy method waits until all containers are healthy. From 8832b95c133bea47d7b03103120508442a8192ac Mon Sep 17 00:00:00 2001 From: Mario Rodriguez Date: Fri, 29 Jul 2022 18:02:51 +0200 Subject: [PATCH 03/28] Fix lint errors --- internal/compose/compose.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/compose/compose.go b/internal/compose/compose.go index b82c94a38c..038e1f8312 100644 --- a/internal/compose/compose.go +++ b/internal/compose/compose.go @@ -331,8 +331,8 @@ func (p *Project) Status(opts CommandOptions) ([]ServiceStatus, error) { func newServiceStatus(description *docker.ContainerDescription, regex *regexp.Regexp) (*ServiceStatus, error) { logger.Debugf("Image container: \"%v\"", description.Config.Image) matches := regex.FindStringSubmatch(description.Name) - if matches == nil || len(matches) == 0 { - return nil, fmt.Errorf("Unknown container %v", description.Name) + if len(matches) == 0 { + return nil, fmt.Errorf("container not recognised: %v", description.Name) } logger.Debugf("Matches: %v", matches) From 725c734de35a60407ed6024fff1ef1468913ed57 Mon Sep 17 00:00:00 2001 From: Mario Rodriguez Date: Fri, 29 Jul 2022 18:03:00 +0200 Subject: [PATCH 04/28] Add license header --- internal/stack/status.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/internal/stack/status.go b/internal/stack/status.go index 3c5203fda8..669d20651e 100644 --- a/internal/stack/status.go +++ b/internal/stack/status.go @@ -1,3 +1,7 @@ +// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +// or more contributor license agreements. Licensed under the Elastic License; +// you may not use this file except in compliance with the Elastic License. + package stack import ( From be66f84a9183c6463da025e95c1aef78b55a67ad Mon Sep 17 00:00:00 2001 From: Mario Rodriguez Date: Tue, 9 Aug 2022 18:21:21 +0200 Subject: [PATCH 05/28] Manage use case with stack down - no containers --- cmd/stack.go | 4 ++++ internal/compose/compose.go | 3 +++ 2 files changed, 7 insertions(+) diff --git a/cmd/stack.go b/cmd/stack.go index 2c64706ed5..a0f255e63e 100644 --- a/cmd/stack.go +++ b/cmd/stack.go @@ -331,6 +331,10 @@ func printInitConfig(cmd *cobra.Command, profile *profile.Profile) error { } func printStatus(cmd *cobra.Command, servicesStatus []compose.ServiceStatus) { + if len(servicesStatus) == 0 { + cmd.Printf(" - No service running\n") + return + } t := table.NewWriter() t.AppendHeader(table.Row{"Service", "Version", "Status"}) diff --git a/internal/compose/compose.go b/internal/compose/compose.go index 038e1f8312..33c0f7a39a 100644 --- a/internal/compose/compose.go +++ b/internal/compose/compose.go @@ -310,6 +310,9 @@ func (p *Project) Status(opts CommandOptions) ([]ServiceStatus, error) { } containerIDs := strings.Fields(b.String()) + if len(containerIDs) == 0 { + return services, nil + } containerDescriptions, err := docker.InspectContainers(containerIDs...) if err != nil { From 33184f5a79bfc972e49bd264953c6708bf2e3ece Mon Sep 17 00:00:00 2001 From: Mario Rodriguez Date: Tue, 9 Aug 2022 19:23:57 +0200 Subject: [PATCH 06/28] Use labels instead of regex to retrieve the service name --- internal/compose/compose.go | 15 ++++----------- internal/docker/docker.go | 3 ++- 2 files changed, 6 insertions(+), 12 deletions(-) diff --git a/internal/compose/compose.go b/internal/compose/compose.go index 33c0f7a39a..7c3928cd22 100644 --- a/internal/compose/compose.go +++ b/internal/compose/compose.go @@ -10,7 +10,6 @@ import ( "io" "os" "os/exec" - "regexp" "strconv" "strings" "time" @@ -319,9 +318,8 @@ func (p *Project) Status(opts CommandOptions) ([]ServiceStatus, error) { return nil, err } - var serviceNameRegex = regexp.MustCompile(fmt.Sprintf("^/%v_(.*)_\\d+$", p.name)) for _, containerDescription := range containerDescriptions { - service, err := newServiceStatus(&containerDescription, serviceNameRegex) + service, err := newServiceStatus(&containerDescription) if err != nil { return nil, err } @@ -331,17 +329,12 @@ func (p *Project) Status(opts CommandOptions) ([]ServiceStatus, error) { return services, nil } -func newServiceStatus(description *docker.ContainerDescription, regex *regexp.Regexp) (*ServiceStatus, error) { +func newServiceStatus(description *docker.ContainerDescription) (*ServiceStatus, error) { logger.Debugf("Image container: \"%v\"", description.Config.Image) - matches := regex.FindStringSubmatch(description.Name) - if len(matches) == 0 { - return nil, fmt.Errorf("container not recognised: %v", description.Name) - } - - logger.Debugf("Matches: %v", matches) + logger.Debugf("Service: \"%v\"", description.Config.Labels["com.docker.compose.service"]) service := ServiceStatus{ ID: description.ID, - Name: matches[1], + Name: description.Config.Labels["com.docker.compose.service"], Status: description.State.Status, Version: getVersionFromDockerImage(description.Config.Image), } diff --git a/internal/docker/docker.go b/internal/docker/docker.go index c0077964e2..44362a2c39 100644 --- a/internal/docker/docker.go +++ b/internal/docker/docker.go @@ -28,7 +28,8 @@ type NetworkDescription struct { // ContainerDescription describes the Docker container. type ContainerDescription struct { Config struct { - Image string + Image string + Labels map[string]string } ID string Name string From 6f0303100a889adb31de17f754b38aa34c798805 Mon Sep 17 00:00:00 2001 From: Mario Rodriguez Date: Tue, 9 Aug 2022 19:50:08 +0200 Subject: [PATCH 07/28] Add tests for getting version from docker image --- internal/compose/compose_test.go | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/internal/compose/compose_test.go b/internal/compose/compose_test.go index 4d893a1d67..8f09f1fe00 100644 --- a/internal/compose/compose_test.go +++ b/internal/compose/compose_test.go @@ -31,3 +31,21 @@ func TestIntOrStringYaml(t *testing.T) { }) } } + +func TestGetVersionFromDockerImage(t *testing.T) { + cases := []struct { + dockerImage string + expected int + }{ + {"docker.test/test:1.42.0", "1.42.0"}, + {"docker.test/test", "latest"}, + } + + for _, c := range cases { + t.Run(c.dockerImage, func(t *testing.T) { + version := getVersionFromDockerImage(c.dockerImage) + require.NoError(t, err) + assert.Equal(t, c.expected, version) + }) + } +} From 234a44eb1d5a7da809085c55a24c6f9900a9bd36 Mon Sep 17 00:00:00 2001 From: Mario Rodriguez Date: Tue, 9 Aug 2022 20:06:21 +0200 Subject: [PATCH 08/28] fix test --- internal/compose/compose_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/compose/compose_test.go b/internal/compose/compose_test.go index 8f09f1fe00..40760376e5 100644 --- a/internal/compose/compose_test.go +++ b/internal/compose/compose_test.go @@ -35,7 +35,7 @@ func TestIntOrStringYaml(t *testing.T) { func TestGetVersionFromDockerImage(t *testing.T) { cases := []struct { dockerImage string - expected int + expected string }{ {"docker.test/test:1.42.0", "1.42.0"}, {"docker.test/test", "latest"}, From da64d9e27bd8b653e5b968a28a56a0c525d50bc5 Mon Sep 17 00:00:00 2001 From: Mario Rodriguez Date: Tue, 9 Aug 2022 20:09:15 +0200 Subject: [PATCH 09/28] fix test --- internal/compose/compose_test.go | 1 - 1 file changed, 1 deletion(-) diff --git a/internal/compose/compose_test.go b/internal/compose/compose_test.go index 40760376e5..be8733902d 100644 --- a/internal/compose/compose_test.go +++ b/internal/compose/compose_test.go @@ -44,7 +44,6 @@ func TestGetVersionFromDockerImage(t *testing.T) { for _, c := range cases { t.Run(c.dockerImage, func(t *testing.T) { version := getVersionFromDockerImage(c.dockerImage) - require.NoError(t, err) assert.Equal(t, c.expected, version) }) } From 91e61d1707488ab11db8f0262a3e53b2c9344a57 Mon Sep 17 00:00:00 2001 From: Mario Rodriguez Date: Tue, 9 Aug 2022 20:43:35 +0200 Subject: [PATCH 10/28] Add tests for newServiceStatus --- internal/compose/compose.go | 3 +- internal/compose/compose_test.go | 146 +++++++++++++++++++++++++++++++ 2 files changed, 147 insertions(+), 2 deletions(-) diff --git a/internal/compose/compose.go b/internal/compose/compose.go index 7c3928cd22..5e2df25544 100644 --- a/internal/compose/compose.go +++ b/internal/compose/compose.go @@ -51,7 +51,6 @@ type service struct { type ServiceStatus struct { ID string Name string - Running bool Status string Version string } @@ -338,7 +337,7 @@ func newServiceStatus(description *docker.ContainerDescription) (*ServiceStatus, Status: description.State.Status, Version: getVersionFromDockerImage(description.Config.Image), } - if description.State.Health != nil { + if description.State.Health != nil && description.State.Status == "running" { service.Status = fmt.Sprintf("%v (%v)", service.Status, description.State.Health.Status) } diff --git a/internal/compose/compose_test.go b/internal/compose/compose_test.go index be8733902d..f303b4ff47 100644 --- a/internal/compose/compose_test.go +++ b/internal/compose/compose_test.go @@ -6,10 +6,13 @@ package compose import ( "testing" + "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "gopkg.in/yaml.v3" + + "github.com/elastic/elastic-package/internal/docker" ) func TestIntOrStringYaml(t *testing.T) { @@ -48,3 +51,146 @@ func TestGetVersionFromDockerImage(t *testing.T) { }) } } + +func TestNewServiceStatus(t *testing.T) { + cases := []struct { + name string + description docker.ContainerDescription + expected ServiceStatus + }{ + { + name: "commonService", + description: docker.ContainerDescription{ + Config: struct { + Image string + Labels map[string]string + }{ + Image: "docker.test:1.42.0", + Labels: map[string]string{"com.docker.compose.service": "myservice", "foo": "bar"}, + }, + ID: "123456789ab", + Name: "project-my-service", + State: struct { + Status string + ExitCode int + Health *struct { + Status string + Log []struct { + Start time.Time + ExitCode int + Output string + } + } + }{ + Status: "running", + ExitCode: 0, + Health: &struct { + Status string + Log []struct { + Start time.Time + ExitCode int + Output string + } + }{ + Status: "healthy", + }, + }, + }, + expected: ServiceStatus{ + ID: "123456789ab", + Name: "myservice", + Status: "running (healthy)", + Version: "1.42.0", + }, + }, + { + name: "exitedService", + description: docker.ContainerDescription{ + Config: struct { + Image string + Labels map[string]string + }{ + Image: "docker.test:1.42.0", + Labels: map[string]string{"com.docker.compose.service": "myservice", "foo": "bar"}, + }, + ID: "123456789ab", + Name: "project-my-service", + State: struct { + Status string + ExitCode int + Health *struct { + Status string + Log []struct { + Start time.Time + ExitCode int + Output string + } + } + }{ + Status: "exited", + ExitCode: 128, + Health: nil, + }, + }, + expected: ServiceStatus{ + ID: "123456789ab", + Name: "myservice", + Status: "exited", + Version: "1.42.0", + }, + }, + { + name: "startingService", + description: docker.ContainerDescription{ + Config: struct { + Image string + Labels map[string]string + }{ + Image: "docker.test:1.42.0", + Labels: map[string]string{"com.docker.compose.service": "myservice", "foo": "bar"}, + }, + ID: "123456789ab", + Name: "project-my-service", + State: struct { + Status string + ExitCode int + Health *struct { + Status string + Log []struct { + Start time.Time + ExitCode int + Output string + } + } + }{ + Status: "running", + ExitCode: 0, + Health: &struct { + Status string + Log []struct { + Start time.Time + ExitCode int + Output string + } + }{ + Status: "starting", + }, + }, + }, + expected: ServiceStatus{ + ID: "123456789ab", + Name: "myservice", + Status: "running (starting)", + Version: "1.42.0", + }, + }, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + serviceStatus, err := newServiceStatus(&c.description) + require.NoError(t, err) + assert.Equal(t, &c.expected, serviceStatus) + }) + } +} From e6da583a10209eaf69484868b148a987fc084041 Mon Sep 17 00:00:00 2001 From: Mario Rodriguez Date: Wed, 10 Aug 2022 19:10:45 +0200 Subject: [PATCH 11/28] review error messages --- cmd/stack.go | 2 +- internal/compose/compose.go | 9 +-------- internal/stack/status.go | 4 +--- 3 files changed, 3 insertions(+), 12 deletions(-) diff --git a/cmd/stack.go b/cmd/stack.go index a0f255e63e..43f9c94e76 100644 --- a/cmd/stack.go +++ b/cmd/stack.go @@ -265,7 +265,7 @@ func setupStackCommand() *cobraext.Command { Profile: profile, }) if err != nil { - return errors.Wrap(err, "failed updating the stack images") + return errors.Wrap(err, "failed getting stack status") } cmd.Println("Status of Elastic stack services:") diff --git a/internal/compose/compose.go b/internal/compose/compose.go index 5e2df25544..5de4cfd756 100644 --- a/internal/compose/compose.go +++ b/internal/compose/compose.go @@ -304,6 +304,7 @@ func (p *Project) Status(opts CommandOptions) ([]ServiceStatus, error) { var b bytes.Buffer if err := p.runDockerComposeCmd(dockerComposeOptions{args: args, env: opts.Env, stdout: &b}); err != nil { + logger.Errorf("is Elastic stack created/running?") return nil, err } @@ -482,11 +483,3 @@ func (p *Project) ContainerName(serviceName string) string { } return fmt.Sprintf("%s-%s-1", p.name, serviceName) } - -// ContainerStatus method returns the status for the given service. -func (p *Project) ContainerStatus(serviceName string) string { - if p.dockerComposeV1 { - return fmt.Sprintf("%s_%s_1", p.name, serviceName) - } - return fmt.Sprintf("%s-%s-1", p.name, serviceName) -} diff --git a/internal/stack/status.go b/internal/stack/status.go index 669d20651e..cf4c256515 100644 --- a/internal/stack/status.go +++ b/internal/stack/status.go @@ -5,8 +5,6 @@ package stack import ( - "github.com/pkg/errors" - "github.com/elastic/elastic-package/internal/compose" ) @@ -17,7 +15,7 @@ func Status(options Options) ([]compose.ServiceStatus, error) { statusServices, err := dockerComposeStatus(opts) if err != nil { - return nil, errors.Wrap(err, "stack status failed") + return nil, err } return statusServices, nil From 1cb64198336149aaeb7e81ef55e29e21c298181f Mon Sep 17 00:00:00 2001 From: Mario Rodriguez Date: Wed, 10 Aug 2022 19:38:44 +0200 Subject: [PATCH 12/28] Add exit code --- internal/compose/compose.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/internal/compose/compose.go b/internal/compose/compose.go index 5de4cfd756..4ead93a752 100644 --- a/internal/compose/compose.go +++ b/internal/compose/compose.go @@ -338,9 +338,12 @@ func newServiceStatus(description *docker.ContainerDescription) (*ServiceStatus, Status: description.State.Status, Version: getVersionFromDockerImage(description.Config.Image), } - if description.State.Health != nil && description.State.Status == "running" { + if description.State.Status == "running" { service.Status = fmt.Sprintf("%v (%v)", service.Status, description.State.Health.Status) } + if description.State.Status == "exited" { + service.Status = fmt.Sprintf("%v (%v)", service.Status, description.State.ExitCode) + } return &service, nil } From feabe050392e27d554ab822bb1e5674c18103224 Mon Sep 17 00:00:00 2001 From: Mario Rodriguez Date: Wed, 10 Aug 2022 19:39:40 +0200 Subject: [PATCH 13/28] update test --- internal/compose/compose_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/compose/compose_test.go b/internal/compose/compose_test.go index f303b4ff47..c8f775e393 100644 --- a/internal/compose/compose_test.go +++ b/internal/compose/compose_test.go @@ -135,7 +135,7 @@ func TestNewServiceStatus(t *testing.T) { expected: ServiceStatus{ ID: "123456789ab", Name: "myservice", - Status: "exited", + Status: "exited (128)", Version: "1.42.0", }, }, From 16203f20b120ca78c3f7a55f4ec3859ce05a76a9 Mon Sep 17 00:00:00 2001 From: Mario Rodriguez Date: Wed, 10 Aug 2022 21:33:43 +0200 Subject: [PATCH 14/28] List all services filtering out is_ready ones --- internal/compose/compose.go | 12 ++++++++---- internal/stack/compose.go | 5 ++--- internal/stack/status.go | 7 ++----- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/internal/compose/compose.go b/internal/compose/compose.go index 4ead93a752..aa1176db38 100644 --- a/internal/compose/compose.go +++ b/internal/compose/compose.go @@ -30,6 +30,9 @@ const ( waitForHealthyInterval = 1 * time.Second ) +// serviceLabelDockerCompose is the label with the service name created by docker-compose +const serviceLabelDockerCompose = "com.docker.compose.service" + // Project represents a Docker Compose project. type Project struct { name string @@ -297,9 +300,7 @@ func (p *Project) Status(opts CommandOptions) ([]ServiceStatus, error) { args := p.baseArgs() args = append(args, "ps") args = append(args, "-q") - args = append(args, opts.Services...) - logger.Debugf("Services to check: %v", opts.Services) var services []ServiceStatus var b bytes.Buffer @@ -319,6 +320,9 @@ func (p *Project) Status(opts CommandOptions) ([]ServiceStatus, error) { } for _, containerDescription := range containerDescriptions { + if strings.Contains(containerDescription.Config.Labels[serviceLabelDockerCompose], "is_ready") { + continue + } service, err := newServiceStatus(&containerDescription) if err != nil { return nil, err @@ -331,10 +335,10 @@ func (p *Project) Status(opts CommandOptions) ([]ServiceStatus, error) { func newServiceStatus(description *docker.ContainerDescription) (*ServiceStatus, error) { logger.Debugf("Image container: \"%v\"", description.Config.Image) - logger.Debugf("Service: \"%v\"", description.Config.Labels["com.docker.compose.service"]) + logger.Debugf("Service: \"%v\"", description.Config.Labels[serviceLabelDockerCompose]) service := ServiceStatus{ ID: description.ID, - Name: description.Config.Labels["com.docker.compose.service"], + Name: description.Config.Labels[serviceLabelDockerCompose], Status: description.State.Status, Version: getVersionFromDockerImage(description.Config.Image), } diff --git a/internal/stack/compose.go b/internal/stack/compose.go index 7eebe9d789..cf4d418672 100644 --- a/internal/stack/compose.go +++ b/internal/stack/compose.go @@ -157,14 +157,13 @@ func dockerComposeStatus(options Options) ([]compose.ServiceStatus, error) { withEnv(stackVariantAsEnv(options.StackVersion)). withEnvs(options.Profile.ComposeEnvVars()). build(), - Services: options.Services, } - statusServices, err := p.Status(statusOptions) + servicesStatus, err := p.Status(statusOptions) if err != nil { return nil, errors.Wrap(err, "running command failed") } - return statusServices, nil + return servicesStatus, nil } func withDependentServices(services []string) []string { diff --git a/internal/stack/status.go b/internal/stack/status.go index cf4c256515..c78b0e663c 100644 --- a/internal/stack/status.go +++ b/internal/stack/status.go @@ -10,13 +10,10 @@ import ( // Status shows the status for each service func Status(options Options) ([]compose.ServiceStatus, error) { - opts := options - opts.Services = observedServices - - statusServices, err := dockerComposeStatus(opts) + servicesStatus, err := dockerComposeStatus(options) if err != nil { return nil, err } - return statusServices, nil + return servicesStatus, nil } From 74ecca5ca559e736cb913a9b909ed503fa3ccfa3 Mon Sep 17 00:00:00 2001 From: Mario Rodriguez Date: Thu, 11 Aug 2022 00:29:45 +0200 Subject: [PATCH 15/28] Move filtering to stack package --- internal/compose/compose.go | 3 --- internal/stack/compose.go | 4 +++- internal/stack/status.go | 15 ++++++++++++++- 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/internal/compose/compose.go b/internal/compose/compose.go index aa1176db38..0e971cd6da 100644 --- a/internal/compose/compose.go +++ b/internal/compose/compose.go @@ -320,9 +320,6 @@ func (p *Project) Status(opts CommandOptions) ([]ServiceStatus, error) { } for _, containerDescription := range containerDescriptions { - if strings.Contains(containerDescription.Config.Labels[serviceLabelDockerCompose], "is_ready") { - continue - } service, err := newServiceStatus(&containerDescription) if err != nil { return nil, err diff --git a/internal/stack/compose.go b/internal/stack/compose.go index cf4d418672..f584788fb1 100644 --- a/internal/stack/compose.go +++ b/internal/stack/compose.go @@ -14,6 +14,8 @@ import ( "github.com/elastic/elastic-package/internal/profile" ) +const readyServicesSuffix = "is_ready" + type envBuilder struct { vars []string } @@ -182,7 +184,7 @@ func withIsReadyServices(services []string) []string { var allServices []string for _, aService := range services { - allServices = append(allServices, aService, fmt.Sprintf("%s_is_ready", aService)) + allServices = append(allServices, aService, fmt.Sprintf("%s_%s", aService, readyServicesSuffix)) } return allServices } diff --git a/internal/stack/status.go b/internal/stack/status.go index c78b0e663c..52715d5187 100644 --- a/internal/stack/status.go +++ b/internal/stack/status.go @@ -5,7 +5,10 @@ package stack import ( + "strings" + "github.com/elastic/elastic-package/internal/compose" + "github.com/elastic/elastic-package/internal/logger" ) // Status shows the status for each service @@ -15,5 +18,15 @@ func Status(options Options) ([]compose.ServiceStatus, error) { return nil, err } - return servicesStatus, nil + var services []compose.ServiceStatus + for _, status := range servicesStatus { + // filter the is_ready services + if strings.Contains(status.Name, readyServicesSuffix) { + logger.Debugf("Filtering out service: %s (%s)", status.Name, status.ID) + continue + } + services = append(services, status) + } + + return services, nil } From a62a69111bd43ab0016d644202f2f1a34acbd4a6 Mon Sep 17 00:00:00 2001 From: Mario Rodriguez Date: Thu, 11 Aug 2022 11:04:22 +0200 Subject: [PATCH 16/28] Removed unused version option --- internal/stack/compose.go | 1 - 1 file changed, 1 deletion(-) diff --git a/internal/stack/compose.go b/internal/stack/compose.go index f584788fb1..02669e637c 100644 --- a/internal/stack/compose.go +++ b/internal/stack/compose.go @@ -156,7 +156,6 @@ func dockerComposeStatus(options Options) ([]compose.ServiceStatus, error) { statusOptions := compose.CommandOptions{ Env: newEnvBuilder(). - withEnv(stackVariantAsEnv(options.StackVersion)). withEnvs(options.Profile.ComposeEnvVars()). build(), } From b50c9f14d63fafbf30514718973b487016ae975f Mon Sep 17 00:00:00 2001 From: Mario Rodriguez Date: Thu, 11 Aug 2022 12:02:14 +0200 Subject: [PATCH 17/28] Add tests for stack status command --- .ci/Jenkinsfile | 8 +++---- scripts/test-stack-command.sh | 41 +++++++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 4 deletions(-) diff --git a/.ci/Jenkinsfile b/.ci/Jenkinsfile index bd83b96f47..17fcd87002 100644 --- a/.ci/Jenkinsfile +++ b/.ci/Jenkinsfile @@ -97,10 +97,10 @@ pipeline { dir("${BASE_DIR}") { script { def basicTasks = [ - 'stack-command-default': generateTestCommandStage(command: 'test-stack-command-default', artifacts: ['build/elastic-stack-dump/stack/*/logs/*.log', 'build/elastic-stack-dump/stack/*/logs/fleet-server-internal/*']), - 'stack-command-oldest': generateTestCommandStage(command: 'test-stack-command-oldest', artifacts: ['build/elastic-stack-dump/stack/*/logs/*.log', 'build/elastic-stack-dump/stack/*/logs/fleet-server-internal/*']), - 'stack-command-7x': generateTestCommandStage(command: 'test-stack-command-7x', artifacts: ['build/elastic-stack-dump/stack/*/logs/*.log', 'build/elastic-stack-dump/stack/*/logs/fleet-server-internal/*']), - 'stack-command-8x': generateTestCommandStage(command: 'test-stack-command-8x', artifacts: ['build/elastic-stack-dump/stack/*/logs/*.log', 'build/elastic-stack-dump/stack/*/logs/fleet-server-internal/*']), + 'stack-command-default': generateTestCommandStage(command: 'test-stack-command-default', artifacts: ['build/elastic-stack-dump/stack/*/logs/*.log', 'build/elastic-stack-dump/stack/*/logs/fleet-server-internal/*', 'build/elastic-stack-status/*.txt']), + 'stack-command-oldest': generateTestCommandStage(command: 'test-stack-command-oldest', artifacts: ['build/elastic-stack-dump/stack/*/logs/*.log', 'build/elastic-stack-dump/stack/*/logs/fleet-server-internal/*', 'build/elastic-stack-status/*.txt']), + 'stack-command-7x': generateTestCommandStage(command: 'test-stack-command-7x', artifacts: ['build/elastic-stack-dump/stack/*/logs/*.log', 'build/elastic-stack-dump/stack/*/logs/fleet-server-internal/*', 'build/elastic-stack-status/*.txt']), + 'stack-command-8x': generateTestCommandStage(command: 'test-stack-command-8x', artifacts: ['build/elastic-stack-dump/stack/*/logs/*.log', 'build/elastic-stack-dump/stack/*/logs/fleet-server-internal/*', 'build/elastic-stack-status/*.txt']), 'check-packages-with-kind': generateTestCommandStage(command: 'test-check-packages-with-kind', artifacts: ['build/test-results/*.xml', 'build/kubectl-dump.txt', 'build/elastic-stack-dump/check-*/logs/*.log', 'build/elastic-stack-dump/check-*/logs/fleet-server-internal/*'], junitArtifacts: true, publishCoverage: true), 'check-packages-other': generateTestCommandStage(command: 'test-check-packages-other', artifacts: ['build/test-results/*.xml', 'build/elastic-stack-dump/check-*/logs/*.log', 'build/elastic-stack-dump/check-*/logs/fleet-server-internal/*'], junitArtifacts: true, publishCoverage: true), 'check-packages-with-custom-agent': generateTestCommandStage(command: 'test-check-packages-with-custom-agent', artifacts: ['build/test-results/*.xml', 'build/elastic-stack-dump/check-*/logs/*.log', 'build/elastic-stack-dump/check-*/logs/fleet-server-internal/*'], junitArtifacts: true, publishCoverage: true), diff --git a/scripts/test-stack-command.sh b/scripts/test-stack-command.sh index 44bbf16ebf..077d16f96a 100755 --- a/scripts/test-stack-command.sh +++ b/scripts/test-stack-command.sh @@ -16,13 +16,31 @@ cleanup() { exit $r } +default_version() { + grep "DefaultStackVersion =" internal/install/stack_version.go | awk '{print $3}' | tr -d '"' +} + +clean_status_output() { + local output_file="$1" + cat ${output_file} | grep "│" | tr -d ' ' +} + trap cleanup EXIT ARG_VERSION="" +EXPECTED_VERSION=$(default_version) if [ "${VERSION}" != "default" ]; then ARG_VERSION="--version ${VERSION}" + EXPECTED_VERSION=${VERSION} fi +OUTPUT_PATH_STATUS="build/elastic-stack-status" +mkdir -p ${OUTPUT_PATH_STATUS} + +# Initial status empty +elastic-package stack status 2> ${OUTPUT_PATH_STATUS}/initial.txt +grep "\- No service running" ${OUTPUT_PATH_STATUS}/initial.txt + # Update the stack elastic-package stack update -v ${ARG_VERSION} @@ -32,3 +50,26 @@ elastic-package stack up -d -v ${ARG_VERSION} # Verify it's accessible eval "$(elastic-package stack shellinit)" curl --cacert ${ELASTIC_PACKAGE_CA_CERT} -f ${ELASTIC_PACKAGE_KIBANA_HOST}/login | grep kbn-injected-metadata >/dev/null # healthcheck + +# Check status with running services +echo "Expected version : ${EXPECTED_VERSION}" +cat < ${OUTPUT_PATH_STATUS}/expected_running.txt +Status of Elastic stack services: +╭──────────────────┬─────────┬───────────────────╮ +│ SERVICE │ VERSION │ STATUS │ +├──────────────────┼─────────┼───────────────────┤ +│ elastic-agent │ ${EXPECTED_VERSION} │ running (healthy) │ +│ elasticsearch │ ${EXPECTED_VERSION} │ running (healthy) │ +│ fleet-server │ ${EXPECTED_VERSION} │ running (healthy) │ +│ kibana │ ${EXPECTED_VERSION} │ running (healthy) │ +│ package-registry │ latest │ running (healthy) │ +╰──────────────────┴─────────┴───────────────────╯ +EOF + +elastic-package stack status 2> ${OUTPUT_PATH_STATUS}/running.txt + +# Remove spaces to avoid issues with spaces between columns +clean_status_output "${OUTPUT_PATH_STATUS}/expected_running.txt" > ${OUTPUT_PATH_STATUS}/expected_no_spaces.txt +clean_status_output "${OUTPUT_PATH_STATUS}/running.txt" > ${OUTPUT_PATH_STATUS}/running_no_spaces.txt + +diff -q ${OUTPUT_PATH_STATUS}/running_no_spaces.txt ${OUTPUT_PATH_STATUS}/expected_no_spaces.txt From d4e2e7227d2719ebd1b0ff66fec1190698314a31 Mon Sep 17 00:00:00 2001 From: Mario Rodriguez Date: Thu, 11 Aug 2022 12:23:12 +0200 Subject: [PATCH 18/28] Add verbose --- scripts/test-stack-command.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/test-stack-command.sh b/scripts/test-stack-command.sh index 077d16f96a..36351b9d55 100755 --- a/scripts/test-stack-command.sh +++ b/scripts/test-stack-command.sh @@ -38,7 +38,7 @@ OUTPUT_PATH_STATUS="build/elastic-stack-status" mkdir -p ${OUTPUT_PATH_STATUS} # Initial status empty -elastic-package stack status 2> ${OUTPUT_PATH_STATUS}/initial.txt +elastic-package stack status -v 2> ${OUTPUT_PATH_STATUS}/initial.txt grep "\- No service running" ${OUTPUT_PATH_STATUS}/initial.txt # Update the stack @@ -66,7 +66,7 @@ Status of Elastic stack services: ╰──────────────────┴─────────┴───────────────────╯ EOF -elastic-package stack status 2> ${OUTPUT_PATH_STATUS}/running.txt +elastic-package stack status -v 2> ${OUTPUT_PATH_STATUS}/running.txt # Remove spaces to avoid issues with spaces between columns clean_status_output "${OUTPUT_PATH_STATUS}/expected_running.txt" > ${OUTPUT_PATH_STATUS}/expected_no_spaces.txt From 31453445eb1f990145dc158381f735b4f368eb74 Mon Sep 17 00:00:00 2001 From: Mario Rodriguez Date: Thu, 11 Aug 2022 12:47:28 +0200 Subject: [PATCH 19/28] Add stack version parameter to get STACK_VERSION_VARIANT --- cmd/stack.go | 9 ++++++++- internal/stack/compose.go | 1 + scripts/test-stack-command.sh | 4 ++-- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/cmd/stack.go b/cmd/stack.go index 43f9c94e76..c8344cdf6d 100644 --- a/cmd/stack.go +++ b/cmd/stack.go @@ -261,8 +261,14 @@ func setupStackCommand() *cobraext.Command { return errors.Wrap(err, "error loading profile") } + stackVersion, err := cmd.Flags().GetString(cobraext.StackVersionFlagName) + if err != nil { + return cobraext.FlagParsingError(err, cobraext.StackVersionFlagName) + } + servicesStatus, err := stack.Status(stack.Options{ - Profile: profile, + StackVersion: stackVersion, + Profile: profile, }) if err != nil { return errors.Wrap(err, "failed getting stack status") @@ -273,6 +279,7 @@ func setupStackCommand() *cobraext.Command { return nil }, } + statusCommand.Flags().StringP(cobraext.StackVersionFlagName, "", install.DefaultStackVersion, cobraext.StackVersionFlagDescription) cmd := &cobra.Command{ Use: "stack", diff --git a/internal/stack/compose.go b/internal/stack/compose.go index 02669e637c..f584788fb1 100644 --- a/internal/stack/compose.go +++ b/internal/stack/compose.go @@ -156,6 +156,7 @@ func dockerComposeStatus(options Options) ([]compose.ServiceStatus, error) { statusOptions := compose.CommandOptions{ Env: newEnvBuilder(). + withEnv(stackVariantAsEnv(options.StackVersion)). withEnvs(options.Profile.ComposeEnvVars()). build(), } diff --git a/scripts/test-stack-command.sh b/scripts/test-stack-command.sh index 36351b9d55..41306a3005 100755 --- a/scripts/test-stack-command.sh +++ b/scripts/test-stack-command.sh @@ -38,7 +38,7 @@ OUTPUT_PATH_STATUS="build/elastic-stack-status" mkdir -p ${OUTPUT_PATH_STATUS} # Initial status empty -elastic-package stack status -v 2> ${OUTPUT_PATH_STATUS}/initial.txt +elastic-package stack status -v ${ARG_VERSION} 2> ${OUTPUT_PATH_STATUS}/initial.txt grep "\- No service running" ${OUTPUT_PATH_STATUS}/initial.txt # Update the stack @@ -66,7 +66,7 @@ Status of Elastic stack services: ╰──────────────────┴─────────┴───────────────────╯ EOF -elastic-package stack status -v 2> ${OUTPUT_PATH_STATUS}/running.txt +elastic-package stack status -v ${ARG_VERSION} 2> ${OUTPUT_PATH_STATUS}/running.txt # Remove spaces to avoid issues with spaces between columns clean_status_output "${OUTPUT_PATH_STATUS}/expected_running.txt" > ${OUTPUT_PATH_STATUS}/expected_no_spaces.txt From ba14df8ab5c4b5c114e7f6f84424656a7e203272 Mon Sep 17 00:00:00 2001 From: Mario Rodriguez Date: Thu, 11 Aug 2022 13:05:01 +0200 Subject: [PATCH 20/28] Add subfolder per version to keep all files --- scripts/test-stack-command.sh | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/scripts/test-stack-command.sh b/scripts/test-stack-command.sh index 41306a3005..c7257724c9 100755 --- a/scripts/test-stack-command.sh +++ b/scripts/test-stack-command.sh @@ -34,7 +34,7 @@ if [ "${VERSION}" != "default" ]; then EXPECTED_VERSION=${VERSION} fi -OUTPUT_PATH_STATUS="build/elastic-stack-status" +OUTPUT_PATH_STATUS="build/elastic-stack-status/${VERSION}" mkdir -p ${OUTPUT_PATH_STATUS} # Initial status empty @@ -52,7 +52,6 @@ eval "$(elastic-package stack shellinit)" curl --cacert ${ELASTIC_PACKAGE_CA_CERT} -f ${ELASTIC_PACKAGE_KIBANA_HOST}/login | grep kbn-injected-metadata >/dev/null # healthcheck # Check status with running services -echo "Expected version : ${EXPECTED_VERSION}" cat < ${OUTPUT_PATH_STATUS}/expected_running.txt Status of Elastic stack services: ╭──────────────────┬─────────┬───────────────────╮ From 48d5a256a7a191b2dab57f7b33874b3b5076ae7e Mon Sep 17 00:00:00 2001 From: Mario Rodriguez Date: Thu, 11 Aug 2022 13:52:08 +0200 Subject: [PATCH 21/28] Update artifact rules --- .ci/Jenkinsfile | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.ci/Jenkinsfile b/.ci/Jenkinsfile index 17fcd87002..7db07ce651 100644 --- a/.ci/Jenkinsfile +++ b/.ci/Jenkinsfile @@ -97,10 +97,10 @@ pipeline { dir("${BASE_DIR}") { script { def basicTasks = [ - 'stack-command-default': generateTestCommandStage(command: 'test-stack-command-default', artifacts: ['build/elastic-stack-dump/stack/*/logs/*.log', 'build/elastic-stack-dump/stack/*/logs/fleet-server-internal/*', 'build/elastic-stack-status/*.txt']), - 'stack-command-oldest': generateTestCommandStage(command: 'test-stack-command-oldest', artifacts: ['build/elastic-stack-dump/stack/*/logs/*.log', 'build/elastic-stack-dump/stack/*/logs/fleet-server-internal/*', 'build/elastic-stack-status/*.txt']), - 'stack-command-7x': generateTestCommandStage(command: 'test-stack-command-7x', artifacts: ['build/elastic-stack-dump/stack/*/logs/*.log', 'build/elastic-stack-dump/stack/*/logs/fleet-server-internal/*', 'build/elastic-stack-status/*.txt']), - 'stack-command-8x': generateTestCommandStage(command: 'test-stack-command-8x', artifacts: ['build/elastic-stack-dump/stack/*/logs/*.log', 'build/elastic-stack-dump/stack/*/logs/fleet-server-internal/*', 'build/elastic-stack-status/*.txt']), + 'stack-command-default': generateTestCommandStage(command: 'test-stack-command-default', artifacts: ['build/elastic-stack-dump/stack/*/logs/*.log', 'build/elastic-stack-dump/stack/*/logs/fleet-server-internal/*', 'build/elastic-stack-status/*/*']), + 'stack-command-oldest': generateTestCommandStage(command: 'test-stack-command-oldest', artifacts: ['build/elastic-stack-dump/stack/*/logs/*.log', 'build/elastic-stack-dump/stack/*/logs/fleet-server-internal/*', 'build/elastic-stack-status/*/*']), + 'stack-command-7x': generateTestCommandStage(command: 'test-stack-command-7x', artifacts: ['build/elastic-stack-dump/stack/*/logs/*.log', 'build/elastic-stack-dump/stack/*/logs/fleet-server-internal/*', 'build/elastic-stack-status/*/*']), + 'stack-command-8x': generateTestCommandStage(command: 'test-stack-command-8x', artifacts: ['build/elastic-stack-dump/stack/*/logs/*.log', 'build/elastic-stack-dump/stack/*/logs/fleet-server-internal/*', 'build/elastic-stack-status/*/*']), 'check-packages-with-kind': generateTestCommandStage(command: 'test-check-packages-with-kind', artifacts: ['build/test-results/*.xml', 'build/kubectl-dump.txt', 'build/elastic-stack-dump/check-*/logs/*.log', 'build/elastic-stack-dump/check-*/logs/fleet-server-internal/*'], junitArtifacts: true, publishCoverage: true), 'check-packages-other': generateTestCommandStage(command: 'test-check-packages-other', artifacts: ['build/test-results/*.xml', 'build/elastic-stack-dump/check-*/logs/*.log', 'build/elastic-stack-dump/check-*/logs/fleet-server-internal/*'], junitArtifacts: true, publishCoverage: true), 'check-packages-with-custom-agent': generateTestCommandStage(command: 'test-check-packages-with-custom-agent', artifacts: ['build/test-results/*.xml', 'build/elastic-stack-dump/check-*/logs/*.log', 'build/elastic-stack-dump/check-*/logs/fleet-server-internal/*'], junitArtifacts: true, publishCoverage: true), From 9455dceae3a79c03718e20873cd2f102b56ea85f Mon Sep 17 00:00:00 2001 From: Mario Rodriguez Date: Thu, 11 Aug 2022 16:20:34 +0200 Subject: [PATCH 22/28] Remove unused field --- internal/compose/compose_test.go | 9 +++------ internal/docker/docker.go | 1 - 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/internal/compose/compose_test.go b/internal/compose/compose_test.go index c8f775e393..98a27dedc2 100644 --- a/internal/compose/compose_test.go +++ b/internal/compose/compose_test.go @@ -68,8 +68,7 @@ func TestNewServiceStatus(t *testing.T) { Image: "docker.test:1.42.0", Labels: map[string]string{"com.docker.compose.service": "myservice", "foo": "bar"}, }, - ID: "123456789ab", - Name: "project-my-service", + ID: "123456789ab", State: struct { Status string ExitCode int @@ -113,8 +112,7 @@ func TestNewServiceStatus(t *testing.T) { Image: "docker.test:1.42.0", Labels: map[string]string{"com.docker.compose.service": "myservice", "foo": "bar"}, }, - ID: "123456789ab", - Name: "project-my-service", + ID: "123456789ab", State: struct { Status string ExitCode int @@ -149,8 +147,7 @@ func TestNewServiceStatus(t *testing.T) { Image: "docker.test:1.42.0", Labels: map[string]string{"com.docker.compose.service": "myservice", "foo": "bar"}, }, - ID: "123456789ab", - Name: "project-my-service", + ID: "123456789ab", State: struct { Status string ExitCode int diff --git a/internal/docker/docker.go b/internal/docker/docker.go index 44362a2c39..4fbae3f0b0 100644 --- a/internal/docker/docker.go +++ b/internal/docker/docker.go @@ -32,7 +32,6 @@ type ContainerDescription struct { Labels map[string]string } ID string - Name string State struct { Status string ExitCode int From d6e72815afe3f6320f469fbea3068efee07ec3a6 Mon Sep 17 00:00:00 2001 From: Mario Rodriguez Date: Thu, 11 Aug 2022 17:04:04 +0200 Subject: [PATCH 23/28] review logger messages --- internal/compose/compose.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/internal/compose/compose.go b/internal/compose/compose.go index 0e971cd6da..f33d1553ff 100644 --- a/internal/compose/compose.go +++ b/internal/compose/compose.go @@ -324,6 +324,7 @@ func (p *Project) Status(opts CommandOptions) ([]ServiceStatus, error) { if err != nil { return nil, err } + logger.Debugf("Adding Service: \"%v\"", service.Name) services = append(services, *service) } @@ -331,8 +332,6 @@ func (p *Project) Status(opts CommandOptions) ([]ServiceStatus, error) { } func newServiceStatus(description *docker.ContainerDescription) (*ServiceStatus, error) { - logger.Debugf("Image container: \"%v\"", description.Config.Image) - logger.Debugf("Service: \"%v\"", description.Config.Labels[serviceLabelDockerCompose]) service := ServiceStatus{ ID: description.ID, Name: description.Config.Labels[serviceLabelDockerCompose], From b01c18e831ad3cc28225e14c11cea032b1def75e Mon Sep 17 00:00:00 2001 From: Mario Rodriguez Date: Thu, 11 Aug 2022 17:11:23 +0200 Subject: [PATCH 24/28] Remove ID field - not used --- internal/compose/compose.go | 2 -- internal/compose/compose_test.go | 3 --- internal/stack/status.go | 3 +-- 3 files changed, 1 insertion(+), 7 deletions(-) diff --git a/internal/compose/compose.go b/internal/compose/compose.go index f33d1553ff..964af27320 100644 --- a/internal/compose/compose.go +++ b/internal/compose/compose.go @@ -52,7 +52,6 @@ type service struct { } type ServiceStatus struct { - ID string Name string Status string Version string @@ -333,7 +332,6 @@ func (p *Project) Status(opts CommandOptions) ([]ServiceStatus, error) { func newServiceStatus(description *docker.ContainerDescription) (*ServiceStatus, error) { service := ServiceStatus{ - ID: description.ID, Name: description.Config.Labels[serviceLabelDockerCompose], Status: description.State.Status, Version: getVersionFromDockerImage(description.Config.Image), diff --git a/internal/compose/compose_test.go b/internal/compose/compose_test.go index 98a27dedc2..85c017eeea 100644 --- a/internal/compose/compose_test.go +++ b/internal/compose/compose_test.go @@ -96,7 +96,6 @@ func TestNewServiceStatus(t *testing.T) { }, }, expected: ServiceStatus{ - ID: "123456789ab", Name: "myservice", Status: "running (healthy)", Version: "1.42.0", @@ -131,7 +130,6 @@ func TestNewServiceStatus(t *testing.T) { }, }, expected: ServiceStatus{ - ID: "123456789ab", Name: "myservice", Status: "exited (128)", Version: "1.42.0", @@ -175,7 +173,6 @@ func TestNewServiceStatus(t *testing.T) { }, }, expected: ServiceStatus{ - ID: "123456789ab", Name: "myservice", Status: "running (starting)", Version: "1.42.0", diff --git a/internal/stack/status.go b/internal/stack/status.go index 52715d5187..3128bad96f 100644 --- a/internal/stack/status.go +++ b/internal/stack/status.go @@ -20,9 +20,8 @@ func Status(options Options) ([]compose.ServiceStatus, error) { var services []compose.ServiceStatus for _, status := range servicesStatus { - // filter the is_ready services if strings.Contains(status.Name, readyServicesSuffix) { - logger.Debugf("Filtering out service: %s (%s)", status.Name, status.ID) + logger.Debugf("Filtering out service: %s", status.Name) continue } services = append(services, status) From 5ade04f6ae55dd4f9e61b2df819f9a12faf195ea Mon Sep 17 00:00:00 2001 From: Mario Rodriguez Date: Thu, 11 Aug 2022 20:31:01 +0200 Subject: [PATCH 25/28] Use docker ps to get containers Use docker ps with filter parameter to get the containers related to a docker-compose scenario. This allows us to avoid loading environment variables and the profiles to get this information. --- cmd/stack.go | 23 +---- internal/compose/compose.go | 69 ------------- internal/compose/compose_test.go | 157 ---------------------------- internal/docker/docker.go | 15 +++ internal/stack/compose.go | 89 ++++++++++++---- internal/stack/compose_test.go | 169 +++++++++++++++++++++++++++++++ internal/stack/status.go | 7 +- 7 files changed, 258 insertions(+), 271 deletions(-) create mode 100644 internal/stack/compose_test.go diff --git a/cmd/stack.go b/cmd/stack.go index c8344cdf6d..f65932a1ee 100644 --- a/cmd/stack.go +++ b/cmd/stack.go @@ -14,7 +14,6 @@ import ( "github.com/elastic/elastic-package/internal/cobraext" "github.com/elastic/elastic-package/internal/common" - "github.com/elastic/elastic-package/internal/compose" "github.com/elastic/elastic-package/internal/install" "github.com/elastic/elastic-package/internal/profile" "github.com/elastic/elastic-package/internal/stack" @@ -252,24 +251,7 @@ func setupStackCommand() *cobraext.Command { Use: "status", Short: "Show status of the stack services", RunE: func(cmd *cobra.Command, args []string) error { - profileName, err := cmd.Flags().GetString(cobraext.ProfileFlagName) - if err != nil { - return cobraext.FlagParsingError(err, cobraext.ProfileFlagName) - } - profile, err := profile.LoadProfile(profileName) - if err != nil { - return errors.Wrap(err, "error loading profile") - } - - stackVersion, err := cmd.Flags().GetString(cobraext.StackVersionFlagName) - if err != nil { - return cobraext.FlagParsingError(err, cobraext.StackVersionFlagName) - } - - servicesStatus, err := stack.Status(stack.Options{ - StackVersion: stackVersion, - Profile: profile, - }) + servicesStatus, err := stack.Status() if err != nil { return errors.Wrap(err, "failed getting stack status") } @@ -279,7 +261,6 @@ func setupStackCommand() *cobraext.Command { return nil }, } - statusCommand.Flags().StringP(cobraext.StackVersionFlagName, "", install.DefaultStackVersion, cobraext.StackVersionFlagDescription) cmd := &cobra.Command{ Use: "stack", @@ -337,7 +318,7 @@ func printInitConfig(cmd *cobra.Command, profile *profile.Profile) error { return nil } -func printStatus(cmd *cobra.Command, servicesStatus []compose.ServiceStatus) { +func printStatus(cmd *cobra.Command, servicesStatus []stack.ServiceStatus) { if len(servicesStatus) == 0 { cmd.Printf(" - No service running\n") return diff --git a/internal/compose/compose.go b/internal/compose/compose.go index 964af27320..b40d4590ff 100644 --- a/internal/compose/compose.go +++ b/internal/compose/compose.go @@ -30,9 +30,6 @@ const ( waitForHealthyInterval = 1 * time.Second ) -// serviceLabelDockerCompose is the label with the service name created by docker-compose -const serviceLabelDockerCompose = "com.docker.compose.service" - // Project represents a Docker Compose project. type Project struct { name string @@ -51,12 +48,6 @@ type service struct { Environment map[string]string } -type ServiceStatus struct { - Name string - Status string - Version string -} - type portMapping struct { ExternalIP string ExternalPort int @@ -294,66 +285,6 @@ func (p *Project) Logs(opts CommandOptions) ([]byte, error) { return b.Bytes(), nil } -// Status returns status services for the selected service in the Docker Compose project. -func (p *Project) Status(opts CommandOptions) ([]ServiceStatus, error) { - args := p.baseArgs() - args = append(args, "ps") - args = append(args, "-q") - - var services []ServiceStatus - var b bytes.Buffer - - if err := p.runDockerComposeCmd(dockerComposeOptions{args: args, env: opts.Env, stdout: &b}); err != nil { - logger.Errorf("is Elastic stack created/running?") - return nil, err - } - - containerIDs := strings.Fields(b.String()) - if len(containerIDs) == 0 { - return services, nil - } - - containerDescriptions, err := docker.InspectContainers(containerIDs...) - if err != nil { - return nil, err - } - - for _, containerDescription := range containerDescriptions { - service, err := newServiceStatus(&containerDescription) - if err != nil { - return nil, err - } - logger.Debugf("Adding Service: \"%v\"", service.Name) - services = append(services, *service) - } - - return services, nil -} - -func newServiceStatus(description *docker.ContainerDescription) (*ServiceStatus, error) { - service := ServiceStatus{ - Name: description.Config.Labels[serviceLabelDockerCompose], - Status: description.State.Status, - Version: getVersionFromDockerImage(description.Config.Image), - } - if description.State.Status == "running" { - service.Status = fmt.Sprintf("%v (%v)", service.Status, description.State.Health.Status) - } - if description.State.Status == "exited" { - service.Status = fmt.Sprintf("%v (%v)", service.Status, description.State.ExitCode) - } - - return &service, nil -} - -func getVersionFromDockerImage(dockerImage string) string { - fields := strings.Split(dockerImage, ":") - if len(fields) == 2 { - return fields[1] - } - return "latest" -} - // WaitForHealthy method waits until all containers are healthy. func (p *Project) WaitForHealthy(opts CommandOptions) error { // Read container IDs diff --git a/internal/compose/compose_test.go b/internal/compose/compose_test.go index 85c017eeea..4d893a1d67 100644 --- a/internal/compose/compose_test.go +++ b/internal/compose/compose_test.go @@ -6,13 +6,10 @@ package compose import ( "testing" - "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "gopkg.in/yaml.v3" - - "github.com/elastic/elastic-package/internal/docker" ) func TestIntOrStringYaml(t *testing.T) { @@ -34,157 +31,3 @@ func TestIntOrStringYaml(t *testing.T) { }) } } - -func TestGetVersionFromDockerImage(t *testing.T) { - cases := []struct { - dockerImage string - expected string - }{ - {"docker.test/test:1.42.0", "1.42.0"}, - {"docker.test/test", "latest"}, - } - - for _, c := range cases { - t.Run(c.dockerImage, func(t *testing.T) { - version := getVersionFromDockerImage(c.dockerImage) - assert.Equal(t, c.expected, version) - }) - } -} - -func TestNewServiceStatus(t *testing.T) { - cases := []struct { - name string - description docker.ContainerDescription - expected ServiceStatus - }{ - { - name: "commonService", - description: docker.ContainerDescription{ - Config: struct { - Image string - Labels map[string]string - }{ - Image: "docker.test:1.42.0", - Labels: map[string]string{"com.docker.compose.service": "myservice", "foo": "bar"}, - }, - ID: "123456789ab", - State: struct { - Status string - ExitCode int - Health *struct { - Status string - Log []struct { - Start time.Time - ExitCode int - Output string - } - } - }{ - Status: "running", - ExitCode: 0, - Health: &struct { - Status string - Log []struct { - Start time.Time - ExitCode int - Output string - } - }{ - Status: "healthy", - }, - }, - }, - expected: ServiceStatus{ - Name: "myservice", - Status: "running (healthy)", - Version: "1.42.0", - }, - }, - { - name: "exitedService", - description: docker.ContainerDescription{ - Config: struct { - Image string - Labels map[string]string - }{ - Image: "docker.test:1.42.0", - Labels: map[string]string{"com.docker.compose.service": "myservice", "foo": "bar"}, - }, - ID: "123456789ab", - State: struct { - Status string - ExitCode int - Health *struct { - Status string - Log []struct { - Start time.Time - ExitCode int - Output string - } - } - }{ - Status: "exited", - ExitCode: 128, - Health: nil, - }, - }, - expected: ServiceStatus{ - Name: "myservice", - Status: "exited (128)", - Version: "1.42.0", - }, - }, - { - name: "startingService", - description: docker.ContainerDescription{ - Config: struct { - Image string - Labels map[string]string - }{ - Image: "docker.test:1.42.0", - Labels: map[string]string{"com.docker.compose.service": "myservice", "foo": "bar"}, - }, - ID: "123456789ab", - State: struct { - Status string - ExitCode int - Health *struct { - Status string - Log []struct { - Start time.Time - ExitCode int - Output string - } - } - }{ - Status: "running", - ExitCode: 0, - Health: &struct { - Status string - Log []struct { - Start time.Time - ExitCode int - Output string - } - }{ - Status: "starting", - }, - }, - }, - expected: ServiceStatus{ - Name: "myservice", - Status: "running (starting)", - Version: "1.42.0", - }, - }, - } - - for _, c := range cases { - t.Run(c.name, func(t *testing.T) { - serviceStatus, err := newServiceStatus(&c.description) - require.NoError(t, err) - assert.Equal(t, &c.expected, serviceStatus) - }) - } -} diff --git a/internal/docker/docker.go b/internal/docker/docker.go index 4fbae3f0b0..0f0c337b2b 100644 --- a/internal/docker/docker.go +++ b/internal/docker/docker.go @@ -90,6 +90,21 @@ func ContainerID(containerName string) (string, error) { return containerIDs[0], nil } +// ContainerIDsWithLabel function returns all the container IDs filtering per label. +func ContainerIDsWithLabel(label string) ([]string, error) { + cmd := exec.Command("docker", "ps", "-a", "--filter", "label="+label, "--format", "{{.ID}}") + errOutput := new(bytes.Buffer) + cmd.Stderr = errOutput + + logger.Debugf("output command: %s", cmd) + output, err := cmd.Output() + if err != nil { + return []string{}, errors.Wrapf(err, "error getting containers with label \"%s\" (stderr=%q)", label, errOutput.String()) + } + containerIDs := strings.Fields(string(output)) + return containerIDs, nil +} + // InspectNetwork function returns the network description for the selected network. func InspectNetwork(network string) ([]NetworkDescription, error) { cmd := exec.Command("docker", "network", "inspect", network) diff --git a/internal/stack/compose.go b/internal/stack/compose.go index f584788fb1..8409014508 100644 --- a/internal/stack/compose.go +++ b/internal/stack/compose.go @@ -6,16 +6,32 @@ package stack import ( "fmt" + "strings" "github.com/pkg/errors" "github.com/elastic/elastic-package/internal/compose" + "github.com/elastic/elastic-package/internal/docker" "github.com/elastic/elastic-package/internal/install" + "github.com/elastic/elastic-package/internal/logger" "github.com/elastic/elastic-package/internal/profile" ) +type ServiceStatus struct { + Name string + Status string + Version string +} + const readyServicesSuffix = "is_ready" +const ( + // serviceLabelDockerCompose is the label with the service name created by docker-compose + serviceLabelDockerCompose = "com.docker.compose.service" + // projectLabelDockerCompose is the label with the project name created by docker-compose + projectLabelDockerCompose = "com.docker.compose.project" +) + type envBuilder struct { vars []string } @@ -148,26 +164,6 @@ func dockerComposeDown(options Options) error { return nil } -func dockerComposeStatus(options Options) ([]compose.ServiceStatus, error) { - p, err := compose.NewProject(DockerComposeProjectName, options.Profile.FetchPath(profile.SnapshotFile)) - if err != nil { - return nil, errors.Wrap(err, "could not create docker compose project") - } - - statusOptions := compose.CommandOptions{ - Env: newEnvBuilder(). - withEnv(stackVariantAsEnv(options.StackVersion)). - withEnvs(options.Profile.ComposeEnvVars()). - build(), - } - - servicesStatus, err := p.Status(statusOptions) - if err != nil { - return nil, errors.Wrap(err, "running command failed") - } - return servicesStatus, nil -} - func withDependentServices(services []string) []string { for _, aService := range services { if aService == "elastic-agent" { @@ -188,3 +184,56 @@ func withIsReadyServices(services []string) []string { } return allServices } + +func dockerComposeStatus() ([]ServiceStatus, error) { + var services []ServiceStatus + // query directly to docker to avoid load environment variables (e.g. STACK_VERSION_VARIANT) and profiles + containerIDs, err := docker.ContainerIDsWithLabel(fmt.Sprintf("%s=%s", projectLabelDockerCompose, DockerComposeProjectName)) + if err != nil { + return nil, err + } + + if len(containerIDs) == 0 { + return services, nil + } + + containerDescriptions, err := docker.InspectContainers(containerIDs...) + if err != nil { + return nil, err + } + + for _, containerDescription := range containerDescriptions { + service, err := newServiceStatus(&containerDescription) + if err != nil { + return nil, err + } + logger.Debugf("Adding Service: \"%v\"", service.Name) + services = append(services, *service) + } + + return services, nil +} + +func newServiceStatus(description *docker.ContainerDescription) (*ServiceStatus, error) { + service := ServiceStatus{ + Name: description.Config.Labels[serviceLabelDockerCompose], + Status: description.State.Status, + Version: getVersionFromDockerImage(description.Config.Image), + } + if description.State.Status == "running" { + service.Status = fmt.Sprintf("%v (%v)", service.Status, description.State.Health.Status) + } + if description.State.Status == "exited" { + service.Status = fmt.Sprintf("%v (%v)", service.Status, description.State.ExitCode) + } + + return &service, nil +} + +func getVersionFromDockerImage(dockerImage string) string { + fields := strings.Split(dockerImage, ":") + if len(fields) == 2 { + return fields[1] + } + return "latest" +} diff --git a/internal/stack/compose_test.go b/internal/stack/compose_test.go new file mode 100644 index 0000000000..5ad495b1b6 --- /dev/null +++ b/internal/stack/compose_test.go @@ -0,0 +1,169 @@ +// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +// or more contributor license agreements. Licensed under the Elastic License; +// you may not use this file except in compliance with the Elastic License. + +package stack + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/elastic/elastic-package/internal/docker" +) + +func TestGetVersionFromDockerImage(t *testing.T) { + cases := []struct { + dockerImage string + expected string + }{ + {"docker.test/test:1.42.0", "1.42.0"}, + {"docker.test/test", "latest"}, + } + + for _, c := range cases { + t.Run(c.dockerImage, func(t *testing.T) { + version := getVersionFromDockerImage(c.dockerImage) + assert.Equal(t, c.expected, version) + }) + } +} + +func TestNewServiceStatus(t *testing.T) { + cases := []struct { + name string + description docker.ContainerDescription + expected ServiceStatus + }{ + { + name: "commonService", + description: docker.ContainerDescription{ + Config: struct { + Image string + Labels map[string]string + }{ + Image: "docker.test:1.42.0", + Labels: map[string]string{"com.docker.compose.service": "myservice", "foo": "bar"}, + }, + ID: "123456789ab", + State: struct { + Status string + ExitCode int + Health *struct { + Status string + Log []struct { + Start time.Time + ExitCode int + Output string + } + } + }{ + Status: "running", + ExitCode: 0, + Health: &struct { + Status string + Log []struct { + Start time.Time + ExitCode int + Output string + } + }{ + Status: "healthy", + }, + }, + }, + expected: ServiceStatus{ + Name: "myservice", + Status: "running (healthy)", + Version: "1.42.0", + }, + }, + { + name: "exitedService", + description: docker.ContainerDescription{ + Config: struct { + Image string + Labels map[string]string + }{ + Image: "docker.test:1.42.0", + Labels: map[string]string{"com.docker.compose.service": "myservice", "foo": "bar"}, + }, + ID: "123456789ab", + State: struct { + Status string + ExitCode int + Health *struct { + Status string + Log []struct { + Start time.Time + ExitCode int + Output string + } + } + }{ + Status: "exited", + ExitCode: 128, + Health: nil, + }, + }, + expected: ServiceStatus{ + Name: "myservice", + Status: "exited (128)", + Version: "1.42.0", + }, + }, + { + name: "startingService", + description: docker.ContainerDescription{ + Config: struct { + Image string + Labels map[string]string + }{ + Image: "docker.test:1.42.0", + Labels: map[string]string{"com.docker.compose.service": "myservice", "foo": "bar"}, + }, + ID: "123456789ab", + State: struct { + Status string + ExitCode int + Health *struct { + Status string + Log []struct { + Start time.Time + ExitCode int + Output string + } + } + }{ + Status: "running", + ExitCode: 0, + Health: &struct { + Status string + Log []struct { + Start time.Time + ExitCode int + Output string + } + }{ + Status: "starting", + }, + }, + }, + expected: ServiceStatus{ + Name: "myservice", + Status: "running (starting)", + Version: "1.42.0", + }, + }, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + serviceStatus, err := newServiceStatus(&c.description) + require.NoError(t, err) + assert.Equal(t, &c.expected, serviceStatus) + }) + } +} diff --git a/internal/stack/status.go b/internal/stack/status.go index 3128bad96f..163d6a81bc 100644 --- a/internal/stack/status.go +++ b/internal/stack/status.go @@ -7,18 +7,17 @@ package stack import ( "strings" - "github.com/elastic/elastic-package/internal/compose" "github.com/elastic/elastic-package/internal/logger" ) // Status shows the status for each service -func Status(options Options) ([]compose.ServiceStatus, error) { - servicesStatus, err := dockerComposeStatus(options) +func Status() ([]ServiceStatus, error) { + servicesStatus, err := dockerComposeStatus() if err != nil { return nil, err } - var services []compose.ServiceStatus + var services []ServiceStatus for _, status := range servicesStatus { if strings.Contains(status.Name, readyServicesSuffix) { logger.Debugf("Filtering out service: %s", status.Name) From 47809d6ddf16a6b6c67fa7a7d927a9b44a9e4e23 Mon Sep 17 00:00:00 2001 From: Mario Rodriguez Date: Thu, 11 Aug 2022 20:41:41 +0200 Subject: [PATCH 26/28] Sort services status array --- internal/stack/status.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/internal/stack/status.go b/internal/stack/status.go index 163d6a81bc..ecde8a1e0a 100644 --- a/internal/stack/status.go +++ b/internal/stack/status.go @@ -5,6 +5,7 @@ package stack import ( + "sort" "strings" "github.com/elastic/elastic-package/internal/logger" @@ -26,5 +27,7 @@ func Status() ([]ServiceStatus, error) { services = append(services, status) } + sort.Slice(services, func(i, j int) bool { return services[i].Name < services[j].Name }) + return services, nil } From 46a380d609e6cfa3174fe4509eb6bb4ff612988d Mon Sep 17 00:00:00 2001 From: Mario Rodriguez Date: Thu, 11 Aug 2022 20:56:36 +0200 Subject: [PATCH 27/28] Removed --version from test stack script --- scripts/test-stack-command.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/test-stack-command.sh b/scripts/test-stack-command.sh index c7257724c9..132593ae33 100755 --- a/scripts/test-stack-command.sh +++ b/scripts/test-stack-command.sh @@ -38,7 +38,7 @@ OUTPUT_PATH_STATUS="build/elastic-stack-status/${VERSION}" mkdir -p ${OUTPUT_PATH_STATUS} # Initial status empty -elastic-package stack status -v ${ARG_VERSION} 2> ${OUTPUT_PATH_STATUS}/initial.txt +elastic-package stack status 2> ${OUTPUT_PATH_STATUS}/initial.txt grep "\- No service running" ${OUTPUT_PATH_STATUS}/initial.txt # Update the stack @@ -65,7 +65,7 @@ Status of Elastic stack services: ╰──────────────────┴─────────┴───────────────────╯ EOF -elastic-package stack status -v ${ARG_VERSION} 2> ${OUTPUT_PATH_STATUS}/running.txt +elastic-package stack status -v 2> ${OUTPUT_PATH_STATUS}/running.txt # Remove spaces to avoid issues with spaces between columns clean_status_output "${OUTPUT_PATH_STATUS}/expected_running.txt" > ${OUTPUT_PATH_STATUS}/expected_no_spaces.txt From 0f77b1d06a865561dda997e97d29981add44c3b7 Mon Sep 17 00:00:00 2001 From: Mario Rodriguez Date: Fri, 12 Aug 2022 13:33:42 +0200 Subject: [PATCH 28/28] Set key and value as parameters in ContainerIDsWithLabel --- internal/docker/docker.go | 3 ++- internal/stack/compose.go | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/internal/docker/docker.go b/internal/docker/docker.go index 0f0c337b2b..1bd6b8f0e5 100644 --- a/internal/docker/docker.go +++ b/internal/docker/docker.go @@ -91,7 +91,8 @@ func ContainerID(containerName string) (string, error) { } // ContainerIDsWithLabel function returns all the container IDs filtering per label. -func ContainerIDsWithLabel(label string) ([]string, error) { +func ContainerIDsWithLabel(key, value string) ([]string, error) { + label := fmt.Sprintf("%s=%s", key, value) cmd := exec.Command("docker", "ps", "-a", "--filter", "label="+label, "--format", "{{.ID}}") errOutput := new(bytes.Buffer) cmd.Stderr = errOutput diff --git a/internal/stack/compose.go b/internal/stack/compose.go index 8409014508..38b150df3f 100644 --- a/internal/stack/compose.go +++ b/internal/stack/compose.go @@ -188,7 +188,7 @@ func withIsReadyServices(services []string) []string { func dockerComposeStatus() ([]ServiceStatus, error) { var services []ServiceStatus // query directly to docker to avoid load environment variables (e.g. STACK_VERSION_VARIANT) and profiles - containerIDs, err := docker.ContainerIDsWithLabel(fmt.Sprintf("%s=%s", projectLabelDockerCompose, DockerComposeProjectName)) + containerIDs, err := docker.ContainerIDsWithLabel(projectLabelDockerCompose, DockerComposeProjectName) if err != nil { return nil, err }