mirror of https://github.com/grafana/grafana
Provisioning: Begin using secrets store (#108044)
- Provisioning: Begin using secrets store - Refactor integration with secrets store - Add back the legacy service - Separate concerns for encrypt and decrypt - Handle update within Encrypt function - Add interface for secure value service - Add feature flag for using secrets service - Add the dual service for temporary solution. * Add first integration tests for encrypted tokens * Add integration test for app platform secrets * Validate it has the name or not * Create wire provider * Always save to the secret if provided secret --------- Co-authored-by: Roberto Jimenez Sanchez <roberto.jimenez@grafana.com> Co-authored-by: Roberto Jiménez Sánchez <jszroberto@gmail.com>kristina/correlation-migrate
parent
68b9a5f57c
commit
d39a47a89b
@ -0,0 +1,37 @@ |
||||
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) |
||||
} |
@ -0,0 +1,154 @@ |
||||
// 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 |
||||
} |
@ -0,0 +1,111 @@ |
||||
// Code generated by mockery v2.52.4. DO NOT EDIT.
|
||||
|
||||
package mocks |
||||
|
||||
import ( |
||||
context "context" |
||||
|
||||
service "github.com/grafana/grafana/pkg/registry/apis/secret/service" |
||||
mock "github.com/stretchr/testify/mock" |
||||
) |
||||
|
||||
// MockDecryptService is an autogenerated mock type for the DecryptService type
|
||||
type MockDecryptService struct { |
||||
mock.Mock |
||||
} |
||||
|
||||
type MockDecryptService_Expecter struct { |
||||
mock *mock.Mock |
||||
} |
||||
|
||||
func (_m *MockDecryptService) EXPECT() *MockDecryptService_Expecter { |
||||
return &MockDecryptService_Expecter{mock: &_m.Mock} |
||||
} |
||||
|
||||
// Decrypt provides a mock function with given fields: ctx, namespace, names
|
||||
func (_m *MockDecryptService) Decrypt(ctx context.Context, namespace string, names ...string) (map[string]service.DecryptResult, error) { |
||||
_va := make([]interface{}, len(names)) |
||||
for _i := range names { |
||||
_va[_i] = names[_i] |
||||
} |
||||
var _ca []interface{} |
||||
_ca = append(_ca, ctx, namespace) |
||||
_ca = append(_ca, _va...) |
||||
ret := _m.Called(_ca...) |
||||
|
||||
if len(ret) == 0 { |
||||
panic("no return value specified for Decrypt") |
||||
} |
||||
|
||||
var r0 map[string]service.DecryptResult |
||||
var r1 error |
||||
if rf, ok := ret.Get(0).(func(context.Context, string, ...string) (map[string]service.DecryptResult, error)); ok { |
||||
return rf(ctx, namespace, names...) |
||||
} |
||||
if rf, ok := ret.Get(0).(func(context.Context, string, ...string) map[string]service.DecryptResult); ok { |
||||
r0 = rf(ctx, namespace, names...) |
||||
} else { |
||||
if ret.Get(0) != nil { |
||||
r0 = ret.Get(0).(map[string]service.DecryptResult) |
||||
} |
||||
} |
||||
|
||||
if rf, ok := ret.Get(1).(func(context.Context, string, ...string) error); ok { |
||||
r1 = rf(ctx, namespace, names...) |
||||
} else { |
||||
r1 = ret.Error(1) |
||||
} |
||||
|
||||
return r0, r1 |
||||
} |
||||
|
||||
// MockDecryptService_Decrypt_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Decrypt'
|
||||
type MockDecryptService_Decrypt_Call struct { |
||||
*mock.Call |
||||
} |
||||
|
||||
// Decrypt is a helper method to define mock.On call
|
||||
// - ctx context.Context
|
||||
// - namespace string
|
||||
// - names ...string
|
||||
func (_e *MockDecryptService_Expecter) Decrypt(ctx interface{}, namespace interface{}, names ...interface{}) *MockDecryptService_Decrypt_Call { |
||||
return &MockDecryptService_Decrypt_Call{Call: _e.mock.On("Decrypt", |
||||
append([]interface{}{ctx, namespace}, names...)...)} |
||||
} |
||||
|
||||
func (_c *MockDecryptService_Decrypt_Call) Run(run func(ctx context.Context, namespace string, names ...string)) *MockDecryptService_Decrypt_Call { |
||||
_c.Call.Run(func(args mock.Arguments) { |
||||
variadicArgs := make([]string, len(args)-2) |
||||
for i, a := range args[2:] { |
||||
if a != nil { |
||||
variadicArgs[i] = a.(string) |
||||
} |
||||
} |
||||
run(args[0].(context.Context), args[1].(string), variadicArgs...) |
||||
}) |
||||
return _c |
||||
} |
||||
|
||||
func (_c *MockDecryptService_Decrypt_Call) Return(_a0 map[string]service.DecryptResult, _a1 error) *MockDecryptService_Decrypt_Call { |
||||
_c.Call.Return(_a0, _a1) |
||||
return _c |
||||
} |
||||
|
||||
func (_c *MockDecryptService_Decrypt_Call) RunAndReturn(run func(context.Context, string, ...string) (map[string]service.DecryptResult, error)) *MockDecryptService_Decrypt_Call { |
||||
_c.Call.Return(run) |
||||
return _c |
||||
} |
||||
|
||||
// NewMockDecryptService creates a new instance of MockDecryptService. 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 NewMockDecryptService(t interface { |
||||
mock.TestingT |
||||
Cleanup(func()) |
||||
}) *MockDecryptService { |
||||
mock := &MockDecryptService{} |
||||
mock.Mock.Test(t) |
||||
|
||||
t.Cleanup(func() { mock.AssertExpectations(t) }) |
||||
|
||||
return mock |
||||
} |
@ -0,0 +1,98 @@ |
||||
package secrets |
||||
|
||||
import ( |
||||
"context" |
||||
|
||||
provisioning "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1" |
||||
"github.com/grafana/grafana/pkg/registry/apis/secret/service" |
||||
"github.com/grafana/grafana/pkg/services/featuremgmt" |
||||
grafanasecrets "github.com/grafana/grafana/pkg/services/secrets" |
||||
) |
||||
|
||||
func ProvideRepositorySecrets( |
||||
features featuremgmt.FeatureToggles, |
||||
legacySecretsSvc grafanasecrets.Service, |
||||
secretsSvc *service.SecureValueService, |
||||
decryptSvc service.DecryptService, |
||||
) RepositorySecrets { |
||||
return NewRepositorySecrets(features, NewSecretsService(secretsSvc, decryptSvc), 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) |
||||
} |
||||
|
||||
// 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) { |
||||
if s.features.IsEnabled(ctx, featuremgmt.FlagProvisioningSecretsService) { |
||||
encrypted, err := s.secretsSvc.Encrypt(ctx, r.GetNamespace(), name, data) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
return []byte(encrypted), err |
||||
} |
||||
|
||||
encrypted, err := s.legacySecrets.Encrypt(ctx, []byte(data)) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
return encrypted, nil |
||||
} |
||||
|
||||
// Decrypt attempts to retrieve and decrypt secret data for a repository.
|
||||
// If the provisioning secrets service feature flag is enabled, it tries the new secrets service first.
|
||||
// - On success, returns the decrypted data.
|
||||
// - On failure, falls back to the legacy secrets service.
|
||||
//
|
||||
// If the feature flag is disabled, it tries the legacy secrets service first.
|
||||
// - On success, returns the decrypted data.
|
||||
// - On failure, falls back to the new secrets service.
|
||||
//
|
||||
// This dual-path logic is intended to support migration between secret backends.
|
||||
func (s *repositorySecrets) Decrypt(ctx context.Context, r *provisioning.Repository, nameOrValue string) ([]byte, error) { |
||||
if s.features.IsEnabled(ctx, featuremgmt.FlagProvisioningSecretsService) { |
||||
data, err := s.secretsSvc.Decrypt(ctx, r.GetNamespace(), nameOrValue) |
||||
if err == nil { |
||||
return data, nil |
||||
} |
||||
|
||||
// If the new service fails, fall back to legacy
|
||||
return s.legacySecrets.Decrypt(ctx, []byte(nameOrValue)) |
||||
} |
||||
|
||||
// If the new service is disabled, use the legacy service first
|
||||
data, err := s.legacySecrets.Decrypt(ctx, []byte(nameOrValue)) |
||||
if err == nil { |
||||
return data, nil |
||||
} |
||||
|
||||
return s.secretsSvc.Decrypt(ctx, r.GetNamespace(), nameOrValue) |
||||
} |
@ -0,0 +1,158 @@ |
||||
// Code generated by mockery v2.52.4. DO NOT EDIT.
|
||||
|
||||
package secrets |
||||
|
||||
import ( |
||||
context "context" |
||||
|
||||
v0alpha1 "github.com/grafana/grafana/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 |
||||
} |
||||
|
||||
// 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 |
||||
} |
@ -0,0 +1,237 @@ |
||||
package secrets |
||||
|
||||
import ( |
||||
"context" |
||||
"errors" |
||||
"testing" |
||||
|
||||
provisioning "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1" |
||||
"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 |
||||
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().Decrypt(s.ctx, "test-namespace", "encrypted-value").Return([]byte("decrypted-data"), nil) |
||||
}, |
||||
expectedResult: []byte("decrypted-data"), |
||||
}, |
||||
{ |
||||
name: "legacy service success", |
||||
namespace: "test-namespace", |
||||
featureEnabled: false, |
||||
setupMocks: func(s *testSetup) { |
||||
s.expectFeatureFlag(false) |
||||
s.mockLegacy.EXPECT().Decrypt(s.ctx, []byte("encrypted-value")).Return([]byte("decrypted-legacy-data"), nil) |
||||
}, |
||||
expectedResult: []byte("decrypted-legacy-data"), |
||||
}, |
||||
{ |
||||
name: "new service fails, fallback to legacy succeeds", |
||||
namespace: "test-namespace", |
||||
featureEnabled: true, |
||||
setupMocks: func(s *testSetup) { |
||||
s.expectFeatureFlag(true) |
||||
s.mockSecrets.EXPECT().Decrypt(s.ctx, "test-namespace", "encrypted-value").Return(nil, errors.New("new service failed")) |
||||
s.mockLegacy.EXPECT().Decrypt(s.ctx, []byte("encrypted-value")).Return([]byte("decrypted-fallback-data"), nil) |
||||
}, |
||||
expectedResult: []byte("decrypted-fallback-data"), |
||||
}, |
||||
{ |
||||
name: "legacy service fails, fallback to new succeeds", |
||||
namespace: "test-namespace", |
||||
featureEnabled: false, |
||||
setupMocks: func(s *testSetup) { |
||||
s.expectFeatureFlag(false) |
||||
s.mockLegacy.EXPECT().Decrypt(s.ctx, []byte("encrypted-value")).Return(nil, errors.New("legacy service failed")) |
||||
s.mockSecrets.EXPECT().Decrypt(s.ctx, "test-namespace", "encrypted-value").Return([]byte("decrypted-new-data"), nil) |
||||
}, |
||||
expectedResult: []byte("decrypted-new-data"), |
||||
}, |
||||
{ |
||||
name: "both services fail (feature flag enabled)", |
||||
namespace: "test-namespace", |
||||
featureEnabled: true, |
||||
setupMocks: func(s *testSetup) { |
||||
s.expectFeatureFlag(true) |
||||
s.mockSecrets.EXPECT().Decrypt(s.ctx, "test-namespace", "encrypted-value").Return(nil, errors.New("new service failed")) |
||||
s.mockLegacy.EXPECT().Decrypt(s.ctx, []byte("encrypted-value")).Return(nil, errors.New("legacy service failed")) |
||||
}, |
||||
expectedError: "legacy service failed", |
||||
}, |
||||
{ |
||||
name: "both services fail (feature flag disabled)", |
||||
namespace: "test-namespace", |
||||
featureEnabled: false, |
||||
setupMocks: func(s *testSetup) { |
||||
s.expectFeatureFlag(false) |
||||
s.mockLegacy.EXPECT().Decrypt(s.ctx, []byte("encrypted-value")).Return(nil, errors.New("legacy service failed")) |
||||
s.mockSecrets.EXPECT().Decrypt(s.ctx, "test-namespace", "encrypted-value").Return(nil, errors.New("new service failed")) |
||||
}, |
||||
expectedError: "new service failed", |
||||
}, |
||||
{ |
||||
name: "custom namespace handling", |
||||
namespace: "custom-namespace", |
||||
featureEnabled: true, |
||||
setupMocks: func(s *testSetup) { |
||||
s.expectFeatureFlag(true) |
||||
s.mockSecrets.EXPECT().Decrypt(s.ctx, "custom-namespace", "encrypted-value").Return([]byte("test-data"), nil) |
||||
}, |
||||
expectedResult: []byte("test-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, "encrypted-value") |
||||
|
||||
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) |
||||
} |
||||
}) |
||||
} |
||||
} |
@ -0,0 +1,412 @@ |
||||
package secrets |
||||
|
||||
import ( |
||||
"context" |
||||
"errors" |
||||
"testing" |
||||
|
||||
secretv1beta1 "github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1" |
||||
"github.com/grafana/grafana/pkg/apimachinery/identity" |
||||
"github.com/grafana/grafana/pkg/registry/apis/provisioning/secrets/mocks" |
||||
"github.com/grafana/grafana/pkg/registry/apis/secret/contracts" |
||||
"github.com/grafana/grafana/pkg/registry/apis/secret/service" |
||||
"github.com/grafana/grafana/pkg/registry/apis/secret/xkube" |
||||
"github.com/stretchr/testify/assert" |
||||
"github.com/stretchr/testify/mock" |
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" |
||||
) |
||||
|
||||
func TestNewSecretsService(t *testing.T) { |
||||
mockSecretsSvc := NewMockSecureValueService(t) |
||||
mockDecryptSvc := &mocks.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(*MockSecureValueService, *mocks.MockDecryptService) |
||||
expectedName string |
||||
expectedError string |
||||
}{ |
||||
{ |
||||
name: "successfully create new secret", |
||||
namespace: "test-namespace", |
||||
secretName: "test-secret", |
||||
data: "secret-data", |
||||
setupMocks: func(mockSecretsSvc *MockSecureValueService, mockDecryptSvc *mocks.MockDecryptService) { |
||||
// Assert Read call with correct parameters
|
||||
mockSecretsSvc.EXPECT().Read( |
||||
mock.MatchedBy(func(ctx context.Context) bool { |
||||
requester, err := identity.GetRequester(ctx) |
||||
return err == nil && requester != nil && requester.GetUID() == ":test-uid" |
||||
}), |
||||
xkube.Namespace("test-namespace"), |
||||
"test-secret", |
||||
).Return(nil, contracts.ErrSecureValueNotFound) |
||||
|
||||
// Assert Create call with detailed validation
|
||||
mockSecretsSvc.EXPECT().Create( |
||||
mock.MatchedBy(func(ctx context.Context) bool { |
||||
requester, err := identity.GetRequester(ctx) |
||||
return err == nil && requester != nil && requester.GetUID() == ":test-uid" |
||||
}), |
||||
mock.MatchedBy(func(sv *secretv1beta1.SecureValue) bool { |
||||
if sv.Namespace != "test-namespace" || sv.Name != "test-secret" { |
||||
return false |
||||
} |
||||
if sv.Spec.Description != "provisioning: test-secret" { |
||||
return false |
||||
} |
||||
if sv.Spec.Value == nil { |
||||
return false |
||||
} |
||||
if len(sv.Spec.Decrypters) != 1 || sv.Spec.Decrypters[0] != svcName { |
||||
return false |
||||
} |
||||
// Verify the actual secret value
|
||||
if sv.Spec.Value.DangerouslyExposeAndConsumeValue() != "secret-data" { |
||||
return false |
||||
} |
||||
return true |
||||
}), |
||||
":test-uid", |
||||
).Return(&secretv1beta1.SecureValue{ |
||||
ObjectMeta: metav1.ObjectMeta{ |
||||
Name: "test-secret", |
||||
Namespace: "test-namespace", |
||||
}, |
||||
}, nil) |
||||
}, |
||||
expectedName: "test-secret", |
||||
}, |
||||
{ |
||||
name: "successfully update existing secret", |
||||
namespace: "test-namespace", |
||||
secretName: "existing-secret", |
||||
data: "new-secret-data", |
||||
setupMocks: func(mockSecretsSvc *MockSecureValueService, mockDecryptSvc *mocks.MockDecryptService) { |
||||
existingSecret := &secretv1beta1.SecureValue{ |
||||
ObjectMeta: metav1.ObjectMeta{ |
||||
Name: "existing-secret", |
||||
Namespace: "test-namespace", |
||||
}, |
||||
Spec: secretv1beta1.SecureValueSpec{ |
||||
Description: "provisioning: existing-secret", |
||||
Decrypters: []string{svcName}, |
||||
}, |
||||
} |
||||
|
||||
// Assert Read call with context validation
|
||||
mockSecretsSvc.EXPECT().Read( |
||||
mock.MatchedBy(func(ctx context.Context) bool { |
||||
requester, err := identity.GetRequester(ctx) |
||||
return err == nil && requester != nil && requester.GetUID() == ":test-uid" |
||||
}), |
||||
xkube.Namespace("test-namespace"), |
||||
"existing-secret", |
||||
).Return(existingSecret, nil) |
||||
|
||||
// Assert Update call with detailed validation
|
||||
mockSecretsSvc.EXPECT().Update( |
||||
mock.MatchedBy(func(ctx context.Context) bool { |
||||
requester, err := identity.GetRequester(ctx) |
||||
return err == nil && requester != nil && requester.GetUID() == ":test-uid" |
||||
}), |
||||
mock.MatchedBy(func(sv *secretv1beta1.SecureValue) bool { |
||||
if sv.Namespace != "test-namespace" || sv.Name != "existing-secret" { |
||||
return false |
||||
} |
||||
if sv.Spec.Value == nil { |
||||
return false |
||||
} |
||||
// Verify the updated secret value
|
||||
if sv.Spec.Value.DangerouslyExposeAndConsumeValue() != "new-secret-data" { |
||||
return false |
||||
} |
||||
return true |
||||
}), |
||||
":test-uid", |
||||
).Return(&secretv1beta1.SecureValue{ |
||||
ObjectMeta: metav1.ObjectMeta{ |
||||
Name: "existing-secret", |
||||
Namespace: "test-namespace", |
||||
}, |
||||
}, true, nil) |
||||
}, |
||||
expectedName: "existing-secret", |
||||
}, |
||||
{ |
||||
name: "error reading existing secret", |
||||
namespace: "test-namespace", |
||||
secretName: "test-secret", |
||||
data: "secret-data", |
||||
setupMocks: func(mockSecretsSvc *MockSecureValueService, mockDecryptSvc *mocks.MockDecryptService) { |
||||
mockSecretsSvc.EXPECT().Read( |
||||
mock.MatchedBy(func(ctx context.Context) bool { |
||||
requester, err := identity.GetRequester(ctx) |
||||
return err == nil && requester != nil && requester.GetUID() == ":test-uid" |
||||
}), |
||||
xkube.Namespace("test-namespace"), |
||||
"test-secret", |
||||
).Return(nil, errors.New("database error")) |
||||
}, |
||||
expectedError: "database error", |
||||
}, |
||||
{ |
||||
name: "error creating new secret", |
||||
namespace: "test-namespace", |
||||
secretName: "test-secret", |
||||
data: "secret-data", |
||||
setupMocks: func(mockSecretsSvc *MockSecureValueService, mockDecryptSvc *mocks.MockDecryptService) { |
||||
mockSecretsSvc.EXPECT().Read( |
||||
mock.MatchedBy(func(ctx context.Context) bool { |
||||
requester, err := identity.GetRequester(ctx) |
||||
return err == nil && requester != nil && requester.GetUID() == ":test-uid" |
||||
}), |
||||
xkube.Namespace("test-namespace"), |
||||
"test-secret", |
||||
).Return(nil, contracts.ErrSecureValueNotFound) |
||||
|
||||
mockSecretsSvc.EXPECT().Create( |
||||
mock.MatchedBy(func(ctx context.Context) bool { |
||||
requester, err := identity.GetRequester(ctx) |
||||
return err == nil && requester != nil && requester.GetUID() == ":test-uid" |
||||
}), |
||||
mock.MatchedBy(func(sv *secretv1beta1.SecureValue) bool { |
||||
return sv.Namespace == "test-namespace" && |
||||
sv.Name == "test-secret" && |
||||
sv.Spec.Value != nil && |
||||
sv.Spec.Value.DangerouslyExposeAndConsumeValue() == "secret-data" |
||||
}), |
||||
":test-uid", |
||||
).Return(nil, 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 *MockSecureValueService, mockDecryptSvc *mocks.MockDecryptService) { |
||||
existingSecret := &secretv1beta1.SecureValue{ |
||||
ObjectMeta: metav1.ObjectMeta{ |
||||
Name: "existing-secret", |
||||
Namespace: "test-namespace", |
||||
}, |
||||
Spec: secretv1beta1.SecureValueSpec{ |
||||
Description: "provisioning: existing-secret", |
||||
Decrypters: []string{svcName}, |
||||
}, |
||||
} |
||||
|
||||
mockSecretsSvc.EXPECT().Read( |
||||
mock.MatchedBy(func(ctx context.Context) bool { |
||||
requester, err := identity.GetRequester(ctx) |
||||
return err == nil && requester != nil && requester.GetUID() == ":test-uid" |
||||
}), |
||||
xkube.Namespace("test-namespace"), |
||||
"existing-secret", |
||||
).Return(existingSecret, nil) |
||||
|
||||
mockSecretsSvc.EXPECT().Update( |
||||
mock.MatchedBy(func(ctx context.Context) bool { |
||||
requester, err := identity.GetRequester(ctx) |
||||
return err == nil && requester != nil && requester.GetUID() == ":test-uid" |
||||
}), |
||||
mock.MatchedBy(func(sv *secretv1beta1.SecureValue) bool { |
||||
return sv.Namespace == "test-namespace" && |
||||
sv.Name == "existing-secret" && |
||||
sv.Spec.Value != nil && |
||||
sv.Spec.Value.DangerouslyExposeAndConsumeValue() == "new-secret-data" |
||||
}), |
||||
":test-uid", |
||||
).Return(nil, false, errors.New("update failed")) |
||||
}, |
||||
expectedError: "update failed", |
||||
}, |
||||
} |
||||
|
||||
for _, tt := range tests { |
||||
t.Run(tt.name, func(t *testing.T) { |
||||
mockSecretsSvc := NewMockSecureValueService(t) |
||||
mockDecryptSvc := &mocks.MockDecryptService{} |
||||
|
||||
tt.setupMocks(mockSecretsSvc, mockDecryptSvc) |
||||
|
||||
svc := NewSecretsService(mockSecretsSvc, mockDecryptSvc) |
||||
|
||||
ctx := context.Background() |
||||
ctx = identity.WithRequester(ctx, &identity.StaticRequester{ |
||||
UserUID: "test-uid", |
||||
}) |
||||
|
||||
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_NoIdentity(t *testing.T) { |
||||
mockSecretsSvc := NewMockSecureValueService(t) |
||||
mockDecryptSvc := &mocks.MockDecryptService{} |
||||
|
||||
svc := NewSecretsService(mockSecretsSvc, mockDecryptSvc) |
||||
|
||||
ctx := context.Background() |
||||
|
||||
result, err := svc.Encrypt(ctx, "test-namespace", "test-secret", "secret-data") |
||||
|
||||
assert.Error(t, err) |
||||
assert.Empty(t, result) |
||||
} |
||||
|
||||
func TestSecretsService_Decrypt(t *testing.T) { |
||||
tests := []struct { |
||||
name string |
||||
namespace string |
||||
secretName string |
||||
setupMocks func(*MockSecureValueService, *mocks.MockDecryptService) |
||||
expectedResult []byte |
||||
expectedError string |
||||
}{ |
||||
{ |
||||
name: "successfully decrypt secret", |
||||
namespace: "test-namespace", |
||||
secretName: "test-secret", |
||||
setupMocks: func(mockSecretsSvc *MockSecureValueService, mockDecryptSvc *mocks.MockDecryptService) { |
||||
exposedValue := secretv1beta1.NewExposedSecureValue("decrypted-data") |
||||
mockResult := service.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 |
||||
}), |
||||
"test-namespace", |
||||
"test-secret", |
||||
).Return(map[string]service.DecryptResult{ |
||||
"test-secret": mockResult, |
||||
}, nil) |
||||
}, |
||||
expectedResult: []byte("decrypted-data"), |
||||
}, |
||||
{ |
||||
name: "decrypt service error", |
||||
namespace: "test-namespace", |
||||
secretName: "test-secret", |
||||
setupMocks: func(mockSecretsSvc *MockSecureValueService, mockDecryptSvc *mocks.MockDecryptService) { |
||||
mockDecryptSvc.EXPECT().Decrypt( |
||||
mock.MatchedBy(func(ctx context.Context) bool { |
||||
return ctx != nil |
||||
}), |
||||
"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 *MockSecureValueService, mockDecryptSvc *mocks.MockDecryptService) { |
||||
mockDecryptSvc.EXPECT().Decrypt( |
||||
mock.MatchedBy(func(ctx context.Context) bool { |
||||
return ctx != nil |
||||
}), |
||||
"test-namespace", |
||||
"test-secret", |
||||
).Return(map[string]service.DecryptResult{}, nil) |
||||
}, |
||||
expectedError: contracts.ErrDecryptNotFound.Error(), |
||||
}, |
||||
{ |
||||
name: "decrypt result has error", |
||||
namespace: "test-namespace", |
||||
secretName: "test-secret", |
||||
setupMocks: func(mockSecretsSvc *MockSecureValueService, mockDecryptSvc *mocks.MockDecryptService) { |
||||
mockResult := service.NewDecryptResultErr(errors.New("decryption failed")) |
||||
|
||||
mockDecryptSvc.EXPECT().Decrypt( |
||||
mock.MatchedBy(func(ctx context.Context) bool { |
||||
return ctx != nil |
||||
}), |
||||
"test-namespace", |
||||
"test-secret", |
||||
).Return(map[string]service.DecryptResult{ |
||||
"test-secret": mockResult, |
||||
}, nil) |
||||
}, |
||||
expectedError: "decryption failed", |
||||
}, |
||||
} |
||||
|
||||
for _, tt := range tests { |
||||
t.Run(tt.name, func(t *testing.T) { |
||||
mockSecretsSvc := NewMockSecureValueService(t) |
||||
mockDecryptSvc := &mocks.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 StaticRequester
|
||||
func TestSecretsService_Decrypt_StaticRequesterCreation(t *testing.T) { |
||||
mockSecretsSvc := NewMockSecureValueService(t) |
||||
mockDecryptSvc := &mocks.MockDecryptService{} |
||||
|
||||
exposedValue := secretv1beta1.NewExposedSecureValue("test-data") |
||||
mockResult := service.NewDecryptResultValue(&exposedValue) |
||||
|
||||
// Create a more detailed context matcher to verify the StaticRequester 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 |
||||
}), |
||||
"test-namespace", |
||||
"test-secret", |
||||
).Return(map[string]service.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) |
||||
} |
@ -0,0 +1,226 @@ |
||||
// Code generated by mockery v2.52.4. DO NOT EDIT.
|
||||
|
||||
package secrets |
||||
|
||||
import ( |
||||
context "context" |
||||
|
||||
v1beta1 "github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1" |
||||
mock "github.com/stretchr/testify/mock" |
||||
|
||||
xkube "github.com/grafana/grafana/pkg/registry/apis/secret/xkube" |
||||
) |
||||
|
||||
// MockSecureValueService is an autogenerated mock type for the SecureValueService type
|
||||
type MockSecureValueService struct { |
||||
mock.Mock |
||||
} |
||||
|
||||
type MockSecureValueService_Expecter struct { |
||||
mock *mock.Mock |
||||
} |
||||
|
||||
func (_m *MockSecureValueService) EXPECT() *MockSecureValueService_Expecter { |
||||
return &MockSecureValueService_Expecter{mock: &_m.Mock} |
||||
} |
||||
|
||||
// Create provides a mock function with given fields: ctx, sv, actorUID
|
||||
func (_m *MockSecureValueService) Create(ctx context.Context, sv *v1beta1.SecureValue, actorUID string) (*v1beta1.SecureValue, error) { |
||||
ret := _m.Called(ctx, sv, actorUID) |
||||
|
||||
if len(ret) == 0 { |
||||
panic("no return value specified for Create") |
||||
} |
||||
|
||||
var r0 *v1beta1.SecureValue |
||||
var r1 error |
||||
if rf, ok := ret.Get(0).(func(context.Context, *v1beta1.SecureValue, string) (*v1beta1.SecureValue, error)); ok { |
||||
return rf(ctx, sv, actorUID) |
||||
} |
||||
if rf, ok := ret.Get(0).(func(context.Context, *v1beta1.SecureValue, string) *v1beta1.SecureValue); ok { |
||||
r0 = rf(ctx, sv, actorUID) |
||||
} else { |
||||
if ret.Get(0) != nil { |
||||
r0 = ret.Get(0).(*v1beta1.SecureValue) |
||||
} |
||||
} |
||||
|
||||
if rf, ok := ret.Get(1).(func(context.Context, *v1beta1.SecureValue, string) error); ok { |
||||
r1 = rf(ctx, sv, actorUID) |
||||
} else { |
||||
r1 = ret.Error(1) |
||||
} |
||||
|
||||
return r0, r1 |
||||
} |
||||
|
||||
// MockSecureValueService_Create_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Create'
|
||||
type MockSecureValueService_Create_Call struct { |
||||
*mock.Call |
||||
} |
||||
|
||||
// Create is a helper method to define mock.On call
|
||||
// - ctx context.Context
|
||||
// - sv *v1beta1.SecureValue
|
||||
// - actorUID string
|
||||
func (_e *MockSecureValueService_Expecter) Create(ctx interface{}, sv interface{}, actorUID interface{}) *MockSecureValueService_Create_Call { |
||||
return &MockSecureValueService_Create_Call{Call: _e.mock.On("Create", ctx, sv, actorUID)} |
||||
} |
||||
|
||||
func (_c *MockSecureValueService_Create_Call) Run(run func(ctx context.Context, sv *v1beta1.SecureValue, actorUID string)) *MockSecureValueService_Create_Call { |
||||
_c.Call.Run(func(args mock.Arguments) { |
||||
run(args[0].(context.Context), args[1].(*v1beta1.SecureValue), args[2].(string)) |
||||
}) |
||||
return _c |
||||
} |
||||
|
||||
func (_c *MockSecureValueService_Create_Call) Return(_a0 *v1beta1.SecureValue, _a1 error) *MockSecureValueService_Create_Call { |
||||
_c.Call.Return(_a0, _a1) |
||||
return _c |
||||
} |
||||
|
||||
func (_c *MockSecureValueService_Create_Call) RunAndReturn(run func(context.Context, *v1beta1.SecureValue, string) (*v1beta1.SecureValue, error)) *MockSecureValueService_Create_Call { |
||||
_c.Call.Return(run) |
||||
return _c |
||||
} |
||||
|
||||
// Read provides a mock function with given fields: ctx, namespace, name
|
||||
func (_m *MockSecureValueService) Read(ctx context.Context, namespace xkube.Namespace, name string) (*v1beta1.SecureValue, error) { |
||||
ret := _m.Called(ctx, namespace, name) |
||||
|
||||
if len(ret) == 0 { |
||||
panic("no return value specified for Read") |
||||
} |
||||
|
||||
var r0 *v1beta1.SecureValue |
||||
var r1 error |
||||
if rf, ok := ret.Get(0).(func(context.Context, xkube.Namespace, string) (*v1beta1.SecureValue, error)); ok { |
||||
return rf(ctx, namespace, name) |
||||
} |
||||
if rf, ok := ret.Get(0).(func(context.Context, xkube.Namespace, string) *v1beta1.SecureValue); ok { |
||||
r0 = rf(ctx, namespace, name) |
||||
} else { |
||||
if ret.Get(0) != nil { |
||||
r0 = ret.Get(0).(*v1beta1.SecureValue) |
||||
} |
||||
} |
||||
|
||||
if rf, ok := ret.Get(1).(func(context.Context, xkube.Namespace, string) error); ok { |
||||
r1 = rf(ctx, namespace, name) |
||||
} else { |
||||
r1 = ret.Error(1) |
||||
} |
||||
|
||||
return r0, r1 |
||||
} |
||||
|
||||
// MockSecureValueService_Read_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Read'
|
||||
type MockSecureValueService_Read_Call struct { |
||||
*mock.Call |
||||
} |
||||
|
||||
// Read is a helper method to define mock.On call
|
||||
// - ctx context.Context
|
||||
// - namespace xkube.Namespace
|
||||
// - name string
|
||||
func (_e *MockSecureValueService_Expecter) Read(ctx interface{}, namespace interface{}, name interface{}) *MockSecureValueService_Read_Call { |
||||
return &MockSecureValueService_Read_Call{Call: _e.mock.On("Read", ctx, namespace, name)} |
||||
} |
||||
|
||||
func (_c *MockSecureValueService_Read_Call) Run(run func(ctx context.Context, namespace xkube.Namespace, name string)) *MockSecureValueService_Read_Call { |
||||
_c.Call.Run(func(args mock.Arguments) { |
||||
run(args[0].(context.Context), args[1].(xkube.Namespace), args[2].(string)) |
||||
}) |
||||
return _c |
||||
} |
||||
|
||||
func (_c *MockSecureValueService_Read_Call) Return(_a0 *v1beta1.SecureValue, _a1 error) *MockSecureValueService_Read_Call { |
||||
_c.Call.Return(_a0, _a1) |
||||
return _c |
||||
} |
||||
|
||||
func (_c *MockSecureValueService_Read_Call) RunAndReturn(run func(context.Context, xkube.Namespace, string) (*v1beta1.SecureValue, error)) *MockSecureValueService_Read_Call { |
||||
_c.Call.Return(run) |
||||
return _c |
||||
} |
||||
|
||||
// Update provides a mock function with given fields: ctx, newSecureValue, actorUID
|
||||
func (_m *MockSecureValueService) Update(ctx context.Context, newSecureValue *v1beta1.SecureValue, actorUID string) (*v1beta1.SecureValue, bool, error) { |
||||
ret := _m.Called(ctx, newSecureValue, actorUID) |
||||
|
||||
if len(ret) == 0 { |
||||
panic("no return value specified for Update") |
||||
} |
||||
|
||||
var r0 *v1beta1.SecureValue |
||||
var r1 bool |
||||
var r2 error |
||||
if rf, ok := ret.Get(0).(func(context.Context, *v1beta1.SecureValue, string) (*v1beta1.SecureValue, bool, error)); ok { |
||||
return rf(ctx, newSecureValue, actorUID) |
||||
} |
||||
if rf, ok := ret.Get(0).(func(context.Context, *v1beta1.SecureValue, string) *v1beta1.SecureValue); ok { |
||||
r0 = rf(ctx, newSecureValue, actorUID) |
||||
} else { |
||||
if ret.Get(0) != nil { |
||||
r0 = ret.Get(0).(*v1beta1.SecureValue) |
||||
} |
||||
} |
||||
|
||||
if rf, ok := ret.Get(1).(func(context.Context, *v1beta1.SecureValue, string) bool); ok { |
||||
r1 = rf(ctx, newSecureValue, actorUID) |
||||
} else { |
||||
r1 = ret.Get(1).(bool) |
||||
} |
||||
|
||||
if rf, ok := ret.Get(2).(func(context.Context, *v1beta1.SecureValue, string) error); ok { |
||||
r2 = rf(ctx, newSecureValue, actorUID) |
||||
} else { |
||||
r2 = ret.Error(2) |
||||
} |
||||
|
||||
return r0, r1, r2 |
||||
} |
||||
|
||||
// MockSecureValueService_Update_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Update'
|
||||
type MockSecureValueService_Update_Call struct { |
||||
*mock.Call |
||||
} |
||||
|
||||
// Update is a helper method to define mock.On call
|
||||
// - ctx context.Context
|
||||
// - newSecureValue *v1beta1.SecureValue
|
||||
// - actorUID string
|
||||
func (_e *MockSecureValueService_Expecter) Update(ctx interface{}, newSecureValue interface{}, actorUID interface{}) *MockSecureValueService_Update_Call { |
||||
return &MockSecureValueService_Update_Call{Call: _e.mock.On("Update", ctx, newSecureValue, actorUID)} |
||||
} |
||||
|
||||
func (_c *MockSecureValueService_Update_Call) Run(run func(ctx context.Context, newSecureValue *v1beta1.SecureValue, actorUID string)) *MockSecureValueService_Update_Call { |
||||
_c.Call.Run(func(args mock.Arguments) { |
||||
run(args[0].(context.Context), args[1].(*v1beta1.SecureValue), args[2].(string)) |
||||
}) |
||||
return _c |
||||
} |
||||
|
||||
func (_c *MockSecureValueService_Update_Call) Return(_a0 *v1beta1.SecureValue, _a1 bool, _a2 error) *MockSecureValueService_Update_Call { |
||||
_c.Call.Return(_a0, _a1, _a2) |
||||
return _c |
||||
} |
||||
|
||||
func (_c *MockSecureValueService_Update_Call) RunAndReturn(run func(context.Context, *v1beta1.SecureValue, string) (*v1beta1.SecureValue, bool, error)) *MockSecureValueService_Update_Call { |
||||
_c.Call.Return(run) |
||||
return _c |
||||
} |
||||
|
||||
// NewMockSecureValueService creates a new instance of MockSecureValueService. 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 NewMockSecureValueService(t interface { |
||||
mock.TestingT |
||||
Cleanup(func()) |
||||
}) *MockSecureValueService { |
||||
mock := &MockSecureValueService{} |
||||
mock.Mock.Test(t) |
||||
|
||||
t.Cleanup(func() { mock.AssertExpectations(t) }) |
||||
|
||||
return mock |
||||
} |
File diff suppressed because one or more lines are too long
@ -0,0 +1,177 @@ |
||||
// Code generated by mockery v2.52.4. DO NOT EDIT.
|
||||
|
||||
package featuremgmt |
||||
|
||||
import ( |
||||
context "context" |
||||
|
||||
mock "github.com/stretchr/testify/mock" |
||||
) |
||||
|
||||
// MockFeatureToggles is an autogenerated mock type for the FeatureToggles type
|
||||
type MockFeatureToggles struct { |
||||
mock.Mock |
||||
} |
||||
|
||||
type MockFeatureToggles_Expecter struct { |
||||
mock *mock.Mock |
||||
} |
||||
|
||||
func (_m *MockFeatureToggles) EXPECT() *MockFeatureToggles_Expecter { |
||||
return &MockFeatureToggles_Expecter{mock: &_m.Mock} |
||||
} |
||||
|
||||
// GetEnabled provides a mock function with given fields: ctx
|
||||
func (_m *MockFeatureToggles) GetEnabled(ctx context.Context) map[string]bool { |
||||
ret := _m.Called(ctx) |
||||
|
||||
if len(ret) == 0 { |
||||
panic("no return value specified for GetEnabled") |
||||
} |
||||
|
||||
var r0 map[string]bool |
||||
if rf, ok := ret.Get(0).(func(context.Context) map[string]bool); ok { |
||||
r0 = rf(ctx) |
||||
} else { |
||||
if ret.Get(0) != nil { |
||||
r0 = ret.Get(0).(map[string]bool) |
||||
} |
||||
} |
||||
|
||||
return r0 |
||||
} |
||||
|
||||
// MockFeatureToggles_GetEnabled_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetEnabled'
|
||||
type MockFeatureToggles_GetEnabled_Call struct { |
||||
*mock.Call |
||||
} |
||||
|
||||
// GetEnabled is a helper method to define mock.On call
|
||||
// - ctx context.Context
|
||||
func (_e *MockFeatureToggles_Expecter) GetEnabled(ctx interface{}) *MockFeatureToggles_GetEnabled_Call { |
||||
return &MockFeatureToggles_GetEnabled_Call{Call: _e.mock.On("GetEnabled", ctx)} |
||||
} |
||||
|
||||
func (_c *MockFeatureToggles_GetEnabled_Call) Run(run func(ctx context.Context)) *MockFeatureToggles_GetEnabled_Call { |
||||
_c.Call.Run(func(args mock.Arguments) { |
||||
run(args[0].(context.Context)) |
||||
}) |
||||
return _c |
||||
} |
||||
|
||||
func (_c *MockFeatureToggles_GetEnabled_Call) Return(_a0 map[string]bool) *MockFeatureToggles_GetEnabled_Call { |
||||
_c.Call.Return(_a0) |
||||
return _c |
||||
} |
||||
|
||||
func (_c *MockFeatureToggles_GetEnabled_Call) RunAndReturn(run func(context.Context) map[string]bool) *MockFeatureToggles_GetEnabled_Call { |
||||
_c.Call.Return(run) |
||||
return _c |
||||
} |
||||
|
||||
// IsEnabled provides a mock function with given fields: ctx, flag
|
||||
func (_m *MockFeatureToggles) IsEnabled(ctx context.Context, flag string) bool { |
||||
ret := _m.Called(ctx, flag) |
||||
|
||||
if len(ret) == 0 { |
||||
panic("no return value specified for IsEnabled") |
||||
} |
||||
|
||||
var r0 bool |
||||
if rf, ok := ret.Get(0).(func(context.Context, string) bool); ok { |
||||
r0 = rf(ctx, flag) |
||||
} else { |
||||
r0 = ret.Get(0).(bool) |
||||
} |
||||
|
||||
return r0 |
||||
} |
||||
|
||||
// MockFeatureToggles_IsEnabled_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'IsEnabled'
|
||||
type MockFeatureToggles_IsEnabled_Call struct { |
||||
*mock.Call |
||||
} |
||||
|
||||
// IsEnabled is a helper method to define mock.On call
|
||||
// - ctx context.Context
|
||||
// - flag string
|
||||
func (_e *MockFeatureToggles_Expecter) IsEnabled(ctx interface{}, flag interface{}) *MockFeatureToggles_IsEnabled_Call { |
||||
return &MockFeatureToggles_IsEnabled_Call{Call: _e.mock.On("IsEnabled", ctx, flag)} |
||||
} |
||||
|
||||
func (_c *MockFeatureToggles_IsEnabled_Call) Run(run func(ctx context.Context, flag string)) *MockFeatureToggles_IsEnabled_Call { |
||||
_c.Call.Run(func(args mock.Arguments) { |
||||
run(args[0].(context.Context), args[1].(string)) |
||||
}) |
||||
return _c |
||||
} |
||||
|
||||
func (_c *MockFeatureToggles_IsEnabled_Call) Return(_a0 bool) *MockFeatureToggles_IsEnabled_Call { |
||||
_c.Call.Return(_a0) |
||||
return _c |
||||
} |
||||
|
||||
func (_c *MockFeatureToggles_IsEnabled_Call) RunAndReturn(run func(context.Context, string) bool) *MockFeatureToggles_IsEnabled_Call { |
||||
_c.Call.Return(run) |
||||
return _c |
||||
} |
||||
|
||||
// IsEnabledGlobally provides a mock function with given fields: flag
|
||||
func (_m *MockFeatureToggles) IsEnabledGlobally(flag string) bool { |
||||
ret := _m.Called(flag) |
||||
|
||||
if len(ret) == 0 { |
||||
panic("no return value specified for IsEnabledGlobally") |
||||
} |
||||
|
||||
var r0 bool |
||||
if rf, ok := ret.Get(0).(func(string) bool); ok { |
||||
r0 = rf(flag) |
||||
} else { |
||||
r0 = ret.Get(0).(bool) |
||||
} |
||||
|
||||
return r0 |
||||
} |
||||
|
||||
// MockFeatureToggles_IsEnabledGlobally_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'IsEnabledGlobally'
|
||||
type MockFeatureToggles_IsEnabledGlobally_Call struct { |
||||
*mock.Call |
||||
} |
||||
|
||||
// IsEnabledGlobally is a helper method to define mock.On call
|
||||
// - flag string
|
||||
func (_e *MockFeatureToggles_Expecter) IsEnabledGlobally(flag interface{}) *MockFeatureToggles_IsEnabledGlobally_Call { |
||||
return &MockFeatureToggles_IsEnabledGlobally_Call{Call: _e.mock.On("IsEnabledGlobally", flag)} |
||||
} |
||||
|
||||
func (_c *MockFeatureToggles_IsEnabledGlobally_Call) Run(run func(flag string)) *MockFeatureToggles_IsEnabledGlobally_Call { |
||||
_c.Call.Run(func(args mock.Arguments) { |
||||
run(args[0].(string)) |
||||
}) |
||||
return _c |
||||
} |
||||
|
||||
func (_c *MockFeatureToggles_IsEnabledGlobally_Call) Return(_a0 bool) *MockFeatureToggles_IsEnabledGlobally_Call { |
||||
_c.Call.Return(_a0) |
||||
return _c |
||||
} |
||||
|
||||
func (_c *MockFeatureToggles_IsEnabledGlobally_Call) RunAndReturn(run func(string) bool) *MockFeatureToggles_IsEnabledGlobally_Call { |
||||
_c.Call.Return(run) |
||||
return _c |
||||
} |
||||
|
||||
// NewMockFeatureToggles creates a new instance of MockFeatureToggles. 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 NewMockFeatureToggles(t interface { |
||||
mock.TestingT |
||||
Cleanup(func()) |
||||
}) *MockFeatureToggles { |
||||
mock := &MockFeatureToggles{} |
||||
mock.Mock.Test(t) |
||||
|
||||
t.Cleanup(func() { mock.AssertExpectations(t) }) |
||||
|
||||
return mock |
||||
} |
|
@ -0,0 +1,447 @@ |
||||
package provisioning |
||||
|
||||
import ( |
||||
"context" |
||||
"encoding/base64" |
||||
"fmt" |
||||
"strings" |
||||
"testing" |
||||
|
||||
provisioning "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1" |
||||
"github.com/grafana/grafana/pkg/registry/apis/provisioning/secrets" |
||||
"github.com/stretchr/testify/require" |
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" |
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" |
||||
) |
||||
|
||||
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") |
||||
|
||||
// Move encrypted token mutation
|
||||
for _, expectedField := range test.expectedFields { |
||||
value, decrypted := encryptedField(t, secretsService, nil, 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") |
||||
|
||||
for _, expectedField := range test.expectedFields { |
||||
value, decrypted := encryptedField(t, secretsService, nil, 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) { |
||||
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 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 |
||||
} |
@ -0,0 +1,24 @@ |
||||
{ |
||||
"apiVersion": "provisioning.grafana.app/v0alpha1", |
||||
"kind": "Repository", |
||||
"metadata": { |
||||
"name": "{{ or .Name "git-repository" }}" |
||||
}, |
||||
"spec": { |
||||
"title": "{{ or .Title .Name "Git repository" }}", |
||||
"description": "{{ or .Description .Name "Load grafana dashboard from fake repository" }}", |
||||
"type": "git", |
||||
"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": { |
||||
"enabled": {{ if .SyncEnabled }} true {{ else }} false {{ end }}, |
||||
"target": "{{ or .SyncTarget "folder" }}", |
||||
"intervalSeconds": {{ or .SyncIntervalSeconds 60 }} |
||||
}, |
||||
"workflows": [] |
||||
} |
||||
} |
Loading…
Reference in new issue