1
0
forked from clan/clan-core

Compare commits

..

2 Commits

Author SHA1 Message Date
3a73b4cb74 WIP: try to improve nixd completion 2024-05-29 09:27:52 +02:00
968749ee63 add: options.nix 2024-05-28 20:10:05 +02:00
86 changed files with 747 additions and 1666 deletions

View File

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

View File

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

View File

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

View File

@ -10,7 +10,6 @@ in
hostPkgs = pkgs; hostPkgs = pkgs;
# speed-up evaluation # speed-up evaluation
defaults = { defaults = {
nix.package = pkgs.nixVersions.latest;
documentation.enable = lib.mkDefault false; documentation.enable = lib.mkDefault false;
boot.isContainer = true; boot.isContainer = true;

View File

@ -10,7 +10,6 @@ in
defaults = { defaults = {
documentation.enable = lib.mkDefault false; documentation.enable = lib.mkDefault false;
nix.settings.min-free = 0; nix.settings.min-free = 0;
nix.package = pkgs.nixVersions.latest;
}; };
# to accept external dependencies such as disko # to accept external dependencies such as disko

View File

@ -3,15 +3,9 @@
options.clan.static-hosts = { options.clan.static-hosts = {
excludeHosts = lib.mkOption { excludeHosts = lib.mkOption {
type = lib.types.listOf lib.types.str; type = lib.types.listOf lib.types.str;
default = default = [ config.clanCore.machineName ];
if config.clan.static-hosts.topLevelDomain != "" then [ ] else [ config.clanCore.machineName ];
description = "Hosts that should be excluded"; description = "Hosts that should be excluded";
}; };
topLevelDomain = lib.mkOption {
type = lib.types.str;
default = "";
description = "Top level domain to reach hosts";
};
}; };
config.networking.hosts = config.networking.hosts =
@ -30,15 +24,7 @@
let let
path = zerotierIpMachinePath machine; path = zerotierIpMachinePath machine;
in in
if builtins.pathExists path then if builtins.pathExists path then lib.nameValuePair (builtins.readFile path) [ machine ] else null
lib.nameValuePair (builtins.readFile path) (
if (config.clan.static-hosts.topLevelDomain == "") then
[ machine ]
else
[ "${machine}.${config.clan.static-hosts.topLevelDomain}" ]
)
else
null
) filteredMachines ) filteredMachines
); );
} }

View File

