Files
Pulse/internal/models/profile_validation.go
rcourtman 8c7581d32c feat(profiles): add AI-assisted profile suggestions
Add ability for users to describe what kind of agent profile they need
in natural language, and have AI generate a suggestion with name,
description, config values, and rationale.

- Add ProfileSuggestionHandler with schema-aware prompting
- Add SuggestProfileModal component with example prompts
- Update AgentProfilesPanel with suggest button and description field
- Streamline ValidConfigKeys to only agent-supported settings
- Update profile validation tests for simplified schema
2026-01-15 13:24:18 +00:00

318 lines
8.1 KiB
Go

package models
import (
"fmt"
"regexp"
"strings"
"time"
)
// ConfigKeyDefinition defines a valid configuration key with its type and constraints.
type ConfigKeyDefinition struct {
Key string // Config key name
Type ConfigType // Expected value type
Description string // Human-readable description
Default interface{} // Default value (nil if required)
Required bool // Whether the key is required
Min *float64 // Minimum value for numbers
Max *float64 // Maximum value for numbers
Pattern string // Regex pattern for strings
Enum []string // Allowed values for enums
}
// ConfigType represents the type of a configuration value.
type ConfigType string
const (
ConfigTypeString ConfigType = "string"
ConfigTypeBool ConfigType = "bool"
ConfigTypeInt ConfigType = "int"
ConfigTypeFloat ConfigType = "float"
ConfigTypeDuration ConfigType = "duration"
ConfigTypeEnum ConfigType = "enum"
)
// ValidConfigKeys defines agent configuration keys that are actually applied by the agent.
// These match the keys handled in applyRemoteSettings() in cmd/pulse-agent/main.go.
var ValidConfigKeys = []ConfigKeyDefinition{
{
Key: "interval",
Type: ConfigTypeDuration,
Description: "Polling interval for metrics collection",
Default: "30s",
},
{
Key: "enable_host",
Type: ConfigTypeBool,
Description: "Enable host monitoring (metrics + command execution)",
Default: true,
},
{
Key: "enable_docker",
Type: ConfigTypeBool,
Description: "Enable Docker container monitoring",
Default: true,
},
{
Key: "enable_kubernetes",
Type: ConfigTypeBool,
Description: "Enable Kubernetes workload monitoring",
Default: false,
},
{
Key: "enable_proxmox",
Type: ConfigTypeBool,
Description: "Enable Proxmox mode for node registration",
Default: false,
},
{
Key: "proxmox_type",
Type: ConfigTypeEnum,
Description: "Proxmox type override (pve or pbs; auto-detect if unset)",
Default: "auto",
Enum: []string{"pve", "pbs", "auto"},
},
{
Key: "docker_runtime",
Type: ConfigTypeEnum,
Description: "Container runtime preference (auto, docker, podman)",
Default: "auto",
Enum: []string{"auto", "docker", "podman"},
},
{
Key: "disable_auto_update",
Type: ConfigTypeBool,
Description: "Disable automatic agent updates",
Default: false,
},
{
Key: "disable_docker_update_checks",
Type: ConfigTypeBool,
Description: "Disable Docker image update detection",
Default: false,
},
{
Key: "kube_include_all_pods",
Type: ConfigTypeBool,
Description: "Include all non-succeeded pods in Kubernetes reports",
Default: false,
},
{
Key: "kube_include_all_deployments",
Type: ConfigTypeBool,
Description: "Include all deployments in Kubernetes reports",
Default: false,
},
{
Key: "log_level",
Type: ConfigTypeEnum,
Description: "Agent log verbosity level",
Default: "info",
Enum: []string{"debug", "info", "warn", "error"},
},
{
Key: "report_ip",
Type: ConfigTypeString,
Description: "Override the reported IP address for the agent",
Default: "",
},
{
Key: "disable_ceph",
Type: ConfigTypeBool,
Description: "Disable local Ceph status polling",
Default: false,
},
}
// ValidationError represents a validation error for a config key.
type ValidationError struct {
Key string
Message string
}
func (e ValidationError) Error() string {
return fmt.Sprintf("%s: %s", e.Key, e.Message)
}
// ValidationResult holds the result of config validation.
type ValidationResult struct {
Valid bool
Errors []ValidationError
Warnings []ValidationError
}
// ProfileValidator validates agent profile configurations.
type ProfileValidator struct {
keyDefs map[string]ConfigKeyDefinition
}
// NewProfileValidator creates a new profile validator.
func NewProfileValidator() *ProfileValidator {
keyDefs := make(map[string]ConfigKeyDefinition)
for _, def := range ValidConfigKeys {
keyDefs[def.Key] = def
}
return &ProfileValidator{keyDefs: keyDefs}
}
// Validate validates an agent profile configuration.
func (v *ProfileValidator) Validate(config AgentConfigMap) ValidationResult {
result := ValidationResult{Valid: true}
// Check for unknown keys
for key := range config {
if _, ok := v.keyDefs[key]; !ok {
result.Warnings = append(result.Warnings, ValidationError{
Key: key,
Message: "Unknown configuration key (will be ignored by agent)",
})
}
}
// Validate known keys
for key, value := range config {
def, ok := v.keyDefs[key]
if !ok {
continue // Skip unknown keys (already warned)
}
if err := v.validateValue(def, value); err != nil {
result.Valid = false
result.Errors = append(result.Errors, ValidationError{
Key: key,
Message: err.Error(),
})
}
}
// Check for required keys
for _, def := range v.keyDefs {
if def.Required {
if _, ok := config[def.Key]; !ok {
result.Valid = false
result.Errors = append(result.Errors, ValidationError{
Key: def.Key,
Message: "Required configuration key is missing",
})
}
}
}
return result
}
// validateValue validates a single configuration value against its definition.
func (v *ProfileValidator) validateValue(def ConfigKeyDefinition, value interface{}) error {
if value == nil {
if def.Required {
return fmt.Errorf("value cannot be null")
}
return nil
}
switch def.Type {
case ConfigTypeString:
s, ok := value.(string)
if !ok {
return fmt.Errorf("expected string, got %T", value)
}
if def.Pattern != "" {
re, err := regexp.Compile(def.Pattern)
if err != nil {
return fmt.Errorf("invalid pattern in definition: %v", err)
}
if !re.MatchString(s) {
return fmt.Errorf("value does not match pattern %s", def.Pattern)
}
}
case ConfigTypeBool:
if _, ok := value.(bool); !ok {
return fmt.Errorf("expected boolean, got %T", value)
}
case ConfigTypeInt:
var num float64
switch n := value.(type) {
case int:
num = float64(n)
case int64:
num = float64(n)
case float64:
if n != float64(int64(n)) {
return fmt.Errorf("expected integer, got float")
}
num = n
default:
return fmt.Errorf("expected integer, got %T", value)
}
if def.Min != nil && num < *def.Min {
return fmt.Errorf("value %v is below minimum %v", num, *def.Min)
}
if def.Max != nil && num > *def.Max {
return fmt.Errorf("value %v exceeds maximum %v", num, *def.Max)
}
case ConfigTypeFloat:
var num float64
switch n := value.(type) {
case int:
num = float64(n)
case int64:
num = float64(n)
case float64:
num = n
default:
return fmt.Errorf("expected number, got %T", value)
}
if def.Min != nil && num < *def.Min {
return fmt.Errorf("value %v is below minimum %v", num, *def.Min)
}
if def.Max != nil && num > *def.Max {
return fmt.Errorf("value %v exceeds maximum %v", num, *def.Max)
}
case ConfigTypeDuration:
s, ok := value.(string)
if !ok {
return fmt.Errorf("expected duration string, got %T", value)
}
if _, err := time.ParseDuration(s); err != nil {
return fmt.Errorf("invalid duration format: %v", err)
}
case ConfigTypeEnum:
s, ok := value.(string)
if !ok {
return fmt.Errorf("expected string, got %T", value)
}
found := false
for _, allowed := range def.Enum {
if strings.EqualFold(s, allowed) {
found = true
break
}
}
if !found {
return fmt.Errorf("value must be one of: %s", strings.Join(def.Enum, ", "))
}
}
return nil
}
// GetConfigKeyDefinitions returns all valid configuration key definitions.
func GetConfigKeyDefinitions() []ConfigKeyDefinition {
return ValidConfigKeys
}
// GetConfigKeyDefinition returns the definition for a specific key.
func GetConfigKeyDefinition(key string) (ConfigKeyDefinition, bool) {
for _, def := range ValidConfigKeys {
if def.Key == key {
return def, true
}
}
return ConfigKeyDefinition{}, false
}