Skip to content

Commit 10f1ef8

Browse files
javrudskyHarness
authored andcommitted
feat: [fme-7194]: Added CCM GraphQL for budget, metadata and recommendations (#47)
* [fme-7194] - Return NewToolResultError instead of generic error * [fme-7194] - Merge with master * [fme-7194] - Fixing view id issue * [fme-7168] - Fixing tool name. fetch_ccm_metadata to get_ccm_metadata * [fme-7168] - Adding Perspective Recommendations operation * [fme-7168] - Added Fetch CCM Metadata Tool (FetchCcmMetadata graphQL Operation) * [fme-7154] Adding master changes * [fme-7154] - Added GraphQL perspective Budget tool * [fme-7133] - Using MM/DD/YYYY format in LastPeriodCostCcmPerspective tool. Removing unused imports * [fme-7133] - Merge with master * [fme-7133] Added Get Perspective Grid with budget graphql query * [fme-5745] - Added perspective time series graphql API * [fme-4352] - Fixing group by AWS fields constants * [fme-4352] Fix for key values filter parameter config * [fme-4352] Improving description for MCP tool for ccm_perspective_grid * [fme-4352] Fixing filter field description * [fme-4352] - Adding filtering and grouping by key/value field
1 parent 06b64a2 commit 10f1ef8

File tree

7 files changed

+214
-29
lines changed

7 files changed

+214
-29
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ Toolset Name: `cloudcostmanagement`
9999
- `ccm_perspective_summary_with_budget`: Query a summary of cost perspectives with budget information in Harness Cloud Cost Management, including detailed cost and budget data grouped by time.
100100
- `ccm_perspective_budget`: Query the budget information for a perspective in Harness Cloud Cost Management.
101101
- `get_ccm_metadata`: Retrieves metadata about available cloud connectors, cost data sources, default perspectives, and currency preferences in Harness Cloud Cost Management.
102+
- `ccm_perspective_recommendations`: PerspectiveRecommendations: Returns monthly cost, savings, and a list of open recommendations for a perspective in Harness Cloud Cost Management.
102103
- `get_ccm_commitment_coverage`: Get commitment coverage information for an account in Harness Cloud Cost Management
103104

104105
#### Chaos Engineering Toolset

client/ccmcommons/ccmgraphqlqueries.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,3 +192,26 @@ query FetchCcmMetaData {
192192
}
193193
}
194194
`
195+
const CCMPerspectiveRecommendationsQuery = `
196+
query PerspectiveRecommendations($filter: RecommendationFilterDTOInput) {
197+
recommendationStatsV2(filter: $filter) {
198+
totalMonthlyCost
199+
totalMonthlySaving
200+
count
201+
__typename
202+
}
203+
recommendationsV2(filter: $filter) {
204+
items {
205+
clusterName
206+
namespace
207+
id
208+
resourceType
209+
resourceName
210+
monthlyCost
211+
monthlySaving
212+
__typename
213+
}
214+
__typename
215+
}
216+
}
217+
`

client/ccmgraphqlperspectives.go

Lines changed: 55 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"fmt"
66
"time"
77
"strings"
8+
"encoding/json"
89
"log/slog"
910
"github.com/harness/harness-mcp/client/dto"
1011
"github.com/harness/harness-mcp/client/ccmcommons"
@@ -20,7 +21,7 @@ func (r *CloudCostManagementService) PerspectiveGrid(ctx context.Context, scope
2021

2122
gqlQuery := ccmcommons.CCMPerspectiveGridQuery
2223
variables := map[string]any{
23-
"filters": buildFilters(options.TimeFilter, options.Filters, options.KeyValueFilters),
24+
"filters": buildFilters(options.ViewId, options.TimeFilter, options.Filters, options.KeyValueFilters),
2425
"groupBy": buildGroupBy(options.GroupBy, outputFields, outputKeyValueFields),
2526
"limit": options.Limit,
2627
"offset": options.Offset,
@@ -36,7 +37,7 @@ func (r *CloudCostManagementService) PerspectiveGrid(ctx context.Context, scope
3637
"variables": variables,
3738
}
3839

39-
slog.Debug("PerspectiveGrid", "Payload", payload)
40+
debugPayload("PerspectiveGrid", payload)
4041
result := new(dto.CCMPerspectiveGridResponse)
4142
err := r.Client.Post(ctx, path, nil, payload, &result)
4243
if err != nil {
@@ -60,7 +61,7 @@ func (r *CloudCostManagementService) PerspectiveTimeSeries(ctx context.Context,
6061
}
6162

6263
variables := map[string]any{
63-
"filters": buildFilters(options.TimeFilter, options.Filters, options.KeyValueFilters),
64+
"filters": buildFilters(options.ViewId, options.TimeFilter, options.Filters, options.KeyValueFilters),
6465
"groupBy": []map[string]any{timeTruncGroupBy, entityGroupBy[0]},
6566
"limit": options.Limit,
6667
"offset": options.Offset,
@@ -90,7 +91,7 @@ func (r *CloudCostManagementService) PerspectiveSummaryWithBudget(ctx context.Co
9091

9192
gqlQuery := ccmcommons.CCMPerspectiveSummaryWithBudgetQuery
9293
variables := map[string]any{
93-
"filters": buildFilters(options.TimeFilter, options.Filters, options.KeyValueFilters),
94+
"filters": buildFilters(options.ViewId, options.TimeFilter, options.Filters, options.KeyValueFilters),
9495
"groupBy": buildGroupBy(options.GroupBy, outputFields, outputKeyValueFields),
9596
"limit": options.Limit,
9697
"offset": options.Offset,
@@ -106,7 +107,7 @@ func (r *CloudCostManagementService) PerspectiveSummaryWithBudget(ctx context.Co
106107
"variables": variables,
107108
}
108109

109-
slog.Debug("PerspectiveSummaryWithBudget", "Payload", payload)
110+
debugPayload("PerspectiveSummaryWithBudget", payload)
110111
result := new(dto.CCMPerspectiveSummaryWithBudgetResponse)
111112
err := r.Client.Post(ctx, path, nil, payload, &result)
112113
if err != nil {
@@ -129,7 +130,7 @@ func (r *CloudCostManagementService) PerspectiveBudget(ctx context.Context, scop
129130
"variables": variables,
130131
}
131132

132-
slog.Debug("PerspectiveBudget", "Payload", payload)
133+
debugPayload("PerspectiveBudget", payload)
133134
result := new(dto.CCMPerspectiveBudgetResponse)
134135
err := r.Client.Post(ctx, path, nil, payload, &result)
135136
if err != nil {
@@ -151,7 +152,7 @@ func (r *CloudCostManagementService) GetCcmMetadata(ctx context.Context, scope d
151152
"variables": variables,
152153
}
153154

154-
slog.Debug("FetchCcmMetadata", "Payload", payload)
155+
debugPayload("FetchCcmMetadata", payload)
155156
result := new(dto.CCMMetadataResponse)
156157
err := r.Client.Post(ctx, path, nil, payload, &result)
157158
if err != nil {
@@ -160,13 +161,42 @@ func (r *CloudCostManagementService) GetCcmMetadata(ctx context.Context, scope d
160161
return result, nil
161162
}
162163

163-
func buildFilters(timeFilters string, idFilters dto.CCMGraphQLFilters, keyValueFilters dto.CCMGraphQLKeyValueFilters) ([]map[string]any) {
164+
func (r *CloudCostManagementService) PerspectiveRecommendations(ctx context.Context, scope dto.Scope, options *dto.CCMPerspectiveRecommendationsOptions) (*dto.CCMPerspectiveRecommendationsResponse, error) {
165+
path := fmt.Sprintf(ccmPerspectiveGraphQLPath, options.AccountId, options.AccountId)
166+
167+
gqlQuery := ccmcommons.CCMPerspectiveRecommendationsQuery
168+
169+
variables := map[string]any{
170+
"filter": map[string]any{
171+
"perspectiveFilters": buildFilters(options.ViewId, options.TimeFilter, options.Filters, options.KeyValueFilters),
172+
"limit": options.Limit,
173+
"offset": options.Offset,
174+
"minSaving": options.MinSaving,
175+
"recommendationStates": options.RecommendationStates,
176+
},
177+
}
178+
179+
payload := map[string]any{
180+
"query": gqlQuery,
181+
"operationName": "PerspectiveRecommendations",
182+
"variables": variables,
183+
}
184+
185+
debugPayload("PerspectiveRecommendations", payload)
186+
result := new(dto.CCMPerspectiveRecommendationsResponse)
187+
err := r.Client.Post(ctx, path, nil, payload, &result)
188+
if err != nil {
189+
return nil, fmt.Errorf("failed to get perspective recommendations: %w", err)
190+
}
191+
return result, nil
192+
}
193+
194+
func buildFilters(viewId string ,timeFilters string, idFilters dto.CCMGraphQLFilters, keyValueFilters dto.CCMGraphQLKeyValueFilters) ([]map[string]any) {
164195
filters := []map[string]any{}
165196
viewFilter := []map[string]any{
166197
{
167198
"viewMetadataFilter": map[string]any{
168-
//"viewId": options.ViewId,
169-
"viewId": "VZf-WROOTyeczYa4FMkhYg",
199+
"viewId": viewId,
170200
"isPreview": false,
171201
},
172202
},
@@ -177,8 +207,6 @@ func buildFilters(timeFilters string, idFilters dto.CCMGraphQLFilters, keyValueF
177207
filters = append(filters, buildFieldFilters(idFilters, outputFields)...)
178208
filters = append(filters, buildKeyValueFieldFilters(keyValueFilters, outputKeyValueFields)...)
179209

180-
slog.Debug("PerspectiveGrid", "FILTERS", filters)
181-
182210
return filters
183211
}
184212

@@ -470,3 +498,18 @@ func buildGroupBy(input map[string]any, outputFields []map[string]string, output
470498
}
471499
return defaultGroupBy
472500
}
501+
502+
func debugPayload(operation string, payload map[string]any) {
503+
jsonPayload := mapToJSONString(payload)
504+
slog.Debug("-----------", "----------", "--------------")
505+
slog.Debug(operation, "Payload", jsonPayload)
506+
slog.Debug("-----------", "----------", "--------------")
507+
}
508+
509+
func mapToJSONString(m map[string]any) (string) {
510+
b, err := json.MarshalIndent(m, "", " ")
511+
if err != nil {
512+
return ""
513+
}
514+
return string(b)
515+
}

client/dto/ccmgraphqlperspectives.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,3 +211,34 @@ type CCMMetadataResponse struct {
211211
CCMMetadata CCMMetadata `json:"ccmMetaData"`
212212
} `json:"data"`
213213
}
214+
215+
type CCMPerspectiveRecommendationsOptions struct {
216+
AccountId string `json:"account_id"`
217+
ViewId string `json:"view_id"`
218+
TimeFilter string `json:"time_filter"`
219+
Limit int32 `json:"limit"`
220+
Offset int32 `json:"offset"`
221+
MinSaving int `json:"min_saving"`
222+
Filters CCMGraphQLFilters
223+
KeyValueFilters CCMGraphQLKeyValueFilters
224+
RecommendationStates []string
225+
}
226+
227+
type CCMRecommendationStatsV2 struct {
228+
TotalMonthlyCost float64 `json:"totalMonthlyCost"`
229+
TotalMonthlySaving float64 `json:"totalMonthlySaving"`
230+
Count int `json:"count"`
231+
Typename string `json:"__typename"`
232+
}
233+
234+
type CCMRecommendationsV2 struct {
235+
Items []any `json:"items"`
236+
Typename string `json:"__typename"`
237+
}
238+
239+
type CCMPerspectiveRecommendationsResponse struct {
240+
Data struct {
241+
RecommendationStatsV2 CCMRecommendationStatsV2 `json:"recommendationStatsV2"`
242+
RecommendationsV2 CCMRecommendationsV2 `json:"recommendationsV2"`
243+
} `json:"data"`
244+
}

pkg/ccmcommons/ccmconstants.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,13 @@ Query detailed time series perspective data in Harness Cloud Cost Management.
2323
var CCMPerspectiveSummaryWithBudgetDescription = `
2424
Query detailed time series perspective data in Harness Cloud Cost Management.
2525
`
26+
var CCMGetCcmMetadataDescription = `
27+
Get metadata about available cloud connectors, cost data sources, default perspectives, and currency preferences in Harness Cloud Cost Management.
28+
`
29+
30+
var CCMPerspectiveRecommendationsDescription = `
31+
Returns monthly cost, savings, and a list of open recommendations for a perspective in Harness Cloud Cost Management.
32+
`
2633

2734
var commonFilterDesc = `It is applied using LIKE operator in an array. Example: ["value1", "value2", ...]`
2835
var CCMFilterFields = []map[string]string{

0 commit comments

Comments
 (0)