Files
nomad/scheduler/annotate.go
Paweł Bęza 43885f6854 Allow for in-place update when affinity or spread was changed (#25109)
Similarly to #6732 it removes checking affinity and spread for inplace update.
Both affinity and spread should be as soft preference for Nomad scheduler rather than strict constraint. Therefore modifying them should not trigger job reallocation.

Fixes #25070
Co-authored-by: Tim Gross <tgross@hashicorp.com>
2025-02-14 14:33:18 -05:00

216 lines
5.2 KiB
Go

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package scheduler
import (
"strconv"
"github.com/hashicorp/nomad/nomad/structs"
)
const (
AnnotationForcesCreate = "forces create"
AnnotationForcesDestroy = "forces destroy"
AnnotationForcesInplaceUpdate = "forces in-place update"
AnnotationForcesDestructiveUpdate = "forces create/destroy update"
)
// UpdateTypes denote the type of update to occur against the task group.
const (
UpdateTypeIgnore = "ignore"
UpdateTypeCreate = "create"
UpdateTypeDestroy = "destroy"
UpdateTypeMigrate = "migrate"
UpdateTypeCanary = "canary"
UpdateTypeInplaceUpdate = "in-place update"
UpdateTypeDestructiveUpdate = "create/destroy update"
)
// Annotate takes the diff between the old and new version of a Job, the
// scheduler's plan annotations and will add annotations to the diff to aide
// human understanding of the plan.
//
// Currently the things that are annotated are:
// * Task group changes will be annotated with:
// - Count up and count down changes
// - Update counts (creates, destroys, migrates, etc)
//
// * Task changes will be annotated with:
// - forces create/destroy update
// - forces in-place update
func Annotate(diff *structs.JobDiff, annotations *structs.PlanAnnotations) error {
tgDiffs := diff.TaskGroups
if len(tgDiffs) == 0 {
return nil
}
for _, tgDiff := range tgDiffs {
if err := annotateTaskGroup(tgDiff, annotations); err != nil {
return err
}
}
return nil
}
// annotateTaskGroup takes a task group diff and annotates it.
func annotateTaskGroup(diff *structs.TaskGroupDiff, annotations *structs.PlanAnnotations) error {
// Annotate the updates
if annotations != nil {
tg, ok := annotations.DesiredTGUpdates[diff.Name]
if ok {
if diff.Updates == nil {
diff.Updates = make(map[string]uint64, 6)
}
if tg.Ignore != 0 {
diff.Updates[UpdateTypeIgnore] = tg.Ignore
}
if tg.Place != 0 {
diff.Updates[UpdateTypeCreate] = tg.Place
}
if tg.Migrate != 0 {
diff.Updates[UpdateTypeMigrate] = tg.Migrate
}
if tg.Stop != 0 {
diff.Updates[UpdateTypeDestroy] = tg.Stop
}
if tg.Canary != 0 {
diff.Updates[UpdateTypeCanary] = tg.Canary
}
if tg.InPlaceUpdate != 0 {
diff.Updates[UpdateTypeInplaceUpdate] = tg.InPlaceUpdate
}
if tg.DestructiveUpdate != 0 {
diff.Updates[UpdateTypeDestructiveUpdate] = tg.DestructiveUpdate
}
}
}
// Annotate the count
if err := annotateCountChange(diff); err != nil {
return err
}
// Annotate the tasks.
taskDiffs := diff.Tasks
if len(taskDiffs) == 0 {
return nil
}
for _, taskDiff := range taskDiffs {
annotateTask(taskDiff, diff)
}
return nil
}
// annotateCountChange takes a task group diff and annotates the count
// parameter.
func annotateCountChange(diff *structs.TaskGroupDiff) error {
var countDiff *structs.FieldDiff
for _, diff := range diff.Fields {
if diff.Name == "Count" {
countDiff = diff
break
}
}
// Didn't find
if countDiff == nil {
return nil
}
var oldV, newV int
var err error
if countDiff.Old == "" {
oldV = 0
} else {
oldV, err = strconv.Atoi(countDiff.Old)
if err != nil {
return err
}
}
if countDiff.New == "" {
newV = 0
} else {
newV, err = strconv.Atoi(countDiff.New)
if err != nil {
return err
}
}
if oldV < newV {
countDiff.Annotations = append(countDiff.Annotations, AnnotationForcesCreate)
} else if newV < oldV {
countDiff.Annotations = append(countDiff.Annotations, AnnotationForcesDestroy)
}
return nil
}
// annotateCountChange takes a task diff and annotates it.
func annotateTask(diff *structs.TaskDiff, parent *structs.TaskGroupDiff) {
if diff.Type == structs.DiffTypeNone {
return
}
// The whole task group is changing
if parent.Type == structs.DiffTypeAdded || parent.Type == structs.DiffTypeDeleted {
if diff.Type == structs.DiffTypeAdded {
diff.Annotations = append(diff.Annotations, AnnotationForcesCreate)
return
} else if diff.Type == structs.DiffTypeDeleted {
diff.Annotations = append(diff.Annotations, AnnotationForcesDestroy)
return
}
}
// All changes to primitive fields result in a destructive update except
// KillTimeout
destructive := false
FieldsLoop:
for _, fDiff := range diff.Fields {
switch fDiff.Name {
case "KillTimeout":
continue
default:
destructive = true
break FieldsLoop
}
}
// Object changes that can be done in-place are log configs, services,
// constraints, affinity or spread.
if !destructive {
ObjectsLoop:
for _, oDiff := range diff.Objects {
switch oDiff.Name {
case "Service", "Constraint", "Affinity", "Spread":
continue
case "LogConfig":
for _, fDiff := range oDiff.Fields {
switch fDiff.Name {
// force a destructive update if logger was enabled or disabled
case "Disabled":
destructive = true
break ObjectsLoop
}
}
continue
default:
destructive = true
break ObjectsLoop
}
}
}
if destructive {
diff.Annotations = append(diff.Annotations, AnnotationForcesDestructiveUpdate)
} else {
diff.Annotations = append(diff.Annotations, AnnotationForcesInplaceUpdate)
}
}