// Container image builder for Ubuntu and Chainguard-based images with extensible tools package main import ( "context" "fmt" "dagger/containers/internal/dagger" "gopkg.in/yaml.v3" ) type Containers struct{} // Configuration structures type ToolsConfig struct { Core CoreTools `yaml:"core"` Tools map[string]ToolDef `yaml:"tools"` } type CoreTools struct { Git string `yaml:"git"` Jq string `yaml:"jq"` Yq string `yaml:"yq"` Node20 string `yaml:"node20"` Node22 string `yaml:"node22"` Node24 string `yaml:"node24"` } type ToolDef struct { Version string `yaml:"version"` Description string `yaml:"description"` } // Load tools configuration from YAML func (m *Containers) loadConfig(ctx context.Context, configFile *dagger.File) (*ToolsConfig, error) { content, err := configFile.Contents(ctx) if err != nil { return nil, err } var config ToolsConfig if err := yaml.Unmarshal([]byte(content), &config); err != nil { return nil, err } return &config, nil } // Build Ubuntu base image with core tools func (m *Containers) buildUbuntuBase(ctx context.Context, config *ToolsConfig, platform dagger.Platform) *dagger.Container { ctr := dag.Container(dagger.ContainerOpts{Platform: platform}).From("ubuntu:24.04") // Determine architecture for downloads arch := "amd64" if platform == "linux/arm64" { arch = "arm64" } // Update and install base dependencies ctr = ctr.WithExec([]string{"apt-get", "update"}). WithExec([]string{"apt-get", "install", "-y", "ca-certificates", "curl", "wget", "gnupg", "lsb-release", }) // Install Git ctr = ctr.WithExec([]string{"apt-get", "install", "-y", "git"}) // Install jq ctr = ctr.WithExec([]string{"sh", "-c", fmt.Sprintf("curl -sL https://github.com/jqlang/jq/releases/download/jq-%s/jq-linux-%s -o /usr/local/bin/jq && chmod +x /usr/local/bin/jq", config.Core.Jq, arch)}) // Install yq ctr = ctr.WithExec([]string{"sh", "-c", fmt.Sprintf("curl -sL https://github.com/mikefarah/yq/releases/download/v%s/yq_linux_%s -o /usr/local/bin/yq && chmod +x /usr/local/bin/yq", config.Core.Yq, arch)}) // Install Node.js versions using nvm approach ctr = ctr.WithExec([]string{"sh", "-c", "curl -fsSL https://deb.nodesource.com/setup_20.x | bash -"}). WithExec([]string{"apt-get", "install", "-y", "nodejs"}) // Clean up ctr = ctr.WithExec([]string{"apt-get", "clean"}). WithExec([]string{"rm", "-rf", "/var/lib/apt/lists/*"}) return ctr } // Build Chainguard base image with core tools func (m *Containers) buildChainguardBase(ctx context.Context, config *ToolsConfig, platform dagger.Platform) *dagger.Container { // Using Chainguard's Wolfi-based images which are minimal ctr := dag.Container(dagger.ContainerOpts{Platform: platform}).From("cgr.dev/chainguard/wolfi-base:latest") // Determine architecture for downloads arch := "amd64" if platform == "linux/arm64" { arch = "arm64" } // Install core tools using apk (Wolfi package manager) ctr = ctr.WithExec([]string{"apk", "update"}). WithExec([]string{"apk", "add", "git", "curl", "wget", "bash", "ca-certificates"}) // Create /usr/local/bin if it doesn't exist ctr = ctr.WithExec([]string{"mkdir", "-p", "/usr/local/bin"}) // Install jq ctr = ctr.WithExec([]string{"sh", "-c", fmt.Sprintf("wget -q https://github.com/jqlang/jq/releases/download/jq-%s/jq-linux-%s -O /usr/local/bin/jq && chmod +x /usr/local/bin/jq", config.Core.Jq, arch)}) // Install yq ctr = ctr.WithExec([]string{"sh", "-c", fmt.Sprintf("wget -q https://github.com/mikefarah/yq/releases/download/v%s/yq_linux_%s -O /usr/local/bin/yq && chmod +x /usr/local/bin/yq", config.Core.Yq, arch)}) // Install Node.js ctr = ctr.WithExec([]string{"apk", "add", "nodejs", "npm"}) return ctr } // Build Ubuntu image with specific tool func (m *Containers) buildUbuntuTool(ctx context.Context, base *dagger.Container, toolName string, toolDef ToolDef, platform dagger.Platform) *dagger.Container { ctr := base // Determine architecture for downloads arch := "amd64" goArch := "x86_64" if platform == "linux/arm64" { arch = "arm64" goArch = "aarch64" } switch toolName { case "kustomize": ctr = ctr.WithExec([]string{"sh", "-c", fmt.Sprintf("curl -sL https://github.com/kubernetes-sigs/kustomize/releases/download/kustomize%%2Fv%s/kustomize_v%s_linux_%s.tar.gz | tar xz -C /usr/local/bin", toolDef.Version, toolDef.Version, arch)}) case "dagger": ctr = ctr.WithExec([]string{"sh", "-c", fmt.Sprintf("curl -sL https://github.com/dagger/dagger/releases/download/v%s/dagger_v%s_linux_%s.tar.gz | tar xz -C /usr/local/bin", toolDef.Version, toolDef.Version, arch)}) case "esphome": ctr = ctr.WithExec([]string{"apt-get", "update"}). WithExec([]string{"apt-get", "install", "-y", "python3", "python3-pip", "python3-venv"}). WithExec([]string{"pip3", "install", "--break-system-packages", fmt.Sprintf("esphome==%s", toolDef.Version)}). WithExec([]string{"apt-get", "clean"}) case "zola": ctr = ctr.WithExec([]string{"sh", "-c", fmt.Sprintf("curl -sL https://github.com/getzola/zola/releases/download/v%s/zola-v%s-%s-unknown-linux-gnu.tar.gz | tar xz -C /usr/local/bin", toolDef.Version, toolDef.Version, goArch)}) case "yamllint": ctr = ctr.WithExec([]string{"apt-get", "update"}). WithExec([]string{"apt-get", "install", "-y", "python3", "python3-pip"}). WithExec([]string{"pip3", "install", "--break-system-packages", fmt.Sprintf("yamllint==%s", toolDef.Version)}). WithExec([]string{"apt-get", "clean"}) } return ctr } // Build Chainguard image with specific tool func (m *Containers) buildChainguardTool(ctx context.Context, base *dagger.Container, toolName string, toolDef ToolDef, platform dagger.Platform) *dagger.Container { ctr := base // Determine architecture for downloads arch := "amd64" goArch := "x86_64" if platform == "linux/arm64" { arch = "arm64" goArch = "aarch64" } switch toolName { case "kustomize": ctr = ctr.WithExec([]string{"sh", "-c", fmt.Sprintf("wget -q -O - https://github.com/kubernetes-sigs/kustomize/releases/download/kustomize%%2Fv%s/kustomize_v%s_linux_%s.tar.gz | tar xz -C /usr/local/bin", toolDef.Version, toolDef.Version, arch)}) case "dagger": ctr = ctr.WithExec([]string{"sh", "-c", fmt.Sprintf("wget -q -O - https://github.com/dagger/dagger/releases/download/v%s/dagger_v%s_linux_%s.tar.gz | tar xz -C /usr/local/bin", toolDef.Version, toolDef.Version, arch)}) case "esphome": ctr = ctr.WithExec([]string{"apk", "add", "python3", "py3-pip"}). WithExec([]string{"pip3", "install", fmt.Sprintf("esphome==%s", toolDef.Version)}) case "zola": ctr = ctr.WithExec([]string{"sh", "-c", fmt.Sprintf("wget -q -O - https://github.com/getzola/zola/releases/download/v%s/zola-v%s-%s-unknown-linux-gnu.tar.gz | tar xz -C /usr/local/bin", toolDef.Version, toolDef.Version, goArch)}) case "yamllint": ctr = ctr.WithExec([]string{"apk", "add", "python3", "py3-pip"}). WithExec([]string{"pip3", "install", fmt.Sprintf("yamllint==%s", toolDef.Version)}) } return ctr } // BuildAll builds all container images for multiple architectures func (m *Containers) BuildAll(ctx context.Context, // Configuration file configFile *dagger.File, // Registry to push images to // +optional registry string, ) error { // Load configuration config, err := m.loadConfig(ctx, configFile) if err != nil { return fmt.Errorf("failed to load config: %w", err) } platforms := []dagger.Platform{"linux/amd64", "linux/arm64"} // Build and publish Ubuntu base ubuntuBaseTag := "ubuntu-runner:24.04" if registry != "" { ubuntuBaseTag = fmt.Sprintf("%s/%s", registry, ubuntuBaseTag) } var ubuntuBaseVariants []*dagger.Container for _, platform := range platforms { ubuntuBase := m.buildUbuntuBase(ctx, config, platform) ubuntuBaseVariants = append(ubuntuBaseVariants, ubuntuBase) } if _, err := dag.Container().Publish(ctx, ubuntuBaseTag, dagger.ContainerPublishOpts{ PlatformVariants: ubuntuBaseVariants, }); err != nil { return fmt.Errorf("failed to publish ubuntu base: %w", err) } fmt.Printf("Published (multi-arch): %s\n", ubuntuBaseTag) // Build and publish tool-specific images for toolName, toolDef := range config.Tools { // Ubuntu variant var ubuntuToolVariants []*dagger.Container for _, platform := range platforms { ubuntuBase := m.buildUbuntuBase(ctx, config, platform) ubuntuTool := m.buildUbuntuTool(ctx, ubuntuBase, toolName, toolDef, platform) ubuntuToolVariants = append(ubuntuToolVariants, ubuntuTool) } ubuntuTag := fmt.Sprintf("ubuntu-%s:%s", toolName, toolDef.Version) if registry != "" { ubuntuTag = fmt.Sprintf("%s/%s", registry, ubuntuTag) } if _, err := dag.Container().Publish(ctx, ubuntuTag, dagger.ContainerPublishOpts{ PlatformVariants: ubuntuToolVariants, }); err != nil { return fmt.Errorf("failed to publish ubuntu-%s: %w", toolName, err) } fmt.Printf("Published (multi-arch): %s\n", ubuntuTag) // Chainguard variant var chainguardToolVariants []*dagger.Container for _, platform := range platforms { chainguardBase := m.buildChainguardBase(ctx, config, platform) chainguardTool := m.buildChainguardTool(ctx, chainguardBase, toolName, toolDef, platform) chainguardToolVariants = append(chainguardToolVariants, chainguardTool) } chainguardTag := fmt.Sprintf("%s:%s", toolName, toolDef.Version) if registry != "" { chainguardTag = fmt.Sprintf("%s/%s", registry, chainguardTag) } if _, err := dag.Container().Publish(ctx, chainguardTag, dagger.ContainerPublishOpts{ PlatformVariants: chainguardToolVariants, }); err != nil { return fmt.Errorf("failed to publish %s: %w", toolName, err) } fmt.Printf("Published (multi-arch): %s\n", chainguardTag) } return nil } // Build a single tool image func (m *Containers) BuildTool(ctx context.Context, // Tool name to build (e.g., "kustomize", "dagger") tool string, // Configuration file configFile *dagger.File, // Base image type: "ubuntu" or "chainguard" // +optional // +default="ubuntu" base string, // Platform to build for // +optional // +default="linux/amd64" platform dagger.Platform, ) (*dagger.Container, error) { // Load configuration config, err := m.loadConfig(ctx, configFile) if err != nil { return nil, fmt.Errorf("failed to load config: %w", err) } toolDef, ok := config.Tools[tool] if !ok { return nil, fmt.Errorf("tool %s not found in config", tool) } var baseImage *dagger.Container if base == "ubuntu" { baseImage = m.buildUbuntuBase(ctx, config, platform) return m.buildUbuntuTool(ctx, baseImage, tool, toolDef, platform), nil } else { baseImage = m.buildChainguardBase(ctx, config, platform) return m.buildChainguardTool(ctx, baseImage, tool, toolDef, platform), nil } }