# Loom Loom 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. ## 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`: ```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: ```yaml 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: ```sh make docker-build docker-push IMG=/loom:tag ``` Install the CRDs: ```sh make install ``` Deploy the controller: ```sh make deploy IMG=/loom:tag ``` ## Development Generate CRDs and code: ```sh make manifests generate ``` Run tests: ```sh make test ``` Run locally (for debugging): ```sh make install run ``` ## Uninstall ```sh kubectl delete -k config/samples/ make uninstall make undeploy ``` ## License Copyright 2025 Evan Jarrett. Licensed under the Apache License, Version 2.0.