Chore: Return correct error for name taken and validation error on add/update datasource (#70465)

This commit is contained in:
Kousik Mitra
2023-07-17 16:27:19 +02:00
committed by GitHub
parent 0ffd359801
commit 60496fbae3
5 changed files with 243 additions and 7 deletions
+7 -1
View File
@@ -1,6 +1,10 @@
package datasources
import "errors"
import (
"errors"
"github.com/grafana/grafana/pkg/util/errutil"
)
var (
ErrDataSourceNotFound = errors.New("data source not found")
@@ -11,4 +15,6 @@ var (
ErrDataSourceFailedGenerateUniqueUid = errors.New("failed to generate unique datasource ID")
ErrDataSourceIdentifierNotSet = errors.New("unique identifier and org id are needed to be able to get or delete a datasource")
ErrDatasourceIsReadOnly = errors.New("data source is readonly, can only be updated from configuration")
ErrDataSourceNameInvalid = errutil.NewBase(errutil.StatusValidationFailed, "datasource.nameInvalid", errutil.WithPublicMessage("Invalid datasource name."))
ErrDataSourceURLInvalid = errutil.NewBase(errutil.StatusValidationFailed, "datasource.urlInvalid", errutil.WithPublicMessage("Invalid datasource url."))
)
@@ -4,6 +4,7 @@ import (
"context"
"crypto/tls"
"encoding/json"
"errors"
"fmt"
"net/http"
"strconv"
@@ -26,6 +27,11 @@ import (
"github.com/grafana/grafana/pkg/setting"
)
const (
maxDatasourceNameLen = 190
maxDatasourceUrlLen = 255
)
type Service struct {
SQLStore Store
SecretsStore kvstore.SecretsKVStore
@@ -172,6 +178,10 @@ func (s *Service) GetDataSourcesByType(ctx context.Context, query *datasources.G
func (s *Service) AddDataSource(ctx context.Context, cmd *datasources.AddDataSourceCommand) (*datasources.DataSource, error) {
var dataSource *datasources.DataSource
if err := validateFields(cmd.Name, cmd.URL); err != nil {
return dataSource, err
}
return dataSource, s.db.InTransaction(ctx, func(ctx context.Context) error {
var err error
@@ -231,6 +241,11 @@ func (s *Service) DeleteDataSource(ctx context.Context, cmd *datasources.DeleteD
func (s *Service) UpdateDataSource(ctx context.Context, cmd *datasources.UpdateDataSourceCommand) (*datasources.DataSource, error) {
var dataSource *datasources.DataSource
if err := validateFields(cmd.Name, cmd.URL); err != nil {
return dataSource, err
}
return dataSource, s.db.InTransaction(ctx, func(ctx context.Context) error {
var err error
@@ -243,6 +258,21 @@ func (s *Service) UpdateDataSource(ctx context.Context, cmd *datasources.UpdateD
return err
}
if cmd.Name != "" && cmd.Name != dataSource.Name {
query := &datasources.GetDataSourceQuery{
Name: cmd.Name,
OrgID: cmd.OrgID,
}
exist, err := s.SQLStore.GetDataSource(ctx, query)
if exist != nil {
return datasources.ErrDataSourceNameExists
}
if err != nil && !errors.Is(err, datasources.ErrDataSourceNotFound) {
return err
}
}
err = s.fillWithSecureJSONData(ctx, cmd, dataSource)
if err != nil {
return err
@@ -631,6 +661,18 @@ func (s *Service) fillWithSecureJSONData(ctx context.Context, cmd *datasources.U
return nil
}
func validateFields(name, url string) error {
if len(name) > maxDatasourceNameLen {
return datasources.ErrDataSourceNameInvalid.Errorf("max length is %d", maxDatasourceNameLen)
}
if len(url) > maxDatasourceUrlLen {
return datasources.ErrDataSourceURLInvalid.Errorf("max length is %d", maxDatasourceUrlLen)
}
return nil
}
func readQuotaConfig(cfg *setting.Cfg) (*quota.Map, error) {
limits := &quota.Map{}
@@ -9,8 +9,10 @@ import (
"testing"
"time"
"github.com/google/uuid"
sdkhttpclient "github.com/grafana/grafana-plugin-sdk-go/backend/httpclient"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/components/simplejson"
@@ -18,6 +20,7 @@ import (
"github.com/grafana/grafana/pkg/infra/httpclient"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/accesscontrol/actest"
acmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock"
"github.com/grafana/grafana/pkg/services/datasources"
"github.com/grafana/grafana/pkg/services/featuremgmt"
@@ -45,6 +48,147 @@ func (d *dataSourceMockRetriever) GetDataSource(ctx context.Context, query *data
return nil, datasources.ErrDataSourceNotFound
}
func TestService_AddDataSource(t *testing.T) {
cfg := &setting.Cfg{}
t.Run("should return validation error if command validation failed", func(t *testing.T) {
sqlStore := db.InitTestDB(t)
secretsService := secretsmng.SetupTestService(t, fakes.NewFakeSecretsStore())
secretsStore := secretskvs.NewSQLSecretsKVStore(sqlStore, secretsService, log.New("test.logger"))
quotaService := quotatest.New(false, nil)
mockPermission := acmock.NewMockedPermissionsService()
dsService, err := ProvideService(sqlStore, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), actest.FakeAccessControl{}, mockPermission, quotaService)
require.NoError(t, err)
cmd := &datasources.AddDataSourceCommand{
OrgID: 1,
Name: string(make([]byte, 256)),
}
_, err = dsService.AddDataSource(context.Background(), cmd)
require.EqualError(t, err, "[datasource.nameInvalid] max length is 190")
cmd = &datasources.AddDataSourceCommand{
OrgID: 1,
URL: string(make([]byte, 256)),
}
_, err = dsService.AddDataSource(context.Background(), cmd)
require.EqualError(t, err, "[datasource.urlInvalid] max length is 255")
})
}
func TestService_UpdateDataSource(t *testing.T) {
cfg := &setting.Cfg{}
t.Run("should return not found error if datasource not found", func(t *testing.T) {
sqlStore := db.InitTestDB(t)
secretsService := secretsmng.SetupTestService(t, fakes.NewFakeSecretsStore())
secretsStore := secretskvs.NewSQLSecretsKVStore(sqlStore, secretsService, log.New("test.logger"))
quotaService := quotatest.New(false, nil)
mockPermission := acmock.NewMockedPermissionsService()
dsService, err := ProvideService(sqlStore, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), actest.FakeAccessControl{}, mockPermission, quotaService)
require.NoError(t, err)
cmd := &datasources.UpdateDataSourceCommand{
UID: uuid.New().String(),
ID: 1,
OrgID: 1,
}
_, err = dsService.UpdateDataSource(context.Background(), cmd)
require.ErrorIs(t, err, datasources.ErrDataSourceNotFound)
})
t.Run("should return validation error if command validation failed", func(t *testing.T) {
sqlStore := db.InitTestDB(t)
secretsService := secretsmng.SetupTestService(t, fakes.NewFakeSecretsStore())
secretsStore := secretskvs.NewSQLSecretsKVStore(sqlStore, secretsService, log.New("test.logger"))
quotaService := quotatest.New(false, nil)
mockPermission := acmock.NewMockedPermissionsService()
dsService, err := ProvideService(sqlStore, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), actest.FakeAccessControl{}, mockPermission, quotaService)
require.NoError(t, err)
cmd := &datasources.UpdateDataSourceCommand{
ID: 1,
OrgID: 1,
Name: string(make([]byte, 256)),
}
_, err = dsService.UpdateDataSource(context.Background(), cmd)
require.EqualError(t, err, "[datasource.nameInvalid] max length is 190")
cmd = &datasources.UpdateDataSourceCommand{
ID: 1,
OrgID: 1,
URL: string(make([]byte, 256)),
}
_, err = dsService.UpdateDataSource(context.Background(), cmd)
require.EqualError(t, err, "[datasource.urlInvalid] max length is 255")
})
t.Run("should return no error if updated datasource", func(t *testing.T) {
sqlStore := db.InitTestDB(t)
secretsService := secretsmng.SetupTestService(t, fakes.NewFakeSecretsStore())
secretsStore := secretskvs.NewSQLSecretsKVStore(sqlStore, secretsService, log.New("test.logger"))
quotaService := quotatest.New(false, nil)
mockPermission := acmock.NewMockedPermissionsService()
dsService, err := ProvideService(sqlStore, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), actest.FakeAccessControl{}, mockPermission, quotaService)
require.NoError(t, err)
mockPermission.On("SetPermissions", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return([]accesscontrol.ResourcePermission{}, nil)
ds, err := dsService.AddDataSource(context.Background(), &datasources.AddDataSourceCommand{
OrgID: 1,
Name: "test-datasource",
})
require.NoError(t, err)
cmd := &datasources.UpdateDataSourceCommand{
ID: ds.ID,
OrgID: ds.OrgID,
Name: "test-datasource-updated",
}
_, err = dsService.UpdateDataSource(context.Background(), cmd)
require.NoError(t, err)
})
t.Run("should return error if datasource with same name exist", func(t *testing.T) {
sqlStore := db.InitTestDB(t)
secretsService := secretsmng.SetupTestService(t, fakes.NewFakeSecretsStore())
secretsStore := secretskvs.NewSQLSecretsKVStore(sqlStore, secretsService, log.New("test.logger"))
quotaService := quotatest.New(false, nil)
mockPermission := acmock.NewMockedPermissionsService()
dsService, err := ProvideService(sqlStore, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), actest.FakeAccessControl{}, mockPermission, quotaService)
require.NoError(t, err)
mockPermission.On("SetPermissions", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return([]accesscontrol.ResourcePermission{}, nil)
dsToUpdate, err := dsService.AddDataSource(context.Background(), &datasources.AddDataSourceCommand{
OrgID: 1,
Name: "test-datasource",
})
require.NoError(t, err)
existingDs, err := dsService.AddDataSource(context.Background(), &datasources.AddDataSourceCommand{
OrgID: 1,
Name: "name already taken",
})
require.NoError(t, err)
cmd := &datasources.UpdateDataSourceCommand{
ID: dsToUpdate.ID,
OrgID: dsToUpdate.OrgID,
Name: existingDs.Name,
}
_, err = dsService.UpdateDataSource(context.Background(), cmd)
require.ErrorIs(t, err, datasources.ErrDataSourceNameExists)
})
}
func TestService_NameScopeResolver(t *testing.T) {
retriever := &dataSourceMockRetriever{[]*datasources.DataSource{
{Name: "test-datasource", UID: "1"},