github.com/Ilhicas/nomad@v1.0.4-0.20210304152020-e86851182bc3/command/deployment_status.go (about)

     1  package command
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"sort"
     7  	"strings"
     8  
     9  	"github.com/hashicorp/nomad/api"
    10  	"github.com/hashicorp/nomad/api/contexts"
    11  	"github.com/posener/complete"
    12  )
    13  
    14  type DeploymentStatusCommand struct {
    15  	Meta
    16  }
    17  
    18  func (c *DeploymentStatusCommand) Help() string {
    19  	helpText := `
    20  Usage: nomad deployment status [options] <deployment id>
    21  
    22    Status is used to display the status of a deployment. The status will display
    23    the number of desired changes as well as the currently applied changes.
    24  
    25    When ACLs are enabled, this command requires a token with the 'read-job'
    26    capability for the deployment's namespace.
    27  
    28  General Options:
    29  
    30    ` + generalOptionsUsage(usageOptsDefault) + `
    31  
    32  Status Options:
    33  
    34    -verbose
    35      Display full information.
    36  
    37    -json
    38      Output the deployment in its JSON format.
    39  
    40    -t
    41      Format and display deployment using a Go template.
    42  `
    43  	return strings.TrimSpace(helpText)
    44  }
    45  
    46  func (c *DeploymentStatusCommand) Synopsis() string {
    47  	return "Display the status of a deployment"
    48  }
    49  
    50  func (c *DeploymentStatusCommand) AutocompleteFlags() complete.Flags {
    51  	return mergeAutocompleteFlags(c.Meta.AutocompleteFlags(FlagSetClient),
    52  		complete.Flags{
    53  			"-verbose": complete.PredictNothing,
    54  			"-json":    complete.PredictNothing,
    55  			"-t":       complete.PredictAnything,
    56  		})
    57  }
    58  
    59  func (c *DeploymentStatusCommand) AutocompleteArgs() complete.Predictor {
    60  	return complete.PredictFunc(func(a complete.Args) []string {
    61  		client, err := c.Meta.Client()
    62  		if err != nil {
    63  			return nil
    64  		}
    65  
    66  		resp, _, err := client.Search().PrefixSearch(a.Last, contexts.Deployments, nil)
    67  		if err != nil {
    68  			return []string{}
    69  		}
    70  		return resp.Matches[contexts.Deployments]
    71  	})
    72  }
    73  
    74  func (c *DeploymentStatusCommand) Name() string { return "deployment status" }
    75  
    76  func (c *DeploymentStatusCommand) Run(args []string) int {
    77  	var json, verbose bool
    78  	var tmpl string
    79  
    80  	flags := c.Meta.FlagSet(c.Name(), FlagSetClient)
    81  	flags.Usage = func() { c.Ui.Output(c.Help()) }
    82  	flags.BoolVar(&verbose, "verbose", false, "")
    83  	flags.BoolVar(&json, "json", false, "")
    84  	flags.StringVar(&tmpl, "t", "", "")
    85  
    86  	if err := flags.Parse(args); err != nil {
    87  		return 1
    88  	}
    89  
    90  	// Check that we got exactly one argument
    91  	args = flags.Args()
    92  	if l := len(args); l > 1 {
    93  		c.Ui.Error("This command takes one argument: <deployment id>")
    94  		c.Ui.Error(commandErrorText(c))
    95  		return 1
    96  	}
    97  
    98  	// Truncate the id unless full length is requested
    99  	length := shortId
   100  	if verbose {
   101  		length = fullId
   102  	}
   103  
   104  	// Get the HTTP client
   105  	client, err := c.Meta.Client()
   106  	if err != nil {
   107  		c.Ui.Error(fmt.Sprintf("Error initializing client: %s", err))
   108  		return 1
   109  	}
   110  
   111  	// List if no arguments are provided
   112  	if len(args) == 0 {
   113  		deploys, _, err := client.Deployments().List(nil)
   114  		if err != nil {
   115  			c.Ui.Error(fmt.Sprintf("Error retrieving deployments: %s", err))
   116  			return 1
   117  		}
   118  
   119  		c.Ui.Output(formatDeployments(deploys, length))
   120  		return 0
   121  	}
   122  
   123  	// Do a prefix lookup
   124  	dID := args[0]
   125  	deploy, possible, err := getDeployment(client.Deployments(), dID)
   126  	if err != nil {
   127  		c.Ui.Error(fmt.Sprintf("Error retrieving deployment: %s", err))
   128  		return 1
   129  	}
   130  
   131  	if len(possible) != 0 {
   132  		c.Ui.Error(fmt.Sprintf("Prefix matched multiple deployments\n\n%s", formatDeployments(possible, length)))
   133  		return 1
   134  	}
   135  
   136  	if json || len(tmpl) > 0 {
   137  		out, err := Format(json, tmpl, deploy)
   138  		if err != nil {
   139  			c.Ui.Error(err.Error())
   140  			return 1
   141  		}
   142  
   143  		c.Ui.Output(out)
   144  		return 0
   145  	}
   146  
   147  	c.Ui.Output(c.Colorize().Color(formatDeployment(client, deploy, length)))
   148  	return 0
   149  }
   150  
   151  func getDeployment(client *api.Deployments, dID string) (match *api.Deployment, possible []*api.Deployment, err error) {
   152  	// First attempt an immediate lookup if we have a proper length
   153  	if len(dID) == 36 {
   154  		d, _, err := client.Info(dID, nil)
   155  		if err != nil {
   156  			return nil, nil, err
   157  		}
   158  
   159  		return d, nil, nil
   160  	}
   161  
   162  	dID = strings.Replace(dID, "-", "", -1)
   163  	if len(dID) == 1 {
   164  		return nil, nil, fmt.Errorf("Identifier must contain at least two characters.")
   165  	}
   166  	if len(dID)%2 == 1 {
   167  		// Identifiers must be of even length, so we strip off the last byte
   168  		// to provide a consistent user experience.
   169  		dID = dID[:len(dID)-1]
   170  	}
   171  
   172  	// Have to do a prefix lookup
   173  	deploys, _, err := client.PrefixList(dID)
   174  	if err != nil {
   175  		return nil, nil, err
   176  	}
   177  
   178  	l := len(deploys)
   179  	switch {
   180  	case l == 0:
   181  		return nil, nil, fmt.Errorf("Deployment ID %q matched no deployments", dID)
   182  	case l == 1:
   183  		return deploys[0], nil, nil
   184  	default:
   185  		return nil, deploys, nil
   186  	}
   187  }
   188  
   189  func formatDeployment(c *api.Client, d *api.Deployment, uuidLength int) string {
   190  	if d == nil {
   191  		return "No deployment found"
   192  	}
   193  	// Format the high-level elements
   194  	high := []string{
   195  		fmt.Sprintf("ID|%s", limit(d.ID, uuidLength)),
   196  		fmt.Sprintf("Job ID|%s", d.JobID),
   197  		fmt.Sprintf("Job Version|%d", d.JobVersion),
   198  		fmt.Sprintf("Status|%s", d.Status),
   199  		fmt.Sprintf("Description|%s", d.StatusDescription),
   200  	}
   201  
   202  	base := formatKV(high)
   203  
   204  	// Fetch and Format Multi-region info
   205  	if d.IsMultiregion {
   206  		regions, err := fetchMultiRegionDeployments(c, d)
   207  		if err != nil {
   208  			base += "\n\nError fetching Multiregion deployments\n\n"
   209  		} else if len(regions) > 0 {
   210  			base += "\n\n[bold]Multiregion Deployment[reset]\n"
   211  			base += formatMultiregionDeployment(regions, uuidLength)
   212  		}
   213  	}
   214  
   215  	if len(d.TaskGroups) == 0 {
   216  		return base
   217  	}
   218  	base += "\n\n[bold]Deployed[reset]\n"
   219  	base += formatDeploymentGroups(d, uuidLength)
   220  	return base
   221  }
   222  
   223  type regionResult struct {
   224  	region string
   225  	d      *api.Deployment
   226  	err    error
   227  }
   228  
   229  func fetchMultiRegionDeployments(c *api.Client, d *api.Deployment) (map[string]*api.Deployment, error) {
   230  	results := make(map[string]*api.Deployment)
   231  
   232  	job, _, err := c.Jobs().Info(d.JobID, &api.QueryOptions{})
   233  	if err != nil {
   234  		return nil, err
   235  	}
   236  
   237  	requests := make(chan regionResult, len(job.Multiregion.Regions))
   238  	for i := 0; i < cap(requests); i++ {
   239  		go func(itr int) {
   240  			region := job.Multiregion.Regions[itr]
   241  			d, err := fetchRegionDeployment(c, d, region)
   242  			requests <- regionResult{d: d, err: err, region: region.Name}
   243  		}(i)
   244  	}
   245  	for i := 0; i < cap(requests); i++ {
   246  		res := <-requests
   247  		if res.err != nil {
   248  			key := fmt.Sprintf("%s (error)", res.region)
   249  			results[key] = &api.Deployment{}
   250  			continue
   251  		}
   252  		results[res.region] = res.d
   253  
   254  	}
   255  	return results, nil
   256  }
   257  
   258  func fetchRegionDeployment(c *api.Client, d *api.Deployment, region *api.MultiregionRegion) (*api.Deployment, error) {
   259  	if region == nil {
   260  		return nil, errors.New("Region not found")
   261  	}
   262  
   263  	opts := &api.QueryOptions{Region: region.Name}
   264  	deploys, _, err := c.Jobs().Deployments(d.JobID, false, opts)
   265  	if err != nil {
   266  		return nil, err
   267  	}
   268  	for _, dep := range deploys {
   269  		if dep.JobVersion == d.JobVersion {
   270  			return dep, nil
   271  		}
   272  	}
   273  	return nil, fmt.Errorf("Could not find job version %d for region", d.JobVersion)
   274  }
   275  
   276  func formatMultiregionDeployment(regions map[string]*api.Deployment, uuidLength int) string {
   277  	rowString := "Region|ID|Status"
   278  	rows := make([]string, len(regions)+1)
   279  	rows[0] = rowString
   280  	i := 1
   281  	for k, v := range regions {
   282  		row := fmt.Sprintf("%s|%s|%s", k, limit(v.ID, uuidLength), v.Status)
   283  		rows[i] = row
   284  		i++
   285  	}
   286  	sort.Strings(rows)
   287  	return formatList(rows)
   288  }
   289  
   290  func formatDeploymentGroups(d *api.Deployment, uuidLength int) string {
   291  	// Detect if we need to add these columns
   292  	var canaries, autorevert, progressDeadline bool
   293  	tgNames := make([]string, 0, len(d.TaskGroups))
   294  	for name, state := range d.TaskGroups {
   295  		tgNames = append(tgNames, name)
   296  		if state.AutoRevert {
   297  			autorevert = true
   298  		}
   299  		if state.DesiredCanaries > 0 {
   300  			canaries = true
   301  		}
   302  		if state.ProgressDeadline != 0 {
   303  			progressDeadline = true
   304  		}
   305  	}
   306  
   307  	// Sort the task group names to get a reliable ordering
   308  	sort.Strings(tgNames)
   309  
   310  	// Build the row string
   311  	rowString := "Task Group|"
   312  	if autorevert {
   313  		rowString += "Auto Revert|"
   314  	}
   315  	if canaries {
   316  		rowString += "Promoted|"
   317  	}
   318  	rowString += "Desired|"
   319  	if canaries {
   320  		rowString += "Canaries|"
   321  	}
   322  	rowString += "Placed|Healthy|Unhealthy"
   323  	if progressDeadline {
   324  		rowString += "|Progress Deadline"
   325  	}
   326  
   327  	rows := make([]string, len(d.TaskGroups)+1)
   328  	rows[0] = rowString
   329  	i := 1
   330  	for _, tg := range tgNames {
   331  		state := d.TaskGroups[tg]
   332  		row := fmt.Sprintf("%s|", tg)
   333  		if autorevert {
   334  			row += fmt.Sprintf("%v|", state.AutoRevert)
   335  		}
   336  		if canaries {
   337  			if state.DesiredCanaries > 0 {
   338  				row += fmt.Sprintf("%v|", state.Promoted)
   339  			} else {
   340  				row += fmt.Sprintf("%v|", "N/A")
   341  			}
   342  		}
   343  		row += fmt.Sprintf("%d|", state.DesiredTotal)
   344  		if canaries {
   345  			row += fmt.Sprintf("%d|", state.DesiredCanaries)
   346  		}
   347  		row += fmt.Sprintf("%d|%d|%d", state.PlacedAllocs, state.HealthyAllocs, state.UnhealthyAllocs)
   348  		if progressDeadline {
   349  			if state.RequireProgressBy.IsZero() {
   350  				row += fmt.Sprintf("|%v", "N/A")
   351  			} else {
   352  				row += fmt.Sprintf("|%v", formatTime(state.RequireProgressBy))
   353  			}
   354  		}
   355  		rows[i] = row
   356  		i++
   357  	}
   358  
   359  	return formatList(rows)
   360  }