evan.jarrett.net / loom
Kubernetes Operator for Tangled Spindles
Pull this image
docker pull atcr.io/evan.jarrett.net/loom:latest
Overview
Loom
Loom is a Kubernetes operator that runs CI/CD pipeline workflows from tangled.org. It creates ephemeral Jobs in response to events (pushes, pull requests) and streams logs back to the tangled.org platform.
Architecture
┌─────────────────────────────────────────────────────────────┐
│ Loom Operator Pod │
│ │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ Controller Manager │ │
│ │ - Watches SpindleSet CRD │ │
│ │ - Creates/monitors Kubernetes Jobs │ │
│ └────────────────────────────────────────────────────────┘ │
│ │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ Embedded Spindle Server │ │
│ │ - WebSocket connection to tangled.org knots │ │
│ │ - Database, queue, secrets vault │ │
│ │ - KubernetesEngine (creates Jobs) │ │
│ └────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
│
│ creates
▼
┌───────────────────────────────┐
│ Kubernetes Job (per workflow) │
│ │
│ Init: setup-user, clone-repo │
│ Main: runner binary + image │
└───────────────────────────────┘
Components
Controller (cmd/controller) - The Kubernetes operator that:
- Connects to tangled.org knots via WebSocket to receive pipeline events
- Creates
SpindleSetcustom resources for each pipeline run - Reconciles SpindleSets into Kubernetes Jobs
- Manages secrets injection and cleanup
Runner (cmd/runner) - A lightweight binary injected into workflow pods that:
- Executes workflow steps sequentially
- Emits structured JSON log events for real-time status updates
- Handles step-level environment variable injection
How It Works
- A push or PR event triggers a pipeline on tangled.org
- Loom receives the event via WebSocket and parses the workflow YAML
- A
SpindleSetCR is created with the pipeline specification - The controller creates a Kubernetes Job with:
- Init containers for user setup and repository cloning
- The runner binary injected via shared volume
- The user’s workflow image as the main container
- The runner executes steps and streams logs back to the controller
- On completion, the SpindleSet and its resources are cleaned up
Features
- Multi-architecture support: Schedule workflows on amd64 or arm64 nodes
- Rootless container builds: Buildah support with user namespace configuration
- Secret management: Repository secrets injected as environment variables with log masking
- Resource profiles: Configure CPU/memory based on node labels
- Automatic cleanup: TTL-based Job cleanup and orphan detection
Configuration
Loom ConfigMap
Loom is configured via a ConfigMap mounted at /etc/loom/config.yaml:
maxConcurrentJobs: 10
template:
resourceProfiles:
- nodeSelector:
kubernetes.io/arch: amd64
resources:
requests:
cpu: "500m"
memory: "1Gi"
limits:
cpu: "2"
memory: "4Gi"
- nodeSelector:
kubernetes.io/arch: arm64
resources:
requests:
cpu: "500m"
memory: "1Gi"
limits:
cpu: "2"
memory: "4Gi"
Spindle Environment Variables
The embedded spindle server is configured via environment variables:
| Variable | Required | Description |
|---|---|---|
SPINDLE_SERVER_LISTEN_ADDR |
Yes | HTTP server address (e.g., 0.0.0.0:6555) |
SPINDLE_SERVER_DB_PATH |
Yes | SQLite database path |
SPINDLE_SERVER_HOSTNAME |
Yes | Hostname for spindle DID |
SPINDLE_SERVER_OWNER |
Yes | Owner DID (e.g., did:web:example.com) |
SPINDLE_SERVER_JETSTREAM_ENDPOINT |
Yes | Bluesky jetstream WebSocket URL |
SPINDLE_SERVER_MAX_JOB_COUNT |
No | Max concurrent workflows (default: 2) |
SPINDLE_SERVER_SECRETS_PROVIDER |
No | sqlite or openbao (default: sqlite) |
Workflow Format
Workflows are defined in .tangled/workflows/*.yaml in your repository:
image: golang:1.24
architecture: amd64
steps:
- name: Build
command: go build ./...
- name: Test
command: go test ./...
Security
Job Pod Security
Jobs run with hardened security contexts:
- Non-root user (UID 1000)
- Minimal capabilities (only SETUID/SETGID for buildah)
- No service account token mounting
- Unconfined seccomp (required for buildah user namespaces)
Secrets
Repository secrets are:
- Stored in the spindle vault (SQLite or OpenBao)
- Injected as environment variables via Kubernetes Secrets
- Masked in log output
Prerequisites
- go version v1.24.0+
- docker version 17.03+
- kubectl version v1.11.3+
- Access to a Kubernetes v1.11.3+ cluster
Deployment
Build and push the image:
make docker-build docker-push IMG=<registry>/loom:tag
Install the CRDs:
make install
Deploy the controller:
make deploy IMG=<registry>/loom:tag
Development
Generate CRDs and code:
make manifests generate
Run tests:
make test
Run locally (for debugging):
make install run
Uninstall
kubectl delete -k config/samples/
make uninstall
make undeploy
License
Copyright 2025 Evan Jarrett.
Licensed under the Apache License, Version 2.0.
Tags
sha256:081a2ccf132d9f93636fac2d65de035d8c48e02f5326776d3536cb67a7c053bd
docker pull atcr.io/evan.jarrett.net/loom:latest
sha256:68b8c226c28ee020b9499b08eae52e3e0677b2e54c283fbdfd832058834b85d9
helm pull oci://atcr.io/evan.jarrett.net/loom --version 0.0.1
Manifests
sha256:081a2ccf132d9f93636fac2d65de035d8c48e02f5326776d3536cb67a7c053bd
sha256:68b8c226c28ee020b9499b08eae52e3e0677b2e54c283fbdfd832058834b85d9
sha256:7f2e5dd0e80941de4fab6b931071ff2e21b6276e06a85a00ac253e5675c105f2
sha256:183edc460df070c34234d3364be3e97ce65eab0fbb7e62f17db4e6440d93e8b5
sha256:7fffee176c182955702bfaeacf60e8fcfdc115cbaac0581f2233307d3b336cfb
sha256:01e4dcde65ace2e9efe54a33968aedc7826b0e733583d70519dd89e214b6068b
sha256:9ec4ec6afd9c8ca02153b9304369c8245cdfcfa4751c12fc628a229e0cbf387f
sha256:585fb9d2e1b4534e51b54917d0e5fb2df5d0db1254cc874ea61ae1e420752251
sha256:f250be920c60dcad0021955bada21aef9dd5b709fab4aebac76ad140294fc57d
sha256:762be9ac6925e2c5288d5a1b563978921d5abc439616b8efbb7755d48eed8fc3
sha256:14f3fd5c75769923c599c309b795cf81cfeba09b35f2e0cf5ef1d964866d67df
sha256:d7e4651606a365cba71fb429814931afb4b33682aaae18975ccd9914a9e3c90c
sha256:f0d9b92ce03e0245ab2fb1c934dd6e44ad64f12b3b1b26da3dec8b58ca5bc41b
sha256:6198fb4b55f971ff382788359c5b61a6864b04a56230fe1ca2ed511f943d4786
sha256:5ebd285ac309cd13b819c0635ab6893fda8238b327e18eedc1356cc4277152e9
sha256:79b3921c04b02e954a4f4ff38b84475011ac708b92e857746c5995142d038357
sha256:4d7137c7d9e210532ae0f6e3c8a551e24a497ee4901b1c19f4132640fabcb414
sha256:d907549b28a20dc198103adc8d40dddba57c7dca199a344595644e5ca7e85cff
sha256:8b8cb50571eda8f31b9e8e021565b9ea255d04feffa6d7bb82beb82462ff8bbf
sha256:e76e5c9aae3a720ff890cb127160a4358b14d6b66f8561442c3612b3fdf27d66