diff --git a/cmd/stack.go b/cmd/stack.go index b24164ad84..db67328579 100644 --- a/cmd/stack.go +++ b/cmd/stack.go @@ -7,6 +7,7 @@ package cmd import ( "fmt" "strings" + "time" "github.com/olekukonko/tablewriter" "github.com/olekukonko/tablewriter/renderer" @@ -367,9 +368,31 @@ func printStatus(cmd *cobra.Command, servicesStatus []stack.ServiceStatus) { tablewriter.WithRenderer(renderer.NewColorized(config)), tablewriter.WithConfig(defaultTableConfig), ) - table.Header("Service", "Version", "Status") + table.Header("Service", "Version", "Status", "Image Build Date", "VCS Ref") for _, service := range servicesStatus { - table.Append(service.Name, service.Version, service.Status) + var buildDate, vcsRef string + if service.Labels != nil { + buildDate = formatTime(service.Labels.BuildDate) + vcsRef = truncate(service.Labels.VCSRef, 10) + } + table.Append(service.Name, service.Version, service.Status, buildDate, vcsRef) } table.Render() } + +// formatTime returns the given RFC3339 time formated as 2006-01-02T15:04Z. +// If the value is not in RFC3339 format, then it is returned as-is. +func formatTime(maybeRFC3339Time string) string { + if t, err := time.Parse(time.RFC3339, maybeRFC3339Time); err == nil { + return t.UTC().Format("2006-01-02T15:04Z") + } + return maybeRFC3339Time +} + +// truncate truncates text if it is longer than maxLength. +func truncate(text string, maxLength int) string { + if len(text) > maxLength { + return text[:maxLength] + } + return text +} diff --git a/internal/docker/docker.go b/internal/docker/docker.go index 210a4c3272..f6e2c66e69 100644 --- a/internal/docker/docker.go +++ b/internal/docker/docker.go @@ -49,6 +49,10 @@ type ConfigLabels struct { ComposeProject string `json:"com.docker.compose.project"` ComposeService string `json:"com.docker.compose.service"` ComposeVersion string `json:"com.docker.compose.version"` + + // http://label-schema.org/rc1/ Labels + BuildDate string `json:"org.label-schema.build-date,omitempty"` // This label contains the Date/Time the image was built. The value SHOULD be formatted according to RFC 3339. + VCSRef string `json:"org.label-schema.vcs-ref,omitempty"` // Identifier for the version of the source code from which this image was built. For example if the version control system is git this is the SHA. } // String function dumps string representation of the container description. diff --git a/internal/stack/compose.go b/internal/stack/compose.go index 483e638e80..a7270183b1 100644 --- a/internal/stack/compose.go +++ b/internal/stack/compose.go @@ -18,6 +18,7 @@ type ServiceStatus struct { Name string Status string Version string + Labels *docker.ConfigLabels // Container labels. } const readyServicesSuffix = "is_ready" @@ -214,6 +215,7 @@ func newServiceStatus(description *docker.ContainerDescription) (*ServiceStatus, Name: description.Config.Labels.ComposeService, Status: description.State.Status, Version: getVersionFromDockerImage(description.Config.Image), + Labels: &description.Config.Labels, } if description.State.Status == "running" { healthStatus := "unknown health" diff --git a/internal/stack/compose_test.go b/internal/stack/compose_test.go index 96be46942c..496ce48ecd 100644 --- a/internal/stack/compose_test.go +++ b/internal/stack/compose_test.go @@ -78,6 +78,7 @@ func TestNewServiceStatus(t *testing.T) { Name: "myservice", Status: "running (healthy)", Version: "1.42.0", + Labels: &docker.ConfigLabels{ComposeService: "myservice"}, }, }, { @@ -112,6 +113,7 @@ func TestNewServiceStatus(t *testing.T) { Name: "myservice", Status: "exited (128)", Version: "1.42.0", + Labels: &docker.ConfigLabels{ComposeService: "myservice"}, }, }, { @@ -155,6 +157,7 @@ func TestNewServiceStatus(t *testing.T) { Name: "myservice", Status: "running (starting)", Version: "1.42.0", + Labels: &docker.ConfigLabels{ComposeService: "myservice"}, }, }, } diff --git a/scripts/test-stack-command.sh b/scripts/test-stack-command.sh index 7dec417547..16aa64b193 100755 --- a/scripts/test-stack-command.sh +++ b/scripts/test-stack-command.sh @@ -49,7 +49,12 @@ default_version() { clean_status_output() { local output_file="$1" - cat "${output_file}" | grep "│" | tr -d ' ' + # This removes the 'IMAGE BUILD DATE" and 'VCS REF' columns and + # removes the whitespace between columns. + grep "│" "${output_file}" \ + | sed 's/│/|/g' \ + | cut -d '|' -f 1-4,7- \ + | tr -d ' ' } trap cleanup EXIT @@ -131,20 +136,20 @@ curl --cacert "${ELASTIC_PACKAGE_CA_CERT}" -f "${ELASTIC_PACKAGE_KIBANA_HOST}/lo # Check status with running services cat < "${OUTPUT_PATH_STATUS}/expected_running.txt" Status of Elastic stack services: -╭──────────────────┬─────────┬───────────────────╮ -│ SERVICE │ VERSION │ STATUS │ -├──────────────────┼─────────┼───────────────────┤ -│ elastic-agent │ ${EXPECTED_AGENT_VERSION} │ running (healthy) │ -│ elasticsearch │ ${EXPECTED_VERSION} │ running (healthy) │ -│ fleet-server │ ${EXPECTED_AGENT_VERSION} │ running (healthy) │ -│ kibana │ ${EXPECTED_VERSION} │ running (healthy) │ -│ package-registry │ latest │ running (healthy) │ -╰──────────────────┴─────────┴───────────────────╯ +╭──────────────────┬─────────────────────┬───────────────────┬───────────────────┬────────────╮ +│ SERVICE │ VERSION │ STATUS │ IMAGE BUILD DATE │ VCS REF │ +├──────────────────┼─────────────────────┼───────────────────┼───────────────────┼────────────┤ +│ elastic-agent │ ${EXPECTED_AGENT_VERSION} │ running (healthy) │ 2024-08-22T02:44Z │ b96a4ca8fa │ +│ elasticsearch │ ${EXPECTED_VERSION} │ running (healthy) │ 2024-08-22T13:26Z │ 1362d56865 │ +│ fleet-server │ ${EXPECTED_AGENT_VERSION} │ running (healthy) │ 2024-08-22T02:44Z │ b96a4ca8fa │ +│ kibana │ ${EXPECTED_VERSION} │ running (healthy) │ 2024-08-22T11:09Z │ cdcdfddd3f │ +│ package-registry │ latest │ running (healthy) │ │ │ +╰──────────────────┴─────────────────────┴───────────────────┴───────────────────┴────────────╯ EOF NO_COLOR=true elastic-package stack status -v 2> "${OUTPUT_PATH_STATUS}/running.txt" -# Remove spaces to avoid issues with spaces between columns +# Remove dates, commit IDs, and spaces to avoid issues. 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"