github.com/mattyr/nomad@v0.3.3-0.20160919021406-3485a065154a/command/run.go (about)

     1  package command
     2  
     3  import (
     4  	"bytes"
     5  	"encoding/gob"
     6  	"encoding/json"
     7  	"fmt"
     8  	"os"
     9  	"regexp"
    10  	"strconv"
    11  	"strings"
    12  	"time"
    13  
    14  	"github.com/hashicorp/nomad/api"
    15  	"github.com/hashicorp/nomad/nomad/structs"
    16  )
    17  
    18  var (
    19  	// enforceIndexRegex is a regular expression which extracts the enforcement error
    20  	enforceIndexRegex = regexp.MustCompile(`\((Enforcing job modify index.*)\)`)
    21  )
    22  
    23  type RunCommand struct {
    24  	Meta
    25  	JobGetter
    26  }
    27  
    28  func (c *RunCommand) Help() string {
    29  	helpText := `
    30  Usage: nomad run [options] <path>
    31  
    32    Starts running a new job or updates an existing job using
    33    the specification located at <path>. This is the main command
    34    used to interact with Nomad.
    35  
    36    If the supplied path is "-", the jobfile is read from stdin. Otherwise
    37    it is read from the file at the supplied path or downloaded and
    38    read from URL specified.
    39  
    40    Upon successful job submission, this command will immediately
    41    enter an interactive monitor. This is useful to watch Nomad's
    42    internals make scheduling decisions and place the submitted work
    43    onto nodes. The monitor will end once job placement is done. It
    44    is safe to exit the monitor early using ctrl+c.
    45  
    46    On successful job submission and scheduling, exit code 0 will be
    47    returned. If there are job placement issues encountered
    48    (unsatisfiable constraints, resource exhaustion, etc), then the
    49    exit code will be 2. Any other errors, including client connection
    50    issues or internal errors, are indicated by exit code 1.
    51  
    52    If the job has specified the region, the -region flag and NOMAD_REGION
    53    environment variable are overridden and the the job's region is used.
    54  
    55    The run command will set the vault_token of the job based on the following
    56    precedence, going from highest to lowest: the -vault-token flag, the
    57    $VAULT_TOKEN environment variable and finally the value in the job file.
    58  
    59  General Options:
    60  
    61    ` + generalOptionsUsage() + `
    62  
    63  Run Options:
    64  
    65    -check-index
    66      If set, the job is only registered or updated if the the passed
    67      job modify index matches the server side version. If a check-index value of
    68      zero is passed, the job is only registered if it does not yet exist. If a
    69      non-zero value is passed, it ensures that the job is being updated from a
    70      known state. The use of this flag is most common in conjunction with plan
    71      command.
    72  
    73    -detach
    74      Return immediately instead of entering monitor mode. After job submission,
    75      the evaluation ID will be printed to the screen, which can be used to
    76      examine the evaluation using the eval-status command.
    77  
    78    -verbose
    79      Display full information.
    80  
    81    -vault-token
    82      If set, the passed Vault token is stored in the job before sending to the
    83      Nomad servers. This allows passing the Vault token without storing it in
    84      the job file. This overrides the token found in $VAULT_TOKEN environment
    85      variable and that found in the job.
    86  
    87    -output
    88      Output the JSON that would be submitted to the HTTP API without submitting
    89      the job.
    90  `
    91  	return strings.TrimSpace(helpText)
    92  }
    93  
    94  func (c *RunCommand) Synopsis() string {
    95  	return "Run a new job or update an existing job"
    96  }
    97  
    98  func (c *RunCommand) Run(args []string) int {
    99  	var detach, verbose, output bool
   100  	var checkIndexStr, vaultToken string
   101  
   102  	flags := c.Meta.FlagSet("run", FlagSetClient)
   103  	flags.Usage = func() { c.Ui.Output(c.Help()) }
   104  	flags.BoolVar(&detach, "detach", false, "")
   105  	flags.BoolVar(&verbose, "verbose", false, "")
   106  	flags.BoolVar(&output, "output", false, "")
   107  	flags.StringVar(&checkIndexStr, "check-index", "", "")
   108  	flags.StringVar(&vaultToken, "vault-token", "", "")
   109  
   110  	if err := flags.Parse(args); err != nil {
   111  		return 1
   112  	}
   113  
   114  	// Truncate the id unless full length is requested
   115  	length := shortId
   116  	if verbose {
   117  		length = fullId
   118  	}
   119  
   120  	// Check that we got exactly one argument
   121  	args = flags.Args()
   122  	if len(args) != 1 {
   123  		c.Ui.Error(c.Help())
   124  		return 1
   125  	}
   126  
   127  	// Check that we got exactly one node
   128  	args = flags.Args()
   129  	if len(args) != 1 {
   130  		c.Ui.Error(c.Help())
   131  		return 1
   132  	}
   133  
   134  	// Get Job struct from Jobfile
   135  	job, err := c.JobGetter.StructJob(args[0])
   136  	if err != nil {
   137  		c.Ui.Error(fmt.Sprintf("Error getting job struct: %s", err))
   138  		return 1
   139  	}
   140  
   141  	// Initialize any fields that need to be.
   142  	job.Canonicalize()
   143  
   144  	// Check that the job is valid
   145  	if err := job.Validate(); err != nil {
   146  		c.Ui.Error(fmt.Sprintf("Error validating job: %v", err))
   147  		return 1
   148  	}
   149  
   150  	// Check if the job is periodic.
   151  	periodic := job.IsPeriodic()
   152  
   153  	// Parse the Vault token
   154  	if vaultToken == "" {
   155  		// Check the environment variable
   156  		vaultToken = os.Getenv("VAULT_TOKEN")
   157  	}
   158  
   159  	if vaultToken != "" {
   160  		job.VaultToken = vaultToken
   161  	}
   162  
   163  	// Convert it to something we can use
   164  	apiJob, err := convertStructJob(job)
   165  	if err != nil {
   166  		c.Ui.Error(fmt.Sprintf("Error converting job: %s", err))
   167  		return 1
   168  	}
   169  
   170  	if output {
   171  		req := api.RegisterJobRequest{Job: apiJob}
   172  		buf, err := json.MarshalIndent(req, "", "    ")
   173  		if err != nil {
   174  			c.Ui.Error(fmt.Sprintf("Error converting job: %s", err))
   175  			return 1
   176  		}
   177  
   178  		c.Ui.Output(string(buf))
   179  		return 0
   180  	}
   181  
   182  	// Get the HTTP client
   183  	client, err := c.Meta.Client()
   184  	if err != nil {
   185  		c.Ui.Error(fmt.Sprintf("Error initializing client: %s", err))
   186  		return 1
   187  	}
   188  
   189  	// Force the region to be that of the job.
   190  	if r := job.Region; r != "" {
   191  		client.SetRegion(r)
   192  	}
   193  
   194  	// Parse the check-index
   195  	checkIndex, enforce, err := parseCheckIndex(checkIndexStr)
   196  	if err != nil {
   197  		c.Ui.Error(fmt.Sprintf("Error parsing check-index value %q: %v", checkIndexStr, err))
   198  		return 1
   199  	}
   200  
   201  	// Submit the job
   202  	var evalID string
   203  	if enforce {
   204  		evalID, _, err = client.Jobs().EnforceRegister(apiJob, checkIndex, nil)
   205  	} else {
   206  		evalID, _, err = client.Jobs().Register(apiJob, nil)
   207  	}
   208  	if err != nil {
   209  		if strings.Contains(err.Error(), api.RegisterEnforceIndexErrPrefix) {
   210  			// Format the error specially if the error is due to index
   211  			// enforcement
   212  			matches := enforceIndexRegex.FindStringSubmatch(err.Error())
   213  			if len(matches) == 2 {
   214  				c.Ui.Error(matches[1]) // The matched group
   215  				c.Ui.Error("Job not updated")
   216  				return 1
   217  			}
   218  		}
   219  
   220  		c.Ui.Error(fmt.Sprintf("Error submitting job: %s", err))
   221  		return 1
   222  	}
   223  
   224  	// Check if we should enter monitor mode
   225  	if detach || periodic {
   226  		c.Ui.Output("Job registration successful")
   227  		if periodic {
   228  			now := time.Now().UTC()
   229  			next := job.Periodic.Next(now)
   230  			c.Ui.Output(fmt.Sprintf("Approximate next launch time: %s (%s from now)",
   231  				formatTime(next), formatTimeDifference(now, next, time.Second)))
   232  		} else {
   233  			c.Ui.Output("Evaluation ID: " + evalID)
   234  		}
   235  
   236  		return 0
   237  	}
   238  
   239  	// Detach was not specified, so start monitoring
   240  	mon := newMonitor(c.Ui, client, length)
   241  	return mon.monitor(evalID, false)
   242  
   243  }
   244  
   245  // parseCheckIndex parses the check-index flag and returns the index, whether it
   246  // was set and potentially an error during parsing.
   247  func parseCheckIndex(input string) (uint64, bool, error) {
   248  	if input == "" {
   249  		return 0, false, nil
   250  	}
   251  
   252  	u, err := strconv.ParseUint(input, 10, 64)
   253  	return u, true, err
   254  }
   255  
   256  // convertStructJob is used to take a *structs.Job and convert it to an *api.Job.
   257  // This function is just a hammer and probably needs to be revisited.
   258  func convertStructJob(in *structs.Job) (*api.Job, error) {
   259  	gob.Register([]map[string]interface{}{})
   260  	gob.Register([]interface{}{})
   261  	var apiJob *api.Job
   262  	buf := new(bytes.Buffer)
   263  	if err := gob.NewEncoder(buf).Encode(in); err != nil {
   264  		return nil, err
   265  	}
   266  	if err := gob.NewDecoder(buf).Decode(&apiJob); err != nil {
   267  		return nil, err
   268  	}
   269  	return apiJob, nil
   270  }