mirror of
https://github.com/kemko/nomad.git
synced 2026-01-01 16:05:42 +03:00
deployment status
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
215
command/deployment_status.go
Normal file
215
command/deployment_status.go
Normal 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
|
||||
}
|
||||
34
command/deployment_status_test.go
Normal file
34
command/deployment_status_test.go
Normal 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()
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user