Skip to content

Commit da30ca2

Browse files
ispeakc0deHarness
authored andcommitted
feat: [CHAOS-9029]: Integrate the chaos experiment APIs (#29)
* fix: [CHAOS-9029]: Added pagination in list API and updated readme Signed-off-by: Shubham Chaudhary <[email protected]> * feat: [CHAOS-9029]: rename the chaostools name Signed-off-by: Shubham Chaudhary <[email protected]> * feat: [CHAOS-9029]: Add the internal paths Signed-off-by: Shubham Chaudhary <[email protected]> * feat: [CHAOS-9029]: Integrate the chaos experiment APIs Signed-off-by: Shubham Chaudhary <[email protected]>
1 parent aefc82c commit da30ca2

File tree

7 files changed

+384
-0
lines changed

7 files changed

+384
-0
lines changed

README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,15 @@ Toolset Name: `cloudcostmanagement`
6161
- `list_ccm_cost_categories_detail`: List all cost categories details for a specified account.
6262
- `get_ccm_cost_category`: Retrieve a cost category detail by Id for a specified account.
6363

64+
#### Chaos Engineering Toolset
65+
66+
Toolset Name: `chaos`
67+
68+
- `chaos_experiments_list`: List all the chaos experiments for a specific account.
69+
- `chaos_experiment_describe`: Get details of a specific chaos experiment.
70+
- `chaos_experiment_run`: Run a specific chaos experiment.
71+
- `chaos_experiment_run_result`: Get the result of a specific chaos experiment run.
72+
6473
#### Logs Toolset
6574

6675
Toolset Name: `logs`

client/chaos.go

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
package client
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"github.com/harness/harness-mcp/client/dto"
7+
)
8+
9+
const (
10+
// Base API paths
11+
chaosListExperimentsPath = "api/rest/v2/experiment"
12+
chaosGetExperimentPath = "api/rest/v2/experiments/%s"
13+
chaosGetExperimentRunPath = "api/rest/v2/experiments/%s/run"
14+
chaosExperimentRunPath = "api/rest/v2/experiments/%s/run"
15+
16+
// Prefix to prepend for external API calls
17+
externalChaosManagerPathPrefix = "chaos/manager/"
18+
)
19+
20+
type ChaosService struct {
21+
Client *Client
22+
UseInternalPaths bool
23+
}
24+
25+
func (c *ChaosService) buildPath(basePath string) string {
26+
if c.UseInternalPaths {
27+
return basePath
28+
}
29+
return externalChaosManagerPathPrefix + basePath
30+
}
31+
32+
func (c *ChaosService) ListExperiments(ctx context.Context, scope dto.Scope, pagination *dto.PaginationOptions) (*dto.ListExperimentResponse, error) {
33+
var (
34+
pathTemplate = c.buildPath(chaosListExperimentsPath)
35+
path = fmt.Sprintf(pathTemplate)
36+
params = make(map[string]string)
37+
)
38+
39+
// Set default pagination
40+
setDefaultPagination(pagination)
41+
42+
// Add pagination parameters
43+
params["page"] = fmt.Sprintf("%d", pagination.Page)
44+
params["limit"] = fmt.Sprintf("%d", pagination.Size)
45+
// Add scope parameters
46+
params = addIdentifierParams(params, scope)
47+
48+
listExperiments := new(dto.ListExperimentResponse)
49+
err := c.Client.Get(ctx, path, params, nil, listExperiments)
50+
if err != nil {
51+
return nil, fmt.Errorf("failed to list experiments: %w", err)
52+
}
53+
54+
return listExperiments, nil
55+
}
56+
57+
func (c *ChaosService) GetExperiment(ctx context.Context, scope dto.Scope, experimentID string) (*dto.GetExperimentResponse, error) {
58+
var (
59+
pathTemplate = c.buildPath(chaosGetExperimentPath)
60+
path = fmt.Sprintf(pathTemplate, experimentID)
61+
params = make(map[string]string)
62+
)
63+
64+
// Add scope parameters
65+
params = addIdentifierParams(params, scope)
66+
67+
getExperiment := new(dto.GetExperimentResponse)
68+
err := c.Client.Get(ctx, path, params, nil, getExperiment)
69+
if err != nil {
70+
return nil, fmt.Errorf("failed to get experiment: %w", err)
71+
}
72+
73+
return getExperiment, nil
74+
}
75+
76+
func (c *ChaosService) GetExperimentRun(ctx context.Context, scope dto.Scope, experimentID, experimentRunID string) (*dto.ChaosExperimentRun, error) {
77+
var (
78+
pathTemplate = c.buildPath(chaosGetExperimentRunPath)
79+
path = fmt.Sprintf(pathTemplate, experimentID)
80+
params = make(map[string]string)
81+
)
82+
83+
params["experimentRunId"] = experimentRunID
84+
// Add scope parameters
85+
params = addIdentifierParams(params, scope)
86+
87+
getExperimentRun := new(dto.ChaosExperimentRun)
88+
err := c.Client.Get(ctx, path, params, nil, getExperimentRun)
89+
if err != nil {
90+
return nil, fmt.Errorf("failed to get experiment run: %w", err)
91+
}
92+
93+
return getExperimentRun, nil
94+
}
95+
96+
func (c *ChaosService) RunExperiment(ctx context.Context, scope dto.Scope, experimentID string) (*dto.RunChaosExperimentResponse, error) {
97+
var (
98+
pathTemplate = c.buildPath(chaosExperimentRunPath)
99+
path = fmt.Sprintf(pathTemplate, experimentID)
100+
params = make(map[string]string)
101+
)
102+
103+
// Add scope parameters
104+
params["isIdentity"] = "false"
105+
params = addIdentifierParams(params, scope)
106+
107+
experimentRun := new(dto.RunChaosExperimentResponse)
108+
err := c.Client.Post(ctx, path, params, nil, experimentRun)
109+
if err != nil {
110+
return nil, fmt.Errorf("failed to run experiment: %w", err)
111+
}
112+
113+
return experimentRun, nil
114+
}
115+
116+
func addIdentifierParams(params map[string]string, scope dto.Scope) map[string]string {
117+
params["accountIdentifier"] = scope.AccountID
118+
params["projectIdentifier"] = scope.ProjectID
119+
params["organizationIdentifier"] = scope.OrgID
120+
return params
121+
}

