Assay
Lightweight Lua runtime for Kubernetes. Verification, scripting, and web services.
What is Assay?
Assay is a single ~9 MB binary that replaces 50-250 MB Python/Node/kubectl containers in Kubernetes Jobs. It provides a full-featured Lua runtime with built-in HTTP client/server, database access, WebSocket, JWT signing, templates, and 19 embedded Kubernetes-native libraries.
One binary, auto-detected behavior:
Scripts that call http.serve() become web services. Scripts that call http.get() and exit are
jobs. Same binary, same builtins.
Why Assay?
Container image size comparison (compressed pull):
+------------------------------------------------------------------+
| Docker image size comparison (compressed pull) |
| |
| Assay Full ## 6 MB |
| Python alpine ########## 17 MB |
| bitnami/kubectl #################### 35 MB |
| Python slim ########################## 43 MB |
| Node.js alpine ################################## 57 MB |
| alpine/k8s ######################################## 60 MB |
| Deno ############################################ 75 |
| Node.js slim ############################################### |
| Bun ############################################### |
| postman/newman ############################################### |
+------------------------------------------------------------------+
| Runtime | Compressed | On-disk | vs Assay | Sandbox | K8s-native |
|---|---|---|---|---|---|
| Assay | 6 MB | 13 MB | 1x | Yes | Yes |
| Python alpine | 17 MB | 50 MB | 3x | No | No |
| bitnami/kubectl | 35 MB | 90 MB | 6x | No | Partial |
| Python slim | 43 MB | 130 MB | 9x | No | No |
| Node.js alpine | 57 MB | 180 MB | 12x | No | No |
| alpine/k8s | 60 MB | 150 MB | 10x | No | Partial |
Installation
Pre-built Binary (fastest)
Download from GitHub Releases:
# Linux (x86_64, static — runs on any distro, no dependencies)
# macOS (Apple Silicon)
Docker
Cargo
From Source
Usage
Two Modes
Assay auto-detects behavior by file extension:
1. YAML Check Mode (Orchestration)
Run structured verification checks with retry, backoff, and JSON output:
# checks.yaml
timeout: 120s
retries: 3
backoff: 5s
parallel: false
checks:
- name: grafana-healthy
type: http
url: http://grafana.monitoring:80/api/health
expect:
status: 200
json: ".database == \"ok\""
- name: prometheus-targets
type: prometheus
url: http://prometheus.monitoring:9090
query: "count(up)"
expect:
min: 1
- name: custom-check
type: script
file: verify.lua
2. Lua Script Mode (Direct Execution)
Run Lua scripts with all builtins available:
#!/usr/bin/assay
-- HTTP health check with JWT auth
local token = crypto.
local resp = http.
assert.
log.
Shebang Support
Assay supports shebang for executable Lua scripts:
#!/usr/bin/assay
log.
Built-in API Reference
All builtins are available to .lua scripts. YAML check mode uses a sandboxed subset (http, json,
yaml, assert, log, env, sleep, time, base64).
HTTP Client
| Function | Description |
|---|---|
http.get(url, opts?) |
GET request, returns {status, body, ...} |
http.post(url, body, opts?) |
POST request (auto-JSON if table) |
http.put(url, body, opts?) |
PUT request |
http.patch(url, body, opts?) |
PATCH request |
http.delete(url, opts?) |
DELETE request |
http.serve(port, routes) |
Start HTTP server (blocking) |
opts.headers = {["X-Key"] = "value"} |
Custom headers |
routes = {GET = {["/path"] = handler}} |
Route table for server |
Serialization
| Function | Description |
|---|---|
json.parse(str) |
Parse JSON string to Lua table |
json.encode(table) |
Encode Lua table to JSON string |
yaml.parse(str) |
Parse YAML string to Lua table |
yaml.encode(table) |
Encode Lua table to YAML string |
toml.parse(str) |
Parse TOML string to Lua table |
toml.encode(table) |
Encode Lua table to TOML string |
base64.encode(str) |
Base64 encode |
base64.decode(str) |
Base64 decode |
Filesystem
| Function | Description |
|---|---|
fs.read(path) |
Read file to string |
fs.write(path, s) |
Write string to file |
Cryptography
| Function | Description |
|---|---|
crypto.jwt_sign(claims, key, alg) |
Sign JWT (HS256/384/512, RS256/384/512) |
crypto.hash(str, alg) |
Hash string (sha256, sha384, sha512, etc.) |
crypto.random(len) |
Secure random string (hex) |
Regular Expressions
| Function | Description |
|---|---|
regex.match(pattern, str) |
Test if pattern matches |
regex.find(pattern, str) |
Find first match |
regex.find_all(pattern, str) |
Find all matches |
regex.replace(pattern, str, r) |
Replace matches |
Database (SQL)
| Function | Description |
|---|---|
db.connect(url) |
Connect to database (Postgres/MySQL/SQLite) |
db.query(conn, sql, params?) |
Execute query, return rows |
db.execute(conn, sql, params?) |
Execute statement, return affected count |
db.close(conn) |
Close connection |
Supported URLs:
postgres://user:pass@host:5432/dbnamemysql://user:pass@host:3306/dbnamesqlite:///path/to/file.db
WebSocket
| Function | Description |
|---|---|
ws.connect(url) |
Connect to WebSocket server |
ws.send(conn, msg) |
Send message |
ws.recv(conn) |
Receive message (blocking) |
ws.close(conn) |
Close connection |
Templates (Jinja2-compatible)
| Function | Description |
|---|---|
template.render(path, vars) |
Render template file |
template.render_string(tmpl, v) |
Render template string |
Async
| Function | Description |
|---|---|
async.spawn(fn) |
Spawn async task, returns handle |
async.spawn_interval(fn, ms) |
Spawn recurring task, returns handle |
handle:await() |
Wait for task completion |
handle:cancel() |
Cancel recurring task |
Assertions
| Function | Description |
|---|---|
assert.eq(a, b, msg?) |
Assert equal |
assert.gt(a, b, msg?) |
Assert greater than |
assert.lt(a, b, msg?) |
Assert less than |
assert.contains(str, sub, msg?) |
Assert substring |
assert.not_nil(val, msg?) |
Assert not nil |
assert.matches(str, pat, msg?) |
Assert regex match |
Logging
| Function | Description |
|---|---|
log.info(msg) |
Info log |
log.warn(msg) |
Warning log |
log.error(msg) |
Error log |
Utilities
| Function | Description |
|---|---|
env.get(key) |
Get environment variable |
sleep(secs) |
Sleep for seconds |
time() |
Unix timestamp (seconds) |
Stdlib Modules
Assay embeds 19 Lua modules for Kubernetes-native operations. Use require("assay.<module>"):
| Module | Description |
|---|---|
assay.prometheus |
Query metrics, alerts, targets, rules, label values, series |
assay.alertmanager |
Manage alerts, silences, receivers, config |
assay.loki |
Push logs, query, labels, series |
assay.grafana |
Health checks, dashboards, datasources |
assay.k8s |
30+ resource types, CRDs, readiness checks |
assay.argocd |
Apps, sync, health, projects, repositories |
assay.kargo |
Stages, freight, promotions, verification |
assay.flux |
GitRepositories, Kustomizations, HelmReleases |
assay.traefik |
Routers, services, middlewares, entrypoints |
assay.vault |
KV secrets, policies, auth, transit, PKI |
assay.openbao |
Alias for vault (OpenBao API-compatible) |
assay.certmanager |
Certificates, issuers, ACME challenges |
assay.eso |
ExternalSecrets, SecretStores, ClusterSecretStores |
assay.dex |
OIDC discovery, JWKS, health |
assay.crossplane |
Providers, XRDs, compositions, managed resources |
assay.velero |
Backups, restores, schedules, storage locations |
assay.temporal |
Workflows, task queues, schedules |
assay.harbor |
Projects, repositories, artifacts, vulnerability scanning |
assay.healthcheck |
HTTP checks, JSON path, body matching, latency, multi-check |
Example:
local prom = require
local result = prom.
log.
Examples
HTTP Health Check
#!/usr/bin/assay
local resp = http.
assert.
local data = json.
assert.
log.
JWT Authentication to API
#!/usr/bin/assay
-- Read RSA private key from file
local key = fs.
-- Sign JWT with RS256
local token = crypto.
-- Call API with JWT
local resp = http.
assert.
local users = json.
log.
Database Query
#!/usr/bin/assay
local pg = db.
-- Parameterized query (safe from SQL injection)
local rows = db.
for _, row in ipairs
db.
Web Server
#!/usr/bin/assay
-- Simple API server
http.
Prometheus Verification
#!/usr/bin/assay
local prom = require
-- Check Prometheus is up
local targets = prom.
local up_count = 0
for _, target in ipairs
assert.
log.
-- Query metrics
local result = prom.
log.
YAML Check Mode
YAML check mode provides structured orchestration with retry, backoff, and parallel execution:
# Global config
timeout: 120s # Max time for all checks
retries: 3 # Retry failed checks
backoff: 5s # Wait between retries
parallel: false # Run checks sequentially (true = parallel)
checks:
# HTTP check with JSON path assertion
- name: api-health
type: http
url: https://api.example.com/health
expect:
status: 200
json: ".status == \"healthy\""
# Prometheus query check
- name: high-cpu
type: prometheus
url: http://prometheus:9090
query: "avg(rate(cpu_usage[5m]))"
expect:
max: 0.8 # Alert if CPU > 80%
# Custom Lua script check
- name: database-check
type: script
file: verify-db.lua
env:
DB_URL: postgres://user:pass@postgres:5432/mydb
Check types:
type: http— HTTP request with status/body/JSON assertionstype: prometheus— PromQL query with min/max assertionstype: script— Custom Lua script (sandboxed builtins)
Output is structured JSON:
Exit code: 0 if all checks pass, 1 if any fail.
Development
Build
Test
Lint
Format
Run Examples
Self-contained scripts (no external services needed):
Kubernetes examples (require services running in-cluster):
Architecture
+------------------------------------------------------------------+
| Assay v0.3.0 (~9 MB static MUSL binary, Alpine container) |
| |
| CLI (auto-detected by file extension): |
| assay config.yaml (.yaml -> check orchestration) |
| assay script.lua (.lua -> run script) |
| |
| Shebang support: |
| #!/usr/bin/assay (works like #!/usr/bin/python3) |
| |
| Rust Core: |
| Config parser (YAML) -> Runner (retry/backoff/timeout) |
| -> Structured JSON output -> Exit code (0/1) |
| |
| Lua Runtime (mlua + Lua 5.5): |
| - 64 MB memory limit per VM |
| - Fresh VM per check (YAML mode) |
| - Single VM per script (Lua mode) |
| - Async support via tokio LocalSet |
| |
| Rust Builtins (all available to .lua scripts): |
| http.{get,post,put,patch,delete,serve} |
| ws.{connect,send,recv,close} |
| json.{parse,encode} yaml.{parse,encode} toml.{parse,encode} |
| fs.{read,write} base64.{encode,decode} |
| crypto.{jwt_sign,hash,random} regex.{match,find,replace} |
| db.{connect,query,execute,close} (postgres, mysql, sqlite) |
| template.{render,render_string} |
| assert.{eq,gt,lt,contains,not_nil,matches} |
| log.{info,warn,error} env.get sleep time |
| async.{spawn,spawn_interval} |
| |
| Lua Stdlib (embedded .lua files via include_dir!): |
| Monitoring: prometheus, alertmanager, loki, grafana |
| K8s/GitOps: k8s, argocd, kargo, flux, traefik |
| Security: vault, openbao, certmanager, eso, dex |
| Infra: crossplane, velero, temporal, harbor |
| Utilities: healthcheck |
+------------------------------------------------------------------+
Use Cases
- ArgoCD/Kargo Hooks: PostSync verification, PreSync validation, health checks
- Kubernetes Jobs: Database migrations, API configuration, secret rotation
- Lightweight Web Services: Webhook receivers, API proxies, mock servers, dashboards
- Platform Automation: Operational tasks, cross-service connectivity checks, report generation
- Verification: E2E tests, smoke tests, integration tests
Why Lua 5.5?
Assay uses Lua 5.5 (released Dec 2025) over LuaJIT for:
- Global declarations: Catches accidental globals (reduces bugs)
- Named vararg tables: Cleaner function signatures
- Incremental major GC: Smoother latency for long-running servers
- Native int64: Better for timestamps, IDs
- MUSL static linking: No assembler issues
Our scripts are I/O bound (HTTP calls, database queries). LuaJIT's 5-10x CPU speedup provides negligible benefit (<1% of total job time).
License
MIT
Contributing
Contributions welcome! Please open an issue or PR on GitHub.