···11+# Buildah Container Builds in Loom
22+33+This document describes the buildah integration in Loom, enabling workflows to build and push container images.
44+55+## Quick Start
66+77+```yaml
88+# .tangled/workflows/build.yaml
99+image: golang:1.24-bookworm
1010+architecture: amd64
1111+1212+steps:
1313+ - name: Build app
1414+ command: go build -o app ./cmd
1515+1616+ - name: Build and push container
1717+ command: |
1818+ # buildah is available in PATH
1919+ buildah bud --storage-driver=overlay -t registry.example.com/myapp:latest .
2020+ buildah push --storage-driver=overlay registry.example.com/myapp:latest
2121+```
2222+2323+For registry authentication, see [Registry Authentication](#2-with-registry-authentication).
2424+2525+## Overview
2626+2727+Loom Jobs can now build container images using **buildah**, a rootless, daemonless container building tool. This implementation provides:
2828+2929+- ✅ **Rootless operation** - All containers run as UID 10000 (non-root)
3030+- ✅ **No privileged mode** - Maintains strict security posture
3131+- ✅ **No caching** - Ephemeral builds like GitHub Actions runners
3232+- ✅ **Registry authentication** - Optional Docker config secret mounting
3333+- ✅ **Native architecture only** - Builds for the node's native arch (amd64/arm64)
3434+- ✅ **Simple usage** - `buildah` command available in PATH, no full paths needed
3535+3636+## Architecture Decision
3737+3838+### Why Buildah?
3939+4040+After comprehensive research comparing container building options in Kubernetes (2025):
4141+4242+| Option | Status | Security | Performance | Verdict |
4343+|--------|--------|----------|-------------|---------|
4444+| **Docker-in-Docker** | Active | ❌ Requires privileged mode | Good | ❌ Incompatible |
4545+| **Kaniko** | ⚠️ Archived (Google) | ✅ Rootless-capable | Poor (63% slower) | ❌ Not recommended |
4646+| **Buildah** | ✅ Active | ✅ True rootless | Excellent | ✅ **Selected** |
4747+| **BuildKit** | ✅ Active | ✅ Rootless-capable | Best | ⚠️ Alternative |
4848+4949+**Buildah was chosen for:**
5050+1. True rootless operation with user namespace support (K8s 1.33+)
5151+2. Simpler configuration than BuildKit
5252+3. Excellent performance
5353+4. Active development (Red Hat/Podman project)
5454+5. Better for OpenShift/Kubernetes environments
5555+5656+## Implementation Details
5757+5858+### Pod Structure
5959+6060+When a Job is created, the following components are configured:
6161+6262+#### 1. Pod Security Context
6363+```yaml
6464+securityContext:
6565+ runAsNonRoot: true
6666+ runAsUser: 10000
6767+ fsGroup: 10000
6868+ seccompProfile:
6969+ type: RuntimeDefault
7070+```
7171+7272+**Note:** User namespaces (`hostUsers: false`) for enhanced security require Kubernetes 1.33+ and are not yet available in the current API version. Buildah still works in rootless mode without it.
7373+7474+#### 2. Init Containers
7575+7676+**a. install-runner** (existing)
7777+- Copies `/loom-runner` binary to shared volume
7878+7979+**b. configure-buildah** (new)
8080+- Image: `quay.io/buildah/stable:latest`
8181+- Configures buildah storage driver (`/var/lib/containers/storage.conf`)
8282+- Copies `buildah` binary to `/runner-bin/buildah`
8383+- Runs as UID 10000 (non-root)
8484+8585+**c. clone-repo** (existing)
8686+- Clones git repository
8787+8888+#### 3. Main Container (runner)
8989+9090+Mounts:
9191+- `/tangled/workspace` - Git repository workspace
9292+- `/runner-bin/` - Loom runner and buildah binaries
9393+- `/var/lib/containers` - Buildah storage (emptyDir)
9494+- `/home/user/.docker/config.json` - Registry credentials (optional)
9595+9696+### Buildah Configuration
9797+9898+The init container creates this storage configuration:
9999+100100+```toml
101101+[storage]
102102+driver = "overlay"
103103+runroot = "/var/lib/containers/runroot"
104104+graphroot = "/var/lib/containers/storage"
105105+106106+[storage.options]
107107+additionalimagestores = []
108108+109109+[storage.options.overlay]
110110+mount_program = "/usr/bin/fuse-overlayfs"
111111+mountopt = "nodev,metacopy=on"
112112+```
113113+114114+This enables:
115115+- **Overlay storage driver** - Efficient layer management
116116+- **fuse-overlayfs** - Rootless overlay mounts
117117+- **Ephemeral storage** - No persistent caching (like GitHub Actions)
118118+119119+## Usage
120120+121121+### 1. Basic Container Build
122122+123123+```yaml
124124+# .tangled/workflows/build.yaml
125125+image: golang:1.24-bookworm
126126+architecture: amd64
127127+128128+steps:
129129+ - name: Build app
130130+ command: go build -o app ./cmd
131131+132132+ - name: Build container
133133+ command: |
134134+ # buildah is in PATH (via /runner-bin)
135135+ buildah bud \
136136+ --storage-driver=overlay \
137137+ -t registry.example.com/myapp:latest \
138138+ .
139139+140140+ - name: Push container
141141+ command: |
142142+ buildah push \
143143+ --storage-driver=overlay \
144144+ registry.example.com/myapp:latest
145145+```
146146+147147+**Note:** The `buildah` binary is automatically available in PATH. You don't need to use the full path `/runner-bin/buildah`.
148148+149149+### 2. With Registry Authentication
150150+151151+**Step 1: Create registry credentials secret**
152152+153153+```bash
154154+kubectl create secret docker-registry registry-credentials \
155155+ --docker-server=registry.example.com \
156156+ --docker-username=myuser \
157157+ --docker-password=mypassword
158158+```
159159+160160+**Step 2: Configure in Loom ConfigMap**
161161+162162+```yaml
163163+apiVersion: v1
164164+kind: ConfigMap
165165+metadata:
166166+ name: loom-config
167167+data:
168168+ config.yaml: |
169169+ template:
170170+ registryCredentialsSecret: registry-credentials
171171+```
172172+173173+**Step 3: Use in workflow**
174174+175175+```yaml
176176+steps:
177177+ - name: Build and push
178178+ command: |
179179+ # buildah automatically uses /home/user/.docker/config.json
180180+ buildah bud -t registry.example.com/myapp:latest .
181181+ buildah push registry.example.com/myapp:latest
182182+```
183183+184184+### 3. Multi-Tag Builds
185185+186186+```yaml
187187+steps:
188188+ - name: Build with multiple tags
189189+ command: |
190190+ buildah bud \
191191+ --storage-driver=overlay \
192192+ -t registry.example.com/myapp:${TANGLED_COMMIT_SHA} \
193193+ -t registry.example.com/myapp:latest \
194194+ -t registry.example.com/myapp:${GITHUB_REF_NAME} \
195195+ .
196196+197197+ - name: Push all tags
198198+ command: |
199199+ buildah push registry.example.com/myapp:${TANGLED_COMMIT_SHA}
200200+ buildah push registry.example.com/myapp:latest
201201+ buildah push registry.example.com/myapp:${GITHUB_REF_NAME}
202202+```
203203+204204+## Environment Variables
205205+206206+The runner container has access to:
207207+208208+| Variable | Description | Example |
209209+|----------|-------------|---------|
210210+| `PATH` | Search path for executables | `/runner-bin:/usr/local/bin:...` |
211211+| `TANGLED_WORKFLOW` | Workflow filename | `build.yaml` |
212212+| `TANGLED_PIPELINE_ID` | Unique pipeline ID | `abc123` |
213213+| `TANGLED_REPO_URL` | Git repository URL | `https://github.com/user/repo` |
214214+| `TANGLED_COMMIT_SHA` | Git commit SHA | `abc123def456...` |
215215+| `TANGLED_ARCHITECTURE` | Target architecture | `amd64` or `arm64` |
216216+| `CI` | CI environment flag | `true` |
217217+218218+**Note:** The `PATH` includes `/runner-bin` first, making both `loom-runner` and `buildah` available without full paths.
219219+220220+## Security Model
221221+222222+### Current Security Posture
223223+224224+✅ **Implemented:**
225225+- All containers run as UID 10000 (non-root)
226226+- `allowPrivilegeEscalation: false` on all containers
227227+- All Linux capabilities dropped
228228+- `seccompProfile: RuntimeDefault`
229229+- Read-only root filesystems where possible
230230+- No ServiceAccount token mounting
231231+- No privileged mode
232232+233233+⏳ **Future (K8s 1.33+):**
234234+- User namespaces (`hostUsers: false`) for enhanced isolation
235235+236236+### What buildah CAN do in this configuration:
237237+238238+✅ Build container images from Dockerfiles
239239+✅ Push to registries with credentials
240240+✅ Tag images
241241+✅ Inspect images
242242+✅ Run rootless containers for builds
243243+244244+### What buildah CANNOT do:
245245+246246+❌ Access host namespaces
247247+❌ Mount host paths
248248+❌ Run privileged operations
249249+❌ Escape the container sandbox
250250+251251+## Troubleshooting
252252+253253+### Issue: Permission denied writing to /var/lib/containers
254254+255255+**Cause:** fsGroup not set correctly
256256+**Solution:** Verify pod security context has `fsGroup: 10000`
257257+258258+### Issue: buildah: command not found
259259+260260+**Cause:** Init container failed to copy buildah binary
261261+**Solution:** Check init container logs:
262262+```bash
263263+kubectl logs <pod-name> -c configure-buildah
264264+```
265265+266266+### Issue: Error pushing to registry - authentication required
267267+268268+**Cause:** Registry credentials not mounted
269269+**Solution:**
270270+1. Verify secret exists: `kubectl get secret registry-credentials`
271271+2. Verify ConfigMap references it: `kubectl get cm loom-config -o yaml`
272272+3. Check mount in pod: `kubectl exec <pod-name> -- ls -la /home/user/.docker/`
273273+274274+### Issue: overlay mount failed
275275+276276+**Cause:** Node doesn't support overlay or fuse-overlayfs
277277+**Solution:** Ensure node kernel supports overlayfs (Linux 3.18+) and fuse-overlayfs is available
278278+279279+### Issue: Slow builds
280280+281281+**Expected:** No layer caching in MVP (ephemeral like GitHub Actions)
282282+**Future:** Implement persistent volume for buildah storage to enable caching
283283+284284+## Comparison with Other CI/CD Systems
285285+286286+| Feature | Loom (Buildah) | GitHub Actions | GitLab CI | Jenkins |
287287+|---------|---------------|----------------|-----------|---------|
288288+| Rootless builds | ✅ UID 10000 | ⚠️ root in container | ⚠️ Varies | ⚠️ Varies |
289289+| Privileged mode | ❌ Never | ❌ Not by default | ⚠️ Often used | ⚠️ Often used |
290290+| Layer caching | ❌ MVP (ephemeral) | ❌ Ephemeral | ✅ Optional | ✅ Optional |
291291+| Security isolation | ✅ Strong | ✅ Strong | ⚠️ Medium | ⚠️ Varies |
292292+| Multi-arch | ✅ Native only | ✅ Native + QEMU | ✅ Native + QEMU | ⚠️ Varies |
293293+294294+## Files Modified
295295+296296+### API Types
297297+- `api/v1alpha1/spindleset_types.go`
298298+ - Added `RegistryCredentialsSecret` field to `SpindleTemplate`
299299+300300+### Job Builder
301301+- `internal/jobbuilder/job_template.go`
302302+ - Added `configure-buildah` init container
303303+ - Added `buildah-storage` volume
304304+ - Added `buildRunnerVolumeMounts()` helper
305305+ - Added `buildVolumes()` helper
306306+ - Optional registry credentials mount support
307307+308308+### Examples
309309+- `config/samples/workflow-buildah-example.yaml` - Complete example workflow
310310+- `config/samples/registry-secret-example.yaml` - Registry credential examples
311311+312312+## Future Enhancements
313313+314314+### 1. Persistent Layer Caching
315315+Add PersistentVolumeClaim for `/var/lib/containers/storage`:
316316+317317+```yaml
318318+volumes:
319319+- name: buildah-storage
320320+ persistentVolumeClaim:
321321+ claimName: buildah-cache
322322+```
323323+324324+**Benefits:**
325325+- Faster rebuilds (reuse layers)
326326+- Reduced network bandwidth
327327+- Lower registry costs
328328+329329+**Trade-offs:**
330330+- Requires storage provisioning
331331+- Need cache cleanup strategy
332332+- Multi-tenancy considerations
333333+334334+### 2. BuildKit Alternative
335335+Support both buildah and BuildKit via workflow configuration:
336336+337337+```yaml
338338+# .tangled/workflows/build.yaml
339339+builder: buildkit # or buildah (default)
340340+```
341341+342342+**BuildKit advantages:**
343343+- 63% faster in benchmarks
344344+- Advanced caching (registry-based)
345345+- Parallel layer processing
346346+347347+### 3. Multi-Platform Builds
348348+Enable cross-platform builds with QEMU:
349349+350350+```yaml
351351+architecture: amd64
352352+platforms:
353353+ - linux/amd64
354354+ - linux/arm64
355355+```
356356+357357+### 4. Build Arguments from Secrets
358358+Inject secrets as build arguments:
359359+360360+```yaml
361361+steps:
362362+ - name: Build with secrets
363363+ command: |
364364+ buildah bud \
365365+ --secret id=token,env=GITHUB_TOKEN \
366366+ -t myapp:latest .
367367+```
368368+369369+## References
370370+371371+- **Buildah Documentation**: https://github.com/containers/buildah
372372+- **Buildah Rootless Guide**: https://github.com/containers/buildah/blob/main/docs/tutorials/05-rootless.md
373373+- **Loom Architecture**: `/home/data/loom/ARCHITECTURE.md`
374374+- **Implementation Research**: See planning discussion for detailed comparison of building options
375375+376376+## Summary
377377+378378+Buildah integration provides Loom with secure, rootless container building capabilities. The implementation:
379379+380380+✅ Maintains strict security (no privileged mode, rootless operation)
381381+✅ Provides simple, flexible workflow syntax (use buildah commands directly)
382382+✅ Supports registry authentication
383383+✅ Works like ephemeral GitHub Actions runners (no caching in MVP)
384384+✅ Enables full CI/CD pipelines: test → build → containerize → deploy
385385+386386+Next steps: Test in production, add persistent caching, consider BuildKit as alternative.
+49
CONFIGURATION.md
···189189 value: "spindle" # Default mount path
190190```
191191192192+## Security Model
193193+194194+### Secrets Management
195195+196196+Loom integrates with the embedded spindle server's secrets management system:
197197+198198+**Adding Secrets:**
199199+```bash
200200+curl -X POST http://loom:6555/xrpc/sh.tangled.repo.addSecret \
201201+ -H "Authorization: Bearer <did-token>" \
202202+ -d '{
203203+ "repo": "at://did:plc:your-did/sh.tangled.repo/your-repo",
204204+ "key": "NPM_TOKEN",
205205+ "value": "npm_xxxxx"
206206+ }'
207207+```
208208+209209+**How Secrets Work:**
210210+1. Secrets are stored in the vault (SQLite or OpenBao)
211211+2. When a pipeline runs, Loom retrieves secrets for that repository
212212+3. A Kubernetes Secret is created per SpindleSet
213213+4. Job pods receive secrets as environment variables via `envFrom`
214214+215215+**Important Notes:**
216216+- Secret keys must be valid bash identifiers (`^[a-zA-Z_][a-zA-Z0-9_]*$`)
217217+- Secrets are injected directly (e.g., `NPM_TOKEN`, not `TANGLED_NPM_TOKEN`)
218218+- Cluster operators with filesystem/database access can view secrets
219219+- For operator-blind secrets, configure OpenBao with seal/unseal or cloud KMS
220220+221221+### Job Pod Security
222222+223223+Job pods run with hardened security contexts:
224224+225225+```yaml
226226+securityContext:
227227+ runAsNonRoot: true
228228+ runAsUser: 10000
229229+ readOnlyRootFilesystem: true
230230+ allowPrivilegeEscalation: false
231231+ capabilities:
232232+ drop: ["ALL"]
233233+```
234234+235235+**Service Account Isolation:**
236236+- Jobs use `spindle-job-runner` ServiceAccount (not controller SA)
237237+- ServiceAccount token mounting is disabled
238238+- Jobs have zero Kubernetes API permissions
239239+- Prevents jobs from reading other repos' secrets via K8s API
240240+192241## Persistence
193242194243### SQLite Database
+1-1
PLAN.md
···380380- Implement Nix store caching (PVC)
381381382382### Phase 8: Advanced Features
383383-- Kaniko/Buildah integration for container builds
383383+- ✅ **Buildah integration for container builds** (MVP completed - no caching)
384384- Service containers (like GitHub Actions services)
385385- Matrix builds (multiple arch/version combinations)
386386- Caching strategies (build cache, dependencies)
+25
api/v1alpha1/spindleset_types.go
···6666 // +optional
6767 SkipClone bool `json:"skipClone,omitempty"`
68686969+ // Secrets contains the repository secrets to inject into workflow Jobs.
7070+ // Retrieved from the secrets vault and stored here for the controller to consume.
7171+ // +optional
7272+ Secrets []SecretData `json:"secrets,omitempty"`
7373+6974 // Workflows is the list of workflows to execute in this pipeline.
7075 // +kubebuilder:validation:MinItems=1
7176 Workflows []WorkflowSpec `json:"workflows"`
7777+}
7878+7979+// SecretData represents a single secret key-value pair for injection into Jobs.
8080+type SecretData struct {
8181+ // Key is the environment variable name (e.g., "GITHUB_TOKEN").
8282+ // +kubebuilder:validation:Required
8383+ // +kubebuilder:validation:Pattern=`^[a-zA-Z_][a-zA-Z0-9_]*$`
8484+ Key string `json:"key"`
8585+8686+ // Value is the secret value in plaintext.
8787+ // This field should only be populated transiently during SpindleSet creation
8888+ // and consumed immediately by the controller to create a Kubernetes Secret.
8989+ // +kubebuilder:validation:Required
9090+ Value string `json:"value"`
7291}
73927493// WorkflowSpec defines a workflow to execute as part of a pipeline.
···167186 // Affinity defines scheduling constraints for spindle job pods.
168187 // +optional
169188 Affinity *corev1.Affinity `json:"affinity,omitempty"`
189189+190190+ // RegistryCredentialsSecret is the name of a kubernetes.io/dockerconfigjson secret
191191+ // containing registry credentials for buildah to use when pushing images.
192192+ // If specified, the secret is mounted at /home/user/.docker/config.json.
193193+ // +optional
194194+ RegistryCredentialsSecret string `json:"registryCredentialsSecret,omitempty"`
170195}
171196172197// SpindleSetStatus defines the observed state of SpindleSet.
+20
api/v1alpha1/zz_generated.deepcopy.go
···3434 *out = make([]string, len(*in))
3535 copy(*out, *in)
3636 }
3737+ if in.Secrets != nil {
3838+ in, out := &in.Secrets, &out.Secrets
3939+ *out = make([]SecretData, len(*in))
4040+ copy(*out, *in)
4141+ }
3742 if in.Workflows != nil {
3843 in, out := &in.Workflows, &out.Workflows
3944 *out = make([]WorkflowSpec, len(*in))
···7277 return nil
7378 }
7479 out := new(ResourceProfile)
8080+ in.DeepCopyInto(out)
8181+ return out
8282+}
8383+8484+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
8585+func (in *SecretData) DeepCopyInto(out *SecretData) {
8686+ *out = *in
8787+}
8888+8989+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SecretData.
9090+func (in *SecretData) DeepCopy() *SecretData {
9191+ if in == nil {
9292+ return nil
9393+ }
9494+ out := new(SecretData)
7595 in.DeepCopyInto(out)
7696 return out
7797}
+8-9
cmd/controller/main.go
···191191 ResourceProfiles: profiles,
192192 }
193193194194- kubeEngine := engine.NewKubernetesEngine(mgr.GetClient(), mgr.GetConfig(), namespace, template)
195195-196196- // Create engines map with kubernetes engine
197197- engines := map[string]models.Engine{
198198- "kubernetes": kubeEngine,
199199- }
200200-201201- // Use spindle's New() function to create spindle server with our custom engine
202202- s, err := spindle.New(ctx, cfg, engines)
194194+ // Create spindle server first (without engine) to get access to vault
195195+ s, err := spindle.New(ctx, cfg, map[string]models.Engine{})
203196 if err != nil {
204197 return nil, fmt.Errorf("failed to create spindle: %w", err)
205198 }
199199+200200+ // Now create kubernetes engine with access to vault
201201+ kubeEngine := engine.NewKubernetesEngine(mgr.GetClient(), mgr.GetConfig(), namespace, template, s.Vault())
202202+203203+ // Register the engine with spindle by adding to the engines map
204204+ s.Engines()["kubernetes"] = kubeEngine
206205207206 return s, nil
208207}
+30
config/crd/bases/loom.j5t.io_spindlesets.yaml
···9090 repoURL:
9191 description: RepoURL is the Git repository URL to clone.
9292 type: string
9393+ secrets:
9494+ description: |-
9595+ Secrets contains the repository secrets to inject into workflow Jobs.
9696+ Retrieved from the secrets vault and stored here for the controller to consume.
9797+ items:
9898+ description: SecretData represents a single secret key-value
9999+ pair for injection into Jobs.
100100+ properties:
101101+ key:
102102+ description: Key is the environment variable name (e.g.,
103103+ "GITHUB_TOKEN").
104104+ pattern: ^[a-zA-Z_][a-zA-Z0-9_]*$
105105+ type: string
106106+ value:
107107+ description: |-
108108+ Value is the secret value in plaintext.
109109+ This field should only be populated transiently during SpindleSet creation
110110+ and consumed immediately by the controller to create a Kubernetes Secret.
111111+ type: string
112112+ required:
113113+ - key
114114+ - value
115115+ type: object
116116+ type: array
93117 skipClone:
94118 description: SkipClone indicates whether to skip the clone init
95119 container entirely.
···11171141 x-kubernetes-list-type: atomic
11181142 type: object
11191143 type: object
11441144+ registryCredentialsSecret:
11451145+ description: |-
11461146+ RegistryCredentialsSecret is the name of a kubernetes.io/dockerconfigjson secret
11471147+ containing registry credentials for buildah to use when pushing images.
11481148+ If specified, the secret is mounted at /home/user/.docker/config.json.
11491149+ type: string
11201150 resourceProfiles:
11211151 description: |-
11221152 ResourceProfiles is an ordered list of resource configurations based on node labels.
+1
config/rbac/kustomization.yaml
···55# runtime. Be sure to update RoleBinding and ClusterRoleBinding
66# subjects if changing service account names.
77- service_account.yaml
88+- spindle_job_service_account.yaml
89- role.yaml
910- role_binding.yaml
1011- leader_election_role.yaml
+13
config/rbac/spindle_job_service_account.yaml
···11+# ServiceAccount for spindle Job pods
22+# This account has ZERO Kubernetes API permissions for security isolation
33+# Jobs should not need to interact with the Kubernetes API
44+apiVersion: v1
55+kind: ServiceAccount
66+metadata:
77+ name: spindle-job-runner
88+ namespace: system
99+automountServiceAccountToken: false
1010+---
1111+# Note: No Role or RoleBinding created intentionally
1212+# Job pods should have no permissions to read Secrets, list Pods, etc.
1313+# This prevents malicious workflows from accessing other users' secrets via K8s API
+148
config/samples/registry-secret-example.yaml
···11+# Example: Creating a registry credentials secret for buildah
22+#
33+# This secret provides authentication for buildah to push images to a container registry.
44+# The secret is mounted at /home/user/.docker/config.json in Job pods when
55+# the SpindleTemplate has registryCredentialsSecret set.
66+#
77+# Usage:
88+# 1. Create the secret using one of the methods below
99+# 2. Configure your SpindleSet template to reference it:
1010+#
1111+# apiVersion: v1
1212+# kind: ConfigMap
1313+# metadata:
1414+# name: loom-config
1515+# data:
1616+# config.yaml: |
1717+# template:
1818+# registryCredentialsSecret: registry-credentials
1919+#
2020+# Method 1: Create secret using kubectl (recommended)
2121+# =====================================================
2222+#
2323+# For Docker Hub:
2424+# kubectl create secret docker-registry registry-credentials \
2525+# --docker-server=docker.io \
2626+# --docker-username=YOUR_USERNAME \
2727+# --docker-password=YOUR_PASSWORD \
2828+# --docker-email=YOUR_EMAIL
2929+#
3030+# For GitHub Container Registry (ghcr.io):
3131+# kubectl create secret docker-registry registry-credentials \
3232+# --docker-server=ghcr.io \
3333+# --docker-username=YOUR_GITHUB_USERNAME \
3434+# --docker-password=YOUR_GITHUB_PAT \
3535+# --docker-email=YOUR_EMAIL
3636+#
3737+# For Google Container Registry (gcr.io):
3838+# kubectl create secret docker-registry registry-credentials \
3939+# --docker-server=gcr.io \
4040+# --docker-username=_json_key \
4141+# --docker-password="$(cat keyfile.json)" \
4242+# --docker-email=YOUR_EMAIL
4343+#
4444+# For Amazon ECR:
4545+# # First get ECR login token
4646+# ECR_TOKEN=$(aws ecr get-login-password --region us-east-1)
4747+# kubectl create secret docker-registry registry-credentials \
4848+# --docker-server=123456789012.dkr.ecr.us-east-1.amazonaws.com \
4949+# --docker-username=AWS \
5050+# --docker-password="$ECR_TOKEN" \
5151+# --docker-email=YOUR_EMAIL
5252+#
5353+# For a private registry:
5454+# kubectl create secret docker-registry registry-credentials \
5555+# --docker-server=registry.example.com \
5656+# --docker-username=YOUR_USERNAME \
5757+# --docker-password=YOUR_PASSWORD \
5858+# --docker-email=YOUR_EMAIL
5959+6060+---
6161+# Method 2: Create secret from YAML manifest
6262+# ===========================================
6363+#
6464+# First, create a Docker config file:
6565+#
6666+# {
6767+# "auths": {
6868+# "registry.example.com": {
6969+# "username": "YOUR_USERNAME",
7070+# "password": "YOUR_PASSWORD",
7171+# "email": "YOUR_EMAIL",
7272+# "auth": "BASE64_ENCODED_USERNAME:PASSWORD"
7373+# }
7474+# }
7575+# }
7676+#
7777+# To generate the "auth" field:
7878+# echo -n "YOUR_USERNAME:YOUR_PASSWORD" | base64
7979+#
8080+# Then base64 encode the entire JSON:
8181+# cat docker-config.json | base64 -w 0
8282+#
8383+# Finally, create the secret manifest:
8484+8585+apiVersion: v1
8686+kind: Secret
8787+metadata:
8888+ name: registry-credentials
8989+ namespace: default # Change to your namespace
9090+type: kubernetes.io/dockerconfigjson
9191+data:
9292+ # Replace this with your base64-encoded Docker config JSON
9393+ # Example format after base64 encoding:
9494+ .dockerconfigjson: ewoJImF1dGhzIjogewoJCSJyZWdpc3RyeS5leGFtcGxlLmNvbSI6IHsKCQkJInVzZXJuYW1lIjogIllPVVJfVVNFUk5BTUUiLAoJCQkicGFzc3dvcmQiOiAiWU9VUl9QQVNTV09SRCIsCgkJCSJlbWFpbCI6ICJZT1VSX0VNQUlMIiwKCQkJImF1dGgiOiAiV1U5VlVsOVZVMFZTVGtGTlJUcFpUMVZTWDFCQlUxTlhUMUpFIgoJCX0KCX0KfQo=
9595+9696+---
9797+# Method 3: Multiple registry support
9898+# ====================================
9999+#
100100+# You can configure credentials for multiple registries in a single secret:
101101+102102+apiVersion: v1
103103+kind: Secret
104104+metadata:
105105+ name: registry-credentials-multi
106106+ namespace: default
107107+type: kubernetes.io/dockerconfigjson
108108+stringData:
109109+ # Using stringData allows you to provide the JSON directly without base64 encoding
110110+ .dockerconfigjson: |
111111+ {
112112+ "auths": {
113113+ "docker.io": {
114114+ "username": "dockerhub-user",
115115+ "password": "dockerhub-token",
116116+ "auth": "ZG9ja2VyaHViLXVzZXI6ZG9ja2VyaHViLXRva2Vu"
117117+ },
118118+ "ghcr.io": {
119119+ "username": "github-user",
120120+ "password": "ghp_github_personal_access_token",
121121+ "auth": "Z2l0aHViLXVzZXI6Z2hwX2dpdGh1Yl9wZXJzb25hbF9hY2Nlc3NfdG9rZW4="
122122+ },
123123+ "registry.example.com": {
124124+ "username": "private-user",
125125+ "password": "private-password",
126126+ "auth": "cHJpdmF0ZS11c2VyOnByaXZhdGUtcGFzc3dvcmQ="
127127+ }
128128+ }
129129+ }
130130+131131+---
132132+# Verification
133133+# ============
134134+#
135135+# After creating the secret, verify it:
136136+# kubectl get secret registry-credentials -o yaml
137137+#
138138+# To decode and view the contents:
139139+# kubectl get secret registry-credentials -o jsonpath='{.data.\.dockerconfigjson}' | base64 -d | jq
140140+#
141141+# Test buildah authentication in a pod:
142142+# kubectl run -it --rm buildah-test \
143143+# --image=quay.io/buildah/stable:latest \
144144+# --overrides='{"spec":{"volumes":[{"name":"docker-config","secret":{"secretName":"registry-credentials"}}],"containers":[{"name":"buildah","image":"quay.io/buildah/stable:latest","command":["sh"],"volumeMounts":[{"name":"docker-config","mountPath":"/home/user/.docker","readOnly":true}]}]}}' \
145145+# -- sh
146146+#
147147+# # Inside the pod:
148148+# buildah login --get-login registry.example.com
+76
config/samples/workflow-buildah-example.yaml
···11+# Example workflow demonstrating buildah usage in Loom
22+#
33+# This workflow shows how to:
44+# 1. Run tests in a standard container image
55+# 2. Build an application binary
66+# 3. Build a container image using buildah
77+# 4. Push the image to a registry
88+#
99+# Prerequisites:
1010+# - Create a registry credentials secret (see registry-secret-example.yaml)
1111+# - Configure the secret in your SpindleSet template:
1212+# template:
1313+# registryCredentialsSecret: registry-credentials
1414+1515+# Workflow metadata
1616+name: build-and-push.yaml
1717+image: golang:1.24-bookworm
1818+architecture: amd64
1919+2020+# Workflow steps
2121+steps:
2222+ # Step 1: Run tests
2323+ - name: Run tests
2424+ command: |
2525+ echo "Running Go tests..."
2626+ go test -v ./...
2727+2828+ # Step 2: Build application binary
2929+ - name: Build application
3030+ command: |
3131+ echo "Building application binary..."
3232+ CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o /tangled/workspace/app ./cmd
3333+3434+ # Step 3: Create Dockerfile
3535+ - name: Create Dockerfile
3636+ command: |
3737+ cat > /tangled/workspace/Dockerfile <<'EOF'
3838+ FROM alpine:latest
3939+ RUN apk add --no-cache ca-certificates
4040+ COPY app /usr/local/bin/app
4141+ ENTRYPOINT ["/usr/local/bin/app"]
4242+ EOF
4343+ echo "Dockerfile created"
4444+4545+ # Step 4: Build container image with buildah
4646+ - name: Build container image
4747+ command: |
4848+ echo "Building container image with buildah..."
4949+ cd /tangled/workspace
5050+5151+ # Build the image (buildah is in PATH via /runner-bin)
5252+ buildah bud \
5353+ --storage-driver=overlay \
5454+ --tag registry.example.com/myapp:${TANGLED_COMMIT_SHA} \
5555+ --tag registry.example.com/myapp:latest \
5656+ .
5757+5858+ echo "Container image built successfully"
5959+6060+ # Step 5: Push image to registry
6161+ - name: Push container image
6262+ command: |
6363+ echo "Pushing image to registry..."
6464+6565+ # Push both tags (buildah automatically uses credentials from /home/user/.docker/config.json)
6666+ buildah push \
6767+ --storage-driver=overlay \
6868+ registry.example.com/myapp:${TANGLED_COMMIT_SHA}
6969+7070+ buildah push \
7171+ --storage-driver=overlay \
7272+ registry.example.com/myapp:latest
7373+7474+ echo "Image pushed successfully"
7575+ echo "Image: registry.example.com/myapp:${TANGLED_COMMIT_SHA}"
7676+ echo "Image: registry.example.com/myapp:latest"