github.com/anth0d/nomad@v0.0.0-20221214183521-ae3a0a2cad06/command/job_validate.go (about)

     1  package command
     2  
     3  import (
     4  	"fmt"
     5  	"os"
     6  	"strings"
     7  
     8  	"github.com/hashicorp/go-multierror"
     9  	"github.com/hashicorp/nomad/api"
    10  	"github.com/hashicorp/nomad/command/agent"
    11  	"github.com/hashicorp/nomad/helper/pointer"
    12  	"github.com/posener/complete"
    13  )
    14  
    15  type JobValidateCommand struct {
    16  	Meta
    17  	JobGetter
    18  }
    19  
    20  func (c *JobValidateCommand) Help() string {
    21  	helpText := `
    22  Usage: nomad job validate [options] <path>
    23  Alias: nomad validate
    24  
    25    Checks if a given HCL job file has a valid specification. This can be used to
    26    check for any syntax errors or validation problems with a job.
    27  
    28    If the supplied path is "-", the jobfile is read from stdin. Otherwise
    29    it is read from the file at the supplied path or downloaded and
    30    read from URL specified.
    31  
    32    The run command will set the vault_token of the job based on the following
    33    precedence, going from highest to lowest: the -vault-token flag, the
    34    $VAULT_TOKEN environment variable and finally the value in the job file.
    35  
    36    When ACLs are enabled, this command requires a token with the 'read-job'
    37    capability for the job's namespace.
    38  
    39  General Options:
    40  
    41    ` + generalOptionsUsage(usageOptsDefault) + `
    42  
    43  Validate Options:
    44  
    45    -json
    46      Parses the job file as JSON. If the outer object has a Job field, such as
    47      from "nomad job inspect" or "nomad run -output", the value of the field is
    48      used as the job.
    49  
    50    -hcl1
    51      Parses the job file as HCLv1. Takes precedence over "-hcl2-strict".
    52  
    53    -hcl2-strict
    54      Whether an error should be produced from the HCL2 parser where a variable
    55      has been supplied which is not defined within the root variables. Defaults
    56      to true, but ignored if "-hcl1" is also defined.
    57  
    58    -vault-token
    59      Used to validate if the user submitting the job has permission to run the job
    60      according to its Vault policies. A Vault token must be supplied if the vault
    61      stanza allow_unauthenticated is disabled in the Nomad server configuration.
    62      If the -vault-token flag is set, the passed Vault token is added to the jobspec
    63      before sending to the Nomad servers. This allows passing the Vault token
    64      without storing it in the job file. This overrides the token found in the
    65      $VAULT_TOKEN environment variable and the vault_token field in the job file.
    66      This token is cleared from the job after validating and cannot be used within
    67      the job executing environment. Use the vault stanza when templating in a job
    68      with a Vault token.
    69  
    70    -vault-namespace
    71      If set, the passed Vault namespace is stored in the job before sending to the
    72      Nomad servers.
    73  
    74    -var 'key=value'
    75      Variable for template, can be used multiple times.
    76  
    77    -var-file=path
    78      Path to HCL2 file containing user variables.
    79  `
    80  	return strings.TrimSpace(helpText)
    81  }
    82  
    83  func (c *JobValidateCommand) Synopsis() string {
    84  	return "Checks if a given job specification is valid"
    85  }
    86  
    87  func (c *JobValidateCommand) AutocompleteFlags() complete.Flags {
    88  	return complete.Flags{
    89  		"-hcl1":            complete.PredictNothing,
    90  		"-hcl2-strict":     complete.PredictNothing,
    91  		"-vault-token":     complete.PredictAnything,
    92  		"-vault-namespace": complete.PredictAnything,
    93  		"-var":             complete.PredictAnything,
    94  		"-var-file":        complete.PredictFiles("*.var"),
    95  	}
    96  }
    97  
    98  func (c *JobValidateCommand) AutocompleteArgs() complete.Predictor {
    99  	return complete.PredictOr(
   100  		complete.PredictFiles("*.nomad"),
   101  		complete.PredictFiles("*.hcl"),
   102  		complete.PredictFiles("*.json"),
   103  	)
   104  }
   105  
   106  func (c *JobValidateCommand) Name() string { return "job validate" }
   107  
   108  func (c *JobValidateCommand) Run(args []string) int {
   109  	var vaultToken, vaultNamespace string
   110  
   111  	flagSet := c.Meta.FlagSet(c.Name(), FlagSetClient)
   112  	flagSet.Usage = func() { c.Ui.Output(c.Help()) }
   113  	flagSet.BoolVar(&c.JobGetter.JSON, "json", false, "")
   114  	flagSet.BoolVar(&c.JobGetter.HCL1, "hcl1", false, "")
   115  	flagSet.BoolVar(&c.JobGetter.Strict, "hcl2-strict", true, "")
   116  	flagSet.StringVar(&vaultToken, "vault-token", "", "")
   117  	flagSet.StringVar(&vaultNamespace, "vault-namespace", "", "")
   118  	flagSet.Var(&c.JobGetter.Vars, "var", "")
   119  	flagSet.Var(&c.JobGetter.VarFiles, "var-file", "")
   120  
   121  	if err := flagSet.Parse(args); err != nil {
   122  		return 1
   123  	}
   124  
   125  	// Check that we got exactly one node
   126  	args = flagSet.Args()
   127  	if len(args) != 1 {
   128  		c.Ui.Error("This command takes one argument: <path>")
   129  		c.Ui.Error(commandErrorText(c))
   130  		return 1
   131  	}
   132  
   133  	if c.JobGetter.HCL1 {
   134  		c.JobGetter.Strict = false
   135  	}
   136  
   137  	if err := c.JobGetter.Validate(); err != nil {
   138  		c.Ui.Error(fmt.Sprintf("Invalid job options: %s", err))
   139  		return 1
   140  	}
   141  
   142  	// Get Job struct from Jobfile
   143  	job, err := c.JobGetter.Get(args[0])
   144  	if err != nil {
   145  		c.Ui.Error(fmt.Sprintf("Error getting job struct: %s", err))
   146  		return 1
   147  	}
   148  
   149  	// Get the HTTP client
   150  	client, err := c.Meta.Client()
   151  	if err != nil {
   152  		c.Ui.Error(fmt.Sprintf("Error initializing client: %s", err))
   153  		return 255
   154  	}
   155  
   156  	// Force the region to be that of the job.
   157  	if r := job.Region; r != nil {
   158  		client.SetRegion(*r)
   159  	}
   160  
   161  	// Parse the Vault token
   162  	if vaultToken == "" {
   163  		// Check the environment variable
   164  		vaultToken = os.Getenv("VAULT_TOKEN")
   165  	}
   166  
   167  	if vaultToken != "" {
   168  		job.VaultToken = pointer.Of(vaultToken)
   169  	}
   170  
   171  	if vaultNamespace != "" {
   172  		job.VaultNamespace = pointer.Of(vaultNamespace)
   173  	}
   174  
   175  	// Check that the job is valid
   176  	jr, _, err := client.Jobs().Validate(job, nil)
   177  	if err != nil {
   178  		jr, err = c.validateLocal(job)
   179  	}
   180  	if err != nil {
   181  		c.Ui.Error(fmt.Sprintf("Error validating job: %s", err))
   182  		return 1
   183  	}
   184  
   185  	if jr != nil && !jr.DriverConfigValidated {
   186  		c.Ui.Output(
   187  			c.Colorize().Color("[bold][yellow]Driver configuration not validated since connection to Nomad agent couldn't be established.[reset]\n"))
   188  	}
   189  
   190  	if jr != nil && jr.Error != "" {
   191  		c.Ui.Error(
   192  			c.Colorize().Color("[bold][red]Job validation errors:[reset]"))
   193  		c.Ui.Error(jr.Error)
   194  		return 1
   195  	}
   196  
   197  	// Print any warnings if there are any
   198  	if jr.Warnings != "" {
   199  		c.Ui.Output(
   200  			c.Colorize().Color(fmt.Sprintf("[bold][yellow]Job Warnings:\n%s[reset]\n", jr.Warnings)))
   201  	}
   202  
   203  	// Done!
   204  	c.Ui.Output(
   205  		c.Colorize().Color("[bold][green]Job validation successful[reset]"))
   206  	return 0
   207  }
   208  
   209  // validateLocal validates without talking to a Nomad agent
   210  func (c *JobValidateCommand) validateLocal(aj *api.Job) (*api.JobValidateResponse, error) {
   211  	var out api.JobValidateResponse
   212  
   213  	job := agent.ApiJobToStructJob(aj)
   214  	job.Canonicalize()
   215  
   216  	if vErr := job.Validate(); vErr != nil {
   217  		if merr, ok := vErr.(*multierror.Error); ok {
   218  			for _, err := range merr.Errors {
   219  				out.ValidationErrors = append(out.ValidationErrors, err.Error())
   220  			}
   221  			out.Error = merr.Error()
   222  		} else {
   223  			out.ValidationErrors = append(out.ValidationErrors, vErr.Error())
   224  			out.Error = vErr.Error()
   225  		}
   226  	}
   227  
   228  	out.Warnings = mergeMultierrorWarnings(job.Warnings())
   229  	return &out, nil
   230  }
   231  
   232  func mergeMultierrorWarnings(errs ...error) string {
   233  	if len(errs) == 0 {
   234  		return ""
   235  	}
   236  
   237  	var mErr multierror.Error
   238  	_ = multierror.Append(&mErr, errs...)
   239  
   240  	mErr.ErrorFormat = warningsFormatter
   241  
   242  	return mErr.Error()
   243  }
   244  
   245  func warningsFormatter(es []error) string {
   246  	sb := strings.Builder{}
   247  	sb.WriteString(fmt.Sprintf("%d warning(s):\n", len(es)))
   248  
   249  	for i := range es {
   250  		sb.WriteString(fmt.Sprintf("\n* %s", es[i]))
   251  	}
   252  
   253  	return sb.String()
   254  }