loom

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 SpindleSet custom 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

  1. A push or PR event triggers a pipeline on tangled.org
  2. Loom receives the event via WebSocket and parses the workflow YAML
  3. A SpindleSet CR is created with the pipeline specification
  4. 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
  5. The runner executes steps and streams logs back to the controller
  6. 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

latest Multi-arch Attestations
sha256:081a2ccf132d9f93636fac2d65de035d8c48e02f5326776d3536cb67a7c053bd
linux/amd64 linux/arm64
docker pull atcr.io/evan.jarrett.net/loom:latest
0.0.1 Helm chart
sha256:68b8c226c28ee020b9499b08eae52e3e0677b2e54c283fbdfd832058834b85d9
helm pull oci://atcr.io/evan.jarrett.net/loom --version 0.0.1

Manifests

Multi-arch Attestations
sha256:081a2ccf132d9f93636fac2d65de035d8c48e02f5326776d3536cb67a7c053bd
Tags: latest
linux/amd64 linux/arm64
Helm Chart
sha256:68b8c226c28ee020b9499b08eae52e3e0677b2e54c283fbdfd832058834b85d9
Tags: 0.0.1
Multi-arch Attestations
sha256:7f2e5dd0e80941de4fab6b931071ff2e21b6276e06a85a00ac253e5675c105f2
(untagged)
linux/amd64 linux/arm64
Multi-arch Attestations
sha256:183edc460df070c34234d3364be3e97ce65eab0fbb7e62f17db4e6440d93e8b5
(untagged)
linux/amd64 linux/arm64
Multi-arch Attestations
sha256:7fffee176c182955702bfaeacf60e8fcfdc115cbaac0581f2233307d3b336cfb
(untagged)
linux/amd64 linux/arm64
Multi-arch Attestations
sha256:01e4dcde65ace2e9efe54a33968aedc7826b0e733583d70519dd89e214b6068b
(untagged)
linux/amd64 linux/arm64
Image
sha256:9ec4ec6afd9c8ca02153b9304369c8245cdfcfa4751c12fc628a229e0cbf387f
(untagged)
Image
sha256:585fb9d2e1b4534e51b54917d0e5fb2df5d0db1254cc874ea61ae1e420752251
(untagged)
Image
sha256:f250be920c60dcad0021955bada21aef9dd5b709fab4aebac76ad140294fc57d
(untagged)
Image
sha256:762be9ac6925e2c5288d5a1b563978921d5abc439616b8efbb7755d48eed8fc3
(untagged)
Image
sha256:14f3fd5c75769923c599c309b795cf81cfeba09b35f2e0cf5ef1d964866d67df
(untagged)
Image
sha256:d7e4651606a365cba71fb429814931afb4b33682aaae18975ccd9914a9e3c90c
(untagged)
Image
sha256:f0d9b92ce03e0245ab2fb1c934dd6e44ad64f12b3b1b26da3dec8b58ca5bc41b
(untagged)
Image
sha256:6198fb4b55f971ff382788359c5b61a6864b04a56230fe1ca2ed511f943d4786
(untagged)
Image
sha256:5ebd285ac309cd13b819c0635ab6893fda8238b327e18eedc1356cc4277152e9
(untagged)
Image
sha256:79b3921c04b02e954a4f4ff38b84475011ac708b92e857746c5995142d038357
(untagged)
Image
sha256:4d7137c7d9e210532ae0f6e3c8a551e24a497ee4901b1c19f4132640fabcb414
(untagged)
Image
sha256:d907549b28a20dc198103adc8d40dddba57c7dca199a344595644e5ca7e85cff
(untagged)
Image
sha256:8b8cb50571eda8f31b9e8e021565b9ea255d04feffa6d7bb82beb82462ff8bbf
(untagged)
Image
sha256:e76e5c9aae3a720ff890cb127160a4358b14d6b66f8561442c3612b3fdf27d66
(untagged)