mirror of
https://github.com/kemko/nomad.git
synced 2026-01-01 16:05:42 +03:00
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:
@@ -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
|
||||
|
||||
@@ -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
32
client/config/users.go
Normal 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,
|
||||
}
|
||||
}
|
||||
59
client/config/users_test.go
Normal file
59
client/config/users_test.go
Normal 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)
|
||||
}
|
||||
@@ -902,6 +902,8 @@ func convertClientConfig(agentConfig *Config) (*clientconfig.Config, error) {
|
||||
}
|
||||
conf.Drain = drainConfig
|
||||
|
||||
conf.Users = clientconfig.UsersConfigFromAgent(agentConfig.Client.Users)
|
||||
|
||||
return conf, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
100
nomad/structs/config/users.go
Normal file
100
nomad/structs/config/users.go
Normal 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),
|
||||
}
|
||||
}
|
||||
139
nomad/structs/config/users_test.go
Normal file
139
nomad/structs/config/users_test.go
Normal 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)
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user