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:f3cd2e6fe83b7cf3e1a9945d89fefd50b24cde923cf5313ef6cd691d0df4defe
sha256:abbcf87344eaf7751067f1df58f93697cedbfc1b59b1f3f638a2d1eaa3a69d27
sha256:30d7f33c7f15ff3c6a1e4302575dcebfe31f3e2403486a2902a6bf30d44c7cdd
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:f3cd2e6fe83b7cf3e1a9945d89fefd50b24cde923cf5313ef6cd691d0df4defe
sha256:e67e0ec6b6d928ca2f37e34c18187d580e1e7ae5f6af6ada9e3cd2c428634d1e
sha256:c87962a2158736d73f1c5dd56ffc3f2999fa8051885eec249c52909e04407345
sha256:5d24aef68a6af64c43c3c8c6acd070f7252673b6c2af86908629fc70eb01277e
sha256:84506d70d5d64a286589b1e5971731738a676a4170c3a831705b9a523e75b588
sha256:28c754fb4d9d8b72d39f9c7c47f0d0b43e0f1873a3333acf5534312eef18207d
sha256:b8b87dfd4fa663bb8cddd231eb646a0a911de1722986bddbab7167732fc1f5a8
sha256:d52b0e675eaf78e285a671542a0715dc869edace2f8c98c9cd11bfd279c86db3
sha256:f38f338c333420cc5551f9fd44861fde8427be0bdce0684d1f26b0cc29996f78
sha256:44e492d1470b7fcbc97c613e2ee46bdf53265f39fe4502bb62244d5eaa0f0934
sha256:b9c73d6411221131b0fa5ae517e8ed31182794e5f9387367906e031e4d896a74
sha256:445fb73cfe4cec6f262acc341a1ec254ef965a2cf36ab1257d3e63d48c0d716a
sha256:cf1bd42ee990029b14453bb29b7312e09e40ff4b73c78c9d590166308794e4cf
sha256:500736528469921242eb1cbc5051889a74c3554cecf6c706beb4f514d8e250ac
sha256:0a30747ce66f090ae77cc55d502b5a9be29373e0ef9d5d9b87cc0abe6dddc829
sha256:02d9fec77f0e9edbbcdf3a9ac79868084cd71afe10913b4b4732ea2bc0ca8dec
sha256:5386b5222330a12e2de06d55d11a2be9965bf737aed66629ab0e2885fca76fd1
sha256:a65d6060b8ba7e87edc89a58d5837f8b5c924d17bddf74e8f78daa7fa1a50b40
sha256:c49458078a926023d6b829c65593bb649efe28e259bffd66b2166d2e8bdeb3bd
sha256:1ec11ca72d8624b132dfa88082c0c6c84f48d0c83ab473d72496cc6441b07875
sha256:8e84095186a20fe0a2d7d27481e54281c8b8f36884a4ac03eded8c252492e2fc
sha256:df241c46bc6c0bc2e1d222f35da8aeaeb797dc52f6bc3b6538c0f6bf1d3fa277
sha256:6b9b9e569c44a16d81be500774429bd4bca87765630876be6210472abf26642e
sha256:2cafd46e4fdebcd402e3ce767f82d34ce6ce2e1db69af0fc76f25252328739e5
sha256:af32cac63af36a7dd37a16c066432f190c379ef5a9f6786c6002ad134526f1c5
sha256:2c108c837dec9d69f76467e9712e880e0b44f38756b6ec94f8ac0bec1edf93a9
sha256:95e6aea3f95c8dbc39039d01d8301939ff65deee247cfdb7692ba76f4ce1fb71
sha256:081905be75a99f5bc4c8501450ed452548ae69d42a506c2477053de77509cb71
sha256:17a06fab2909100ff48700479cec3f2705031c88944ee575d05626402d967236
sha256:f46bcd23277d1498119b93876b5bbf9cde48efc2c7d06483ade41382cc79d78d
sha256:67b05e5db1e2a409dacd16fbefb9a156c99e1e700fb87bb1a8490c92fde37136
sha256:e8e88fd92619009e6fdd3dd0138c80e9e646b2fe1b569b2d9a82ca9f4c1d5018
sha256:2a48ca10633ba85d0bdebf6674d6ea03c9cdd6d93caa691f3efc3599638604cb
sha256:9bc88a8e420f74bab2d132036b1e5aeb10686df1b8017cc668cf0ab70456a2cb
sha256:39c4f9fb11b9434e5dccfbdd03757d0e02442e618470359e179cfc0632ec1867
sha256:4e3757942ec9dd1b9e792131dc8a0ec891ecbb0db4ceb5abc00990040be04e1d
sha256:f1dab2033bb4616aca33850f871228924fbd993c6a03e80b91471d175b249185
sha256:92a49d5cc60613f4b5df25f7454026c66bd9da63081de7304448dbfeac8f4978
sha256:92d3079779e76cf0f7ff4159040c790347087a28f362df171467d81ff17f705a
sha256:76dd34caa723f82710fe6698ff3b71bc5c7b4f5c475dd0f0c8a4800a6c9324ca
sha256:7aa7b64a358ff1c2555a2f05e235b6e4684b9548bf76e869ceb45568b125e379
sha256:68b8c226c28ee020b9499b08eae52e3e0677b2e54c283fbdfd832058834b85d9