Part 05: HomeLab as Substrate — A Recap
"This is not the first series. Read the first one if you have time. Read this part if you don't."
Why
K8s.Dsl is a plugin to HomeLab. To understand the plugin, you need to understand the host. The host is the HomeLab Docker series, 56 parts of CLI-first meta-orchestration that I am not going to ask you to read in full before you can read this one. Instead, this part is the recap: just enough HomeLab background to follow the K8s.Dsl plugin design from Act II onward.
If you have already read homelab-docker, you can skip this part. If you have not, read this and the cross-links should give you enough context.
The thing called HomeLab
HomeLab is a CLI-first meta-orchestrator for local infrastructure. It does one job: takes a typed declaration (a YAML file backed by C# [Builder] types) and turns it into a running development environment on Vagrant VMs. Specifically, it can:
- Build VM images via Packer
- Provision VMs via Vagrant
- Generate Docker Compose stacks
- Configure Traefik for routing
- Issue TLS certificates from a self-signed CA
- Manage DNS (hosts file or PiHole)
- Configure GitLab Omnibus inside the lab
The series is spec-driven: HomeLab itself does not yet exist as code. The 56 parts are the design, written before the implementation, intended to be argued and revised in public.
The killer architectural commitment is thin CLI, fat lib. The CLI project (HomeLab.Cli) contains only System.CommandLine shells. Every verb is a five-line pattern: parse args → construct typed request → call one method on the lib → render → exit. The lib (HomeLab) contains all the business logic. The two never overlap, and an architecture test enforces it. This matters for K8s.Dsl because the K8s.Dsl plugin will follow exactly the same pattern: a Lib NuGet for everything substantive, a Cli NuGet for the verb shells.
The six-stage pipeline
Every HomeLab verb runs the same pipeline:
| Stage | Job |
|---|---|
| 0. Validate | Schema validation, Ops.Dsl [MetaConstraint] checks |
| 1. Resolve | Load plugins, deep-merge local overrides, hydrate the typed config |
| 2. Plan | Project the config to an Ops.Dsl IR, build the action DAG |
| 3. Generate | Emit Packer HCL, Vagrantfile, compose YAML, Traefik YAML, certs |
| 4. Apply | Call the binary wrappers in DAG order |
| 5. Verify | Run health probes from Ops.Observability declarations |
Each stage is a [Injectable] IHomeLabStage implementation. Each one returns Result<HomeLabContext>. The pipeline runs them in order; on failure, it short-circuits and reports which stage failed via the event bus.
K8s.Dsl plugs into this pipeline by adding contributors at stage 3 (IK8sManifestContributor, IHelmReleaseContributor) and an extra phase at stage 4 (kubeadm/k3s bootstrap) and stage 5 (cluster health probes). It does not modify the pipeline itself.
The plugin contract
IHomeLabPlugin is the umbrella contract:
public interface IHomeLabPlugin
{
string Name { get; }
string Version { get; }
void Initialize(IPluginContext context);
}public interface IHomeLabPlugin
{
string Name { get; }
string Version { get; }
void Initialize(IPluginContext context);
}Plus role-shaped sub-contracts a plugin can ship implementations of:
IMachineTypeContributor— adds a new VM kindIPackerBundleContributor— adds Packer build stepsIComposeFileContributor— adds compose servicesITraefikContributor— adds Traefik routingIDnsProvider— provides DNSITlsCertificateProvider— issues certsIContainerEngine— adds a container runtime (Docker, Podman, ...)ISecretStore— provides secretsIBackupProvider— provides backups
Plugins ship as NuGet packages with a homelab.plugin.json manifest. The HomeLab plugin host loads them, scans their assemblies for [Injectable] types, and registers everything into the same DI container the core uses. After loading, plugin contributors are indistinguishable from in-tree contributors at injection time.
K8s.Dsl is a plugin. It adds new role contracts (IK8sManifestContributor, IHelmReleaseContributor, IClusterDistribution, IKubeconfigStore, IGitOpsRepoGenerator, IArgoCdAppContributor) that are also discovered via [Injectable] and follow the same lifecycle.
K8s.Dsl additionally adds a new dimension of pluggability we will explore in Part 11: CLI verb groups. The plugin ships its own top-level verb tree (homelab k8s ...) that the HomeLab CLI command builder discovers via IHomeLabVerbGroup and [VerbGroup("k8s")] attributes on IHomeLabVerbCommand implementations in the plugin assembly.
The toolbelt
HomeLab is built on 19 in-house FrenchExDev libraries. K8s.Dsl uses the same 19. They are mandatory, not optional.
| Library | What it provides |
|---|---|
Injectable |
Source-generated DI registration via [Injectable] |
Result |
Result, Result<T>, Result<T, TError> for explicit failure |
Builder |
[Builder] source-generated async, validated, cycle-safe builders |
Guard |
Argument validation at boundaries |
Clock |
IClock, SystemClock, FakeClock for testable time |
FiniteStateMachine |
Source-generated state machines for lifecycle modeling |
Saga |
Compensable long-running transactions |
Reactive |
IEventStream<T> over System.Reactive |
Options |
Option<T> for explicit absence |
Mapper |
Source-generated DTO ↔ domain mapping |
Mediator |
CLI verb dispatch via IRequestHandler<TReq, TRes> |
Outbox |
Reliable event delivery with replay |
BinaryWrapper |
Source-generated typed wrappers around external CLIs |
GitLab.Ci.Yaml |
Typed .gitlab-ci.yml builders |
Alpine.Version |
Alpine release discovery + drift detection |
Requirements |
[ForRequirement], [Verifies] for traceability |
QualityGate |
dotnet quality-gate test — the dev-loop bar |
Ddd + Entity.Dsl |
DDD aggregates with invariants |
Dsl |
The M3 metamodel framework |
HttpClient |
[TypedHttpClient] for typed HTTP calls |
K8s.Dsl uses all of them. We will see one runnable code block per library in the K8s.Dsl context in the parts ahead. The toolbelt is the most underrated part of the HomeLab story — it is what makes the design feasible with so little custom code.
See homelab-docker Part 11 for the full tour.
DevLab and the dogfood loops
HomeLab does not ship with a fictional demo project. The thing it stands up is DevLab, the real GitLab + runners + box registry + NuGet feed + docs site that the FrenchExDev team uses to develop HomeLab itself. There are five dogfood loops:
- Source: GitLab in DevLab hosts HomeLab's source code.
- Packages: CI in DevLab publishes HomeLab's NuGets to baget in DevLab.
- Boxes: HomeLab publishes its Vagrant boxes to a registry in DevLab.
- Backups: The backup framework backs up the GitLab that holds HomeLab's source, and a periodic restore-test job verifies it.
- Docs: The blog you are reading is hosted in DevLab.
K8s.Dsl extends this story with two more dogfood loops:
- Helm charts: K8s.Dsl plugin ships Helm chart templates; CI in DevLab publishes them to a chart museum in DevLab.
- GitOps: K8s.Dsl generates the GitOps repository structure; the repo lives in DevLab's GitLab; ArgoCD inside the cluster watches it; workloads deploy. We will see this in Part 33.
The dogfood pivot is the most consequential decision in either series. Every part of every series describes a real component that runs on the author's actual workstation, not a fictional one.
Multi-instance and the instance registry
homelab-docker Part 51 introduced the multi-instance pattern: the user can run many HomeLab instances on the same workstation, each in its own subnet, with its own VM-name prefix, its own DNS namespace, its own cert CA. The instance registry at ~/.homelab/instances.json tracks every instance and refuses to allocate overlapping subnets.
K8s.Dsl uses this pattern unchanged. Each Kubernetes cluster is one HomeLab instance. The registry does not care that the instance happens to host a k8s cluster — it just allocates a subnet, picks a name prefix, and gets out of the way.
This is what makes multi-client isolation (Part 03) possible. The instance registry was already the right primitive; K8s.Dsl just consumes it.
What you need to know to read the rest
If you read this part and only this part, here is the minimum context for the remaining 45 parts:
- HomeLab is a plugin host. Plugins ship as NuGets, declare a manifest, register
[Injectable]services, and integrate with the existing pipeline. - K8s.Dsl is one such plugin. It ships as two NuGets —
K8sDsl(the lib) andK8sDsl.Cli(the verb shells). - The pipeline has six stages. K8s.Dsl adds contributors at the Generate stage and extra logic at the Apply / Verify stages.
- The toolbelt is mandatory. Every cross-cutting concern in K8s.Dsl uses the same 19 libraries the core uses.
- DevLab is real. Every example in this series refers to a real workstation, real VMs, real GitLab, real ArgoCD, real workloads.
- Multi-instance isolation works at the HomeLab layer. K8s.Dsl benefits from it without doing anything K8s-specific.
That is the substrate. From Part 06 onward, we build the K8s.Dsl plugin on top of it.
Cross-links to the substrate
If you have time later, the most important parts of homelab-docker to read are:
- Part 03 — Thin CLI, Fat Lib: the architectural rule we mirror in the K8s.Dsl plugin
- Part 07 — The Pipeline: the six stages
- Part 10 — The Plugin System: the plugin contract
- Part 11 — The FrenchExDev Toolbelt: the 19 libraries
- Part 30 — Topology Composition: how topologies become VM lists
- Part 32 — Compose Contributors Pattern: the contributor merge rules
- Part 51 — Running Many Labs Side-By-Side: the multi-instance registry
- Part 53 — Writing a HomeLab Plugin: the end-to-end plugin walkthrough