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  }