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.
1. The kind: inherit the shared base
Section titled “1. The kind: inherit the shared base”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 ].
2. The registry: instances of the kind
Section titled “2. The registry: instances of the kind”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 organizationden.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).
Nesting under another kind
Section titled “Nesting under another kind”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.
See also
Section titled “See also”den.schemareference — every attribute a kind entry accepts- Entities — how entities and scopes work
- Case Study: Access Control & Environments — a full custom registry (
environment) wired into host and user resolution