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
573 lines
16 KiB
Go
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"`
|
|
}
|