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

View File

@ -1,50 +1,33 @@
{ self, ... }:
{ ... }:
{
perSystem =
{ ... }:
{
nodes,
pkgs,
lib,
...
}:
let
dependencies = [
pkgs.disko
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";
# 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 = {
substituters = lib.mkForce [ ];
hashed-mirrors = null;
connect-timeout = lib.mkForce 3;
flake-registry = pkgs.writeText "flake-registry" ''{"flakes":[],"version":2}'';
experimental-features = [
"nix-command"
"flakes"
];
};
};
testScript = ''
start_all()
machine.succeed("clan flash --debug --flake ${../..} --yes --disk main /dev/vdb test_install_machine")
'';
} { inherit pkgs self; };
};
# nix.settings = {
# substituters = lib.mkForce [ ];
# hashed-mirrors = null;
# connect-timeout = lib.mkForce 3;
# flake-registry = pkgs.writeText "flake-registry" ''{"flakes":[],"version":2}'';
# experimental-features = [
# "nix-command"
# "flakes"
# ];
# };
# };
# testScript = ''
# start_all()
# machine.succeed("clan --debug --flake ${../..} flash --yes --disk main /dev/vdb test_install_machine")
# '';
# } { inherit pkgs self; };
# };
};
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -15,74 +15,56 @@ Let's get your development environment up and running:
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:
```bash
curl --proto '=https' --tlsv1.2 -sSf -L https://install.determinate.systems/nix | sh -s -- install
```
- 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
curl --proto '=https' --tlsv1.2 -sSf -L https://install.determinate.systems/nix | sh -s -- install
```
2. **Install direnv**:
- To automatically setup a devshell on entering the directory
```bash
nix profile install nixpkgs#nix-direnv-flakes
```
- To automatically setup a devshell on entering the directory
```bash
nix profile install nixpkgs#nix-direnv-flakes
```
3. **Add direnv to your shell**:
- 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`
- 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`
```bash
echo 'eval "$(direnv hook zsh)"' >> ~/.zshrc && echo 'eval "$(direnv hook bash)"' >> ~/.bashrc && eval "$SHELL"
```
```bash
echo 'eval "$(direnv hook zsh)"' >> ~/.zshrc && echo 'eval "$(direnv hook bash)"' >> ~/.bashrc && eval "$SHELL"
```
4. **Create a Gitea Account**:
- Register an account on https://git.clan.lol
- Fork the [clan-core](https://git.clan.lol/clan/clan-core) repository
- Clone the repository and navigate to it
- Add a new remote called upstream:
```bash
git remote add upstream gitea@git.clan.lol:clan/clan-core.git
```
5. **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:
- Register an account on https://git.clan.lol
- Fork the [clan-core](https://git.clan.lol/clan/clan-core) repository
- Clone the repository and navigate to it
- Add a new remote called upstream:
```bash
git remote add upstream gitea@git.clan.lol:clan/clan-core.git
```
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

View File

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

View File

@ -1,4 +1,4 @@
site_name: Clan Documentation
site_name: Clan Docs
site_url: https://docs.clan.lol
repo_url: https://git.clan.lol/clan/clan-core/
repo_name: clan-core
@ -96,7 +96,7 @@ site_dir: out
theme:
font: false
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
features:
- navigation.instant
@ -105,8 +105,7 @@ theme:
- content.code.copy
- content.tabs.link
icon:
repo: fontawesome/brands/git-alt
custom_dir: overrides
repo: fontawesome/brands/git
palette:
# Palette toggle for light mode
@ -129,6 +128,8 @@ theme:
extra_css:
- static/extra.css
- static/asciinema-player/custom-theme.css
- static/asciinema-player/asciinema-player.css
extra:
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**"
### Step 2. Flash Custom Installer
Using clan flash enables the inclusion of ssh public keys.
It also allows to set language and keymap currently in the installer image.
Using clan flash enables the inclusion of ssh public keys and disables ssh password authentication.
It also includes the language and keymap currently used into the installer image.
```bash
clan flash --flake git+https://git.clan.lol/clan/clan-core \
--ssh-pubkey $HOME/.ssh/id_ed25519.pub \
--keymap en \
--language en \
--disk main /dev/sd<X> \
flash-installer
clan --flake git+https://git.clan.lol/clan/clan-core flash flash-installer --disk main /dev/sd<X>
```
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."
The `clan flash` utility will erase the disk. Make sure to specify the correct device

View File

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

View File

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

View File

@ -17,33 +17,6 @@ let
cfg = config.clan;
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 = {
directory = mkOption {
type = types.path;
@ -60,27 +33,15 @@ in
default = { };
description = "Allows to include machine-specific modules i.e. machines.\${name} = { ... }";
};
# Checks are performed in 'buildClan'
# Not everyone uses flake-parts
meta = {
name = lib.mkOption {
type = types.nullOr types.str;
default = null;
description = "Needs to be (globally) unique, as this determines the folder name where the flake gets downloaded to.";
};
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";
};
clanName = mkOption {
type = types.str;
description = "Needs to be (globally) unique, as this determines the folder name where the flake gets downloaded to.";
};
clanIcon = mkOption {
type = types.nullOr types.path;
default = null;
description = "A path to an icon to be used for the clan, should be the same for all machines";
};
pkgsForSystem = mkOption {
type = types.functionTo types.raw;
default = _system: null;
@ -91,7 +52,6 @@ in
clanInternals = lib.mkOption {
type = lib.types.submodule {
options = {
meta = 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); };
machinesFunc = lib.mkOption { type = lib.types.attrsOf (lib.types.attrsOf lib.types.unspecified); };
@ -105,8 +65,9 @@ in
directory
specialArgs
machines
clanName
clanIcon
pkgsForSystem
meta
;
};
};

View File

@ -7,58 +7,16 @@
directory, # The directory containing the machines subdirectory
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} = { ... }
# DEPRECATED: use meta.name instead
clanName ? null, # Needs to be (globally) unique, as this determines the folder name where the flake gets downloaded to.
# DEPRECATED: use meta.icon instead
clanName, # Needs to be (globally) unique, as this determines the folder name where the flake gets downloaded to.
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.
# This improves performance, but all nipxkgs.* options will be ignored.
}:
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") (
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 =
machineName:
# CLAN_MACHINE_SETTINGS_FILE allows to override the settings file temporarily
@ -100,15 +58,11 @@ let
(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;
clanCore.clanName = clanName;
clanCore.clanIcon = clanIcon;
clanCore.clanDir = directory;
clanCore.machineName = name;
nixpkgs.hostPlatform = lib.mkDefault system;
# speeds up nix commands by using the nixpkgs from the host system (especially useful in VMs)
@ -173,15 +127,10 @@ let
) supportedSystems
);
in
builtins.deepSeq deprecationWarnings {
{
inherit nixosConfigurations;
clanInternals = {
# Evaluated clan meta
# Merged /clan/meta.json with overrides from buildClan
meta = mergedMeta;
# machine specifics
machines = configsPerSystem;
machinesFunc = configsFuncPerSystem;
all-machines-json = lib.mapAttrs (

View File

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

View File

@ -2,7 +2,6 @@ import argparse
import json
import logging
from ..completions import add_dynamic_completer, complete_machines
from ..errors import ClanError
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:
if args.flake is None:
raise ClanError("Could not find clan flake toplevel directory")
machine = Machine(name=args.machine, flake=args.flake)
create_backup(machine=machine, provider=args.provider)
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"
)
add_dynamic_completer(machines_parser, complete_machines)
parser.add_argument("--provider", type=str, help="backup provider to use")
parser.set_defaults(func=create_command)

View File

@ -3,7 +3,6 @@ import json
import subprocess
from dataclasses import dataclass
from ..completions import add_dynamic_completer, complete_machines
from ..errors import ClanError
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:
if args.flake is None:
raise ClanError("Could not find clan flake toplevel directory")
machine = Machine(name=args.machine, flake=args.flake)
backups = list_backups(machine=machine, provider=args.provider)
for backup in backups:
@ -58,9 +55,8 @@ def list_command(args: argparse.Namespace) -> 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"
)
add_dynamic_completer(machines_parser, complete_machines)
parser.add_argument("--provider", type=str, help="backup provider to filter by")
parser.set_defaults(func=list_command)

View File

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

View File

@ -56,7 +56,7 @@ def handle_output(process: subprocess.Popen, log: Log) -> tuple[str, str]:
sys.stderr.buffer.write(ret)
sys.stderr.flush()
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:
@ -101,19 +101,13 @@ TIME_TABLE = TimeTable()
def run(
cmd: list[str],
*,
input: bytes | None = None, # noqa: A002
env: dict[str, str] | None = None,
cwd: Path = Path.cwd(),
log: Log = Log.STDERR,
check: bool = True,
error_msg: str | None = None,
) -> CmdOut:
if input:
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()}")
glog.debug(f"$: {shlex.join(cmd)} \nCaller: {get_caller()}")
tstart = datetime.now()
# Start the subprocess
@ -126,10 +120,7 @@ def run(
)
stdout_buf, stderr_buf = handle_output(process, log)
if input:
process.communicate(input)
else:
process.wait()
rc = process.wait()
tend = datetime.now()
global TIME_TABLE
@ -145,7 +136,7 @@ def run(
msg=error_msg,
)
if check and process.returncode != 0:
if check and rc != 0:
raise ClanCmdError(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
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:
if args.value == []:
print(
@ -316,7 +307,7 @@ def register_parser(
)
# 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(
"--machine",
"-m",
@ -354,6 +345,13 @@ def register_parser(
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:
if argv is None:

View File

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

View File

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

View File

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

View File

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

View File

@ -2,7 +2,6 @@ import os
import subprocess
from pathlib import Path
from clan_cli.cmd import Log, run
from clan_cli.machines.machines import Machine
from clan_cli.nix import nix_shell
@ -16,25 +15,25 @@ class SecretStore(SecretStoreBase):
def set(
self, service: str, name: str, value: bytes, groups: list[str]
) -> Path | None:
run(
subprocess.run(
nix_shell(
["nixpkgs#pass"],
["pass", "insert", "-m", f"machines/{self.machine.name}/{name}"],
),
input=value,
log=Log.BOTH,
error_msg=f"Failed to insert secret {name}",
check=True,
)
return None # we manage the files outside of the git repo
def get(self, service: str, name: str) -> bytes:
return run(
return subprocess.run(
nix_shell(
["nixpkgs#pass"],
["pass", "show", f"machines/{self.machine.name}/{name}"],
),
error_msg=f"Failed to get secret {name}",
).stdout.encode("utf-8")
check=True,
stdout=subprocess.PIPE,
).stdout
def exists(self, service: str, name: str) -> bool:
password_store = os.environ.get(
@ -49,7 +48,7 @@ class SecretStore(SecretStoreBase):
)
hashes = []
hashes.append(
run(
subprocess.run(
nix_shell(
["nixpkgs#git"],
[
@ -62,15 +61,13 @@ class SecretStore(SecretStoreBase):
f"machines/{self.machine.name}",
],
),
check=False,
)
.stdout.encode("utf-8")
.strip()
stdout=subprocess.PIPE,
).stdout.strip()
)
for symlink in Path(password_store).glob(f"machines/{self.machine.name}/**/*"):
if symlink.is_symlink():
hashes.append(
run(
subprocess.run(
nix_shell(
["nixpkgs#git"],
[
@ -83,10 +80,8 @@ class SecretStore(SecretStoreBase):
str(symlink),
],
),
check=False,
)
.stdout.encode("utf-8")
.strip()
stdout=subprocess.PIPE,
).stdout.strip()
)
# 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 ..cmd import Log, run
from ..completions import add_dynamic_completer, complete_machines
from ..machines.machines import Machine
from ..nix import nix_shell
@ -33,8 +32,6 @@ def upload_secrets(machine: Machine) -> None:
" ".join(["ssh"] + ssh_cmd[2:]),
"-az",
"--delete",
"--chown=root:root",
"--chmod=D700,F600",
f"{tempdir!s}/",
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:
machines_parser = parser.add_argument(
parser.add_argument(
"machine",
help="The machine to upload secrets to",
)
add_dynamic_completer(machines_parser, complete_machines)
parser.set_defaults(func=upload_command)

View File

@ -3,6 +3,7 @@ import importlib
import json
import logging
import os
import re
import shutil
import textwrap
from collections.abc import Sequence
@ -12,7 +13,6 @@ from tempfile import TemporaryDirectory
from typing import Any
from .cmd import Log, run
from .completions import add_dynamic_completer, complete_machines
from .errors import ClanError
from .facts.secret_modules import SecretStoreBase
from .machines.machines import Machine
@ -21,6 +21,63 @@ from .nix import nix_shell
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(
machine: Machine,
*,
@ -28,9 +85,7 @@ def flash_machine(
disks: dict[str, str],
system_config: dict[str, Any],
dry_run: bool,
write_efi_boot_entries: bool,
debug: bool,
extra_args: list[str] = [],
) -> None:
secret_facts_module = importlib.import_module(machine.secret_facts_module)
secret_facts_store: SecretStoreBase = secret_facts_module.SecretStore(
@ -57,8 +112,6 @@ def flash_machine(
disko_install.append("sudo")
disko_install.append("disko-install")
if write_efi_boot_entries:
disko_install.append("--write-efi-boot-entries")
if dry_run:
disko_install.append("--dry-run")
if debug:
@ -75,8 +128,6 @@ def flash_machine(
json.dumps(system_config),
]
)
disko_install.extend(["--option", "dry-run", "true"])
disko_install.extend(extra_args)
cmd = nix_shell(
["nixpkgs#disko"],
@ -97,8 +148,6 @@ class FlashOptions:
mode: str
language: str
keymap: str
write_efi_boot_entries: bool
nix_options: list[str]
class AppendDiskAction(argparse.Action):
@ -127,10 +176,8 @@ def flash_command(args: argparse.Namespace) -> None:
confirm=not args.yes,
debug=args.debug,
mode=args.mode,
language=args.language,
language=args.lang,
keymap=args.keymap,
write_efi_boot_entries=args.write_efi_boot_entries,
nix_options=args.option,
)
machine = Machine(opts.machine, flake=opts.flake)
@ -144,22 +191,40 @@ def flash_command(args: argparse.Namespace) -> None:
if ask != "y":
return
extra_config: dict[str, Any] = {}
if opts.ssh_keys_path:
root_keys = []
for key_path in opts.ssh_keys_path:
try:
root_keys.append(key_path.read_text())
except OSError as e:
raise ClanError(f"Cannot read SSH public key file: {key_path}: {e}")
extra_config["users"] = {
"users": {"root": {"openssh": {"authorizedKeys": {"keys": root_keys}}}}
}
if opts.keymap:
extra_config["console"] = {"keyMap": opts.keymap}
root_keys = read_public_key_contents(opts.ssh_keys_path)
if opts.confirm and not root_keys:
msg = "Should we add your SSH public keys to the root user? [y/N] "
ask = input(msg)
if ask == "y":
pubkeys = list_available_ssh_keys()
root_keys.extend(read_public_key_contents(pubkeys))
else:
raise ClanError(
"No SSH public keys provided. Use --ssh-pubkey to add keys."
)
elif not opts.confirm and not root_keys:
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:
extra_config["i18n"] = {"defaultLocale": opts.language}
localectl = get_keymap_and_locale()
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(
machine,
@ -168,19 +233,15 @@ def flash_command(args: argparse.Namespace) -> None:
system_config=extra_config,
dry_run=opts.dry_run,
debug=opts.debug,
write_efi_boot_entries=opts.write_efi_boot_entries,
extra_args=opts.nix_options,
)
def register_parser(parser: argparse.ArgumentParser) -> None:
machines_parser = parser.add_argument(
parser.add_argument(
"machine",
type=str,
help="machine to install",
)
add_dynamic_completer(machines_parser, complete_machines)
parser.add_argument(
"--disk",
type=str,
@ -190,14 +251,12 @@ def register_parser(parser: argparse.ArgumentParser) -> None:
help="device to flash to",
default={},
)
mode_help = textwrap.dedent(
"""\
mode_help = textwrap.dedent("""\
Specify the mode of operation. Valid modes are: format, mount."
Format will format the disk before installing.
Mount will mount the disk before installing.
Mount is useful for updating an existing system without losing data.
"""
)
""")
parser.add_argument(
"--mode",
type=str,
@ -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",
)
parser.add_argument(
"--language",
"--lang",
type=str,
help="system language",
)
@ -234,16 +293,4 @@ def register_parser(parser: argparse.ArgumentParser) -> None:
default=False,
action="store_true",
)
parser.add_argument(
"--write-efi-boot-entries",
help=textwrap.dedent(
"""
Write EFI boot entries to the NVRAM of the system for the installed system.
Specify this option if you plan to boot from this disk on the current machine,
but not if you plan to move the disk to another machine.
"""
).strip(),
default=False,
action="store_true",
)
parser.set_defaults(func=flash_command)

View File

@ -5,7 +5,6 @@ from .create import register_create_parser
from .delete import register_delete_parser
from .install import register_install_parser
from .list import register_list_parser
from .show import register_show_parser
from .update import register_update_parser
@ -63,17 +62,6 @@ Examples:
)
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",
help="Install a machine",

View File

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

View File

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

View File

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

View File

@ -1,4 +1,5 @@
import argparse
import dataclasses
import json
import logging
from pathlib import Path
@ -11,15 +12,24 @@ from ..nix import nix_config, nix_eval
log = logging.getLogger(__name__)
@dataclasses.dataclass
class MachineInfo:
machine_name: str
machine_description: str | None
machine_icon: str | None
@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()
system = config["system"]
cmd = nix_eval(
[
f"{flake_url}#clanInternals.machines.{system}",
"--apply",
"builtins.attrNames",
"""builtins.mapAttrs (name: attrs: {
inherit (attrs.config.clanCore) machineDescription machineIcon machineName;
})""",
"--json",
]
)
@ -27,13 +37,27 @@ def list_machines(flake_url: str | Path, debug: bool) -> list[str]:
proc = run_no_stdout(cmd)
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:
flake_path = Path(args.flake).resolve()
for name in list_machines(flake_path, args.debug):
print(name)
print("Listing all machines:\n")
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:

View File

@ -41,10 +41,9 @@ class QMPWrapper:
class Machine:
name: str
flake: str | Path
name: str
data: MachineData
nix_options: list[str]
eval_cache: dict[str, str]
build_cache: dict[str, Path]
_flake_path: Path | None
@ -56,7 +55,6 @@ class Machine:
name: str,
flake: Path | str,
deployment_info: dict | None = None,
nix_options: list[str] = [],
machine: MachineData | None = None,
) -> None:
"""
@ -78,7 +76,6 @@ class Machine:
self.build_cache: dict[str, Path] = {}
self._flake_path: Path | None = None
self._deployment_info: None | dict = deployment_info
self.nix_options = nix_options
state_dir = vm_state_dir(flake_url=str(self.flake), vm_name=self.data.name)
@ -245,9 +242,9 @@ class Machine:
flake = f"path:{self.flake_dir}"
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":
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 os
import shlex
import subprocess
import sys
from ..cmd import run
from ..completions import add_dynamic_completer, complete_machines
from ..errors import ClanError
from ..facts.generate import generate_facts
from ..facts.upload import upload_secrets
@ -54,7 +53,11 @@ def upload_sources(
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
# 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))
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:
return json.loads(proc.stdout)["path"]
except (json.JSONDecodeError, OSError) as e:
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 ""
extra_args = host.meta.get("extra_args", [])
cmd = [
"nixos-rebuild",
"switch",
*extra_args,
"--fast",
"--option",
"keep-going",
@ -116,7 +124,6 @@ def deploy_nixos(machines: MachineGroup) -> None:
"true",
"--build-host",
"",
*machine.nix_options,
"--flake",
f"{path}#{machine.name}",
]
@ -136,9 +143,7 @@ def update(args: argparse.Namespace) -> None:
raise ClanError("Could not find clan flake toplevel directory")
machines = []
if len(args.machines) == 1 and args.target_host is not None:
machine = Machine(
name=args.machines[0], flake=args.flake, nix_options=args.option
)
machine = Machine(name=args.machines[0], flake=args.flake)
machine.target_host_address = args.target_host
machines.append(machine)
@ -148,7 +153,7 @@ def update(args: argparse.Namespace) -> None:
else:
if len(args.machines) == 0:
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):
continue
try:
@ -168,13 +173,13 @@ def update(args: argparse.Namespace) -> None:
print(machine, file=sys.stderr)
else:
machines = get_selected_machines(args.flake, args.option, args.machines)
machines = get_selected_machines(args.flake, args.machines)
deploy_nixos(MachineGroup(machines))
def register_update_parser(parser: argparse.ArgumentParser) -> None:
machines_parser = parser.add_argument(
parser.add_argument(
"machines",
type=str,
nargs="*",
@ -182,9 +187,6 @@ def register_update_parser(parser: argparse.ArgumentParser) -> None:
metavar="MACHINE",
help="machine to update. If no machine is specified, all machines will be updated.",
)
add_dynamic_completer(machines_parser, complete_machines)
parser.add_argument(
"--target-host",
type=str,

View File

@ -1,7 +1,6 @@
import argparse
from pathlib import Path
from ..completions import add_dynamic_completer, complete_machines
from ..errors import ClanError
from ..git import commit_files
from ..machines.types import machine_name_type, validate_hostname
@ -148,28 +147,25 @@ def register_machines_parser(parser: argparse.ArgumentParser) -> None:
# Parser
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
)
add_dynamic_completer(get_machine_parser, complete_machines)
get_parser.set_defaults(func=get_command)
# Parser
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
)
add_dynamic_completer(remove_machine_parser, complete_machines)
remove_parser.set_defaults(func=remove_command)
# Parser
add_secret_parser = subparser.add_parser(
"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
)
add_dynamic_completer(machine_add_secret_parser, complete_machines)
add_secret_parser.add_argument(
"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", help="remove a group's access to a secret"
)
machine_remove_parser = remove_secret_parser.add_argument(
"machine", help="the name of the machine", type=machine_name_type
remove_secret_parser.add_argument(
"machine", help="the name of the group", type=machine_name_type
)
add_dynamic_completer(machine_remove_parser, complete_machines)
remove_secret_parser.add_argument(
"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 .. import tty
from ..completions import add_dynamic_completer, complete_secrets
from ..errors import ClanError
from ..git import commit_files
from .folders import (
@ -154,12 +153,8 @@ def remove_command(args: argparse.Namespace) -> None:
remove_secret(Path(args.flake), args.secret)
def add_secret_argument(parser: argparse.ArgumentParser, autocomplete: bool) -> None:
secrets_parser = parser.add_argument(
"secret", help="the name of the secret", type=secret_name_type
)
if autocomplete:
add_dynamic_completer(secrets_parser, complete_secrets)
def add_secret_argument(parser: argparse.ArgumentParser) -> None:
parser.add_argument("secret", help="the name of the secret", type=secret_name_type)
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_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_set = subparser.add_parser("set", help="set a secret")
add_secret_argument(parser_set, False)
add_secret_argument(parser_set)
parser_set.add_argument(
"--group",
type=str,
@ -364,10 +359,10 @@ def register_secrets_parser(subparser: argparse._SubParsersAction) -> None:
parser_set.set_defaults(func=set_command)
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.set_defaults(func=rename_command)
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)

View File

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

View File

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

View File

@ -1,4 +1,7 @@
import json
import tempfile
from pathlib import Path
from typing import Any
import pytest
from cli import Cli
@ -11,6 +14,46 @@ from clan_cli.errors import ClanError
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(
test_flake: FlakeForTest,
temporary_home: Path,
@ -19,14 +62,25 @@ def test_configure_machine(
) -> None:
cli = Cli()
cli.run(
[
"--flake",
str(test_flake.path),
"config",
"-m",
"machine1",
"clan.jitsi.enable",
"true",
]
)
# clear the output buffer
capsys.readouterr()
# read a option value
cli.run(
[
"config",
"--flake",
str(test_flake.path),
"config",
"-m",
"machine1",
"clan.jitsi.enable",
@ -34,7 +88,7 @@ def test_configure_machine(
)
# read the output
assert capsys.readouterr().out == "false\n"
assert capsys.readouterr().out == "true\n"
def test_walk_jsonschema_all_types() -> None:

View File

@ -17,12 +17,10 @@ def test_create_flake(
capsys: pytest.CaptureFixture,
temporary_home: Path,
cli: Cli,
clan_core: Path,
) -> None:
flake_dir = temporary_home / "test-flake"
url = f"{clan_core}#default"
cli.run(["flakes", "create", str(flake_dir), f"--url={url}"])
cli.run(["flakes", "create", str(flake_dir)])
assert (flake_dir / ".clan-flake").exists()
monkeypatch.chdir(flake_dir)
cli.run(["machines", "create", "machine1"])
@ -49,34 +47,16 @@ def test_create_flake(
flake_outputs["nixosConfigurations"]["machine1"]
except KeyError:
pytest.fail("nixosConfigurations.machine1 not found in flake outputs")
@pytest.mark.impure
def test_ui_template(
monkeypatch: pytest.MonkeyPatch,
capsys: pytest.CaptureFixture,
temporary_home: Path,
cli: Cli,
clan_core: Path,
) -> None:
flake_dir = temporary_home / "test-flake"
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,
# configure machine1
capsys.readouterr()
cli.run(["config", "--machine", "machine1", "services.openssh.enable", ""])
capsys.readouterr()
cli.run(
[
"config",
"--machine",
"machine1",
"services.openssh.enable",
"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.run(
[
"flakes",
"inspect",
"--flake",
str(test_flake_with_core.path),
"flakes",
"inspect",
"--machine",
"vm1",
]

View File

@ -21,55 +21,55 @@ def test_import_sops(
monkeypatch.setenv("SOPS_AGE_KEY", age_keys[1].privkey)
cli.run(
[
"--flake",
str(test_flake.path),
"secrets",
"machines",
"add",
"--flake",
str(test_flake.path),
"machine1",
age_keys[0].pubkey,
]
)
cli.run(
[
"--flake",
str(test_flake.path),
"secrets",
"users",
"add",
"--flake",
str(test_flake.path),
"user1",
age_keys[1].pubkey,
]
)
cli.run(
[
"--flake",
str(test_flake.path),
"secrets",
"users",
"add",
"--flake",
str(test_flake.path),
"user2",
age_keys[2].pubkey,
]
)
cli.run(
[
"--flake",
str(test_flake.path),
"secrets",
"groups",
"add-user",
"--flake",
str(test_flake.path),
"group1",
"user1",
]
)
cli.run(
[
"--flake",
str(test_flake.path),
"secrets",
"groups",
"add-user",
"--flake",
str(test_flake.path),
"group1",
"user2",
]
@ -78,10 +78,10 @@ def test_import_sops(
# To edit:
# SOPS_AGE_KEY=AGE-SECRET-KEY-1U5ENXZQAY62NC78Y2WC0SEGRRMAEEKH79EYY5TH4GPFWJKEAY0USZ6X7YQ sops --age age14tva0txcrl0zes05x7gkx56qd6wd9q3nwecjac74xxzz4l47r44sv3fz62 ./data/secrets.yaml
cmd = [
"secrets",
"import-sops",
"--flake",
str(test_flake.path),
"secrets",
"import-sops",
"--group",
"group1",
"--machine",
@ -91,10 +91,10 @@ def test_import_sops(
cli.run(cmd)
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())
assert users == ["user1", "user2"]
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"

View File

@ -9,11 +9,11 @@ def test_machine_subcommands(
) -> None:
cli = Cli()
cli.run(
["machines", "create", "--flake", str(test_flake_with_core.path), "machine1"]
["--flake", str(test_flake_with_core.path), "machines", "create", "machine1"]
)
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()
@ -21,19 +21,12 @@ def test_machine_subcommands(
assert "vm1" 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(
["machines", "delete", "--flake", str(test_flake_with_core.path), "machine1"]
["--flake", str(test_flake_with_core.path), "machines", "delete", "machine1"]
)
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()
assert "machine1" not in out.out

View File

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

View File

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

View File

@ -23,11 +23,11 @@ def test_secrets_upload(
cli = Cli()
cli.run(
[
"--flake",
str(test_flake_with_core.path),
"secrets",
"users",
"add",
"--flake",
str(test_flake_with_core.path),
"user1",
age_keys[0].pubkey,
]
@ -35,18 +35,18 @@ def test_secrets_upload(
cli.run(
[
"--flake",
str(test_flake_with_core.path),
"secrets",
"machines",
"add",
"--flake",
str(test_flake_with_core.path),
"vm1",
age_keys[1].pubkey,
]
)
monkeypatch.setenv("SOPS_NIX_SECRET", age_keys[0].privkey)
cli.run(
["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")
@ -55,7 +55,7 @@ def test_secrets_upload(
new_text = flake.read_text().replace("__CLAN_TARGET_ADDRESS__", addr)
flake.write_text(new_text)
cli.run(["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
sops_key = test_flake_with_core.path.joinpath("key.txt")

View File

@ -86,7 +86,7 @@ def test_inspect(
test_flake_with_core: FlakeForTest, capsys: pytest.CaptureFixture
) -> None:
cli = Cli()
cli.run(["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
assert "Cores" in out.out

View File

@ -47,7 +47,7 @@ class MainApplication(Adw.Application):
None,
)
self.window: MainWindow | None = None
self.window: "MainWindow" | None = None
self.connect("activate", self.on_activate)
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.key_gen = key_gen
# 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.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()
# 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

View File

@ -37,4 +37,6 @@ writeShellApplication {
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>) ++ [ ({...}: {
nixpkgs.hostPlatform = builtins.currentSystem;} ) ] ; })).options"
},
"home-manager": {
"expr": "(builtins.getFlake \"github:nix-community/home-manager\").homeConfigurations.<name>.options"
}
"clan": {
"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 { };
}
// lib.optionalAttrs pkgs.stdenv.isLinux {
wayland-proxy-virtwl = pkgs.callPackage ./wayland-proxy-virtwl { };
# halalify zerotierone
zerotierone = pkgs.zerotierone.overrideAttrs (_old: {
meta = _old.meta // {

View File

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

View File

@ -98,6 +98,9 @@ let
in
{
clan = {
clanName = "clan-core";
directory = self;
# To build a generic installer image (without ssh pubkeys),
# use the following command:
# $ 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 shlex
import subprocess
parser = argparse.ArgumentParser()
@ -12,17 +11,14 @@ args = parser.parse_args()
if not args.reviewers and not args.no_review:
parser.error("either --reviewers or --no-review must be given")
cmd = [
"tea-create-pr",
"origin",
"upstream",
"main",
"--assignees",
",".join(["clan-bot", *args.reviewers]),
*(["--labels", "needs-review"] if not args.no_review else []),
*args.args,
]
print("Running:", shlex.join(cmd))
subprocess.run(cmd)
subprocess.run(
[
"tea-create-pr",
"origin",
"main",
"--assignees",
",".join(["clan-bot", *args.reviewers]),
*(["--labels", "needs-review"] if not args.no_review else []),
*args.args,
]
)

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

@ -1,46 +1,17 @@
#!/usr/bin/env bash
set -euo pipefail
remoteFork="${1:-origin}"
remoteUpstream="${2:-upstream}"
targetBranch="${3:-main}"
shift && shift && shift
remoteName="${1:-origin}"
targetBranch="${2:-main}"
shift && shift
TMPDIR="$(mktemp -d)"
currentBranch="$(git rev-parse --abbrev-ref HEAD)"
user_unparsed="$(tea whoami)"
user="$(echo "$user_unparsed" | tr -d '\n' | cut -f4 -d' ')"
user="$(tea login list -o simple | cut -d" " -f4 | head -n1)"
tempRemoteBranch="$user-$currentBranch"
root_dir=$(git rev-parse --show-toplevel)
nix fmt -- --fail-on-change
# Function to check if a remote exists
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
git log --reverse --pretty="format:%s%n%n%b%n%n" "$remoteName/$targetBranch..HEAD" > "$TMPDIR"/commit-msg
$EDITOR "$TMPDIR"/commit-msg
@ -52,13 +23,11 @@ rest=$(echo "$COMMIT_MSG" | tail -n+2)
if [[ "$firstLine" == "$rest" ]]; then
rest=""
fi
git push --force -u "$remoteFork" HEAD:refs/heads/"$tempRemoteBranch"
git push --force -u "$remoteName" HEAD:refs/heads/"$tempRemoteBranch"
tea pr create \
--repo "$repo" \
--head "$user:$tempRemoteBranch" \
--title "$firstLine" \
--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",
{
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 outputPath = path.join(distPath, "index.html");
const postcss = require("postcss");
const css_url = require("postcss-url");
fs.readFile(manifestPath, { encoding: "utf8" }, (err, data) => {
if (err) {
return console.error("Failed to read manifest:", err);
@ -28,48 +25,19 @@ fs.readFile(manifestPath, { encoding: "utf8" }, (err, data) => {
// Add linked stylesheets
assets.forEach((asset) => {
// console.log(asset);
if (asset.src === "index.html") {
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}">`;
});
}
asset.css.forEach((cssEntry) => {
htmlContent += `\n <link rel="stylesheet" href="${cssEntry}">`;
});
});
htmlContent += `
</head>
<body>
<div id="app"></div>
`;
</head>
<body>
<div id="app"></div>
`;
// Add scripts
assets.forEach((asset) => {
if (asset.file.endsWith(".js")) {
console.log(`Relinking js script: ${asset.file}`);
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",
"license": "MIT",
"dependencies": {
"material-icons": "^1.13.12",
"solid-js": "^1.8.11"
},
"devDependencies": {
"@eslint/js": "^9.3.0",
"@tailwindcss/typography": "^0.5.13",
"@types/node": "^20.12.12",
"@typescript-eslint/parser": "^7.10.0",
"autoprefixer": "^10.4.19",
@ -22,10 +20,8 @@
"daisyui": "^4.11.1",
"eslint": "^8.57.0",
"eslint-plugin-tailwindcss": "^3.17.0",
"json-schema-faker": "^0.5.6",
"json-schema-to-ts": "^3.1.0",
"postcss": "^8.4.38",
"postcss-url": "^10.1.3",
"prettier": "^3.2.5",
"solid-devtools": "^0.29.2",
"tailwindcss": "^3.4.3",
@ -1486,34 +1482,6 @@
"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": {
"version": "7.20.5",
"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_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": {
"version": "3.1.0",
"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",
"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": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/culori/-/culori-3.3.0.tgz",
@ -2667,19 +2623,6 @@
"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": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz",
@ -2859,12 +2802,6 @@
"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": {
"version": "4.3.7",
"resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz",
@ -3260,53 +3197,6 @@
"integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
"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": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/json-schema-to-ts/-/json-schema-to-ts-3.1.0.tgz",
@ -3344,15 +3234,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": {
"version": "4.5.4",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
@ -3433,18 +3314,6 @@
"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": {
"version": "4.6.2",
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
@ -3497,11 +3366,6 @@
"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": {
"version": "5.1.7",
"resolved": "https://registry.npmjs.org/merge-anything/-/merge-anything-5.1.7.tgz",
@ -3687,15 +3551,6 @@
"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": {
"version": "0.9.4",
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
@ -4012,73 +3867,6 @@
"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": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
@ -4501,12 +4289,6 @@
"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": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
@ -5226,15 +5008,6 @@
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
"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": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",

View File

@ -13,28 +13,24 @@
"license": "MIT",
"devDependencies": {
"@eslint/js": "^9.3.0",
"@tailwindcss/typography": "^0.5.13",
"@types/node": "^20.12.12",
"@typescript-eslint/parser": "^7.10.0",
"autoprefixer": "^10.4.19",
"classnames": "^2.5.1",
"daisyui": "^4.11.1",
"eslint": "^8.57.0",
"eslint-plugin-tailwindcss": "^3.17.0",
"json-schema-faker": "^0.5.6",
"json-schema-to-ts": "^3.1.0",
"postcss": "^8.4.38",
"postcss-url": "^10.1.3",
"prettier": "^3.2.5",
"solid-devtools": "^0.29.2",
"tailwindcss": "^3.4.3",
"typescript": "^5.4.5",
"typescript-eslint": "^7.10.0",
"vite": "^5.0.11",
"vite-plugin-solid": "^2.8.2"
"vite-plugin-solid": "^2.8.2",
"eslint-plugin-tailwindcss": "^3.17.0"
},
"dependencies": {
"material-icons": "^1.13.12",
"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 { Nested } from "./nested";
import { Layout } from "./layout/layout";
import { Route, Router } from "./Routes";
import cx from "classnames";
import { Nested } from "./nested";
// Global state
const [route, setRoute] = createSignal<Route>("machines");
export { route, setRoute };
type Route = "home" | "machines";
const App: Component = () => {
const [route, setRoute] = createSignal<Route>("home");
return (
<CountProvider>
<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>
</CountProvider>
);

View File

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

View File

@ -3,7 +3,7 @@ import { render } from "solid-js/web";
import "./index.css";
import App from "./App";
import { getFakeResponse } from "../mock";
const root = document.getElementById("app");
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
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 { Header } from "./header";
import { Sidebar } from "../Sidebar";
import { route, setRoute } from "../App";
interface LayoutProps {
children: JSXElement;
}
export const Layout: Component<LayoutProps> = (props) => {
return (
<>
<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>
</>
);
return <div class="grid grid-cols-8">{props.children}</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");
/** @type {import('tailwindcss').Config} */
module.exports = {
content: ["./src/**/*.{js,jsx,ts,tsx}"],
theme: {
extend: {},
},
plugins: [typography, daisyui],
plugins: [daisyui],
};

View File

@ -11,7 +11,7 @@
npmDeps = pkgs.fetchNpmDeps {
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.
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";
path = ./new-clan;
};
empty = {
description = "A empty clan template. Primarily for usage with the clan ui";
path = ./empty;
};
default = self.templates.new-clan;
};
}