I find these `mkProvider` and `mkRequester` functions really nice. Also fixes doc generation related to https://github.com/NixOS/nixpkgs/pull/353513
196 lines
7.7 KiB
Markdown
196 lines
7.7 KiB
Markdown
# Contracts {#contracts}
|
|
|
|
A contract decouples modules that use a functionality from modules that provide it. A first
|
|
intuition for contracts is they are generally related to accessing a shared resource.
|
|
|
|
A few examples of contracts are generating SSL certificates, creating a user or knowing which files
|
|
and folders to backup.
|
|
Indeed, when generating certificates, the service using those do not care how they were created.
|
|
They just need to know where the certificate files are located.
|
|
|
|
A contract is made between a requester module and a provider module.
|
|
For example, a backup contract can be made between the Nextcloud service and the Restic service.
|
|
The former is the requester - the one wanted to be backed up -
|
|
and the latter is the provider of the contract - the one backing up files.
|
|
|
|
## Schema {#contracts-schema}
|
|
|
|
In practice, a contract is an attrset of options with a defined behavior.
|
|
Currently, the schema for a requester is:
|
|
|
|
```nix
|
|
let
|
|
inherit (lib) mkOption;
|
|
inherit (lib.types) submodule;
|
|
in
|
|
config.${requester}.${contractname} = submodule {
|
|
request = mkOption {
|
|
type = contracts.${contractname}.request;
|
|
default = {
|
|
# Values set by the requester
|
|
};
|
|
};
|
|
|
|
result = mkOption {
|
|
type = contracts.${contractname}.result;
|
|
};
|
|
};
|
|
```
|
|
|
|
For a provider, it is:
|
|
|
|
```nix
|
|
let
|
|
inherit (lib) mkOption;
|
|
inherit (lib.types) anything submodule;
|
|
in
|
|
config.${provider}.${contractname} = submodule ({ options, ... }: {
|
|
request = mkOption {
|
|
type = contracts.${contractname}.request;
|
|
};
|
|
|
|
result = mkOption {
|
|
type = contracts.${contractname}.result;
|
|
default = {
|
|
# Values set by the provider
|
|
# Can depend on values set by the requester through the `options` variable.
|
|
};
|
|
};
|
|
|
|
settings = mkOption {
|
|
type = anything;
|
|
};
|
|
});
|
|
```
|
|
|
|
## Contract Tests {#contracts-test}
|
|
|
|
To make sure all providers module of a contract have the same behavior,
|
|
generic NixOS VM tests exist per contract.
|
|
They are generic because they work on any module,
|
|
as long as the module implements the contract of course.
|
|
|
|
For example, the [generic test][generic] for backup contract is instantiated for Restic [here][restic test].
|
|
|
|
[generic]: @REPO@/modules/contracts/backup/test.nix
|
|
[restic test]: @REPO@/test/contracts/backup.nix
|
|
|
|
## Videos {#contracts-videos}
|
|
|
|
Two videos exist of me presenting the topic,
|
|
the first at [NixCon North America in spring of 2024][NixConNA2024]
|
|
and the second at [NixCon in Berlin in fall of 2024][NixConBerlin2024].
|
|
|
|
[NixConNA2024]: https://www.youtube.com/watch?v=lw7PgphB9qM
|
|
[NixConBerlin2024]: https://www.youtube.com/watch?v=CP0hR6w1csc
|
|
|
|
## Why do we need this new concept? {#contracts-why}
|
|
|
|
Currently in nixpkgs, every module needing access to a shared resource must implement the logic
|
|
needed to setup that resource themselves. Similarly, if the module is mature enough to let the user
|
|
select a particular implementation, the code lives inside that module.
|
|
|
|

|
|
|
|
This has a few disadvantages:
|
|
|
|
- This leads to a lot of **duplicated code**. If a module wants to support a new implementation of a
|
|
contract, the maintainers of that module must write code to make that happen.
|
|
- This also leads to **tight coupling**. The code written by the maintainers cannot be reused in
|
|
other modules, apart from copy pasting.
|
|
- There is also a **lack of separation of concerns**. The maintainers of a service must be experts
|
|
in all implementations they let the users choose from.
|
|
- Finally, this is **not extensible**. If you, the user of the module, want to use another
|
|
implementation that is not supported, you are out of luck. You can always dive into the module's
|
|
code and extend it, but that is not an optimal experience.
|
|
|
|
We do believe that the decoupling contracts provides helps alleviate all the issues outlined above
|
|
which makes it an essential step towards more adoption of Nix, if only in the self hosting scene.
|
|
|
|

