github.com/hernad/nomad@v1.6.112/nomad/structs/funcs.go (about) 1 // Copyright (c) HashiCorp, Inc. 2 // SPDX-License-Identifier: MPL-2.0 3 4 package structs 5 6 import ( 7 "crypto/subtle" 8 "encoding/base64" 9 "encoding/binary" 10 "fmt" 11 "math" 12 "sort" 13 "strconv" 14 "strings" 15 16 "github.com/hashicorp/go-set" 17 "github.com/hernad/nomad/acl" 18 "golang.org/x/crypto/blake2b" 19 ) 20 21 // RemoveAllocs is used to remove any allocs with the given IDs 22 // from the list of allocations 23 func RemoveAllocs(allocs []*Allocation, remove []*Allocation) []*Allocation { 24 if len(remove) == 0 { 25 return allocs 26 } 27 // Convert remove into a set 28 removeSet := make(map[string]struct{}) 29 for _, remove := range remove { 30 removeSet[remove.ID] = struct{}{} 31 } 32 33 r := make([]*Allocation, 0, len(allocs)) 34 for _, alloc := range allocs { 35 if _, ok := removeSet[alloc.ID]; !ok { 36 r = append(r, alloc) 37 } 38 } 39 return r 40 } 41 42 func AllocSubset(allocs []*Allocation, subset []*Allocation) bool { 43 if len(subset) == 0 { 44 return true 45 } 46 // Convert allocs into a map 47 allocMap := make(map[string]struct{}) 48 for _, alloc := range allocs { 49 allocMap[alloc.ID] = struct{}{} 50 } 51 52 for _, alloc := range subset { 53 if _, ok := allocMap[alloc.ID]; !ok { 54 return false 55 } 56 } 57 return true 58 } 59 60 // FilterTerminalAllocs filters out all allocations in a terminal state and 61 // returns the latest terminal allocations. 62 func FilterTerminalAllocs(allocs []*Allocation) ([]*Allocation, map[string]*Allocation) { 63 terminalAllocsByName := make(map[string]*Allocation) 64 n := len(allocs) 65 66 for i := 0; i < n; i++ { 67 if allocs[i].TerminalStatus() { 68 69 // Add the allocation to the terminal allocs map if it's not already 70 // added or has a higher create index than the one which is 71 // currently present. 72 alloc, ok := terminalAllocsByName[allocs[i].Name] 73 if !ok || alloc.CreateIndex < allocs[i].CreateIndex { 74 terminalAllocsByName[allocs[i].Name] = allocs[i] 75 } 76 77 // Remove the allocation 78 allocs[i], allocs[n-1] = allocs[n-1], nil 79 i-- 80 n-- 81 } 82 } 83 84 return allocs[:n], terminalAllocsByName 85 } 86 87 // SplitTerminalAllocs splits allocs into non-terminal and terminal allocs, with 88 // the terminal allocs indexed by node->alloc.name. 89 func SplitTerminalAllocs(allocs []*Allocation) ([]*Allocation, TerminalByNodeByName) { 90 var alive []*Allocation 91 var terminal = make(TerminalByNodeByName) 92 93 for _, alloc := range allocs { 94 if alloc.TerminalStatus() { 95 terminal.Set(alloc) 96 } else { 97 alive = append(alive, alloc) 98 } 99 } 100 101 return alive, terminal 102 } 103 104 // TerminalByNodeByName is a map of NodeID->Allocation.Name->Allocation used by 105 // the sysbatch scheduler for locating the most up-to-date terminal allocations. 106 type TerminalByNodeByName map[string]map[string]*Allocation 107 108 func (a TerminalByNodeByName) Set(allocation *Allocation) { 109 node := allocation.NodeID 110 name := allocation.Name 111 112 if _, exists := a[node]; !exists { 113 a[node] = make(map[string]*Allocation) 114 } 115 116 if previous, exists := a[node][name]; !exists { 117 a[node][name] = allocation 118 } else if previous.CreateIndex < allocation.CreateIndex { 119 // keep the newest version of the terminal alloc for the coordinate 120 a[node][name] = allocation 121 } 122 } 123 124 func (a TerminalByNodeByName) Get(nodeID, name string) (*Allocation, bool) { 125 if _, exists := a[nodeID]; !exists { 126 return nil, false 127 } 128 129 if _, exists := a[nodeID][name]; !exists { 130 return nil, false 131 } 132 133 return a[nodeID][name], true 134 } 135 136 // AllocsFit checks if a given set of allocations will fit on a node. 137 // The netIdx can optionally be provided if its already been computed. 138 // If the netIdx is provided, it is assumed that the client has already 139 // ensured there are no collisions. If checkDevices is set to true, we check if 140 // there is a device oversubscription. 141 func AllocsFit(node *Node, allocs []*Allocation, netIdx *NetworkIndex, checkDevices bool) (bool, string, *ComparableResources, error) { 142 // Compute the allocs' utilization from zero 143 used := new(ComparableResources) 144 145 reservedCores := map[uint16]struct{}{} 146 var coreOverlap bool 147 148 // For each alloc, add the resources 149 for _, alloc := range allocs { 150 // Do not consider the resource impact of terminal allocations 151 if alloc.ClientTerminalStatus() { 152 continue 153 } 154 155 cr := alloc.ComparableResources() 156 used.Add(cr) 157 158 // Adding the comparable resource unions reserved core sets, need to check if reserved cores overlap 159 for _, core := range cr.Flattened.Cpu.ReservedCores { 160 if _, ok := reservedCores[core]; ok { 161 coreOverlap = true 162 } else { 163 reservedCores[core] = struct{}{} 164 } 165 } 166 } 167 168 if coreOverlap { 169 return false, "cores", used, nil 170 } 171 172 // Check that the node resources (after subtracting reserved) are a 173 // super set of those that are being allocated 174 available := node.ComparableResources() 175 available.Subtract(node.ComparableReservedResources()) 176 if superset, dimension := available.Superset(used); !superset { 177 return false, dimension, used, nil 178 } 179 180 // Create the network index if missing 181 if netIdx == nil { 182 netIdx = NewNetworkIndex() 183 defer netIdx.Release() 184 185 if err := netIdx.SetNode(node); err != nil { 186 // To maintain backward compatibility with when SetNode 187 // returned collision+reason like AddAllocs, return 188 // this as a reason instead of an error. 189 return false, fmt.Sprintf("reserved node port collision: %v", err), used, nil 190 } 191 if collision, reason := netIdx.AddAllocs(allocs); collision { 192 return false, fmt.Sprintf("reserved alloc port collision: %v", reason), used, nil 193 } 194 } 195 196 // Check if the network is overcommitted 197 if netIdx.Overcommitted() { 198 return false, "bandwidth exceeded", used, nil 199 } 200 201 // Check devices 202 if checkDevices { 203 accounter := NewDeviceAccounter(node) 204 if accounter.AddAllocs(allocs) { 205 return false, "device oversubscribed", used, nil 206 } 207 } 208 209 // Allocations fit! 210 return true, "", used, nil 211 } 212 213 func computeFreePercentage(node *Node, util *ComparableResources) (freePctCpu, freePctRam float64) { 214 // COMPAT(0.11): Remove in 0.11 215 reserved := node.ComparableReservedResources() 216 res := node.ComparableResources() 217 218 // Determine the node availability 219 nodeCpu := float64(res.Flattened.Cpu.CpuShares) 220 nodeMem := float64(res.Flattened.Memory.MemoryMB) 221 if reserved != nil { 222 nodeCpu -= float64(reserved.Flattened.Cpu.CpuShares) 223 nodeMem -= float64(reserved.Flattened.Memory.MemoryMB) 224 } 225 226 // Compute the free percentage 227 freePctCpu = 1 - (float64(util.Flattened.Cpu.CpuShares) / nodeCpu) 228 freePctRam = 1 - (float64(util.Flattened.Memory.MemoryMB) / nodeMem) 229 return freePctCpu, freePctRam 230 } 231 232 // ScoreFitBinPack computes a fit score to achieve pinbacking behavior. 233 // Score is in [0, 18] 234 // 235 // It's the BestFit v3 on the Google work published here: 236 // http://www.columbia.edu/~cs2035/courses/ieor4405.S13/datacenter_scheduling.ppt 237 func ScoreFitBinPack(node *Node, util *ComparableResources) float64 { 238 freePctCpu, freePctRam := computeFreePercentage(node, util) 239 240 // Total will be "maximized" the smaller the value is. 241 // At 100% utilization, the total is 2, while at 0% util it is 20. 242 total := math.Pow(10, freePctCpu) + math.Pow(10, freePctRam) 243 244 // Invert so that the "maximized" total represents a high-value 245 // score. Because the floor is 20, we simply use that as an anchor. 246 // This means at a perfect fit, we return 18 as the score. 247 score := 20.0 - total 248 249 // Bound the score, just in case 250 // If the score is over 18, that means we've overfit the node. 251 if score > 18.0 { 252 score = 18.0 253 } else if score < 0 { 254 score = 0 255 } 256 return score 257 } 258 259 // ScoreFitSpread computes a fit score to achieve spread behavior. 260 // Score is in [0, 18] 261 // 262 // This is equivalent to Worst Fit of 263 // http://www.columbia.edu/~cs2035/courses/ieor4405.S13/datacenter_scheduling.ppt 264 func ScoreFitSpread(node *Node, util *ComparableResources) float64 { 265 freePctCpu, freePctRam := computeFreePercentage(node, util) 266 total := math.Pow(10, freePctCpu) + math.Pow(10, freePctRam) 267 score := total - 2 268 269 if score > 18.0 { 270 score = 18.0 271 } else if score < 0 { 272 score = 0 273 } 274 return score 275 } 276 277 func CopySliceConstraints(s []*Constraint) []*Constraint { 278 l := len(s) 279 if l == 0 { 280 return nil 281 } 282 283 c := make([]*Constraint, l) 284 for i, v := range s { 285 c[i] = v.Copy() 286 } 287 return c 288 } 289 290 func CopySliceAffinities(s []*Affinity) []*Affinity { 291 l := len(s) 292 if l == 0 { 293 return nil 294 } 295 296 c := make([]*Affinity, l) 297 for i, v := range s { 298 c[i] = v.Copy() 299 } 300 return c 301 } 302 303 func CopySliceSpreads(s []*Spread) []*Spread { 304 l := len(s) 305 if l == 0 { 306 return nil 307 } 308 309 c := make([]*Spread, l) 310 for i, v := range s { 311 c[i] = v.Copy() 312 } 313 return c 314 } 315 316 func CopySliceSpreadTarget(s []*SpreadTarget) []*SpreadTarget { 317 l := len(s) 318 if l == 0 { 319 return nil 320 } 321 322 c := make([]*SpreadTarget, l) 323 for i, v := range s { 324 c[i] = v.Copy() 325 } 326 return c 327 } 328 329 func CopySliceNodeScoreMeta(s []*NodeScoreMeta) []*NodeScoreMeta { 330 l := len(s) 331 if l == 0 { 332 return nil 333 } 334 335 c := make([]*NodeScoreMeta, l) 336 for i, v := range s { 337 c[i] = v.Copy() 338 } 339 return c 340 } 341 342 // VaultPoliciesSet takes the structure returned by VaultPolicies and returns 343 // the set of required policies 344 func VaultPoliciesSet(policies map[string]map[string]*Vault) []string { 345 s := set.New[string](10) 346 for _, tgp := range policies { 347 for _, tp := range tgp { 348 if tp != nil { 349 s.InsertAll(tp.Policies) 350 } 351 } 352 } 353 return s.List() 354 } 355 356 // VaultNamespaceSet takes the structure returned by VaultPolicies and 357 // returns a set of required namespaces 358 func VaultNamespaceSet(policies map[string]map[string]*Vault) []string { 359 s := set.New[string](10) 360 for _, tgp := range policies { 361 for _, tp := range tgp { 362 if tp != nil && tp.Namespace != "" { 363 s.Insert(tp.Namespace) 364 } 365 } 366 } 367 return s.List() 368 } 369 370 // DenormalizeAllocationJobs is used to attach a job to all allocations that are 371 // non-terminal and do not have a job already. This is useful in cases where the 372 // job is normalized. 373 func DenormalizeAllocationJobs(job *Job, allocs []*Allocation) { 374 if job != nil { 375 for _, alloc := range allocs { 376 if alloc.Job == nil && !alloc.TerminalStatus() { 377 alloc.Job = job 378 } 379 } 380 } 381 } 382 383 // AllocName returns the name of the allocation given the input. 384 func AllocName(job, group string, idx uint) string { 385 return job + "." + group + "[" + strconv.FormatUint(uint64(idx), 10) + "]" 386 } 387 388 // AllocSuffix returns the alloc index suffix that was added by the AllocName 389 // function above. 390 func AllocSuffix(name string) string { 391 idx := strings.LastIndex(name, "[") 392 if idx == -1 { 393 return "" 394 } 395 suffix := name[idx:] 396 return suffix 397 } 398 399 // ACLPolicyListHash returns a consistent hash for a set of policies. 400 func ACLPolicyListHash(policies []*ACLPolicy) string { 401 cacheKeyHash, err := blake2b.New256(nil) 402 if err != nil { 403 panic(err) 404 } 405 for _, policy := range policies { 406 _, _ = cacheKeyHash.Write([]byte(policy.Name)) 407 _ = binary.Write(cacheKeyHash, binary.BigEndian, policy.ModifyIndex) 408 } 409 cacheKey := string(cacheKeyHash.Sum(nil)) 410 return cacheKey 411 } 412 413 // CompileACLObject compiles a set of ACL policies into an ACL object with a cache 414 func CompileACLObject(cache *ACLCache[*acl.ACL], policies []*ACLPolicy) (*acl.ACL, error) { 415 // Sort the policies to ensure consistent ordering 416 sort.Slice(policies, func(i, j int) bool { 417 return policies[i].Name < policies[j].Name 418 }) 419 420 // Determine the cache key 421 cacheKey := ACLPolicyListHash(policies) 422 entry, ok := cache.Get(cacheKey) 423 if ok { 424 return entry.Get(), nil 425 } 426 427 // Parse the policies 428 parsed := make([]*acl.Policy, 0, len(policies)) 429 for _, policy := range policies { 430 p, err := acl.Parse(policy.Rules) 431 if err != nil { 432 return nil, fmt.Errorf("failed to parse %q: %v", policy.Name, err) 433 } 434 parsed = append(parsed, p) 435 } 436 437 // Create the ACL object 438 aclObj, err := acl.NewACL(false, parsed) 439 if err != nil { 440 return nil, fmt.Errorf("failed to construct ACL: %v", err) 441 } 442 443 // Update the cache 444 cache.Add(cacheKey, aclObj) 445 return aclObj, nil 446 } 447 448 // GenerateMigrateToken will create a token for a client to access an 449 // authenticated volume of another client to migrate data for sticky volumes. 450 func GenerateMigrateToken(allocID, nodeSecretID string) (string, error) { 451 h, err := blake2b.New512([]byte(nodeSecretID)) 452 if err != nil { 453 return "", err 454 } 455 456 _, _ = h.Write([]byte(allocID)) 457 458 return base64.URLEncoding.EncodeToString(h.Sum(nil)), nil 459 } 460 461 // CompareMigrateToken returns true if two migration tokens can be computed and 462 // are equal. 463 func CompareMigrateToken(allocID, nodeSecretID, otherMigrateToken string) bool { 464 h, err := blake2b.New512([]byte(nodeSecretID)) 465 if err != nil { 466 return false 467 } 468 469 _, _ = h.Write([]byte(allocID)) 470 471 otherBytes, err := base64.URLEncoding.DecodeString(otherMigrateToken) 472 if err != nil { 473 return false 474 } 475 return subtle.ConstantTimeCompare(h.Sum(nil), otherBytes) == 1 476 } 477 478 // ParsePortRanges parses the passed port range string and returns a list of the 479 // ports. The specification is a comma separated list of either port numbers or 480 // port ranges. A port number is a single integer and a port range is two 481 // integers separated by a hyphen. As an example the following spec would 482 // convert to: ParsePortRanges("10,12-14,16") -> []uint64{10, 12, 13, 14, 16} 483 func ParsePortRanges(spec string) ([]uint64, error) { 484 parts := strings.Split(spec, ",") 485 486 // Hot path the empty case 487 if len(parts) == 1 && parts[0] == "" { 488 return nil, nil 489 } 490 491 ports := make(map[uint64]struct{}) 492 for _, part := range parts { 493 part = strings.TrimSpace(part) 494 rangeParts := strings.Split(part, "-") 495 l := len(rangeParts) 496 switch l { 497 case 1: 498 if val := rangeParts[0]; val == "" { 499 return nil, fmt.Errorf("can't specify empty port") 500 } else { 501 port, err := strconv.ParseUint(val, 10, 0) 502 if err != nil { 503 return nil, err 504 } 505 506 if port > MaxValidPort { 507 return nil, fmt.Errorf("port must be < %d but found %d", MaxValidPort, port) 508 } 509 ports[port] = struct{}{} 510 } 511 case 2: 512 // We are parsing a range 513 start, err := strconv.ParseUint(rangeParts[0], 10, 0) 514 if err != nil { 515 return nil, err 516 } 517 518 end, err := strconv.ParseUint(rangeParts[1], 10, 0) 519 if err != nil { 520 return nil, err 521 } 522 523 if end < start { 524 return nil, fmt.Errorf("invalid range: starting value (%v) less than ending (%v) value", end, start) 525 } 526 527 // Full range validation is below but prevent creating 528 // arbitrarily large arrays here 529 if end > MaxValidPort { 530 return nil, fmt.Errorf("port must be < %d but found %d", MaxValidPort, end) 531 } 532 533 for i := start; i <= end; i++ { 534 ports[i] = struct{}{} 535 } 536 default: 537 return nil, fmt.Errorf("can only parse single port numbers or port ranges (ex. 80,100-120,150)") 538 } 539 } 540 541 var results []uint64 542 for port := range ports { 543 if port == 0 { 544 return nil, fmt.Errorf("port must be > 0") 545 } 546 if port > MaxValidPort { 547 return nil, fmt.Errorf("port must be < %d but found %d", MaxValidPort, port) 548 } 549 results = append(results, port) 550 } 551 552 sort.Slice(results, func(i, j int) bool { 553 return results[i] < results[j] 554 }) 555 return results, nil 556 }