deployment status

This commit is contained in:
Alex Dadgar
2017-06-30 12:35:59 -07:00
parent dc3d500119
commit bab25f6834
9 changed files with 274 additions and 24 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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] <deployment id>
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
}

View File

@@ -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()
}

View File

@@ -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,

View File

@@ -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
}

View File

@@ -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)

View File

@@ -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
}

View File

@@ -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