Ctrl K
ComunidadGuías…

cli/schedule.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
package cli

import (
	"context"
	"fmt"
	"io"
	"strings"
	"time"

	"golang.org/x/xerrors"

	"github.com/coder/coder/v2/cli/cliui"
	"github.com/coder/coder/v2/coderd/schedule/cron"
	"github.com/coder/coder/v2/coderd/util/ptr"
	"github.com/coder/coder/v2/coderd/util/tz"
	"github.com/coder/coder/v2/codersdk"
	"github.com/coder/serpent"
)

const (
	scheduleShowDescriptionLong = `Shows the following information for the given workspace(s):
  * The automatic start schedule
  * The next scheduled start time
  * The duration after which it will stop
  * The next scheduled stop time
`
	scheduleStartDescriptionLong = `Schedules a workspace to regularly start at a specific time.
Schedule format: <start-time> [day-of-week] [location].
  * Start-time (required) is accepted either in 12-hour (hh:mm{am|pm}) format, or 24-hour format hh:mm.
  * Day-of-week (optional) allows specifying in the cron format, e.g. 1,3,5 or Mon-Fri.
    Aliases such as @daily are not supported.
    Default: * (every day)
  * Location (optional) must be a valid location in the IANA timezone database.
    If omitted, we will fall back to either the TZ environment variable or /etc/localtime.
    You can check your corresponding location by visiting https://ipinfo.io - it shows in the demo widget on the right.
`
	scheduleStopDescriptionLong = `Schedules a workspace to stop after a given duration has elapsed.
  * Workspace runtime is measured from the time that the workspace build completed.
  * The minimum scheduled stop time is 1 minute.
  * The workspace template may place restrictions on the maximum shutdown time.
  * Changes to workspace schedules only take effect upon the next build of the workspace,
    and do not affect a running instance of a workspace.

When enabling scheduled stop, enter a duration in one of the following formats:
  * 3h2m (3 hours and two minutes)
  * 3h   (3 hours)
  * 2m   (2 minutes)
  * 2    (2 minutes)
`
	scheduleExtendDescriptionLong = `Extends the workspace deadline.
  * The new stop time is calculated from *now*.
  * The new stop time must be at least 30 minutes in the future.
  * The workspace template may restrict the maximum workspace runtime.
`
)

func (r *RootCmd) schedules() *serpent.Command {
	scheduleCmd := &serpent.Command{
		Annotations: workspaceCommand,
		Use:         "schedule { show | start | stop | extend } <workspace>",
		Short:       "Schedule automated start and stop times for workspaces",
		Handler: func(inv *serpent.Invocation) error {
			return inv.Command.HelpHandler(inv)
		},
		Children: []*serpent.Command{
			r.scheduleShow(),
			r.scheduleStart(),
			r.scheduleStop(),
			r.scheduleExtend(),
		},
	}

	return scheduleCmd
}

// newScheduleFormatter returns the output formatter shared by "schedule
// show", "schedule start", "schedule stop", and "schedule extend" so their
// table and JSON output (and -c/--column selection) are interchangeable.
func newScheduleFormatter() *cliui.OutputFormatter {
	return cliui.NewOutputFormatter(
		cliui.TableFormat(
			[]scheduleListRow{},
			[]string{
				"workspace",
				"starts at",
				"starts next",
				"stops after",
				"stops next",
			},
		),
		cliui.JSONFormat(),
	)
}

// scheduleShow() is just a wrapper for list() with some different defaults.
func (r *RootCmd) scheduleShow() *serpent.Command {
	var (
		filter    cliui.WorkspaceFilter
		formatter = newScheduleFormatter()
	)
	showCmd := &serpent.Command{
		Use:   "show <workspace | --search <query> | --all>",
		Short: "Show workspace schedules",
		Long:  scheduleShowDescriptionLong,
		Middleware: serpent.Chain(
			serpent.RequireRangeArgs(0, 1),
		),
		Handler: func(inv *serpent.Invocation) error {
			client, err := r.InitClient(inv)
			if err != nil {
				return err
			}
			// To preserve existing behavior, if an argument is passed we will
			// only show the schedule for that workspace, resolved exactly by
			// [owner/]workspace rather than a name filter query, which matches
			// substrings and, without an owner, only the current user's workspaces.
			var res []scheduleListRow
			if len(inv.Args) == 1 {
				workspace, err := client.ResolveWorkspace(inv.Context(), inv.Args[0])
				if err != nil {
					return xerrors.Errorf("get workspace: %w", err)
				}
				res = []scheduleListRow{scheduleListRowFromWorkspace(time.Now(), workspace)}
			} else {
				res, err = QueryConvertWorkspaces(inv.Context(), client, filter.Filter(), scheduleListRowFromWorkspace)
				if err != nil {
					return err
				}
			}

			out, err := formatter.Format(inv.Context(), res)
			if err != nil {
				return err
			}

			if out == "" {
				cliui.Infof(inv.Stderr, "No schedules found.")
				return nil
			}

			_, err = fmt.Fprintln(inv.Stdout, out)
			return err
		},
	}
	filter.AttachOptions(&showCmd.Options)
	formatter.AttachOptions(&showCmd.Options)
	return showCmd
}

