mirror of
https://git.mirrors.martin98.com/https://github.com/SigNoz/signoz
synced 2025-07-28 00:41:59 +08:00

* feat: init app/cloud_integrations * feat: get API test started for cloudintegrations account lifecycle * feat: cloudintegrations: get controller started * feat: cloud integrations: add cloudintegrations.Controller to APIHandler and servers * feat: cloud integrations: get routes started * feat: cloud integrations: get accounts table schema started * feat: cloud integrations: get cloudProviderAccountsSQLRepository started * feat: cloud integrations: cloudProviderAccountsSQLRepository.listAccounts * feat: cloud integrations: http handler and controller plumbing for /generate-connection-url * feat: cloud integrations: cloudProviderAccountsSQLRepository.upsert * feat: cloud integrations: finish up with /generate-connection-url * feat: cloud integrations: add cloudProviderAccountsRepository.get * feat: cloud integrations: add API test expectation for being able to get account status * feat: cloud integrations: add http handler and controller method for getting account status * feat: cloud integrations: ensure unconnected accounts aren't included in list of connected accounts * feat: cloud integrations: add test expectation for agent check in request * feat: cloud integrations: agent check in API * feat: cloud integrations: ensure polling for status after agent check in works * feat: cloud integrations: ensure account included in connected account list after agent check in * feat: cloud integrations: add API expectation for updating account config * feat: cloud integrations: API for updating cloud account config * feat: cloud integrations: expectation for agent receiving latest config after account config update * feat: cloud integrations: expectation for disconnecting cloud accounts from UI * feat: cloud integrations: API for disconnecting cloud accounts * feat: cloud integrations: some cleanup * feat: cloud integrations: some more cleanup * feat: cloud integrations: repo: scope rows by cloud provider * feat: testutils: refactor out helper for creating a test sqlite DB * feat: cloud integrations: controller: add test validating regeneration of connection url * feat: cloud integrations: controller: validations for agent check ins * feat: cloud integrations: connected account response structure * feat: cloud integrations: API response account structure * feat: cloud integrations: some more cleanup * feat: cloud integrations: remove cloudProviderAccountsRepository.GetById * feat: cloud integrations: shouldn't be able to disconnect non-existent account * feat: cloud integrations: validate agents can't check in to cloud account with 2 signoz ids * feat: cloud integrations: ensure agents can't check in to cloud account with 2 signoz ids * feat: cloud integrations: remove stray import of ee/model in cloudintegrations controller
248 lines
6.6 KiB
Go
248 lines
6.6 KiB
Go
package cloudintegrations
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"slices"
|
|
"time"
|
|
|
|
"github.com/jmoiron/sqlx"
|
|
"go.signoz.io/signoz/pkg/query-service/model"
|
|
)
|
|
|
|
var SupportedCloudProviders = []string{
|
|
"aws",
|
|
}
|
|
|
|
func validateCloudProviderName(name string) *model.ApiError {
|
|
if !slices.Contains(SupportedCloudProviders, name) {
|
|
return model.BadRequest(fmt.Errorf("invalid cloud provider: %s", name))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
type Controller struct {
|
|
repo cloudProviderAccountsRepository
|
|
}
|
|
|
|
func NewController(db *sqlx.DB) (
|
|
*Controller, error,
|
|
) {
|
|
repo, err := newCloudProviderAccountsRepository(db)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("couldn't create cloud provider accounts repo: %w", err)
|
|
}
|
|
|
|
return &Controller{
|
|
repo: repo,
|
|
}, nil
|
|
}
|
|
|
|
type Account struct {
|
|
Id string `json:"id"`
|
|
CloudAccountId string `json:"cloud_account_id"`
|
|
Config AccountConfig `json:"config"`
|
|
Status AccountStatus `json:"status"`
|
|
}
|
|
|
|
type ConnectedAccountsListResponse struct {
|
|
Accounts []Account `json:"accounts"`
|
|
}
|
|
|
|
func (c *Controller) ListConnectedAccounts(
|
|
ctx context.Context, cloudProvider string,
|
|
) (
|
|
*ConnectedAccountsListResponse, *model.ApiError,
|
|
) {
|
|
if apiErr := validateCloudProviderName(cloudProvider); apiErr != nil {
|
|
return nil, apiErr
|
|
}
|
|
|
|
accountRecords, apiErr := c.repo.listConnected(ctx, cloudProvider)
|
|
if apiErr != nil {
|
|
return nil, model.WrapApiError(apiErr, "couldn't list cloud accounts")
|
|
}
|
|
|
|
connectedAccounts := []Account{}
|
|
for _, a := range accountRecords {
|
|
connectedAccounts = append(connectedAccounts, a.account())
|
|
}
|
|
|
|
return &ConnectedAccountsListResponse{
|
|
Accounts: connectedAccounts,
|
|
}, nil
|
|
}
|
|
|
|
type GenerateConnectionUrlRequest struct {
|
|
// Optional. To be specified for updates.
|
|
AccountId *string `json:"account_id,omitempty"`
|
|
|
|
AccountConfig AccountConfig `json:"account_config"`
|
|
|
|
AgentConfig SigNozAgentConfig `json:"agent_config"`
|
|
}
|
|
|
|
type SigNozAgentConfig struct {
|
|
// The region in which SigNoz agent should be installed.
|
|
Region string `json:"region"`
|
|
}
|
|
|
|
type GenerateConnectionUrlResponse struct {
|
|
AccountId string `json:"account_id"`
|
|
ConnectionUrl string `json:"connection_url"`
|
|
}
|
|
|
|
func (c *Controller) GenerateConnectionUrl(
|
|
ctx context.Context, cloudProvider string, req GenerateConnectionUrlRequest,
|
|
) (*GenerateConnectionUrlResponse, *model.ApiError) {
|
|
// Account connection with a simple connection URL may not be available for all providers.
|
|
if cloudProvider != "aws" {
|
|
return nil, model.BadRequest(fmt.Errorf("unsupported cloud provider: %s", cloudProvider))
|
|
}
|
|
|
|
account, apiErr := c.repo.upsert(
|
|
ctx, cloudProvider, req.AccountId, &req.AccountConfig, nil, nil, nil,
|
|
)
|
|
if apiErr != nil {
|
|
return nil, model.WrapApiError(apiErr, "couldn't upsert cloud account")
|
|
}
|
|
|
|
// TODO(Raj): Add actual cloudformation template for AWS integration after it has been shipped.
|
|
connectionUrl := fmt.Sprintf(
|
|
"https://%s.console.aws.amazon.com/cloudformation/home?region=%s#/stacks/quickcreate?stackName=SigNozIntegration/",
|
|
req.AgentConfig.Region, req.AgentConfig.Region,
|
|
)
|
|
|
|
return &GenerateConnectionUrlResponse{
|
|
AccountId: account.Id,
|
|
ConnectionUrl: connectionUrl,
|
|
}, nil
|
|
}
|
|
|
|
type AccountStatusResponse struct {
|
|
Id string `json:"id"`
|
|
Status AccountStatus `json:"status"`
|
|
}
|
|
|
|
func (c *Controller) GetAccountStatus(
|
|
ctx context.Context, cloudProvider string, accountId string,
|
|
) (
|
|
*AccountStatusResponse, *model.ApiError,
|
|
) {
|
|
if apiErr := validateCloudProviderName(cloudProvider); apiErr != nil {
|
|
return nil, apiErr
|
|
}
|
|
|
|
account, apiErr := c.repo.get(ctx, cloudProvider, accountId)
|
|
if apiErr != nil {
|
|
return nil, apiErr
|
|
}
|
|
|
|
resp := AccountStatusResponse{
|
|
Id: account.Id,
|
|
Status: account.status(),
|
|
}
|
|
|
|
return &resp, nil
|
|
}
|
|
|
|
type AgentCheckInRequest struct {
|
|
AccountId string `json:"account_id"`
|
|
CloudAccountId string `json:"cloud_account_id"`
|
|
// Arbitrary cloud specific Agent data
|
|
Data map[string]any `json:"data,omitempty"`
|
|
}
|
|
|
|
type AgentCheckInResponse struct {
|
|
Account AccountRecord `json:"account"`
|
|
}
|
|
|
|
func (c *Controller) CheckInAsAgent(
|
|
ctx context.Context, cloudProvider string, req AgentCheckInRequest,
|
|
) (*AgentCheckInResponse, *model.ApiError) {
|
|
if apiErr := validateCloudProviderName(cloudProvider); apiErr != nil {
|
|
return nil, apiErr
|
|
}
|
|
|
|
existingAccount, apiErr := c.repo.get(ctx, cloudProvider, req.AccountId)
|
|
if existingAccount != nil && existingAccount.CloudAccountId != nil && *existingAccount.CloudAccountId != req.CloudAccountId {
|
|
return nil, model.BadRequest(fmt.Errorf(
|
|
"can't check in with new %s account id %s for account %s with existing %s id %s",
|
|
cloudProvider, req.CloudAccountId, existingAccount.Id, cloudProvider, *existingAccount.CloudAccountId,
|
|
))
|
|
}
|
|
|
|
existingAccount, apiErr = c.repo.getConnectedCloudAccount(ctx, cloudProvider, req.CloudAccountId)
|
|
if existingAccount != nil && existingAccount.Id != req.AccountId {
|
|
return nil, model.BadRequest(fmt.Errorf(
|
|
"can't check in to %s account %s with id %s. already connected with id %s",
|
|
cloudProvider, req.CloudAccountId, req.AccountId, existingAccount.Id,
|
|
))
|
|
}
|
|
|
|
agentReport := AgentReport{
|
|
TimestampMillis: time.Now().UnixMilli(),
|
|
Data: req.Data,
|
|
}
|
|
|
|
account, apiErr := c.repo.upsert(
|
|
ctx, cloudProvider, &req.AccountId, nil, &req.CloudAccountId, &agentReport, nil,
|
|
)
|
|
if apiErr != nil {
|
|
return nil, model.WrapApiError(apiErr, "couldn't upsert cloud account")
|
|
}
|
|
|
|
return &AgentCheckInResponse{
|
|
Account: *account,
|
|
}, nil
|
|
}
|
|
|
|
type UpdateAccountConfigRequest struct {
|
|
Config AccountConfig `json:"config"`
|
|
}
|
|
|
|
func (c *Controller) UpdateAccountConfig(
|
|
ctx context.Context,
|
|
cloudProvider string,
|
|
accountId string,
|
|
req UpdateAccountConfigRequest,
|
|
) (*Account, *model.ApiError) {
|
|
if apiErr := validateCloudProviderName(cloudProvider); apiErr != nil {
|
|
return nil, apiErr
|
|
}
|
|
|
|
accountRecord, apiErr := c.repo.upsert(
|
|
ctx, cloudProvider, &accountId, &req.Config, nil, nil, nil,
|
|
)
|
|
if apiErr != nil {
|
|
return nil, model.WrapApiError(apiErr, "couldn't upsert cloud account")
|
|
}
|
|
|
|
account := accountRecord.account()
|
|
|
|
return &account, nil
|
|
}
|
|
|
|
func (c *Controller) DisconnectAccount(
|
|
ctx context.Context, cloudProvider string, accountId string,
|
|
) (*AccountRecord, *model.ApiError) {
|
|
if apiErr := validateCloudProviderName(cloudProvider); apiErr != nil {
|
|
return nil, apiErr
|
|
}
|
|
|
|
account, apiErr := c.repo.get(ctx, cloudProvider, accountId)
|
|
if apiErr != nil {
|
|
return nil, model.WrapApiError(apiErr, "couldn't disconnect account")
|
|
}
|
|
|
|
tsNow := time.Now()
|
|
account, apiErr = c.repo.upsert(
|
|
ctx, cloudProvider, &accountId, nil, nil, nil, &tsNow,
|
|
)
|
|
if apiErr != nil {
|
|
return nil, model.WrapApiError(apiErr, "couldn't disconnect account")
|
|
}
|
|
|
|
return account, nil
|
|
}
|