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
sha256:262f9f77c73ee7c182c0b9edcfc6d1b0eb8311b52725c4c3d80fe3b1b4e6ab9a
linux/amd64 sha256:7aa7b64a358ff1c2555a2f05e235b6e4684b9548bf76e869ceb45568b125e379
linux/arm64 sha256:76dd34caa723f82710fe6698ff3b71bc5c7b4f5c475dd0f0c8a4800a6c9324ca
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
sha256:262f9f77c73ee7c182c0b9edcfc6d1b0eb8311b52725c4c3d80fe3b1b4e6ab9a
Tags: latest
linux/amd64 linux/arm64
Multi-arch
sha256:081a2ccf132d9f93636fac2d65de035d8c48e02f5326776d3536cb67a7c053bd
(untagged)
linux/amd64 linux/arm64
Helm Chart
sha256:68b8c226c28ee020b9499b08eae52e3e0677b2e54c283fbdfd832058834b85d9
Tags: 0.0.1
Multi-arch
sha256:7f2e5dd0e80941de4fab6b931071ff2e21b6276e06a85a00ac253e5675c105f2
(untagged)
linux/amd64 linux/arm64
Multi-arch
sha256:183edc460df070c34234d3364be3e97ce65eab0fbb7e62f17db4e6440d93e8b5
(untagged)
linux/amd64 linux/arm64
Multi-arch
sha256:7fffee176c182955702bfaeacf60e8fcfdc115cbaac0581f2233307d3b336cfb
(untagged)
linux/amd64 linux/arm64
Multi-arch
sha256:01e4dcde65ace2e9efe54a33968aedc7826b0e733583d70519dd89e214b6068b
(untagged)
linux/amd64 linux/arm64