func (r *RootCmd) scheduleStart() *serpent.Command {
	formatter := newScheduleFormatter()
	cmd := &serpent.Command{
		Use: "start <workspace-name> { <start-time> [day-of-week] [location] | manual }",
		Long: scheduleStartDescriptionLong + "\n" + FormatExamples(
			Example{
				Description: "Set the workspace to start at 9:30am (in Dublin) from Monday to Friday",
				Command:     "coder schedule start my-workspace 9:30AM Mon-Fri Europe/Dublin",
			},
		),
		Short: "Edit workspace start schedule",
		Middleware: serpent.Chain(
			serpent.RequireRangeArgs(2, 4),
		),
		Handler: func(inv *serpent.Invocation) error {
			client, err := r.InitClient(inv)
			if err != nil {
				return err
			}
			workspace, err := client.ResolveWorkspace(inv.Context(), inv.Args[0])
			if err != nil {
				return err
			}

			// Autostart configuration is not supported for prebuilt workspaces.
			// Prebuild lifecycle is managed by the reconciliation loop, with scheduling behavior
			// defined per preset at the template level, not per workspace.
			if workspace.IsPrebuild {
				return xerrors.Errorf("autostart configuration is not supported for prebuilt workspaces")
			}

			var schedStr *string
			if inv.Args[1] != "manual" {
				sched, err := parseCLISchedule(inv.Args[1:]...)
				if err != nil {
					return err
				}

				schedStr = ptr.Ref(sched.String())

				// Check if the template has autostart requirements that may conflict
				// with the user's schedule.
				template, err := client.Template(inv.Context(), workspace.TemplateID)
				if err != nil {
					return xerrors.Errorf("get template: %w", err)
				}

				if len(template.AutostartRequirement.DaysOfWeek) > 0 {
					_, _ = fmt.Fprintf(
						inv.Stderr,
						"Warning: your workspace template restricts autostart to the following days: %s.\n"+
							"Your workspace may only autostart on these days.\n",
						strings.Join(template.AutostartRequirement.DaysOfWeek, ", "),
					)
				}
			}

			err = client.UpdateWorkspaceAutostart(inv.Context(), workspace.ID, codersdk.UpdateWorkspaceAutostartRequest{
				Schedule: schedStr,
			})
			if err != nil {
				return err
			}

			updated, err := client.ResolveWorkspace(inv.Context(), inv.Args[0])
			if err != nil {
				return err
			}
			return displaySchedule(inv.Context(), formatter, updated, inv.Stdout)
		},
	}
	formatter.AttachOptions(&cmd.Options)
	return cmd
}

func (r *RootCmd) scheduleStop() *serpent.Command {
	formatter := newScheduleFormatter()
	cmd := &serpent.Command{
		Use: "stop <workspace-name> { <duration> | manual }",
		Long: scheduleStopDescriptionLong + "\n" + FormatExamples(
			Example{
				Command: "coder schedule stop my-workspace 2h30m",
			},
		),
		Short: "Edit workspace stop schedule",
		Middleware: serpent.Chain(
			serpent.RequireNArgs(2),
		),
		Handler: func(inv *serpent.Invocation) error {
			client, err := r.InitClient(inv)
			if err != nil {
				return err
			}
			workspace, err := client.ResolveWorkspace(inv.Context(), inv.Args[0])
			if err != nil {
				return err
			}

			// Autostop configuration is not supported for prebuilt workspaces.
			// Prebuild lifecycle is managed by the reconciliation loop, with scheduling behavior
			// defined per preset at the template level, not per workspace.
			if workspace.IsPrebuild {
				return xerrors.Errorf("autostop configuration is not supported for prebuilt workspaces")
			}

			var durMillis *int64
			if inv.Args[1] != "manual" {
				dur, err := parseDuration(inv.Args[1])
				if err != nil {
					return err
				}
				durMillis = ptr.Ref(dur.Milliseconds())
			}

			if err := client.UpdateWorkspaceTTL(inv.Context(), workspace.ID, codersdk.UpdateWorkspaceTTLRequest{
				TTLMillis: durMillis,
			}); err != nil {
				return err
			}

			updated, err := client.ResolveWorkspace(inv.Context(), inv.Args[0])
			if err != nil {
				return err
			}
			return displaySchedule(inv.Context(), formatter, updated, inv.Stdout)
		},
	}
	formatter.AttachOptions(&cmd.Options)
	return cmd
}

