commit
15fad96c21
|
@ -1,5 +1,9 @@
|
||||||
## 0.6.0 (Unreleased)
|
## 0.6.0 (Unreleased)
|
||||||
|
|
||||||
|
__BACKWARDS INCOMPATIBILITIES:__
|
||||||
|
* cli: When given a prefix that does not resolve to a particular object,
|
||||||
|
commands now return exit code 1 rather than 0.
|
||||||
|
|
||||||
IMPROVEMENTS:
|
IMPROVEMENTS:
|
||||||
* core: Rolling updates based on allocation health [GH-2621, GH-2634, GH-2799]
|
* core: Rolling updates based on allocation health [GH-2621, GH-2634, GH-2799]
|
||||||
* core: New deployment object to track job updates [GH-2621, GH-2634, GH-2799]
|
* core: New deployment object to track job updates [GH-2621, GH-2634, GH-2799]
|
||||||
|
|
|
@ -42,7 +42,7 @@ func (c *DeploymentFailCommand) Synopsis() string {
|
||||||
func (c *DeploymentFailCommand) Run(args []string) int {
|
func (c *DeploymentFailCommand) Run(args []string) int {
|
||||||
var detach, verbose bool
|
var detach, verbose bool
|
||||||
|
|
||||||
flags := c.Meta.FlagSet("deployment resume", FlagSetClient)
|
flags := c.Meta.FlagSet("deployment fail", FlagSetClient)
|
||||||
flags.Usage = func() { c.Ui.Output(c.Help()) }
|
flags.Usage = func() { c.Ui.Output(c.Help()) }
|
||||||
flags.BoolVar(&detach, "detach", false, "")
|
flags.BoolVar(&detach, "detach", false, "")
|
||||||
flags.BoolVar(&verbose, "verbose", false, "")
|
flags.BoolVar(&verbose, "verbose", false, "")
|
||||||
|
@ -81,8 +81,8 @@ func (c *DeploymentFailCommand) Run(args []string) int {
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(possible) != 0 {
|
if len(possible) != 0 {
|
||||||
c.Ui.Output(fmt.Sprintf("Prefix matched multiple deployments\n\n%s", formatDeployments(possible, length)))
|
c.Ui.Error(fmt.Sprintf("Prefix matched multiple deployments\n\n%s", formatDeployments(possible, length)))
|
||||||
return 0
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
u, _, err := client.Deployments().Fail(deploy.ID, nil)
|
u, _, err := client.Deployments().Fail(deploy.ID, nil)
|
||||||
|
|
|
@ -73,8 +73,8 @@ func (c *DeploymentPauseCommand) Run(args []string) int {
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(possible) != 0 {
|
if len(possible) != 0 {
|
||||||
c.Ui.Output(fmt.Sprintf("Prefix matched multiple deployments\n\n%s", formatDeployments(possible, length)))
|
c.Ui.Error(fmt.Sprintf("Prefix matched multiple deployments\n\n%s", formatDeployments(possible, length)))
|
||||||
return 0
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, _, err := client.Deployments().Pause(deploy.ID, true, nil); err != nil {
|
if _, _, err := client.Deployments().Pause(deploy.ID, true, nil); err != nil {
|
||||||
|
|
|
@ -30,12 +30,9 @@ General Options:
|
||||||
|
|
||||||
Promote Options:
|
Promote Options:
|
||||||
|
|
||||||
-all
|
|
||||||
All promotes all task groups in the deployment.
|
|
||||||
|
|
||||||
-group
|
-group
|
||||||
Group may be specified many times and is used to promote that particular
|
Group may be specified many times and is used to promote that particular
|
||||||
group.
|
group. If no specific groups are specified, all groups are promoted.
|
||||||
|
|
||||||
-detach
|
-detach
|
||||||
Return immediately instead of entering monitor mode. After deployment
|
Return immediately instead of entering monitor mode. After deployment
|
||||||
|
@ -49,16 +46,15 @@ Promote Options:
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *DeploymentPromoteCommand) Synopsis() string {
|
func (c *DeploymentPromoteCommand) Synopsis() string {
|
||||||
return "Manually fail a deployment"
|
return "Promote canaries in a deployment"
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *DeploymentPromoteCommand) Run(args []string) int {
|
func (c *DeploymentPromoteCommand) Run(args []string) int {
|
||||||
var all, detach, verbose bool
|
var detach, verbose bool
|
||||||
var groups []string
|
var groups []string
|
||||||
|
|
||||||
flags := c.Meta.FlagSet("deployment resume", FlagSetClient)
|
flags := c.Meta.FlagSet("deployment promote", FlagSetClient)
|
||||||
flags.Usage = func() { c.Ui.Output(c.Help()) }
|
flags.Usage = func() { c.Ui.Output(c.Help()) }
|
||||||
flags.BoolVar(&all, "all", false, "")
|
|
||||||
flags.BoolVar(&detach, "detach", false, "")
|
flags.BoolVar(&detach, "detach", false, "")
|
||||||
flags.BoolVar(&verbose, "verbose", false, "")
|
flags.BoolVar(&verbose, "verbose", false, "")
|
||||||
flags.Var((*flaghelper.StringFlag)(&groups), "group", "")
|
flags.Var((*flaghelper.StringFlag)(&groups), "group", "")
|
||||||
|
@ -73,10 +69,6 @@ func (c *DeploymentPromoteCommand) Run(args []string) int {
|
||||||
c.Ui.Error(c.Help())
|
c.Ui.Error(c.Help())
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
if !all && len(groups) == 0 {
|
|
||||||
c.Ui.Error("Either -all or one or more -group flags must be specified.")
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
dID := args[0]
|
dID := args[0]
|
||||||
|
|
||||||
// Truncate the id unless full length is requested
|
// Truncate the id unless full length is requested
|
||||||
|
@ -100,19 +92,19 @@ func (c *DeploymentPromoteCommand) Run(args []string) int {
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(possible) != 0 {
|
if len(possible) != 0 {
|
||||||
c.Ui.Output(fmt.Sprintf("Prefix matched multiple deployments\n\n%s", formatDeployments(possible, length)))
|
c.Ui.Error(fmt.Sprintf("Prefix matched multiple deployments\n\n%s", formatDeployments(possible, length)))
|
||||||
return 0
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
var u *api.DeploymentUpdateResponse
|
var u *api.DeploymentUpdateResponse
|
||||||
if all {
|
if len(groups) == 0 {
|
||||||
u, _, err = client.Deployments().PromoteAll(deploy.ID, nil)
|
u, _, err = client.Deployments().PromoteAll(deploy.ID, nil)
|
||||||
} else {
|
} else {
|
||||||
u, _, err = client.Deployments().PromoteGroups(deploy.ID, groups, nil)
|
u, _, err = client.Deployments().PromoteGroups(deploy.ID, groups, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.Ui.Error(fmt.Sprintf("Error failing deployment: %s", err))
|
c.Ui.Error(fmt.Sprintf("Error promoting deployment: %s", err))
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -27,14 +27,6 @@ func TestDeploymentPromoteCommand_Fails(t *testing.T) {
|
||||||
if code := cmd.Run([]string{"-address=nope", "12"}); code != 1 {
|
if code := cmd.Run([]string{"-address=nope", "12"}); code != 1 {
|
||||||
t.Fatalf("expected exit code 1, got: %d", code)
|
t.Fatalf("expected exit code 1, got: %d", code)
|
||||||
}
|
}
|
||||||
if out := ui.ErrorWriter.String(); !strings.Contains(out, "flags must be specified") {
|
|
||||||
t.Fatalf("expected missing flags error, got: %s", out)
|
|
||||||
}
|
|
||||||
ui.ErrorWriter.Reset()
|
|
||||||
|
|
||||||
if code := cmd.Run([]string{"-address=nope", "-all", "12"}); code != 1 {
|
|
||||||
t.Fatalf("expected exit code 1, got: %d", code)
|
|
||||||
}
|
|
||||||
if out := ui.ErrorWriter.String(); !strings.Contains(out, "Error retrieving deployment") {
|
if out := ui.ErrorWriter.String(); !strings.Contains(out, "Error retrieving deployment") {
|
||||||
t.Fatalf("expected failed query error, got: %s", out)
|
t.Fatalf("expected failed query error, got: %s", out)
|
||||||
}
|
}
|
||||||
|
|
|
@ -79,8 +79,8 @@ func (c *DeploymentResumeCommand) Run(args []string) int {
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(possible) != 0 {
|
if len(possible) != 0 {
|
||||||
c.Ui.Output(fmt.Sprintf("Prefix matched multiple deployments\n\n%s", formatDeployments(possible, length)))
|
c.Ui.Error(fmt.Sprintf("Prefix matched multiple deployments\n\n%s", formatDeployments(possible, length)))
|
||||||
return 0
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
u, _, err := client.Deployments().Pause(deploy.ID, false, nil)
|
u, _, err := client.Deployments().Pause(deploy.ID, false, nil)
|
||||||
|
|
|
@ -84,8 +84,8 @@ func (c *DeploymentStatusCommand) Run(args []string) int {
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(possible) != 0 {
|
if len(possible) != 0 {
|
||||||
c.Ui.Output(fmt.Sprintf("Prefix matched multiple deployments\n\n%s", formatDeployments(possible, length)))
|
c.Ui.Error(fmt.Sprintf("Prefix matched multiple deployments\n\n%s", formatDeployments(possible, length)))
|
||||||
return 0
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
if json || len(tmpl) > 0 {
|
if json || len(tmpl) > 0 {
|
||||||
|
@ -177,9 +177,12 @@ func formatDeploymentGroups(d *api.Deployment, uuidLength int) string {
|
||||||
if autorevert {
|
if autorevert {
|
||||||
rowString += "Auto Revert|"
|
rowString += "Auto Revert|"
|
||||||
}
|
}
|
||||||
|
if canaries {
|
||||||
|
rowString += "Promoted|"
|
||||||
|
}
|
||||||
rowString += "Desired|"
|
rowString += "Desired|"
|
||||||
if canaries {
|
if canaries {
|
||||||
rowString += "Canaries|Promoted|"
|
rowString += "Canaries|"
|
||||||
}
|
}
|
||||||
rowString += "Placed|Healthy|Unhealthy"
|
rowString += "Placed|Healthy|Unhealthy"
|
||||||
|
|
||||||
|
@ -191,10 +194,12 @@ func formatDeploymentGroups(d *api.Deployment, uuidLength int) string {
|
||||||
if autorevert {
|
if autorevert {
|
||||||
row += fmt.Sprintf("%v|", state.AutoRevert)
|
row += fmt.Sprintf("%v|", state.AutoRevert)
|
||||||
}
|
}
|
||||||
|
if canaries {
|
||||||
|
row += fmt.Sprintf("%v|", state.Promoted)
|
||||||
|
}
|
||||||
row += fmt.Sprintf("%d|", state.DesiredTotal)
|
row += fmt.Sprintf("%d|", state.DesiredTotal)
|
||||||
if canaries {
|
if canaries {
|
||||||
row += fmt.Sprintf("%d|", state.DesiredCanaries)
|
row += fmt.Sprintf("%d|", state.DesiredCanaries)
|
||||||
row += fmt.Sprintf("%v|", state.Promoted)
|
|
||||||
}
|
}
|
||||||
row += fmt.Sprintf("%d|%d|%d", state.PlacedAllocs, state.HealthyAllocs, state.UnhealthyAllocs)
|
row += fmt.Sprintf("%d|%d|%d", state.PlacedAllocs, state.HealthyAllocs, state.UnhealthyAllocs)
|
||||||
rows[i] = row
|
rows[i] = row
|
||||||
|
|
|
@ -137,8 +137,8 @@ func (c *EvalStatusCommand) Run(args []string) int {
|
||||||
failures,
|
failures,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
c.Ui.Output(fmt.Sprintf("Prefix matched multiple evaluations\n\n%s", formatList(out)))
|
c.Ui.Error(fmt.Sprintf("Prefix matched multiple evaluations\n\n%s", formatList(out)))
|
||||||
return 0
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we are in monitor mode, monitor and exit
|
// If we are in monitor mode, monitor and exit
|
||||||
|
|
|
@ -160,8 +160,8 @@ func (f *FSCommand) Run(args []string) int {
|
||||||
if len(allocs) > 1 {
|
if len(allocs) > 1 {
|
||||||
// Format the allocs
|
// Format the allocs
|
||||||
out := formatAllocListStubs(allocs, verbose, length)
|
out := formatAllocListStubs(allocs, verbose, length)
|
||||||
f.Ui.Output(fmt.Sprintf("Prefix matched multiple allocations\n\n%s", out))
|
f.Ui.Error(fmt.Sprintf("Prefix matched multiple allocations\n\n%s", out))
|
||||||
return 0
|
return 1
|
||||||
}
|
}
|
||||||
// Prefix lookup matched a single allocation
|
// Prefix lookup matched a single allocation
|
||||||
alloc, _, err := client.Allocations().Info(allocs[0].ID, nil)
|
alloc, _, err := client.Allocations().Info(allocs[0].ID, nil)
|
||||||
|
|
|
@ -23,6 +23,9 @@ General Options:
|
||||||
|
|
||||||
Inspect Options:
|
Inspect Options:
|
||||||
|
|
||||||
|
-version <job version>
|
||||||
|
Display only the history for the given job version.
|
||||||
|
|
||||||
-json
|
-json
|
||||||
Output the job in its JSON format.
|
Output the job in its JSON format.
|
||||||
|
|
||||||
|
@ -38,12 +41,13 @@ func (c *InspectCommand) Synopsis() string {
|
||||||
|
|
||||||
func (c *InspectCommand) Run(args []string) int {
|
func (c *InspectCommand) Run(args []string) int {
|
||||||
var json bool
|
var json bool
|
||||||
var tmpl string
|
var tmpl, versionStr string
|
||||||
|
|
||||||
flags := c.Meta.FlagSet("inspect", FlagSetClient)
|
flags := c.Meta.FlagSet("inspect", FlagSetClient)
|
||||||
flags.Usage = func() { c.Ui.Output(c.Help()) }
|
flags.Usage = func() { c.Ui.Output(c.Help()) }
|
||||||
flags.BoolVar(&json, "json", false, "")
|
flags.BoolVar(&json, "json", false, "")
|
||||||
flags.StringVar(&tmpl, "t", "", "")
|
flags.StringVar(&tmpl, "t", "", "")
|
||||||
|
flags.StringVar(&versionStr, "version", "", "")
|
||||||
|
|
||||||
if err := flags.Parse(args); err != nil {
|
if err := flags.Parse(args); err != nil {
|
||||||
return 1
|
return 1
|
||||||
|
@ -93,12 +97,23 @@ func (c *InspectCommand) Run(args []string) int {
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
if len(jobs) > 1 && strings.TrimSpace(jobID) != jobs[0].ID {
|
if len(jobs) > 1 && strings.TrimSpace(jobID) != jobs[0].ID {
|
||||||
c.Ui.Output(fmt.Sprintf("Prefix matched multiple jobs\n\n%s", createStatusListOutput(jobs)))
|
c.Ui.Error(fmt.Sprintf("Prefix matched multiple jobs\n\n%s", createStatusListOutput(jobs)))
|
||||||
return 0
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
var version *uint64
|
||||||
|
if versionStr != "" {
|
||||||
|
v, _, err := parseVersion(versionStr)
|
||||||
|
if err != nil {
|
||||||
|
c.Ui.Error(fmt.Sprintf("Error parsing version value %q: %v", versionStr, err))
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
version = &v
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prefix lookup matched a single job
|
// Prefix lookup matched a single job
|
||||||
job, _, err := client.Jobs().Info(jobs[0].ID, nil)
|
job, err := getJob(client, jobs[0].ID, version)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.Ui.Error(fmt.Sprintf("Error inspecting job: %s", err))
|
c.Ui.Error(fmt.Sprintf("Error inspecting job: %s", err))
|
||||||
return 1
|
return 1
|
||||||
|
@ -132,3 +147,25 @@ func (c *InspectCommand) Run(args []string) int {
|
||||||
c.Ui.Output(out)
|
c.Ui.Output(out)
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getJob retrieves the job optionally at a particular version.
|
||||||
|
func getJob(client *api.Client, jobID string, version *uint64) (*api.Job, error) {
|
||||||
|
if version == nil {
|
||||||
|
job, _, err := client.Jobs().Info(jobID, nil)
|
||||||
|
return job, err
|
||||||
|
}
|
||||||
|
|
||||||
|
versions, _, _, err := client.Jobs().Versions(jobID, false, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, j := range versions {
|
||||||
|
if *j.Version != *version {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return j, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, fmt.Errorf("job %q with version %d couldn't be found", jobID, *version)
|
||||||
|
}
|
||||||
|
|
|
@ -82,8 +82,8 @@ func (c *JobDeploymentsCommand) Run(args []string) int {
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
if len(jobs) > 1 && strings.TrimSpace(jobID) != jobs[0].ID {
|
if len(jobs) > 1 && strings.TrimSpace(jobID) != jobs[0].ID {
|
||||||
c.Ui.Output(fmt.Sprintf("Prefix matched multiple jobs\n\n%s", createStatusListOutput(jobs)))
|
c.Ui.Error(fmt.Sprintf("Prefix matched multiple jobs\n\n%s", createStatusListOutput(jobs)))
|
||||||
return 0
|
return 1
|
||||||
}
|
}
|
||||||
jobID = jobs[0].ID
|
jobID = jobs[0].ID
|
||||||
|
|
||||||
|
|
|
@ -100,8 +100,8 @@ func (c *JobHistoryCommand) Run(args []string) int {
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
if len(jobs) > 1 && strings.TrimSpace(jobID) != jobs[0].ID {
|
if len(jobs) > 1 && strings.TrimSpace(jobID) != jobs[0].ID {
|
||||||
c.Ui.Output(fmt.Sprintf("Prefix matched multiple jobs\n\n%s", createStatusListOutput(jobs)))
|
c.Ui.Error(fmt.Sprintf("Prefix matched multiple jobs\n\n%s", createStatusListOutput(jobs)))
|
||||||
return 0
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prefix lookup matched a single job
|
// Prefix lookup matched a single job
|
||||||
|
|
|
@ -0,0 +1,136 @@
|
||||||
|
package command
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/hashicorp/nomad/api"
|
||||||
|
flaghelper "github.com/hashicorp/nomad/helper/flag-helpers"
|
||||||
|
)
|
||||||
|
|
||||||
|
type JobPromoteCommand struct {
|
||||||
|
Meta
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *JobPromoteCommand) Help() string {
|
||||||
|
helpText := `
|
||||||
|
Usage: nomad job promote [options] <job id>
|
||||||
|
|
||||||
|
Promote is used to promote task groups in the most recent deployment for the
|
||||||
|
given job. Promotion should occur when the deployment has placed canaries for a
|
||||||
|
task group and those canaries have been deemed healthy. When a task group is
|
||||||
|
promoted, the rolling upgrade of the remaining allocations is unblocked. If the
|
||||||
|
canaries are found to be unhealthy, the deployment may either be failed using
|
||||||
|
the "nomad deployment fail" command, the job can be failed forward by submitting
|
||||||
|
a new version or failed backwards by reverting to an older version using the
|
||||||
|
"nomad job revert" command.
|
||||||
|
|
||||||
|
General Options:
|
||||||
|
|
||||||
|
` + generalOptionsUsage() + `
|
||||||
|
|
||||||
|
Promote Options:
|
||||||
|
|
||||||
|
-group
|
||||||
|
Group may be specified many times and is used to promote that particular
|
||||||
|
group. If no specific groups are specified, all groups are promoted.
|
||||||
|
|
||||||
|
-detach
|
||||||
|
Return immediately instead of entering monitor mode. After deployment
|
||||||
|
resume, the evaluation ID will be printed to the screen, which can be used
|
||||||
|
to examine the evaluation using the eval-status command.
|
||||||
|
|
||||||
|
-verbose
|
||||||
|
Display full information.
|
||||||
|
`
|
||||||
|
return strings.TrimSpace(helpText)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *JobPromoteCommand) Synopsis() string {
|
||||||
|
return "Promote a job's canaries"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *JobPromoteCommand) Run(args []string) int {
|
||||||
|
var detach, verbose bool
|
||||||
|
var groups []string
|
||||||
|
|
||||||
|
flags := c.Meta.FlagSet("job promote", FlagSetClient)
|
||||||
|
flags.Usage = func() { c.Ui.Output(c.Help()) }
|
||||||
|
flags.BoolVar(&detach, "detach", false, "")
|
||||||
|
flags.BoolVar(&verbose, "verbose", false, "")
|
||||||
|
flags.Var((*flaghelper.StringFlag)(&groups), "group", "")
|
||||||
|
|
||||||
|
if err := flags.Parse(args); err != nil {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that we got no arguments
|
||||||
|
args = flags.Args()
|
||||||
|
if l := len(args); l != 1 {
|
||||||
|
c.Ui.Error(c.Help())
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Truncate the id unless full length is requested
|
||||||
|
length := shortId
|
||||||
|
if verbose {
|
||||||
|
length = fullId
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the HTTP client
|
||||||
|
client, err := c.Meta.Client()
|
||||||
|
if err != nil {
|
||||||
|
c.Ui.Error(fmt.Sprintf("Error initializing client: %s", err))
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the job exists
|
||||||
|
jobID := args[0]
|
||||||
|
jobs, _, err := client.Jobs().PrefixList(jobID)
|
||||||
|
if err != nil {
|
||||||
|
c.Ui.Error(fmt.Sprintf("Error promoting job: %s", err))
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
if len(jobs) == 0 {
|
||||||
|
c.Ui.Error(fmt.Sprintf("No job(s) with prefix or id %q found", jobID))
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
if len(jobs) > 1 && strings.TrimSpace(jobID) != jobs[0].ID {
|
||||||
|
c.Ui.Error(fmt.Sprintf("Prefix matched multiple jobs\n\n%s", createStatusListOutput(jobs)))
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
jobID = jobs[0].ID
|
||||||
|
|
||||||
|
// Do a prefix lookup
|
||||||
|
deploy, _, err := client.Jobs().LatestDeployment(jobID, nil)
|
||||||
|
if err != nil {
|
||||||
|
c.Ui.Error(fmt.Sprintf("Error retrieving deployment: %s", err))
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
if deploy == nil {
|
||||||
|
c.Ui.Error(fmt.Sprintf("Job %q has no deployment to promote", jobID))
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
var u *api.DeploymentUpdateResponse
|
||||||
|
if len(groups) == 0 {
|
||||||
|
u, _, err = client.Deployments().PromoteAll(deploy.ID, nil)
|
||||||
|
} else {
|
||||||
|
u, _, err = client.Deployments().PromoteGroups(deploy.ID, groups, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
c.Ui.Error(fmt.Sprintf("Error promoting deployment %q for job %q: %s", deploy.ID, jobID, err))
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nothing to do
|
||||||
|
evalCreated := u.EvalID != ""
|
||||||
|
if detach || !evalCreated {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
mon := newMonitor(c.Ui, client, length)
|
||||||
|
return mon.monitor(u.EvalID, false)
|
||||||
|
}
|
|
@ -0,0 +1,34 @@
|
||||||
|
package command
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/mitchellh/cli"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestJobPromoteCommand_Implements(t *testing.T) {
|
||||||
|
var _ cli.Command = &JobPromoteCommand{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestJobPromoteCommand_Fails(t *testing.T) {
|
||||||
|
ui := new(cli.MockUi)
|
||||||
|
cmd := &JobPromoteCommand{Meta: Meta{Ui: ui}}
|
||||||
|
|
||||||
|
// Fails on misuse
|
||||||
|
if code := cmd.Run([]string{"some", "bad", "args"}); code != 1 {
|
||||||
|
t.Fatalf("expected exit code 1, got: %d", code)
|
||||||
|
}
|
||||||
|
if out := ui.ErrorWriter.String(); !strings.Contains(out, cmd.Help()) {
|
||||||
|
t.Fatalf("expected help output, got: %s", out)
|
||||||
|
}
|
||||||
|
ui.ErrorWriter.Reset()
|
||||||
|
|
||||||
|
if code := cmd.Run([]string{"-address=nope", "12"}); code != 1 {
|
||||||
|
t.Fatalf("expected exit code 1, got: %d", code)
|
||||||
|
}
|
||||||
|
if out := ui.ErrorWriter.String(); !strings.Contains(out, "Error promoting") {
|
||||||
|
t.Fatalf("expected failed to promote error, got: %s", out)
|
||||||
|
}
|
||||||
|
ui.ErrorWriter.Reset()
|
||||||
|
}
|
|
@ -91,8 +91,8 @@ func (c *JobRevertCommand) Run(args []string) int {
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
if len(jobs) > 1 && strings.TrimSpace(jobID) != jobs[0].ID {
|
if len(jobs) > 1 && strings.TrimSpace(jobID) != jobs[0].ID {
|
||||||
c.Ui.Output(fmt.Sprintf("Prefix matched multiple jobs\n\n%s", createStatusListOutput(jobs)))
|
c.Ui.Error(fmt.Sprintf("Prefix matched multiple jobs\n\n%s", createStatusListOutput(jobs)))
|
||||||
return 0
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prefix lookup matched a single job
|
// Prefix lookup matched a single job
|
||||||
|
|
|
@ -136,8 +136,8 @@ func (l *LogsCommand) Run(args []string) int {
|
||||||
if len(allocs) > 1 {
|
if len(allocs) > 1 {
|
||||||
// Format the allocs
|
// Format the allocs
|
||||||
out := formatAllocListStubs(allocs, verbose, length)
|
out := formatAllocListStubs(allocs, verbose, length)
|
||||||
l.Ui.Output(fmt.Sprintf("Prefix matched multiple allocations\n\n%s", out))
|
l.Ui.Error(fmt.Sprintf("Prefix matched multiple allocations\n\n%s", out))
|
||||||
return 0
|
return 1
|
||||||
}
|
}
|
||||||
// Prefix lookup matched a single allocation
|
// Prefix lookup matched a single allocation
|
||||||
alloc, _, err := client.Allocations().Info(allocs[0].ID, nil)
|
alloc, _, err := client.Allocations().Info(allocs[0].ID, nil)
|
||||||
|
|
|
@ -124,8 +124,8 @@ func (c *NodeDrainCommand) Run(args []string) int {
|
||||||
node.Status)
|
node.Status)
|
||||||
}
|
}
|
||||||
// Dump the output
|
// Dump the output
|
||||||
c.Ui.Output(fmt.Sprintf("Prefix matched multiple nodes\n\n%s", formatList(out)))
|
c.Ui.Error(fmt.Sprintf("Prefix matched multiple nodes\n\n%s", formatList(out)))
|
||||||
return 0
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prefix lookup matched a single node
|
// Prefix lookup matched a single node
|
||||||
|
|
|
@ -231,8 +231,8 @@ func (c *NodeStatusCommand) Run(args []string) int {
|
||||||
node.Status)
|
node.Status)
|
||||||
}
|
}
|
||||||
// Dump the output
|
// Dump the output
|
||||||
c.Ui.Output(fmt.Sprintf("Prefix matched multiple nodes\n\n%s", formatList(out)))
|
c.Ui.Error(fmt.Sprintf("Prefix matched multiple nodes\n\n%s", formatList(out)))
|
||||||
return 0
|
return 1
|
||||||
}
|
}
|
||||||
// Prefix lookup matched a single node
|
// Prefix lookup matched a single node
|
||||||
node, _, err := client.Nodes().Info(nodes[0].ID, nil)
|
node, _, err := client.Nodes().Info(nodes[0].ID, nil)
|
||||||
|
|
|
@ -121,8 +121,8 @@ func (c *StatusCommand) Run(args []string) int {
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
if len(jobs) > 1 && strings.TrimSpace(jobID) != jobs[0].ID {
|
if len(jobs) > 1 && strings.TrimSpace(jobID) != jobs[0].ID {
|
||||||
c.Ui.Output(fmt.Sprintf("Prefix matched multiple jobs\n\n%s", createStatusListOutput(jobs)))
|
c.Ui.Error(fmt.Sprintf("Prefix matched multiple jobs\n\n%s", createStatusListOutput(jobs)))
|
||||||
return 0
|
return 1
|
||||||
}
|
}
|
||||||
// Prefix lookup matched a single job
|
// Prefix lookup matched a single job
|
||||||
job, _, err := client.Jobs().Info(jobs[0].ID, nil)
|
job, _, err := client.Jobs().Info(jobs[0].ID, nil)
|
||||||
|
|
|
@ -113,16 +113,18 @@ func TestStatusCommand_Run(t *testing.T) {
|
||||||
if !strings.Contains(out, "Created At") {
|
if !strings.Contains(out, "Created At") {
|
||||||
t.Fatal("should have created header")
|
t.Fatal("should have created header")
|
||||||
}
|
}
|
||||||
|
ui.ErrorWriter.Reset()
|
||||||
ui.OutputWriter.Reset()
|
ui.OutputWriter.Reset()
|
||||||
|
|
||||||
// Query jobs with prefix match
|
// Query jobs with prefix match
|
||||||
if code := cmd.Run([]string{"-address=" + url, "job"}); code != 0 {
|
if code := cmd.Run([]string{"-address=" + url, "job"}); code != 1 {
|
||||||
t.Fatalf("expected exit 0, got: %d", code)
|
t.Fatalf("expected exit 1, got: %d", code)
|
||||||
}
|
}
|
||||||
out = ui.OutputWriter.String()
|
out = ui.ErrorWriter.String()
|
||||||
if !strings.Contains(out, "job1_sfx") || !strings.Contains(out, "job2_sfx") {
|
if !strings.Contains(out, "job1_sfx") || !strings.Contains(out, "job2_sfx") {
|
||||||
t.Fatalf("expected job1_sfx and job2_sfx, got: %s", out)
|
t.Fatalf("expected job1_sfx and job2_sfx, got: %s", out)
|
||||||
}
|
}
|
||||||
|
ui.ErrorWriter.Reset()
|
||||||
ui.OutputWriter.Reset()
|
ui.OutputWriter.Reset()
|
||||||
|
|
||||||
// Query a single job with prefix match
|
// Query a single job with prefix match
|
||||||
|
|
|
@ -94,8 +94,8 @@ func (c *StopCommand) Run(args []string) int {
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
if len(jobs) > 1 && strings.TrimSpace(jobID) != jobs[0].ID {
|
if len(jobs) > 1 && strings.TrimSpace(jobID) != jobs[0].ID {
|
||||||
c.Ui.Output(fmt.Sprintf("Prefix matched multiple jobs\n\n%s", createStatusListOutput(jobs)))
|
c.Ui.Error(fmt.Sprintf("Prefix matched multiple jobs\n\n%s", createStatusListOutput(jobs)))
|
||||||
return 0
|
return 1
|
||||||
}
|
}
|
||||||
// Prefix lookup matched a single job
|
// Prefix lookup matched a single job
|
||||||
job, _, err := client.Jobs().Info(jobs[0].ID, nil)
|
job, _, err := client.Jobs().Info(jobs[0].ID, nil)
|
||||||
|
|
|
@ -144,6 +144,11 @@ func Commands(metaPtr *command.Meta) map[string]cli.CommandFactory {
|
||||||
Meta: meta,
|
Meta: meta,
|
||||||
}, nil
|
}, nil
|
||||||
},
|
},
|
||||||
|
"job promote": func() (cli.Command, error) {
|
||||||
|
return &command.JobPromoteCommand{
|
||||||
|
Meta: meta,
|
||||||
|
}, nil
|
||||||
|
},
|
||||||
"job revert": func() (cli.Command, error) {
|
"job revert": func() (cli.Command, error) {
|
||||||
return &command.JobRevertCommand{
|
return &command.JobRevertCommand{
|
||||||
Meta: meta,
|
Meta: meta,
|
||||||
|
|
2
main.go
2
main.go
|
@ -30,7 +30,7 @@ func RunCustom(args []string, commands map[string]cli.CommandFactory) int {
|
||||||
"deployment resume", "deployment fail", "deployment promote":
|
"deployment resume", "deployment fail", "deployment promote":
|
||||||
case "executor":
|
case "executor":
|
||||||
case "fs ls", "fs cat", "fs stat":
|
case "fs ls", "fs cat", "fs stat":
|
||||||
case "job deployments", "job dispatch", "job history", "job revert":
|
case "job deployments", "job dispatch", "job history", "job promote", "job revert":
|
||||||
case "operator raft", "operator raft list-peers", "operator raft remove-peer":
|
case "operator raft", "operator raft list-peers", "operator raft remove-peer":
|
||||||
case "syslog":
|
case "syslog":
|
||||||
default:
|
default:
|
||||||
|
|
|
@ -122,9 +122,13 @@ func (d *Deployment) Pause(args *structs.DeploymentPauseRequest, reply *structs.
|
||||||
}
|
}
|
||||||
|
|
||||||
if !deploy.Active() {
|
if !deploy.Active() {
|
||||||
|
if args.Pause {
|
||||||
return fmt.Errorf("can't pause terminal deployment")
|
return fmt.Errorf("can't pause terminal deployment")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("can't resume terminal deployment")
|
||||||
|
}
|
||||||
|
|
||||||
// Call into the deployment watcher
|
// Call into the deployment watcher
|
||||||
return d.srv.deploymentWatcher.PauseDeployment(args, reply)
|
return d.srv.deploymentWatcher.PauseDeployment(args, reply)
|
||||||
}
|
}
|
||||||
|
|
|
@ -78,6 +78,12 @@ job "docs" {
|
||||||
last stable job on deployment failure. A job is marked as stable if all the
|
last stable job on deployment failure. A job is marked as stable if all the
|
||||||
allocations as part of its deployment were marked healthy.
|
allocations as part of its deployment were marked healthy.
|
||||||
|
|
||||||
|
- `canary` `(int: 0)` - Specifies that changes to the job that would result in
|
||||||
|
destructive updates should create the specified number of canaries without
|
||||||
|
stopping any previous allocations. Once the operator determines the canaries
|
||||||
|
are healthy, they can be promoted which unblocks a rolling update of the
|
||||||
|
remaining allocations at a rate of `max_parallel`.
|
||||||
|
|
||||||
- `stagger` `(string: "30s")` - Specifies the delay between migrating
|
- `stagger` `(string: "30s")` - Specifies the delay between migrating
|
||||||
allocations off nodes marked for draining. This is specified using a label
|
allocations off nodes marked for draining. This is specified using a label
|
||||||
suffix like "30s" or "1h".
|
suffix like "30s" or "1h".
|
||||||
|
@ -120,10 +126,7 @@ update {
|
||||||
|
|
||||||
This example creates a canary allocation when the job is updated. The canary is
|
This example creates a canary allocation when the job is updated. The canary is
|
||||||
created without stopping any previous allocations from the job and allows
|
created without stopping any previous allocations from the job and allows
|
||||||
operators to determine if the new version of the job should be rolled out. Once
|
operators to determine if the new version of the job should be rolled out.
|
||||||
the operator has determined the new job should be deployed, the deployment can
|
|
||||||
be promoted and a rolling update will occur performing 3 updates at a time till
|
|
||||||
the remainder of the groups allocations have been rolled to the new version.
|
|
||||||
|
|
||||||
```hcl
|
```hcl
|
||||||
update {
|
update {
|
||||||
|
@ -132,6 +135,47 @@ update {
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Once the operator has determined the new job should be deployed, the deployment
|
||||||
|
can be promoted and a rolling update will occur performing 3 updates at a time
|
||||||
|
until the remainder of the groups allocations have been rolled to the new
|
||||||
|
version.
|
||||||
|
|
||||||
|
```text
|
||||||
|
# Promote the canaries for the job.
|
||||||
|
$ nomad job promote <job-id>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Blue/Green Upgrades
|
||||||
|
|
||||||
|
By setting the canary count equal to that of the task group, blue/green
|
||||||
|
deployments can be achieved. When a new version of the job is submitted, instead
|
||||||
|
of doing a rolling upgrade of the existing allocations, the new version of the
|
||||||
|
group is deployed along side the existing set. While this duplicates the
|
||||||
|
resources required during the upgrade process, it allows very safe deployments
|
||||||
|
as the original version of the group is untouched.
|
||||||
|
|
||||||
|
```hcl
|
||||||
|
group "api-server" {
|
||||||
|
count = 3
|
||||||
|
|
||||||
|
update {
|
||||||
|
canary = 3
|
||||||
|
max_parallel = 3
|
||||||
|
}
|
||||||
|
...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Once the operator is satisfied that the new version of the group is stable, the
|
||||||
|
group can be promoted which will result in all allocations for the old versions
|
||||||
|
of the group to be shutdown. This completes the upgrade from blue to green, or
|
||||||
|
old to new version.
|
||||||
|
|
||||||
|
```text
|
||||||
|
# Promote the canaries for the job.
|
||||||
|
$ nomad job promote <job-id>
|
||||||
|
```
|
||||||
|
|
||||||
### Serial Upgrades
|
### Serial Upgrades
|
||||||
|
|
||||||
This example uses a serial upgrade strategy, meaning exactly one task group will
|
This example uses a serial upgrade strategy, meaning exactly one task group will
|
||||||
|
|
Loading…
Reference in New Issue