open-nomad/command/job_scaling_events.go

221 lines
5.5 KiB
Go

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package command
import (
"fmt"
"sort"
"strconv"
"strings"
"time"
"github.com/hashicorp/nomad/api"
"github.com/mitchellh/cli"
"github.com/posener/complete"
)
// Ensure JobScalingEventsCommand satisfies the cli.Command interface.
var _ cli.Command = &JobScalingEventsCommand{}
// JobScalingEventsCommand implements cli.Command.
type JobScalingEventsCommand struct {
Meta
}
// Help satisfies the cli.Command Help function.
func (j *JobScalingEventsCommand) Help() string {
helpText := `
Usage: nomad job scaling-events [options] <args>
List the scaling events for the specified job.
When ACLs are enabled, this command requires a token with either the
'read-job' or 'read-job-scaling' capability for the job's namespace. The
'list-jobs' capability is required to run the command with a job prefix
instead of the exact job ID.
General Options:
` + generalOptionsUsage(usageOptsDefault) + `
Scaling-Events Options:
-verbose
Display full information.
`
return strings.TrimSpace(helpText)
}
// Synopsis satisfies the cli.Command Synopsis function.
func (j *JobScalingEventsCommand) Synopsis() string {
return "Display the most recent scaling events for a job"
}
func (j *JobScalingEventsCommand) AutocompleteFlags() complete.Flags {
return mergeAutocompleteFlags(j.Meta.AutocompleteFlags(FlagSetClient),
complete.Flags{
"-verbose": complete.PredictNothing,
})
}
// Name returns the name of this command.
func (j *JobScalingEventsCommand) Name() string { return "job scaling-events" }
// Run satisfies the cli.Command Run function.
func (j *JobScalingEventsCommand) Run(args []string) int {
var verbose bool
flags := j.Meta.FlagSet(j.Name(), FlagSetClient)
flags.Usage = func() { j.Ui.Output(j.Help()) }
flags.BoolVar(&verbose, "verbose", false, "")
if err := flags.Parse(args); err != nil {
return 1
}
args = flags.Args()
if len(args) != 1 {
j.Ui.Error("This command takes one argument: <job_id>")
j.Ui.Error(commandErrorText(j))
return 1
}
// Get the HTTP client.
client, err := j.Meta.Client()
if err != nil {
j.Ui.Error(fmt.Sprintf("Error initializing client: %s", err))
return 1
}
// Check if the job exists
jobIDPrefix := strings.TrimSpace(args[0])
jobID, namespace, err := j.JobIDByPrefix(client, jobIDPrefix, nil)
if err != nil {
j.Ui.Error(err.Error())
return 1
}
q := &api.QueryOptions{Namespace: namespace}
events, _, err := client.Jobs().ScaleStatus(jobID, q)
if err != nil {
j.Ui.Error(fmt.Sprintf("Error listing scaling events: %s", err))
return 1
}
// Check if any of the task groups have scaling events, otherwise exit
// indicating there are not any.
var haveEvents bool
for _, tg := range events.TaskGroups {
if tg.Events != nil {
haveEvents = true
break
}
}
if !haveEvents {
j.Ui.Output("No events found")
return 0
}
// Create our sorted list of events and output.
sortedList := sortedScalingEventList(events)
j.Ui.Output(formatList(formatScalingEventListOutput(sortedList, verbose, 0)))
return 0
}
func formatScalingEventListOutput(e scalingEventList, verbose bool, limit int) []string {
// If the limit is zero, aka no limit or the limit is greater than the
// number of events we have then set it this to the length of the event
// list.
if limit == 0 || limit > len(e) {
limit = len(e)
}
// Create the initial output heading.
output := make([]string, limit+1)
output[0] = "Task Group|Count|PrevCount"
// If we are outputting verbose information, add these fields to the header
// and then add our end date field.
if verbose {
output[0] += "|Error|Message|Eval ID"
}
output[0] += "|Date"
var i int
for i < limit {
output[i+1] = fmt.Sprintf("%s|%s|%v", e[i].name, valueOrNil(e[i].event.Count), e[i].event.PreviousCount)
if verbose {
output[i+1] += fmt.Sprintf("|%v|%s|%s",
e[i].event.Error, e[i].event.Message, valueOrNil(e[i].event.EvalID))
}
output[i+1] += fmt.Sprintf("|%v", formatTime(time.Unix(0, int64(e[i].event.Time))))
i++
}
return output
}
// sortedScalingEventList generates a time sorted list of scaling events as
// provided by the api.JobScaleStatusResponse.
func sortedScalingEventList(e *api.JobScaleStatusResponse) []groupEvent {
// sortedList is our output list.
var sortedList scalingEventList
// Iterate over the response object to create a sorted list.
for group, status := range e.TaskGroups {
for _, event := range status.Events {
sortedList = append(sortedList, groupEvent{name: group, event: event})
}
}
sort.Sort(sortedList)
return sortedList
}
// valueOrNil helps format the event output in cases where the object has a
// potential to be nil.
func valueOrNil(i interface{}) string {
switch t := i.(type) {
case *int64:
if t != nil {
return strconv.FormatInt(*t, 10)
}
case *string:
if t != nil {
return *t
}
}
return ""
}
// scalingEventList is a helper list of all events for the job which allows us
// to sort based on time.
type scalingEventList []groupEvent
// groupEvent contains all the required information of an individual group
// scaling event.
type groupEvent struct {
name string
event api.ScalingEvent
}
// Len satisfies the Len function on the sort.Interface.
func (s scalingEventList) Len() int {
return len(s)
}
// Less satisfies the Less function on the sort.Interface and sorts by the
// event time.
func (s scalingEventList) Less(i, j int) bool {
return s[i].event.Time > s[j].event.Time
}
// Swap satisfies the Swap function on the sort.Interface.
func (s scalingEventList) Swap(i, j int) {
s[i], s[j] = s[j], s[i]
}