1package command
2
3import (
4	"fmt"
5	"sort"
6	"strings"
7
8	"github.com/hashicorp/nomad/api"
9	"github.com/hashicorp/nomad/api/contexts"
10	"github.com/posener/complete"
11)
12
13type DeploymentStatusCommand struct {
14	Meta
15}
16
17func (c *DeploymentStatusCommand) Help() string {
18	helpText := `
19Usage: nomad deployment status [options] <deployment id>
20
21  Status is used to display the status of a deployment. The status will display
22  the number of desired changes as well as the currently applied changes.
23
24General Options:
25
26  ` + generalOptionsUsage() + `
27
28Status Options:
29
30  -verbose
31    Display full information.
32
33  -json
34    Output the deployment in its JSON format.
35
36  -t
37    Format and display deployment using a Go template.
38`
39	return strings.TrimSpace(helpText)
40}
41
42func (c *DeploymentStatusCommand) Synopsis() string {
43	return "Display the status of a deployment"
44}
45
46func (c *DeploymentStatusCommand) AutocompleteFlags() complete.Flags {
47	return mergeAutocompleteFlags(c.Meta.AutocompleteFlags(FlagSetClient),
48		complete.Flags{
49			"-verbose": complete.PredictNothing,
50			"-json":    complete.PredictNothing,
51			"-t":       complete.PredictAnything,
52		})
53}
54
55func (c *DeploymentStatusCommand) AutocompleteArgs() complete.Predictor {
56	return complete.PredictFunc(func(a complete.Args) []string {
57		client, err := c.Meta.Client()
58		if err != nil {
59			return nil
60		}
61
62		resp, _, err := client.Search().PrefixSearch(a.Last, contexts.Deployments, nil)
63		if err != nil {
64			return []string{}
65		}
66		return resp.Matches[contexts.Deployments]
67	})
68}
69
70func (c *DeploymentStatusCommand) Name() string { return "deployment status" }
71
72func (c *DeploymentStatusCommand) Run(args []string) int {
73	var json, verbose bool
74	var tmpl string
75
76	flags := c.Meta.FlagSet(c.Name(), FlagSetClient)
77	flags.Usage = func() { c.Ui.Output(c.Help()) }
78	flags.BoolVar(&verbose, "verbose", false, "")
79	flags.BoolVar(&json, "json", false, "")
80	flags.StringVar(&tmpl, "t", "", "")
81
82	if err := flags.Parse(args); err != nil {
83		return 1
84	}
85
86	// Check that we got exactly one argument
87	args = flags.Args()
88	if l := len(args); l != 1 {
89		c.Ui.Error("This command takes one argument: <deployment id>")
90		c.Ui.Error(commandErrorText(c))
91		return 1
92	}
93
94	dID := args[0]
95
96	// Truncate the id unless full length is requested
97	length := shortId
98	if verbose {
99		length = fullId
100	}
101
102	// Get the HTTP client
103	client, err := c.Meta.Client()
104	if err != nil {
105		c.Ui.Error(fmt.Sprintf("Error initializing client: %s", err))
106		return 1
107	}
108
109	// Do a prefix lookup
110	deploy, possible, err := getDeployment(client.Deployments(), dID)
111	if err != nil {
112		c.Ui.Error(fmt.Sprintf("Error retrieving deployment: %s", err))
113		return 1
114	}
115
116	if len(possible) != 0 {
117		c.Ui.Error(fmt.Sprintf("Prefix matched multiple deployments\n\n%s", formatDeployments(possible, length)))
118		return 1
119	}
120
121	if json || len(tmpl) > 0 {
122		out, err := Format(json, tmpl, deploy)
123		if err != nil {
124			c.Ui.Error(err.Error())
125			return 1
126		}
127
128		c.Ui.Output(out)
129		return 0
130	}
131
132	c.Ui.Output(c.Colorize().Color(formatDeployment(deploy, length)))
133	return 0
134}
135
136func getDeployment(client *api.Deployments, dID string) (match *api.Deployment, possible []*api.Deployment, err error) {
137	// First attempt an immediate lookup if we have a proper length
138	if len(dID) == 36 {
139		d, _, err := client.Info(dID, nil)
140		if err != nil {
141			return nil, nil, err
142		}
143
144		return d, nil, nil
145	}
146
147	dID = strings.Replace(dID, "-", "", -1)
148	if len(dID) == 1 {
149		return nil, nil, fmt.Errorf("Identifier must contain at least two characters.")
150	}
151	if len(dID)%2 == 1 {
152		// Identifiers must be of even length, so we strip off the last byte
153		// to provide a consistent user experience.
154		dID = dID[:len(dID)-1]
155	}
156
157	// Have to do a prefix lookup
158	deploys, _, err := client.PrefixList(dID)
159	if err != nil {
160		return nil, nil, err
161	}
162
163	l := len(deploys)
164	switch {
165	case l == 0:
166		return nil, nil, fmt.Errorf("Deployment ID %q matched no deployments", dID)
167	case l == 1:
168		return deploys[0], nil, nil
169	default:
170		return nil, deploys, nil
171	}
172}
173
174func formatDeployment(d *api.Deployment, uuidLength int) string {
175	if d == nil {
176		return "No deployment found"
177	}
178	// Format the high-level elements
179	high := []string{
180		fmt.Sprintf("ID|%s", limit(d.ID, uuidLength)),
181		fmt.Sprintf("Job ID|%s", d.JobID),
182		fmt.Sprintf("Job Version|%d", d.JobVersion),
183		fmt.Sprintf("Status|%s", d.Status),
184		fmt.Sprintf("Description|%s", d.StatusDescription),
185	}
186
187	base := formatKV(high)
188	if len(d.TaskGroups) == 0 {
189		return base
190	}
191	base += "\n\n[bold]Deployed[reset]\n"
192	base += formatDeploymentGroups(d, uuidLength)
193	return base
194}
195
196func formatDeploymentGroups(d *api.Deployment, uuidLength int) string {
197	// Detect if we need to add these columns
198	var canaries, autorevert, progressDeadline bool
199	tgNames := make([]string, 0, len(d.TaskGroups))
200	for name, state := range d.TaskGroups {
201		tgNames = append(tgNames, name)
202		if state.AutoRevert {
203			autorevert = true
204		}
205		if state.DesiredCanaries > 0 {
206			canaries = true
207		}
208		if state.ProgressDeadline != 0 {
209			progressDeadline = true
210		}
211	}
212
213	// Sort the task group names to get a reliable ordering
214	sort.Strings(tgNames)
215
216	// Build the row string
217	rowString := "Task Group|"
218	if autorevert {
219		rowString += "Auto Revert|"
220	}
221	if canaries {
222		rowString += "Promoted|"
223	}
224	rowString += "Desired|"
225	if canaries {
226		rowString += "Canaries|"
227	}
228	rowString += "Placed|Healthy|Unhealthy"
229	if progressDeadline {
230		rowString += "|Progress Deadline"
231	}
232
233	rows := make([]string, len(d.TaskGroups)+1)
234	rows[0] = rowString
235	i := 1
236	for _, tg := range tgNames {
237		state := d.TaskGroups[tg]
238		row := fmt.Sprintf("%s|", tg)
239		if autorevert {
240			row += fmt.Sprintf("%v|", state.AutoRevert)
241		}
242		if canaries {
243			if state.DesiredCanaries > 0 {
244				row += fmt.Sprintf("%v|", state.Promoted)
245			} else {
246				row += fmt.Sprintf("%v|", "N/A")
247			}
248		}
249		row += fmt.Sprintf("%d|", state.DesiredTotal)
250		if canaries {
251			row += fmt.Sprintf("%d|", state.DesiredCanaries)
252		}
253		row += fmt.Sprintf("%d|%d|%d", state.PlacedAllocs, state.HealthyAllocs, state.UnhealthyAllocs)
254		if progressDeadline {
255			if state.RequireProgressBy.IsZero() {
256				row += fmt.Sprintf("|%v", "N/A")
257			} else {
258				row += fmt.Sprintf("|%v", formatTime(state.RequireProgressBy))
259			}
260		}
261		rows[i] = row
262		i++
263	}
264
265	return formatList(rows)
266}
267