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
| Crate | Responsibility |
|---|---|
xianyun-core | Configuration loading, merging and validation; variable expansion; naming; ordered maps; topological sort |
xianyun-pki | The dev CA and the trust store integrations |
xianyun-executor | The CommandRunner trait, logging, the log-filter terminal UI, small file and data-directory helpers |
xianyun-backends | Stateless wrappers for docker, git, helm, /etc/hosts, kubectl, and kustomize |
xianyun-cluster | Cluster drivers, DNS zone discovery, kubeconfig handling |
xianyun-deploy | Deployers, reloaders, the orchestrator, the registry cache, diagnostics |
xianyun-update | Repository updates and lifecycle hooks |
xianyun-infra | Embedded Kubernetes overlays (the dev CA and the Traefik TLS store) |
xianyun-cli | The xianyun binary: command tree and wiring |
xianyun-testutil | A shared mock CommandRunner for tests |
xianyun-mcp | The 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):
- Load the configuration.
- Start the managed registry cache, if one is configured. A failure here is a warning, not an error (ADR-0015).
- Start the cluster through the driver, then fetch its kubeconfig and wait for the API to answer.
- Resolve the cluster’s DNS domain.
- Clone the repositories that edit-mode deployers and reloaders need.
- Prepare the dev CA.
- Run
edit_mode_preparehooks. - 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:
- Read the file and parse it into a generic structure.
- Resolve
extends:fragments and merge them under the base. - Merge
xianyun.local.yaml, if present. - Detect the
versionfield, which is required. - Load
xianyun-project.yamlfrom each edit-mode repository directory. - Seed the variable resolver from an unvalidated decode, and resolve the profile name so the predefined variables exist.
- Resolve variable definitions, then expand
${VAR}across the merged tree. - Validate against
schema/config.v2.schema.json. - Decode into typed structs, apply defaults, and upgrade to the latest version.
- Fetch deployer files that came from remote fragments.
- Run semantic validation.
Changing the schema
- Update
schema/config.v2.schema.json. - Update the Rust types in
crates/xianyun-core/src/config/. - Add or update a file in
examples/, and runxianyun config validateagainst it. CI does this for every top-level example. - 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-pattern | Why it breaks things |
|---|---|
| Hardcoded resource, namespace, or secret names | Couples the tool to one application |
| Generating manifests or config templates | Xianyun becomes another configuration system next to Helm |
| Business logic in the CLI crate | Untestable without the command-line framework |
| Global mutable state | Hidden dependencies; the registries are the one sanctioned exception, and they’re written once at startup |
| Environment variables for application config | Bypasses validation, discovery, and migration |
| Platform conditionals in front of an interface | They belong inside an implementation |
| Returning raw command output from a backend | Backends return structured data |
| Capability flags on a registry | Use configuration |
#![allow(dead_code)] | Dead code usually means a partial implementation to finish |