Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
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
107 changes: 107 additions & 0 deletions docs/components/DigitalOcean.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ import { CardGrid, LinkCard } from "@astrojs/starlight/components";
<LinkCard title="Delete Snapshot" href="#delete-snapshot" description="Delete a DigitalOcean snapshot" />
<LinkCard title="Get Alert Policy" href="#get-alert-policy" description="Fetch details of a DigitalOcean monitoring alert policy" />
<LinkCard title="Get App" href="#get-app" description="Fetch details of a DigitalOcean App Platform application by ID" />
<LinkCard title="Get Cluster Configuration" href="#get-cluster-configuration" description="Retrieve the configuration details for a database cluster" />
<LinkCard title="Get Database" href="#get-database" description="Retrieve details of a specific managed database instance" />
<LinkCard title="Get Droplet" href="#get-droplet" description="Fetch details of a DigitalOcean Droplet by ID" />
<LinkCard title="Get Droplet Metrics" href="#get-droplet-metrics" description="Fetch CPU, memory, and network bandwidth metrics for a DigitalOcean Droplet" />
<LinkCard title="Get Object" href="#get-object" description="Retrieve an object and its metadata from DigitalOcean Spaces Object Storage" />
Expand Down Expand Up @@ -1125,6 +1127,111 @@ Returns the app object including:
}
```

<a id="get-cluster-configuration"></a>

## Get Cluster Configuration

The Get Cluster Configuration component retrieves the active configuration for a DigitalOcean Managed Database cluster.

### Use Cases

- **Audit workflows**: Inspect the active cluster configuration for reporting or compliance checks
- **Validation**: Compare the current cluster configuration before updates or maintenance
- **Operational visibility**: Retrieve engine-specific settings that affect behavior and performance

### Configuration

- **Database Cluster**: The managed database cluster to inspect (required)

### Output

Returns the cluster configuration including:
- **databaseClusterId**: The cluster UUID
- **databaseClusterName**: The cluster name
- **config**: The configuration object returned by the DigitalOcean API

### Important Notes

- If you use custom token scopes, this action requires `database:read`
- The keys inside `config` vary by database engine

### Example Output

```json
{
"config": {
"autovacuum_naptime": 60,
"backtrack_commit_timeout": 30,
"default_toast_compression": "pglz",
"idle_in_transaction_session_timeout": 0,
"jit": true,
"max_parallel_workers": 8
},
"databaseClusterId": "65b497a5-1674-4b1a-a122-01aebe761ef7",
"databaseClusterName": "superplane-db-test"
}
```

<a id="get-database"></a>

## Get Database

The Get Database component retrieves a managed database from a DigitalOcean cluster and enriches it with cluster context.

### Use Cases

- **Routing decisions**: Inspect the database and cluster state before directing traffic or jobs
- **Operational checks**: Review engine, region, and connection details before maintenance steps
- **Audit workflows**: Retrieve the current database and cluster context for reporting or validation

### Configuration

- **Database Cluster**: The managed database cluster containing the database (required)
- **Database**: The database to retrieve (required)

### Output

Returns the requested database enriched with cluster details, including:
- **name**: The database name
- **databaseClusterId**: The cluster UUID
- **databaseClusterName**: The cluster name
- **engine**: The cluster engine
- **version**: The cluster engine version
- **region**: The cluster region
- **status**: The cluster status
- **connection**: Connection information when available
- **database**: The raw database object returned by the API

### Important Notes

- If you use custom token scopes, this action requires `database:read`
- Database management is not supported for Caching or Valkey clusters

### Example Output

```json
{
"connection": {
"database": "defaultdb",
"host": "superplane-db-test-do-user-1234567-0.j.db.ondigitalocean.com",
"port": 25060,
"ssl": true,
"uri": "postgresql://doadmin@superplane-db-test-do-user-1234567-0.j.db.ondigitalocean.com:25060/defaultdb?sslmode=require",
"user": "doadmin"
},
"database": {
"name": "app_db"
},
"databaseClusterId": "65b497a5-1674-4b1a-a122-01aebe761ef7",
"databaseClusterName": "superplane-db-test",
"engine": "pg",
"name": "app_db",
"region": "nyc1",
"status": "online",
"version": "17"
}
```

<a id="get-droplet"></a>

## Get Droplet
Expand Down
126 changes: 126 additions & 0 deletions pkg/integrations/digitalocean/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -2289,6 +2289,132 @@ func (c *Client) ListApps() ([]App, error) {
return response.Apps, nil
}

type DatabaseClusterConnection struct {
Host string `json:"host,omitempty"`
Port int `json:"port,omitempty"`
User string `json:"user,omitempty"`
Database string `json:"database,omitempty"`
URI string `json:"uri,omitempty"`
SSL bool `json:"ssl,omitempty"`
}

type DatabaseCluster struct {
ID string `json:"id"`
Name string `json:"name"`
Engine string `json:"engine,omitempty"`
Version string `json:"version,omitempty"`
Region string `json:"region,omitempty"`
Size string `json:"size,omitempty"`
NumNodes int `json:"num_nodes,omitempty"`
Status string `json:"status,omitempty"`
CreatedAt string `json:"created_at,omitempty"`
PrivateNetworkUUID string `json:"private_network_uuid,omitempty"`
Connection *DatabaseClusterConnection `json:"connection,omitempty"`
PrivateConnection *DatabaseClusterConnection `json:"private_connection,omitempty"`
}

type Database struct {
Name string `json:"name"`
}

func (c *Client) ListDatabaseClusters() ([]DatabaseCluster, error) {
url := fmt.Sprintf("%s/databases?per_page=200", c.BaseURL)
responseBody, err := c.execRequest(http.MethodGet, url, nil)
if err != nil {
return nil, err
}

var response struct {
Databases []DatabaseCluster `json:"databases"`
}

if err := json.Unmarshal(responseBody, &response); err != nil {
return nil, fmt.Errorf("error parsing response: %v", err)
}

return response.Databases, nil
}

func (c *Client) GetDatabaseCluster(clusterID string) (*DatabaseCluster, error) {
url := fmt.Sprintf("%s/databases/%s", c.BaseURL, clusterID)
responseBody, err := c.execRequest(http.MethodGet, url, nil)
if err != nil {
return nil, err
}

var response struct {
Database DatabaseCluster `json:"database"`
}

if err := json.Unmarshal(responseBody, &response); err != nil {
return nil, fmt.Errorf("error parsing response: %v", err)
}

return &response.Database, nil
}

func (c *Client) GetDatabaseClusterConfig(clusterID string) (map[string]any, error) {
url := fmt.Sprintf("%s/databases/%s/config", c.BaseURL, clusterID)
responseBody, err := c.execRequest(http.MethodGet, url, nil)
if err != nil {
return nil, err
}

var response struct {
Config map[string]any `json:"config"`
}

if err := json.Unmarshal(responseBody, &response); err != nil {
return nil, fmt.Errorf("error parsing response: %v", err)
}

if response.Config == nil {
return map[string]any{}, nil
}

return response.Config, nil
}

func (c *Client) ListDatabases(clusterID string) ([]Database, error) {
url := fmt.Sprintf("%s/databases/%s/dbs", c.BaseURL, clusterID)
responseBody, err := c.execRequest(http.MethodGet, url, nil)
if err != nil {
return nil, err
}

var response struct {
Databases []Database `json:"dbs"`
}

if err := json.Unmarshal(responseBody, &response); err != nil {
return nil, fmt.Errorf("error parsing response: %v", err)
}

return response.Databases, nil
}

func (c *Client) GetDatabase(clusterID, databaseName string) (map[string]any, error) {
url := fmt.Sprintf("%s/databases/%s/dbs/%s", c.BaseURL, clusterID, url.PathEscape(databaseName))
responseBody, err := c.execRequest(http.MethodGet, url, nil)
if err != nil {
return nil, err
}

var response struct {
Database map[string]any `json:"db"`
}

if err := json.Unmarshal(responseBody, &response); err != nil {
return nil, fmt.Errorf("error parsing response: %v", err)
}

if response.Database == nil {
return map[string]any{}, nil
}

return response.Database, nil
}

// AppNodeMetadata stores metadata about an app for display in the UI
type AppNodeMetadata struct {
AppID string `json:"appId" mapstructure:"appId"`
Expand Down
115 changes: 115 additions & 0 deletions pkg/integrations/digitalocean/database_metadata.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
package digitalocean

import (
"fmt"
"strings"

"github.com/mitchellh/mapstructure"
"github.com/superplanehq/superplane/pkg/core"
)

type DatabaseNodeMetadata struct {
DatabaseClusterID string `json:"databaseClusterId" mapstructure:"databaseClusterId"`
DatabaseClusterName string `json:"databaseClusterName" mapstructure:"databaseClusterName"`
DatabaseName string `json:"databaseName" mapstructure:"databaseName"`
}

func resolveDatabaseClusterMetadata(ctx core.SetupContext, clusterID string) error {
if strings.Contains(clusterID, "{{") {
return ctx.Metadata.Set(DatabaseNodeMetadata{
DatabaseClusterID: clusterID,
DatabaseClusterName: clusterID,
})
}

var existing DatabaseNodeMetadata
err := mapstructure.Decode(ctx.Metadata.Get(), &existing)
if err == nil && existing.DatabaseClusterID == clusterID && existing.DatabaseClusterName != "" {
return nil
}

client, err := NewClient(ctx.HTTP, ctx.Integration)
if err != nil {
return fmt.Errorf("failed to create client: %w", err)
}

clusters, err := client.ListDatabaseClusters()
if err != nil {
return fmt.Errorf("failed to list database clusters: %w", err)
}

clusterName := clusterID
found := false
for _, cluster := range clusters {
if cluster.ID != clusterID {
continue
}

found = true
if cluster.Name != "" {
clusterName = cluster.Name
}
break
}

if !found {
return fmt.Errorf("database cluster %q was not found", clusterID)
}

return ctx.Metadata.Set(DatabaseNodeMetadata{
DatabaseClusterID: clusterID,
DatabaseClusterName: clusterName,
DatabaseName: existing.DatabaseName,
})
}

func resolveDatabaseMetadata(ctx core.SetupContext, clusterID, databaseName string) error {
if err := resolveDatabaseClusterMetadata(ctx, clusterID); err != nil {
return err
}

if strings.Contains(databaseName, "{{") {
var existing DatabaseNodeMetadata
_ = mapstructure.Decode(ctx.Metadata.Get(), &existing)
existing.DatabaseName = databaseName
return ctx.Metadata.Set(existing)
}

var existing DatabaseNodeMetadata
if err := mapstructure.Decode(ctx.Metadata.Get(), &existing); err == nil {
if existing.DatabaseClusterID == clusterID && existing.DatabaseName == databaseName {
return nil
}
}

client, err := NewClient(ctx.HTTP, ctx.Integration)
if err != nil {
return fmt.Errorf("failed to create client: %w", err)
}

databases, err := client.ListDatabases(clusterID)
if err != nil {
return fmt.Errorf("failed to list databases for cluster %q: %w", clusterID, err)
}

found := false
for _, database := range databases {
if database.Name != databaseName {
continue
}
found = true
break
}

if !found {
return fmt.Errorf("database %q was not found in cluster %q", databaseName, clusterID)
}

existing.DatabaseClusterID = clusterID
if existing.DatabaseClusterName == "" {
existing.DatabaseClusterName = clusterID
}
existing.DatabaseName = databaseName

return ctx.Metadata.Set(existing)
}
2 changes: 2 additions & 0 deletions pkg/integrations/digitalocean/digitalocean.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,8 @@ func (d *DigitalOcean) Components() []core.Component {
&UpdateAlertPolicy{},
&DeleteAlertPolicy{},
&GetDropletMetrics{},
&GetDatabase{},
&GetClusterConfiguration{},
&GetObject{},
&PutObject{},
&CopyObject{},
Expand Down
Loading
Loading