Skip to content

Commit

Permalink
dynamic host volumes quotas
Browse files Browse the repository at this point in the history
Allow users to configure a host volumes quota in MB. This will be enforced at
the time of provisioning via create/register RPCs. This changeset is the CE
version of ENT/2114.

Ref: hashicorp/nomad-enterprise#2114
Ref: https://hashicorp.atlassian.net/browse/NET-11549
  • Loading branch information
tgross committed Jan 15, 2025
1 parent 46bd0b1 commit bc0a119
Show file tree
Hide file tree
Showing 6 changed files with 120 additions and 12 deletions.
5 changes: 5 additions & 0 deletions api/quota.go
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,11 @@ type QuotaStorageResources struct {
// Variable.EncryptedData, in megabytes (2^20 bytes). A value of zero is
// treated as unlimited and a negative value is treated as fully disallowed.
VariablesMB int `hcl:"variables"`

// HostVolumesMB is the maximum provisioned size of all dynamic host
// volumes, in megabytes (2^20 bytes). A value of zero is treated as
// unlimited and a negative value is treated as fully disallowed.
HostVolumesMB int `hcl:"host_volumes"`
}

// QuotaUsage is the resource usage of a Quota
Expand Down
40 changes: 33 additions & 7 deletions command/quota_apply.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"os"
"strings"

humanize "github.com/dustin/go-humanize"
multierror "github.com/hashicorp/go-multierror"
"github.com/hashicorp/hcl"
"github.com/hashicorp/hcl/hcl/ast"
Expand Down Expand Up @@ -319,19 +320,44 @@ func parseStorageResource(storageBlocks *ast.ObjectList) (*api.QuotaStorageResou
return nil, errors.New("only one storage block is allowed")
}
block := storageBlocks.Items[0]
valid := []string{"variables"}
valid := []string{"variables", "host_volumes"}
if err := helper.CheckHCLKeys(block.Val, valid); err != nil {
return nil, err
}
var storage api.QuotaStorageResources
var m map[string]interface{}
if err := hcl.DecodeObject(&storage, block.Val); err != nil {

var m map[string]any
if err := hcl.DecodeObject(&m, block.Val); err != nil {
return nil, err
}
if err := mapstructure.WeakDecode(m, &storage); err != nil {
return nil, err

variablesLimit, err := parseQuotaMegabytes(m["variables"])
if err != nil {
return nil, fmt.Errorf("invalid variables limit: %v", err)
}
hostVolumesLimit, err := parseQuotaMegabytes(m["host_volumes"])
if err != nil {
return nil, fmt.Errorf("invalid host_volumes limit: %v", err)
}

return &api.QuotaStorageResources{
VariablesMB: variablesLimit,
HostVolumesMB: hostVolumesLimit,
}, nil
}

func parseQuotaMegabytes(raw any) (int, error) {
switch val := raw.(type) {
case string:
b, err := humanize.ParseBytes(val)
if err != nil {
return 0, fmt.Errorf("could not parse value as bytes: %v", err)
}
return int(b >> 20), nil
case int:
return val, nil
default:
return 0, fmt.Errorf("invalid type %T", raw)
}
return &storage, nil
}

func parseDeviceResource(result *[]*api.RequestedDevice, list *ast.ObjectList) error {
Expand Down
53 changes: 53 additions & 0 deletions command/quota_apply_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@ import (
"testing"

"github.com/hashicorp/cli"
"github.com/hashicorp/nomad/api"
"github.com/hashicorp/nomad/ci"
"github.com/hashicorp/nomad/helper/pointer"
"github.com/shoenig/test/must"
)

func TestQuotaApplyCommand_Implements(t *testing.T) {
Expand Down Expand Up @@ -38,3 +41,53 @@ func TestQuotaApplyCommand_Fails(t *testing.T) {
}
ui.ErrorWriter.Reset()
}

func TestQuotaParse(t *testing.T) {

in := []byte(`
name = "default-quota"
description = "Limit the shared default namespace"
limit {
region = "global"
region_limit {
cores = 0
cpu = 2500
memory = 1000
memory_max = 1000
device "nvidia/gpu/1080ti" {
count = 1
}
storage {
variables = 1000 # in MB
host_volumes = "100 GiB"
}
}
}
`)

spec, err := parseQuotaSpec(in)
must.NoError(t, err)

must.Eq(t, &api.QuotaSpec{
Name: "default-quota",
Description: "Limit the shared default namespace",
Limits: []*api.QuotaLimit{{
Region: "global",
RegionLimit: &api.QuotaResources{
CPU: pointer.Of(2500),
Cores: pointer.Of(0),
MemoryMB: pointer.Of(1000),
MemoryMaxMB: pointer.Of(1000),
Devices: []*api.RequestedDevice{{
Name: "nvidia/gpu/1080ti",
Count: pointer.Of(uint64(1)),
}},
Storage: &api.QuotaStorageResources{
VariablesMB: 1000,
HostVolumesMB: 102_400,
},
},
}},
}, spec)
}
10 changes: 6 additions & 4 deletions command/quota_init.go
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,8 @@ limit {
count = 1
}
storage {
variables = 1000
variables = 1000 # in MB
host_volumes = 100000 # in MB
}
}
}
Expand All @@ -151,9 +152,10 @@ var defaultJsonQuotaSpec = strings.TrimSpace(`
"Count": 1
}
],
"Storage": {
"Variables": 1000
}
"Storage": {
"Variables": 1000,
"HostVolumes": 100000
}
}
}
]
Expand Down
8 changes: 7 additions & 1 deletion nomad/state/state_store_host_volumes.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,14 +66,20 @@ func (s *StateStore) UpsertHostVolume(index uint64, vol *structs.HostVolume) err
if err != nil {
return err
}
var old *structs.HostVolume
if obj != nil {
old := obj.(*structs.HostVolume)
old = obj.(*structs.HostVolume)
vol.CreateIndex = old.CreateIndex
vol.CreateTime = old.CreateTime
} else {
vol.CreateIndex = index
}

err = s.enforceHostVolumeQuotaTxn(txn, index, vol, old, true)
if err != nil {
return err
}

// If the fingerprint is written from the node before the create RPC handler
// completes, we'll never update from the initial pending, so reconcile that
// here
Expand Down
16 changes: 16 additions & 0 deletions nomad/state/state_store_host_volumes_ce.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1

//go:build !ent

package state

import "github.com/hashicorp/nomad/nomad/structs"

func (s *StateStore) EnforceHostVolumeQuota(_ *structs.HostVolume, _ *structs.HostVolume) error {
return nil
}

func (s *StateStore) enforceHostVolumeQuotaTxn(_ Txn, _ uint64, _ *structs.HostVolume, _ *structs.HostVolume, _ bool) error {
return nil
}

0 comments on commit bc0a119

Please sign in to comment.