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  }