diff --git a/tool/tsh/common/tsh.go b/tool/tsh/common/tsh.go index 2b003deca2fb2..97de0161f56ea 100644 --- a/tool/tsh/common/tsh.go +++ b/tool/tsh/common/tsh.go @@ -4855,19 +4855,26 @@ func onShow(cf *CLIConf) error { return nil } +func humanFriendlyValidUntilDuration(validUntil time.Time, clock clockwork.Clock) string { + duration := validUntil.Sub(clock.Now()) + switch { + case duration <= 0: + return "EXPIRED" + case duration < time.Minute: + return "valid for <1m" + default: + return fmt.Sprintf("valid for %s", + // Since duration is truncated to minute, duration.String() always + // ends with 0s. + strings.TrimRight(duration.Truncate(time.Minute).String(), "0s"), + ) + } +} + // printStatus prints the status of the profile. func printStatus(debug bool, p *profileInfo, env map[string]string, isActive bool) { + clock := clockwork.NewRealClock() var prefix string - humanDuration := "EXPIRED" - duration := time.Until(p.ValidUntil) - if duration.Nanoseconds() > 0 { - humanDuration = fmt.Sprintf("valid for %v", duration.Round(time.Minute)) - // If certificate is valid for less than a minute, display "<1m" instead of "0s". - if duration < time.Minute { - humanDuration = "valid for <1m" - } - } - proxyURL := p.getProxyURLLine(isActive, env) cluster := p.getClusterLine(isActive, env) kubeCluster := p.getKubeClusterLine(isActive, env) @@ -4926,7 +4933,7 @@ func printStatus(debug bool, p *profileInfo, env map[string]string, isActive boo fmt.Printf(" Allowed Resources: %s\n", allowedResourcesStr) } } - fmt.Printf(" Valid until: %v [%v]\n", p.ValidUntil, humanDuration) + fmt.Printf(" Valid until: %v [%v]\n", p.ValidUntil, humanFriendlyValidUntilDuration(p.ValidUntil, clock)) fmt.Printf(" Extensions: %v\n", strings.Join(p.Extensions, ", ")) if debug { diff --git a/tool/tsh/common/tsh_test.go b/tool/tsh/common/tsh_test.go index 7890918a2b7c2..3d3f8de4b0ed5 100644 --- a/tool/tsh/common/tsh_test.go +++ b/tool/tsh/common/tsh_test.go @@ -50,6 +50,7 @@ import ( "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" "github.com/gravitational/trace" + "github.com/jonboulle/clockwork" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" otlp "go.opentelemetry.io/proto/otlp/trace/v1" @@ -7209,3 +7210,40 @@ func TestSetEnvVariables(t *testing.T) { }) } } + +func Test_humanFriendlyValidUntilDuration(t *testing.T) { + clock := clockwork.NewFakeClock() + tests := []struct { + name string + input time.Time + expect string + }{ + { + name: "expired", + input: clock.Now().Add(-time.Minute), + expect: "EXPIRED", + }, + { + name: "less than one minute", + input: clock.Now().Add(time.Second * 30), + expect: "valid for <1m", + }, + { + name: "truncate", + input: clock.Now().Add(time.Minute*5 + time.Second*50), + expect: "valid for 5m", + }, + { + name: "hours", + input: clock.Now().Add(time.Hour*12 + time.Minute*34 + time.Second*10), + expect: "valid for 12h34m", + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + actual := humanFriendlyValidUntilDuration(test.input, clock) + require.Equal(t, test.expect, actual) + }) + } +}