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:262f9f77c73ee7c182c0b9edcfc6d1b0eb8311b52725c4c3d80fe3b1b4e6ab9a
sha256:7aa7b64a358ff1c2555a2f05e235b6e4684b9548bf76e869ceb45568b125e379
sha256:76dd34caa723f82710fe6698ff3b71bc5c7b4f5c475dd0f0c8a4800a6c9324ca
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:262f9f77c73ee7c182c0b9edcfc6d1b0eb8311b52725c4c3d80fe3b1b4e6ab9a
sha256:081a2ccf132d9f93636fac2d65de035d8c48e02f5326776d3536cb67a7c053bd
sha256:68b8c226c28ee020b9499b08eae52e3e0677b2e54c283fbdfd832058834b85d9
sha256:7f2e5dd0e80941de4fab6b931071ff2e21b6276e06a85a00ac253e5675c105f2
sha256:183edc460df070c34234d3364be3e97ce65eab0fbb7e62f17db4e6440d93e8b5
sha256:7fffee176c182955702bfaeacf60e8fcfdc115cbaac0581f2233307d3b336cfb
sha256:01e4dcde65ace2e9efe54a33968aedc7826b0e733583d70519dd89e214b6068b