Skip to content

Commit cb3da05

Browse files
javrudskyHarness
authored andcommitted
feat: [fme-8278]: Added update and delete CCM Perspective tool. Also Update recommendation stats and override savings (#88)
* [fme-8278] Removed refactor for code deduplication requested in PR review * Merge branch 'master' into fme-8278_ccm_update_persp * [fme-8278] Using slog library * [fme-8278] Merge with master * [fme-8278] Added destructive hint annotation to mutable tools * [fme-8278] Update mutable recommendations tools description * [fme-8278] Added prompt and Improved descriptions for mutable perspective APIs to ask for confirmation before proceed. * [fme-8278] Applied AI review comments * [fme-8278] Merge with master to solve conflitcs on PR * [fme-8278] Added delete CCM Perspective tool * [fme-8278] Added Update perspective tool * [fme-8259] Added Override recommendations Savings and Update recommendation State * [fme-8259] Applying some fixes from previous PR * [fme-8233] Applying review changes * [fme-8259] Adding Recommendations State update and Override Savings APIs (CCM) * [fme8259] Added update recommedation state API * [fme-8233] - Merge with master * [fme-8233] Improving
1 parent e7920b3 commit cb3da05

File tree

16 files changed

+682
-47
lines changed

16 files changed

+682
-47
lines changed

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,8 @@ Toolset Name: `ccm`
140140
- `get_last_period_cost_ccm_perspective`: Retrieves the cost for a specified period and perspective within a given account.
141141
- `get_last_twelve_months_cost_ccm_perspective`: Retrieves a monthly cost breakdown for the past 12 months for a perspective within a specified account.
142142
- `create_ccm_perspective`: Creates a perspective for a specified account.
143+
- `update_ccm_perspective`: Updates a perspective for a specified account.
144+
- `delete_ccm_perspective`: Deletes a perspective for a specified account.
143145
- `ccm_perspective_grid`: Query detailed cost perspective data in Harness Cloud Cost Management.
144146
- `ccm_perspective_time_series`: Query detailed cost perspective data, grouped by time in Harness Cloud Cost Management.
145147
- `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.
@@ -150,6 +152,8 @@ Toolset Name: `ccm`
150152
- `list_ccm_recommendations`: Returns a filterable list of cost-optimization recommendations in Harness Cloud Cost Management.
151153
- `list_ccm_recommendations_by_resource_type`: Returns a aggregated statistics of cloud cost optimization recommendations grouped by resource type within a given account in Harness Cloud Cost Management.
152154
- `get_ccm_recommendations_stats`: Returns overall statistics for cloud cost optimization recommendations within a given account in Harness Cloud Cost Management.
155+
- `update_ccm_recommendation_state`: Marks a recommendation as Applied/Open/Ignored in Harness Cloud Cost Management
156+
- `override_ccm_recommendation_savings`: Overrides savings for a recommendation in Harness Cloud Cost Management
153157
- `get_ccm_commitment_coverage`: Get commitment coverage information for an account in Harness Cloud Cost Management
154158
- `get_ccm_commitment_savings`: Get commitment savings information for an account in Harness Cloud Cost Management
155159
- `get_ccm_commitment_utilisation`: Get commitment utilisation information for an account in Harness Cloud Cost Management broken down by Reserved Instances and Savings Plans in day wise granularity.

client/ccmperspectives.go

Lines changed: 51 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,17 @@ package client
33
import (
44
"context"
55
"fmt"
6-
"log/slog"
76
"github.com/harness/harness-mcp/client/dto"
87
"github.com/harness/harness-mcp/pkg/utils"
8+
"log/slog"
99
)
1010
const (
1111
ccmPerspetiveDetailListPath = ccmBasePath + "/perspective/getAllPerspectives?accountIdentifier=%s"
1212
ccmGetPerspectivePath = ccmBasePath + "/perspective"
1313
ccmGetLastPeriodCostPerspectivePath = ccmBasePath + "/perspective/lastPeriodCost"
1414
ccmGetLastTwelveMonthCostPerspectivePath = ccmBasePath + "/perspective/lastYearMonthlyCost"
1515
ccmCreatePerspectivePath = ccmBasePath + "/perspective"
16+
ccmDeletePerspectivePath = ccmCreatePerspectivePath
1617
)
1718

1819
func (r *CloudCostManagementService) ListPerspectivesDetail(ctx context.Context, scope dto.Scope, opts *dto.CCMListPerspectivesDetailOptions) (*dto.CCMPerspectivesDetailList, error) {
@@ -121,7 +122,7 @@ func (r *CloudCostManagementService) GetLastTwelveMonthsCostPerspective(ctx cont
121122
return items, nil
122123
}
123124

124-
func (r *CloudCostManagementService) CreatePerspective(ctx context.Context, scope dto.Scope, opts *dto.CCMCreatePerspectiveOptions) (*dto.CCMCreatePerspectiveResponse, error) {
125+
func (r *CloudCostManagementService) CreateOrUpdatePerspective(ctx context.Context, scope dto.Scope, opts *dto.CCMCreatePerspectiveOptions, update bool) (*dto.CCMCreatePerspectiveResponse, error) {
125126
path := ccmCreatePerspectivePath
126127
params := make(map[string]string)
127128
if opts == nil {
@@ -134,20 +135,31 @@ func (r *CloudCostManagementService) CreatePerspective(ctx context.Context, scop
134135
params["updateTotalCost"] = utils.BoolToString(opts.UpdateTotalCost)
135136

136137
// Body payload
137-
body := map[string]interface{}{
138+
body := map[string]any{
138139
"name": opts.Body.Name,
139140
"viewVersion": opts.Body.ViewVersion,
140-
"viewTimeRange": map[string]interface{}{
141+
"viewTimeRange": map[string]any{
141142
"viewTimeRangeType": opts.Body.ViewTimeRange.ViewTimeRangeType,
142143
"startTime": opts.Body.ViewTimeRange.StartTime,
143144
"endTime": opts.Body.ViewTimeRange.EndTime,
144145
},
145146
"viewType": opts.Body.ViewType,
146147
"viewState": opts.Body.ViewState,
147-
"viewRules": opts.Body.ViewRules,
148148
}
149149

150-
awsPreferences := map[string]interface{}{
150+
if len(opts.Body.ViewRules) > 0 {
151+
body["viewRules"] = opts.Body.ViewRules
152+
}
153+
154+
if opts.Body.ViewVisualization != (dto.CCMViewVisualization{}) {
155+
body["viewVisualization"] = opts.Body.ViewVisualization
156+
}
157+
158+
if update {
159+
body["uuid"] = opts.Body.UUID
160+
}
161+
162+
awsPreferences := map[string]any{
151163
"includeDiscounts": opts.Body.ViewPreferences.AwsPreferences.IncludeDiscounts,
152164
"includeCredits": opts.Body.ViewPreferences.AwsPreferences.IncludeCredits,
153165
"includeRefunds": opts.Body.ViewPreferences.AwsPreferences.IncludeRefunds,
@@ -158,19 +170,19 @@ func (r *CloudCostManagementService) CreatePerspective(ctx context.Context, scop
158170
awsPreferences["awsCost"] = opts.Body.ViewPreferences.AwsPreferences.AwsCost
159171
}
160172

161-
viewPreferences := map[string]interface{}{
173+
viewPreferences := map[string]any{
162174
"showAnomalies": opts.Body.ViewPreferences.ShowAnomalies,
163175
"includeOthers": opts.Body.ViewPreferences.IncludeOthers,
164176
"includeUnallocatedCost": opts.Body.ViewPreferences.IncludeUnallocatedCost,
165177
"awsPreferences": awsPreferences,
166-
"gcpPreferences": map[string]interface{}{
178+
"gcpPreferences": map[string]any{
167179
"includeDiscounts": opts.Body.ViewPreferences.GcpPreferences.IncludeDiscounts,
168180
"includeTaxes": opts.Body.ViewPreferences.GcpPreferences.IncludeTaxes,
169181
},
170182
}
171183

172184
if opts.Body.ViewPreferences.AzureViewPreferences.CostType != "" {
173-
azureViewPreferences := map[string]interface{}{
185+
azureViewPreferences := map[string]any{
174186
"costType": opts.Body.ViewPreferences.AzureViewPreferences.CostType,
175187
}
176188
viewPreferences["azureViewPreferences"] = azureViewPreferences
@@ -186,12 +198,38 @@ func (r *CloudCostManagementService) CreatePerspective(ctx context.Context, scop
186198
body["folderId"] = opts.Body.FolderId
187199
}
188200

189-
slog.Debug("Create Perspective", "Body", body)
190201
item := new(dto.CCMCreatePerspectiveResponse)
191-
err := r.Client.Post(ctx, path, params, body, &item)
192-
if err != nil {
193-
return nil, fmt.Errorf("failed to create cloud cost management perspective: %w", err)
202+
if !update {
203+
slog.Debug("Creating perspective", "body", body)
204+
err := r.Client.Post(ctx, path, params, body, &item)
205+
if err != nil {
206+
return nil, fmt.Errorf("Failed to create cloud cost management perspective: %w", err)
207+
}
208+
} else {
209+
slog.Debug("Updating perspective", "body", body)
210+
err := r.Client.Put(ctx, path, params, body, &item)
211+
if err != nil {
212+
return nil, fmt.Errorf("Failed to update cloud cost management perspective: %w", err)
213+
}
194214
}
195215

196216
return item, nil
197217
}
218+
219+
func (r *CloudCostManagementService) DeletePerspective(ctx context.Context, scope dto.Scope, accountId string, perspectiveId string) (*dto.CCMBaseResponse, error) {
220+
path := ccmDeletePerspectivePath
221+
params := make(map[string]string)
222+
addScope(scope, params)
223+
224+
params["accountId"] = accountId
225+
params["perspectiveId"] = perspectiveId
226+
227+
response := new(dto.CCMBaseResponse)
228+
229+
err := r.Client.Delete(ctx, path, params, nil, &response)
230+
if err != nil {
231+
return nil, fmt.Errorf("failed to delete a cloud cost management perspective: %w", err)
232+
}
233+
234+
return response, nil
235+
}

client/ccmrecommendations.go

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,16 @@ import (
44
"context"
55
"fmt"
66
"github.com/harness/harness-mcp/client/dto"
7+
"strconv"
78
)
89
const (
910
ccmRecommendationsListPath = ccmBasePath + "/recommendation/overview/list?accountIdentifier=%s"
1011
ccmRecommendationsByResourceTypeListPath = ccmBasePath + "/recommendation/overview/resource-type/stats?accountIdentifier=%s"
1112
ccmRecommendationsStatsPath = ccmBasePath + "/recommendation/overview/stats?accountIdentifier=%s"
13+
ccmUpdateRecommendationState = ccmBasePath + "/recommendation/overview/change-state?accountIdentifier=%s"
14+
ccmOverrideRecommendationSavings = ccmBasePath + "/recommendation/overview/override-savings?accountIdentifier=%s"
1215
)
1316

14-
1517
func (r *CloudCostManagementService) ListRecommendations(ctx context.Context, scope dto.Scope, accountId string, options map[string]any) (*map[string]any, error) {
1618

1719
return r.getRecommendations(ctx, scope, accountId, options, ccmRecommendationsListPath)
@@ -27,6 +29,52 @@ func (r *CloudCostManagementService) GetRecommendationsStats(ctx context.Context
2729
return r.getRecommendations(ctx, scope, accountId, options, ccmRecommendationsStatsPath)
2830
}
2931

32+
func (r *CloudCostManagementService) UpdateRecommendationState(
33+
ctx context.Context,
34+
scope dto.Scope,
35+
accountId string,
36+
recommendationId string,
37+
state string,
38+
) (*map[string]any, error) {
39+
40+
path := fmt.Sprintf(ccmUpdateRecommendationState, accountId)
41+
params := make(map[string]string)
42+
params["recommendationId"] = recommendationId
43+
params["state"] = state
44+
45+
resp := new(map[string]any)
46+
47+
err := r.Client.Post(ctx, path, params, nil, &resp)
48+
if err != nil {
49+
return nil, fmt.Errorf("Failed to update cloud cost management Recommendation state: %w", err)
50+
}
51+
52+
return resp, nil
53+
}
54+
55+
func (r *CloudCostManagementService) OverrideRecommendationSavings(
56+
ctx context.Context,
57+
scope dto.Scope,
58+
accountId string,
59+
recommendationId string,
60+
savings float64,
61+
) (*map[string]any, error) {
62+
63+
path := fmt.Sprintf(ccmOverrideRecommendationSavings, accountId)
64+
params := make(map[string]string)
65+
params["recommendationId"] = recommendationId
66+
params["overriddenSavings"] = strconv.FormatFloat(savings, 'f', -1, 64)
67+
68+
resp := new(map[string]any)
69+
70+
err := r.Client.Put(ctx, path, params, nil, &resp)
71+
if err != nil {
72+
return nil, fmt.Errorf("Failed to override cloud cost management Recommendation savings: %w", err)
73+
}
74+
75+
return resp, nil
76+
}
77+
3078
func (r *CloudCostManagementService) getRecommendations(
3179
ctx context.Context,
3280
scope dto.Scope,

0 commit comments

Comments
 (0)