Files
a8fb5210f4 Advanced analysis clean (#1868)
* created roadmap and yaml claude agent

* Update roadmap.md

* feat: Clean advanced analysis implementation - core agents, engine, artifacts

* Remove unrelated files - keep only advanced analysis implementation

* fix: Fix goroutine leak in hosted agent rate limiter

- Added stop channel and stopped flag to RateLimiter struct
- Modified replenishTokens to listen for stop signal and exit cleanly
- Added Stop() method to gracefully shutdown rate limiter
- Added Stop() method to HostedAgent to cleanup rate limiter on shutdown

Fixes cursor bot issue: Rate Limiter Goroutine Leak

* fix: Fix analyzer config and model validation bugs

Bug 1: Analyzer Config Missing File Path
- Added filePath to DeploymentStatus analyzer config in convertAnalyzerToSpec
- Sets namespace-specific path (cluster-resources/deployments/{namespace}.json)
- Falls back to generic path (cluster-resources/deployments.json) if no namespace
- Fixes LocalAgent.analyzeDeploymentStatus backward compatibility

Bug 2: HealthCheck Fails Model Validation
- Changed Ollama model validation from prefix match to exact match
- Prevents false positives where llama2:13b would match request for llama2:7b
- Ensures agent only reports healthy when exact model is available

Both fixes address cursor bot reported issues and maintain backward compatibility.

* fixing lint errors

* fixing lint errors

* adding CLI flags

* fix: resolve linting errors for CI

- Remove unnecessary nil check in host_kernel_configs.go (len() for nil slices is zero)
- Remove unnecessary fmt.Sprintf() calls in ceph.go for static strings
- Apply go fmt formatting fixes

Fixes failing lint CI check

* fix: resolve CI failures in build-test workflow and Ollama tests

1. Fix GitHub Actions workflow logic error:
   - Replace problematic contains() expression with explicit job result checks
   - Properly handle failure and cancelled states for each job
   - Prevents false positive failures in success summary job

2. Fix Ollama agent parseLLMResponse panics:
   - Add proper error handling for malformed JSON in LLM responses
   - Return error when JSON is found but invalid (instead of silent fallback)
   - Add error when no meaningful content can be parsed from response
   - Prevents nil pointer dereference in test assertions

Fixes failing build-test/success and build-test/test CI checks

* fix: resolve all CI failures and cursor bot issues

1. Fix disable-ollama flag logic bug:
   - Remove disable-ollama from advanced analysis trigger condition
   - Prevents unintended advanced analysis mode when no agents registered
   - Allows proper fallback to legacy analysis

2. Fix diff test consistency:
   - Update test expectations to match function behavior (lines with newlines)
   - Ensures consistency between streaming and non-streaming diff paths

3. Fix Ollama agent error handling:
   - Add proper error return for malformed JSON in LLM responses
   - Add meaningful content validation for markdown parsing
   - Prevents nil pointer panics in test assertions

4. Fix analysis engine mock agent:
   - Mock agent now processes and returns results for all provided analyzers
   - Fixes test expectation mismatch (expected 8 results, got 1)

Resolves all failing CI checks: lint, test, and success workflow logic

---------

Co-authored-by: Noah Campbell <noah.edward.campbell@gmail.com>
2025-09-30 11:44:46 -05:00

980 lines
28 KiB
Go

package generators
import (
"context"
"fmt"
"regexp"
"strings"
"github.com/pkg/errors"
analyzer "github.com/replicatedhq/troubleshoot/pkg/analyze"
"github.com/replicatedhq/troubleshoot/pkg/constants"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"k8s.io/klog/v2"
)
// AnalyzerGenerator generates analyzer specifications from requirements
type AnalyzerGenerator struct {
templates map[string]AnalyzerTemplate
validators map[string]RequirementValidator
}
// AnalyzerTemplate defines how to generate analyzers for specific requirement types
type AnalyzerTemplate struct {
Name string
Description string
Category string
Priority int
Generator func(ctx context.Context, req interface{}) ([]analyzer.AnalyzerSpec, error)
Validator func(req interface{}) error
}
// RequirementValidator validates requirement specifications
type RequirementValidator func(requirement interface{}) error
// GenerationOptions configures analyzer generation
type GenerationOptions struct {
IncludeOptional bool
Strict bool
DefaultPriority int
CategoryFilter []string
CustomTemplates map[string]AnalyzerTemplate
}
// NewAnalyzerGenerator creates a new analyzer generator with default templates
func NewAnalyzerGenerator() *AnalyzerGenerator {
g := &AnalyzerGenerator{
templates: make(map[string]AnalyzerTemplate),
validators: make(map[string]RequirementValidator),
}
// Register default templates
g.registerDefaultTemplates()
g.registerDefaultValidators()
return g
}
// GenerateAnalyzers creates analyzer specifications from requirements
func (g *AnalyzerGenerator) GenerateAnalyzers(ctx context.Context, requirements *analyzer.RequirementSpec, opts *GenerationOptions) ([]analyzer.AnalyzerSpec, error) {
ctx, span := otel.Tracer(constants.LIB_TRACER_NAME).Start(ctx, "AnalyzerGenerator.GenerateAnalyzers")
defer span.End()
if requirements == nil {
return nil, errors.New("requirements cannot be nil")
}
if opts == nil {
opts = &GenerationOptions{
IncludeOptional: true,
DefaultPriority: 5,
}
}
var allSpecs []analyzer.AnalyzerSpec
// Generate Kubernetes version analyzers
if specs, err := g.generateKubernetesAnalyzers(ctx, &requirements.Spec.Kubernetes, opts); err == nil {
allSpecs = append(allSpecs, specs...)
} else {
klog.Warningf("Failed to generate Kubernetes analyzers: %v", err)
}
// Generate resource requirement analyzers
if specs, err := g.generateResourceAnalyzers(ctx, &requirements.Spec.Resources, opts); err == nil {
allSpecs = append(allSpecs, specs...)
} else {
klog.Warningf("Failed to generate resource analyzers: %v", err)
}
// Generate storage requirement analyzers
if specs, err := g.generateStorageAnalyzers(ctx, &requirements.Spec.Storage, opts); err == nil {
allSpecs = append(allSpecs, specs...)
} else {
klog.Warningf("Failed to generate storage analyzers: %v", err)
}
// Generate network requirement analyzers
if specs, err := g.generateNetworkAnalyzers(ctx, &requirements.Spec.Network, opts); err == nil {
allSpecs = append(allSpecs, specs...)
} else {
klog.Warningf("Failed to generate network analyzers: %v", err)
}
// Generate custom analyzers
for _, customReq := range requirements.Spec.Custom {
if specs, err := g.generateCustomAnalyzers(ctx, &customReq, opts); err == nil {
allSpecs = append(allSpecs, specs...)
} else {
klog.Warningf("Failed to generate custom analyzer %s: %v", customReq.Name, err)
}
}
// Apply category filtering if specified
if len(opts.CategoryFilter) > 0 {
allSpecs = g.filterByCategory(allSpecs, opts.CategoryFilter)
}
// Sort by priority (higher priority first)
g.sortByPriority(allSpecs)
span.SetAttributes(
attribute.Int("total_generated", len(allSpecs)),
attribute.String("requirements_name", requirements.Metadata.Name),
attribute.Bool("include_optional", opts.IncludeOptional),
)
return allSpecs, nil
}
// generateKubernetesAnalyzers creates analyzers for Kubernetes requirements
func (g *AnalyzerGenerator) generateKubernetesAnalyzers(ctx context.Context, req *analyzer.KubernetesRequirements, opts *GenerationOptions) ([]analyzer.AnalyzerSpec, error) {
var specs []analyzer.AnalyzerSpec
// Kubernetes version check analyzer
if req.MinVersion != "" || req.MaxVersion != "" {
spec := analyzer.AnalyzerSpec{
Name: "kubernetes-version-requirement",
Type: "cluster",
Category: "kubernetes",
Priority: 10,
Config: map[string]interface{}{
"checkName": "Kubernetes Version Check",
"minVersion": req.MinVersion,
"maxVersion": req.MaxVersion,
"outcomes": g.generateVersionOutcomes(req.MinVersion, req.MaxVersion),
},
}
specs = append(specs, spec)
}
// Required components analyzer
if len(req.Required) > 0 {
spec := analyzer.AnalyzerSpec{
Name: "kubernetes-components-required",
Type: "cluster",
Category: "kubernetes",
Priority: 9,
Config: map[string]interface{}{
"checkName": "Required Components Check",
"required": req.Required,
"outcomes": g.generateComponentOutcomes(req.Required, true),
},
}
specs = append(specs, spec)
}
// Forbidden components analyzer
if len(req.Forbidden) > 0 {
spec := analyzer.AnalyzerSpec{
Name: "kubernetes-components-forbidden",
Type: "cluster",
Category: "kubernetes",
Priority: 8,
Config: map[string]interface{}{
"checkName": "Forbidden Components Check",
"forbidden": req.Forbidden,
"outcomes": g.generateComponentOutcomes(req.Forbidden, false),
},
}
specs = append(specs, spec)
}
return specs, nil
}
// generateResourceAnalyzers creates analyzers for resource requirements
func (g *AnalyzerGenerator) generateResourceAnalyzers(ctx context.Context, req *analyzer.ResourceRequirements, opts *GenerationOptions) ([]analyzer.AnalyzerSpec, error) {
var specs []analyzer.AnalyzerSpec
// Node resources analyzer
if req.CPU.Min != "" || req.Memory.Min != "" || req.Disk.Min != "" {
spec := analyzer.AnalyzerSpec{
Name: "node-resources-requirement",
Type: "resources",
Category: "capacity",
Priority: 9,
Config: map[string]interface{}{
"checkName": "Node Resources Check",
"cpu": req.CPU,
"memory": req.Memory,
"disk": req.Disk,
"outcomes": g.generateResourceOutcomes(req),
},
}
specs = append(specs, spec)
}
// Cluster capacity analyzer
if req.CPU.Min != "" || req.Memory.Min != "" {
spec := analyzer.AnalyzerSpec{
Name: "cluster-capacity-requirement",
Type: "resources",
Category: "capacity",
Priority: 8,
Config: map[string]interface{}{
"checkName": "Cluster Capacity Check",
"requirements": req,
"outcomes": g.generateClusterCapacityOutcomes(req),
},
}
specs = append(specs, spec)
}
return specs, nil
}
// generateStorageAnalyzers creates analyzers for storage requirements
func (g *AnalyzerGenerator) generateStorageAnalyzers(ctx context.Context, req *analyzer.StorageRequirements, opts *GenerationOptions) ([]analyzer.AnalyzerSpec, error) {
var specs []analyzer.AnalyzerSpec
// Storage class analyzer
if len(req.Classes) > 0 {
spec := analyzer.AnalyzerSpec{
Name: "storage-class-requirement",
Type: "storage",
Category: "storage",
Priority: 8,
Config: map[string]interface{}{
"checkName": "Storage Class Check",
"storageClass": req.Classes[0], // Use first class as primary
"outcomes": g.generateStorageClassOutcomes(req.Classes),
},
}
specs = append(specs, spec)
}
// Persistent volume analyzer
if req.MinCapacity != "" {
spec := analyzer.AnalyzerSpec{
Name: "persistent-volume-requirement",
Type: "storage",
Category: "storage",
Priority: 7,
Config: map[string]interface{}{
"checkName": "Persistent Volume Capacity Check",
"minCapacity": req.MinCapacity,
"accessModes": req.AccessModes,
"outcomes": g.generatePVOutcomes(req),
},
}
specs = append(specs, spec)
}
return specs, nil
}
// generateNetworkAnalyzers creates analyzers for network requirements
func (g *AnalyzerGenerator) generateNetworkAnalyzers(ctx context.Context, req *analyzer.NetworkRequirements, opts *GenerationOptions) ([]analyzer.AnalyzerSpec, error) {
var specs []analyzer.AnalyzerSpec
// Port connectivity analyzer
for _, port := range req.Ports {
spec := analyzer.AnalyzerSpec{
Name: fmt.Sprintf("port-connectivity-%d", port.Port),
Type: "network",
Category: "networking",
Priority: 7,
Config: map[string]interface{}{
"checkName": fmt.Sprintf("Port %d Connectivity Check", port.Port),
"port": port.Port,
"protocol": port.Protocol,
"required": port.Required,
"outcomes": g.generatePortOutcomes(port),
},
}
specs = append(specs, spec)
}
// General connectivity analyzer
if len(req.Connectivity) > 0 {
spec := analyzer.AnalyzerSpec{
Name: "network-connectivity-requirement",
Type: "network",
Category: "networking",
Priority: 6,
Config: map[string]interface{}{
"checkName": "Network Connectivity Check",
"connectivity": req.Connectivity,
"outcomes": g.generateConnectivityOutcomes(req.Connectivity),
},
}
specs = append(specs, spec)
}
return specs, nil
}
// generateCustomAnalyzers creates analyzers for custom requirements
func (g *AnalyzerGenerator) generateCustomAnalyzers(ctx context.Context, req *analyzer.CustomRequirement, opts *GenerationOptions) ([]analyzer.AnalyzerSpec, error) {
var specs []analyzer.AnalyzerSpec
// Check if we have a template for this custom type
template, exists := g.templates[req.Type]
if exists {
customSpecs, err := template.Generator(ctx, req)
if err != nil {
return nil, errors.Wrapf(err, "failed to generate custom analyzer %s", req.Name)
}
specs = append(specs, customSpecs...)
} else {
// Generic custom analyzer
spec := analyzer.AnalyzerSpec{
Name: req.Name,
Type: req.Type,
Category: "custom",
Priority: opts.DefaultPriority,
Config: map[string]interface{}{
"checkName": req.Name,
"condition": req.Condition,
"context": req.Context,
"outcomes": g.generateCustomOutcomes(req),
},
}
specs = append(specs, spec)
}
return specs, nil
}
// Outcome generation methods
func (g *AnalyzerGenerator) generateVersionOutcomes(minVersion, maxVersion string) []map[string]interface{} {
var outcomes []map[string]interface{}
// Pass condition
passCondition := "true"
if minVersion != "" && maxVersion != "" {
passCondition = fmt.Sprintf(">= %s && < %s", minVersion, maxVersion)
} else if minVersion != "" {
passCondition = fmt.Sprintf(">= %s", minVersion)
} else if maxVersion != "" {
passCondition = fmt.Sprintf("< %s", maxVersion)
}
outcomes = append(outcomes, map[string]interface{}{
"pass": map[string]interface{}{
"when": passCondition,
"message": "Kubernetes version meets requirements",
},
})
// Fail condition
if minVersion != "" {
outcomes = append(outcomes, map[string]interface{}{
"fail": map[string]interface{}{
"when": fmt.Sprintf("< %s", minVersion),
"message": fmt.Sprintf("Kubernetes version is below minimum required version %s", minVersion),
},
})
}
if maxVersion != "" {
outcomes = append(outcomes, map[string]interface{}{
"fail": map[string]interface{}{
"when": fmt.Sprintf(">= %s", maxVersion),
"message": fmt.Sprintf("Kubernetes version is at or above maximum supported version %s", maxVersion),
},
})
}
return outcomes
}
func (g *AnalyzerGenerator) generateComponentOutcomes(components []string, required bool) []map[string]interface{} {
var outcomes []map[string]interface{}
for _, component := range components {
if required {
outcomes = append(outcomes, map[string]interface{}{
"fail": map[string]interface{}{
"when": fmt.Sprintf("missing %s", component),
"message": fmt.Sprintf("Required component %s is missing", component),
},
})
} else {
outcomes = append(outcomes, map[string]interface{}{
"fail": map[string]interface{}{
"when": fmt.Sprintf("present %s", component),
"message": fmt.Sprintf("Forbidden component %s is present", component),
},
})
}
}
// Default pass outcome
if required {
outcomes = append(outcomes, map[string]interface{}{
"pass": map[string]interface{}{
"message": "All required components are present",
},
})
} else {
outcomes = append(outcomes, map[string]interface{}{
"pass": map[string]interface{}{
"message": "No forbidden components are present",
},
})
}
return outcomes
}
func (g *AnalyzerGenerator) generateResourceOutcomes(req *analyzer.ResourceRequirements) []map[string]interface{} {
var outcomes []map[string]interface{}
// CPU requirements
if req.CPU.Min != "" {
outcomes = append(outcomes, map[string]interface{}{
"fail": map[string]interface{}{
"when": fmt.Sprintf("cpu < %s", req.CPU.Min),
"message": fmt.Sprintf("Insufficient CPU resources. Minimum required: %s", req.CPU.Min),
},
})
}
// Memory requirements
if req.Memory.Min != "" {
outcomes = append(outcomes, map[string]interface{}{
"fail": map[string]interface{}{
"when": fmt.Sprintf("memory < %s", req.Memory.Min),
"message": fmt.Sprintf("Insufficient memory resources. Minimum required: %s", req.Memory.Min),
},
})
}
// Disk requirements
if req.Disk.Min != "" {
outcomes = append(outcomes, map[string]interface{}{
"warn": map[string]interface{}{
"when": fmt.Sprintf("disk < %s", req.Disk.Min),
"message": fmt.Sprintf("Low disk space. Minimum recommended: %s", req.Disk.Min),
},
})
}
// Pass condition
outcomes = append(outcomes, map[string]interface{}{
"pass": map[string]interface{}{
"message": "Resource requirements are satisfied",
},
})
return outcomes
}
func (g *AnalyzerGenerator) generateClusterCapacityOutcomes(req *analyzer.ResourceRequirements) []map[string]interface{} {
var outcomes []map[string]interface{}
outcomes = append(outcomes, map[string]interface{}{
"fail": map[string]interface{}{
"when": "clusterCapacity < requirements",
"message": "Cluster does not have sufficient capacity to meet requirements",
},
})
outcomes = append(outcomes, map[string]interface{}{
"warn": map[string]interface{}{
"when": "clusterCapacity < requirements * 1.2",
"message": "Cluster capacity is close to requirements. Consider adding buffer capacity",
},
})
outcomes = append(outcomes, map[string]interface{}{
"pass": map[string]interface{}{
"message": "Cluster has sufficient capacity for requirements",
},
})
return outcomes
}
func (g *AnalyzerGenerator) generateStorageClassOutcomes(classes []string) []map[string]interface{} {
var outcomes []map[string]interface{}
for _, class := range classes {
outcomes = append(outcomes, map[string]interface{}{
"fail": map[string]interface{}{
"when": fmt.Sprintf("storageClass == %s && !exists", class),
"message": fmt.Sprintf("Required storage class %s does not exist", class),
},
})
}
outcomes = append(outcomes, map[string]interface{}{
"pass": map[string]interface{}{
"message": "Required storage classes are available",
},
})
return outcomes
}
func (g *AnalyzerGenerator) generatePVOutcomes(req *analyzer.StorageRequirements) []map[string]interface{} {
var outcomes []map[string]interface{}
outcomes = append(outcomes, map[string]interface{}{
"fail": map[string]interface{}{
"when": fmt.Sprintf("availableCapacity < %s", req.MinCapacity),
"message": fmt.Sprintf("Insufficient storage capacity. Minimum required: %s", req.MinCapacity),
},
})
if len(req.AccessModes) > 0 {
for _, mode := range req.AccessModes {
outcomes = append(outcomes, map[string]interface{}{
"warn": map[string]interface{}{
"when": fmt.Sprintf("!accessMode.%s", mode),
"message": fmt.Sprintf("Access mode %s may not be supported", mode),
},
})
}
}
outcomes = append(outcomes, map[string]interface{}{
"pass": map[string]interface{}{
"message": "Storage requirements are satisfied",
},
})
return outcomes
}
func (g *AnalyzerGenerator) generatePortOutcomes(port analyzer.PortRequirement) []map[string]interface{} {
var outcomes []map[string]interface{}
if port.Required {
outcomes = append(outcomes, map[string]interface{}{
"fail": map[string]interface{}{
"when": fmt.Sprintf("port.%d.%s == false", port.Port, strings.ToLower(port.Protocol)),
"message": fmt.Sprintf("Required port %d/%s is not accessible", port.Port, port.Protocol),
},
})
}
outcomes = append(outcomes, map[string]interface{}{
"pass": map[string]interface{}{
"when": fmt.Sprintf("port.%d.%s == true", port.Port, strings.ToLower(port.Protocol)),
"message": fmt.Sprintf("Port %d/%s is accessible", port.Port, port.Protocol),
},
})
return outcomes
}
func (g *AnalyzerGenerator) generateConnectivityOutcomes(connectivity []string) []map[string]interface{} {
var outcomes []map[string]interface{}
for _, target := range connectivity {
outcomes = append(outcomes, map[string]interface{}{
"fail": map[string]interface{}{
"when": fmt.Sprintf("connectivity.%s == false", target),
"message": fmt.Sprintf("Cannot reach %s", target),
},
})
}
outcomes = append(outcomes, map[string]interface{}{
"pass": map[string]interface{}{
"message": "All connectivity requirements are satisfied",
},
})
return outcomes
}
func (g *AnalyzerGenerator) generateCustomOutcomes(req *analyzer.CustomRequirement) []map[string]interface{} {
var outcomes []map[string]interface{}
// Parse the condition and generate appropriate outcomes
condition := req.Condition
if condition == "" {
condition = "true"
}
// Basic pattern matching for common conditions
if strings.Contains(condition, ">=") || strings.Contains(condition, ">") {
outcomes = append(outcomes, map[string]interface{}{
"fail": map[string]interface{}{
"when": fmt.Sprintf("!(%s)", condition),
"message": fmt.Sprintf("Custom requirement '%s' not met", req.Name),
},
})
}
outcomes = append(outcomes, map[string]interface{}{
"pass": map[string]interface{}{
"when": condition,
"message": fmt.Sprintf("Custom requirement '%s' is satisfied", req.Name),
},
})
return outcomes
}
// Helper methods
func (g *AnalyzerGenerator) filterByCategory(specs []analyzer.AnalyzerSpec, categories []string) []analyzer.AnalyzerSpec {
if len(categories) == 0 {
return specs
}
var filtered []analyzer.AnalyzerSpec
categorySet := make(map[string]bool)
for _, cat := range categories {
categorySet[cat] = true
}
for _, spec := range specs {
if categorySet[spec.Category] {
filtered = append(filtered, spec)
}
}
return filtered
}
func (g *AnalyzerGenerator) sortByPriority(specs []analyzer.AnalyzerSpec) {
// Simple bubble sort by priority (higher first)
n := len(specs)
for i := 0; i < n-1; i++ {
for j := 0; j < n-1-i; j++ {
if specs[j].Priority < specs[j+1].Priority {
specs[j], specs[j+1] = specs[j+1], specs[j]
}
}
}
}
// Template and validator registration
func (g *AnalyzerGenerator) registerDefaultTemplates() {
// Register built-in analyzer templates
g.templates["database"] = AnalyzerTemplate{
Name: "Database Analyzer",
Description: "Analyzes database connectivity and requirements",
Category: "database",
Priority: 7,
Generator: g.generateDatabaseAnalyzer,
Validator: g.validateDatabaseRequirement,
}
g.templates["api"] = AnalyzerTemplate{
Name: "API Analyzer",
Description: "Analyzes API endpoint connectivity and requirements",
Category: "api",
Priority: 6,
Generator: g.generateAPIAnalyzer,
Validator: g.validateAPIRequirement,
}
}
func (g *AnalyzerGenerator) registerDefaultValidators() {
g.validators["version"] = func(req interface{}) error {
// Validate version format
versionStr, ok := req.(string)
if !ok {
return errors.New("version must be a string")
}
// Basic semantic version validation
versionRegex := regexp.MustCompile(`^v?\d+\.\d+(\.\d+)?(-.*)?$`)
if !versionRegex.MatchString(versionStr) {
return errors.Errorf("invalid version format: %s", versionStr)
}
return nil
}
g.validators["resource"] = func(req interface{}) error {
// Validate resource specifications
resourceStr, ok := req.(string)
if !ok {
return errors.New("resource must be a string")
}
// Validate resource format (e.g., "100m", "1Gi", "500Mi")
resourceRegex := regexp.MustCompile(`^(\d+(\.\d+)?)(m|Mi|Gi|Ti|Ki|k|M|G|T)?$`)
if !resourceRegex.MatchString(resourceStr) {
return errors.Errorf("invalid resource format: %s", resourceStr)
}
return nil
}
}
// Custom analyzer generators
func (g *AnalyzerGenerator) generateDatabaseAnalyzer(ctx context.Context, req interface{}) ([]analyzer.AnalyzerSpec, error) {
customReq, ok := req.(*analyzer.CustomRequirement)
if !ok {
return nil, errors.New("invalid database requirement type")
}
spec := analyzer.AnalyzerSpec{
Name: fmt.Sprintf("database-%s", customReq.Name),
Type: "database",
Category: "database",
Priority: 7,
Config: map[string]interface{}{
"checkName": fmt.Sprintf("Database %s Check", customReq.Name),
"uri": customReq.Context["uri"],
"timeout": "10s",
"outcomes": []map[string]interface{}{
{
"fail": map[string]interface{}{
"when": "error",
"message": "Database connection failed",
},
},
{
"pass": map[string]interface{}{
"message": "Database connection successful",
},
},
},
},
}
return []analyzer.AnalyzerSpec{spec}, nil
}
func (g *AnalyzerGenerator) generateAPIAnalyzer(ctx context.Context, req interface{}) ([]analyzer.AnalyzerSpec, error) {
customReq, ok := req.(*analyzer.CustomRequirement)
if !ok {
return nil, errors.New("invalid API requirement type")
}
spec := analyzer.AnalyzerSpec{
Name: fmt.Sprintf("api-%s", customReq.Name),
Type: "http",
Category: "api",
Priority: 6,
Config: map[string]interface{}{
"checkName": fmt.Sprintf("API %s Check", customReq.Name),
"get": map[string]interface{}{
"url": customReq.Context["url"],
},
"outcomes": []map[string]interface{}{
{
"fail": map[string]interface{}{
"when": "status != 200",
"message": "API endpoint is not accessible",
},
},
{
"pass": map[string]interface{}{
"when": "status == 200",
"message": "API endpoint is accessible",
},
},
},
},
}
return []analyzer.AnalyzerSpec{spec}, nil
}
// Custom requirement validators
func (g *AnalyzerGenerator) validateDatabaseRequirement(req interface{}) error {
customReq, ok := req.(*analyzer.CustomRequirement)
if !ok {
return errors.New("invalid requirement type")
}
if customReq.Context == nil {
return errors.New("database requirement must have context")
}
if _, exists := customReq.Context["uri"]; !exists {
return errors.New("database requirement must specify 'uri' in context")
}
return nil
}
func (g *AnalyzerGenerator) validateAPIRequirement(req interface{}) error {
customReq, ok := req.(*analyzer.CustomRequirement)
if !ok {
return errors.New("invalid requirement type")
}
if customReq.Context == nil {
return errors.New("API requirement must have context")
}
if _, exists := customReq.Context["url"]; !exists {
return errors.New("API requirement must specify 'url' in context")
}
return nil
}
// RegisterTemplate registers a custom analyzer template
func (g *AnalyzerGenerator) RegisterTemplate(name string, template AnalyzerTemplate) error {
if name == "" {
return errors.New("template name cannot be empty")
}
if template.Generator == nil {
return errors.New("template generator cannot be nil")
}
g.templates[name] = template
return nil
}
// RegisterValidator registers a custom requirement validator
func (g *AnalyzerGenerator) RegisterValidator(name string, validator RequirementValidator) error {
if name == "" {
return errors.New("validator name cannot be empty")
}
if validator == nil {
return errors.New("validator cannot be nil")
}
g.validators[name] = validator
return nil
}
// ValidateRequirements validates a requirement specification
func (g *AnalyzerGenerator) ValidateRequirements(ctx context.Context, requirements *analyzer.RequirementSpec) error {
if requirements == nil {
return errors.New("requirements cannot be nil")
}
// Validate Kubernetes requirements
if err := g.validateKubernetesRequirements(&requirements.Spec.Kubernetes); err != nil {
return errors.Wrap(err, "invalid Kubernetes requirements")
}
// Validate resource requirements
if err := g.validateResourceRequirements(&requirements.Spec.Resources); err != nil {
return errors.Wrap(err, "invalid resource requirements")
}
// Validate storage requirements
if err := g.validateStorageRequirements(&requirements.Spec.Storage); err != nil {
return errors.Wrap(err, "invalid storage requirements")
}
// Validate network requirements
if err := g.validateNetworkRequirements(&requirements.Spec.Network); err != nil {
return errors.Wrap(err, "invalid network requirements")
}
// Validate custom requirements
for i, customReq := range requirements.Spec.Custom {
if err := g.validateCustomRequirement(&customReq); err != nil {
return errors.Wrapf(err, "invalid custom requirement at index %d", i)
}
}
return nil
}
func (g *AnalyzerGenerator) validateKubernetesRequirements(req *analyzer.KubernetesRequirements) error {
if req.MinVersion != "" {
if err := g.validators["version"](req.MinVersion); err != nil {
return errors.Wrap(err, "invalid minVersion")
}
}
if req.MaxVersion != "" {
if err := g.validators["version"](req.MaxVersion); err != nil {
return errors.Wrap(err, "invalid maxVersion")
}
}
return nil
}
func (g *AnalyzerGenerator) validateResourceRequirements(req *analyzer.ResourceRequirements) error {
if req.CPU.Min != "" {
if err := g.validators["resource"](req.CPU.Min); err != nil {
return errors.Wrap(err, "invalid CPU minimum")
}
}
if req.Memory.Min != "" {
if err := g.validators["resource"](req.Memory.Min); err != nil {
return errors.Wrap(err, "invalid memory minimum")
}
}
if req.Disk.Min != "" {
if err := g.validators["resource"](req.Disk.Min); err != nil {
return errors.Wrap(err, "invalid disk minimum")
}
}
return nil
}
func (g *AnalyzerGenerator) validateStorageRequirements(req *analyzer.StorageRequirements) error {
if req.MinCapacity != "" {
if err := g.validators["resource"](req.MinCapacity); err != nil {
return errors.Wrap(err, "invalid minCapacity")
}
}
// Validate access modes
validAccessModes := map[string]bool{
"ReadWriteOnce": true,
"ReadOnlyMany": true,
"ReadWriteMany": true,
}
for _, mode := range req.AccessModes {
if !validAccessModes[mode] {
return errors.Errorf("invalid access mode: %s", mode)
}
}
return nil
}
func (g *AnalyzerGenerator) validateNetworkRequirements(req *analyzer.NetworkRequirements) error {
for _, port := range req.Ports {
if port.Port <= 0 || port.Port > 65535 {
return errors.Errorf("invalid port number: %d", port.Port)
}
validProtocols := map[string]bool{
"TCP": true,
"UDP": true,
}
if port.Protocol != "" && !validProtocols[strings.ToUpper(port.Protocol)] {
return errors.Errorf("invalid protocol: %s", port.Protocol)
}
}
return nil
}
func (g *AnalyzerGenerator) validateCustomRequirement(req *analyzer.CustomRequirement) error {
if req.Name == "" {
return errors.New("custom requirement name cannot be empty")
}
if req.Type == "" {
return errors.New("custom requirement type cannot be empty")
}
// Check if we have a specific validator for this type
if validator, exists := g.validators[req.Type]; exists {
return validator(req)
}
// Check if we have a template with validator for this type
if template, exists := g.templates[req.Type]; exists && template.Validator != nil {
return template.Validator(req)
}
return nil
}