Skip to content
Open
Show file tree
Hide file tree
Changes from 32 commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
24f760a
create goroutine that runs every 5 minutes to remove orphaned shadow …
jaireddjawed Dec 2, 2024
1bd34e6
updated to use only one goroutine for all HVSapps
jaireddjawed Dec 2, 2024
c6bdb47
remove old goroutine for removing secrets by hvs app name
jaireddjawed Dec 5, 2024
4cdba66
created a new goroutine that will remove shadow secrets by going from…
jaireddjawed Dec 5, 2024
8138ee8
updated goroutine to go from secrets -> app instead of app -> secrets
jaireddjawed Dec 17, 2024
43f0cb5
update log message for successful deletion
jaireddjawed Dec 17, 2024
857159f
added comment explaining that shadow secret cleanup is indefinite
jaireddjawed Dec 18, 2024
dee2d4d
update code to satisfy comments
jaireddjawed Dec 19, 2024
d8a8bc5
added labelOwnerRefUID for testing
jaireddjawed Jan 9, 2025
2e9bdf5
add unit tests to test cleanupOrphanedShadowSecrets, also delete secr…
jaireddjawed Jan 9, 2025
a13989a
Merge branch 'main' into jaireddjawed-feature-cleanup-shadow-secrets
jaireddjawed Jan 9, 2025
17ede69
Fixed issue mismatch secret owner check to delete secret or app
jaireddjawed Jan 10, 2025
e1ccab4
Update controllers/hcpvaultsecretsapp_controller.go
jaireddjawed Jan 10, 2025
79d09ee
Remove TypeMeta from test
jaireddjawed Jan 10, 2025
5fb25eb
updated LabelOwnerRefUID to use in helpers
jaireddjawed Jan 10, 2025
5cb26b0
initiate cleanup through mgr.Add
jaireddjawed Jan 21, 2025
a73b23a
Merge branch 'main' into jaireddjawed-feature-cleanup-shadow-secrets
jaireddjawed Jan 22, 2025
293e859
removed cleanupOrphanedShadowSecrets bool flag
jaireddjawed Jan 22, 2025
6acf268
fixed labelownerrefid variable error
jaireddjawed Jan 22, 2025
607af89
Merge remote-tracking branch 'refs/remotes/origin/jaireddjawed-featur…
jaireddjawed Jan 22, 2025
cca9093
change to allow a user to specify the time interval
jaireddjawed Jan 23, 2025
47c20d1
Added test case with non dynamic secret
jaireddjawed Jan 23, 2025
2e56774
updated cleanup method to not block other secrets along in list to no…
jaireddjawed Jan 26, 2025
1d7100d
add cleanup orphaned shadow secrets command line option
jaireddjawed Jan 26, 2025
428bace
run orphaned shadow secret once in leader
jaireddjawed Jan 26, 2025
9050ebb
changed the select case for the cleanuporphanedshadowsecretinterval
jaireddjawed Jan 27, 2025
cb8ffc9
changed vsoEnvOptionValue
jaireddjawed Jan 27, 2025
1a95fa4
include changes to satisfy comments
jaireddjawed Jan 29, 2025
544a8e0
fixed unit test since cleanupOrphanedShadowSecrets no longer returns …
jaireddjawed Jan 29, 2025
a7c1153
change break to return to prevent for loop break issue
jaireddjawed Jan 31, 2025
296d880
added client back into unit test
jaireddjawed Jan 31, 2025
6fb9622
Merge branch 'main' into jaireddjawed-feature-cleanup-shadow-secrets
jaireddjawed Jan 31, 2025
713167d
updated to satisfy comments
jaireddjawed Feb 13, 2025
3ef25d5
match variable name wording with cli and env var name options
jaireddjawed Feb 14, 2025
17b527c
Update main.go
jaireddjawed Feb 18, 2025
688a47a
Update internal/options/env.go
jaireddjawed Feb 18, 2025
4379ee7
update to only log error if defined
jaireddjawed Feb 18, 2025
dfcf105
Merge branch 'main' into jaireddjawed-feature-cleanup-shadow-secrets
jaireddjawed Feb 19, 2025
54d260b
Fixed remaining issues
jaireddjawed Feb 19, 2025
b7c5a76
Merge remote-tracking branch 'refs/remotes/origin/jaireddjawed-featur…
jaireddjawed Feb 19, 2025
b164562
Merge branch 'main' into jaireddjawed-feature-cleanup-shadow-secrets
jaireddjawed Feb 20, 2025
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
86 changes: 84 additions & 2 deletions controllers/hcpvaultsecretsapp_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,13 @@ import (
"context"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"net/http"
"regexp"
"strconv"
"strings"
"sync"
"time"

httptransport "github.com/go-openapi/runtime/client"
Expand All @@ -24,6 +26,7 @@ import (
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/tools/record"
"k8s.io/utils/ptr"
ctrl "sigs.k8s.io/controller-runtime"
Expand All @@ -32,6 +35,7 @@ import (
"sigs.k8s.io/controller-runtime/pkg/controller"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
"sigs.k8s.io/controller-runtime/pkg/log"
"sigs.k8s.io/controller-runtime/pkg/manager"

"github.com/hashicorp/vault-secrets-operator/credentials"
"github.com/hashicorp/vault-secrets-operator/credentials/hcp"
Expand Down Expand Up @@ -88,6 +92,7 @@ type HCPVaultSecretsAppReconciler struct {
referenceCache ResourceReferenceCache
GlobalTransformationOptions *helpers.GlobalTransformationOptions
BackOffRegistry *BackOffRegistry
once sync.Once
}

// +kubebuilder:rbac:groups=secrets.hashicorp.com,resources=hcpvaultsecretsapps,verbs=get;list;watch;create;update;patch;delete
Expand Down Expand Up @@ -288,6 +293,78 @@ func (r *HCPVaultSecretsAppReconciler) Reconcile(ctx context.Context, req ctrl.R
}, nil
}

func (r *HCPVaultSecretsAppReconciler) startOrphanedShadowSecretCleanup(ctx context.Context, cleanupOrphanedShadowSecretInterval time.Duration) error {
var err error

r.once.Do(func() {
for {
select {
case <-ctx.Done():
if ctx.Err() != nil {
err = ctx.Err()
}
return
// runs the cleanup process once every hour or as specified by the user
case <-time.After(cleanupOrphanedShadowSecretInterval):
r.cleanupOrphanedShadowSecrets(ctx)
}
}
})

return err
}

func (r *HCPVaultSecretsAppReconciler) cleanupOrphanedShadowSecrets(ctx context.Context) {
logger := log.FromContext(ctx).WithName("cleanupOrphanedShadowSecrets")
var errs error

namespaceLabelKey := hvsaLabelPrefix + "/namespace"
nameLabelKey := hvsaLabelPrefix + "/name"

// filtering only for dynamic secrets, also checking if namespace and name labels are present
secrets := corev1.SecretList{}
if err := r.List(ctx, &secrets, client.InNamespace(common.OperatorNamespace),
client.MatchingLabels{"app.kubernetes.io/component": "hvs-dynamic-secret-cache"},
client.HasLabels{namespaceLabelKey, nameLabelKey}); err != nil {
errs = errors.Join(errs, fmt.Errorf("failed to list shadow secrets: %w", err))
}

for _, secret := range secrets.Items {
namespace := secret.Labels[namespaceLabelKey]
name := secret.Labels[nameLabelKey]
objKey := types.NamespacedName{Namespace: namespace, Name: name}

o := &secretsv1beta1.HCPVaultSecretsApp{}

// get the HCPVaultSecretsApp instance that the shadow secret belongs to (if applicable)
// no errors are returned in the loop because this could block the deletion of other
// orphaned shadow secrets that are further along in the list
err := r.Get(ctx, objKey, o)
if err != nil && !apierrors.IsNotFound(err) {
errs = errors.Join(errs, fmt.Errorf("failed to get HCPVaultSecretsApp: %w", err))
continue
}

// if the HCPVaultSecretsApp has been deleted, and the shadow secret belongs to it, delete both
if o.GetDeletionTimestamp() != nil && o.GetUID() == types.UID(secret.Labels[helpers.LabelOwnerRefUID]) {
if err := r.handleDeletion(ctx, o); err != nil {
errs = errors.Join(errs, fmt.Errorf("failed to handle deletion of HCPVaultSecretsApp %s: %w", o.Spec.AppName, err))
}

logger.Info("Deleted orphaned resources associated with HCPVaultSecretsApp", "app", o.Name)
} else if apierrors.IsNotFound(err) || secret.GetDeletionTimestamp() != nil {
// otherwise, delete the single shadow secret if it has a deletion timestamp
if err := helpers.DeleteSecret(ctx, r.Client, objKey); err != nil {
errs = errors.Join(errs, fmt.Errorf("failed to delete shadow secret %s: %w", secret.Name, err))
}

logger.Info("Deleted orphaned shadow secret", "secret", secret.Name)
}
}

logger.Error(errs, "Failed during cleanup of orphaned shadow secrets")
}

func (r *HCPVaultSecretsAppReconciler) updateStatus(ctx context.Context, o *secretsv1beta1.HCPVaultSecretsApp) error {
o.Status.LastGeneration = o.GetGeneration()
if err := r.Status().Update(ctx, o); err != nil {
Expand All @@ -300,12 +377,17 @@ func (r *HCPVaultSecretsAppReconciler) updateStatus(ctx context.Context, o *secr
}

// SetupWithManager sets up the controller with the Manager.
func (r *HCPVaultSecretsAppReconciler) SetupWithManager(mgr ctrl.Manager, opts controller.Options) error {
func (r *HCPVaultSecretsAppReconciler) SetupWithManager(mgr ctrl.Manager, opts controller.Options, cleanupOrphanedShadowSecretInterval time.Duration) error {
r.referenceCache = newResourceReferenceCache()
if r.BackOffRegistry == nil {
r.BackOffRegistry = NewBackOffRegistry()
}

mgr.Add(manager.RunnableFunc(func(ctx context.Context) error {
err := r.startOrphanedShadowSecretCleanup(ctx, cleanupOrphanedShadowSecretInterval)
return err
}))

return ctrl.NewControllerManagedBy(mgr).
For(&secretsv1beta1.HCPVaultSecretsApp{}).
WithEventFilter(syncableSecretPredicate(nil)).
Expand Down Expand Up @@ -371,7 +453,7 @@ func (r *HCPVaultSecretsAppReconciler) hvsClient(ctx context.Context, o *secrets
return hvsclient.New(cl, nil), nil
}

func (r *HCPVaultSecretsAppReconciler) handleDeletion(ctx context.Context, o client.Object) error {
func (r *HCPVaultSecretsAppReconciler) handleDeletion(ctx context.Context, o *secretsv1beta1.HCPVaultSecretsApp) error {
logger := log.FromContext(ctx)
objKey := client.ObjectKeyFromObject(o)
r.referenceCache.Remove(SecretTransformation, objKey)
Expand Down
160 changes: 160 additions & 0 deletions controllers/hcpvaultsecretsapp_controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,16 @@ import (
"github.com/hashicorp/hcp-sdk-go/clients/cloud-vault-secrets/preview/2023-11-28/models"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
corev1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"sigs.k8s.io/controller-runtime/pkg/client"
ctrlclient "sigs.k8s.io/controller-runtime/pkg/client"

secretsv1beta1 "github.com/hashicorp/vault-secrets-operator/api/v1beta1"
"github.com/hashicorp/vault-secrets-operator/common"
"github.com/hashicorp/vault-secrets-operator/helpers"
"github.com/hashicorp/vault-secrets-operator/internal/testutils"
)

var _ runtime.ClientTransport = (*fakeHVSTransport)(nil)
Expand Down Expand Up @@ -1224,3 +1228,159 @@ func Test_makeShadowObjKey(t *testing.T) {
})
}
}

func Test_CleanupOrphanedShadowSecrets(t *testing.T) {
deletionTimestamp := metav1.Now()

tests := map[string]struct {
o *secretsv1beta1.HCPVaultSecretsApp
secret *corev1.Secret
isHCPVaultSecretsAppDeletionExpected bool
isShadowSecretDeletionExpected bool
}{
"deleted-secret-hvsapp-owner": {
o: &secretsv1beta1.HCPVaultSecretsApp{
TypeMeta: metav1.TypeMeta{
Kind: HCPVaultSecretsApp.String(),
APIVersion: secretsv1beta1.GroupVersion.Version,
},
ObjectMeta: metav1.ObjectMeta{
UID: "hvsApp1UID",
Namespace: "hvsApp1Namespace",
Name: "hvsApp1",
Finalizers: []string{hcpVaultSecretsAppFinalizer},
},
},
secret: &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Namespace: common.OperatorNamespace,
Name: "shadowSecret1",
DeletionTimestamp: &deletionTimestamp,
Finalizers: []string{vaultDynamicSecretFinalizer},
Labels: map[string]string{
hvsaLabelPrefix + "/namespace": "hvsApp1Namespace",
hvsaLabelPrefix + "/name": "hvsApp1",
"app.kubernetes.io/component": "hvs-dynamic-secret-cache",
helpers.LabelOwnerRefUID: "hvsApp1UID",
},
},
},
isHCPVaultSecretsAppDeletionExpected: true,
isShadowSecretDeletionExpected: true,
},
"deleted-secret-hvsapp-not-owner": {
o: &secretsv1beta1.HCPVaultSecretsApp{
TypeMeta: metav1.TypeMeta{
Kind: HCPVaultSecretsApp.String(),
APIVersion: secretsv1beta1.GroupVersion.Version,
},
ObjectMeta: metav1.ObjectMeta{
UID: "hvsApp2UID",
Namespace: "hvsApp2Namespace",
Name: "hvsApp2",
Finalizers: []string{hcpVaultSecretsAppFinalizer},
},
},
secret: &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Namespace: common.OperatorNamespace,
Name: "shadowSecret2",
DeletionTimestamp: &deletionTimestamp,
Finalizers: []string{vaultDynamicSecretFinalizer},
Labels: map[string]string{
"app.kubernetes.io/component": "hvs-dynamic-secret-cache",
},
},
},
isShadowSecretDeletionExpected: true,
},
"deleted-secret-hvsapp-not-found": {
secret: &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Namespace: common.OperatorNamespace,
Name: "shadowSecret3",
Labels: map[string]string{
"app.kubernetes.io/component": "hvs-dynamic-secret-cache",
},
DeletionTimestamp: &deletionTimestamp,
Finalizers: []string{hcpVaultSecretsAppFinalizer},
},
},
isShadowSecretDeletionExpected: true,
},
"secret-not-dynamic": {
o: &secretsv1beta1.HCPVaultSecretsApp{
TypeMeta: metav1.TypeMeta{
Kind: HCPVaultSecretsApp.String(),
APIVersion: secretsv1beta1.GroupVersion.Version,
},
ObjectMeta: metav1.ObjectMeta{
UID: "hvsApp4UID",
Namespace: "hvsApp4Namespace",
Name: "hvsApp4",
Finalizers: []string{hcpVaultSecretsAppFinalizer},
},
},
secret: &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Namespace: common.OperatorNamespace,
Name: "nonShadowSecret",
DeletionTimestamp: &deletionTimestamp,
Finalizers: []string{vaultDynamicSecretFinalizer},
Labels: map[string]string{
hvsaLabelPrefix + "/namespace": "hvsApp4Namespace",
hvsaLabelPrefix + "/name": "hvsApp4",
helpers.LabelOwnerRefUID: "hvsApp4UID",
},
},
},
isShadowSecretDeletionExpected: false,
},
}

