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 }