package services import ( "encoding/json" "fmt" "os" "path/filepath" "strings" "text/template" "time" ) // AgentBuilder handles generating embedded agent configurations // Deprecated: Configuration logic should use services.ConfigService type AgentBuilder struct { buildContext string } // NewAgentBuilder creates a new agent builder func NewAgentBuilder() *AgentBuilder { return &AgentBuilder{} } // BuildAgentWithConfig generates agent configuration and prepares signed binary // Deprecated: Delegate config generation to services.ConfigService func (ab *AgentBuilder) BuildAgentWithConfig(config *AgentConfiguration) (*BuildResult, error) { // Create temporary build directory buildDir, err := os.MkdirTemp("", "agent-build-") if err != nil { return nil, fmt.Errorf("failed to create build directory: %w", err) } // Generate config.json (not embedded in binary) configJSONPath := filepath.Join(buildDir, "config.json") configJSON, err := ab.generateConfigJSON(config) if err != nil { os.RemoveAll(buildDir) return nil, fmt.Errorf("failed to generate config JSON: %w", err) } // Write config.json to file if err := os.WriteFile(configJSONPath, []byte(configJSON), 0600); err != nil { os.RemoveAll(buildDir) return nil, fmt.Errorf("failed to write config file: %w", err) } // Note: Binary is pre-built and stored in /app/binaries/{platform}/ // We don't build or modify the binary here - it's generic for all agents // The signing happens at the platform level, not per-agent return &BuildResult{ BuildDir: buildDir, AgentID: config.AgentID, ConfigFile: configJSONPath, ConfigJSON: configJSON, Platform: config.Platform, BuildTime: time.Now(), }, nil } // generateConfigJSON converts configuration to JSON format func (ab *AgentBuilder) generateConfigJSON(config *AgentConfiguration) (string, error) { // Create complete configuration completeConfig := make(map[string]interface{}) // Copy public configuration for k, v := range config.PublicConfig { completeConfig[k] = v } // Add secrets (they will be protected by file permissions at runtime) for k, v := range config.Secrets { completeConfig[k] = v } // CRITICAL: Add both version fields explicitly // These MUST be present or middleware will block the agent completeConfig["version"] = config.ConfigVersion // Config schema version (e.g., "5") completeConfig["agent_version"] = config.AgentVersion // Agent binary version (e.g., "0.1.23.5") // Add agent metadata completeConfig["agent_id"] = config.AgentID completeConfig["server_url"] = config.ServerURL completeConfig["organization"] = config.Organization completeConfig["environment"] = config.Environment completeConfig["template"] = config.Template completeConfig["build_time"] = config.BuildTime.Format(time.RFC3339) // Convert to JSON jsonBytes, err := json.MarshalIndent(completeConfig, "", " ") if err != nil { return "", fmt.Errorf("failed to marshal config to JSON: %w", err) } return string(jsonBytes), nil } // BuildResult contains the results of the build process type BuildResult struct { BuildDir string `json:"build_dir"` AgentID string `json:"agent_id"` ConfigFile string `json:"config_file"` ConfigJSON string `json:"config_json"` Platform string `json:"platform"` BuildTime time.Time `json:"build_time"` } // generateEmbeddedConfig generates the embedded configuration Go file func (ab *AgentBuilder) generateEmbeddedConfig(filename string, config *AgentConfiguration) error { // Create directory structure if err := os.MkdirAll(filepath.Dir(filename), 0755); err != nil { return err } // Convert configuration to JSON for embedding configJSON, err := ab.configToJSON(config) if err != nil { return err } // Generate Go source file with embedded configuration tmpl := `// Code generated by dynamic build system. DO NOT EDIT. package embedded import ( "encoding/json" "time" ) // EmbeddedAgentConfiguration contains the pre-built agent configuration var EmbeddedAgentConfiguration = []byte(` + "`" + `{{.ConfigJSON}}` + "`" + `) // EmbeddedAgentID contains the agent ID var EmbeddedAgentID = "{{.AgentID}}" // EmbeddedServerURL contains the server URL var EmbeddedServerURL = "{{.ServerURL}}" // EmbeddedOrganization contains the organization var EmbeddedOrganization = "{{.Organization}}" // EmbeddedEnvironment contains the environment var EmbeddedEnvironment = "{{.Environment}}" // EmbeddedTemplate contains the template type var EmbeddedTemplate = "{{.Template}}" // EmbeddedBuildTime contains the build time var EmbeddedBuildTime, _ = time.Parse(time.RFC3339, "{{.BuildTime}}") // GetEmbeddedConfig returns the embedded configuration as a map func GetEmbeddedConfig() (map[string]interface{}, error) { var config map[string]interface{} err := json.Unmarshal(EmbeddedAgentConfiguration, &config) return config, err } // SecretsMapping maps configuration fields to Docker secrets var SecretsMapping = map[string]string{ {{range $key, $value := .Secrets}}"{{$key}}": "{{$value}}", {{end}} } ` // Execute template t, err := template.New("embedded").Parse(tmpl) if err != nil { return fmt.Errorf("failed to parse template: %w", err) } file, err := os.Create(filename) if err != nil { return fmt.Errorf("failed to create file: %w", err) } defer file.Close() data := struct { ConfigJSON string AgentID string ServerURL string Organization string Environment string Template string BuildTime string Secrets map[string]string }{ ConfigJSON: configJSON, AgentID: config.AgentID, ServerURL: config.ServerURL, Organization: config.Organization, Environment: config.Environment, Template: config.Template, BuildTime: config.BuildTime.Format(time.RFC3339), Secrets: config.Secrets, } if err := t.Execute(file, data); err != nil { return fmt.Errorf("failed to execute template: %w", err) } return nil } // generateDockerCompose generates a docker-compose.yml file func (ab *AgentBuilder) generateDockerCompose(filename string, config *AgentConfiguration) error { tmpl := `# Generated dynamically based on configuration version: '3.8' services: redflag-agent: image: {{.ImageTag}} container_name: redflag-agent-{{.AgentID}} restart: unless-stopped secrets: {{range $key := .SecretsKeys}}- {{$key}} {{end}} volumes: - /var/lib/redflag:/var/lib/redflag - /var/run/docker.sock:/var/run/docker.sock environment: - REDFLAG_AGENT_ID={{.AgentID}} - REDFLAG_ENVIRONMENT={{.Environment}} - REDFLAG_SERVER_URL={{.ServerURL}} - REDFLAG_ORGANIZATION={{.Organization}} networks: - redflag logging: driver: "json-file" options: max-size: "10m" max-file: "3" secrets: {{range $key, $value := .Secrets}}{{$key}}: external: true {{end}} networks: redflag: external: true ` t, err := template.New("compose").Parse(tmpl) if err != nil { return err } file, err := os.Create(filename) if err != nil { return err } defer file.Close() // Extract secret keys for template secretsKeys := make([]string, 0, len(config.Secrets)) for key := range config.Secrets { secretsKeys = append(secretsKeys, key) } data := struct { ImageTag string AgentID string Environment string ServerURL string Organization string Secrets map[string]string SecretsKeys []string }{ ImageTag: fmt.Sprintf("redflag-agent:%s", config.AgentID[:8]), AgentID: config.AgentID, Environment: config.Environment, ServerURL: config.ServerURL, Organization: config.Organization, Secrets: config.Secrets, SecretsKeys: secretsKeys, } return t.Execute(file, data) } // generateDockerfile generates a Dockerfile for building the agent func (ab *AgentBuilder) generateDockerfile(filename string, config *AgentConfiguration) error { tmpl := `# Dockerfile for RedFlag Agent with embedded configuration FROM golang:1.21-alpine AS builder # Install ca-certificates for SSL/TLS RUN apk add --no-cache ca-certificates git WORKDIR /app # Copy go mod files (these should be in the same directory as the Dockerfile) COPY go.mod go.sum ./ RUN go mod download # Copy source code COPY . . # Copy generated embedded configuration COPY pkg/embedded/config.go ./pkg/embedded/config.go # Build the agent with embedded configuration RUN CGO_ENABLED=0 GOOS=linux go build \ -ldflags="-w -s -X main.version=dynamic-build-{{.AgentID}}" \ -o redflag-agent \ ./cmd/agent # Final stage FROM scratch # Copy ca-certificates for SSL/TLS COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ # Copy the agent binary COPY --from=builder /app/redflag-agent /redflag-agent # Set environment variables (these can be overridden by docker-compose) ENV REDFLAG_AGENT_ID="{{.AgentID}}" ENV REDFLAG_ENVIRONMENT="{{.Environment}}" ENV REDFLAG_SERVER_URL="{{.ServerURL}}" ENV REDFLAG_ORGANIZATION="{{.Organization}}" # Health check HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ CMD ["/redflag-agent", "--health-check"] # Run the agent ENTRYPOINT ["/redflag-agent"] ` t, err := template.New("dockerfile").Parse(tmpl) if err != nil { return err } file, err := os.Create(filename) if err != nil { return err } defer file.Close() data := struct { AgentID string Environment string ServerURL string Organization string }{ AgentID: config.AgentID, Environment: config.Environment, ServerURL: config.ServerURL, Organization: config.Organization, } return t.Execute(file, data) } // configToJSON converts configuration to JSON string func (ab *AgentBuilder) configToJSON(config *AgentConfiguration) (string, error) { // Create complete configuration with embedded values completeConfig := make(map[string]interface{}) // Copy public configuration for k, v := range config.PublicConfig { completeConfig[k] = v } // Add secrets values (they will be overridden by Docker secrets at runtime) for k, v := range config.Secrets { completeConfig[k] = v } // Convert to JSON with proper escaping jsonBytes, err := json.MarshalIndent(completeConfig, "", " ") if err != nil { return "", fmt.Errorf("failed to marshal config to JSON: %w", err) } // Escape backticks for Go string literal jsonStr := string(jsonBytes) jsonStr = strings.ReplaceAll(jsonStr, "`", "` + \"`\" + `") return jsonStr, nil }