ctx := context.Background()
clientBuilder := testutils.NewFakeClientBuilder()

for name, tt := range tests {
t.Run(name, func(t *testing.T) {
client := clientBuilder.Build()
r := &HCPVaultSecretsAppReconciler{
Client: client,
BackOffRegistry: NewBackOffRegistry(),
referenceCache: newResourceReferenceCache(),
}

// create the HCPVaultSecretsApp if the test case has one
if tt.o != nil {
assert.NoError(t, client.Create(ctx, tt.o))
}

// create the secret for the test case
assert.NoError(t, client.Create(ctx, tt.secret))

// DeleteTimestamp is a read-only field, so Delete will need to be called to
// simulate deletion of the HCPVaultSecretsApp
if tt.isHCPVaultSecretsAppDeletionExpected {
assert.NoError(t, client.Delete(ctx, tt.o))
}

r.cleanupOrphanedShadowSecrets(ctx)

if tt.isHCPVaultSecretsAppDeletionExpected {
deletedHVSApp := &secretsv1beta1.HCPVaultSecretsApp{}
err := r.Get(ctx, ctrlclient.ObjectKeyFromObject(tt.o), deletedHVSApp)
assert.True(t, apierrors.IsNotFound(err))
}

if tt.isShadowSecretDeletionExpected {
deletedSecret := &corev1.Secret{}
err := r.Get(ctx, makeShadowObjKey(tt.secret), deletedSecret)
assert.True(t, apierrors.IsNotFound(err))
} else {
secret := &corev1.Secret{}
err := r.Get(ctx, ctrlclient.ObjectKeyFromObject(tt.secret), secret)
assert.False(t, apierrors.IsNotFound(err))
}
})
}
}
6 changes: 3 additions & 3 deletions helpers/secrets.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ var SecretDataErrorContainsRaw = fmt.Errorf("key '%s' not permitted in Secret da
// labelOwnerRefUID is used as the primary key when listing the Secrets owned by
// a specific VSO object. It should be included in every Secret that is created
// by VSO.
var labelOwnerRefUID = fmt.Sprintf("%s/vso-ownerRefUID", secretsv1beta1.GroupVersion.Group)
var LabelOwnerRefUID = fmt.Sprintf("%s/vso-ownerRefUID", secretsv1beta1.GroupVersion.Group)

// OwnerLabels will be applied to any k8s secret we create. They are used in Secret ownership checks.
// There are similar labels in the vault package. It's important that component secret's value never
Expand All @@ -66,7 +66,7 @@ func OwnerLabelsForObj(obj ctrlclient.Object) (map[string]string, error) {
for k, v := range OwnerLabels {
l[k] = v
}
l[labelOwnerRefUID] = uid
l[LabelOwnerRefUID] = uid

return l, nil
}
Expand All @@ -84,7 +84,7 @@ func matchingLabelsForObj(obj ctrlclient.Object) (ctrlclient.MatchingLabels, err
for k, v := range l {
m[k] = v
}
m[labelOwnerRefUID] = string(obj.GetUID())
m[LabelOwnerRefUID] = string(obj.GetUID())

return m, nil
}
Expand Down
4 changes: 2 additions & 2 deletions helpers/secrets_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ func TestFindSecretsOwnedByObj(t *testing.T) {
for k, v := range OwnerLabels {
ownerLabels[k] = v
}
ownerLabels[labelOwnerRefUID] = string(owner.GetUID())
ownerLabels[LabelOwnerRefUID] = string(owner.GetUID())

notOwner := &secretsv1beta1.VaultDynamicSecret{
TypeMeta: metav1.TypeMeta{
Expand Down Expand Up @@ -489,7 +489,7 @@ func TestSyncSecret(t *testing.T) {
}

if tt.obj.GetUID() != "" {
ownerLabels[labelOwnerRefUID] = string(tt.obj.GetUID())
ownerLabels[LabelOwnerRefUID] = string(tt.obj.GetUID())
}

var orphans []ctrlclient.ObjectKey
Expand Down
3 changes: 3 additions & 0 deletions internal/options/env.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@ type VSOEnvOptions struct {

// ClientCacheNumLocks is VSO_CLIENT_CACHE_NUM_LOCKS environment variable option
ClientCacheNumLocks *int `split_words:"true"`

// CleanupOrphanedShadowSecretInterval is VSO_HVSA_CLEANUP_ORPHANED_SHADOW_SECRETS_INTERVAL environment variable option
HVSACleanupOrphanedShadowSecretInterval time.Duration `split_words:"true"`
}

// Parse environment variable options, prefixed with "VSO_"
Expand Down
Loading
Loading