diff --git a/nomad/mock/mock.go b/nomad/mock/mock.go index 7779d06a4..cf3126b74 100644 --- a/nomad/mock/mock.go +++ b/nomad/mock/mock.go @@ -2308,13 +2308,12 @@ func SecureVariable() *structs.SecureVariableDecrypted { env := envs[envIdx] domain := fake.DomainName() path := strings.ReplaceAll(env+"."+domain, ".", "/") - // owner := fake.Person() createIdx := uint64(rand.Intn(100) + 100) createDT := fake.DateRange(time.Now().AddDate(0, -1, 0), time.Now()) sv := &structs.SecureVariableDecrypted{ SecureVariableMetadata: structs.SecureVariableMetadata{ Path: path, - Namespace: "default", + Namespace: structs.DefaultNamespace, CreateIndex: createIdx, ModifyIndex: createIdx, CreateTime: createDT, @@ -2393,7 +2392,7 @@ func SecureVariableEncrypted() *structs.SecureVariableEncrypted { sv := &structs.SecureVariableEncrypted{ SecureVariableMetadata: structs.SecureVariableMetadata{ Path: path, - Namespace: "default", + Namespace: structs.DefaultNamespace, CreateIndex: createIdx, ModifyIndex: createIdx, CreateTime: createDT, diff --git a/nomad/secure_variables_endpoint.go b/nomad/secure_variables_endpoint.go index e114c6ea2..7f42d9585 100644 --- a/nomad/secure_variables_endpoint.go +++ b/nomad/secure_variables_endpoint.go @@ -76,6 +76,10 @@ func (sv *SecureVariables) Upsert( return err } + if err := sv.enforceQuota(uArgs); err != nil { + return err + } + // Update via Raft. out, index, err := sv.srv.raftApply(structs.SecureVariableUpsertRequestType, uArgs) if err != nil { diff --git a/nomad/secure_variables_endpoint_oss.go b/nomad/secure_variables_endpoint_oss.go new file mode 100644 index 000000000..46c88aeda --- /dev/null +++ b/nomad/secure_variables_endpoint_oss.go @@ -0,0 +1,10 @@ +//go:build !ent +// +build !ent + +package nomad + +import "github.com/hashicorp/nomad/nomad/structs" + +func (sv *SecureVariables) enforceQuota(uArgs structs.SecureVariablesEncryptedUpsertRequest) error { + return nil +} diff --git a/nomad/state/state_store.go b/nomad/state/state_store.go index 23850eb44..139f2972a 100644 --- a/nomad/state/state_store.go +++ b/nomad/state/state_store.go @@ -6628,20 +6628,6 @@ func getPreemptedAllocDesiredDescription(preemptedByAllocID string) string { return fmt.Sprintf("Preempted by alloc ID %v", preemptedByAllocID) } -// SecureVariablesQuotas queries all the quotas and is used only for -// snapshot/restore and key rotation -func (s *StateStore) SecureVariablesQuotas(ws memdb.WatchSet) (memdb.ResultIterator, error) { - txn := s.db.ReadTxn() - - iter, err := txn.Get(TableSecureVariablesQuotas, indexID) - if err != nil { - return nil, err - } - - ws.Add(iter.WatchCh()) - return iter, nil -} - // UpsertRootKeyMeta saves root key meta or updates it in-place. func (s *StateStore) UpsertRootKeyMeta(index uint64, rootKeyMeta *structs.RootKeyMeta) error { txn := s.db.WriteTxn(index) diff --git a/nomad/state/state_store_secure_variables.go b/nomad/state/state_store_secure_variables.go index 270701398..3c7b808eb 100644 --- a/nomad/state/state_store_secure_variables.go +++ b/nomad/state/state_store_secure_variables.go @@ -141,6 +141,11 @@ func (s *StateStore) upsertSecureVariableImpl(index uint64, txn *txn, sv *struct return fmt.Errorf("secure variable lookup failed: %v", err) } + existingQuota, err := txn.First(TableSecureVariablesQuotas, indexID, sv.Namespace) + if err != nil { + return fmt.Errorf("secure variable quota lookup failed: %v", err) + } + // Setup the indexes correctly now := time.Now().Round(0) if existing != nil { @@ -165,6 +170,24 @@ func (s *StateStore) upsertSecureVariableImpl(index uint64, txn *txn, sv *struct if err := txn.Insert(TableSecureVariables, sv); err != nil { return fmt.Errorf("secure variable insert failed: %v", err) } + + // Track quota usage + var quotaUsed *structs.SecureVariablesQuota + if existingQuota != nil { + quotaUsed = existingQuota.(*structs.SecureVariablesQuota) + quotaUsed = quotaUsed.Copy() + } else { + quotaUsed = &structs.SecureVariablesQuota{ + Namespace: sv.Namespace, + CreateIndex: index, + } + } + quotaUsed.Size += uint64(len(sv.Data)) + quotaUsed.ModifyIndex = index + if err := txn.Insert(TableSecureVariablesQuotas, quotaUsed); err != nil { + return fmt.Errorf("secure variable quota insert failed: %v", err) + } + *updated = true return nil } @@ -225,6 +248,10 @@ func (s *StateStore) DeleteSecureVariableTxn(index uint64, namespace, path strin if existing == nil { return fmt.Errorf("secure variable not found") } + existingQuota, err := txn.First(TableSecureVariablesQuotas, indexID, namespace) + if err != nil { + return fmt.Errorf("secure variable quota lookup failed: %v", err) + } // Delete the variable if err := txn.Delete(TableSecureVariables, existing); err != nil { @@ -234,5 +261,47 @@ func (s *StateStore) DeleteSecureVariableTxn(index uint64, namespace, path strin return fmt.Errorf("index update failed: %v", err) } + // Track quota usage + if existingQuota != nil { + quotaUsed := existingQuota.(*structs.SecureVariablesQuota) + quotaUsed = quotaUsed.Copy() + sv := existing.(*structs.SecureVariableEncrypted) + quotaUsed.Size -= uint64(len(sv.Data)) + quotaUsed.ModifyIndex = index + if err := txn.Insert(TableSecureVariablesQuotas, quotaUsed); err != nil { + return fmt.Errorf("secure variable quota insert failed: %v", err) + } + } + return nil } + +// SecureVariablesQuotas queries all the quotas and is used only for +// snapshot/restore and key rotation +func (s *StateStore) SecureVariablesQuotas(ws memdb.WatchSet) (memdb.ResultIterator, error) { + txn := s.db.ReadTxn() + + iter, err := txn.Get(TableSecureVariablesQuotas, indexID) + if err != nil { + return nil, err + } + + ws.Add(iter.WatchCh()) + return iter, nil +} + +// SecureVariablesQuotaByNamespace queries for quotas for a particular namespace +func (s *StateStore) SecureVariablesQuotaByNamespace(ws memdb.WatchSet, namespace string) (*structs.SecureVariablesQuota, error) { + txn := s.db.ReadTxn() + watchCh, raw, err := txn.FirstWatch(TableSecureVariablesQuotas, indexID, namespace) + if err != nil { + return nil, fmt.Errorf("secure variable quota lookup failed: %v", err) + } + ws.Add(watchCh) + + if raw == nil { + return nil, nil + } + quotaUsed := raw.(*structs.SecureVariablesQuota) + return quotaUsed, nil +} diff --git a/nomad/state/state_store_secure_variables_test.go b/nomad/state/state_store_secure_variables_test.go index 2a4c6e9f0..86ef6bcda 100644 --- a/nomad/state/state_store_secure_variables_test.go +++ b/nomad/state/state_store_secure_variables_test.go @@ -34,7 +34,7 @@ func TestStateStore_UpsertSecureVariables(t *testing.T) { svs, svm := mockSecureVariables(2, 2) t.Log(printSecureVariables(svs)) insertIndex := uint64(20) - t.Run("1 create new varibles", func(t *testing.T) { + t.Run("1 create new variables", func(t *testing.T) { // SubTest Marker: This ensures new secure variables are inserted as // expected with their correct indexes, along with an update to the index // table. @@ -71,7 +71,16 @@ func TestStateStore_UpsertSecureVariables(t *testing.T) { svm[sv.Path] = &nv } require.Equal(t, len(svs), count, "incorrect number of secure variables found") + + var expectedQuotaSize uint64 + for _, v := range svs { + expectedQuotaSize += uint64(len(v.Data)) + } + quotaUsed, err := testState.SecureVariablesQuotaByNamespace(ws, structs.DefaultNamespace) + require.NoError(t, err) + require.Equal(t, expectedQuotaSize, quotaUsed.Size) }) + svs = svm.List() t.Log(printSecureVariables(svs)) t.Run("1a fetch variable", func(t *testing.T) { @@ -231,13 +240,19 @@ func TestStateStore_DeleteSecureVariable(t *testing.T) { require.NoError(t, err) var delete1Count int + var expectedQuotaSize uint64 // Iterate all the stored variables and assert we have the expected // number. for raw := iter.Next(); raw != nil; raw = iter.Next() { delete1Count++ + v := raw.(*structs.SecureVariableEncrypted) + expectedQuotaSize += uint64(len(v.Data)) } require.Equal(t, 1, delete1Count, "unexpected number of variables in table") + quotaUsed, err := testState.SecureVariablesQuotaByNamespace(ws, structs.DefaultNamespace) + require.NoError(t, err) + require.Equal(t, expectedQuotaSize, quotaUsed.Size) // SubTest Marker: Delete the remaining variable and ensure all indexes // are updated as expected and the table is empty. diff --git a/nomad/structs/secure_variables.go b/nomad/structs/secure_variables.go index 4abaa7f46..126cc0d8f 100644 --- a/nomad/structs/secure_variables.go +++ b/nomad/structs/secure_variables.go @@ -166,6 +166,15 @@ type SecureVariablesQuota struct { ModifyIndex uint64 } +func (svq *SecureVariablesQuota) Copy() *SecureVariablesQuota { + if svq == nil { + return nil + } + nq := new(SecureVariablesQuota) + *nq = *svq + return nq +} + type SecureVariablesUpsertRequest struct { Data []*SecureVariableDecrypted WriteRequest