Skip to main content
Welcome. This site supports keyboard navigation and screen readers. Press ? at any time for keyboard shortcuts. Press [ to focus the sidebar, ] to focus the content. High-contrast themes are available via the toolbar.
serard@dev00:~/cv

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);
}

Plus role-shaped sub-contracts a plugin can ship implementations of:

  • IMachineTypeContributor — adds a new VM kind
  • IPackerBundleContributor — adds Packer build steps
  • IComposeFileContributor — adds compose services
  • ITraefikContributor — adds Traefik routing
  • IDnsProvider — provides DNS
  • ITlsCertificateProvider — issues certs
  • IContainerEngine — adds a container runtime (Docker, Podman, ...)
  • ISecretStore — provides secrets
  • IBackupProvider — 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:

  1. Source: GitLab in DevLab hosts HomeLab's source code.
  2. Packages: CI in DevLab publishes HomeLab's NuGets to baget in DevLab.
  3. Boxes: HomeLab publishes its Vagrant boxes to a registry in DevLab.
  4. Backups: The backup framework backs up the GitLab that holds HomeLab's source, and a periodic restore-test job verifies it.
  5. Docs: The blog you are reading is hosted in DevLab.

K8s.Dsl extends this story with two more dogfood loops:

  1. Helm charts: K8s.Dsl plugin ships Helm chart templates; CI in DevLab publishes them to a chart museum in DevLab.
  2. 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:

  1. HomeLab is a plugin host. Plugins ship as NuGets, declare a manifest, register [Injectable] services, and integrate with the existing pipeline.
  2. K8s.Dsl is one such plugin. It ships as two NuGets — K8sDsl (the lib) and K8sDsl.Cli (the verb shells).
  3. The pipeline has six stages. K8s.Dsl adds contributors at the Generate stage and extra logic at the Apply / Verify stages.
  4. The toolbelt is mandatory. Every cross-cutting concern in K8s.Dsl uses the same 19 libraries the core uses.
  5. DevLab is real. Every example in this series refers to a real workstation, real VMs, real GitLab, real ArgoCD, real workloads.
  6. 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.


If you have time later, the most important parts of homelab-docker to read are:


⬇ Download