Secrets: garbage collection (#110247)

* clean up older secret versions

* start gargbage collection worker as background service

* make gen-go

* fix typo

* make update-workspace

* undo go mod changes

* undo go work sum changes

* Update pkg/registry/apis/secret/garbagecollectionworker/worker.go

Co-authored-by: Matheus Macabu <macabu@users.noreply.github.com>

* Update pkg/registry/apis/secret/garbagecollectionworker/worker.go

Co-authored-by: Matheus Macabu <macabu@users.noreply.github.com>

* default gc_worker_batch_size to 1 minute

* fix typo

* fix typo

* add test to ensure cleaning up secure values is idempotent

* make gen-go

* make update-workspace

* undo go.mod and .sum changes

* undo enterprise imports

---------

Co-authored-by: Matheus Macabu <macabu.matheus@gmail.com>
Co-authored-by: Matheus Macabu <macabu@users.noreply.github.com>
This commit is contained in:
Bruno
2025-09-02 11:11:01 -03:00
committed by GitHub
co-authored by Matheus Macabu Matheus Macabu
parent d5eb3e291a
commit f8cd7049e8
30 changed files with 1069 additions and 35 deletions
@@ -4,6 +4,7 @@ import (
"fmt"
"slices"
"testing"
"time"
"github.com/mitchellh/copystructure"
"github.com/stretchr/testify/require"
@@ -20,7 +21,9 @@ import (
type modelSecureValue struct {
*secretv1beta1.SecureValue
active bool
active bool
created time.Time
leaseCreated time.Time
}
// A simplified model of the grafana secrets manager
@@ -69,8 +72,8 @@ func (m *model) readActiveVersion(namespace, name string) *modelSecureValue {
return nil
}
func (m *model) create(sv *secretv1beta1.SecureValue, actorUID string) (*secretv1beta1.SecureValue, error) {
modelSv := &modelSecureValue{sv, false}
func (m *model) create(now time.Time, sv *secretv1beta1.SecureValue) (*secretv1beta1.SecureValue, error) {
modelSv := &modelSecureValue{SecureValue: sv, active: false, created: now}
modelSv.Status.Version = m.getNewVersionNumber(modelSv.Namespace, modelSv.Name)
modelSv.Status.ExternalID = fmt.Sprintf("%d", modelSv.Status.Version)
m.secureValues = append(m.secureValues, modelSv)
@@ -78,7 +81,7 @@ func (m *model) create(sv *secretv1beta1.SecureValue, actorUID string) (*secretv
return modelSv.SecureValue, nil
}
func (m *model) update(newSecureValue *secretv1beta1.SecureValue, actorUID string) (*secretv1beta1.SecureValue, bool, error) {
func (m *model) update(now time.Time, newSecureValue *secretv1beta1.SecureValue) (*secretv1beta1.SecureValue, bool, error) {
// If the payload doesn't contain a value, get the value from current version
if newSecureValue.Spec.Value == nil {
sv := m.readActiveVersion(newSecureValue.Namespace, newSecureValue.Name)
@@ -87,7 +90,7 @@ func (m *model) update(newSecureValue *secretv1beta1.SecureValue, actorUID strin
}
newSecureValue.Spec.Value = sv.Spec.Value
}
createdSv, err := m.create(newSecureValue, actorUID)
createdSv, err := m.create(now, newSecureValue)
return createdSv, true, err
}
@@ -141,6 +144,22 @@ func (m *model) read(namespace, name string) (*secretv1beta1.SecureValue, error)
return modelSv.SecureValue, nil
}
func (m *model) leaseInactiveSecureValues(now time.Time, minAge, leaseTTL time.Duration, maxBatchSize uint16) ([]*modelSecureValue, error) {
out := make([]*modelSecureValue, 0)
for _, sv := range m.secureValues {
if len(out) >= int(maxBatchSize) {
break
}
if !sv.active && now.Sub(sv.created) > minAge && now.Sub(sv.leaseCreated) > leaseTTL {
sv.leaseCreated = now
out = append(out, sv)
}
}
return out, nil
}
var (
decryptersGen = rapid.SampledFrom([]string{"svc1", "svc2", "svc3", "svc4", "svc5"})
nameGen = rapid.SampledFrom([]string{"n1", "n2", "n3", "n4", "n5"})
@@ -204,16 +223,17 @@ func TestModel(t *testing.T) {
t.Parallel()
m := newModel()
now := time.Now()
// Create a secure value
sv1, err := m.create(deepCopy(sv), "actor-uid")
sv1, err := m.create(now, deepCopy(sv))
require.NoError(t, err)
require.Equal(t, sv.Namespace, sv1.Namespace)
require.Equal(t, sv.Name, sv1.Name)
require.EqualValues(t, 1, sv1.Status.Version)
// Create a new version of a secure value
sv2, err := m.create(deepCopy(sv), "actor-uid")
sv2, err := m.create(now, deepCopy(sv))
require.NoError(t, err)
require.Equal(t, sv.Namespace, sv2.Namespace)
require.Equal(t, sv.Name, sv2.Name)
@@ -225,11 +245,13 @@ func TestModel(t *testing.T) {
m := newModel()
sv1, err := m.create(deepCopy(sv), "actor-uid")
now := time.Now()
sv1, err := m.create(now, deepCopy(sv))
require.NoError(t, err)
// Create a new version of a secure value by updating it
sv2, _, err := m.update(deepCopy(sv1), "actor-uid")
sv2, _, err := m.update(now, deepCopy(sv1))
require.NoError(t, err)
require.Equal(t, sv.Namespace, sv2.Namespace)
require.Equal(t, sv.Name, sv2.Name)
@@ -239,14 +261,14 @@ func TestModel(t *testing.T) {
sv3 := deepCopy(sv2)
sv3.Name = "i_dont_exist"
sv3.Spec.Value = nil
_, _, err = m.update(sv3, "actor-uid")
_, _, err = m.update(now, sv3)
require.ErrorIs(t, err, contracts.ErrSecureValueNotFound)
// Updating a value that doesn't exist creates a new version
sv4 := deepCopy(sv3)
sv4.Name = "i_dont_exist"
sv4.Spec.Value = ptr.To(secretv1beta1.NewExposedSecureValue("sv4"))
sv4, _, err = m.update(sv4, "actor-uid")
sv4, _, err = m.update(now, sv4)
require.NoError(t, err)
require.EqualValues(t, 1, sv4.Status.Version)
})
@@ -255,8 +277,9 @@ func TestModel(t *testing.T) {
t.Parallel()
m := newModel()
now := time.Now()
sv1, err := m.create(deepCopy(sv), "actor-uid")
sv1, err := m.create(now, deepCopy(sv))
require.NoError(t, err)
// Deleting a secure value
@@ -275,6 +298,7 @@ func TestModel(t *testing.T) {
t.Parallel()
m := newModel()
now := time.Now()
// No secure values exist yet
list, err := m.list(sv.Namespace)
@@ -282,7 +306,7 @@ func TestModel(t *testing.T) {
require.Equal(t, 0, len(list.Items))
// Create a secure value
sv1, err := m.create(deepCopy(sv), "actor-uid")
sv1, err := m.create(now, deepCopy(sv))
require.NoError(t, err)
// 1 secure value exists and it should be returned
@@ -298,6 +322,7 @@ func TestModel(t *testing.T) {
t.Parallel()
m := newModel()
now := time.Now()
// Decrypting a secure value that does not exist
result, err := m.decrypt("decrypter", "namespace", "name")
@@ -308,7 +333,7 @@ func TestModel(t *testing.T) {
// Create a secure value
secret := "v1"
sv1, err := m.create(deepCopy(sv), "actor-uid")
sv1, err := m.create(now, deepCopy(sv))
require.NoError(t, err)
// Decrypt the just created secure value
@@ -333,7 +358,7 @@ func TestStateMachine(t *testing.T) {
"create": func(t *rapid.T) {
sv := anySecureValueGen.Draw(t, "sv")
modelCreatedSv, modelErr := model.create(deepCopy(sv), "actor-uid")
modelCreatedSv, modelErr := model.create(sut.Clock.Now(), deepCopy(sv))
createdSv, err := sut.CreateSv(t.Context(), testutils.CreateSvWithSv(deepCopy(sv)))
if err != nil || modelErr != nil {
@@ -346,7 +371,7 @@ func TestStateMachine(t *testing.T) {
},
"update": func(t *rapid.T) {
sv := updateSecureValueGen.Draw(t, "sv")
modelCreatedSv, _, modelErr := model.update(deepCopy(sv), "actor-uid")
modelCreatedSv, _, modelErr := model.update(sut.Clock.Now(), deepCopy(sv))
createdSv, err := sut.UpdateSv(t.Context(), deepCopy(sv))
if err != nil || modelErr != nil {
require.ErrorIs(t, err, modelErr)