mirror of
https://github.com/SigNoz/signoz.git
synced 2026-03-17 10:22:11 +00:00
Compare commits
29 Commits
feat/fix-u
...
refactor/c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0601c28782 | ||
|
|
580610dbfa | ||
|
|
2d2aa02a81 | ||
|
|
dd9723ad13 | ||
|
|
3651469416 | ||
|
|
febce75734 | ||
|
|
e1616f3487 | ||
|
|
4b94287ac7 | ||
|
|
1575c7c54c | ||
|
|
8def3f835b | ||
|
|
11ed15f4c5 | ||
|
|
f47877cca9 | ||
|
|
bb2b9215ba | ||
|
|
3111904223 | ||
|
|
003e2c30d8 | ||
|
|
00fe516d10 | ||
|
|
0305f4f7db | ||
|
|
c60019a6dc | ||
|
|
acde2a37fa | ||
|
|
945241a52a | ||
|
|
e967f80c86 | ||
|
|
a09dc325de | ||
|
|
379b4f7fc4 | ||
|
|
5e536ae077 | ||
|
|
234585e642 | ||
|
|
2cc14f1ad4 | ||
|
|
dc4ed4d239 | ||
|
|
7281c36873 | ||
|
|
40288776e8 |
82
pkg/modules/cloudintegration/cloudintegration.go
Normal file
82
pkg/modules/cloudintegration/cloudintegration.go
Normal file
@@ -0,0 +1,82 @@
|
||||
package cloudintegration
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/types/cloudintegrationtypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/dashboardtypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
)
|
||||
|
||||
type Module interface {
|
||||
// CreateConnectionArtifact generates cloud provider specific connection information,
|
||||
// client side handles how this information is shown
|
||||
CreateConnectionArtifact(
|
||||
ctx context.Context,
|
||||
orgID valuer.UUID,
|
||||
provider cloudintegrationtypes.CloudProviderType,
|
||||
request *cloudintegrationtypes.ConnectionArtifactRequest,
|
||||
) (*cloudintegrationtypes.ConnectionArtifact, error)
|
||||
|
||||
// GetAccountStatus returns agent connection status for a cloud integration account
|
||||
GetAccountStatus(ctx context.Context, orgID, accountID valuer.UUID) (*cloudintegrationtypes.AccountStatus, error)
|
||||
|
||||
// ListConnectedAccounts lists accounts where agent is connected
|
||||
ListConnectedAccounts(ctx context.Context, orgID valuer.UUID) (*cloudintegrationtypes.ConnectedAccounts, error)
|
||||
|
||||
// DisconnectAccount soft deletes/removes a cloud integration account.
|
||||
DisconnectAccount(ctx context.Context, orgID, accountID valuer.UUID) error
|
||||
|
||||
// UpdateAccountConfig updates the configuration of an existing cloud account for a specific organization.
|
||||
UpdateAccountConfig(
|
||||
ctx context.Context,
|
||||
orgID,
|
||||
accountID valuer.UUID,
|
||||
config *cloudintegrationtypes.UpdateAccountConfigRequest,
|
||||
) (*cloudintegrationtypes.Account, error)
|
||||
|
||||
// ListServicesMetadata returns list of services metadata for a cloud provider attached with the integrationID.
|
||||
// This just returns a summary of the service and not the whole service definition
|
||||
ListServicesMetadata(ctx context.Context, orgID valuer.UUID, integrationID *valuer.UUID) (*cloudintegrationtypes.ServicesMetadata, error)
|
||||
|
||||
// GetService returns service definition details for a serviceID. This returns config and
|
||||
// other details required to show in service details page on web client.
|
||||
GetService(ctx context.Context, orgID valuer.UUID, integrationID *valuer.UUID, serviceID string) (*cloudintegrationtypes.Service, error)
|
||||
|
||||
// UpdateServiceConfig updates cloud integration service config
|
||||
UpdateServiceConfig(
|
||||
ctx context.Context,
|
||||
orgID valuer.UUID,
|
||||
serviceID string,
|
||||
config *cloudintegrationtypes.UpdateServiceConfigRequest,
|
||||
) (*cloudintegrationtypes.UpdateServiceConfigResponse, error)
|
||||
|
||||
// AgentCheckIn is called by agent to heartbeat and get latest config in response.
|
||||
AgentCheckIn(
|
||||
ctx context.Context,
|
||||
orgID valuer.UUID,
|
||||
req *cloudintegrationtypes.AgentCheckInRequest,
|
||||
) (*cloudintegrationtypes.AgentCheckInResponse, error)
|
||||
|
||||
// GetDashboardByID returns dashboard JSON for a given dashboard id.
|
||||
// this only returns the dashboard when the service (embedded in dashboard id) is enabled
|
||||
// in the org for any cloud integration account
|
||||
GetDashboardByID(ctx context.Context, orgID valuer.UUID, id string) (*dashboardtypes.Dashboard, error)
|
||||
|
||||
// GetAllDashboards returns list of dashboards across all connected cloud integration accounts
|
||||
// for enabled services in the org. This list gets added to dashboard list page
|
||||
GetAllDashboards(ctx context.Context, orgID valuer.UUID) ([]*dashboardtypes.Dashboard, error)
|
||||
}
|
||||
|
||||
type Handler interface {
|
||||
AgentCheckIn(http.ResponseWriter, *http.Request)
|
||||
GenerateConnectionArtifact(http.ResponseWriter, *http.Request)
|
||||
ListConnectedAccounts(http.ResponseWriter, *http.Request)
|
||||
GetAccountStatus(http.ResponseWriter, *http.Request)
|
||||
ListServices(http.ResponseWriter, *http.Request)
|
||||
GetServiceDetails(http.ResponseWriter, *http.Request)
|
||||
UpdateAccountConfig(http.ResponseWriter, *http.Request)
|
||||
UpdateServiceConfig(http.ResponseWriter, *http.Request)
|
||||
DisconnectAccount(http.ResponseWriter, *http.Request)
|
||||
}
|
||||
152
pkg/modules/cloudintegration/implcloudintegration/store.go
Normal file
152
pkg/modules/cloudintegration/implcloudintegration/store.go
Normal file
@@ -0,0 +1,152 @@
|
||||
package implcloudintegration
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/sqlstore"
|
||||
"github.com/SigNoz/signoz/pkg/types/cloudintegrationtypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
)
|
||||
|
||||
type store struct {
|
||||
store sqlstore.SQLStore
|
||||
}
|
||||
|
||||
func NewStore(sqlStore sqlstore.SQLStore) cloudintegrationtypes.Store {
|
||||
return &store{store: sqlStore}
|
||||
}
|
||||
|
||||
func (s *store) GetAccountByID(ctx context.Context, orgID, id valuer.UUID, provider cloudintegrationtypes.CloudProviderType) (*cloudintegrationtypes.StorableCloudIntegration, error) {
|
||||
account := new(cloudintegrationtypes.StorableCloudIntegration)
|
||||
err := s.store.BunDB().NewSelect().Model(account).
|
||||
Where("id = ?", id).
|
||||
Where("org_id = ?", orgID).
|
||||
Where("provider = ?", provider).
|
||||
Scan(ctx)
|
||||
if err != nil {
|
||||
return nil, s.store.WrapNotFoundErrf(err, cloudintegrationtypes.ErrCodeCloudIntegrationNotFound, "cloud integration account with id %s not found", id)
|
||||
}
|
||||
return account, nil
|
||||
}
|
||||
|
||||
func (s *store) CreateAccount(ctx context.Context, orgID valuer.UUID, account *cloudintegrationtypes.StorableCloudIntegration) (*cloudintegrationtypes.StorableCloudIntegration, error) {
|
||||
now := time.Now()
|
||||
if account.ID.IsZero() {
|
||||
account.ID = valuer.GenerateUUID()
|
||||
}
|
||||
account.OrgID = orgID
|
||||
account.CreatedAt = now
|
||||
account.UpdatedAt = now
|
||||
|
||||
_, err := s.store.BunDBCtx(ctx).NewInsert().Model(account).Exec(ctx)
|
||||
if err != nil {
|
||||
return nil, s.store.WrapAlreadyExistsErrf(err, errors.CodeAlreadyExists, "cloud integration account with id %s already exists", account.ID)
|
||||
}
|
||||
|
||||
return account, nil
|
||||
}
|
||||
|
||||
func (s *store) UpdateAccount(ctx context.Context, account *cloudintegrationtypes.StorableCloudIntegration) error {
|
||||
account.UpdatedAt = time.Now()
|
||||
_, err := s.store.BunDBCtx(ctx).NewUpdate().Model(account).
|
||||
Where("id = ?", account.ID).
|
||||
Where("org_id = ?", account.OrgID).
|
||||
Where("provider = ?", account.Provider).
|
||||
Exec(ctx)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *store) RemoveAccount(ctx context.Context, orgID, id valuer.UUID, provider cloudintegrationtypes.CloudProviderType) error {
|
||||
_, err := s.store.BunDBCtx(ctx).NewUpdate().Model((*cloudintegrationtypes.StorableCloudIntegration)(nil)).
|
||||
Set("removed_at = ?", time.Now()).
|
||||
Where("id = ?", id).
|
||||
Where("org_id = ?", orgID).
|
||||
Where("provider = ?", provider).
|
||||
Exec(ctx)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *store) GetConnectedAccounts(ctx context.Context, orgID valuer.UUID, provider cloudintegrationtypes.CloudProviderType) ([]*cloudintegrationtypes.StorableCloudIntegration, error) {
|
||||
var accounts []*cloudintegrationtypes.StorableCloudIntegration
|
||||
err := s.store.BunDB().NewSelect().Model(&accounts).
|
||||
Where("org_id = ?", orgID).
|
||||
Where("provider = ?", provider).
|
||||
Where("removed_at IS NULL").
|
||||
Where("account_id IS NOT NULL").
|
||||
Where("last_agent_report IS NOT NULL").
|
||||
Order("created_at ASC").
|
||||
Scan(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return accounts, nil
|
||||
}
|
||||
|
||||
func (s *store) GetConnectedAccount(ctx context.Context, orgID valuer.UUID, provider cloudintegrationtypes.CloudProviderType, providerAccountID string) (*cloudintegrationtypes.StorableCloudIntegration, error) {
|
||||
account := new(cloudintegrationtypes.StorableCloudIntegration)
|
||||
err := s.store.BunDB().NewSelect().Model(account).
|
||||
Where("org_id = ?", orgID).
|
||||
Where("provider = ?", provider).
|
||||
Where("account_id = ?", providerAccountID).
|
||||
Where("last_agent_report IS NOT NULL").
|
||||
Where("removed_at IS NULL").
|
||||
Scan(ctx)
|
||||
if err != nil {
|
||||
return nil, s.store.WrapNotFoundErrf(err, cloudintegrationtypes.ErrCodeCloudIntegrationNotFound, "connected account with provider account id %s not found", providerAccountID)
|
||||
}
|
||||
return account, nil
|
||||
}
|
||||
|
||||
func (s *store) GetServiceByType(ctx context.Context, cloudIntegrationID valuer.UUID, serviceType string) (*cloudintegrationtypes.StorableCloudIntegrationService, error) {
|
||||
service := new(cloudintegrationtypes.StorableCloudIntegrationService)
|
||||
err := s.store.BunDB().NewSelect().Model(service).
|
||||
Where("cloud_integration_id = ?", cloudIntegrationID).
|
||||
Where("type = ?", serviceType).
|
||||
Scan(ctx)
|
||||
if err != nil {
|
||||
return nil, s.store.WrapNotFoundErrf(err, cloudintegrationtypes.ErrCodeCloudIntegrationNotFound, "cloud integration service with type %s not found", serviceType)
|
||||
}
|
||||
return service, nil
|
||||
}
|
||||
|
||||
func (s *store) CreateService(ctx context.Context, cloudIntegrationID valuer.UUID, service *cloudintegrationtypes.StorableCloudIntegrationService) (*cloudintegrationtypes.StorableCloudIntegrationService, error) {
|
||||
now := time.Now()
|
||||
if service.ID.IsZero() {
|
||||
service.ID = valuer.GenerateUUID()
|
||||
}
|
||||
service.CloudIntegrationID = cloudIntegrationID
|
||||
if service.CreatedAt.IsZero() {
|
||||
service.CreatedAt = now
|
||||
}
|
||||
service.UpdatedAt = now
|
||||
|
||||
_, err := s.store.BunDBCtx(ctx).NewInsert().Model(service).Exec(ctx)
|
||||
if err != nil {
|
||||
return nil, s.store.WrapAlreadyExistsErrf(err, errors.CodeAlreadyExists, "cloud integration service with type %s already exists", service.Type)
|
||||
}
|
||||
|
||||
return service, nil
|
||||
}
|
||||
|
||||
func (s *store) UpdateService(ctx context.Context, cloudIntegrationID valuer.UUID, service *cloudintegrationtypes.StorableCloudIntegrationService) error {
|
||||
service.CloudIntegrationID = cloudIntegrationID
|
||||
service.UpdatedAt = time.Now()
|
||||
_, err := s.store.BunDBCtx(ctx).NewUpdate().Model(service).
|
||||
Where("cloud_integration_id = ?", cloudIntegrationID).
|
||||
Where("type = ?", service.Type).
|
||||
Exec(ctx)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *store) GetServices(ctx context.Context, cloudIntegrationID valuer.UUID) ([]*cloudintegrationtypes.StorableCloudIntegrationService, error) {
|
||||
var services []*cloudintegrationtypes.StorableCloudIntegrationService
|
||||
err := s.store.BunDB().NewSelect().Model(&services).
|
||||
Where("cloud_integration_id = ?", cloudIntegrationID).
|
||||
Scan(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return services, nil
|
||||
}
|
||||
516
pkg/modules/cloudintegration/implcloudintegration/store_test.go
Normal file
516
pkg/modules/cloudintegration/implcloudintegration/store_test.go
Normal file
@@ -0,0 +1,516 @@
|
||||
package implcloudintegration
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/DATA-DOG/go-sqlmock"
|
||||
"github.com/SigNoz/signoz/pkg/factory/factorytest"
|
||||
"github.com/SigNoz/signoz/pkg/instrumentation/instrumentationtest"
|
||||
"github.com/SigNoz/signoz/pkg/signoz"
|
||||
"github.com/SigNoz/signoz/pkg/sqlmigration"
|
||||
"github.com/SigNoz/signoz/pkg/sqlmigrator"
|
||||
"github.com/SigNoz/signoz/pkg/sqlschema"
|
||||
"github.com/SigNoz/signoz/pkg/sqlschema/sqlitesqlschema"
|
||||
"github.com/SigNoz/signoz/pkg/sqlstore"
|
||||
"github.com/SigNoz/signoz/pkg/sqlstore/sqlitesqlstore"
|
||||
"github.com/SigNoz/signoz/pkg/telemetrystore"
|
||||
"github.com/SigNoz/signoz/pkg/telemetrystore/telemetrystoretest"
|
||||
"github.com/SigNoz/signoz/pkg/types"
|
||||
"github.com/SigNoz/signoz/pkg/types/cloudintegrationtypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// newTestDB creates a real SQLite DB, runs all migrations (matching production),
|
||||
// and returns the underlying sqlstore so callers can seed data.
|
||||
func newTestDB(t *testing.T) sqlstore.SQLStore {
|
||||
t.Helper()
|
||||
|
||||
ctx := context.Background()
|
||||
settings := instrumentationtest.New().ToProviderSettings()
|
||||
|
||||
f, err := os.CreateTemp("", "signoz-test-*.db")
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() { os.Remove(f.Name()) })
|
||||
f.Close()
|
||||
|
||||
sqlStore, err := sqlitesqlstore.New(ctx, settings, sqlstore.Config{
|
||||
Provider: "sqlite",
|
||||
Connection: sqlstore.ConnectionConfig{
|
||||
MaxOpenConns: 10,
|
||||
},
|
||||
Sqlite: sqlstore.SqliteConfig{
|
||||
Path: f.Name(),
|
||||
Mode: "delete",
|
||||
BusyTimeout: 5000 * time.Millisecond,
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
sqlSchema, err := sqlitesqlschema.New(ctx, settings, sqlschema.Config{}, sqlStore)
|
||||
require.NoError(t, err)
|
||||
|
||||
telemetryStore := telemetrystoretest.New(
|
||||
telemetrystore.Config{Provider: "clickhouse"},
|
||||
sqlmock.QueryMatcherRegexp,
|
||||
)
|
||||
|
||||
migrationFactories := signoz.NewSQLMigrationProviderFactories(
|
||||
sqlStore,
|
||||
sqlSchema,
|
||||
telemetryStore,
|
||||
factorytest.NewSettings(),
|
||||
)
|
||||
|
||||
migrations, err := sqlmigration.New(ctx, settings, sqlmigration.Config{}, migrationFactories)
|
||||
require.NoError(t, err)
|
||||
|
||||
m := sqlmigrator.New(ctx, settings, sqlStore, migrations, sqlmigrator.Config{
|
||||
Lock: sqlmigrator.Lock{
|
||||
Timeout: 30 * time.Second,
|
||||
Interval: 1 * time.Second,
|
||||
},
|
||||
})
|
||||
require.NoError(t, m.Migrate(ctx))
|
||||
|
||||
return sqlStore
|
||||
}
|
||||
|
||||
// seedOrg inserts a row into the organizations table and returns its ID.
|
||||
// This is required because cloud_integration.org_id has a FK to organizations.id.
|
||||
// Each call produces a uniquely named org so multiple orgs can coexist in the same DB.
|
||||
func seedOrg(t *testing.T, db sqlstore.SQLStore) valuer.UUID {
|
||||
t.Helper()
|
||||
ctx := context.Background()
|
||||
|
||||
// Use a fresh UUID as the display/name to guarantee uniqueness.
|
||||
uniqueName := valuer.GenerateUUID().String()
|
||||
org := types.NewOrganization(uniqueName, uniqueName)
|
||||
_, err := db.BunDB().NewInsert().Model(org).Exec(ctx)
|
||||
require.NoError(t, err)
|
||||
return org.ID
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func makeAccount(orgID valuer.UUID, provider cloudintegrationtypes.CloudProviderType) *cloudintegrationtypes.StorableCloudIntegration {
|
||||
return &cloudintegrationtypes.StorableCloudIntegration{
|
||||
Provider: provider,
|
||||
Config: `{"region":"us-east-1"}`,
|
||||
OrgID: orgID,
|
||||
}
|
||||
}
|
||||
|
||||
func ptr[T any](v T) *T { return &v }
|
||||
|
||||
// newTestStore is a convenience wrapper that returns a ready-to-use store and a
|
||||
// pre-seeded org ID. Tests that need a second org should call seedOrg separately.
|
||||
func newTestStore(t *testing.T) (cloudintegrationtypes.Store, valuer.UUID) {
|
||||
t.Helper()
|
||||
db := newTestDB(t)
|
||||
orgID := seedOrg(t, db)
|
||||
return NewStore(db), orgID
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Account tests
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func TestCreateAccount_Success(t *testing.T) {
|
||||
s, orgID := newTestStore(t)
|
||||
ctx := context.Background()
|
||||
|
||||
out, err := s.CreateAccount(ctx, orgID, makeAccount(orgID, cloudintegrationtypes.CloudProviderTypeAWS))
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.False(t, out.ID.IsZero())
|
||||
assert.Equal(t, orgID, out.OrgID)
|
||||
assert.Equal(t, cloudintegrationtypes.CloudProviderTypeAWS, out.Provider)
|
||||
assert.False(t, out.CreatedAt.IsZero())
|
||||
assert.False(t, out.UpdatedAt.IsZero())
|
||||
}
|
||||
|
||||
func TestCreateAccount_DuplicateID(t *testing.T) {
|
||||
s, orgID := newTestStore(t)
|
||||
ctx := context.Background()
|
||||
|
||||
out, err := s.CreateAccount(ctx, orgID, makeAccount(orgID, cloudintegrationtypes.CloudProviderTypeAWS))
|
||||
require.NoError(t, err)
|
||||
|
||||
dup := makeAccount(orgID, cloudintegrationtypes.CloudProviderTypeAWS)
|
||||
dup.ID = out.ID
|
||||
_, err = s.CreateAccount(ctx, orgID, dup)
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func TestGetAccountByID_Found(t *testing.T) {
|
||||
s, orgID := newTestStore(t)
|
||||
ctx := context.Background()
|
||||
|
||||
created, err := s.CreateAccount(ctx, orgID, makeAccount(orgID, cloudintegrationtypes.CloudProviderTypeAWS))
|
||||
require.NoError(t, err)
|
||||
|
||||
got, err := s.GetAccountByID(ctx, orgID, created.ID, cloudintegrationtypes.CloudProviderTypeAWS)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, created.ID, got.ID)
|
||||
}
|
||||
|
||||
func TestGetAccountByID_NotFound(t *testing.T) {
|
||||
s, orgID := newTestStore(t)
|
||||
ctx := context.Background()
|
||||
|
||||
_, err := s.GetAccountByID(ctx, orgID, valuer.GenerateUUID(), cloudintegrationtypes.CloudProviderTypeAWS)
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func TestGetAccountByID_WrongOrg(t *testing.T) {
|
||||
s, orgID := newTestStore(t)
|
||||
ctx := context.Background()
|
||||
|
||||
created, err := s.CreateAccount(ctx, orgID, makeAccount(orgID, cloudintegrationtypes.CloudProviderTypeAWS))
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = s.GetAccountByID(ctx, valuer.GenerateUUID(), created.ID, cloudintegrationtypes.CloudProviderTypeAWS)
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func TestGetAccountByID_WrongProvider(t *testing.T) {
|
||||
s, orgID := newTestStore(t)
|
||||
ctx := context.Background()
|
||||
|
||||
created, err := s.CreateAccount(ctx, orgID, makeAccount(orgID, cloudintegrationtypes.CloudProviderTypeAWS))
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = s.GetAccountByID(ctx, orgID, created.ID, cloudintegrationtypes.CloudProviderTypeAzure)
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func TestUpdateAccount(t *testing.T) {
|
||||
s, orgID := newTestStore(t)
|
||||
ctx := context.Background()
|
||||
|
||||
created, err := s.CreateAccount(ctx, orgID, makeAccount(orgID, cloudintegrationtypes.CloudProviderTypeAWS))
|
||||
require.NoError(t, err)
|
||||
|
||||
created.Config = `{"region":"eu-west-1"}`
|
||||
require.NoError(t, s.UpdateAccount(ctx, created))
|
||||
|
||||
got, err := s.GetAccountByID(ctx, orgID, created.ID, cloudintegrationtypes.CloudProviderTypeAWS)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, `{"region":"eu-west-1"}`, got.Config)
|
||||
}
|
||||
|
||||
func TestUpdateAccount_SetsUpdatedAt(t *testing.T) {
|
||||
s, orgID := newTestStore(t)
|
||||
ctx := context.Background()
|
||||
|
||||
created, err := s.CreateAccount(ctx, orgID, makeAccount(orgID, cloudintegrationtypes.CloudProviderTypeAWS))
|
||||
require.NoError(t, err)
|
||||
originalUpdatedAt := created.UpdatedAt
|
||||
|
||||
time.Sleep(2 * time.Millisecond)
|
||||
require.NoError(t, s.UpdateAccount(ctx, created))
|
||||
|
||||
got, err := s.GetAccountByID(ctx, orgID, created.ID, cloudintegrationtypes.CloudProviderTypeAWS)
|
||||
require.NoError(t, err)
|
||||
assert.True(t, got.UpdatedAt.After(originalUpdatedAt))
|
||||
}
|
||||
|
||||
func TestRemoveAccount_SoftDelete(t *testing.T) {
|
||||
s, orgID := newTestStore(t)
|
||||
ctx := context.Background()
|
||||
|
||||
created, err := s.CreateAccount(ctx, orgID, makeAccount(orgID, cloudintegrationtypes.CloudProviderTypeAWS))
|
||||
require.NoError(t, err)
|
||||
|
||||
require.NoError(t, s.RemoveAccount(ctx, orgID, created.ID, cloudintegrationtypes.CloudProviderTypeAWS))
|
||||
|
||||
// Row still fetchable by ID (soft-delete only sets removed_at).
|
||||
got, err := s.GetAccountByID(ctx, orgID, created.ID, cloudintegrationtypes.CloudProviderTypeAWS)
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, got.RemovedAt)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Connected account tests
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func TestGetConnectedAccounts_Empty(t *testing.T) {
|
||||
s, orgID := newTestStore(t)
|
||||
ctx := context.Background()
|
||||
|
||||
accounts, err := s.GetConnectedAccounts(ctx, orgID, cloudintegrationtypes.CloudProviderTypeAWS)
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, accounts)
|
||||
}
|
||||
|
||||
func TestGetConnectedAccounts_OnlyConnectedReturned(t *testing.T) {
|
||||
s, orgID := newTestStore(t)
|
||||
ctx := context.Background()
|
||||
|
||||
// Not connected: no account_id, no last_agent_report.
|
||||
_, err := s.CreateAccount(ctx, orgID, makeAccount(orgID, cloudintegrationtypes.CloudProviderTypeAWS))
|
||||
require.NoError(t, err)
|
||||
|
||||
// Connected: has account_id and last_agent_report.
|
||||
connected := makeAccount(orgID, cloudintegrationtypes.CloudProviderTypeAWS)
|
||||
connected.AccountID = ptr("123456789012")
|
||||
connected.LastAgentReport = &cloudintegrationtypes.StorableAgentReport{
|
||||
TimestampMillis: time.Now().UnixMilli(),
|
||||
}
|
||||
_, err = s.CreateAccount(ctx, orgID, connected)
|
||||
require.NoError(t, err)
|
||||
|
||||
accounts, err := s.GetConnectedAccounts(ctx, orgID, cloudintegrationtypes.CloudProviderTypeAWS)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, accounts, 1)
|
||||
assert.Equal(t, ptr("123456789012"), accounts[0].AccountID)
|
||||
}
|
||||
|
||||
func TestGetConnectedAccounts_ExcludesRemoved(t *testing.T) {
|
||||
s, orgID := newTestStore(t)
|
||||
ctx := context.Background()
|
||||
|
||||
in := makeAccount(orgID, cloudintegrationtypes.CloudProviderTypeAWS)
|
||||
in.AccountID = ptr("123456789012")
|
||||
in.LastAgentReport = &cloudintegrationtypes.StorableAgentReport{TimestampMillis: time.Now().UnixMilli()}
|
||||
created, err := s.CreateAccount(ctx, orgID, in)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.NoError(t, s.RemoveAccount(ctx, orgID, created.ID, cloudintegrationtypes.CloudProviderTypeAWS))
|
||||
|
||||
accounts, err := s.GetConnectedAccounts(ctx, orgID, cloudintegrationtypes.CloudProviderTypeAWS)
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, accounts)
|
||||
}
|
||||
|
||||
func TestGetConnectedAccounts_IsolatedByOrg(t *testing.T) {
|
||||
db := newTestDB(t)
|
||||
ctx := context.Background()
|
||||
|
||||
org1 := seedOrg(t, db)
|
||||
org2 := seedOrg(t, db)
|
||||
s := NewStore(db)
|
||||
|
||||
in := makeAccount(org1, cloudintegrationtypes.CloudProviderTypeAWS)
|
||||
in.AccountID = ptr("111111111111")
|
||||
in.LastAgentReport = &cloudintegrationtypes.StorableAgentReport{TimestampMillis: time.Now().UnixMilli()}
|
||||
_, err := s.CreateAccount(ctx, org1, in)
|
||||
require.NoError(t, err)
|
||||
|
||||
accounts, err := s.GetConnectedAccounts(ctx, org2, cloudintegrationtypes.CloudProviderTypeAWS)
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, accounts)
|
||||
}
|
||||
|
||||
func TestGetConnectedAccount_Found(t *testing.T) {
|
||||
s, orgID := newTestStore(t)
|
||||
ctx := context.Background()
|
||||
|
||||
in := makeAccount(orgID, cloudintegrationtypes.CloudProviderTypeAWS)
|
||||
in.AccountID = ptr("123456789012")
|
||||
in.LastAgentReport = &cloudintegrationtypes.StorableAgentReport{TimestampMillis: time.Now().UnixMilli()}
|
||||
created, err := s.CreateAccount(ctx, orgID, in)
|
||||
require.NoError(t, err)
|
||||
|
||||
got, err := s.GetConnectedAccount(ctx, orgID, cloudintegrationtypes.CloudProviderTypeAWS, "123456789012")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, created.ID, got.ID)
|
||||
}
|
||||
|
||||
func TestGetConnectedAccount_NotFound(t *testing.T) {
|
||||
s, orgID := newTestStore(t)
|
||||
ctx := context.Background()
|
||||
|
||||
_, err := s.GetConnectedAccount(ctx, orgID, cloudintegrationtypes.CloudProviderTypeAWS, "nonexistent")
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func TestGetConnectedAccount_ExcludesRemoved(t *testing.T) {
|
||||
s, orgID := newTestStore(t)
|
||||
ctx := context.Background()
|
||||
|
||||
in := makeAccount(orgID, cloudintegrationtypes.CloudProviderTypeAWS)
|
||||
in.AccountID = ptr("123456789012")
|
||||
in.LastAgentReport = &cloudintegrationtypes.StorableAgentReport{TimestampMillis: time.Now().UnixMilli()}
|
||||
created, err := s.CreateAccount(ctx, orgID, in)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.NoError(t, s.RemoveAccount(ctx, orgID, created.ID, cloudintegrationtypes.CloudProviderTypeAWS))
|
||||
|
||||
_, err = s.GetConnectedAccount(ctx, orgID, cloudintegrationtypes.CloudProviderTypeAWS, "123456789012")
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Service tests
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// createConnectedAccount is a helper that inserts a fully connected account.
|
||||
func createConnectedAccount(t *testing.T, s cloudintegrationtypes.Store, orgID valuer.UUID, providerAccountID string) *cloudintegrationtypes.StorableCloudIntegration {
|
||||
t.Helper()
|
||||
in := makeAccount(orgID, cloudintegrationtypes.CloudProviderTypeAWS)
|
||||
in.AccountID = ptr(providerAccountID)
|
||||
in.LastAgentReport = &cloudintegrationtypes.StorableAgentReport{TimestampMillis: time.Now().UnixMilli()}
|
||||
created, err := s.CreateAccount(context.Background(), orgID, in)
|
||||
require.NoError(t, err)
|
||||
return created
|
||||
}
|
||||
|
||||
func makeService(svcType string) *cloudintegrationtypes.StorableCloudIntegrationService {
|
||||
return &cloudintegrationtypes.StorableCloudIntegrationService{
|
||||
Type: valuer.NewString(svcType),
|
||||
Config: `{"enabled":true}`,
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateService_Success(t *testing.T) {
|
||||
s, orgID := newTestStore(t)
|
||||
ctx := context.Background()
|
||||
account := createConnectedAccount(t, s, orgID, "123456789012")
|
||||
|
||||
svc, err := s.CreateService(ctx, account.ID, makeService("aws_rds"))
|
||||
require.NoError(t, err)
|
||||
assert.False(t, svc.ID.IsZero())
|
||||
assert.Equal(t, account.ID, svc.CloudIntegrationID)
|
||||
assert.Equal(t, valuer.NewString("aws_rds"), svc.Type)
|
||||
}
|
||||
|
||||
func TestCreateService_DuplicateType(t *testing.T) {
|
||||
s, orgID := newTestStore(t)
|
||||
ctx := context.Background()
|
||||
account := createConnectedAccount(t, s, orgID, "123456789012")
|
||||
|
||||
_, err := s.CreateService(ctx, account.ID, makeService("aws_rds"))
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = s.CreateService(ctx, account.ID, makeService("aws_rds"))
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func TestGetServiceByType_Found(t *testing.T) {
|
||||
s, orgID := newTestStore(t)
|
||||
ctx := context.Background()
|
||||
account := createConnectedAccount(t, s, orgID, "123456789012")
|
||||
|
||||
created, err := s.CreateService(ctx, account.ID, makeService("aws_s3"))
|
||||
require.NoError(t, err)
|
||||
|
||||
got, err := s.GetServiceByType(ctx, account.ID, "aws_s3")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, created.ID, got.ID)
|
||||
}
|
||||
|
||||
func TestGetServiceByType_NotFound(t *testing.T) {
|
||||
s, orgID := newTestStore(t)
|
||||
ctx := context.Background()
|
||||
account := createConnectedAccount(t, s, orgID, "123456789012")
|
||||
|
||||
_, err := s.GetServiceByType(ctx, account.ID, "aws_lambda")
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func TestUpdateService(t *testing.T) {
|
||||
s, orgID := newTestStore(t)
|
||||
ctx := context.Background()
|
||||
account := createConnectedAccount(t, s, orgID, "123456789012")
|
||||
|
||||
created, err := s.CreateService(ctx, account.ID, makeService("aws_ec2"))
|
||||
require.NoError(t, err)
|
||||
|
||||
created.Config = `{"enabled":false}`
|
||||
require.NoError(t, s.UpdateService(ctx, account.ID, created))
|
||||
|
||||
got, err := s.GetServiceByType(ctx, account.ID, "aws_ec2")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, `{"enabled":false}`, got.Config)
|
||||
}
|
||||
|
||||
func TestUpdateService_SetsUpdatedAt(t *testing.T) {
|
||||
s, orgID := newTestStore(t)
|
||||
ctx := context.Background()
|
||||
account := createConnectedAccount(t, s, orgID, "123456789012")
|
||||
|
||||
created, err := s.CreateService(ctx, account.ID, makeService("aws_ec2"))
|
||||
require.NoError(t, err)
|
||||
originalUpdatedAt := created.UpdatedAt
|
||||
|
||||
time.Sleep(2 * time.Millisecond)
|
||||
require.NoError(t, s.UpdateService(ctx, account.ID, created))
|
||||
|
||||
got, err := s.GetServiceByType(ctx, account.ID, "aws_ec2")
|
||||
require.NoError(t, err)
|
||||
assert.True(t, got.UpdatedAt.After(originalUpdatedAt))
|
||||
}
|
||||
|
||||
func TestGetServices_Empty(t *testing.T) {
|
||||
s, orgID := newTestStore(t)
|
||||
ctx := context.Background()
|
||||
account := createConnectedAccount(t, s, orgID, "123456789012")
|
||||
|
||||
services, err := s.GetServices(ctx, account.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, services)
|
||||
}
|
||||
|
||||
func TestGetServices_Multiple(t *testing.T) {
|
||||
s, orgID := newTestStore(t)
|
||||
ctx := context.Background()
|
||||
account := createConnectedAccount(t, s, orgID, "123456789012")
|
||||
|
||||
for _, svcType := range []string{"aws_rds", "aws_s3", "aws_ec2"} {
|
||||
_, err := s.CreateService(ctx, account.ID, makeService(svcType))
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
services, err := s.GetServices(ctx, account.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, services, 3)
|
||||
}
|
||||
|
||||
func TestGetServices_IsolatedByAccount(t *testing.T) {
|
||||
s, orgID := newTestStore(t)
|
||||
ctx := context.Background()
|
||||
|
||||
account1 := createConnectedAccount(t, s, orgID, "111111111111")
|
||||
account2 := createConnectedAccount(t, s, orgID, "222222222222")
|
||||
|
||||
_, err := s.CreateService(ctx, account1.ID, makeService("aws_rds"))
|
||||
require.NoError(t, err)
|
||||
|
||||
services, err := s.GetServices(ctx, account2.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, services)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// LastAgentReport round-trip
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func TestLastAgentReport_RoundTrip(t *testing.T) {
|
||||
s, orgID := newTestStore(t)
|
||||
ctx := context.Background()
|
||||
|
||||
report := &cloudintegrationtypes.StorableAgentReport{
|
||||
TimestampMillis: 1700000000000,
|
||||
Data: map[string]any{"key": "value"},
|
||||
}
|
||||
in := makeAccount(orgID, cloudintegrationtypes.CloudProviderTypeAWS)
|
||||
in.AccountID = ptr("123456789012")
|
||||
in.LastAgentReport = report
|
||||
|
||||
created, err := s.CreateAccount(ctx, orgID, in)
|
||||
require.NoError(t, err)
|
||||
|
||||
got, err := s.GetAccountByID(ctx, orgID, created.ID, cloudintegrationtypes.CloudProviderTypeAWS)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, got.LastAgentReport)
|
||||
assert.Equal(t, report.TimestampMillis, got.LastAgentReport.TimestampMillis)
|
||||
assert.Equal(t, "value", got.LastAgentReport.Data["key"])
|
||||
}
|
||||
49
pkg/types/cloudintegrationtypes/account.go
Normal file
49
pkg/types/cloudintegrationtypes/account.go
Normal file
@@ -0,0 +1,49 @@
|
||||
package cloudintegrationtypes
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
)
|
||||
|
||||
type (
|
||||
ConnectedAccounts struct {
|
||||
Accounts []*Account `json:"accounts"`
|
||||
}
|
||||
|
||||
GettableConnectedAccounts = ConnectedAccounts
|
||||
|
||||
UpdateAccountConfigRequest struct {
|
||||
AWS *AWSAccountConfig `json:"aws"`
|
||||
}
|
||||
|
||||
UpdatableAccountConfig = UpdateAccountConfigRequest
|
||||
)
|
||||
|
||||
type (
|
||||
Account struct {
|
||||
Id string `json:"id"`
|
||||
ProviderAccountId *string `json:"providerAccountID,omitempty"`
|
||||
Provider CloudProviderType `json:"provider"`
|
||||
RemovedAt *time.Time `json:"removedAt,omitempty"`
|
||||
AgentReport *AgentReport `json:"agentReport,omitempty"`
|
||||
OrgID valuer.UUID `json:"orgID"`
|
||||
Config *AccountConfig `json:"accountConfig,omitempty"`
|
||||
}
|
||||
|
||||
GettableAccount = Account
|
||||
)
|
||||
|
||||
// AgentReport represents heartbeats sent by the agent.
|
||||
type AgentReport struct {
|
||||
TimestampMillis int64 `json:"timestampMillis"`
|
||||
Data map[string]any `json:"data"`
|
||||
}
|
||||
|
||||
type AccountConfig struct {
|
||||
AWS *AWSAccountConfig `json:"aws,omitempty"`
|
||||
}
|
||||
|
||||
type AWSAccountConfig struct {
|
||||
Regions []string `json:"regions"`
|
||||
}
|
||||
82
pkg/types/cloudintegrationtypes/cloudintegration.go
Normal file
82
pkg/types/cloudintegrationtypes/cloudintegration.go
Normal file
@@ -0,0 +1,82 @@
|
||||
package cloudintegrationtypes
|
||||
|
||||
import (
|
||||
"database/sql/driver"
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/uptrace/bun"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/types"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrCodeCloudIntegrationNotFound = errors.MustNewCode("cloud_integration_not_found")
|
||||
)
|
||||
|
||||
// StorableCloudIntegration represents a cloud integration stored in the database.
|
||||
// This is also referred as "Account" in the context of cloud integrations.
|
||||
type StorableCloudIntegration struct {
|
||||
bun.BaseModel `bun:"table:cloud_integration"`
|
||||
|
||||
types.Identifiable
|
||||
types.TimeAuditable
|
||||
Provider CloudProviderType `json:"provider" bun:"provider,type:text"`
|
||||
// Config is provider specific data in JSON string format
|
||||
Config string `json:"config" bun:"config,type:text"`
|
||||
AccountID *string `json:"account_id" bun:"account_id,type:text"`
|
||||
LastAgentReport *StorableAgentReport `json:"last_agent_report" bun:"last_agent_report,type:text"`
|
||||
RemovedAt *time.Time `json:"removed_at" bun:"removed_at,type:timestamp,nullzero"`
|
||||
OrgID valuer.UUID `bun:"org_id,type:text"`
|
||||
}
|
||||
|
||||
// StorableAgentReport represents the last heartbeat and arbitrary data sent by the agent
|
||||
// as of now there is no use case for Data field, but keeping it for backwards compatibility with older structure.
|
||||
type StorableAgentReport struct {
|
||||
TimestampMillis int64 `json:"timestamp_millis"`
|
||||
Data map[string]any `json:"data"`
|
||||
}
|
||||
|
||||
// StorableCloudIntegrationService is to store service config for a cloud integration, which is a cloud provider specific configuration.
|
||||
type StorableCloudIntegrationService struct {
|
||||
bun.BaseModel `bun:"table:cloud_integration_service,alias:cis"`
|
||||
|
||||
types.Identifiable
|
||||
types.TimeAuditable
|
||||
Type valuer.String `bun:"type,type:text,notnull,unique:cloud_integration_id_type"`
|
||||
// Config is cloud provider's service specific data in JSON string format
|
||||
Config string `bun:"config,type:text"`
|
||||
CloudIntegrationID valuer.UUID `bun:"cloud_integration_id,type:text,notnull,unique:cloud_integration_id_type,references:cloud_integration(id),on_delete:cascade"`
|
||||
}
|
||||
|
||||
// Scan scans value from DB.
|
||||
func (r *StorableAgentReport) Scan(src any) error {
|
||||
var data []byte
|
||||
switch v := src.(type) {
|
||||
case []byte:
|
||||
data = v
|
||||
case string:
|
||||
data = []byte(v)
|
||||
default:
|
||||
return errors.NewInternalf(errors.CodeInternal, "tried to scan from %T instead of string or bytes", src)
|
||||
}
|
||||
return json.Unmarshal(data, r)
|
||||
}
|
||||
|
||||
// Value creates value to be stored in DB.
|
||||
func (r *StorableAgentReport) Value() (driver.Value, error) {
|
||||
if r == nil {
|
||||
return nil, errors.NewInternalf(errors.CodeInternal, "agent report is nil")
|
||||
}
|
||||
|
||||
serialized, err := json.Marshal(r)
|
||||
if err != nil {
|
||||
return nil, errors.WrapInternalf(
|
||||
err, errors.CodeInternal, "couldn't serialize agent report to JSON",
|
||||
)
|
||||
}
|
||||
// Return as string instead of []byte to ensure PostgreSQL stores as text, not bytes
|
||||
return string(serialized), nil
|
||||
}
|
||||
41
pkg/types/cloudintegrationtypes/cloudprovider.go
Normal file
41
pkg/types/cloudintegrationtypes/cloudprovider.go
Normal file
@@ -0,0 +1,41 @@
|
||||
package cloudintegrationtypes
|
||||
|
||||
import (
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
)
|
||||
|
||||
// CloudProviderType type alias.
|
||||
type CloudProviderType struct{ valuer.String }
|
||||
|
||||
var (
|
||||
// cloud providers.
|
||||
CloudProviderTypeAWS = CloudProviderType{valuer.NewString("aws")}
|
||||
CloudProviderTypeAzure = CloudProviderType{valuer.NewString("azure")}
|
||||
|
||||
// errors.
|
||||
ErrCodeCloudProviderInvalidInput = errors.MustNewCode("invalid_cloud_provider")
|
||||
|
||||
AWSIntegrationUserEmail = valuer.MustNewEmail("aws-integration@signoz.io")
|
||||
AzureIntegrationUserEmail = valuer.MustNewEmail("azure-integration@signoz.io")
|
||||
)
|
||||
|
||||
// CloudIntegrationUserEmails is the list of valid emails for Cloud One Click integrations.
|
||||
// This is used for validation and restrictions in different contexts, across codebase.
|
||||
var CloudIntegrationUserEmails = []valuer.Email{
|
||||
AWSIntegrationUserEmail,
|
||||
AzureIntegrationUserEmail,
|
||||
}
|
||||
|
||||
// NewCloudProvider returns a new CloudProviderType from a string.
|
||||
// It validates the input and returns an error if the input is not valid cloud provider.
|
||||
func NewCloudProvider(provider string) (CloudProviderType, error) {
|
||||
switch provider {
|
||||
case CloudProviderTypeAWS.StringValue():
|
||||
return CloudProviderTypeAWS, nil
|
||||
case CloudProviderTypeAzure.StringValue():
|
||||
return CloudProviderTypeAzure, nil
|
||||
default:
|
||||
return CloudProviderType{}, errors.NewInvalidInputf(ErrCodeCloudProviderInvalidInput, "invalid cloud provider: %s", provider)
|
||||
}
|
||||
}
|
||||
96
pkg/types/cloudintegrationtypes/connection.go
Normal file
96
pkg/types/cloudintegrationtypes/connection.go
Normal file
@@ -0,0 +1,96 @@
|
||||
package cloudintegrationtypes
|
||||
|
||||
import "github.com/SigNoz/signoz/pkg/types/integrationtypes"
|
||||
|
||||
// request for creating connection artifact.
|
||||
type (
|
||||
PostableConnectionArtifact = ConnectionArtifactRequest
|
||||
|
||||
ConnectionArtifactRequest struct {
|
||||
Aws *AWSConnectionArtifactRequest `json:"aws"`
|
||||
}
|
||||
|
||||
AWSConnectionArtifactRequest struct {
|
||||
DeploymentRegion string `json:"deploymentRegion"`
|
||||
Regions []string `json:"regions"`
|
||||
}
|
||||
)
|
||||
|
||||
type (
|
||||
ConnectionArtifact struct {
|
||||
Aws *AWSConnectionArtifact `json:"aws"`
|
||||
}
|
||||
|
||||
AWSConnectionArtifact struct {
|
||||
ConnectionUrl string `json:"connectionURL"`
|
||||
}
|
||||
|
||||
GettableConnectionArtifact = ConnectionArtifact
|
||||
)
|
||||
|
||||
type (
|
||||
AccountStatus struct {
|
||||
Id string `json:"id"`
|
||||
ProviderAccountId *string `json:"providerAccountID,omitempty"`
|
||||
Status integrationtypes.AccountStatus `json:"status"`
|
||||
}
|
||||
|
||||
GettableAccountStatus = AccountStatus
|
||||
)
|
||||
|
||||
type (
|
||||
AgentCheckInRequest struct {
|
||||
// older backward compatible fields are mapped to new fields
|
||||
// CloudIntegrationId string `json:"cloudIntegrationId"`
|
||||
// AccountId string `json:"accountId"`
|
||||
|
||||
// New fields
|
||||
ProviderAccountId string `json:"providerAccountId"`
|
||||
CloudAccountId string `json:"cloudAccountId"`
|
||||
|
||||
Data map[string]any `json:"data,omitempty"`
|
||||
}
|
||||
|
||||
PostableAgentCheckInRequest struct {
|
||||
AgentCheckInRequest
|
||||
// following are backward compatible fields for older running agents
|
||||
// which gets mapped to new fields in AgentCheckInRequest
|
||||
CloudIntegrationId string `json:"cloud_integration_id"`
|
||||
CloudAccountId string `json:"cloud_account_id"`
|
||||
}
|
||||
|
||||
GettableAgentCheckInResponse struct {
|
||||
AgentCheckInResponse
|
||||
|
||||
CloudIntegrationId string `json:"cloud_integration_id"`
|
||||
AccountId string `json:"account_id"`
|
||||
}
|
||||
|
||||
AgentCheckInResponse struct {
|
||||
// Older fields for backward compatibility are mapped to new fields below
|
||||
// CloudIntegrationId string `json:"cloud_integration_id"`
|
||||
// AccountId string `json:"account_id"`
|
||||
|
||||
// New fields
|
||||
ProviderAccountId string `json:"providerAccountId"`
|
||||
CloudAccountId string `json:"cloudAccountId"`
|
||||
|
||||
// IntegrationConfig populates data related to integration that is required for an agent
|
||||
// to start collecting telemetry data
|
||||
// keeping JSON key snake_case for backward compatibility
|
||||
IntegrationConfig *IntegrationConfig `json:"integration_config,omitempty"`
|
||||
}
|
||||
|
||||
IntegrationConfig struct {
|
||||
EnabledRegions []string `json:"enabledRegions"` // backward compatible
|
||||
Telemetry *AWSCollectionStrategy `json:"telemetry,omitempty"` // backward compatible
|
||||
|
||||
// new fields
|
||||
AWS *AWSIntegrationConfig `json:"aws,omitempty"`
|
||||
}
|
||||
|
||||
AWSIntegrationConfig struct {
|
||||
EnabledRegions []string `json:"enabledRegions"`
|
||||
Telemetry *AWSCollectionStrategy `json:"telemetry,omitempty"`
|
||||
}
|
||||
)
|
||||
103
pkg/types/cloudintegrationtypes/regions.go
Normal file
103
pkg/types/cloudintegrationtypes/regions.go
Normal file
@@ -0,0 +1,103 @@
|
||||
package cloudintegrationtypes
|
||||
|
||||
import (
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
)
|
||||
|
||||
var (
|
||||
CodeInvalidCloudRegion = errors.MustNewCode("invalid_cloud_region")
|
||||
CodeMismatchCloudProvider = errors.MustNewCode("cloud_provider_mismatch")
|
||||
)
|
||||
|
||||
// List of all valid cloud regions on Amazon Web Services.
|
||||
var ValidAWSRegions = map[string]struct{}{
|
||||
"af-south-1": {}, // Africa (Cape Town).
|
||||
"ap-east-1": {}, // Asia Pacific (Hong Kong).
|
||||
"ap-northeast-1": {}, // Asia Pacific (Tokyo).
|
||||
"ap-northeast-2": {}, // Asia Pacific (Seoul).
|
||||
"ap-northeast-3": {}, // Asia Pacific (Osaka).
|
||||
"ap-south-1": {}, // Asia Pacific (Mumbai).
|
||||
"ap-south-2": {}, // Asia Pacific (Hyderabad).
|
||||
"ap-southeast-1": {}, // Asia Pacific (Singapore).
|
||||
"ap-southeast-2": {}, // Asia Pacific (Sydney).
|
||||
"ap-southeast-3": {}, // Asia Pacific (Jakarta).
|
||||
"ap-southeast-4": {}, // Asia Pacific (Melbourne).
|
||||
"ca-central-1": {}, // Canada (Central).
|
||||
"ca-west-1": {}, // Canada West (Calgary).
|
||||
"eu-central-1": {}, // Europe (Frankfurt).
|
||||
"eu-central-2": {}, // Europe (Zurich).
|
||||
"eu-north-1": {}, // Europe (Stockholm).
|
||||
"eu-south-1": {}, // Europe (Milan).
|
||||
"eu-south-2": {}, // Europe (Spain).
|
||||
"eu-west-1": {}, // Europe (Ireland).
|
||||
"eu-west-2": {}, // Europe (London).
|
||||
"eu-west-3": {}, // Europe (Paris).
|
||||
"il-central-1": {}, // Israel (Tel Aviv).
|
||||
"me-central-1": {}, // Middle East (UAE).
|
||||
"me-south-1": {}, // Middle East (Bahrain).
|
||||
"sa-east-1": {}, // South America (Sao Paulo).
|
||||
"us-east-1": {}, // US East (N. Virginia).
|
||||
"us-east-2": {}, // US East (Ohio).
|
||||
"us-west-1": {}, // US West (N. California).
|
||||
"us-west-2": {}, // US West (Oregon).
|
||||
}
|
||||
|
||||
// List of all valid cloud regions for Microsoft Azure.
|
||||
var ValidAzureRegions = map[string]struct{}{
|
||||
"australiacentral": {}, // Australia Central
|
||||
"australiacentral2": {}, // Australia Central 2
|
||||
"australiaeast": {}, // Australia East
|
||||
"australiasoutheast": {}, // Australia Southeast
|
||||
"austriaeast": {}, // Austria East
|
||||
"belgiumcentral": {}, // Belgium Central
|
||||
"brazilsouth": {}, // Brazil South
|
||||
"brazilsoutheast": {}, // Brazil Southeast
|
||||
"canadacentral": {}, // Canada Central
|
||||
"canadaeast": {}, // Canada East
|
||||
"centralindia": {}, // Central India
|
||||
"centralus": {}, // Central US
|
||||
"chilecentral": {}, // Chile Central
|
||||
"denmarkeast": {}, // Denmark East
|
||||
"eastasia": {}, // East Asia
|
||||
"eastus": {}, // East US
|
||||
"eastus2": {}, // East US 2
|
||||
"francecentral": {}, // France Central
|
||||
"francesouth": {}, // France South
|
||||
"germanynorth": {}, // Germany North
|
||||
"germanywestcentral": {}, // Germany West Central
|
||||
"indonesiacentral": {}, // Indonesia Central
|
||||
"israelcentral": {}, // Israel Central
|
||||
"italynorth": {}, // Italy North
|
||||
"japaneast": {}, // Japan East
|
||||
"japanwest": {}, // Japan West
|
||||
"koreacentral": {}, // Korea Central
|
||||
"koreasouth": {}, // Korea South
|
||||
"malaysiawest": {}, // Malaysia West
|
||||
"mexicocentral": {}, // Mexico Central
|
||||
"newzealandnorth": {}, // New Zealand North
|
||||
"northcentralus": {}, // North Central US
|
||||
"northeurope": {}, // North Europe
|
||||
"norwayeast": {}, // Norway East
|
||||
"norwaywest": {}, // Norway West
|
||||
"polandcentral": {}, // Poland Central
|
||||
"qatarcentral": {}, // Qatar Central
|
||||
"southafricanorth": {}, // South Africa North
|
||||
"southafricawest": {}, // South Africa West
|
||||
"southcentralus": {}, // South Central US
|
||||
"southindia": {}, // South India
|
||||
"southeastasia": {}, // Southeast Asia
|
||||
"spaincentral": {}, // Spain Central
|
||||
"swedencentral": {}, // Sweden Central
|
||||
"switzerlandnorth": {}, // Switzerland North
|
||||
"switzerlandwest": {}, // Switzerland West
|
||||
"uaecentral": {}, // UAE Central
|
||||
"uaenorth": {}, // UAE North
|
||||
"uksouth": {}, // UK South
|
||||
"ukwest": {}, // UK West
|
||||
"westcentralus": {}, // West Central US
|
||||
"westeurope": {}, // West Europe
|
||||
"westindia": {}, // West India
|
||||
"westus": {}, // West US
|
||||
"westus2": {}, // West US 2
|
||||
"westus3": {}, // West US 3
|
||||
}
|
||||
211
pkg/types/cloudintegrationtypes/service.go
Normal file
211
pkg/types/cloudintegrationtypes/service.go
Normal file
@@ -0,0 +1,211 @@
|
||||
package cloudintegrationtypes
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/types"
|
||||
"github.com/SigNoz/signoz/pkg/types/dashboardtypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
)
|
||||
|
||||
var S3Sync = valuer.NewString("s3sync")
|
||||
|
||||
type (
|
||||
ServicesMetadata struct {
|
||||
Services []*ServiceMetadata `json:"services"`
|
||||
}
|
||||
|
||||
// ServiceMetadata helps to quickly list available services and whether it is enabled or not.
|
||||
// As getting complete service definition is a heavy operation and the response is also large,
|
||||
// initial integration page load can be very slow.
|
||||
ServiceMetadata struct {
|
||||
ServiceDefinitionMetadata
|
||||
// if the service is enabled for the account
|
||||
Enabled bool `json:"enabled"`
|
||||
}
|
||||
|
||||
GettableServicesMetadata = ServicesMetadata
|
||||
|
||||
Service struct {
|
||||
ServiceDefinition
|
||||
ServiceConfig *ServiceConfig `json:"serviceConfig"`
|
||||
}
|
||||
|
||||
GettableService = Service
|
||||
|
||||
UpdateServiceConfigRequest struct {
|
||||
CloudIntegrationId valuer.UUID `json:"cloudIntegrationId"`
|
||||
ServiceConfig *ServiceConfig `json:"serviceConfig"`
|
||||
}
|
||||
|
||||
UpdateServiceConfigResponse struct {
|
||||
Id string `json:"id"` // service id
|
||||
CloudIntegrationId valuer.UUID `json:"cloudIntegrationId"`
|
||||
ServiceConfig *ServiceConfig `json:"serviceConfig"`
|
||||
}
|
||||
)
|
||||
|
||||
type ServiceConfig struct {
|
||||
AWS *AWSServiceConfig `json:"aws,omitempty"`
|
||||
}
|
||||
|
||||
type AWSServiceConfig struct {
|
||||
Logs *AWSServiceLogsConfig `json:"logs"`
|
||||
Metrics *AWSServiceMetricsConfig `json:"metrics"`
|
||||
}
|
||||
|
||||
// AWSServiceLogsConfig is AWS specific logs config for a service
|
||||
// NOTE: the JSON keys are snake case for backward compatibility with existing agents.
|
||||
type AWSServiceLogsConfig struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
S3Buckets map[string][]string `json:"s3_buckets,omitempty"`
|
||||
}
|
||||
|
||||
type AWSServiceMetricsConfig struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
}
|
||||
|
||||
// DefinitionMetadata represents service definition metadata. This is useful for showing service overview.
|
||||
type ServiceDefinitionMetadata struct {
|
||||
Id string `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Icon string `json:"icon"`
|
||||
}
|
||||
|
||||
type ServiceDefinition struct {
|
||||
ServiceDefinitionMetadata
|
||||
Overview string `json:"overview"` // markdown
|
||||
Assets Assets `json:"assets"`
|
||||
SupportedSignals SupportedSignals `json:"supported_signals"`
|
||||
DataCollected DataCollected `json:"dataCollected"`
|
||||
Strategy *CollectionStrategy `json:"telemetryCollectionStrategy"`
|
||||
}
|
||||
|
||||
// CollectionStrategy is cloud provider specific configuration for signal collection,
|
||||
// this is used by agent to understand the nitty-gritty for collecting telemetry for the cloud provider.
|
||||
type CollectionStrategy struct {
|
||||
AWS *AWSCollectionStrategy `json:"aws,omitempty"`
|
||||
}
|
||||
|
||||
// Assets represents the collection of dashboards.
|
||||
type Assets struct {
|
||||
Dashboards []Dashboard `json:"dashboards"`
|
||||
}
|
||||
|
||||
// SupportedSignals for cloud provider's service.
|
||||
type SupportedSignals struct {
|
||||
Logs bool `json:"logs"`
|
||||
Metrics bool `json:"metrics"`
|
||||
}
|
||||
|
||||
// DataCollected is curated static list of metrics and logs, this is shown as part of service overview.
|
||||
type DataCollected struct {
|
||||
Logs []CollectedLogAttribute `json:"logs"`
|
||||
Metrics []CollectedMetric `json:"metrics"`
|
||||
}
|
||||
|
||||
// CollectedLogAttribute represents a log attribute that is present in all log entries for a service,
|
||||
// this is shown as part of service overview.
|
||||
type CollectedLogAttribute struct {
|
||||
Name string `json:"name"`
|
||||
Path string `json:"path"`
|
||||
Type string `json:"type"`
|
||||
}
|
||||
|
||||
// CollectedMetric represents a metric that is collected for a service, this is shown as part of service overview.
|
||||
type CollectedMetric struct {
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
Unit string `json:"unit"`
|
||||
Description string `json:"description"`
|
||||
}
|
||||
|
||||
// AWSCollectionStrategy represents signal collection strategy for AWS services.
|
||||
// this is AWS specific.
|
||||
// NOTE: this structure is still using snake case, for backward compatibility,
|
||||
// with existing agents.
|
||||
type AWSCollectionStrategy struct {
|
||||
Metrics *AWSMetricsStrategy `json:"aws_metrics,omitempty"`
|
||||
Logs *AWSLogsStrategy `json:"aws_logs,omitempty"`
|
||||
S3Buckets map[string][]string `json:"s3_buckets,omitempty"` // Only available in S3 Sync Service Type in AWS
|
||||
}
|
||||
|
||||
// AWSMetricsStrategy represents metrics collection strategy for AWS services.
|
||||
// this is AWS specific.
|
||||
// NOTE: this structure is still using snake case, for backward compatibility,
|
||||
// with existing agents.
|
||||
type AWSMetricsStrategy struct {
|
||||
// to be used as https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-cloudwatch-metricstream.html#cfn-cloudwatch-metricstream-includefilters
|
||||
StreamFilters []struct {
|
||||
// json tags here are in the shape expected by AWS API as detailed at
|
||||
// https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-cloudwatch-metricstream-metricstreamfilter.html
|
||||
Namespace string `json:"Namespace"`
|
||||
MetricNames []string `json:"MetricNames,omitempty"`
|
||||
} `json:"cloudwatch_metric_stream_filters"`
|
||||
}
|
||||
|
||||
// AWSLogsStrategy represents logs collection strategy for AWS services.
|
||||
// this is AWS specific.
|
||||
// NOTE: this structure is still using snake case, for backward compatibility,
|
||||
// with existing agents.
|
||||
type AWSLogsStrategy struct {
|
||||
Subscriptions []struct {
|
||||
// subscribe to all logs groups with specified prefix.
|
||||
// eg: `/aws/rds/`
|
||||
LogGroupNamePrefix string `json:"log_group_name_prefix"`
|
||||
|
||||
// https://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/FilterAndPatternSyntax.html
|
||||
// "" implies no filtering is required.
|
||||
FilterPattern string `json:"filter_pattern"`
|
||||
} `json:"cloudwatch_logs_subscriptions"`
|
||||
}
|
||||
|
||||
// Dashboard represents a dashboard definition for cloud integration.
|
||||
// This is used to show available pre-made dashboards for a service,
|
||||
// hence has additional fields like id, title and description
|
||||
type Dashboard struct {
|
||||
Id string `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
Definition dashboardtypes.StorableDashboardData `json:"definition,omitempty"`
|
||||
}
|
||||
|
||||
// UTILS
|
||||
|
||||
// GetCloudIntegrationDashboardID returns the dashboard id for a cloud integration, given the cloud provider, service id, and dashboard id.
|
||||
// This is used to generate unique dashboard ids for cloud integration, and also to parse the dashboard id to get the cloud provider and service id when needed.
|
||||
func GetCloudIntegrationDashboardID(cloudProvider CloudProviderType, svcId, dashboardId string) string {
|
||||
return fmt.Sprintf("cloud-integration--%s--%s--%s", cloudProvider, svcId, dashboardId)
|
||||
}
|
||||
|
||||
// GetDashboardsFromAssets returns the list of dashboards for the cloud provider service from definition.
|
||||
func GetDashboardsFromAssets(
|
||||
svcId string,
|
||||
orgID valuer.UUID,
|
||||
cloudProvider CloudProviderType,
|
||||
createdAt time.Time,
|
||||
assets Assets,
|
||||
) []*dashboardtypes.Dashboard {
|
||||
dashboards := make([]*dashboardtypes.Dashboard, 0)
|
||||
|
||||
for _, d := range assets.Dashboards {
|
||||
author := fmt.Sprintf("%s-integration", cloudProvider)
|
||||
dashboards = append(dashboards, &dashboardtypes.Dashboard{
|
||||
ID: GetCloudIntegrationDashboardID(cloudProvider, svcId, d.Id),
|
||||
Locked: true,
|
||||
OrgID: orgID,
|
||||
Data: d.Definition,
|
||||
TimeAuditable: types.TimeAuditable{
|
||||
CreatedAt: createdAt,
|
||||
UpdatedAt: createdAt,
|
||||
},
|
||||
UserAuditable: types.UserAuditable{
|
||||
CreatedBy: author,
|
||||
UpdatedBy: author,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return dashboards
|
||||
}
|
||||
41
pkg/types/cloudintegrationtypes/store.go
Normal file
41
pkg/types/cloudintegrationtypes/store.go
Normal file
@@ -0,0 +1,41 @@
|
||||
package cloudintegrationtypes
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
)
|
||||
|
||||
type Store interface {
|
||||
// GetAccountByID returns a cloud integration account by id
|
||||
GetAccountByID(ctx context.Context, orgID, id valuer.UUID, provider CloudProviderType) (*StorableCloudIntegration, error)
|
||||
|
||||
// CreateAccount creates a new cloud integration account
|
||||
CreateAccount(ctx context.Context, orgID valuer.UUID, account *StorableCloudIntegration) (*StorableCloudIntegration, error)
|
||||
|
||||
// UpdateAccount updates an existing cloud integration account
|
||||
UpdateAccount(ctx context.Context, account *StorableCloudIntegration) error
|
||||
|
||||
// RemoveAccount marks a cloud integration account as removed by setting the RemovedAt field
|
||||
RemoveAccount(ctx context.Context, orgID, id valuer.UUID, provider CloudProviderType) error
|
||||
|
||||
// GetConnectedAccounts returns all the cloud integration accounts for the org and cloud provider
|
||||
GetConnectedAccounts(ctx context.Context, orgID valuer.UUID, provider CloudProviderType) ([]*StorableCloudIntegration, error)
|
||||
|
||||
// GetConnectedAccount for given provider
|
||||
GetConnectedAccount(ctx context.Context, orgID valuer.UUID, provider CloudProviderType, providerAccountID string) (*StorableCloudIntegration, error)
|
||||
|
||||
// cloud_integration_service related methods
|
||||
|
||||
// GetServiceByType returns the cloud integration service for the given cloud integration id and service type
|
||||
GetServiceByType(ctx context.Context, cloudIntegrationID valuer.UUID, serviceType string) (*StorableCloudIntegrationService, error)
|
||||
|
||||
// CreateService creates a new cloud integration service for the given cloud integration id and service type
|
||||
CreateService(ctx context.Context, cloudIntegrationID valuer.UUID, service *StorableCloudIntegrationService) (*StorableCloudIntegrationService, error)
|
||||
|
||||
// UpdateService updates an existing cloud integration service for the given cloud integration id and service type
|
||||
UpdateService(ctx context.Context, cloudIntegrationID valuer.UUID, service *StorableCloudIntegrationService) error
|
||||
|
||||
// GetServices returns all the cloud integration services for the given cloud integration id
|
||||
GetServices(ctx context.Context, cloudIntegrationID valuer.UUID) ([]*StorableCloudIntegrationService, error)
|
||||
}
|
||||
Reference in New Issue
Block a user