Indeed those are interesting topic but as mentioned by @waffle8946 these are out of scope for the RFC.
About 1. Package Overrides as a Use Case did you know some modules allowed you to override the package? Like this one.
Indeed those are interesting topic but as mentioned by @waffle8946 these are out of scope for the RFC.
About 1. Package Overrides as a Use Case did you know some modules allowed you to override the package? Like this one.
Is that because youāre not looking to design a generic interface/implementation mechanism? It just felt like another use case to inform the design if that was the goal. But perhaps you were thinking about something much more specific? To me, your interface idea looks in principle quite generic and not limited to services, and Iād be delighted if the same mechanism could be applied to different domains. But, sure, it also makes sense to focus on a specific use case if thatās what you are looking to solve.
And in any case Iām happy about this proposal, because I think more thought out software engineering methodology is what Nix needs.
Yes, but we havenāt demonstrated this idea for services yet. Of course work can be done in nixpkgs in parallel, but thatās a whole other discussion to be had there. Adding that to this RFC means more to bikeshed about, at least from past observation.
If we can solve more issues even with this RFC, I would be delighted. But I donāt see at all how it could be applied to the problems you described. If you have any idea I would be happy to hear it!
Indeed the goal is to decouple services - modules in nixpkgs - using structural typing.
Ah, now I see what you mean! I wasnāt suggesting adding those use cases to this RFC (I agree that wouldnāt be a good idea)ājust that keeping additional use cases in mind might help refine the abstractions further. By thinking about corner cases from other domains, the resulting interfaces could become even neater and more versatile. To be clear, Iām not looking to hijack this RFC or propose competing ideas.
Fair enough! I think Iāll have to flesh out applying this to the cases I mentioned a bit more.
Perhaps I can clarify where Iām coming from.
The motivation for my thoughts comes from experiences with Nix the language, nixpkgs, and NixOS. Package overriding felt like an example that I encounter often, but it may have been more misleading than helpful. (I hadnāt even thought about NixOS modules, which perhaps underscores that Iām not communicating very clearly. ) In general, a lot of what feels incoherent to me seems to stem from the lack of tools to encourage structural typing or coherent interfaces. Without these, the path of least resistance often leads to ad hoc solutions.
So my thinking isnāt primarily about āmaking package overrides easierā but about encouraging some sound software engineering principles and providing tools to support them, primarily structural typing, contracts and the subsequent ability to test components in isolation (Iāve actually framed it more as Haskell style type classes, but I think thereās no big difference). I believe approaches like these will demonstrate their value when applied, and I hope people will over time apply them to other parts of the ecosystem, making everything more maintainable and cohesive.
This is why Iām excited by your RFC from a general perspective; I hope it will have a ripple effect beyond service decoupling.
Iām excited because all the comments so far are really positive. Thank you all for that!
If you see some issues, speak up! If not, Iāll be writing the real RFC soon with an accompanying draft PR.
So, first of all I do think that having a way to decouple services themselves from the stuff they need around the is useful. One reason is that you donāt have all of it one the same machine (larger setups donāt have applications and their DB servers on the same machine for instance).
In fact, I asked myself a similar question regarding when to provide nginx config for a module (Guidelines / Recommendations for when to configure nginx in a service module? Ā· Issue #277723 Ā· NixOS/nixpkgs Ā· GitHub), though I didnāt really like my conclusion which was essentially that it only makes sense if itās āactually necessaryā, i.e. if the config is more than just a fcgi_pass/proxy_pass (as itās in the case of Nextcloud and most PHP applications in general).
Before I give feedback, I have a few questions regarding my understanding of your proposal:
in the āFile Backup Contractā: which module is the ārequester sideā here? The nextcloud module? I wouldāve expected so (given that this request/provider thingy was suggested as solution to supporting >1 reverse proxy for instance). OTOH it seems as if the information is mostly used for the restic module and the Nextcloud module mainly gives information about what to back up.
So my question mainly boils down to āwhy is Nextcloud requesting a backup here?ā.
In the āSecrets contractā you add a āContractā section: this is what the requester-side requests, correct? And is this also what the Nextcloud module (assuming itās the requester) can use? I think this would answer my first question.
Now, where does the ārequestā on the sops module come from?
Then, to a few technical questions:
restartUnits = [ "phpfpm-nextcloud.service" ];
in the mkRequester
-call merged with additional declarations of the same option? (Assuming same priority, i.e. I donāt use mkForce
).I hope these questions make sense even though I probably missed some information.
Short of technical questions, thereās another thing Iād like to bring up: this is a powerful tool that brings a lot more flexibility. Now, nixpkgs is a project with a lot of things being done ad-hoc where the line between doing the right thing and pissing someone off is sometimes pretty thin.
From an RFC Iād wish some base rules on how maintainers (and users) should collaborate here. Obviously, this should just be a default ruleset, if provider/requester maintainers of a subsystem can agree on different rules, thatās fine. In the end I donāt want to constrain people, but make sure that the lifeās of maintainers from affected modules donāt get significantly harder:
I think I forgot about something on that end, but I canāt remember now.
I wrote these as questions on purpose because I donāt have good answers myself yet, but Iād like to share my thoughts and get other people to share their thoughts on that.
Any updates to this?
I didnāt have time yet to right a proper response to @Ma27. I wanted to do that first and then write the RFC and the draft PR. Thatās about it
In the āFile Backup Contractā: which module is the ārequester sideā here? The nextcloud module? I wouldāve expected so (given that this request/provider thingy was suggested as solution to supporting >1 reverse proxy for instance).
Indeed, Nextcloud is what I called the requester. You correctly noticed that I abused the vocabulary I chose arbitrarily and it doesnāt really fit here.
OTOH it seems as if the information is mostly used for the restic module and the Nextcloud module mainly gives information about what to back up.
Right. Here, Nextcloud doesnāt need any info from Restic. I suppose we could see this as a special case of a more general contract where the requester needs info from the provider and similarly in the opposite way.
So my question mainly boils down to āwhy is Nextcloud requesting a backup here?ā
TBH I canāt find a word/concept that express correctly that Nextcloud gives information about what files to backup as well as fits well with other backups.
In the āSecrets contractā you add a āContractā section: this is what the requester-side requests, correct? And is this also what the Nextcloud module (assuming itās the requester) can use? I think this would answer my first question.
Correct. The requester (Nextcloud) here tells the provider (Sops) the various properties the secret should have (mode, owner, group, etc.). Here, itās a bidirectional contract because the provider (Sops) will also tell the requester (Nextcloud) some info (the path where the secret will be located).
Now, where does the ārequestā on the sops module come from?
Iām not sure about what youāre asking here, so let me elaborate with the services.nextcloud.config.adminpassFile
option. Letās assume it uses the secret contract under the shb.nextcloud.adminPass
option and the Sops file contains the corresponding secret at nextcloud/adminpass
. The user would create the Sops secret like so, letting Sops know about Nextcloudās request:
shb.sops.secret."nextcloud/adminpass".request = config.shb.nextcloud.adminPass.request;
And then the user would let Nextcloud know about Sopsā result - the path of the secret.
adminPass.result = config.shb.sops.secret."nextcloud/adminpass".result;
Does that help clarify what you were asking about?
One other aspect thatās not obvious here is that the user complements the request when they define the sops.secret
. That option is an attrsOf
, so the user must give a name there which corresponds to where the secret lives in the Sops file.
So you have:
- In the repo you linked you define the options on the requester-side in a function. Is it somehow possible to āinjectā additional options into the requester from somewhere else? E.g. itās possible to inject additional options into the option tree (including submodules) from other modules, hence the questions.
Not readily with the functions. Like you mention later, the requesters and providers use their own sub-modules. Until now, I didnāt see the use for adding options not part of the original contract to this sub-module. If the requester needs two contracts, I think it makes sense to have two of those sub-modules.
Itās extremely probable Iām wrong on this point. And Iād love if someone can show me a counter-example to work with. I can imagine we can merge the result of mkRequester
calls.
- Do you have any ideas on how to make sure that requesters and their providers are discoverable, e.g. through the manual? E.g. we generate documentation for all options, but I donāt see yet how & where these functions fit in.
I didnāt dive much into this yet. Itās annoying IMO to have all options repeated in the documentation for each requester/provider contract. Taking the shb.nextcloud.adminPass
secret contract example, we should just see in the doc nextcloud.adminPass
and type = contract.secretContract;
with a link to the contract. Something like that. Currently, we see all options as you can see in the doc of my project. We can probably make this happen by using a new argument to the submodule
function?
We will also want to build an index of all requester services and provider services using a particular contract. One way to make this happen is to have this new submodule
argument in the style of contract = nullOr str;
which specifies which contract the submodule belongs to. I donāt like this solution because it tramples on structural typing. It also makes it necessary to think about namespacing. If one contract is called backup
, does that mean itās the true and only backup contract? How would we name others? As Iām writing this, I realize we need to name a contract and Iām doing that already. So maybe this is okay.
I agree itās an important to get this right from the start.
- Given the current implementations, how well does error reporting work? Iām a little afraid that this is a potential source for even more obscure errors.
I donāt think itās worse, but it clearly doesnāt help that the contract name is not appearing anywhere in the error message.
Here also, we should get this right from the start.
nixpkgs is a project with a lot of things being done ad-hoc where the line between doing the right thing and pissing someone off is sometimes pretty thin.
Haha thatās definitely true My style is to lead by example. Itās foolish to want to make everything switch to this style in one PR and was never my intention. We should start small, very small and build from there.
Ideally, the first PR would introduce an āeasyā contract that provides value. IMO the best one currently for this is the backup contract. It adds value because thereās not much about backup in nixpkgs services and little ad-hoc pre-existing work on this. A bad contract to start would be the reverse proxy one. There should be about 3 to 5 services, each with a different maintainer, that agree to implement this backup contract. The goal here is to start a trend because more and more maintainers realize itās a good style. Iād of course love if other initiatives to create PRs for other contracts or services led by others.
Btw, Iām not talking about the draft PR accompanying the RFC. That PR should implement a few diverse contracts and cover a lot of edge cases, to be sure we get this right. That PR will probably be closed after we agree on it and from that would stem other PRs, the one I talked about above included.
In the end I donāt want to constrain people, but make sure that the lifeās of maintainers from affected modules donāt get significantly harder
Very good point, we should also get this right from the start.
- How do we expose maintainer information to e.g. the option search? I think itās neither useful if I support Nextcloud+httpd (given I hardly know anything about the latter) nor do I want to spend time on playing first-level support, i.e. forwarding bugreports for providers to the responsible people myself.
About triage, I think thatās a broader issue than just related to this RFC. One first step is we could surface the meta.maintainers
field on a submodule in the related options. For example, I donāt see the maintainers defined here anywhere in the options documentation. I searched in nixpkgs issues but couldnāt find anything related to surfacing that field.
Honestly, Iām not sure how to make it obvious to the user who to contact in case of an issue here. I see 3 groups of maintainers:
Of course, they could overlap. I would even expect it to be common for all or most of services relying on a contract to be part of the maintainer group of that contract. After all, those using it have high stakes in the contract being useful for them.
I think though that contracts will in time lower the maintenance burden in general thanks to reusability. The key here will be relying heavily on the NixOS generic tests for each contract. Ideally, the differences between the httpd provider and nginx provider for a given contract will be ironed out and you, the maintainer of Nextcloud which uses the reverse proxy contract as a requester, shouldnāt see the difference. And this would be enforced thanks to an extensive test suite that each provider will need to pass.
The beauty here is that those tests are generic. I mean that if we discover an issue with httpd and we add a test case for it, that test case will be automatically applied to all other providers at the same, maybe discovering some other bugs or at least avoiding some future regression. This is to me very appealing.
Everyone using a contract will benefit from shared knowledge.
I know this is a bit idealistic and implementations will always differ but thatās already the case in software in general. This wonāt be solved now but the reward for writing a test case for those generic tests will be substantially higher than currently. I hope that will be appealing to others.
In time, I think this will allow you, the Nextcloud maintainer, to have less work maintaining the integration will all supported reverse proxies.
Speaking of tests, I also really would like if we embraced using more web automation frameworks like selenium or playwright. I know itās tedious to implement and maintain as Iām doing that right now in my project. But the cost is maybe worth the effort if one test covers multiple providers. I can imagine itās usefulness for a LDAP or SSO contract for example.
One could even imagine creating a matrix out of the current Nextcloud test suite, running it for each reverse proxy provider.
Again, Iām maybe a bit idealistic here, but I know from experience I much prefer dealing with a failing test than with an angry customer - hum - I mean with an issue created by a user as understanding the underlying issue there is always much more time consuming.
- How long is it OK to wait on version updates for provider maintainers to fix their code?
Eh, thatās a tough one. Iām not sure we should be imposing hard constraints here but Iām admittedly not a maintainer of a big nixpkgs service relied upon by a lot of people. I was hoping you would have an idea on how to answer this
Another Nextcloud example: we have a bunch of conditionals in place that check for the Nextcloud version in nginx because of certain differences in how the configuration must look like.
The versions difference is really interesting. At first glance I would say this falls on the shoulders of the maintainers of the Nginx provider, freeing your shoulders from dealing with that. Which is one of the goal of contracts in the first place.
Generally, I think there should be a timeout when itās OK to (temporarily) mark a provider as broken (btw, how would we mark providers as broken?
). This is especially relevant for security releases that require changes in modules.
I think it will be impossible to update a contract as well as all the providers and requesters. We should probably treat this similarly to how updates to databases are handled:
In other words, any migration will be a multi step process.
Iāll admit I dont fully understand the rfc on the implementation level, so I may be off the mark here:
I think this is best explained with microvm. Looking at all those hypervisors, itās safe to reason this could be abstracted into a āhypervisorā contract. However, if you have a machine that requires a hypervisor with virtiofs support, only certain āvirtiofs capableā āhypervisorā providers can accommodate that. āVirtiofs capable hypervisorā would be the subset you want, but we canāt manually create a subset for every possible combination of hypervisor features.
Normally Iād use unions as well, but how do we describe a particular contract that can only exist when another is present? I.e. a union between āhypervisorā and āvirtiofs capableā is great, but āvirtiofs capableā can only exist if āhypervisorā already exists. I.e. it sounds to me like we need a flexible system to validate that a requester is requesting a valid contract, and that a producer is producing a valid contract, which to me sounds like we need contracts to come with rules to determine how they can be combined, excluded, or selected.
The first sulotion Iād reach for is feature flags, but maybe the existence of certain optional keys could be flags themselves? If no additional options are needed than they can just be keys to empty sets.
Alternatively contracts could come with a list of validation functions, and a special set of contract merging function (union, intersection, negation, and derived functions such as conditional and disjoint union) could be created which do the job on their name, but always merge the validation function list, which in turn can generate warnings or errors.
We also need to consider disjoint unions. For example Attic can use the local filesystem xor an s3 bucket, but not both at the same time.
I think this should be opened as a proper RFC in GitHub (maybe as a draft state?) to get more traction/eyes on it.
Agreed! Iām working on writing the RFC and the accompanying draft PR but itās not there yet.
On the bright side, Iām happy and proud to announce that this RFC as well as my project itās based upon are now sponsored by the NGI Zero Core fund: NLnet; SelfHostBlocks. I donāt know the details but NixOS Foundation is a sponsor and provides help for grantees, I found that pretty cool. I applied in July 2024 for the October batch, it got a bit delayed on their side but now itās done. This wonāt magically grant me more free time but will help me prioritize finishing this for sure.
Great news everyone. @fricklerhandwerk and I as well as @kiara and @lassulus hacked on this during the Zürich 25.05 ZHF hackathon and we made great progress. I invite you to read this section of the report Zürich 25.05 ZHF hackathon report. We created a repo with what we will propose to upstream. GitHub - fricklerhandwerk/module-interfaces
Before upstreaming though, Iāll be now working on migrating SelfHostBlocks (my project from which this originates from) to this new pattern and see if there are any tweaks needed to be made. For example, how does documentation look like and how well does this integrate with the generic NixOS tests for contracts? I already made some tests during the hackathon and things looks good.
The TL; DR: we have worked out a way for the module type system to make the following possible and type check:
{ config, ... }:
{
services.myservice.password.provider = sops.secrets."myservice-password";
}
Thatās right, itās a one way connection from the end userās perspective but there is still wiring in both directions happening behind the scenes.
As for such a contract, defining it is done this way:
{ lib, ... }:
let
inherit (lib) mkOption types;
in
{
config.interfaces.secrets = {
description = "generate a secret that is passed out of band to the nix store";
input = input: {
options.owner = mkOption {
type = types.str;
};
options.group = mkOption {
type = types.str;
default = "root";
};
options.mode = mkOption {
type = types.str;
default = "0400";
};
};
output = output: {
options.path = mkOption {
type = types.str;
};
};
};
}
Finally, using this contract as a consumer and provider is done this way:
{ lib, ... }:
let
inherit (lib) mkOption;
in
{
options = {
services.myservice.password = mkOption {
type = config.interfaces.password.consumer;
};
sops.secrets = mkOption {
type = config.interfaces.password.provider;
};
};
}
Iām leaving out a few details here, like how the consumer and provider actually access the input and outputs, but thatās all in the repo. Iām quite biased here, but I find this very slick. Iām really happy of this progress.
This looks like exactly the type of thing I need for a wirenix refactor Iām chewing on. What might stop me from using this pattern now in advance of the RFC (aside from a missing license in the repo )? It canāt be worse than the current untyped method of handling this Iām using.
@ttamttam1 In my opinion the pattern is sound, for what amounts to pure functions I think itās rather usable already. As noted in the report you canāt have the equivalent of side effects yet, i.e. computing a value and also manipulating config
. Not sure when I will find time to figure that out, but I need this and will almost certainly eventually sit down and just do it. Contributions appreciated of course!
Also thanks for the reminder, added a license.