mirror of https://github.com/grafana/grafana
Provisioning: delete secrets on repository deletion (#108113)
- Add hooks to git, github and github webhooks to remove the. - Implement deletion in secrets package. - Add `Mutator` interface and hooks so that we can register any mutator. - Add unit test coverage to those mutators. - Move provider specific mutation from the massive `register.go` to the respective packages (e.g. `git` , `github`, etc). - Add integration test for removal. - Change the decryption fallback to simply check for the repository prefix.pull/108110/head
parent
4a779c4ccb
commit
56543db16a
@ -0,0 +1,9 @@ |
||||
package controller |
||||
|
||||
import ( |
||||
"context" |
||||
|
||||
"k8s.io/apimachinery/pkg/runtime" |
||||
) |
||||
|
||||
type Mutator func(ctx context.Context, obj runtime.Object) error |
@ -0,0 +1,36 @@ |
||||
package git |
||||
|
||||
import ( |
||||
"context" |
||||
|
||||
"k8s.io/apimachinery/pkg/runtime" |
||||
|
||||
provisioning "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1" |
||||
"github.com/grafana/grafana/pkg/registry/apis/provisioning/controller" |
||||
"github.com/grafana/grafana/pkg/registry/apis/provisioning/secrets" |
||||
) |
||||
|
||||
func Mutator(secrets secrets.RepositorySecrets) controller.Mutator { |
||||
return func(ctx context.Context, obj runtime.Object) error { |
||||
repo, ok := obj.(*provisioning.Repository) |
||||
if !ok { |
||||
return nil |
||||
} |
||||
|
||||
if repo.Spec.Git == nil { |
||||
return nil |
||||
} |
||||
|
||||
if repo.Spec.Git.Token != "" { |
||||
secretName := repo.Name + gitTokenSecretSuffix |
||||
nameOrValue, err := secrets.Encrypt(ctx, repo, secretName, repo.Spec.Git.Token) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
repo.Spec.Git.EncryptedToken = nameOrValue |
||||
repo.Spec.Git.Token = "" |
||||
} |
||||
|
||||
return nil |
||||
} |
||||
} |
@ -0,0 +1,159 @@ |
||||
package git |
||||
|
||||
import ( |
||||
"context" |
||||
"errors" |
||||
"testing" |
||||
|
||||
provisioning "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1" |
||||
"github.com/grafana/grafana/pkg/registry/apis/provisioning/secrets" |
||||
"github.com/stretchr/testify/assert" |
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" |
||||
"k8s.io/apimachinery/pkg/runtime" |
||||
) |
||||
|
||||
func TestMutator(t *testing.T) { |
||||
tests := []struct { |
||||
name string |
||||
obj runtime.Object |
||||
token string |
||||
setupMocks func(*secrets.MockRepositorySecrets) |
||||
expectedToken string |
||||
expectedEncryptedToken string |
||||
expectedError string |
||||
}{ |
||||
{ |
||||
name: "successful token encryption", |
||||
obj: &provisioning.Repository{ |
||||
ObjectMeta: metav1.ObjectMeta{ |
||||
Name: "test-repo", |
||||
Namespace: "default", |
||||
}, |
||||
Spec: provisioning.RepositorySpec{ |
||||
Git: &provisioning.GitRepositoryConfig{ |
||||
Token: "secret-token", |
||||
}, |
||||
}, |
||||
}, |
||||
setupMocks: func(mockSecrets *secrets.MockRepositorySecrets) { |
||||
mockSecrets.EXPECT().Encrypt( |
||||
context.Background(), |
||||
&provisioning.Repository{ |
||||
ObjectMeta: metav1.ObjectMeta{ |
||||
Name: "test-repo", |
||||
Namespace: "default", |
||||
}, |
||||
Spec: provisioning.RepositorySpec{ |
||||
Git: &provisioning.GitRepositoryConfig{ |
||||
Token: "secret-token", |
||||
}, |
||||
}, |
||||
}, |
||||
"test-repo"+gitTokenSecretSuffix, |
||||
"secret-token", |
||||
).Return([]byte("encrypted-token"), nil) |
||||
}, |
||||
expectedToken: "", |
||||
expectedEncryptedToken: "encrypted-token", |
||||
}, |
||||
{ |
||||
name: "encryption error", |
||||
obj: &provisioning.Repository{ |
||||
ObjectMeta: metav1.ObjectMeta{ |
||||
Name: "test-repo", |
||||
Namespace: "default", |
||||
}, |
||||
Spec: provisioning.RepositorySpec{ |
||||
Git: &provisioning.GitRepositoryConfig{ |
||||
Token: "secret-token", |
||||
}, |
||||
}, |
||||
}, |
||||
setupMocks: func(mockSecrets *secrets.MockRepositorySecrets) { |
||||
mockSecrets.EXPECT().Encrypt( |
||||
context.Background(), |
||||
&provisioning.Repository{ |
||||
ObjectMeta: metav1.ObjectMeta{ |
||||
Name: "test-repo", |
||||
Namespace: "default", |
||||
}, |
||||
Spec: provisioning.RepositorySpec{ |
||||
Git: &provisioning.GitRepositoryConfig{ |
||||
Token: "secret-token", |
||||
}, |
||||
}, |
||||
}, |
||||
"test-repo"+gitTokenSecretSuffix, |
||||
"secret-token", |
||||
).Return(nil, errors.New("encryption failed")) |
||||
}, |
||||
expectedError: "encryption failed", |
||||
}, |
||||
{ |
||||
name: "no git spec", |
||||
obj: &provisioning.Repository{ |
||||
ObjectMeta: metav1.ObjectMeta{ |
||||
Name: "test-repo", |
||||
Namespace: "default", |
||||
}, |
||||
Spec: provisioning.RepositorySpec{ |
||||
Git: nil, |
||||
}, |
||||
}, |
||||
setupMocks: func(mockSecrets *secrets.MockRepositorySecrets) { |
||||
// No expectations
|
||||
}, |
||||
}, |
||||
{ |
||||
name: "empty token", |
||||
obj: &provisioning.Repository{ |
||||
ObjectMeta: metav1.ObjectMeta{ |
||||
Name: "test-repo", |
||||
Namespace: "default", |
||||
}, |
||||
Spec: provisioning.RepositorySpec{ |
||||
Git: &provisioning.GitRepositoryConfig{ |
||||
Token: "", |
||||
}, |
||||
}, |
||||
}, |
||||
setupMocks: func(mockSecrets *secrets.MockRepositorySecrets) { |
||||
// No expectations
|
||||
}, |
||||
}, |
||||
{ |
||||
name: "non-repository object", |
||||
obj: &runtime.Unknown{}, |
||||
setupMocks: func(mockSecrets *secrets.MockRepositorySecrets) { |
||||
// No expectations
|
||||
}, |
||||
}, |
||||
} |
||||
|
||||
for _, tt := range tests { |
||||
t.Run(tt.name, func(t *testing.T) { |
||||
mockSecrets := secrets.NewMockRepositorySecrets(t) |
||||
tt.setupMocks(mockSecrets) |
||||
|
||||
mutator := Mutator(mockSecrets) |
||||
err := mutator(context.Background(), tt.obj) |
||||
|
||||
if tt.expectedError != "" { |
||||
assert.Error(t, err) |
||||
assert.Contains(t, err.Error(), tt.expectedError) |
||||
} else { |
||||
assert.NoError(t, err) |
||||
|
||||
// Check that token was cleared and encrypted token was set
|
||||
if repo, ok := tt.obj.(*provisioning.Repository); ok && repo.Spec.Git != nil { |
||||
if tt.expectedEncryptedToken != "" { |
||||
// Token should be cleared after encryption
|
||||
assert.Empty(t, repo.Spec.Git.Token, "Token should be cleared after encryption") |
||||
// EncryptedToken should be set to the expected value
|
||||
assert.Equal(t, tt.expectedEncryptedToken, string(repo.Spec.Git.EncryptedToken), "EncryptedToken should match expected value") |
||||
} |
||||
} |
||||
} |
||||
}) |
||||
} |
||||
} |
@ -0,0 +1,36 @@ |
||||
package github |
||||
|
||||
import ( |
||||
"context" |
||||
|
||||
"k8s.io/apimachinery/pkg/runtime" |
||||
|
||||
provisioning "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1" |
||||
"github.com/grafana/grafana/pkg/registry/apis/provisioning/controller" |
||||
"github.com/grafana/grafana/pkg/registry/apis/provisioning/secrets" |
||||
) |
||||
|
||||
func Mutator(secrets secrets.RepositorySecrets) controller.Mutator { |
||||
return func(ctx context.Context, obj runtime.Object) error { |
||||
repo, ok := obj.(*provisioning.Repository) |
||||
if !ok { |
||||
return nil |
||||
} |
||||
|
||||
if repo.Spec.GitHub == nil { |
||||
return nil |
||||
} |
||||
|
||||
if repo.Spec.GitHub.Token != "" { |
||||
secretName := repo.Name + githubTokenSecretSuffix |
||||
nameOrValue, err := secrets.Encrypt(ctx, repo, secretName, repo.Spec.GitHub.Token) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
repo.Spec.GitHub.EncryptedToken = nameOrValue |
||||
repo.Spec.GitHub.Token = "" |
||||
} |
||||
|
||||
return nil |
||||
} |
||||
} |
@ -0,0 +1,159 @@ |
||||
package github |
||||
|
||||
import ( |
||||
"context" |
||||
"errors" |
||||
"testing" |
||||
|
||||
provisioning "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1" |
||||
"github.com/grafana/grafana/pkg/registry/apis/provisioning/secrets" |
||||
"github.com/stretchr/testify/assert" |
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" |
||||
"k8s.io/apimachinery/pkg/runtime" |
||||
) |
||||
|
||||
func TestMutator(t *testing.T) { |
||||
tests := []struct { |
||||
name string |
||||
obj runtime.Object |
||||
token string |
||||
setupMocks func(*secrets.MockRepositorySecrets) |
||||
expectedToken string |
||||
expectedEncryptedToken string |
||||
expectedError string |
||||
}{ |
||||
{ |
||||
name: "successful token encryption", |
||||
obj: &provisioning.Repository{ |
||||
ObjectMeta: metav1.ObjectMeta{ |
||||
Name: "test-repo", |
||||
Namespace: "default", |
||||
}, |
||||
Spec: provisioning.RepositorySpec{ |
||||
GitHub: &provisioning.GitHubRepositoryConfig{ |
||||
Token: "secret-token", |
||||
}, |
||||
}, |
||||
}, |
||||
setupMocks: func(mockSecrets *secrets.MockRepositorySecrets) { |
||||
mockSecrets.EXPECT().Encrypt( |
||||
context.Background(), |
||||
&provisioning.Repository{ |
||||
ObjectMeta: metav1.ObjectMeta{ |
||||
Name: "test-repo", |
||||
Namespace: "default", |
||||
}, |
||||
Spec: provisioning.RepositorySpec{ |
||||
GitHub: &provisioning.GitHubRepositoryConfig{ |
||||
Token: "secret-token", |
||||
}, |
||||
}, |
||||
}, |
||||
"test-repo"+githubTokenSecretSuffix, |
||||
"secret-token", |
||||
).Return([]byte("encrypted-token"), nil) |
||||
}, |
||||
expectedToken: "", |
||||
expectedEncryptedToken: "encrypted-token", |
||||
}, |
||||
{ |
||||
name: "encryption error", |
||||
obj: &provisioning.Repository{ |
||||
ObjectMeta: metav1.ObjectMeta{ |
||||
Name: "test-repo", |
||||
Namespace: "default", |
||||
}, |
||||
Spec: provisioning.RepositorySpec{ |
||||
GitHub: &provisioning.GitHubRepositoryConfig{ |
||||
Token: "secret-token", |
||||
}, |
||||
}, |
||||
}, |
||||
setupMocks: func(mockSecrets *secrets.MockRepositorySecrets) { |
||||
mockSecrets.EXPECT().Encrypt( |
||||
context.Background(), |
||||
&provisioning.Repository{ |
||||
ObjectMeta: metav1.ObjectMeta{ |
||||
Name: "test-repo", |
||||
Namespace: "default", |
||||
}, |
||||
Spec: provisioning.RepositorySpec{ |
||||
GitHub: &provisioning.GitHubRepositoryConfig{ |
||||
Token: "secret-token", |
||||
}, |
||||
}, |
||||
}, |
||||
"test-repo"+githubTokenSecretSuffix, |
||||
"secret-token", |
||||
).Return(nil, errors.New("encryption failed")) |
||||
}, |
||||
expectedError: "encryption failed", |
||||
}, |
||||
{ |
||||
name: "no github spec", |
||||
obj: &provisioning.Repository{ |
||||
ObjectMeta: metav1.ObjectMeta{ |
||||
Name: "test-repo", |
||||
Namespace: "default", |
||||
}, |
||||
Spec: provisioning.RepositorySpec{ |
||||
GitHub: nil, |
||||
}, |
||||
}, |
||||
setupMocks: func(_ *secrets.MockRepositorySecrets) { |
||||
// No expectations
|
||||
}, |
||||
}, |
||||
{ |
||||
name: "empty token", |
||||
obj: &provisioning.Repository{ |
||||
ObjectMeta: metav1.ObjectMeta{ |
||||
Name: "test-repo", |
||||
Namespace: "default", |
||||
}, |
||||
Spec: provisioning.RepositorySpec{ |
||||
GitHub: &provisioning.GitHubRepositoryConfig{ |
||||
Token: "", |
||||
}, |
||||
}, |
||||
}, |
||||
setupMocks: func(_ *secrets.MockRepositorySecrets) { |
||||
// No expectations
|
||||
}, |
||||
}, |
||||
{ |
||||
name: "non-repository object", |
||||
obj: &runtime.Unknown{}, |
||||
setupMocks: func(_ *secrets.MockRepositorySecrets) { |
||||
// No expectations
|
||||
}, |
||||
}, |
||||
} |
||||
|
||||
for _, tt := range tests { |
||||
t.Run(tt.name, func(t *testing.T) { |
||||
mockSecrets := secrets.NewMockRepositorySecrets(t) |
||||
tt.setupMocks(mockSecrets) |
||||
|
||||
mutator := Mutator(mockSecrets) |
||||
err := mutator(context.Background(), tt.obj) |
||||
|
||||
if tt.expectedError != "" { |
||||
assert.Error(t, err) |
||||
assert.Contains(t, err.Error(), tt.expectedError) |
||||
} else { |
||||
assert.NoError(t, err) |
||||
|
||||
// Check that token was cleared and encrypted token was set
|
||||
if repo, ok := tt.obj.(*provisioning.Repository); ok && repo.Spec.GitHub != nil { |
||||
if tt.expectedEncryptedToken != "" { |
||||
// Token should be cleared after encryption
|
||||
assert.Empty(t, repo.Spec.GitHub.Token, "Token should be cleared after encryption") |
||||
// EncryptedToken should be set to the expected value
|
||||
assert.Equal(t, tt.expectedEncryptedToken, string(repo.Spec.GitHub.EncryptedToken), "EncryptedToken should match expected value") |
||||
} |
||||
} |
||||
} |
||||
}) |
||||
} |
||||
} |
@ -0,0 +1,35 @@ |
||||
package webhooks |
||||
|
||||
import ( |
||||
"context" |
||||
|
||||
provisioning "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1" |
||||
"github.com/grafana/grafana/pkg/registry/apis/provisioning/controller" |
||||
"github.com/grafana/grafana/pkg/registry/apis/provisioning/secrets" |
||||
"k8s.io/apimachinery/pkg/runtime" |
||||
) |
||||
|
||||
func Mutator(secrets secrets.RepositorySecrets) controller.Mutator { |
||||
return func(ctx context.Context, obj runtime.Object) error { |
||||
repo, ok := obj.(*provisioning.Repository) |
||||
if !ok { |
||||
return nil |
||||
} |
||||
|
||||
if repo.Status.Webhook == nil { |
||||
return nil |
||||
} |
||||
|
||||
if repo.Status.Webhook.Secret != "" { |
||||
secretName := repo.Name + webhookSecretSuffix |
||||
nameOrValue, err := secrets.Encrypt(ctx, repo, secretName, repo.Status.Webhook.Secret) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
repo.Status.Webhook.EncryptedSecret = nameOrValue |
||||
repo.Status.Webhook.Secret = "" |
||||
} |
||||
|
||||
return nil |
||||
} |
||||
} |
@ -0,0 +1,157 @@ |
||||
package webhooks |
||||
|
||||
import ( |
||||
"context" |
||||
"errors" |
||||
"testing" |
||||
|
||||
provisioning "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1" |
||||
"github.com/grafana/grafana/pkg/registry/apis/provisioning/secrets" |
||||
"github.com/stretchr/testify/assert" |
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" |
||||
"k8s.io/apimachinery/pkg/runtime" |
||||
) |
||||
|
||||
func TestMutator(t *testing.T) { |
||||
tests := []struct { |
||||
name string |
||||
obj runtime.Object |
||||
secret string |
||||
setupMocks func(*secrets.MockRepositorySecrets) |
||||
expectedEncryptedSecret string |
||||
expectedError string |
||||
}{ |
||||
{ |
||||
name: "successful secret encryption", |
||||
obj: &provisioning.Repository{ |
||||
ObjectMeta: metav1.ObjectMeta{ |
||||
Name: "test-repo", |
||||
Namespace: "default", |
||||
}, |
||||
Status: provisioning.RepositoryStatus{ |
||||
Webhook: &provisioning.WebhookStatus{ |
||||
Secret: "webhook-secret", |
||||
}, |
||||
}, |
||||
}, |
||||
setupMocks: func(mockSecrets *secrets.MockRepositorySecrets) { |
||||
mockSecrets.EXPECT().Encrypt( |
||||
context.Background(), |
||||
&provisioning.Repository{ |
||||
ObjectMeta: metav1.ObjectMeta{ |
||||
Name: "test-repo", |
||||
Namespace: "default", |
||||
}, |
||||
Status: provisioning.RepositoryStatus{ |
||||
Webhook: &provisioning.WebhookStatus{ |
||||
Secret: "webhook-secret", |
||||
}, |
||||
}, |
||||
}, |
||||
"test-repo-webhook-secret", |
||||
"webhook-secret", |
||||
).Return([]byte("encrypted-webhook-secret"), nil) |
||||
}, |
||||
expectedEncryptedSecret: "encrypted-webhook-secret", |
||||
}, |
||||
{ |
||||
name: "encryption error", |
||||
obj: &provisioning.Repository{ |
||||
ObjectMeta: metav1.ObjectMeta{ |
||||
Name: "test-repo", |
||||
Namespace: "default", |
||||
}, |
||||
Status: provisioning.RepositoryStatus{ |
||||
Webhook: &provisioning.WebhookStatus{ |
||||
Secret: "webhook-secret", |
||||
}, |
||||
}, |
||||
}, |
||||
setupMocks: func(mockSecrets *secrets.MockRepositorySecrets) { |
||||
mockSecrets.EXPECT().Encrypt( |
||||
context.Background(), |
||||
&provisioning.Repository{ |
||||
ObjectMeta: metav1.ObjectMeta{ |
||||
Name: "test-repo", |
||||
Namespace: "default", |
||||
}, |
||||
Status: provisioning.RepositoryStatus{ |
||||
Webhook: &provisioning.WebhookStatus{ |
||||
Secret: "webhook-secret", |
||||
}, |
||||
}, |
||||
}, |
||||
"test-repo-webhook-secret", |
||||
"webhook-secret", |
||||
).Return(nil, errors.New("encryption failed")) |
||||
}, |
||||
expectedError: "encryption failed", |
||||
}, |
||||
{ |
||||
name: "no webhook status", |
||||
obj: &provisioning.Repository{ |
||||
ObjectMeta: metav1.ObjectMeta{ |
||||
Name: "test-repo", |
||||
Namespace: "default", |
||||
}, |
||||
Status: provisioning.RepositoryStatus{ |
||||
Webhook: nil, |
||||
}, |
||||
}, |
||||
setupMocks: func(_ *secrets.MockRepositorySecrets) { |
||||
// No expectations
|
||||
}, |
||||
}, |
||||
{ |
||||
name: "empty secret", |
||||
obj: &provisioning.Repository{ |
||||
ObjectMeta: metav1.ObjectMeta{ |
||||
Name: "test-repo", |
||||
Namespace: "default", |
||||
}, |
||||
Status: provisioning.RepositoryStatus{ |
||||
Webhook: &provisioning.WebhookStatus{ |
||||
Secret: "", |
||||
}, |
||||
}, |
||||
}, |
||||
setupMocks: func(_ *secrets.MockRepositorySecrets) { |
||||
// No expectations
|
||||
}, |
||||
}, |
||||
{ |
||||
name: "non-repository object", |
||||
obj: &runtime.Unknown{}, |
||||
setupMocks: func(_ *secrets.MockRepositorySecrets) { |
||||
// No expectations
|
||||
}, |
||||
}, |
||||
} |
||||
|
||||
for _, tt := range tests { |
||||
t.Run(tt.name, func(t *testing.T) { |
||||
mockSecrets := secrets.NewMockRepositorySecrets(t) |
||||
tt.setupMocks(mockSecrets) |
||||
|
||||
mutator := Mutator(mockSecrets) |
||||
err := mutator(context.Background(), tt.obj) |
||||
|
||||
if tt.expectedError != "" { |
||||
assert.Error(t, err) |
||||
assert.Contains(t, err.Error(), tt.expectedError) |
||||
} else { |
||||
assert.NoError(t, err) |
||||
|
||||
// Check that secret was cleared and encrypted secret was set
|
||||
if repo, ok := tt.obj.(*provisioning.Repository); ok && repo.Status.Webhook != nil { |
||||
if tt.expectedEncryptedSecret != "" { |
||||
// Secret should be cleared after encryption
|
||||
assert.Empty(t, repo.Status.Webhook.Secret, "Secret should be cleared after encryption") |
||||
// EncryptedSecret should be set to the expected value
|
||||
assert.Equal(t, tt.expectedEncryptedSecret, string(repo.Status.Webhook.EncryptedSecret), "EncryptedSecret should match expected value") |
||||
} |
||||
} |
||||
} |
||||
}) |
||||
} |
||||
} |
Loading…
Reference in new issue