Skip to content

Commit d57b791

Browse files
javrudskyHarness
authored andcommitted
feat: [fme-8300]: Added functions to create Jira and Service Now tickets (#122)
* [fme-8300] Merge with master * [fme-8300] Merge with master * [fme-8300] Improving descriptions and removing unused parameters * Merged with master * [fme-8300] Merge with master * [fme-8278] Removed refactor for code deduplication requested in PR review * Merge branch 'master' into fme-8278_ccm_update_persp * [fme-8300] Adding information to list jira projects and list jira issue types related functions * [fme-8300] Added list Jira projects and List Jira issue types tools to get correct data for Jira Ticket creation * [fme-8300] Added list Jira project tool to create a ticket for CCM Recomendations * [fme-8278] Using slog library * [fme-8300] Fixing some issues when creating Jira tickets * [fme-8278] Merge with master * [fme-8300] Handling Jira and Service Now differences * Merge branch 'fme-8278_ccm_update_persp' into fme-8300_ccm_create_jira_4_recomm * [fme-8278] Added destructive hint annotation to mutable tools * [fme-8300] Adding default filter if nil to filter
1 parent f54b056 commit d57b791

File tree

7 files changed

+724
-46
lines changed

7 files changed

+724
-46
lines changed

README.md

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -154,10 +154,19 @@ Toolset Name: `ccm`
154154
- `list_ccm_recommendations`: Returns a filterable list of cost-optimization recommendations in Harness Cloud Cost Management.
155155
- `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.
156156
- `get_ccm_recommendations_stats`: Returns overall statistics for cloud cost optimization recommendations within a given account in Harness Cloud Cost Management.
157-
- `update_ccm_recommendation_state`: Marks a recommendation as Applied/Open/Ignored in Harness Cloud Cost Management
158-
- `override_ccm_recommendation_savings`: Overrides savings for a recommendation in Harness Cloud Cost Management
159-
- `get_ccm_commitment_coverage`: Get commitment coverage information for an account in Harness Cloud Cost Management
160-
- `get_ccm_commitment_savings`: Get commitment savings information for an account in Harness Cloud Cost Management
157+
- `update_ccm_recommendation_state`: Marks a recommendation as Applied/Open/Ignored in Harness Cloud Cost Management.
158+
- `override_ccm_recommendation_savings`: Overrides savings for a recommendation in Harness Cloud Cost Management.
159+
- `create_jira_ticket_for_ccm_recommendation`: Creates a Jira ticket for a recommendation in Harness Cloud Cost Management.
160+
- `create_service_now_ticket_for_ccm_recommendation`: Creates a Service Now ticket for a recommendation in Harness Cloud Cost Management.
161+
- `get_ec2_recommendation_detail`: Returns ECS Recommendation details for the given Recommendation identifier.
162+
- `get_azure_vm_recommendation_detail`: Returns Azure Vm Recommendation details for the given Recommendation identifier.
163+
- `get_ecs_service_recommendation_detail`: Returns ECS Service Recommendation details for the given Recommendation identifier.
164+
- `get_node_pool_recommendation_detail`: Returns Node Pool Recommendation details for the given Recommendation identifier.
165+
- `get_workload_recommendation_detail`: Returns Workload Recommendation details for the given Recommendation identifier.
166+
- `list_jira_projects`: Returns a list of Jira projects available to create tickets for recommendations in Harness Cloud Cost Management.
167+
- `list_jira_issue_types`: Returns a list of Jira Issue types available to create tickets for recommendations in Harness Cloud Cost Management.
168+
- `get_ccm_commitment_coverage`: Get commitment coverage information for an account in Harness Cloud Cost Management.
169+
- `get_ccm_commitment_savings`: Get commitment savings information for an account in Harness Cloud Cost Management.
161170
- `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.
162171
- `get_ccm_estimated_savings`: Get estimated savings information for a cloud account in Harness Cloud Cost Management
163172
- `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.

client/ccmrecommendations.go

Lines changed: 222 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -3,43 +3,58 @@ package client
33
import (
44
"context"
55
"fmt"
6+
"log/slog"
67
"strconv"
78

89
"github.com/harness/harness-mcp/client/dto"
910
)
1011

1112
const (
12-
ccmRecommendationsListPath = ccmBasePath + "/recommendation/overview/list?accountIdentifier=%s"
13-
ccmRecommendationsByResourceTypeListPath = ccmBasePath + "/recommendation/overview/resource-type/stats?accountIdentifier=%s"
14-
ccmRecommendationsStatsPath = ccmBasePath + "/recommendation/overview/stats?accountIdentifier=%s"
15-
ccmUpdateRecommendationState = ccmBasePath + "/recommendation/overview/change-state?accountIdentifier=%s"
16-
ccmOverrideRecommendationSavings = ccmBasePath + "/recommendation/overview/override-savings?accountIdentifier=%s"
13+
ngBasePath = "ng/api"
14+
ccmRecommendationsListPath = ccmBasePath + "/recommendation/overview/list?accountIdentifier=%s"
15+
ccmRecommendationsByResourceTypeListPath = ccmBasePath + "/recommendation/overview/resource-type/stats?accountIdentifier=%s"
16+
ccmRecommendationsStatsPath = ccmBasePath + "/recommendation/overview/stats?accountIdentifier=%s"
17+
ccmUpdateRecommendationStatePath = ccmBasePath + "/recommendation/overview/change-state?accountIdentifier=%s"
18+
ccmOverrideRecommendationSavingsPath = ccmBasePath + "/recommendation/overview/override-savings?accountIdentifier=%s"
19+
ccmCreateRecommendationJiraTicketPath = ccmBasePath + "/recommendation/jira/create?accountIdentifier=%s"
20+
ccmCreateRecommendationServiceNowTicketPath = ccmBasePath + "/recommendation/servicenow/create?accountIdentifier=%s"
21+
ccmGetRecommendationDetailPath = ccmBasePath + "/recommendation/details/%s?accountIdentifier=%s&id=%s"
22+
ccmTicketToolSettingsPath = ngBasePath + "/settings?accountIdentifier=%s&category=CE&group=ticketing_preferences"
23+
ccmJiraProjectsPath = ngBasePath + "/jira/projects?accountIdentifier=%s&connectorRef=%s"
24+
ccmJiraMetadataPath = ngBasePath + "/jira/createMetadata?accountIdentifier=%s"
1725
)
1826

19-
func (r *CloudCostManagementService) ListRecommendations(ctx context.Context, scope dto.Scope, accountId string, options map[string]any) (*map[string]any, error) {
27+
const (
28+
ec2Path = "ec2-instance"
29+
azureVmPath = "azure-vm"
30+
ecsServicePath = "ecs-service"
31+
nodePoolPath = "node-pool"
32+
workloadPath = "workload"
33+
)
2034

21-
return r.getRecommendations(ctx, scope, accountId, options, ccmRecommendationsListPath)
35+
func (r *CloudCostManagementService) ListRecommendations(ctx context.Context, accountId string, options map[string]any) (*map[string]any, error) {
36+
37+
return r.getRecommendations(ctx, accountId, options, ccmRecommendationsListPath)
2238
}
2339

24-
func (r *CloudCostManagementService) ListRecommendationsByResourceType(ctx context.Context, scope dto.Scope, accountId string, options map[string]any) (*map[string]any, error) {
40+
func (r *CloudCostManagementService) ListRecommendationsByResourceType(ctx context.Context, accountId string, options map[string]any) (*map[string]any, error) {
2541

26-
return r.getRecommendations(ctx, scope, accountId, options, ccmRecommendationsByResourceTypeListPath)
42+
return r.getRecommendations(ctx, accountId, options, ccmRecommendationsByResourceTypeListPath)
2743
}
2844

29-
func (r *CloudCostManagementService) GetRecommendationsStats(ctx context.Context, scope dto.Scope, accountId string, options map[string]any) (*map[string]any, error) {
45+
func (r *CloudCostManagementService) GetRecommendationsStats(ctx context.Context, accountId string, options map[string]any) (*map[string]any, error) {
3046

31-
return r.getRecommendations(ctx, scope, accountId, options, ccmRecommendationsStatsPath)
47+
return r.getRecommendations(ctx, accountId, options, ccmRecommendationsStatsPath)
3248
}
3349

3450
func (r *CloudCostManagementService) UpdateRecommendationState(
3551
ctx context.Context,
36-
scope dto.Scope,
3752
accountId string,
3853
recommendationId string,
3954
state string,
4055
) (*map[string]any, error) {
4156

42-
path := fmt.Sprintf(ccmUpdateRecommendationState, accountId)
57+
path := fmt.Sprintf(ccmUpdateRecommendationStatePath, accountId)
4358
params := make(map[string]string)
4459
params["recommendationId"] = recommendationId
4560
params["state"] = state
@@ -56,13 +71,11 @@ func (r *CloudCostManagementService) UpdateRecommendationState(
5671

5772
func (r *CloudCostManagementService) OverrideRecommendationSavings(
5873
ctx context.Context,
59-
scope dto.Scope,
6074
accountId string,
6175
recommendationId string,
6276
savings float64,
6377
) (*map[string]any, error) {
64-
65-
path := fmt.Sprintf(ccmOverrideRecommendationSavings, accountId)
78+
path := fmt.Sprintf(ccmOverrideRecommendationSavingsPath, accountId)
6679
params := make(map[string]string)
6780
params["recommendationId"] = recommendationId
6881
params["overriddenSavings"] = strconv.FormatFloat(savings, 'f', -1, 64)
@@ -79,15 +92,13 @@ func (r *CloudCostManagementService) OverrideRecommendationSavings(
7992

8093
func (r *CloudCostManagementService) getRecommendations(
8194
ctx context.Context,
82-
scope dto.Scope,
8395
accountId string,
8496
options map[string]any,
8597
url string,
8698
) (*map[string]any, error) {
8799

88100
path := fmt.Sprintf(url, accountId)
89101
params := make(map[string]string)
90-
addScope(scope, params)
91102

92103
items := new(map[string]any)
93104

@@ -98,3 +109,196 @@ func (r *CloudCostManagementService) getRecommendations(
98109

99110
return items, nil
100111
}
112+
113+
func (r *CloudCostManagementService) CreateJiraTicket(
114+
ctx context.Context,
115+
accountId string,
116+
ticketDetails dto.CCMTicketDetails,
117+
) (*map[string]any, error) {
118+
119+
url := fmt.Sprintf(ccmCreateRecommendationJiraTicketPath, accountId) // Define ccmCreateJiraIssue as needed
120+
return r.createTicket(ctx, accountId, ticketDetails, url)
121+
}
122+
123+
func (r *CloudCostManagementService) CreateServiceNowTicket(
124+
ctx context.Context,
125+
accountId string,
126+
ticketDetails dto.CCMTicketDetails,
127+
) (*map[string]any, error) {
128+
129+
url := fmt.Sprintf(ccmCreateRecommendationServiceNowTicketPath, accountId) // Define ccmCreateJiraIssue as needed
130+
return r.createTicket(ctx, accountId, ticketDetails, url)
131+
}
132+
133+
func (r *CloudCostManagementService) createTicket(
134+
ctx context.Context,
135+
accountId string,
136+
ticketDetails dto.CCMTicketDetails,
137+
url string,
138+
) (*map[string]any, error) {
139+
140+
// Fist check if ticketing tool is available from settings
141+
platform, err := r.getTicketingToolSettings(ctx, accountId)
142+
143+
if err != nil {
144+
return nil, fmt.Errorf("Failed to get ticket tool settings in cloud cost management recommendations: %w", err)
145+
}
146+
147+
if platform != ticketDetails.Platform {
148+
return nil, fmt.Errorf("Ticketing tool not available for this account: %s", ticketDetails.Platform)
149+
}
150+
151+
body := map[string]any{
152+
"recommendationId": ticketDetails.RecommendationId,
153+
"resourceType": ticketDetails.ResourceType,
154+
"connectorRef": ticketDetails.ConnectorRef,
155+
"fields": ticketDetails.Fields,
156+
}
157+
158+
if ticketDetails.Platform == dto.TicketPlatformJira {
159+
body["projectKey"] = ticketDetails.ProjectKey
160+
body["issueType"] = ticketDetails.TicketType
161+
} else {
162+
body["ticketType"] = ticketDetails.TicketType
163+
}
164+
165+
slog.Debug("Creating CCM Ticket", "accountId", accountId, "jiraDetails", body, "ticketType", ticketDetails.TicketType)
166+
167+
resp := new(map[string]any)
168+
169+
err = r.Client.Post(ctx, url, nil, body, map[string]string{}, &resp)
170+
if err != nil {
171+
return nil, fmt.Errorf("Failed to create CCM Jira issue: %w", err)
172+
}
173+
174+
return resp, nil
175+
}
176+
177+
func (r *CloudCostManagementService) GetEc2RecommendationDetail(ctx context.Context, options dto.CCMRecommendationDetailOptions) (*map[string]any, error) {
178+
return r.getRecommendationDetail(ctx, options, ec2Path)
179+
}
180+
181+
func (r *CloudCostManagementService) GetAzureVmRecommendationDetail(ctx context.Context, options dto.CCMRecommendationDetailOptions) (*map[string]any, error) {
182+
return r.getRecommendationDetail(ctx, options, azureVmPath)
183+
}
184+
185+
func (r *CloudCostManagementService) GetEcsServiceRecommendationDetail(ctx context.Context, options dto.CCMRecommendationDetailOptions) (*map[string]any, error) {
186+
return r.getRecommendationDetail(ctx, options, ecsServicePath)
187+
}
188+
189+
func (r *CloudCostManagementService) GetNodePoolRecommendationDetail(ctx context.Context, options dto.CCMRecommendationDetailOptions) (*map[string]any, error) {
190+
return r.getRecommendationDetail(ctx, options, nodePoolPath)
191+
}
192+
193+
func (r *CloudCostManagementService) GetWorkloadRecommendationDetail(ctx context.Context, options dto.CCMRecommendationDetailOptions) (*map[string]any, error) {
194+
return r.getRecommendationDetail(ctx, options, workloadPath)
195+
}
196+
197+
func (r *CloudCostManagementService) ListJiraProjects(
198+
ctx context.Context,
199+
accountId string,
200+
connector string,
201+
) (*map[string]any, error) {
202+
203+
path := fmt.Sprintf(ccmJiraProjectsPath, accountId, connector)
204+
205+
items := new(map[string]any)
206+
207+
err := r.Client.Get(ctx, path, nil, nil, &items)
208+
if err != nil {
209+
return nil, fmt.Errorf("Failed to list Jira Projects: %w", err)
210+
}
211+
212+
return items, nil
213+
}
214+
215+
func (r *CloudCostManagementService) ListJiraIssueTypes(
216+
ctx context.Context,
217+
options dto.CCMJiraIssueTypesOptions,
218+
) (*dto.CCMJiraIssueTypesList, error) {
219+
220+
path := fmt.Sprintf(ccmJiraMetadataPath, options.AccountId)
221+
222+
params := map[string]string{
223+
"connectorRef": options.ConnectorRef,
224+
"projectKey": options.ProjectKey,
225+
}
226+
227+
resp := new(dto.CCMJiraIssueTypesResponse)
228+
err := r.Client.Get(ctx, path, params, nil, &resp)
229+
if err != nil {
230+
return nil, fmt.Errorf("Failed to list Jira Issue Types: %w", err)
231+
}
232+
slog.Debug("Jira Issue Types Response", "response", resp)
233+
items := ExtractIssueTypesList(*resp)
234+
slog.Debug("Jira Issue Types Response", "items", items)
235+
return &items, nil
236+
}
237+
238+
func (r *CloudCostManagementService) getRecommendationDetail(
239+
ctx context.Context,
240+
options dto.CCMRecommendationDetailOptions,
241+
typePath string,
242+
) (*map[string]any, error) {
243+
244+
path := fmt.Sprintf(ccmGetRecommendationDetailPath, typePath, options.AccountIdentifier, options.RecommendationId)
245+
params := make(map[string]string)
246+
247+
if typePath == ecsServicePath || typePath == workloadPath {
248+
params["from"] = options.From
249+
params["to"] = options.To
250+
}
251+
252+
if typePath == ecsServicePath {
253+
params["bufferPercentage"] = strconv.FormatInt(options.BufferPercentage, 10)
254+
}
255+
256+
items := new(map[string]any)
257+
258+
err := r.Client.Get(ctx, path, params, nil, &items)
259+
260+
if err != nil {
261+
return nil, fmt.Errorf("Failed to list cloud cost management recommendations: %w", err)
262+
}
263+
264+
return items, nil
265+
}
266+
267+
// Returns dto.TicketPlatformJira or dto.TicketPlatformServiceNow based on the settings
268+
func (r *CloudCostManagementService) getTicketingToolSettings(
269+
ctx context.Context,
270+
accountId string,
271+
) (string, error) {
272+
273+
path := fmt.Sprintf(ccmTicketToolSettingsPath, accountId)
274+
275+
resp := new(dto.CCMTicketToolSettingsResponse)
276+
err := r.Client.Get(ctx, path, nil, nil, &resp)
277+
if err != nil {
278+
return "", fmt.Errorf("Failed to get ticket tool settings in cloud cost management recommendations: %w", err)
279+
}
280+
281+
return ExtractTicketingToolValue(*resp)
282+
}
283+
284+
func ExtractTicketingToolValue(resp dto.CCMTicketToolSettingsResponse) (string, error) {
285+
for _, d := range resp.Data {
286+
if d.Setting.Identifier == "ticketing_tool" {
287+
slog.Debug("Current ticketing tool setting", "setting", d.Setting.Value)
288+
return d.Setting.Value, nil
289+
}
290+
}
291+
292+
slog.Debug("Current ticketing tool setting not found")
293+
return "", fmt.Errorf("ticketing_tool setting not found")
294+
}
295+
296+
func ExtractIssueTypesList(resp dto.CCMJiraIssueTypesResponse) dto.CCMJiraIssueTypesList {
297+
var issueTypes []dto.CCMJiraIssueType
298+
for _, project := range resp.Data.Projects {
299+
for _, issueType := range project.IssueTypes {
300+
issueTypes = append(issueTypes, issueType)
301+
}
302+
}
303+
return dto.CCMJiraIssueTypesList{CCMBaseResponse: resp.CCMBaseResponse, Data: issueTypes}
304+
}

0 commit comments

Comments
 (0)