Files

382 lines
10 KiB
Go

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.6")
// 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
}