Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
41c93c9
build(flake): pin nix-tf-schema
KiaraGrouwstra Aug 3, 2026
2bd0017
feat(modules/lib): add schema-driven resourceTypes generator
KiaraGrouwstra Aug 3, 2026
ee31283
feat(flake): expose provider-schema packages and an update app
KiaraGrouwstra Aug 3, 2026
acafb6b
feat(services/forgejo): vendor the provider schema
KiaraGrouwstra Aug 3, 2026
7295a64
test(services/forgejo): cover repository tracker and wiki blocks
KiaraGrouwstra Aug 3, 2026
dc214ea
refactor(services/forgejo): extract test fixtures into fixtures.nix
KiaraGrouwstra Aug 3, 2026
a466e91
style(modules/lib): apply nixfmt to the schema generator
KiaraGrouwstra Aug 3, 2026
f4de973
refactor(services/forgejo): derive resourceTypes from the vendored sc…
KiaraGrouwstra Aug 3, 2026
b51d1b9
feat(services/forgejo): add schema coverage and options-doc checks
KiaraGrouwstra Aug 3, 2026
d596dab
docs(services/forgejo): document schema-derived resources
KiaraGrouwstra Aug 3, 2026
a427db6
feat(services/keycloak): vendor the provider schema
KiaraGrouwstra Aug 3, 2026
ddf3e32
fix(services/keycloak): drop three resources absent from provider 5.8.0
KiaraGrouwstra Aug 3, 2026
c327056
refactor(services/keycloak): extract test fixtures into fixtures.nix
KiaraGrouwstra Aug 3, 2026
5bcc4b3
refactor(services/keycloak): derive resourceTypes from the vendored s…
KiaraGrouwstra Aug 3, 2026
af9f9af
feat(services/keycloak): support resources new in provider 5.8.0
KiaraGrouwstra Aug 3, 2026
758f466
docs(services/keycloak): document schema-derived resources
KiaraGrouwstra Aug 3, 2026
e890701
docs(CLAUDE.md): rewrite the provider contract for schema-driven gene…
KiaraGrouwstra Aug 3, 2026
671b033
docs(CLAUDE.md): correct the stale keycloak status rows
KiaraGrouwstra Aug 3, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
224 changes: 166 additions & 58 deletions CLAUDE.md

Large diffs are not rendered by default.

17 changes: 17 additions & 0 deletions flake.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

201 changes: 197 additions & 4 deletions flake.nix
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,18 @@

inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";

# Generic Terraform-schema <-> Nix conversion helpers, used to derive each
# pairing's resource surface from its vendored provider schema.
#
# Pulled in source-only (`flake = false`): its own flake builds `lib` from
# *its* nixpkgs pin for x86_64 alone, and we evaluate for aarch64 too. We
# instantiate `conversion.nix` against our own `pkgs` instead, so there is one
# nixpkgs in play and both systems work.
inputs.nix-tf-schema = {
url = "git+https://git.fediversity.eu/fediversity/nix-tf-schema";
flake = false;
};

outputs =
inputs:
let
Expand All @@ -22,22 +34,200 @@
cfg
];
};

# the pairings, by service name. each `lib.nix` exposes the packaged
# provider and its source address, which is all the schema tooling needs.
pairingLibs = pkgs: {
forgejo = import ./services/forgejo/lib.nix {
inherit pkgs;
nixTfSchema = inputs.nix-tf-schema;
};
keycloak = import ./services/keycloak/lib.nix {
inherit pkgs;
nixTfSchema = inputs.nix-tf-schema;
};
};

# A NixOS module cannot reach a flake input by path, so the schema library
# a pairing derives its resource surface from is threaded in as a module
# argument. Wrapping the exported modules keeps that plumbing invisible to
# anyone importing them.
withSchemaLib = module: {
imports = [ module ];
_module.args.nixTfSchema = inputs.nix-tf-schema;
};

# `<svc>-provider-schema`: the normalized schema of the pinned provider,
# extracted in a sandbox (`tofu providers schema -json`). This is the
# source for the vendored `services/<svc>/provider-schema.json`; it is
# never imported at eval time, because the flake evaluates for aarch64 too
# and IFD would mean running a foreign-arch provider binary.
providerSchemas =
pkgs:
let
conv = pkgs.callPackage "${inputs.nix-tf-schema}/conversion.nix" { };
in
lib.mapAttrs' (
svc: l:
lib.nameValuePair "${svc}-provider-schema" (
conv.mkProviderSchemaFile {
inherit (l) provider;
source = l.providerSource;
}
)
) (pairingLibs pkgs);

