mirror of
https://github.com/kemko/nomad.git
synced 2026-01-02 00:15:43 +03:00
The plan annotations table isn't sorted by task group, which makes for a less beautiful UX and a flaky test.
242 lines
7.3 KiB
Go
242 lines
7.3 KiB
Go
// Copyright (c) HashiCorp, Inc.
|
|
// SPDX-License-Identifier: BUSL-1.1
|
|
|
|
package command
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/hashicorp/cli"
|
|
"github.com/hashicorp/nomad/api"
|
|
"github.com/hashicorp/nomad/ci"
|
|
"github.com/hashicorp/nomad/helper/pointer"
|
|
"github.com/hashicorp/nomad/helper/uuid"
|
|
"github.com/hashicorp/nomad/nomad/mock"
|
|
"github.com/hashicorp/nomad/nomad/structs"
|
|
"github.com/posener/complete"
|
|
"github.com/shoenig/test/must"
|
|
)
|
|
|
|
func TestEvalStatusCommand_Implements(t *testing.T) {
|
|
ci.Parallel(t)
|
|
var _ cli.Command = &EvalStatusCommand{}
|
|
}
|
|
|
|
func TestEvalStatusCommand_Fails(t *testing.T) {
|
|
ci.Parallel(t)
|
|
srv, _, url := testServer(t, false, nil)
|
|
defer srv.Shutdown()
|
|
|
|
ui := cli.NewMockUi()
|
|
cmd := &EvalStatusCommand{Meta: Meta{Ui: ui}}
|
|
|
|
// Fails on misuse
|
|
if code := cmd.Run([]string{"some", "bad", "args"}); code != 1 {
|
|
t.Fatalf("expected exit code 1, got: %d", code)
|
|
}
|
|
if out := ui.ErrorWriter.String(); !strings.Contains(out, commandErrorText(cmd)) {
|
|
t.Fatalf("expected help output, got: %s", out)
|
|
}
|
|
ui.ErrorWriter.Reset()
|
|
|
|
// Fails on eval lookup failure
|
|
if code := cmd.Run([]string{"-address=" + url, "3E55C771-76FC-423B-BCED-3E5314F433B1"}); code != 1 {
|
|
t.Fatalf("expect exit 1, got: %d", code)
|
|
}
|
|
if out := ui.ErrorWriter.String(); !strings.Contains(out, "No evaluation(s) with prefix or id") {
|
|
t.Fatalf("expect not found error, got: %s", out)
|
|
}
|
|
ui.ErrorWriter.Reset()
|
|
|
|
// Fails on connection failure
|
|
if code := cmd.Run([]string{"-address=nope", "12345678-abcd-efab-cdef-123456789abc"}); code != 1 {
|
|
t.Fatalf("expected exit code 1, got: %d", code)
|
|
}
|
|
if out := ui.ErrorWriter.String(); !strings.Contains(out, "Error querying evaluation") {
|
|
t.Fatalf("expected failed query error, got: %s", out)
|
|
}
|
|
ui.ErrorWriter.Reset()
|
|
|
|
// Failed on both -json and -t options are specified
|
|
if code := cmd.Run([]string{"-address=" + url, "-json", "-t", "{{.ID}}",
|
|
"12345678-abcd-efab-cdef-123456789abc"}); code != 1 {
|
|
t.Fatalf("expected exit 1, got: %d", code)
|
|
}
|
|
if out := ui.ErrorWriter.String(); !strings.Contains(out, "Both json and template formatting are not allowed") {
|
|
t.Fatalf("expected getting formatter error, got: %s", out)
|
|
}
|
|
|
|
}
|
|
|
|
func TestEvalStatusCommand_AutocompleteArgs(t *testing.T) {
|
|
ci.Parallel(t)
|
|
|
|
srv, _, url := testServer(t, true, nil)
|
|
defer srv.Shutdown()
|
|
|
|
ui := cli.NewMockUi()
|
|
cmd := &EvalStatusCommand{Meta: Meta{Ui: ui, flagAddress: url}}
|
|
|
|
// Create a fake eval
|
|
state := srv.Agent.Server().State()
|
|
e := mock.Eval()
|
|
must.NoError(t, state.UpsertEvals(structs.MsgTypeTestSetup, 1000, []*structs.Evaluation{e}))
|
|
|
|
prefix := e.ID[:5]
|
|
args := complete.Args{Last: prefix}
|
|
predictor := cmd.AutocompleteArgs()
|
|
|
|
res := predictor.Predict(args)
|
|
must.SliceLen(t, 1, res)
|
|
must.Eq(t, e.ID, res[0])
|
|
}
|
|
|
|
func TestEvalStatusCommand_Format(t *testing.T) {
|
|
now := time.Now().UTC()
|
|
ui := cli.NewMockUi()
|
|
cmd := &EvalStatusCommand{Meta: Meta{Ui: ui}}
|
|
|
|
eval := &api.Evaluation{
|
|
ID: uuid.Generate(),
|
|
Priority: 50,
|
|
Type: api.JobTypeService,
|
|
TriggeredBy: structs.EvalTriggerAllocStop,
|
|
Namespace: api.DefaultNamespace,
|
|
JobID: "example",
|
|
JobModifyIndex: 0,
|
|
DeploymentID: uuid.Generate(),
|
|
Status: api.EvalStatusComplete,
|
|
StatusDescription: "complete",
|
|
NextEval: "",
|
|
PreviousEval: uuid.Generate(),
|
|
BlockedEval: uuid.Generate(),
|
|
RelatedEvals: []*api.EvaluationStub{{
|
|
ID: uuid.Generate(),
|
|
Priority: 50,
|
|
Type: "service",
|
|
TriggeredBy: "queued-allocs",
|
|
Namespace: api.DefaultNamespace,
|
|
JobID: "example",
|
|
DeploymentID: "",
|
|
Status: "pending",
|
|
StatusDescription: "",
|
|
WaitUntil: time.Time{},
|
|
NextEval: "",
|
|
PreviousEval: uuid.Generate(),
|
|
BlockedEval: "",
|
|
CreateIndex: 0,
|
|
ModifyIndex: 0,
|
|
CreateTime: 0,
|
|
ModifyTime: 0,
|
|
}},
|
|
FailedTGAllocs: map[string]*api.AllocationMetric{"web": {
|
|
NodesEvaluated: 6,
|
|
NodesFiltered: 4,
|
|
NodesInPool: 10,
|
|
NodesAvailable: map[string]int{},
|
|
ClassFiltered: map[string]int{},
|
|
ConstraintFiltered: map[string]int{"${attr.kernel.name} = linux": 2},
|
|
NodesExhausted: 2,
|
|
ClassExhausted: map[string]int{},
|
|
DimensionExhausted: map[string]int{"memory": 2},
|
|
QuotaExhausted: []string{},
|
|
ResourcesExhausted: map[string]*api.Resources{"web": {
|
|
Cores: pointer.Of(3),
|
|
}},
|
|
Scores: map[string]float64{},
|
|
AllocationTime: 0,
|
|
CoalescedFailures: 0,
|
|
ScoreMetaData: []*api.NodeScoreMeta{},
|
|
}},
|
|
PlanAnnotations: &api.PlanAnnotations{
|
|
DesiredTGUpdates: map[string]*api.DesiredUpdates{"web": {Place: 10}},
|
|
PreemptedAllocs: []*api.AllocationListStub{
|
|
{
|
|
ID: uuid.Generate(),
|
|
JobID: "another",
|
|
NodeID: uuid.Generate(),
|
|
TaskGroup: "db",
|
|
DesiredStatus: "evict",
|
|
JobVersion: 3,
|
|
ClientStatus: "complete",
|
|
CreateTime: now.Add(-10 * time.Minute).UnixNano(),
|
|
ModifyTime: now.Add(-2 * time.Second).UnixNano(),
|
|
},
|
|
},
|
|
},
|
|
ClassEligibility: map[string]bool{},
|
|
EscapedComputedClass: true,
|
|
QuotaLimitReached: "",
|
|
QueuedAllocations: map[string]int{},
|
|
SnapshotIndex: 1001,
|
|
CreateIndex: 999,
|
|
ModifyIndex: 1003,
|
|
CreateTime: now.UnixNano(),
|
|
ModifyTime: now.Add(time.Second).UnixNano(),
|
|
}
|
|
|
|
placed := []*api.AllocationListStub{
|
|
{
|
|
ID: uuid.Generate(),
|
|
NodeID: uuid.Generate(),
|
|
TaskGroup: "web",
|
|
DesiredStatus: "run",
|
|
JobVersion: 2,
|
|
ClientStatus: "running",
|
|
CreateTime: now.Add(-10 * time.Second).UnixNano(),
|
|
ModifyTime: now.Add(-2 * time.Second).UnixNano(),
|
|
},
|
|
{
|
|
ID: uuid.Generate(),
|
|
NodeID: uuid.Generate(),
|
|
TaskGroup: "web",
|
|
JobVersion: 2,
|
|
DesiredStatus: "run",
|
|
ClientStatus: "pending",
|
|
CreateTime: now.Add(-3 * time.Second).UnixNano(),
|
|
ModifyTime: now.Add(-1 * time.Second).UnixNano(),
|
|
},
|
|
{
|
|
ID: uuid.Generate(),
|
|
NodeID: uuid.Generate(),
|
|
TaskGroup: "web",
|
|
JobVersion: 2,
|
|
DesiredStatus: "run",
|
|
ClientStatus: "pending",
|
|
CreateTime: now.Add(-4 * time.Second).UnixNano(),
|
|
ModifyTime: now.UnixNano(),
|
|
},
|
|
}
|
|
|
|
cmd.formatEvalStatus(eval, placed, false, shortId)
|
|
out := ui.OutputWriter.String()
|
|
|
|
// there isn't much logic here, so this is just a smoke test
|
|
must.StrContains(t, out, `
|
|
Failed Placements
|
|
Task Group "web" (failed to place 1 allocation):
|
|
* Constraint "${attr.kernel.name} = linux": 2 nodes excluded by filter
|
|
* Resources exhausted on 2 nodes
|
|
* Dimension "memory" exhausted on 2 nodes`)
|
|
|
|
must.StrContains(t, out, `Related Evaluations`)
|
|
must.StrContains(t, out, `Placed Allocations`)
|
|
must.StrContains(t, out, `Plan Annotations`)
|
|
must.StrContains(t, out, `Preempted Allocations`)
|
|
}
|
|
|
|
func TestEvalStatus_FormatPlanAnnotations(t *testing.T) {
|
|
|
|
updates := map[string]*api.DesiredUpdates{
|
|
"foo": {Place: 1, Ignore: 2, Canary: 1},
|
|
"bar": {Place: 1, Stop: 3, Reconnect: 2},
|
|
}
|
|
|
|
out := formatPlanAnnotations(updates, false)
|
|
must.Eq(t, `Task Group Ignore Place Stop InPlace Destructive Canary Reconnect
|
|
bar 0 1 3 0 0 0 2
|
|
foo 2 1 0 0 0 1 0`, out)
|
|
}
|