debian-forge-composer/internal/container/bootc_generator.go
robojerk 4eeaa43c39
Some checks failed
Tests / 🛃 Unit tests (push) Failing after 13s
Tests / 🗄 DB tests (push) Failing after 19s
Tests / 🐍 Lint python scripts (push) Failing after 1s
Tests / ⌨ Golang Lint (push) Failing after 1s
Tests / 📦 Packit config lint (push) Failing after 1s
Tests / 🔍 Check source preparation (push) Failing after 1s
Tests / 🔍 Check for valid snapshot urls (push) Failing after 1s
Tests / 🔍 Check for missing or unused runner repos (push) Failing after 1s
Tests / 🐚 Shellcheck (push) Failing after 1s
Tests / 📦 RPMlint (push) Failing after 1s
Tests / Gitlab CI trigger helper (push) Failing after 1s
Tests / 🎀 kube-linter (push) Failing after 1s
Tests / 🧹 cloud-cleaner-is-enabled (push) Successful in 3s
Tests / 🔍 Check spec file osbuild/images dependencies (push) Failing after 1s
did stuff
2025-08-26 10:34:42 -07:00

573 lines
16 KiB
Go

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"`
}