Compare commits

...

39 Commits

Author SHA1 Message Date
a-kenji
165c04f4bf WIP manpage 2024-05-30 12:59:46 +02:00
a-kenji
132066b319 init 2024-05-29 10:47:39 +02:00
clan-bot
d138e29a53 Merge pull request 'Consistently pass nix options to underlying tools' (#1488) from pass-nix-options into main 2024-05-29 08:25:53 +00:00
clan-bot
a7febba9c8 Merge pull request 'clan: clarify default backend' (#1490) from a-kenji-cli/facts-clarify into main 2024-05-29 08:23:06 +00:00
Jörg Thalheim
f0f97baa65 drop global argparse flags
They get shadowed by subargparser options.
2024-05-29 10:21:35 +02:00
a-kenji
c2dc94507e clan: clarify default backend 2024-05-29 10:17:22 +02:00
clan-bot
7c0aaab463 Merge pull request 'clan: add epilog to facts subcommands' (#1489) from a-kenji-cli/expand-examples into main 2024-05-29 08:15:46 +00:00
Jörg Thalheim
5dcac604d1 backup cli: make sure we have a flake 2024-05-29 10:14:14 +02:00
Jörg Thalheim
96746b7c98 flash: add write-efi-boot-entries flag 2024-05-29 10:14:14 +02:00
Jörg Thalheim
2ae50b7398 allow to override nix options in update/install/flash commands 2024-05-29 10:14:14 +02:00
a-kenji
3c905c5072 clan: add epilog to facts subcommands 2024-05-29 10:10:23 +02:00
Jörg Thalheim
5b926f57cc cli: also register common flags in subcommands
When a user runs --help on a subcommand they don't see some options such
as --options or --flake. To fix this we now register all common flags
also in subcommands.
2024-05-29 09:29:49 +02:00
clan-bot
b9788a5dba Merge pull request 'clan/docs.py: remove epilog from the reference overview' (#1487) from a-kenji-cli/docs/reference-overview into main 2024-05-28 18:05:07 +00:00
a-kenji
7078f09872 clan/docs.py: remove epilog from the reference overview 2024-05-28 20:01:48 +02:00
Mic92
1aa7808c02 Merge pull request 'Update Contributing guide to external developers' (#1484) from Qubasa/clan-core:main into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/1484
2024-05-28 16:12:11 +00:00
Qubasa
ba8a51101d Update Contributing guide to external developers 2024-05-28 18:06:31 +02:00
clan-bot
de69c970aa Merge pull request 'packaging: package clan gui for many distros' (#1485) from DavHau-dave into main 2024-05-28 15:54:08 +00:00
DavHau
fe5fa6a85d packaging: package clan gui for many distros 2024-05-28 17:50:32 +02:00
clan-bot
de74febf64 Merge pull request 'packaging: package clan gui for many distros' (#1483) from DavHau-dave into main 2024-05-28 15:37:18 +00:00
DavHau
3b6483e819 packaging: package clan gui for many distros 2024-05-28 17:33:55 +02:00
clan-bot
dcd6ad0983 Merge pull request 'Docs: fix relative links to git.clan.lol' (#1482) from hsjobeki-main into main 2024-05-28 15:18:45 +00:00
Johannes Kirschbauer
567d979243 Docs: fix relative links to git.clan.lol 2024-05-28 17:14:16 +02:00
clan-bot
c81a8681b0 Merge pull request 'clan/docs.py: add epilog to reference docs' (#1481) from a-kenji-docs/epilog into main 2024-05-28 15:13:57 +00:00
a-kenji
31cde90819 clan/docs.py: add epilog to reference docs
Fixes #1469
2024-05-28 17:08:46 +02:00
clan-bot
a77bf5bf21 Merge pull request 'Docs: use offline fonts' (#1480) from hsjobeki-main into main 2024-05-28 15:05:22 +00:00
Johannes Kirschbauer
4befa80eb8 Docs: use offline fonts 2024-05-28 16:58:59 +02:00
clan-bot
52584662a8 Merge pull request 'Fix typos' (#1477) from a-kenji-fix/typos into main 2024-05-28 13:02:19 +00:00
a-kenji
de147f63e9 Fix typos 2024-05-28 14:58:38 +02:00
clan-bot
96c33dec7a Merge pull request 'consistent rename cLAN -> Clan' (#1475) from rename into main 2024-05-28 11:38:57 +00:00
Jörg Thalheim
3c0b5f0867 drop deprecated mdDoc 2024-05-28 13:35:11 +02:00
clan-bot
c252f11c1f Merge pull request 'docs/secrets: improve chapter assigning access' (#1474) from DavHau-dave into main 2024-05-28 11:11:46 +00:00
DavHau
f1f040397d docs/secrets: improve chapter assigning access
Since we already walk the user through creating a secret in an earlier step, it makes more sense explain first how to add machines/users to an existing secret instead of creating  a new one
2024-05-28 13:08:19 +02:00
clan-bot
418e9937cb Merge pull request 'clan: add descriptions for reference documentation' (#1473) from a-kenji-cli/doc into main 2024-05-28 09:40:50 +00:00
a-kenji
c34664429c clan: add descriptions for reference documentation 2024-05-28 11:37:32 +02:00
clan-bot
6fe5928297 Merge pull request 'clan: add run_no_stdout function suppressing stdout' (#1472) from a-kenji-run_no_stdout into main 2024-05-28 09:17:14 +00:00
a-kenji
eee99730d1 clan: add run_no_stdout function suppressing stdout
Add the `run_no_stdout` function suppressing stdout by default.This
keeps the noise down on most commands, while still
stayingdebuggable.Stdout will be active when the `--debug` flag is
passed to the cli.

Fixes #1443
2024-05-28 11:13:55 +02:00
clan-bot
9394760e3b Merge pull request 'editor: improve nixpkgs option completions' (#1470) from a-kenji-improve/editor into main 2024-05-28 07:38:09 +00:00
a-kenji
a0b0e1a0ac editor: improve nixpkgs option completions 2024-05-28 09:34:57 +02:00
Jörg Thalheim
e2d7e6e86c consistent rename cLAN -> Clan 2024-05-27 15:54:17 +02:00
44 changed files with 1005 additions and 298 deletions

View File

@@ -145,14 +145,14 @@
machine.succeed("echo testing > /var/test-backups/somefile")
# create
machine.succeed("clan --debug --flake ${self} backups create test-backup")
machine.succeed("clan backups create --debug --flake ${self} test-backup")
machine.wait_until_succeeds("! systemctl is-active borgbackup-job-test-backup >&2")
machine.succeed("test -f /run/mount-external-disk")
machine.succeed("test -f /run/unmount-external-disk")
# list
backup_id = json.loads(machine.succeed("borg-job-test-backup list --json"))["archives"][0]["archive"]
out = machine.succeed("clan --debug --flake ${self} backups list test-backup").strip()
out = machine.succeed("clan backups list --debug --flake ${self} test-backup").strip()
print(out)
assert backup_id in out, f"backup {backup_id} not found in {out}"
localbackup_id = "hdd::/mnt/external-disk/snapshot.0"
@@ -160,14 +160,14 @@
## borgbackup restore
machine.succeed("rm -f /var/test-backups/somefile")
machine.succeed(f"clan --debug --flake ${self} backups restore test-backup borgbackup 'test-backup::borg@machine:.::{backup_id}' >&2")
machine.succeed(f"clan backups restore --debug --flake ${self} test-backup borgbackup 'test-backup::borg@machine:.::{backup_id}' >&2")
assert machine.succeed("cat /var/test-backups/somefile").strip() == "testing", "restore failed"
machine.succeed("test -f /var/test-service/pre-restore-command")
machine.succeed("test -f /var/test-service/post-restore-command")
## localbackup restore
machine.succeed("rm -f /var/test-backups/somefile /var/test-service/{pre,post}-restore-command")
machine.succeed(f"clan --debug --flake ${self} backups restore test-backup localbackup '{localbackup_id}' >&2")
machine.succeed(f"clan backups restore --debug --flake ${self} test-backup localbackup '{localbackup_id}' >&2")
assert machine.succeed("cat /var/test-backups/somefile").strip() == "testing", "restore failed"
machine.succeed("test -f /var/test-service/pre-restore-command")
machine.succeed("test -f /var/test-service/post-restore-command")

View File

@@ -1,33 +1,58 @@
{ ... }:
{ self, ... }:
{
perSystem =
{ ... }:
{
# checks = pkgs.lib.mkIf (pkgs.stdenv.isLinux) {
# flash = (import ../lib/test-base.nix) {
# name = "flash";
# nodes.target = {
# virtualisation.emptyDiskImages = [ 4096 ];
# virtualisation.memorySize = 3000;
# environment.systemPackages = [ self.packages.${pkgs.system}.clan-cli ];
# environment.etc."install-closure".source = "${closureInfo}/store-paths";
nodes,
pkgs,
lib,
...
}:
let
dependencies = [
self
pkgs.stdenv.drvPath
pkgs.jq
pkgs.disko
pkgs.stdenvNoCC.drvPath
pkgs.openssl
pkgs.curl
self.clanInternals.machines.${pkgs.hostPlatform.system}.test_install_machine.config.system.build.toplevel
self.clanInternals.machines.${pkgs.hostPlatform.system}.test_install_machine.config.system.build.diskoScript
self.clanInternals.machines.${pkgs.hostPlatform.system}.test_install_machine.config.system.clan.deployment.file
self.clanInternals.machines.${pkgs.hostPlatform.system}.test_install_machine.pkgs.disko
] ++ builtins.map (i: i.outPath) (builtins.attrValues self.inputs);
closureInfo = pkgs.closureInfo { rootPaths = dependencies; };
in
{
# Currently disabled...
checks = pkgs.lib.mkIf (false && pkgs.stdenv.isLinux) {
flash = (import ../lib/test-base.nix) {
name = "flash";
nodes.target = {
virtualisation.emptyDiskImages = [ 4096 ];
virtualisation.memorySize = 3000;
environment.systemPackages = [ self.packages.${pkgs.system}.clan-cli ];
environment.etc."install-closure".source = "${closureInfo}/store-paths";
# nix.settings = {
# substituters = lib.mkForce [ ];
# hashed-mirrors = null;
# connect-timeout = lib.mkForce 3;
# flake-registry = pkgs.writeText "flake-registry" ''{"flakes":[],"version":2}'';
# experimental-features = [
# "nix-command"
# "flakes"
# ];
# };
# };
# testScript = ''
# start_all()
# machine.succeed("clan --debug --flake ${../..} flash --yes --disk main /dev/vdb test_install_machine")
# '';
# } { inherit pkgs self; };
# };
nix.settings = {
substituters = lib.mkForce [ ];
hashed-mirrors = null;
connect-timeout = lib.mkForce 3;
flake-registry = pkgs.writeText "flake-registry" ''{"flakes":[],"version":2}'';
experimental-features = [
"nix-command"
"flakes"
];
};
};
testScript = ''
start_all()
machine.succeed("nix-store --verify-path ${
self.clanInternals.machines.${pkgs.hostPlatform.system}.test_install_machine.config.system.build.diskoScript
}")
machine.execute("timeout 30 clan flash --debug --flake ${../..} --yes --disk main /dev/vdb test_install_machine")
'';
} { inherit pkgs self; };
};
};
}

View File

@@ -2,8 +2,8 @@
{
clan.machines.test_install_machine = {
clan.networking.targetHost = "test_install_machine";
fileSystems."/".device = lib.mkDefault "/dev/null";
boot.loader.grub.device = lib.mkDefault "/dev/null";
fileSystems."/".device = lib.mkDefault "/dev/vdb";
boot.loader.grub.device = lib.mkDefault "/dev/vdb";
imports = [ self.nixosModules.test_install_machine ];
};
@@ -98,7 +98,7 @@
client.succeed("${pkgs.coreutils}/bin/install -Dm 600 ${../lib/ssh/privkey} /root/.ssh/id_ed25519")
client.wait_until_succeeds("ssh -o StrictHostKeyChecking=accept-new -v root@target hostname")
client.succeed("clan --debug --flake ${../..} machines install --yes test_install_machine root@target >&2")
client.succeed("clan machines install --debug --flake ${../..} --yes test_install_machine root@target >&2")
try:
target.shutdown()
except BrokenPipeError:

View File

@@ -9,7 +9,7 @@
# - cli frontend: https://github.com/localsend/localsend/issues/11
# - ipv6 support: https://github.com/localsend/localsend/issues/549
options.clan.localsend = {
enable = lib.mkEnableOption (lib.mdDoc "enable the localsend module");
enable = lib.mkEnableOption "enable the localsend module";
defaultLocation = lib.mkOption {
type = lib.types.str;
description = "The default download location";

4
docs/.gitignore vendored
View File

@@ -1 +1,3 @@
/site/reference
/site/reference
/site/static/Roboto-Regular.ttf
/site/static/FiraCode-VF.ttf

View File

@@ -22,9 +22,9 @@ Let's get your development environment up and running:
2. **Install direnv**:
- Download the direnv package from [here](https://direnv.net/docs/installation.html) or run the following command:
- To automatically setup a devshell on entering the directory
```bash
curl -sfL https://direnv.net/install.sh | bash
nix profile install nixpkgs#nix-direnv-flakes
```
3. **Add direnv to your shell**:
@@ -36,9 +36,14 @@ Let's get your development environment up and running:
echo 'eval "$(direnv hook zsh)"' >> ~/.zshrc && echo 'eval "$(direnv hook bash)"' >> ~/.bashrc && eval "$SHELL"
```
4. **Clone the Repository and Navigate**:
- Clone this repository and navigate to it.
4. **Create a Gitea Account**:
- Register an account on https://git.clan.lol
- Fork the [clan-core](https://git.clan.lol/clan/clan-core) repository
- Clone the repository and navigate to it
- Add a new remote called upstream:
```bash
git remote add upstream gitea@git.clan.lol:clan/clan-core.git
```
5. **Allow .envrc**:
@@ -48,59 +53,68 @@ Let's get your development environment up and running:
```
- Execute `direnv allow` to automatically execute the shell script `.envrc` when entering the directory.
# Setting Up Your Git Workflow
Let's set up your Git workflow to collaborate effectively:
1. **Register Your Gitea Account Locally**:
- Execute the following command to add your Gitea account locally:
```bash
tea login add
```
- Fill out the prompt as follows:
- URL of Gitea instance: `https://git.clan.lol`
- Name of new Login [gitea.gchq.icu]: `gitea.gchq.icu:7171`
- Do you have an access token? No
- Username: YourUsername
- Password: YourPassword
- Set Optional settings: No
2. **Git Workflow**:
1. Add your changes to Git using `git add <file1> <file2>`.
2. Run `nix fmt` to lint your files.
3. Commit your changes with a descriptive message: `git commit -a -m "My descriptive commit message"`.
4. Make sure your branch has the latest changes from upstream by executing:
6. **(Optional) Install Git Hooks**:
- To syntax check your code you can run:
```bash
git fetch && git rebase origin/main --autostash
nix fmt
```
- To make this automatic install the git hooks
```bash
./scripts/pre-commit
```
5. Use `git status` to check for merge conflicts.
6. If conflicts exist, resolve them. Here's a tutorial for resolving conflicts in [VSCode](https://code.visualstudio.com/docs/sourcecontrol/overview#_merge-conflicts).
7. After resolving conflicts, execute `git merge --continue` and repeat step 5 until there are no conflicts.
3. **Create a Pull Request**:
- To automatically open a pull request that gets merged if all tests pass, execute:
```bash
merge-after-ci
```
4. **Review Your Pull Request**:
- Visit https://git.clan.lol and go to the project page. Check under "Pull Requests" for any issues with your pull request.
5. **Push Your Changes**:
- If there are issues, fix them and redo step 2. Afterward, execute:
```bash
git push origin HEAD:YourUsername-main
```
- This will directly push to your open pull request.
7. **Open a Pull Request**:
- Go to the webinterface and open up a pull request
# Debugging
Here are some methods for debugging and testing the clan-cli:
## See all possible packages and tests
To quickly show all possible packages and tests execute:
```bash
nix flake show --system no-eval
```
Under `checks` you will find all tests that are executed in our CI. Under `packages` you find all our projects.
```
git+file:///home/lhebendanz/Projects/clan-core
├───apps
│ └───x86_64-linux
│ ├───install-vm: app
│ └───install-vm-nogui: app
├───checks
│ └───x86_64-linux
│ ├───borgbackup omitted (use '--all-systems' to show)
│ ├───check-for-breakpoints omitted (use '--all-systems' to show)
│ ├───clan-dep-age omitted (use '--all-systems' to show)
│ ├───clan-dep-bash omitted (use '--all-systems' to show)
│ ├───clan-dep-e2fsprogs omitted (use '--all-systems' to show)
│ ├───clan-dep-fakeroot omitted (use '--all-systems' to show)
│ ├───clan-dep-git omitted (use '--all-systems' to show)
│ ├───clan-dep-nix omitted (use '--all-systems' to show)
│ ├───clan-dep-openssh omitted (use '--all-systems' to show)
│ ├───"clan-dep-python3.11-mypy" omitted (use '--all-systems' to show)
├───packages
│ └───x86_64-linux
│ ├───clan-cli omitted (use '--all-systems' to show)
│ ├───clan-cli-docs omitted (use '--all-systems' to show)
│ ├───clan-ts-api omitted (use '--all-systems' to show)
│ ├───clan-vm-manager omitted (use '--all-systems' to show)
│ ├───default omitted (use '--all-systems' to show)
│ ├───deploy-docs omitted (use '--all-systems' to show)
│ ├───docs omitted (use '--all-systems' to show)
│ ├───editor omitted (use '--all-systems' to show)
└───templates
├───default: template: Initialize a new clan flake
└───new-clan: template: Initialize a new clan flake
```
You can execute every test separately by following the tree path `nix build .#checks.x86_64-linux.clan-pytest` for example.
## Test Locally in Devshell with Breakpoints
To test the cli locally in a development environment and set breakpoints for debugging, follow these steps:
@@ -150,12 +164,14 @@ If you need to inspect the Nix sandbox while running tests, follow these steps:
2. Use `cntr` and `psgrep` to attach to the Nix sandbox. This allows you to interactively debug your code while it's paused. For example:
```bash
cntr exec -w your_sandbox_name
psgrep -a -x your_python_process_name
cntr attach <container id, container name or process id>
```
Or you can also use the [nix breakpoint hook](https://nixos.org/manual/nixpkgs/stable/#breakpointhook)
# Standards
Every new module name should be in kebab-case.
Every fact definition, where possible should be in kebab-case.
- Every new module name should be in kebab-case.
- Every fact definition, where possible should be in kebab-case.

View File

@@ -39,7 +39,7 @@ exclude_docs: |
nav:
- Blog:
- blog/index.md
- blog/index.md
- Getting started:
- index.md
- Installer: getting-started/installer.md
@@ -94,6 +94,7 @@ docs_dir: site
site_dir: out
theme:
font: false
logo: https://clan.lol/static/logo/clan-white.png
favicon: https://clan.lol/static/logo/clan-dark.png
name: material
@@ -105,8 +106,6 @@ theme:
- content.tabs.link
icon:
repo: fontawesome/brands/git
font:
code: Roboto Mono
palette:
# Palette toggle for light mode
@@ -128,6 +127,7 @@ theme:
name: Switch to light mode
extra_css:
- static/extra.css
- static/asciinema-player/custom-theme.css
- static/asciinema-player/asciinema-player.css
@@ -142,7 +142,6 @@ extra:
- icon: fontawesome/solid/rss
link: /feed_rss_created.xml
plugins:
- search
- blog

View File

@@ -4,6 +4,8 @@
clan-cli-docs,
asciinema-player-js,
asciinema-player-css,
roboto,
fira-code,
...
}:
let
@@ -33,6 +35,10 @@ pkgs.stdenv.mkDerivation {
mkdir -p ./site/static/asciinema-player
ln -snf ${asciinema-player-js} ./site/static/asciinema-player/asciinema-player.min.js
ln -snf ${asciinema-player-css} ./site/static/asciinema-player/asciinema-player.css
# Link to fonts
ln -snf ${roboto}/share/fonts/truetype/Roboto-Regular.ttf ./site/static/
ln -snf ${fira-code}/share/fonts/truetype/FiraCode-VF.ttf ./site/static/
'';
buildPhase = ''

View File

@@ -40,13 +40,14 @@ def sanitize(text: str) -> str:
return text.replace(">", "\\>")
def replace_store_path(text: str) -> Path:
def replace_store_path(text: str) -> tuple[str, str]:
res = text
if text.startswith("/nix/store/"):
res = "https://git.clan.lol/clan/clan-core/src/branch/main/" + str(
Path(*Path(text).parts[4:])
)
return Path(res)
name = Path(res).name
return (res, name)
def render_option_header(name: str) -> str:
@@ -108,9 +109,10 @@ def render_option(name: str, option: dict[str, Any], level: int = 3) -> str:
"""
decls = option.get("declarations", [])
source_path = replace_store_path(decls[0])
source_path, name = replace_store_path(decls[0])
print(source_path, name)
res += f"""
:simple-git: [{source_path.name}]({source_path})
:simple-git: [{name}]({source_path})
"""
res += "\n"
@@ -160,7 +162,7 @@ def produce_clan_core_docs() -> None:
for option_name, info in options.items():
outfile = f"{module_name}/index.md"
# Create seperate files for nested options
# Create separate files for nested options
if len(option_name.split(".")) <= 2:
# i.e. clan-core.clanDir
output = core_outputs.get(

View File

@@ -5,6 +5,8 @@
clan-cli-docs,
asciinema-player-js,
asciinema-player-css,
roboto,
fira-code,
...
}:
pkgs.mkShell {
@@ -18,7 +20,12 @@ pkgs.mkShell {
echo "Generated API documentation in './site/reference/' "
mkdir -p ./site/static/asciinema-player
ln -snf ${asciinema-player-js} ./site/static/asciinema-player/asciinema-player.min.js
ln -snf ${asciinema-player-css} ./site/static/asciinema-player/asciinema-player.css
# Link to fonts
ln -snf ${roboto}/share/fonts/truetype/Roboto-Regular.ttf ./site/static/
ln -snf ${fira-code}/share/fonts/truetype/FiraCode-VF.ttf ./site/static/
'';
}

View File

@@ -46,7 +46,7 @@ sudo umount /dev/sdb1
It also includes the language and keymap currently used into the installer image.
```bash
clan --flake git+https://git.clan.lol/clan/clan-core flash flash-installer --disk main /dev/sd<X>
clan flash --flake git+https://git.clan.lol/clan/clan-core flash-installer --disk main /dev/sd<X>
```
!!! Danger "Specifying the wrong device can lead to unrecoverable data loss."

View File

@@ -106,17 +106,20 @@ In your nixos configuration you can get a path to secrets like this `config.sops
### Assigning Access
By default, secrets are encrypted for your key. To specify which users and machines can access a secret:
When using `clan secrets set <secret>` without arguments, secrets are encrypted for the key of the user named like your current $USER.
```bash
clan secrets set --machine <machine1> --machine <machine2> --user <user1> --user <user2> <secret_name>
```
You can also just add machines/users to existing secrets:
To add machines/users to an existing secret use:
```bash
clan secrets machines add-secret <machine_name> <secret_name>
```
Alternatively specify users and machines while creating a secret:
```bash
clan secrets set --machine <machine1> --machine <machine2> --user <user1> --user <user2> <secret_name>
```
## Advanced
In this section we go into more advanced secret management topics.
@@ -188,11 +191,9 @@ Since our clan secret module will auto-import secrets that are encrypted for a p
you can now remove `sops.secrets.<secrets> = { };` unless you need to specify more options for the secret like owner/group of the secret file.
## Indepth Explanation
The secrets system conceptually knows two different entities:
- **Machine**: consumes secrets

View File

@@ -0,0 +1,13 @@
@font-face {
font-family: "Roboto";
src: url(./Roboto-Regular.ttf) format('truetype');
}
@font-face {
font-family: "Fira Code";
src: url(./FiraCode-VF.ttf) format('truetype');
}
:root {
--md-text-font: "Roboto";
--md-code-font: "Fira Code";
}

View File

@@ -122,7 +122,7 @@ in
cores = lib.mkOption {
type = lib.types.ints.positive;
default = 1;
description = lib.mdDoc ''
description = ''
Specify the number of cores the guest is permitted to use.
The number can be higher than the available cores on the
host system.
@@ -132,7 +132,7 @@ in
memorySize = lib.mkOption {
type = lib.types.ints.positive;
default = 1024;
description = lib.mdDoc ''
description = ''
The memory size in megabytes of the virtual machine.
'';
};
@@ -140,7 +140,7 @@ in
graphics = lib.mkOption {
type = lib.types.bool;
default = true;
description = lib.mdDoc ''
description = ''
Whether to run QEMU with a graphics window, or in nographic mode.
Serial console will be enabled on both settings, but this will
change the preferred console.
@@ -150,7 +150,7 @@ in
waypipe = lib.mkOption {
type = lib.types.bool;
default = false;
description = lib.mdDoc ''
description = ''
Whether to use waypipe for native wayland passthrough, or not.
'';
};

View File

@@ -81,7 +81,7 @@ in
};
};
settings = lib.mkOption {
description = lib.mdDoc "override the network config in /var/lib/zerotier/bla/$network.json";
description = "override the network config in /var/lib/zerotier/bla/$network.json";
type = lib.types.submodule { freeformType = (pkgs.formats.json { }).type; };
};
};

0
pkgs/clan-cli/clan.1 Normal file
View File

View File

@@ -51,19 +51,14 @@ class AppendOptionAction(argparse.Action):
lst.append(values[1])
def create_parser(prog: str | None = None) -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
prog=prog,
description="The clan cli tool.",
epilog=(
"""
Online reference for the clan cli tool: https://docs.clan.lol/reference/cli/
For more detailed information, visit: https://docs.clan.lol
"""
),
formatter_class=argparse.RawTextHelpFormatter,
)
def flake_path(arg: str) -> str | Path:
flake_dir = Path(arg).resolve()
if flake_dir.exists() and flake_dir.is_dir():
return flake_dir
return arg
def add_common_flags(parser: argparse.ArgumentParser) -> None:
parser.add_argument(
"--debug",
help="Enable debug logging",
@@ -80,12 +75,6 @@ For more detailed information, visit: https://docs.clan.lol
default=[],
)
def flake_path(arg: str) -> str | Path:
flake_dir = Path(arg).resolve()
if flake_dir.exists() and flake_dir.is_dir():
return flake_dir
return arg
parser.add_argument(
"--flake",
help="path to the flake where the clan resides in, can be a remote flake or local, can be set through the [CLAN_DIR] environment variable",
@@ -94,6 +83,30 @@ For more detailed information, visit: https://docs.clan.lol
type=flake_path,
)
def register_common_flags(parser: argparse.ArgumentParser) -> None:
has_subparsers = False
for action in parser._actions:
if isinstance(action, argparse._SubParsersAction):
for choice, child_parser in action.choices.items():
has_subparsers = True
register_common_flags(child_parser)
if not has_subparsers:
add_common_flags(parser)
def create_parser(prog: str | None = None) -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
prog=prog,
description="The clan cli tool.",
epilog=(
"""
Online reference for the clan cli tool: https://docs.clan.lol/reference/cli/
For more detailed information, visit: https://docs.clan.lol
"""
),
formatter_class=argparse.RawTextHelpFormatter,
)
subparsers = parser.add_subparsers()
parser_backups = subparsers.add_parser(
@@ -156,6 +169,7 @@ For more detailed information, visit: https://docs.clan.lol/getting-started
parser_ssh = subparsers.add_parser(
"ssh",
help="ssh to a remote machine",
description="ssh to a remote machine",
epilog=(
"""
This subcommand allows seamless ssh access to the nixos-image builders.
@@ -207,7 +221,7 @@ For more detailed information, visit: https://docs.clan.lol/getting-started/secr
This subcommand provides an interface to facts of clan machines.
Facts are artifacts that a service can generate.
There are public and secret facts.
There are public and secret facts.
Public facts can be referenced by other machines directly.
Public facts can include: ip addresses, public keys.
Secret facts can include: passwords, private keys.
@@ -222,7 +236,7 @@ Examples:
$ clan facts generate
Will generate facts for all machines.
$ clan facts generate --service [SERVICE] --regenerate
Will regenerate facts, if they are already generated for a specific service.
This is especially useful for resetting certain passwords while leaving the rest
@@ -249,7 +263,7 @@ Examples:
List all the machines managed by clan.
$ clan machines update [MACHINES]
Will update the specified machine [MACHINE], if [MACHINE] is omitted, the command
Will update the specified machine [MACHINE], if [MACHINE] is omitted, the command
will attempt to update every configured machine.
$ clan machines install [MACHINES] [TARGET_HOST]
@@ -262,20 +276,30 @@ For more detailed information, visit: https://docs.clan.lol/getting-started/depl
)
machines.register_parser(parser_machine)
parser_vms = subparsers.add_parser("vms", help="manage virtual machines")
parser_vms = subparsers.add_parser(
"vms", help="manage virtual machines", description="manage virtual machines"
)
vms.register_parser(parser_vms)
parser_history = subparsers.add_parser("history", help="manage history")
parser_history = subparsers.add_parser(
"history",
help="manage history",
description="manage history",
)
history.register_parser(parser_history)
parser_flash = subparsers.add_parser(
"flash", help="flash machines to usb sticks or into isos"
"flash",
help="flash machines to usb sticks or into isos",
description="flash machines to usb sticks or into isos",
)
flash.register_parser(parser_flash)
if argcomplete:
argcomplete.autocomplete(parser)
register_common_flags(parser)
return parser

View File

@@ -33,6 +33,8 @@ def create_backup(machine: Machine, provider: str | None = None) -> None:
def create_command(args: argparse.Namespace) -> None:
if args.flake is None:
raise ClanError("Could not find clan flake toplevel directory")
machine = Machine(name=args.machine, flake=args.flake)
create_backup(machine=machine, provider=args.provider)

View File

@@ -48,6 +48,8 @@ def list_backups(machine: Machine, provider: str | None = None) -> list[Backup]:
def list_command(args: argparse.Namespace) -> None:
if args.flake is None:
raise ClanError("Could not find clan flake toplevel directory")
machine = Machine(name=args.machine, flake=args.flake)
backups = list_backups(machine=machine, provider=args.provider)
for backup in backups:

View File

@@ -62,6 +62,8 @@ def restore_backup(
def restore_command(args: argparse.Namespace) -> None:
if args.flake is None:
raise ClanError("Could not find clan flake toplevel directory")
machine = Machine(name=args.machine, flake=args.flake)
restore_backup(
machine=machine,

View File

@@ -140,3 +140,23 @@ def run(
raise ClanCmdError(cmd_out)
return cmd_out
def run_no_stdout(
cmd: list[str],
*,
env: dict[str, str] | None = None,
cwd: Path = Path.cwd(),
log: Log = Log.STDERR,
check: bool = True,
error_msg: str | None = None,
) -> CmdOut:
"""
Like run, but automatically suppresses stdout, if not in DEBUG log level.
If in DEBUG log level the stdout of commands will be shown.
"""
if logging.getLogger(__name__.split(".")[0]).isEnabledFor(logging.DEBUG):
return run(cmd, env=env, log=log, check=check, error_msg=error_msg)
else:
log = Log.NONE
return run(cmd, env=env, log=log, check=check, error_msg=error_msg)

View File

@@ -16,10 +16,54 @@ def register_parser(parser: argparse.ArgumentParser) -> None:
required=True,
)
check_parser = subparser.add_parser("check", help="check if facts are up to date")
check_parser = subparser.add_parser(
"check",
help="check if facts are up to date",
epilog=(
"""
This subcommand allows checking if all facts are up to date.
Examples:
$ clan facts check [MACHINE]
Will check facts for the specified machine.
For more detailed information, visit: https://docs.clan.lol/getting-started/secrets/
"""
),
formatter_class=argparse.RawTextHelpFormatter,
)
register_check_parser(check_parser)
list_parser = subparser.add_parser("list", help="list all facts")
list_parser = subparser.add_parser(
"list",
help="list all facts",
epilog=(
"""
This subcommand allows listing all public facts for a specific machine.
The resulting list will be a json string with the name of the fact as its key
and the fact itself as it's value.
This is how an example output might look like:
```
{
"[FACT_NAME]": "[FACT]"
}
```
Examples:
$ clan facts list [MACHINE]
Will list facts for the specified machine.
For more detailed information, visit: https://docs.clan.lol/getting-started/secrets/
"""
),
formatter_class=argparse.RawTextHelpFormatter,
)
register_list_parser(list_parser)
parser_generate = subparser.add_parser(
@@ -62,5 +106,26 @@ For more detailed information, visit: https://docs.clan.lol/getting-started/secr
)
register_generate_parser(parser_generate)
parser_upload = subparser.add_parser("upload", help="upload secrets for machines")
parser_upload = subparser.add_parser(
"upload",
help="upload secrets for machines",
epilog=(
"""
This subcommand allows uploading secrets to remote machines.
If using sops as a secret backend it will upload the private key to the machine.
If using password store it uploads all the secrets you manage to the machine.
The default backend is sops.
Examples:
$ clan facts upload [MACHINE]
Will upload secrets to a specific machine.
For more detailed information, visit: https://docs.clan.lol/getting-started/secrets/
"""
),
formatter_class=argparse.RawTextHelpFormatter,
)
register_upload_parser(parser_upload)

View File

@@ -209,9 +209,9 @@ def generate_facts(
def generate_command(args: argparse.Namespace) -> None:
if len(args.machines) == 0:
machines = get_all_machines(args.flake)
machines = get_all_machines(args.flake, args.option)
else:
machines = get_selected_machines(args.flake, args.machines)
machines = get_selected_machines(args.flake, args.option, args.machines)
generate_facts(machines, args.service, args.regenerate)

View File

@@ -85,7 +85,9 @@ def flash_machine(
disks: dict[str, str],
system_config: dict[str, Any],
dry_run: bool,
write_efi_boot_entries: bool,
debug: bool,
extra_args: list[str] = [],
) -> None:
secret_facts_module = importlib.import_module(machine.secret_facts_module)
secret_facts_store: SecretStoreBase = secret_facts_module.SecretStore(
@@ -112,6 +114,8 @@ def flash_machine(
disko_install.append("sudo")
disko_install.append("disko-install")
if write_efi_boot_entries:
disko_install.append("--write-efi-boot-entries")
if dry_run:
disko_install.append("--dry-run")
if debug:
@@ -128,6 +132,8 @@ def flash_machine(
json.dumps(system_config),
]
)
disko_install.extend(["--option", "dry-run", "true"])
disko_install.extend(extra_args)
cmd = nix_shell(
["nixpkgs#disko"],
@@ -148,6 +154,8 @@ class FlashOptions:
mode: str
language: str
keymap: str
write_efi_boot_entries: bool
nix_options: list[str]
class AppendDiskAction(argparse.Action):
@@ -178,6 +186,8 @@ def flash_command(args: argparse.Namespace) -> None:
mode=args.mode,
language=args.lang,
keymap=args.keymap,
write_efi_boot_entries=args.write_efi_boot_entries,
nix_options=args.options,
)
machine = Machine(opts.machine, flake=opts.flake)
@@ -233,6 +243,8 @@ def flash_command(args: argparse.Namespace) -> None:
system_config=extra_config,
dry_run=opts.dry_run,
debug=opts.debug,
write_efi_boot_entries=opts.write_efi_boot_entries,
extra_args=opts.nix_options,
)
@@ -251,12 +263,14 @@ def register_parser(parser: argparse.ArgumentParser) -> None:
help="device to flash to",
default={},
)
mode_help = textwrap.dedent("""\
mode_help = textwrap.dedent(
"""\
Specify the mode of operation. Valid modes are: format, mount."
Format will format the disk before installing.
Mount will mount the disk before installing.
Mount is useful for updating an existing system without losing data.
""")
"""
)
parser.add_argument(
"--mode",
type=str,
@@ -293,4 +307,16 @@ def register_parser(parser: argparse.ArgumentParser) -> None:
default=False,
action="store_true",
)
parser.add_argument(
"--write-efi-boot-entries",
help=textwrap.dedent(
"""
Write EFI boot entries to the NVRAM of the system for the installed system.
Specify this option if you plan to boot from this disk on the current machine,
but not if you plan to move the disk to another machine.
"""
).strip(),
default=False,
action="store_true",
)
parser.set_defaults(func=flash_command)

View File

@@ -26,6 +26,7 @@ def install_nixos(
debug: bool = False,
password: str | None = None,
no_reboot: bool = False,
extra_args: list[str] = [],
) -> None:
secret_facts_module = importlib.import_module(machine.secret_facts_module)
log.info(f"installing {machine.name}")
@@ -56,6 +57,7 @@ def install_nixos(
f"{machine.flake}#{machine.name}",
"--extra-files",
str(tmpdir),
*extra_args,
]
if no_reboot:
@@ -95,6 +97,7 @@ class InstallOptions:
debug: bool
no_reboot: bool
json_ssh_deploy: dict[str, str] | None
nix_options: list[str]
def install_command(args: argparse.Namespace) -> None:
@@ -127,6 +130,7 @@ def install_command(args: argparse.Namespace) -> None:
debug=args.debug,
no_reboot=args.no_reboot,
json_ssh_deploy=json_ssh_deploy,
nix_options=args.option,
)
machine = Machine(opts.machine, flake=opts.flake)
machine.target_host_address = opts.target_host
@@ -142,6 +146,7 @@ def install_command(args: argparse.Namespace) -> None:
debug=opts.debug,
password=password,
no_reboot=opts.no_reboot,
extra_args=opts.nix_options,
)

View File

@@ -7,7 +7,7 @@ from .machines import Machine
# function to speedup eval if we want to evauluate all machines
def get_all_machines(flake_dir: Path) -> list[Machine]:
def get_all_machines(flake_dir: Path, nix_options: list[str]) -> list[Machine]:
config = nix_config()
system = config["system"]
json_path = run(
@@ -19,13 +19,20 @@ def get_all_machines(flake_dir: Path) -> list[Machine]:
machines = []
for name, machine_data in machines_json.items():
machines.append(
Machine(name=name, flake=flake_dir, deployment_info=machine_data)
Machine(
name=name,
flake=flake_dir,
deployment_info=machine_data,
nix_options=nix_options,
)
)
return machines
def get_selected_machines(flake_dir: Path, machine_names: list[str]) -> list[Machine]:
def get_selected_machines(
flake_dir: Path, nix_options: list[str], machine_names: list[str]
) -> list[Machine]:
machines = []
for name in machine_names:
machines.append(Machine(name=name, flake=flake_dir))
machines.append(Machine(name=name, flake=flake_dir, nix_options=nix_options))
return machines

View File

@@ -6,7 +6,7 @@ from pathlib import Path
from clan_cli.api import API
from ..cmd import Log, run
from ..cmd import run_no_stdout
from ..nix import nix_config, nix_eval
log = logging.getLogger(__name__)
@@ -34,10 +34,7 @@ def list_machines(flake_url: str | Path, debug: bool) -> dict[str, MachineInfo]:
]
)
if not debug:
proc = run(cmd, log=Log.NONE)
else:
proc = run(cmd)
proc = run_no_stdout(cmd)
res = proc.stdout.strip()
machines_dict = json.loads(res)

View File

@@ -10,7 +10,7 @@ from clan_cli.clan_uri import ClanURI, MachineData
from clan_cli.dirs import vm_state_dir
from clan_cli.qemu.qmp import QEMUMonitorProtocol
from ..cmd import run
from ..cmd import run_no_stdout
from ..errors import ClanError
from ..nix import nix_build, nix_config, nix_eval, nix_metadata
from ..ssh import Host, parse_deployment_address
@@ -41,9 +41,10 @@ class QMPWrapper:
class Machine:
flake: str | Path
name: str
flake: str | Path
data: MachineData
nix_options: list[str]
eval_cache: dict[str, str]
build_cache: dict[str, Path]
_flake_path: Path | None
@@ -55,6 +56,7 @@ class Machine:
name: str,
flake: Path | str,
deployment_info: dict | None = None,
nix_options: list[str] = [],
machine: MachineData | None = None,
) -> None:
"""
@@ -76,6 +78,7 @@ class Machine:
self.build_cache: dict[str, Path] = {}
self._flake_path: Path | None = None
self._deployment_info: None | dict = deployment_info
self.nix_options = nix_options
state_dir = vm_state_dir(flake_url=str(self.flake), vm_name=self.data.name)
@@ -197,7 +200,7 @@ class Machine:
config_json.flush()
file_info = json.loads(
run(
run_no_stdout(
nix_eval(
[
"--impure",
@@ -242,15 +245,15 @@ class Machine:
flake = f"path:{self.flake_dir}"
args += [
f'{flake}#clanInternals.machines."{system}".{self.data.name}.{attr}',
*nix_options,
f'{flake}#clanInternals.machines."{system}".{self.data.name}.{attr}'
]
args += nix_options + self.nix_options
if method == "eval":
output = run(nix_eval(args)).stdout.strip()
output = run_no_stdout(nix_eval(args)).stdout.strip()
return output
elif method == "build":
outpath = run(nix_build(args)).stdout.strip()
outpath = run_no_stdout(nix_build(args)).stdout.strip()
return Path(outpath)
else:
raise ValueError(f"Unknown method {method}")

View File

@@ -110,11 +110,9 @@ def deploy_nixos(machines: MachineGroup) -> None:
ssh_arg += " -i " + host.key if host.key else ""
extra_args = host.meta.get("extra_args", [])
cmd = [
"nixos-rebuild",
"switch",
*extra_args,
"--fast",
"--option",
"keep-going",
@@ -124,6 +122,7 @@ def deploy_nixos(machines: MachineGroup) -> None:
"true",
"--build-host",
"",
*machine.nix_options,
"--flake",
f"{path}#{machine.name}",
]
@@ -143,7 +142,9 @@ def update(args: argparse.Namespace) -> None:
raise ClanError("Could not find clan flake toplevel directory")
machines = []
if len(args.machines) == 1 and args.target_host is not None:
machine = Machine(name=args.machines[0], flake=args.flake)
machine = Machine(
name=args.machines[0], flake=args.flake, nix_options=args.option
)
machine.target_host_address = args.target_host
machines.append(machine)
@@ -153,7 +154,7 @@ def update(args: argparse.Namespace) -> None:
else:
if len(args.machines) == 0:
ignored_machines = []
for machine in get_all_machines(args.flake):
for machine in get_all_machines(args.flake, args.option):
if machine.deployment_info.get("requireExplicitUpdate", False):
continue
try:
@@ -173,7 +174,7 @@ def update(args: argparse.Namespace) -> None:
print(machine, file=sys.stderr)
else:
machines = get_selected_machines(args.flake, args.machines)
machines = get_selected_machines(args.flake, args.option, args.machines)
deploy_nixos(MachineGroup(machines))

View File

@@ -22,7 +22,7 @@ class Option:
md_li += indent_next(
f"\n{self.description.strip()}" if self.description else ""
)
md_li += indent_next(f"\n{self.epilog.strip()}" if self.epilog else "")
# md_li += indent_next(f"\n{self.epilog.strip()}" if self.epilog else "")
return md_li
@@ -82,13 +82,54 @@ class Category:
md_li += indent_all(
f"{self.description.strip()}\n" if self.description else "", 4
)
md_li += "\n"
md_li += indent_all(f"{self.epilog.strip()}\n" if self.epilog else "", 4)
md_li += "\n"
return md_li
def epilog_to_md(text: str) -> str:
"""
Convert the epilog to md
"""
after_examples = False
md = ""
# md += text
for line in text.split("\n"):
if line.strip() == "Examples:":
after_examples = True
md += "### Examples"
md += "\n"
else:
if after_examples:
if line.strip().startswith("$"):
md += f"`{line}`"
md += "\n"
md += "\n"
else:
if contains_https_link(line):
line = convert_to_markdown_link(line)
md += line
md += "\n"
else:
md += line
md += "\n"
return md
import re
def contains_https_link(line: str) -> bool:
pattern = r"https://\S+"
return re.search(pattern, line) is not None
def convert_to_markdown_link(line: str) -> str:
pattern = r"(https://\S+)"
# Replacement pattern to convert it to a Markdown link
return re.sub(pattern, r"[\1](\1)", line)
def indent_next(text: str, indent_size: int = 4) -> str:
"""
Indent all lines in a string except the first line.
@@ -136,7 +177,7 @@ def get_subcommands(
continue
if isinstance(action, argparse._SubParsersAction):
continue # Subparsers handled sperately
continue # Subparsers handled separately
option_strings = ", ".join(action.option_strings)
if option_strings:
@@ -178,7 +219,7 @@ def get_subcommands(
Category(
title=f"{parent} {name}",
description=subparser.description,
epilog=subparser.epilog,
# epilog=subparser.epilog,
level=level,
options=_options,
positionals=_positionals,
@@ -222,6 +263,7 @@ def collect_commands() -> list[Category]:
options=_options,
positionals=_positionals,
subcommands=_subcommands,
epilog=subparser.epilog,
level=1,
)
)
@@ -242,6 +284,139 @@ def collect_commands() -> list[Category]:
return result
class ManPage:
def __init__(self, name: str, section: int) -> None:
self.name = name
self.section = section
def add_description(self, description: str) -> None:
self.description = description
# def add_option(self, option, description):
# self.options[option] = description
def control(self, control: str, content: str | None = None) -> None:
if content:
self.manpage = self.manpage + f".{control} {content}"
else:
self.manpage = self.manpage + f".{control}"
self.manpage = self.manpage + "\n"
def line(self, content: str | None = None) -> None:
self.manpage = self.manpage + f"{content}"
self.manpage = self.manpage + "\n"
def newline(self) -> None:
self.manpage = self.manpage + "\n"
def paragraph(self) -> None:
self.newline()
self.control("PP")
def contribute(self) -> None:
"""
Contributing section of the manpages
should only be shown on the root page.
"""
self.control("SH", "CONTRIBUTE")
self.control(
"PP",
)
self.line("Bug reports, contributions and forks are welcome.")
self.newline()
self.control(
"PP",
)
self.line(
"The code lives on gitea, you can use the issue tracker to file bugs, or issues and give feedback."
)
self.newline()
self.line(self.link("https://git.clan.lol/clan/clan-core"))
self.paragraph()
self.line(
"There is a matrix channel available where you can give feedback, or get feedback."
)
self.line("Share your usage patterns or share tips and tricks.")
self.newline()
self.line(self.link("https://matrix.to/#/#clan:lassul.us"))
def link(self, link: str) -> str:
"""
Format a link
"""
return "\[la]" + link + "\[ra]"
def render(self) -> str:
self.manpage = ""
self.control("nh")
self.control("TH", 'CLAN 1 2023 clan "User Manuals"')
self.control("SH", "NAME")
self.control(
"PP",
)
# overview
self.line("clan - the clan cli tool")
self.newline()
# synopsis
self.control("SH", "SYNOPSIS")
self.control(
"PP",
)
self.line("clan [OPTIONS] [SUBCOMMAND]")
self.newline()
# description
self.control("SH", "DESCRIPTION")
self.control(
"PP",
)
self.line("clan is a distributed systems manager for your fingertips.")
self.line("It knows how to update your machines.")
self.line(
"It knows how to generate and manage secrets and facts for your services."
)
self.line(
"It knows which services have state in which directories and can manage backups for machines and services."
)
self.paragraph()
# manpage = f"NAME\n\t{self.name} - {self.description}\n\nDESCRIPTION\n\t{self.description}\n\nOPTIONS\n"
# for options in self.options.items():
# print(options)
# for option, desc in self.options.items():
# manpage += f"\t-{option}\n\t\t{desc}\n"
self.contribute()
return self.manpage
def build_manpage() -> None:
"""
Build the reference and write to the out path.
"""
cmds = collect_commands()
# print(cmds)
# start the manpage with information collect_commands() doesn't expose
man = ManPage("clan", 1)
man.add_description("The clan cli tool.")
print(man.render())
for command in cmds:
if command.title == "facts":
print(command)
# TODO: gather all level one subcommands
# folder = Path("out")
# folder.mkdir(parents=True, exist_ok=True)
pass
def build_command_reference() -> None:
"""
Function that will build the reference
@@ -253,13 +428,12 @@ def build_command_reference() -> None:
folder.mkdir(parents=True, exist_ok=True)
# Index file
markdown = "# CLI Overview\n\n"
markdown = "#Overview\n\n"
categories_fmt = ""
for cat in cmds:
categories_fmt += f"{cat.to_md_li()}\n\n" if cat.to_md_li() else ""
if categories_fmt:
markdown += """## Overview\n\n"""
markdown += '<div class="grid cards" markdown>\n\n'
markdown += categories_fmt
markdown += "</div>"
@@ -280,6 +454,7 @@ def build_command_reference() -> None:
markdown = files.get(folder / f"{filename}.md", "")
markdown += f"{'#'*(cmd.level)} {cmd.title.capitalize()}\n\n"
markdown += f"{cmd.description}\n\n" if cmd.description else ""
# usage: clan vms run [-h] machine
@@ -320,6 +495,8 @@ def build_command_reference() -> None:
markdown += indent_all(commands_fmt)
markdown += "\n"
markdown += f"{epilog_to_md(cmd.epilog)}\n\n" if cmd.epilog else ""
files[folder / f"{filename}.md"] = markdown
for fname, content in files.items():
@@ -330,13 +507,15 @@ def build_command_reference() -> None:
def main() -> None:
if len(sys.argv) != 2:
print("Usage: python docs.py <command>")
print("Available commands: reference")
print("Available commands: reference, manpage")
sys.exit(1)
command = sys.argv[1]
if command == "reference":
build_command_reference()
if command == "manpage":
build_manpage()
if __name__ == "__main__":

View File

@@ -11,10 +11,10 @@ def test_backups(
cli.run(
[
"--flake",
str(test_flake_with_core.path),
"backups",
"list",
"--flake",
str(test_flake_with_core.path),
"vm1",
]
)

View File

@@ -39,9 +39,9 @@ def test_set_some_option(
cli = Cli()
cli.run(
[
"config",
"--flake",
str(test_flake.path),
"config",
"--quiet",
"--options-file",
example_options,
@@ -64,9 +64,9 @@ def test_configure_machine(
cli.run(
[
"config",
"--flake",
str(test_flake.path),
"config",
"-m",
"machine1",
"clan.jitsi.enable",
@@ -78,9 +78,9 @@ def test_configure_machine(
# read a option value
cli.run(
[
"config",
"--flake",
str(test_flake.path),
"config",
"-m",
"machine1",
"clan.jitsi.enable",

View File

@@ -15,10 +15,10 @@ def test_flakes_inspect(
cli = Cli()
cli.run(
[
"--flake",
str(test_flake_with_core.path),
"flakes",
"inspect",
"--flake",
str(test_flake_with_core.path),
"--machine",
"vm1",
]

View File

@@ -21,55 +21,55 @@ def test_import_sops(
monkeypatch.setenv("SOPS_AGE_KEY", age_keys[1].privkey)
cli.run(
[
"--flake",
str(test_flake.path),
"secrets",
"machines",
"add",
"--flake",
str(test_flake.path),
"machine1",
age_keys[0].pubkey,
]
)
cli.run(
[
"--flake",
str(test_flake.path),
"secrets",
"users",
"add",
"--flake",
str(test_flake.path),
"user1",
age_keys[1].pubkey,
]
)
cli.run(
[
"--flake",
str(test_flake.path),
"secrets",
"users",
"add",
"--flake",
str(test_flake.path),
"user2",
age_keys[2].pubkey,
]
)
cli.run(
[
"--flake",
str(test_flake.path),
"secrets",
"groups",
"add-user",
"--flake",
str(test_flake.path),
"group1",
"user1",
]
)
cli.run(
[
"--flake",
str(test_flake.path),
"secrets",
"groups",
"add-user",
"--flake",
str(test_flake.path),
"group1",
"user2",
]
@@ -78,10 +78,10 @@ def test_import_sops(
# To edit:
# SOPS_AGE_KEY=AGE-SECRET-KEY-1U5ENXZQAY62NC78Y2WC0SEGRRMAEEKH79EYY5TH4GPFWJKEAY0USZ6X7YQ sops --age age14tva0txcrl0zes05x7gkx56qd6wd9q3nwecjac74xxzz4l47r44sv3fz62 ./data/secrets.yaml
cmd = [
"--flake",
str(test_flake.path),
"secrets",
"import-sops",
"--flake",
str(test_flake.path),
"--group",
"group1",
"--machine",
@@ -91,10 +91,10 @@ def test_import_sops(
cli.run(cmd)
capsys.readouterr()
cli.run(["--flake", str(test_flake.path), "secrets", "users", "list"])
cli.run(["secrets", "users", "list", "--flake", str(test_flake.path)])
users = sorted(capsys.readouterr().out.rstrip().split())
assert users == ["user1", "user2"]
capsys.readouterr()
cli.run(["--flake", str(test_flake.path), "secrets", "get", "secret-key"])
cli.run(["secrets", "get", "--flake", str(test_flake.path), "secret-key"])
assert capsys.readouterr().out == "secret-value"

View File

@@ -9,11 +9,11 @@ def test_machine_subcommands(
) -> None:
cli = Cli()
cli.run(
["--flake", str(test_flake_with_core.path), "machines", "create", "machine1"]
["machines", "create", "--flake", str(test_flake_with_core.path), "machine1"]
)
capsys.readouterr()
cli.run(["--flake", str(test_flake_with_core.path), "machines", "list"])
cli.run(["machines", "list", "--flake", str(test_flake_with_core.path)])
out = capsys.readouterr()
@@ -22,11 +22,11 @@ def test_machine_subcommands(
assert "vm2" in out.out
cli.run(
["--flake", str(test_flake_with_core.path), "machines", "delete", "machine1"]
["machines", "delete", "--flake", str(test_flake_with_core.path), "machine1"]
)
capsys.readouterr()
cli.run(["--flake", str(test_flake_with_core.path), "machines", "list"])
cli.run(["machines", "list", "--flake", str(test_flake_with_core.path)])
out = capsys.readouterr()
assert "machine1" not in out.out

View File

@@ -27,11 +27,11 @@ def _test_identities(
cli.run(
[
"--flake",
str(test_flake.path),
"secrets",
what,
"add",
"--flake",
str(test_flake.path),
"foo",
age_keys[0].pubkey,
]
@@ -41,11 +41,11 @@ def _test_identities(
with pytest.raises(ClanError): # raises "foo already exists"
cli.run(
[
"--flake",
str(test_flake.path),
"secrets",
what,
"add",
"--flake",
str(test_flake.path),
"foo",
age_keys[0].pubkey,
]
@@ -54,11 +54,11 @@ def _test_identities(
# rotate the key
cli.run(
[
"--flake",
str(test_flake.path),
"secrets",
what,
"add",
"--flake",
str(test_flake.path),
"-f",
"foo",
age_keys[1].privkey,
@@ -68,11 +68,11 @@ def _test_identities(
capsys.readouterr() # empty the buffer
cli.run(
[
"--flake",
str(test_flake.path),
"secrets",
what,
"get",
"--flake",
str(test_flake.path),
"foo",
]
)
@@ -80,18 +80,18 @@ def _test_identities(
assert age_keys[1].pubkey in out.out
capsys.readouterr() # empty the buffer
cli.run(["--flake", str(test_flake.path), "secrets", what, "list"])
cli.run(["secrets", what, "list", "--flake", str(test_flake.path)])
out = capsys.readouterr() # empty the buffer
assert "foo" in out.out
cli.run(["--flake", str(test_flake.path), "secrets", what, "remove", "foo"])
cli.run(["secrets", what, "remove", "--flake", str(test_flake.path), "foo"])
assert not (sops_folder / what / "foo" / "key.json").exists()
with pytest.raises(ClanError): # already removed
cli.run(["--flake", str(test_flake.path), "secrets", what, "remove", "foo"])
cli.run(["secrets", what, "remove", "--flake", str(test_flake.path), "foo"])
capsys.readouterr()
cli.run(["--flake", str(test_flake.path), "secrets", what, "list"])
cli.run(["secrets", what, "list", "--flake", str(test_flake.path)])
out = capsys.readouterr()
assert "foo" not in out.out
@@ -113,17 +113,17 @@ def test_groups(
) -> None:
cli = Cli()
capsys.readouterr() # empty the buffer
cli.run(["--flake", str(test_flake.path), "secrets", "groups", "list"])
cli.run(["secrets", "groups", "list", "--flake", str(test_flake.path)])
assert capsys.readouterr().out == ""
with pytest.raises(ClanError): # machine does not exist yet
cli.run(
[
"--flake",
str(test_flake.path),
"secrets",
"groups",
"add-machine",
"--flake",
str(test_flake.path),
"group1",
"machine1",
]
@@ -131,33 +131,33 @@ def test_groups(
with pytest.raises(ClanError): # user does not exist yet
cli.run(
[
"--flake",
str(test_flake.path),
"secrets",
"groups",
"add-user",
"--flake",
str(test_flake.path),
"groupb1",
"user1",
]
)
cli.run(
[
"--flake",
str(test_flake.path),
"secrets",
"machines",
"add",
"--flake",
str(test_flake.path),
"machine1",
age_keys[0].pubkey,
]
)
cli.run(
[
"--flake",
str(test_flake.path),
"secrets",
"groups",
"add-machine",
"--flake",
str(test_flake.path),
"group1",
"machine1",
]
@@ -166,11 +166,11 @@ def test_groups(
# Should this fail?
cli.run(
[
"--flake",
str(test_flake.path),
"secrets",
"groups",
"add-machine",
"--flake",
str(test_flake.path),
"group1",
"machine1",
]
@@ -178,51 +178,51 @@ def test_groups(
cli.run(
[
"--flake",
str(test_flake.path),
"secrets",
"users",
"add",
"--flake",
str(test_flake.path),
"user1",
age_keys[0].pubkey,
]
)
cli.run(
[
"--flake",
str(test_flake.path),
"secrets",
"groups",
"add-user",
"--flake",
str(test_flake.path),
"group1",
"user1",
]
)
capsys.readouterr() # empty the buffer
cli.run(["--flake", str(test_flake.path), "secrets", "groups", "list"])
cli.run(["secrets", "groups", "list", "--flake", str(test_flake.path)])
out = capsys.readouterr().out
assert "user1" in out
assert "machine1" in out
cli.run(
[
"--flake",
str(test_flake.path),
"secrets",
"groups",
"remove-user",
"--flake",
str(test_flake.path),
"group1",
"user1",
]
)
cli.run(
[
"--flake",
str(test_flake.path),
"secrets",
"groups",
"remove-machine",
"--flake",
str(test_flake.path),
"group1",
"machine1",
]
@@ -251,90 +251,90 @@ def test_secrets(
) -> None:
cli = Cli()
capsys.readouterr() # empty the buffer
cli.run(["--flake", str(test_flake.path), "secrets", "list"])
cli.run(["secrets", "list", "--flake", str(test_flake.path)])
assert capsys.readouterr().out == ""
monkeypatch.setenv("SOPS_NIX_SECRET", "foo")
monkeypatch.setenv("SOPS_AGE_KEY_FILE", str(test_flake.path / ".." / "age.key"))
cli.run(["--flake", str(test_flake.path), "secrets", "key", "generate"])
cli.run(["secrets", "key", "generate", "--flake", str(test_flake.path)])
capsys.readouterr() # empty the buffer
cli.run(["--flake", str(test_flake.path), "secrets", "key", "show"])
cli.run(["secrets", "key", "show", "--flake", str(test_flake.path)])
key = capsys.readouterr().out
assert key.startswith("age1")
cli.run(
["--flake", str(test_flake.path), "secrets", "users", "add", "testuser", key]
["secrets", "users", "add", "--flake", str(test_flake.path), "testuser", key]
)
with pytest.raises(ClanError): # does not exist yet
cli.run(["--flake", str(test_flake.path), "secrets", "get", "nonexisting"])
cli.run(["--flake", str(test_flake.path), "secrets", "set", "initialkey"])
cli.run(["secrets", "get", "--flake", str(test_flake.path), "nonexisting"])
cli.run(["secrets", "set", "--flake", str(test_flake.path), "initialkey"])
capsys.readouterr()
cli.run(["--flake", str(test_flake.path), "secrets", "get", "initialkey"])
cli.run(["secrets", "get", "--flake", str(test_flake.path), "initialkey"])
assert capsys.readouterr().out == "foo"
capsys.readouterr()
cli.run(["--flake", str(test_flake.path), "secrets", "users", "list"])
cli.run(["secrets", "users", "list", "--flake", str(test_flake.path)])
users = capsys.readouterr().out.rstrip().split("\n")
assert len(users) == 1, f"users: {users}"
owner = users[0]
monkeypatch.setenv("EDITOR", "cat")
cli.run(["--flake", str(test_flake.path), "secrets", "set", "--edit", "initialkey"])
cli.run(["secrets", "set", "--edit", "--flake", str(test_flake.path), "initialkey"])
monkeypatch.delenv("EDITOR")
cli.run(["--flake", str(test_flake.path), "secrets", "rename", "initialkey", "key"])
cli.run(["secrets", "rename", "--flake", str(test_flake.path), "initialkey", "key"])
capsys.readouterr() # empty the buffer
cli.run(["--flake", str(test_flake.path), "secrets", "list"])
cli.run(["secrets", "list", "--flake", str(test_flake.path)])
assert capsys.readouterr().out == "key\n"
capsys.readouterr() # empty the buffer
cli.run(["--flake", str(test_flake.path), "secrets", "list", "nonexisting"])
cli.run(["secrets", "list", "--flake", str(test_flake.path), "nonexisting"])
assert capsys.readouterr().out == ""
capsys.readouterr() # empty the buffer
cli.run(["--flake", str(test_flake.path), "secrets", "list", "key"])
cli.run(["secrets", "list", "--flake", str(test_flake.path), "key"])
assert capsys.readouterr().out == "key\n"
cli.run(
[
"--flake",
str(test_flake.path),
"secrets",
"machines",
"add",
"--flake",
str(test_flake.path),
"machine1",
age_keys[1].pubkey,
]
)
cli.run(
[
"--flake",
str(test_flake.path),
"secrets",
"machines",
"add-secret",
"--flake",
str(test_flake.path),
"machine1",
"key",
]
)
capsys.readouterr()
cli.run(["--flake", str(test_flake.path), "secrets", "machines", "list"])
cli.run(["secrets", "machines", "list", "--flake", str(test_flake.path)])
assert capsys.readouterr().out == "machine1\n"
with use_key(age_keys[1].privkey, monkeypatch):
capsys.readouterr()
cli.run(["--flake", str(test_flake.path), "secrets", "get", "key"])
cli.run(["secrets", "get", "--flake", str(test_flake.path), "key"])
assert capsys.readouterr().out == "foo"
# rotate machines key
cli.run(
[
"--flake",
str(test_flake.path),
"secrets",
"machines",
"add",
"--flake",
str(test_flake.path),
"-f",
"machine1",
age_keys[0].privkey,
@@ -344,17 +344,17 @@ def test_secrets(
# should also rotate the encrypted secret
with use_key(age_keys[0].privkey, monkeypatch):
capsys.readouterr()
cli.run(["--flake", str(test_flake.path), "secrets", "get", "key"])
cli.run(["secrets", "get", "--flake", str(test_flake.path), "key"])
assert capsys.readouterr().out == "foo"
cli.run(
[
"--flake",
str(test_flake.path),
"secrets",
"machines",
"remove-secret",
"--flake",
str(test_flake.path),
"machine1",
"key",
]
@@ -362,37 +362,37 @@ def test_secrets(
cli.run(
[
"--flake",
str(test_flake.path),
"secrets",
"users",
"add",
"--flake",
str(test_flake.path),
"user1",
age_keys[1].pubkey,
]
)
cli.run(
[
"--flake",
str(test_flake.path),
"secrets",
"users",
"add-secret",
"--flake",
str(test_flake.path),
"user1",
"key",
]
)
capsys.readouterr()
with use_key(age_keys[1].privkey, monkeypatch):
cli.run(["--flake", str(test_flake.path), "secrets", "get", "key"])
cli.run(["secrets", "get", "--flake", str(test_flake.path), "key"])
assert capsys.readouterr().out == "foo"
cli.run(
[
"--flake",
str(test_flake.path),
"secrets",
"users",
"remove-secret",
"--flake",
str(test_flake.path),
"user1",
"key",
]
@@ -401,44 +401,44 @@ def test_secrets(
with pytest.raises(ClanError): # does not exist yet
cli.run(
[
"--flake",
str(test_flake.path),
"secrets",
"groups",
"add-secret",
"--flake",
str(test_flake.path),
"admin-group",
"key",
]
)
cli.run(
[
"--flake",
str(test_flake.path),
"secrets",
"groups",
"add-user",
"--flake",
str(test_flake.path),
"admin-group",
"user1",
]
)
cli.run(
[
"--flake",
str(test_flake.path),
"secrets",
"groups",
"add-user",
"--flake",
str(test_flake.path),
"admin-group",
owner,
]
)
cli.run(
[
"--flake",
str(test_flake.path),
"secrets",
"groups",
"add-secret",
"--flake",
str(test_flake.path),
"admin-group",
"key",
]
@@ -447,10 +447,10 @@ def test_secrets(
capsys.readouterr() # empty the buffer
cli.run(
[
"--flake",
str(test_flake.path),
"secrets",
"set",
"--flake",
str(test_flake.path),
"--group",
"admin-group",
"key2",
@@ -459,28 +459,28 @@ def test_secrets(
with use_key(age_keys[1].privkey, monkeypatch):
capsys.readouterr()
cli.run(["--flake", str(test_flake.path), "secrets", "get", "key"])
cli.run(["secrets", "get", "--flake", str(test_flake.path), "key"])
assert capsys.readouterr().out == "foo"
# extend group will update secrets
cli.run(
[
"--flake",
str(test_flake.path),
"secrets",
"users",
"add",
"--flake",
str(test_flake.path),
"user2",
age_keys[2].pubkey,
]
)
cli.run(
[
"--flake",
str(test_flake.path),
"secrets",
"groups",
"add-user",
"--flake",
str(test_flake.path),
"admin-group",
"user2",
]
@@ -488,16 +488,16 @@ def test_secrets(
with use_key(age_keys[2].privkey, monkeypatch): # user2
capsys.readouterr()
cli.run(["--flake", str(test_flake.path), "secrets", "get", "key"])
cli.run(["secrets", "get", "--flake", str(test_flake.path), "key"])
assert capsys.readouterr().out == "foo"
cli.run(
[
"--flake",
str(test_flake.path),
"secrets",
"groups",
"remove-user",
"--flake",
str(test_flake.path),
"admin-group",
"user2",
]
@@ -505,24 +505,24 @@ def test_secrets(
with pytest.raises(ClanError), use_key(age_keys[2].privkey, monkeypatch):
# user2 is not in the group anymore
capsys.readouterr()
cli.run(["--flake", str(test_flake.path), "secrets", "get", "key"])
cli.run(["secrets", "get", "--flake", str(test_flake.path), "key"])
print(capsys.readouterr().out)
cli.run(
[
"--flake",
str(test_flake.path),
"secrets",
"groups",
"remove-secret",
"--flake",
str(test_flake.path),
"admin-group",
"key",
]
)
cli.run(["--flake", str(test_flake.path), "secrets", "remove", "key"])
cli.run(["--flake", str(test_flake.path), "secrets", "remove", "key2"])
cli.run(["secrets", "remove", "--flake", str(test_flake.path), "key"])
cli.run(["secrets", "remove", "--flake", str(test_flake.path), "key2"])
capsys.readouterr() # empty the buffer
cli.run(["--flake", str(test_flake.path), "secrets", "list"])
cli.run(["secrets", "list", "--flake", str(test_flake.path)])
assert capsys.readouterr().out == ""

View File

@@ -24,27 +24,27 @@ def test_generate_secret(
cli = Cli()
cli.run(
[
"--flake",
str(test_flake_with_core.path),
"secrets",
"users",
"add",
"--flake",
str(test_flake_with_core.path),
"user1",
age_keys[0].pubkey,
]
)
cli.run(
[
"--flake",
str(test_flake_with_core.path),
"secrets",
"groups",
"add-user",
"--flake",
str(test_flake_with_core.path),
"admins",
"user1",
]
)
cmd = ["--flake", str(test_flake_with_core.path), "facts", "generate", "vm1"]
cmd = ["facts", "generate", "--flake", str(test_flake_with_core.path), "vm1"]
cli.run(cmd)
has_secret(test_flake_with_core.path, "vm1-age.key")
has_secret(test_flake_with_core.path, "vm1-zerotier-identity-secret")
@@ -60,7 +60,7 @@ def test_generate_secret(
secret1_mtime = identity_secret.lstat().st_mtime_ns
# test idempotency for vm1 and also generate for vm2
cli.run(["facts", "generate"])
cli.run(["facts", "generate", "--flake", str(test_flake_with_core.path)])
assert age_key.lstat().st_mtime_ns == age_key_mtime
assert identity_secret.lstat().st_mtime_ns == secret1_mtime

View File

@@ -23,11 +23,11 @@ def test_secrets_upload(
cli = Cli()
cli.run(
[
"--flake",
str(test_flake_with_core.path),
"secrets",
"users",
"add",
"--flake",
str(test_flake_with_core.path),
"user1",
age_keys[0].pubkey,
]
@@ -35,18 +35,18 @@ def test_secrets_upload(
cli.run(
[
"--flake",
str(test_flake_with_core.path),
"secrets",
"machines",
"add",
"--flake",
str(test_flake_with_core.path),
"vm1",
age_keys[1].pubkey,
]
)
monkeypatch.setenv("SOPS_NIX_SECRET", age_keys[0].privkey)
cli.run(
["--flake", str(test_flake_with_core.path), "secrets", "set", "vm1-age.key"]
["secrets", "set", "--flake", str(test_flake_with_core.path), "vm1-age.key"]
)
flake = test_flake_with_core.path.joinpath("flake.nix")
@@ -55,7 +55,7 @@ def test_secrets_upload(
new_text = flake.read_text().replace("__CLAN_TARGET_ADDRESS__", addr)
flake.write_text(new_text)
cli.run(["--flake", str(test_flake_with_core.path), "facts", "upload", "vm1"])
cli.run(["facts", "upload", "--flake", str(test_flake_with_core.path), "vm1"])
# the flake defines this path as the location where the sops key should be installed
sops_key = test_flake_with_core.path.joinpath("key.txt")

View File

@@ -86,7 +86,7 @@ def test_inspect(
test_flake_with_core: FlakeForTest, capsys: pytest.CaptureFixture
) -> None:
cli = Cli()
cli.run(["--flake", str(test_flake_with_core.path), "vms", "inspect", "vm1"])
cli.run(["vms", "inspect", "--flake", str(test_flake_with_core.path), "vm1"])
out = capsys.readouterr() # empty the buffer
assert "Cores" in out.out

View File

@@ -10,7 +10,9 @@
},
"options": {
"nixos": {
"expr": "(builtins.getFlake \"github:nixos/nixpkgs\").nixosConfigurations.<name>.options"
"expr": "(let pkgs = import <nixpkgs> { }; in (pkgs.lib.evalModules { modules
= (import <nixpkgs/nixos/modules/module-list.nix>) ++ [ ({...}: {
nixpkgs.hostPlatform = builtins.currentSystem;} ) ] ; })).options"
},
"home-manager": {
"expr": "(builtins.getFlake \"github:nix-community/home-manager\").homeConfigurations.<name>.options"

View File

@@ -7,6 +7,7 @@
./installer/flake-module.nix
./schemas/flake-module.nix
./webview-ui/flake-module.nix
./gui-installer/flake-module.nix
];
perSystem =

View File

@@ -0,0 +1,33 @@
{
perSystem =
{ pkgs, ... }:
let
nfpmConfig = pkgs.writeText "clan-nfpm-config.yaml" (
builtins.toJSON {
name = "clan-gui-installer";
contents = [
{
src = "${./gui-installer.sh}";
dst = "/usr/bin/clan-vm-manager";
}
];
}
);
installerFor =
packager:
pkgs.runCommand "gui-installer" { } ''
mkdir build
cd build
${pkgs.nfpm}/bin/nfpm package --config ${nfpmConfig} --packager ${packager}
mkdir $out
mv * $out/
'';
in
{
packages.gui-installer-apk = installerFor "apk";
packages.gui-installer-archlinux = installerFor "archlinux";
packages.gui-installer-deb = installerFor "deb";
packages.gui-installer-rpm = installerFor "rpm";
};
}

View File

@@ -0,0 +1,54 @@
#! /bin/sh
# create temp dir and ensure it is always cleaned
trap 'clean_temp_dir' EXIT
temp_dir=$(mktemp -d)
clean_temp_dir() {
rm -rf "$temp_dir"
}
is_nix_installed() {
if [ -n "$(command -v nix)" ]; then
return 0
else
return 1
fi
}
install_nix() {
curl --proto '=https' --tlsv1.2 -sSf -L \
https://install.determinate.systems/nix \
> "$temp_dir"/install_nix.sh
NIX_INSTALLER_DIAGNOSTIC_ENDPOINT="" sh "$temp_dir"/install_nix.sh install
}
ask_then_install_nix() {
echo "Clan requires Nix to be installed. Would you like to install it now? (y/n)"
read -r response
if [ "$response" = "y" ]; then
install_nix
else
echo "Clan cannot run without Nix. Exiting."
exit 1
fi
}
ensure_nix_installed() {
if ! is_nix_installed; then
ask_then_install_nix
fi
}
start_clan_gui() {
exec nix run \
https://git.clan.lol/clan/clan-core/archive/main.tar.gz#clan-vm-manager \
--extra-experimental-features flakes nix-command -- "$@"
}
main() {
ensure_nix_installed
start_clan_gui "$@"
}
main "$@"

213
templates/new-clan/flake.lock generated Normal file
View File

@@ -0,0 +1,213 @@
{
"nodes": {
"clan-core": {
"inputs": {
"disko": "disko",
"flake-parts": "flake-parts",
"nixos-generators": "nixos-generators",
"nixos-images": "nixos-images",
"nixpkgs": "nixpkgs",
"sops-nix": "sops-nix",
"treefmt-nix": "treefmt-nix"
},
"locked": {
"lastModified": 1716971153,
"narHash": "sha256-3QXK2SJRLMBOB1urY64WpXPKIWcq1LilBO8xeD6cBZg=",
"ref": "refs/heads/main",
"rev": "d138e29a53678c94c600a4aeb125c2ece0267271",
"revCount": 2911,
"type": "git",
"url": "https://git.clan.lol/clan/clan-core"
},
"original": {
"type": "git",
"url": "https://git.clan.lol/clan/clan-core"
}
},
"disko": {
"inputs": {
"nixpkgs": [
"clan-core",
"nixpkgs"
]
},
"locked": {
"lastModified": 1716394172,
"narHash": "sha256-B+pNhV8GFeCj9/MoH+qtGqKbgv6fU4hGaw2+NoYYtB0=",
"owner": "nix-community",
"repo": "disko",
"rev": "23c63fb09334c3e8958b57e2ddc3870b75b9111d",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "disko",
"type": "github"
}
},
"flake-parts": {
"inputs": {
"nixpkgs-lib": [
"clan-core",
"nixpkgs"
]
},
"locked": {
"lastModified": 1715865404,
"narHash": "sha256-/GJvTdTpuDjNn84j82cU6bXztE0MSkdnTWClUCRub78=",
"owner": "hercules-ci",
"repo": "flake-parts",
"rev": "8dc45382d5206bd292f9c2768b8058a8fd8311d9",
"type": "github"
},
"original": {
"owner": "hercules-ci",
"repo": "flake-parts",
"type": "github"
}
},
"nixlib": {
"locked": {
"lastModified": 1712450863,
"narHash": "sha256-K6IkdtMtq9xktmYPj0uaYc8NsIqHuaAoRBaMgu9Fvrw=",
"owner": "nix-community",
"repo": "nixpkgs.lib",
"rev": "3c62b6a12571c9a7f65ab037173ee153d539905f",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "nixpkgs.lib",
"type": "github"
}
},
"nixos-2311": {
"locked": {
"lastModified": 1715818734,
"narHash": "sha256-WvAJWCwPj/6quKcsgsvQYyZRxV8ho/yUzj0HZQ34DVU=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "95742536dc6debb5a8b8b78b27001c38f369f1e7",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "release-23.11",
"repo": "nixpkgs",
"type": "github"
}
},
"nixos-generators": {
"inputs": {
"nixlib": "nixlib",
"nixpkgs": [
"clan-core",
"nixpkgs"
]
},
"locked": {
"lastModified": 1716123454,
"narHash": "sha256-U2o4UPM/UsEyIX2p11+YEQgR9HY3PmjZ2mRl/x5e4xo=",
"owner": "nix-community",
"repo": "nixos-generators",
"rev": "a63e0c83dd83fe28cc571b97129e13373436bd82",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "nixos-generators",
"type": "github"
}
},
"nixos-images": {
"inputs": {
"nixos-2311": "nixos-2311",
"nixos-unstable": [
"clan-core",
"nixpkgs"
]
},
"locked": {
"lastModified": 1716132123,
"narHash": "sha256-rATSWbPaKQfZGaemu0tHL2xfCzVIVwpuTjk+KSBC+k4=",
"owner": "nix-community",
"repo": "nixos-images",
"rev": "8c9cab8c44434c12dafc465fbf61a710c5bceb08",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "nixos-images",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1716127062,
"narHash": "sha256-2rk8FqB/iQV2d0vQLs684/Tj5PUHaS1sFwG7fng5vXE=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "8a2555763c48e2410054de3f52f7310ce3241ec5",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-unstable-small",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"clan-core": "clan-core"
}
},
"sops-nix": {
"inputs": {
"nixpkgs": [
"clan-core",
"nixpkgs"
],
"nixpkgs-stable": [
"clan-core"
]
},
"locked": {
"lastModified": 1716087663,
"narHash": "sha256-zuSAGlx8Qk0OILGCC2GUyZ58/SJ5R3GZdeUNQ6IS0fQ=",
"owner": "Mic92",
"repo": "sops-nix",
"rev": "0bf1808e70ce80046b0cff821c019df2b19aabf5",
"type": "github"
},
"original": {
"owner": "Mic92",
"repo": "sops-nix",
"type": "github"
}
},
"treefmt-nix": {
"inputs": {
"nixpkgs": [
"clan-core",
"nixpkgs"
]
},
"locked": {
"lastModified": 1715940852,
"narHash": "sha256-wJqHMg/K6X3JGAE9YLM0LsuKrKb4XiBeVaoeMNlReZg=",
"owner": "numtide",
"repo": "treefmt-nix",
"rev": "2fba33a182602b9d49f0b2440513e5ee091d838b",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "treefmt-nix",
"type": "github"
}
}
},
"root": "root",
"version": 7
}