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