func (r *RootCmd) scheduleExtend() *serpent.Command {
	formatter := newScheduleFormatter()
	extendCmd := &serpent.Command{
		Use:     "extend <workspace-name> <duration from now>",
		Aliases: []string{"override-stop"},
		Short:   "Extend the stop time of a currently running workspace instance.",
		Long: scheduleExtendDescriptionLong + "\n" + FormatExamples(
			Example{
				Command: "coder schedule extend my-workspace 90m",
			},
		),
		Middleware: serpent.Chain(
			serpent.RequireNArgs(2),
		),
		Handler: func(inv *serpent.Invocation) error {
			client, err := r.InitClient(inv)
			if err != nil {
				return err
			}
			extendDuration, err := parseDuration(inv.Args[1])
			if err != nil {
				return err
			}

			workspace, err := client.ResolveWorkspace(inv.Context(), inv.Args[0])
			if err != nil {
				return xerrors.Errorf("get workspace: %w", err)
			}

			// Deadline extensions are not supported for prebuilt workspaces.
			// Prebuild lifecycle is managed by the reconciliation loop, with TTL behavior
			// defined per preset at the template level, not per workspace.
			if workspace.IsPrebuild {
				return xerrors.Errorf("extend configuration is not supported for prebuilt workspaces")
			}

			loc, err := tz.TimezoneIANA()
			if err != nil {
				loc = time.UTC // best effort
			}

			if extendDuration < 29*time.Minute {
				_, _ = fmt.Fprintf(
					inv.Stdout,
					"Please specify a duration of at least 30 minutes.\n",
				)
				return nil
			}

			newDeadline := time.Now().In(loc).Add(extendDuration)
			if err := client.PutExtendWorkspace(inv.Context(), workspace.ID, codersdk.PutExtendWorkspaceRequest{
				Deadline: newDeadline,
			}); err != nil {
				return err
			}

			updated, err := client.ResolveWorkspace(inv.Context(), inv.Args[0])
			if err != nil {
				return err
			}
			return displaySchedule(inv.Context(), formatter, updated, inv.Stdout)
		},
	}
	formatter.AttachOptions(&extendCmd.Options)
	return extendCmd
}

func displaySchedule(ctx context.Context, formatter *cliui.OutputFormatter, ws codersdk.Workspace, out io.Writer) error {
	rows := []scheduleListRow{scheduleListRowFromWorkspace(time.Now(), ws)}
	rendered, err := formatter.Format(ctx, rows)
	if err != nil {
		return err
	}
	_, err = fmt.Fprintln(out, rendered)
	return err
}

// scheduleListRow is a row in the schedule list.
// this is required for proper JSON output.
type scheduleListRow struct {
	WorkspaceName string `json:"workspace" table:"workspace,default_sort"`
	StartsAt      string `json:"starts_at" table:"starts at"`
	StartsNext    string `json:"starts_next" table:"starts next"`
	StopsAfter    string `json:"stops_after" table:"stops after"`
	StopsNext     string `json:"stops_next" table:"stops next"`
}

func scheduleListRowFromWorkspace(now time.Time, workspace codersdk.Workspace) scheduleListRow {
	autostartDisplay := ""
	nextStartDisplay := ""
	if !ptr.NilOrEmpty(workspace.AutostartSchedule) {
		if sched, err := cron.Weekly(*workspace.AutostartSchedule); err == nil {
			autostartDisplay = sched.Humanize()
			nextStartDisplay = timeDisplay(sched.Next(now))
		}
	}

	autostopDisplay := ""
	nextStopDisplay := ""
	if !ptr.NilOrZero(workspace.TTLMillis) {
		dur := time.Duration(*workspace.TTLMillis) * time.Millisecond
		autostopDisplay = durationDisplay(dur)
		if !workspace.LatestBuild.Deadline.IsZero() && workspace.LatestBuild.Transition == codersdk.WorkspaceTransitionStart {
			nextStopDisplay = timeDisplay(workspace.LatestBuild.Deadline.Time)
		}
	}
	return scheduleListRow{
		WorkspaceName: workspace.OwnerName + "/" + workspace.Name,
		StartsAt:      autostartDisplay,
		StartsNext:    nextStartDisplay,
		StopsAfter:    autostopDisplay,
		StopsNext:     nextStopDisplay,
	}
}