Skip to content

Define Your Own Entity Kind

host, user and home are not special-cased: each is a key of den.schema that inherits a shared base, plus a registry of instances that a policy resolves into the pipeline. A kind of your own — an organization, an environment, a site — is built the same way, in three steps.

den.schema.conf is the base every built-in kind inherits. den declares:

den.schema.host.inherits = [ den.schema.conf ];
den.schema.user.inherits = [ den.schema.conf ];
den.schema.home.inherits = [ den.schema.conf ];

Declare your kind the same way, then add its own options:

{ den, lib, ... }:
{
den.schema.organization = {
inherits = [ den.schema.conf ];
options.label = lib.mkOption { type = lib.types.str; };
};
}

Anything on den.schema.conf — your own shared options, and den’s hasAspect — now lands on every organization, as it does on every host. Shared defaults go on conf; kind-specific ones on the kind. inherits takes a list, so a kind can build on another kind as well: inherits = [ den.schema.conf den.schema.site ].

Instances live in an option you declare. Build its element type from the kind with den.lib.schema.mkInstanceType:

{ den, lib, ... }:
{
options.den.organizations = lib.mkOption {
type = lib.types.attrsOf (
den.lib.schema.mkInstanceType {
strict = den.schema.organization.isStrict;
} den.schema.organization
);
default = { };
};
config.den.organizations = {
acme.label = "Acme";
globex.label = "Globex";
};
}

Each instance gets name (its registry key), the kind’s options, everything inherited from conf, and hasAspect.

Pass strict = den.schema.<kind>.isStrict so the registry follows the kind’s strict mode: with den.schema.organization.isStrict = true, a typo such as den.organizations.acme.lable is refused instead of stored.

3. Resolution: make each instance an entity

Section titled “3. Resolution: make each instance an entity”

A registry holds data. To make each instance an entity — a scope in the pipeline, with its kind’s includes and policies — resolve it from a policy:

{ den, lib, config, ... }:
{
# flake -> organization: one entity scope per instance
den.policies.to-organizations =
_:
lib.mapAttrsToList (
_: organization: den.lib.policy.resolve.to "organization" { inherit organization; }
) config.den.organizations;
den.schema.flake.includes = [ den.policies.to-organizations ];
}

The binding name (organization) is the kind name, so aspects and policies take it as an argument the way they take host:

den.aspects.org-dns =
{ organization, ... }:
{
dns.records.${organization.name} = organization.label;
};
# fires once for every organization
den.schema.organization.includes = [ den.aspects.org-dns ];

From here the kind behaves like the built-in ones: den.schema.organization.includes and excludes apply to every instance, policies guarded on { organization, ... } fire per instance, and content can be collected with policy.instantiate (the terranix demo collects a custom class the same way).

To place a kind inside another entity the way user sits under host, set its parent and resolve it from the parent’s scope instead of from the flake:

den.schema.site.parent = "organization";
den.policies.organization-to-sites =
{ organization, ... }:
map (site: den.lib.policy.resolve.to "site" { inherit site; }) organization.sites;
den.schema.organization.includes = [ den.policies.organization-to-sites ];

parent places the kind in the entity DAG used for descendant fan-out and for class-module argument promotion, so a site scope also sees organization.

ContributeCommunitySponsor