Provisioning: Use inline secrets for gitsync (#109908)
Co-authored-by: Clarity-89 <homes89@ukr.net> Co-authored-by: Roberto Jimenez Sanchez <roberto.jimenez@grafana.com>
This commit is contained in:
co-authored by
Clarity-89
Roberto Jimenez Sanchez
parent
04f392d37b
commit
ce65391067
@@ -31,7 +31,7 @@ type RepoGetter interface {
|
||||
// Given a repository configuration, return it as a repository instance
|
||||
// This will only error for un-recoverable system errors
|
||||
// the repository instance may or may not be valid/healthy
|
||||
AsRepository(ctx context.Context, cfg *provisioning.Repository) (repository.Repository, error)
|
||||
RepositoryFromConfig(ctx context.Context, r *provisioning.Repository) (repository.Repository, error)
|
||||
}
|
||||
|
||||
const loggerName = "provisioning-repository-controller"
|
||||
@@ -223,7 +223,7 @@ func (rc *RepositoryController) handleDelete(ctx context.Context, obj *provision
|
||||
|
||||
// Process any finalizers
|
||||
if len(obj.Finalizers) > 0 {
|
||||
repo, err := rc.repoGetter.AsRepository(ctx, obj)
|
||||
repo, err := rc.repoGetter.RepositoryFromConfig(ctx, obj)
|
||||
if err != nil {
|
||||
logger.Warn("unable to get repository for cleanup")
|
||||
} else {
|
||||
@@ -438,7 +438,7 @@ func (rc *RepositoryController) process(item *queueItem) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
repo, err := rc.repoGetter.AsRepository(ctx, obj)
|
||||
repo, err := rc.repoGetter.RepositoryFromConfig(ctx, obj)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to create repository from configuration: %w", err)
|
||||
}
|
||||
|
||||
@@ -3,13 +3,14 @@ package provisioning
|
||||
import (
|
||||
"context"
|
||||
|
||||
"k8s.io/apiserver/pkg/authorization/authorizer"
|
||||
"k8s.io/apiserver/pkg/registry/rest"
|
||||
"k8s.io/kube-openapi/pkg/spec3"
|
||||
|
||||
provisioning "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/provisioning/controller"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/provisioning/jobs"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/provisioning/repository"
|
||||
"k8s.io/apiserver/pkg/authorization/authorizer"
|
||||
"k8s.io/apiserver/pkg/registry/rest"
|
||||
"k8s.io/kube-openapi/pkg/spec3"
|
||||
)
|
||||
|
||||
type Extra interface {
|
||||
@@ -17,7 +18,7 @@ type Extra interface {
|
||||
UpdateStorage(storage map[string]rest.Storage) error
|
||||
PostProcessOpenAPI(oas *spec3.OpenAPI) error
|
||||
GetJobWorkers() []jobs.Worker
|
||||
AsRepository(ctx context.Context, r *provisioning.Repository) (repository.Repository, error)
|
||||
AsRepository(ctx context.Context, r *provisioning.Repository, secure repository.SecureValues) (repository.Repository, error)
|
||||
RepositoryTypes() []provisioning.RepositoryType
|
||||
Mutators() []controller.Mutator
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@ import (
|
||||
client "github.com/grafana/grafana/apps/provisioning/pkg/generated/clientset/versioned/typed/provisioning/v0alpha1"
|
||||
informers "github.com/grafana/grafana/apps/provisioning/pkg/generated/informers/externalversions"
|
||||
listers "github.com/grafana/grafana/apps/provisioning/pkg/generated/listers/provisioning/v0alpha1"
|
||||
commonMeta "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1"
|
||||
"github.com/grafana/grafana/pkg/apimachinery/identity"
|
||||
apiutils "github.com/grafana/grafana/pkg/apimachinery/utils"
|
||||
"github.com/grafana/grafana/pkg/apiserver/readonly"
|
||||
@@ -56,7 +57,6 @@ import (
|
||||
"github.com/grafana/grafana/pkg/registry/apis/provisioning/resources"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/provisioning/resources/signature"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/provisioning/safepath"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/provisioning/secrets"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/provisioning/usage"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/secret"
|
||||
"github.com/grafana/grafana/pkg/services/apiserver"
|
||||
@@ -98,20 +98,19 @@ type APIBuilder struct {
|
||||
jobs.Queue
|
||||
jobs.Store
|
||||
}
|
||||
jobHistoryConfig *JobHistoryConfig
|
||||
jobHistoryLoki *jobs.LokiJobHistory
|
||||
resourceLister resources.ResourceLister
|
||||
repositoryLister listers.RepositoryLister
|
||||
legacyMigrator legacy.LegacyMigrator
|
||||
storageStatus dualwrite.Service
|
||||
unified resource.ResourceClient
|
||||
decryptSvc secret.DecryptService
|
||||
repositorySecrets secrets.RepositorySecrets // << Will be removed when the decryptSvc usage is stable
|
||||
client client.ProvisioningV0alpha1Interface
|
||||
access authlib.AccessChecker
|
||||
mutators []controller.Mutator
|
||||
statusPatcher *controller.RepositoryStatusPatcher
|
||||
healthChecker *controller.HealthChecker
|
||||
jobHistoryConfig *JobHistoryConfig
|
||||
jobHistoryLoki *jobs.LokiJobHistory
|
||||
resourceLister resources.ResourceLister
|
||||
repositoryLister listers.RepositoryLister
|
||||
legacyMigrator legacy.LegacyMigrator
|
||||
storageStatus dualwrite.Service
|
||||
unified resource.ResourceClient
|
||||
decrypter repository.Decrypter
|
||||
client client.ProvisioningV0alpha1Interface
|
||||
access authlib.AccessChecker
|
||||
mutators []controller.Mutator
|
||||
statusPatcher *controller.RepositoryStatusPatcher
|
||||
healthChecker *controller.HealthChecker
|
||||
// Extras provides additional functionality to the API.
|
||||
extras []Extra
|
||||
availableRepositoryTypes map[provisioning.RepositoryType]bool
|
||||
@@ -130,7 +129,6 @@ func NewAPIBuilder(
|
||||
storageStatus dualwrite.Service,
|
||||
usageStats usagestats.Service,
|
||||
decryptSvc secret.DecryptService,
|
||||
repositorySecrets secrets.RepositorySecrets,
|
||||
access authlib.AccessChecker,
|
||||
tracer tracing.Tracer,
|
||||
extraBuilders []ExtraBuilder,
|
||||
@@ -141,8 +139,8 @@ func NewAPIBuilder(
|
||||
resourceLister := resources.NewResourceLister(unified, unified, legacyMigrator, storageStatus)
|
||||
|
||||
mutators := []controller.Mutator{
|
||||
git.Mutator(repositorySecrets),
|
||||
github.Mutator(repositorySecrets),
|
||||
git.Mutator(),
|
||||
github.Mutator(),
|
||||
}
|
||||
|
||||
b := &APIBuilder{
|
||||
@@ -159,8 +157,7 @@ func NewAPIBuilder(
|
||||
legacyMigrator: legacyMigrator,
|
||||
storageStatus: storageStatus,
|
||||
unified: unified,
|
||||
decryptSvc: decryptSvc,
|
||||
repositorySecrets: repositorySecrets,
|
||||
decrypter: repository.DecryptService(decryptSvc),
|
||||
access: access,
|
||||
jobHistoryConfig: jobHistoryConfig,
|
||||
availableRepositoryTypes: map[provisioning.RepositoryType]bool{
|
||||
@@ -233,7 +230,6 @@ func RegisterAPIService(
|
||||
storageStatus dualwrite.Service,
|
||||
usageStats usagestats.Service,
|
||||
decryptSvc secret.DecryptService,
|
||||
repositorySecrets secrets.RepositorySecrets,
|
||||
tracer tracing.Tracer,
|
||||
extraBuilders []ExtraBuilder,
|
||||
) (*APIBuilder, error) {
|
||||
@@ -251,7 +247,6 @@ func RegisterAPIService(
|
||||
legacyMigrator, storageStatus,
|
||||
usageStats,
|
||||
decryptSvc,
|
||||
repositorySecrets,
|
||||
access,
|
||||
tracer,
|
||||
extraBuilders,
|
||||
@@ -562,7 +557,7 @@ func (b *APIBuilder) Validate(ctx context.Context, a admission.Attributes, o adm
|
||||
return nil
|
||||
}
|
||||
|
||||
repo, err := b.asRepository(ctx, obj)
|
||||
repo, err := b.asRepository(ctx, obj, a.GetOldObject())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -571,7 +566,7 @@ func (b *APIBuilder) Validate(ctx context.Context, a admission.Attributes, o adm
|
||||
cfg := repo.Config()
|
||||
|
||||
if a.GetOperation() == admission.Update {
|
||||
oldRepo, err := b.asRepository(ctx, a.GetOldObject())
|
||||
oldRepo, err := b.asRepository(ctx, a.GetOldObject(), nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("get old repository for update: %w", err)
|
||||
}
|
||||
@@ -1263,7 +1258,7 @@ func (b *APIBuilder) GetRepository(ctx context.Context, name string) (repository
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return b.asRepository(ctx, obj)
|
||||
return b.asRepository(ctx, obj, nil)
|
||||
}
|
||||
|
||||
func (b *APIBuilder) GetHealthyRepository(ctx context.Context, name string) (repository.Repository, error) {
|
||||
@@ -1283,7 +1278,7 @@ func (b *APIBuilder) GetHealthyRepository(ctx context.Context, name string) (rep
|
||||
return repo, err
|
||||
}
|
||||
|
||||
func (b *APIBuilder) asRepository(ctx context.Context, obj runtime.Object) (repository.Repository, error) {
|
||||
func (b *APIBuilder) asRepository(ctx context.Context, obj runtime.Object, old runtime.Object) (repository.Repository, error) {
|
||||
if obj == nil {
|
||||
return nil, fmt.Errorf("missing repository object")
|
||||
}
|
||||
@@ -1291,13 +1286,30 @@ func (b *APIBuilder) asRepository(ctx context.Context, obj runtime.Object) (repo
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("expected repository configuration")
|
||||
}
|
||||
return b.AsRepository(ctx, r)
|
||||
|
||||
// Copy previous values if they exist
|
||||
if old != nil {
|
||||
o, ok := old.(*provisioning.Repository)
|
||||
if ok && !o.Secure.IsZero() {
|
||||
if r.Secure.Token.IsZero() {
|
||||
r.Secure.Token = o.Secure.Token
|
||||
}
|
||||
if r.Secure.WebhookSecret.IsZero() {
|
||||
r.Secure.WebhookSecret = o.Secure.WebhookSecret
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return b.RepositoryFromConfig(ctx, r)
|
||||
}
|
||||
|
||||
func (b *APIBuilder) AsRepository(ctx context.Context, r *provisioning.Repository) (repository.Repository, error) {
|
||||
func (b *APIBuilder) RepositoryFromConfig(ctx context.Context, r *provisioning.Repository) (repository.Repository, error) {
|
||||
// Prepare a decrypter
|
||||
secure := b.decrypter(r)
|
||||
|
||||
// Try first with any extra
|
||||
for _, extra := range b.extras {
|
||||
r, err := extra.AsRepository(ctx, r)
|
||||
r, err := extra.AsRepository(ctx, r, secure)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("convert repository for extra %T: %w", extra, err)
|
||||
}
|
||||
@@ -1307,6 +1319,15 @@ func (b *APIBuilder) AsRepository(ctx context.Context, r *provisioning.Repositor
|
||||
}
|
||||
}
|
||||
|
||||
var token commonMeta.RawSecureValue
|
||||
if r.Secure.Token.IsZero() {
|
||||
t, err := secure.Token(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to decrypt token: %w", err)
|
||||
}
|
||||
token = t
|
||||
}
|
||||
|
||||
switch r.Spec.Type {
|
||||
case provisioning.BitbucketRepositoryType:
|
||||
return nil, errors.New("repository type bitbucket is not available")
|
||||
@@ -1315,26 +1336,15 @@ func (b *APIBuilder) AsRepository(ctx context.Context, r *provisioning.Repositor
|
||||
case provisioning.LocalRepositoryType:
|
||||
return local.NewLocal(r, b.localFileResolver), nil
|
||||
case provisioning.GitRepositoryType:
|
||||
// Decrypt token if needed
|
||||
token := r.Spec.Git.Token
|
||||
if token == "" && len(r.Spec.Git.EncryptedToken) > 0 {
|
||||
decrypted, err := b.repositorySecrets.Decrypt(ctx, r, string(r.Spec.Git.EncryptedToken))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("decrypt git token: %w", err)
|
||||
}
|
||||
token = string(decrypted)
|
||||
}
|
||||
|
||||
cfg := git.RepositoryConfig{
|
||||
URL: r.Spec.Git.URL,
|
||||
Branch: r.Spec.Git.Branch,
|
||||
Path: r.Spec.Git.Path,
|
||||
TokenUser: r.Spec.Git.TokenUser,
|
||||
Token: token,
|
||||
EncryptedToken: r.Spec.Git.EncryptedToken,
|
||||
URL: r.Spec.Git.URL,
|
||||
Branch: r.Spec.Git.Branch,
|
||||
Path: r.Spec.Git.Path,
|
||||
TokenUser: r.Spec.Git.TokenUser,
|
||||
Token: token,
|
||||
}
|
||||
|
||||
return git.NewGitRepository(ctx, r, cfg, b.repositorySecrets)
|
||||
return git.NewGitRepository(ctx, r, cfg)
|
||||
case provisioning.GitHubRepositoryType:
|
||||
logger := logging.FromContext(ctx).With("url", r.Spec.GitHub.URL, "branch", r.Spec.GitHub.Branch, "path", r.Spec.GitHub.Path)
|
||||
logger.Info("Instantiating Github repository")
|
||||
@@ -1344,30 +1354,19 @@ func (b *APIBuilder) AsRepository(ctx context.Context, r *provisioning.Repositor
|
||||
return nil, fmt.Errorf("github configuration is required for nano git")
|
||||
}
|
||||
|
||||
// Decrypt GitHub token if needed
|
||||
ghToken := ghCfg.Token
|
||||
if ghToken == "" && len(ghCfg.EncryptedToken) > 0 {
|
||||
decrypted, err := b.repositorySecrets.Decrypt(ctx, r, string(ghCfg.EncryptedToken))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("decrypt github token: %w", err)
|
||||
}
|
||||
ghToken = string(decrypted)
|
||||
}
|
||||
|
||||
gitCfg := git.RepositoryConfig{
|
||||
URL: ghCfg.URL,
|
||||
Branch: ghCfg.Branch,
|
||||
Path: ghCfg.Path,
|
||||
Token: ghToken,
|
||||
EncryptedToken: ghCfg.EncryptedToken,
|
||||
URL: ghCfg.URL,
|
||||
Branch: ghCfg.Branch,
|
||||
Path: ghCfg.Path,
|
||||
Token: token,
|
||||
}
|
||||
|
||||
gitRepo, err := git.NewGitRepository(ctx, r, gitCfg, b.repositorySecrets)
|
||||
gitRepo, err := git.NewGitRepository(ctx, r, gitCfg)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error creating git repository: %w", err)
|
||||
}
|
||||
|
||||
ghRepo, err := github.NewGitHub(ctx, r, gitRepo, b.ghFactory, ghToken, b.repositorySecrets)
|
||||
ghRepo, err := github.NewGitHub(ctx, r, gitRepo, b.ghFactory, token)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error creating github repository: %w", err)
|
||||
}
|
||||
|
||||
@@ -12,7 +12,6 @@ type GitRepository interface {
|
||||
repository.Writer
|
||||
repository.Reader
|
||||
repository.StageableRepository
|
||||
repository.Hooks
|
||||
URL() string
|
||||
Branch() string
|
||||
}
|
||||
|
||||
@@ -9,10 +9,9 @@ import (
|
||||
|
||||
provisioning "github.com/grafana/grafana/apps/provisioning/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 {
|
||||
func Mutator() controller.Mutator {
|
||||
return func(ctx context.Context, obj runtime.Object) error {
|
||||
repo, ok := obj.(*provisioning.Repository)
|
||||
if !ok {
|
||||
@@ -40,16 +39,6 @@ func Mutator(secrets secrets.RepositorySecrets) controller.Mutator {
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,98 +2,23 @@ package git
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
provisioning "github.com/grafana/grafana/apps/provisioning/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"
|
||||
|
||||
provisioning "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1"
|
||||
)
|
||||
|
||||
func TestMutator(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
obj runtime.Object
|
||||
token string
|
||||
setupMocks func(*secrets.MockRepositorySecrets)
|
||||
expectedToken string
|
||||
expectedEncryptedToken string
|
||||
expectedError string
|
||||
expectedURL string
|
||||
name string
|
||||
obj runtime.Object
|
||||
token string
|
||||
expectedError string
|
||||
expectedURL string
|
||||
}{
|
||||
{
|
||||
name: "successful token encryption",
|
||||
obj: &provisioning.Repository{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-repo",
|
||||
Namespace: "default",
|
||||
},
|
||||
Spec: provisioning.RepositorySpec{
|
||||
Type: provisioning.GitRepositoryType,
|
||||
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{
|
||||
Type: provisioning.GitRepositoryType,
|
||||
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{
|
||||
Type: provisioning.GitRepositoryType,
|
||||
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{
|
||||
Type: provisioning.GitRepositoryType,
|
||||
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{
|
||||
@@ -106,11 +31,7 @@ func TestMutator(t *testing.T) {
|
||||
Git: nil,
|
||||
},
|
||||
},
|
||||
setupMocks: func(mockSecrets *secrets.MockRepositorySecrets) {
|
||||
// No expectations
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: "no git spec for git repository type",
|
||||
obj: &provisioning.Repository{
|
||||
@@ -123,9 +44,6 @@ func TestMutator(t *testing.T) {
|
||||
Git: nil,
|
||||
},
|
||||
},
|
||||
setupMocks: func(mockSecrets *secrets.MockRepositorySecrets) {
|
||||
// No expectations
|
||||
},
|
||||
expectedError: "git configuration is required for git repository type",
|
||||
},
|
||||
{
|
||||
@@ -137,21 +55,13 @@ func TestMutator(t *testing.T) {
|
||||
},
|
||||
Spec: provisioning.RepositorySpec{
|
||||
Type: provisioning.GitRepositoryType,
|
||||
Git: &provisioning.GitRepositoryConfig{
|
||||
Token: "",
|
||||
},
|
||||
Git: &provisioning.GitRepositoryConfig{},
|
||||
},
|
||||
},
|
||||
setupMocks: func(mockSecrets *secrets.MockRepositorySecrets) {
|
||||
// No expectations
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "non-repository object",
|
||||
obj: &runtime.Unknown{},
|
||||
setupMocks: func(mockSecrets *secrets.MockRepositorySecrets) {
|
||||
// No expectations
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "URL normalization - add .git suffix",
|
||||
@@ -167,9 +77,6 @@ func TestMutator(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
setupMocks: func(mockSecrets *secrets.MockRepositorySecrets) {
|
||||
// No expectations
|
||||
},
|
||||
expectedURL: "https://github.com/grafana/grafana.git",
|
||||
},
|
||||
{
|
||||
@@ -186,9 +93,6 @@ func TestMutator(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
setupMocks: func(mockSecrets *secrets.MockRepositorySecrets) {
|
||||
// No expectations
|
||||
},
|
||||
expectedURL: "https://github.com/grafana/grafana.git",
|
||||
},
|
||||
{
|
||||
@@ -205,9 +109,6 @@ func TestMutator(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
setupMocks: func(mockSecrets *secrets.MockRepositorySecrets) {
|
||||
// No expectations
|
||||
},
|
||||
expectedURL: "https://github.com/grafana/grafana.git",
|
||||
},
|
||||
{
|
||||
@@ -224,9 +125,6 @@ func TestMutator(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
setupMocks: func(mockSecrets *secrets.MockRepositorySecrets) {
|
||||
// No expectations
|
||||
},
|
||||
expectedURL: "https://github.com/grafana/grafana.git",
|
||||
},
|
||||
{
|
||||
@@ -243,19 +141,13 @@ func TestMutator(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
setupMocks: func(mockSecrets *secrets.MockRepositorySecrets) {
|
||||
// No expectations
|
||||
},
|
||||
expectedURL: "",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
mockSecrets := secrets.NewMockRepositorySecrets(t)
|
||||
tt.setupMocks(mockSecrets)
|
||||
|
||||
mutator := Mutator(mockSecrets)
|
||||
mutator := Mutator()
|
||||
err := mutator(context.Background(), tt.obj)
|
||||
|
||||
if tt.expectedError != "" {
|
||||
@@ -266,13 +158,6 @@ func TestMutator(t *testing.T) {
|
||||
|
||||
// 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")
|
||||
}
|
||||
|
||||
// Check URL normalization
|
||||
if tt.expectedURL != "" {
|
||||
assert.Equal(t, tt.expectedURL, repo.Spec.Git.URL, "URL should be normalized correctly")
|
||||
|
||||
@@ -17,9 +17,9 @@ import (
|
||||
|
||||
"github.com/grafana/grafana-app-sdk/logging"
|
||||
provisioning "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1"
|
||||
common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/provisioning/repository"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/provisioning/safepath"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/provisioning/secrets"
|
||||
"github.com/grafana/nanogit"
|
||||
"github.com/grafana/nanogit/log"
|
||||
"github.com/grafana/nanogit/options"
|
||||
@@ -27,16 +27,12 @@ import (
|
||||
"github.com/grafana/nanogit/protocol/hash"
|
||||
)
|
||||
|
||||
//nolint:gosec // This is a constant for a secret suffix
|
||||
const gitTokenSecretSuffix = "-git-token"
|
||||
|
||||
type RepositoryConfig struct {
|
||||
URL string
|
||||
Branch string
|
||||
TokenUser string
|
||||
Token string
|
||||
EncryptedToken []byte
|
||||
Path string
|
||||
URL string
|
||||
Branch string
|
||||
TokenUser string
|
||||
Token common.RawSecureValue
|
||||
Path string
|
||||
}
|
||||
|
||||
// Make sure all public functions of this struct call the (*gitRepository).logger function, to ensure the Git repo details are included.
|
||||
@@ -44,23 +40,21 @@ type gitRepository struct {
|
||||
config *provisioning.Repository
|
||||
gitConfig RepositoryConfig
|
||||
client nanogit.Client
|
||||
secrets secrets.RepositorySecrets
|
||||
}
|
||||
|
||||
func NewGitRepository(
|
||||
ctx context.Context,
|
||||
config *provisioning.Repository,
|
||||
gitConfig RepositoryConfig,
|
||||
secrets secrets.RepositorySecrets,
|
||||
) (GitRepository, error) {
|
||||
var opts []options.Option
|
||||
if len(gitConfig.Token) > 0 {
|
||||
if !gitConfig.Token.IsZero() {
|
||||
tokenUser := gitConfig.TokenUser
|
||||
if tokenUser == "" {
|
||||
tokenUser = "git"
|
||||
}
|
||||
|
||||
opts = append(opts, options.WithBasicAuth(tokenUser, gitConfig.Token))
|
||||
opts = append(opts, options.WithBasicAuth(tokenUser, string(gitConfig.Token)))
|
||||
}
|
||||
|
||||
client, err := nanogit.NewHTTPClient(gitConfig.URL, opts...)
|
||||
@@ -72,7 +66,6 @@ func NewGitRepository(
|
||||
config: config,
|
||||
gitConfig: gitConfig,
|
||||
client: client,
|
||||
secrets: secrets,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -106,10 +99,10 @@ func (r *gitRepository) Validate() (list field.ErrorList) {
|
||||
list = append(list, field.Invalid(field.NewPath("spec", t, "branch"), cfg.Branch, "invalid branch name"))
|
||||
}
|
||||
|
||||
// If the repository has workflows, we require a token or encrypted token
|
||||
// Readonly repositories may not need a token (if public)
|
||||
if len(r.config.Spec.Workflows) > 0 {
|
||||
if cfg.Token == "" && len(cfg.EncryptedToken) == 0 {
|
||||
list = append(list, field.Required(field.NewPath("spec", t, "token"), "a git access token is required"))
|
||||
if cfg.Token == "" && r.config.Secure.Token.IsZero() {
|
||||
list = append(list, field.Required(field.NewPath("secure", "token"), "a git access token is required"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -166,7 +159,7 @@ func (r *gitRepository) Test(ctx context.Context) (*provisioning.TestResults, er
|
||||
Success: false,
|
||||
Errors: []provisioning.ErrorDetails{{
|
||||
Type: metav1.CauseTypeFieldValueInvalid,
|
||||
Field: field.NewPath("spec", t, "token").String(),
|
||||
Field: field.NewPath("secure", "token").String(),
|
||||
Detail: detail,
|
||||
}},
|
||||
}, nil
|
||||
@@ -832,23 +825,3 @@ func (r *gitRepository) logger(ctx context.Context, ref string) (context.Context
|
||||
|
||||
return ctx, logger
|
||||
}
|
||||
|
||||
func (r *gitRepository) OnCreate(_ context.Context) ([]map[string]interface{}, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (r *gitRepository) OnUpdate(_ context.Context) ([]map[string]interface{}, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (r *gitRepository) OnDelete(ctx context.Context) error {
|
||||
logger := logging.FromContext(ctx)
|
||||
secretName := r.config.Name + gitTokenSecretSuffix
|
||||
if err := r.secrets.Delete(ctx, r.config, secretName); err != nil {
|
||||
return fmt.Errorf("delete git token secret: %w", err)
|
||||
}
|
||||
|
||||
logger.Info("Deleted git token secret", "secretName", secretName)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -14,7 +14,6 @@ import (
|
||||
|
||||
provisioning "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/provisioning/repository"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/provisioning/secrets"
|
||||
"github.com/grafana/nanogit"
|
||||
"github.com/grafana/nanogit/mocks"
|
||||
"github.com/grafana/nanogit/protocol"
|
||||
@@ -152,7 +151,7 @@ func TestGitRepository_Validate(t *testing.T) {
|
||||
Token: "", // Empty token
|
||||
},
|
||||
want: field.ErrorList{
|
||||
field.Required(field.NewPath("spec", "test_type", "token"), "a git access token is required"),
|
||||
field.Required(field.NewPath("secure", "token"), "a git access token is required"),
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -291,10 +290,9 @@ func TestNewGit(t *testing.T) {
|
||||
Path: "configs",
|
||||
}
|
||||
|
||||
mockSecrets := secrets.NewMockRepositorySecrets(t)
|
||||
// This should succeed in creating the client but won't be able to connect
|
||||
// We just test that the basic structure is created correctly
|
||||
gitRepo, err := NewGitRepository(ctx, config, gitConfig, mockSecrets)
|
||||
gitRepo, err := NewGitRepository(ctx, config, gitConfig)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, gitRepo)
|
||||
require.Equal(t, "https://git.example.com/owner/repo.git", gitRepo.URL())
|
||||
@@ -502,7 +500,7 @@ func TestGitRepository_Test(t *testing.T) {
|
||||
Errors: []provisioning.ErrorDetails{
|
||||
{
|
||||
Type: metav1.CauseTypeFieldValueInvalid,
|
||||
Field: field.NewPath("spec", "test_type", "token").String(),
|
||||
Field: field.NewPath("secure", "token").String(),
|
||||
Detail: "failed check if authorized: auth error",
|
||||
},
|
||||
},
|
||||
@@ -523,7 +521,7 @@ func TestGitRepository_Test(t *testing.T) {
|
||||
Errors: []provisioning.ErrorDetails{
|
||||
{
|
||||
Type: metav1.CauseTypeFieldValueInvalid,
|
||||
Field: field.NewPath("spec", "test_type", "token").String(),
|
||||
Field: field.NewPath("secure", "token").String(),
|
||||
Detail: "not authorized",
|
||||
},
|
||||
},
|
||||
@@ -1862,8 +1860,7 @@ func TestNewGitRepository(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
mockSecrets := secrets.NewMockRepositorySecrets(t)
|
||||
gitRepo, err := NewGitRepository(ctx, config, tt.gitConfig, mockSecrets)
|
||||
gitRepo, err := NewGitRepository(ctx, config, tt.gitConfig)
|
||||
|
||||
if tt.wantError {
|
||||
require.Error(t, err)
|
||||
@@ -2822,8 +2819,7 @@ func TestGitRepository_NewGitRepository_ClientError(t *testing.T) {
|
||||
Path: "configs",
|
||||
}
|
||||
|
||||
mockSecrets := secrets.NewMockRepositorySecrets(t)
|
||||
gitRepo, err := NewGitRepository(ctx, config, gitConfig, mockSecrets)
|
||||
gitRepo, err := NewGitRepository(ctx, config, gitConfig)
|
||||
|
||||
// We expect this to fail during client creation
|
||||
require.Error(t, err)
|
||||
@@ -3737,82 +3733,6 @@ func TestGitRepository_CompareFiles_FilesOutsideConfiguredPath_AllStatuses(t *te
|
||||
}
|
||||
}
|
||||
|
||||
func TestGitRepository_OnDelete(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
setupMock func(*secrets.MockRepositorySecrets)
|
||||
config *provisioning.Repository
|
||||
expectedError string
|
||||
}{
|
||||
{
|
||||
name: "successful secret deletion",
|
||||
setupMock: func(mockSecrets *secrets.MockRepositorySecrets) {
|
||||
mockSecrets.EXPECT().Delete(
|
||||
context.Background(),
|
||||
&provisioning.Repository{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-repo",
|
||||
Namespace: "default",
|
||||
},
|
||||
},
|
||||
"test-repo"+gitTokenSecretSuffix,
|
||||
).Return(nil)
|
||||
},
|
||||
config: &provisioning.Repository{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-repo",
|
||||
Namespace: "default",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "secret deletion error",
|
||||
setupMock: func(mockSecrets *secrets.MockRepositorySecrets) {
|
||||
mockSecrets.EXPECT().Delete(
|
||||
context.Background(),
|
||||
&provisioning.Repository{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-repo",
|
||||
Namespace: "default",
|
||||
},
|
||||
},
|
||||
"test-repo"+gitTokenSecretSuffix,
|
||||
).Return(errors.New("failed to delete secret"))
|
||||
},
|
||||
config: &provisioning.Repository{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-repo",
|
||||
Namespace: "default",
|
||||
},
|
||||
},
|
||||
expectedError: "delete git token secret: failed to delete secret",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
mockSecrets := secrets.NewMockRepositorySecrets(t)
|
||||
tt.setupMock(mockSecrets)
|
||||
|
||||
gitRepo := &gitRepository{
|
||||
config: tt.config,
|
||||
secrets: mockSecrets,
|
||||
}
|
||||
|
||||
err := gitRepo.OnDelete(context.Background())
|
||||
|
||||
if tt.expectedError != "" {
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), tt.expectedError)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
mockSecrets.AssertExpectations(t)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGitRepository_Move(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
|
||||
@@ -6,6 +6,8 @@ import (
|
||||
|
||||
"github.com/google/go-github/v70/github"
|
||||
"golang.org/x/oauth2"
|
||||
|
||||
common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1"
|
||||
)
|
||||
|
||||
// Factory creates new GitHub clients.
|
||||
@@ -20,16 +22,15 @@ func ProvideFactory() *Factory {
|
||||
return &Factory{}
|
||||
}
|
||||
|
||||
func (r *Factory) New(ctx context.Context, ghToken string) Client {
|
||||
func (r *Factory) New(ctx context.Context, ghToken common.RawSecureValue) Client {
|
||||
if r.Client != nil {
|
||||
return NewClient(github.NewClient(r.Client))
|
||||
}
|
||||
|
||||
tokenSrc := oauth2.StaticTokenSource(
|
||||
&oauth2.Token{AccessToken: ghToken},
|
||||
)
|
||||
|
||||
if len(ghToken) > 0 {
|
||||
if !ghToken.IsZero() {
|
||||
tokenSrc := oauth2.StaticTokenSource(
|
||||
&oauth2.Token{AccessToken: string(ghToken)},
|
||||
)
|
||||
tokenClient := oauth2.NewClient(ctx, tokenSrc)
|
||||
return NewClient(github.NewClient(tokenClient))
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Code generated by mockery v2.52.4. DO NOT EDIT.
|
||||
// Code generated by mockery v2.53.4. DO NOT EDIT.
|
||||
|
||||
package github
|
||||
|
||||
@@ -503,168 +503,6 @@ func (_c *MockGithubRepository_Move_Call) RunAndReturn(run func(context.Context,
|
||||
return _c
|
||||
}
|
||||
|
||||
// OnCreate provides a mock function with given fields: ctx
|
||||
func (_m *MockGithubRepository) OnCreate(ctx context.Context) ([]map[string]interface{}, error) {
|
||||
ret := _m.Called(ctx)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for OnCreate")
|
||||
}
|
||||
|
||||
var r0 []map[string]interface{}
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context) ([]map[string]interface{}, error)); ok {
|
||||
return rf(ctx)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(context.Context) []map[string]interface{}); ok {
|
||||
r0 = rf(ctx)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]map[string]interface{})
|
||||
}
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(context.Context) error); ok {
|
||||
r1 = rf(ctx)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// MockGithubRepository_OnCreate_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'OnCreate'
|
||||
type MockGithubRepository_OnCreate_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// OnCreate is a helper method to define mock.On call
|
||||
// - ctx context.Context
|
||||
func (_e *MockGithubRepository_Expecter) OnCreate(ctx interface{}) *MockGithubRepository_OnCreate_Call {
|
||||
return &MockGithubRepository_OnCreate_Call{Call: _e.mock.On("OnCreate", ctx)}
|
||||
}
|
||||
|
||||
func (_c *MockGithubRepository_OnCreate_Call) Run(run func(ctx context.Context)) *MockGithubRepository_OnCreate_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run(args[0].(context.Context))
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockGithubRepository_OnCreate_Call) Return(_a0 []map[string]interface{}, _a1 error) *MockGithubRepository_OnCreate_Call {
|
||||
_c.Call.Return(_a0, _a1)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockGithubRepository_OnCreate_Call) RunAndReturn(run func(context.Context) ([]map[string]interface{}, error)) *MockGithubRepository_OnCreate_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// OnDelete provides a mock function with given fields: ctx
|
||||
func (_m *MockGithubRepository) OnDelete(ctx context.Context) error {
|
||||
ret := _m.Called(ctx)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for OnDelete")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context) error); ok {
|
||||
r0 = rf(ctx)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// MockGithubRepository_OnDelete_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'OnDelete'
|
||||
type MockGithubRepository_OnDelete_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// OnDelete is a helper method to define mock.On call
|
||||
// - ctx context.Context
|
||||
func (_e *MockGithubRepository_Expecter) OnDelete(ctx interface{}) *MockGithubRepository_OnDelete_Call {
|
||||
return &MockGithubRepository_OnDelete_Call{Call: _e.mock.On("OnDelete", ctx)}
|
||||
}
|
||||
|
||||
func (_c *MockGithubRepository_OnDelete_Call) Run(run func(ctx context.Context)) *MockGithubRepository_OnDelete_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run(args[0].(context.Context))
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockGithubRepository_OnDelete_Call) Return(_a0 error) *MockGithubRepository_OnDelete_Call {
|
||||
_c.Call.Return(_a0)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockGithubRepository_OnDelete_Call) RunAndReturn(run func(context.Context) error) *MockGithubRepository_OnDelete_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// OnUpdate provides a mock function with given fields: ctx
|
||||
func (_m *MockGithubRepository) OnUpdate(ctx context.Context) ([]map[string]interface{}, error) {
|
||||
ret := _m.Called(ctx)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for OnUpdate")
|
||||
}
|
||||
|
||||
var r0 []map[string]interface{}
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context) ([]map[string]interface{}, error)); ok {
|
||||
return rf(ctx)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(context.Context) []map[string]interface{}); ok {
|
||||
r0 = rf(ctx)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]map[string]interface{})
|
||||
}
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(context.Context) error); ok {
|
||||
r1 = rf(ctx)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// MockGithubRepository_OnUpdate_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'OnUpdate'
|
||||
type MockGithubRepository_OnUpdate_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// OnUpdate is a helper method to define mock.On call
|
||||
// - ctx context.Context
|
||||
func (_e *MockGithubRepository_Expecter) OnUpdate(ctx interface{}) *MockGithubRepository_OnUpdate_Call {
|
||||
return &MockGithubRepository_OnUpdate_Call{Call: _e.mock.On("OnUpdate", ctx)}
|
||||
}
|
||||
|
||||
func (_c *MockGithubRepository_OnUpdate_Call) Run(run func(ctx context.Context)) *MockGithubRepository_OnUpdate_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run(args[0].(context.Context))
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockGithubRepository_OnUpdate_Call) Return(_a0 []map[string]interface{}, _a1 error) *MockGithubRepository_OnUpdate_Call {
|
||||
_c.Call.Return(_a0, _a1)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockGithubRepository_OnUpdate_Call) RunAndReturn(run func(context.Context) ([]map[string]interface{}, error)) *MockGithubRepository_OnUpdate_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// Owner provides a mock function with no fields
|
||||
func (_m *MockGithubRepository) Owner() string {
|
||||
ret := _m.Called()
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Code generated by mockery v2.52.4. DO NOT EDIT.
|
||||
// Code generated by mockery v2.53.4. DO NOT EDIT.
|
||||
|
||||
package github
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Code generated by mockery v2.52.4. DO NOT EDIT.
|
||||
// Code generated by mockery v2.53.4. DO NOT EDIT.
|
||||
|
||||
package github
|
||||
|
||||
|
||||
@@ -8,10 +8,9 @@ import (
|
||||
|
||||
provisioning "github.com/grafana/grafana/apps/provisioning/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 {
|
||||
func Mutator() controller.Mutator {
|
||||
return func(ctx context.Context, obj runtime.Object) error {
|
||||
repo, ok := obj.(*provisioning.Repository)
|
||||
if !ok {
|
||||
@@ -31,16 +30,6 @@ func Mutator(secrets secrets.RepositorySecrets) controller.Mutator {
|
||||
repo.Spec.GitHub.URL = url
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,25 +2,21 @@ package github
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
provisioning "github.com/grafana/grafana/apps/provisioning/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"
|
||||
|
||||
provisioning "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1"
|
||||
)
|
||||
|
||||
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 string
|
||||
obj runtime.Object
|
||||
token string
|
||||
expectedError string
|
||||
}{
|
||||
{
|
||||
name: "trims trailing .git and slash from GitHub URL",
|
||||
@@ -35,9 +31,6 @@ func TestMutator(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
setupMocks: func(mockSecrets *secrets.MockRepositorySecrets) {},
|
||||
expectedToken: "",
|
||||
expectedEncryptedToken: "",
|
||||
},
|
||||
{
|
||||
name: "trims only trailing slash from GitHub URL",
|
||||
@@ -52,9 +45,6 @@ func TestMutator(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
setupMocks: func(mockSecrets *secrets.MockRepositorySecrets) {},
|
||||
expectedToken: "",
|
||||
expectedEncryptedToken: "",
|
||||
},
|
||||
{
|
||||
name: "trims only trailing .git from GitHub URL",
|
||||
@@ -69,9 +59,6 @@ func TestMutator(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
setupMocks: func(mockSecrets *secrets.MockRepositorySecrets) {},
|
||||
expectedToken: "",
|
||||
expectedEncryptedToken: "",
|
||||
},
|
||||
{
|
||||
name: "does not trim if no .git or slash",
|
||||
@@ -86,76 +73,6 @@ func TestMutator(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
setupMocks: func(mockSecrets *secrets.MockRepositorySecrets) {},
|
||||
expectedToken: "",
|
||||
expectedEncryptedToken: "",
|
||||
},
|
||||
{
|
||||
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",
|
||||
@@ -168,9 +85,6 @@ func TestMutator(t *testing.T) {
|
||||
GitHub: nil,
|
||||
},
|
||||
},
|
||||
setupMocks: func(_ *secrets.MockRepositorySecrets) {
|
||||
// No expectations
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "empty token",
|
||||
@@ -180,30 +94,19 @@ func TestMutator(t *testing.T) {
|
||||
Namespace: "default",
|
||||
},
|
||||
Spec: provisioning.RepositorySpec{
|
||||
GitHub: &provisioning.GitHubRepositoryConfig{
|
||||
Token: "",
|
||||
},
|
||||
GitHub: &provisioning.GitHubRepositoryConfig{},
|
||||
},
|
||||
},
|
||||
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)
|
||||
mutator := Mutator()
|
||||
err := mutator(context.Background(), tt.obj)
|
||||
|
||||
if tt.expectedError != "" {
|
||||
@@ -211,16 +114,6 @@ func TestMutator(t *testing.T) {
|
||||
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")
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -7,25 +7,20 @@ import (
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/grafana/grafana-app-sdk/logging"
|
||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||
|
||||
provisioning "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1"
|
||||
common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/provisioning/repository"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/provisioning/repository/git"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/provisioning/safepath"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/provisioning/secrets"
|
||||
)
|
||||
|
||||
//nolint:gosec // This is a constant for a secret suffix
|
||||
const githubTokenSecretSuffix = "-github-token"
|
||||
|
||||
// Make sure all public functions of this struct call the (*githubRepository).logger function, to ensure the GH repo details are included.
|
||||
type githubRepository struct {
|
||||
git.GitRepository
|
||||
config *provisioning.Repository
|
||||
gh Client // assumes github.com base URL
|
||||
secrets secrets.RepositorySecrets
|
||||
config *provisioning.Repository
|
||||
gh Client // assumes github.com base URL
|
||||
|
||||
owner string
|
||||
repo string
|
||||
@@ -42,7 +37,6 @@ type GithubRepository interface {
|
||||
repository.Reader
|
||||
repository.RepositoryWithURLs
|
||||
repository.StageableRepository
|
||||
repository.Hooks
|
||||
Owner() string
|
||||
Repo() string
|
||||
Client() Client
|
||||
@@ -53,8 +47,7 @@ func NewGitHub(
|
||||
config *provisioning.Repository,
|
||||
gitRepo git.GitRepository,
|
||||
factory *Factory,
|
||||
token string,
|
||||
secrets secrets.RepositorySecrets,
|
||||
token common.RawSecureValue,
|
||||
) (GithubRepository, error) {
|
||||
owner, repo, err := ParseOwnerRepoGithub(config.Spec.GitHub.URL)
|
||||
if err != nil {
|
||||
@@ -67,7 +60,6 @@ func NewGitHub(
|
||||
gh: factory.New(ctx, token), // TODO, baseURL from config
|
||||
owner: owner,
|
||||
repo: repo,
|
||||
secrets: secrets,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -242,23 +234,3 @@ func (r *githubRepository) RefURLs(ctx context.Context, ref string) (*provisioni
|
||||
|
||||
return urls, nil
|
||||
}
|
||||
|
||||
func (r *githubRepository) OnCreate(_ context.Context) ([]map[string]interface{}, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (r *githubRepository) OnUpdate(_ context.Context) ([]map[string]interface{}, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (r *githubRepository) OnDelete(ctx context.Context) error {
|
||||
logger := logging.FromContext(ctx)
|
||||
secretName := r.config.Name + githubTokenSecretSuffix
|
||||
if err := r.secrets.Delete(ctx, r.config, secretName); err != nil {
|
||||
return fmt.Errorf("delete github token secret: %w", err)
|
||||
}
|
||||
|
||||
logger.Info("Deleted github token secret", "secretName", secretName)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -15,9 +15,9 @@ import (
|
||||
field "k8s.io/apimachinery/pkg/util/validation/field"
|
||||
|
||||
provisioning "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1"
|
||||
common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/provisioning/repository"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/provisioning/repository/git"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/provisioning/secrets"
|
||||
)
|
||||
|
||||
func TestNewGitHub(t *testing.T) {
|
||||
@@ -81,16 +81,13 @@ func TestNewGitHub(t *testing.T) {
|
||||
|
||||
gitRepo := git.NewMockGitRepository(t)
|
||||
|
||||
mockSecrets := secrets.NewMockRepositorySecrets(t)
|
||||
|
||||
// Call the function under test
|
||||
repo, err := NewGitHub(
|
||||
context.Background(),
|
||||
tt.config,
|
||||
gitRepo,
|
||||
factory,
|
||||
tt.token,
|
||||
mockSecrets,
|
||||
common.RawSecureValue(tt.token),
|
||||
)
|
||||
|
||||
// Check results
|
||||
@@ -179,10 +176,14 @@ func TestGitHubRepositoryValidate(t *testing.T) {
|
||||
GitHub: &provisioning.GitHubRepositoryConfig{
|
||||
URL: "https://github.com/grafana/grafana",
|
||||
Branch: "main",
|
||||
Token: "valid-token",
|
||||
Path: "dashboards",
|
||||
},
|
||||
},
|
||||
Secure: provisioning.SecureValues{
|
||||
Token: common.InlineSecureValue{
|
||||
Name: "with-name",
|
||||
},
|
||||
},
|
||||
},
|
||||
mockSetup: func(m *git.MockGitRepository) {
|
||||
m.On("Config").Return(&provisioning.Repository{
|
||||
@@ -190,10 +191,14 @@ func TestGitHubRepositoryValidate(t *testing.T) {
|
||||
GitHub: &provisioning.GitHubRepositoryConfig{
|
||||
URL: "https://github.com/grafana/grafana",
|
||||
Branch: "main",
|
||||
Token: "valid-token",
|
||||
Path: "dashboards",
|
||||
},
|
||||
},
|
||||
Secure: provisioning.SecureValues{
|
||||
Token: common.InlineSecureValue{
|
||||
Name: "with-name",
|
||||
},
|
||||
},
|
||||
})
|
||||
m.On("Validate").Return(field.ErrorList{})
|
||||
},
|
||||
@@ -223,7 +228,11 @@ func TestGitHubRepositoryValidate(t *testing.T) {
|
||||
GitHub: &provisioning.GitHubRepositoryConfig{
|
||||
URL: "",
|
||||
Branch: "main",
|
||||
Token: "valid-token",
|
||||
},
|
||||
},
|
||||
Secure: provisioning.SecureValues{
|
||||
Token: common.InlineSecureValue{
|
||||
Name: "with-name",
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -233,7 +242,11 @@ func TestGitHubRepositoryValidate(t *testing.T) {
|
||||
GitHub: &provisioning.GitHubRepositoryConfig{
|
||||
URL: "",
|
||||
Branch: "main",
|
||||
Token: "valid-token",
|
||||
},
|
||||
},
|
||||
Secure: provisioning.SecureValues{
|
||||
Token: common.InlineSecureValue{
|
||||
Name: "with-name",
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -248,7 +261,11 @@ func TestGitHubRepositoryValidate(t *testing.T) {
|
||||
GitHub: &provisioning.GitHubRepositoryConfig{
|
||||
URL: "invalid-url",
|
||||
Branch: "main",
|
||||
Token: "valid-token",
|
||||
},
|
||||
},
|
||||
Secure: provisioning.SecureValues{
|
||||
Token: common.InlineSecureValue{
|
||||
Name: "with-name",
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -258,7 +275,11 @@ func TestGitHubRepositoryValidate(t *testing.T) {
|
||||
GitHub: &provisioning.GitHubRepositoryConfig{
|
||||
URL: "invalid-url",
|
||||
Branch: "main",
|
||||
Token: "valid-token",
|
||||
},
|
||||
},
|
||||
Secure: provisioning.SecureValues{
|
||||
Token: common.InlineSecureValue{
|
||||
Name: "with-name",
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -273,7 +294,11 @@ func TestGitHubRepositoryValidate(t *testing.T) {
|
||||
GitHub: &provisioning.GitHubRepositoryConfig{
|
||||
URL: "https://gitlab.com/grafana/grafana",
|
||||
Branch: "main",
|
||||
Token: "valid-token",
|
||||
},
|
||||
},
|
||||
Secure: provisioning.SecureValues{
|
||||
Token: common.InlineSecureValue{
|
||||
Name: "with-name",
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -283,7 +308,11 @@ func TestGitHubRepositoryValidate(t *testing.T) {
|
||||
GitHub: &provisioning.GitHubRepositoryConfig{
|
||||
URL: "https://gitlab.com/grafana/grafana",
|
||||
Branch: "main",
|
||||
Token: "valid-token",
|
||||
},
|
||||
},
|
||||
Secure: provisioning.SecureValues{
|
||||
Token: common.InlineSecureValue{
|
||||
Name: "with-name",
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -339,7 +368,11 @@ func TestGitHubRepositoryTest(t *testing.T) {
|
||||
GitHub: &provisioning.GitHubRepositoryConfig{
|
||||
URL: "https://github.com/grafana/grafana",
|
||||
Branch: "main",
|
||||
Token: "valid-token",
|
||||
},
|
||||
},
|
||||
Secure: provisioning.SecureValues{
|
||||
Token: common.InlineSecureValue{
|
||||
Name: "with-name",
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -361,7 +394,11 @@ func TestGitHubRepositoryTest(t *testing.T) {
|
||||
GitHub: &provisioning.GitHubRepositoryConfig{
|
||||
URL: "invalid-url",
|
||||
Branch: "main",
|
||||
Token: "valid-token",
|
||||
},
|
||||
},
|
||||
Secure: provisioning.SecureValues{
|
||||
Token: common.InlineSecureValue{
|
||||
Name: "with-name",
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -878,7 +915,11 @@ func TestGitHubRepositoryDelegation(t *testing.T) {
|
||||
GitHub: &provisioning.GitHubRepositoryConfig{
|
||||
URL: "https://github.com/grafana/grafana",
|
||||
Branch: "main",
|
||||
Token: "test-token",
|
||||
},
|
||||
},
|
||||
Secure: provisioning.SecureValues{
|
||||
Token: common.InlineSecureValue{
|
||||
Name: "with-name",
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -1095,7 +1136,6 @@ func TestGitHubRepositoryAccessors(t *testing.T) {
|
||||
GitHub: &provisioning.GitHubRepositoryConfig{
|
||||
URL: "https://github.com/grafana/grafana",
|
||||
Branch: "main",
|
||||
Token: "test-token",
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -1137,82 +1177,6 @@ func TestGitHubRepositoryAccessors(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestGitHubRepository_OnDelete(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
setupMock func(*secrets.MockRepositorySecrets)
|
||||
config *provisioning.Repository
|
||||
expectedError string
|
||||
}{
|
||||
{
|
||||
name: "successful secret deletion",
|
||||
setupMock: func(mockSecrets *secrets.MockRepositorySecrets) {
|
||||
mockSecrets.EXPECT().Delete(
|
||||
context.Background(),
|
||||
&provisioning.Repository{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-repo",
|
||||
Namespace: "default",
|
||||
},
|
||||
},
|
||||
"test-repo"+githubTokenSecretSuffix,
|
||||
).Return(nil)
|
||||
},
|
||||
config: &provisioning.Repository{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-repo",
|
||||
Namespace: "default",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "secret deletion error",
|
||||
setupMock: func(mockSecrets *secrets.MockRepositorySecrets) {
|
||||
mockSecrets.EXPECT().Delete(
|
||||
context.Background(),
|
||||
&provisioning.Repository{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-repo",
|
||||
Namespace: "default",
|
||||
},
|
||||
},
|
||||
"test-repo"+githubTokenSecretSuffix,
|
||||
).Return(errors.New("failed to delete secret"))
|
||||
},
|
||||
config: &provisioning.Repository{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-repo",
|
||||
Namespace: "default",
|
||||
},
|
||||
},
|
||||
expectedError: "delete github token secret: failed to delete secret",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
mockSecrets := secrets.NewMockRepositorySecrets(t)
|
||||
tt.setupMock(mockSecrets)
|
||||
|
||||
githubRepo := &githubRepository{
|
||||
config: tt.config,
|
||||
secrets: mockSecrets,
|
||||
}
|
||||
|
||||
err := githubRepo.OnDelete(context.Background())
|
||||
|
||||
if tt.expectedError != "" {
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), tt.expectedError)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
mockSecrets.AssertExpectations(t)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGithubRepository_Move(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
@@ -1262,7 +1226,6 @@ func TestGithubRepository_Move(t *testing.T) {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// Create mock git repository
|
||||
mockGitRepo := git.NewMockGitRepository(t)
|
||||
mockSecrets := &secrets.MockRepositorySecrets{}
|
||||
|
||||
// Setup mock expectations
|
||||
tt.setupMock(mockGitRepo)
|
||||
@@ -1285,7 +1248,6 @@ func TestGithubRepository_Move(t *testing.T) {
|
||||
GitRepository: mockGitRepo,
|
||||
owner: "example",
|
||||
repo: "repo",
|
||||
secrets: mockSecrets,
|
||||
}
|
||||
|
||||
// Execute move operation
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
provisioning "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1"
|
||||
common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/secret/contracts"
|
||||
)
|
||||
|
||||
type Decrypter = func(r *provisioning.Repository) SecureValues
|
||||
|
||||
type SecureValues interface {
|
||||
Token(ctx context.Context) (common.RawSecureValue, error)
|
||||
WebhookSecret(ctx context.Context) (common.RawSecureValue, error)
|
||||
}
|
||||
|
||||
type secureValues struct {
|
||||
svc contracts.DecryptService
|
||||
names provisioning.SecureValues
|
||||
namespace string
|
||||
}
|
||||
|
||||
func (s *secureValues) get(ctx context.Context, sv common.InlineSecureValue) (common.RawSecureValue, error) {
|
||||
if !sv.Create.IsZero() {
|
||||
return sv.Create, nil // If this was called before the value is actually saved
|
||||
}
|
||||
if sv.Name == "" {
|
||||
return "", nil
|
||||
}
|
||||
results, err := s.svc.Decrypt(ctx, provisioning.GROUP, s.namespace, sv.Name)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to call decrypt service: %w", err)
|
||||
}
|
||||
|
||||
v, found := results[sv.Name]
|
||||
if !found {
|
||||
return "", fmt.Errorf("not found")
|
||||
}
|
||||
if v.Error() != nil {
|
||||
return "", v.Error()
|
||||
}
|
||||
return common.RawSecureValue(*v.Value()), nil
|
||||
}
|
||||
|
||||
func (s *secureValues) Token(ctx context.Context) (common.RawSecureValue, error) {
|
||||
return s.get(ctx, s.names.Token)
|
||||
}
|
||||
|
||||
func (s *secureValues) WebhookSecret(ctx context.Context) (common.RawSecureValue, error) {
|
||||
return s.get(ctx, s.names.WebhookSecret)
|
||||
}
|
||||
|
||||
func DecryptService(svc contracts.DecryptService) Decrypter {
|
||||
return func(r *provisioning.Repository) SecureValues {
|
||||
return &secureValues{svc: svc, names: r.Secure, namespace: r.Namespace}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
provisioning "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1"
|
||||
secretv1beta1 "github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1"
|
||||
"github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/secret/contracts"
|
||||
)
|
||||
|
||||
func TestRepositorySecureValues(t *testing.T) {
|
||||
type expectedDecryptedResult struct {
|
||||
value string
|
||||
error string
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
config *provisioning.Repository
|
||||
decrypt decryptFn
|
||||
token expectedDecryptedResult
|
||||
webhook expectedDecryptedResult
|
||||
}{
|
||||
{
|
||||
name: "referenced by name",
|
||||
config: &provisioning.Repository{
|
||||
Secure: provisioning.SecureValues{
|
||||
Token: v0alpha1.InlineSecureValue{
|
||||
Name: "secret",
|
||||
},
|
||||
},
|
||||
},
|
||||
decrypt: func(t *testing.T, names ...string) (map[string]contracts.DecryptResult, error) {
|
||||
require.Equal(t, []string{"secret"}, names)
|
||||
val := secretv1beta1.NewExposedSecureValue(names[0])
|
||||
return map[string]contracts.DecryptResult{
|
||||
names[0]: contracts.NewDecryptResultValue(&val),
|
||||
}, nil
|
||||
},
|
||||
token: expectedDecryptedResult{
|
||||
value: "secret",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "when create exists, use it",
|
||||
config: &provisioning.Repository{
|
||||
Secure: provisioning.SecureValues{
|
||||
Token: v0alpha1.InlineSecureValue{
|
||||
Create: "secret",
|
||||
},
|
||||
},
|
||||
},
|
||||
decrypt: func(t *testing.T, names ...string) (map[string]contracts.DecryptResult, error) {
|
||||
t.Fatal("decrypt should not be called when Create is set")
|
||||
return nil, nil
|
||||
},
|
||||
token: expectedDecryptedResult{
|
||||
value: "secret",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "avoid decrypt when no values are configured",
|
||||
config: &provisioning.Repository{
|
||||
Secure: provisioning.SecureValues{},
|
||||
},
|
||||
decrypt: func(t *testing.T, names ...string) (map[string]contracts.DecryptResult, error) {
|
||||
t.Fatal("decrypt should not be called when no values are configured")
|
||||
return nil, nil
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "propagate error from service",
|
||||
config: &provisioning.Repository{
|
||||
Secure: provisioning.SecureValues{
|
||||
WebhookSecret: v0alpha1.InlineSecureValue{
|
||||
Name: "secret",
|
||||
},
|
||||
},
|
||||
},
|
||||
decrypt: func(t *testing.T, names ...string) (map[string]contracts.DecryptResult, error) {
|
||||
require.Equal(t, []string{"secret"}, names)
|
||||
return map[string]contracts.DecryptResult{
|
||||
names[0]: contracts.NewDecryptResultErr(fmt.Errorf("error for name")),
|
||||
}, nil
|
||||
},
|
||||
webhook: expectedDecryptedResult{
|
||||
error: "error for name",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "not found",
|
||||
config: &provisioning.Repository{
|
||||
Secure: provisioning.SecureValues{
|
||||
Token: v0alpha1.InlineSecureValue{
|
||||
Name: "secret",
|
||||
},
|
||||
},
|
||||
},
|
||||
decrypt: func(t *testing.T, names ...string) (map[string]contracts.DecryptResult, error) {
|
||||
return map[string]contracts.DecryptResult{}, nil
|
||||
},
|
||||
token: expectedDecryptedResult{
|
||||
error: "not found", // it is not in the results above
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "not found",
|
||||
config: &provisioning.Repository{
|
||||
Secure: provisioning.SecureValues{
|
||||
WebhookSecret: v0alpha1.InlineSecureValue{
|
||||
Name: "secret",
|
||||
},
|
||||
},
|
||||
},
|
||||
decrypt: func(t *testing.T, names ...string) (map[string]contracts.DecryptResult, error) {
|
||||
return nil, fmt.Errorf("explode")
|
||||
},
|
||||
webhook: expectedDecryptedResult{
|
||||
error: "failed to call decrypt service",
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
decrypter := DecryptService(&dummyDecryptService{t: t, fn: tt.decrypt})
|
||||
decrypted := decrypter(tt.config)
|
||||
|
||||
token, err := decrypted.Token(context.Background())
|
||||
if tt.token.error != "" {
|
||||
require.ErrorContains(t, err, tt.token.error)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, tt.token.value, string(token))
|
||||
}
|
||||
|
||||
webhook, err := decrypted.WebhookSecret(context.Background())
|
||||
if tt.webhook.error != "" {
|
||||
require.ErrorContains(t, err, tt.webhook.error)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, tt.webhook.value, string(webhook))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type decryptFn = func(t *testing.T, names ...string) (map[string]contracts.DecryptResult, error)
|
||||
|
||||
type dummyDecryptService struct {
|
||||
t *testing.T
|
||||
fn decryptFn
|
||||
}
|
||||
|
||||
func (d *dummyDecryptService) Decrypt(_ context.Context, _ string, _ string, names ...string) (map[string]contracts.DecryptResult, error) {
|
||||
return d.fn(d.t, names...)
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
package secrets
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/grafana/grafana/pkg/services/secrets"
|
||||
)
|
||||
|
||||
// A secrets encryption service. It only operates on values, no names or similar.
|
||||
// It is likely we will need to change this when the multi-tenant service comes around.
|
||||
//
|
||||
// FIXME: this is a temporary service/package until we can make use of
|
||||
// the new secrets service in app platform.
|
||||
//
|
||||
//go:generate mockery --name LegacyService --structname MockLegacyService --inpackage --filename legacy_secret_mock.go --with-expecter
|
||||
type LegacyService interface {
|
||||
Encrypt(ctx context.Context, data []byte) ([]byte, error)
|
||||
Decrypt(ctx context.Context, data []byte) ([]byte, error)
|
||||
}
|
||||
|
||||
var _ LegacyService = (*singleTenant)(nil)
|
||||
|
||||
type singleTenant struct {
|
||||
inner secrets.Service
|
||||
}
|
||||
|
||||
func NewSingleTenant(svc secrets.Service) LegacyService {
|
||||
return &singleTenant{svc}
|
||||
}
|
||||
|
||||
func (s *singleTenant) Encrypt(ctx context.Context, data []byte) ([]byte, error) {
|
||||
return s.inner.Encrypt(ctx, data, secrets.WithoutScope())
|
||||
}
|
||||
|
||||
func (s *singleTenant) Decrypt(ctx context.Context, data []byte) ([]byte, error) {
|
||||
return s.inner.Decrypt(ctx, data)
|
||||
}
|
||||
@@ -1,154 +0,0 @@
|
||||
// Code generated by mockery v2.52.4. DO NOT EDIT.
|
||||
|
||||
package secrets
|
||||
|
||||
import (
|
||||
context "context"
|
||||
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
// MockLegacyService is an autogenerated mock type for the LegacyService type
|
||||
type MockLegacyService struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
type MockLegacyService_Expecter struct {
|
||||
mock *mock.Mock
|
||||
}
|
||||
|
||||
func (_m *MockLegacyService) EXPECT() *MockLegacyService_Expecter {
|
||||
return &MockLegacyService_Expecter{mock: &_m.Mock}
|
||||
}
|
||||
|
||||
// Decrypt provides a mock function with given fields: ctx, data
|
||||
func (_m *MockLegacyService) Decrypt(ctx context.Context, data []byte) ([]byte, error) {
|
||||
ret := _m.Called(ctx, data)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Decrypt")
|
||||
}
|
||||
|
||||
var r0 []byte
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, []byte) ([]byte, error)); ok {
|
||||
return rf(ctx, data)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(context.Context, []byte) []byte); ok {
|
||||
r0 = rf(ctx, data)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]byte)
|
||||
}
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(context.Context, []byte) error); ok {
|
||||
r1 = rf(ctx, data)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// MockLegacyService_Decrypt_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Decrypt'
|
||||
type MockLegacyService_Decrypt_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// Decrypt is a helper method to define mock.On call
|
||||
// - ctx context.Context
|
||||
// - data []byte
|
||||
func (_e *MockLegacyService_Expecter) Decrypt(ctx interface{}, data interface{}) *MockLegacyService_Decrypt_Call {
|
||||
return &MockLegacyService_Decrypt_Call{Call: _e.mock.On("Decrypt", ctx, data)}
|
||||
}
|
||||
|
||||
func (_c *MockLegacyService_Decrypt_Call) Run(run func(ctx context.Context, data []byte)) *MockLegacyService_Decrypt_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run(args[0].(context.Context), args[1].([]byte))
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockLegacyService_Decrypt_Call) Return(_a0 []byte, _a1 error) *MockLegacyService_Decrypt_Call {
|
||||
_c.Call.Return(_a0, _a1)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockLegacyService_Decrypt_Call) RunAndReturn(run func(context.Context, []byte) ([]byte, error)) *MockLegacyService_Decrypt_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// Encrypt provides a mock function with given fields: ctx, data
|
||||
func (_m *MockLegacyService) Encrypt(ctx context.Context, data []byte) ([]byte, error) {
|
||||
ret := _m.Called(ctx, data)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Encrypt")
|
||||
}
|
||||
|
||||
var r0 []byte
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, []byte) ([]byte, error)); ok {
|
||||
return rf(ctx, data)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(context.Context, []byte) []byte); ok {
|
||||
r0 = rf(ctx, data)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]byte)
|
||||
}
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(context.Context, []byte) error); ok {
|
||||
r1 = rf(ctx, data)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// MockLegacyService_Encrypt_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Encrypt'
|
||||
type MockLegacyService_Encrypt_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// Encrypt is a helper method to define mock.On call
|
||||
// - ctx context.Context
|
||||
// - data []byte
|
||||
func (_e *MockLegacyService_Expecter) Encrypt(ctx interface{}, data interface{}) *MockLegacyService_Encrypt_Call {
|
||||
return &MockLegacyService_Encrypt_Call{Call: _e.mock.On("Encrypt", ctx, data)}
|
||||
}
|
||||
|
||||
func (_c *MockLegacyService_Encrypt_Call) Run(run func(ctx context.Context, data []byte)) *MockLegacyService_Encrypt_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run(args[0].(context.Context), args[1].([]byte))
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockLegacyService_Encrypt_Call) Return(_a0 []byte, _a1 error) *MockLegacyService_Encrypt_Call {
|
||||
_c.Call.Return(_a0, _a1)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockLegacyService_Encrypt_Call) RunAndReturn(run func(context.Context, []byte) ([]byte, error)) *MockLegacyService_Encrypt_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// NewMockLegacyService creates a new instance of MockLegacyService. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
|
||||
// The first argument is typically a *testing.T value.
|
||||
func NewMockLegacyService(t interface {
|
||||
mock.TestingT
|
||||
Cleanup(func())
|
||||
}) *MockLegacyService {
|
||||
mock := &MockLegacyService{}
|
||||
mock.Mock.Test(t)
|
||||
|
||||
t.Cleanup(func() { mock.AssertExpectations(t) })
|
||||
|
||||
return mock
|
||||
}
|
||||
@@ -1,112 +0,0 @@
|
||||
package secrets
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
"github.com/grafana/grafana-app-sdk/logging"
|
||||
provisioning "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/secret"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/secret/contracts"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
legacysecrets "github.com/grafana/grafana/pkg/services/secrets"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
)
|
||||
|
||||
func ProvideRepositorySecrets(
|
||||
features featuremgmt.FeatureToggles,
|
||||
legacySecretsSvc legacysecrets.Service,
|
||||
secretsSvc contracts.SecureValueClient,
|
||||
decryptSvc secret.DecryptService,
|
||||
cfg *setting.Cfg,
|
||||
) RepositorySecrets {
|
||||
return NewRepositorySecrets(features, NewSecretsService(secretsSvc, decryptSvc, cfg.SecretsManagement.GrpcGrafanaServiceName), NewSingleTenant(legacySecretsSvc))
|
||||
}
|
||||
|
||||
//go:generate mockery --name RepositorySecrets --structname MockRepositorySecrets --inpackage --filename repository_secrets_mock.go --with-expecter
|
||||
type RepositorySecrets interface {
|
||||
Encrypt(ctx context.Context, r *provisioning.Repository, name string, data string) (nameOrValue []byte, err error)
|
||||
Decrypt(ctx context.Context, r *provisioning.Repository, nameOrValue string) (data []byte, err error)
|
||||
Delete(ctx context.Context, r *provisioning.Repository, nameOrValue string) error
|
||||
}
|
||||
|
||||
// repositorySecrets provides a unified interface for encrypting and decrypting repository secrets,
|
||||
// supporting both the legacy and new secrets services. The active backend is determined by the
|
||||
// FlagProvisioningSecretsService feature flag:
|
||||
// - If enabled, operations use the new secrets service.
|
||||
// - If disabled, operations use the legacy secrets service.
|
||||
//
|
||||
// This abstraction enables a seamless migration path between secret backends without breaking
|
||||
// existing functionality. Once migration is complete and the legacy service is deprecated,
|
||||
// this wrapper should be removed.
|
||||
type repositorySecrets struct {
|
||||
features featuremgmt.FeatureToggles
|
||||
secretsSvc Service
|
||||
legacySecrets LegacyService
|
||||
}
|
||||
|
||||
func NewRepositorySecrets(features featuremgmt.FeatureToggles, secretsSvc Service, legacySecrets LegacyService) RepositorySecrets {
|
||||
return &repositorySecrets{
|
||||
features: features,
|
||||
secretsSvc: secretsSvc,
|
||||
legacySecrets: legacySecrets,
|
||||
}
|
||||
}
|
||||
|
||||
// Encrypt encrypts the data and returns the name or value of the encrypted data
|
||||
// If the feature flag is disabled, it uses the legacy secrets service
|
||||
// If the feature flag is enabled, it uses the secrets service
|
||||
func (s *repositorySecrets) Encrypt(ctx context.Context, r *provisioning.Repository, name string, data string) (nameOrValue []byte, err error) {
|
||||
logger := logging.FromContext(ctx).With("name", name, "namespace", r.GetNamespace())
|
||||
if s.features.IsEnabled(ctx, featuremgmt.FlagProvisioningSecretsService) {
|
||||
logger.Info("Encrypting secret with new secrets service")
|
||||
encrypted, err := s.secretsSvc.Encrypt(ctx, r.GetNamespace(), name, data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return []byte(encrypted), err
|
||||
}
|
||||
|
||||
logger.Info("Encrypting secret with legacy secrets service")
|
||||
encrypted, err := s.legacySecrets.Encrypt(ctx, []byte(data))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return encrypted, nil
|
||||
}
|
||||
|
||||
// Decrypt retrieves and decrypts secret data for a repository, supporting migration between secret backends.
|
||||
// The backend used for decryption is determined by a heuristic:
|
||||
// - If the provided nameOrValue starts with the repository name, it is assumed to be a Kubernetes secret name
|
||||
// and the new secrets service is used for decryption.
|
||||
// - Otherwise, it is treated as a legacy secret value and the legacy secrets service is used.
|
||||
//
|
||||
// HACK: This approach relies on checking the prefix of nameOrValue to distinguish between secret backends.
|
||||
// This is a temporary workaround to support both backends during migration and should be removed once
|
||||
// migration is complete.
|
||||
//
|
||||
// This method ensures compatibility and minimizes disruption during the transition between secret backends.
|
||||
func (s *repositorySecrets) Decrypt(ctx context.Context, r *provisioning.Repository, nameOrValue string) ([]byte, error) {
|
||||
logger := logging.FromContext(ctx)
|
||||
// HACK: this is a hack to identify if the name is a potential Kubernetes name for a secret.
|
||||
if strings.HasPrefix(nameOrValue, r.GetName()) {
|
||||
logger.Info("Decrypting secret with new secrets service")
|
||||
return s.secretsSvc.Decrypt(ctx, r.GetNamespace(), nameOrValue)
|
||||
} else {
|
||||
logger.Info("Decrypting secret with legacy secrets service")
|
||||
return s.legacySecrets.Decrypt(ctx, []byte(nameOrValue))
|
||||
}
|
||||
}
|
||||
|
||||
func (s *repositorySecrets) Delete(ctx context.Context, r *provisioning.Repository, nameOrValue string) error {
|
||||
if s.features.IsEnabled(ctx, featuremgmt.FlagProvisioningSecretsService) {
|
||||
err := s.secretsSvc.Delete(ctx, r.GetNamespace(), nameOrValue)
|
||||
if err != nil && !errors.Is(err, contracts.ErrSecureValueNotFound) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,206 +0,0 @@
|
||||
// Code generated by mockery v2.52.4. DO NOT EDIT.
|
||||
|
||||
package secrets
|
||||
|
||||
import (
|
||||
context "context"
|
||||
|
||||
v0alpha1 "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1"
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
// MockRepositorySecrets is an autogenerated mock type for the RepositorySecrets type
|
||||
type MockRepositorySecrets struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
type MockRepositorySecrets_Expecter struct {
|
||||
mock *mock.Mock
|
||||
}
|
||||
|
||||
func (_m *MockRepositorySecrets) EXPECT() *MockRepositorySecrets_Expecter {
|
||||
return &MockRepositorySecrets_Expecter{mock: &_m.Mock}
|
||||
}
|
||||
|
||||
// Decrypt provides a mock function with given fields: ctx, r, nameOrValue
|
||||
func (_m *MockRepositorySecrets) Decrypt(ctx context.Context, r *v0alpha1.Repository, nameOrValue string) ([]byte, error) {
|
||||
ret := _m.Called(ctx, r, nameOrValue)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Decrypt")
|
||||
}
|
||||
|
||||
var r0 []byte
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, *v0alpha1.Repository, string) ([]byte, error)); ok {
|
||||
return rf(ctx, r, nameOrValue)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(context.Context, *v0alpha1.Repository, string) []byte); ok {
|
||||
r0 = rf(ctx, r, nameOrValue)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]byte)
|
||||
}
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(context.Context, *v0alpha1.Repository, string) error); ok {
|
||||
r1 = rf(ctx, r, nameOrValue)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// MockRepositorySecrets_Decrypt_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Decrypt'
|
||||
type MockRepositorySecrets_Decrypt_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// Decrypt is a helper method to define mock.On call
|
||||
// - ctx context.Context
|
||||
// - r *v0alpha1.Repository
|
||||
// - nameOrValue string
|
||||
func (_e *MockRepositorySecrets_Expecter) Decrypt(ctx interface{}, r interface{}, nameOrValue interface{}) *MockRepositorySecrets_Decrypt_Call {
|
||||
return &MockRepositorySecrets_Decrypt_Call{Call: _e.mock.On("Decrypt", ctx, r, nameOrValue)}
|
||||
}
|
||||
|
||||
func (_c *MockRepositorySecrets_Decrypt_Call) Run(run func(ctx context.Context, r *v0alpha1.Repository, nameOrValue string)) *MockRepositorySecrets_Decrypt_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run(args[0].(context.Context), args[1].(*v0alpha1.Repository), args[2].(string))
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockRepositorySecrets_Decrypt_Call) Return(data []byte, err error) *MockRepositorySecrets_Decrypt_Call {
|
||||
_c.Call.Return(data, err)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockRepositorySecrets_Decrypt_Call) RunAndReturn(run func(context.Context, *v0alpha1.Repository, string) ([]byte, error)) *MockRepositorySecrets_Decrypt_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// Delete provides a mock function with given fields: ctx, r, nameOrValue
|
||||
func (_m *MockRepositorySecrets) Delete(ctx context.Context, r *v0alpha1.Repository, nameOrValue string) error {
|
||||
ret := _m.Called(ctx, r, nameOrValue)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Delete")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, *v0alpha1.Repository, string) error); ok {
|
||||
r0 = rf(ctx, r, nameOrValue)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// MockRepositorySecrets_Delete_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Delete'
|
||||
type MockRepositorySecrets_Delete_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// Delete is a helper method to define mock.On call
|
||||
// - ctx context.Context
|
||||
// - r *v0alpha1.Repository
|
||||
// - nameOrValue string
|
||||
func (_e *MockRepositorySecrets_Expecter) Delete(ctx interface{}, r interface{}, nameOrValue interface{}) *MockRepositorySecrets_Delete_Call {
|
||||
return &MockRepositorySecrets_Delete_Call{Call: _e.mock.On("Delete", ctx, r, nameOrValue)}
|
||||
}
|
||||
|
||||
func (_c *MockRepositorySecrets_Delete_Call) Run(run func(ctx context.Context, r *v0alpha1.Repository, nameOrValue string)) *MockRepositorySecrets_Delete_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run(args[0].(context.Context), args[1].(*v0alpha1.Repository), args[2].(string))
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockRepositorySecrets_Delete_Call) Return(_a0 error) *MockRepositorySecrets_Delete_Call {
|
||||
_c.Call.Return(_a0)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockRepositorySecrets_Delete_Call) RunAndReturn(run func(context.Context, *v0alpha1.Repository, string) error) *MockRepositorySecrets_Delete_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// Encrypt provides a mock function with given fields: ctx, r, name, data
|
||||
func (_m *MockRepositorySecrets) Encrypt(ctx context.Context, r *v0alpha1.Repository, name string, data string) ([]byte, error) {
|
||||
ret := _m.Called(ctx, r, name, data)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Encrypt")
|
||||
}
|
||||
|
||||
var r0 []byte
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, *v0alpha1.Repository, string, string) ([]byte, error)); ok {
|
||||
return rf(ctx, r, name, data)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(context.Context, *v0alpha1.Repository, string, string) []byte); ok {
|
||||
r0 = rf(ctx, r, name, data)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]byte)
|
||||
}
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(context.Context, *v0alpha1.Repository, string, string) error); ok {
|
||||
r1 = rf(ctx, r, name, data)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// MockRepositorySecrets_Encrypt_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Encrypt'
|
||||
type MockRepositorySecrets_Encrypt_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// Encrypt is a helper method to define mock.On call
|
||||
// - ctx context.Context
|
||||
// - r *v0alpha1.Repository
|
||||
// - name string
|
||||
// - data string
|
||||
func (_e *MockRepositorySecrets_Expecter) Encrypt(ctx interface{}, r interface{}, name interface{}, data interface{}) *MockRepositorySecrets_Encrypt_Call {
|
||||
return &MockRepositorySecrets_Encrypt_Call{Call: _e.mock.On("Encrypt", ctx, r, name, data)}
|
||||
}
|
||||
|
||||
func (_c *MockRepositorySecrets_Encrypt_Call) Run(run func(ctx context.Context, r *v0alpha1.Repository, name string, data string)) *MockRepositorySecrets_Encrypt_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run(args[0].(context.Context), args[1].(*v0alpha1.Repository), args[2].(string), args[3].(string))
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockRepositorySecrets_Encrypt_Call) Return(nameOrValue []byte, err error) *MockRepositorySecrets_Encrypt_Call {
|
||||
_c.Call.Return(nameOrValue, err)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockRepositorySecrets_Encrypt_Call) RunAndReturn(run func(context.Context, *v0alpha1.Repository, string, string) ([]byte, error)) *MockRepositorySecrets_Encrypt_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// NewMockRepositorySecrets creates a new instance of MockRepositorySecrets. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
|
||||
// The first argument is typically a *testing.T value.
|
||||
func NewMockRepositorySecrets(t interface {
|
||||
mock.TestingT
|
||||
Cleanup(func())
|
||||
}) *MockRepositorySecrets {
|
||||
mock := &MockRepositorySecrets{}
|
||||
mock.Mock.Test(t)
|
||||
|
||||
t.Cleanup(func() { mock.AssertExpectations(t) })
|
||||
|
||||
return mock
|
||||
}
|
||||
@@ -1,322 +0,0 @@
|
||||
package secrets
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
provisioning "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/secret/contracts"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
type testSetup struct {
|
||||
rs RepositorySecrets
|
||||
mockFeatures *featuremgmt.MockFeatureToggles
|
||||
mockSecrets *MockService
|
||||
mockLegacy *MockLegacyService
|
||||
repo *provisioning.Repository
|
||||
ctx context.Context
|
||||
}
|
||||
|
||||
func setupTest(t *testing.T, namespace string) *testSetup {
|
||||
mockFeatures := featuremgmt.NewMockFeatureToggles(t)
|
||||
mockSecrets := NewMockService(t)
|
||||
mockLegacy := NewMockLegacyService(t)
|
||||
|
||||
rs := NewRepositorySecrets(mockFeatures, mockSecrets, mockLegacy)
|
||||
|
||||
repo := &provisioning.Repository{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-repo",
|
||||
Namespace: namespace,
|
||||
},
|
||||
}
|
||||
|
||||
return &testSetup{
|
||||
rs: rs,
|
||||
mockFeatures: mockFeatures,
|
||||
mockSecrets: mockSecrets,
|
||||
mockLegacy: mockLegacy,
|
||||
repo: repo,
|
||||
ctx: context.Background(),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *testSetup) expectFeatureFlag(enabled bool) {
|
||||
s.mockFeatures.EXPECT().IsEnabled(
|
||||
mock.AnythingOfType("context.backgroundCtx"),
|
||||
featuremgmt.FlagProvisioningSecretsService,
|
||||
).Return(enabled)
|
||||
}
|
||||
|
||||
func TestRepositorySecrets_Encrypt(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
namespace string
|
||||
featureEnabled bool
|
||||
setupMocks func(*testSetup)
|
||||
expectedResult []byte
|
||||
expectedError string
|
||||
}{
|
||||
{
|
||||
name: "new service success",
|
||||
namespace: "test-namespace",
|
||||
featureEnabled: true,
|
||||
setupMocks: func(s *testSetup) {
|
||||
s.expectFeatureFlag(true)
|
||||
s.mockSecrets.EXPECT().Encrypt(s.ctx, "test-namespace", "test-secret", "secret-data").Return("encrypted-name", nil)
|
||||
},
|
||||
expectedResult: []byte("encrypted-name"),
|
||||
},
|
||||
{
|
||||
name: "legacy service success",
|
||||
namespace: "test-namespace",
|
||||
featureEnabled: false,
|
||||
setupMocks: func(s *testSetup) {
|
||||
s.expectFeatureFlag(false)
|
||||
s.mockLegacy.EXPECT().Encrypt(s.ctx, []byte("secret-data")).Return([]byte("encrypted-legacy-data"), nil)
|
||||
},
|
||||
expectedResult: []byte("encrypted-legacy-data"),
|
||||
},
|
||||
{
|
||||
name: "new service error",
|
||||
namespace: "test-namespace",
|
||||
featureEnabled: true,
|
||||
setupMocks: func(s *testSetup) {
|
||||
s.expectFeatureFlag(true)
|
||||
s.mockSecrets.EXPECT().Encrypt(s.ctx, "test-namespace", "test-secret", "secret-data").Return("", errors.New("encryption failed"))
|
||||
},
|
||||
expectedError: "encryption failed",
|
||||
},
|
||||
{
|
||||
name: "legacy service error",
|
||||
namespace: "test-namespace",
|
||||
featureEnabled: false,
|
||||
setupMocks: func(s *testSetup) {
|
||||
s.expectFeatureFlag(false)
|
||||
s.mockLegacy.EXPECT().Encrypt(s.ctx, []byte("secret-data")).Return(nil, errors.New("legacy encryption failed"))
|
||||
},
|
||||
expectedError: "legacy encryption failed",
|
||||
},
|
||||
{
|
||||
name: "empty namespace handling",
|
||||
namespace: "",
|
||||
featureEnabled: true,
|
||||
setupMocks: func(s *testSetup) {
|
||||
s.expectFeatureFlag(true)
|
||||
s.mockSecrets.EXPECT().Encrypt(s.ctx, "", "test-secret", "secret-data").Return("encrypted", nil)
|
||||
},
|
||||
expectedResult: []byte("encrypted"),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
setup := setupTest(t, tt.namespace)
|
||||
tt.setupMocks(setup)
|
||||
|
||||
result, err := setup.rs.Encrypt(setup.ctx, setup.repo, "test-secret", "secret-data")
|
||||
|
||||
if tt.expectedError != "" {
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), tt.expectedError)
|
||||
assert.Nil(t, result)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tt.expectedResult, result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRepositorySecrets_Decrypt(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
namespace string
|
||||
nameOrValue string
|
||||
setupMocks func(*testSetup)
|
||||
expectedResult []byte
|
||||
expectedError string
|
||||
}{
|
||||
{
|
||||
name: "new service success - name starts with repo name",
|
||||
namespace: "test-namespace",
|
||||
nameOrValue: "test-repo-secret-name",
|
||||
setupMocks: func(s *testSetup) {
|
||||
s.mockSecrets.EXPECT().Decrypt(s.ctx, "test-namespace", "test-repo-secret-name").Return([]byte("decrypted-data"), nil)
|
||||
},
|
||||
expectedResult: []byte("decrypted-data"),
|
||||
},
|
||||
{
|
||||
name: "new service error - name starts with repo name",
|
||||
namespace: "test-namespace",
|
||||
nameOrValue: "test-repo-secret-name",
|
||||
setupMocks: func(s *testSetup) {
|
||||
s.mockSecrets.EXPECT().Decrypt(s.ctx, "test-namespace", "test-repo-secret-name").Return(nil, errors.New("new service failed"))
|
||||
},
|
||||
expectedError: "new service failed",
|
||||
},
|
||||
{
|
||||
name: "legacy service success - name does not start with repo name",
|
||||
namespace: "test-namespace",
|
||||
nameOrValue: "legacy-encrypted-value",
|
||||
setupMocks: func(s *testSetup) {
|
||||
s.mockLegacy.EXPECT().Decrypt(s.ctx, []byte("legacy-encrypted-value")).Return([]byte("decrypted-legacy-data"), nil)
|
||||
},
|
||||
expectedResult: []byte("decrypted-legacy-data"),
|
||||
},
|
||||
{
|
||||
name: "legacy service error - name does not start with repo name",
|
||||
namespace: "test-namespace",
|
||||
nameOrValue: "legacy-encrypted-value",
|
||||
setupMocks: func(s *testSetup) {
|
||||
s.mockLegacy.EXPECT().Decrypt(s.ctx, []byte("legacy-encrypted-value")).Return(nil, errors.New("legacy service failed"))
|
||||
},
|
||||
expectedError: "legacy service failed",
|
||||
},
|
||||
{
|
||||
name: "new service empty bytes - name starts with repo name",
|
||||
namespace: "test-namespace",
|
||||
nameOrValue: "test-repo-secret-name",
|
||||
setupMocks: func(s *testSetup) {
|
||||
s.mockSecrets.EXPECT().Decrypt(s.ctx, "test-namespace", "test-repo-secret-name").Return([]byte{}, nil)
|
||||
},
|
||||
expectedResult: []byte{},
|
||||
},
|
||||
{
|
||||
name: "legacy service empty bytes - name does not start with repo name",
|
||||
namespace: "test-namespace",
|
||||
nameOrValue: "legacy-encrypted-value",
|
||||
setupMocks: func(s *testSetup) {
|
||||
s.mockLegacy.EXPECT().Decrypt(s.ctx, []byte("legacy-encrypted-value")).Return([]byte{}, nil)
|
||||
},
|
||||
expectedResult: []byte{},
|
||||
},
|
||||
{
|
||||
name: "custom namespace handling - name starts with repo name",
|
||||
namespace: "custom-namespace",
|
||||
nameOrValue: "test-repo-secret-name",
|
||||
setupMocks: func(s *testSetup) {
|
||||
s.mockSecrets.EXPECT().Decrypt(s.ctx, "custom-namespace", "test-repo-secret-name").Return([]byte("test-data"), nil)
|
||||
},
|
||||
expectedResult: []byte("test-data"),
|
||||
},
|
||||
{
|
||||
name: "exact repo name match - should use new service",
|
||||
namespace: "test-namespace",
|
||||
nameOrValue: "test-repo",
|
||||
setupMocks: func(s *testSetup) {
|
||||
s.mockSecrets.EXPECT().Decrypt(s.ctx, "test-namespace", "test-repo").Return([]byte("exact-match-data"), nil)
|
||||
},
|
||||
expectedResult: []byte("exact-match-data"),
|
||||
},
|
||||
{
|
||||
name: "partial repo name match - should use legacy service",
|
||||
namespace: "test-namespace",
|
||||
nameOrValue: "test-rep",
|
||||
setupMocks: func(s *testSetup) {
|
||||
s.mockLegacy.EXPECT().Decrypt(s.ctx, []byte("test-rep")).Return([]byte("partial-match-data"), nil)
|
||||
},
|
||||
expectedResult: []byte("partial-match-data"),
|
||||
},
|
||||
{
|
||||
name: "empty name - should use legacy service",
|
||||
namespace: "test-namespace",
|
||||
nameOrValue: "",
|
||||
setupMocks: func(s *testSetup) {
|
||||
s.mockLegacy.EXPECT().Decrypt(s.ctx, []byte("")).Return([]byte("empty-name-data"), nil)
|
||||
},
|
||||
expectedResult: []byte("empty-name-data"),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
setup := setupTest(t, tt.namespace)
|
||||
tt.setupMocks(setup)
|
||||
|
||||
result, err := setup.rs.Decrypt(setup.ctx, setup.repo, tt.nameOrValue)
|
||||
|
||||
if tt.expectedError != "" {
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), tt.expectedError)
|
||||
assert.Nil(t, result)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tt.expectedResult, result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRepositorySecrets_Delete(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
namespace string
|
||||
featureEnabled bool
|
||||
setupMocks func(*testSetup)
|
||||
expectedError string
|
||||
}{
|
||||
{
|
||||
name: "new service delete success",
|
||||
namespace: "test-namespace",
|
||||
featureEnabled: true,
|
||||
setupMocks: func(s *testSetup) {
|
||||
s.expectFeatureFlag(true)
|
||||
s.mockSecrets.EXPECT().Delete(s.ctx, "test-namespace", "secret-to-delete").Return(nil)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "new service delete error",
|
||||
namespace: "test-namespace",
|
||||
featureEnabled: true,
|
||||
setupMocks: func(s *testSetup) {
|
||||
s.expectFeatureFlag(true)
|
||||
s.mockSecrets.EXPECT().Delete(s.ctx, "test-namespace", "secret-to-delete").Return(errors.New("delete failed"))
|
||||
},
|
||||
expectedError: "delete failed",
|
||||
},
|
||||
{
|
||||
name: "new service secret not found - should succeed",
|
||||
namespace: "test-namespace",
|
||||
featureEnabled: true,
|
||||
setupMocks: func(s *testSetup) {
|
||||
s.expectFeatureFlag(true)
|
||||
s.mockSecrets.EXPECT().Delete(s.ctx, "test-namespace", "non-existent-secret").Return(contracts.ErrSecureValueNotFound)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "nothing for legacy",
|
||||
namespace: "custom-namespace",
|
||||
featureEnabled: true,
|
||||
setupMocks: func(s *testSetup) {
|
||||
s.expectFeatureFlag(false)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
setup := setupTest(t, tt.namespace)
|
||||
tt.setupMocks(setup)
|
||||
|
||||
secretName := "secret-to-delete"
|
||||
if tt.name == "new service secret not found - should succeed" {
|
||||
secretName = "non-existent-secret"
|
||||
}
|
||||
err := setup.rs.Delete(setup.ctx, setup.repo, secretName)
|
||||
if tt.expectedError != "" {
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), tt.expectedError)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
setup.mockSecrets.AssertExpectations(t)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,165 +0,0 @@
|
||||
package secrets
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
|
||||
provisioning "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/secret"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/secret/contracts"
|
||||
)
|
||||
|
||||
const svcName = provisioning.GROUP
|
||||
|
||||
//go:generate mockery --name SecureValueClient --structname MockSecureValueClient --inpackage --filename secure_value_client_mock.go --with-expecter
|
||||
type SecureValueClient = secret.SecureValueClient
|
||||
|
||||
//go:generate mockery --name Service --structname MockService --inpackage --filename secret_mock.go --with-expecter
|
||||
type Service interface {
|
||||
Encrypt(ctx context.Context, namespace, name string, data string) (string, error)
|
||||
Decrypt(ctx context.Context, namespace string, name string) ([]byte, error)
|
||||
Delete(ctx context.Context, namespace string, name string) error
|
||||
}
|
||||
|
||||
var _ Service = (*secretsService)(nil)
|
||||
|
||||
//go:generate mockery --name DecryptService --structname MockDecryptService --srcpkg=github.com/grafana/grafana/pkg/registry/apis/secret --filename decrypt_service_mock.go --with-expecter
|
||||
type secretsService struct {
|
||||
secureValues SecureValueClient
|
||||
decryptSvc secret.DecryptService
|
||||
decrypterServiceName string
|
||||
}
|
||||
|
||||
func NewSecretsService(secretsSvc SecureValueClient, decryptSvc secret.DecryptService, grpcGrafanaServiceName string) Service {
|
||||
return &secretsService{
|
||||
secureValues: secretsSvc,
|
||||
decryptSvc: decryptSvc,
|
||||
decrypterServiceName: grpcGrafanaServiceName,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *secretsService) Encrypt(ctx context.Context, namespace, name string, data string) (string, error) {
|
||||
client, err := s.secureValues.Client(ctx, namespace)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Try to get existing secret
|
||||
existingUnstructured, err := client.Get(ctx, name, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
// If secret doesn't exist (not found error), we'll create it
|
||||
// For other errors, return the error
|
||||
if !errors.Is(err, contracts.ErrSecureValueNotFound) {
|
||||
// Check if it's a k8s not found error
|
||||
if !isNotFoundError(err) {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if existingUnstructured != nil {
|
||||
// Update the value directly in the unstructured object
|
||||
if err := unstructured.SetNestedField(existingUnstructured.Object, data, "spec", "value"); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Update using dynamic client
|
||||
result, err := client.Update(ctx, existingUnstructured, metav1.UpdateOptions{})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return result.GetName(), nil
|
||||
}
|
||||
|
||||
decrypters := []string{svcName}
|
||||
if s.decrypterServiceName != "" {
|
||||
decrypters = append(decrypters, s.decrypterServiceName)
|
||||
}
|
||||
|
||||
// Create the secret directly as unstructured
|
||||
secret := &unstructured.Unstructured{
|
||||
Object: map[string]interface{}{
|
||||
"apiVersion": "secret.grafana.app/v1beta1",
|
||||
"kind": "SecureValue",
|
||||
"metadata": map[string]interface{}{
|
||||
"namespace": namespace,
|
||||
"name": name,
|
||||
},
|
||||
"spec": map[string]interface{}{
|
||||
"description": "provisioning: " + name,
|
||||
"value": data,
|
||||
"decrypters": decrypters,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Create new secret
|
||||
finalSecret, err := client.Create(ctx, secret, metav1.CreateOptions{})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return finalSecret.GetName(), nil
|
||||
}
|
||||
|
||||
func (s *secretsService) Decrypt(ctx context.Context, namespace string, name string) ([]byte, error) {
|
||||
results, err := s.decryptSvc.Decrypt(ctx, svcName, namespace, name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if res, ok := results[name]; ok {
|
||||
if res.Error() == nil {
|
||||
return []byte(res.Value().DangerouslyExposeAndConsumeValue()), nil
|
||||
}
|
||||
|
||||
return nil, res.Error()
|
||||
}
|
||||
|
||||
return nil, contracts.ErrDecryptNotFound
|
||||
}
|
||||
|
||||
func (s *secretsService) Delete(ctx context.Context, namespace string, name string) error {
|
||||
client, err := s.secureValues.Client(ctx, namespace)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := client.Delete(ctx, name, metav1.DeleteOptions{}); err != nil {
|
||||
// FIXME: This is a temporary workaround until the client abstraction properly handles
|
||||
// k8s not found errors. The client should normalize these errors to return contracts.ErrSecureValueNotFound
|
||||
if isNotFoundError(err) {
|
||||
return contracts.ErrSecureValueNotFound
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Helper function to check if error is a not found error
|
||||
// FIXME: This is a temporary workaround until the client abstraction properly handles
|
||||
// k8s not found errors. The client should normalize these errors to return contracts.ErrSecureValueNotFound
|
||||
func isNotFoundError(err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check for Grafana's secure value not found error
|
||||
if errors.Is(err, contracts.ErrSecureValueNotFound) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Check for k8s not found error
|
||||
if apierrors.IsNotFound(err) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Fallback for generic not found error messages
|
||||
return err.Error() == "not found"
|
||||
}
|
||||
@@ -1,203 +0,0 @@
|
||||
// Code generated by mockery v2.53.4. DO NOT EDIT.
|
||||
|
||||
package secrets
|
||||
|
||||
import (
|
||||
context "context"
|
||||
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
// MockService is an autogenerated mock type for the Service type
|
||||
type MockService struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
type MockService_Expecter struct {
|
||||
mock *mock.Mock
|
||||
}
|
||||
|
||||
func (_m *MockService) EXPECT() *MockService_Expecter {
|
||||
return &MockService_Expecter{mock: &_m.Mock}
|
||||
}
|
||||
|
||||
// Decrypt provides a mock function with given fields: ctx, namespace, name
|
||||
func (_m *MockService) Decrypt(ctx context.Context, namespace string, name string) ([]byte, error) {
|
||||
ret := _m.Called(ctx, namespace, name)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Decrypt")
|
||||
}
|
||||
|
||||
var r0 []byte
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, string, string) ([]byte, error)); ok {
|
||||
return rf(ctx, namespace, name)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(context.Context, string, string) []byte); ok {
|
||||
r0 = rf(ctx, namespace, name)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]byte)
|
||||
}
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(context.Context, string, string) error); ok {
|
||||
r1 = rf(ctx, namespace, name)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// MockService_Decrypt_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Decrypt'
|
||||
type MockService_Decrypt_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// Decrypt is a helper method to define mock.On call
|
||||
// - ctx context.Context
|
||||
// - namespace string
|
||||
// - name string
|
||||
func (_e *MockService_Expecter) Decrypt(ctx interface{}, namespace interface{}, name interface{}) *MockService_Decrypt_Call {
|
||||
return &MockService_Decrypt_Call{Call: _e.mock.On("Decrypt", ctx, namespace, name)}
|
||||
}
|
||||
|
||||
func (_c *MockService_Decrypt_Call) Run(run func(ctx context.Context, namespace string, name string)) *MockService_Decrypt_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run(args[0].(context.Context), args[1].(string), args[2].(string))
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockService_Decrypt_Call) Return(_a0 []byte, _a1 error) *MockService_Decrypt_Call {
|
||||
_c.Call.Return(_a0, _a1)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockService_Decrypt_Call) RunAndReturn(run func(context.Context, string, string) ([]byte, error)) *MockService_Decrypt_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// Delete provides a mock function with given fields: ctx, namespace, name
|
||||
func (_m *MockService) Delete(ctx context.Context, namespace string, name string) error {
|
||||
ret := _m.Called(ctx, namespace, name)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Delete")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, string, string) error); ok {
|
||||
r0 = rf(ctx, namespace, name)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// MockService_Delete_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Delete'
|
||||
type MockService_Delete_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// Delete is a helper method to define mock.On call
|
||||
// - ctx context.Context
|
||||
// - namespace string
|
||||
// - name string
|
||||
func (_e *MockService_Expecter) Delete(ctx interface{}, namespace interface{}, name interface{}) *MockService_Delete_Call {
|
||||
return &MockService_Delete_Call{Call: _e.mock.On("Delete", ctx, namespace, name)}
|
||||
}
|
||||
|
||||
func (_c *MockService_Delete_Call) Run(run func(ctx context.Context, namespace string, name string)) *MockService_Delete_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run(args[0].(context.Context), args[1].(string), args[2].(string))
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockService_Delete_Call) Return(_a0 error) *MockService_Delete_Call {
|
||||
_c.Call.Return(_a0)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockService_Delete_Call) RunAndReturn(run func(context.Context, string, string) error) *MockService_Delete_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// Encrypt provides a mock function with given fields: ctx, namespace, name, data
|
||||
func (_m *MockService) Encrypt(ctx context.Context, namespace string, name string, data string) (string, error) {
|
||||
ret := _m.Called(ctx, namespace, name, data)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Encrypt")
|
||||
}
|
||||
|
||||
var r0 string
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, string, string, string) (string, error)); ok {
|
||||
return rf(ctx, namespace, name, data)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(context.Context, string, string, string) string); ok {
|
||||
r0 = rf(ctx, namespace, name, data)
|
||||
} else {
|
||||
r0 = ret.Get(0).(string)
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(context.Context, string, string, string) error); ok {
|
||||
r1 = rf(ctx, namespace, name, data)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// MockService_Encrypt_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Encrypt'
|
||||
type MockService_Encrypt_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// Encrypt is a helper method to define mock.On call
|
||||
// - ctx context.Context
|
||||
// - namespace string
|
||||
// - name string
|
||||
// - data string
|
||||
func (_e *MockService_Expecter) Encrypt(ctx interface{}, namespace interface{}, name interface{}, data interface{}) *MockService_Encrypt_Call {
|
||||
return &MockService_Encrypt_Call{Call: _e.mock.On("Encrypt", ctx, namespace, name, data)}
|
||||
}
|
||||
|
||||
func (_c *MockService_Encrypt_Call) Run(run func(ctx context.Context, namespace string, name string, data string)) *MockService_Encrypt_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run(args[0].(context.Context), args[1].(string), args[2].(string), args[3].(string))
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockService_Encrypt_Call) Return(_a0 string, _a1 error) *MockService_Encrypt_Call {
|
||||
_c.Call.Return(_a0, _a1)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockService_Encrypt_Call) RunAndReturn(run func(context.Context, string, string, string) (string, error)) *MockService_Encrypt_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// NewMockService creates a new instance of MockService. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
|
||||
// The first argument is typically a *testing.T value.
|
||||
func NewMockService(t interface {
|
||||
mock.TestingT
|
||||
Cleanup(func())
|
||||
}) *MockService {
|
||||
mock := &MockService{}
|
||||
mock.Mock.Test(t)
|
||||
|
||||
t.Cleanup(func() { mock.AssertExpectations(t) })
|
||||
|
||||
return mock
|
||||
}
|
||||
@@ -1,573 +0,0 @@
|
||||
package secrets
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/client-go/dynamic"
|
||||
|
||||
secretv1beta1 "github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/secret"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/secret/contracts"
|
||||
)
|
||||
|
||||
// mockDynamicInterface implements a simplified version of the dynamic.ResourceInterface
|
||||
type mockDynamicInterface struct {
|
||||
dynamic.ResourceInterface
|
||||
getResult *unstructured.Unstructured
|
||||
getErr error
|
||||
createResult *unstructured.Unstructured
|
||||
createErr error
|
||||
updateResult *unstructured.Unstructured
|
||||
updateErr error
|
||||
deleteErr error
|
||||
}
|
||||
|
||||
func (m *mockDynamicInterface) Get(ctx context.Context, name string, options metav1.GetOptions, subresources ...string) (*unstructured.Unstructured, error) {
|
||||
return m.getResult, m.getErr
|
||||
}
|
||||
|
||||
func (m *mockDynamicInterface) Create(ctx context.Context, obj *unstructured.Unstructured, options metav1.CreateOptions, subresources ...string) (*unstructured.Unstructured, error) {
|
||||
return m.createResult, m.createErr
|
||||
}
|
||||
|
||||
func (m *mockDynamicInterface) Update(ctx context.Context, obj *unstructured.Unstructured, options metav1.UpdateOptions, subresources ...string) (*unstructured.Unstructured, error) {
|
||||
return m.updateResult, m.updateErr
|
||||
}
|
||||
|
||||
func (m *mockDynamicInterface) Delete(ctx context.Context, name string, options metav1.DeleteOptions, subresources ...string) error {
|
||||
return m.deleteErr
|
||||
}
|
||||
|
||||
func TestNewSecretsService(t *testing.T) {
|
||||
mockSecretsSvc := NewMockSecureValueClient(t)
|
||||
mockDecryptSvc := &secret.MockDecryptService{}
|
||||
|
||||
svc := NewSecretsService(mockSecretsSvc, mockDecryptSvc, "")
|
||||
|
||||
assert.NotNil(t, svc)
|
||||
assert.IsType(t, &secretsService{}, svc)
|
||||
}
|
||||
|
||||
//nolint:gocyclo // This test is complex but it's a good test for the SecretsService.
|
||||
func TestSecretsService_Encrypt(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
namespace string
|
||||
secretName string
|
||||
data string
|
||||
setupMocks func(*MockSecureValueClient, *secret.MockDecryptService, *mockDynamicInterface)
|
||||
expectedName string
|
||||
expectedError string
|
||||
}{
|
||||
{
|
||||
name: "successfully create new secret",
|
||||
namespace: "test-namespace",
|
||||
secretName: "test-secret",
|
||||
data: "secret-data",
|
||||
setupMocks: func(mockSecretsSvc *MockSecureValueClient, mockDecryptSvc *secret.MockDecryptService, mockResourceInterface *mockDynamicInterface) {
|
||||
// Setup client to return the mock resource interface
|
||||
mockSecretsSvc.EXPECT().Client(mock.Anything, "test-namespace").Return(mockResourceInterface, nil)
|
||||
|
||||
// Mock Get call to return not found error (secret doesn't exist)
|
||||
mockResourceInterface.getResult = nil
|
||||
mockResourceInterface.getErr = contracts.ErrSecureValueNotFound
|
||||
|
||||
// Mock Create call
|
||||
mockResourceInterface.createResult = &unstructured.Unstructured{
|
||||
Object: map[string]interface{}{
|
||||
"metadata": map[string]interface{}{
|
||||
"name": "test-secret",
|
||||
"namespace": "test-namespace",
|
||||
},
|
||||
},
|
||||
}
|
||||
mockResourceInterface.createErr = nil
|
||||
},
|
||||
expectedName: "test-secret",
|
||||
},
|
||||
{
|
||||
name: "successfully update existing secret",
|
||||
namespace: "test-namespace",
|
||||
secretName: "existing-secret",
|
||||
data: "new-secret-data",
|
||||
setupMocks: func(mockSecretsSvc *MockSecureValueClient, mockDecryptSvc *secret.MockDecryptService, mockResourceInterface *mockDynamicInterface) {
|
||||
existingSecret := &unstructured.Unstructured{
|
||||
Object: map[string]interface{}{
|
||||
"metadata": map[string]interface{}{
|
||||
"name": "existing-secret",
|
||||
"namespace": "test-namespace",
|
||||
},
|
||||
"spec": map[string]interface{}{
|
||||
"description": "provisioning: existing-secret",
|
||||
"decrypters": []string{svcName},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Setup client to return the mock resource interface
|
||||
mockSecretsSvc.EXPECT().Client(mock.Anything, "test-namespace").Return(mockResourceInterface, nil)
|
||||
|
||||
// Mock Get call to return existing secret
|
||||
mockResourceInterface.getResult = existingSecret
|
||||
mockResourceInterface.getErr = nil
|
||||
|
||||
// Mock Update call
|
||||
mockResourceInterface.updateResult = &unstructured.Unstructured{
|
||||
Object: map[string]interface{}{
|
||||
"metadata": map[string]interface{}{
|
||||
"name": "existing-secret",
|
||||
"namespace": "test-namespace",
|
||||
},
|
||||
},
|
||||
}
|
||||
mockResourceInterface.updateErr = nil
|
||||
},
|
||||
expectedName: "existing-secret",
|
||||
},
|
||||
{
|
||||
name: "error reading existing secret",
|
||||
namespace: "test-namespace",
|
||||
secretName: "test-secret",
|
||||
data: "secret-data",
|
||||
setupMocks: func(mockSecretsSvc *MockSecureValueClient, mockDecryptSvc *secret.MockDecryptService, mockResourceInterface *mockDynamicInterface) {
|
||||
// Setup client to return the mock resource interface
|
||||
mockSecretsSvc.EXPECT().Client(mock.Anything, "test-namespace").Return(mockResourceInterface, nil)
|
||||
|
||||
// Mock Get call to return error
|
||||
mockResourceInterface.getResult = nil
|
||||
mockResourceInterface.getErr = errors.New("database error")
|
||||
},
|
||||
expectedError: "database error",
|
||||
},
|
||||
{
|
||||
name: "error creating new secret",
|
||||
namespace: "test-namespace",
|
||||
secretName: "test-secret",
|
||||
data: "secret-data",
|
||||
setupMocks: func(mockSecretsSvc *MockSecureValueClient, mockDecryptSvc *secret.MockDecryptService, mockResourceInterface *mockDynamicInterface) {
|
||||
// Setup client to return the mock resource interface
|
||||
mockSecretsSvc.EXPECT().Client(mock.Anything, "test-namespace").Return(mockResourceInterface, nil)
|
||||
|
||||
// Mock Get call to return not found error
|
||||
mockResourceInterface.getResult = nil
|
||||
mockResourceInterface.getErr = contracts.ErrSecureValueNotFound
|
||||
|
||||
// Mock Create call to return error
|
||||
mockResourceInterface.createResult = nil
|
||||
mockResourceInterface.createErr = errors.New("creation failed")
|
||||
},
|
||||
expectedError: "creation failed",
|
||||
},
|
||||
{
|
||||
name: "error updating existing secret",
|
||||
namespace: "test-namespace",
|
||||
secretName: "existing-secret",
|
||||
data: "new-secret-data",
|
||||
setupMocks: func(mockSecretsSvc *MockSecureValueClient, mockDecryptSvc *secret.MockDecryptService, mockResourceInterface *mockDynamicInterface) {
|
||||
existingSecret := &unstructured.Unstructured{
|
||||
Object: map[string]interface{}{
|
||||
"metadata": map[string]interface{}{
|
||||
"name": "existing-secret",
|
||||
"namespace": "test-namespace",
|
||||
},
|
||||
"spec": map[string]interface{}{
|
||||
"description": "provisioning: existing-secret",
|
||||
"decrypters": []string{svcName},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Setup client to return the mock resource interface
|
||||
mockSecretsSvc.EXPECT().Client(mock.Anything, "test-namespace").Return(mockResourceInterface, nil)
|
||||
|
||||
// Mock Get call to return existing secret
|
||||
mockResourceInterface.getResult = existingSecret
|
||||
mockResourceInterface.getErr = nil
|
||||
|
||||
// Mock Update call to return error
|
||||
mockResourceInterface.updateResult = nil
|
||||
mockResourceInterface.updateErr = errors.New("update failed")
|
||||
},
|
||||
expectedError: "update failed",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
mockSecretsSvc := NewMockSecureValueClient(t)
|
||||
mockDecryptSvc := &secret.MockDecryptService{}
|
||||
mockResourceInterface := &mockDynamicInterface{}
|
||||
|
||||
tt.setupMocks(mockSecretsSvc, mockDecryptSvc, mockResourceInterface)
|
||||
|
||||
svc := NewSecretsService(mockSecretsSvc, mockDecryptSvc, "")
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
result, err := svc.Encrypt(ctx, tt.namespace, tt.secretName, tt.data)
|
||||
|
||||
if tt.expectedError != "" {
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), tt.expectedError)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tt.expectedName, result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSecretsService_Encrypt_ClientError(t *testing.T) {
|
||||
mockSecretsSvc := NewMockSecureValueClient(t)
|
||||
mockDecryptSvc := &secret.MockDecryptService{}
|
||||
|
||||
// Setup client to return error
|
||||
mockSecretsSvc.EXPECT().Client(mock.Anything, "test-namespace").Return(nil, errors.New("client error"))
|
||||
|
||||
svc := NewSecretsService(mockSecretsSvc, mockDecryptSvc, "")
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
result, err := svc.Encrypt(ctx, "test-namespace", "test-secret", "secret-data")
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "client error")
|
||||
assert.Empty(t, result)
|
||||
}
|
||||
|
||||
func TestSecretsService_Decrypt(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
namespace string
|
||||
secretName string
|
||||
setupMocks func(*MockSecureValueClient, *secret.MockDecryptService)
|
||||
expectedResult []byte
|
||||
expectedError string
|
||||
}{
|
||||
{
|
||||
name: "successfully decrypt secret",
|
||||
namespace: "test-namespace",
|
||||
secretName: "test-secret",
|
||||
setupMocks: func(mockSecretsSvc *MockSecureValueClient, mockDecryptSvc *secret.MockDecryptService) {
|
||||
exposedValue := secretv1beta1.NewExposedSecureValue("decrypted-data")
|
||||
mockResult := secret.NewDecryptResultValue(&exposedValue)
|
||||
|
||||
mockDecryptSvc.EXPECT().Decrypt(
|
||||
mock.MatchedBy(func(ctx context.Context) bool {
|
||||
// Verify that the context is not nil (the service creates a new StaticRequester)
|
||||
return ctx != nil
|
||||
}),
|
||||
svcName,
|
||||
"test-namespace",
|
||||
"test-secret",
|
||||
).Return(map[string]secret.DecryptResult{
|
||||
"test-secret": mockResult,
|
||||
}, nil)
|
||||
},
|
||||
expectedResult: []byte("decrypted-data"),
|
||||
},
|
||||
{
|
||||
name: "decrypt service error",
|
||||
namespace: "test-namespace",
|
||||
secretName: "test-secret",
|
||||
setupMocks: func(mockSecretsSvc *MockSecureValueClient, mockDecryptSvc *secret.MockDecryptService) {
|
||||
mockDecryptSvc.EXPECT().Decrypt(
|
||||
mock.MatchedBy(func(ctx context.Context) bool {
|
||||
return ctx != nil
|
||||
}),
|
||||
svcName,
|
||||
"test-namespace",
|
||||
"test-secret",
|
||||
).Return(nil, errors.New("decrypt service error"))
|
||||
},
|
||||
expectedError: "decrypt service error",
|
||||
},
|
||||
{
|
||||
name: "secret not found in results",
|
||||
namespace: "test-namespace",
|
||||
secretName: "test-secret",
|
||||
setupMocks: func(mockSecretsSvc *MockSecureValueClient, mockDecryptSvc *secret.MockDecryptService) {
|
||||
mockDecryptSvc.EXPECT().Decrypt(
|
||||
mock.MatchedBy(func(ctx context.Context) bool {
|
||||
return ctx != nil
|
||||
}),
|
||||
svcName,
|
||||
"test-namespace",
|
||||
"test-secret",
|
||||
).Return(map[string]secret.DecryptResult{}, nil)
|
||||
},
|
||||
expectedError: secret.ErrDecryptNotFound.Error(),
|
||||
},
|
||||
{
|
||||
name: "decrypt result has error",
|
||||
namespace: "test-namespace",
|
||||
secretName: "test-secret",
|
||||
setupMocks: func(mockSecretsSvc *MockSecureValueClient, mockDecryptSvc *secret.MockDecryptService) {
|
||||
mockResult := secret.NewDecryptResultErr(errors.New("decryption failed"))
|
||||
|
||||
mockDecryptSvc.EXPECT().Decrypt(
|
||||
mock.MatchedBy(func(ctx context.Context) bool {
|
||||
return ctx != nil
|
||||
}),
|
||||
svcName,
|
||||
"test-namespace",
|
||||
"test-secret",
|
||||
).Return(map[string]secret.DecryptResult{
|
||||
"test-secret": mockResult,
|
||||
}, nil)
|
||||
},
|
||||
expectedError: "decryption failed",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
mockSecretsSvc := NewMockSecureValueClient(t)
|
||||
mockDecryptSvc := &secret.MockDecryptService{}
|
||||
|
||||
tt.setupMocks(mockSecretsSvc, mockDecryptSvc)
|
||||
|
||||
svc := NewSecretsService(mockSecretsSvc, mockDecryptSvc, "")
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
result, err := svc.Decrypt(ctx, tt.namespace, tt.secretName)
|
||||
|
||||
if tt.expectedError != "" {
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), tt.expectedError)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tt.expectedResult, result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Test to verify that the Decrypt method creates the correct service identity context
|
||||
func TestSecretsService_Decrypt_ServiceIdentityContext(t *testing.T) {
|
||||
mockSecretsSvc := NewMockSecureValueClient(t)
|
||||
mockDecryptSvc := &secret.MockDecryptService{}
|
||||
|
||||
exposedValue := secretv1beta1.NewExposedSecureValue("test-data")
|
||||
mockResult := secret.NewDecryptResultValue(&exposedValue)
|
||||
|
||||
// Create a more detailed context matcher to verify the service identity context is created correctly
|
||||
mockDecryptSvc.EXPECT().Decrypt(
|
||||
mock.MatchedBy(func(ctx context.Context) bool {
|
||||
// At minimum, verify the context is not nil and is different from the original
|
||||
return ctx != nil
|
||||
}),
|
||||
svcName,
|
||||
"test-namespace",
|
||||
"test-secret",
|
||||
).Return(map[string]secret.DecryptResult{
|
||||
"test-secret": mockResult,
|
||||
}, nil)
|
||||
|
||||
svc := NewSecretsService(mockSecretsSvc, mockDecryptSvc, "")
|
||||
|
||||
ctx := context.Background()
|
||||
result, err := svc.Decrypt(ctx, "test-namespace", "test-secret")
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []byte("test-data"), result)
|
||||
}
|
||||
|
||||
func TestSecretsService_Delete(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
namespace string
|
||||
secretName string
|
||||
setupMocks func(mockSecretsSvc *MockSecureValueClient, mockDecryptSvc *secret.MockDecryptService, mockResourceInterface *mockDynamicInterface)
|
||||
expectedError string
|
||||
}{
|
||||
{
|
||||
name: "delete success",
|
||||
namespace: "test-namespace",
|
||||
secretName: "test-secret",
|
||||
setupMocks: func(mockSecretsSvc *MockSecureValueClient, mockDecryptSvc *secret.MockDecryptService, mockResourceInterface *mockDynamicInterface) {
|
||||
// Setup client to return the mock resource interface
|
||||
mockSecretsSvc.EXPECT().Client(mock.Anything, "test-namespace").Return(mockResourceInterface, nil)
|
||||
|
||||
// Mock Delete call
|
||||
mockResourceInterface.deleteErr = nil
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "delete returns error",
|
||||
namespace: "test-namespace",
|
||||
secretName: "test-secret",
|
||||
setupMocks: func(mockSecretsSvc *MockSecureValueClient, mockDecryptSvc *secret.MockDecryptService, mockResourceInterface *mockDynamicInterface) {
|
||||
// Setup client to return the mock resource interface
|
||||
mockSecretsSvc.EXPECT().Client(mock.Anything, "test-namespace").Return(mockResourceInterface, nil)
|
||||
|
||||
// Mock Delete call to return error
|
||||
mockResourceInterface.deleteErr = errors.New("delete failed")
|
||||
},
|
||||
expectedError: "delete failed",
|
||||
},
|
||||
{
|
||||
name: "client error",
|
||||
namespace: "test-namespace",
|
||||
secretName: "test-secret",
|
||||
setupMocks: func(mockSecretsSvc *MockSecureValueClient, mockDecryptSvc *secret.MockDecryptService, mockResourceInterface *mockDynamicInterface) {
|
||||
// Setup client to return error
|
||||
mockSecretsSvc.EXPECT().Client(mock.Anything, "test-namespace").Return(nil, errors.New("client error"))
|
||||
},
|
||||
expectedError: "client error",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
mockSecretsSvc := NewMockSecureValueClient(t)
|
||||
mockDecryptSvc := &secret.MockDecryptService{}
|
||||
mockResourceInterface := &mockDynamicInterface{}
|
||||
|
||||
tt.setupMocks(mockSecretsSvc, mockDecryptSvc, mockResourceInterface)
|
||||
|
||||
svc := NewSecretsService(mockSecretsSvc, mockDecryptSvc, "")
|
||||
ctx := context.Background()
|
||||
|
||||
err := svc.Delete(ctx, tt.namespace, tt.secretName)
|
||||
|
||||
if tt.expectedError != "" {
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), tt.expectedError)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsNotFoundError(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
err error
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
name: "nil error",
|
||||
err: nil,
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "grafana secure value not found error",
|
||||
err: contracts.ErrSecureValueNotFound,
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "k8s not found error",
|
||||
err: apierrors.NewNotFound(schema.GroupResource{Group: "secret.grafana.app", Resource: "securevalues"}, "test-secret"),
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "generic not found error message",
|
||||
err: errors.New("not found"),
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "other error",
|
||||
err: errors.New("internal server error"),
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "wrapped grafana error",
|
||||
err: errors.New("wrapped: " + contracts.ErrSecureValueNotFound.Error()),
|
||||
expected: false, // wrapped errors won't match errors.Is unless properly wrapped
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := isNotFoundError(tt.err)
|
||||
assert.Equal(t, tt.expected, result, "isNotFoundError(%v) = %v, want %v", tt.err, result, tt.expected)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSecretsService_Encrypt_WithK8sNotFoundError(t *testing.T) {
|
||||
mockSecretsSvc := NewMockSecureValueClient(t)
|
||||
mockDecryptSvc := &secret.MockDecryptService{}
|
||||
mockResourceInterface := &mockDynamicInterface{}
|
||||
|
||||
// Setup client to return the mock resource interface
|
||||
mockSecretsSvc.EXPECT().Client(mock.Anything, "test-namespace").Return(mockResourceInterface, nil)
|
||||
|
||||
// Mock Get call to return k8s not found error
|
||||
k8sNotFoundErr := apierrors.NewNotFound(schema.GroupResource{Group: "secret.grafana.app", Resource: "securevalues"}, "test-secret")
|
||||
mockResourceInterface.getResult = nil
|
||||
mockResourceInterface.getErr = k8sNotFoundErr
|
||||
|
||||
// Mock Create call to succeed
|
||||
mockResourceInterface.createResult = &unstructured.Unstructured{
|
||||
Object: map[string]interface{}{
|
||||
"metadata": map[string]interface{}{
|
||||
"name": "test-secret",
|
||||
"namespace": "test-namespace",
|
||||
},
|
||||
},
|
||||
}
|
||||
mockResourceInterface.createErr = nil
|
||||
|
||||
svc := NewSecretsService(mockSecretsSvc, mockDecryptSvc, "")
|
||||
ctx := context.Background()
|
||||
|
||||
result, err := svc.Encrypt(ctx, "test-namespace", "test-secret", "secret-data")
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "test-secret", result)
|
||||
}
|
||||
|
||||
func TestSecretsService_Delete_WithK8sNotFoundError(t *testing.T) {
|
||||
mockSecretsSvc := NewMockSecureValueClient(t)
|
||||
mockDecryptSvc := &secret.MockDecryptService{}
|
||||
mockResourceInterface := &mockDynamicInterface{}
|
||||
|
||||
// Setup client to return the mock resource interface
|
||||
mockSecretsSvc.EXPECT().Client(mock.Anything, "test-namespace").Return(mockResourceInterface, nil)
|
||||
|
||||
// Mock Delete call to return k8s not found error
|
||||
k8sNotFoundErr := apierrors.NewNotFound(schema.GroupResource{Group: "secret.grafana.app", Resource: "securevalues"}, "test-secret")
|
||||
mockResourceInterface.deleteErr = k8sNotFoundErr
|
||||
|
||||
svc := NewSecretsService(mockSecretsSvc, mockDecryptSvc, "")
|
||||
ctx := context.Background()
|
||||
|
||||
err := svc.Delete(ctx, "test-namespace", "test-secret")
|
||||
|
||||
// Should return contracts.ErrSecureValueNotFound instead of k8s error
|
||||
assert.Error(t, err)
|
||||
assert.ErrorIs(t, err, contracts.ErrSecureValueNotFound)
|
||||
}
|
||||
|
||||
func TestSecretsService_Delete_WithGrafanaNotFoundError(t *testing.T) {
|
||||
mockSecretsSvc := NewMockSecureValueClient(t)
|
||||
mockDecryptSvc := &secret.MockDecryptService{}
|
||||
mockResourceInterface := &mockDynamicInterface{}
|
||||
|
||||
// Setup client to return the mock resource interface
|
||||
mockSecretsSvc.EXPECT().Client(mock.Anything, "test-namespace").Return(mockResourceInterface, nil)
|
||||
|
||||
// Mock Delete call to return Grafana not found error
|
||||
mockResourceInterface.deleteErr = contracts.ErrSecureValueNotFound
|
||||
|
||||
svc := NewSecretsService(mockSecretsSvc, mockDecryptSvc, "")
|
||||
ctx := context.Background()
|
||||
|
||||
err := svc.Delete(ctx, "test-namespace", "test-secret")
|
||||
|
||||
// Should return contracts.ErrSecureValueNotFound
|
||||
assert.Error(t, err)
|
||||
assert.ErrorIs(t, err, contracts.ErrSecureValueNotFound)
|
||||
}
|
||||
@@ -1,97 +0,0 @@
|
||||
// Code generated by mockery v2.53.4. DO NOT EDIT.
|
||||
|
||||
package secrets
|
||||
|
||||
import (
|
||||
context "context"
|
||||
|
||||
dynamic "k8s.io/client-go/dynamic"
|
||||
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
// MockSecureValueClient is an autogenerated mock type for the SecureValueClient type
|
||||
type MockSecureValueClient struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
type MockSecureValueClient_Expecter struct {
|
||||
mock *mock.Mock
|
||||
}
|
||||
|
||||
func (_m *MockSecureValueClient) EXPECT() *MockSecureValueClient_Expecter {
|
||||
return &MockSecureValueClient_Expecter{mock: &_m.Mock}
|
||||
}
|
||||
|
||||
// Client provides a mock function with given fields: ctx, namespace
|
||||
func (_m *MockSecureValueClient) Client(ctx context.Context, namespace string) (dynamic.ResourceInterface, error) {
|
||||
ret := _m.Called(ctx, namespace)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Client")
|
||||
}
|
||||
|
||||
var r0 dynamic.ResourceInterface
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, string) (dynamic.ResourceInterface, error)); ok {
|
||||
return rf(ctx, namespace)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(context.Context, string) dynamic.ResourceInterface); ok {
|
||||
r0 = rf(ctx, namespace)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(dynamic.ResourceInterface)
|
||||
}
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(context.Context, string) error); ok {
|
||||
r1 = rf(ctx, namespace)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// MockSecureValueClient_Client_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Client'
|
||||
type MockSecureValueClient_Client_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// Client is a helper method to define mock.On call
|
||||
// - ctx context.Context
|
||||
// - namespace string
|
||||
func (_e *MockSecureValueClient_Expecter) Client(ctx interface{}, namespace interface{}) *MockSecureValueClient_Client_Call {
|
||||
return &MockSecureValueClient_Client_Call{Call: _e.mock.On("Client", ctx, namespace)}
|
||||
}
|
||||
|
||||
func (_c *MockSecureValueClient_Client_Call) Run(run func(ctx context.Context, namespace string)) *MockSecureValueClient_Client_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run(args[0].(context.Context), args[1].(string))
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockSecureValueClient_Client_Call) Return(_a0 dynamic.ResourceInterface, _a1 error) *MockSecureValueClient_Client_Call {
|
||||
_c.Call.Return(_a0, _a1)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockSecureValueClient_Client_Call) RunAndReturn(run func(context.Context, string) (dynamic.ResourceInterface, error)) *MockSecureValueClient_Client_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// NewMockSecureValueClient creates a new instance of MockSecureValueClient. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
|
||||
// The first argument is typically a *testing.T value.
|
||||
func NewMockSecureValueClient(t interface {
|
||||
mock.TestingT
|
||||
Cleanup(func())
|
||||
}) *MockSecureValueClient {
|
||||
mock := &MockSecureValueClient{}
|
||||
mock.Mock.Test(t)
|
||||
|
||||
t.Cleanup(func() { mock.AssertExpectations(t) })
|
||||
|
||||
return mock
|
||||
}
|
||||
@@ -89,22 +89,31 @@ func (s *testConnector) Connect(ctx context.Context, name string, opts runtime.O
|
||||
// HACK: Set the name and namespace if not set so that the temporary repository can be created
|
||||
// This can be removed once we deprecate legacy secrets is deprecated or we use InLineSecureValues as we
|
||||
// use the same field and repository name to detect which one to use.
|
||||
if cfg.GetName() == "" {
|
||||
if name == "new" {
|
||||
// HACK: frontend is passing a "new" we need to remove the hack there as well
|
||||
// Otherwise creation will fail as `new` is a reserved word. Not relevant here as we only "test"
|
||||
name = "hack-on-hack-for-new"
|
||||
if name == "new" {
|
||||
// HACK: frontend is passing a "new" we need to remove the hack there as well
|
||||
// Otherwise creation will fail as `new` is a reserved word. Not relevant here as we only "test"
|
||||
name = "hack-on-hack-for-new"
|
||||
} else {
|
||||
// Copy previous secure values if they exist
|
||||
old, _ := s.getter.GetRepository(ctx, name)
|
||||
if old != nil && !old.Config().Secure.IsZero() {
|
||||
secure := old.Config().Secure
|
||||
if cfg.Secure.Token.IsZero() {
|
||||
cfg.Secure.Token = secure.Token
|
||||
}
|
||||
if cfg.Secure.WebhookSecret.IsZero() {
|
||||
cfg.Secure.WebhookSecret = secure.WebhookSecret
|
||||
}
|
||||
}
|
||||
|
||||
cfg.SetName(name)
|
||||
}
|
||||
|
||||
cfg.SetName(name)
|
||||
if cfg.GetNamespace() == "" {
|
||||
cfg.SetNamespace(ns)
|
||||
}
|
||||
|
||||
// Create a temporary repository
|
||||
tmp, err := s.getter.AsRepository(ctx, &cfg)
|
||||
tmp, err := s.getter.RepositoryFromConfig(ctx, &cfg)
|
||||
if err != nil {
|
||||
responder.Error(err)
|
||||
return
|
||||
|
||||
@@ -18,7 +18,7 @@ type RepoGetter interface {
|
||||
// Given a repository configuration, return it as a repository instance
|
||||
// This will only error for un-recoverable system errors
|
||||
// the repository instance may or may not be valid/healthy
|
||||
AsRepository(ctx context.Context, cfg *provisioning.Repository) (repository.Repository, error)
|
||||
RepositoryFromConfig(ctx context.Context, cfg *provisioning.Repository) (repository.Repository, error)
|
||||
}
|
||||
|
||||
type ClientGetter interface {
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
package webhooks
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
provisioning "github.com/grafana/grafana/apps/provisioning/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
|
||||
}
|
||||
}
|
||||
@@ -1,157 +0,0 @@
|
||||
package webhooks
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
provisioning "github.com/grafana/grafana/apps/provisioning/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")
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -18,7 +18,6 @@ import (
|
||||
"github.com/grafana/grafana/pkg/registry/apis/provisioning/repository/git"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/provisioning/repository/github"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/provisioning/resources"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/provisioning/secrets"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/provisioning/webhooks/pullrequest"
|
||||
"github.com/grafana/grafana/pkg/services/apiserver"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
@@ -47,7 +46,6 @@ func isPublicURL(url string) bool {
|
||||
func ProvideWebhooks(
|
||||
cfg *setting.Cfg,
|
||||
features featuremgmt.FeatureToggles,
|
||||
repositorySecrets secrets.RepositorySecrets,
|
||||
ghFactory *github.Factory,
|
||||
renderer rendering.Service,
|
||||
blobstore resource.ResourceClient,
|
||||
@@ -79,7 +77,6 @@ func ProvideWebhooks(
|
||||
render,
|
||||
webhook,
|
||||
urlProvider,
|
||||
repositorySecrets,
|
||||
ghFactory,
|
||||
parsers,
|
||||
[]jobs.Worker{pullRequestWorker},
|
||||
@@ -95,7 +92,6 @@ type WebhookExtra struct {
|
||||
render *renderConnector
|
||||
webhook *webhookConnector
|
||||
urlProvider func(namespace string) string
|
||||
secrets secrets.RepositorySecrets
|
||||
ghFactory *github.Factory
|
||||
parsers resources.ParserFactory
|
||||
workers []jobs.Worker
|
||||
@@ -106,7 +102,6 @@ func NewWebhookExtra(
|
||||
render *renderConnector,
|
||||
webhook *webhookConnector,
|
||||
urlProvider func(namespace string) string,
|
||||
secrets secrets.RepositorySecrets,
|
||||
ghFactory *github.Factory,
|
||||
parsers resources.ParserFactory,
|
||||
workers []jobs.Worker,
|
||||
@@ -116,7 +111,6 @@ func NewWebhookExtra(
|
||||
render: render,
|
||||
webhook: webhook,
|
||||
urlProvider: urlProvider,
|
||||
secrets: secrets,
|
||||
ghFactory: ghFactory,
|
||||
parsers: parsers,
|
||||
workers: workers,
|
||||
@@ -136,9 +130,7 @@ func (e *WebhookExtra) Authorize(ctx context.Context, a authorizer.Attributes) (
|
||||
|
||||
// Mutators returns the mutators for the webhook extra
|
||||
func (e *WebhookExtra) Mutators() []controller.Mutator {
|
||||
return []controller.Mutator{
|
||||
Mutator(e.secrets),
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateStorage updates the storage with both render and webhook connectors
|
||||
@@ -164,7 +156,7 @@ func (e *WebhookExtra) GetJobWorkers() []jobs.Worker {
|
||||
}
|
||||
|
||||
// AsRepository delegates repository creation to the webhook connector
|
||||
func (e *WebhookExtra) AsRepository(ctx context.Context, r *provisioning.Repository) (repository.Repository, error) {
|
||||
func (e *WebhookExtra) AsRepository(ctx context.Context, r *provisioning.Repository, secure repository.SecureValues) (repository.Repository, error) {
|
||||
// Only handle GitHub repositories with webhooks if URL is public
|
||||
if r.Spec.Type == provisioning.GitHubRepositoryType && e.isPublic {
|
||||
gvr := provisioning.RepositoryResourceInfo.GroupVersionResource()
|
||||
@@ -186,34 +178,33 @@ func (e *WebhookExtra) AsRepository(ctx context.Context, r *provisioning.Reposit
|
||||
}
|
||||
|
||||
// Decrypt GitHub token if needed
|
||||
ghToken := ghCfg.Token
|
||||
if ghToken == "" && len(ghCfg.EncryptedToken) > 0 {
|
||||
decrypted, err := e.secrets.Decrypt(ctx, r, string(ghCfg.EncryptedToken))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("decrypt github token: %w", err)
|
||||
}
|
||||
ghToken = string(decrypted)
|
||||
ghToken, err := secure.Token(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("decrypt github token: %w", err)
|
||||
}
|
||||
webhookSecret, err := secure.WebhookSecret(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("decrypt webhookSecret: %w", err)
|
||||
}
|
||||
|
||||
gitCfg := git.RepositoryConfig{
|
||||
URL: ghCfg.URL,
|
||||
Branch: ghCfg.Branch,
|
||||
Path: ghCfg.Path,
|
||||
Token: ghToken,
|
||||
EncryptedToken: ghCfg.EncryptedToken,
|
||||
URL: ghCfg.URL,
|
||||
Branch: ghCfg.Branch,
|
||||
Path: ghCfg.Path,
|
||||
Token: ghToken,
|
||||
}
|
||||
|
||||
gitRepo, err := git.NewGitRepository(ctx, r, gitCfg, e.secrets)
|
||||
gitRepo, err := git.NewGitRepository(ctx, r, gitCfg)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error creating git repository: %w", err)
|
||||
}
|
||||
|
||||
basicRepo, err := github.NewGitHub(ctx, r, gitRepo, e.ghFactory, ghToken, e.secrets)
|
||||
basicRepo, err := github.NewGitHub(ctx, r, gitRepo, e.ghFactory, ghToken)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error creating github repository: %w", err)
|
||||
}
|
||||
|
||||
return NewGithubWebhookRepository(basicRepo, webhookURL, e.secrets), nil
|
||||
return NewGithubWebhookRepository(basicRepo, webhookURL, webhookSecret), nil
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
|
||||
@@ -10,18 +10,16 @@ import (
|
||||
|
||||
"github.com/google/go-github/v70/github"
|
||||
"github.com/google/uuid"
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
|
||||
"github.com/grafana/grafana-app-sdk/logging"
|
||||
provisioning "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1"
|
||||
common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/provisioning/repository"
|
||||
pgh "github.com/grafana/grafana/pkg/registry/apis/provisioning/repository/github"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/provisioning/secrets"
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
)
|
||||
|
||||
var subscribedEvents = []string{"push", "pull_request"}
|
||||
|
||||
//nolint:gosec // This is a constant for a secret suffix
|
||||
const webhookSecretSuffix = "-webhook-secret"
|
||||
var subscribedEvents = []string{"pull_request", "push"} // same order as slices.Sort()
|
||||
|
||||
type WebhookRepository interface {
|
||||
Webhook(ctx context.Context, req *http.Request) (*provisioning.WebhookResponse, error)
|
||||
@@ -39,7 +37,7 @@ type githubWebhookRepository struct {
|
||||
config *provisioning.Repository
|
||||
owner string
|
||||
repo string
|
||||
secrets secrets.RepositorySecrets
|
||||
secret common.RawSecureValue
|
||||
gh pgh.Client
|
||||
webhookURL string
|
||||
}
|
||||
@@ -47,7 +45,7 @@ type githubWebhookRepository struct {
|
||||
func NewGithubWebhookRepository(
|
||||
basic pgh.GithubRepository,
|
||||
webhookURL string,
|
||||
secrets secrets.RepositorySecrets,
|
||||
secret common.RawSecureValue,
|
||||
) GithubWebhookRepository {
|
||||
return &githubWebhookRepository{
|
||||
GithubRepository: basic,
|
||||
@@ -56,7 +54,7 @@ func NewGithubWebhookRepository(
|
||||
repo: basic.Repo(),
|
||||
gh: basic.Client(),
|
||||
webhookURL: webhookURL,
|
||||
secrets: secrets,
|
||||
secret: secret,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,12 +64,11 @@ func (r *githubWebhookRepository) Webhook(ctx context.Context, req *http.Request
|
||||
return nil, fmt.Errorf("unexpected webhook request")
|
||||
}
|
||||
|
||||
secret, err := r.secrets.Decrypt(ctx, r.config, string(r.config.Status.Webhook.EncryptedSecret))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to decrypt secret: %w", err)
|
||||
if r.secret.IsZero() {
|
||||
return nil, fmt.Errorf("missing webhook secret")
|
||||
}
|
||||
|
||||
payload, err := github.ValidatePayload(req, secret)
|
||||
payload, err := github.ValidatePayload(req, []byte(r.secret))
|
||||
if err != nil {
|
||||
return nil, apierrors.NewUnauthorized("invalid signature")
|
||||
}
|
||||
@@ -239,8 +236,6 @@ func (r *githubWebhookRepository) updateWebhook(ctx context.Context) (pgh.Webhoo
|
||||
return pgh.WebhookConfig{}, false, fmt.Errorf("get webhook: %w", err)
|
||||
}
|
||||
|
||||
hook.Secret = r.config.Status.Webhook.Secret // we always random gen this, so don't use it for mustUpdate below.
|
||||
|
||||
var mustUpdate bool
|
||||
|
||||
if hook.URL != r.webhookURL {
|
||||
@@ -248,6 +243,7 @@ func (r *githubWebhookRepository) updateWebhook(ctx context.Context) (pgh.Webhoo
|
||||
hook.URL = r.webhookURL
|
||||
}
|
||||
|
||||
slices.Sort(hook.Events) // consistent order for comparison
|
||||
if !slices.Equal(hook.Events, subscribedEvents) {
|
||||
mustUpdate = true
|
||||
hook.Events = subscribedEvents
|
||||
@@ -263,7 +259,6 @@ func (r *githubWebhookRepository) updateWebhook(ctx context.Context) (pgh.Webhoo
|
||||
return pgh.WebhookConfig{}, false, fmt.Errorf("could not generate secret: %w", err)
|
||||
}
|
||||
hook.Secret = secret.String()
|
||||
|
||||
if err := r.gh.EditWebhook(ctx, r.owner, r.repo, hook); err != nil {
|
||||
return pgh.WebhookConfig{}, false, fmt.Errorf("edit webhook: %w", err)
|
||||
}
|
||||
@@ -304,10 +299,16 @@ func (r *githubWebhookRepository) OnCreate(ctx context.Context) ([]map[string]in
|
||||
"value": &provisioning.WebhookStatus{
|
||||
ID: hook.ID,
|
||||
URL: hook.URL,
|
||||
Secret: hook.Secret,
|
||||
SubscribedEvents: hook.Events,
|
||||
},
|
||||
},
|
||||
{
|
||||
"op": "replace",
|
||||
"path": "/secure/webhookSecret",
|
||||
"value": map[string]string{
|
||||
"create": hook.Secret,
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -316,42 +317,34 @@ func (r *githubWebhookRepository) OnUpdate(ctx context.Context) ([]map[string]in
|
||||
return nil, nil
|
||||
}
|
||||
ctx, _ = r.logger(ctx, "")
|
||||
hook, _, err := r.updateWebhook(ctx)
|
||||
if err != nil {
|
||||
hook, changed, err := r.updateWebhook(ctx)
|
||||
if err != nil || !changed {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return []map[string]interface{}{
|
||||
{
|
||||
"op": "replace",
|
||||
"path": "/status/webhook",
|
||||
"value": &provisioning.WebhookStatus{
|
||||
ID: hook.ID,
|
||||
URL: hook.URL,
|
||||
Secret: hook.Secret,
|
||||
SubscribedEvents: hook.Events,
|
||||
},
|
||||
// update the webhook and secret
|
||||
return []map[string]any{{
|
||||
"op": "replace",
|
||||
"path": "/status/webhook",
|
||||
"value": &provisioning.WebhookStatus{
|
||||
ID: hook.ID,
|
||||
URL: hook.URL,
|
||||
SubscribedEvents: hook.Events,
|
||||
},
|
||||
}, nil
|
||||
}, {
|
||||
"op": "replace",
|
||||
"path": "/secure/webhookSecret",
|
||||
"value": map[string]string{
|
||||
"create": hook.Secret,
|
||||
},
|
||||
}}, nil
|
||||
}
|
||||
|
||||
func (r *githubWebhookRepository) OnDelete(ctx context.Context) error {
|
||||
ctx, logger := r.logger(ctx, "")
|
||||
if err := r.GithubRepository.OnDelete(ctx); err != nil {
|
||||
return fmt.Errorf("on delete from basic github repository: %w", err)
|
||||
}
|
||||
|
||||
if r.config.Status.Webhook == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
secretName := r.config.Name + webhookSecretSuffix
|
||||
if err := r.secrets.Delete(ctx, r.config, secretName); err != nil {
|
||||
return fmt.Errorf("delete webhook secret: %w", err)
|
||||
}
|
||||
|
||||
logger.Info("Deleted webhook secret", "secretName", secretName)
|
||||
|
||||
return r.deleteWebhook(ctx)
|
||||
}
|
||||
|
||||
|
||||
@@ -14,13 +14,15 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
provisioning "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/provisioning/repository/github"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/provisioning/secrets"
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
provisioning "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1"
|
||||
common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/provisioning/repository/github"
|
||||
)
|
||||
|
||||
func TestParseWebhooks(t *testing.T) {
|
||||
@@ -114,9 +116,7 @@ func TestGitHubRepository_Webhook(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
config *provisioning.Repository
|
||||
webhookSecret string
|
||||
setupRequest func() *http.Request
|
||||
mockSetup func(t *testing.T, mockSecrets *secrets.MockRepositorySecrets)
|
||||
expected *provisioning.WebhookResponse
|
||||
expectedError error
|
||||
}{
|
||||
@@ -138,30 +138,6 @@ func TestGitHubRepository_Webhook(t *testing.T) {
|
||||
},
|
||||
expectedError: fmt.Errorf("unexpected webhook request"),
|
||||
},
|
||||
{
|
||||
name: "secret decryption error",
|
||||
config: &provisioning.Repository{
|
||||
Spec: provisioning.RepositorySpec{
|
||||
GitHub: &provisioning.GitHubRepositoryConfig{
|
||||
Branch: "main",
|
||||
},
|
||||
},
|
||||
Status: provisioning.RepositoryStatus{
|
||||
Webhook: &provisioning.WebhookStatus{
|
||||
EncryptedSecret: []byte("encrypted-secret"),
|
||||
},
|
||||
},
|
||||
},
|
||||
setupRequest: func() *http.Request {
|
||||
req, _ := http.NewRequest("POST", "/webhook", nil)
|
||||
return req
|
||||
},
|
||||
mockSetup: func(t *testing.T, mockSecrets *secrets.MockRepositorySecrets) {
|
||||
mockSecrets.EXPECT().Decrypt(mock.Anything, mock.Anything, "encrypted-secret").
|
||||
Return(nil, errors.New("decryption failed"))
|
||||
},
|
||||
expectedError: fmt.Errorf("failed to decrypt secret: decryption failed"),
|
||||
},
|
||||
{
|
||||
name: "invalid signature",
|
||||
config: &provisioning.Repository{
|
||||
@@ -171,22 +147,15 @@ func TestGitHubRepository_Webhook(t *testing.T) {
|
||||
},
|
||||
},
|
||||
Status: provisioning.RepositoryStatus{
|
||||
Webhook: &provisioning.WebhookStatus{
|
||||
EncryptedSecret: []byte("encrypted-secret"),
|
||||
},
|
||||
Webhook: &provisioning.WebhookStatus{},
|
||||
},
|
||||
},
|
||||
webhookSecret: "webhook-secret",
|
||||
setupRequest: func() *http.Request {
|
||||
req, _ := http.NewRequest("POST", "/webhook", strings.NewReader("invalid payload"))
|
||||
req.Header.Set("X-Hub-Signature-256", "invalid")
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
return req
|
||||
},
|
||||
mockSetup: func(t *testing.T, mockSecrets *secrets.MockRepositorySecrets) {
|
||||
mockSecrets.EXPECT().Decrypt(mock.Anything, mock.Anything, "encrypted-secret").
|
||||
Return([]byte("webhook-secret"), nil)
|
||||
},
|
||||
expectedError: apierrors.NewUnauthorized("invalid signature"),
|
||||
},
|
||||
{
|
||||
@@ -198,12 +167,9 @@ func TestGitHubRepository_Webhook(t *testing.T) {
|
||||
},
|
||||
},
|
||||
Status: provisioning.RepositoryStatus{
|
||||
Webhook: &provisioning.WebhookStatus{
|
||||
EncryptedSecret: []byte("encrypted-secret"),
|
||||
},
|
||||
Webhook: &provisioning.WebhookStatus{},
|
||||
},
|
||||
},
|
||||
webhookSecret: "webhook-secret",
|
||||
setupRequest: func() *http.Request {
|
||||
payload := `{}`
|
||||
req, _ := http.NewRequest("POST", "/webhook", strings.NewReader(payload))
|
||||
@@ -218,10 +184,6 @@ func TestGitHubRepository_Webhook(t *testing.T) {
|
||||
|
||||
return req
|
||||
},
|
||||
mockSetup: func(t *testing.T, mockSecrets *secrets.MockRepositorySecrets) {
|
||||
mockSecrets.EXPECT().Decrypt(mock.Anything, mock.Anything, "encrypted-secret").
|
||||
Return([]byte("webhook-secret"), nil)
|
||||
},
|
||||
expected: &provisioning.WebhookResponse{
|
||||
Code: http.StatusOK,
|
||||
Message: "ping received",
|
||||
@@ -239,12 +201,9 @@ func TestGitHubRepository_Webhook(t *testing.T) {
|
||||
},
|
||||
},
|
||||
Status: provisioning.RepositoryStatus{
|
||||
Webhook: &provisioning.WebhookStatus{
|
||||
EncryptedSecret: []byte("encrypted-secret"),
|
||||
},
|
||||
Webhook: &provisioning.WebhookStatus{},
|
||||
},
|
||||
},
|
||||
webhookSecret: "webhook-secret",
|
||||
setupRequest: func() *http.Request {
|
||||
payload := `{
|
||||
"ref": "refs/heads/feature",
|
||||
@@ -264,10 +223,6 @@ func TestGitHubRepository_Webhook(t *testing.T) {
|
||||
|
||||
return req
|
||||
},
|
||||
mockSetup: func(t *testing.T, mockSecrets *secrets.MockRepositorySecrets) {
|
||||
mockSecrets.EXPECT().Decrypt(mock.Anything, mock.Anything, "encrypted-secret").
|
||||
Return([]byte("webhook-secret"), nil)
|
||||
},
|
||||
expected: &provisioning.WebhookResponse{
|
||||
Code: http.StatusOK,
|
||||
},
|
||||
@@ -287,12 +242,9 @@ func TestGitHubRepository_Webhook(t *testing.T) {
|
||||
},
|
||||
},
|
||||
Status: provisioning.RepositoryStatus{
|
||||
Webhook: &provisioning.WebhookStatus{
|
||||
EncryptedSecret: []byte("encrypted-secret"),
|
||||
},
|
||||
Webhook: &provisioning.WebhookStatus{},
|
||||
},
|
||||
},
|
||||
webhookSecret: "webhook-secret",
|
||||
setupRequest: func() *http.Request {
|
||||
payload := `{
|
||||
"ref": "refs/heads/main",
|
||||
@@ -312,10 +264,6 @@ func TestGitHubRepository_Webhook(t *testing.T) {
|
||||
|
||||
return req
|
||||
},
|
||||
mockSetup: func(t *testing.T, mockSecrets *secrets.MockRepositorySecrets) {
|
||||
mockSecrets.EXPECT().Decrypt(mock.Anything, mock.Anything, "encrypted-secret").
|
||||
Return([]byte("webhook-secret"), nil)
|
||||
},
|
||||
expected: &provisioning.WebhookResponse{
|
||||
Code: http.StatusAccepted,
|
||||
Job: &provisioning.JobSpec{
|
||||
@@ -336,12 +284,9 @@ func TestGitHubRepository_Webhook(t *testing.T) {
|
||||
},
|
||||
},
|
||||
Status: provisioning.RepositoryStatus{
|
||||
Webhook: &provisioning.WebhookStatus{
|
||||
EncryptedSecret: []byte("encrypted-secret"),
|
||||
},
|
||||
Webhook: &provisioning.WebhookStatus{},
|
||||
},
|
||||
},
|
||||
webhookSecret: "webhook-secret",
|
||||
setupRequest: func() *http.Request {
|
||||
payload := `{
|
||||
"ref": "refs/heads/main"
|
||||
@@ -358,10 +303,6 @@ func TestGitHubRepository_Webhook(t *testing.T) {
|
||||
|
||||
return req
|
||||
},
|
||||
mockSetup: func(t *testing.T, mockSecrets *secrets.MockRepositorySecrets) {
|
||||
mockSecrets.EXPECT().Decrypt(mock.Anything, mock.Anything, "encrypted-secret").
|
||||
Return([]byte("webhook-secret"), nil)
|
||||
},
|
||||
expectedError: fmt.Errorf("missing repository in push event"),
|
||||
},
|
||||
{
|
||||
@@ -373,12 +314,9 @@ func TestGitHubRepository_Webhook(t *testing.T) {
|
||||
},
|
||||
},
|
||||
Status: provisioning.RepositoryStatus{
|
||||
Webhook: &provisioning.WebhookStatus{
|
||||
EncryptedSecret: []byte("encrypted-secret"),
|
||||
},
|
||||
Webhook: &provisioning.WebhookStatus{},
|
||||
},
|
||||
},
|
||||
webhookSecret: "webhook-secret",
|
||||
setupRequest: func() *http.Request {
|
||||
payload := `{
|
||||
"ref": "refs/heads/main",
|
||||
@@ -398,10 +336,6 @@ func TestGitHubRepository_Webhook(t *testing.T) {
|
||||
|
||||
return req
|
||||
},
|
||||
mockSetup: func(t *testing.T, mockSecrets *secrets.MockRepositorySecrets) {
|
||||
mockSecrets.EXPECT().Decrypt(mock.Anything, mock.Anything, "encrypted-secret").
|
||||
Return([]byte("webhook-secret"), nil)
|
||||
},
|
||||
expectedError: fmt.Errorf("repository mismatch"),
|
||||
},
|
||||
{
|
||||
@@ -416,12 +350,9 @@ func TestGitHubRepository_Webhook(t *testing.T) {
|
||||
},
|
||||
},
|
||||
Status: provisioning.RepositoryStatus{
|
||||
Webhook: &provisioning.WebhookStatus{
|
||||
EncryptedSecret: []byte("encrypted-secret"),
|
||||
},
|
||||
Webhook: &provisioning.WebhookStatus{},
|
||||
},
|
||||
},
|
||||
webhookSecret: "webhook-secret",
|
||||
setupRequest: func() *http.Request {
|
||||
payload := `{
|
||||
"ref": "refs/heads/main",
|
||||
@@ -441,10 +372,6 @@ func TestGitHubRepository_Webhook(t *testing.T) {
|
||||
|
||||
return req
|
||||
},
|
||||
mockSetup: func(t *testing.T, mockSecrets *secrets.MockRepositorySecrets) {
|
||||
mockSecrets.EXPECT().Decrypt(mock.Anything, mock.Anything, "encrypted-secret").
|
||||
Return([]byte("webhook-secret"), nil)
|
||||
},
|
||||
expected: &provisioning.WebhookResponse{
|
||||
Code: http.StatusOK,
|
||||
},
|
||||
@@ -461,12 +388,9 @@ func TestGitHubRepository_Webhook(t *testing.T) {
|
||||
},
|
||||
},
|
||||
Status: provisioning.RepositoryStatus{
|
||||
Webhook: &provisioning.WebhookStatus{
|
||||
EncryptedSecret: []byte("encrypted-secret"),
|
||||
},
|
||||
Webhook: &provisioning.WebhookStatus{},
|
||||
},
|
||||
},
|
||||
webhookSecret: "webhook-secret",
|
||||
setupRequest: func() *http.Request {
|
||||
payload := `{
|
||||
"action": "opened",
|
||||
@@ -497,10 +421,6 @@ func TestGitHubRepository_Webhook(t *testing.T) {
|
||||
|
||||
return req
|
||||
},
|
||||
mockSetup: func(t *testing.T, mockSecrets *secrets.MockRepositorySecrets) {
|
||||
mockSecrets.EXPECT().Decrypt(mock.Anything, mock.Anything, "encrypted-secret").
|
||||
Return([]byte("webhook-secret"), nil)
|
||||
},
|
||||
expected: &provisioning.WebhookResponse{
|
||||
Code: http.StatusAccepted,
|
||||
Message: "pull request: opened",
|
||||
@@ -528,12 +448,9 @@ func TestGitHubRepository_Webhook(t *testing.T) {
|
||||
},
|
||||
},
|
||||
Status: provisioning.RepositoryStatus{
|
||||
Webhook: &provisioning.WebhookStatus{
|
||||
EncryptedSecret: []byte("encrypted-secret"),
|
||||
},
|
||||
Webhook: &provisioning.WebhookStatus{},
|
||||
},
|
||||
},
|
||||
webhookSecret: "webhook-secret",
|
||||
setupRequest: func() *http.Request {
|
||||
payload := `{
|
||||
"action": "synchronize",
|
||||
@@ -564,10 +481,6 @@ func TestGitHubRepository_Webhook(t *testing.T) {
|
||||
|
||||
return req
|
||||
},
|
||||
mockSetup: func(t *testing.T, mockSecrets *secrets.MockRepositorySecrets) {
|
||||
mockSecrets.EXPECT().Decrypt(mock.Anything, mock.Anything, "encrypted-secret").
|
||||
Return([]byte("webhook-secret"), nil)
|
||||
},
|
||||
expected: &provisioning.WebhookResponse{
|
||||
Code: http.StatusAccepted,
|
||||
Message: "pull request: synchronize",
|
||||
@@ -592,12 +505,9 @@ func TestGitHubRepository_Webhook(t *testing.T) {
|
||||
},
|
||||
},
|
||||
Status: provisioning.RepositoryStatus{
|
||||
Webhook: &provisioning.WebhookStatus{
|
||||
EncryptedSecret: []byte("encrypted-secret"),
|
||||
},
|
||||
Webhook: &provisioning.WebhookStatus{},
|
||||
},
|
||||
},
|
||||
webhookSecret: "webhook-secret",
|
||||
setupRequest: func() *http.Request {
|
||||
payload := `{
|
||||
"action": "opened",
|
||||
@@ -628,10 +538,6 @@ func TestGitHubRepository_Webhook(t *testing.T) {
|
||||
|
||||
return req
|
||||
},
|
||||
mockSetup: func(t *testing.T, mockSecrets *secrets.MockRepositorySecrets) {
|
||||
mockSecrets.EXPECT().Decrypt(mock.Anything, mock.Anything, "encrypted-secret").
|
||||
Return([]byte("webhook-secret"), nil)
|
||||
},
|
||||
expected: &provisioning.WebhookResponse{
|
||||
Code: http.StatusOK,
|
||||
Message: "ignoring pull request event as develop is not the configured branch",
|
||||
@@ -646,12 +552,9 @@ func TestGitHubRepository_Webhook(t *testing.T) {
|
||||
},
|
||||
},
|
||||
Status: provisioning.RepositoryStatus{
|
||||
Webhook: &provisioning.WebhookStatus{
|
||||
EncryptedSecret: []byte("encrypted-secret"),
|
||||
},
|
||||
Webhook: &provisioning.WebhookStatus{},
|
||||
},
|
||||
},
|
||||
webhookSecret: "webhook-secret",
|
||||
setupRequest: func() *http.Request {
|
||||
payload := `{
|
||||
"action": "closed",
|
||||
@@ -682,10 +585,6 @@ func TestGitHubRepository_Webhook(t *testing.T) {
|
||||
|
||||
return req
|
||||
},
|
||||
mockSetup: func(t *testing.T, mockSecrets *secrets.MockRepositorySecrets) {
|
||||
mockSecrets.EXPECT().Decrypt(mock.Anything, mock.Anything, "encrypted-secret").
|
||||
Return([]byte("webhook-secret"), nil)
|
||||
},
|
||||
expected: &provisioning.WebhookResponse{
|
||||
Code: http.StatusOK,
|
||||
Message: "ignore pull request event: closed",
|
||||
@@ -700,12 +599,9 @@ func TestGitHubRepository_Webhook(t *testing.T) {
|
||||
},
|
||||
},
|
||||
Status: provisioning.RepositoryStatus{
|
||||
Webhook: &provisioning.WebhookStatus{
|
||||
EncryptedSecret: []byte("encrypted-secret"),
|
||||
},
|
||||
Webhook: &provisioning.WebhookStatus{},
|
||||
},
|
||||
},
|
||||
webhookSecret: "webhook-secret",
|
||||
setupRequest: func() *http.Request {
|
||||
payload := `{
|
||||
"action": "opened",
|
||||
@@ -733,10 +629,6 @@ func TestGitHubRepository_Webhook(t *testing.T) {
|
||||
|
||||
return req
|
||||
},
|
||||
mockSetup: func(t *testing.T, mockSecrets *secrets.MockRepositorySecrets) {
|
||||
mockSecrets.EXPECT().Decrypt(mock.Anything, mock.Anything, "encrypted-secret").
|
||||
Return([]byte("webhook-secret"), nil)
|
||||
},
|
||||
expectedError: fmt.Errorf("missing repository in pull request event"),
|
||||
},
|
||||
{
|
||||
@@ -746,12 +638,9 @@ func TestGitHubRepository_Webhook(t *testing.T) {
|
||||
// GitHub config is intentionally missing
|
||||
},
|
||||
Status: provisioning.RepositoryStatus{
|
||||
Webhook: &provisioning.WebhookStatus{
|
||||
EncryptedSecret: []byte("encrypted-secret"),
|
||||
},
|
||||
Webhook: &provisioning.WebhookStatus{},
|
||||
},
|
||||
},
|
||||
webhookSecret: "webhook-secret",
|
||||
setupRequest: func() *http.Request {
|
||||
payload := `{
|
||||
"action": "opened",
|
||||
@@ -782,10 +671,6 @@ func TestGitHubRepository_Webhook(t *testing.T) {
|
||||
|
||||
return req
|
||||
},
|
||||
mockSetup: func(t *testing.T, mockSecrets *secrets.MockRepositorySecrets) {
|
||||
mockSecrets.EXPECT().Decrypt(mock.Anything, mock.Anything, "encrypted-secret").
|
||||
Return([]byte("webhook-secret"), nil)
|
||||
},
|
||||
expectedError: fmt.Errorf("missing GitHub config"),
|
||||
},
|
||||
{
|
||||
@@ -797,12 +682,9 @@ func TestGitHubRepository_Webhook(t *testing.T) {
|
||||
},
|
||||
},
|
||||
Status: provisioning.RepositoryStatus{
|
||||
Webhook: &provisioning.WebhookStatus{
|
||||
EncryptedSecret: []byte("encrypted-secret"),
|
||||
},
|
||||
Webhook: &provisioning.WebhookStatus{},
|
||||
},
|
||||
},
|
||||
webhookSecret: "webhook-secret",
|
||||
setupRequest: func() *http.Request {
|
||||
payload := `{
|
||||
"action": "opened",
|
||||
@@ -833,10 +715,6 @@ func TestGitHubRepository_Webhook(t *testing.T) {
|
||||
|
||||
return req
|
||||
},
|
||||
mockSetup: func(t *testing.T, mockSecrets *secrets.MockRepositorySecrets) {
|
||||
mockSecrets.EXPECT().Decrypt(mock.Anything, mock.Anything, "encrypted-secret").
|
||||
Return([]byte("webhook-secret"), nil)
|
||||
},
|
||||
expectedError: fmt.Errorf("repository mismatch"),
|
||||
},
|
||||
{
|
||||
@@ -848,12 +726,9 @@ func TestGitHubRepository_Webhook(t *testing.T) {
|
||||
},
|
||||
},
|
||||
Status: provisioning.RepositoryStatus{
|
||||
Webhook: &provisioning.WebhookStatus{
|
||||
EncryptedSecret: []byte("encrypted-secret"),
|
||||
},
|
||||
Webhook: &provisioning.WebhookStatus{},
|
||||
},
|
||||
},
|
||||
webhookSecret: "webhook-secret",
|
||||
setupRequest: func() *http.Request {
|
||||
payload := `{
|
||||
"action": "opened",
|
||||
@@ -873,39 +748,8 @@ func TestGitHubRepository_Webhook(t *testing.T) {
|
||||
|
||||
return req
|
||||
},
|
||||
mockSetup: func(t *testing.T, mockSecrets *secrets.MockRepositorySecrets) {
|
||||
mockSecrets.EXPECT().Decrypt(mock.Anything, mock.Anything, "encrypted-secret").
|
||||
Return([]byte("webhook-secret"), nil)
|
||||
},
|
||||
expectedError: fmt.Errorf("expected PR in event"),
|
||||
},
|
||||
{
|
||||
name: "secret decryption error with new secrets store",
|
||||
config: &provisioning.Repository{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Namespace: "default",
|
||||
},
|
||||
Spec: provisioning.RepositorySpec{
|
||||
GitHub: &provisioning.GitHubRepositoryConfig{
|
||||
Branch: "main",
|
||||
},
|
||||
},
|
||||
Status: provisioning.RepositoryStatus{
|
||||
Webhook: &provisioning.WebhookStatus{
|
||||
EncryptedSecret: []byte("test-secret"),
|
||||
},
|
||||
},
|
||||
},
|
||||
setupRequest: func() *http.Request {
|
||||
req, _ := http.NewRequest("POST", "/webhook", nil)
|
||||
return req
|
||||
},
|
||||
mockSetup: func(t *testing.T, mockSecrets *secrets.MockRepositorySecrets) {
|
||||
mockSecrets.EXPECT().Decrypt(mock.Anything, mock.Anything, "test-secret").
|
||||
Return(nil, errors.New("decryption failed"))
|
||||
},
|
||||
expectedError: fmt.Errorf("failed to decrypt secret: decryption failed"),
|
||||
},
|
||||
{
|
||||
name: "ping event with new secrets store",
|
||||
config: &provisioning.Repository{
|
||||
@@ -918,12 +762,9 @@ func TestGitHubRepository_Webhook(t *testing.T) {
|
||||
},
|
||||
},
|
||||
Status: provisioning.RepositoryStatus{
|
||||
Webhook: &provisioning.WebhookStatus{
|
||||
EncryptedSecret: []byte("test-secret"),
|
||||
},
|
||||
Webhook: &provisioning.WebhookStatus{},
|
||||
},
|
||||
},
|
||||
webhookSecret: "webhook-secret",
|
||||
setupRequest: func() *http.Request {
|
||||
payload := `{}`
|
||||
req, _ := http.NewRequest("POST", "/webhook", strings.NewReader(payload))
|
||||
@@ -938,10 +779,6 @@ func TestGitHubRepository_Webhook(t *testing.T) {
|
||||
|
||||
return req
|
||||
},
|
||||
mockSetup: func(t *testing.T, mockSecrets *secrets.MockRepositorySecrets) {
|
||||
mockSecrets.EXPECT().Decrypt(mock.Anything, mock.Anything, "test-secret").
|
||||
Return([]byte("webhook-secret"), nil)
|
||||
},
|
||||
expected: &provisioning.WebhookResponse{
|
||||
Code: http.StatusOK,
|
||||
Message: "ping received",
|
||||
@@ -963,12 +800,9 @@ func TestGitHubRepository_Webhook(t *testing.T) {
|
||||
},
|
||||
},
|
||||
Status: provisioning.RepositoryStatus{
|
||||
Webhook: &provisioning.WebhookStatus{
|
||||
EncryptedSecret: []byte("test-secret"),
|
||||
},
|
||||
Webhook: &provisioning.WebhookStatus{},
|
||||
},
|
||||
},
|
||||
webhookSecret: "webhook-secret",
|
||||
setupRequest: func() *http.Request {
|
||||
payload := `{
|
||||
"ref": "refs/heads/main",
|
||||
@@ -988,10 +822,6 @@ func TestGitHubRepository_Webhook(t *testing.T) {
|
||||
|
||||
return req
|
||||
},
|
||||
mockSetup: func(t *testing.T, mockSecrets *secrets.MockRepositorySecrets) {
|
||||
mockSecrets.EXPECT().Decrypt(mock.Anything, mock.Anything, "test-secret").
|
||||
Return([]byte("webhook-secret"), nil)
|
||||
},
|
||||
expected: &provisioning.WebhookResponse{
|
||||
Code: http.StatusAccepted,
|
||||
Job: &provisioning.JobSpec{
|
||||
@@ -1012,12 +842,9 @@ func TestGitHubRepository_Webhook(t *testing.T) {
|
||||
},
|
||||
},
|
||||
Status: provisioning.RepositoryStatus{
|
||||
Webhook: &provisioning.WebhookStatus{
|
||||
EncryptedSecret: []byte("encrypted-secret"),
|
||||
},
|
||||
Webhook: &provisioning.WebhookStatus{},
|
||||
},
|
||||
},
|
||||
webhookSecret: "webhook-secret",
|
||||
setupRequest: func() *http.Request {
|
||||
payload := `{}`
|
||||
req, _ := http.NewRequest("POST", "/webhook", strings.NewReader(payload))
|
||||
@@ -1032,10 +859,6 @@ func TestGitHubRepository_Webhook(t *testing.T) {
|
||||
|
||||
return req
|
||||
},
|
||||
mockSetup: func(t *testing.T, mockSecrets *secrets.MockRepositorySecrets) {
|
||||
mockSecrets.EXPECT().Decrypt(mock.Anything, mock.Anything, "encrypted-secret").
|
||||
Return([]byte("webhook-secret"), nil)
|
||||
},
|
||||
expected: &provisioning.WebhookResponse{
|
||||
Code: http.StatusNotImplemented,
|
||||
Message: "unsupported messageType: team",
|
||||
@@ -1045,20 +868,12 @@ func TestGitHubRepository_Webhook(t *testing.T) {
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// Create a mock secrets service
|
||||
mockSecrets := secrets.NewMockRepositorySecrets(t)
|
||||
|
||||
// Set up the mock expectations
|
||||
if tt.mockSetup != nil {
|
||||
tt.mockSetup(t, mockSecrets)
|
||||
}
|
||||
|
||||
// Create a GitHub repository with the test config
|
||||
repo := &githubWebhookRepository{
|
||||
config: tt.config,
|
||||
owner: "grafana",
|
||||
repo: "grafana",
|
||||
secrets: mockSecrets,
|
||||
config: tt.config,
|
||||
owner: "grafana",
|
||||
repo: "grafana",
|
||||
secret: common.RawSecureValue("webhook-secret"),
|
||||
}
|
||||
|
||||
// Call the Webhook method
|
||||
@@ -1070,7 +885,7 @@ func TestGitHubRepository_Webhook(t *testing.T) {
|
||||
var statusErr *apierrors.StatusError
|
||||
if errors.As(tt.expectedError, &statusErr) {
|
||||
var actualStatusErr *apierrors.StatusError
|
||||
require.True(t, errors.As(err, &actualStatusErr), "Expected StatusError but got different error type")
|
||||
require.True(t, errors.As(err, &actualStatusErr), "Expected StatusError but got different error type: %T", err)
|
||||
require.Equal(t, statusErr.Status().Message, actualStatusErr.Status().Message)
|
||||
require.Equal(t, statusErr.Status().Code, actualStatusErr.Status().Code)
|
||||
} else {
|
||||
@@ -1097,9 +912,6 @@ func TestGitHubRepository_Webhook(t *testing.T) {
|
||||
require.Nil(t, response.Job)
|
||||
}
|
||||
}
|
||||
|
||||
// Verify all mock expectations were met
|
||||
mockSecrets.AssertExpectations(t)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1202,9 +1014,8 @@ func TestGitHubRepository_OnCreate(t *testing.T) {
|
||||
},
|
||||
webhookURL: "https://example.com/webhook",
|
||||
expectedHook: &provisioning.WebhookStatus{
|
||||
ID: 123,
|
||||
URL: "https://example.com/webhook",
|
||||
Secret: "test-secret",
|
||||
ID: 123,
|
||||
URL: "https://example.com/webhook",
|
||||
},
|
||||
expectedError: nil,
|
||||
},
|
||||
@@ -1270,12 +1081,21 @@ func TestGitHubRepository_OnCreate(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
if tt.expectedHook != nil {
|
||||
require.NotNil(t, hookOps)
|
||||
require.Len(t, hookOps, 1)
|
||||
require.Len(t, hookOps, 2)
|
||||
require.Equal(t, "replace", hookOps[0]["op"])
|
||||
require.Equal(t, "/status/webhook", hookOps[0]["path"])
|
||||
require.Equal(t, tt.expectedHook.ID, hookOps[0]["value"].(*provisioning.WebhookStatus).ID)
|
||||
require.Equal(t, tt.expectedHook.URL, hookOps[0]["value"].(*provisioning.WebhookStatus).URL)
|
||||
require.NotEmpty(t, hookOps[0]["value"].(*provisioning.WebhookStatus).Secret) // Secret is randomly generated, so just check it's not empty
|
||||
|
||||
require.Equal(t, "replace", hookOps[1]["op"])
|
||||
require.Equal(t, "/secure/webhookSecret", hookOps[1]["path"])
|
||||
vals, ok := hookOps[1]["value"].(map[string]string)
|
||||
require.True(t, ok, "expected webhookSecret as map")
|
||||
require.Len(t, vals, 1, "with one property")
|
||||
require.NotEmpty(t, vals["create"], "secret should be created")
|
||||
|
||||
_, err := uuid.Parse(vals["create"])
|
||||
require.NoError(t, err, "the secret is a valid UUID")
|
||||
} else {
|
||||
require.Nil(t, hookOps)
|
||||
}
|
||||
@@ -1607,7 +1427,6 @@ func TestGitHubRepository_OnUpdate(t *testing.T) {
|
||||
URL: "https://example.com/webhook",
|
||||
Events: subscribedEvents,
|
||||
}, nil)
|
||||
|
||||
// No EditWebhook call expected since no changes needed
|
||||
},
|
||||
config: &provisioning.Repository{
|
||||
@@ -1618,19 +1437,18 @@ func TestGitHubRepository_OnUpdate(t *testing.T) {
|
||||
},
|
||||
Status: provisioning.RepositoryStatus{
|
||||
Webhook: &provisioning.WebhookStatus{
|
||||
ID: 123,
|
||||
URL: "https://example.com/webhook",
|
||||
Secret: "secret",
|
||||
ID: 123,
|
||||
URL: "https://example.com/webhook",
|
||||
},
|
||||
},
|
||||
Secure: provisioning.SecureValues{
|
||||
WebhookSecret: common.InlineSecureValue{
|
||||
Name: "valid-secret",
|
||||
},
|
||||
},
|
||||
},
|
||||
webhookURL: "https://example.com/webhook",
|
||||
expectedHook: &provisioning.WebhookStatus{
|
||||
ID: 123,
|
||||
URL: "https://example.com/webhook",
|
||||
SubscribedEvents: subscribedEvents,
|
||||
Secret: "secret",
|
||||
},
|
||||
webhookURL: "https://example.com/webhook",
|
||||
expectedHook: nil, // nothing changed
|
||||
expectedError: nil,
|
||||
},
|
||||
}
|
||||
@@ -1662,17 +1480,22 @@ func TestGitHubRepository_OnUpdate(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
if tt.expectedHook != nil {
|
||||
require.NotNil(t, hookOps)
|
||||
require.Len(t, hookOps, 1)
|
||||
require.Len(t, hookOps, 2)
|
||||
require.Equal(t, "replace", hookOps[0]["op"])
|
||||
require.Equal(t, "/status/webhook", hookOps[0]["path"])
|
||||
require.Equal(t, tt.expectedHook.ID, hookOps[0]["value"].(*provisioning.WebhookStatus).ID)
|
||||
require.Equal(t, tt.expectedHook.URL, hookOps[0]["value"].(*provisioning.WebhookStatus).URL)
|
||||
if tt.expectedHook.Secret != "" {
|
||||
require.Equal(t, tt.expectedHook.Secret, hookOps[0]["value"].(*provisioning.WebhookStatus).Secret)
|
||||
} else {
|
||||
require.NotEmpty(t, hookOps[0]["value"].(*provisioning.WebhookStatus).Secret) // Secret is randomly generated, so just check it's not empty
|
||||
}
|
||||
require.ElementsMatch(t, tt.expectedHook.SubscribedEvents, hookOps[0]["value"].(*provisioning.WebhookStatus).SubscribedEvents)
|
||||
|
||||
require.Equal(t, "replace", hookOps[1]["op"])
|
||||
require.Equal(t, "/secure/webhookSecret", hookOps[1]["path"])
|
||||
vals, ok := hookOps[1]["value"].(map[string]string)
|
||||
require.True(t, ok, "expected webhookSecret as map")
|
||||
require.Len(t, vals, 1, "with one property")
|
||||
require.NotEmpty(t, vals["create"], "secret should be created")
|
||||
|
||||
_, err := uuid.Parse(vals["create"])
|
||||
require.NoError(t, err, "the secret is a valid UUID")
|
||||
} else {
|
||||
require.Nil(t, hookOps)
|
||||
}
|
||||
@@ -1687,17 +1510,14 @@ func TestGitHubRepository_OnUpdate(t *testing.T) {
|
||||
func TestGitHubRepository_OnDelete(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
setupMock func(m *github.MockClient, mockRepo *github.MockGithubRepository, mockSecrets *secrets.MockRepositorySecrets)
|
||||
setupMock func(m *github.MockClient)
|
||||
config *provisioning.Repository
|
||||
webhookURL string
|
||||
expectedError error
|
||||
}{
|
||||
{
|
||||
name: "successfully delete webhook",
|
||||
setupMock: func(m *github.MockClient, mockRepo *github.MockGithubRepository, mockSecrets *secrets.MockRepositorySecrets) {
|
||||
mockRepo.On("OnDelete", mock.Anything).Return(nil)
|
||||
mockSecrets.EXPECT().Delete(mock.Anything, mock.Anything, mock.Anything).Return(nil)
|
||||
// Mock deleting the webhook
|
||||
setupMock: func(m *github.MockClient) {
|
||||
m.On("DeleteWebhook", mock.Anything, "grafana", "grafana", int64(123)).
|
||||
Return(nil)
|
||||
},
|
||||
@@ -1721,10 +1541,8 @@ func TestGitHubRepository_OnDelete(t *testing.T) {
|
||||
expectedError: nil,
|
||||
},
|
||||
{
|
||||
name: "no webhook URL provided",
|
||||
setupMock: func(_ *github.MockClient, mockRepo *github.MockGithubRepository, _ *secrets.MockRepositorySecrets) {
|
||||
mockRepo.On("OnDelete", mock.Anything).Return(nil)
|
||||
},
|
||||
name: "no webhook URL provided",
|
||||
setupMock: func(_ *github.MockClient) {},
|
||||
config: &provisioning.Repository{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-repo",
|
||||
@@ -1740,8 +1558,7 @@ func TestGitHubRepository_OnDelete(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "webhook not found in status",
|
||||
setupMock: func(_ *github.MockClient, mockRepo *github.MockGithubRepository, _ *secrets.MockRepositorySecrets) {
|
||||
mockRepo.On("OnDelete", mock.Anything).Return(nil)
|
||||
setupMock: func(_ *github.MockClient) {
|
||||
// No secrets deletion or webhook deletion mocks needed - method returns early when webhook is nil
|
||||
},
|
||||
config: &provisioning.Repository{
|
||||
@@ -1760,35 +1577,9 @@ func TestGitHubRepository_OnDelete(t *testing.T) {
|
||||
webhookURL: "https://example.com/webhook",
|
||||
expectedError: nil, // No error expected - method returns early when webhook is nil
|
||||
},
|
||||
{
|
||||
name: "error on delete from basic github repository",
|
||||
setupMock: func(_ *github.MockClient, mockRepo *github.MockGithubRepository, _ *secrets.MockRepositorySecrets) {
|
||||
mockRepo.On("OnDelete", mock.Anything).Return(fmt.Errorf("failed to delete webhook"))
|
||||
},
|
||||
config: &provisioning.Repository{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-repo",
|
||||
},
|
||||
Spec: provisioning.RepositorySpec{
|
||||
GitHub: &provisioning.GitHubRepositoryConfig{
|
||||
Branch: "main",
|
||||
},
|
||||
},
|
||||
Status: provisioning.RepositoryStatus{
|
||||
Webhook: &provisioning.WebhookStatus{
|
||||
ID: 123,
|
||||
URL: "https://example.com/webhook",
|
||||
},
|
||||
},
|
||||
},
|
||||
webhookURL: "https://example.com/webhook",
|
||||
expectedError: fmt.Errorf("on delete from basic github repository: failed to delete webhook"),
|
||||
},
|
||||
{
|
||||
name: "error deleting webhook",
|
||||
setupMock: func(m *github.MockClient, mockRepo *github.MockGithubRepository, mockSecrets *secrets.MockRepositorySecrets) {
|
||||
mockRepo.On("OnDelete", mock.Anything).Return(nil)
|
||||
mockSecrets.EXPECT().Delete(mock.Anything, mock.Anything, mock.Anything).Return(nil)
|
||||
setupMock: func(m *github.MockClient) {
|
||||
// Mock webhook deletion failure
|
||||
m.On("DeleteWebhook", mock.Anything, "grafana", "grafana", int64(123)).
|
||||
Return(fmt.Errorf("failed to delete webhook"))
|
||||
@@ -1819,15 +1610,13 @@ func TestGitHubRepository_OnDelete(t *testing.T) {
|
||||
// Setup mock GitHub client
|
||||
mockGH := github.NewMockClient(t)
|
||||
mockRepo := github.NewMockGithubRepository(t)
|
||||
mockSecrets := secrets.NewMockRepositorySecrets(t)
|
||||
tt.setupMock(mockGH, mockRepo, mockSecrets)
|
||||
tt.setupMock(mockGH)
|
||||
|
||||
// Create repository with mock
|
||||
repo := &githubWebhookRepository{
|
||||
GithubRepository: mockRepo,
|
||||
gh: mockGH,
|
||||
config: tt.config,
|
||||
secrets: mockSecrets,
|
||||
owner: "grafana",
|
||||
repo: "grafana",
|
||||
webhookURL: tt.webhookURL,
|
||||
@@ -1849,156 +1638,3 @@ func TestGitHubRepository_OnDelete(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGitHubRepository_OnDelete_WithSecrets(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
setupMock func(m *github.MockClient, mockRepo *github.MockGithubRepository, mockSecrets *secrets.MockRepositorySecrets)
|
||||
config *provisioning.Repository
|
||||
webhookURL string
|
||||
expectedError string
|
||||
}{
|
||||
{
|
||||
name: "successful deletion with secrets",
|
||||
setupMock: func(m *github.MockClient, mockRepo *github.MockGithubRepository, mockSecrets *secrets.MockRepositorySecrets) {
|
||||
mockRepo.On("OnDelete", mock.Anything).Return(nil)
|
||||
mockSecrets.EXPECT().Delete(
|
||||
mock.Anything,
|
||||
&provisioning.Repository{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-repo",
|
||||
Namespace: "default",
|
||||
},
|
||||
Spec: provisioning.RepositorySpec{
|
||||
GitHub: &provisioning.GitHubRepositoryConfig{
|
||||
Branch: "main",
|
||||
},
|
||||
},
|
||||
Status: provisioning.RepositoryStatus{
|
||||
Webhook: &provisioning.WebhookStatus{
|
||||
ID: 123,
|
||||
URL: "https://example.com/webhook",
|
||||
},
|
||||
},
|
||||
},
|
||||
"test-repo"+webhookSecretSuffix,
|
||||
).Return(nil)
|
||||
m.On("DeleteWebhook", mock.Anything, "grafana", "grafana", int64(123)).Return(nil)
|
||||
},
|
||||
config: &provisioning.Repository{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-repo",
|
||||
Namespace: "default",
|
||||
},
|
||||
Spec: provisioning.RepositorySpec{
|
||||
GitHub: &provisioning.GitHubRepositoryConfig{
|
||||
Branch: "main",
|
||||
},
|
||||
},
|
||||
Status: provisioning.RepositoryStatus{
|
||||
Webhook: &provisioning.WebhookStatus{
|
||||
ID: 123,
|
||||
URL: "https://example.com/webhook",
|
||||
},
|
||||
},
|
||||
},
|
||||
webhookURL: "https://example.com/webhook",
|
||||
},
|
||||
{
|
||||
name: "secret deletion error",
|
||||
setupMock: func(_ *github.MockClient, mockRepo *github.MockGithubRepository, mockSecrets *secrets.MockRepositorySecrets) {
|
||||
mockRepo.On("OnDelete", mock.Anything).Return(nil)
|
||||
mockSecrets.EXPECT().Delete(
|
||||
mock.Anything,
|
||||
&provisioning.Repository{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-repo",
|
||||
Namespace: "default",
|
||||
},
|
||||
Spec: provisioning.RepositorySpec{
|
||||
GitHub: &provisioning.GitHubRepositoryConfig{
|
||||
Branch: "main",
|
||||
},
|
||||
},
|
||||
Status: provisioning.RepositoryStatus{
|
||||
Webhook: &provisioning.WebhookStatus{
|
||||
ID: 123,
|
||||
URL: "https://example.com/webhook",
|
||||
},
|
||||
},
|
||||
},
|
||||
"test-repo"+webhookSecretSuffix,
|
||||
).Return(errors.New("failed to delete webhook secret"))
|
||||
},
|
||||
config: &provisioning.Repository{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-repo",
|
||||
Namespace: "default",
|
||||
},
|
||||
Spec: provisioning.RepositorySpec{
|
||||
GitHub: &provisioning.GitHubRepositoryConfig{
|
||||
Branch: "main",
|
||||
},
|
||||
},
|
||||
Status: provisioning.RepositoryStatus{
|
||||
Webhook: &provisioning.WebhookStatus{
|
||||
ID: 123,
|
||||
URL: "https://example.com/webhook",
|
||||
},
|
||||
},
|
||||
},
|
||||
webhookURL: "https://example.com/webhook",
|
||||
expectedError: "delete webhook secret: failed to delete webhook secret",
|
||||
},
|
||||
{
|
||||
name: "no webhook URL - no secrets deletion",
|
||||
setupMock: func(_ *github.MockClient, mockRepo *github.MockGithubRepository, _ *secrets.MockRepositorySecrets) {
|
||||
mockRepo.On("OnDelete", mock.Anything).Return(nil)
|
||||
},
|
||||
config: &provisioning.Repository{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-repo",
|
||||
Namespace: "default",
|
||||
},
|
||||
Spec: provisioning.RepositorySpec{
|
||||
GitHub: &provisioning.GitHubRepositoryConfig{
|
||||
Branch: "main",
|
||||
},
|
||||
},
|
||||
},
|
||||
webhookURL: "",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
mockClient := github.NewMockClient(t)
|
||||
mockRepo := github.NewMockGithubRepository(t)
|
||||
mockSecrets := secrets.NewMockRepositorySecrets(t)
|
||||
tt.setupMock(mockClient, mockRepo, mockSecrets)
|
||||
|
||||
repo := &githubWebhookRepository{
|
||||
GithubRepository: mockRepo,
|
||||
gh: mockClient,
|
||||
config: tt.config,
|
||||
secrets: mockSecrets,
|
||||
owner: "grafana",
|
||||
repo: "grafana",
|
||||
webhookURL: tt.webhookURL,
|
||||
}
|
||||
|
||||
err := repo.OnDelete(context.Background())
|
||||
|
||||
if tt.expectedError != "" {
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), tt.expectedError)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
mockClient.AssertExpectations(t)
|
||||
mockRepo.AssertExpectations(t)
|
||||
mockSecrets.AssertExpectations(t)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -172,7 +172,7 @@ func (s *webhookConnector) updateLastEvent(ctx context.Context, repo repository.
|
||||
eventAge := time.Since(lastEvent)
|
||||
|
||||
if repo.Config().Status.Webhook != nil && (eventAge > time.Minute) {
|
||||
patchOp := map[string]interface{}{
|
||||
patchOp := map[string]any{
|
||||
"op": "replace",
|
||||
"path": "/status/webhook/lastEvent",
|
||||
"value": time.Now().UnixMilli(),
|
||||
|
||||
@@ -12,7 +12,6 @@ import (
|
||||
"github.com/grafana/grafana/pkg/registry/apis/iam/noopstorage"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/ofrep"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/provisioning"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/provisioning/secrets"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/query"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/secret"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/service"
|
||||
@@ -45,7 +44,6 @@ var WireSet = wire.NewSet(
|
||||
datasource.RegisterAPIService,
|
||||
folders.RegisterAPIService,
|
||||
iam.RegisterAPIService,
|
||||
secrets.ProvideRepositorySecrets,
|
||||
provisioning.RegisterAPIService,
|
||||
service.RegisterAPIService,
|
||||
query.RegisterAPIService,
|
||||
|
||||
Reference in New Issue
Block a user