diff --git a/api/deployments.go b/api/deployments.go index feeedad8a..36e86e6df 100644 --- a/api/deployments.go +++ b/api/deployments.go @@ -139,6 +139,7 @@ type Deployment struct { // DeploymentState tracks the state of a deployment for a given task group. type DeploymentState struct { + AutoRevert bool Promoted bool DesiredCanaries int DesiredTotal int diff --git a/command/deployment_list.go b/command/deployment_list.go index 16215e2bd..600a4e0df 100644 --- a/command/deployment_list.go +++ b/command/deployment_list.go @@ -34,15 +34,11 @@ func (c *DeploymentListCommand) Synopsis() string { } func (c *DeploymentListCommand) Run(args []string) int { - var diff, full, verbose bool - var versionStr string + var verbose bool - flags := c.Meta.FlagSet("job history", FlagSetClient) + flags := c.Meta.FlagSet("deployment list", FlagSetClient) flags.Usage = func() { c.Ui.Output(c.Help()) } - flags.BoolVar(&diff, "p", false, "") - flags.BoolVar(&full, "full", false, "") flags.BoolVar(&verbose, "verbose", false, "") - flags.StringVar(&versionStr, "job-version", "", "") if err := flags.Parse(args); err != nil { return 1 diff --git a/command/deployment_status.go b/command/deployment_status.go new file mode 100644 index 000000000..5a6b89a80 --- /dev/null +++ b/command/deployment_status.go @@ -0,0 +1,215 @@ +package command + +import ( + "fmt" + "strings" + + "github.com/hashicorp/nomad/api" +) + +type DeploymentStatusCommand struct { + Meta +} + +func (c *DeploymentStatusCommand) Help() string { + helpText := ` +Usage: nomad deployment status [options] + +Status is used to display the status of a deployment. The status will display +the number of desired changes as well as the currently applied changes. + +General Options: + + ` + generalOptionsUsage() + ` + +Status Options: + + -verbose + Display full information. + + -json + Output the allocation in its JSON format. + + -t + Format and display allocation using a Go template. +` + return strings.TrimSpace(helpText) +} + +func (c *DeploymentStatusCommand) Synopsis() string { + return "Display the status of a deployment" +} + +func (c *DeploymentStatusCommand) Run(args []string) int { + var json, verbose bool + var tmpl string + + flags := c.Meta.FlagSet("deployment status", FlagSetClient) + flags.Usage = func() { c.Ui.Output(c.Help()) } + flags.BoolVar(&verbose, "verbose", false, "") + flags.BoolVar(&json, "json", false, "") + flags.StringVar(&tmpl, "t", "", "") + + 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 + } + + dID := args[0] + + // 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 + } + + // Do a prefix lookup + deploy, possible, err := getDeployment(client.Deployments(), dID) + if err != nil { + c.Ui.Error(fmt.Sprintf("Error retrieving deployment: %s", err)) + return 1 + } + + if len(possible) != 0 { + c.Ui.Output(fmt.Sprintf("Prefix matched multiple deployments\n\n%s", formatDeployments(possible, length))) + return 0 + } + + var format string + if json && len(tmpl) > 0 { + c.Ui.Error("Both -json and -t are not allowed") + return 1 + } else if json { + format = "json" + } else if len(tmpl) > 0 { + format = "template" + } + if len(format) > 0 { + f, err := DataFormat(format, tmpl) + if err != nil { + c.Ui.Error(fmt.Sprintf("Error getting formatter: %s", err)) + return 1 + } + + out, err := f.TransformData(deploy) + if err != nil { + c.Ui.Error(fmt.Sprintf("Error formatting the data: %s", err)) + return 1 + } + c.Ui.Output(out) + return 0 + } + + c.Ui.Output(c.Colorize().Color(formatDeployment(deploy, length))) + return 0 +} + +func getDeployment(client *api.Deployments, dID string) (match *api.Deployment, possible []*api.Deployment, err error) { + // First attempt an immediate lookup if we have a proper length + if len(dID) == 36 { + d, _, err := client.Info(dID, nil) + if err != nil { + return nil, nil, err + } + + return d, nil, nil + } + + dID = strings.Replace(dID, "-", "", -1) + if len(dID) == 1 { + return nil, nil, fmt.Errorf("Identifier must contain at least two characters.") + } + if len(dID)%2 == 1 { + // Identifiers must be of even length, so we strip off the last byte + // to provide a consistent user experience. + dID = dID[:len(dID)-1] + } + + // Have to do a prefix lookup + deploys, _, err := client.PrefixList(dID) + if err != nil { + return nil, nil, err + } + + l := len(deploys) + switch { + case l == 0: + return nil, nil, fmt.Errorf("Deployment ID %q matched no deployments", dID) + case l == 1: + return deploys[0], nil, nil + default: + return nil, deploys, nil + } +} + +func formatDeployment(d *api.Deployment, uuidLength int) string { + // Format the high-level elements + high := []string{ + fmt.Sprintf("ID|%s", limit(d.ID, uuidLength)), + fmt.Sprintf("Job ID|%s", limit(d.JobID, uuidLength)), + fmt.Sprintf("Job Version|%d", d.JobVersion), + fmt.Sprintf("Status|%s", d.Status), + fmt.Sprintf("Description|%s", d.StatusDescription), + } + + base := formatKV(high) + if len(d.TaskGroups) == 0 { + return base + } + base += "\n\n[bold]Deployed[reset]\n" + + // Detect if we need to add these columns + canaries, autorevert := false, false + for _, state := range d.TaskGroups { + if state.AutoRevert { + autorevert = true + } + if state.DesiredCanaries > 0 { + canaries = true + } + } + + // Build the row string + rowString := "Task Group|" + if autorevert { + rowString += "Auto Revert|" + } + rowString += "Desired|" + if canaries { + rowString += "Canaries|" + } + rowString += "Placed|Healthy|Unhealthy" + + rows := make([]string, len(d.TaskGroups)+1) + rows[0] = rowString + i := 1 + for tg, state := range d.TaskGroups { + row := fmt.Sprintf("%s|", tg) + if autorevert { + row += fmt.Sprintf("%v|", state.AutoRevert) + } + row += fmt.Sprintf("%d|", state.DesiredTotal) + if canaries { + row += fmt.Sprintf("%d|", state.DesiredCanaries) + } + row += fmt.Sprintf("%d|%d|%d", state.PlacedAllocs, state.HealthyAllocs, state.UnhealthyAllocs) + rows[i] = row + i++ + } + + base += formatList(rows) + return base +} diff --git a/command/deployment_status_test.go b/command/deployment_status_test.go new file mode 100644 index 000000000..e257b1600 --- /dev/null +++ b/command/deployment_status_test.go @@ -0,0 +1,34 @@ +package command + +import ( + "strings" + "testing" + + "github.com/mitchellh/cli" +) + +func TestDeploymentStatusCommand_Implements(t *testing.T) { + var _ cli.Command = &DeploymentStatusCommand{} +} + +func TestDeploymentStatusCommand_Fails(t *testing.T) { + ui := new(cli.MockUi) + cmd := &DeploymentStatusCommand{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 retrieving deployment") { + t.Fatalf("expected failed query error, got: %s", out) + } + ui.ErrorWriter.Reset() +} diff --git a/commands.go b/commands.go index 7b8fa0d9b..5fafe16dc 100644 --- a/commands.go +++ b/commands.go @@ -64,6 +64,11 @@ func Commands(metaPtr *command.Meta) map[string]cli.CommandFactory { Meta: meta, }, nil }, + "deployment status": func() (cli.Command, error) { + return &command.DeploymentStatusCommand{ + Meta: meta, + }, nil + }, "eval-status": func() (cli.Command, error) { return &command.EvalStatusCommand{ Meta: meta, diff --git a/nomad/deploymentwatcher/deployment_watcher.go b/nomad/deploymentwatcher/deployment_watcher.go index 3151c910b..1e751d3ca 100644 --- a/nomad/deploymentwatcher/deployment_watcher.go +++ b/nomad/deploymentwatcher/deployment_watcher.go @@ -59,10 +59,6 @@ type deploymentWatcher struct { // j is the job the deployment is for j *structs.Job - // autorevert is used to lookup if an task group should autorevert on - // unhealthy allocations - autorevert map[string]bool - // outstandingBatch marks whether an outstanding function exists to create // the evaluation. Access should be done through the lock outstandingBatch bool @@ -89,7 +85,6 @@ func newDeploymentWatcher(parent context.Context, queryLimiter *rate.Limiter, queryLimiter: queryLimiter, d: d, j: j, - autorevert: make(map[string]bool, len(j.TaskGroups)), watchers: watchers, deploymentTriggers: triggers, logger: logger, @@ -97,15 +92,6 @@ func newDeploymentWatcher(parent context.Context, queryLimiter *rate.Limiter, exitFn: exitFn, } - // Determine what task groups will trigger an autorevert - for _, tg := range j.TaskGroups { - autorevert := false - if tg.Update != nil && tg.Update.AutoRevert { - autorevert = true - } - w.autorevert[tg.Name] = autorevert - } - // Start the long lived watcher that scans for allocation updates go w.watch() @@ -145,7 +131,8 @@ func (w *deploymentWatcher) SetAllocHealth( } // Check if the group has autorevert set - if !w.autorevert[alloc.TaskGroup] { + group, ok := w.d.TaskGroups[alloc.TaskGroup] + if !ok || !group.AutoRevert { continue } @@ -313,7 +300,8 @@ func (w *deploymentWatcher) watch() { if alloc.DeploymentStatus.IsUnhealthy() { // Check if the group has autorevert set - if w.autorevert[alloc.TaskGroup] { + group, ok := w.d.TaskGroups[alloc.TaskGroup] + if ok && group.AutoRevert { rollback = true } diff --git a/nomad/state/state_store.go b/nomad/state/state_store.go index fbb2aed72..fb6aeb5e8 100644 --- a/nomad/state/state_store.go +++ b/nomad/state/state_store.go @@ -331,7 +331,7 @@ func (s *StateStore) DeploymentByID(ws memdb.WatchSet, deploymentID string) (*st func (s *StateStore) deploymentByIDImpl(ws memdb.WatchSet, deploymentID string, txn *memdb.Txn) (*structs.Deployment, error) { watchCh, existing, err := txn.FirstWatch("deployment", "id", deploymentID) if err != nil { - return nil, fmt.Errorf("node lookup failed: %v", err) + return nil, fmt.Errorf("deployment lookup failed: %v", err) } ws.Add(watchCh) diff --git a/nomad/structs/structs.go b/nomad/structs/structs.go index 6066f56d3..eb1784ad4 100644 --- a/nomad/structs/structs.go +++ b/nomad/structs/structs.go @@ -3993,6 +3993,10 @@ func (d *Deployment) GoString() string { // DeploymentState tracks the state of a deployment for a given task group. type DeploymentState struct { + // AutoRevert marks whether the task group has indicated the job should be + // reverted on failure + AutoRevert bool + // Promoted marks whether the canaries have been promoted Promoted bool @@ -4020,6 +4024,7 @@ func (d *DeploymentState) GoString() string { base += fmt.Sprintf("\nPlaced: %d", d.PlacedAllocs) base += fmt.Sprintf("\nHealthy: %d", d.HealthyAllocs) base += fmt.Sprintf("\nUnhealthy: %d", d.UnhealthyAllocs) + base += fmt.Sprintf("\nAutoRevert: %v", d.AutoRevert) return base } diff --git a/scheduler/reconcile.go b/scheduler/reconcile.go index e6765719b..d36904f9c 100644 --- a/scheduler/reconcile.go +++ b/scheduler/reconcile.go @@ -234,7 +234,13 @@ func (a *allocReconciler) computeGroup(group string, all allocSet) { dstate, existingDeployment = a.deployment.TaskGroups[group] } if !existingDeployment { - dstate = &structs.DeploymentState{} + autorevert := false + if tg.Update != nil && tg.Update.AutoRevert { + autorevert = true + } + dstate = &structs.DeploymentState{ + AutoRevert: autorevert, + } } // Handle stopping unneeded canaries and tracking placed canaries