mirror of
https://github.com/coder/coder.git
synced 2025-07-09 11:45:56 +00:00
This commit makes the following changes: - Partially reverts the changes of feat: update workspace deadline when workspace ttl updated #2165, making the deadline of a running workspace build independant of TTL, once started. - CLI: updating a workspace TTL no longer updates the deadline of the workspace. - UI: updating a workspace TTL no longer updates the deadline of the workspace. - Drive-by: API: When creating a workspace, default TTL to min(12 hours, template max_ttl) if not instructed otherwise. - Drive-by: CLI: list: measure workspace extension correctly (+X in last column) from the time the provisioner job was completed - Drive-by: WorkspaceSchedule: show timezone of schedule if it is set, defaulting to dayjs guess otherwise. - Drive-by: WorkspaceScheduleForm: fixed an issue where deleting the "TTL" value in the form would show the text "Your workspace will shut down a few seconds after start".
173 lines
4.7 KiB
Go
173 lines
4.7 KiB
Go
package cli
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/jedib0t/go-pretty/v6/table"
|
|
"github.com/spf13/cobra"
|
|
|
|
"github.com/coder/coder/cli/cliui"
|
|
"github.com/coder/coder/coderd/autobuild/schedule"
|
|
"github.com/coder/coder/coderd/util/ptr"
|
|
"github.com/coder/coder/codersdk"
|
|
)
|
|
|
|
func list() *cobra.Command {
|
|
var (
|
|
columns []string
|
|
)
|
|
cmd := &cobra.Command{
|
|
Annotations: workspaceCommand,
|
|
Use: "list",
|
|
Short: "List all workspaces",
|
|
Aliases: []string{"ls"},
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
client, err := createClient(cmd)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
workspaces, err := client.Workspaces(cmd.Context(), codersdk.WorkspaceFilter{})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if len(workspaces) == 0 {
|
|
_, _ = fmt.Fprintln(cmd.OutOrStdout(), cliui.Styles.Prompt.String()+"No workspaces found! Create one:")
|
|
_, _ = fmt.Fprintln(cmd.OutOrStdout())
|
|
_, _ = fmt.Fprintln(cmd.OutOrStdout(), " "+cliui.Styles.Code.Render("coder create <name>"))
|
|
_, _ = fmt.Fprintln(cmd.OutOrStdout())
|
|
return nil
|
|
}
|
|
users, err := client.Users(cmd.Context(), codersdk.UsersRequest{})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
usersByID := map[uuid.UUID]codersdk.User{}
|
|
for _, user := range users {
|
|
usersByID[user.ID] = user
|
|
}
|
|
|
|
tableWriter := cliui.Table()
|
|
header := table.Row{"workspace", "template", "status", "last built", "outdated", "autostart", "ttl"}
|
|
tableWriter.AppendHeader(header)
|
|
tableWriter.SortBy([]table.SortBy{{
|
|
Name: "workspace",
|
|
}})
|
|
tableWriter.SetColumnConfigs(cliui.FilterTableColumns(header, columns))
|
|
|
|
for _, workspace := range workspaces {
|
|
status := ""
|
|
inProgress := false
|
|
if workspace.LatestBuild.Job.Status == codersdk.ProvisionerJobRunning ||
|
|
workspace.LatestBuild.Job.Status == codersdk.ProvisionerJobCanceling {
|
|
inProgress = true
|
|
}
|
|
|
|
switch workspace.LatestBuild.Transition {
|
|
case codersdk.WorkspaceTransitionStart:
|
|
status = "Running"
|
|
if inProgress {
|
|
status = "Starting"
|
|
}
|
|
case codersdk.WorkspaceTransitionStop:
|
|
status = "Stopped"
|
|
if inProgress {
|
|
status = "Stopping"
|
|
}
|
|
case codersdk.WorkspaceTransitionDelete:
|
|
status = "Deleted"
|
|
if inProgress {
|
|
status = "Deleting"
|
|
}
|
|
}
|
|
if workspace.LatestBuild.Job.Status == codersdk.ProvisionerJobFailed {
|
|
status = "Failed"
|
|
}
|
|
|
|
duration := time.Now().UTC().Sub(workspace.LatestBuild.Job.CreatedAt).Truncate(time.Second)
|
|
autostartDisplay := "-"
|
|
if !ptr.NilOrEmpty(workspace.AutostartSchedule) {
|
|
if sched, err := schedule.Weekly(*workspace.AutostartSchedule); err == nil {
|
|
autostartDisplay = sched.Cron()
|
|
}
|
|
}
|
|
|
|
autostopDisplay := "-"
|
|
if !ptr.NilOrZero(workspace.TTLMillis) {
|
|
dur := time.Duration(*workspace.TTLMillis) * time.Millisecond
|
|
autostopDisplay = durationDisplay(dur)
|
|
if has, ext := hasExtension(workspace); has {
|
|
autostopDisplay += fmt.Sprintf(" (+%s)", durationDisplay(ext.Round(time.Minute)))
|
|
}
|
|
}
|
|
|
|
user := usersByID[workspace.OwnerID]
|
|
tableWriter.AppendRow(table.Row{
|
|
user.Username + "/" + workspace.Name,
|
|
workspace.TemplateName,
|
|
status,
|
|
durationDisplay(duration),
|
|
workspace.Outdated,
|
|
autostartDisplay,
|
|
autostopDisplay,
|
|
})
|
|
}
|
|
_, err = fmt.Fprintln(cmd.OutOrStdout(), tableWriter.Render())
|
|
return err
|
|
},
|
|
}
|
|
cmd.Flags().StringArrayVarP(&columns, "column", "c", nil,
|
|
"Specify a column to filter in the table.")
|
|
return cmd
|
|
}
|
|
|
|
func hasExtension(ws codersdk.Workspace) (bool, time.Duration) {
|
|
if ws.LatestBuild.Transition != codersdk.WorkspaceTransitionStart {
|
|
return false, 0
|
|
}
|
|
if ws.LatestBuild.Job.CompletedAt == nil {
|
|
return false, 0
|
|
}
|
|
if ws.LatestBuild.Deadline.IsZero() {
|
|
return false, 0
|
|
}
|
|
if ws.TTLMillis == nil {
|
|
return false, 0
|
|
}
|
|
ttl := time.Duration(*ws.TTLMillis) * time.Millisecond
|
|
delta := ws.LatestBuild.Deadline.Add(-ttl).Sub(*ws.LatestBuild.Job.CompletedAt)
|
|
if delta < time.Minute {
|
|
return false, 0
|
|
}
|
|
|
|
return true, delta
|
|
}
|
|
|
|
func durationDisplay(d time.Duration) string {
|
|
duration := d
|
|
if duration > time.Hour {
|
|
duration = duration.Truncate(time.Hour)
|
|
}
|
|
if duration > time.Minute {
|
|
duration = duration.Truncate(time.Minute)
|
|
}
|
|
days := 0
|
|
for duration.Hours() > 24 {
|
|
days++
|
|
duration -= 24 * time.Hour
|
|
}
|
|
durationDisplay := duration.String()
|
|
if days > 0 {
|
|
durationDisplay = fmt.Sprintf("%dd%s", days, durationDisplay)
|
|
}
|
|
if strings.HasSuffix(durationDisplay, "m0s") {
|
|
durationDisplay = durationDisplay[:len(durationDisplay)-2]
|
|
}
|
|
if strings.HasSuffix(durationDisplay, "h0m") {
|
|
durationDisplay = durationDisplay[:len(durationDisplay)-2]
|
|
}
|
|
return durationDisplay
|
|
}
|