-
-
Notifications
You must be signed in to change notification settings - Fork 20k
Contracts rfc functions #485453
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Contracts rfc functions #485453
Changes from all commits
ca00371
ab72ed6
45bcadf
da39055
e39ea8f
4a1814e
df410d5
be3b731
328978e
4df3c48
3e6a375
68983f4
3fb7107
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||
|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,62 @@ | ||||||||
| { lib }: | ||||||||
| let | ||||||||
| inherit (lib) optionalAttrs; | ||||||||
|
|
||||||||
| mkContractFunctions = | ||||||||
| { | ||||||||
| mkConsumerOptions, | ||||||||
| mkProviderOptions, | ||||||||
| }: | ||||||||
| { | ||||||||
| mkConsumer = inputDefaults: { | ||||||||
| options = { | ||||||||
| input = mkConsumerOptions inputDefaults; | ||||||||
|
|
||||||||
| output = mkProviderOptions { } // { | ||||||||
| visible = "shallow"; | ||||||||
| }; | ||||||||
| }; | ||||||||
| }; | ||||||||
|
Comment on lines
+11
to
+19
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. mkConsumer = inputDefaults:
lib.setDefaultModuleLocation modulePath {
input = mkConsumerOptions inputDefaults;
output = mkProviderOptions { } // {
visible = "shallow";
};
}; |
||||||||
|
|
||||||||
| mkProvider = | ||||||||
| { | ||||||||
| outputDefaults ? { }, | ||||||||
| providerOptions ? { }, | ||||||||
| }: | ||||||||
| { | ||||||||
| options = { | ||||||||
| input = mkConsumerOptions { } // { | ||||||||
| visible = "shallow"; | ||||||||
| }; | ||||||||
| } | ||||||||
| // optionalAttrs (outputDefaults != { }) { | ||||||||
| output = mkProviderOptions outputDefaults; | ||||||||
| } | ||||||||
| // optionalAttrs (providerOptions != { }) { | ||||||||
| inherit providerOptions; | ||||||||
| }; | ||||||||
| }; | ||||||||
|
|
||||||||
| # Used for documentation | ||||||||
| allOptions = { | ||||||||
| input = mkConsumerOptions { }; | ||||||||
| output = mkProviderOptions { }; | ||||||||
| }; | ||||||||
| }; | ||||||||
|
|
||||||||
| importContract = | ||||||||
| module: | ||||||||
| let | ||||||||
| importedModule = import module { inherit lib; }; | ||||||||
| in | ||||||||
| mkContractFunctions { | ||||||||
| inherit (importedModule) mkConsumerOptions mkProviderOptions; | ||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||
| } | ||||||||
| // { | ||||||||
| inherit (importedModule) description behaviorTest; | ||||||||
| }; | ||||||||
|
Comment on lines
+47
to
+57
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This loses the module's filepath, which'll lead to confusing messages for errors generated from within the contract's code. Along with my other suggestion of changing
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I tried setting an option wrong, specifically setting this one to It doesn't seem too bad to me. TBH after checking in nixpkgs I still have no idea how to use
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I mean if the contract itself has a bug, say during development. Not the user code. It confusingly points to
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. See my other review for a partial fix. |
||||||||
| in | ||||||||
| { | ||||||||
| filebackup = importContract ./filebackup.nix; | ||||||||
| secrets = importContract ./secrets.nix; | ||||||||
| } | ||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,177 @@ | ||
| { lib, ... }: | ||
| let | ||
| inherit (lib) mkOption; | ||
| inherit (lib.types) | ||
| listOf | ||
| nonEmptyListOf | ||
| submodule | ||
| str | ||
| ; | ||
|
|
||
| coerce = x: y: if x != null then x else y; | ||
| in | ||
| { | ||
| description = '' | ||
| Contract for backing up files. | ||
|
|
||
| The consumer dictates what [folders to backup](#opt-contracts.filebackup.input.sourceDirectories) | ||
| and the provider will execute the backup | ||
| and store a snapshot in a backup repository. | ||
| The exact details on how to configure acces to the repository | ||
| is specific for each provider. | ||
|
|
||
| Every provider exposes a [backup systemd service](#opt-contracts.filebackup.output.backupService) | ||
| and a [script](#opt-contracts.filebackup.output.restoreScript) to list snaphosts | ||
| and restore from a given snapshot. | ||
| ''; | ||
|
|
||
| mkConsumerOptions = | ||
| { | ||
| user ? "", | ||
| sourceDirectories ? [ "/var/lib/example" ], | ||
| sourceDirectoriesText ? null, | ||
| excludePatterns ? [ ], | ||
| beforeBackupHooks ? [ ], | ||
| afterBackupHooks ? [ ], | ||
| ... | ||
| }: | ||
| mkOption { | ||
| description = '' | ||
| Consumer part of the [filebackup contract](#opt-contracts.filebackup). | ||
| ''; | ||
|
|
||
| default = { }; | ||
|
|
||
| type = submodule { | ||
| options = { | ||
| user = mkOption { | ||
| description = '' | ||
| Unix user doing the backup. | ||
| ''; | ||
| type = str; | ||
| example = "vaultwarden"; | ||
| default = user; | ||
| }; | ||
|
|
||
| sourceDirectories = mkOption ( | ||
| { | ||
| description = "Directories to backup."; | ||
| type = nonEmptyListOf str; | ||
| example = "/var/lib/vaultwarden"; | ||
| default = sourceDirectories; | ||
| } | ||
| # This pattern is clumsy but necessary to support | ||
| # referencing to the config attrset from withing the options' defaults. | ||
| // lib.optionalAttrs (sourceDirectoriesText != null) { | ||
| defaultText = sourceDirectoriesText; | ||
| } | ||
| ); | ||
|
|
||
| excludePatterns = mkOption { | ||
| description = "File patterns to exclude."; | ||
| type = listOf str; | ||
| default = excludePatterns; | ||
| }; | ||
|
|
||
| beforeBackupHooks = mkOption { | ||
| description = "Hooks to run before backup."; | ||
| type = listOf str; | ||
| default = beforeBackupHooks; | ||
| }; | ||
|
|
||
| afterBackupHooks = mkOption { | ||
| description = "Hooks to run after backup."; | ||
| type = listOf str; | ||
| default = afterBackupHooks; | ||
| }; | ||
| }; | ||
| }; | ||
| }; | ||
|
|
||
| mkProviderOptions = | ||
| { | ||
| restoreScript ? null, | ||
| restoreScriptText ? null, | ||
| backupService ? null, | ||
| backupServiceText ? null, | ||
| }: | ||
| let | ||
| fallbackRestoreName = "restoreScript"; | ||
| fallbackBackupServiceName = "backup.service"; | ||
| in | ||
| mkOption { | ||
| description = '' | ||
| Consumer part of the [filebackup contract](#opt-contracts.filebackup). | ||
| ''; | ||
|
|
||
| default = { }; | ||
|
|
||
| type = submodule { | ||
| options = { | ||
| restoreScript = mkOption ( | ||
| { | ||
| description = '' | ||
| Name of script that can restore the given sourceDirectories. | ||
|
|
||
| To list snapshots, run: | ||
|
|
||
| ```bash | ||
| $ ${coerce (coerce restoreScriptText restoreScript) fallbackRestoreName} snapshots | ||
| ``` | ||
|
|
||
| To restore a given snapshot, run: | ||
|
|
||
| ```bash | ||
| $ ${coerce (coerce restoreScriptText restoreScript) fallbackRestoreName} restore latest | ||
| ''; | ||
| type = str; | ||
| } | ||
| // ( | ||
| if (restoreScript != null) then | ||
| { | ||
| default = restoreScript; | ||
| } | ||
| else | ||
| { | ||
| example = fallbackRestoreName; | ||
| } | ||
| ) | ||
| # This pattern is clumsy but necessary to support | ||
| # referencing to the pkgs attrset from withing the options' defaults. | ||
| // lib.optionalAttrs (restoreScriptText != null) { | ||
| defaultText = restoreScriptText; | ||
| } | ||
| ); | ||
|
|
||
| backupService = | ||
| mkOption { | ||
| description = '' | ||
| Name of service backing up the given sourceDirectories. | ||
|
|
||
| Usually, this service will be run at regular intervals thanks to a systemd timer. | ||
| This is dependent on the actual contract provider used. | ||
|
|
||
| It can also be ran manually with: | ||
|
|
||
| ```bash | ||
| $ systemctl start ${coerce (coerce backupServiceText backupServiceText) fallbackBackupServiceName} | ||
| ``` | ||
| ''; | ||
| type = str; | ||
| } | ||
| // ( | ||
| if (backupService != null) then | ||
| { | ||
| default = backupService; | ||
| } | ||
| else | ||
| { | ||
| example = fallbackBackupServiceName; | ||
| } | ||
| ); | ||
| }; | ||
| }; | ||
| }; | ||
|
|
||
| behaviorTest = import ./filebackup/test.nix { inherit lib; }; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,133 @@ | ||
| { | ||
| lib, | ||
| }: | ||
| let | ||
| inherit (lib) mkOption types; | ||
| in | ||
| { | ||
| name, | ||
| providerRoot, | ||
| extraModules ? [ ], | ||
| }: | ||
| { | ||
| name = "contracts_filebackup_${name}"; | ||
|
|
||
| nodes.machine = | ||
| { config, ... }: | ||
| { | ||
| imports = extraModules; | ||
|
|
||
| options.test = { | ||
| repository = mkOption { | ||
| type = types.str; | ||
| default = "/opt/repository"; | ||
| }; | ||
| username = mkOption { | ||
| type = types.str; | ||
| default = "me"; | ||
| }; | ||
| sourceDirectories = mkOption { | ||
| type = types.listOf types.str; | ||
| default = [ | ||
| "/opt/files/A" | ||
| "/opt/files/B" | ||
| ]; | ||
| }; | ||
| }; | ||
|
|
||
| config = lib.mkMerge [ | ||
| (lib.setAttrByPath providerRoot { | ||
| input = { | ||
| inherit (config.test) sourceDirectories; | ||
| user = config.test.username; | ||
| }; | ||
| }) | ||
| (lib.mkIf (config.test.username != "root") { | ||
| users.users.${config.test.username} = { | ||
| isSystemUser = true; | ||
| group = config.test.username; | ||
| }; | ||
| users.groups.${config.test.username} = { }; | ||
| }) | ||
| ]; | ||
| }; | ||
|
|
||
| extraPythonPackages = p: [ p.dictdiffer ]; | ||
|
|
||
| testScript = | ||
| { nodes, ... }: | ||
| let | ||
| cfg = nodes.machine; | ||
| inherit (lib.getAttrFromPath providerRoot nodes.machine) output; | ||
| in | ||
| '' | ||
| from dictdiffer import diff # type: ignore | ||
|
|
||
| username = "${cfg.test.username}" | ||
| sourceDirectories = [ ${lib.concatMapStringsSep ", " (x: ''"${x}"'') cfg.test.sourceDirectories} ] | ||
|
|
||
| def list_files(dir): | ||
| files_and_content = {} | ||
|
|
||
| files = machine.succeed(f"""find {dir} -type f""").split("\n")[:-1] | ||
|
|
||
| for f in files: | ||
| content = machine.succeed(f"""cat {f}""").strip() | ||
| files_and_content[f] = content | ||
|
|
||
| return files_and_content | ||
|
|
||
| def assert_files(dir, files): | ||
| result = list(diff(list_files(dir), files)) | ||
| if len(result) > 0: | ||
| raise Exception("Unexpected files:", result) | ||
|
|
||
| with subtest("Create initial content"): | ||
| for path in sourceDirectories: | ||
| machine.succeed(f""" | ||
| mkdir -p {path} | ||
| echo repo_fileA_1 > {path}/fileA | ||
| echo repo_fileB_1 > {path}/fileB | ||
|
|
||
| chown {username}: -R {path} | ||
| chmod go-rwx -R {path} | ||
| """) | ||
|
|
||
| for path in sourceDirectories: | ||
| assert_files(path, { | ||
| f'{path}/fileA': 'repo_fileA_1', | ||
| f'{path}/fileB': 'repo_fileB_1', | ||
| }) | ||
|
|
||
| with subtest("First backup in repo"): | ||
| print(machine.succeed("systemctl cat ${output.backupService}")) | ||
| machine.succeed("systemctl start ${output.backupService}") | ||
|
|
||
| with subtest("New content"): | ||
| for path in sourceDirectories: | ||
| machine.succeed(f""" | ||
| echo repo_fileA_2 > {path}/fileA | ||
| echo repo_fileB_2 > {path}/fileB | ||
| """) | ||
|
|
||
| assert_files(path, { | ||
| f'{path}/fileA': 'repo_fileA_2', | ||
| f'{path}/fileB': 'repo_fileB_2', | ||
| }) | ||
|
|
||
| with subtest("Delete content"): | ||
| for path in sourceDirectories: | ||
| machine.succeed(f"""rm -r {path}/*""") | ||
|
|
||
| assert_files(path, {}) | ||
|
|
||
| with subtest("Restore initial content from repo"): | ||
| machine.succeed("""${output.restoreScript} restore latest""") | ||
|
|
||
| for path in sourceDirectories: | ||
| assert_files(path, { | ||
| f'{path}/fileA': 'repo_fileA_1', | ||
| f'{path}/fileB': 'repo_fileB_1', | ||
| }) | ||
| ''; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.