Skip to content

Commit 55523ec

Browse files
tehuttgross
andauthored
Add NodeMaxAllocations to client configuration (#25785)
* Set MaxAllocations in client config Add NodeAllocationTracker struct to Node struct Evaluate MaxAllocations in AllocsFit function Set up cli config parsing Integrate maxAllocs into AllocatedResources view Co-authored-by: Tim Gross <[email protected]> --------- Co-authored-by: Tim Gross <[email protected]>
1 parent 15c01e5 commit 55523ec

File tree

18 files changed

+311
-17
lines changed

18 files changed

+311
-17
lines changed

.changelog/25785.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
```release-note:improvement
2+
client: add ability to set maximum allocation count by adding node_max_allocs to client configuration
3+
```

api/nodes.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -572,6 +572,7 @@ type Node struct {
572572
LastDrain *DrainMetadata
573573
CreateIndex uint64
574574
ModifyIndex uint64
575+
NodeMaxAllocs int
575576
}
576577

577578
type NodeResources struct {

client/client.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1628,6 +1628,8 @@ func (c *Client) setupNode() error {
16281628
if _, ok := node.Meta[envoy.DefaultTransparentProxyOutboundPortParam]; !ok {
16291629
node.Meta[envoy.DefaultTransparentProxyOutboundPortParam] = envoy.DefaultTransparentProxyOutboundPort
16301630
}
1631+
// Set NodeMaxAllocs before dynamic configuration is set
1632+
node.NodeMaxAllocs = newConfig.NodeMaxAllocs
16311633

16321634
// Since node.Meta will get dynamic metadata merged in, save static metadata
16331635
// here.

client/config/config.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -375,6 +375,10 @@ type Config struct {
375375

376376
// ExtraAllocHooks are run with other allocation hooks, mainly for testing.
377377
ExtraAllocHooks []interfaces.RunnerHook
378+
379+
// NodeMaxAllocs is an optional field that sets the maximum number of
380+
// allocations a node can be assigned. Defaults to 0 and ignored if unset.
381+
NodeMaxAllocs int
378382
}
379383

380384
type APIListenerRegistrar interface {

command/agent/agent.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -787,6 +787,7 @@ func convertClientConfig(agentConfig *Config) (*clientconfig.Config, error) {
787787
if agentConfig.Client.NetworkInterface != "" {
788788
conf.NetworkInterface = agentConfig.Client.NetworkInterface
789789
}
790+
conf.NodeMaxAllocs = agentConfig.Client.NodeMaxAllocs
790791

791792
// handle rpc yamux configuration
792793
conf.RPCSessionConfig = yamux.DefaultConfig()

command/agent/config.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -415,6 +415,10 @@ type ClientConfig struct {
415415

416416
// ExtraKeysHCL is used by hcl to surface unexpected keys
417417
ExtraKeysHCL []string `hcl:",unusedKeys" json:"-"`
418+
419+
// NodeMaxAllocs sets the maximum number of allocations per node
420+
// Defaults to 0 and ignored if unset.
421+
NodeMaxAllocs int `hcl:"node_max_allocs"`
418422
}
419423

420424
func (c *ClientConfig) Copy() *ClientConfig {
@@ -2652,6 +2656,9 @@ func (a *ClientConfig) Merge(b *ClientConfig) *ClientConfig {
26522656
result.Drain = a.Drain.Merge(b.Drain)
26532657
result.Users = a.Users.Merge(b.Users)
26542658

2659+
if b.NodeMaxAllocs != 0 {
2660+
result.NodeMaxAllocs = b.NodeMaxAllocs
2661+
}
26552662
return &result
26562663
}
26572664

command/agent/config_parse_test.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -696,7 +696,10 @@ var sample0 = &Config{
696696
RPC: "host.example.com",
697697
Serf: "host.example.com",
698698
},
699-
Client: &ClientConfig{ServerJoin: &ServerJoin{}},
699+
Client: &ClientConfig{
700+
ServerJoin: &ServerJoin{},
701+
NodeMaxAllocs: 5,
702+
},
700703
Server: &ServerConfig{
701704
Enabled: true,
702705
BootstrapExpect: 3,

command/agent/config_test.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1872,3 +1872,27 @@ func Test_mergeKEKProviderConfigs(t *testing.T) {
18721872
},
18731873
}, result)
18741874
}
1875+
1876+
func TestConfig_LoadClientNodeMaxAllocs(t *testing.T) {
1877+
ci.Parallel(t)
1878+
testCases := []struct {
1879+
fileName string
1880+
}{
1881+
{
1882+
fileName: "test-resources/client_with_maxallocs.hcl",
1883+
},
1884+
{
1885+
fileName: "test-resources/client_with_maxallocs.json",
1886+
},
1887+
}
1888+
for _, tc := range testCases {
1889+
t.Run("minimal client expect defaults", func(t *testing.T) {
1890+
defaultConfig := DefaultConfig()
1891+
agentConfig, err := LoadConfig(tc.fileName)
1892+
must.NoError(t, err)
1893+
agentConfig = defaultConfig.Merge(agentConfig)
1894+
must.Eq(t, 5, agentConfig.Client.NodeMaxAllocs)
1895+
})
1896+
}
1897+
1898+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# Copyright (c) HashiCorp, Inc.
2+
# SPDX-License-Identifier: BUSL-1.1
3+
4+
client {
5+
enabled = true
6+
node_max_allocs = 5
7+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"client": {
3+
"enabled": true,
4+
"node_max_allocs": 5
5+
}
6+
}

command/agent/testdata/sample0.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,9 @@
4545
"client_auto_join": false,
4646
"token": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"
4747
},
48+
"client": {
49+
"node_max_allocs": 5
50+
},
4851
"data_dir": "/opt/data/nomad/data",
4952
"datacenter": "dc1",
5053
"enable_syslog": true,

command/node_status.go

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -954,16 +954,21 @@ func getAllocatedResources(client *api.Client, runningAllocs []*api.Allocation,
954954
mem += *alloc.Resources.MemoryMB
955955
disk += *alloc.Resources.DiskMB
956956
}
957+
allocCount := strconv.Itoa(len(runningAllocs))
957958

959+
if node.NodeMaxAllocs != 0 {
960+
allocCount = fmt.Sprintf("%d/%d", len(runningAllocs), node.NodeMaxAllocs)
961+
}
958962
resources := make([]string, 2)
959-
resources[0] = "CPU|Memory|Disk"
960-
resources[1] = fmt.Sprintf("%d/%d MHz|%s/%s|%s/%s",
963+
resources[0] = "CPU|Memory|Disk|Alloc Count"
964+
resources[1] = fmt.Sprintf("%d/%d MHz|%s/%s|%s/%s|%s",
961965
cpu,
962966
*total.CPU,
963967
humanize.IBytes(uint64(mem*bytesPerMegabyte)),
964968
humanize.IBytes(uint64(*total.MemoryMB*bytesPerMegabyte)),
965969
humanize.IBytes(uint64(disk*bytesPerMegabyte)),
966-
humanize.IBytes(uint64(*total.DiskMB*bytesPerMegabyte)))
970+
humanize.IBytes(uint64(*total.DiskMB*bytesPerMegabyte)),
971+
allocCount)
967972

968973
return resources
969974
}

nomad/structs/funcs.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,11 @@ func (a TerminalByNodeByName) Get(nodeID, name string) (*Allocation, bool) {
141141
func AllocsFit(node *Node, allocs []*Allocation, netIdx *NetworkIndex, checkDevices bool) (bool, string, *ComparableResources, error) {
142142
// Compute the allocs' utilization from zero
143143
used := new(ComparableResources)
144-
144+
if node.NodeMaxAllocs != 0 {
145+
if node.NodeMaxAllocs < len(allocs) {
146+
return false, "max allocation exceeded", used, fmt.Errorf("plan exceeds max allocation")
147+
}
148+
}
145149
reservedCores := map[uint16]struct{}{}
146150
var coreOverlap bool
147151

nomad/structs/funcs_test.go

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -716,6 +716,81 @@ func TestScoreFitBinPack(t *testing.T) {
716716
}
717717
}
718718

719+
func TestAllocsFit_MaxNodeAllocs(t *testing.T) {
720+
ci.Parallel(t)
721+
baseAlloc := &Allocation{
722+
AllocatedResources: &AllocatedResources{
723+
Tasks: map[string]*AllocatedTaskResources{
724+
"web": {
725+
Cpu: AllocatedCpuResources{
726+
CpuShares: 1000,
727+
ReservedCores: []uint16{},
728+
},
729+
Memory: AllocatedMemoryResources{
730+
MemoryMB: 1024,
731+
},
732+
},
733+
},
734+
Shared: AllocatedSharedResources{
735+
DiskMB: 5000,
736+
Networks: Networks{
737+
{
738+
Mode: "host",
739+
IP: "10.0.0.1",
740+
ReservedPorts: []Port{{Label: "main", Value: 8000}},
741+
},
742+
},
743+
Ports: AllocatedPorts{
744+
{
745+
Label: "main",
746+
Value: 8000,
747+
HostIP: "10.0.0.1",
748+
},
749+
},
750+
},
751+
},
752+
}
753+
754+
testCases := []struct {
755+
name string
756+
allocations []*Allocation
757+
expectErr bool
758+
maxAllocs int
759+
}{
760+
{
761+
name: "happy_path",
762+
allocations: []*Allocation{baseAlloc},
763+
expectErr: false,
764+
maxAllocs: 2,
765+
},
766+
{
767+
name: "too many allocs",
768+
allocations: []*Allocation{baseAlloc, baseAlloc, baseAlloc},
769+
expectErr: true,
770+
maxAllocs: 2,
771+
},
772+
}
773+
774+
for _, tc := range testCases {
775+
t.Run(tc.name, func(t *testing.T) {
776+
n := node2k()
777+
n.NodeMaxAllocs = tc.maxAllocs
778+
fit, dim, used, err := AllocsFit(n, tc.allocations, nil, false)
779+
if !tc.expectErr {
780+
must.NoError(t, err)
781+
must.True(t, fit)
782+
must.Eq(t, 1000, used.Flattened.Cpu.CpuShares)
783+
must.Eq(t, 1024, used.Flattened.Memory.MemoryMB)
784+
} else {
785+
must.False(t, fit)
786+
must.StrContains(t, dim, "max allocation exceeded")
787+
must.ErrorContains(t, err, "plan exceeds max allocation")
788+
must.Eq(t, 0, used.Flattened.Cpu.CpuShares)
789+
must.Eq(t, 0, used.Flattened.Memory.MemoryMB)
790+
}
791+
})
792+
}
793+
}
719794
func TestACLPolicyListHash(t *testing.T) {
720795
ci.Parallel(t)
721796

nomad/structs/structs.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2163,6 +2163,9 @@ type Node struct {
21632163
// LastDrain contains metadata about the most recent drain operation
21642164
LastDrain *DrainMetadata
21652165

2166+
// NodeMaxAllocs defaults to 0 unless set in the client config
2167+
NodeMaxAllocs int
2168+
21662169
// LastMissedHeartbeatIndex stores the Raft index when the node last missed
21672170
// a heartbeat. It resets to zero once the node is marked as ready again.
21682171
LastMissedHeartbeatIndex uint64
@@ -2325,7 +2328,6 @@ func (n *Node) HasEvent(msg string) bool {
23252328

23262329
// Stub returns a summarized version of the node
23272330
func (n *Node) Stub(fields *NodeStubFields) *NodeListStub {
2328-
23292331
addr, _, _ := net.SplitHostPort(n.HTTPAddr)
23302332

23312333
s := &NodeListStub{

0 commit comments

Comments
 (0)