mirror of
https://github.com/replicatedhq/troubleshoot.git
synced 2026-04-15 07:16:34 +00:00
* 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>
980 lines
28 KiB
Go
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
|
|
}
|