2023-04-10 15:36:59 +00:00
|
|
|
// Copyright (c) HashiCorp, Inc.
|
|
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
|
|
|
2020-10-06 07:58:46 +00:00
|
|
|
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.
|
|
|
|
|
2023-03-09 20:00:04 +00:00
|
|
|
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.
|
2020-11-19 21:38:08 +00:00
|
|
|
|
2020-10-06 07:58:46 +00:00
|
|
|
General Options:
|
|
|
|
|
2020-11-19 16:15:23 +00:00
|
|
|
` + generalOptionsUsage(usageOptsDefault) + `
|
2020-10-06 07:58:46 +00:00
|
|
|
|
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2023-03-03 19:43:20 +00:00
|
|
|
// 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)
|
2020-10-06 07:58:46 +00:00
|
|
|
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]
|
|
|
|
}
|