exec2: add a client.users configuration block (#20093)

* exec: add a client.users configuration block

For now just add min/max dynamic user values; soon we can also absorb
the "user.denylist" and "user.checked_drivers" options from the
deprecated client.options map.

* give the no-op pool implementation a better name

* use explicit error types to make referencing them cleaner in tests

* use import alias to not shadow package name
This commit is contained in:
Seth Hoenig
2024-03-08 16:02:32 -06:00
committed by GitHub
parent 26a27bb12c
commit 286dce7a2a
10 changed files with 376 additions and 4 deletions

View File

@@ -477,8 +477,8 @@ func NewClient(cfg *config.Config, consulCatalog consul.CatalogAPI, consulProxie
// Create the dynamic workload users pool
c.users = dynamic.New(&dynamic.PoolConfig{
MinUGID: 80_000, // TODO(shoenig) plumb client config
MaxUGID: 89_999, // TODO(shoenig) plumb client config
MinUGID: cfg.Users.MinDynamicUser,
MaxUGID: cfg.Users.MaxDynamicUser,
})
// Create the cpu core partition manager

View File

@@ -328,6 +328,9 @@ type Config struct {
// Drain configuration from the agent's config file.
Drain *DrainConfig
// Uesrs configuration from the agent's config file.
Users *UsersConfig
// ExtraAllocHooks are run with other allocation hooks, mainly for testing.
ExtraAllocHooks []interfaces.RunnerHook
}
@@ -757,6 +760,7 @@ func (c *Config) Copy() *Config {
nc.TemplateConfig = c.TemplateConfig.Copy()
nc.ReservableCores = slices.Clone(c.ReservableCores)
nc.Artifact = c.Artifact.Copy()
nc.Users = c.Users.Copy()
return &nc
}
@@ -805,6 +809,10 @@ func DefaultConfig() *Config {
CgroupParent: "nomad.slice", // SETH todo
MaxDynamicPort: structs.DefaultMinDynamicPort,
MinDynamicPort: structs.DefaultMaxDynamicPort,
Users: &UsersConfig{
MinDynamicUser: 80_000,
MaxDynamicUser: 89_999,
},
}
return cfg

32
client/config/users.go Normal file
View File

@@ -0,0 +1,32 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package config
import sconfig "github.com/hashicorp/nomad/nomad/structs/config"
// UsersConfig configures things related to operating system users.
type UsersConfig struct {
// MinDynamicUser is the lowest uid/gid for use in the dynamic users pool.
MinDynamicUser int
// MaxDynamicUser is the highest uid/gid for use in the dynamic users pool.
MaxDynamicUser int
}
func UsersConfigFromAgent(c *sconfig.UsersConfig) *UsersConfig {
return &UsersConfig{
MinDynamicUser: *c.MinDynamicUser,
MaxDynamicUser: *c.MaxDynamicUser,
}
}
func (u *UsersConfig) Copy() *UsersConfig {
if u == nil {
return nil
}
return &UsersConfig{
MinDynamicUser: u.MinDynamicUser,
MaxDynamicUser: u.MaxDynamicUser,
}
}

View File

@@ -0,0 +1,59 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package config
import (
"testing"
"github.com/hashicorp/nomad/ci"
"github.com/hashicorp/nomad/nomad/structs/config"
"github.com/shoenig/test/must"
)
func TestUsersConfigFromAgent(t *testing.T) {
ci.Parallel(t)
cases := []struct {
name string
config *config.UsersConfig
exp *UsersConfig
}{
{
name: "from default",
config: config.DefaultUsersConfig(),
exp: &UsersConfig{
MinDynamicUser: 80_000,
MaxDynamicUser: 89_999,
},
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
got := UsersConfigFromAgent(tc.config)
must.Eq(t, tc.exp, got)
})
}
}
func TestUsersConfig_Copy(t *testing.T) {
ci.Parallel(t)
orig := &UsersConfig{
MinDynamicUser: 70100,
MaxDynamicUser: 70200,
}
configCopy := orig.Copy()
must.Eq(t, orig, configCopy)
// modify copy and make sure original does not change
configCopy.MinDynamicUser = 100
configCopy.MaxDynamicUser = 200
must.Eq(t, &UsersConfig{
MinDynamicUser: 70100,
MaxDynamicUser: 70200,
}, orig)
}

View File

@@ -902,6 +902,8 @@ func convertClientConfig(agentConfig *Config) (*clientconfig.Config, error) {
}
conf.Drain = drainConfig
conf.Users = clientconfig.UsersConfigFromAgent(agentConfig.Client.Users)
return conf, nil
}

