github.com/hernad/nomad@v1.6.112/command/job_dispatch.go (about)

     1  // Copyright (c) HashiCorp, Inc.
     2  // SPDX-License-Identifier: MPL-2.0
     3  
     4  package command
     5  
     6  import (
     7  	"fmt"
     8  	"io"
     9  	"os"
    10  	"strings"
    11  
    12  	"github.com/hernad/nomad/api"
    13  	flaghelper "github.com/hernad/nomad/helper/flags"
    14  	"github.com/posener/complete"
    15  )
    16  
    17  type JobDispatchCommand struct {
    18  	Meta
    19  }
    20  
    21  func (c *JobDispatchCommand) Help() string {
    22  	helpText := `
    23  Usage: nomad job dispatch [options] <parameterized job> [input source]
    24  
    25    Dispatch creates an instance of a parameterized job. A data payload to the
    26    dispatched instance can be provided via stdin by using "-" or by specifying a
    27    path to a file. Metadata can be supplied by using the meta flag one or more
    28    times.
    29  
    30    An optional idempotency token can be used to prevent more than one instance
    31    of the job to be dispatched. If an instance with the same token already
    32    exists, the command returns without any action.
    33  
    34    Upon successful creation, the dispatched job ID will be printed and the
    35    triggered evaluation will be monitored. This can be disabled by supplying the
    36    detach flag.
    37  
    38    When ACLs are enabled, this command requires a token with the 'dispatch-job'
    39    capability for the job's namespace. The 'list-jobs' capability is required to
    40    run the command with a job prefix instead of the exact job ID. The 'read-job'
    41    capability is required to monitor the resulting evaluation when -detach is
    42    not used.
    43  
    44  General Options:
    45  
    46    ` + generalOptionsUsage(usageOptsDefault) + `
    47  
    48  Dispatch Options:
    49  
    50    -meta <key>=<value>
    51      Meta takes a key/value pair separated by "=". The metadata key will be
    52      merged into the job's metadata. The job may define a default value for the
    53      key which is overridden when dispatching. The flag can be provided more than
    54      once to inject multiple metadata key/value pairs. Arbitrary keys are not
    55      allowed. The parameterized job must allow the key to be merged.
    56  
    57    -detach
    58      Return immediately instead of entering monitor mode. After job dispatch,
    59      the evaluation ID will be printed to the screen, which can be used to
    60      examine the evaluation using the eval-status command.
    61  
    62    -idempotency-token
    63      Optional identifier used to prevent more than one instance of the job from
    64      being dispatched.
    65  
    66    -id-prefix-template
    67      Optional prefix template for dispatched job IDs.
    68  
    69    -verbose
    70      Display full information.
    71  `
    72  	return strings.TrimSpace(helpText)
    73  }
    74  
    75  func (c *JobDispatchCommand) Synopsis() string {
    76  	return "Dispatch an instance of a parameterized job"
    77  }
    78  
    79  func (c *JobDispatchCommand) AutocompleteFlags() complete.Flags {
    80  	return mergeAutocompleteFlags(c.Meta.AutocompleteFlags(FlagSetClient),
    81  		complete.Flags{
    82  			"-meta":              complete.PredictAnything,
    83  			"-detach":            complete.PredictNothing,
    84  			"-idempotency-token": complete.PredictAnything,
    85  			"-verbose":           complete.PredictNothing,
    86  		})
    87  }
    88  
    89  func (c *JobDispatchCommand) AutocompleteArgs() complete.Predictor {
    90  	return complete.PredictFunc(func(a complete.Args) []string {
    91  		client, err := c.Meta.Client()
    92  		if err != nil {
    93  			return nil
    94  		}
    95  
    96  		resp, _, err := client.Jobs().PrefixList(a.Last)
    97  		if err != nil {
    98  			return []string{}
    99  		}
   100  
   101  		// filter by parameterized jobs
   102  		matches := make([]string, 0, len(resp))
   103  		for _, job := range resp {
   104  			if job.ParameterizedJob {
   105  				matches = append(matches, job.ID)
   106  			}
   107  		}
   108  		return matches
   109  
   110  	})
   111  }
   112  
   113  func (c *JobDispatchCommand) Name() string { return "job dispatch" }
   114  
   115  func (c *JobDispatchCommand) Run(args []string) int {
   116  	var detach, verbose bool
   117  	var idempotencyToken string
   118  	var meta []string
   119  	var idPrefixTemplate string
   120  
   121  	flags := c.Meta.FlagSet(c.Name(), FlagSetClient)
   122  	flags.Usage = func() { c.Ui.Output(c.Help()) }
   123  	flags.BoolVar(&detach, "detach", false, "")
   124  	flags.BoolVar(&verbose, "verbose", false, "")
   125  	flags.StringVar(&idempotencyToken, "idempotency-token", "", "")
   126  	flags.Var((*flaghelper.StringFlag)(&meta), "meta", "")
   127  	flags.StringVar(&idPrefixTemplate, "id-prefix-template", "", "")
   128  
   129  	if err := flags.Parse(args); err != nil {
   130  		return 1
   131  	}
   132  
   133  	// Truncate the id unless full length is requested
   134  	length := shortId
   135  	if verbose {
   136  		length = fullId
   137  	}
   138  
   139  	// Check that we got one or two arguments
   140  	args = flags.Args()
   141  	if l := len(args); l < 1 || l > 2 {
   142  		c.Ui.Error("This command takes one or two argument: <parameterized job> [input source]")
   143  		c.Ui.Error(commandErrorText(c))
   144  		return 1
   145  	}
   146  
   147  	var payload []byte
   148  	var readErr error
   149  
   150  	// Read the input
   151  	if len(args) == 2 {
   152  		switch args[1] {
   153  		case "-":
   154  			payload, readErr = io.ReadAll(os.Stdin)
   155  		default:
   156  			payload, readErr = os.ReadFile(args[1])
   157  		}
   158  		if readErr != nil {
   159  			c.Ui.Error(fmt.Sprintf("Error reading input data: %v", readErr))
   160  			return 1
   161  		}
   162  	}
   163  
   164  	// Build the meta
   165  	metaMap := make(map[string]string, len(meta))
   166  	for _, m := range meta {
   167  		split := strings.SplitN(m, "=", 2)
   168  		if len(split) != 2 {
   169  			c.Ui.Error(fmt.Sprintf("Error parsing meta value: %v", m))
   170  			return 1
   171  		}
   172  
   173  		metaMap[split[0]] = split[1]
   174  	}
   175  
   176  	// Get the HTTP client
   177  	client, err := c.Meta.Client()
   178  	if err != nil {
   179  		c.Ui.Error(fmt.Sprintf("Error initializing client: %s", err))
   180  		return 1
   181  	}
   182  
   183  	// Check if the job exists
   184  	jobIDPrefix := strings.TrimSpace(args[0])
   185  	jobID, namespace, err := c.JobIDByPrefix(client, jobIDPrefix, func(j *api.JobListStub) bool {
   186  		return j.ParameterizedJob
   187  	})
   188  	if err != nil {
   189  		c.Ui.Error(err.Error())
   190  		return 1
   191  	}
   192  
   193  	// Dispatch the job
   194  	w := &api.WriteOptions{
   195  		IdempotencyToken: idempotencyToken,
   196  		Namespace:        namespace,
   197  	}
   198  	resp, _, err := client.Jobs().Dispatch(jobID, metaMap, payload, idPrefixTemplate, w)
   199  	if err != nil {
   200  		c.Ui.Error(fmt.Sprintf("Failed to dispatch job: %s", err))
   201  		return 1
   202  	}
   203  
   204  	// See if an evaluation was created. If the job is periodic there will be no
   205  	// eval.
   206  	evalCreated := resp.EvalID != ""
   207  
   208  	basic := []string{
   209  		fmt.Sprintf("Dispatched Job ID|%s", resp.DispatchedJobID),
   210  	}
   211  	if evalCreated {
   212  		basic = append(basic, fmt.Sprintf("Evaluation ID|%s", limit(resp.EvalID, length)))
   213  	}
   214  	c.Ui.Output(formatKV(basic))
   215  
   216  	// Nothing to do
   217  	if detach || !evalCreated {
   218  		return 0
   219  	}
   220  
   221  	c.Ui.Output("")
   222  	mon := newMonitor(c.Ui, client, length)
   223  	return mon.monitor(resp.EvalID)
   224  }