client/dto/chaos.go

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
package dto
2+
3+
type ListExperimentResponse struct {
4+
Data []ExperimentV2 `json:"data"`
5+
}
6+
7+
type GetExperimentResponse struct {
8+
ExperimentID string `json:"ExperimentID"`
9+
Identity string `json:"Identity"`
10+
InfraID string `json:"InfraID"`
11+
InfraType string `json:"InfraType"`
12+
ExperimentType string `json:"ExperimentType"`
13+
Revision []WorkflowRevision `json:"Revision"`
14+
RecentExperimentRunDetails []ExperimentRunDetail `json:"RecentExperimentRunDetails"`
15+
}
16+
17+
type ExperimentRunDetail struct {
18+
ResiliencyScore float64 `json:"ResiliencyScore"`
19+
ExperimentRunID string `json:"ExperimentRunID"`
20+
Phase string `json:"Phase"`
21+
UpdatedAt int64 `json:"updatedAt"`
22+
}
23+
24+
type WorkflowRevision struct {
25+
RevisionID string `json:"RevisionID"`
26+
ExperimentManifest string `json:"ExperimentManifest"`
27+
UpdatedAt int64 `json:"UpdatedAt"`
28+
}
29+
30+
type ChaosExperimentRun struct {
31+
NotifyID string `json:"notifyID"`
32+
ResiliencyScore float64 `json:"resiliencyScore"`
33+
ExperimentType string `json:"experimentType"`
34+
InfraID string `json:"infraID"`
35+
ExperimentName string `json:"experimentName"`
36+
Phase string `json:"phase"`
37+
ExecutionData string `json:"executionData"`
38+
ErrorResponse string `json:"errorResponse"`
39+
}
40+
41+
type RunChaosExperimentResponse struct {
42+
NotifyID string `json:"notifyId"`
43+
DelegateTaskID string `json:"delegateTaskId"`
44+
ExperimentRunID string `json:"experimentRunId"`
45+
ExperimentID string `json:"experimentId"`
46+
ExperimentName string `json:"experimentName"`
47+
}
48+
49+
type ExperimentV2 struct {
50+
WorkflowType *string `json:"workflowType"`
51+
IsCronEnabled *bool `json:"isCronEnabled"`
52+
Infra *ChaosInfraV2 `json:"infra"`
53+
ExperimentID string `json:"experimentID"`
54+
WorkflowID string `json:"workflowID"`
55+
Name string `json:"name"`
56+
Description string `json:"description"`
57+
UpdatedAt string `json:"updatedAt"`
58+
CreatedAt string `json:"createdAt"`
59+
}
60+
61+
type ChaosInfraV2 struct {
62+
Name string `json:"identity"`
63+
EnvironmentID string `json:"environmentId"`
64+
}

