package container import ( "crypto/rand" "encoding/json" "fmt" "os" "os/exec" "path/filepath" "strings" "time" "github.com/sirupsen/logrus" ) type BootcGenerator struct { logger *logrus.Logger config *BootcConfig registry *ContainerRegistry signer *ContainerSigner validator *ContainerValidator } type BootcConfig struct { BaseImage string `json:"base_image"` OutputDir string `json:"output_dir"` RegistryURL string `json:"registry_url"` SigningKey string `json:"signing_key"` Compression string `json:"compression"` Metadata map[string]string `json:"metadata"` } type ContainerRegistry struct { URL string `json:"url"` Username string `json:"username"` Password string `json:"password"` Insecure bool `json:"insecure"` Headers map[string]string `json:"headers"` } type ContainerSigner struct { PrivateKey string `json:"private_key"` PublicKey string `json:"public_key"` Algorithm string `json:"algorithm"` } type ContainerValidator struct { SecurityScan bool `json:"security_scan"` PolicyCheck bool `json:"policy_check"` VulnerabilityScan bool `json:"vulnerability_scan"` } type BootcContainer struct { ID string `json:"id"` Name string `json:"name"` Tag string `json:"tag"` Digest string `json:"digest"` Size int64 `json:"size"` Architecture string `json:"architecture"` OS string `json:"os"` Variant string `json:"variant"` Layers []ContainerLayer `json:"layers"` Metadata map[string]interface{} `json:"metadata"` CreatedAt time.Time `json:"created_at"` Signed bool `json:"signed"` Signature string `json:"signature,omitempty"` } type ContainerLayer struct { Digest string `json:"digest"` Size int64 `json:"size"` MediaType string `json:"media_type"` Created time.Time `json:"created"` } type ContainerVariant struct { Name string `json:"name"` Description string `json:"description"` Packages []string `json:"packages"` Services []string `json:"services"` Config map[string]interface{} `json:"config"` } func NewBootcGenerator(config *BootcConfig, logger *logrus.Logger) *BootcGenerator { generator := &BootcGenerator{ logger: logger, config: config, registry: NewContainerRegistry(), signer: NewContainerSigner(), validator: NewContainerValidator(), } return generator } func NewContainerRegistry() *ContainerRegistry { return &ContainerRegistry{ URL: "localhost:5000", Insecure: true, Headers: make(map[string]string), } } func NewContainerSigner() *ContainerSigner { return &ContainerSigner{ Algorithm: "sha256", } } func NewContainerValidator() *ContainerValidator { return &ContainerValidator{ SecurityScan: true, PolicyCheck: true, VulnerabilityScan: true, } } func (bg *BootcGenerator) GenerateContainer(blueprint *Blueprint, variant string) (*BootcContainer, error) { bg.logger.Infof("Generating bootc container for blueprint: %s, variant: %s", blueprint.Name, variant) // Create container structure container := &BootcContainer{ ID: generateContainerID(), Name: fmt.Sprintf("%s-%s", blueprint.Name, variant), Tag: "latest", Architecture: blueprint.Architecture, OS: "linux", Variant: variant, Layers: []ContainerLayer{}, Metadata: make(map[string]interface{}), CreatedAt: time.Now(), } // Generate container layers if err := bg.generateLayers(container, blueprint, variant); err != nil { return nil, fmt.Errorf("failed to generate layers: %w", err) } // Build container image if err := bg.buildContainer(container); err != nil { return nil, fmt.Errorf("failed to build container: %w", err) } // Sign container if configured if bg.config.SigningKey != "" { if err := bg.signContainer(container); err != nil { bg.logger.Warnf("Failed to sign container: %v", err) } else { container.Signed = true } } // Validate container if err := bg.validateContainer(container); err != nil { bg.logger.Warnf("Container validation failed: %v", err) } // Push to registry if configured if bg.config.RegistryURL != "" { if err := bg.pushToRegistry(container); err != nil { bg.logger.Warnf("Failed to push to registry: %v", err) } } bg.logger.Infof("Successfully generated container: %s", container.ID) return container, nil } func (bg *BootcGenerator) generateLayers(container *BootcContainer, blueprint *Blueprint, variant string) error { // Create base layer baseLayer := ContainerLayer{ Digest: generateDigest(), Size: 0, MediaType: "application/vnd.oci.image.layer.v1.tar+gzip", Created: time.Now(), } container.Layers = append(container.Layers, baseLayer) // Create package layer packageLayer := ContainerLayer{ Digest: generateDigest(), Size: 0, MediaType: "application/vnd.oci.image.layer.v1.tar+gzip", Created: time.Now(), } container.Layers = append(container.Layers, packageLayer) // Create configuration layer configLayer := ContainerLayer{ Digest: generateDigest(), Size: 0, MediaType: "application/vnd.oci.image.layer.v1.tar+gzip", Created: time.Now(), } container.Layers = append(container.Layers, configLayer) // Set metadata container.Metadata["blueprint"] = blueprint.Name container.Metadata["variant"] = variant container.Metadata["packages"] = blueprint.Packages.Include container.Metadata["users"] = blueprint.Users container.Metadata["customizations"] = blueprint.Customizations return nil } func (bg *BootcGenerator) buildContainer(container *BootcContainer) error { // Create output directory outputDir := filepath.Join(bg.config.OutputDir, container.Name) if err := os.MkdirAll(outputDir, 0755); err != nil { return fmt.Errorf("failed to create output directory: %w", err) } // Generate Dockerfile dockerfilePath := filepath.Join(outputDir, "Dockerfile") if err := bg.generateDockerfile(dockerfilePath, container); err != nil { return fmt.Errorf("failed to generate Dockerfile: %w", err) } // Build container image if err := bg.dockerBuild(outputDir, container); err != nil { return fmt.Errorf("failed to build container: %w", err) } // Calculate final size container.Size = bg.calculateContainerSize(container) return nil } func (bg *BootcGenerator) generateDockerfile(path string, container *BootcContainer) error { content := fmt.Sprintf(`# Debian Bootc Container: %s FROM debian:bookworm-slim # Set metadata LABEL org.opencontainers.image.title="%s" LABEL org.opencontainers.image.description="Debian atomic container for %s" LABEL org.opencontainers.image.vendor="Debian Forge" LABEL org.opencontainers.image.version="1.0.0" # Install packages RUN apt-get update && apt-get install -y \\ %s \\ && rm -rf /var/lib/apt/lists/* # Create users %s # Set customizations %s # Set working directory WORKDIR /root # Default command CMD ["/bin/bash"] `, container.Name, container.Name, container.Variant, strings.Join(container.Metadata["packages"].([]string), " \\\n "), bg.generateUserCommands(container), bg.generateCustomizationCommands(container)) return os.WriteFile(path, []byte(content), 0644) } func (bg *BootcGenerator) generateUserCommands(container *BootcContainer) string { var commands []string if users, ok := container.Metadata["users"].([]BlueprintUser); ok { for _, user := range users { commands = append(commands, fmt.Sprintf("RUN useradd -m -s %s %s", user.Shell, user.Name)) } } return strings.Join(commands, "\n") } func (bg *BootcGenerator) generateCustomizationCommands(container *BootcContainer) string { var commands []string if customizations, ok := container.Metadata["customizations"].(BlueprintCustomizations); ok { if customizations.Hostname != "" { commands = append(commands, fmt.Sprintf("RUN echo '%s' > /etc/hostname", customizations.Hostname)) } if customizations.Timezone != "" { commands = append(commands, fmt.Sprintf("RUN ln -sf /usr/share/zoneinfo/%s /etc/localtime", customizations.Timezone)) } } return strings.Join(commands, "\n") } func (bg *BootcGenerator) dockerBuild(context string, container *BootcContainer) error { // Build command cmd := exec.Command("docker", "build", "-t", container.Name+":"+container.Tag, context) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr if err := cmd.Run(); err != nil { return fmt.Errorf("docker build failed: %w", err) } // Get image digest digestCmd := exec.Command("docker", "images", "--digests", "--format", "{{.Digest}}", container.Name+":"+container.Tag) digestOutput, err := digestCmd.Output() if err != nil { bg.logger.Warnf("Failed to get image digest: %v", err) } else { container.Digest = strings.TrimSpace(string(digestOutput)) } return nil } func (bg *BootcGenerator) calculateContainerSize(container *BootcContainer) int64 { // Get image size from Docker cmd := exec.Command("docker", "images", "--format", "{{.Size}}", container.Name+":"+container.Tag) output, err := cmd.Output() if err != nil { bg.logger.Warnf("Failed to get image size: %v", err) return 0 } // Parse size (e.g., "123.4MB" -> 123400000) sizeStr := strings.TrimSpace(string(output)) // Simple size parsing - in production, use proper size parsing library return 0 // Placeholder } func (bg *BootcGenerator) signContainer(container *BootcContainer) error { if bg.signer.PrivateKey == "" { return fmt.Errorf("no signing key configured") } // Use cosign to sign the container cmd := exec.Command("cosign", "sign", "--key", bg.signer.PrivateKey, container.Name+":"+container.Tag) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr if err := cmd.Run(); err != nil { return fmt.Errorf("cosign signing failed: %w", err) } // Get signature signatureCmd := exec.Command("cosign", "verify", "--key", bg.signer.PublicKey, container.Name+":"+container.Tag) signatureOutput, err := signatureCmd.Output() if err != nil { return fmt.Errorf("failed to verify signature: %w", err) } container.Signature = strings.TrimSpace(string(signatureOutput)) return nil } func (bg *BootcGenerator) validateContainer(container *BootcContainer) error { if !bg.validator.SecurityScan { return nil } // Run security scan if err := bg.runSecurityScan(container); err != nil { return fmt.Errorf("security scan failed: %w", err) } // Run policy check if bg.validator.PolicyCheck { if err := bg.runPolicyCheck(container); err != nil { return fmt.Errorf("policy check failed: %w", err) } } // Run vulnerability scan if bg.validator.VulnerabilityScan { if err := bg.runVulnerabilityScan(container); err != nil { return fmt.Errorf("vulnerability scan failed: %w", err) } } return nil } func (bg *BootcGenerator) runSecurityScan(container *BootcContainer) error { // Use Trivy for security scanning cmd := exec.Command("trivy", "image", "--format", "json", container.Name+":"+container.Tag) output, err := cmd.Output() if err != nil { return fmt.Errorf("trivy scan failed: %w", err) } // Parse scan results var scanResults map[string]interface{} if err := json.Unmarshal(output, &scanResults); err != nil { return fmt.Errorf("failed to parse scan results: %w", err) } // Check for high/critical vulnerabilities if vulnerabilities, ok := scanResults["Vulnerabilities"].([]interface{}); ok { for _, vuln := range vulnerabilities { if vulnMap, ok := vuln.(map[string]interface{}); ok { if severity, ok := vulnMap["Severity"].(string); ok { if severity == "HIGH" || severity == "CRITICAL" { bg.logger.Warnf("High/Critical vulnerability found: %v", vulnMap) } } } } } return nil } func (bg *BootcGenerator) runPolicyCheck(container *BootcContainer) error { // Use Open Policy Agent for policy checking // This is a placeholder - implement actual policy checking bg.logger.Info("Running policy check on container") return nil } func (bg *BootcGenerator) runVulnerabilityScan(container *BootcContainer) error { // Use Grype for vulnerability scanning cmd := exec.Command("grype", "--output", "json", container.Name+":"+container.Tag) output, err := cmd.Output() if err != nil { return fmt.Errorf("grype scan failed: %w", err) } // Parse vulnerability results var vulnResults map[string]interface{} if err := json.Unmarshal(output, &vulnResults); err != nil { return fmt.Errorf("failed to parse vulnerability results: %w", err) } // Process vulnerabilities bg.logger.Infof("Vulnerability scan completed for container: %s", container.Name) return nil } func (bg *BootcGenerator) pushToRegistry(container *BootcContainer) error { // Tag for registry registryTag := fmt.Sprintf("%s/%s:%s", bg.config.RegistryURL, container.Name, container.Tag) // Tag image tagCmd := exec.Command("docker", "tag", container.Name+":"+container.Tag, registryTag) if err := tagCmd.Run(); err != nil { return fmt.Errorf("failed to tag image: %w", err) } // Push to registry pushCmd := exec.Command("docker", "push", registryTag) pushCmd.Stdout = os.Stdout pushCmd.Stderr = os.Stderr if err := pushCmd.Run(); err != nil { return fmt.Errorf("failed to push image: %w", err) } bg.logger.Infof("Successfully pushed container to registry: %s", registryTag) return nil } func (bg *BootcGenerator) ListContainerVariants() []ContainerVariant { return []ContainerVariant{ { Name: "minimal", Description: "Minimal Debian system without desktop environment", Packages: []string{"task-minimal", "systemd", "systemd-sysv"}, Services: []string{"systemd-sysctl", "systemd-random-seed"}, Config: map[string]interface{}{ "hostname": "debian-minimal", "timezone": "UTC", }, }, { Name: "desktop", Description: "Debian with desktop environment", Packages: []string{"task-gnome-desktop", "gnome-core", "systemd"}, Services: []string{"gdm", "systemd-sysctl"}, Config: map[string]interface{}{ "hostname": "debian-desktop", "timezone": "UTC", "desktop": "gnome", }, }, { Name: "server", Description: "Server-optimized Debian system", Packages: []string{"task-server", "systemd", "openssh-server"}, Services: []string{"ssh", "systemd-sysctl"}, Config: map[string]interface{}{ "hostname": "debian-server", "timezone": "UTC", "ssh": true, }, }, { Name: "development", Description: "Development environment Debian system", Packages: []string{"build-essential", "git", "python3", "nodejs"}, Services: []string{"systemd-sysctl"}, Config: map[string]interface{}{ "hostname": "debian-dev", "timezone": "UTC", "dev_tools": true, }, }, } } func (bg *BootcGenerator) GetContainerInfo(containerID string) (*BootcContainer, error) { // Get container information from Docker cmd := exec.Command("docker", "inspect", containerID) output, err := cmd.Output() if err != nil { return nil, fmt.Errorf("failed to inspect container: %w", err) } // Parse inspect output var inspectResults []map[string]interface{} if err := json.Unmarshal(output, &inspectResults); err != nil { return nil, fmt.Errorf("failed to parse inspect results: %w", err) } if len(inspectResults) == 0 { return nil, fmt.Errorf("container not found") } // Convert to our container structure container := &BootcContainer{ ID: containerID, Name: inspectResults[0]["Name"].(string), CreatedAt: time.Now(), // Parse from inspect results Metadata: make(map[string]interface{}), } return container, nil } // Helper functions func generateContainerID() string { bytes := make([]byte, 16) rand.Read(bytes) return fmt.Sprintf("%x", bytes) } func generateDigest() string { bytes := make([]byte, 32) rand.Read(bytes) return fmt.Sprintf("sha256:%x", bytes) } // Blueprint types (imported from blueprintapi) type Blueprint struct { Name string `json:"name"` Packages BlueprintPackages `json:"packages"` Users []BlueprintUser `json:"users"` Customizations BlueprintCustomizations `json:"customizations"` } type BlueprintPackages struct { Include []string `json:"include"` } type BlueprintUser struct { Name string `json:"name"` Shell string `json:"shell"` } type BlueprintCustomizations struct { Hostname string `json:"hostname"` Timezone string `json:"timezone"` }