Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions 5 internal/azureclient/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -324,3 +324,8 @@ func (c *AzureClient) GetResourceByID(ctx context.Context, resourceID string) (i
return nil, fmt.Errorf("unsupported resource type: %s", parsed.ResourceType)
}
}

// GetCache returns the Azure cache instance
func (c *AzureClient) GetCache() *AzureCache {
return c.cache
}
91 changes: 91 additions & 0 deletions 91 internal/azureclient/detector.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
package azureclient

import (
"context"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"strings"

"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
)

// MakeDetectorAPICall makes an HTTP request to Azure Management API for detector operations
func (c *AzureClient) MakeDetectorAPICall(ctx context.Context, url string, subscriptionID string) (*http.Response, error) {
// Create HTTP client with Azure authentication
client := &http.Client{}

// Create request
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
return nil, fmt.Errorf("failed to create request: %v", err)
}

// Get access token for the request
token, err := c.credential.GetToken(ctx, policy.TokenRequestOptions{
Scopes: []string{"https://management.azure.com/.default"},
})
if err != nil {
return nil, fmt.Errorf("failed to get access token: %v", err)
}

// Set headers
req.Header.Set("Authorization", "Bearer "+token.Token)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("User-Agent", "AKS-MCP")

// Make the request
resp, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("failed to make request: %v", err)
}

return resp, nil
}

// ParseResourceID extracts subscription, resource group, and cluster name from AKS resource ID
func ParseAKSResourceID(resourceID string) (subscriptionID, resourceGroup, clusterName string, err error) {
// Expected format: /subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.ContainerService/managedClusters/{clusterName}
parts := strings.Split(strings.TrimPrefix(resourceID, "/"), "/")

if len(parts) < 8 || !strings.EqualFold(parts[0], "subscriptions") || !strings.EqualFold(parts[2], "resourceGroups") ||
!strings.EqualFold(parts[4], "providers") || !strings.EqualFold(parts[5], "Microsoft.ContainerService") || !strings.EqualFold(parts[6], "managedClusters") {
return "", "", "", fmt.Errorf("invalid AKS resource ID format: %s", resourceID)
}

subscriptionID = parts[1]
resourceGroup = parts[3]
clusterName = parts[7]

return subscriptionID, resourceGroup, clusterName, nil
}

// HandleDetectorAPIResponse reads and handles the response from detector API calls
func HandleDetectorAPIResponse(resp *http.Response) ([]byte, error) {
defer func() {
if err := resp.Body.Close(); err != nil {
log.Printf("Warning: failed to close response body: %v", err)
}
}()

body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed to read response body: %v", err)
}

if resp.StatusCode != http.StatusOK {
var errorMsg map[string]interface{}
if err := json.Unmarshal(body, &errorMsg); err == nil {
if msg, ok := errorMsg["error"].(map[string]interface{}); ok {
if message, ok := msg["message"].(string); ok {
return nil, fmt.Errorf("API error (%d): %s", resp.StatusCode, message)
}
}
}
return nil, fmt.Errorf("API error (%d): %s", resp.StatusCode, string(body))
}

return body, nil
}
141 changes: 141 additions & 0 deletions 141 internal/components/detectors/client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
package detectors

import (
"context"
"encoding/json"
"fmt"
"net/url"
"strings"

"github.com/Azure/aks-mcp/internal/azureclient"
)

// DetectorClient wraps Azure API calls with caching
type DetectorClient struct {
azClient *azureclient.AzureClient
cache *azureclient.AzureCache
}

// NewDetectorClient creates a new detector client
func NewDetectorClient(azClient *azureclient.AzureClient) *DetectorClient {
return &DetectorClient{
azClient: azClient,
cache: azClient.GetCache(),
}
}

// ListDetectors lists all detectors for a cluster with caching
func (c *DetectorClient) ListDetectors(ctx context.Context, subscriptionID, resourceGroup, clusterName string) (*DetectorListResponse, error) {
// Create cache key
cacheKey := fmt.Sprintf("detectors:list:%s:%s:%s", subscriptionID, resourceGroup, clusterName)

// Check cache first
if cached, found := c.cache.Get(cacheKey); found {
if detectors, ok := cached.(*DetectorListResponse); ok {
return detectors, nil
}
}

// Build API URL
apiURL := fmt.Sprintf("https://management.azure.com/subscriptions/%s/resourceGroups/%s/providers/Microsoft.ContainerService/managedClusters/%s/detectors?api-version=2024-08-01",
url.PathEscape(subscriptionID),
url.PathEscape(resourceGroup),
url.PathEscape(clusterName))

// Make API call
resp, err := c.azClient.MakeDetectorAPICall(ctx, apiURL, subscriptionID)
if err != nil {
return nil, fmt.Errorf("failed to call detector list API: %v", err)
}

// Handle response
body, err := azureclient.HandleDetectorAPIResponse(resp)
if err != nil {
return nil, fmt.Errorf("failed to handle detector list response: %v", err)
}

// Parse response
var detectorList DetectorListResponse
if err := json.Unmarshal(body, &detectorList); err != nil {
return nil, fmt.Errorf("failed to parse detector list response: %v", err)
}

// Cache the result
c.cache.Set(cacheKey, &detectorList)

return &detectorList, nil
}

