Skip to content

Commit 0ec4c52

Browse files
archit-harnessHarness
authored andcommitted
[feat]: [DBOPS-1394]: Adding support for get_database_info to retrieve database metadata for a given schema identifier (#63)
* [feat]: [DBOPS-1394]: Addressing comments * [feat]: [DBOPS-1394]: Addressing comments * [feat]: [DBOPS-1394]: Addressing comments * [feat]: [DBOPS-1394]: Adding support for get_database_info to retrieve database metadata for a given schema identifier * [feat]: [DBOPS-1394]: Adding support for get_database_info to retrieve database metadata for a given schema identifier * [feat]: [DBOPS-1394]: Adding support for get_database_info to retrieve database metadata for a given schema identifier
1 parent e404d85 commit 0ec4c52

File tree

9 files changed

+1558
-4
lines changed

9 files changed

+1558
-4
lines changed

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,12 @@ Toolset Name: `cloudcostmanagement`
119119
- `get_ccm_commitment_coverage`: Get commitment coverage information for an account in Harness Cloud Cost Management
120120
- `get_ccm_commitment_savings`: Get commitment savings information for an account in Harness Cloud Cost Management
121121

122+
#### Database Operations Toolset
123+
124+
Toolset Name: `dbops`
125+
126+
- `get_database_schema_info`: Retrieves metadata about a database schema including its identifier, instance identifier, and database type.
127+
122128
#### Chaos Engineering Toolset
123129

124130
Toolset Name: `chaos`

client/common/scopeutils.go

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
package common
2+
3+
import (
4+
"strings"
5+
6+
"github.com/harness/harness-mcp/client/dto"
7+
)
8+
9+
const (
10+
// Scope constants
11+
AccountScope = "account"
12+
OrgScope = "org"
13+
ProjectScope = "project"
14+
15+
// Prefix constants
16+
AccountPrefix = "account."
17+
OrgPrefix = "org."
18+
)
19+
20+
// GetScopeOfEntity determines the scope level of an entity reference
21+
func GetScopeOfEntity(entityRef string) string {
22+
if strings.HasPrefix(entityRef, AccountPrefix) {
23+
return AccountScope
24+
}
25+
if strings.HasPrefix(entityRef, OrgPrefix) {
26+
return OrgScope
27+
}
28+
return ProjectScope
29+
}
30+
31+
// GetEntityIDFromRef extracts the actual entity ID from a scoped reference
32+
func GetEntityIDFromRef(entityRef string) string {
33+
if entityRef == "" {
34+
return ""
35+
}
36+
if strings.HasPrefix(entityRef, AccountPrefix) {
37+
if len(entityRef) <= len(AccountPrefix) {
38+
return ""
39+
}
40+
return entityRef[len(AccountPrefix):]
41+
}
42+
if strings.HasPrefix(entityRef, OrgPrefix) {
43+
if len(entityRef) <= len(OrgPrefix) {
44+
return ""
45+
}
46+
return entityRef[len(OrgPrefix):]
47+
}
48+
return entityRef
49+
}
50+
51+
// IsScopeValid checks if the provided scope parameters are valid
52+
func IsScopeValid(scope dto.Scope, entityScope string) bool {
53+
switch entityScope {
54+
case AccountScope:
55+
return scope.AccountID != ""
56+
case OrgScope:
57+
return scope.AccountID != "" && scope.OrgID != ""
58+
case ProjectScope:
59+
return scope.AccountID != "" && scope.OrgID != "" && scope.ProjectID != ""
60+
default:
61+
return false
62+
}
63+
}
64+
65+
// GetScopeFromEntityRef creates a scope object from an entity reference
66+
func GetScopeFromEntityRef(baseScope dto.Scope, entityRef string) dto.Scope {
67+
entityScope := GetScopeOfEntity(entityRef)
68+
69+
switch entityScope {
70+
case AccountScope:
71+
return dto.Scope{
72+
AccountID: baseScope.AccountID,
73+
}
74+
case OrgScope:
75+
return dto.Scope{
76+
AccountID: baseScope.AccountID,
77+
OrgID: baseScope.OrgID,
78+
}
79+
default: // ProjectScope
80+
return baseScope
81+
}
82+
}

client/dbops/dbops.go

Lines changed: 279 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,279 @@
1+
package dbops
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"strings"
7+
8+
"github.com/harness/harness-mcp/client"
9+
"github.com/harness/harness-mcp/client/common"
10+
"github.com/harness/harness-mcp/client/dbops/generated"
11+
"github.com/harness/harness-mcp/client/dto"
12+
)
13+
14+
// DbInfo represents the database information including schema identifier, instance identifier, and database type
15+
type DbInfo struct {
16+
SchemaIdentifier string
17+
InstanceIdentifier string
18+
DatabaseType string
19+
}
20+
21+
// Client is the client for database operations
22+
type Client struct {
23+
DBOpsClient *generated.ClientWithResponses
24+
ConnectorClient *client.ConnectorService
25+
}
26+
27+
// NewClient creates a new dbops client
28+
func NewClient(dbOpsClient *generated.ClientWithResponses, connectorClient *client.ConnectorService) *Client {
29+
return &Client{
30+
DBOpsClient: dbOpsClient,
31+
ConnectorClient: connectorClient,
32+
}
33+
}
34+
35+
// validateDBInfoParams validates the parameters required for database info operations
36+
func validateDBInfoParams(accountID, org, project, dbSchemaIdentifier string) error {
37+
if accountID == "" {
38+
return fmt.Errorf("accountID cannot be empty")
39+
}
40+
if org == "" {
41+
return fmt.Errorf("org cannot be empty")
42+
}
43+
if project == "" {
44+
return fmt.Errorf("project cannot be empty")
45+
}
46+
if dbSchemaIdentifier == "" {
47+
return fmt.Errorf("dbSchemaIdentifier cannot be empty")
48+
}
49+
return nil
50+
}
51+
52+
// validateConnectorParams validates the parameters required for connector operations
53+
func validateConnectorParams(accountID, connectorID string) error {
54+
if accountID == "" {
55+
return fmt.Errorf("accountID cannot be empty")
56+
}
57+
if connectorID == "" {
58+
return fmt.Errorf("connectorID cannot be empty")
59+
}
60+
return nil
61+
}
62+
63+
// GetDatabaseInfo returns the schema identifier, instance identifier, and database type for a given database schema
64+
func (c *Client) GetDatabaseInfo(ctx context.Context, accountID, org, project, dbSchemaIdentifier string) (*DbInfo, error) {
65+
// Validate input parameters
66+
if err := validateDBInfoParams(accountID, org, project, dbSchemaIdentifier); err != nil {
67+
return nil, fmt.Errorf("failed to get database info: %w", err)
68+
}
69+
70+
// Step 1: Query V1GetProjDbSchemaWithResponse to get schema for given identifier
71+
params := &generated.V1GetProjDbSchemaParams{
72+
HarnessAccount: &accountID,
73+
}
74+
schemaResp, err := c.DBOpsClient.V1GetProjDbSchemaWithResponse(
75+
ctx,
76+
org,
77+
project,
78+
dbSchemaIdentifier,
79+
params,
80+
)
81+
if err != nil {
82+
return nil, fmt.Errorf("failed to get database schema: %w", err)
83+
}
84+
85+
if schemaResp.JSON200 == nil {
86+
return nil, fmt.Errorf("failed to get database info: invalid schema response")
87+
}
88+
89+
schema := schemaResp.JSON200
90+
91+
// Initialize the result with schema information
92+
result := &DbInfo{
93+
SchemaIdentifier: schema.Identifier,
94+
}
95+
96+
// Step 2: Get instance identifier
97+
var instanceIdentifier string
98+
var connectorID string
99+
100+
if schema.PrimaryDbInstanceId != nil && *schema.PrimaryDbInstanceId != "" {
101+
// Use PrimaryDbInstanceId
102+
instanceIdentifier = *schema.PrimaryDbInstanceId
103+
104+
// Get instance details
105+
instanceParams := &generated.V1GetProjDbSchemaInstanceParams{
106+
HarnessAccount: &accountID,
107+
}
108+
instanceResp, err := c.DBOpsClient.V1GetProjDbSchemaInstanceWithResponse(
109+
ctx,
110+
org,
111+
project,
112+
dbSchemaIdentifier,
113+
instanceIdentifier,
114+
instanceParams,
115+
)
116+
if err != nil {
117+
return nil, fmt.Errorf("failed to get database instance: %w", err)
118+
}
119+
120+
if instanceResp.JSON200 == nil {
121+
return nil, fmt.Errorf("invalid instance response")
122+
}
123+
124+
// Get connector ID from instance
125+
connectorID = instanceResp.JSON200.Connector
126+
result.InstanceIdentifier = instanceResp.JSON200.Identifier
127+
} else {
128+
// Step 3: If PrimaryDbInstanceId is empty, make list API call
129+
instancesResp, err := c.DBOpsClient.V1ListProjDbSchemaInstanceWithResponse(
130+
ctx,
131+
org,
132+
project,
133+
dbSchemaIdentifier,
134+
&generated.V1ListProjDbSchemaInstanceParams{
135+
HarnessAccount: &accountID,
136+
},
137+
generated.V1ListProjDbSchemaInstanceJSONRequestBody{},
138+
)
139+
if err != nil {
140+
return nil, fmt.Errorf("failed to list database instances: %w", err)
141+
}
142+
143+
if instancesResp.JSON200 == nil {
144+
return nil, fmt.Errorf("failed to get database info: no instances response")
145+
}
146+
147+
// Get the first instance
148+
instances := *instancesResp.JSON200
149+
if len(instances) == 0 {
150+
return nil, fmt.Errorf("no instances found for the schema")
151+
}
152+
firstInstance := instances[0]
153+
result.InstanceIdentifier = firstInstance.Identifier
154+
connectorID = firstInstance.Connector
155+
}
156+
157+
// Check if connectorID is empty
158+
if connectorID == "" {
159+
return nil, fmt.Errorf("no connector ID found for the database instance")
160+
}
161+
162+
// Get JDBC connector URL and determine database type
163+
_, dbType, err := c.getJDBCConnectorUrl(ctx, accountID, org, project, connectorID)
164+
if err != nil {
165+
return nil, err
166+
}
167+
168+
// Set the database type
169+
result.DatabaseType = dbType
170+
171+
return result, nil
172+
}
173+
174+
// getJDBCConnectorUrl retrieves the JDBC connector URL and database type from the connector service
175+
func (c *Client) getJDBCConnectorUrl(ctx context.Context, accountID, org, project, connectorID string) (string, string, error) {
176+
// Validate input parameters
177+
if err := validateConnectorParams(accountID, connectorID); err != nil {
178+
return "", "", fmt.Errorf("failed to get JDBC connector URL: %w", err)
179+
}
180+
// Create base scope for connector API
181+
baseScope := dto.Scope{
182+
AccountID: accountID,
183+
OrgID: org,
184+
ProjectID: project,
185+
}
186+
187+
// Process connector ID to handle scoped references
188+
entityScope := common.GetScopeOfEntity(connectorID)
189+
scope := common.GetScopeFromEntityRef(baseScope, connectorID)
190+
actualConnectorID := common.GetEntityIDFromRef(connectorID)
191+
192+
// Validate scope
193+
if !common.IsScopeValid(scope, entityScope) {
194+
return "", "", fmt.Errorf("failed to get JDBC connector URL: invalid scope for connector ID: %s", connectorID)
195+
}
196+
197+
// Call GetConnector to get connector details
198+
connector, err := c.ConnectorClient.GetConnector(ctx, scope, actualConnectorID)
199+
if err != nil {
200+
return "", "", fmt.Errorf("failed to get connector: %w", err)
201+
}
202+
203+
// Check if connector response is valid
204+
if connector == nil {
205+
return "", "", fmt.Errorf("failed to get JDBC connector URL: invalid connector response")
206+
}
207+
208+
// Check if connector type is JDBC
209+
if connector.Connector.Type != "JDBC" {
210+
return "", "", fmt.Errorf("failed to get JDBC connector URL: connector type is not JDBC: %s", connector.Connector.Type)
211+
}
212+
213+
// Extract URL from connector spec
214+
url := ""
215+
if connector.Connector.Spec != nil {
216+
if urlValue, exists := connector.Connector.Spec["url"]; exists {
217+
if urlStr, ok := urlValue.(string); ok {
218+
url = urlStr
219+
}
220+
}
221+
}
222+
223+
if url == "" {
224+
return "", "", fmt.Errorf("failed to get JDBC connector URL: no URL found in connector spec")
225+
}
226+
227+
// Extract database type from URL
228+
dbType := extractDatabaseTypeFromURL(url)
229+
230+
return url, dbType, nil
231+
}
232+
233+
// Database type constants
234+
const (
235+
DBTypePostgres = "POSTGRES"
236+
DBTypeMongoDB = "MONGODB"
237+
DBTypeMSSQL = "MSSQL"
238+
DBTypeUnknown = "UNKNOWN"
239+
)
240+
241+
// extractDatabaseTypeFromURL extracts the database type from a JDBC or MongoDB URL
242+
// Handles special cases for common database types and normalizes the output
243+
func extractDatabaseTypeFromURL(url string) string {
244+
// Validate input parameter
245+
if url == "" {
246+
return DBTypeUnknown
247+
}
248+
url = strings.ToLower(url)
249+
250+
// Check if it's a MongoDB URL
251+
if strings.HasPrefix(url, "mongodb:") {
252+
return DBTypeMongoDB
253+
}
254+
255+
// For JDBC URLs in format jdbc:<database>:...
256+
if strings.HasPrefix(url, "jdbc:") {
257+
if len(url) <= len("jdbc:") {
258+
return DBTypeUnknown
259+
}
260+
// Extract the database type between jdbc: and the next :
261+
parts := strings.SplitN(url[5:], ":", 2) // Skip "jdbc:" prefix
262+
if len(parts) > 0 {
263+
dbType := parts[0]
264+
// Handle special cases
265+
switch dbType {
266+
case "postgresql":
267+
return DBTypePostgres
268+
case "sqlserver":
269+
return DBTypeMSSQL
270+
default:
271+
// Convert to uppercase
272+
return strings.ToUpper(dbType)
273+
}
274+
}
275+
}
276+
277+
// Default to unknown if we can't determine
278+
return DBTypeUnknown
279+
}

0 commit comments

Comments
 (0)