# `<svc>-rendered-fixtures`: each pairing's fixtures rendered through the
# real option system and renderer. Build before and after a change to the
# resource surface and diff -- an empty diff proves the wire format is
# untouched. The `baseUrl` here only has to be stable across the two
# builds; the module's own default is what a real system uses.
renderedFixtures =
pkgs:
let
libs = pairingLibs pkgs;
inherit (import ./modules/lib/render-fixtures.nix { inherit pkgs; }) renderFixtures;
urlOption = default: lib.mkOption { inherit default; };
in
{
forgejo-rendered-fixtures = renderFixtures {
name = "forgejo";
options = libs.forgejo.resourceOptions // {
baseUrl = urlOption "http://localhost:3000";
};
tfConfig = libs.forgejo.forgejoTfConfig;
fixtures = import ./services/forgejo/fixtures.nix;
};

keycloak-rendered-fixtures = renderFixtures {
name = "keycloak";
options = libs.keycloak.resourceOptions // {
baseUrl = urlOption "http://localhost:8080";
adminRealm = urlOption "master";
};
tfConfig = libs.keycloak.keycloakTfConfig;
fixtures = import ./services/keycloak/fixtures.nix;
};
};

# `<svc>-schema-coverage`: the pairing's coverage table. Building it forces
# the generator's drift assertions on their own, so schema drift fails a
# check that names it rather than whichever VM test happens to eval first.
#
# A pairing opts in by exporting `coverage`, i.e. by deriving its resource
# surface from the vendored schema.
schemaCoverage =
pkgs:
let
inherit (import ./modules/lib/schema-report.nix { inherit pkgs; }) mkCoverageReport;
in
lib.mapAttrs' (
svc: l:
lib.nameValuePair "${svc}-schema-coverage" (mkCoverageReport {
name = svc;
inherit (l) coverage;
})
) (lib.filterAttrs (_: l: l ? coverage) (pairingLibs pkgs));

# `<svc>-options-doc`: the pairing's user-facing option surface as
# `options.json`. Build it before and after a change and diff the two --
# that is the record of what the API gained, lost or retyped, which
# rendered `.tf.json` alone cannot show (an option nobody sets renders to
# nothing either way).
optionsDocs =
pkgs:
lib.mapAttrs' (
svc: l:
lib.nameValuePair "${svc}-options-doc"
(pkgs.nixosOptionsDoc {
inherit
(
(lib.evalModules {
modules = [ { options.services.${svc}.runtime = l.resourceOptions; } ];
})
)
options
;
warningsAreErrors = true;
}).optionsJSON
) (pairingLibs pkgs);

# `<svc>-schema-current`: the authoritative drift check. The eval-time
# assertions in `modules/lib/tf-schema.nix` compare version strings; this
# one compares content, so a provider that changes a schema without
# changing its version still fails CI.
#
# A pairing opts in by vendoring the file; once its `lib.nix` derives the
# resource surface from `schema.nix` the file is load-bearing and cannot
# quietly disappear again.
schemaChecks =
pkgs:
lib.mapAttrs'
(
name: fresh:
let
svc = lib.removeSuffix "-provider-schema" name;
in
lib.nameValuePair "${svc}-schema-current" (
pkgs.runCommand "${svc}-schema-current" { nativeBuildInputs = [ pkgs.diffutils ]; } ''
if ! diff -u ${./services}/${svc}/provider-schema.json ${fresh}; then
echo >&2
echo "services/${svc}/provider-schema.json is stale; run 'nix run .#update-provider-schemas'" >&2
exit 1
fi
touch "$out"
''
)
)
(
lib.filterAttrs (
name: _:
lib.pathExists (./services + "/${lib.removeSuffix "-provider-schema" name}/provider-schema.json")
) (providerSchemas pkgs)
);
in
{
# NixOS module entrypoint: enables a Nixpkgs service and reconciles its
# runtime state via OpenTofu after the primary unit starts.
nixosModules.default = ./modules;
nixosModules.forgejo = ./services/forgejo/module.nix;
nixosModules.keycloak = ./services/keycloak/module.nix;
nixosModules.default = withSchemaLib ./modules;
nixosModules.forgejo = withSchemaLib ./services/forgejo/module.nix;
nixosModules.keycloak = withSchemaLib ./services/keycloak/module.nix;

nixosConfigurations = lib.mapAttrs' (
name: cfg: lib.nameValuePair "example-${name}" (exampleSystem "x86_64-linux" cfg)
) examples;

packages = lib.mapAttrs (
system: _pkgs: lib.mapAttrs (_name: cfg: (exampleSystem system cfg).config.system.build.vm) examples
system: pkgs:
lib.mapAttrs (_name: cfg: (exampleSystem system cfg).config.system.build.vm) examples
// providerSchemas pkgs
// renderedFixtures pkgs
) inputs.nixpkgs.legacyPackages;

