diff --git a/cmd/stack.go b/cmd/stack.go index 63899ba2e..91a6502a1 100644 --- a/cmd/stack.go +++ b/cmd/stack.go @@ -7,6 +7,7 @@ package cmd import ( "fmt" "strings" + "time" "github.com/jedib0t/go-pretty/table" @@ -350,11 +351,28 @@ func printStatus(cmd *cobra.Command, servicesStatus []stack.ServiceStatus) { return } t := table.NewWriter() - t.AppendHeader(table.Row{"Service", "Version", "Status"}) + t.AppendHeader(table.Row{"Service", "Version", "Status", "Image Build Date", "VCS Ref"}) for _, service := range servicesStatus { - t.AppendRow(table.Row{service.Name, service.Version, service.Status}) + t.AppendRow(table.Row{service.Name, service.Version, service.Status, formatTime(service.Labels.BuildDate), truncate(service.Labels.VCSRef, 10)}) } t.SetStyle(table.StyleRounded) cmd.Println(t.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 fedfe3a15..4f4f9d553 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 483e638e8..a7270183b 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 96be46942..496ce48ec 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 b057ae16b..5e8b63283 100755 --- a/scripts/test-stack-command.sh +++ b/scripts/test-stack-command.sh @@ -32,7 +32,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 @@ -90,20 +95,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_VERSION} │ running (healthy) │ -│ elasticsearch │ ${EXPECTED_VERSION} │ running (healthy) │ -│ fleet-server │ ${EXPECTED_VERSION} │ running (healthy) │ -│ kibana │ ${EXPECTED_VERSION} │ running (healthy) │ -│ package-registry │ latest │ running (healthy) │ -╰──────────────────┴─────────┴───────────────────╯ +╭──────────────────┬─────────────────────┬───────────────────┬───────────────────┬────────────╮ +│ SERVICE │ VERSION │ STATUS │ IMAGE BUILD DATE │ VCS REF │ +├──────────────────┼─────────────────────┼───────────────────┼───────────────────┼────────────┤ +│ elastic-agent │ ${EXPECTED_VERSION} │ running (healthy) │ 2024-08-22T02:44Z │ b96a4ca8fa │ +│ elasticsearch │ ${EXPECTED_VERSION} │ running (healthy) │ 2024-08-22T13:26Z │ 1362d56865 │ +│ fleet-server │ ${EXPECTED_VERSION} │ running (healthy) │ 2024-08-22T02:44Z │ b96a4ca8fa │ +│ kibana │ ${EXPECTED_VERSION} │ running (healthy) │ 2024-08-22T11:09Z │ cdcdfddd3f │ +│ package-registry │ latest │ running (healthy) │ │ │ +╰──────────────────┴─────────────────────┴───────────────────┴───────────────────┴────────────╯ EOF 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"