@ -20,7 +20,7 @@ let
if builtins.pathExists fullPath then builtins.readFile fullPath else null if builtins.pathExists fullPath then builtins.readFile fullPath else null
) machines; ) machines;
networkIds = lib.filter (machine: machine != null) networkIdsUnchecked; networkIds = lib.filter (machine: machine != null) networkIdsUnchecked;
networkId = if builtins.length networkIds == 0 then null else builtins.elemAt networkIds 0; networkId = builtins.elemAt networkIds 0;
in in
#TODO:trace on multiple found network-ids #TODO:trace on multiple found network-ids
#TODO:trace on no single found networkId #TODO:trace on no single found networkId
@ -38,7 +38,7 @@ in
machines = builtins.readDir machineDir; machines = builtins.readDir machineDir;
zerotierIpMachinePath = machines: machineDir + machines + "/facts/zerotier-ip"; zerotierIpMachinePath = machines: machineDir + machines + "/facts/zerotier-ip";
filteredMachines = lib.filterAttrs ( filteredMachines = lib.filterAttrs (
name: _: !(lib.elem name config.clan.zerotier-static-peers.excludeHosts) name: _: !(lib.elem name config.clan.static-hosts.excludeHosts)
) machines; ) machines;
hosts = lib.mapAttrsToList (host: _: host) ( hosts = lib.mapAttrsToList (host: _: host) (
lib.mapAttrs' ( lib.mapAttrs' (

View File

@ -27,8 +27,7 @@
packages = [ packages = [
select-shell select-shell
pkgs.tea pkgs.tea
# Better error messages than nix 2.18 pkgs.nix
pkgs.nixVersions.latest
self'.packages.tea-create-pr self'.packages.tea-create-pr
self'.packages.merge-after-ci self'.packages.merge-after-ci
self'.packages.pending-reviews self'.packages.pending-reviews

View File

@ -15,74 +15,56 @@ Let's get your development environment up and running:
1. **Install Nix Package Manager**: 1. **Install Nix Package Manager**:
- You can install the Nix package manager by either [downloading the Nix installer](https://github.com/DeterminateSystems/nix-installer/releases) or running this command: - You can install the Nix package manager by either [downloading the Nix installer](https://github.com/DeterminateSystems/nix-installer/releases) or running this command:
```bash ```bash
curl --proto '=https' --tlsv1.2 -sSf -L https://install.determinate.systems/nix | sh -s -- install curl --proto '=https' --tlsv1.2 -sSf -L https://install.determinate.systems/nix | sh -s -- install
``` ```
2. **Install direnv**: 2. **Install direnv**:
- To automatically setup a devshell on entering the directory - To automatically setup a devshell on entering the directory
```bash ```bash
nix profile install nixpkgs#nix-direnv-flakes nix profile install nixpkgs#nix-direnv-flakes
``` ```
3. **Add direnv to your shell**: 3. **Add direnv to your shell**:
- Direnv needs to [hook into your shell](https://direnv.net/docs/hook.html) to work. - Direnv needs to [hook into your shell](https://direnv.net/docs/hook.html) to work.
You can do this by executing following command. The example below will setup direnv for `zsh` and `bash` You can do this by executing following command. The example below will setup direnv for `zsh` and `bash`
```bash ```bash
echo 'eval "$(direnv hook zsh)"' >> ~/.zshrc && echo 'eval "$(direnv hook bash)"' >> ~/.bashrc && eval "$SHELL" echo 'eval "$(direnv hook zsh)"' >> ~/.zshrc && echo 'eval "$(direnv hook bash)"' >> ~/.bashrc && eval "$SHELL"
``` ```
4. **Create a Gitea Account**: 4. **Create a Gitea Account**:
- Register an account on https://git.clan.lol - Register an account on https://git.clan.lol
- Fork the [clan-core](https://git.clan.lol/clan/clan-core) repository - Fork the [clan-core](https://git.clan.lol/clan/clan-core) repository
- Clone the repository and navigate to it - Clone the repository and navigate to it
- Add a new remote called upstream: - Add a new remote called upstream:
```bash ```bash
git remote add upstream gitea@git.clan.lol:clan/clan-core.git git remote add upstream gitea@git.clan.lol:clan/clan-core.git
```
5. **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 [git.clan.lol]:
- Do you have an access token? No
- Username: YourUsername
- Password: YourPassword
- Set Optional settings: No
6. **Allow .envrc**:
- When you enter the directory, you'll receive an error message like this:
```bash
direnv: error .envrc is blocked. Run `direnv allow` to approve its content
```
- Execute `direnv allow` to automatically execute the shell script `.envrc` when entering the directory.
7. **(Optional) Install Git Hooks**:
- To syntax check your code you can run:
```bash
nix fmt
```
- To make this automatic install the git hooks
```bash
./scripts/pre-commit
```
8. **Open a Pull Request**:
- To automatically open up a pull request you can use our tool called:
``` ```
merge-after-ci --reviewers Mic92 Lassulus Qubasa
5. **Allow .envrc**:
- When you enter the directory, you'll receive an error message like this:
```bash
direnv: error .envrc is blocked. Run `direnv allow` to approve its content
```
- Execute `direnv allow` to automatically execute the shell script `.envrc` when entering the directory.
6. **(Optional) Install Git Hooks**:
- To syntax check your code you can run:
```bash
nix fmt
``` ```
- To make this automatic install the git hooks
```bash
./scripts/pre-commit
```
7. **Open a Pull Request**:
- Go to the webinterface and open up a pull request
# Debugging # Debugging

View File

@ -16,26 +16,15 @@ def define_env(env: Any) -> None:
@env.macro @env.macro
def asciinema(name: str) -> str: def asciinema(name: str) -> str:
return f"""<div id="{name}"> return f"""<div id="{name}">
<script src="{asciinema_dir}/asciinema-player.min.js"></script>
<script> <script>
// Function to load the script and then create the Asciinema player AsciinemaPlayer.create('{video_dir + name}',
function loadAsciinemaPlayer() {{ document.getElementById("{name}"), {{
var script = document.createElement('script'); loop: true,
script.src = "{asciinema_dir}/asciinema-player.min.js"; autoPlay: true,
script.onload = function() {{ controls: false,
AsciinemaPlayer.create('{video_dir + name}', document.getElementById("{name}"), {{ speed: 1.5,
loop: true, theme: "solarized-light"
autoPlay: true, }});
controls: false,
speed: 1.5,
theme: "solarized-light"
}});
}};
document.head.appendChild(script);
}}
// Load the Asciinema player script
loadAsciinemaPlayer();
</script> </script>
<link rel="stylesheet" type="text/css" href="{asciinema_dir}/asciinema-player.css" />
</div>""" </div>"""

View File

@ -1,4 +1,4 @@
site_name: Clan Documentation site_name: Clan Docs
site_url: https://docs.clan.lol site_url: https://docs.clan.lol
repo_url: https://git.clan.lol/clan/clan-core/ repo_url: https://git.clan.lol/clan/clan-core/
repo_name: clan-core repo_name: clan-core
@ -96,7 +96,7 @@ site_dir: out
theme: theme:
font: false font: false
logo: https://clan.lol/static/logo/clan-white.png logo: https://clan.lol/static/logo/clan-white.png
favicon: https://clan.lol/static/dark-favicon/128x128.png favicon: https://clan.lol/static/logo/clan-dark.png
name: material name: material
features: features:
- navigation.instant - navigation.instant
@ -105,8 +105,7 @@ theme:
- content.code.copy - content.code.copy
- content.tabs.link - content.tabs.link
icon: icon:
repo: fontawesome/brands/git-alt repo: fontawesome/brands/git
custom_dir: overrides
palette: palette:
# Palette toggle for light mode # Palette toggle for light mode
@ -129,6 +128,8 @@ theme:
extra_css: extra_css:
- static/extra.css - static/extra.css
- static/asciinema-player/custom-theme.css
- static/asciinema-player/asciinema-player.css
extra: extra:
social: social:

View File

@ -1,12 +0,0 @@
{% extends "base.html" %}
{% block extrahead %}
<meta property="og:title" content="Clan - Documentation, Blog & Getting Started Guide" />
<meta property="og:description" content="Documentation for Clan. The peer-to-peer machine deployment framework." />
<meta property="og:image" content="https://clan.lol/static/dark-favicon/128x128.png" />
<meta property="og:url" content="https://docs.clan.lol" />
<meta property="og:type" content="website" />
<meta property="og:site_name" content="Clan" />
<meta property="og:locale" content="en_US" />
{% endblock %}

View File

@ -42,22 +42,13 @@ sudo umount /dev/sdb1
=== "**Linux OS**" === "**Linux OS**"
### Step 2. Flash Custom Installer ### Step 2. Flash Custom Installer
Using clan flash enables the inclusion of ssh public keys. Using clan flash enables the inclusion of ssh public keys and disables ssh password authentication.
It also allows to set language and keymap currently in the installer image. It also includes the language and keymap currently used into the installer image.
```bash ```bash
clan flash --flake git+https://git.clan.lol/clan/clan-core \ clan --flake git+https://git.clan.lol/clan/clan-core flash flash-installer --disk main /dev/sd<X>
--ssh-pubkey $HOME/.ssh/id_ed25519.pub \
--keymap en \
--language en \
--disk main /dev/sd<X> \
flash-installer
``` ```
The `--ssh-pubkey`, `--language` and `--keymap` are optional.
Replace `$HOME/.ssh/id_ed25519.pub` with a path to your SSH public key.
If you do not have an ssh key yet, you can generate one with `ssh-keygen -t ed25519` command.
!!! Danger "Specifying the wrong device can lead to unrecoverable data loss." !!! Danger "Specifying the wrong device can lead to unrecoverable data loss."
The `clan flash` utility will erase the disk. Make sure to specify the correct device The `clan flash` utility will erase the disk. Make sure to specify the correct device

View File

@ -7,11 +7,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1717177033, "lastModified": 1716394172,
"narHash": "sha256-G3CZJafCO8WDy3dyA2EhpUJEmzd5gMJ2IdItAg0Hijw=", "narHash": "sha256-B+pNhV8GFeCj9/MoH+qtGqKbgv6fU4hGaw2+NoYYtB0=",
"owner": "nix-community", "owner": "nix-community",
"repo": "disko", "repo": "disko",
"rev": "0274af4c92531ebfba4a5bd493251a143bc51f3c", "rev": "23c63fb09334c3e8958b57e2ddc3870b75b9111d",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -27,11 +27,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1717285511, "lastModified": 1715865404,
"narHash": "sha256-iKzJcpdXih14qYVcZ9QC9XuZYnPc6T8YImb6dX166kw=", "narHash": "sha256-/GJvTdTpuDjNn84j82cU6bXztE0MSkdnTWClUCRub78=",
"owner": "hercules-ci", "owner": "hercules-ci",
"repo": "flake-parts", "repo": "flake-parts",
"rev": "2a55567fcf15b1b1c7ed712a2c6fadaec7412ea8", "rev": "8dc45382d5206bd292f9c2768b8058a8fd8311d9",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -57,11 +57,11 @@
}, },
"nixos-2311": { "nixos-2311": {
"locked": { "locked": {
"lastModified": 1717017538, "lastModified": 1715818734,
"narHash": "sha256-S5kltvDDfNQM3xx9XcvzKEOyN2qk8Sa+aSOLqZ+1Ujc=", "narHash": "sha256-WvAJWCwPj/6quKcsgsvQYyZRxV8ho/yUzj0HZQ34DVU=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "64e468fd2652105710d86cd2ae3e65a5a6d58dec", "rev": "95742536dc6debb5a8b8b78b27001c38f369f1e7",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -79,11 +79,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1716210724, "lastModified": 1716123454,
"narHash": "sha256-iqQa3omRcHGpWb1ds75jS9ruA5R39FTmAkeR3J+ve1w=", "narHash": "sha256-U2o4UPM/UsEyIX2p11+YEQgR9HY3PmjZ2mRl/x5e4xo=",
"owner": "nix-community", "owner": "nix-community",
"repo": "nixos-generators", "repo": "nixos-generators",
"rev": "d14b286322c7f4f897ca4b1726ce38cb68596c94", "rev": "a63e0c83dd83fe28cc571b97129e13373436bd82",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -100,11 +100,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1717040312, "lastModified": 1716132123,
"narHash": "sha256-yI/en4IxuCEClIUpIs3QTyYCCtmSPLOhwLJclfNwdeg=", "narHash": "sha256-rATSWbPaKQfZGaemu0tHL2xfCzVIVwpuTjk+KSBC+k4=",
"owner": "nix-community", "owner": "nix-community",
"repo": "nixos-images", "repo": "nixos-images",
"rev": "47bfb55316e105390dd761e0b6e8e0be09462b67", "rev": "8c9cab8c44434c12dafc465fbf61a710c5bceb08",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -115,11 +115,11 @@
}, },
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1717298511, "lastModified": 1716127062,
"narHash": "sha256-9sXuJn/nL+9ImeYtlspTvjt83z1wIgU+9AwfNbnq+tI=", "narHash": "sha256-2rk8FqB/iQV2d0vQLs684/Tj5PUHaS1sFwG7fng5vXE=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "6634a0509e9e81e980b129435fbbec518ab246d0", "rev": "8a2555763c48e2410054de3f52f7310ce3241ec5",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -148,11 +148,11 @@
"nixpkgs-stable": [] "nixpkgs-stable": []
}, },
"locked": { "locked": {
"lastModified": 1717297459, "lastModified": 1716087663,
"narHash": "sha256-cZC2f68w5UrJ1f+2NWGV9Gx0dEYmxwomWN2B0lx0QRA=", "narHash": "sha256-zuSAGlx8Qk0OILGCC2GUyZ58/SJ5R3GZdeUNQ6IS0fQ=",
"owner": "Mic92", "owner": "Mic92",
"repo": "sops-nix", "repo": "sops-nix",
"rev": "ab2a43b0d21d1d37d4d5726a892f714eaeb4b075", "rev": "0bf1808e70ce80046b0cff821c019df2b19aabf5",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -168,11 +168,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1717278143, "lastModified": 1715940852,
"narHash": "sha256-u10aDdYrpiGOLoxzY/mJ9llST9yO8Q7K/UlROoNxzDw=", "narHash": "sha256-wJqHMg/K6X3JGAE9YLM0LsuKrKb4XiBeVaoeMNlReZg=",
"owner": "numtide", "owner": "numtide",
"repo": "treefmt-nix", "repo": "treefmt-nix",
"rev": "3eb96ca1ae9edf792a8e0963cc92fddfa5a87706", "rev": "2fba33a182602b9d49f0b2440513e5ee091d838b",
"type": "github" "type": "github"
}, },
"original": { "original": {

View File

@ -24,14 +24,10 @@
}; };
outputs = outputs =
inputs@{ flake-parts, self, ... }: inputs@{ flake-parts, ... }:
flake-parts.lib.mkFlake { inherit inputs; } ( flake-parts.lib.mkFlake { inherit inputs; } (
{ ... }: { ... }:
{ {
clan = {
# meta.name = "clan-core";
directory = self;
};
systems = [ systems = [
"x86_64-linux" "x86_64-linux"
"aarch64-linux" "aarch64-linux"

View File

@ -17,33 +17,6 @@ let
cfg = config.clan; cfg = config.clan;
in in
{ {
imports = [
# TODO: figure out how to print the deprecation warning
# "${inputs.nixpkgs}/nixos/modules/misc/assertions.nix"
(lib.mkRenamedOptionModule
[
"clan"
"clanName"
]
[
"clan"
"meta"
"name"
]
)
(lib.mkRenamedOptionModule
[
"clan"
"clanIcon"
]
[
"clan"
"meta"
"icon"
]
)
];
options.clan = { options.clan = {
directory = mkOption { directory = mkOption {
type = types.path; type = types.path;
@ -60,27 +33,15 @@ in
default = { }; default = { };
description = "Allows to include machine-specific modules i.e. machines.\${name} = { ... }"; description = "Allows to include machine-specific modules i.e. machines.\${name} = { ... }";
}; };
clanName = mkOption {
# Checks are performed in 'buildClan' type = types.str;
# Not everyone uses flake-parts description = "Needs to be (globally) unique, as this determines the folder name where the flake gets downloaded to.";
meta = { };
name = lib.mkOption { clanIcon = mkOption {
type = types.nullOr types.str; type = types.nullOr types.path;
default = null; default = null;
description = "Needs to be (globally) unique, as this determines the folder name where the flake gets downloaded to."; description = "A path to an icon to be used for the clan, should be the same for all machines";
};
icon = mkOption {
type = types.nullOr types.path;
default = null;
description = "A path to an icon to be used for the clan in the GUI";
};
description = mkOption {
type = types.nullOr types.str;
default = null;
description = "A short description of the clan";
};
}; };
pkgsForSystem = mkOption { pkgsForSystem = mkOption {
type = types.functionTo types.raw; type = types.functionTo types.raw;
default = _system: null; default = _system: null;
@ -91,7 +52,6 @@ in
clanInternals = lib.mkOption { clanInternals = lib.mkOption {
type = lib.types.submodule { type = lib.types.submodule {
options = { options = {
meta = lib.mkOption { type = lib.types.attrsOf lib.types.unspecified; };
all-machines-json = lib.mkOption { type = lib.types.attrsOf lib.types.unspecified; }; all-machines-json = lib.mkOption { type = lib.types.attrsOf lib.types.unspecified; };
machines = lib.mkOption { type = lib.types.attrsOf (lib.types.attrsOf lib.types.unspecified); }; machines = lib.mkOption { type = lib.types.attrsOf (lib.types.attrsOf lib.types.unspecified); };
machinesFunc = lib.mkOption { type = lib.types.attrsOf (lib.types.attrsOf lib.types.unspecified); }; machinesFunc = lib.mkOption { type = lib.types.attrsOf (lib.types.attrsOf lib.types.unspecified); };
@ -105,8 +65,9 @@ in
directory directory
specialArgs specialArgs
machines machines
clanName
clanIcon
pkgsForSystem pkgsForSystem
meta
; ;
}; };
}; };

View File

@ -7,58 +7,16 @@
directory, # The directory containing the machines subdirectory directory, # The directory containing the machines subdirectory
specialArgs ? { }, # Extra arguments to pass to nixosSystem i.e. useful to make self available specialArgs ? { }, # Extra arguments to pass to nixosSystem i.e. useful to make self available
machines ? { }, # allows to include machine-specific modules i.e. machines.${name} = { ... } machines ? { }, # allows to include machine-specific modules i.e. machines.${name} = { ... }
# DEPRECATED: use meta.name instead clanName, # Needs to be (globally) unique, as this determines the folder name where the flake gets downloaded to.
clanName ? null, # Needs to be (globally) unique, as this determines the folder name where the flake gets downloaded to.
# DEPRECATED: use meta.icon instead
clanIcon ? null, # A path to an icon to be used for the clan, should be the same for all machines clanIcon ? null, # A path to an icon to be used for the clan, should be the same for all machines
meta ? { }, # A set containing clan meta: name :: string, icon :: string, description :: string
pkgsForSystem ? (_system: null), # A map from arch to pkgs, if specified this nixpkgs will be only imported once for each system. pkgsForSystem ? (_system: null), # A map from arch to pkgs, if specified this nixpkgs will be only imported once for each system.
# This improves performance, but all nipxkgs.* options will be ignored. # This improves performance, but all nipxkgs.* options will be ignored.
}: }:
let let
deprecationWarnings = [
(lib.warnIf (
clanName != null
) "clanName is deprecated, please use meta.name instead. ${clanName}" null)
(lib.warnIf (clanIcon != null) "clanIcon is deprecated, please use meta.icon instead" null)
];
machinesDirs = lib.optionalAttrs (builtins.pathExists "${directory}/machines") ( machinesDirs = lib.optionalAttrs (builtins.pathExists "${directory}/machines") (
builtins.readDir (directory + /machines) builtins.readDir (directory + /machines)
); );
mergedMeta =
let
metaFromFile =
if (builtins.pathExists "${directory}/clan/meta.json") then
let
settings = builtins.fromJSON (builtins.readFile "${directory}/clan/meta.json");
in
settings
else
{ };
legacyMeta = lib.filterAttrs (_: v: v != null) {
name = clanName;
icon = clanIcon;
};
optionsMeta = lib.filterAttrs (_: v: v != null) meta;
warnings =
builtins.map (
name:
if
metaFromFile.${name} or null != optionsMeta.${name} or null && optionsMeta.${name} or null != null
then
lib.warn "meta.${name} is set in different places. (exlicit option meta.${name} overrides ${directory}/clan/meta.json)" null
else
null
) (builtins.attrNames metaFromFile)
++ [ (if (res.name or null == null) then (throw "meta.name should be set") else null) ];
res = metaFromFile // legacyMeta // optionsMeta;
in
# Print out warnings before returning the merged result
builtins.deepSeq warnings res;
machineSettings = machineSettings =
machineName: machineName:
# CLAN_MACHINE_SETTINGS_FILE allows to override the settings file temporarily # CLAN_MACHINE_SETTINGS_FILE allows to override the settings file temporarily
@ -100,15 +58,11 @@ let
(machines.${name} or { }) (machines.${name} or { })
( (
{ {
# Settings
clanCore.clanDir = directory;
# Inherited from clan wide settings
clanCore.clanName = meta.name or clanName;
clanCore.clanIcon = meta.icon or clanIcon;
# Machine specific settings
clanCore.machineName = name;
networking.hostName = lib.mkDefault name; networking.hostName = lib.mkDefault name;
clanCore.clanName = clanName;
clanCore.clanIcon = clanIcon;
clanCore.clanDir = directory;
clanCore.machineName = name;
nixpkgs.hostPlatform = lib.mkDefault system; nixpkgs.hostPlatform = lib.mkDefault system;
# speeds up nix commands by using the nixpkgs from the host system (especially useful in VMs) # speeds up nix commands by using the nixpkgs from the host system (especially useful in VMs)
@ -173,15 +127,10 @@ let
) supportedSystems ) supportedSystems
); );
in in
builtins.deepSeq deprecationWarnings { {
inherit nixosConfigurations; inherit nixosConfigurations;
clanInternals = { clanInternals = {
# Evaluated clan meta
# Merged /clan/meta.json with overrides from buildClan
meta = mergedMeta;
# machine specifics
machines = configsPerSystem; machines = configsPerSystem;
machinesFunc = configsFuncPerSystem; machinesFunc = configsFuncPerSystem;
all-machines-json = lib.mapAttrs ( all-machines-json = lib.mapAttrs (

View File

@ -51,14 +51,19 @@ class AppendOptionAction(argparse.Action):
lst.append(values[1]) lst.append(values[1])
def flake_path(arg: str) -> str | Path: def create_parser(prog: str | None = None) -> argparse.ArgumentParser:
flake_dir = Path(arg).resolve() parser = argparse.ArgumentParser(
if flake_dir.exists() and flake_dir.is_dir(): prog=prog,
return flake_dir description="The clan cli tool.",
return arg 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 add_common_flags(parser: argparse.ArgumentParser) -> None:
parser.add_argument( parser.add_argument(
"--debug", "--debug",
help="Enable debug logging", help="Enable debug logging",
@ -75,6 +80,12 @@ def add_common_flags(parser: argparse.ArgumentParser) -> None:
default=[], 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( parser.add_argument(
"--flake", "--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", 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",
@ -83,30 +94,6 @@ def add_common_flags(parser: argparse.ArgumentParser) -> None:
type=flake_path, 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() subparsers = parser.add_subparsers()
parser_backups = subparsers.add_parser( parser_backups = subparsers.add_parser(
@ -152,13 +139,12 @@ For more detailed information, visit: https://docs.clan.lol/getting-started
), ),
formatter_class=argparse.RawTextHelpFormatter, formatter_class=argparse.RawTextHelpFormatter,
) )
flakes.register_parser(parser_flake) flakes.register_parser(parser_flake)
parser_config = subparsers.add_parser( parser_config = subparsers.add_parser(
"config", "config",
help="read a nixos configuration option", help="set nixos configuration",
description="read a nixos configuration option", description="set nixos configuration",
epilog=( epilog=(
""" """
""" """
@ -222,7 +208,7 @@ For more detailed information, visit: https://docs.clan.lol/getting-started/secr
This subcommand provides an interface to facts of clan machines. This subcommand provides an interface to facts of clan machines.
Facts are artifacts that a service can generate. 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 be referenced by other machines directly.
Public facts can include: ip addresses, public keys. Public facts can include: ip addresses, public keys.
Secret facts can include: passwords, private keys. Secret facts can include: passwords, private keys.
@ -237,7 +223,7 @@ Examples:
$ clan facts generate $ clan facts generate
Will generate facts for all machines. Will generate facts for all machines.
$ clan facts generate --service [SERVICE] --regenerate $ clan facts generate --service [SERVICE] --regenerate
Will regenerate facts, if they are already generated for a specific service. Will regenerate facts, if they are already generated for a specific service.
This is especially useful for resetting certain passwords while leaving the rest This is especially useful for resetting certain passwords while leaving the rest
@ -264,7 +250,7 @@ Examples:
List all the machines managed by clan. List all the machines managed by clan.
$ clan machines update [MACHINES] $ 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. will attempt to update every configured machine.
$ clan machines install [MACHINES] [TARGET_HOST] $ clan machines install [MACHINES] [TARGET_HOST]
@ -299,8 +285,6 @@ For more detailed information, visit: https://docs.clan.lol/getting-started/depl
if argcomplete: if argcomplete:
argcomplete.autocomplete(parser) argcomplete.autocomplete(parser)
register_common_flags(parser)
return parser return parser

View File

@ -2,7 +2,6 @@ import argparse
import json import json
import logging import logging
from ..completions import add_dynamic_completer, complete_machines
from ..errors import ClanError from ..errors import ClanError
from ..machines.machines import Machine from ..machines.machines import Machine
@ -34,17 +33,13 @@ def create_backup(machine: Machine, provider: str | None = None) -> None:
def create_command(args: argparse.Namespace) -> 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) machine = Machine(name=args.machine, flake=args.flake)
create_backup(machine=machine, provider=args.provider) create_backup(machine=machine, provider=args.provider)
def register_create_parser(parser: argparse.ArgumentParser) -> None: def register_create_parser(parser: argparse.ArgumentParser) -> None:
machines_parser = parser.add_argument( parser.add_argument(
"machine", type=str, help="machine in the flake to create backups of" "machine", type=str, help="machine in the flake to create backups of"
) )
add_dynamic_completer(machines_parser, complete_machines)
parser.add_argument("--provider", type=str, help="backup provider to use") parser.add_argument("--provider", type=str, help="backup provider to use")
parser.set_defaults(func=create_command) parser.set_defaults(func=create_command)

View File

@ -3,7 +3,6 @@ import json
import subprocess import subprocess
from dataclasses import dataclass from dataclasses import dataclass
from ..completions import add_dynamic_completer, complete_machines
from ..errors import ClanError from ..errors import ClanError
from ..machines.machines import Machine from ..machines.machines import Machine
@ -49,8 +48,6 @@ def list_backups(machine: Machine, provider: str | None = None) -> list[Backup]:
def list_command(args: argparse.Namespace) -> None: 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) machine = Machine(name=args.machine, flake=args.flake)
backups = list_backups(machine=machine, provider=args.provider) backups = list_backups(machine=machine, provider=args.provider)
for backup in backups: for backup in backups:
@ -58,9 +55,8 @@ def list_command(args: argparse.Namespace) -> None:
def register_list_parser(parser: argparse.ArgumentParser) -> None: def register_list_parser(parser: argparse.ArgumentParser) -> None:
machines_parser = parser.add_argument( parser.add_argument(
"machine", type=str, help="machine in the flake to show backups of" "machine", type=str, help="machine in the flake to show backups of"
) )
add_dynamic_completer(machines_parser, complete_machines)
parser.add_argument("--provider", type=str, help="backup provider to filter by") parser.add_argument("--provider", type=str, help="backup provider to filter by")
parser.set_defaults(func=list_command) parser.set_defaults(func=list_command)

View File

@ -62,8 +62,6 @@ def restore_backup(
def restore_command(args: argparse.Namespace) -> None: 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) machine = Machine(name=args.machine, flake=args.flake)
restore_backup( restore_backup(
machine=machine, machine=machine,

View File

@ -56,7 +56,7 @@ def handle_output(process: subprocess.Popen, log: Log) -> tuple[str, str]:
sys.stderr.buffer.write(ret) sys.stderr.buffer.write(ret)
sys.stderr.flush() sys.stderr.flush()
stderr_buf += ret stderr_buf += ret
return stdout_buf.decode("utf-8", "replace"), stderr_buf.decode("utf-8", "replace") return stdout_buf.decode("utf-8"), stderr_buf.decode("utf-8")
class TimeTable: class TimeTable:
@ -101,19 +101,13 @@ TIME_TABLE = TimeTable()
def run( def run(
cmd: list[str], cmd: list[str],
*, *,
input: bytes | None = None, # noqa: A002
env: dict[str, str] | None = None, env: dict[str, str] | None = None,
cwd: Path = Path.cwd(), cwd: Path = Path.cwd(),
log: Log = Log.STDERR, log: Log = Log.STDERR,
check: bool = True, check: bool = True,
error_msg: str | None = None, error_msg: str | None = None,
) -> CmdOut: ) -> CmdOut:
if input: glog.debug(f"$: {shlex.join(cmd)} \nCaller: {get_caller()}")
glog.debug(
f"""$: echo "{input.decode('utf-8', 'replace')}" | {shlex.join(cmd)} \nCaller: {get_caller()}"""
)
else:
glog.debug(f"$: {shlex.join(cmd)} \nCaller: {get_caller()}")
tstart = datetime.now() tstart = datetime.now()
# Start the subprocess # Start the subprocess
@ -126,10 +120,7 @@ def run(
) )
stdout_buf, stderr_buf = handle_output(process, log) stdout_buf, stderr_buf = handle_output(process, log)
if input: rc = process.wait()
process.communicate(input)
else:
process.wait()
tend = datetime.now() tend = datetime.now()
global TIME_TABLE global TIME_TABLE
@ -145,7 +136,7 @@ def run(
msg=error_msg, msg=error_msg,
) )
if check and process.returncode != 0: if check and rc != 0:
raise ClanCmdError(cmd_out) raise ClanCmdError(cmd_out)
return cmd_out return cmd_out

View File

@ -1,157 +0,0 @@
import argparse
import json
import subprocess
import threading
from collections.abc import Callable, Iterable
from types import ModuleType
from typing import Any
from .cmd import run
from .nix import nix_eval
"""
This module provides dynamic completions.
The completions should feel fast.
We target a maximum of 1second on our average machine.
"""
argcomplete: ModuleType | None = None
try:
import argcomplete # type: ignore[no-redef]
except ImportError:
pass
# The default completion timeout for commands
COMPLETION_TIMEOUT: int = 3
def clan_dir(flake: str | None) -> str | None:
from .dirs import get_clan_flake_toplevel_or_env
path_result = get_clan_flake_toplevel_or_env()
return str(path_result) if path_result is not None else None
def complete_machines(
prefix: str, parsed_args: argparse.Namespace, **kwargs: Any
) -> Iterable[str]:
"""
Provides completion functionality for machine names configured in the clan.
"""
machines: list[str] = []
def run_cmd() -> None:
try:
# In my tests this was consistently faster than:
# nix eval .#nixosConfigurations --apply builtins.attrNames
cmd = ["nix", "flake", "show", "--system", "no-eval", "--json"]
if (clan_dir_result := clan_dir(None)) is not None:
cmd.append(clan_dir_result)
result = subprocess.run(
cmd,
check=True,
capture_output=True,
text=True,
)
data = json.loads(result.stdout)
try:
machines.extend(data.get("nixosConfigurations").keys())
except KeyError:
pass
except subprocess.CalledProcessError:
pass
thread = threading.Thread(target=run_cmd)
thread.start()
thread.join(timeout=COMPLETION_TIMEOUT)
if thread.is_alive():
return iter([])
machines_dict = {name: "machine" for name in machines}
return machines_dict
def complete_services_for_machine(
prefix: str, parsed_args: argparse.Namespace, **kwargs: Any
) -> Iterable[str]:
"""
Provides completion functionality for machine facts generation services.
"""
services: list[str] = []
# TODO: consolidate, if multiple machines are used
machines: list[str] = parsed_args.machines
def run_cmd() -> None:
try:
if (clan_dir_result := clan_dir(None)) is not None:
flake = clan_dir_result
else:
flake = "."
services_result = json.loads(
run(
nix_eval(
flags=[
f"{flake}#nixosConfigurations.{machines[0]}.config.clanCore.facts.services",
"--apply",
"builtins.attrNames",
],
),
).stdout.strip()
)
services.extend(services_result)
except subprocess.CalledProcessError:
pass
thread = threading.Thread(target=run_cmd)
thread.start()
thread.join(timeout=COMPLETION_TIMEOUT)
if thread.is_alive():
return iter([])
services_dict = {name: "service" for name in services}
return services_dict
def complete_secrets(
prefix: str, parsed_args: argparse.Namespace, **kwargs: Any
) -> Iterable[str]:
"""
Provides completion functionality for clan secrets
"""
from pathlib import Path
from .secrets.secrets import ListSecretsOptions, list_secrets
if (clan_dir_result := clan_dir(None)) is not None:
flake = clan_dir_result
else:
flake = "."
options = ListSecretsOptions(
flake=Path(flake),
pattern=None,
)
secrets = list_secrets(options.flake, options.pattern)
secrets_dict = {name: "secret" for name in secrets}
return secrets_dict
def add_dynamic_completer(
action: argparse.Action,
completer: Callable[..., Iterable[str]],
) -> None:
"""
Add a completion function to an argparse action, this will only be added,
if the argcomplete module is loaded.
"""
if argcomplete:
# mypy doesn't check this correctly, so we ignore it
action.completer = completer # type: ignore[attr-defined]

View File

@ -150,15 +150,6 @@ def read_machine_option_value(
return out return out
def get_option(args: argparse.Namespace) -> None:
print(
read_machine_option_value(
args.flake, args.machine, args.option, args.show_trace
)
)
# Currently writing is disabled
def get_or_set_option(args: argparse.Namespace) -> None: def get_or_set_option(args: argparse.Namespace) -> None:
if args.value == []: if args.value == []:
print( print(
@ -316,7 +307,7 @@ def register_parser(
) )
# inject callback function to process the input later # inject callback function to process the input later
parser.set_defaults(func=get_option) parser.set_defaults(func=get_or_set_option)
parser.add_argument( parser.add_argument(
"--machine", "--machine",
"-m", "-m",
@ -354,6 +345,13 @@ def register_parser(
type=str, type=str,
) )
parser.add_argument(
"value",
# force this arg to be set
nargs="*",
help="option value to set (if omitted, the current value is printed)",
)
def main(argv: list[str] | None = None) -> None: def main(argv: list[str] | None = None) -> None:
if argv is None: if argv is None:

View File

@ -16,54 +16,10 @@ def register_parser(parser: argparse.ArgumentParser) -> None:
required=True, required=True,
) )
check_parser = subparser.add_parser( check_parser = subparser.add_parser("check", help="check if facts are up to date")
"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) register_check_parser(check_parser)
list_parser = subparser.add_parser( list_parser = subparser.add_parser("list", help="list all facts")
"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) register_list_parser(list_parser)
parser_generate = subparser.add_parser( parser_generate = subparser.add_parser(
@ -106,26 +62,5 @@ For more detailed information, visit: https://docs.clan.lol/getting-started/secr
) )
register_generate_parser(parser_generate) register_generate_parser(parser_generate)
parser_upload = subparser.add_parser( parser_upload = subparser.add_parser("upload", help="upload secrets for machines")
"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) register_upload_parser(parser_upload)

View File

@ -2,7 +2,6 @@ import argparse
import importlib import importlib
import logging import logging
from ..completions import add_dynamic_completer, complete_machines
from ..machines.machines import Machine from ..machines.machines import Machine
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@ -55,12 +54,10 @@ def check_command(args: argparse.Namespace) -> None:
def register_check_parser(parser: argparse.ArgumentParser) -> None: def register_check_parser(parser: argparse.ArgumentParser) -> None:
machines_parser = parser.add_argument( parser.add_argument(
"machine", "machine",
help="The machine to check secrets for", help="The machine to check secrets for",
) )
add_dynamic_completer(machines_parser, complete_machines)
parser.add_argument( parser.add_argument(
"--service", "--service",
help="the service to check", help="the service to check",

View File

@ -9,11 +9,6 @@ from tempfile import TemporaryDirectory
from clan_cli.cmd import run from clan_cli.cmd import run
from ..completions import (
add_dynamic_completer,
complete_machines,
complete_services_for_machine,
)
from ..errors import ClanError from ..errors import ClanError
from ..git import commit_files from ..git import commit_files
from ..machines.inventory import get_all_machines, get_selected_machines from ..machines.inventory import get_all_machines, get_selected_machines
@ -32,7 +27,6 @@ def read_multiline_input(prompt: str = "Finish with Ctrl-D") -> str:
""" """
print(prompt, flush=True) print(prompt, flush=True)
proc = subprocess.run(["cat"], stdout=subprocess.PIPE, text=True) proc = subprocess.run(["cat"], stdout=subprocess.PIPE, text=True)
log.info("Input received. Processing...")
return proc.stdout return proc.stdout
@ -215,30 +209,26 @@ def generate_facts(
def generate_command(args: argparse.Namespace) -> None: def generate_command(args: argparse.Namespace) -> None:
if len(args.machines) == 0: if len(args.machines) == 0:
machines = get_all_machines(args.flake, args.option) machines = get_all_machines(args.flake)
else: else:
machines = get_selected_machines(args.flake, args.option, args.machines) machines = get_selected_machines(args.flake, args.machines)
generate_facts(machines, args.service, args.regenerate) generate_facts(machines, args.service, args.regenerate)
def register_generate_parser(parser: argparse.ArgumentParser) -> None: def register_generate_parser(parser: argparse.ArgumentParser) -> None:
machines_parser = parser.add_argument( parser.add_argument(
"machines", "machines",
type=str, type=str,
help="machine to generate facts for. if empty, generate facts for all machines", help="machine to generate facts for. if empty, generate facts for all machines",
nargs="*", nargs="*",
default=[], default=[],
) )
add_dynamic_completer(machines_parser, complete_machines) parser.add_argument(
service_parser = parser.add_argument(
"--service", "--service",
type=str, type=str,
help="service to generate facts for, if empty, generate facts for every service", help="service to generate facts for, if empty, generate facts for every service",
default=None, default=None,
) )
add_dynamic_completer(service_parser, complete_services_for_machine)
parser.add_argument( parser.add_argument(
"--regenerate", "--regenerate",
type=bool, type=bool,

View File

@ -3,7 +3,6 @@ import importlib
import json import json
import logging import logging
from ..completions import add_dynamic_completer, complete_machines
from ..machines.machines import Machine from ..machines.machines import Machine
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@ -38,10 +37,8 @@ def get_command(args: argparse.Namespace) -> None:
def register_list_parser(parser: argparse.ArgumentParser) -> None: def register_list_parser(parser: argparse.ArgumentParser) -> None:
machines_parser = parser.add_argument( parser.add_argument(
"machine", "machine",
help="The machine to print facts for", help="The machine to print facts for",
) )
add_dynamic_completer(machines_parser, complete_machines)
parser.set_defaults(func=get_command) parser.set_defaults(func=get_command)

View File

@ -2,7 +2,6 @@ import os
import subprocess import subprocess
from pathlib import Path from pathlib import Path
from clan_cli.cmd import Log, run
from clan_cli.machines.machines import Machine from clan_cli.machines.machines import Machine
from clan_cli.nix import nix_shell from clan_cli.nix import nix_shell
@ -16,25 +15,25 @@ class SecretStore(SecretStoreBase):
def set( def set(
self, service: str, name: str, value: bytes, groups: list[str] self, service: str, name: str, value: bytes, groups: list[str]
) -> Path | None: ) -> Path | None:
run( subprocess.run(
nix_shell( nix_shell(
["nixpkgs#pass"], ["nixpkgs#pass"],
["pass", "insert", "-m", f"machines/{self.machine.name}/{name}"], ["pass", "insert", "-m", f"machines/{self.machine.name}/{name}"],
), ),
input=value, input=value,
log=Log.BOTH, check=True,
error_msg=f"Failed to insert secret {name}",
) )
return None # we manage the files outside of the git repo return None # we manage the files outside of the git repo
def get(self, service: str, name: str) -> bytes: def get(self, service: str, name: str) -> bytes:
return run( return subprocess.run(
nix_shell( nix_shell(
["nixpkgs#pass"], ["nixpkgs#pass"],
["pass", "show", f"machines/{self.machine.name}/{name}"], ["pass", "show", f"machines/{self.machine.name}/{name}"],
), ),
error_msg=f"Failed to get secret {name}", check=True,
).stdout.encode("utf-8") stdout=subprocess.PIPE,
).stdout
def exists(self, service: str, name: str) -> bool: def exists(self, service: str, name: str) -> bool:
password_store = os.environ.get( password_store = os.environ.get(
@ -49,7 +48,7 @@ class SecretStore(SecretStoreBase):
) )
hashes = [] hashes = []
hashes.append( hashes.append(
run( subprocess.run(
nix_shell( nix_shell(
["nixpkgs#git"], ["nixpkgs#git"],
[ [
@ -62,15 +61,13 @@ class SecretStore(SecretStoreBase):
f"machines/{self.machine.name}", f"machines/{self.machine.name}",
], ],
), ),
check=False, stdout=subprocess.PIPE,
) ).stdout.strip()
.stdout.encode("utf-8")
.strip()
) )
for symlink in Path(password_store).glob(f"machines/{self.machine.name}/**/*"): for symlink in Path(password_store).glob(f"machines/{self.machine.name}/**/*"):
if symlink.is_symlink(): if symlink.is_symlink():
hashes.append( hashes.append(
run( subprocess.run(
nix_shell( nix_shell(
["nixpkgs#git"], ["nixpkgs#git"],
[ [
@ -83,10 +80,8 @@ class SecretStore(SecretStoreBase):
str(symlink), str(symlink),
], ],
), ),
check=False, stdout=subprocess.PIPE,
) ).stdout.strip()
.stdout.encode("utf-8")
.strip()
) )
# we sort the hashes to make sure that the order is always the same # we sort the hashes to make sure that the order is always the same

View File

@ -5,7 +5,6 @@ from pathlib import Path
from tempfile import TemporaryDirectory from tempfile import TemporaryDirectory
from ..cmd import Log, run from ..cmd import Log, run
from ..completions import add_dynamic_completer, complete_machines
from ..machines.machines import Machine from ..machines.machines import Machine
from ..nix import nix_shell from ..nix import nix_shell
@ -33,8 +32,6 @@ def upload_secrets(machine: Machine) -> None:
" ".join(["ssh"] + ssh_cmd[2:]), " ".join(["ssh"] + ssh_cmd[2:]),
"-az", "-az",
"--delete", "--delete",
"--chown=root:root",
"--chmod=D700,F600",
f"{tempdir!s}/", f"{tempdir!s}/",
f"{host.user}@{host.host}:{machine.secrets_upload_directory}/", f"{host.user}@{host.host}:{machine.secrets_upload_directory}/",
], ],
@ -49,10 +46,8 @@ def upload_command(args: argparse.Namespace) -> None:
def register_upload_parser(parser: argparse.ArgumentParser) -> None: def register_upload_parser(parser: argparse.ArgumentParser) -> None:
machines_parser = parser.add_argument( parser.add_argument(
"machine", "machine",
help="The machine to upload secrets to", help="The machine to upload secrets to",
) )
add_dynamic_completer(machines_parser, complete_machines)
parser.set_defaults(func=upload_command) parser.set_defaults(func=upload_command)

View File

@ -3,6 +3,7 @@ import importlib
import json import json
import logging import logging
import os import os
import re
import shutil import shutil
import textwrap import textwrap
from collections.abc import Sequence from collections.abc import Sequence
@ -12,7 +13,6 @@ from tempfile import TemporaryDirectory
from typing import Any from typing import Any
from .cmd import Log, run from .cmd import Log, run
from .completions import add_dynamic_completer, complete_machines
from .errors import ClanError from .errors import ClanError
from .facts.secret_modules import SecretStoreBase from .facts.secret_modules import SecretStoreBase
from .machines.machines import Machine from .machines.machines import Machine
@ -21,6 +21,63 @@ from .nix import nix_shell
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
def list_available_ssh_keys(ssh_dir: Path = Path("~/.ssh").expanduser()) -> list[Path]:
"""
Function to list all available SSH public keys in the default .ssh directory.
Returns a list of paths to available public key files.
"""
public_key_patterns = ["*.pub"]
available_keys: list[Path] = []
# Check for public key files
for pattern in public_key_patterns:
for key_path in ssh_dir.glob(pattern):
if key_path.is_file():
available_keys.append(key_path)
return available_keys
def read_public_key_contents(public_keys: list[Path]) -> list[str]:
"""
Function to read and return the contents of available SSH public keys.
Returns a list containing the contents of each public key.
"""
public_key_contents = []
for key_path in public_keys:
try:
with open(key_path.expanduser()) as key_file:
public_key_contents.append(key_file.read().strip())
except FileNotFoundError:
log.error(f"Public key file not found: {key_path}")
return public_key_contents
def get_keymap_and_locale() -> dict[str, str]:
locale = "en_US.UTF-8"
keymap = "en"
# Execute the `localectl status` command
result = run(["localectl", "status"])
if result.returncode == 0:
output = result.stdout
# Extract the Keymap (X11 Layout)
keymap_match = re.search(r"X11 Layout:\s+(.*)", output)
if keymap_match:
keymap = keymap_match.group(1)
# Extract the System Locale (LANG only)
locale_match = re.search(r"System Locale:\s+LANG=(.*)", output)
if locale_match:
locale = locale_match.group(1)
return {"keymap": keymap, "locale": locale}
def flash_machine( def flash_machine(
machine: Machine, machine: Machine,
*, *,
@ -28,9 +85,7 @@ def flash_machine(
disks: dict[str, str], disks: dict[str, str],
system_config: dict[str, Any], system_config: dict[str, Any],
dry_run: bool, dry_run: bool,
write_efi_boot_entries: bool,
debug: bool, debug: bool,
extra_args: list[str] = [],
) -> None: ) -> None:
secret_facts_module = importlib.import_module(machine.secret_facts_module) secret_facts_module = importlib.import_module(machine.secret_facts_module)
secret_facts_store: SecretStoreBase = secret_facts_module.SecretStore( secret_facts_store: SecretStoreBase = secret_facts_module.SecretStore(
@ -57,8 +112,6 @@ def flash_machine(
disko_install.append("sudo") disko_install.append("sudo")
disko_install.append("disko-install") disko_install.append("disko-install")
if write_efi_boot_entries:
disko_install.append("--write-efi-boot-entries")
if dry_run: if dry_run:
disko_install.append("--dry-run") disko_install.append("--dry-run")
if debug: if debug:
@ -75,8 +128,6 @@ def flash_machine(
json.dumps(system_config), json.dumps(system_config),
] ]
) )
disko_install.extend(["--option", "dry-run", "true"])
disko_install.extend(extra_args)
cmd = nix_shell( cmd = nix_shell(
["nixpkgs#disko"], ["nixpkgs#disko"],
@ -97,8 +148,6 @@ class FlashOptions:
mode: str mode: str
language: str language: str
keymap: str keymap: str
write_efi_boot_entries: bool
nix_options: list[str]
class AppendDiskAction(argparse.Action): class AppendDiskAction(argparse.Action):
@ -127,10 +176,8 @@ def flash_command(args: argparse.Namespace) -> None:
confirm=not args.yes, confirm=not args.yes,
debug=args.debug, debug=args.debug,
mode=args.mode, mode=args.mode,
language=args.language, language=args.lang,
keymap=args.keymap, keymap=args.keymap,
write_efi_boot_entries=args.write_efi_boot_entries,
nix_options=args.option,
) )
machine = Machine(opts.machine, flake=opts.flake) machine = Machine(opts.machine, flake=opts.flake)
@ -144,22 +191,40 @@ def flash_command(args: argparse.Namespace) -> None:
if ask != "y": if ask != "y":
return return
extra_config: dict[str, Any] = {} root_keys = read_public_key_contents(opts.ssh_keys_path)
if opts.ssh_keys_path: if opts.confirm and not root_keys:
root_keys = [] msg = "Should we add your SSH public keys to the root user? [y/N] "
for key_path in opts.ssh_keys_path: ask = input(msg)
try: if ask == "y":
root_keys.append(key_path.read_text()) pubkeys = list_available_ssh_keys()
except OSError as e: root_keys.extend(read_public_key_contents(pubkeys))
raise ClanError(f"Cannot read SSH public key file: {key_path}: {e}") else:
extra_config["users"] = { raise ClanError(
"users": {"root": {"openssh": {"authorizedKeys": {"keys": root_keys}}}} "No SSH public keys provided. Use --ssh-pubkey to add keys."
} )
if opts.keymap: elif not opts.confirm and not root_keys:
extra_config["console"] = {"keyMap": opts.keymap} pubkeys = list_available_ssh_keys()
root_keys.extend(read_public_key_contents(pubkeys))
# If ssh-pubkeys set, we don't need to ask for confirmation
elif opts.confirm and root_keys:
pass
elif not opts.confirm and root_keys:
pass
else:
raise ClanError("Invalid state")
if opts.language: localectl = get_keymap_and_locale()
extra_config["i18n"] = {"defaultLocale": opts.language} extra_config = {
"users": {
"users": {"root": {"openssh": {"authorizedKeys": {"keys": root_keys}}}}
},
"console": {
"keyMap": opts.keymap if opts.keymap else localectl["keymap"],
},
"i18n": {
"defaultLocale": opts.language if opts.language else localectl["locale"],
},
}
flash_machine( flash_machine(
machine, machine,
@ -168,19 +233,15 @@ def flash_command(args: argparse.Namespace) -> None:
system_config=extra_config, system_config=extra_config,
dry_run=opts.dry_run, dry_run=opts.dry_run,
debug=opts.debug, debug=opts.debug,
write_efi_boot_entries=opts.write_efi_boot_entries,
extra_args=opts.nix_options,
) )
def register_parser(parser: argparse.ArgumentParser) -> None: def register_parser(parser: argparse.ArgumentParser) -> None:
machines_parser = parser.add_argument( parser.add_argument(
"machine", "machine",
type=str, type=str,
help="machine to install", help="machine to install",
) )
add_dynamic_completer(machines_parser, complete_machines)
parser.add_argument( parser.add_argument(
"--disk", "--disk",
type=str, type=str,
@ -190,14 +251,12 @@ def register_parser(parser: argparse.ArgumentParser) -> None:
help="device to flash to", help="device to flash to",
default={}, default={},
) )
mode_help = textwrap.dedent( mode_help = textwrap.dedent("""\
"""\
Specify the mode of operation. Valid modes are: format, mount." Specify the mode of operation. Valid modes are: format, mount."
Format will format the disk before installing. Format will format the disk before installing.
Mount will mount the disk before installing. Mount will mount the disk before installing.
Mount is useful for updating an existing system without losing data. Mount is useful for updating an existing system without losing data.
""" """)
)
parser.add_argument( parser.add_argument(
"--mode", "--mode",
type=str, type=str,
@ -213,7 +272,7 @@ def register_parser(parser: argparse.ArgumentParser) -> None:
help="ssh pubkey file to add to the root user. Can be used multiple times", help="ssh pubkey file to add to the root user. Can be used multiple times",
) )
parser.add_argument( parser.add_argument(
"--language", "--lang",
type=str, type=str,
help="system language", help="system language",
) )
@ -234,16 +293,4 @@ def register_parser(parser: argparse.ArgumentParser) -> None:
default=False, default=False,
action="store_true", 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) parser.set_defaults(func=flash_command)

View File

@ -5,7 +5,6 @@ from .create import register_create_parser
from .delete import register_delete_parser from .delete import register_delete_parser
from .install import register_install_parser from .install import register_install_parser
from .list import register_list_parser from .list import register_list_parser
from .show import register_show_parser
from .update import register_update_parser from .update import register_update_parser
@ -63,17 +62,6 @@ Examples:
) )
register_list_parser(list_parser) register_list_parser(list_parser)
show_parser = subparser.add_parser(
"show",
help="Show a machine",
epilog=(
"""
This subcommand shows the details of a machine managed by this clan like icon, description, etc
"""
),
)
register_show_parser(show_parser)
install_parser = subparser.add_parser( install_parser = subparser.add_parser(
"install", "install",
help="Install a machine", help="Install a machine",

View File

@ -1,7 +1,6 @@
import argparse import argparse
import shutil import shutil
from ..completions import add_dynamic_completer, complete_machines
from ..dirs import specific_machine_dir from ..dirs import specific_machine_dir
from ..errors import ClanError from ..errors import ClanError
@ -15,7 +14,5 @@ def delete_command(args: argparse.Namespace) -> None:
def register_delete_parser(parser: argparse.ArgumentParser) -> None: def register_delete_parser(parser: argparse.ArgumentParser) -> None:
machines_parser = parser.add_argument("host", type=str) parser.add_argument("host", type=str)
add_dynamic_completer(machines_parser, complete_machines)
parser.set_defaults(func=delete_command) parser.set_defaults(func=delete_command)

View File

@ -8,7 +8,6 @@ from pathlib import Path
from tempfile import TemporaryDirectory from tempfile import TemporaryDirectory
from ..cmd import Log, run from ..cmd import Log, run
from ..completions import add_dynamic_completer, complete_machines
from ..facts.generate import generate_facts from ..facts.generate import generate_facts
from ..machines.machines import Machine from ..machines.machines import Machine
from ..nix import nix_shell from ..nix import nix_shell
@ -27,7 +26,6 @@ def install_nixos(
debug: bool = False, debug: bool = False,
password: str | None = None, password: str | None = None,
no_reboot: bool = False, no_reboot: bool = False,
extra_args: list[str] = [],
) -> None: ) -> None:
secret_facts_module = importlib.import_module(machine.secret_facts_module) secret_facts_module = importlib.import_module(machine.secret_facts_module)
log.info(f"installing {machine.name}") log.info(f"installing {machine.name}")
@ -58,7 +56,6 @@ def install_nixos(
f"{machine.flake}#{machine.name}", f"{machine.flake}#{machine.name}",
"--extra-files", "--extra-files",
str(tmpdir), str(tmpdir),
*extra_args,
] ]
if no_reboot: if no_reboot:
@ -98,7 +95,6 @@ class InstallOptions:
debug: bool debug: bool
no_reboot: bool no_reboot: bool
json_ssh_deploy: dict[str, str] | None json_ssh_deploy: dict[str, str] | None
nix_options: list[str]
def install_command(args: argparse.Namespace) -> None: def install_command(args: argparse.Namespace) -> None:
@ -131,7 +127,6 @@ def install_command(args: argparse.Namespace) -> None:
debug=args.debug, debug=args.debug,
no_reboot=args.no_reboot, no_reboot=args.no_reboot,
json_ssh_deploy=json_ssh_deploy, json_ssh_deploy=json_ssh_deploy,
nix_options=args.option,
) )
machine = Machine(opts.machine, flake=opts.flake) machine = Machine(opts.machine, flake=opts.flake)
machine.target_host_address = opts.target_host machine.target_host_address = opts.target_host
@ -147,7 +142,6 @@ def install_command(args: argparse.Namespace) -> None:
debug=opts.debug, debug=opts.debug,
password=password, password=password,
no_reboot=opts.no_reboot, no_reboot=opts.no_reboot,
extra_args=opts.nix_options,
) )
@ -189,14 +183,11 @@ def register_install_parser(parser: argparse.ArgumentParser) -> None:
help="do not ask for confirmation", help="do not ask for confirmation",
default=False, default=False,
) )
parser.add_argument(
machines_parser = parser.add_argument(
"machine", "machine",
type=str, type=str,
help="machine to install", help="machine to install",
) )
add_dynamic_completer(machines_parser, complete_machines)
parser.add_argument( parser.add_argument(
"target_host", "target_host",
type=str, type=str,

View File

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

View File

@ -1,4 +1,5 @@
import argparse import argparse
import dataclasses
import json import json
import logging import logging
from pathlib import Path from pathlib import Path
@ -11,15 +12,24 @@ from ..nix import nix_config, nix_eval
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@dataclasses.dataclass
class MachineInfo:
machine_name: str
machine_description: str | None
machine_icon: str | None
@API.register @API.register
def list_machines(flake_url: str | Path, debug: bool) -> list[str]: def list_machines(flake_url: str | Path, debug: bool) -> dict[str, MachineInfo]:
config = nix_config() config = nix_config()
system = config["system"] system = config["system"]
cmd = nix_eval( cmd = nix_eval(
[ [
f"{flake_url}#clanInternals.machines.{system}", f"{flake_url}#clanInternals.machines.{system}",
"--apply", "--apply",
"builtins.attrNames", """builtins.mapAttrs (name: attrs: {
inherit (attrs.config.clanCore) machineDescription machineIcon machineName;
})""",
"--json", "--json",
] ]
) )
@ -27,13 +37,27 @@ def list_machines(flake_url: str | Path, debug: bool) -> list[str]:
proc = run_no_stdout(cmd) proc = run_no_stdout(cmd)
res = proc.stdout.strip() res = proc.stdout.strip()
return json.loads(res) machines_dict = json.loads(res)
return {
k: MachineInfo(
machine_name=v.get("machineName"),
machine_description=v.get("machineDescription", None),
machine_icon=v.get("machineIcon", None),
)
for k, v in machines_dict.items()
}
def list_command(args: argparse.Namespace) -> None: def list_command(args: argparse.Namespace) -> None:
flake_path = Path(args.flake).resolve() flake_path = Path(args.flake).resolve()
for name in list_machines(flake_path, args.debug): print("Listing all machines:\n")
print(name) print("Source: ", flake_path)
print("-" * 40)
for name, machine in list_machines(flake_path, args.debug).items():
description = machine.machine_description or "[no description]"
print(f"{name}\n: {description}\n")
print("-" * 40)
def register_list_parser(parser: argparse.ArgumentParser) -> None: def register_list_parser(parser: argparse.ArgumentParser) -> None:

View File

@ -41,10 +41,9 @@ class QMPWrapper:
class Machine: class Machine:
name: str
flake: str | Path flake: str | Path
name: str
data: MachineData data: MachineData
nix_options: list[str]
eval_cache: dict[str, str] eval_cache: dict[str, str]
build_cache: dict[str, Path] build_cache: dict[str, Path]
_flake_path: Path | None _flake_path: Path | None
@ -56,7 +55,6 @@ class Machine:
name: str, name: str,
flake: Path | str, flake: Path | str,
deployment_info: dict | None = None, deployment_info: dict | None = None,
nix_options: list[str] = [],
machine: MachineData | None = None, machine: MachineData | None = None,
) -> None: ) -> None:
""" """
@ -78,7 +76,6 @@ class Machine:
self.build_cache: dict[str, Path] = {} self.build_cache: dict[str, Path] = {}
self._flake_path: Path | None = None self._flake_path: Path | None = None
self._deployment_info: None | dict = deployment_info 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) state_dir = vm_state_dir(flake_url=str(self.flake), vm_name=self.data.name)
@ -245,9 +242,9 @@ class Machine:
flake = f"path:{self.flake_dir}" flake = f"path:{self.flake_dir}"
args += [ args += [
f'{flake}#clanInternals.machines."{system}".{self.data.name}.{attr}' f'{flake}#clanInternals.machines."{system}".{self.data.name}.{attr}',
*nix_options,
] ]
args += nix_options + self.nix_options
if method == "eval": if method == "eval":
output = run_no_stdout(nix_eval(args)).stdout.strip() output = run_no_stdout(nix_eval(args)).stdout.strip()

View File

@ -1,60 +0,0 @@
import argparse
import dataclasses
import json
import logging
from pathlib import Path
from clan_cli.api import API
from ..cmd import run_no_stdout
from ..completions import add_dynamic_completer, complete_machines
from ..nix import nix_config, nix_eval
from .types import machine_name_type
log = logging.getLogger(__name__)
@dataclasses.dataclass
class MachineInfo:
machine_name: str
machine_description: str | None
machine_icon: str | None
@API.register
def show_machine(flake_url: str | Path, machine_name: str, debug: bool) -> MachineInfo:
config = nix_config()
system = config["system"]
cmd = nix_eval(
[
f"{flake_url}#clanInternals.machines.{system}.{machine_name}",
"--apply",
"machine: { inherit (machine.config.clanCore) machineDescription machineIcon machineName; }",
"--json",
]
)
proc = run_no_stdout(cmd)
res = proc.stdout.strip()
machine = json.loads(res)
return MachineInfo(
machine_name=machine.get("machineName"),
machine_description=machine.get("machineDescription", None),
machine_icon=machine.get("machineIcon", None),
)
def show_command(args: argparse.Namespace) -> None:
flake_path = Path(args.flake).resolve()
machine = show_machine(flake_path, args.machine, args.debug)
print(f"Name: {machine.machine_name}")
print(f"Description: {machine.machine_description or ''}")
print(f"Icon: {machine.machine_icon or ''}")
def register_show_parser(parser: argparse.ArgumentParser) -> None:
parser.set_defaults(func=show_command)
machine_parser = parser.add_argument(
"machine", help="the name of the machine", type=machine_name_type
)
add_dynamic_completer(machine_parser, complete_machines)

View File

@ -3,10 +3,9 @@ import json
import logging import logging
import os import os
import shlex import shlex
import subprocess
import sys import sys
from ..cmd import run
from ..completions import add_dynamic_completer, complete_machines
from ..errors import ClanError from ..errors import ClanError
from ..facts.generate import generate_facts from ..facts.generate import generate_facts
from ..facts.upload import upload_secrets from ..facts.upload import upload_secrets
@ -54,7 +53,11 @@ def upload_sources(
path, path,
] ]
) )
run(cmd, env=env, error_msg="failed to upload sources") proc = subprocess.run(cmd, stdout=subprocess.PIPE, env=env, check=False)
if proc.returncode != 0:
raise ClanError(
f"failed to upload sources: {shlex.join(cmd)} failed with {proc.returncode}"
)
return path return path
# Slow path: we need to upload all sources to the remote machine # Slow path: we need to upload all sources to the remote machine
@ -70,13 +73,16 @@ def upload_sources(
] ]
) )
log.info("run %s", shlex.join(cmd)) log.info("run %s", shlex.join(cmd))
proc = run(cmd, error_msg="failed to upload sources") proc = subprocess.run(cmd, stdout=subprocess.PIPE, check=False)
if proc.returncode != 0:
raise ClanError(
f"failed to upload sources: {shlex.join(cmd)} failed with {proc.returncode}"
)
try: try:
return json.loads(proc.stdout)["path"] return json.loads(proc.stdout)["path"]
except (json.JSONDecodeError, OSError) as e: except (json.JSONDecodeError, OSError) as e:
raise ClanError( raise ClanError(
f"failed to parse output of {shlex.join(cmd)}: {e}\nGot: {proc.stdout}" f"failed to parse output of {shlex.join(cmd)}: {e}\nGot: {proc.stdout.decode('utf-8', 'replace')}"
) )
@ -104,9 +110,11 @@ def deploy_nixos(machines: MachineGroup) -> None:
ssh_arg += " -i " + host.key if host.key else "" ssh_arg += " -i " + host.key if host.key else ""
extra_args = host.meta.get("extra_args", [])
cmd = [ cmd = [
"nixos-rebuild", "nixos-rebuild",
"switch", "switch",
*extra_args,
"--fast", "--fast",
"--option", "--option",
"keep-going", "keep-going",
@ -116,7 +124,6 @@ def deploy_nixos(machines: MachineGroup) -> None:
"true", "true",
"--build-host", "--build-host",
"", "",
*machine.nix_options,
"--flake", "--flake",
f"{path}#{machine.name}", f"{path}#{machine.name}",
] ]
@ -136,9 +143,7 @@ def update(args: argparse.Namespace) -> None:
raise ClanError("Could not find clan flake toplevel directory") raise ClanError("Could not find clan flake toplevel directory")
machines = [] machines = []
if len(args.machines) == 1 and args.target_host is not None: if len(args.machines) == 1 and args.target_host is not None:
machine = Machine( machine = Machine(name=args.machines[0], flake=args.flake)
name=args.machines[0], flake=args.flake, nix_options=args.option
)
machine.target_host_address = args.target_host machine.target_host_address = args.target_host
machines.append(machine) machines.append(machine)
@ -148,7 +153,7 @@ def update(args: argparse.Namespace) -> None:
else: else:
if len(args.machines) == 0: if len(args.machines) == 0:
ignored_machines = [] ignored_machines = []
for machine in get_all_machines(args.flake, args.option): for machine in get_all_machines(args.flake):
if machine.deployment_info.get("requireExplicitUpdate", False): if machine.deployment_info.get("requireExplicitUpdate", False):
continue continue
try: try:
@ -168,13 +173,13 @@ def update(args: argparse.Namespace) -> None:
print(machine, file=sys.stderr) print(machine, file=sys.stderr)
else: else:
machines = get_selected_machines(args.flake, args.option, args.machines) machines = get_selected_machines(args.flake, args.machines)
deploy_nixos(MachineGroup(machines)) deploy_nixos(MachineGroup(machines))
def register_update_parser(parser: argparse.ArgumentParser) -> None: def register_update_parser(parser: argparse.ArgumentParser) -> None:
machines_parser = parser.add_argument( parser.add_argument(
"machines", "machines",
type=str, type=str,
nargs="*", nargs="*",
@ -182,9 +187,6 @@ def register_update_parser(parser: argparse.ArgumentParser) -> None:
metavar="MACHINE", metavar="MACHINE",
help="machine to update. If no machine is specified, all machines will be updated.", help="machine to update. If no machine is specified, all machines will be updated.",
) )
add_dynamic_completer(machines_parser, complete_machines)
parser.add_argument( parser.add_argument(
"--target-host", "--target-host",
type=str, type=str,

View File

@ -1,7 +1,6 @@
import argparse import argparse
from pathlib import Path from pathlib import Path
from ..completions import add_dynamic_completer, complete_machines
from ..errors import ClanError from ..errors import ClanError
from ..git import commit_files from ..git import commit_files
from ..machines.types import machine_name_type, validate_hostname from ..machines.types import machine_name_type, validate_hostname
@ -148,28 +147,25 @@ def register_machines_parser(parser: argparse.ArgumentParser) -> None:
# Parser # Parser
get_parser = subparser.add_parser("get", help="get a machine public key") get_parser = subparser.add_parser("get", help="get a machine public key")
get_machine_parser = get_parser.add_argument( get_parser.add_argument(
"machine", help="the name of the machine", type=machine_name_type "machine", help="the name of the machine", type=machine_name_type
) )
add_dynamic_completer(get_machine_parser, complete_machines)
get_parser.set_defaults(func=get_command) get_parser.set_defaults(func=get_command)
# Parser # Parser
remove_parser = subparser.add_parser("remove", help="remove a machine") remove_parser = subparser.add_parser("remove", help="remove a machine")
remove_machine_parser = remove_parser.add_argument( remove_parser.add_argument(
"machine", help="the name of the machine", type=machine_name_type "machine", help="the name of the machine", type=machine_name_type
) )
add_dynamic_completer(remove_machine_parser, complete_machines)
remove_parser.set_defaults(func=remove_command) remove_parser.set_defaults(func=remove_command)
# Parser # Parser
add_secret_parser = subparser.add_parser( add_secret_parser = subparser.add_parser(
"add-secret", help="allow a machine to access a secret" "add-secret", help="allow a machine to access a secret"
) )
machine_add_secret_parser = add_secret_parser.add_argument( add_secret_parser.add_argument(
"machine", help="the name of the machine", type=machine_name_type "machine", help="the name of the machine", type=machine_name_type
) )
add_dynamic_completer(machine_add_secret_parser, complete_machines)
add_secret_parser.add_argument( add_secret_parser.add_argument(
"secret", help="the name of the secret", type=secret_name_type "secret", help="the name of the secret", type=secret_name_type
) )
@ -179,10 +175,9 @@ def register_machines_parser(parser: argparse.ArgumentParser) -> None:
remove_secret_parser = subparser.add_parser( remove_secret_parser = subparser.add_parser(
"remove-secret", help="remove a group's access to a secret" "remove-secret", help="remove a group's access to a secret"
) )
machine_remove_parser = remove_secret_parser.add_argument( remove_secret_parser.add_argument(
"machine", help="the name of the machine", type=machine_name_type "machine", help="the name of the group", type=machine_name_type
) )
add_dynamic_completer(machine_remove_parser, complete_machines)
remove_secret_parser.add_argument( remove_secret_parser.add_argument(
"secret", help="the name of the secret", type=secret_name_type "secret", help="the name of the secret", type=secret_name_type
) )

View File

@ -9,7 +9,6 @@ from pathlib import Path
from typing import IO from typing import IO
from .. import tty from .. import tty
from ..completions import add_dynamic_completer, complete_secrets
from ..errors import ClanError from ..errors import ClanError
from ..git import commit_files from ..git import commit_files
from .folders import ( from .folders import (
@ -154,12 +153,8 @@ def remove_command(args: argparse.Namespace) -> None:
remove_secret(Path(args.flake), args.secret) remove_secret(Path(args.flake), args.secret)
def add_secret_argument(parser: argparse.ArgumentParser, autocomplete: bool) -> None: def add_secret_argument(parser: argparse.ArgumentParser) -> None:
secrets_parser = parser.add_argument( parser.add_argument("secret", help="the name of the secret", type=secret_name_type)
"secret", help="the name of the secret", type=secret_name_type
)
if autocomplete:
add_dynamic_completer(secrets_parser, complete_secrets)
def machines_folder(flake_dir: Path, group: str) -> Path: def machines_folder(flake_dir: Path, group: str) -> Path:
@ -328,11 +323,11 @@ def register_secrets_parser(subparser: argparse._SubParsersAction) -> None:
parser_list.set_defaults(func=list_command) parser_list.set_defaults(func=list_command)
parser_get = subparser.add_parser("get", help="get a secret") parser_get = subparser.add_parser("get", help="get a secret")
add_secret_argument(parser_get, True) add_secret_argument(parser_get)
parser_get.set_defaults(func=get_command) parser_get.set_defaults(func=get_command)
parser_set = subparser.add_parser("set", help="set a secret") parser_set = subparser.add_parser("set", help="set a secret")
add_secret_argument(parser_set, False) add_secret_argument(parser_set)
parser_set.add_argument( parser_set.add_argument(
"--group", "--group",
type=str, type=str,
@ -364,10 +359,10 @@ def register_secrets_parser(subparser: argparse._SubParsersAction) -> None:
parser_set.set_defaults(func=set_command) parser_set.set_defaults(func=set_command)
parser_rename = subparser.add_parser("rename", help="rename a secret") parser_rename = subparser.add_parser("rename", help="rename a secret")
add_secret_argument(parser_rename, True) add_secret_argument(parser_rename)
parser_rename.add_argument("new_name", type=str, help="the new name of the secret") parser_rename.add_argument("new_name", type=str, help="the new name of the secret")
parser_rename.set_defaults(func=rename_command) parser_rename.set_defaults(func=rename_command)
parser_remove = subparser.add_parser("remove", help="remove a secret") parser_remove = subparser.add_parser("remove", help="remove a secret")
add_secret_argument(parser_remove, True) add_secret_argument(parser_remove)
parser_remove.set_defaults(func=remove_command) parser_remove.set_defaults(func=remove_command)

View File

@ -29,7 +29,6 @@
mypy, mypy,
nixpkgs, nixpkgs,
clan-core-path, clan-core-path,
gitMinimal,
}: }:
let let
# Dependencies that are directly used in the project # Dependencies that are directly used in the project
@ -114,13 +113,7 @@ python3.pkgs.buildPythonApplication {
format = "pyproject"; format = "pyproject";
# Arguments for the wrapper to unset LD_LIBRARY_PATH to avoid glibc version issues # Arguments for the wrapper to unset LD_LIBRARY_PATH to avoid glibc version issues
makeWrapperArgs = [ makeWrapperArgs = [ "--unset LD_LIBRARY_PATH" ];
"--unset LD_LIBRARY_PATH"
"--suffix"
"PATH"
":"
"${gitMinimal}/bin/git"
];
# Build-time dependencies. # Build-time dependencies.
nativeBuildInputs = [ nativeBuildInputs = [

View File

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

View File

@ -1,4 +1,7 @@
import json
import tempfile
from pathlib import Path from pathlib import Path
from typing import Any
import pytest import pytest
from cli import Cli from cli import Cli
@ -11,6 +14,46 @@ from clan_cli.errors import ClanError
example_options = f"{Path(config.__file__).parent}/jsonschema/options.json" example_options = f"{Path(config.__file__).parent}/jsonschema/options.json"
# use pytest.parametrize
@pytest.mark.parametrize(
"args,expected",
[
(["name", "DavHau"], {"name": "DavHau"}),
(
["kernelModules", "foo", "bar", "baz"],
{"kernelModules": ["foo", "bar", "baz"]},
),
(["services.opt", "test"], {"services": {"opt": "test"}}),
(["userIds.DavHau", "42"], {"userIds": {"DavHau": 42}}),
],
)
def test_set_some_option(
args: list[str],
expected: dict[str, Any],
test_flake: FlakeForTest,
) -> None:
# create temporary file for out_file
with tempfile.NamedTemporaryFile() as out_file:
with open(out_file.name, "w") as f:
json.dump({}, f)
cli = Cli()
cli.run(
[
"--flake",
str(test_flake.path),
"config",
"--quiet",
"--options-file",
example_options,
"--settings-file",
out_file.name,
*args,
]
)
json_out = json.loads(open(out_file.name).read())
assert json_out == expected
def test_configure_machine( def test_configure_machine(
test_flake: FlakeForTest, test_flake: FlakeForTest,
temporary_home: Path, temporary_home: Path,
@ -19,14 +62,25 @@ def test_configure_machine(
) -> None: ) -> None:
cli = Cli() cli = Cli()
cli.run(
[
"--flake",
str(test_flake.path),
"config",
"-m",
"machine1",
"clan.jitsi.enable",
"true",
]
)
# clear the output buffer # clear the output buffer
capsys.readouterr() capsys.readouterr()
# read a option value # read a option value
cli.run( cli.run(
[ [
"config",
"--flake", "--flake",
str(test_flake.path), str(test_flake.path),
"config",
"-m", "-m",
"machine1", "machine1",
"clan.jitsi.enable", "clan.jitsi.enable",
@ -34,7 +88,7 @@ def test_configure_machine(
) )
# read the output # read the output
assert capsys.readouterr().out == "false\n" assert capsys.readouterr().out == "true\n"
def test_walk_jsonschema_all_types() -> None: def test_walk_jsonschema_all_types() -> None:

View File

@ -17,12 +17,10 @@ def test_create_flake(
capsys: pytest.CaptureFixture, capsys: pytest.CaptureFixture,
temporary_home: Path, temporary_home: Path,
cli: Cli, cli: Cli,
clan_core: Path,
) -> None: ) -> None:
flake_dir = temporary_home / "test-flake" flake_dir = temporary_home / "test-flake"
url = f"{clan_core}#default" cli.run(["flakes", "create", str(flake_dir)])
cli.run(["flakes", "create", str(flake_dir), f"--url={url}"])
assert (flake_dir / ".clan-flake").exists() assert (flake_dir / ".clan-flake").exists()
monkeypatch.chdir(flake_dir) monkeypatch.chdir(flake_dir)
cli.run(["machines", "create", "machine1"]) cli.run(["machines", "create", "machine1"])
@ -49,34 +47,16 @@ def test_create_flake(
flake_outputs["nixosConfigurations"]["machine1"] flake_outputs["nixosConfigurations"]["machine1"]
except KeyError: except KeyError:
pytest.fail("nixosConfigurations.machine1 not found in flake outputs") pytest.fail("nixosConfigurations.machine1 not found in flake outputs")
# configure machine1
capsys.readouterr()
@pytest.mark.impure cli.run(["config", "--machine", "machine1", "services.openssh.enable", ""])
def test_ui_template( capsys.readouterr()
monkeypatch: pytest.MonkeyPatch, cli.run(
capsys: pytest.CaptureFixture, [
temporary_home: Path, "config",
cli: Cli, "--machine",
clan_core: Path, "machine1",
) -> None: "services.openssh.enable",
flake_dir = temporary_home / "test-flake" "true",
url = f"{clan_core}#empty" ]
cli.run(["flakes", "create", str(flake_dir), f"--url={url}"])
assert (flake_dir / ".clan-flake").exists()
monkeypatch.chdir(flake_dir)
cli.run(["machines", "create", "machine1"])
capsys.readouterr() # flush cache
cli.run(["machines", "list"])
assert "machine1" in capsys.readouterr().out
flake_show = subprocess.run(
["nix", "flake", "show", "--json"],
check=True,
capture_output=True,
text=True,
) )
flake_outputs = json.loads(flake_show.stdout)
try:
flake_outputs["nixosConfigurations"]["machine1"]
except KeyError:
pytest.fail("nixosConfigurations.machine1 not found in flake outputs")

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -23,11 +23,11 @@ def test_secrets_upload(
cli = Cli() cli = Cli()
cli.run( cli.run(
[ [
"--flake",
str(test_flake_with_core.path),
"secrets", "secrets",
"users", "users",
"add", "add",
"--flake",
str(test_flake_with_core.path),
"user1", "user1",
age_keys[0].pubkey, age_keys[0].pubkey,
] ]
@ -35,18 +35,18 @@ def test_secrets_upload(
cli.run( cli.run(
[ [
"--flake",
str(test_flake_with_core.path),
"secrets", "secrets",
"machines", "machines",
"add", "add",
"--flake",
str(test_flake_with_core.path),
"vm1", "vm1",
age_keys[1].pubkey, age_keys[1].pubkey,
] ]
) )
monkeypatch.setenv("SOPS_NIX_SECRET", age_keys[0].privkey) monkeypatch.setenv("SOPS_NIX_SECRET", age_keys[0].privkey)
cli.run( cli.run(
["secrets", "set", "--flake", str(test_flake_with_core.path), "vm1-age.key"] ["--flake", str(test_flake_with_core.path), "secrets", "set", "vm1-age.key"]
) )
flake = test_flake_with_core.path.joinpath("flake.nix") 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) new_text = flake.read_text().replace("__CLAN_TARGET_ADDRESS__", addr)
flake.write_text(new_text) flake.write_text(new_text)
cli.run(["facts", "upload", "--flake", str(test_flake_with_core.path), "vm1"]) cli.run(["--flake", str(test_flake_with_core.path), "facts", "upload", "vm1"])
# the flake defines this path as the location where the sops key should be installed # 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") 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 test_flake_with_core: FlakeForTest, capsys: pytest.CaptureFixture
) -> None: ) -> None:
cli = Cli() cli = Cli()
cli.run(["vms", "inspect", "--flake", str(test_flake_with_core.path), "vm1"]) cli.run(["--flake", str(test_flake_with_core.path), "vms", "inspect", "vm1"])
out = capsys.readouterr() # empty the buffer out = capsys.readouterr() # empty the buffer
assert "Cores" in out.out assert "Cores" in out.out

View File

@ -47,7 +47,7 @@ class MainApplication(Adw.Application):
None, None,
) )
self.window: MainWindow | None = None self.window: "MainWindow" | None = None
self.connect("activate", self.on_activate) self.connect("activate", self.on_activate)
self.connect("shutdown", self.on_shutdown) self.connect("shutdown", self.on_shutdown)

View File

@ -29,7 +29,7 @@ class GKVStore(GObject.GObject, Gio.ListModel, Generic[K, V]):
self.gtype = gtype self.gtype = gtype
self.key_gen = key_gen self.key_gen = key_gen
# From Python 3.7 onwards dictionaries are ordered by default # From Python 3.7 onwards dictionaries are ordered by default
self._items: dict[K, V] = dict() self._items: "dict[K, V]" = dict()
################################## ##################################
# # # #

View File

@ -46,12 +46,6 @@ class WebView:
self.method_registry: dict[str, Callable] = methods self.method_registry: dict[str, Callable] = methods
self.webview = WebKit.WebView() self.webview = WebKit.WebView()
settings = self.webview.get_settings()
# settings.
settings.set_property("enable-developer-extras", True)
self.webview.set_settings(settings)
self.manager = self.webview.get_user_content_manager() self.manager = self.webview.get_user_content_manager()
# Can be called with: window.webkit.messageHandlers.gtk.postMessage("...") # Can be called with: window.webkit.messageHandlers.gtk.postMessage("...")
# Important: it seems postMessage must be given some payload, otherwise it won't trigger the event # Important: it seems postMessage must be given some payload, otherwise it won't trigger the event

View File

@ -37,4 +37,6 @@ writeShellApplication {
exec ${lib.getExe codium} --user-data-dir "$DATA_DIR" "$@" exec ${lib.getExe codium} --user-data-dir "$DATA_DIR" "$@"
''; '';
derivationArgs.passthru.completion-options = import ./completion-options.nix;
} }

View File

@ -0,0 +1,17 @@
let
flake = builtins.getFlake "https://git.clan.lol/clan/clan-core/archive/main.tar.gz";
nixpkgs = flake.inputs.nixpkgs;
pkgs = nixpkgs.legacyPackages.${builtins.currentSystem};
clanCore = flake.outputs.nixosModules.clanCore;
clanModules = flake.outputs.clanModules;
allNixosModules = (import "${nixpkgs}/nixos/modules/module-list.nix") ++ [
"${nixpkgs}/nixos/modules/misc/assertions.nix"
{ nixpkgs.hostPlatform = "x86_64-linux"; }
];
clanCoreNixosModules = [
clanCore
# { clanCore.clanDir = ./.; }
] ++ allNixosModules ++ (builtins.attrValues clanModules);
clanCoreNixos = pkgs.nixos { imports = clanCoreNixosModules; };
in
clanCoreNixos.options

View File

@ -14,9 +14,12 @@
= (import <nixpkgs/nixos/modules/module-list.nix>) ++ [ ({...}: { = (import <nixpkgs/nixos/modules/module-list.nix>) ++ [ ({...}: {
nixpkgs.hostPlatform = builtins.currentSystem;} ) ] ; })).options" nixpkgs.hostPlatform = builtins.currentSystem;} ) ] ; })).options"
}, },
"home-manager": { "clan": {
"expr": "(builtins.getFlake \"github:nix-community/home-manager\").homeConfigurations.<name>.options" "expr": "let pkgs = import <nixpkgs> { }; flake = builtins.getFlake \"https://git.clan.lol/clan/clan-core/archive/main.tar.gz\"; clanCore = flake.outputs.nixosModules.clanCore; clanModules = flake.outputs.clanModules; allNixosModules = (import <nixpkgs/nixos/modules/module-list.nix>) ++ [<nixpkgs/nixos/modules/misc/assertions.nix> {nixpkgs.hostPlatform = \"x86_64-linux\";}]; clanCoreNixosModules = [clanCore] ++ allNixosModules ++ (builtins.attrValues clanModules); clanCoreNixos = pkgs.nixos { imports = clanCoreNixosModules;}; in clanCoreNixos.options"
} },
"clan-core": {
"expr": "(builtins.getFlake \"/home/kenji/git/clan-projects/clan-core\").packages.x86_64-linux.editor.passthru.completion-options",
},
} }
} }
} }

View File

@ -29,6 +29,7 @@
editor = pkgs.callPackage ./editor/clan-edit-codium.nix { }; editor = pkgs.callPackage ./editor/clan-edit-codium.nix { };
} }
// lib.optionalAttrs pkgs.stdenv.isLinux { // lib.optionalAttrs pkgs.stdenv.isLinux {
wayland-proxy-virtwl = pkgs.callPackage ./wayland-proxy-virtwl { };
# halalify zerotierone # halalify zerotierone
zerotierone = pkgs.zerotierone.overrideAttrs (_old: { zerotierone = pkgs.zerotierone.overrideAttrs (_old: {
meta = _old.meta // { meta = _old.meta // {

View File

@ -8,9 +8,8 @@ clean_temp_dir() {
rm -rf "$temp_dir" rm -rf "$temp_dir"
} }
is_installed() { is_nix_installed() {
name=$1 if [ -n "$(command -v nix)" ]; then
if [ -n "$(command -v "$name")" ]; then
return 0 return 0
else else
return 1 return 1
@ -18,18 +17,9 @@ is_installed() {
} }
install_nix() { install_nix() {
if is_installed curl; then curl --proto '=https' --tlsv1.2 -sSf -L \
curl --proto '=https' --tlsv1.2 -sSf -L \ https://install.determinate.systems/nix \
https://install.determinate.systems/nix \ > "$temp_dir"/install_nix.sh
> "$temp_dir"/install_nix.sh
elif is_installed wget; then
wget -qO- \
https://install.determinate.systems/nix \
> "$temp_dir"/install_nix.sh
else
echo "Either curl or wget is required to install Nix. Exiting."
exit 1
fi
NIX_INSTALLER_DIAGNOSTIC_ENDPOINT="" sh "$temp_dir"/install_nix.sh install NIX_INSTALLER_DIAGNOSTIC_ENDPOINT="" sh "$temp_dir"/install_nix.sh install
} }
@ -45,17 +35,15 @@ ask_then_install_nix() {
} }
ensure_nix_installed() { ensure_nix_installed() {
if ! is_installed nix; then if ! is_nix_installed; then
ask_then_install_nix ask_then_install_nix
fi fi
} }
start_clan_gui() { start_clan_gui() {
PATH="${PATH:+$PATH:}/nix/var/nix/profiles/default/bin" \ exec nix run \
exec nix run \ https://git.clan.lol/clan/clan-core/archive/main.tar.gz#clan-vm-manager \
https://git.clan.lol/clan/clan-core/archive/main.tar.gz#clan-vm-manager \ --extra-experimental-features flakes nix-command -- "$@"
--no-accept-flake-config \
--extra-experimental-features flakes nix-command -- "$@"
} }
main() { main() {

View File

@ -98,6 +98,9 @@ let
in in
{ {
clan = { clan = {
clanName = "clan-core";
directory = self;
# To build a generic installer image (without ssh pubkeys), # To build a generic installer image (without ssh pubkeys),
# use the following command: # use the following command:
# $ nix build .#iso-installer # $ nix build .#iso-installer

26
pkgs/merge-after-ci/merge-after-ci.py Executable file → Normal file
View File

@ -1,5 +1,4 @@
import argparse import argparse
import shlex
import subprocess import subprocess
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
@ -12,17 +11,14 @@ args = parser.parse_args()
if not args.reviewers and not args.no_review: if not args.reviewers and not args.no_review:
parser.error("either --reviewers or --no-review must be given") parser.error("either --reviewers or --no-review must be given")
cmd = [ subprocess.run(
"tea-create-pr", [
"origin", "tea-create-pr",
"upstream", "origin",
"main", "main",
"--assignees", "--assignees",
",".join(["clan-bot", *args.reviewers]), ",".join(["clan-bot", *args.reviewers]),
*(["--labels", "needs-review"] if not args.no_review else []), *(["--labels", "needs-review"] if not args.no_review else []),
*args.args, *args.args,
] ]
)
print("Running:", shlex.join(cmd))
subprocess.run(cmd)

49
pkgs/tea-create-pr/script.sh Executable file → Normal file
View File

@ -1,46 +1,17 @@
#!/usr/bin/env bash #!/usr/bin/env bash
set -euo pipefail set -euo pipefail
remoteFork="${1:-origin}" remoteName="${1:-origin}"
remoteUpstream="${2:-upstream}" targetBranch="${2:-main}"
targetBranch="${3:-main}" shift && shift
shift && shift && shift
TMPDIR="$(mktemp -d)" TMPDIR="$(mktemp -d)"
currentBranch="$(git rev-parse --abbrev-ref HEAD)" currentBranch="$(git rev-parse --abbrev-ref HEAD)"
user_unparsed="$(tea whoami)" user="$(tea login list -o simple | cut -d" " -f4 | head -n1)"
user="$(echo "$user_unparsed" | tr -d '\n' | cut -f4 -d' ')"
tempRemoteBranch="$user-$currentBranch" tempRemoteBranch="$user-$currentBranch"
root_dir=$(git rev-parse --show-toplevel)
nix fmt -- --fail-on-change
# Function to check if a remote exists git log --reverse --pretty="format:%s%n%n%b%n%n" "$remoteName/$targetBranch..HEAD" > "$TMPDIR"/commit-msg
check_remote() {
if git remote get-url "$1" > /dev/null 2>&1; then
return 0
else
return 1
fi
}
# Check if the remote 'upstream' is defined
if ! check_remote "$remoteUpstream"; then
echo "Error: $remoteUpstream remote is not defined."
echo "Please fork the repository and add the $remoteUpstream remote."
echo "$ git remote add $remoteUpstream <upstream-url>"
exit 1
fi
treefmt --fail-on-change -C "$root_dir"
upstream_url=$(git remote get-url "$remoteUpstream")
set -x
git fetch "$remoteUpstream" && git rebase "$remoteUpstream"/main --autostash
set +x
repo=$(echo "$upstream_url" | sed -E 's#.*:([^/]+/[^.]+)\.git#\1#')
git log --reverse --pretty="format:%s%n%n%b%n%n" "$remoteUpstream/$targetBranch..HEAD" > "$TMPDIR"/commit-msg
$EDITOR "$TMPDIR"/commit-msg $EDITOR "$TMPDIR"/commit-msg
@ -52,13 +23,11 @@ rest=$(echo "$COMMIT_MSG" | tail -n+2)
if [[ "$firstLine" == "$rest" ]]; then if [[ "$firstLine" == "$rest" ]]; then
rest="" rest=""
fi fi
git push --force -u "$remoteName" HEAD:refs/heads/"$tempRemoteBranch"
git push --force -u "$remoteFork" HEAD:refs/heads/"$tempRemoteBranch"
tea pr create \ tea pr create \
--repo "$repo" \
--head "$user:$tempRemoteBranch" \
--title "$firstLine" \ --title "$firstLine" \
--description "$rest" \ --description "$rest" \
--head "$tempRemoteBranch" \
--base "$targetBranch" \
"$@" "$@"

View File

@ -0,0 +1,35 @@
{
wayland-proxy-virtwl,
fetchFromGitHub,
libdrm,
ocaml-ng,
}:
let
ocaml-wayland = ocaml-ng.ocamlPackages_5_0.wayland.overrideAttrs (_old: {
src = fetchFromGitHub {
owner = "Mic92";
repo = "ocaml-wayland";
rev = "f6910aa5b626fa582cc000d4fe7b50182d11b439";
hash = "sha256-cg3HLezWTxWoYWSrirOV12gv1CRz1gMIOT7j3j3v5EA=";
};
});
in
wayland-proxy-virtwl.overrideAttrs (_old: {
src = fetchFromGitHub {
owner = "Mic92";
repo = "wayland-proxy-virtwl";
rev = "652fca9d4e006a2bdeba920dfaf53190c5373a7d";
hash = "sha256-VgpqxjHgueK9eQSX987PF0KvscpzkScOzFkW3haYCOw=";
};
buildInputs =
[ libdrm ]
++ (with ocaml-ng.ocamlPackages_5_0; [
ocaml-wayland
dune-configurator
eio_main
ppx_cstruct
cmdliner
logs
ppx_cstruct
]);
})

View File

@ -19,7 +19,6 @@ export default tseslint.config(
"error", "error",
{ {
callees: ["cx"], callees: ["cx"],
whitelist: ["material-icons"],
}, },
], ],
}, },

View File

@ -5,9 +5,6 @@ const distPath = path.resolve(__dirname, "dist");
const manifestPath = path.join(distPath, ".vite/manifest.json"); const manifestPath = path.join(distPath, ".vite/manifest.json");
const outputPath = path.join(distPath, "index.html"); const outputPath = path.join(distPath, "index.html");
const postcss = require("postcss");
const css_url = require("postcss-url");
fs.readFile(manifestPath, { encoding: "utf8" }, (err, data) => { fs.readFile(manifestPath, { encoding: "utf8" }, (err, data) => {
if (err) { if (err) {
return console.error("Failed to read manifest:", err); return console.error("Failed to read manifest:", err);
@ -28,48 +25,19 @@ fs.readFile(manifestPath, { encoding: "utf8" }, (err, data) => {
// Add linked stylesheets // Add linked stylesheets
assets.forEach((asset) => { assets.forEach((asset) => {
// console.log(asset); asset.css.forEach((cssEntry) => {
if (asset.src === "index.html") { htmlContent += `\n <link rel="stylesheet" href="${cssEntry}">`;
asset.css.forEach((cssEntry) => { });
// css to be processed
const css = fs.readFileSync(`dist/${cssEntry}`, "utf8");
// process css
postcss()
.use(
css_url({
url: (asset, dir) => {
const res = path.basename(asset.url);
console.log(`Rewriting CSS url(): ${asset.url} to ${res}`);
return res;
},
})
)
.process(css, {
from: `dist/${cssEntry}`,
to: `dist/${cssEntry}`,
})
.then((result) => {
fs.writeFileSync(`dist/${cssEntry}`, result.css, "utf8");
});
// Extend the HTML content with the linked stylesheet
console.log(`Relinking html css stylesheet: ${cssEntry}`);
htmlContent += `\n <link rel="stylesheet" href="${cssEntry}">`;
});
}
}); });
htmlContent += ` htmlContent += `
</head> </head>
<body> <body>
<div id="app"></div> <div id="app"></div>
`; `;
// Add scripts // Add scripts
assets.forEach((asset) => { assets.forEach((asset) => {
if (asset.file.endsWith(".js")) { if (asset.file.endsWith(".js")) {
console.log(`Relinking js script: ${asset.file}`);
htmlContent += `\n <script src="${asset.file}"></script>`; htmlContent += `\n <script src="${asset.file}"></script>`;
} }
}); });

View File

@ -1,16 +0,0 @@
import { JSONSchemaFaker } from "json-schema-faker";
import { schema } from "./api/index";
import { OperationNames } from "./src/message";
const faker = JSONSchemaFaker;
faker.option({
alwaysFakeOptionals: true,
});
const getFakeResponse = (method: OperationNames, data: any) => {
const fakeData = faker.generate(schema.properties[method].properties.return);
return fakeData;
};
export { getFakeResponse };

View File

@ -9,12 +9,10 @@
"version": "0.0.1", "version": "0.0.1",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"material-icons": "^1.13.12",
"solid-js": "^1.8.11" "solid-js": "^1.8.11"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.3.0", "@eslint/js": "^9.3.0",
"@tailwindcss/typography": "^0.5.13",
"@types/node": "^20.12.12", "@types/node": "^20.12.12",
"@typescript-eslint/parser": "^7.10.0", "@typescript-eslint/parser": "^7.10.0",
"autoprefixer": "^10.4.19", "autoprefixer": "^10.4.19",
@ -22,10 +20,8 @@
"daisyui": "^4.11.1", "daisyui": "^4.11.1",
"eslint": "^8.57.0", "eslint": "^8.57.0",
"eslint-plugin-tailwindcss": "^3.17.0", "eslint-plugin-tailwindcss": "^3.17.0",
"json-schema-faker": "^0.5.6",
"json-schema-to-ts": "^3.1.0", "json-schema-to-ts": "^3.1.0",
"postcss": "^8.4.38", "postcss": "^8.4.38",
"postcss-url": "^10.1.3",
"prettier": "^3.2.5", "prettier": "^3.2.5",
"solid-devtools": "^0.29.2", "solid-devtools": "^0.29.2",
"tailwindcss": "^3.4.3", "tailwindcss": "^3.4.3",
@ -1486,34 +1482,6 @@
"solid-js": "^1.6.12" "solid-js": "^1.6.12"
} }
}, },
"node_modules/@tailwindcss/typography": {
"version": "0.5.13",
"resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.13.tgz",
"integrity": "sha512-ADGcJ8dX21dVVHIwTRgzrcunY6YY9uSlAHHGVKvkA+vLc5qLwEszvKts40lx7z0qc4clpjclwLeK5rVCV2P/uw==",
"dev": true,
"dependencies": {
"lodash.castarray": "^4.4.0",
"lodash.isplainobject": "^4.0.6",
"lodash.merge": "^4.6.2",
"postcss-selector-parser": "6.0.10"
},
"peerDependencies": {
"tailwindcss": ">=3.0.0 || insiders"
}
},
"node_modules/@tailwindcss/typography/node_modules/postcss-selector-parser": {
"version": "6.0.10",
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz",
"integrity": "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==",
"dev": true,
"dependencies": {
"cssesc": "^3.0.0",
"util-deprecate": "^1.0.2"
},
"engines": {
"node": ">=4"
}
},
"node_modules/@types/babel__core": { "node_modules/@types/babel__core": {
"version": "7.20.5", "version": "7.20.5",
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
@ -2022,12 +1990,6 @@
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
} }
}, },
"node_modules/call-me-maybe": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.2.tgz",
"integrity": "sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ==",
"dev": true
},
"node_modules/callsites": { "node_modules/callsites": {
"version": "3.1.0", "version": "3.1.0",
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
@ -2221,12 +2183,6 @@
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="
}, },
"node_modules/cuint": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/cuint/-/cuint-0.2.2.tgz",
"integrity": "sha512-d4ZVpCW31eWwCMe1YT3ur7mUDnTXbgwyzaL320DrcRT45rfjYxkt5QWLrmOJ+/UEAI2+fQgKe/fCjR8l4TpRgw==",
"dev": true
},
"node_modules/culori": { "node_modules/culori": {
"version": "3.3.0", "version": "3.3.0",
"resolved": "https://registry.npmjs.org/culori/-/culori-3.3.0.tgz", "resolved": "https://registry.npmjs.org/culori/-/culori-3.3.0.tgz",
@ -2667,19 +2623,6 @@
"url": "https://opencollective.com/eslint" "url": "https://opencollective.com/eslint"
} }
}, },
"node_modules/esprima": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
"integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
"dev": true,
"bin": {
"esparse": "bin/esparse.js",
"esvalidate": "bin/esvalidate.js"
},
"engines": {
"node": ">=4"
}
},
"node_modules/esquery": { "node_modules/esquery": {
"version": "1.5.0", "version": "1.5.0",
"resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz",
@ -2859,12 +2802,6 @@
"url": "https://github.com/sponsors/isaacs" "url": "https://github.com/sponsors/isaacs"
} }
}, },
"node_modules/format-util": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/format-util/-/format-util-1.0.5.tgz",
"integrity": "sha512-varLbTj0e0yVyRpqQhuWV+8hlePAgaoFRhNFj50BNjEIrw1/DphHSObtqwskVCPWNgzwPoQrZAbfa/SBiicNeg==",
"dev": true
},
"node_modules/fraction.js": { "node_modules/fraction.js": {
"version": "4.3.7", "version": "4.3.7",
"resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz",
@ -3260,53 +3197,6 @@
"integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
"dev": true "dev": true
}, },
"node_modules/json-schema-faker": {
"version": "0.5.6",
"resolved": "https://registry.npmjs.org/json-schema-faker/-/json-schema-faker-0.5.6.tgz",
"integrity": "sha512-u/cFC26/GDxh2vPiAC8B8xVvpXAW+QYtG2mijEbKrimCk8IHtiwQBjCE8TwvowdhALWq9IcdIWZ+/8ocXvdL3Q==",
"dev": true,
"dependencies": {
"json-schema-ref-parser": "^6.1.0",
"jsonpath-plus": "^7.2.0"
},
"bin": {
"jsf": "bin/gen.cjs"
}
},
"node_modules/json-schema-ref-parser": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/json-schema-ref-parser/-/json-schema-ref-parser-6.1.0.tgz",
"integrity": "sha512-pXe9H1m6IgIpXmE5JSb8epilNTGsmTb2iPohAXpOdhqGFbQjNeHHsZxU+C8w6T81GZxSPFLeUoqDJmzxx5IGuw==",
"deprecated": "Please switch to @apidevtools/json-schema-ref-parser",
"dev": true,
"dependencies": {
"call-me-maybe": "^1.0.1",
"js-yaml": "^3.12.1",
"ono": "^4.0.11"
}
},
"node_modules/json-schema-ref-parser/node_modules/argparse": {
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
"integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
"dev": true,
"dependencies": {
"sprintf-js": "~1.0.2"
}
},
"node_modules/json-schema-ref-parser/node_modules/js-yaml": {
"version": "3.14.1",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz",
"integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==",
"dev": true,
"dependencies": {
"argparse": "^1.0.7",
"esprima": "^4.0.0"
},
"bin": {
"js-yaml": "bin/js-yaml.js"
}
},
"node_modules/json-schema-to-ts": { "node_modules/json-schema-to-ts": {
"version": "3.1.0", "version": "3.1.0",
"resolved": "https://registry.npmjs.org/json-schema-to-ts/-/json-schema-to-ts-3.1.0.tgz", "resolved": "https://registry.npmjs.org/json-schema-to-ts/-/json-schema-to-ts-3.1.0.tgz",
@ -3344,15 +3234,6 @@
"node": ">=6" "node": ">=6"
} }
}, },
"node_modules/jsonpath-plus": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/jsonpath-plus/-/jsonpath-plus-7.2.0.tgz",
"integrity": "sha512-zBfiUPM5nD0YZSBT/o/fbCUlCcepMIdP0CJZxM1+KgA4f2T206f6VAg9e7mX35+KlMaIc5qXW34f3BnwJ3w+RA==",
"dev": true,
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/keyv": { "node_modules/keyv": {
"version": "4.5.4", "version": "4.5.4",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
@ -3433,18 +3314,6 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/lodash.castarray": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/lodash.castarray/-/lodash.castarray-4.4.0.tgz",
"integrity": "sha512-aVx8ztPv7/2ULbArGJ2Y42bG1mEQ5mGjpdvrbJcJFU3TbYybe+QlLS4pst9zV52ymy2in1KpFPiZnAOATxD4+Q==",
"dev": true
},
"node_modules/lodash.isplainobject": {
"version": "4.0.6",
"resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
"integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==",
"dev": true
},
"node_modules/lodash.merge": { "node_modules/lodash.merge": {
"version": "4.6.2", "version": "4.6.2",
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
@ -3497,11 +3366,6 @@
"semver": "bin/semver" "semver": "bin/semver"
} }
}, },
"node_modules/material-icons": {
"version": "1.13.12",
"resolved": "https://registry.npmjs.org/material-icons/-/material-icons-1.13.12.tgz",
"integrity": "sha512-/2YoaB79IjUK2B2JB+vIXXYGtBfHb/XG66LvoKVM5ykHW7yfrV5SP6d7KLX6iijY6/G9GqwgtPQ/sbhFnOURVA=="
},
"node_modules/merge-anything": { "node_modules/merge-anything": {
"version": "5.1.7", "version": "5.1.7",
"resolved": "https://registry.npmjs.org/merge-anything/-/merge-anything-5.1.7.tgz", "resolved": "https://registry.npmjs.org/merge-anything/-/merge-anything-5.1.7.tgz",
@ -3687,15 +3551,6 @@
"wrappy": "1" "wrappy": "1"
} }
}, },
"node_modules/ono": {
"version": "4.0.11",
"resolved": "https://registry.npmjs.org/ono/-/ono-4.0.11.tgz",
"integrity": "sha512-jQ31cORBFE6td25deYeD80wxKBMj+zBmHTrVxnc6CKhx8gho6ipmWM5zj/oeoqioZ99yqBls9Z/9Nss7J26G2g==",
"dev": true,
"dependencies": {
"format-util": "^1.0.3"
}
},
"node_modules/optionator": { "node_modules/optionator": {
"version": "0.9.4", "version": "0.9.4",
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
@ -4012,73 +3867,6 @@
"node": ">=4" "node": ">=4"
} }
}, },
"node_modules/postcss-url": {
"version": "10.1.3",
"resolved": "https://registry.npmjs.org/postcss-url/-/postcss-url-10.1.3.tgz",
"integrity": "sha512-FUzyxfI5l2tKmXdYc6VTu3TWZsInayEKPbiyW+P6vmmIrrb4I6CGX0BFoewgYHLK+oIL5FECEK02REYRpBvUCw==",
"dev": true,
"dependencies": {
"make-dir": "~3.1.0",
"mime": "~2.5.2",
"minimatch": "~3.0.4",
"xxhashjs": "~0.2.2"
},
"engines": {
"node": ">=10"
},
"peerDependencies": {
"postcss": "^8.0.0"
}
},
"node_modules/postcss-url/node_modules/brace-expansion": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"dev": true,
"dependencies": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
}
},
"node_modules/postcss-url/node_modules/make-dir": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz",
"integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==",
"dev": true,
"dependencies": {
"semver": "^6.0.0"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/postcss-url/node_modules/mime": {
"version": "2.5.2",
"resolved": "https://registry.npmjs.org/mime/-/mime-2.5.2.tgz",
"integrity": "sha512-tqkh47FzKeCPD2PUiPB6pkbMzsCasjxAfC62/Wap5qrUWcb+sFasXUC5I3gYM5iBM8v/Qpn4UK0x+j0iHyFPDg==",
"dev": true,
"bin": {
"mime": "cli.js"
},
"engines": {
"node": ">=4.0.0"
}
},
"node_modules/postcss-url/node_modules/minimatch": {
"version": "3.0.8",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.8.tgz",
"integrity": "sha512-6FsRAQsxQ61mw+qP1ZzbL9Bc78x2p5OqNgNpnoAFLTrX8n5Kxph0CsnhmKKNXTWjXqU5L0pGPR7hYk+XWZr60Q==",
"dev": true,
"dependencies": {
"brace-expansion": "^1.1.7"
},
"engines": {
"node": "*"
}
},
"node_modules/postcss-value-parser": { "node_modules/postcss-value-parser": {
"version": "4.2.0", "version": "4.2.0",
"resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
@ -4501,12 +4289,6 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/sprintf-js": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
"integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==",
"dev": true
},
"node_modules/string-width": { "node_modules/string-width": {
"version": "5.1.2", "version": "5.1.2",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
@ -5226,15 +5008,6 @@
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
"dev": true "dev": true
}, },
"node_modules/xxhashjs": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/xxhashjs/-/xxhashjs-0.2.2.tgz",
"integrity": "sha512-AkTuIuVTET12tpsVIQo+ZU6f/qDmKuRUcjaqR+OIvm+aCBsZ95i7UVY5WJ9TMsSaZ0DA2WxoZ4acu0sPH+OKAw==",
"dev": true,
"dependencies": {
"cuint": "^0.2.2"
}
},
"node_modules/yallist": { "node_modules/yallist": {
"version": "3.1.1", "version": "3.1.1",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",

View File

@ -13,28 +13,24 @@
"license": "MIT", "license": "MIT",
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.3.0", "@eslint/js": "^9.3.0",
"@tailwindcss/typography": "^0.5.13",
"@types/node": "^20.12.12", "@types/node": "^20.12.12",
"@typescript-eslint/parser": "^7.10.0", "@typescript-eslint/parser": "^7.10.0",
"autoprefixer": "^10.4.19", "autoprefixer": "^10.4.19",
"classnames": "^2.5.1", "classnames": "^2.5.1",
"daisyui": "^4.11.1", "daisyui": "^4.11.1",
"eslint": "^8.57.0", "eslint": "^8.57.0",
"eslint-plugin-tailwindcss": "^3.17.0",
"json-schema-faker": "^0.5.6",
"json-schema-to-ts": "^3.1.0", "json-schema-to-ts": "^3.1.0",
"postcss": "^8.4.38", "postcss": "^8.4.38",
"postcss-url": "^10.1.3",
"prettier": "^3.2.5", "prettier": "^3.2.5",
"solid-devtools": "^0.29.2", "solid-devtools": "^0.29.2",
"tailwindcss": "^3.4.3", "tailwindcss": "^3.4.3",
"typescript": "^5.4.5", "typescript": "^5.4.5",
"typescript-eslint": "^7.10.0", "typescript-eslint": "^7.10.0",
"vite": "^5.0.11", "vite": "^5.0.11",
"vite-plugin-solid": "^2.8.2" "vite-plugin-solid": "^2.8.2",
"eslint-plugin-tailwindcss": "^3.17.0"
}, },
"dependencies": { "dependencies": {
"material-icons": "^1.13.12",
"solid-js": "^1.8.11" "solid-js": "^1.8.11"
} }
} }

View File

@ -1,18 +1,59 @@
import { createSignal, type Component } from "solid-js"; import { Match, Switch, createSignal, type Component } from "solid-js";
import { CountProvider } from "./Config"; import { CountProvider } from "./Config";
// import { Nested } from "./nested";
import { Layout } from "./layout/layout"; import { Layout } from "./layout/layout";
import { Route, Router } from "./Routes"; import cx from "classnames";
import { Nested } from "./nested";
// Global state type Route = "home" | "machines";
const [route, setRoute] = createSignal<Route>("machines");
export { route, setRoute };
const App: Component = () => { const App: Component = () => {
const [route, setRoute] = createSignal<Route>("home");
return ( return (
<CountProvider> <CountProvider>
<Layout> <Layout>
<Router route={route} /> <div class="col-span-1">
<div class={cx("text-zinc-500")}>Navigation</div>
<ul>
<li>
<button
onClick={() => setRoute("home")}
classList={{ "bg-blue-500": route() === "home" }}
>
Home
</button>
</li>
<li>
{" "}
<button
onClick={() => setRoute("machines")}
classList={{ "bg-blue-500": route() === "machines" }}
>
Machines
</button>
</li>
</ul>
</div>
<div class="col-span-7">
<div>{route()}</div>
<Switch fallback={<p>{route()} not found</p>}>
<Match when={route() == "home"}>
<Nested />
</Match>
<Match when={route() == "machines"}>
<div class="grid grid-cols-3 gap-2">
<div class="h-10 w-20 bg-red-500">red</div>
<div class="h-10 w-20 bg-green-500">green</div>
<div class="h-10 w-20 bg-blue-500">blue</div>
<div class="h-10 w-20 bg-yellow-500">yellow</div>
<div class="h-10 w-20 bg-purple-500">purple</div>
<div class="h-10 w-20 bg-cyan-500">cyan</div>
<div class="h-10 w-20 bg-pink-500">pink</div>
</div>
</Match>
</Switch>
</div>
</Layout> </Layout>
</CountProvider> </CountProvider>
); );

View File

@ -10,7 +10,7 @@ import { OperationResponse, pyApi } from "./message";
export const makeCountContext = () => { export const makeCountContext = () => {
const [machines, setMachines] = createSignal< const [machines, setMachines] = createSignal<
OperationResponse<"list_machines"> OperationResponse<"list_machines">
>([]); >({});
const [loading, setLoading] = createSignal(false); const [loading, setLoading] = createSignal(false);
pyApi.list_machines.receive((machines) => { pyApi.list_machines.receive((machines) => {
@ -41,7 +41,7 @@ export const CountContext = createContext<CountContextType>([
loading: () => false, loading: () => false,
// eslint-disable-next-line // eslint-disable-next-line
machines: () => ([]), machines: () => ({}),
}, },
{ {
// eslint-disable-next-line // eslint-disable-next-line

View File

@ -1,32 +0,0 @@
import { Accessor, For, Match, Switch } from "solid-js";
import { MachineListView } from "./routes/machines/view";
import { colors } from "./routes/colors/view";
export type Route = keyof typeof routes;
export const routes = {
machines: {
child: MachineListView,
label: "Machines",
icon: "devices_other",
},
colors: {
child: colors,
label: "Colors",
icon: "color_lens",
},
};
interface RouterProps {
route: Accessor<Route>;
}
export const Router = (props: RouterProps) => {
const { route } = props;
return (
<Switch fallback={<p>route {route()} not found</p>}>
<For each={Object.entries(routes)}>
{([key, { child }]) => <Match when={route() === key}>{child}</Match>}
</For>
</Switch>
);
};

View File

@ -1,33 +0,0 @@
import { Accessor, For, Setter } from "solid-js";
import { Route, routes } from "./Routes";
interface SidebarProps {
route: Accessor<Route>;
setRoute: Setter<Route>;
}
export const Sidebar = (props: SidebarProps) => {
const { route, setRoute } = props;
return (
<aside class="min-h-screen w-80 bg-base-100">
<div class="sticky top-0 z-20 hidden items-center gap-2 bg-base-100/90 px-4 py-2 shadow-sm backdrop-blur lg:flex">
Icon
</div>
<ul class="menu px-4 py-0">
<For each={Object.entries(routes)}>
{([key, { label, icon }]) => (
<li>
<button
onClick={() => setRoute(key as Route)}
class="group"
classList={{ "bg-blue-500": route() === key }}
>
<span class="material-icons">{icon}</span>
{label}
</button>
</li>
)}
</For>
</ul>
</aside>
);
};

View File

@ -1,5 +1,3 @@
@import 'material-icons/iconfont/filled.css';
/* List of icons: https://marella.me/material-icons/demo/ */
@tailwind base; @tailwind base;
@tailwind components; @tailwind components;
@tailwind utilities; @tailwind utilities;

View File

@ -3,7 +3,7 @@ import { render } from "solid-js/web";
import "./index.css"; import "./index.css";
import App from "./App"; import App from "./App";
import { getFakeResponse } from "../mock";
const root = document.getElementById("app"); const root = document.getElementById("app");
window.clan = window.clan || {}; window.clan = window.clan || {};
@ -14,26 +14,5 @@ if (import.meta.env.DEV && !(root instanceof HTMLElement)) {
); );
} }
console.log(import.meta.env);
if (import.meta.env.DEV) {
console.log("Development mode");
window.webkit = window.webkit || {
messageHandlers: {
gtk: {
postMessage: (postMessage) => {
const { method, data } = postMessage;
console.debug("Python API call", { method, data });
setTimeout(() => {
const mock = getFakeResponse(method, data);
console.log("mock", { mock });
window.clan[method](JSON.stringify(mock));
}, 1000);
},
},
},
};
}
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
render(() => <App />, root!); render(() => <App />, root!);

View File

@ -1,19 +0,0 @@
export const Header = () => {
return (
<div class="navbar bg-base-100">
<div class="flex-none">
<button class="btn btn-square btn-ghost">
<span class="material-icons">home</span>
</button>
</div>
<div class="flex-1">
<a class="btn btn-ghost text-xl">Clan</a>
</div>
<div class="flex-none">
<button class="btn btn-square btn-ghost">
<span class="material-icons">menu</span>
</button>
</div>
</div>
);
};

View File

@ -1,35 +1,9 @@
import { Component, JSXElement } from "solid-js"; import { Component, JSXElement } from "solid-js";
import { Header } from "./header";
import { Sidebar } from "../Sidebar";
import { route, setRoute } from "../App";
interface LayoutProps { interface LayoutProps {
children: JSXElement; children: JSXElement;
} }
export const Layout: Component<LayoutProps> = (props) => { export const Layout: Component<LayoutProps> = (props) => {
return ( return <div class="grid grid-cols-8">{props.children}</div>;
<>
<div class="drawer bg-base-100 lg:drawer-open">
<input
id="toplevel-drawer"
type="checkbox"
class="drawer-toggle hidden"
/>
<div class="drawer-content">
<Header />
{props.children}
</div>
<div class="drawer-side z-40">
<label
for="toplevel-drawer"
aria-label="close sidebar"
class="drawer-overlay"
></label>
<Sidebar route={route} setRoute={setRoute} />
</div>
</div>
</>
);
}; };

View File

@ -0,0 +1,39 @@
import { For, Match, Switch, createEffect, type Component } from "solid-js";
import { useCountContext } from "./Config";
export const Nested: Component = () => {
const [{ machines, loading }, { getMachines }] = useCountContext();
const list = () => Object.values(machines());
createEffect(() => {
console.log("1", list());
});
createEffect(() => {
console.log("2", machines());
});
return (
<div>
<button onClick={() => getMachines()} class="btn btn-primary">
Get machines
</button>
<div></div>
<Switch>
<Match when={loading()}>Loading...</Match>
<Match when={!loading() && Object.entries(machines()).length === 0}>
No machines found
</Match>
<Match when={!loading()}>
<For each={list()}>
{(entry, i) => (
<li>
{i() + 1}: {entry.machine_name}{" "}
{entry.machine_description || "No description"}
</li>
)}
</For>
</Match>
</Switch>
</div>
);
};

View File

@ -1,13 +0,0 @@
export const colors = () => {
return (
<div class="grid grid-cols-3 gap-2">
<div class="h-10 w-20 bg-red-500">red</div>
<div class="h-10 w-20 bg-green-500">green</div>
<div class="h-10 w-20 bg-blue-500">blue</div>
<div class="h-10 w-20 bg-yellow-500">yellow</div>
<div class="h-10 w-20 bg-purple-500">purple</div>
<div class="h-10 w-20 bg-cyan-500">cyan</div>
<div class="h-10 w-20 bg-pink-500">pink</div>
</div>
);
};

View File

@ -1,78 +0,0 @@
import { For, Match, Switch, createEffect, type Component } from "solid-js";
import { useCountContext } from "../../Config";
import { route } from "@/src/App";
export const MachineListView: Component = () => {
const [{ machines, loading }, { getMachines }] = useCountContext();
createEffect(() => {
if (route() === "machines") getMachines();
});
return (
<div class="max-w-screen-lg">
<div class="tooltip" data-tip="Refresh ">
<button class="btn btn-ghost" onClick={() => getMachines()}>
<span class="material-icons ">refresh</span>
</button>
</div>
<Switch>
<Match when={loading()}>
{/* Loading skeleton */}
<div>
<div class="card card-side m-2 bg-base-100 shadow-lg">
<figure class="pl-2">
<div class="skeleton size-12"></div>
</figure>
<div class="card-body">
<h2 class="card-title">
<div class="skeleton h-12 w-80"></div>
</h2>
<div class="skeleton h-8 w-72"></div>
</div>
</div>
</div>
</Match>
<Match when={!loading() && machines().length === 0}>
No machines found
</Match>
<Match when={!loading()}>
<ul>
<For each={machines()}>
{(entry) => (
<li>
<div class="card card-side m-2 bg-base-100 shadow-lg">
<figure class="pl-2">
<span class="material-icons content-center text-5xl">
devices_other
</span>
</figure>
<div class="card-body flex-row justify-between">
<div class="flex flex-col">
<h2 class="card-title">{entry}</h2>
{/*
<p
classList={{
"text-gray-400": !entry.machine_description,
"text-gray-600": !!entry.machine_description,
}}
>
{entry.machine_description || "No description"}
</p>
*/}
</div>
<div>
<button class="btn btn-ghost">
<span class="material-icons">more_vert</span>
</button>
</div>
</div>
</div>
</li>
)}
</For>
</ul>
</Match>
</Switch>
</div>
);
};

View File

@ -1,11 +1,9 @@
const typography = require("@tailwindcss/typography");
const daisyui = require("daisyui"); const daisyui = require("daisyui");
/** @type {import('tailwindcss').Config} */ /** @type {import('tailwindcss').Config} */
module.exports = { module.exports = {
content: ["./src/**/*.{js,jsx,ts,tsx}"], content: ["./src/**/*.{js,jsx,ts,tsx}"],
theme: { theme: {
extend: {}, extend: {},
}, },
plugins: [typography, daisyui], plugins: [daisyui],
}; };

View File

@ -11,7 +11,7 @@
npmDeps = pkgs.fetchNpmDeps { npmDeps = pkgs.fetchNpmDeps {
src = ./app; src = ./app;
hash = "sha256-EadzSkIsV/cJtdxpIUvvpQhu5h3VyF8bLMpwfksNmWQ="; hash = "sha256-E0++hupVKnDqmLk7ljoMcqcI4w+DIMlfRYRPbKUsT2c=";
}; };
# The prepack script runs the build script, which we'd rather do in the build phase. # The prepack script runs the build script, which we'd rather do in the build phase.
npmPackFlags = [ "--ignore-scripts" ]; npmPackFlags = [ "--ignore-scripts" ];

View File

@ -1,2 +0,0 @@
# DO NOT DELETE
# This file is used by the clan cli to discover a clan flake

View File

@ -1,5 +0,0 @@
{
"name": "My Empty Clan",
"description": "some nice description",
"icon": "A path to the png"
}

View File

@ -1,16 +0,0 @@
# Clan configuration file
# TODO: This file is used as a template for the simple GUI workflow
{
inputs.clan-core.url = "git+https://git.clan.lol/clan/clan-core";
outputs =
{ self, clan-core, ... }:
let
clan = clan-core.lib.buildClan {
# This clan builds all its configuration out of the current directory
directory = self;
};
in
{
inherit (clan) nixosConfigurations clanInternals;
};
}

View File

@ -5,10 +5,6 @@
description = "Initialize a new clan flake"; description = "Initialize a new clan flake";
path = ./new-clan; path = ./new-clan;
}; };
empty = {
description = "A empty clan template. Primarily for usage with the clan ui";
path = ./empty;
};
default = self.templates.new-clan; default = self.templates.new-clan;
}; };
} }