Clan Exports Guide
Clan exports provide a powerful system for sharing structured data between machines, instances, and roles within your Clan infrastructure. This is essential for building distributed systems like Kubernetes clusters or VPN meshes where nodes need to discover each other dynamically.
1. The Concept of Exports
When a machine evaluates a clanService, it can "export" data. This data is collected globally by the Clan flake into an attribute set (clan.exports). Other machines can then query this global set to configure themselves.
Exports are strictly typed using Export Interfaces. By default, Clan provides interfaces like endpoints, peer, networking, auth, dataMesher, and generators.
2. Creating a Custom Export Interface
If the built-in interfaces don't fit your needs (e.g., you need to share Kubernetes cluster IDs, IP addresses, and ports), you can create a custom export interface.
Custom interfaces are defined in your flake's clan configuration (often located in flake-parts/default.nix).
# flake-parts/default.nix
{
flake = {
clan = {
# Define custom export interfaces here
exportInterfaces.kubernetesMesh = { lib, ... }: {
options.clusterId = lib.mkOption {
type = lib.types.int;
description = "The unique ID of the Kubernetes cluster";
};
options.address = lib.mkOption {
type = lib.types.str;
description = "The IP address of the cluster";
};
};
modules = {
"@andrewthomaslee/kubernetes" = ../clanServices/kubernetes;
};
};
};
}
3. Exporting Data from a Service
In your service definition (e.g., clanServices/kubernetes/default.nix), you must declare which interfaces your service exports using manifest.exports.out.
Then, use mkExports within perInstance (or perMachine) to publish the data.
# clanServices/kubernetes/default.nix
{ clanLib, lib, ... }: {
_class = "clan.service";
manifest.name = "kubernetes";
# Declare the custom interface
manifest.exports.out = [ "kubernetesMesh" ];
roles.server = {
perInstance = { settings, mkExports, ... }: {
# Export the structured data
exports = mkExports {
kubernetesMesh.clusterId = settings.id;
kubernetesMesh.address = settings.address;
};
nixosModule = { ... }: {
# Local machine configuration...
};
};
};
}
4. Sharing Data Between Instances of the Same Service
To read exported data, you use clanLib.selectExports.
Crucial Detail: selectExports requires a predicate function that filters scopes, not an attribute set as older documentation might suggest.
To share data across multiple instances of the same clanService (e.g., cluster-1 and cluster-2), filter the scope by serviceName.
roles.server = {
perInstance = { exports, clanLib, ... }: {
nixosModule = { ... }: {
# 1. Retrieve all exports for the "kubernetes" service across ALL instances
environment.etc."k8s-mesh.json".text = builtins.toJSON (
clanLib.selectExports (scope: scope.serviceName == "kubernetes") exports
);
};
};
};
When you evaluate this, the returned data structure will map the fully qualified scope keys to the exported data, allowing cluster-1 to see cluster-2's configuration:
{
"kubernetes:cluster-1:server:node-1": {
"kubernetesMesh": {
"address": "10.67.67.1",
"clusterId": 1
}
},
"kubernetes:cluster-2:server:node-2": {
"kubernetesMesh": {
"address": "10.67.67.2",
"clusterId": 2
}
}
}
5. Advanced Filtering and Consuming Exports
The clanLib.selectExports function takes a predicate function that evaluates the scope of each export.
The scope object contains four properties parsed from the export's internal key:
- serviceName
- instanceName
- roleName
- machineName
You can filter exports by combining these properties logically.
# Filter by Service AND Instance (Only nodes in the same cluster instance)
clanLib.selectExports (scope:
scope.serviceName == "kubernetes" &&
scope.instanceName == instanceName
) exports
# Filter by Service AND Role (Only get data from 'server' nodes)
clanLib.selectExports (scope:
scope.serviceName == "kubernetes" &&
scope.roleName == "server"
) exports
Best Practices for Consuming Exported Data
selectExports returns an attribute set where the keys are the internal scope keys (e.g., "kubernetes:cluster-1:server:node-1") and the values are the structured exported data.
To consume this, use standard Nix library functions like lib.attrValues or lib.mapAttrsToList to extract the exact data you need.
# Example: Extracting all cluster IP addresses from the exports
let
# 1. Filter the exports
kubernetesExports = clanLib.selectExports (scope: scope.serviceName == "kubernetes") exports;
# 2. Extract just the values (ignoring the scope keys)
exportValues = lib.attrValues kubernetesExports;
# 3. Map over the values to extract the specific nested data
clusterAddresses = map (data: data.kubernetesMesh.address) exportValues;
in {
# Result: [ "10.67.67.1" "10.67.67.2" ]
environment.etc."cluster-ips.txt".text = lib.concatStringsSep "\n" clusterAddresses;
}
This pattern of Filter -> Extract Values -> Map is the most robust way to consume structured data from clan.exports.