Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 48 additions & 4 deletions command/job_plan.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,12 @@ Plan Options:

-verbose
Increase diff verbosity.

-json-output
Output the plan result in JSON format.

-t
Format and display the plan result using a Go template.
`
return strings.TrimSpace(helpText)
}
Expand All @@ -120,10 +126,12 @@ func (c *JobPlanCommand) AutocompleteFlags() complete.Flags {
"-policy-override": complete.PredictNothing,
"-verbose": complete.PredictNothing,
"-json": complete.PredictNothing,
"-json-output": complete.PredictNothing,
"-hcl2-strict": complete.PredictNothing,
"-vault-namespace": complete.PredictAnything,
"-var": complete.PredictAnything,
"-var-file": complete.PredictFiles("*.var"),
"-t": complete.PredictAnything,
})
}

Expand All @@ -137,24 +145,31 @@ func (c *JobPlanCommand) AutocompleteArgs() complete.Predictor {

func (c *JobPlanCommand) Name() string { return "job plan" }
func (c *JobPlanCommand) Run(args []string) int {
var diff, policyOverride, verbose bool
var vaultNamespace string
var diff, policyOverride, verbose, jsonOutput bool
var vaultNamespace, tmpl string

flagSet := c.Meta.FlagSet(c.Name(), FlagSetClient)
flagSet.Usage = func() { c.Ui.Output(c.Help()) }
flagSet.BoolVar(&diff, "diff", true, "")
flagSet.BoolVar(&policyOverride, "policy-override", false, "")
flagSet.BoolVar(&verbose, "verbose", false, "")
flagSet.BoolVar(&c.JobGetter.JSON, "json", false, "")
flagSet.BoolVar(&jsonOutput, "json-output", false, "")
flagSet.BoolVar(&c.JobGetter.Strict, "hcl2-strict", true, "")
flagSet.StringVar(&vaultNamespace, "vault-namespace", "", "")
flagSet.StringVar(&tmpl, "t", "", "")
flagSet.Var(&c.JobGetter.Vars, "var", "")
flagSet.Var(&c.JobGetter.VarFiles, "var-file", "")

if err := flagSet.Parse(args); err != nil {
return 255
}

if jsonOutput && len(tmpl) > 0 {
c.Ui.Error("Both -json-output and -t formatting are not allowed")
return 255
}

// Check that we got exactly one job
args = flagSet.Args()
if len(args) != 1 {
Expand Down Expand Up @@ -208,7 +223,7 @@ func (c *JobPlanCommand) Run(args []string) int {
}

if job.IsMultiregion() {
return c.multiregionPlan(client, job, opts, diff, verbose)
return c.multiregionPlan(client, job, opts, diff, verbose, jsonOutput, tmpl)
}

// Submit the job
Expand All @@ -218,6 +233,17 @@ func (c *JobPlanCommand) Run(args []string) int {
return 255
}

// If JSON or template output is requested, format the response and return.
if jsonOutput || len(tmpl) > 0 {
out, err := Format(jsonOutput, tmpl, resp)
if err != nil {
c.Ui.Error(err.Error())
return 255
}
c.Ui.Output(out)
return getExitCode(resp)
}

runArgs := strings.Builder{}
for _, varArg := range c.JobGetter.Vars {
runArgs.WriteString(fmt.Sprintf("-var=%q ", varArg))
Expand All @@ -243,7 +269,7 @@ func (c *JobPlanCommand) Run(args []string) int {
return exitCode
}

func (c *JobPlanCommand) multiregionPlan(client *api.Client, job *api.Job, opts *api.PlanOptions, diff, verbose bool) int {
func (c *JobPlanCommand) multiregionPlan(client *api.Client, job *api.Job, opts *api.PlanOptions, diff, verbose, jsonOutput bool, tmpl string) int {

var exitCode int
plans := map[string]*api.JobPlanResponse{}
Expand All @@ -266,6 +292,24 @@ func (c *JobPlanCommand) multiregionPlan(client *api.Client, job *api.Job, opts
return exitCode
}

// If JSON or template output is requested, format the full map of region
// plans and return.
if jsonOutput || len(tmpl) > 0 {
out, err := Format(jsonOutput, tmpl, plans)
if err != nil {
c.Ui.Error(err.Error())
return 255
}
c.Ui.Output(out)
for _, resp := range plans {
regionExitCode := getExitCode(resp)
if regionExitCode > exitCode {
exitCode = regionExitCode
}
}
return exitCode
}

for regionName, resp := range plans {
c.Ui.Output(c.Colorize().Color(fmt.Sprintf("[bold]Region: %q[reset]", regionName)))
regionExitCode := c.outputPlannedJob(job, resp, diff, verbose)
Expand Down
76 changes: 76 additions & 0 deletions command/job_plan_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
package command

import (
"encoding/json"
"os"
"strconv"
"strings"
Expand Down Expand Up @@ -306,3 +307,78 @@ func TestPlanCommand_JSON(t *testing.T) {
must.Eq(t, 255, code)
must.StrContains(t, ui.ErrorWriter.String(), "Error during plan: Put")
}

func TestPlanCommand_JsonOutputAndTemplateMutuallyExclusive(t *testing.T) {
ci.Parallel(t)
ui := cli.NewMockUi()
cmd := &JobPlanCommand{Meta: Meta{Ui: ui}}

args := []string{
"-json-output",
"-t", "{{.JobModifyIndex}}",
"testdata/example-short.json",
}
code := cmd.Run(args)
must.Eq(t, 255, code)
must.StrContains(t, ui.ErrorWriter.String(), "Both -json-output and -t formatting are not allowed")
}

func TestPlanCommand_JsonOutput(t *testing.T) {
// Create a Vault server
v := testutil.NewTestVault(t)
defer v.Stop()

// Create a Nomad server
s := testutil.NewTestServer(t, func(c *testutil.TestServerConfig) {
c.Vaults[0].Address = v.HTTPAddr
c.Vaults[0].Enabled = true
c.Vaults[0].AllowUnauthenticated = pointer.Of(false)
c.Vaults[0].Token = v.RootToken
})
defer s.Stop()

ui := cli.NewMockUi()
cmd := &JobPlanCommand{Meta: Meta{Ui: ui}}
args := []string{"-address", "http://" + s.HTTPAddr, "-json-output", "testdata/example-basic.nomad"}
code := cmd.Run(args)
// Exit code 1 means allocations created/destroyed (no client running to place)
must.Eq(t, 1, code, must.Sprintf("expected exit code 1, got %d: stderr=%s", code, ui.ErrorWriter.String()))

// Verify the output is valid JSON matching the JobPlanResponse structure
out := ui.OutputWriter.Bytes()
var resp api.JobPlanResponse
must.NoError(t, json.Unmarshal(out, &resp), must.Sprintf("output should be valid JSON: %s", string(out)))

// The response should have a Diff and Annotations since we always request diff
must.NotNil(t, resp.Diff)
must.NotNil(t, resp.Annotations)
}

func TestPlanCommand_TemplateOutput(t *testing.T) {
// Create a Vault server
v := testutil.NewTestVault(t)
defer v.Stop()

// Create a Nomad server
s := testutil.NewTestServer(t, func(c *testutil.TestServerConfig) {
c.Vaults[0].Address = v.HTTPAddr
c.Vaults[0].Enabled = true
c.Vaults[0].AllowUnauthenticated = pointer.Of(false)
c.Vaults[0].Token = v.RootToken
})
defer s.Stop()

ui := cli.NewMockUi()
cmd := &JobPlanCommand{Meta: Meta{Ui: ui}}
args := []string{
"-address", "http://" + s.HTTPAddr,
"-t", "{{.Diff.ID}}",
"testdata/example-basic.nomad",
}
code := cmd.Run(args)
must.Eq(t, 1, code, must.Sprintf("expected exit code 1, got %d: stderr=%s", code, ui.ErrorWriter.String()))

// The template should produce the job ID from the diff
out := strings.TrimSpace(ui.OutputWriter.String())
must.Eq(t, "example", out, must.Sprintf("expected job ID 'example', got: %s", out))
}