# `nix run .#update-provider-schemas` after a nixpkgs bump moves a
# provider: refresh the vendored schemas, then `nix flake check` reports
# every resource and attribute that changed.
apps = lib.mapAttrs (_system: pkgs: {
update-provider-schemas = {
type = "app";
program = lib.getExe (
pkgs.writeShellApplication {
name = "update-provider-schemas";
runtimeInputs = [ pkgs.git ];
text = ''
root=$(git rev-parse --show-toplevel)
${lib.concatLines (
lib.mapAttrsToList (name: drv: ''
install -Dm0644 ${drv} "$root/services/${lib.removeSuffix "-provider-schema" name}/provider-schema.json"
echo "updated services/${lib.removeSuffix "-provider-schema" name}/provider-schema.json"
'') (providerSchemas pkgs)
)}
'';
}
);
};
}) inputs.nixpkgs.legacyPackages;

checks = lib.mapAttrs (
system: pkgs:
import ./services/forgejo/checks.nix {
Expand All @@ -52,6 +242,9 @@
name: cfg:
lib.nameValuePair "example-${name}" (exampleSystem system cfg).config.system.build.toplevel
) examples)
// schemaChecks pkgs
// schemaCoverage pkgs
// optionsDocs pkgs
// {
formatting = inputs.self.formatter.${system}.check inputs.self;
}
Expand Down
44 changes: 44 additions & 0 deletions modules/lib/render-fixtures.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Render a pairing's fixtures through the real option system and renderer, as a
# build artifact.
#
# why: once the option surface is generated from a provider schema, "did this
# change what we send the provider?" can only be answered against rendered
# `.tf.json` -- option definitions alone say nothing about defaults, coercions,
# secret substitution, block wrapping or reference resolution. Build
# `<svc>-rendered-fixtures` before and after a change and diff the two; an empty
# diff is proof the wire format is untouched.
#
# The artifact carries no secrets: `<attr>File` inputs are host paths, and the
# renderer has already replaced their values with `${var.<id>}` references.
{ pkgs }:
let
inherit (pkgs) lib;
in
{
# name pairing name, used for the output file
# options the option set a fixture is evaluated against (the pairing's
# `resourceOptions` plus whatever its provider block reads)
# tfConfig the pairing's `<svc>TfConfig`: cfg -> { config; credentials; }
# fixtures fixture name -> a `services.<svc>.runtime` config fragment
renderFixtures =
{
name,
options,
tfConfig,
fixtures,
}:
pkgs.writeText "${name}-rendered-fixtures.json" (
builtins.toJSON (
lib.mapAttrs (
_: fixture:
tfConfig
(lib.evalModules {
modules = [
{ inherit options; }
fixture
];
}).config
) fixtures
)
);
}
67 changes: 67 additions & 0 deletions modules/lib/schema-report.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# Render a pairing's schema coverage as a markdown table, as a build artifact.
#
# why: two jobs at once. Building it forces the generator's drift assertions on
# their own, so `nix flake check` reports schema drift as a named failing check
# instead of burying it inside a VM test. And the output is the review artifact
# for "what does this pairing actually cover?" -- which resources are modelled,
# which are deliberately not, and how much of each resource's surface survives
# the overlay's corrections.
{ pkgs }:
let
inherit (pkgs) lib;

# markdown table cell for a list: `a`, `b` -- or an em dash when empty.
cell = xs: if xs == [ ] then "--" else lib.concatMapStringsSep ", " (x: "`${x}`") xs;
in
{
# name pairing name, used for the output file and the heading
# coverage the `coverage` attrset returned by ./tf-schema.nix
mkCoverageReport =
{ name, coverage }:
let
modelled = lib.length (lib.attrNames coverage.collections);

row =
collection: c:
"| `${collection}` | `${c.type}` | ${toString c.options} of ${toString c.attributes} | ${cell c.secrets} | ${cell c.refs} | ${cell c.omitted} |";

unsupportedSection =
if coverage.unsupported == { } then
"Every resource the provider offers is modelled.\n"
else
''
| Provider resource | Reason |
| --- | --- |
${lib.concatStringsSep "\n" (
lib.mapAttrsToList (type: reason: "| `${type}` | ${reason} |") coverage.unsupported
)}
'';
in
pkgs.writeText "${name}-schema-coverage.md" ''
# ${name} runtime resource coverage

Derived from `services/${name}/provider-schema.json`; regenerated by
`nix build .#checks.<system>.${name}-schema-coverage`. Do not edit.

Provider `${coverage.source}` ${coverage.version}: ${toString modelled} of ${toString coverage.schemaResources} resources modelled.

`Options` counts a collection's top-level options against the settable
top-level attributes the provider schema declares for it; the two differ
exactly by the attributes consumed by references and by the dropped ones.
Nested block attributes are options of their own submodule and are not
counted here.
${lib.optionalString (coverage.omitEverywhere != [ ])
"Dropped from every collection that declares them, on top of each `Omitted` column: ${cell coverage.omitEverywhere}."
}

## Modelled

| `${coverage.runtimePrefix}.<collection>` | Provider resource | Options | Secrets | References | Omitted |
| --- | --- | --- | --- | --- | --- |
${lib.concatStringsSep "\n" (lib.mapAttrsToList row coverage.collections)}

## Not modelled

${unsupportedSection}
'';
}
Loading