Files
soleprint/rig/docs/index.html

499 lines
20 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<link rel="icon" href="data:,">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>rig — local environment installer</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&family=JetBrains+Mono:wght@400;500&display=swap');
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
background: #0a0e17;
color: #e8eaf0;
font-family: 'Inter', sans-serif;
line-height: 1.6;
height: 100vh;
overflow: hidden;
display: flex;
flex-direction: column;
}
header {
padding: 16px 24px;
border-bottom: 1px solid #1e2a4a;
display: flex;
align-items: baseline;
gap: 16px;
flex-shrink: 0;
}
header h1 {
font-family: 'JetBrains Mono', monospace;
font-size: 22px;
font-weight: 600;
letter-spacing: 3px;
color: #0066ff;
}
header .subtitle {
font-size: 13px;
color: #4a5568;
letter-spacing: 1px;
text-transform: uppercase;
}
.layout { display: flex; flex: 1; min-height: 0; }
nav {
display: flex;
flex-direction: column;
width: 200px;
flex-shrink: 0;
background: #121829;
border-right: 1px solid #1e2a4a;
padding: 8px 0;
overflow-y: auto;
}
nav a {
padding: 10px 20px;
font-family: 'JetBrains Mono', monospace;
font-size: 12px;
color: #8892a8;
text-decoration: none;
border-left: 2px solid transparent;
transition: all 0.15s;
cursor: pointer;
}
nav a:hover { color: #e8eaf0; background: #1a2340; }
nav a.active { color: #0066ff; border-left-color: #0066ff; background: #0d1a33; }
main { flex: 1; overflow: auto; padding: 32px 48px; }
.section { display: none; animation: fadeIn 0.2s ease; }
.section.active { display: block; }
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
.section h2 {
font-family: 'JetBrains Mono', monospace;
font-size: 15px;
font-weight: 500;
color: #8892a8;
margin-bottom: 8px;
letter-spacing: 1px;
text-transform: uppercase;
}
.section > p.lede {
font-size: 13px;
color: #4a5568;
margin-bottom: 24px;
max-width: 800px;
}
.prose { max-width: 820px; }
.prose p { font-size: 14px; color: #b4bccf; line-height: 1.7; margin-bottom: 14px; }
.prose p b { color: #e8eaf0; }
.prose ul { margin: 0 0 16px 20px; }
.prose li { font-size: 14px; color: #b4bccf; margin-bottom: 6px; }
.prose h3 {
font-family: 'JetBrains Mono', monospace;
font-size: 13px;
text-transform: uppercase;
color: #e8eaf0;
margin: 32px 0 10px;
letter-spacing: 1px;
}
.prose code, pre code {
font-family: 'JetBrains Mono', monospace;
font-size: 12px;
color: #7ab0ff;
background: #121829;
padding: 1px 5px;
border-radius: 3px;
}
pre {
background: #121829;
border: 1px solid #1e2a4a;
padding: 16px;
overflow: auto;
margin-bottom: 16px;
}
pre code { background: none; padding: 0; }
pre .c { color: #4a5568; }
pre .k { color: #0066ff; }
.graph-container { margin: 16px 0; }
.graph-container img {
display: block;
max-width: 100%;
background: #0a0e17;
border: 1px solid #1e2a4a;
padding: 12px;
}
dl {
display: grid;
grid-template-columns: max-content 1fr;
gap: 10px 24px;
margin: 16px 0;
max-width: 820px;
}
dt {
font-family: 'JetBrains Mono', monospace;
color: #0066ff;
font-size: 13px;
padding-top: 2px;
}
dd { font-size: 14px; color: #b4bccf; line-height: 1.6; }
table { border-collapse: collapse; margin: 16px 0; max-width: 820px; }
th, td {
text-align: left;
padding: 7px 16px 7px 0;
font-size: 13px;
border-bottom: 1px solid #1e2a4a;
color: #b4bccf;
}
th {
font-family: 'JetBrains Mono', monospace;
font-size: 11px;
text-transform: uppercase;
color: #8892a8;
letter-spacing: 1px;
}
td code { white-space: nowrap; }
.note {
border-left: 2px solid #ffc107;
background: #17130a;
padding: 12px 16px;
margin: 16px 0;
max-width: 820px;
}
.note p { margin: 0; font-size: 13px; color: #b4bccf; }
.note b { color: #ffc107; }
.menu-toggle {
display: none;
background: transparent;
border: 1px solid #1e2a4a;
color: #8892a8;
padding: 6px 10px;
font-size: 14px;
cursor: pointer;
line-height: 1;
margin-left: auto;
}
.menu-toggle:hover { background: #1a2340; }
.nav-backdrop {
display: none;
position: absolute;
inset: 0;
background: rgba(0, 0, 0, 0.5);
z-index: 10;
}
.layout.nav-open .nav-backdrop { display: block; }
@media (max-width: 720px) {
header { padding: 10px 12px; gap: 8px; }
header h1 { font-size: 16px; letter-spacing: 1px; }
header .subtitle { display: none; }
.menu-toggle { display: inline-block; }
.layout { position: relative; }
nav {
position: absolute; left: 0; top: 0; bottom: 0;
width: 200px; z-index: 20;
transform: translateX(-100%);
transition: transform 0.2s ease;
box-shadow: 2px 0 8px rgba(0, 0, 0, 0.5);
}
.layout.nav-open nav { transform: translateX(0); }
main { padding: 16px; }
.section h2 { font-size: 13px; }
.prose p, .prose li { font-size: 13px; }
}
</style>
</head>
<body>
<header>
<h1>RIG</h1>
<span class="subtitle">local environment installer</span>
<button class="menu-toggle">&#9776;</button>
</header>
<div class="layout">
<div class="nav-backdrop"></div>
<nav>
<a href="#start">Start here</a>
<a href="#install">Installation</a>
<a href="#environments">Environments</a>
<a href="#overlays">Overlays</a>
<a href="#troubleshooting">Troubleshooting</a>
</nav>
<main>
<section class="section" id="start">
<h2>Start here</h2>
<div class="prose">
<p>rig builds a disposable Kubernetes environment on your machine, so you can
see how a system fits together without access to any of it. It is for
<b>onboarding and exploration</b>, not a production replica.</p>
<p>Most services in it are <b>not real</b>. What has to be faithful is the
topology: names, ports, dependency order, who can reach whom, and how it fails.
That is what makes a whole estate fit on a laptop. A real 20-service platform
does not fit once in 14&nbsp;GB; stubs of about 30&nbsp;MB each fit three times.</p>
<h3>The only prerequisite</h3>
<p><b>Docker.</b> No curl, no jq, no python, no apt repositories to configure.</p>
<pre><code><span class="c"># in the environment directory:</span>
make check <span class="c"># is this machine ready? reports, never changes anything</span>
make deps <span class="c"># install the pinned toolchain; safe to re-run</span>
make cluster up <span class="c"># cluster + registry + addons; ports derive by themselves</span>
</code></pre>
<p>Read <code>make check</code> before <code>make deps</code>: it lists what is
missing, including the steps only you can do, such as anything needing
<code>sudo</code> or a Windows-side restart.</p>
<h3>make cluster up</h3>
<p>It prints what the kind config locks in before spending the time, because
that config is fixed when the cluster is created. Re-running is safe and
continues where an interrupted run stopped, rather than reporting that the
cluster already exists and leaving every node <code>NotReady</code>.</p>
<pre><code>make cluster up <span class="c"># built-in defaults — no profile needed</span>
make cluster up PROFILE=mirror <span class="c"># images through an internal registry</span>
make cluster up OVERLAY=examples/data <span class="c"># an overlay: what runs, kept outside rig</span>
make cluster reset <span class="c"># destroy and rebuild: how an edited kind config takes effect</span>
</code></pre>
<h3>Checking on things</h3>
<dl>
<dt>make cluster list</dt><dd>Every cluster on the machine, its memory cost and its port block. A new cluster usually fails to start because of an old one; <code>make cluster free</code> stops them without deleting.</dd>
<dt>make check</dt><dd>Host, toolchain, and whether this cluster fits, its ports, registry and addons. Only problems print; <code>make check all</code> prints everything.</dd>
<dt>make check mem</dt><dd>Memory in depth: what caps it, how far it climbs, and on WSL the <code>.wslconfig</code> backup and restore.</dd>
<dt>make docs</dt><dd>Serves these pages from a throwaway container. It needs no cluster and no toolchain, because these are the instructions for building both.</dd>
</dl>
</div>
</section>
<section class="section" id="install">
<h2>Installation</h2>
<div class="graph-container">
<a href="viewer.html?src=graphs/01-install.svg"><img src="graphs/01-install.svg" alt="How the installer works"></a>
</div>
<div class="prose">
<p>The installer is a container, not a shell script, because a stock slim Debian
has no curl, wget, jq, python3 or CA bundle: a shell installer could not make a
verified HTTPS request, let alone check one. The container carries its own
toolchain, so the host needs only Docker. Nothing runs inside it afterwards —
kind, kubectl, tilt and jq run natively.</p>
<p>Every tool is a single binary at a pinned version, checked against a published
SHA256. Node images are pinned <b>by digest</b>, so upgrading kind cannot move
your Kubernetes version.</p>
<h3>Two tiers</h3>
<p>A managed or corporate-issued machine is not somewhere to install development
tools by default, so installing cluster tooling is a decision rather than a side
effect:</p>
<table>
<tr><th>tier</th><th>installs</th><th>for</th></tr>
<tr><td><code>core</code></td><td>kubectl, jq</td><td>talk to a cluster someone else runs</td></tr>
<tr><td><code>dev</code></td><td>+ kind, tilt</td><td>build clusters and hot-reload into them</td></tr>
</table>
<pre><code>make deps core <span class="c"># kubectl and jq only — nothing that creates a cluster</span>
make deps <span class="c"># dev, the default</span>
</code></pre>
<h3>Air-gapped</h3>
<pre><code>make deps image full <span class="c"># bakes every binary into the image</span>
docker save …-deps:full | gzip &gt; rig.tgz
<span class="c"># carry that one file in, then:</span>
docker load &lt; rig.tgz &amp;&amp; make cluster up PROFILE=offline <span class="c"># from env.d/offline.env.example</span>
</code></pre>
</div>
</section>
<section class="section" id="environments">
<h2>Environments</h2>
<div class="graph-container">
<a href="viewer.html?src=graphs/02-environment.svg"><img src="graphs/02-environment.svg" alt="Environment derivation"></a>
</div>
<div class="prose">
<p>One folder is one environment. Running several versions of a system at once
means several clusters on one machine, and everything that could collide is
derived from the folder name (the overlay's, when one is named):</p>
<dl>
<dt>cluster + context</dt><dd><code>platform-v2/</code> builds <code>platform-v2</code> on <code>kind-platform-v2</code>.</dd>
<dt>port block</dt><dd>Ten ports from a hash of the name, above 20000 — clear of 80, 443, 3000, 5432, 8000 and 8080.</dd>
<dt>registry + images</dt><dd>Named after the environment, so two copies never share one.</dd>
</dl>
<p>So two copies never collide, and neither one's <code>make cluster down</code>
can touch the other. <code>make check</code> shows the block;
<code>bash ctrl/ports.sh persist</code> freezes it into <code>ctrl/.env</code>.</p>
<h3>Configuration layers</h3>
<p>Weakest first, later wins: built-in defaults → pinned versions → a profile, if
you name one → the overlay's <code>rig.env</code> → <code>ctrl/.env</code> → the
caller's environment. So a value on the command line always beats every file.</p>
<h3>Profiles</h3>
<p>A profile says how this machine reaches the world. rig needs none.</p>
<table>
<tr><th>example</th><th>registry</th><th>for</th></tr>
<tr><td>none</td><td><code>local</code></td><td>the built-in defaults</td></tr>
<tr><td><code>mirror.env.example</code></td><td><code>mirror</code></td><td>images through an internal registry</td></tr>
<tr><td><code>offline.env.example</code></td><td><code>local</code></td><td>air-gapped</td></tr>
</table>
<h3>Registry modes</h3>
<table>
<tr><th>mode</th><th>what it does</th></tr>
<tr><td><code>none</code></td><td>images are built straight into the node</td></tr>
<tr><td><code>local</code></td><td>a registry container wired into the cluster</td></tr>
<tr><td><code>mirror</code></td><td>that container as a pull-through cache of the corporate registry</td></tr>
<tr><td><code>remote</code></td><td>no local container; pull direct with an <code>imagePullSecret</code></td></tr>
</table>
<p><code>mirror</code> is what a locked-down network looks like: images come from
the corporate registry, you do not hammer it, and you keep working when the
connection drops. It is also only reachable from where that registry is routable —
from a laptop, use <code>local</code> or <code>offline</code>.</p>
<p><b>Corporate CA trust is the usual failure.</b> It has to reach three places:
the host Docker daemon, every node's containerd (nodes do not inherit host trust),
and any in-cluster client. Set <code>REGISTRY_CA_FILE</code>;
<code>make check</code> reports which is still missing. Without it the error is
<code>x509: certificate signed by unknown authority</code>.</p>
<h3>LoadBalancer services</h3>
<p>Real manifests use <code>type: LoadBalancer</code>. On a bare local cluster
those Services sit at <code>EXTERNAL-IP &lt;pending&gt;</code> forever, with no
error: the deployment looks healthy and is unreachable. The <code>metallb</code>
addon fixes that, so upstream manifests work unchanged. Its address pool is
derived from the cluster's Docker network at install time, because Docker picks
that subnet and it differs between machines.</p>
<p>Those addresses are reachable from the Linux side, including inside WSL. A
browser on Windows has no route to them: use the environment's HTTP port instead, where
an overlay's gateway listens (its kind config maps it to the host).</p>
<h3>Networking and memory</h3>
<p>The cluster uses kind's built-in networking, which does enforce standard
NetworkPolicy — verified against a no-policy control. A pluggable CNI was tried
and removed: it added policy features nothing here needs, in exchange for a
slower boot and one more thing to get right at creation time.</p>
<p>Every cluster is a running container tree whether you use it or not.
<code>make cluster list</code> shows what each costs, and
<code>make cluster free</code> stops the others without deleting them.</p>
</div>
</section>
<section class="section" id="overlays">
<h2>Overlays</h2>
<div class="prose">
<p>An overlay is one folder, kept outside rig's version control, holding a use
case. rig reads it and never writes into it. Every piece is optional:</p>
<table>
<tr><th>in the overlay</th><th>what rig does with it</th></tr>
<tr><td><code>rig.env</code></td><td>a config layer: addons, namespaces, images — anything a profile could set</td></tr>
<tr><td><code>k8s/overlays/dev/</code></td><td>the manifests the dev loop applies</td></tr>
<tr><td><code>kind-config.yaml.tpl</code></td><td>the cluster's shape, when it needs its own (mounts, ports)</td></tr>
<tr><td><code>addons/&lt;name&gt;.sh</code></td><td>addons, found before rig's own; each applies its own <code>&lt;name&gt;.yaml</code></td></tr>
<tr><td><code>Tiltfile</code></td><td>the workload's half of the dev loop, included by rig's</td></tr>
</table>
<pre><code>cp -r examples/starter local/myenv <span class="c"># local/ is gitignored</span>
OVERLAY=local/myenv make cluster up
</code></pre>
<p>With none named, rig runs its own <code>examples/starter</code>.
<code>examples/data</code> carries postgres, redis and airflow as an overlay's own
addons. A project can carry rig at <code>./rig</code>, or keep it anywhere and name
it with <code>RIG_DIR</code>. <code>.version</code> names the release; the project
records the one it was last run against. See <code>docs/notes/overlay.md</code>.</p>
<h3>What runs in it</h3>
<p>Each component is one of three things, and switching between them is a
one-line change:</p>
<dl>
<dt>real</dt><dd>Built from source and hot-reloaded. What you are working on — usually exactly one.</dd>
<dt>mock</dt><dd>A generic stub with canned responses. Everything you do not care about today.</dd>
<dt>remote</dt><dd>No pod at all: a Service of type <code>ExternalName</code> pointing at the real system. In-cluster DNS resolves identically, so callers never change.</dd>
</dl>
</div>
</section>
<section class="section" id="troubleshooting">
<h2>Troubleshooting</h2>
<div class="prose">
<h3>Tilt stops noticing file changes</h3>
<p>Almost always inotify limits, and it fails silently: nothing errors, changes
just stop being picked up. Defaults on WSL are far too low.
<code>make check</code> reports it and prints the fix.</p>
<h3>Cluster creation dies halfway with a port error</h3>
<p>Docker reports <code>failed to bind host port … address already in use</code>
partway through creating the cluster. <code>make check</code> tests every port in
this environment's block before anything is built.</p>
<h3>Every node stays NotReady</h3>
<p>Usually a cluster created with the default CNI disabled and the real CNI never
installed, after an interrupted first run. Run <code>make cluster up</code> again:
it finishes the missing steps instead of stopping because the cluster exists.</p>
<h3>x509: certificate signed by unknown authority</h3>
<p>Corporate CA trust has not reached one of the three places it needs to be. See
<a href="#environments">Environments</a>.</p>
<h3>kubectl says the context does not exist</h3>
<p>The cluster can exist while its context does not — a reset or a switched
<code>KUBECONFIG</code> loses it. <code>make cluster up</code> detects this and
re-exports the context.</p>
</div>
</section>
</main>
</div>
<script>
(function () {
var layout = document.querySelector('.layout');
var main = document.querySelector('main');
function syncActive() {
var hash = location.hash.slice(1) || 'start';
document.querySelectorAll('.section').forEach(function (s) { s.classList.remove('active'); });
document.querySelectorAll('nav a').forEach(function (a) { a.classList.remove('active'); });
var section = document.getElementById(hash);
if (section) section.classList.add('active');
var link = document.querySelector('nav a[href="#' + hash + '"]');
if (link) link.classList.add('active');
if (main) main.scrollTop = 0;
layout.classList.remove('nav-open');
}
window.addEventListener('hashchange', syncActive);
window.addEventListener('DOMContentLoaded', syncActive);
syncActive();
document.addEventListener('click', function (e) {
if (e.target.closest('.menu-toggle') || e.target.closest('.nav-backdrop')) {
layout.classList.toggle('nav-open');
}
});
})();
</script>
</body>
</html>