Skip to content

Commit e1283cc

Browse files
authored
Merge pull request #54 from Azure/guwe/azmonitor
Feat: Support az monitor metrics (aks platform metrics)
2 parents fbe10e7 + 7aa9020 commit e1283cc

File tree

8 files changed

+417
-9
lines changed

8 files changed

+417
-9
lines changed

internal/components/azaks/registry.go

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
package azaks
22

33
import (
4-
"strings"
5-
4+
"github.com/Azure/aks-mcp/internal/utils"
65
"github.com/mark3labs/mcp-go/mcp"
76
)
87

@@ -13,12 +12,6 @@ type AksCommand struct {
1312
ArgsExample string // Example of command arguments
1413
}
1514

16-
// replaceSpacesWithUnderscores converts spaces to underscores
17-
// to create a valid tool name that follows the [a-z0-9_-] pattern
18-
func replaceSpacesWithUnderscores(s string) string {
19-
return strings.ReplaceAll(s, " ", "_")
20-
}
21-
2215
// // RegisterAz registers the generic az tool
2316
// func RegisterAz() mcp.Tool {
2417
// return mcp.NewTool("Run-az-command",
@@ -34,7 +27,7 @@ func replaceSpacesWithUnderscores(s string) string {
3427
func RegisterAzCommand(cmd AksCommand) mcp.Tool {
3528
// Convert spaces to underscores for valid tool name
3629
commandName := cmd.Name
37-
validToolName := replaceSpacesWithUnderscores(commandName)
30+
validToolName := utils.ReplaceSpacesWithUnderscores(commandName)
3831

3932
description := "Run " + cmd.Name + " command: " + cmd.Description + "."
4033

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
package monitor
2+
3+
import (
4+
"github.com/Azure/aks-mcp/internal/utils"
5+
"github.com/mark3labs/mcp-go/mcp"
6+
)
7+
8+
// MonitorCommand defines a specific az monitor command to be registered as a tool
9+
type MonitorCommand struct {
10+
Name string
11+
Description string
12+
ArgsExample string // Example of command arguments
13+
Category string // Category for the command (e.g., "metrics", "logs")
14+
}
15+
16+
// RegisterMonitorCommand registers a specific az monitor command as an MCP tool
17+
func RegisterMonitorCommand(cmd MonitorCommand) mcp.Tool {
18+
// Convert spaces to underscores for valid tool name
19+
commandName := cmd.Name
20+
validToolName := utils.ReplaceSpacesWithUnderscores(commandName)
21+
22+
description := "Run " + cmd.Name + " command: " + cmd.Description + "."
23+
24+
// Add example if available, with proper punctuation
25+
if cmd.ArgsExample != "" {
26+
description += "\nExample: `" + cmd.ArgsExample + "`"
27+
}
28+
29+
return mcp.NewTool(validToolName,
30+
mcp.WithDescription(description),
31+
mcp.WithString("args",
32+
mcp.Required(),
33+
mcp.Description("Arguments for the `"+cmd.Name+"` command"),
34+
),
35+
)
36+
}
37+
38+
// GetReadOnlyMonitorCommands returns all read-only az monitor commands
39+
func GetReadOnlyMonitorCommands() []MonitorCommand {
40+
return []MonitorCommand{
41+
// Metrics commands
42+
{
43+
Name: "az monitor metrics list",
44+
Description: "List the metric values for a resource",
45+
ArgsExample: "--resource /subscriptions/{subscription}/resourceGroups/{resourceGroup}/providers/Microsoft.Compute/virtualMachines/{vmName} --metric \"Percentage CPU\"",
46+
Category: "metrics",
47+
},
48+
{
49+
Name: "az monitor metrics list-definitions",
50+
Description: "List the metric definitions for a resource",
51+
ArgsExample: "--resource /subscriptions/{subscription}/resourceGroups/{resourceGroup}/providers/Microsoft.ContainerService/managedClusters/{clusterName}",
52+
Category: "metrics",
53+
},
54+
{
55+
Name: "az monitor metrics list-namespaces",
56+
Description: "List the metric namespaces for a resource",
57+
ArgsExample: "--resource /subscriptions/{subscription}/resourceGroups/{resourceGroup}/providers/Microsoft.ContainerService/managedClusters/{clusterName}",
58+
Category: "metrics",
59+
},
60+
}
61+
}
62+
63+
// GetReadWriteMonitorCommands returns all read-write az monitor commands
64+
func GetReadWriteMonitorCommands() []MonitorCommand {
65+
return []MonitorCommand{}
66+
}
67+
68+
// GetAdminMonitorCommands returns all admin az monitor commands
69+
func GetAdminMonitorCommands() []MonitorCommand {
70+
return []MonitorCommand{}
71+
}
Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
package monitor
2+
3+
import (
4+
"testing"
5+
)
6+
7+
func TestGetReadOnlyMonitorCommands_ContainsMetricsCommands(t *testing.T) {
8+
commands := GetReadOnlyMonitorCommands()
9+
10+
// Verify we have the expected number of commands
11+
expectedCommands := 3
12+
if len(commands) != expectedCommands {
13+
t.Errorf("Expected %d read-only commands, got %d", expectedCommands, len(commands))
14+
}
15+
16+
// Check that specific metric commands are included
17+
foundMetricsList := false
18+
foundMetricsDefinitions := false
19+
foundMetricsNamespaces := false
20+
21+
for _, cmd := range commands {
22+
switch cmd.Name {
23+
case "az monitor metrics list":
24+
foundMetricsList = true
25+
if cmd.Description == "" {
26+
t.Error("Expected metrics list command to have a description")
27+
}
28+
if cmd.ArgsExample == "" {
29+
t.Error("Expected metrics list command to have an args example")
30+
}
31+
if cmd.Category != "metrics" {
32+
t.Errorf("Expected metrics list command to have category 'metrics', got '%s'", cmd.Category)
33+
}
34+
case "az monitor metrics list-definitions":
35+
foundMetricsDefinitions = true
36+
if cmd.Description == "" {
37+
t.Error("Expected metrics list-definitions command to have a description")
38+
}
39+
if cmd.ArgsExample == "" {
40+
t.Error("Expected metrics list-definitions command to have an args example")
41+
}
42+
if cmd.Category != "metrics" {
43+
t.Errorf("Expected metrics list-definitions command to have category 'metrics', got '%s'", cmd.Category)
44+
}
45+
case "az monitor metrics list-namespaces":
46+
foundMetricsNamespaces = true
47+
if cmd.Description == "" {
48+
t.Error("Expected metrics list-namespaces command to have a description")
49+
}
50+
if cmd.ArgsExample == "" {
51+
t.Error("Expected metrics list-namespaces command to have an args example")
52+
}
53+
if cmd.Category != "metrics" {
54+
t.Errorf("Expected metrics list-namespaces command to have category 'metrics', got '%s'", cmd.Category)
55+
}
56+
}
57+
}
58+
59+
if !foundMetricsList {
60+
t.Error("Expected to find 'az monitor metrics list' command in read-only commands")
61+
}
62+
63+
if !foundMetricsDefinitions {
64+
t.Error("Expected to find 'az monitor metrics list-definitions' command in read-only commands")
65+
}
66+
67+
if !foundMetricsNamespaces {
68+
t.Error("Expected to find 'az monitor metrics list-namespaces' command in read-only commands")
69+
}
70+
}
71+
72+
func TestRegisterMonitorCommand_MetricsCommands(t *testing.T) {
73+
// Test that metrics list command can be registered
74+
listCmd := MonitorCommand{
75+
Name: "az monitor metrics list",
76+
Description: "List the metric values for a resource",
77+
ArgsExample: "--resource /subscriptions/{subscription}/resourceGroups/{resourceGroup}/providers/Microsoft.Compute/virtualMachines/{vmName} --metric \"Percentage CPU\"",
78+
Category: "metrics",
79+
}
80+
81+
tool := RegisterMonitorCommand(listCmd)
82+
83+
if tool.Name != "az_monitor_metrics_list" {
84+
t.Errorf("Expected tool name 'az_monitor_metrics_list', got '%s'", tool.Name)
85+
}
86+
87+
if tool.Description == "" {
88+
t.Error("Expected tool description to be set")
89+
}
90+
91+
expectedDescription := "Run az monitor metrics list command: List the metric values for a resource.\nExample: `--resource /subscriptions/{subscription}/resourceGroups/{resourceGroup}/providers/Microsoft.Compute/virtualMachines/{vmName} --metric \"Percentage CPU\"`"
92+
if tool.Description != expectedDescription {
93+
t.Errorf("Expected tool description to contain example, got: %s", tool.Description)
94+
}
95+
96+
// Test that metrics list-definitions command can be registered
97+
definitionsCmd := MonitorCommand{
98+
Name: "az monitor metrics list-definitions",
99+
Description: "List the metric definitions for a resource",
100+
ArgsExample: "--resource /subscriptions/{subscription}/resourceGroups/{resourceGroup}/providers/Microsoft.ContainerService/managedClusters/{clusterName}",
101+
Category: "metrics",
102+
}
103+
104+
tool2 := RegisterMonitorCommand(definitionsCmd)
105+
106+
if tool2.Name != "az_monitor_metrics_list-definitions" {
107+
t.Errorf("Expected tool name 'az_monitor_metrics_list-definitions', got '%s'", tool2.Name)
108+
}
109+
110+
if tool2.Description == "" {
111+
t.Error("Expected tool description to be set")
112+
}
113+
}
114+
115+
func TestRegisterMonitorCommand_WithoutArgsExample(t *testing.T) {
116+
// Test command registration without args example
117+
cmd := MonitorCommand{
118+
Name: "az monitor test",
119+
Description: "Test command",
120+
ArgsExample: "",
121+
Category: "test",
122+
}
123+
124+
tool := RegisterMonitorCommand(cmd)
125+
126+
if tool.Name != "az_monitor_test" {
127+
t.Errorf("Expected tool name 'az_monitor_test', got '%s'", tool.Name)
128+
}
129+
130+
expectedDescription := "Run az monitor test command: Test command."
131+
if tool.Description != expectedDescription {
132+
t.Errorf("Expected tool description '%s', got '%s'", expectedDescription, tool.Description)
133+
}
134+
}
135+
136+
func TestGetReadWriteMonitorCommands_IsEmpty(t *testing.T) {
137+
commands := GetReadWriteMonitorCommands()
138+
139+
if len(commands) != 0 {
140+
t.Errorf("Expected read-write commands to be empty, got %d commands", len(commands))
141+
}
142+
}
143+
144+
func TestGetAdminMonitorCommands_IsEmpty(t *testing.T) {
145+
commands := GetAdminMonitorCommands()
146+
147+
if len(commands) != 0 {
148+
t.Errorf("Expected admin commands to be empty, got %d commands", len(commands))
149+
}
150+
}
151+
152+
func TestMonitorCommand_StructFields(t *testing.T) {
153+
cmd := MonitorCommand{
154+
Name: "test name",
155+
Description: "test description",
156+
ArgsExample: "test args",
157+
Category: "test category",
158+
}
159+
160+
if cmd.Name != "test name" {
161+
t.Errorf("Expected Name to be 'test name', got '%s'", cmd.Name)
162+
}
163+
164+
if cmd.Description != "test description" {
165+
t.Errorf("Expected Description to be 'test description', got '%s'", cmd.Description)
166+
}
167+
168+
if cmd.ArgsExample != "test args" {
169+
t.Errorf("Expected ArgsExample to be 'test args', got '%s'", cmd.ArgsExample)
170+
}
171+
172+
if cmd.Category != "test category" {
173+
t.Errorf("Expected Category to be 'test category', got '%s'", cmd.Category)
174+
}
175+
}

internal/security/validator.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,11 @@ var (
5252
"az advisor recommendation list",
5353
"az advisor recommendation show",
5454

55+
// Azure Monitor metrics commands (read-only)
56+
"az monitor metrics list",
57+
"az monitor metrics list-definitions",
58+
"az monitor metrics list-namespaces",
59+
5560
// Other general commands
5661
"az find",
5762
"az version",

0 commit comments

Comments
 (0)