rig major updates
This commit is contained in:
24
rig/examples/starter/Dockerfile.example
Normal file
24
rig/examples/starter/Dockerfile.example
Normal file
@@ -0,0 +1,24 @@
|
||||
# EXAMPLE component image. Copy, rename, replace:
|
||||
# Dockerfile.api -> image <cluster>-api -> image: in k8s/base/api.yaml
|
||||
# COPY paths are relative to the build context the overlay's Tiltfile names (context='.').
|
||||
# Notes: rig's docs/notes/Dockerfile.example.md
|
||||
|
||||
FROM python:3.12-slim
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Dependencies first, in their own layer, so a source edit does not reinstall them.
|
||||
COPY api/requirements.txt ./
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
# Context-relative — see above.
|
||||
COPY api/ ./api/
|
||||
|
||||
# Match this with the containerPort in the manifest and the target of the
|
||||
# Service in front of it.
|
||||
EXPOSE 8000
|
||||
|
||||
CMD ["python", "-m", "api"]
|
||||
|
||||
# live_update: the Tiltfile's sync('api', '/app/api') must match COPY api/ + WORKDIR /app,
|
||||
# or edits silently do nothing.
|
||||
59
rig/examples/starter/README.md
Normal file
59
rig/examples/starter/README.md
Normal file
@@ -0,0 +1,59 @@
|
||||
# `examples/starter` — the overlay rig runs when you name none
|
||||
|
||||
An overlay is one folder, outside rig, that holds what runs: its settings, its
|
||||
manifests, its images. rig reads it and never writes into it — see
|
||||
[`docs/notes/overlay.md`](../../docs/notes/overlay.md) for the contract. This one
|
||||
ships with rig so `make tilt` has something to deploy on a fresh clone, and so a
|
||||
real overlay has a shape to be written against:
|
||||
|
||||
```
|
||||
rig.env settings layered over rig's defaults (this one sets none)
|
||||
k8s/base/ the components, as plain manifests
|
||||
k8s/overlays/dev/ how this environment differs from the base — MANIFESTS_DIR's default
|
||||
Tiltfile the workload's half of the dev loop; rig's ctrl/Tiltfile includes it
|
||||
Dockerfile.example the shape of an image you build yourself
|
||||
```
|
||||
|
||||
To start your own, copy this folder somewhere rig does not track —
|
||||
`rig/local/<name>/`, or a repo of its own — and name it:
|
||||
|
||||
```bash
|
||||
cp -r examples/starter local/myenv
|
||||
OVERLAY=local/myenv make cluster up # or OVERLAY=local/myenv in ctrl/.env
|
||||
```
|
||||
|
||||
The cluster, context and port block then follow the overlay's folder name
|
||||
(`myenv`, `kind-myenv`), so it never collides with another.
|
||||
|
||||
## The two components are examples, not the system
|
||||
|
||||
They exist so the real manifests have a shape to be written against.
|
||||
|
||||
### The three states a component can be in
|
||||
|
||||
Switching between them should be a one-line change, never a rewrite. The DNS
|
||||
name stays the same in every case, so callers never know the difference:
|
||||
|
||||
| state | what exists | when |
|
||||
| --- | --- | --- |
|
||||
| **real** | an image built from source, hot-reloaded | the one thing you are working on |
|
||||
| **mock** | a stub returning canned responses (`example-mock.yaml`) | everything else — most of the estate |
|
||||
| **remote** | no pod at all, just a Service (`example-remote.yaml`) | when the real system is reachable and you want it |
|
||||
|
||||
Most components should be **mock**. What has to be faithful is the topology —
|
||||
names, ports, dependency order, who can reach whom, how it fails. The workloads
|
||||
are noise, and mocking them is what makes several copies of a large estate fit
|
||||
on one laptop.
|
||||
|
||||
## Images
|
||||
|
||||
`Dockerfile.example` is a commented shape, not a working build. Paths in this
|
||||
folder's Tiltfile are relative to this folder, so `context='.'` is the overlay
|
||||
and every `COPY` in the Dockerfile is relative to it:
|
||||
|
||||
```
|
||||
docker_build(CLUSTER + '-api', context='.', dockerfile='Dockerfile.api')
|
||||
```
|
||||
|
||||
The name given to `docker_build` must match `image:` in the manifest — that
|
||||
string is the only thing connecting the two.
|
||||
69
rig/examples/starter/Tiltfile
Normal file
69
rig/examples/starter/Tiltfile
Normal file
@@ -0,0 +1,69 @@
|
||||
# The starter overlay's half of the dev loop. rig's ctrl/Tiltfile includes this once
|
||||
# it has applied k8s/overlays/dev, so every path here is relative to THIS folder.
|
||||
# Facts from rig, via os.getenv: RIG_CLUSTER RIG_CONTEXT RIG_HTTP_PORT RIG_HTTPS_PORT
|
||||
# RIG_TILT_PORT RIG_REGISTRY RIG_OVERLAY_DIR
|
||||
# Notes: rig's docs/notes/overlay.md
|
||||
|
||||
CLUSTER = os.getenv('RIG_CLUSTER')
|
||||
HTTP = os.getenv('RIG_HTTP_PORT')
|
||||
|
||||
# ── Images ─────────────────────────────────────────────────────────────────
|
||||
# (nothing yet — the examples run upstream images. Add docker_build calls here.)
|
||||
|
||||
|
||||
# ── Resources ──────────────────────────────────────────────────────────────
|
||||
# (nothing yet — add k8s_resource calls here to name and order what you deploy.)
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
# Catalogue — paste what you need, delete the rest.
|
||||
# Commented out so this file runs as-is.
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
#
|
||||
# ── build an image ─────────────────────────────────────────────────────────
|
||||
# context= and dockerfile= are both relative to this folder, so every COPY in the
|
||||
# Dockerfile is relative to the context you name here.
|
||||
#
|
||||
# docker_build(
|
||||
# CLUSTER + '-api', # must match `image:` in the manifest —
|
||||
# context='.', # that string is the only thing
|
||||
# dockerfile='Dockerfile.api', # connecting the two
|
||||
# ignore=['.git', 'rig', '.venv', 'node_modules', '__pycache__'],
|
||||
# live_update=[sync('api', '/app/api')],
|
||||
# )
|
||||
#
|
||||
# ── a shared base, built once ──────────────────────────────────────────────
|
||||
# Components that share code build FROM one base image instead of each carrying
|
||||
# a copy of it. Tilt builds the base first when a Dockerfile's FROM names it.
|
||||
#
|
||||
# docker_build(CLUSTER + '-base', context='repodir/base')
|
||||
# docker_build(CLUSTER + '-api', context='repodir/api') # its Dockerfile: FROM <cluster>-base
|
||||
#
|
||||
# ── name and order a resource ──────────────────────────────────────────────
|
||||
# k8s_resource('api', resource_deps=['postgres'], labels=['app'])
|
||||
# k8s_resource('gateway', resource_deps=['api', 'ui'], labels=['app'])
|
||||
#
|
||||
# ── reload the gateway when its config changes ─────────────────────────────
|
||||
# A hash-less configMapGenerator ConfigMap never changes name, so edits do NOT
|
||||
# roll the pod on their own.
|
||||
#
|
||||
# local_resource(
|
||||
# 'gateway-reload',
|
||||
# cmd='kubectl --context %s -n <namespace> rollout restart deployment/gateway' % os.getenv('RIG_CONTEXT'),
|
||||
# deps=['k8s/base/Caddyfile'],
|
||||
# resource_deps=['gateway'],
|
||||
# auto_init=False,
|
||||
# )
|
||||
#
|
||||
# ── manifests that need kustomize flags ────────────────────────────────────
|
||||
# rig applies MANIFESTS_DIR without flags. If a secretGenerator reads above its
|
||||
# kustomization root, set MANIFESTS_DIR=none in rig.env and apply them here instead;
|
||||
# the flag loosens a safety check for the whole build.
|
||||
#
|
||||
# k8s_yaml(kustomize('k8s/overlays/dev', flags=['--load-restrictor=LoadRestrictionsNone']))
|
||||
#
|
||||
# ── reach a service directly, bypassing the gateway ────────────────────────
|
||||
# Prefer the gateway; host ports are shared machine-wide. If you need one, take it
|
||||
# from this environment's own port block.
|
||||
#
|
||||
# k8s_resource('postgres', port_forwards=[str(int(HTTP) + 5) + ':5432'])
|
||||
104
rig/examples/starter/k8s/base/example-mock.yaml
Normal file
104
rig/examples/starter/k8s/base/example-mock.yaml
Normal file
@@ -0,0 +1,104 @@
|
||||
# EXAMPLE — a mocked component. Copy, rename, replace.
|
||||
#
|
||||
# A stub that answers on the right name and port with canned responses. No image
|
||||
# to build: the script is mounted from the ConfigMap, so changing the behaviour
|
||||
# is a kubectl apply, not a rebuild.
|
||||
#
|
||||
# Deliberately boring and readable. This is onboarding material — someone should
|
||||
# be able to read the generated object and recognise what it is.
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: example-service-stub
|
||||
data:
|
||||
# Canned responses by path. Add entries as the contract becomes clear;
|
||||
# anything unmatched returns 404 so a missing route is visible, not silent.
|
||||
routes.json: |
|
||||
{
|
||||
"/health": {"status": 200, "body": {"status": "ok"}},
|
||||
"/v1/example": {"status": 200, "body": {"items": [], "mocked": true}}
|
||||
}
|
||||
serve.py: |
|
||||
import json, os
|
||||
from http.server import BaseHTTPRequestHandler, HTTPServer
|
||||
|
||||
ROUTES = json.load(open("/etc/stub/routes.json"))
|
||||
NAME = os.environ.get("STUB_NAME", "stub")
|
||||
|
||||
class H(BaseHTTPRequestHandler):
|
||||
def do_GET(self):
|
||||
r = ROUTES.get(self.path)
|
||||
if r is None:
|
||||
self.send_response(404)
|
||||
self.end_headers()
|
||||
# Say which stub rejected it — with everything mocked, "404"
|
||||
# alone tells you nothing about where the call actually landed.
|
||||
self.wfile.write(json.dumps(
|
||||
{"error": "no canned route", "stub": NAME, "path": self.path}
|
||||
).encode())
|
||||
return
|
||||
body = json.dumps(r["body"]).encode()
|
||||
self.send_response(r["status"])
|
||||
self.send_header("Content-Type", "application/json")
|
||||
self.send_header("X-Mocked-By", NAME)
|
||||
self.end_headers()
|
||||
self.wfile.write(body)
|
||||
|
||||
def log_message(self, fmt, *args):
|
||||
print("%s %s" % (NAME, fmt % args), flush=True)
|
||||
|
||||
HTTPServer(("0.0.0.0", 8080), H).serve_forever()
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: example-service
|
||||
labels:
|
||||
app: example-service
|
||||
rig.component/impl: mock # so `kubectl get deploy -L rig.component/impl`
|
||||
# shows at a glance what is real and what is not
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: example-service
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: example-service
|
||||
spec:
|
||||
containers:
|
||||
- name: stub
|
||||
image: python:3.12-slim
|
||||
command: ["python3", "/etc/stub/serve.py"]
|
||||
env:
|
||||
- name: STUB_NAME
|
||||
value: example-service
|
||||
ports:
|
||||
- containerPort: 8080
|
||||
volumeMounts:
|
||||
- name: stub
|
||||
mountPath: /etc/stub
|
||||
readinessProbe:
|
||||
httpGet: { path: /health, port: 8080 }
|
||||
initialDelaySeconds: 2
|
||||
# Small enough that a whole estate of these fits alongside the real
|
||||
# thing you are working on.
|
||||
resources:
|
||||
requests: { memory: 32Mi, cpu: 10m }
|
||||
limits: { memory: 64Mi }
|
||||
volumes:
|
||||
- name: stub
|
||||
configMap:
|
||||
name: example-service-stub
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: example-service
|
||||
spec:
|
||||
selector:
|
||||
app: example-service
|
||||
ports:
|
||||
- port: 80
|
||||
targetPort: 8080
|
||||
46
rig/examples/starter/k8s/base/example-remote.yaml
Normal file
46
rig/examples/starter/k8s/base/example-remote.yaml
Normal file
@@ -0,0 +1,46 @@
|
||||
# EXAMPLE — a component that is NOT simulated, pointed at the real system.
|
||||
#
|
||||
# This is the payoff of keeping the topology honest: there is no pod here at
|
||||
# all, yet `example-remote.<namespace>.svc.cluster.local` resolves exactly as it
|
||||
# does when the same component is mocked. Callers are identical in both cases,
|
||||
# so moving a dependency from mocked to real is a one-line change and nothing
|
||||
# downstream is touched.
|
||||
#
|
||||
# Use this when the real system is reachable and you want it in the loop.
|
||||
# Note that reachability depends on where you are running: systems restricted to
|
||||
# a managed workspace will not resolve from a laptop at all, which is the whole
|
||||
# reason most components should stay mocked.
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: example-remote
|
||||
labels:
|
||||
rig.component/impl: remote
|
||||
spec:
|
||||
type: ExternalName
|
||||
externalName: real-system.internal.example.com
|
||||
---
|
||||
# If the real system has no DNS name — only an IP, which is common for legacy
|
||||
# hosts — ExternalName cannot express it. Use a bare Service plus manual
|
||||
# Endpoints instead, and delete the block above.
|
||||
#
|
||||
# apiVersion: v1
|
||||
# kind: Service
|
||||
# metadata:
|
||||
# name: example-remote
|
||||
# labels:
|
||||
# rig.component/impl: remote
|
||||
# spec:
|
||||
# ports:
|
||||
# - port: 80
|
||||
# targetPort: 8080
|
||||
# ---
|
||||
# apiVersion: v1
|
||||
# kind: Endpoints
|
||||
# metadata:
|
||||
# name: example-remote # must match the Service name exactly
|
||||
# subsets:
|
||||
# - addresses:
|
||||
# - ip: 10.0.0.42
|
||||
# ports:
|
||||
# - port: 8080
|
||||
11
rig/examples/starter/k8s/base/kustomization.yaml
Normal file
11
rig/examples/starter/k8s/base/kustomization.yaml
Normal file
@@ -0,0 +1,11 @@
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
|
||||
# The namespace every component lands in. The overlay overrides it, so a rig
|
||||
# modelling two estates can apply the same base twice under different names.
|
||||
namespace: rig
|
||||
|
||||
resources:
|
||||
- namespace.yaml
|
||||
- example-mock.yaml
|
||||
- example-remote.yaml
|
||||
4
rig/examples/starter/k8s/base/namespace.yaml
Normal file
4
rig/examples/starter/k8s/base/namespace.yaml
Normal file
@@ -0,0 +1,4 @@
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: rig
|
||||
22
rig/examples/starter/k8s/overlays/dev/kustomization.yaml
Normal file
22
rig/examples/starter/k8s/overlays/dev/kustomization.yaml
Normal file
@@ -0,0 +1,22 @@
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
|
||||
resources:
|
||||
- ../../base
|
||||
|
||||
# The dev overlay is where a rig says how its estate differs from the base —
|
||||
# which components are real, which are mocked, which point at a live system.
|
||||
# Kept empty on purpose: the base already boots, and an overlay full of examples
|
||||
# is harder to read than one that starts blank.
|
||||
#
|
||||
# The shape a patch takes, for when the first one is needed:
|
||||
#
|
||||
# patches:
|
||||
# - target: {kind: Service, name: example-service}
|
||||
# patch: |
|
||||
# - op: replace
|
||||
# path: /spec/type
|
||||
# value: NodePort
|
||||
# - op: add
|
||||
# path: /spec/ports/0/nodePort
|
||||
# value: 30080
|
||||
9
rig/examples/starter/rig.env
Normal file
9
rig/examples/starter/rig.env
Normal file
@@ -0,0 +1,9 @@
|
||||
# This overlay's settings: layered over rig's defaults, under ctrl/.env and the caller.
|
||||
# The starter sets nothing, so a rig with no overlay named behaves exactly like one with
|
||||
# no overlay at all. Any key a profile could set belongs here; paths are relative to
|
||||
# this folder. PROFILE and OVERLAY are refused — they are what chooses this file.
|
||||
# Notes: rig's docs/notes/overlay.md
|
||||
#
|
||||
# ADDONS="metallb"
|
||||
# K8S_VERSION=v1_36
|
||||
# MANIFESTS_DIR=k8s/overlays/dev
|
||||
Reference in New Issue
Block a user