Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Architecture

Xianyun is a Cargo workspace of eleven crates. It’s a deployment orchestrator, not a configuration generator: it starts a cluster, points Helm and Kustomize at what your config lists, and sequences the workflow. Helm is the source of truth for application configuration, and Xianyun knows nothing about any particular application.

The goal is to close the gap between local development and production Kubernetes by composing standard tools (Helm, Kustomize, k3d, Colima, mirrord, Skaffold) rather than inventing abstractions of its own.

A test for every feature: could a team unrelated to the project that wrote it use the feature without changing it? If not, redesign it.

The design came from the Go project this one forked. Its lesson was that tight coupling to one application turns a developer tool into a maintenance burden for every feature team. The Go source isn’t in this repository; see ADR-0017.

Crates

CrateResponsibility
xianyun-coreConfiguration loading, merging and validation; variable expansion; naming; ordered maps; topological sort
xianyun-pkiThe dev CA and the trust store integrations
xianyun-executorThe CommandRunner trait, logging, the log-filter terminal UI, small file and data-directory helpers
xianyun-backendsStateless wrappers for docker, git, helm, /etc/hosts, kubectl, and kustomize
xianyun-clusterCluster drivers, DNS zone discovery, kubeconfig handling
xianyun-deployDeployers, reloaders, the orchestrator, the registry cache, diagnostics
xianyun-updateRepository updates and lifecycle hooks
xianyun-infraEmbedded Kubernetes overlays (the dev CA and the Traefik TLS store)
xianyun-cliThe xianyun binary: command tree and wiring
xianyun-testutilA shared mock CommandRunner for tests
xianyun-mcpThe MCP server

The API reference documents each crate’s public items. Each crate’s PORT_NOTES.md records decisions made while porting it from Go; the source is authoritative where those notes have fallen behind.

What xianyun up does

In the order the code runs them (crates/xianyun-cli/src/commands/up.rs):

  1. Load the configuration.
  2. Start the managed registry cache, if one is configured. A failure here is a warning, not an error (ADR-0015).
  3. Start the cluster through the driver, then fetch its kubeconfig and wait for the API to answer.
  4. Resolve the cluster’s DNS domain.
  5. Clone the repositories that edit-mode deployers and reloaders need.
  6. Prepare the dev CA.
  7. Run edit_mode_prepare hooks.
  8. Deploy every deployer, in topological order of needs.

xianyun run then starts the reloaders.

Patterns

Stateless backends, injected runner. Backends are thin wrappers around external tools and hold nothing but an injected Arc<dyn CommandRunner>. Managers orchestrate and hold state, and call tools only through a backend. This is the boundary that makes unit testing possible: inject the shared mock and test a manager’s logic with no helm, kubectl, or docker installed. A manager shouldn’t depend on a concrete backend type, only on its trait. Some paths bypass the runner; ADR-0004 lists them.

One registry shape per domain. Cluster drivers, deployers, and reloaders each have a trait and a global registry of factories (register, new_manager, list_types). Nothing registers itself on startup in Rust, so xianyun_cli::drivers::ensure_registered does it explicitly. It registers the drivers and the three deployers (helm, kustomize, namespace). Registration was missing for the deployers until 2026-10-14, so every configuration with one failed with an unknown-type error (GAP-001, closed); crates/xianyun-cli/tests/deploy_registry.rs now fails if one goes missing.

Conditionals behind the interface. Platform selection belongs inside an implementation, not in callers. driver: auto resolves to k3d or Colima inside driver/auto.rs, and the rest of the code never branches on the operating system. Capability flags on registries are the wrong tool; use configuration (for example cluster.k3d.k3s.disable).

A virtual kubeconfig. Xianyun doesn’t read or write your ~/.kube/config as part of a workflow. Each backend call gets a temporary kubeconfig built from the driver’s output, passed through KUBECONFIG, so Xianyun’s clusters stay isolated from your global config and concurrent operations are safe. The drivers enforce this: Colima starts with --activate=false and k3d with --kubeconfig-update-default=false. The one exception is xianyun set-kube-context, which merges into your kubeconfig because you ran it. The temporary files are created with default permissions today (TM-KUBE-002).

Infrastructure as ordinary deployers. Cert-manager, ingress controllers, and operators are declared by the project as regular deployers. Xianyun contributes none. Two Kustomize overlays are built into the binary, builtin:dev-ca and builtin:traefik-tlsstore, because HTTPS is a prerequisite for production-like local development, and each still needs an explicit deployer to install it. Don’t add a built-in overlay unless it directly enables Xianyun’s own job.

Config as data. Configuration is loaded as a JSON value, merged, expanded, validated against an embedded schema, and only then turned into typed structs (ADR-0009). Application config belongs in the config file, not environment variables, which bypass schema validation. The exception is meta-configuration that decides which file to load.

Concurrency with a join guarantee. xianyun run supervises reloaders with a tokio::task::JoinSet, so every task has finished before success is reported.

Errors as values. Library code returns typed errors. Only the entry point turns them into a process exit code, so no business logic calls std::process::exit.

An allowlisted agent surface. The MCP server exposes a reviewed list and fails the build when a command isn’t classified (ADR-0005).

How a configuration loads

The steps in crates/xianyun-core/src/config/caproni.rs, in order:

  1. Read the file and parse it into a generic structure.
  2. Resolve extends: fragments and merge them under the base.
  3. Merge xianyun.local.yaml, if present.
  4. Detect the version field, which is required.
  5. Load xianyun-project.yaml from each edit-mode repository directory.
  6. Seed the variable resolver from an unvalidated decode, and resolve the profile name so the predefined variables exist.
  7. Resolve variable definitions, then expand ${VAR} across the merged tree.
  8. Validate against schema/config.v2.schema.json.
  9. Decode into typed structs, apply defaults, and upgrade to the latest version.
  10. Fetch deployer files that came from remote fragments.
  11. Run semantic validation.

Changing the schema

  1. Update schema/config.v2.schema.json.
  2. Update the Rust types in crates/xianyun-core/src/config/.
  3. Add or update a file in examples/, and run xianyun config validate against it. CI does this for every top-level example.
  4. Update the configuration reference if the change is visible to users.

Mark a field required only if it has no sensible default.

Testing

Managers are unit-tested by injecting xianyun_testutil::MockCommandRunner, which supports first-in-first-out, predicate-matched, and repeating expectations. The NixOS VM tests cover the compiled binary end to end; see Testing. Write the test first, and don’t merge a change that lowers coverage.

Anti-patterns

Anti-patternWhy it breaks things
Hardcoded resource, namespace, or secret namesCouples the tool to one application
Generating manifests or config templatesXianyun becomes another configuration system next to Helm
Business logic in the CLI crateUntestable without the command-line framework
Global mutable stateHidden dependencies; the registries are the one sanctioned exception, and they’re written once at startup
Environment variables for application configBypasses validation, discovery, and migration
Platform conditionals in front of an interfaceThey belong inside an implementation
Returning raw command output from a backendBackends return structured data
Capability flags on a registryUse configuration
#![allow(dead_code)]Dead code usually means a partial implementation to finish