mirror of
https://github.com/kemko/nomad.git
synced 2026-01-03 08:55:43 +03:00
177 lines
4.4 KiB
Go
177 lines
4.4 KiB
Go
// Copyright (c) HashiCorp, Inc.
|
|
// SPDX-License-Identifier: BUSL-1.1
|
|
|
|
// Package dynamic provides a way of allocating UID/GID to be used by Nomad
|
|
// tasks with no associated service users managed by the operating system.
|
|
package dynamic
|
|
|
|
import (
|
|
"errors"
|
|
"math/rand"
|
|
"strconv"
|
|
"sync"
|
|
|
|
"github.com/hashicorp/go-set/v3"
|
|
"github.com/hashicorp/nomad/helper"
|
|
)
|
|
|
|
var (
|
|
ErrPoolExhausted = errors.New("users: uid/gid pool exhausted")
|
|
ErrReleaseUnused = errors.New("users: release of unused uid/gid")
|
|
ErrCannotParse = errors.New("users: unable to parse uid/gid from username")
|
|
)
|
|
|
|
// none indicates no dynamic user
|
|
const none = 0
|
|
|
|
// doNotEnable indicates functionality should be disabled
|
|
const doNotEnable = -1
|
|
|
|
// A UGID is a combination User (UID) and Group (GID). Since Nomad is
|
|
// allocating these values together from the same pool it can ensure they are
|
|
// always matching values, thus encoding them with one value.
|
|
type UGID int
|
|
|
|
// String returns the string representation of a UGID.
|
|
//
|
|
// It's just the numbers.
|
|
func (id UGID) String() string {
|
|
return strconv.Itoa(int(id))
|
|
}
|
|
|
|
// A Pool is used to manage a reserved set of UID/GID values. A UGID can be
|
|
// acquired from the pool and released back to the pool. To support client
|
|
// restarts, specific UGIDs can be marked as in-use and can later be released
|
|
// back into the pool.
|
|
type Pool interface {
|
|
// Restore a UGID currently in use by a Task during a Nomad client restore.
|
|
Restore(UGID)
|
|
|
|
// Acquire returns a UGID that is not currently in use.
|
|
Acquire() (UGID, error)
|
|
|
|
// Release returns a UGID no longer being used into the pool.
|
|
Release(UGID) error
|
|
}
|
|
|
|
// PoolConfig contains options for creating a new Pool.
|
|
type PoolConfig struct {
|
|
// MinUGID is the minimum value for a UGID allocated from the pool.
|
|
MinUGID int
|
|
|
|
// MaxUGID is the maximum value for a UGID allocated from the pool.
|
|
MaxUGID int
|
|
}
|
|
|
|
// disable will return true if either min or max is set to Disable (-1),
|
|
// indicating the client should not enable the dynamic workload users
|
|
// functionality
|
|
func (p *PoolConfig) disable() bool {
|
|
return p.MinUGID == doNotEnable || p.MaxUGID == doNotEnable
|
|
}
|
|
|
|
// New creates a Pool with the given PoolConfig options.
|
|
func New(opts *PoolConfig) Pool {
|
|
if opts == nil {
|
|
panic("bug: users pool cannot be nil")
|
|
}
|
|
if opts.disable() {
|
|
return new(noopPool)
|
|
}
|
|
if opts.MinUGID < 0 {
|
|
panic("bug: users pool min must be >= 0")
|
|
}
|
|
if opts.MaxUGID < opts.MinUGID {
|
|
panic("bug: users pool max must be >= min")
|
|
}
|
|
// a small but reasonable number of tasks to expect
|
|
const defaultPoolCapacity = 32
|
|
return &pool{
|
|
min: UGID(opts.MinUGID),
|
|
max: UGID(opts.MaxUGID),
|
|
lock: new(sync.Mutex),
|
|
used: set.New[UGID](defaultPoolCapacity),
|
|
}
|
|
}
|
|
|
|
// noopPool is an implementation of Pool that does not allow acquiring ugids
|
|
type noopPool struct{}
|
|
|
|
func (*noopPool) Restore(UGID) {}
|
|
func (*noopPool) Acquire() (UGID, error) {
|
|
return 0, errors.New("dynamic workload users disabled")
|
|
}
|
|
func (*noopPool) Release(UGID) error {
|
|
// avoid giving an error if a client is restarted with a new config
|
|
// that disables dynamic workload users but still has a task running
|
|
// making use of one
|
|
return nil
|
|
}
|
|
|
|
type pool struct {
|
|
min UGID
|
|
max UGID
|
|
|
|
lock *sync.Mutex
|
|
used *set.Set[UGID]
|
|
}
|
|
|
|
func (p *pool) Restore(id UGID) {
|
|
helper.WithLock(p.lock, func() {
|
|
p.used.Insert(id)
|
|
})
|
|
}
|
|
|
|
func (p *pool) Acquire() (UGID, error) {
|
|
p.lock.Lock()
|
|
defer p.lock.Unlock()
|
|
|
|
// optimize the case where the pool is exhausted
|
|
if p.used.Size() == int((p.max-p.min)+1) {
|
|
return none, ErrPoolExhausted
|
|
}
|
|
|
|
// attempt to select a random ugid
|
|
if id := p.random(); id != none {
|
|
return id, nil
|
|
}
|
|
|
|
// slow case where we iterate each id looking for one that is not used
|
|
for id := p.min; id <= p.max; id++ {
|
|
if !p.used.Contains(id) {
|
|
p.used.Insert(id)
|
|
return id, nil
|
|
}
|
|
}
|
|
|
|
// we checked for this case up top; if we get here there is a bug
|
|
panic("bug: pool exhausted")
|
|
}
|
|
|
|
// random will attempt to select a random UGID from the pool
|
|
func (p *pool) random() UGID {
|
|
// make up to 10 attempts to find a random unused UGID
|
|
// if all 10 attempts fail, return the sentinel indicating as much
|
|
const maxAttempts = 10
|
|
size := int64(p.max - p.min)
|
|
tries := int(min(maxAttempts, size))
|
|
for attempt := 0; attempt < tries; attempt++ {
|
|
id := UGID(rand.Int63n(size)) + p.min
|
|
if p.used.Insert(id) {
|
|
return id
|
|
}
|
|
}
|
|
return none
|
|
}
|
|
|
|
func (p *pool) Release(id UGID) error {
|
|
p.lock.Lock()
|
|
defer p.lock.Unlock()
|
|
|
|
if !p.used.Remove(id) {
|
|
return ErrReleaseUnused
|
|
}
|
|
|
|
return nil
|
|
}
|