package services import ( "crypto/rand" "encoding/hex" "encoding/json" "fmt" "net/http" "time" "github.com/google/uuid" ) // AgentTemplate defines a template for different agent types type AgentTemplate struct { Name string `json:"name"` Description string `json:"description"` BaseConfig map[string]interface{} `json:"base_config"` Secrets []string `json:"required_secrets"` Validation ValidationRules `json:"validation"` } // ValidationRules defines validation rules for configuration type ValidationRules struct { RequiredFields []string `json:"required_fields"` AllowedValues map[string][]string `json:"allowed_values"` Patterns map[string]string `json:"patterns"` Constraints map[string]interface{} `json:"constraints"` } // PublicKeyResponse represents the server's public key response type PublicKeyResponse struct { PublicKey string `json:"public_key"` Fingerprint string `json:"fingerprint"` Algorithm string `json:"algorithm"` KeySize int `json:"key_size"` } // ConfigBuilder handles dynamic agent configuration generation type ConfigBuilder struct { serverURL string templates map[string]AgentTemplate httpClient *http.Client publicKeyCache map[string]string } // NewConfigBuilder creates a new configuration builder func NewConfigBuilder(serverURL string) *ConfigBuilder { return &ConfigBuilder{ serverURL: serverURL, templates: getAgentTemplates(), httpClient: &http.Client{ Timeout: 30 * time.Second, }, publicKeyCache: make(map[string]string), } } // AgentSetupRequest represents a request to set up a new agent type AgentSetupRequest struct { ServerURL string `json:"server_url" binding:"required"` Environment string `json:"environment" binding:"required"` AgentType string `json:"agent_type" binding:"required,oneof=linux-server windows-workstation docker-host"` Organization string `json:"organization" binding:"required"` CustomSettings map[string]interface{} `json:"custom_settings,omitempty"` DeploymentID string `json:"deployment_id,omitempty"` } // BuildAgentConfig builds a complete agent configuration func (cb *ConfigBuilder) BuildAgentConfig(req AgentSetupRequest) (*AgentConfiguration, error) { // Validate request if err := cb.validateRequest(req); err != nil { return nil, err } // Generate agent ID agentID := uuid.New().String() // Fetch server public key serverPublicKey, err := cb.fetchServerPublicKey(req.ServerURL) if err != nil { return nil, fmt.Errorf("failed to fetch server public key: %w", err) } // Generate registration token registrationToken, err := cb.generateRegistrationToken(agentID) if err != nil { return nil, fmt.Errorf("failed to generate registration token: %w", err) } // Get template template, exists := cb.templates[req.AgentType] if !exists { return nil, fmt.Errorf("unknown agent type: %s", req.AgentType) } // Build base configuration config := cb.buildFromTemplate(template, req.CustomSettings) // Inject deployment-specific values cb.injectDeploymentValues(config, req, agentID, registrationToken, serverPublicKey) // Apply environment-specific defaults cb.applyEnvironmentDefaults(config, req.Environment) // Validate final configuration if err := cb.validateConfiguration(config, template); err != nil { return nil, fmt.Errorf("configuration validation failed: %w", err) } // Separate sensitive and non-sensitive data publicConfig, secrets := cb.separateSecrets(config) // Create Docker secrets if needed var secretsCreated bool var secretsPath string if len(secrets) > 0 { secretsManager := NewSecretsManager() // Generate encryption key if not set if secretsManager.GetEncryptionKey() == "" { key, err := secretsManager.GenerateEncryptionKey() if err != nil { return nil, fmt.Errorf("failed to generate encryption key: %w", err) } secretsManager.SetEncryptionKey(key) } // Create Docker secrets if err := secretsManager.CreateDockerSecrets(secrets); err != nil { return nil, fmt.Errorf("failed to create Docker secrets: %w", err) } secretsCreated = true secretsPath = secretsManager.GetSecretsPath() } // Determine platform from agent type platform := "linux-amd64" // Default if req.AgentType == "windows-workstation" { platform = "windows-amd64" } return &AgentConfiguration{ AgentID: agentID, PublicConfig: publicConfig, Secrets: secrets, Template: req.AgentType, Environment: req.Environment, ServerURL: req.ServerURL, Organization: req.Organization, Platform: platform, ConfigVersion: "5", // Config schema version AgentVersion: "0.1.23.4", // Agent binary version BuildTime: time.Now(), SecretsCreated: secretsCreated, SecretsPath: secretsPath, }, nil } // AgentConfiguration represents a complete agent configuration type AgentConfiguration struct { AgentID string `json:"agent_id"` PublicConfig map[string]interface{} `json:"public_config"` Secrets map[string]string `json:"secrets"` Template string `json:"template"` Environment string `json:"environment"` ServerURL string `json:"server_url"` Organization string `json:"organization"` Platform string `json:"platform"` ConfigVersion string `json:"config_version"` // Config schema version (e.g., "5") AgentVersion string `json:"agent_version"` // Agent binary version (e.g., "0.1.23.5") BuildTime time.Time `json:"build_time"` SecretsCreated bool `json:"secrets_created"` SecretsPath string `json:"secrets_path,omitempty"` } // validateRequest validates the setup request func (cb *ConfigBuilder) validateRequest(req AgentSetupRequest) error { if req.ServerURL == "" { return fmt.Errorf("server_url is required") } if req.Environment == "" { return fmt.Errorf("environment is required") } if req.AgentType == "" { return fmt.Errorf("agent_type is required") } if req.Organization == "" { return fmt.Errorf("organization is required") } // Check if agent type exists if _, exists := cb.templates[req.AgentType]; !exists { return fmt.Errorf("unknown agent type: %s", req.AgentType) } return nil } // fetchServerPublicKey fetches the server's public key with caching func (cb *ConfigBuilder) fetchServerPublicKey(serverURL string) (string, error) { // Check cache first if cached, exists := cb.publicKeyCache[serverURL]; exists { return cached, nil } // Fetch from server resp, err := cb.httpClient.Get(serverURL + "/api/v1/public-key") if err != nil { return "", fmt.Errorf("failed to fetch public key: %w", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return "", fmt.Errorf("server returned status %d", resp.StatusCode) } var keyResp PublicKeyResponse if err := json.NewDecoder(resp.Body).Decode(&keyResp); err != nil { return "", fmt.Errorf("failed to decode public key response: %w", err) } // Cache the key cb.publicKeyCache[serverURL] = keyResp.PublicKey return keyResp.PublicKey, nil } // generateRegistrationToken generates a secure registration token func (cb *ConfigBuilder) generateRegistrationToken(agentID string) (string, error) { bytes := make([]byte, 32) if _, err := rand.Read(bytes); err != nil { return "", err } // Combine agent ID with random bytes for uniqueness data := append([]byte(agentID), bytes...) token := hex.EncodeToString(data) // Ensure token doesn't exceed reasonable length if len(token) > 128 { token = token[:128] } return token, nil } // buildFromTemplate builds configuration from template func (cb *ConfigBuilder) buildFromTemplate(template AgentTemplate, customSettings map[string]interface{}) map[string]interface{} { config := make(map[string]interface{}) // Deep copy base configuration for k, v := range template.BaseConfig { config[k] = cb.deepCopy(v) } // Apply custom settings if customSettings != nil { cb.mergeSettings(config, customSettings) } return config } // injectDeploymentValues injects deployment-specific values into configuration func (cb *ConfigBuilder) injectDeploymentValues(config map[string]interface{}, req AgentSetupRequest, agentID, registrationToken, serverPublicKey string) { config["version"] = "5" // Config schema version (for migration system) config["agent_version"] = "0.1.23.5" // Agent binary version (MUST match the binary being served) config["server_url"] = req.ServerURL config["agent_id"] = agentID config["registration_token"] = registrationToken config["server_public_key"] = serverPublicKey config["organization"] = req.Organization config["environment"] = req.Environment config["agent_type"] = req.AgentType if req.DeploymentID != "" { config["deployment_id"] = req.DeploymentID } } // applyEnvironmentDefaults applies environment-specific configuration defaults func (cb *ConfigBuilder) applyEnvironmentDefaults(config map[string]interface{}, environment string) { environmentDefaults := map[string]interface{}{ "development": map[string]interface{}{ "logging": map[string]interface{}{ "level": "debug", "max_size": 50, "max_backups": 2, "max_age": 7, }, "check_in_interval": 60, // More frequent polling in development }, "staging": map[string]interface{}{ "logging": map[string]interface{}{ "level": "info", "max_size": 100, "max_backups": 3, "max_age": 14, }, "check_in_interval": 180, }, "production": map[string]interface{}{ "logging": map[string]interface{}{ "level": "warn", "max_size": 200, "max_backups": 5, "max_age": 30, }, "check_in_interval": 300, // 5 minutes for production }, "testing": map[string]interface{}{ "logging": map[string]interface{}{ "level": "debug", "max_size": 10, "max_backups": 1, "max_age": 1, }, "check_in_interval": 30, // Very frequent for testing }, } if defaults, exists := environmentDefaults[environment]; exists { if defaultsMap, ok := defaults.(map[string]interface{}); ok { cb.mergeSettings(config, defaultsMap) } } } // validateConfiguration validates the final configuration func (cb *ConfigBuilder) validateConfiguration(config map[string]interface{}, template AgentTemplate) error { // Check required fields for _, field := range template.Validation.RequiredFields { if _, exists := config[field]; !exists { return fmt.Errorf("required field missing: %s", field) } } // Validate allowed values for field, allowedValues := range template.Validation.AllowedValues { if value, exists := config[field]; exists { if strValue, ok := value.(string); ok { if !cb.containsString(allowedValues, strValue) { return fmt.Errorf("invalid value for %s: %s (allowed: %v)", field, strValue, allowedValues) } } } } // Validate constraints for field, constraint := range template.Validation.Constraints { if value, exists := config[field]; exists { if err := cb.validateConstraint(field, value, constraint); err != nil { return err } } } return nil } // separateSecrets separates sensitive data from public configuration func (cb *ConfigBuilder) separateSecrets(config map[string]interface{}) (map[string]interface{}, map[string]string) { publicConfig := make(map[string]interface{}) secrets := make(map[string]string) // Copy all values to public config initially for k, v := range config { publicConfig[k] = cb.deepCopy(v) } // Extract known sensitive fields sensitiveFields := []string{ "registration_token", "server_public_key", } for _, field := range sensitiveFields { if value, exists := publicConfig[field]; exists { if strValue, ok := value.(string); ok { secrets[field] = strValue delete(publicConfig, field) } } } // Extract nested sensitive fields if proxy, exists := publicConfig["proxy"].(map[string]interface{}); exists { if username, exists := proxy["username"].(string); exists && username != "" { secrets["proxy_username"] = username delete(proxy, "username") } if password, exists := proxy["password"].(string); exists && password != "" { secrets["proxy_password"] = password delete(proxy, "password") } } if tls, exists := publicConfig["tls"].(map[string]interface{}); exists { if certFile, exists := tls["cert_file"].(string); exists && certFile != "" { secrets["tls_cert"] = certFile delete(tls, "cert_file") } if keyFile, exists := tls["key_file"].(string); exists && keyFile != "" { secrets["tls_key"] = keyFile delete(tls, "key_file") } if caFile, exists := tls["ca_file"].(string); exists && caFile != "" { secrets["tls_ca"] = caFile delete(tls, "ca_file") } } return publicConfig, secrets } // Helper functions func (cb *ConfigBuilder) deepCopy(value interface{}) interface{} { if m, ok := value.(map[string]interface{}); ok { result := make(map[string]interface{}) for k, v := range m { result[k] = cb.deepCopy(v) } return result } if s, ok := value.([]interface{}); ok { result := make([]interface{}, len(s)) for i, v := range s { result[i] = cb.deepCopy(v) } return result } return value } func (cb *ConfigBuilder) mergeSettings(target map[string]interface{}, source map[string]interface{}) { for key, value := range source { if existing, exists := target[key]; exists { if existingMap, ok := existing.(map[string]interface{}); ok { if sourceMap, ok := value.(map[string]interface{}); ok { cb.mergeSettings(existingMap, sourceMap) continue } } } target[key] = cb.deepCopy(value) } } func (cb *ConfigBuilder) containsString(slice []string, item string) bool { for _, s := range slice { if s == item { return true } } return false } // GetTemplates returns the available agent templates func (cb *ConfigBuilder) GetTemplates() map[string]AgentTemplate { return getAgentTemplates() } // GetTemplate returns a specific agent template func (cb *ConfigBuilder) GetTemplate(agentType string) (AgentTemplate, bool) { template, exists := getAgentTemplates()[agentType] return template, exists } func (cb *ConfigBuilder) validateConstraint(field string, value interface{}, constraint interface{}) error { constraints, ok := constraint.(map[string]interface{}) if !ok { return nil } if numValue, ok := value.(float64); ok { if min, exists := constraints["min"].(float64); exists && numValue < min { return fmt.Errorf("value for %s is below minimum: %f < %f", field, numValue, min) } if max, exists := constraints["max"].(float64); exists && numValue > max { return fmt.Errorf("value for %s is above maximum: %f > %f", field, numValue, max) } } return nil } // getAgentTemplates returns the available agent templates func getAgentTemplates() map[string]AgentTemplate { return map[string]AgentTemplate{ "linux-server": { Name: "Linux Server Agent", Description: "Optimized for Linux server deployments with package management", BaseConfig: map[string]interface{}{ "check_in_interval": 300, "network": map[string]interface{}{ "timeout": 30000000000, "retry_count": 3, "retry_delay": 5000000000, "max_idle_conn": 10, }, "proxy": map[string]interface{}{ "enabled": false, }, "tls": map[string]interface{}{ "insecure_skip_verify": false, }, "logging": map[string]interface{}{ "level": "info", "max_size": 100, "max_backups": 3, "max_age": 28, }, "subsystems": map[string]interface{}{ "apt": map[string]interface{}{ "enabled": true, "timeout": 30000000000, "circuit_breaker": map[string]interface{}{ "enabled": true, "failure_threshold": 3, "failure_window": 600000000000, "open_duration": 1800000000000, "half_open_attempts": 2, }, }, "dnf": map[string]interface{}{ "enabled": true, "timeout": 45000000000, "circuit_breaker": map[string]interface{}{ "enabled": true, "failure_threshold": 3, "failure_window": 600000000000, "open_duration": 1800000000000, "half_open_attempts": 2, }, }, "docker": map[string]interface{}{ "enabled": true, "timeout": 60000000000, "circuit_breaker": map[string]interface{}{ "enabled": true, "failure_threshold": 3, "failure_window": 600000000000, "open_duration": 1800000000000, "half_open_attempts": 2, }, }, "windows": map[string]interface{}{ "enabled": false, }, "winget": map[string]interface{}{ "enabled": false, }, "storage": map[string]interface{}{ "enabled": true, "timeout": 10000000000, "circuit_breaker": map[string]interface{}{ "enabled": true, "failure_threshold": 3, "failure_window": 600000000000, "open_duration": 1800000000000, "half_open_attempts": 2, }, }, }, }, Secrets: []string{"registration_token", "server_public_key"}, Validation: ValidationRules{ RequiredFields: []string{"server_url", "organization"}, AllowedValues: map[string][]string{ "environment": {"development", "staging", "production", "testing"}, }, Patterns: map[string]string{ "server_url": "^https?://.+", }, Constraints: map[string]interface{}{ "check_in_interval": map[string]interface{}{"min": 30, "max": 3600}, }, }, }, "windows-workstation": { Name: "Windows Workstation Agent", Description: "Optimized for Windows workstation deployments", BaseConfig: map[string]interface{}{ "check_in_interval": 300, "network": map[string]interface{}{ "timeout": 30000000000, "retry_count": 3, "retry_delay": 5000000000, "max_idle_conn": 10, }, "proxy": map[string]interface{}{ "enabled": false, }, "tls": map[string]interface{}{ "insecure_skip_verify": false, }, "logging": map[string]interface{}{ "level": "info", "max_size": 100, "max_backups": 3, "max_age": 28, }, "subsystems": map[string]interface{}{ "apt": map[string]interface{}{ "enabled": false, }, "dnf": map[string]interface{}{ "enabled": false, }, "docker": map[string]interface{}{ "enabled": false, }, "windows": map[string]interface{}{ "enabled": true, "timeout": 600000000000, "circuit_breaker": map[string]interface{}{ "enabled": true, "failure_threshold": 2, "failure_window": 900000000000, "open_duration": 3600000000000, "half_open_attempts": 3, }, }, "winget": map[string]interface{}{ "enabled": true, "timeout": 120000000000, "circuit_breaker": map[string]interface{}{ "enabled": true, "failure_threshold": 3, "failure_window": 600000000000, "open_duration": 1800000000000, "half_open_attempts": 2, }, }, "storage": map[string]interface{}{ "enabled": false, }, }, }, Secrets: []string{"registration_token", "server_public_key"}, Validation: ValidationRules{ RequiredFields: []string{"server_url", "organization"}, AllowedValues: map[string][]string{ "environment": {"development", "staging", "production", "testing"}, }, Patterns: map[string]string{ "server_url": "^https?://.+", }, Constraints: map[string]interface{}{ "check_in_interval": map[string]interface{}{"min": 30, "max": 3600}, }, }, }, "docker-host": { Name: "Docker Host Agent", Description: "Optimized for Docker host deployments", BaseConfig: map[string]interface{}{ "check_in_interval": 300, "network": map[string]interface{}{ "timeout": 30000000000, "retry_count": 3, "retry_delay": 5000000000, "max_idle_conn": 10, }, "proxy": map[string]interface{}{ "enabled": false, }, "tls": map[string]interface{}{ "insecure_skip_verify": false, }, "logging": map[string]interface{}{ "level": "info", "max_size": 100, "max_backups": 3, "max_age": 28, }, "subsystems": map[string]interface{}{ "apt": map[string]interface{}{ "enabled": false, }, "dnf": map[string]interface{}{ "enabled": false, }, "docker": map[string]interface{}{ "enabled": true, "timeout": 60000000000, "circuit_breaker": map[string]interface{}{ "enabled": true, "failure_threshold": 3, "failure_window": 600000000000, "open_duration": 1800000000000, "half_open_attempts": 2, }, }, "windows": map[string]interface{}{ "enabled": false, }, "winget": map[string]interface{}{ "enabled": false, }, "storage": map[string]interface{}{ "enabled": false, }, }, }, Secrets: []string{"registration_token", "server_public_key"}, Validation: ValidationRules{ RequiredFields: []string{"server_url", "organization"}, AllowedValues: map[string][]string{ "environment": {"development", "staging", "production", "testing"}, }, Patterns: map[string]string{ "server_url": "^https?://.+", }, Constraints: map[string]interface{}{ "check_in_interval": map[string]interface{}{"min": 30, "max": 3600}, }, }, }, } }