Initial commit
This commit is contained in:
commit
a7514cca82
12 changed files with 988 additions and 0 deletions
445
.dagger/main.go
Normal file
445
.dagger/main.go
Normal file
|
|
@ -0,0 +1,445 @@
|
|||
// Container image builder for Ubuntu and Chainguard-based images with extensible tools
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"dagger/containers/internal/dagger"
|
||||
"fmt"
|
||||
|
||||
"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
|
||||
}
|
||||
|
||||
// archFor returns architecture strings used by various upstream releases.
|
||||
// arch: used by jq/yq/kustomize/dagger ("amd64"|"arm64")
|
||||
// goArch: used by zola ("x86_64"|"aarch64")
|
||||
func (m *Containers) archFor(platform dagger.Platform) (arch, goArch string) {
|
||||
arch = "amd64"
|
||||
goArch = "x86_64"
|
||||
if platform == "linux/arm64" {
|
||||
arch = "arm64"
|
||||
goArch = "aarch64"
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// withHTTPExecutable downloads a file via Dagger HTTP and installs it to dst with +x
|
||||
func (m *Containers) withHTTPExecutable(ctr *dagger.Container, url, dst string) *dagger.Container {
|
||||
file := dag.HTTP(url)
|
||||
return ctr.WithFile(dst, file).WithExec([]string{"chmod", "+x", dst})
|
||||
}
|
||||
|
||||
// withHTTPFile downloads a remote file via Dagger HTTP to a temporary path
|
||||
func (m *Containers) withHTTPFile(ctr *dagger.Container, url, tmpPath string) *dagger.Container {
|
||||
file := dag.HTTP(url)
|
||||
return ctr.WithFile(tmpPath, file)
|
||||
}
|
||||
|
||||
// extractTarGz extracts a .tar.gz archive to destination directory
|
||||
func (m *Containers) extractTarGz(ctr *dagger.Container, archivePath, destDir string) *dagger.Container {
|
||||
return ctr.WithExec([]string{"tar", "-xzf", archivePath, "-C", destDir})
|
||||
}
|
||||
|
||||
// installJq installs jq for the provided arch into /usr/local/bin
|
||||
func (m *Containers) installJq(ctr *dagger.Container, config *ToolsConfig, arch string) *dagger.Container {
|
||||
jqURL := fmt.Sprintf("https://github.com/jqlang/jq/releases/download/jq-%s/jq-linux-%s", config.Core.Jq, arch)
|
||||
ctr = m.withHTTPExecutable(ctr, jqURL, "/usr/local/bin/jq")
|
||||
|
||||
return ctr
|
||||
}
|
||||
|
||||
// installYq installs yq for the provided arch into /usr/local/bin
|
||||
func (m *Containers) installYq(ctr *dagger.Container, config *ToolsConfig, arch string) *dagger.Container {
|
||||
yqURL := fmt.Sprintf("https://github.com/mikefarah/yq/releases/download/v%s/yq_linux_%s", config.Core.Yq, arch)
|
||||
ctr = m.withHTTPExecutable(ctr, yqURL, "/usr/local/bin/yq")
|
||||
|
||||
return ctr
|
||||
}
|
||||
|
||||
// installKustomize downloads and installs kustomize for the given arch
|
||||
func (m *Containers) installKustomize(ctr *dagger.Container, toolDef ToolDef, arch string) *dagger.Container {
|
||||
url := fmt.Sprintf("https://github.com/kubernetes-sigs/kustomize/releases/download/kustomize%%2Fv%s/kustomize_v%s_linux_%s.tar.gz", toolDef.Version, toolDef.Version, arch)
|
||||
tmp := fmt.Sprintf("/tmp/kustomize_v%s_linux_%s.tar.gz", toolDef.Version, arch)
|
||||
ctr = m.withHTTPFile(ctr, url, tmp)
|
||||
|
||||
return m.extractTarGz(ctr, tmp, "/usr/local/bin")
|
||||
}
|
||||
|
||||
// installDagger downloads and installs dagger CLI for the given arch
|
||||
func (m *Containers) installDagger(ctr *dagger.Container, toolDef ToolDef, arch string) *dagger.Container {
|
||||
url := fmt.Sprintf("https://github.com/dagger/dagger/releases/download/v%s/dagger_v%s_linux_%s.tar.gz", toolDef.Version, toolDef.Version, arch)
|
||||
tmp := fmt.Sprintf("/tmp/dagger_v%s_linux_%s.tar.gz", toolDef.Version, arch)
|
||||
ctr = m.withHTTPFile(ctr, url, tmp)
|
||||
|
||||
return m.extractTarGz(ctr, tmp, "/usr/local/bin")
|
||||
}
|
||||
|
||||
// installZola downloads and installs zola for the given goArch
|
||||
func (m *Containers) installZola(ctr *dagger.Container, toolDef ToolDef, goArch string) *dagger.Container {
|
||||
url := fmt.Sprintf("https://github.com/getzola/zola/releases/download/v%s/zola-v%s-%s-unknown-linux-gnu.tar.gz", toolDef.Version, toolDef.Version, goArch)
|
||||
tmp := fmt.Sprintf("/tmp/zola-v%s-%s-unknown-linux-gnu.tar.gz", toolDef.Version, goArch)
|
||||
ctr = m.withHTTPFile(ctr, url, tmp)
|
||||
|
||||
return m.extractTarGz(ctr, tmp, "/usr/local/bin")
|
||||
}
|
||||
|
||||
// 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, _ := m.archFor(platform)
|
||||
// Node.js uses "x64" for amd64 in official tarball names
|
||||
nodeArch := arch
|
||||
if arch == "amd64" {
|
||||
nodeArch = "x64"
|
||||
}
|
||||
|
||||
// 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",
|
||||
"xz-utils",
|
||||
})
|
||||
|
||||
// Install Git
|
||||
ctr = ctr.WithExec([]string{"apt-get", "install", "-y", "git"})
|
||||
|
||||
// Install jq & yq
|
||||
ctr = m.installJq(ctr, config, arch)
|
||||
ctr = m.installYq(ctr, config, arch)
|
||||
|
||||
// Install multiple Node.js versions side-by-side (20, 22, 24)
|
||||
// We fetch official tarballs and create versioned shims: node20/node22/node24 and npm20/npm22/npm24
|
||||
// Default `node`/`npm` will point to Node 24
|
||||
// Prepare Node.js tarballs via Dagger HTTP (cached) and install three versions side-by-side
|
||||
{
|
||||
nodeURL := func(ver string) string {
|
||||
return fmt.Sprintf("https://nodejs.org/dist/v%s/node-v%s-linux-%s.tar.xz", ver, ver, nodeArch)
|
||||
}
|
||||
n20 := dag.HTTP(nodeURL(config.Core.Node20))
|
||||
n22 := dag.HTTP(nodeURL(config.Core.Node22))
|
||||
n24 := dag.HTTP(nodeURL(config.Core.Node24))
|
||||
|
||||
n20Name := fmt.Sprintf("/tmp/node-v%s-linux-%s.tar.xz", config.Core.Node20, nodeArch)
|
||||
n22Name := fmt.Sprintf("/tmp/node-v%s-linux-%s.tar.xz", config.Core.Node22, nodeArch)
|
||||
n24Name := fmt.Sprintf("/tmp/node-v%s-linux-%s.tar.xz", config.Core.Node24, nodeArch)
|
||||
|
||||
ctr = ctr.WithFile(n20Name, n20).
|
||||
WithFile(n22Name, n22).
|
||||
WithFile(n24Name, n24)
|
||||
|
||||
// Use bash for pipefail support (dash/sh doesn't support 'pipefail')
|
||||
ctr = ctr.WithExec([]string{"bash", "-lc",
|
||||
fmt.Sprintf(`set -euo pipefail
|
||||
mkdir -p /opt
|
||||
|
||||
extract_node() {
|
||||
tarball="$1"
|
||||
ver="$2"
|
||||
major="$3"
|
||||
dest="/opt/node-v${ver}"
|
||||
mkdir -p "$dest"
|
||||
# Official tarballs extract as node-v<ver>-linux-<arch>; normalize into /opt/node-v<ver>
|
||||
tar -xJf "$tarball" -C "$dest" --strip-components=1
|
||||
if [ ! -d "$dest" ]; then
|
||||
echo "Expected $dest to exist after extraction" >&2
|
||||
exit 1
|
||||
fi
|
||||
ln -sf "$dest/bin/node" "/usr/local/bin/node${major}"
|
||||
ln -sf "$dest/bin/npm" "/usr/local/bin/npm${major}"
|
||||
if [ -f "$dest/bin/npx" ]; then
|
||||
ln -sf "$dest/bin/npx" "/usr/local/bin/npx${major}"
|
||||
fi
|
||||
}
|
||||
|
||||
extract_node "%s" "%s" 20
|
||||
extract_node "%s" "%s" 22
|
||||
extract_node "%s" "%s" 24
|
||||
|
||||
# Set default to Node 24
|
||||
ln -sf "/usr/local/bin/node24" "/usr/local/bin/node"
|
||||
ln -sf "/usr/local/bin/npm24" "/usr/local/bin/npm"
|
||||
if [ -e "/usr/local/bin/npx24" ]; then
|
||||
ln -sf "/usr/local/bin/npx24" "/usr/local/bin/npx"
|
||||
fi
|
||||
`, n20Name, config.Core.Node20, n22Name, config.Core.Node22, n24Name, config.Core.Node24)})
|
||||
}
|
||||
|
||||
// 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, _ := m.archFor(platform)
|
||||
|
||||
// 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 & yq
|
||||
ctr = m.installJq(ctr, config, arch)
|
||||
ctr = m.installYq(ctr, config, 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, goArch := m.archFor(platform)
|
||||
|
||||
switch toolName {
|
||||
case "kustomize":
|
||||
ctr = m.installKustomize(ctr, toolDef, arch)
|
||||
|
||||
case "dagger":
|
||||
ctr = m.installDagger(ctr, toolDef, 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 = m.installZola(ctr, toolDef, 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, goArch := m.archFor(platform)
|
||||
|
||||
switch toolName {
|
||||
case "kustomize":
|
||||
ctr = m.installKustomize(ctr, toolDef, arch)
|
||||
|
||||
case "dagger":
|
||||
ctr = m.installDagger(ctr, toolDef, 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 = m.installZola(ctr, toolDef, 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,
|
||||
// Optional image name prefix to insert between registry and image name (e.g., "foo" -> registry/foo/image:tag)
|
||||
// +optional
|
||||
prefix string,
|
||||
// Username for authenticating to the registry
|
||||
// +optional
|
||||
registryUsername string,
|
||||
// Password/token for authenticating to the registry
|
||||
// +optional
|
||||
registryPassword *dagger.Secret,
|
||||
) 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 prefix != "" {
|
||||
ubuntuBaseTag = fmt.Sprintf("%s/%s/%s", registry, prefix, ubuntuBaseTag)
|
||||
} else {
|
||||
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)
|
||||
}
|
||||
|
||||
// Configure registry auth if provided
|
||||
publisher := dag.Container()
|
||||
if registry != "" && registryUsername != "" && registryPassword != nil {
|
||||
publisher = publisher.WithRegistryAuth(registry, registryUsername, registryPassword)
|
||||
}
|
||||
|
||||
if _, err := publisher.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 prefix != "" {
|
||||
ubuntuTag = fmt.Sprintf("%s/%s/%s", registry, prefix, ubuntuTag)
|
||||
} else {
|
||||
ubuntuTag = fmt.Sprintf("%s/%s", registry, ubuntuTag)
|
||||
}
|
||||
publisher := dag.Container()
|
||||
if registry != "" && registryUsername != "" && registryPassword != nil {
|
||||
publisher = publisher.WithRegistryAuth(registry, registryUsername, registryPassword)
|
||||
}
|
||||
if _, err := publisher.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 prefix != "" {
|
||||
chainguardTag = fmt.Sprintf("%s/%s/%s", registry, prefix, chainguardTag)
|
||||
} else {
|
||||
chainguardTag = fmt.Sprintf("%s/%s", registry, chainguardTag)
|
||||
}
|
||||
publisher = dag.Container()
|
||||
if registry != "" && registryUsername != "" && registryPassword != nil {
|
||||
publisher = publisher.WithRegistryAuth(registry, registryUsername, registryPassword)
|
||||
}
|
||||
if _, err := publisher.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
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue