Files
nomad/command/alloc_pause.go
Daniel Bennett 4415fabe7d jobspec: time based task execution (#22201)
this is the CE side of an Enterprise-only feature.
a job trying to use this in CE will fail to validate.

to enable daily-scheduled execution entirely client-side,
a job may now contain:

task "name" {
  schedule {
    cron {
      start    = "0 12 * * * *" # may not include "," or "/"
      end      = "0 16"         # partial cron, with only {minute} {hour}
      timezone = "EST"          # anything in your tzdb
    }
  }
...

and everything about the allocation will be placed as usual,
but if outside the specified schedule, the taskrunner will block
on the client, waiting on the schedule start, before proceeding
with the task driver execution, etc.

this includes a taksrunner hook, which watches for the end of
the schedule, at which point it will kill the task.

then, restarts-allowing, a new task will start and again block
waiting for start, and so on.

this also includes all the plumbing required to pipe API calls
through from command->api->agent->server->client, so that
tasks can be force-run, force-paused, or resume the schedule
on demand.
2024-05-22 15:40:25 -05:00

208 lines
5.4 KiB
Go

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package command
import (
"fmt"
"slices"
"strings"
"github.com/hashicorp/nomad/api"
"github.com/hashicorp/nomad/api/contexts"
"github.com/posener/complete"
)
type AllocPauseCommand struct {
Meta
}
func (c *AllocPauseCommand) Help() string {
helpText := `
Usage: nomad alloc pause [options] <allocation> <task>
Set the pause state of an allocation. This command is used to suspend the
operation of a specific alloc and its subtasks. If no task is provided then
all of the allocations subtasks will assume the new pause state.
When ACLs are enabled, this command requires a token with the 'write-job'
capability for the allocation's namespace.
General Options:
` + generalOptionsUsage(usageOptsDefault) + `
Pause Specific Options:
-state=<state>
Specify the schedule state to apply to a task. Must be one of pause, run,
or scheduled. When set to pause the task is halted but the client reports
to the server that the task is still running. When set to run the task is
started regardless of the task schedule. When in scheduled state the task
respects the task schedule state in the task configuration. Defaults to
pause.
-status
Get the current task schedule state status.
-task=<task-name>
Specify the individual task that the action will apply to. If task name is
given with both an argument and the '-task' option, preference is given to
the '-task' option.
-verbose
Show full information.
`
return strings.TrimSpace(helpText)
}
func (c *AllocPauseCommand) Name() string { return "alloc pause" }
func (c *AllocPauseCommand) Run(args []string) int {
var verbose, status bool
var action, task string
flags := c.Meta.FlagSet(c.Name(), FlagSetClient)
flags.Usage = func() { c.Ui.Output(c.Help()) }
flags.BoolVar(&verbose, "verbose", false, "")
flags.StringVar(&action, "state", "pause", "")
flags.BoolVar(&status, "status", false, "")
flags.StringVar(&task, "task", "", "")
if err := flags.Parse(args); err != nil {
return 1
}
// Check that we got exactly one alloc
args = flags.Args()
if len(args) < 1 || len(args) > 2 {
c.Ui.Error("This command takes up to two arguments: <alloc-id> <task>")
c.Ui.Error(commandErrorText(c))
return 1
}
allocID := args[0]
// Truncate the id unless full length is required
length := shortId
if verbose {
length = fullId
}
// Ensure the specified action is valid
actions := []string{"pause", "run", "scheduled"}
if !slices.Contains(actions, action) {
c.Ui.Error(fmt.Sprintf("Pause action must be one of %q, %q, or %q but got %q",
"pause", "run", "scheduled", action,
))
return 1
}
// Query the allocation info
if len(allocID) == 1 {
c.Ui.Error("Alloc ID must contain at least two characters.")
return 1
}
allocID = sanitizeUUIDPrefix(allocID)
// Get the HTTP Client
client, err := c.Meta.Client()
if err != nil {
c.Ui.Error(fmt.Sprintf("Error initializing client: %s", err))
return 1
}
allocs, _, err := client.Allocations().PrefixList(allocID)
if err != nil {
c.Ui.Error(fmt.Sprintf("Error querying allocation: %v", err))
return 1
}
if len(allocs) == 0 {
c.Ui.Error(fmt.Sprintf("No allocation(s) with prefix or id %q found", allocID))
return 1
}
if len(allocs) > 1 {
out := formatAllocListStubs(allocs, verbose, length)
c.Ui.Error(fmt.Sprintf("Prefix matched multiple allocations\n\n%s", out))
return 1
}
// Prefix lookup matched a single allocation, yay
q := &api.QueryOptions{Namespace: allocs[0].Namespace}
alloc, _, err := client.Allocations().Info(allocs[0].ID, q)
if err != nil {
c.Ui.Error(fmt.Sprintf("Error querying allocation: %s", err))
return 1
}
// If -task is not provided then fallback to reading the task name from args
if task == "" && len(args) >= 2 {
task = args[1]
}
// Ensure the task (if specified) exists in the allocation
if task != "" {
err = validateTaskExistsInAllocation(task, alloc)
if err != nil {
c.Ui.Error(err.Error())
return 1
}
}
// If this is a -status request, fetch & print the status, then exit
if status {
query := &api.QueryOptions{
Params: map[string]string{"task": task},
}
state, _, err := client.Allocations().GetPauseState(alloc, query, task)
if err != nil {
c.Ui.Error(fmt.Sprintf("Error getting task pause state: %s", err))
return 1
}
c.Ui.Info(fmt.Sprintf("Pause state: %q", state))
return 0
}
// Send the pause state
err = client.Allocations().SetPauseState(alloc, nil, task, action)
if err != nil {
c.Ui.Error(fmt.Sprintf("Error setting pause state: %s", err))
return 1
}
return 0
}
func (c *AllocPauseCommand) Synopsis() string {
return "Pause a running task"
}
func (c *AllocPauseCommand) AutocompleteFlags() complete.Flags {
return mergeAutocompleteFlags(c.Meta.AutocompleteFlags(FlagSetClient),
complete.Flags{
"-mode": complete.PredictNothing,
"-verbose": complete.PredictNothing,
},
)
}
func (c *AllocPauseCommand) AutocompleteArgs() complete.Predictor {
// Here we only autocomplete allocation names.
// In a similar comment we remark about autocompleting tasks one day.
// Wouldn't that be nice?
return complete.PredictFunc(func(a complete.Args) []string {
client, err := c.Meta.Client()
if err != nil {
return nil
}
resp, _, err := client.Search().PrefixSearch(a.Last, contexts.Allocs, nil)
if err != nil {
return []string{}
}
return resp.Matches[contexts.Allocs]
})
}