// RunDetector executes a specific detector
func (c *DetectorClient) RunDetector(ctx context.Context, subscriptionID, resourceGroup, clusterName, detectorName, startTime, endTime string) (*DetectorRunResponse, error) {
// Build API URL with query parameters
apiURL := fmt.Sprintf("https://management.azure.com/subscriptions/%s/resourcegroups/%s/providers/microsoft.containerservice/managedclusters/%s/detectors/%s?startTime=%s&endTime=%s&api-version=2024-08-01",
url.PathEscape(subscriptionID),
url.PathEscape(resourceGroup),
url.PathEscape(clusterName),
url.PathEscape(detectorName),
url.QueryEscape(startTime),
url.QueryEscape(endTime))

// Make API call
resp, err := c.azClient.MakeDetectorAPICall(ctx, apiURL, subscriptionID)
if err != nil {
return nil, fmt.Errorf("failed to call detector run API: %v", err)
}

// Handle response
body, err := azureclient.HandleDetectorAPIResponse(resp)
if err != nil {
return nil, fmt.Errorf("failed to handle detector run response: %v", err)
}

// Parse response
var detectorRun DetectorRunResponse
if err := json.Unmarshal(body, &detectorRun); err != nil {
return nil, fmt.Errorf("failed to parse detector run response: %v", err)
}

return &detectorRun, nil
}

// GetDetectorsByCategory filters detectors by category from cached list
func (c *DetectorClient) GetDetectorsByCategory(ctx context.Context, subscriptionID, resourceGroup, clusterName, category string) ([]Detector, error) {
// Get full detector list (will use cache if available)
detectorList, err := c.ListDetectors(ctx, subscriptionID, resourceGroup, clusterName)
if err != nil {
return nil, fmt.Errorf("failed to get detector list: %v", err)
}

// Filter by category
var filteredDetectors []Detector
for _, detector := range detectorList.Value {
if strings.EqualFold(detector.Properties.Metadata.Category, category) {
filteredDetectors = append(filteredDetectors, detector)
}
}

return filteredDetectors, nil
}

// RunDetectorsByCategory executes all detectors in a specific category
func (c *DetectorClient) RunDetectorsByCategory(ctx context.Context, subscriptionID, resourceGroup, clusterName, category, startTime, endTime string) ([]DetectorRunResponse, error) {
// Get detectors by category
detectors, err := c.GetDetectorsByCategory(ctx, subscriptionID, resourceGroup, clusterName, category)
if err != nil {
return nil, fmt.Errorf("failed to get detectors by category: %v", err)
}

// Run each detector
var results []DetectorRunResponse
for _, detector := range detectors {
result, err := c.RunDetector(ctx, subscriptionID, resourceGroup, clusterName, detector.Properties.Metadata.ID, startTime, endTime)
if err != nil {
// Log error but continue with other detectors
fmt.Printf("Failed to run detector %s: %v\n", detector.Properties.Metadata.Name, err)
continue
}
results = append(results, *result)
}

return results, nil
}
108 changes: 108 additions & 0 deletions 108 internal/components/detectors/detectors_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
package detectors

import (
"testing"
"time"
)

func TestValidateTimeParameters(t *testing.T) {
now := time.Now()
validStart := now.Add(-1 * time.Hour).Format(time.RFC3339)
validEnd := now.Format(time.RFC3339)

tests := []struct {
name string
startTime string
endTime string
wantErr bool
}{
{
name: "valid time range",
startTime: validStart,
endTime: validEnd,
wantErr: false,
},
{
name: "invalid start time format",
startTime: "invalid-time",
endTime: validEnd,
wantErr: true,
},
{
name: "invalid end time format",
startTime: validStart,
endTime: "invalid-time",
wantErr: true,
},
{
name: "end time before start time",
startTime: validEnd,
endTime: validStart,
wantErr: true,
},
{
name: "time range too long (over 24h)",
startTime: now.Add(-25 * time.Hour).Format(time.RFC3339),
endTime: now.Format(time.RFC3339),
wantErr: true,
},
{
name: "start time too old (over 30 days)",
startTime: now.AddDate(0, 0, -31).Format(time.RFC3339),
endTime: now.AddDate(0, 0, -30).Format(time.RFC3339),
wantErr: true,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := validateTimeParameters(tt.startTime, tt.endTime)
if (err != nil) != tt.wantErr {
t.Errorf("validateTimeParameters() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}

func TestValidateCategory(t *testing.T) {
tests := []struct {
name string
category string
wantErr bool
}{
{
name: "valid category - Best Practices",
category: "Best Practices",
wantErr: false,
},
{
name: "valid category - Node Health",
category: "Node Health",
wantErr: false,
},
{
name: "invalid category",
category: "Invalid Category",
wantErr: true,
},
{
name: "empty category",
category: "",
wantErr: true,
},
{
name: "case sensitive validation",
category: "best practices",
wantErr: true,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := validateCategory(tt.category)
if (err != nil) != tt.wantErr {
t.Errorf("validateCategory() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
Loading
Loading
Morty Proxy This is a proxified and sanitized view of the page, visit original site.