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.