cmd/harness-mcp-server/config/config.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,4 +32,6 @@ type Config struct {
3232
NextgenCEBaseURL string
3333
NextgenCESecret string
3434
McpSvcSecret string
35+
ChaosManagerSvcBaseURL string
36+
ChaosManagerSvcSecret string
3537
}

cmd/harness-mcp-server/main.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,8 @@ var (
146146
NextgenCEBaseURL: viper.GetString("nextgen_ce_base_url"),
147147
NextgenCESecret: viper.GetString("nextgen_ce_secret"),
148148
McpSvcSecret: viper.GetString("mcp_svc_secret"),
149+
ChaosManagerSvcBaseURL: viper.GetString("chaos_manager_svc_base_url"),
150+
ChaosManagerSvcSecret: viper.GetString("chaos_manager_svc_secret"),
149151
}
150152

151153
if err := runStdioServer(ctx, cfg); err != nil {

pkg/harness/chaos.go

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
package harness
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"fmt"
7+
"github.com/harness/harness-mcp/client"
8+
"github.com/harness/harness-mcp/client/dto"
9+
"github.com/harness/harness-mcp/cmd/harness-mcp-server/config"
10+
"github.com/mark3labs/mcp-go/mcp"
11+
"github.com/mark3labs/mcp-go/server"
12+
)
13+
14+
// ListExperimentsTool creates a tool for listing the experiments
15+
func ListExperimentsTool(config *config.Config, client *client.ChaosService) (tool mcp.Tool, handler server.ToolHandlerFunc) {
16+
return mcp.NewTool("chaos_experiments_list",
17+
mcp.WithDescription("List the chaos experiments"),
18+
WithScope(config, false),
19+
WithPagination(),
20+
),
21+
func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
22+
scope, err := fetchScope(config, request, false)
23+
if err != nil {
24+
return mcp.NewToolResultError(err.Error()), nil
25+
}
26+
27+
page, size, err := fetchPagination(request)
28+
if err != nil {
29+
return mcp.NewToolResultError(err.Error()), nil
30+
}
31+
32+
pagination := &dto.PaginationOptions{
33+
Page: page,
34+
Size: size,
35+
}
36+
37+
data, err := client.ListExperiments(ctx, scope, pagination)
38+
if err != nil {
39+
return nil, fmt.Errorf("failed to list experiments: %w", err)
40+
}
41+
42+
r, err := json.Marshal(data)
43+
if err != nil {
44+
return nil, fmt.Errorf("failed to marshal list experiment response: %w", err)
45+
}
46+
47+
return mcp.NewToolResultText(string(r)), nil
48+
}
49+
}
50+
51+
// GetExperimentsTool creates a tool to get the experiment details
52+
func GetExperimentsTool(config *config.Config, client *client.ChaosService) (tool mcp.Tool, handler server.ToolHandlerFunc) {
53+
return mcp.NewTool("chaos_experiment_describe",
54+
mcp.WithDescription("Retrieves information about chaos experiment, allowing users to get an overview and detailed insights for each experiment"),
55+
WithScope(config, false),
56+
),
57+
func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
58+
scope, err := fetchScope(config, request, false)
59+
if err != nil {
60+
return mcp.NewToolResultError(err.Error()), nil
61+
}
62+
63+
experimentID, err := requiredParam[string](request, "experimentID")
64+
if err != nil {
65+
return mcp.NewToolResultError(err.Error()), nil
66+
}
67+
68+
data, err := client.GetExperiment(ctx, scope, experimentID)
69+
if err != nil {
70+
return nil, fmt.Errorf("failed to get experiment: %w", err)
71+
}
72+
73+
r, err := json.Marshal(data)
74+
if err != nil {
75+
return nil, fmt.Errorf("failed to marshal get experiment response: %w", err)
76+
}
77+
78+
return mcp.NewToolResultText(string(r)), nil
79+
}
80+
}
81+
82+
// GetExperimentRunsTool creates a tool to get the experiment run details
83+
func GetExperimentRunsTool(config *config.Config, client *client.ChaosService) (tool mcp.Tool, handler server.ToolHandlerFunc) {
84+
return mcp.NewTool("chaos_experiment_run_result",
85+
mcp.WithDescription("Retrieves run result of chaos experiment runs, helping to describe and summarize the details of each experiment run"),
86+
WithScope(config, false),
87+
),
88+
func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
89+
scope, err := fetchScope(config, request, false)
90+
if err != nil {
91+
return mcp.NewToolResultError(err.Error()), nil
92+
}
93+
94+
experimentID, err := requiredParam[string](request, "experimentID")
95+
if err != nil {
96+
return mcp.NewToolResultError(err.Error()), nil
97+
}
98+
99+
experimentRunID, err := requiredParam[string](request, "experimentRunId")
100+
if err != nil {
101+
return mcp.NewToolResultError(err.Error()), nil
102+
}
103+
104+
data, err := client.GetExperimentRun(ctx, scope, experimentID, experimentRunID)
105+
if err != nil {
106+
return nil, fmt.Errorf("failed to get experiment run: %w", err)
107+
}
108+
109+
r, err := json.Marshal(data)
110+
if err != nil {
111+
return nil, fmt.Errorf("failed to marshal get experiment run response: %w", err)
112+
}
113+
114+
return mcp.NewToolResultText(string(r)), nil
115+
}
116+
}
117+
118+
// RunExperimentTool creates a tool to run the experiment
119+
func RunExperimentTool(config *config.Config, client *client.ChaosService) (tool mcp.Tool, handler server.ToolHandlerFunc) {
120+
return mcp.NewTool("chaos_experiment_run",
121+
mcp.WithDescription("Run the chaos experiment"),
122+
WithScope(config, false),
123+
),
124+
func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
125+
scope, err := fetchScope(config, request, false)
126+
if err != nil {
127+
return mcp.NewToolResultError(err.Error()), nil
128+
}
129+
130+
experimentID, err := requiredParam[string](request, "experimentID")
131+
if err != nil {
132+
return mcp.NewToolResultError(err.Error()), nil
133+
}
134+
135+
data, err := client.RunExperiment(ctx, scope, experimentID)
136+
if err != nil {
137+
return nil, fmt.Errorf("failed to run experiment: %w", err)
138+
}
139+
140+
r, err := json.Marshal(data)
141+
if err != nil {
142+
return nil, fmt.Errorf("failed to marshal run experiment response: %w", err)
143+
}
144+
145+
return mcp.NewToolResultText(string(r)), nil
146+
}
147+
}

0 commit comments

Comments
 (0)