!wfudwzqQUiJYJnqfSY:nixos.org

NixOS Module System

149 Members
30 Servers

Load older messages


SenderMessageTime
10 Nov 2024
@sbc64:matrix.orgsbc64 left the room.20:02:18
@ibizaman:matrix.orgibizaman

I have an option whose default depends on a value defined in the config.

options.shb.restic.databases = mkOption {
  type = attrsOf (submodule ({ name, config, ... }: {
    result = mkOption { type = submodule { options = {
      default = {
        restoreScript = config.settings.repository;
      };
    };};};
  };
};

(I'm skipping a few details in the type here, not sure what is important or not).
Everything works well AFAICT (at least the NixOS tests work fine) until I try to build the documentation of my project. And then I get:

The option `shb.restic.databases.<name>.settings' was accessed but has no value defined. Try setting the option.

Here is where the documentation code is evaluating the modules.

So I tried changing the default to use options instead of config like so: restoreScript = options.settings.value.repository;. The NixOS tests still work fine but I get the same error as above.

I also tried adding a dummy module inside the individualModuleOptionsDocs call that does something like so:

{
  config = {
    shb.restic.databases."<name>".settings = {
      repository = "...";
    };
  };
}

But that's not working either šŸ˜… What's working is if I use a hardcoded string for the default value of the options.

Here is the PR introducing the changes leading to that error. This Github action shows the error. More specifically, here's the type definition and default setting that causes an issue.
Any idea what I should do to keep this computed default value while being able to generate the documentation?

On a totally different topic, this PR introduces 2 contracts in the form of structural typing for backing up files and backing up databases. They are both implemented by Restic. The correct implementation of both contracts is enforced by 2 generic NixOS tests (here and here) and then the Restic implementation is verified here and here.

23:28:04
@ibizaman:matrix.orgibizaman *

I have an option whose default depends on a value defined in the config.

options.shb.restic.databases = mkOption {
  type = attrsOf (submodule ({ name, config, ... }: {
    result = mkOption { type = submodule { options = {
      default = {
        restoreScript = config.settings.repository;
      };
    };};};
  };
};

(I'm skipping a few details in the type here, not sure what is important or not. There's a link to the PR with the full code further down).
Everything works well AFAICT (at least the NixOS tests work fine) until I try to build the documentation of my project. And then I get:

The option `shb.restic.databases.<name>.settings' was accessed but has no value defined. Try setting the option.

Here is where the documentation code is evaluating the modules.

So I tried changing the default to use options instead of config like so: restoreScript = options.settings.value.repository;. The NixOS tests still work fine but I get the same error as above.

I also tried adding a dummy module inside the individualModuleOptionsDocs call that does something like so:

{
  config = {
    shb.restic.databases."<name>".settings = {
      repository = "...";
    };
  };
}

But that's not working either šŸ˜… What's working is if I use a hardcoded string for the default value of the options.

Here is the PR introducing the changes leading to that error. This Github action shows the error. More specifically, here's the type definition and default setting that causes an issue.
Any idea what I should do to keep this computed default value while being able to generate the documentation?

On a totally different topic, this PR introduces 2 contracts in the form of structural typing for backing up files and backing up databases. They are both implemented by Restic. The correct implementation of both contracts is enforced by 2 generic NixOS tests (here and here) and then the Restic implementation is verified here and here.

23:28:39
@ibizaman:matrix.orgibizaman *

I have an option whose default depends on a value defined in the config.

options.shb.restic.databases = mkOption {
  type = attrsOf (submodule ({ name, config, ... }: {
    result = mkOption { type = submodule { options = {
      default = {
        restoreScript = config.settings.repository;
      };
    };};};
  };
};

(I'm skipping a few details in the type here, not sure what is important or not. There's a link to the PR with the full code further down).
Everything works well AFAICT (at least the NixOS tests work fine) until I try to build the documentation of my project. And then I get:

The option `shb.restic.databases.<name>.settings' was accessed but has no value defined. Try setting the option.

Here is where the documentation code is evaluating the modules.

So I tried changing the default to use options instead of config like so: restoreScript = options.settings.value.repository;. The NixOS tests still work fine but I get the same error as above.

I also tried adding a dummy module inside the individualModuleOptionsDocs call that does something like so:

{
  config = {
    shb.restic.databases."<name>".settings = {
      repository = "...";
    };
  };
}

But that's not working either šŸ˜… What's working is if I use a hardcoded string for the default value of the options.

Here is the PR introducing the changes leading to that error. This Github action shows the error. More specifically, here's the type definition and default setting that causes an issue.
Any idea what I should do to keep this computed default value while being able to generate the documentation?

On a totally different topic, this PR introduces 2 contracts in the form of structural typing for backing up files and backing up databases. They are both implemented by Restic. The correct implementation of both contracts is enforced by 2 generic NixOS tests (here and here) and then the Restic implementation is verified here and here.

23:32:16
@h7x4:nani.wtfh7x4 I've usually set defaultText to reflect where it's pulling it's default from. Would that work for you? 23:32:48
@ibizaman:matrix.orgibizamanYessssssssss that worked!!23:36:06
@ibizaman:matrix.orgibizamanI'm happy to go to bed on a positive note, I'll post the update tomorrow :) Thanks!!23:36:53
@h7x4:nani.wtfh7x4Great!23:37:30
11 Nov 2024
@phanirithvij:matrix.orgloudgolem joined the room.08:05:18
@mattsturg:matrix.orgMatt Sturgeon Specifically, if your default is dynamic you probably want defaultText with a literalExpression or literalMD that demonstrates how the default is evaluated. 13:28:36
13 Nov 2024
@inayet:matrix.orginayet joined the room.22:15:46
14 Nov 2024
@nbp:mozilla.orgnbp

ibizaman: I definitely agree on the fact that we need contracts, at the very least for cases where we have multiple implementations. However, I do not know what you attempted to do with mkOption and I want to suggest a very simple idea instead: ā€œoptions are already one form of contractsā€, there is no need to add an additional layer on top of mkOption, and most people should never look into mkOption.

On the other hand imagine the following:

config.contracts.reverse_proxy.<name> = { … };

This could be what is targetted by requesters which are looking for having a reverse proxy.

But how does these contract get honored? We could add an enum type, where each implementation provide a new entry in the enumerated type of the contract as a provider. If the provider is selected for the given contract, then we can forward the configuration to the provider option.

15:10:05
@nbp:mozilla.orgnbp The default implementation of the requester could provide defaults such as "ngnix", but then a user could mkForce it to apache/httpd. 15:11:53
@ibizaman:matrix.orgibizaman

nbp: thanks for the feedback! First I want to say I'm convinced these contracts will really help in nixpkgs but I'm still experimenting on the UX side. So don't take my mkOption experience as something set in stone. I made other contracts without those and they work better I find, so I'll probably remove that experiment at some point.

I considered enums at the start, but the problem I have with those is it's not easily extendable by the end user without modifying nixpkgs directly. At least I don't think so?

What I have currently is the following. My "best" and most recent contracts are currently the backup and database backup contracts. The former is for backing up files, the latter for backing up the output of a command (like pg_dump) without going through an intermediate file. (writing this down, I should probably find a better name than database backup)

This is the backup contract and the database backup one I have now. They're both just attrsets with some pre-defined schema: { requestType = submodule { ... }; resultType = submodule { ... }; }.
The backup contract essentially takes in a list of directories (in the request attribute) and produces (in the result attribute) a backup systemd service and a restore script. This is quite generic and describes IMO the essence of backing up something. For the database backup, it takes in a backup command and a restore command and again produces the backup service and the restore script.

A requester of the backup contract, for example Nextcloud which wants to be backed up, would then provide a shb.nextcloud.backup attribute which uses the requestType. The trick here is that the Nextcloud module fills out the default values of the type. You can see it sets the attributes expected by the contract - user, sourceDirectories and excludePatterns - to something that suits Nextcloud.

A provider of the backup contract, for example Restic, would provide an attribute that uses the requestType and the resultType, here the shb.restic.instances attribute. Here again I use the default values trick to let Restic set the correct name for the backup script and restore script in the result attribute. You can also see that Restic added a settings attribute next to the request and result ones from the contract. This is where each provider can add specific options that are essential for the provider (like for Restic, you must configure the repository where to backup) but are not mandated by the contract.
It would be cool to actually have an enforced schema with this request, result and settings trio but I didn't think yet if it's a good idea nor how it would work.

Putting both sides of the contract together is the end user's responsibility. Here, for backing up Nextcloud files with Restic, it looks like so:

shb.restic.instances."nextcloud" = {
  request = config.shb.nextcloud.backup;
  settings.enable = true;
};

Assuming settings.enable defaults to true, you get a one-liner:

shb.restic.instances."nextcloud".request = config.shb.nextcloud.backup;

The beauty here is that if you had a Vaultwarden instance you want to backup, you can just add the following snippet without knowing anything about how to backup Vaultwarden:

shb.restic.instances."vaultwarden".request = config.shb.vaultwarden.backup;

Similarly, if you wanted to also use BorgBackup to backup Nextcloud and Vaultwarden, you can also just copy paste the snippets and just s/restic/borgbackup:

shb.restic.instances."nextcloud".request = config.shb.nextcloud.backup;
shb.restic.instances."vaultwarden".request = config.shb.vaultwarden.backup;
shb.borgbackup.instances."nextcloud".request = config.shb.nextcloud.backup;
shb.borgbackup.instances."vaultwarden".request = config.shb.vaultwarden.backup;

If you're curious, backing up Vaultwarden is not necessarily complicated. But still, hiding it from the end user is nice.

But now comes the even better part IMO. How do you make sure the Restic and BorgBackup are equivalent (from the contract perspective)? With generic tests! Here's the one for backup contract and here's the one for database backup contract. They are generic because they're a function taking in a location to a module so you plug it in a module that provides the contract and the test will make sure it has the same behavior. Of course you actually need to call that function to test something and that is done here and here respectively for the backup and database backup contract.

These generic tests have a lot of value IMO.

So yeah, that's where I'm at with contracts. If you spelunk in the code of the project, you'll see I have a contract for secrets and ssl. They predate this request, result and settings trio so I'll update them at some point.

One last thing, you'll note that the restore script and backup service in the result part of either backup contract is not really used. That is true. I mostly needed this to make the generic test actually work. But they're still nice for documentation if anything. One result that is actually used is for the secrets contract. Everything I explained above stays the same for this contract, if you want to see how the result is used (which for the secrets contract is the location of the secret on the filesystem), you can look at this doc I wrote.

16:27:03
@ibizaman:matrix.orgibizaman *

nbp: thanks for the feedback! First I want to say I'm convinced these contracts will really help in nixpkgs but I'm still experimenting on the UX side. So don't take my mkOption experience as something set in stone. I made other contracts without those and they work better I find, so I'll probably remove that experiment at some point.

I considered enums at the start, but the problem I have with those is it's not easily extendable by the end user without modifying nixpkgs directly. At least I don't think so?

What I have currently is the following. My "best" and most recent contracts are currently the backup and database backup contracts. The former is for backing up files, the latter for backing up the output of a command (like pg_dump) without going through an intermediate file. (writing this down, I should probably find a better name than database backup)

This is the backup contract and the database backup one I have now. They're both just attrsets with some pre-defined schema: { request = submodule { ... }; result = submodule { ... }; }. (I use requestType and resultType for the database backup contract, but I like it less).
The backup contract essentially takes in a list of directories (in the request attribute) and produces (in the result attribute) a backup systemd service and a restore script. This is quite generic and describes IMO the essence of backing up something. For the database backup, it takes in a backup command and a restore command and again produces the backup service and the restore script.

A requester of the backup contract, for example Nextcloud which wants to be backed up, would then provide a shb.nextcloud.backup attribute which uses the requestType. The trick here is that the Nextcloud module fills out the default values of the type. You can see it sets the attributes expected by the contract - user, sourceDirectories and excludePatterns - to something that suits Nextcloud.

A provider of the backup contract, for example Restic, would provide an attribute that uses the requestType and the resultType, here the shb.restic.instances attribute. Here again I use the default values trick to let Restic set the correct name for the backup script and restore script in the result attribute. You can also see that Restic added a settings attribute next to the request and result ones from the contract. This is where each provider can add specific options that are essential for the provider (like for Restic, you must configure the repository where to backup) but are not mandated by the contract.
It would be cool to actually have an enforced schema with this request, result and settings trio but I didn't think yet if it's a good idea nor how it would work.

Putting both sides of the contract together is the end user's responsibility. Here, for backing up Nextcloud files with Restic, it looks like so:

shb.restic.instances."nextcloud" = {
  request = config.shb.nextcloud.backup;
  settings.enable = true;
};

Assuming settings.enable defaults to true, you get a one-liner:

shb.restic.instances."nextcloud".request = config.shb.nextcloud.backup;

The beauty here is that if you had a Vaultwarden instance you want to backup, you can just add the following snippet without knowing anything about how to backup Vaultwarden:

shb.restic.instances."vaultwarden".request = config.shb.vaultwarden.backup;

Similarly, if you wanted to also use BorgBackup to backup Nextcloud and Vaultwarden, you can also just copy paste the snippets and just s/restic/borgbackup:

shb.restic.instances."nextcloud".request = config.shb.nextcloud.backup;
shb.restic.instances."vaultwarden".request = config.shb.vaultwarden.backup;
shb.borgbackup.instances."nextcloud".request = config.shb.nextcloud.backup;
shb.borgbackup.instances."vaultwarden".request = config.shb.vaultwarden.backup;

If you're curious, backing up Vaultwarden is not necessarily complicated. But still, hiding it from the end user is nice.

But now comes the even better part IMO. How do you make sure the Restic and BorgBackup are equivalent (from the contract perspective)? With generic tests! Here's the one for backup contract and here's the one for database backup contract. They are generic because they're a function taking in a location to a module so you plug it in a module that provides the contract and the test will make sure it has the same behavior. Of course you actually need to call that function to test something and that is done here and here respectively for the backup and database backup contract.

These generic tests have a lot of value IMO.

So yeah, that's where I'm at with contracts. If you spelunk in the code of the project, you'll see I have a contract for secrets and ssl. They predate this request, result and settings trio so I'll update them at some point.

One last thing, you'll note that the restore script and backup service in the result part of either backup contract is not really used. That is true. I mostly needed this to make the generic test actually work. But they're still nice for documentation if anything. One result that is actually used is for the secrets contract. Everything I explained above stays the same for this contract, if you want to see how the result is used (which for the secrets contract is the location of the secret on the filesystem), you can look at this doc I wrote.

16:28:45
@ibizaman:matrix.orgibizaman *

nbp: thanks for the feedback! First I want to say I'm convinced these contracts will really help in nixpkgs but I'm still experimenting on the UX side. So don't take my mkOption experience as something set in stone. I made other contracts without those and they work better I find, so I'll probably remove that experiment at some point.

I considered enums at the start, but the problem I have with those is it's not easily extendable by the end user without modifying nixpkgs directly. At least I don't think so?

What I have currently is the following. My "best" and most recent contracts are currently the backup and database backup contracts. The former is for backing up files, the latter for backing up the output of a command (like pg_dump) without going through an intermediate file. (writing this down, I should probably find a better name than database backup)

This is the backup contract and the database backup one I have now. They're both just attrsets with some pre-defined schema: { request = submodule { ... }; result = submodule { ... }; }. (I use requestType and resultType for the database backup contract, but I like it less).
The backup contract essentially takes in a list of directories (in the request attribute) and produces (in the result attribute) a backup systemd service and a restore script. This is quite generic and describes IMO the essence of backing up something. For the database backup, it takes in a backup command and a restore command and again produces the backup service and the restore script.

A requester of the backup contract, for example Nextcloud which wants to be backed up, would then provide a shb.nextcloud.backup attribute which uses the requestType. The trick here is that the Nextcloud module fills out the default values of the type. You can see it sets the attributes expected by the contract - user, sourceDirectories and excludePatterns - to something that suits Nextcloud.

A provider of the backup contract, for example Restic, would provide an attribute that uses the contracts.backup.request type and the contracts.backup.result type, here the shb.restic.instances attribute. Here again I use the default values trick to let Restic set the correct name for the backup script and restore script in the result attribute. You can also see that Restic added a settings attribute next to the request and result ones from the contract. This is where each provider can add specific options that are essential for the provider (like for Restic, you must configure the repository where to backup) but are not mandated by the contract.
It would be cool to actually have an enforced schema with this request, result and settings trio but I didn't think yet if it's a good idea nor how it would work.

Putting both sides of the contract together is the end user's responsibility. Here, for backing up Nextcloud files with Restic, it looks like so:

shb.restic.instances."nextcloud" = {
  request = config.shb.nextcloud.backup;
  settings.enable = true;
};

Assuming settings.enable defaults to true, you get a one-liner:

shb.restic.instances."nextcloud".request = config.shb.nextcloud.backup;

The beauty here is that if you had a Vaultwarden instance you want to backup, you can just add the following snippet without knowing anything about how to backup Vaultwarden:

shb.restic.instances."vaultwarden".request = config.shb.vaultwarden.backup;

Similarly, if you wanted to also use BorgBackup to backup Nextcloud and Vaultwarden, you can also just copy paste the snippets and just s/restic/borgbackup:

shb.restic.instances."nextcloud".request = config.shb.nextcloud.backup;
shb.restic.instances."vaultwarden".request = config.shb.vaultwarden.backup;
shb.borgbackup.instances."nextcloud".request = config.shb.nextcloud.backup;
shb.borgbackup.instances."vaultwarden".request = config.shb.vaultwarden.backup;

If you're curious, backing up Vaultwarden is not necessarily complicated. But still, hiding it from the end user is nice.

But now comes the even better part IMO. How do you make sure the Restic and BorgBackup are equivalent (from the contract perspective)? With generic tests! Here's the one for backup contract and here's the one for database backup contract. They are generic because they're a function taking in a location to a module so you plug it in a module that provides the contract and the test will make sure it has the same behavior. Of course you actually need to call that function to test something and that is done here and here respectively for the backup and database backup contract.

These generic tests have a lot of value IMO.

So yeah, that's where I'm at with contracts. If you spelunk in the code of the project, you'll see I have a contract for secrets and ssl. They predate this request, result and settings trio so I'll update them at some point.

One last thing, you'll note that the restore script and backup service in the result part of either backup contract is not really used. That is true. I mostly needed this to make the generic test actually work. But they're still nice for documentation if anything. One result that is actually used is for the secrets contract. Everything I explained above stays the same for this contract, if you want to see how the result is used (which for the secrets contract is the location of the secret on the filesystem), you can look at this doc I wrote.

16:31:16
@ibizaman:matrix.orgibizaman *

nbp: thanks for the feedback! First I want to say I'm convinced these contracts will really help in nixpkgs but I'm still experimenting on the UX side. So don't take my mkOption experience as something set in stone. I made other contracts without those and they work better I find, so I'll probably remove that experiment at some point.

I considered enums at the start, but the problem I have with those is it's not easily extendable by the end user without modifying nixpkgs directly. At least I don't think so?

What I have currently is the following. My "best" and most recent contracts are currently the backup and database backup contracts. The former is for backing up files, the latter for backing up the output of a command (like pg_dump) without going through an intermediate file. (writing this down, I should probably find a better name than database backup)

This is the backup contract and the database backup one I have now. They're both just attrsets with some pre-defined schema: { request = submodule { ... }; result = submodule { ... }; }. (I use requestType and resultType for the database backup contract, but I like it less).
The backup contract essentially takes in a list of directories (in the request attribute) and produces (in the result attribute) a backup systemd service and a restore script. This is quite generic and describes IMO the essence of backing up something. For the database backup, it takes in a backup command and a restore command and again produces the backup service and the restore script.

A requester of the backup contract, for example Nextcloud which wants to be backed up, would then provide a shb.nextcloud.backup attribute which uses the requestType. The trick here is that the Nextcloud module fills out the default values of the type. You can see it sets the attributes expected by the contract - user, sourceDirectories and excludePatterns - to something that suits Nextcloud.

A provider of the backup contract, for example Restic, would provide an attribute that uses the contracts.backup.request type and the contracts.backup.result type, here the shb.restic.instances attribute. Here again I use the default values trick to let Restic set the correct name for the backup script and restore script in the result attribute. You can also see that Restic added a settings attribute next to the request and result ones from the contract. This is where each provider can add specific options that are essential for the provider (like for Restic, you must configure the repository where to backup) but are not mandated by the contract.
It would be cool to actually have an enforced schema with this request, result and settings trio but I didn't think yet if it's a good idea nor how it would work.

Putting both sides of the contract together is the end user's responsibility. Here, for backing up Nextcloud files with Restic, it looks like so:

shb.restic.instances."nextcloud" = {
  request = config.shb.nextcloud.backup;
  settings.enable = true;
};

Assuming settings.enable defaults to true, you get a one-liner:

shb.restic.instances."nextcloud".request = config.shb.nextcloud.backup;

The beauty here is that if you had a Vaultwarden instance you want to backup, you can just add the following snippet without knowing anything about how to backup Vaultwarden:

shb.restic.instances."vaultwarden".request = config.shb.vaultwarden.backup;

Similarly, if you wanted to also use BorgBackup to backup Nextcloud and Vaultwarden, you can also just copy paste the snippets and just s/restic/borgbackup:

shb.restic.instances."nextcloud".request = config.shb.nextcloud.backup;
shb.restic.instances."vaultwarden".request = config.shb.vaultwarden.backup;
shb.borgbackup.instances."nextcloud".request = config.shb.nextcloud.backup;
shb.borgbackup.instances."vaultwarden".request = config.shb.vaultwarden.backup;

Of course, you'd need to configure Nextcloud, Vaultwarden, Restic and BorgBackup with specific options. But this snippet is just showing the pluming going on.

If you're curious, backing up Vaultwarden is not necessarily complicated. But still, hiding it from the end user is nice.

But now comes the even better part IMO. How do you make sure the Restic and BorgBackup are equivalent (from the contract perspective)? With generic tests! Here's the one for backup contract and here's the one for database backup contract. They are generic because they're a function taking in a location to a module so you plug it in a module that provides the contract and the test will make sure it has the same behavior. Of course you actually need to call that function to test something and that is done here and here respectively for the backup and database backup contract.

These generic tests have a lot of value IMO.

So yeah, that's where I'm at with contracts. If you spelunk in the code of the project, you'll see I have a contract for secrets and ssl. They predate this request, result and settings trio so I'll update them at some point.

One last thing, you'll note that the restore script and backup service in the result part of either backup contract is not really used. That is true. I mostly needed this to make the generic test actually work. But they're still nice for documentation if anything. One result that is actually used is for the secrets contract. Everything I explained above stays the same for this contract, if you want to see how the result is used (which for the secrets contract is the location of the secret on the filesystem), you can look at this doc I wrote.

16:33:13
@ibizaman:matrix.orgibizaman *

nbp: thanks for the feedback! First I want to say I'm convinced these contracts will really help in nixpkgs but I'm still experimenting on the UX side. So don't take my mkOption experience as something set in stone. I made other contracts without those and they work better I find, so I'll probably remove that experiment at some point.

I considered enums at the start, but the problem I have with those is it's not easily extendable by the end user without modifying nixpkgs directly. At least I don't think so?

What I have currently is the following. My "best" and most recent contracts are currently the backup and database backup contracts. The former is for backing up files, the latter for backing up the output of a command (like pg_dump) without going through an intermediate file. (writing this down, I should probably find a better name than database backup)

This is the backup contract and the database backup one I have now. They're both just attrsets with some pre-defined schema: { request = submodule { ... }; result = submodule { ... }; }. (I use requestType and resultType for the database backup contract, but I like it less).
The backup contract essentially takes in a list of directories (in the request attribute) and produces (in the result attribute) a backup systemd service and a restore script. This is quite generic and describes IMO the essence of backing up something. For the database backup, it takes in a backup command and a restore command and again produces the backup service and the restore script.

A requester of the backup contract, for example Nextcloud which wants to be backed up, would then provide a shb.nextcloud.backup attribute which uses the requestType. The trick here is that the Nextcloud module fills out the default values of the type. You can see it sets the attributes expected by the contract - user, sourceDirectories and excludePatterns - to something that suits Nextcloud.

A provider of the backup contract, for example Restic, would provide an attribute that uses the contracts.backup.request type and the contracts.backup.result type, here the shb.restic.instances attribute. Here again I use the default values trick to let Restic set the correct name for the backup script and restore script in the result attribute. You can also see that Restic added a settings attribute next to the request and result ones from the contract. This is where each provider can add specific options that are essential for the provider (like for Restic, you must configure the repository where to backup) but are not mandated by the contract.
It would be cool to actually have an enforced schema with this request, result and settings trio but I didn't think yet if it's a good idea nor how it would work.

Putting both sides of the contract together is the end user's responsibility. Here, for backing up Nextcloud files with Restic, it looks like so:

shb.restic.instances."nextcloud" = {
  request = config.shb.nextcloud.backup;
  settings.enable = true;
};

Assuming settings.enable defaults to true, you get a one-liner:

shb.restic.instances."nextcloud".request = config.shb.nextcloud.backup;

The beauty here is that if you had a Vaultwarden instance you want to backup, you can just add the following snippet without knowing anything about how to backup Vaultwarden:

shb.restic.instances."vaultwarden".request = config.shb.vaultwarden.backup;

Similarly, if you wanted to also use BorgBackup to backup Nextcloud and Vaultwarden, you can also just copy paste the snippets and just s/restic/borgbackup:

shb.restic.instances."nextcloud".request = config.shb.nextcloud.backup;
shb.restic.instances."vaultwarden".request = config.shb.vaultwarden.backup;
shb.borgbackup.instances."nextcloud".request = config.shb.nextcloud.backup;
shb.borgbackup.instances."vaultwarden".request = config.shb.vaultwarden.backup;

Of course, you'd need to configure Nextcloud, Vaultwarden, Restic and BorgBackup with specific options. But this snippet is just showing the pluming going on.

If you're curious, backing up Vaultwarden is not necessarily complicated. But still, hiding it from the end user is nice.

Using the database backup contract is as easy:

shb.borgbackup.databases."postgres".request = config.shb.postgresql.databasebackup;

Here's the PostgreSQL provider side if you're curious.

But now comes the even better part IMO. How do you make sure the Restic and BorgBackup are equivalent (from the contract perspective)? With generic tests! Here's the one for backup contract and here's the one for database backup contract. They are generic because they're a function taking in a location to a module so you plug it in a module that provides the contract and the test will make sure it has the same behavior. Of course you actually need to call that function to test something and that is done here and here respectively for the backup and database backup contract.

These generic tests have a lot of value IMO.

So yeah, that's where I'm at with contracts. If you spelunk in the code of the project, you'll see I have a contract for secrets and ssl. They predate this request, result and settings trio so I'll update them at some point.

One last thing, you'll note that the restore script and backup service in the result part of either backup contract is not really used. That is true. I mostly needed this to make the generic test actually work. But they're still nice for documentation if anything. One result that is actually used is for the secrets contract. Everything I explained above stays the same for this contract, if you want to see how the result is used (which for the secrets contract is the location of the secret on the filesystem), you can look at this doc I wrote.

16:35:34
@ibizaman:matrix.orgibizaman *

nbp: thanks for the feedback! First I want to say I'm convinced these contracts will really help in nixpkgs but I'm still experimenting on the UX side. So don't take my mkOption experience as something set in stone. I made other contracts without those and they work better I find, so I'll probably remove that experiment at some point.

I considered enums at the start, but the problem I have with those is it's not easily extendable by the end user without modifying nixpkgs directly. At least I don't think so?

What I have currently is the following. My "best" and most recent contracts are currently the backup and database backup contracts. The former is for backing up files, the latter for backing up the output of a command (like pg_dump) without going through an intermediate file. (writing this down, I should probably find a better name than database backup)

This is the backup contract and the database backup one I have now. They're both just attrsets with some pre-defined schema: { request = submodule { ... }; result = submodule { ... }; }. (I use requestType and resultType for the database backup contract, but I like it less).
The backup contract essentially takes in a list of directories (in the request attribute) and produces (in the result attribute) a backup systemd service and a restore script. This is quite generic and describes IMO the essence of backing up something. For the database backup, it takes in a backup command and a restore command and again produces the backup service and the restore script.

A requester of the backup contract, for example Nextcloud which wants to be backed up, would then provide a shb.nextcloud.backup attribute which uses the requestType. The trick here is that the Nextcloud module fills out the default values of the type. You can see it sets the attributes expected by the contract - user, sourceDirectories and excludePatterns - to something that suits Nextcloud.

A provider of the backup contract, for example Restic, would provide an attribute that uses the contracts.backup.request type and the contracts.backup.result type, here the shb.restic.instances attribute. Here again I use the default values trick to let Restic set the correct name for the backup script and restore script in the result attribute. You can also see that Restic added a settings attribute next to the request and result ones from the contract. This is where each provider can add specific options that are essential for the provider (like for Restic, you must configure the repository where to backup) but are not mandated by the contract.
It would be cool to actually have an enforced schema with this request, result and settings trio but I didn't think yet if it's a good idea nor how it would work.

Putting both sides of the contract together is the end user's responsibility. Here, for backing up Nextcloud files with Restic, it looks like so:

shb.restic.instances."nextcloud" = {
  request = config.shb.nextcloud.backup;
  settings.enable = true;
};

Assuming settings.enable defaults to true, you get a one-liner:

shb.restic.instances."nextcloud".request = config.shb.nextcloud.backup;

The beauty here is that if you had a Vaultwarden instance you want to backup, you can just add the following snippet without knowing anything about how to backup Vaultwarden:

shb.restic.instances."vaultwarden".request = config.shb.vaultwarden.backup;

Similarly, if you wanted to also use BorgBackup to backup Nextcloud and Vaultwarden, you can also just copy paste the snippets and just s/restic/borgbackup:

shb.restic.instances."nextcloud".request = config.shb.nextcloud.backup;
shb.restic.instances."vaultwarden".request = config.shb.vaultwarden.backup;
shb.borgbackup.instances."nextcloud".request = config.shb.nextcloud.backup;
shb.borgbackup.instances."vaultwarden".request = config.shb.vaultwarden.backup;

Of course, you'd need to configure Nextcloud, Vaultwarden, Restic and BorgBackup with specific options. But this snippet is just showing the pluming going on.

If you're curious, backing up Vaultwarden is not necessarily complicated. But still, hiding it from the end user is nice.

Using the database backup contract is as easy:

shb.borgbackup.databases."postgres".request = config.shb.postgresql.databasebackup;

Here's the PostgreSQL provider side if you're curious.

But now comes the even better part IMO. How do you make sure the Restic and BorgBackup are equivalent (from the contract perspective)? With generic tests! Here's the one for backup contract and here's the one for database backup contract. They are generic because they're a function taking in a location to a module so you plug in a module that provides the contract and the test will make sure it has the same behavior. Of course you actually need to call that function to test something and that is done here and here respectively for the backup and database backup contract.

These generic tests have a lot of value IMO.

So yeah, that's where I'm at with contracts. If you spelunk in the code of the project, you'll see I have a contract for secrets and ssl. They predate this request, result and settings trio so I'll update them at some point.

One last thing, you'll note that the restore script and backup service in the result part of either backup contract is not really used. That is true. I mostly needed this to make the generic test actually work. But they're still nice for documentation if anything. One result that is actually used is for the secrets contract. Everything I explained above stays the same for this contract, if you want to see how the result is used (which for the secrets contract is the location of the secret on the filesystem), you can look at this doc I wrote.

16:36:21
@ibizaman:matrix.orgibizaman *

nbp: thanks for the feedback! First I want to say I'm convinced these contracts will really help in nixpkgs but I'm still experimenting on the UX side. So don't take my mkOption experience as something set in stone. I made other contracts without those and they work better I find, so I'll probably remove that experiment at some point.

I considered enums at the start, but the problem I have with those is it's not easily extendable by the end user without modifying nixpkgs directly. At least I don't think so?

What I have currently is the following. My "best" and most recent contracts are currently the backup and database backup contracts. The former is for backing up files, the latter for backing up the output of a command (like pg_dump) without going through an intermediate file. (writing this down, I should probably find a better name than database backup)

This is the backup contract and the database backup one I have now. They're both just attrsets with some pre-defined schema: { request = submodule { ... }; result = submodule { ... }; }. (I use requestType and resultType for the database backup contract, but I like it less).
The backup contract essentially takes in a list of directories (in the request attribute) and produces (in the result attribute) a backup systemd service and a restore script. This is quite generic and describes IMO the essence of backing up something. For the database backup, it takes in a backup command and a restore command and again produces the backup service and the restore script.

A requester of the backup contract, for example Nextcloud which wants to be backed up, would then provide a shb.nextcloud.backup attribute which uses the requestType. The trick here is that the Nextcloud module fills out the default values of the type. You can see it sets the attributes expected by the contract - user, sourceDirectories and excludePatterns - to something that suits Nextcloud.

A provider of the backup contract, for example Restic, would provide an attribute that uses the contracts.backup.request type and the contracts.backup.result type, here the shb.restic.instances attribute. Here again I use the default values trick to let Restic set the correct name for the backup script and restore script in the result attribute. You can also see that Restic added a settings attribute next to the request and result ones from the contract. This is where each provider can add specific options that are essential for the provider (like for Restic, you must configure the repository where to backup) but are not mandated by the contract.
It would be cool to actually have an enforced schema with this request, result and settings trio but I didn't think yet if it's a good idea nor how it would work.

Putting both sides of the contract together is the end user's responsibility. Here, for backing up Nextcloud files with Restic, it looks like so:

shb.restic.instances."nextcloud" = {
  request = config.shb.nextcloud.backup;
  settings.enable = true;
};

Assuming settings.enable defaults to true, you get a one-liner:

shb.restic.instances."nextcloud".request = config.shb.nextcloud.backup;

The beauty here is that if you had a Vaultwarden instance you want to backup, you can just add the following snippet without knowing anything about how to backup Vaultwarden:

shb.restic.instances."vaultwarden".request = config.shb.vaultwarden.backup;

Similarly, if you wanted to also use BorgBackup to backup Nextcloud and Vaultwarden, you can also just copy paste the snippets and just s/restic/borgbackup:

shb.restic.instances."nextcloud".request = config.shb.nextcloud.backup;
shb.restic.instances."vaultwarden".request = config.shb.vaultwarden.backup;
shb.borgbackup.instances."nextcloud".request = config.shb.nextcloud.backup;
shb.borgbackup.instances."vaultwarden".request = config.shb.vaultwarden.backup;

Of course, you'd need to configure Nextcloud, Vaultwarden, Restic and BorgBackup with specific options. But this snippet is just showing the pluming going on.

If you're curious, backing up Vaultwarden is not necessarily complicated. But still, hiding it from the end user is nice.

Using the database backup contract is as easy:

shb.borgbackup.databases."postgres".request = config.shb.postgresql.databasebackup;

Here's the PostgreSQL provider side if you're curious.

But now comes the even better part IMO. How do you make sure the Restic and BorgBackup are equivalent (from the contract perspective)? With generic tests! Here's the one for backup contract and here's the one for database backup contract. They are generic because they're a function taking in a location to a module so you plug in a module that provides the contract and the test will make sure it has the same behavior. Of course you actually need to call that function to test something and that is done here for the backup and database backup contract.

These generic tests have a lot of value IMO.

So yeah, that's where I'm at with contracts. If you spelunk in the code of the project, you'll see I have a contract for secrets and ssl. They predate this request, result and settings trio so I'll update them at some point.

One last thing, you'll note that the restore script and backup service in the result part of either backup contract is not really used. That is true. I mostly needed this to make the generic test actually work. But they're still nice for documentation if anything. One result that is actually used is for the secrets contract. Everything I explained above stays the same for this contract, if you want to see how the result is used (which for the secrets contract is the location of the secret on the filesystem), you can look at this doc I wrote.

16:36:46
@nbp:mozilla.orgnbpenum are externally extendable in the module system. This is how desktopManagers / windowManagers are handled.16:40:38
@nbp:mozilla.orgnbp *

enum are externally extendable in the module system. This is how desktopManagers / windowManagers are handled. Or why they got added a while back.
The idea of enums is that every module can add the new entry to enable it-self.

type = lib.types.enum [ "this-service-name" ];

makes sense, as opposed to many other languages, because options can be extended by multiple modules. Thus if all providers were to add extra declarations, you could have an option which enumerate all services which could be used as a provider.

16:48:23
@nbp:mozilla.orgnbp

Here is an example, where a module declare an enum with no "implementation":
https://github.com/NixOS/nixpkgs/blob/9956df1047bba4e000354f77229b0066d7f364e0/nixos/modules/virtualisation/podman/network-socket.nix#L33-L39

And another module provide an implementation for it:
https://github.com/NixOS/nixpkgs/blob/9956df1047bba4e000354f77229b0066d7f364e0/nixos/modules/virtualisation/podman/network-socket-ghostunnel.nix#L12C50-L18

16:59:10
@nbp:mozilla.orgnbp The module which declare the enum [] is equivalent to a contract and the module which declares it as enum [ "its-own-name" ] is equivalent to a provider. 17:02:09
@ibizaman:matrix.orgibizamanThat’s interesting. I have no idea how to compare both solutions, what’s their respective drawbacks and advantages. I took the structural typing approach because I knew that from my programming in Python and Go and it sticked with me. One potential drawback is discoverability although there’s surely a way to remediate to that.19:12:56
15 Nov 2024
@zoechi:matrix.orgzoechi joined the room.10:17:04
@nazarewk:matrix.orgkdn joined the room.15:08:21
17 Nov 2024
@orzklv:matrix.orgOrzklv joined the room.02:28:12
@frontear:matrix.orgfrontear joined the room.02:38:28
@frontear:matrix.orgfrontearApologies in advance for the massive text block, I'm gonna drop this here because I suspect I'll eventually get more visibility from people who can answer this.02:56:13

Show newer messages


Back to Room ListRoom Version: 10