diff --git a/api/client.go b/api/client.go index 8f1a3f1..52f6ed9 100644 --- a/api/client.go +++ b/api/client.go @@ -800,8 +800,19 @@ type Service struct { CreatedAt time.Time `json:"createdAt"` DeployedAt time.Time `json:"deployedAt"` Domains []string `json:"domains"` + // Kind is the workload type: "function" or "batch". Empty when talking to a + // server that predates the field, which is treated as "function". + Kind string `json:"kind"` } +// IsBatch reports whether this is a batch service, for which the deployment and +// domain columns are meaningless. +func (s Service) IsBatch() bool { return s.Kind == "batch" } + +// Deployed reports whether the service has ever been deployed. A batch service +// never is, and a function service may not have been yet. +func (s Service) Deployed() bool { return !s.DeployedAt.IsZero() } + type Build struct { ID string `json:"id"` Constructor string `json:"constructor"` diff --git a/cmd/services.go b/cmd/services.go index d963bf5..4913e8b 100644 --- a/cmd/services.go +++ b/cmd/services.go @@ -78,13 +78,14 @@ var serviceListCmd = &cobra.Command{ return services[i].DeployedAt.Before(services[j].DeployedAt) }) tw := tabwriter.NewWriter(os.Stdout, 0, 0, 1, ' ', 0) - fmt.Fprintln(tw, "NAME\tVERSION\tCREATED\tLAST DEPLOYED\tDOMAINS") + fmt.Fprintln(tw, "NAME\tKIND\tVERSION\tCREATED\tLAST DEPLOYED\tDOMAINS") for _, svc := range services { fmt.Fprintln(tw, strings.Join([]string{ svc.Name, - strconv.FormatInt(svc.Deployment, 10), + serviceKind(svc.Kind), + deployedVersion(svc), humanize.Time(svc.CreatedAt), - humanize.Time(svc.DeployedAt), + lastDeployed(svc), strings.Join(svc.Domains, " "), }, "\t")) } @@ -93,6 +94,44 @@ var serviceListCmd = &cobra.Command{ }), } +// serviceKind renders a service's workload type. +// +// Deliberately uncoloured. tabwriter measures cell width in bytes, so an ANSI +// sequence makes a cell look ~9 characters wider than it prints and the whole +// table skews -- which is already visible wherever colorize() is used inside a +// tabwriter elsewhere in this CLI. A column of its own distinguishes the kinds +// well enough without that cost. +func serviceKind(kind string) string { + if kind == "" { + // A server predating the kind field. Reporting "function" would be a + // guess presented as fact. + return "-" + } + return kind +} + +// deployedVersion renders the deployment version, or a dash when there is no +// deployment to have a version. +func deployedVersion(svc api.Service) string { + if !svc.Deployed() { + return "-" + } + return strconv.FormatInt(svc.Deployment, 10) +} + +// lastDeployed renders the deployment time, or a dash when the service has +// never been deployed. +// +// Humanising the zero timestamp yields "a long while ago", which reads as +// deployed in the distant past. For a batch service that is not merely +// imprecise: it is never deployed at all, by design. +func lastDeployed(svc api.Service) string { + if !svc.Deployed() { + return "-" + } + return humanize.Time(svc.DeployedAt) +} + var ( serviceLogsFollow bool serviceLogsTail bool diff --git a/cmd/services_test.go b/cmd/services_test.go new file mode 100644 index 0000000..2743a49 --- /dev/null +++ b/cmd/services_test.go @@ -0,0 +1,56 @@ +package cmd + +import ( + "testing" + "time" + + "github.com/valar/cli/api" +) + +func TestServiceKind(t *testing.T) { + for _, tc := range []struct{ In, Want string }{ + {"function", "function"}, + {"batch", "batch"}, + // A server predating the field must not be reported as "function": + // that would present a guess as fact. + {"", "-"}, + {"something-new", "something-new"}, + } { + if got := serviceKind(tc.In); got != tc.Want { + t.Errorf("serviceKind(%q) = %q, want %q", tc.In, got, tc.Want) + } + } +} + +// Humanising the zero timestamp yields "a long while ago", which reads as +// deployed in the distant past. A batch service is never deployed at all. +func TestDeployedColumns_NeverDeployed(t *testing.T) { + svc := api.Service{Name: "batchdemo", Kind: "batch"} + if got := lastDeployed(svc); got != "-" { + t.Errorf("lastDeployed = %q, want -", got) + } + if got := deployedVersion(svc); got != "-" { + t.Errorf("deployedVersion = %q, want -", got) + } +} + +func TestDeployedColumns_Deployed(t *testing.T) { + svc := api.Service{Name: "web", Kind: "function", Deployment: 7, DeployedAt: time.Now().Add(-time.Hour)} + if got := deployedVersion(svc); got != "7" { + t.Errorf("deployedVersion = %q, want 7", got) + } + if got := lastDeployed(svc); got == "-" { + t.Error("lastDeployed should render a time for a deployed service") + } +} + +func TestServiceIsBatch(t *testing.T) { + if !(api.Service{Kind: "batch"}).IsBatch() { + t.Error("batch service should report IsBatch") + } + for _, kind := range []string{"function", ""} { + if (api.Service{Kind: kind}).IsBatch() { + t.Errorf("kind %q should not report IsBatch", kind) + } + } +}