diff --git a/nomad/plan_apply.go b/nomad/plan_apply.go index 73761d860681..2e5561190117 100644 --- a/nomad/plan_apply.go +++ b/nomad/plan_apply.go @@ -657,8 +657,6 @@ func evaluateNodePlan(snap *state.StateSnapshot, plan *structs.Plan, nodeID stri return false, "node does not exist", nil } else if node.Status != structs.NodeStatusReady { return false, "node is not ready for placements", nil - } else if node.SchedulingEligibility == structs.NodeSchedulingIneligible { - return false, "node is not eligible", nil } // Get the existing allocations that are non-terminal @@ -667,6 +665,15 @@ func evaluateNodePlan(snap *state.StateSnapshot, plan *structs.Plan, nodeID stri return false, "", fmt.Errorf("failed to get existing allocations for '%s': %v", nodeID, err) } + // If nodeAllocations is a subset of the existing allocations we can continue, + // even if the node is not eligible, as only in-place updates or stop/evict are performed + if structs.AllocSubset(existingAlloc, plan.NodeAllocation[nodeID]) { + return true, "", nil + } + if node.SchedulingEligibility == structs.NodeSchedulingIneligible { + return false, "node is not eligible", nil + } + // Determine the proposed allocation by first removing allocations // that are planned evictions and adding the new allocations. var remove []*structs.Allocation diff --git a/nomad/plan_apply_test.go b/nomad/plan_apply_test.go index 7550baf413d4..b4665ddda36a 100644 --- a/nomad/plan_apply_test.go +++ b/nomad/plan_apply_test.go @@ -886,6 +886,39 @@ func TestPlanApply_EvalNodePlan_UpdateExisting(t *testing.T) { } } +func TestPlanApply_EvalNodePlan_UpdateExisting_Ineligible(t *testing.T) { + t.Parallel() + alloc := mock.Alloc() + state := testStateStore(t) + node := mock.Node() + node.ReservedResources = nil + node.Reserved = nil + node.SchedulingEligibility = structs.NodeSchedulingIneligible + alloc.NodeID = node.ID + alloc.AllocatedResources = structs.NodeResourcesToAllocatedResources(node.NodeResources) + state.UpsertNode(structs.MsgTypeTestSetup, 1000, node) + state.UpsertAllocs(structs.MsgTypeTestSetup, 1001, []*structs.Allocation{alloc}) + snap, _ := state.Snapshot() + + plan := &structs.Plan{ + Job: alloc.Job, + NodeAllocation: map[string][]*structs.Allocation{ + node.ID: {alloc}, + }, + } + + fit, reason, err := evaluateNodePlan(snap, plan, node.ID) + if err != nil { + t.Fatalf("err: %v", err) + } + if !fit { + t.Fatalf("bad") + } + if reason != "" { + t.Fatalf("bad") + } +} + func TestPlanApply_EvalNodePlan_NodeFull_Evict(t *testing.T) { t.Parallel() alloc := mock.Alloc() diff --git a/nomad/structs/funcs.go b/nomad/structs/funcs.go index 5fe4d4c6ef59..d6c13eeccdc0 100644 --- a/nomad/structs/funcs.go +++ b/nomad/structs/funcs.go @@ -63,6 +63,24 @@ func RemoveAllocs(allocs []*Allocation, remove []*Allocation) []*Allocation { return r } +func AllocSubset(allocs []*Allocation, subset []*Allocation) bool { + if len(subset) == 0 { + return true + } + // Convert allocs into a map + allocMap := make(map[string]struct{}) + for _, alloc := range allocs { + allocMap[alloc.ID] = struct{}{} + } + + for _, alloc := range subset { + if _, ok := allocMap[alloc.ID]; !ok { + return false + } + } + return true +} + // FilterTerminalAllocs filters out all allocations in a terminal state and // returns the latest terminal allocations. func FilterTerminalAllocs(allocs []*Allocation) ([]*Allocation, map[string]*Allocation) {