Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: aws integration UI facing api: services #6803

Open
wants to merge 24 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
60ae228
feat: cloud service integrations: get model and repo interface started
raj-k-singh Jan 9, 2025
0922bf2
feat: cloud service integrations: flesh out more of cloud services model
raj-k-singh Jan 9, 2025
3b598f7
feat: cloud integrations: reorganize things a little
raj-k-singh Jan 9, 2025
c915db6
feat: cloud integrations: get svc controller started
raj-k-singh Jan 9, 2025
4d06f6b
feat: cloud integrations: add stubs for EC2 and RDS postgres services
raj-k-singh Jan 10, 2025
f1579ab
feat: cloud integrations: add validation for listing and getting avai…
raj-k-singh Jan 10, 2025
6272099
feat: cloud integrations: refactor helpers in existing integrations c…
raj-k-singh Jan 10, 2025
d004130
feat: cloud integrations: parsing of cloud service definitions
raj-k-singh Jan 10, 2025
2d4a13f
feat: cloud integrations: impl for getCloudProviderService
raj-k-singh Jan 10, 2025
2146620
feat: cloud integrations: some reorganization
raj-k-singh Jan 10, 2025
7bc4c09
feat: cloud integrations: some more cleanup
raj-k-singh Jan 11, 2025
0a3b16d
feat: cloud integrations: add validation for listing available cloud …
raj-k-singh Jan 11, 2025
887b9ee
feat: cloud integrations: API endpoint for listing available cloud pr…
raj-k-singh Jan 11, 2025
f87ff13
feat: cloud integrations: add validation for getting details of a par…
raj-k-singh Jan 11, 2025
c343c8e
feat: cloud integrations: API endpoint for getting details of a service
raj-k-singh Jan 11, 2025
f4b54e2
feat: cloud integrations: add controller validation for configuring c…
raj-k-singh Jan 11, 2025
800848c
feat: cloud integrations: get serviceConfigRepo started
raj-k-singh Jan 12, 2025
efe7304
feat: cloud integrations: service config in service list summaries wh…
raj-k-singh Jan 12, 2025
4036800
feat: cloud integrations: only a supported service for a connected cl…
raj-k-singh Jan 12, 2025
12e1b5b
feat: cloud integrations: add validation for configuring services via…
raj-k-singh Jan 12, 2025
d96f436
feat: cloud integrations: API for configuring services
raj-k-singh Jan 12, 2025
917cdd6
feat: cloud integrations: some cleanup
raj-k-singh Jan 14, 2025
d6da8a6
feat: cloud integrations: fix broken test
raj-k-singh Jan 15, 2025
f7ce4a0
Merge branch 'main' into feat/aws-integration-ui-facing-api-1
raj-k-singh Jan 15, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -37,16 +37,16 @@ type cloudProviderAccountsRepository interface {
func newCloudProviderAccountsRepository(db *sqlx.DB) (
*cloudProviderAccountsSQLRepository, error,
) {
if err := InitSqliteDBIfNeeded(db); err != nil {
return nil, fmt.Errorf("could not init sqlite DB for cloudintegrations: %w", err)
if err := initAccountsSqliteDBIfNeeded(db); err != nil {
return nil, fmt.Errorf("could not init sqlite DB for cloudintegrations accounts: %w", err)
}

return &cloudProviderAccountsSQLRepository{
db: db,
}, nil
}

func InitSqliteDBIfNeeded(db *sqlx.DB) error {
func initAccountsSqliteDBIfNeeded(db *sqlx.DB) error {
if db == nil {
return fmt.Errorf("db is required")
}
Expand All @@ -66,7 +66,7 @@ func InitSqliteDBIfNeeded(db *sqlx.DB) error {
_, err := db.Exec(createTablesStatements)
if err != nil {
return fmt.Errorf(
"could not ensure cloud provider integrations schema in sqlite DB: %w", err,
"could not ensure cloud provider accounts schema in sqlite DB: %w", err,
)
}

Expand Down
217 changes: 217 additions & 0 deletions pkg/query-service/app/cloudintegrations/availableServices.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
package cloudintegrations

import (
"bytes"
"embed"
"encoding/json"
"fmt"
"io/fs"
"path"
"sort"

koanfJson "github.com/knadh/koanf/parsers/json"
"go.signoz.io/signoz/pkg/query-service/app/integrations"
"go.signoz.io/signoz/pkg/query-service/model"
"golang.org/x/exp/maps"
)

func listCloudProviderServices(
cloudProvider string,
) ([]CloudServiceDetails, *model.ApiError) {
cloudServices := availableServices[cloudProvider]
if cloudServices == nil {
return nil, model.NotFoundError(fmt.Errorf(
"unsupported cloud provider: %s", cloudProvider,
))
}

services := maps.Values(cloudServices)
sort.Slice(services, func(i, j int) bool {
return services[i].Id < services[j].Id
})

return services, nil
}

func getCloudProviderService(
cloudProvider string, serviceId string,
) (*CloudServiceDetails, *model.ApiError) {
cloudServices := availableServices[cloudProvider]
if cloudServices == nil {
return nil, model.NotFoundError(fmt.Errorf(
"unsupported cloud provider: %s", cloudProvider,
))
}

svc, exists := cloudServices[serviceId]
if !exists {
return nil, model.NotFoundError(fmt.Errorf(
"%s service not found: %s", cloudProvider, serviceId,
))
}

return &svc, nil
}

// End of API. Logic for reading service definition files follows

// Service details read from ./serviceDefinitions
// { "providerName": { "service_id": {...}} }
var availableServices map[string]map[string]CloudServiceDetails

func init() {
err := readAllServiceDefinitions()
if err != nil {
panic(fmt.Errorf(
"couldn't read cloud service definitions: %w", err,
))
}
}

//go:embed serviceDefinitions/*
var serviceDefinitionFiles embed.FS

func readAllServiceDefinitions() error {
availableServices = map[string]map[string]CloudServiceDetails{}

rootDirName := "serviceDefinitions"

cloudProviderDirs, err := fs.ReadDir(serviceDefinitionFiles, rootDirName)
if err != nil {
return fmt.Errorf("couldn't read dirs in %s: %w", rootDirName, err)
}

for _, d := range cloudProviderDirs {
if !d.IsDir() {
continue
}

cloudProviderDirPath := path.Join(rootDirName, d.Name())
cloudServices, err := readServiceDefinitionsFromDir(cloudProviderDirPath)
if err != nil {
return fmt.Errorf("couldn't read %s service definitions", d.Name())
}

if len(cloudServices) < 1 {
return fmt.Errorf("no %s services could be read", d.Name())
}

availableServices[d.Name()] = cloudServices
}

return nil
}

func readServiceDefinitionsFromDir(cloudProviderDirPath string) (
map[string]CloudServiceDetails, error,
) {
svcDefDirs, err := fs.ReadDir(serviceDefinitionFiles, cloudProviderDirPath)
if err != nil {
return nil, fmt.Errorf("couldn't list integrations dirs: %w", err)
}

svcDefs := map[string]CloudServiceDetails{}

for _, d := range svcDefDirs {
if !d.IsDir() {
continue
}

svcDirPath := path.Join(cloudProviderDirPath, d.Name())
s, err := readServiceDefinition(svcDirPath)
if err != nil {
return nil, fmt.Errorf("couldn't read svc definition for %s: %w", d.Name(), err)
}

_, exists := svcDefs[s.Id]
if exists {
return nil, fmt.Errorf(
"duplicate service definition for id %s at %s", s.Id, d.Name(),
)
}
svcDefs[s.Id] = *s
}

return svcDefs, nil
}

func readServiceDefinition(dirpath string) (*CloudServiceDetails, error) {
integrationJsonPath := path.Join(dirpath, "integration.json")

serializedSpec, err := serviceDefinitionFiles.ReadFile(integrationJsonPath)
if err != nil {
return nil, fmt.Errorf(
"couldn't find integration.json in %s: %w",
dirpath, err,
)
}

integrationSpec, err := koanfJson.Parser().Unmarshal(serializedSpec)
if err != nil {
return nil, fmt.Errorf(
"couldn't parse integration.json from %s: %w",
integrationJsonPath, err,
)
}

hydrated, err := integrations.HydrateFileUris(
integrationSpec, serviceDefinitionFiles, dirpath,
)
if err != nil {
return nil, fmt.Errorf(
"couldn't hydrate files referenced in service definition %s: %w",
integrationJsonPath, err,
)
}

hydratedSpec := hydrated.(map[string]interface{})
hydratedSpecJson, err := koanfJson.Parser().Marshal(hydratedSpec)
if err != nil {
return nil, fmt.Errorf(
"couldn't serialize hydrated integration spec back to JSON %s: %w",
integrationJsonPath, err,
)
}

var serviceDef CloudServiceDetails
decoder := json.NewDecoder(bytes.NewReader(hydratedSpecJson))
decoder.DisallowUnknownFields()
err = decoder.Decode(&serviceDef)
if err != nil {
return nil, fmt.Errorf(
"couldn't parse hydrated JSON spec read from %s: %w",
integrationJsonPath, err,
)
}

err = validateServiceDefinition(serviceDef)
if err != nil {
return nil, fmt.Errorf("invalid service definition %s: %w", serviceDef.Id, err)
}

return &serviceDef, nil

}

func validateServiceDefinition(s CloudServiceDetails) error {
// Validate dashboard data
seenDashboardIds := map[string]interface{}{}
for _, dd := range s.Assets.Dashboards {
did, exists := dd["id"]
if !exists {
return fmt.Errorf("id is required. not specified in dashboard titled %v", dd["title"])
}
dashboardId, ok := did.(string)
if !ok {
return fmt.Errorf("id must be string in dashboard titled %v", dd["title"])
}
if _, seen := seenDashboardIds[dashboardId]; seen {
return fmt.Errorf("multiple dashboards found with id %s", dashboardId)
}
seenDashboardIds[dashboardId] = nil
}

// potentially more to follow

return nil
}
34 changes: 34 additions & 0 deletions pkg/query-service/app/cloudintegrations/availableServices_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package cloudintegrations

import (
"testing"

"github.com/stretchr/testify/require"
"go.signoz.io/signoz/pkg/query-service/model"
)

func TestAvailableServices(t *testing.T) {
require := require.New(t)

// should be able to list available services.
_, apiErr := listCloudProviderServices("bad-cloud-provider")
require.NotNil(apiErr)
require.Equal(model.ErrorNotFound, apiErr.Type())

awsSvcs, apiErr := listCloudProviderServices("aws")
require.Nil(apiErr)
require.Greater(len(awsSvcs), 0)

// should be able to get details of a service
_, apiErr = getCloudProviderService(
"aws", "bad-service-id",
)
require.NotNil(apiErr)
require.Equal(model.ErrorNotFound, apiErr.Type())

svc, apiErr := getCloudProviderService(
"aws", awsSvcs[0].Id,
)
require.Nil(apiErr)
require.Equal(*svc, awsSvcs[0])
}
Loading
Loading