Skip to content

Commit ace12ed

Browse files
sandyydkHarness
authored andcommitted
feat: [CCM-24821]: Support for EC2 analysis initial commit (#120)
* Update description of estimated savings tool call * Update README for get_ccm_commitment_ec2_analysis tool call addition * Add ESR calculation * Update description of the tool call * Add support for EC2 landscape for Commitments * Code cleanup * Fix minor issues * Add tool call for EC2 Analysis * Support for EC2 analysis initial commit
1 parent e74cf55 commit ace12ed

File tree

9 files changed

+429
-23
lines changed

9 files changed

+429
-23
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,7 @@ Toolset Name: `ccm`
158158
- `get_ccm_commitment_savings`: Get commitment savings information for an account in Harness Cloud Cost Management
159159
- `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.
160160
- `get_ccm_estimated_savings`: Get estimated savings information for a cloud account in Harness Cloud Cost Management
161+
- `get_ccm_commitment_ec2_analysis`: Get AWS EC2 commitment analysis for an account in Harness Cloud Cost Management, including RI/SP commitment spend, utilization breakdown, current savings, estimated annualized savings, and ESR.
161162

162163
#### Database Operations Toolset
163164

client/ccmcommitments.go

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,16 @@ import (
55
"encoding/json"
66
"fmt"
77
"sync"
8+
"time"
89

910
"github.com/harness/harness-mcp/client/dto"
11+
"github.com/harness/harness-mcp/pkg/utils"
1012
)
1113

1214
const (
1315
ccmCommitmentEstimatedSavingsPath = ccmCommitmentBasePath + "/accounts/%s/v2/setup/%s/estimated_savings?accountIdentifier=%s"
16+
ccmCommitmentSpendDetailsPath = ccmCommitmentBasePath + "/accounts/%s/v2/spend/detail?accountIdentifier=%s"
17+
ccmCommitmentMasterAccountsPath = ccmCommitmentBasePath + "/accounts/%s/v1/setup/listMasterAccounts?accountIdentifier=%s"
1418

1519
defaultTargetCoveragePercentage = 90.0
1620
)
@@ -98,3 +102,82 @@ func (r *CloudCostManagementService) getEstimatedSavingsResponse(ctx context.Con
98102

99103
return &response, nil
100104
}
105+
106+
func (r *CloudCostManagementService) GetCommitmentSpends(ctx context.Context, scope dto.Scope, opts *dto.CCMCommitmentOptions) (*dto.CCMCommitmentBaseResponse, error) {
107+
108+
path := fmt.Sprintf(ccmCommitmentSpendDetailsPath, scope.AccountID, scope.AccountID)
109+
params := make(map[string]string)
110+
addScope(scope, params)
111+
112+
// Handle nil options by creating default options
113+
if opts == nil {
114+
opts = &dto.CCMCommitmentOptions{}
115+
}
116+
117+
if opts.StartDate != nil && *opts.StartDate != "" {
118+
params["start_date"] = *opts.StartDate
119+
} else {
120+
// Default to last 30 days
121+
params["start_date"] = utils.FormatUnixToYYYYMMDD(time.Now().AddDate(0, 0, -30).Unix())
122+
}
123+
if opts.EndDate != nil && *opts.EndDate != "" {
124+
params["end_date"] = *opts.EndDate
125+
} else {
126+
// Default to last 30 days
127+
params["end_date"] = utils.FormatUnixToYYYYMMDD(time.Now().Unix())
128+
}
129+
130+
var requestPayload = dto.CCMCommitmentAPIFilter{
131+
Service: ccmCommitmentComputeService, // Default value
132+
NetAmortizedCost: utils.ToBoolPtr(false),
133+
}
134+
135+
if opts.Service != nil && *opts.Service != "" {
136+
requestPayload.Service = *opts.Service
137+
}
138+
139+
if opts.IsNetAmortizedCost != nil {
140+
requestPayload.NetAmortizedCost = opts.IsNetAmortizedCost
141+
}
142+
143+
if len(opts.CloudAccountIDs) > 0 {
144+
requestPayload.CloudAccounts = opts.CloudAccountIDs
145+
}
146+
147+
// Temporary slice to hold the strings
148+
spendDetailsResponse := new(dto.CCMCommitmentBaseResponse)
149+
150+
err := r.Client.Post(ctx, path, params, requestPayload, spendDetailsResponse)
151+
if err != nil {
152+
return nil, fmt.Errorf("failed to get cloud cost managment compute spend details with path %s: %w", path, err)
153+
}
154+
155+
return spendDetailsResponse, nil
156+
}
157+
158+
func (r *CloudCostManagementService) GetCommitmentMasterAccounts(ctx context.Context, scope dto.Scope) (*dto.CCMMasterAccountsListResponse, error) {
159+
path := fmt.Sprintf(ccmCommitmentMasterAccountsPath, scope.AccountID, scope.AccountID)
160+
params := make(map[string]string)
161+
addScope(scope, params)
162+
163+
listMasterAccountsResponse := new(dto.CCMCommitmentBaseResponse)
164+
165+
err := r.Client.Post(ctx, path, params, nil, listMasterAccountsResponse)
166+
if err != nil {
167+
return nil, fmt.Errorf("failed to get cloud cost managment master accounts with path %s: %w", path, err)
168+
}
169+
170+
jsonBytes, err := json.Marshal(listMasterAccountsResponse.Response)
171+
if err != nil {
172+
return nil, fmt.Errorf("failed to marshal cloud cost managment master accounts with path %s: %w", path, err)
173+
}
174+
175+
var response dto.CCMMasterAccountsListResponse
176+
177+
err = json.Unmarshal(jsonBytes, &response)
178+
if err != nil {
179+
return nil, fmt.Errorf("failed to unmarshal cloud cost managment master accounts with path %s: %w", path, err)
180+
}
181+
182+
return &response, nil
183+
}

client/ccmcosts.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -154,13 +154,13 @@ func (r *CloudCostManagementService) GetComputeCoverage(ctx context.Context, sco
154154
params["start_date"] = *opts.StartDate
155155
} else {
156156
// Default to last 30 days
157-
params["start_date"] = utils.FormatUnixToMMDDYYYY(time.Now().AddDate(0, 0, -30).Unix())
157+
params["start_date"] = utils.FormatUnixToYYYYMMDD(time.Now().AddDate(0, 0, -30).Unix())
158158
}
159159
if opts.EndDate != nil && *opts.EndDate != "" {
160160
params["end_date"] = *opts.EndDate
161161
} else {
162162
// Default to last 30 days
163-
params["end_date"] = utils.CurrentMMDDYYYY()
163+
params["end_date"] = utils.FormatUnixToYYYYMMDD(time.Now().Unix())
164164
}
165165

166166
var requestPayload = dto.CCMCommitmentAPIFilter{
@@ -205,13 +205,13 @@ func (r *CloudCostManagementService) GetCommitmentSavings(ctx context.Context, s
205205
params["start_date"] = *opts.StartDate
206206
} else {
207207
// Default to last 30 days
208-
params["start_date"] = utils.FormatUnixToMMDDYYYY(time.Now().AddDate(0, 0, -30).Unix())
208+
params["start_date"] = utils.FormatUnixToYYYYMMDD(time.Now().AddDate(0, 0, -30).Unix())
209209
}
210210
if opts.EndDate != nil && *opts.EndDate != "" {
211211
params["end_date"] = *opts.EndDate
212212
} else {
213213
// Default to last 30 days
214-
params["end_date"] = utils.CurrentMMDDYYYY()
214+
params["end_date"] = utils.FormatUnixToYYYYMMDD(time.Now().Unix())
215215
}
216216

217217
var requestPayload = dto.CCMCommitmentAPIFilter{
@@ -256,13 +256,13 @@ func (r *CloudCostManagementService) GetCommitmentUtilisation(ctx context.Contex
256256
params["start_date"] = *opts.StartDate
257257
} else {
258258
// Default to last 30 days
259-
params["start_date"] = utils.FormatUnixToMMDDYYYY(time.Now().AddDate(0, 0, -30).Unix())
259+
params["start_date"] = utils.FormatUnixToYYYYMMDD(time.Now().AddDate(0, 0, -30).Unix())
260260
}
261261
if opts.EndDate != nil && *opts.EndDate != "" {
262262
params["end_date"] = *opts.EndDate
263263
} else {
264264
// Default to last 30 days
265-
params["end_date"] = utils.CurrentMMDDYYYY()
265+
params["end_date"] = utils.FormatUnixToYYYYMMDD(time.Now().Unix())
266266
}
267267

268268
var requestPayload = dto.CCMCommitmentAPIFilter{

client/dto/ccmcommitments.go

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,3 +65,80 @@ type EstimatedSavingsRemoteResponse struct {
6565
CurrentSavings float64 `json:"current_savings"`
6666
EstimatedSavings float64 `json:"estimated_savings"`
6767
}
68+
69+
type ComputeSpendsDetail struct {
70+
Table *ComputeSpendsDetailTable `json:"table,omitempty"`
71+
Chart []*ComputeSpendChart `json:"chart,omitempty"`
72+
}
73+
74+
type ComputeSpendsDetailTable struct {
75+
TotalSpend float64 `json:"total_spend"`
76+
Trend *float64 `json:"trend,omitempty"`
77+
Service *string `json:"service"`
78+
}
79+
80+
type ComputeSpendChart struct {
81+
Day time.Time `json:"date"`
82+
SpendAmount float64 `json:"spend_amount"`
83+
}
84+
85+
type CommitmentUtilisationSummary struct {
86+
Overall *CommitmentUtilisationResponse `json:"overall,omitempty"`
87+
Breakdown []*CommitmentUtilisationResponse `json:"breakdown,omitempty"`
88+
}
89+
90+
type CommitmentEC2AnalysisResponse struct {
91+
CommitmentSpends []*CommitmentSpendsResponse `json:"commitment_spend,omitempty"`
92+
CommitmentUtilisation *CommitmentUtilisationSummary `json:"commitment_utilization,omitempty"`
93+
EstimatedSavings []*CommitmentEstimatedSavings `json:"estimated_savings,omitempty"`
94+
CurrentSavings float64 `json:"current_savings,omitempty"`
95+
ESRYearly float64 `json:"esr_yearly,omitempty"`
96+
}
97+
98+
type CommitmentSpendsResponse struct {
99+
Key string `json:"key"`
100+
Cost float64 `json:"cost"`
101+
YearlyCost float64 `json:"yearly_cost"`
102+
Trend *float64 `json:"trend,omitempty"`
103+
}
104+
105+
type CommitmentUtilisationResponse struct {
106+
Key string `json:"key"`
107+
ComputeSpend float64 `json:"compute_spend"`
108+
Percentage float64 `json:"percentage"`
109+
Trend float64 `json:"trend"`
110+
Utilization float64 `json:"utilization"`
111+
}
112+
113+
type CommitmentUtilizationChart struct {
114+
Day time.Time `json:"date"`
115+
UtilizationPercentage float64 `json:"utilization_percentage"`
116+
}
117+
118+
type CommitmentUtlizationsDetail struct {
119+
Table *CommitmentUtilizationsDetailTable `json:"table,omitempty"`
120+
Chart []*CommitmentUtilizationChart `json:"chart,omitempty"`
121+
}
122+
123+
type CommitmentUtilizationsDetailTable struct {
124+
ComputeSpend float64 `json:"compute_spend"`
125+
Utilization float64 `json:"utilization"`
126+
Percentage float64 `json:"percentage"`
127+
Trend *float64 `json:"trend,omitempty"`
128+
}
129+
130+
type SavingsDetail struct {
131+
Table *SavingsDetailTable `json:"table,omitempty"`
132+
Chart []*SavingsDetailChart `json:"chart,omitempty"`
133+
}
134+
135+
type SavingsDetailTable struct {
136+
Total float64 `json:"total"`
137+
Percentage float64 `json:"percentage"`
138+
Trend *float64 `json:"trend,omitempty"`
139+
}
140+
141+
type SavingsDetailChart struct {
142+
Day time.Time `json:"date"`
143+
Savings float64 `json:"savings"`
144+
}

client/dto/ccmcosts.go

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -82,13 +82,14 @@ type CCMListCostCategoriesOptions struct {
8282
}
8383

8484
type CCMCommitmentOptions struct {
85-
AccountIdentifier *string `json:"accountIdentifier,omitempty"`
86-
CloudAccountIDs []string `json:"cloudAccountId,omitempty"`
87-
Service *string `json:"service,omitempty"`
88-
StartDate *string `json:"startDate,omitempty"`
89-
EndDate *string `json:"endDate,omitempty"`
90-
IsHarnessManaged *bool `json:"isHarnessManaged,omitempty"`
91-
GroupBy *string `json:"groupBy,omitempty"`
85+
AccountIdentifier *string `json:"accountIdentifier,omitempty"`
86+
CloudAccountIDs []string `json:"cloudAccountId,omitempty"`
87+
Service *string `json:"service,omitempty"`
88+
StartDate *string `json:"startDate,omitempty"`
89+
EndDate *string `json:"endDate,omitempty"`
90+
IsHarnessManaged *bool `json:"isHarnessManaged,omitempty"`
91+
GroupBy *string `json:"groupBy,omitempty"`
92+
IsNetAmortizedCost *bool `json:"net_amortized,omitempty"`
9293
}
9394

9495
// CcmCostCategoryList represents a list of cost categories in CCM
@@ -235,4 +236,16 @@ type CCMCommitmentAPIFilter struct {
235236
Service string `json:"service,omitempty"`
236237
IsHarnessManaged *bool `json:"is_harness_managed,omitempty"`
237238
GroupBy *string `json:"group_by,omitempty"`
239+
NetAmortizedCost *bool `json:"net_amortized,omitempty"`
240+
}
241+
242+
type CCMMasterAccountsListResponse struct {
243+
Data struct {
244+
Content []struct {
245+
Connector struct {
246+
Identifier string `json:"identifier"`
247+
Name string `json:"name"`
248+
} `json:"connector"`
249+
} `json:"content"`
250+
} `json:"data"`
238251
}

0 commit comments

Comments
 (0)