|
|
|
|
Indeed, contracts allow:
|
|
|
|
- **Reuse of code**.
|
|
Since the implementation of a contract lives outside of modules using it,
|
|
using that implementation elsewhere is trivial.
|
|
- **Loose coupling**.
|
|
Modules that use a contract do not care how they are implemented,
|
|
as long as the implementation follows the behavior outlined by the contract.
|
|
- Full **separation of concerns** (see diagram below).
|
|
Now, each party's concern is separated with a clear boundary.
|
|
The maintainer of a module using a contract can be different from the maintainers
|
|
of the implementation, allowing them to be experts in their own respective fields.
|
|
But more importantly, the contracts themselves can be created and maintained by the community.
|
|
- Full **extensibility**.
|
|
The final user themselves can choose an implementation,
|
|
even new custom implementations not available in nixpkgs, without changing existing code.
|
|
- **Incremental adoption**.
|
|
Contracts can help bridge a NixOS system with any non-NixOS one.
|
|
For that, one can hardcode a requester or provider module to match
|
|
how the non-NixOS system is configured.
|
|
The responsability falls of course on the user to make sure both system agree on the configuration.
|
|
- Last but not least, **Testability**.
|
|
Thanks to NixOS VM test, we can even go one step further
|
|
by ensuring each implementation of a contract, even custom ones,
|
|
provides required options and behaves as the contract requires.
|
|
|
|

|
|
|
|
## Provided contracts {#contracts-provided}
|
|
|
|
Self Host Blocks is a proving ground of contracts. This repository adds a layer on top of services
|
|
available in nixpkgs to make them work using contracts. In time, we hope to upstream as much of this
|
|
as possible, reducing the quite thick layer that it is now.
|
|
|
|
Provided contracts are:
|
|
|
|
- [SSL generator contract](contracts-ssl.html) to generate SSL certificates.
|
|
Two providers are implemented: self-signed and Let's Encrypt.
|
|
- [Backup contract](contracts-backup.html) to backup directories.
|
|
One provider is implemented: [Restic][].
|
|
- [Database Backup contract](contracts-databasebackup.html) to backup database dumps.
|
|
One provider is implemented: [Restic][].
|
|
- [Secret contract](contracts-secret.html) to provide secrets that are deployed outside of the Nix store.
|
|
One provider is implemented: [SOPS][].
|
|
|
|
[restic]: blocks-restic.html
|
|
[sops]: blocks-sops.html
|
|
|
|
```{=include=} chapters html:into-file=//contracts-ssl.html
|
|
modules/contracts/ssl/docs/default.md
|
|
```
|
|
|
|
```{=include=} chapters html:into-file=//contracts-backup.html
|
|
modules/contracts/backup/docs/default.md
|
|
```
|
|
|
|
```{=include=} chapters html:into-file=//contracts-databasebackup.html
|
|
modules/contracts/databasebackup/docs/default.md
|
|
```
|
|
|
|
```{=include=} chapters html:into-file=//contracts-secret.html
|
|
modules/contracts/secret/docs/default.md
|
|
```
|
|
|
|
## Are there contracts in nixpkgs already? {#contracts-nixpkgs}
|
|
|
|
Actually not quite, but close. There are some ubiquitous options in nixpkgs. Those I found are:
|
|
|
|
- `services.<name>.enable`
|
|
- `services.<name>.package`
|
|
- `services.<name>.openFirewall`
|
|
- `services.<name>.user`
|
|
- `services.<name>.group`
|
|
|
|
What makes those nearly contracts are:
|
|
|
|
- Pretty much every service provides them.
|
|
- Users of a service expects them to exist and expects a consistent type and behavior from them.
|
|
Indeed, everyone knows what happens if you set `enable = true`.
|
|
- Maintainers of a service knows that users expects those options. They also know what behavior the
|
|
user expects when setting those options.
|
|
- The name of the options is the same everywhere.
|
|
|
|
The only thing missing to make these explicit contracts is, well, the contracts themselves.
|
|
Currently, they are conventions and not contracts.
|