Kubernetes Operator for Tangled Spindles
1# Loom
2
3Loom is a Kubernetes operator that runs CI/CD pipeline workflows from [tangled.org](https://tangled.org). It creates ephemeral Jobs in response to events (pushes, pull requests) and streams logs back to the tangled.org platform.
4
5## Architecture
6
7```
8┌─────────────────────────────────────────────────────────────┐
9│ Loom Operator Pod │
10│ │
11│ ┌────────────────────────────────────────────────────────┐ │
12│ │ Controller Manager │ │
13│ │ - Watches SpindleSet CRD │ │
14│ │ - Creates/monitors Kubernetes Jobs │ │
15│ └────────────────────────────────────────────────────────┘ │
16│ │
17│ ┌────────────────────────────────────────────────────────┐ │
18│ │ Embedded Spindle Server │ │
19│ │ - WebSocket connection to tangled.org knots │ │
20│ │ - Database, queue, secrets vault │ │
21│ │ - KubernetesEngine (creates Jobs) │ │
22│ └────────────────────────────────────────────────────────┘ │
23└─────────────────────────────────────────────────────────────┘
24 │
25 │ creates
26 ▼
27 ┌───────────────────────────────┐
28 │ Kubernetes Job (per workflow) │
29 │ │
30 │ Init: setup-user, clone-repo │
31 │ Main: runner binary + image │
32 └───────────────────────────────┘
33```
34
35### Components
36
37**Controller (`cmd/controller`)** - The Kubernetes operator that:
38- Connects to tangled.org knots via WebSocket to receive pipeline events
39- Creates `SpindleSet` custom resources for each pipeline run
40- Reconciles SpindleSets into Kubernetes Jobs
41- Manages secrets injection and cleanup
42
43**Runner (`cmd/runner`)** - A lightweight binary injected into workflow pods that:
44- Executes workflow steps sequentially
45- Emits structured JSON log events for real-time status updates
46- Handles step-level environment variable injection
47
48## How It Works
49
501. A push or PR event triggers a pipeline on tangled.org
512. Loom receives the event via WebSocket and parses the workflow YAML
523. A `SpindleSet` CR is created with the pipeline specification
534. The controller creates a Kubernetes Job with:
54 - Init containers for user setup and repository cloning
55 - The runner binary injected via shared volume
56 - The user's workflow image as the main container
575. The runner executes steps and streams logs back to the controller
586. On completion, the SpindleSet and its resources are cleaned up
59
60## Features
61
62- **Multi-architecture support**: Schedule workflows on amd64 or arm64 nodes
63- **Rootless container builds**: Buildah support with user namespace configuration
64- **Secret management**: Repository secrets injected as environment variables with log masking
65- **Resource profiles**: Configure CPU/memory based on node labels
66- **Automatic cleanup**: TTL-based Job cleanup and orphan detection
67
68## Configuration
69
70### Loom ConfigMap
71
72Loom is configured via a ConfigMap mounted at `/etc/loom/config.yaml`:
73
74```yaml
75maxConcurrentJobs: 10
76template:
77 resourceProfiles:
78 - nodeSelector:
79 kubernetes.io/arch: amd64
80 resources:
81 requests:
82 cpu: "500m"
83 memory: "1Gi"
84 limits:
85 cpu: "2"
86 memory: "4Gi"
87 - nodeSelector:
88 kubernetes.io/arch: arm64
89 resources:
90 requests:
91 cpu: "500m"
92 memory: "1Gi"
93 limits:
94 cpu: "2"
95 memory: "4Gi"
96```
97
98### Spindle Environment Variables
99
100The embedded spindle server is configured via environment variables:
101
102| Variable | Required | Description |
103|----------|----------|-------------|
104| `SPINDLE_SERVER_LISTEN_ADDR` | Yes | HTTP server address (e.g., `0.0.0.0:6555`) |
105| `SPINDLE_SERVER_DB_PATH` | Yes | SQLite database path |
106| `SPINDLE_SERVER_HOSTNAME` | Yes | Hostname for spindle DID |
107| `SPINDLE_SERVER_OWNER` | Yes | Owner DID (e.g., `did:web:example.com`) |
108| `SPINDLE_SERVER_JETSTREAM_ENDPOINT` | Yes | Bluesky jetstream WebSocket URL |
109| `SPINDLE_SERVER_MAX_JOB_COUNT` | No | Max concurrent workflows (default: 2) |
110| `SPINDLE_SERVER_SECRETS_PROVIDER` | No | `sqlite` or `openbao` (default: sqlite) |
111
112## Workflow Format
113
114Workflows are defined in `.tangled/workflows/*.yaml` in your repository:
115
116```yaml
117image: golang:1.24
118architecture: amd64
119
120steps:
121 - name: Build
122 command: go build ./...
123
124 - name: Test
125 command: go test ./...
126```
127
128## Security
129
130### Job Pod Security
131
132Jobs run with hardened security contexts:
133- Non-root user (UID 1000)
134- Minimal capabilities (only SETUID/SETGID for buildah)
135- No service account token mounting
136- Unconfined seccomp (required for buildah user namespaces)
137
138### Secrets
139
140Repository secrets are:
141- Stored in the spindle vault (SQLite or OpenBao)
142- Injected as environment variables via Kubernetes Secrets
143- Masked in log output
144
145## Prerequisites
146
147- go version v1.24.0+
148- docker version 17.03+
149- kubectl version v1.11.3+
150- Access to a Kubernetes v1.11.3+ cluster
151
152## Deployment
153
154Build and push the image:
155
156```sh
157make docker-build docker-push IMG=<registry>/loom:tag
158```
159
160Install the CRDs:
161
162```sh
163make install
164```
165
166Deploy the controller:
167
168```sh
169make deploy IMG=<registry>/loom:tag
170```
171
172## Development
173
174Generate CRDs and code:
175
176```sh
177make manifests generate
178```
179
180Run tests:
181
182```sh
183make test
184```
185
186Run locally (for debugging):
187
188```sh
189make install run
190```
191
192## Uninstall
193
194```sh
195kubectl delete -k config/samples/
196make uninstall
197make undeploy
198```
199
200## License
201
202Copyright 2025 Evan Jarrett.
203
204Licensed under the Apache License, Version 2.0.