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,
|
||||
|
||||
@@ -7,7 +7,6 @@ import (
|
||||
"github.com/grafana/grafana/pkg/infra/httpclient"
|
||||
"github.com/grafana/grafana/pkg/plugins/manager/registry"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/provisioning/repository/github"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/provisioning/secrets"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/secret"
|
||||
"github.com/grafana/grafana/pkg/services/auth"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
@@ -37,7 +36,6 @@ func ProvideTestEnv(
|
||||
idService auth.IDService,
|
||||
githubFactory *github.Factory,
|
||||
decryptService secret.DecryptService,
|
||||
repositorySecrets secrets.RepositorySecrets, // TODO... remove
|
||||
) (*TestEnv, error) {
|
||||
return &TestEnv{
|
||||
TestingT: testingT,
|
||||
@@ -54,7 +52,6 @@ func ProvideTestEnv(
|
||||
IDService: idService,
|
||||
GitHubFactory: githubFactory,
|
||||
DecryptService: decryptService,
|
||||
RepositorySecrets: repositorySecrets, // TODO, remove
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -77,5 +74,4 @@ type TestEnv struct {
|
||||
IDService auth.IDService
|
||||
GitHubFactory *github.Factory
|
||||
DecryptService secret.DecryptService
|
||||
RepositorySecrets secrets.RepositorySecrets // NOTE, this will be removed soon
|
||||
}
|
||||
|
||||
+7
-12
File diff suppressed because one or more lines are too long
@@ -315,13 +315,6 @@ var (
|
||||
RequiresRestart: true,
|
||||
Owner: grafanaAppPlatformSquad,
|
||||
},
|
||||
{
|
||||
Name: "provisioningSecretsService",
|
||||
Description: "Experimental feature to use the secrets service for provisioning instead of the legacy secrets",
|
||||
Stage: FeatureStageExperimental,
|
||||
RequiresRestart: true,
|
||||
Owner: grafanaAppPlatformSquad,
|
||||
},
|
||||
{
|
||||
Name: "grafanaAPIServerEnsureKubectlAccess",
|
||||
Description: "Start an additional https handler and write kubectl options",
|
||||
|
||||
@@ -40,7 +40,6 @@ mlExpressions,experimental,@grafana/alerting-squad,false,false,false
|
||||
datasourceAPIServers,experimental,@grafana/grafana-app-platform-squad,false,true,false
|
||||
grafanaAPIServerWithExperimentalAPIs,experimental,@grafana/grafana-app-platform-squad,true,true,false
|
||||
provisioning,experimental,@grafana/grafana-app-platform-squad,false,true,false
|
||||
provisioningSecretsService,experimental,@grafana/grafana-app-platform-squad,false,true,false
|
||||
grafanaAPIServerEnsureKubectlAccess,experimental,@grafana/grafana-app-platform-squad,true,true,false
|
||||
featureToggleAdminPage,experimental,@grafana/grafana-operator-experience-squad,false,true,false
|
||||
awsAsyncQueryCaching,GA,@grafana/aws-datasources,false,false,false
|
||||
|
||||
|
@@ -171,10 +171,6 @@ const (
|
||||
// Next generation provisioning... and git
|
||||
FlagProvisioning = "provisioning"
|
||||
|
||||
// FlagProvisioningSecretsService
|
||||
// Experimental feature to use the secrets service for provisioning instead of the legacy secrets
|
||||
FlagProvisioningSecretsService = "provisioningSecretsService"
|
||||
|
||||
// FlagGrafanaAPIServerEnsureKubectlAccess
|
||||
// Start an additional https handler and write kubectl options
|
||||
FlagGrafanaAPIServerEnsureKubectlAccess = "grafanaAPIServerEnsureKubectlAccess"
|
||||
|
||||
@@ -2640,7 +2640,8 @@
|
||||
"metadata": {
|
||||
"name": "provisioningSecretsService",
|
||||
"resourceVersion": "1753448760331",
|
||||
"creationTimestamp": "2025-07-25T13:06:00Z"
|
||||
"creationTimestamp": "2025-07-25T13:06:00Z",
|
||||
"deletionTimestamp": "2025-08-20T12:48:19Z"
|
||||
},
|
||||
"spec": {
|
||||
"description": "Experimental feature to use the secrets service for provisioning instead of the legacy secrets",
|
||||
|
||||
@@ -2605,20 +2605,10 @@
|
||||
"type": "string",
|
||||
"default": ""
|
||||
},
|
||||
"encryptedToken": {
|
||||
"description": "Token for accessing the repository, but encrypted. This is not possible to read back to a user decrypted.",
|
||||
"type": "string",
|
||||
"format": "byte",
|
||||
"x-kubernetes-list-type": "atomic"
|
||||
},
|
||||
"path": {
|
||||
"description": "Path is the subdirectory for the Grafana data. If specified, Grafana will ignore anything that is outside this directory in the repository. This is usually something like `grafana/`. Trailing and leading slash are not required. They are always added when needed. The path is relative to the root of the repository, regardless of the leading slash.\n\nWhen specifying something like `grafana-`, we will not look for `grafana-*`; we will only look for files under the directory `/grafana-/`. That means `/grafana-example.json` would not be found.",
|
||||
"type": "string"
|
||||
},
|
||||
"token": {
|
||||
"description": "Token for accessing the repository. If set, it will be encrypted into encryptedToken, then set to an empty string again.",
|
||||
"type": "string"
|
||||
},
|
||||
"tokenUser": {
|
||||
"description": "TokenUser is the user that will be used to access the repository if it's a personal access token.",
|
||||
"type": "string"
|
||||
@@ -2761,12 +2751,6 @@
|
||||
"type": "string",
|
||||
"default": ""
|
||||
},
|
||||
"encryptedToken": {
|
||||
"description": "Token for accessing the repository, but encrypted. This is not possible to read back to a user decrypted.",
|
||||
"type": "string",
|
||||
"format": "byte",
|
||||
"x-kubernetes-list-type": "atomic"
|
||||
},
|
||||
"generateDashboardPreviews": {
|
||||
"description": "Whether we should show dashboard previews for pull requests. By default, this is false (i.e. we will not create previews).",
|
||||
"type": "boolean"
|
||||
@@ -2775,10 +2759,6 @@
|
||||
"description": "Path is the subdirectory for the Grafana data. If specified, Grafana will ignore anything that is outside this directory in the repository. This is usually something like `grafana/`. Trailing and leading slash are not required. They are always added when needed. The path is relative to the root of the repository, regardless of the leading slash.\n\nWhen specifying something like `grafana-`, we will not look for `grafana-*`; we will only look for files under the directory `/grafana-/`. That means `/grafana-example.json` would not be found.",
|
||||
"type": "string"
|
||||
},
|
||||
"token": {
|
||||
"description": "Token for accessing the repository. If set, it will be encrypted into encryptedToken, then set to an empty string again.",
|
||||
"type": "string"
|
||||
},
|
||||
"url": {
|
||||
"description": "The repository URL (e.g. `https://github.com/example/test`).",
|
||||
"type": "string"
|
||||
@@ -2796,20 +2776,10 @@
|
||||
"type": "string",
|
||||
"default": ""
|
||||
},
|
||||
"encryptedToken": {
|
||||
"description": "Token for accessing the repository, but encrypted. This is not possible to read back to a user decrypted.",
|
||||
"type": "string",
|
||||
"format": "byte",
|
||||
"x-kubernetes-list-type": "atomic"
|
||||
},
|
||||
"path": {
|
||||
"description": "Path is the subdirectory for the Grafana data. If specified, Grafana will ignore anything that is outside this directory in the repository. This is usually something like `grafana/`. Trailing and leading slash are not required. They are always added when needed. The path is relative to the root of the repository, regardless of the leading slash.\n\nWhen specifying something like `grafana-`, we will not look for `grafana-*`; we will only look for files under the directory `/grafana-/`. That means `/grafana-example.json` would not be found.",
|
||||
"type": "string"
|
||||
},
|
||||
"token": {
|
||||
"description": "Token for accessing the repository. If set, it will be encrypted into encryptedToken, then set to an empty string again.",
|
||||
"type": "string"
|
||||
},
|
||||
"url": {
|
||||
"description": "The repository URL (e.g. `https://gitlab.com/example/test`).",
|
||||
"type": "string"
|
||||
@@ -2827,20 +2797,10 @@
|
||||
"type": "string",
|
||||
"default": ""
|
||||
},
|
||||
"encryptedToken": {
|
||||
"description": "Token for accessing the repository, but encrypted. This is not possible to read back to a user decrypted.",
|
||||
"type": "string",
|
||||
"format": "byte",
|
||||
"x-kubernetes-list-type": "atomic"
|
||||
},
|
||||
"path": {
|
||||
"description": "Path is the subdirectory for the Grafana data. If specified, Grafana will ignore anything that is outside this directory in the repository. This is usually something like `grafana/`. Trailing and leading slash are not required. They are always added when needed. The path is relative to the root of the repository, regardless of the leading slash.\n\nWhen specifying something like `grafana-`, we will not look for `grafana-*`; we will only look for files under the directory `/grafana-/`. That means `/grafana-example.json` would not be found.",
|
||||
"type": "string"
|
||||
},
|
||||
"token": {
|
||||
"description": "Token for accessing the repository. If set, it will be encrypted into encryptedToken, then set to an empty string again.",
|
||||
"type": "string"
|
||||
},
|
||||
"tokenUser": {
|
||||
"description": "TokenUser is the user that will be used to access the repository if it's a personal access token.",
|
||||
"type": "string"
|
||||
@@ -4213,7 +4173,6 @@
|
||||
]
|
||||
},
|
||||
"com.github.grafana.grafana.apps.provisioning.pkg.apis.provisioning.v0alpha1.SecureValues": {
|
||||
"description": "NOT YET USED FOR REAL -- testing secure value workflow",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"token": {
|
||||
@@ -4226,7 +4185,7 @@
|
||||
]
|
||||
},
|
||||
"webhookSecret": {
|
||||
"description": "Some webhooks (github) require a secret key value",
|
||||
"description": "Some webhooks (including github) require a secret key value",
|
||||
"default": {},
|
||||
"allOf": [
|
||||
{
|
||||
@@ -4422,10 +4381,6 @@
|
||||
"com.github.grafana.grafana.apps.provisioning.pkg.apis.provisioning.v0alpha1.WebhookStatus": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"encryptedSecret": {
|
||||
"type": "string",
|
||||
"format": "byte"
|
||||
},
|
||||
"id": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
@@ -4434,9 +4389,6 @@
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
},
|
||||
"secret": {
|
||||
"type": "string"
|
||||
},
|
||||
"subscribedEvents": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
|
||||
@@ -570,17 +570,9 @@ func withLogs(opts *testinfra.GrafanaOpts) {
|
||||
opts.EnableLog = true
|
||||
}
|
||||
|
||||
func useAppPlatformSecrets(opts *testinfra.GrafanaOpts) {
|
||||
opts.EnableFeatureToggles = append(opts.EnableFeatureToggles,
|
||||
featuremgmt.FlagProvisioningSecretsService,
|
||||
featuremgmt.FlagSecretsManagementAppPlatform,
|
||||
)
|
||||
}
|
||||
|
||||
func runGrafana(t *testing.T, options ...grafanaOption) *provisioningTestHelper {
|
||||
provisioningPath := t.TempDir()
|
||||
opts := testinfra.GrafanaOpts{
|
||||
AppModeProduction: false, // required for experimental APIs
|
||||
EnableFeatureToggles: []string{
|
||||
featuremgmt.FlagProvisioning,
|
||||
},
|
||||
|
||||
@@ -49,12 +49,12 @@ func TestIntegrationProvisioning_CreatingAndGetting(t *testing.T) {
|
||||
require.NoError(t, err, "failed to read back resource")
|
||||
|
||||
// Move encrypted token mutation
|
||||
token, found, err := unstructured.NestedString(output.Object, "spec", "github", "encryptedToken")
|
||||
require.NoError(t, err, "encryptedToken is not a string")
|
||||
token, found, err := unstructured.NestedString(output.Object, "secure", "token", "name")
|
||||
require.NoError(t, err, "secure token name is not a string")
|
||||
if found {
|
||||
unstructured.RemoveNestedField(input.Object, "spec", "github", "token")
|
||||
err = unstructured.SetNestedField(input.Object, token, "spec", "github", "encryptedToken")
|
||||
require.NoError(t, err, "unable to copy encrypted token")
|
||||
require.True(t, strings.HasPrefix("inline-", token)) // name created automatically
|
||||
err = unstructured.SetNestedField(input.Object, token, "secure", "token", "name")
|
||||
require.NoError(t, err, "unable to copy secure token")
|
||||
}
|
||||
|
||||
// Marshal as real objects to ",omitempty" values are tested properly
|
||||
|
||||
@@ -2,9 +2,6 @@ package provisioning
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -13,10 +10,6 @@ import (
|
||||
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/provisioning/secrets"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/secret/contracts"
|
||||
)
|
||||
|
||||
func TestIntegrationProvisioning_InlineSecrets(t *testing.T) {
|
||||
@@ -24,12 +17,12 @@ func TestIntegrationProvisioning_InlineSecrets(t *testing.T) {
|
||||
t.Skip("skipping integration test")
|
||||
}
|
||||
|
||||
helper := runGrafana(t, useAppPlatformSecrets)
|
||||
helper := runGrafana(t)
|
||||
createOptions := metav1.CreateOptions{FieldValidation: "Strict"}
|
||||
ctx := context.Background()
|
||||
|
||||
decryptService := helper.GetEnv().DecryptService
|
||||
require.NotNil(t, decryptService, "decrypt service wired properly")
|
||||
require.NotNil(t, decryptService, "decrypt service not wired properly")
|
||||
|
||||
type expectedField struct {
|
||||
Path []string
|
||||
@@ -107,519 +100,3 @@ func TestIntegrationProvisioning_InlineSecrets(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIntegrationProvisioning_LegacySecrets(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping integration test")
|
||||
}
|
||||
|
||||
helper := runGrafana(t)
|
||||
createOptions := metav1.CreateOptions{FieldValidation: "Strict"}
|
||||
ctx := context.Background()
|
||||
|
||||
type expectedField struct {
|
||||
Path []string
|
||||
ExpectedDecryptedValue string
|
||||
}
|
||||
|
||||
secretsService := helper.GetEnv().RepositorySecrets
|
||||
tests := []struct {
|
||||
name string
|
||||
values map[string]any
|
||||
inputFile string
|
||||
expectedFields []expectedField
|
||||
}{
|
||||
{
|
||||
name: "github token encrypted",
|
||||
values: map[string]any{
|
||||
"Token": "some-token",
|
||||
},
|
||||
inputFile: "testdata/github-readonly.json.tmpl",
|
||||
expectedFields: []expectedField{
|
||||
{
|
||||
Path: []string{"spec", "github", "token"},
|
||||
ExpectedDecryptedValue: "",
|
||||
},
|
||||
{
|
||||
Path: []string{"spec", "github", "encryptedToken"},
|
||||
ExpectedDecryptedValue: "some-token",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "git token encrypted",
|
||||
values: map[string]any{
|
||||
"Token": "some-token",
|
||||
},
|
||||
inputFile: "testdata/git-readonly.json.tmpl",
|
||||
expectedFields: []expectedField{
|
||||
{
|
||||
Path: []string{"spec", "git", "token"},
|
||||
ExpectedDecryptedValue: "",
|
||||
},
|
||||
{
|
||||
Path: []string{"spec", "git", "encryptedToken"},
|
||||
ExpectedDecryptedValue: "some-token",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
input := helper.RenderObject(t, test.inputFile, test.values)
|
||||
_, err := helper.Repositories.Resource.Create(ctx, input, createOptions)
|
||||
require.NoError(t, err, "failed to create resource")
|
||||
|
||||
name := mustNestedString(input.Object, "metadata", "name")
|
||||
output, err := helper.Repositories.Resource.Get(ctx, name, metav1.GetOptions{})
|
||||
require.NoError(t, err, "failed to read back resource")
|
||||
repo := unstructuredToRepository(t, output)
|
||||
|
||||
// Move encrypted token mutation
|
||||
for _, expectedField := range test.expectedFields {
|
||||
value, decrypted := encryptedField(t, secretsService, repo, output.Object, expectedField.Path, expectedField.ExpectedDecryptedValue != "")
|
||||
require.False(t, strings.HasPrefix(value, name), "value should not be prefixed with the repository name")
|
||||
require.Equal(t, expectedField.ExpectedDecryptedValue, decrypted)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIntegrationProvisioning_Secrets_LegacyUpdate(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping integration test")
|
||||
}
|
||||
|
||||
helper := runGrafana(t)
|
||||
createOptions := metav1.CreateOptions{FieldValidation: "Strict"}
|
||||
updateOptions := metav1.UpdateOptions{}
|
||||
ctx := context.Background()
|
||||
|
||||
secretsService := helper.GetEnv().RepositorySecrets
|
||||
|
||||
type expectedField struct {
|
||||
Path []string
|
||||
ExpectedValue string
|
||||
ExpectedDecryptedValue string
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
values map[string]any
|
||||
inputFile string
|
||||
updateValues map[string]any
|
||||
expectedFields []expectedField
|
||||
}{
|
||||
{
|
||||
name: "update github token (legacy secrets)",
|
||||
values: map[string]any{
|
||||
"Token": "initial-token",
|
||||
},
|
||||
inputFile: "testdata/github-readonly.json.tmpl",
|
||||
updateValues: map[string]any{
|
||||
"Token": "updated-token",
|
||||
},
|
||||
expectedFields: []expectedField{
|
||||
{
|
||||
Path: []string{"spec", "github", "token"},
|
||||
ExpectedDecryptedValue: "",
|
||||
},
|
||||
{
|
||||
Path: []string{"spec", "github", "encryptedToken"},
|
||||
ExpectedDecryptedValue: "updated-token",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "update git token (legacy secrets)",
|
||||
values: map[string]any{
|
||||
"Token": "initial-token",
|
||||
},
|
||||
inputFile: "testdata/git-readonly.json.tmpl",
|
||||
updateValues: map[string]any{
|
||||
"Token": "updated-token",
|
||||
},
|
||||
expectedFields: []expectedField{
|
||||
{
|
||||
Path: []string{"spec", "git", "token"},
|
||||
ExpectedDecryptedValue: "",
|
||||
},
|
||||
{
|
||||
Path: []string{"spec", "git", "encryptedToken"},
|
||||
ExpectedDecryptedValue: "updated-token",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
// Create initial resource
|
||||
input := helper.RenderObject(t, test.inputFile, test.values)
|
||||
_, err := helper.Repositories.Resource.Create(ctx, input, createOptions)
|
||||
require.NoError(t, err, "failed to create resource")
|
||||
|
||||
name := mustNestedString(input.Object, "metadata", "name")
|
||||
|
||||
// Prepare updated resource
|
||||
updatedInput := helper.RenderObject(t, test.inputFile, test.updateValues)
|
||||
// Set the same name and resourceVersion for update
|
||||
updatedInput.Object["metadata"].(map[string]any)["name"] = name
|
||||
|
||||
// Fetch current resourceVersion
|
||||
current, err := helper.Repositories.Resource.Get(ctx, name, metav1.GetOptions{})
|
||||
require.NoError(t, err, "failed to get current resource for update")
|
||||
updatedInput.Object["metadata"].(map[string]any)["resourceVersion"] = current.Object["metadata"].(map[string]any)["resourceVersion"]
|
||||
|
||||
_, err = helper.Repositories.Resource.Update(ctx, updatedInput, updateOptions)
|
||||
require.NoError(t, err, "failed to update resource")
|
||||
|
||||
output, err := helper.Repositories.Resource.Get(ctx, name, metav1.GetOptions{})
|
||||
require.NoError(t, err, "failed to read back resource after update")
|
||||
repo := unstructuredToRepository(t, output)
|
||||
|
||||
for _, expectedField := range test.expectedFields {
|
||||
value, decrypted := encryptedField(t, secretsService, repo, output.Object, expectedField.Path, expectedField.ExpectedDecryptedValue != "")
|
||||
require.False(t, strings.HasPrefix(value, name), "value should not be prefixed with the repository name")
|
||||
require.Equal(t, expectedField.ExpectedDecryptedValue, decrypted)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIntegrationProvisioning_Secrets(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping integration test")
|
||||
}
|
||||
|
||||
helper := runGrafana(t, useAppPlatformSecrets)
|
||||
createOptions := metav1.CreateOptions{FieldValidation: "Strict"}
|
||||
ctx := context.Background()
|
||||
|
||||
secretsService := helper.GetEnv().RepositorySecrets
|
||||
|
||||
type expectedField struct {
|
||||
Path []string
|
||||
ExpectedValue string
|
||||
ExpectedDecryptedValue string
|
||||
}
|
||||
// TODO: Add test of fallbacks
|
||||
tests := []struct {
|
||||
name string
|
||||
values map[string]any
|
||||
inputFile string
|
||||
expectedFields []expectedField
|
||||
}{
|
||||
{
|
||||
name: "github token encrypted",
|
||||
values: map[string]any{
|
||||
"Token": "some-token",
|
||||
},
|
||||
inputFile: "testdata/github-readonly.json.tmpl",
|
||||
expectedFields: []expectedField{
|
||||
{
|
||||
Path: []string{"spec", "github", "token"},
|
||||
ExpectedDecryptedValue: "",
|
||||
},
|
||||
{
|
||||
Path: []string{"spec", "github", "encryptedToken"},
|
||||
ExpectedValue: "github-token",
|
||||
ExpectedDecryptedValue: "some-token",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "git token encrypted",
|
||||
values: map[string]any{
|
||||
"Token": "some-token",
|
||||
},
|
||||
inputFile: "testdata/git-readonly.json.tmpl",
|
||||
expectedFields: []expectedField{
|
||||
{
|
||||
Path: []string{"spec", "git", "token"},
|
||||
ExpectedDecryptedValue: "",
|
||||
},
|
||||
{
|
||||
Path: []string{"spec", "git", "encryptedToken"},
|
||||
ExpectedValue: "git-token",
|
||||
ExpectedDecryptedValue: "some-token",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
input := helper.RenderObject(t, test.inputFile, test.values)
|
||||
_, err := helper.Repositories.Resource.Create(ctx, input, createOptions)
|
||||
require.NoError(t, err, "failed to create resource")
|
||||
|
||||
name := mustNestedString(input.Object, "metadata", "name")
|
||||
output, err := helper.Repositories.Resource.Get(ctx, name, metav1.GetOptions{})
|
||||
require.NoError(t, err, "failed to read back resource")
|
||||
repo := unstructuredToRepository(t, output)
|
||||
|
||||
// Move encrypted token mutation
|
||||
for _, expectedField := range test.expectedFields {
|
||||
value, decrypted := encryptedField(t, secretsService, repo, output.Object, expectedField.Path, expectedField.ExpectedDecryptedValue != "")
|
||||
|
||||
if expectedField.ExpectedValue != "" {
|
||||
require.Equal(t, name+"-"+expectedField.ExpectedValue, value)
|
||||
}
|
||||
|
||||
if expectedField.ExpectedDecryptedValue != "" {
|
||||
require.Equal(t, expectedField.ExpectedDecryptedValue, decrypted)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIntegrationProvisioning_Secrets_Update(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping integration test in short mode")
|
||||
}
|
||||
ctx := context.Background()
|
||||
helper := runGrafana(t, useAppPlatformSecrets)
|
||||
secretsService := helper.GetEnv().RepositorySecrets
|
||||
createOptions := metav1.CreateOptions{}
|
||||
updateOptions := metav1.UpdateOptions{}
|
||||
|
||||
type expectedField struct {
|
||||
Path []string
|
||||
ExpectedValue string
|
||||
ExpectedDecryptedValue string
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
inputFile string
|
||||
values map[string]interface{}
|
||||
updateValues map[string]interface{}
|
||||
expectedFields []expectedField
|
||||
updatedFields []expectedField
|
||||
}{
|
||||
{
|
||||
name: "update encrypted git token",
|
||||
inputFile: "testdata/git-readonly.json.tmpl",
|
||||
values: map[string]interface{}{
|
||||
"Token": "initial-token",
|
||||
},
|
||||
updateValues: map[string]interface{}{
|
||||
"Token": "updated-token",
|
||||
},
|
||||
expectedFields: []expectedField{
|
||||
{
|
||||
Path: []string{"spec", "git", "token"},
|
||||
ExpectedDecryptedValue: "",
|
||||
},
|
||||
{
|
||||
Path: []string{"spec", "git", "encryptedToken"},
|
||||
ExpectedValue: "git-token",
|
||||
ExpectedDecryptedValue: "initial-token",
|
||||
},
|
||||
},
|
||||
updatedFields: []expectedField{
|
||||
{
|
||||
Path: []string{"spec", "git", "token"},
|
||||
ExpectedDecryptedValue: "",
|
||||
},
|
||||
{
|
||||
Path: []string{"spec", "git", "encryptedToken"},
|
||||
ExpectedValue: "git-token",
|
||||
ExpectedDecryptedValue: "updated-token",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "update encrypted github token",
|
||||
inputFile: "testdata/github-readonly.json.tmpl",
|
||||
values: map[string]interface{}{
|
||||
"Token": "initial-token",
|
||||
},
|
||||
updateValues: map[string]interface{}{
|
||||
"Token": "updated-token",
|
||||
},
|
||||
expectedFields: []expectedField{
|
||||
{
|
||||
Path: []string{"spec", "github", "token"},
|
||||
ExpectedDecryptedValue: "",
|
||||
},
|
||||
{
|
||||
Path: []string{"spec", "github", "encryptedToken"},
|
||||
ExpectedValue: "github-token",
|
||||
ExpectedDecryptedValue: "initial-token",
|
||||
},
|
||||
},
|
||||
updatedFields: []expectedField{
|
||||
{
|
||||
Path: []string{"spec", "github", "token"},
|
||||
ExpectedDecryptedValue: "",
|
||||
},
|
||||
{
|
||||
Path: []string{"spec", "github", "encryptedToken"},
|
||||
ExpectedValue: "github-token",
|
||||
ExpectedDecryptedValue: "updated-token",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
// Create initial resource
|
||||
input := helper.RenderObject(t, test.inputFile, test.values)
|
||||
_, err := helper.Repositories.Resource.Create(ctx, input, createOptions)
|
||||
require.NoError(t, err, "failed to create resource")
|
||||
|
||||
name := mustNestedString(input.Object, "metadata", "name")
|
||||
output, err := helper.Repositories.Resource.Get(ctx, name, metav1.GetOptions{})
|
||||
require.NoError(t, err, "failed to read back resource")
|
||||
|
||||
// Update the resource
|
||||
updatedInput := helper.RenderObject(t, test.inputFile, test.updateValues)
|
||||
// Set the same name and resourceVersion for update
|
||||
_ = unstructured.SetNestedField(updatedInput.Object, name, "metadata", "name")
|
||||
_ = unstructured.SetNestedField(updatedInput.Object, output.GetResourceVersion(), "metadata", "resourceVersion")
|
||||
_, err = helper.Repositories.Resource.Update(ctx, updatedInput, updateOptions)
|
||||
require.NoError(t, err, "failed to update resource")
|
||||
|
||||
updatedOutput, err := helper.Repositories.Resource.Get(ctx, name, metav1.GetOptions{})
|
||||
require.NoError(t, err, "failed to read back updated resource")
|
||||
updatedRepo := unstructuredToRepository(t, updatedOutput)
|
||||
|
||||
// Check updated fields
|
||||
for _, expectedField := range test.updatedFields {
|
||||
value, decrypted := encryptedField(t, secretsService, updatedRepo, updatedOutput.Object, expectedField.Path, expectedField.ExpectedDecryptedValue != "")
|
||||
|
||||
if expectedField.ExpectedValue != "" {
|
||||
require.Equal(t, name+"-"+expectedField.ExpectedValue, value)
|
||||
}
|
||||
|
||||
if expectedField.ExpectedDecryptedValue != "" {
|
||||
require.Equal(t, expectedField.ExpectedDecryptedValue, decrypted)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIntegrationProvisioning_Secrets_Removal(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping integration test")
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
helper := runGrafana(t, useAppPlatformSecrets)
|
||||
secretsService := helper.GetEnv().RepositorySecrets
|
||||
createOptions := metav1.CreateOptions{}
|
||||
|
||||
type expectedField struct {
|
||||
Path []string
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
inputFile string
|
||||
values map[string]interface{}
|
||||
expectedFields []expectedField
|
||||
updatedFields []expectedField
|
||||
}{
|
||||
{
|
||||
name: "remove encrypted git token",
|
||||
inputFile: "testdata/git-readonly.json.tmpl",
|
||||
values: map[string]interface{}{
|
||||
"Token": "initial-token",
|
||||
},
|
||||
expectedFields: []expectedField{
|
||||
{
|
||||
Path: []string{"spec", "git", "encryptedToken"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "remove encrypted github token",
|
||||
inputFile: "testdata/github-readonly.json.tmpl",
|
||||
values: map[string]interface{}{
|
||||
"Token": "initial-token",
|
||||
},
|
||||
expectedFields: []expectedField{
|
||||
{
|
||||
Path: []string{"spec", "github", "encryptedToken"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
// Create initial resource
|
||||
input := helper.RenderObject(t, test.inputFile, test.values)
|
||||
_, err := helper.Repositories.Resource.Create(ctx, input, createOptions)
|
||||
require.NoError(t, err, "failed to create resource")
|
||||
|
||||
name := mustNestedString(input.Object, "metadata", "name")
|
||||
output, err := helper.Repositories.Resource.Get(ctx, name, metav1.GetOptions{})
|
||||
require.NoError(t, err, "failed to read back resource")
|
||||
|
||||
repo := unstructuredToRepository(t, output)
|
||||
|
||||
// Set the same name and resourceVersion for update
|
||||
err = helper.Repositories.Resource.Delete(ctx, name, metav1.DeleteOptions{})
|
||||
require.NoError(t, err, "failed to delete resource")
|
||||
|
||||
for _, expectedField := range test.expectedFields {
|
||||
secretName, found, err := base64DecodedField(output.Object, expectedField.Path)
|
||||
require.NoError(t, err, "failed to decode base64 value")
|
||||
require.True(t, found, "secretName should be found")
|
||||
require.NotEmpty(t, secretName)
|
||||
|
||||
var lastDecrypted []byte
|
||||
require.Eventually(t, func() bool {
|
||||
lastDecrypted, err = secretsService.Decrypt(ctx, repo, secretName)
|
||||
return err != nil && errors.Is(err, contracts.ErrDecryptNotFound)
|
||||
}, 1000*time.Second, 500*time.Millisecond, "expected ErrDecryptNotFound error, got %v", lastDecrypted)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func encryptedField(t *testing.T, secretsService secrets.RepositorySecrets, repo *provisioning.Repository, obj map[string]any, path []string, expectedValue bool) (string, string) {
|
||||
value, found, err := base64DecodedField(obj, path)
|
||||
if err != nil {
|
||||
require.NoError(t, err, "failed to decode base64 value")
|
||||
}
|
||||
|
||||
if expectedValue {
|
||||
decrypted, err := secretsService.Decrypt(context.Background(), repo, value)
|
||||
require.NoError(t, err, "failed to eecrypt value")
|
||||
return value, string(decrypted)
|
||||
} else {
|
||||
require.False(t, found, "value should not be found")
|
||||
return "", ""
|
||||
}
|
||||
}
|
||||
|
||||
func base64DecodedField(obj map[string]any, path []string) (string, bool, error) {
|
||||
value, found, err := unstructured.NestedFieldNoCopy(obj, path...)
|
||||
if err != nil {
|
||||
return "", false, err
|
||||
}
|
||||
|
||||
if !found {
|
||||
return "", false, nil
|
||||
}
|
||||
|
||||
valueStr, ok := value.(string)
|
||||
if !ok {
|
||||
return "", false, fmt.Errorf("value is not a string")
|
||||
}
|
||||
|
||||
decodedValue, err := base64.StdEncoding.DecodeString(valueStr)
|
||||
if err != nil {
|
||||
return "", false, fmt.Errorf("failed to decode base64 valueStr: %w", err)
|
||||
}
|
||||
|
||||
return string(decodedValue), true, nil
|
||||
}
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
"git": {
|
||||
"url": "{{ or .URL "https://github.com/grafana/grafana-git-sync-demo" }}",
|
||||
"branch": "{{ or .Branch "integration-test" }}",
|
||||
"token": "{{ or .Token "" }}",
|
||||
"path": "{{ or .Path "grafana/" }}"
|
||||
},
|
||||
"sync": {
|
||||
@@ -20,5 +19,8 @@
|
||||
"intervalSeconds": {{ or .SyncIntervalSeconds 60 }}
|
||||
},
|
||||
"workflows": []
|
||||
},
|
||||
"secure": {
|
||||
"token": { "create": "{{ or .Token "" }}" }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,6 @@
|
||||
"url": "{{ or .URL "https://github.com/grafana/grafana-git-sync-demo" }}",
|
||||
"branch": "{{ or .Branch "integration-test" }}",
|
||||
"generateDashboardPreviews": {{ if .GenerateDashboardPreviews }} true {{ else }} false {{ end }},
|
||||
"token": "{{ or .Token "" }}",
|
||||
"path": "{{ or .Path "grafana/" }}"
|
||||
},
|
||||
"sync": {
|
||||
@@ -21,5 +20,8 @@
|
||||
"intervalSeconds": {{ or .SyncIntervalSeconds 60 }}
|
||||
},
|
||||
"workflows": []
|
||||
},
|
||||
"secure": {
|
||||
"token": { "create": "{{ or .Token "" }}" }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,6 @@
|
||||
"url": "{{ or .URL "https://github.com/grafana/grafana-git-sync-demo" }}",
|
||||
"branch": "{{ or .Branch "integration-test" }}",
|
||||
"generateDashboardPreviews": {{ if .GenerateDashboardPreviews }} true {{ else }} false {{ end }},
|
||||
"token": "{{ or .Token "" }}",
|
||||
"path": "{{ or .Path "grafana/" }}"
|
||||
}
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user