View File

@@ -380,6 +380,9 @@ type ClientConfig struct {
// Drain specifies whether to drain the client on shutdown; ignored in dev mode.
Drain *config.DrainConfig `hcl:"drain_on_shutdown"`
// Users is used to configure parameters around operating system users.
Users *config.UsersConfig `hcl:"users"`
// ExtraKeysHCL is used by hcl to surface unexpected keys
ExtraKeysHCL []string `hcl:",unusedKeys" json:"-"`
}
@@ -403,6 +406,7 @@ func (c *ClientConfig) Copy() *ClientConfig {
nc.NomadServiceDiscovery = pointer.Copy(c.NomadServiceDiscovery)
nc.Artifact = c.Artifact.Copy()
nc.Drain = c.Drain.Copy()
nc.Users = c.Users.Copy()
nc.ExtraKeysHCL = slices.Clone(c.ExtraKeysHCL)
return &nc
}
@@ -1356,6 +1360,7 @@ func DefaultConfig() *Config {
NomadServiceDiscovery: pointer.Of(true),
Artifact: config.DefaultArtifactConfig(),
Drain: nil,
Users: config.DefaultUsersConfig(),
},
Server: &ServerConfig{
Enabled: false,
@@ -2361,6 +2366,7 @@ func (a *ClientConfig) Merge(b *ClientConfig) *ClientConfig {
result.Artifact = a.Artifact.Merge(b.Artifact)
result.Drain = a.Drain.Merge(b.Drain)
result.Users = a.Users.Merge(b.Users)
return &result
}

View File

@@ -24,6 +24,9 @@ var (
// 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.
@@ -60,11 +63,21 @@ type PoolConfig struct {
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")
}
@@ -81,6 +94,20 @@ func New(opts *PoolConfig) Pool {
}
}
// 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

View File

@@ -418,8 +418,7 @@ func TestArtifactConfig_Validate(t *testing.T) {
err := a.Validate()
if tc.expErr != "" {
must.Error(t, err)
must.StrContains(t, err.Error(), tc.expErr)
must.ErrorContains(t, err, tc.expErr)
} else {
must.NoError(t, err)
}

View File

@@ -0,0 +1,100 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package config
import (
"errors"
"github.com/hashicorp/nomad/helper/pointer"
)
// UsersConfig configures things related to operating system users.
type UsersConfig struct {
// MinDynamicUser is the lowest uid/gid for use in the dynamic users pool.
MinDynamicUser *int `hcl:"dynamic_user_min"`
// MaxDynamicUser is the highest uid/gid for use in the dynamic users pool.
MaxDynamicUser *int `hcl:"dynamic_user_max"`
}
// Copy returns a deep copy of the Users struct.
func (u *UsersConfig) Copy() *UsersConfig {
if u == nil {
return nil
}
return &UsersConfig{
MinDynamicUser: pointer.Copy(u.MinDynamicUser),
MaxDynamicUser: pointer.Copy(u.MaxDynamicUser),
}
}
// Merge returns a new Users where non-empty/nil fields in the argument have
// higher precedence.
func (u *UsersConfig) Merge(o *UsersConfig) *UsersConfig {
switch {
case u == nil:
return o.Copy()
case o == nil:
return u.Copy()
default:
return &UsersConfig{
MinDynamicUser: pointer.Merge(u.MinDynamicUser, o.MinDynamicUser),
MaxDynamicUser: pointer.Merge(u.MaxDynamicUser, o.MaxDynamicUser),
}
}
}
// Equal returns whether u and o are the same.
func (u *UsersConfig) Equal(o *UsersConfig) bool {
if u == nil || o == nil {
return u == o
}
switch {
case !pointer.Eq(u.MinDynamicUser, o.MinDynamicUser):
return false
case !pointer.Eq(u.MaxDynamicUser, o.MaxDynamicUser):
return false
default:
return true
}
}
var (
errUsersUnset = errors.New("users must not be nil")
errDynamicUserMinUnset = errors.New("dynamic_user_min must be set")
errDynamicUserMinInvalid = errors.New("dynamic_user_min must not be negative")
errDynamicUserMaxUnset = errors.New("dynamic_user_max must be set")
errDynamicUserMaxInvalid = errors.New("dynamic_user_max must not be negative")
)
// Validate whether UsersConfig is valid.
//
// Note that -1 is a valid value for min/max dynamic users, as this is used
// to indicate the dynamic workload users feature should be disabled.
func (u *UsersConfig) Validate() error {
if u == nil {
return errUsersUnset
}
if u.MinDynamicUser == nil {
return errDynamicUserMinUnset
}
if *u.MinDynamicUser < -1 {
return errDynamicUserMinInvalid
}
if u.MaxDynamicUser == nil {
return errDynamicUserMaxUnset
}
if *u.MaxDynamicUser < -1 {
return errDynamicUserMaxInvalid
}
return nil
}
// DefaultUsersConfig returns the default users configuration.
func DefaultUsersConfig() *UsersConfig {
return &UsersConfig{
MinDynamicUser: pointer.Of(80_000),
MaxDynamicUser: pointer.Of(89_999),
}
}

View File

@@ -0,0 +1,139 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package config
import (
"testing"
"github.com/hashicorp/nomad/ci"
"github.com/hashicorp/nomad/helper/pointer"
"github.com/shoenig/test/must"
)
func TestUsersConfig_Copy(t *testing.T) {
ci.Parallel(t)
a := DefaultUsersConfig()
b := a.Copy()
must.Equal(t, a, b)
must.Equal(t, b, a)
a.MaxDynamicUser = pointer.Of(1000)
must.NotEqual(t, a, b)
must.NotEqual(t, b, a)
}
func TestUsersConfig_Merge(t *testing.T) {
ci.Parallel(t)
cases := []struct {
name string
source *UsersConfig
other *UsersConfig
exp *UsersConfig
}{
{
name: "merge all fields",
source: &UsersConfig{
MinDynamicUser: pointer.Of(100),
MaxDynamicUser: pointer.Of(200),
},
other: &UsersConfig{
MinDynamicUser: pointer.Of(3000),
MaxDynamicUser: pointer.Of(4000),
},
exp: &UsersConfig{
MinDynamicUser: pointer.Of(3000),
MaxDynamicUser: pointer.Of(4000),
},
},
{
name: "null source",
source: nil,
other: &UsersConfig{
MinDynamicUser: pointer.Of(100),
MaxDynamicUser: pointer.Of(200),
},
exp: &UsersConfig{
MinDynamicUser: pointer.Of(100),
MaxDynamicUser: pointer.Of(200),
},
},
{
name: "null other",
other: nil,
source: &UsersConfig{
MinDynamicUser: pointer.Of(100),
MaxDynamicUser: pointer.Of(200),
},
exp: &UsersConfig{
MinDynamicUser: pointer.Of(100),
MaxDynamicUser: pointer.Of(200),
},
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
got := tc.source.Merge(tc.other)
must.Equal(t, tc.exp, got)
})
}
}
func TestUsersConfig_Validate(t *testing.T) {
ci.Parallel(t)
// default config should be valid of course
must.NoError(t, DefaultUsersConfig().Validate())
// nil config is not valid
must.ErrorIs(t, ((*UsersConfig)(nil)).Validate(), errUsersUnset)
cases := []struct {
name string
modify func(*UsersConfig)
exp error
}{
{
name: "min dynamic user not set",
modify: func(u *UsersConfig) {
u.MinDynamicUser = nil
},
exp: errDynamicUserMinUnset,
},
{
name: "min dynamic user not valid",
modify: func(u *UsersConfig) {
u.MinDynamicUser = pointer.Of(-2)
},
exp: errDynamicUserMinInvalid,
},
{
name: "max dynamic user not set",
modify: func(u *UsersConfig) {
u.MaxDynamicUser = nil
},
exp: errDynamicUserMaxUnset,
},
{
name: "max dynamic user not valid",
modify: func(u *UsersConfig) {
u.MaxDynamicUser = pointer.Of(-2)
},
exp: errDynamicUserMaxInvalid,
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
u := DefaultUsersConfig()
if tc.modify != nil {
tc.modify(u)
}
err := u.Validate()
must.ErrorIs(t, err, tc.exp)
})
}
}