Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

dynamic host volumes quotas (CE) #24871

Merged
merged 1 commit into from
Jan 17, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
}
Loading