forked from clan/clan-core
Compare commits
132 Commits
flake-upda
...
main
Author | SHA1 | Date | |
---|---|---|---|
0027c46313 | |||
ca2001040b | |||
d6725100ac | |||
503ce29c84 | |||
87444cd2b8 | |||
31eca9e8bc | |||
822afe08b5 | |||
cfb78b0edb | |||
65fd7d3efe | |||
e8241fb7c9 | |||
259d51bdc8 | |||
f6fb52afbf | |||
8089b87bbb | |||
578162425d | |||
dbad63f155 | |||
da8a733899 | |||
8f58f1998d | |||
|
c43fe5187f | ||
0993fe45f6 | |||
ba86b49952 | |||
0b34c340fc | |||
d513f66170 | |||
320fb776ea | |||
1a39957dbb | |||
b5abe4025a | |||
55f4dcc460 | |||
ef4a83f739 | |||
133f2b705f | |||
83fe58e003 | |||
481f926b17 | |||
788eae432a | |||
b7936c4ed2 | |||
750c8df003 | |||
276c39aba4 | |||
90e25eeb76 | |||
56676701ae | |||
bcccf301f0 | |||
e343ba5635 | |||
66fe5ec4fd | |||
f2a884ec30 | |||
d31aa7cf88 | |||
9f19a8e605 | |||
23ef39a2d9 | |||
dda82c49b0 | |||
c91c90a2a6 | |||
5794cdf8fa | |||
01a4748d6b | |||
a8762522c8 | |||
adef52a938 | |||
c8fbf87fc8 | |||
f63e3618c2 | |||
b18d7bfeac | |||
076b98ff00 | |||
6999685bba | |||
f1c02bbd46 | |||
2caa837537 | |||
e1ddbf226a | |||
7cb8c114c2 | |||
5945630870 | |||
ccadac4bb3 | |||
15b77f6b8a | |||
9bf76037aa | |||
d0d973b797 | |||
c1e2bc9ea9 | |||
0eef21e2ef | |||
461aa579c2 | |||
da442c47f6 | |||
491d37ea67 | |||
7e087d18ee | |||
5c75a6490b | |||
750b6aec59 | |||
d138e29a53 | |||
a7febba9c8 | |||
f0f97baa65 | |||
c2dc94507e | |||
7c0aaab463 | |||
5dcac604d1 | |||
96746b7c98 | |||
2ae50b7398 | |||
3c905c5072 | |||
5b926f57cc | |||
b9788a5dba | |||
7078f09872 | |||
1aa7808c02 | |||
ba8a51101d | |||
de69c970aa | |||
fe5fa6a85d | |||
de74febf64 | |||
3b6483e819 | |||
dcd6ad0983 | |||
567d979243 | |||
c81a8681b0 | |||
31cde90819 | |||
a77bf5bf21 | |||
4befa80eb8 | |||
52584662a8 | |||
de147f63e9 | |||
96c33dec7a | |||
3c0b5f0867 | |||
c252f11c1f | |||
f1f040397d | |||
418e9937cb | |||
c34664429c | |||
6fe5928297 | |||
eee99730d1 | |||
9394760e3b | |||
a0b0e1a0ac | |||
dbaa26ccaa | |||
d1591d4485 | |||
c68a8306ba | |||
ec9f605004 | |||
e60efea1f7 | |||
efacb7f184 | |||
67275aac63 | |||
a704a05b15 | |||
01aafc520d | |||
c0a54f539a | |||
e2d7e6e86c | |||
1df4e361f7 | |||
e8bd5ad24b | |||
775f993ecc | |||
0e1478edcd | |||
bfc1203a8a | |||
2bd8b144b9 | |||
b52b2221b0 | |||
64adf17368 | |||
509d8c1dae | |||
295de17640 | |||
b158c2706f | |||
750979c988 | |||
6d7849a03c | |||
f46fd3ace6 |
@ -1,4 +1,4 @@
|
||||
# Contributing to cLAN
|
||||
# Contributing to Clan
|
||||
|
||||
## Live-reloading documentation
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
# Clan Core Repository
|
||||
|
||||
Welcome to the Clan Core Repository, the heart of the [clan.lol](https://clan.lol/) project! This monorepo is the foundation of Clan, a revolutionary open-source project aimed at restoring fun, freedom, and functionality to computing. Here, you'll find all the essential packages, NixOS modules, CLI tools, and tests needed to contribute to and work with the cLAN project. Clan leverages the Nix system to ensure reliability, security, and seamless management of digital environments, putting the power back into the hands of users.
|
||||
Welcome to the Clan Core Repository, the heart of the [clan.lol](https://clan.lol/) project! This monorepo is the foundation of Clan, a revolutionary open-source project aimed at restoring fun, freedom, and functionality to computing. Here, you'll find all the essential packages, NixOS modules, CLI tools, and tests needed to contribute to and work with the Clan project. Clan leverages the Nix system to ensure reliability, security, and seamless management of digital environments, putting the power back into the hands of users.
|
||||
|
||||
## Why Clan?
|
||||
|
||||
|
@ -145,14 +145,14 @@
|
||||
machine.succeed("echo testing > /var/test-backups/somefile")
|
||||
|
||||
# create
|
||||
machine.succeed("clan --debug --flake ${self} backups create test-backup")
|
||||
machine.succeed("clan backups create --debug --flake ${self} test-backup")
|
||||
machine.wait_until_succeeds("! systemctl is-active borgbackup-job-test-backup >&2")
|
||||
machine.succeed("test -f /run/mount-external-disk")
|
||||
machine.succeed("test -f /run/unmount-external-disk")
|
||||
|
||||
# list
|
||||
backup_id = json.loads(machine.succeed("borg-job-test-backup list --json"))["archives"][0]["archive"]
|
||||
out = machine.succeed("clan --debug --flake ${self} backups list test-backup").strip()
|
||||
out = machine.succeed("clan backups list --debug --flake ${self} test-backup").strip()
|
||||
print(out)
|
||||
assert backup_id in out, f"backup {backup_id} not found in {out}"
|
||||
localbackup_id = "hdd::/mnt/external-disk/snapshot.0"
|
||||
@ -160,14 +160,14 @@
|
||||
|
||||
## borgbackup restore
|
||||
machine.succeed("rm -f /var/test-backups/somefile")
|
||||
machine.succeed(f"clan --debug --flake ${self} backups restore test-backup borgbackup 'test-backup::borg@machine:.::{backup_id}' >&2")
|
||||
machine.succeed(f"clan backups restore --debug --flake ${self} test-backup borgbackup 'test-backup::borg@machine:.::{backup_id}' >&2")
|
||||
assert machine.succeed("cat /var/test-backups/somefile").strip() == "testing", "restore failed"
|
||||
machine.succeed("test -f /var/test-service/pre-restore-command")
|
||||
machine.succeed("test -f /var/test-service/post-restore-command")
|
||||
|
||||
## localbackup restore
|
||||
machine.succeed("rm -f /var/test-backups/somefile /var/test-service/{pre,post}-restore-command")
|
||||
machine.succeed(f"clan --debug --flake ${self} backups restore test-backup localbackup '{localbackup_id}' >&2")
|
||||
machine.succeed(f"clan backups restore --debug --flake ${self} test-backup localbackup '{localbackup_id}' >&2")
|
||||
assert machine.succeed("cat /var/test-backups/somefile").strip() == "testing", "restore failed"
|
||||
machine.succeed("test -f /var/test-service/pre-restore-command")
|
||||
machine.succeed("test -f /var/test-service/post-restore-command")
|
||||
|
@ -1,33 +1,50 @@
|
||||
{ ... }:
|
||||
{ self, ... }:
|
||||
{
|
||||
perSystem =
|
||||
{ ... }:
|
||||
{
|
||||
# checks = pkgs.lib.mkIf (pkgs.stdenv.isLinux) {
|
||||
# flash = (import ../lib/test-base.nix) {
|
||||
# name = "flash";
|
||||
# nodes.target = {
|
||||
# virtualisation.emptyDiskImages = [ 4096 ];
|
||||
# virtualisation.memorySize = 3000;
|
||||
# environment.systemPackages = [ self.packages.${pkgs.system}.clan-cli ];
|
||||
# environment.etc."install-closure".source = "${closureInfo}/store-paths";
|
||||
nodes,
|
||||
pkgs,
|
||||
lib,
|
||||
...
|
||||
}:
|
||||
let
|
||||
dependencies = [
|
||||
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";
|
||||
|
||||
# nix.settings = {
|
||||
# substituters = lib.mkForce [ ];
|
||||
# hashed-mirrors = null;
|
||||
# connect-timeout = lib.mkForce 3;
|
||||
# flake-registry = pkgs.writeText "flake-registry" ''{"flakes":[],"version":2}'';
|
||||
# experimental-features = [
|
||||
# "nix-command"
|
||||
# "flakes"
|
||||
# ];
|
||||
# };
|
||||
# };
|
||||
# testScript = ''
|
||||
# start_all()
|
||||
# machine.succeed("clan --debug --flake ${../..} flash --yes --disk main /dev/vdb test_install_machine")
|
||||
# '';
|
||||
# } { inherit pkgs self; };
|
||||
# };
|
||||
nix.settings = {
|
||||
substituters = lib.mkForce [ ];
|
||||
hashed-mirrors = null;
|
||||
connect-timeout = lib.mkForce 3;
|
||||
flake-registry = pkgs.writeText "flake-registry" ''{"flakes":[],"version":2}'';
|
||||
experimental-features = [
|
||||
"nix-command"
|
||||
"flakes"
|
||||
];
|
||||
};
|
||||
};
|
||||
testScript = ''
|
||||
start_all()
|
||||
|
||||
machine.succeed("clan flash --debug --flake ${../..} --yes --disk main /dev/vdb test_install_machine")
|
||||
'';
|
||||
} { inherit pkgs self; };
|
||||
};
|
||||
};
|
||||
}
|
||||
|
@ -2,8 +2,8 @@
|
||||
{
|
||||
clan.machines.test_install_machine = {
|
||||
clan.networking.targetHost = "test_install_machine";
|
||||
fileSystems."/".device = lib.mkDefault "/dev/null";
|
||||
boot.loader.grub.device = lib.mkDefault "/dev/null";
|
||||
fileSystems."/".device = lib.mkDefault "/dev/vdb";
|
||||
boot.loader.grub.device = lib.mkDefault "/dev/vdb";
|
||||
|
||||
imports = [ self.nixosModules.test_install_machine ];
|
||||
};
|
||||
@ -98,7 +98,7 @@
|
||||
client.succeed("${pkgs.coreutils}/bin/install -Dm 600 ${../lib/ssh/privkey} /root/.ssh/id_ed25519")
|
||||
client.wait_until_succeeds("ssh -o StrictHostKeyChecking=accept-new -v root@target hostname")
|
||||
|
||||
client.succeed("clan --debug --flake ${../..} machines install --yes test_install_machine root@target >&2")
|
||||
client.succeed("clan machines install --debug --flake ${../..} --yes test_install_machine root@target >&2")
|
||||
try:
|
||||
target.shutdown()
|
||||
except BrokenPipeError:
|
||||
|
@ -10,6 +10,7 @@ in
|
||||
hostPkgs = pkgs;
|
||||
# speed-up evaluation
|
||||
defaults = {
|
||||
nix.package = pkgs.nixVersions.latest;
|
||||
documentation.enable = lib.mkDefault false;
|
||||
boot.isContainer = true;
|
||||
|
||||
|
@ -10,6 +10,7 @@ in
|
||||
defaults = {
|
||||
documentation.enable = lib.mkDefault false;
|
||||
nix.settings.min-free = 0;
|
||||
nix.package = pkgs.nixVersions.latest;
|
||||
};
|
||||
|
||||
# to accept external dependencies such as disko
|
||||
|
@ -9,7 +9,7 @@
|
||||
# - cli frontend: https://github.com/localsend/localsend/issues/11
|
||||
# - ipv6 support: https://github.com/localsend/localsend/issues/549
|
||||
options.clan.localsend = {
|
||||
enable = lib.mkEnableOption (lib.mdDoc "enable the localsend module");
|
||||
enable = lib.mkEnableOption "enable the localsend module";
|
||||
defaultLocation = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
description = "The default download location";
|
||||
|
@ -3,9 +3,15 @@
|
||||
options.clan.static-hosts = {
|
||||
excludeHosts = lib.mkOption {
|
||||
type = lib.types.listOf lib.types.str;
|
||||
default = [ config.clanCore.machineName ];
|
||||
default =
|
||||
if config.clan.static-hosts.topLevelDomain != "" then [ ] else [ 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 =
|
||||
@ -24,7 +30,15 @@
|
||||
let
|
||||
path = zerotierIpMachinePath machine;
|
||||
in
|
||||
if builtins.pathExists path then lib.nameValuePair (builtins.readFile path) [ machine ] else null
|
||||
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
|
||||
) filteredMachines
|
||||
);
|
||||
}
|
||||
|
@ -20,7 +20,7 @@ let
|
||||
if builtins.pathExists fullPath then builtins.readFile fullPath else null
|
||||
) machines;
|
||||
networkIds = lib.filter (machine: machine != null) networkIdsUnchecked;
|
||||
networkId = builtins.elemAt networkIds 0;
|
||||
networkId = if builtins.length networkIds == 0 then null else 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.static-hosts.excludeHosts)
|
||||
name: _: !(lib.elem name config.clan.zerotier-static-peers.excludeHosts)
|
||||
) machines;
|
||||
hosts = lib.mapAttrsToList (host: _: host) (
|
||||
lib.mapAttrs' (
|
||||
|
13
devShell.nix
13
devShell.nix
@ -1,4 +1,4 @@
|
||||
{ inputs, ... }:
|
||||
{ ... }:
|
||||
{
|
||||
perSystem =
|
||||
{
|
||||
@ -9,7 +9,6 @@
|
||||
}:
|
||||
let
|
||||
writers = pkgs.callPackage ./pkgs/builders/script-writers.nix { };
|
||||
inherit (pkgs.callPackage inputs.git-hooks { }) lib;
|
||||
|
||||
ansiEscapes = {
|
||||
reset = ''\033[0m'';
|
||||
@ -22,18 +21,14 @@
|
||||
select-shell = writers.writePython3Bin "select-shell" {
|
||||
flakeIgnore = [ "E501" ];
|
||||
} ./pkgs/scripts/select-shell.py;
|
||||
|
||||
# run treefmt before each commit
|
||||
install-pre-commit-hook =
|
||||
with lib.git-hooks;
|
||||
pre-commit (wrap.abort-on-change config.treefmt.build.wrapper);
|
||||
in
|
||||
{
|
||||
devShells.default = pkgs.mkShell {
|
||||
packages = [
|
||||
select-shell
|
||||
pkgs.tea
|
||||
pkgs.nix
|
||||
# Better error messages than nix 2.18
|
||||
pkgs.nixVersions.latest
|
||||
self'.packages.tea-create-pr
|
||||
self'.packages.merge-after-ci
|
||||
self'.packages.pending-reviews
|
||||
@ -41,8 +36,6 @@
|
||||
config.treefmt.build.wrapper
|
||||
];
|
||||
shellHook = ''
|
||||
${install-pre-commit-hook}
|
||||
|
||||
echo -e "${ansiEscapes.green}switch to another dev-shell using: select-shell${ansiEscapes.reset}"
|
||||
'';
|
||||
};
|
||||
|
4
docs/.gitignore
vendored
4
docs/.gitignore
vendored
@ -1 +1,3 @@
|
||||
/site/reference
|
||||
/site/reference
|
||||
/site/static/Roboto-Regular.ttf
|
||||
/site/static/FiraCode-VF.ttf
|
@ -15,92 +15,124 @@ 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**:
|
||||
|
||||
- Download the direnv package from [here](https://direnv.net/docs/installation.html) or run the following command:
|
||||
```bash
|
||||
curl -sfL https://direnv.net/install.sh | bash
|
||||
```
|
||||
- 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"
|
||||
```
|
||||
|
||||
4. **Clone the Repository and Navigate**:
|
||||
|
||||
- Clone this repository and navigate to it.
|
||||
|
||||
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.
|
||||
|
||||
# Setting Up Your Git Workflow
|
||||
|
||||
Let's set up your Git workflow to collaborate effectively:
|
||||
|
||||
1. **Register Your Gitea Account Locally**:
|
||||
|
||||
- Execute the following command to add your Gitea account locally:
|
||||
```bash
|
||||
tea login add
|
||||
```
|
||||
- Fill out the prompt as follows:
|
||||
- URL of Gitea instance: `https://git.clan.lol`
|
||||
- Name of new Login [gitea.gchq.icu]: `gitea.gchq.icu:7171`
|
||||
- Do you have an access token? No
|
||||
- Username: YourUsername
|
||||
- Password: YourPassword
|
||||
- Set Optional settings: No
|
||||
|
||||
2. **Git Workflow**:
|
||||
|
||||
1. Add your changes to Git using `git add <file1> <file2>`.
|
||||
2. Run `nix fmt` to lint your files.
|
||||
3. Commit your changes with a descriptive message: `git commit -a -m "My descriptive commit message"`.
|
||||
4. Make sure your branch has the latest changes from upstream by executing:
|
||||
```bash
|
||||
git fetch && git rebase origin/main --autostash
|
||||
echo 'eval "$(direnv hook zsh)"' >> ~/.zshrc && echo 'eval "$(direnv hook bash)"' >> ~/.bashrc && eval "$SHELL"
|
||||
```
|
||||
5. Use `git status` to check for merge conflicts.
|
||||
6. If conflicts exist, resolve them. Here's a tutorial for resolving conflicts in [VSCode](https://code.visualstudio.com/docs/sourcecontrol/overview#_merge-conflicts).
|
||||
7. After resolving conflicts, execute `git merge --continue` and repeat step 5 until there are no conflicts.
|
||||
|
||||
3. **Create a Pull Request**:
|
||||
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
|
||||
```
|
||||
|
||||
- To automatically open a pull request that gets merged if all tests pass, execute:
|
||||
```bash
|
||||
merge-after-ci
|
||||
```
|
||||
5. **Register Your Gitea Account Locally**:
|
||||
|
||||
4. **Review Your Pull Request**:
|
||||
- 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
|
||||
|
||||
- Visit https://git.clan.lol and go to the project page. Check under "Pull Requests" for any issues with your pull request.
|
||||
|
||||
5. **Push Your Changes**:
|
||||
- If there are issues, fix them and redo step 2. Afterward, execute:
|
||||
```bash
|
||||
git push origin HEAD:YourUsername-main
|
||||
```
|
||||
- This will directly push to your open pull request.
|
||||
6. **Allow .envrc**:
|
||||
|
||||
- When you enter the directory, you'll receive an error message like this:
|
||||
```bash
|
||||
direnv: error .envrc is blocked. Run `direnv allow` to approve its content
|
||||
```
|
||||
- Execute `direnv allow` to automatically execute the shell script `.envrc` when entering the directory.
|
||||
|
||||
7. **(Optional) Install Git Hooks**:
|
||||
- To syntax check your code you can run:
|
||||
```bash
|
||||
nix fmt
|
||||
```
|
||||
- To make this automatic install the git hooks
|
||||
```bash
|
||||
./scripts/pre-commit
|
||||
```
|
||||
|
||||
8. **Open a Pull Request**:
|
||||
- To automatically open up a pull request you can use our tool called:
|
||||
```
|
||||
merge-after-ci --reviewers Mic92 Lassulus Qubasa
|
||||
```
|
||||
|
||||
# Debugging
|
||||
|
||||
Here are some methods for debugging and testing the clan-cli:
|
||||
|
||||
## See all possible packages and tests
|
||||
|
||||
To quickly show all possible packages and tests execute:
|
||||
|
||||
```bash
|
||||
nix flake show --system no-eval
|
||||
```
|
||||
|
||||
Under `checks` you will find all tests that are executed in our CI. Under `packages` you find all our projects.
|
||||
|
||||
```
|
||||
git+file:///home/lhebendanz/Projects/clan-core
|
||||
├───apps
|
||||
│ └───x86_64-linux
|
||||
│ ├───install-vm: app
|
||||
│ └───install-vm-nogui: app
|
||||
├───checks
|
||||
│ └───x86_64-linux
|
||||
│ ├───borgbackup omitted (use '--all-systems' to show)
|
||||
│ ├───check-for-breakpoints omitted (use '--all-systems' to show)
|
||||
│ ├───clan-dep-age omitted (use '--all-systems' to show)
|
||||
│ ├───clan-dep-bash omitted (use '--all-systems' to show)
|
||||
│ ├───clan-dep-e2fsprogs omitted (use '--all-systems' to show)
|
||||
│ ├───clan-dep-fakeroot omitted (use '--all-systems' to show)
|
||||
│ ├───clan-dep-git omitted (use '--all-systems' to show)
|
||||
│ ├───clan-dep-nix omitted (use '--all-systems' to show)
|
||||
│ ├───clan-dep-openssh omitted (use '--all-systems' to show)
|
||||
│ ├───"clan-dep-python3.11-mypy" omitted (use '--all-systems' to show)
|
||||
├───packages
|
||||
│ └───x86_64-linux
|
||||
│ ├───clan-cli omitted (use '--all-systems' to show)
|
||||
│ ├───clan-cli-docs omitted (use '--all-systems' to show)
|
||||
│ ├───clan-ts-api omitted (use '--all-systems' to show)
|
||||
│ ├───clan-vm-manager omitted (use '--all-systems' to show)
|
||||
│ ├───default omitted (use '--all-systems' to show)
|
||||
│ ├───deploy-docs omitted (use '--all-systems' to show)
|
||||
│ ├───docs omitted (use '--all-systems' to show)
|
||||
│ ├───editor omitted (use '--all-systems' to show)
|
||||
└───templates
|
||||
├───default: template: Initialize a new clan flake
|
||||
└───new-clan: template: Initialize a new clan flake
|
||||
```
|
||||
|
||||
You can execute every test separately by following the tree path `nix build .#checks.x86_64-linux.clan-pytest` for example.
|
||||
|
||||
## Test Locally in Devshell with Breakpoints
|
||||
|
||||
To test the cli locally in a development environment and set breakpoints for debugging, follow these steps:
|
||||
@ -150,12 +182,14 @@ If you need to inspect the Nix sandbox while running tests, follow these steps:
|
||||
2. Use `cntr` and `psgrep` to attach to the Nix sandbox. This allows you to interactively debug your code while it's paused. For example:
|
||||
|
||||
```bash
|
||||
cntr exec -w your_sandbox_name
|
||||
psgrep -a -x your_python_process_name
|
||||
cntr attach <container id, container name or process id>
|
||||
```
|
||||
|
||||
Or you can also use the [nix breakpoint hook](https://nixos.org/manual/nixpkgs/stable/#breakpointhook)
|
||||
|
||||
|
||||
# Standards
|
||||
|
||||
Every new module name should be in kebab-case.
|
||||
Every fact definition, where possible should be in kebab-case.
|
||||
- Every new module name should be in kebab-case.
|
||||
- Every fact definition, where possible should be in kebab-case.
|
||||
|
@ -20,11 +20,11 @@ There are several reasons for choosing to self-host. These can include:
|
||||
|
||||
Alice wants to self-host a mumble server for her family.
|
||||
|
||||
- She visits to the cLAN website, and follows the instructions on how to install cLAN-OS on her server.
|
||||
- Alice logs into a terminal on her server via SSH (alternatively uses cLAN GUI app)
|
||||
- Using the cLAN CLI or GUI tool, alice creates a new private network for her family (VPN)
|
||||
- Alice now browses a list of curated cLAN modules and finds a module for mumble.
|
||||
- She adds this module to her network using the cLAN tool.
|
||||
- She visits to the Clan website, and follows the instructions on how to install Clan-OS on her server.
|
||||
- Alice logs into a terminal on her server via SSH (alternatively uses Clan GUI app)
|
||||
- Using the Clan CLI or GUI tool, alice creates a new private network for her family (VPN)
|
||||
- Alice now browses a list of curated Clan modules and finds a module for mumble.
|
||||
- She adds this module to her network using the Clan tool.
|
||||
- After that, she uses the clan tool to invite her family members to her network
|
||||
- Other family members join the private network via the invitation.
|
||||
- By accepting the invitation, other members automatically install all required software to interact with the network on their machine.
|
||||
@ -33,7 +33,7 @@ Alice wants to self-host a mumble server for her family.
|
||||
|
||||
Alice wants to add a photos app to her private network
|
||||
|
||||
- She uses the clan CLI or GUI tool to manage her existing private cLAN family network
|
||||
- She uses the clan CLI or GUI tool to manage her existing private Clan family network
|
||||
- She discovers a module for photoprism, and adds it to her server using the tool
|
||||
- Other members who are already part of her network, will receive a notification that an update is required to their environment
|
||||
- After accepting, all new software and services to interact with the new photoprism service will be installed automatically.
|
||||
|
@ -1,4 +1,4 @@
|
||||
# Joining a cLAN network
|
||||
# Joining a Clan network
|
||||
|
||||
## General Description
|
||||
|
||||
@ -8,13 +8,13 @@ Joining a self-hosted infrastructure involves connecting to a network, server, o
|
||||
|
||||
### Story 1: Joining a private network
|
||||
|
||||
Alice' son Bob has never heard of cLAN, but receives an invitation URL from Alice who already set up private cLAN network for her family.
|
||||
Alice' son Bob has never heard of Clan, but receives an invitation URL from Alice who already set up private Clan network for her family.
|
||||
|
||||
Bob opens the invitation link and lands on the cLAN website. He quickly learns about what cLAN is and can see that the invitation is for a private network of his family that hosts a number of services, like a private voice chat and a photo sharing platform.
|
||||
Bob opens the invitation link and lands on the Clan website. He quickly learns about what Clan is and can see that the invitation is for a private network of his family that hosts a number of services, like a private voice chat and a photo sharing platform.
|
||||
|
||||
Bob decides to join the network and follows the instructions to install the cLAN tool on his computer.
|
||||
Bob decides to join the network and follows the instructions to install the Clan tool on his computer.
|
||||
|
||||
Feeding the invitation link to the cLAN tool, bob registers his machine with the network.
|
||||
Feeding the invitation link to the Clan tool, bob registers his machine with the network.
|
||||
|
||||
All programs required to interact with the network will be installed and configured automatically and securely.
|
||||
|
||||
@ -22,7 +22,7 @@ Optionally, bob can customize the configuration of these programs through a simp
|
||||
|
||||
### Story 2: Receiving breaking changes
|
||||
|
||||
The cLAN family network which Bob is part of received an update.
|
||||
The Clan family network which Bob is part of received an update.
|
||||
|
||||
The existing photo sharing service has been removed and replaced with another alternative service. The new photo sharing service requires a different client app to view and upload photos.
|
||||
|
||||
@ -30,7 +30,7 @@ Bob accepts the update. Now his environment will be updated. The old client soft
|
||||
|
||||
Because Bob has customized the previous photo viewing app, he is notified that this customization is no longer valid, as the software has been removed (deprecation message).l
|
||||
|
||||
Optionally, Bob can now customize the new photo viewing software through his cLAN configuration app or via a config file.
|
||||
Optionally, Bob can now customize the new photo viewing software through his Clan configuration app or via a config file.
|
||||
|
||||
## Challenges
|
||||
|
||||
|
@ -1,10 +1,10 @@
|
||||
# cLAN module maintaining
|
||||
# Clan module maintaining
|
||||
|
||||
## General Description
|
||||
|
||||
cLAN modules are pieces of software that can be used by admins to build a private or public infrastructure.
|
||||
Clan modules are pieces of software that can be used by admins to build a private or public infrastructure.
|
||||
|
||||
cLAN modules should have the following properties:
|
||||
Clan modules should have the following properties:
|
||||
|
||||
1. Documented: It should be clear what the module does and how to use it.
|
||||
1. Self contained: A module should be usable as is. If it requires any other software or settings, those should be delivered with the module itself.
|
||||
|
29
docs/main.py
29
docs/main.py
@ -16,15 +16,26 @@ 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>
|
||||
AsciinemaPlayer.create('{video_dir + name}',
|
||||
document.getElementById("{name}"), {{
|
||||
loop: true,
|
||||
autoPlay: true,
|
||||
controls: false,
|
||||
speed: 1.5,
|
||||
theme: "solarized-light"
|
||||
}});
|
||||
// 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();
|
||||
</script>
|
||||
|
||||
<link rel="stylesheet" type="text/css" href="{asciinema_dir}/asciinema-player.css" />
|
||||
</div>"""
|
||||
|
@ -1,4 +1,4 @@
|
||||
site_name: Clan Docs
|
||||
site_name: Clan Documentation
|
||||
site_url: https://docs.clan.lol
|
||||
repo_url: https://git.clan.lol/clan/clan-core/
|
||||
repo_name: clan-core
|
||||
@ -39,7 +39,7 @@ exclude_docs: |
|
||||
|
||||
nav:
|
||||
- Blog:
|
||||
- blog/index.md
|
||||
- blog/index.md
|
||||
- Getting started:
|
||||
- index.md
|
||||
- Installer: getting-started/installer.md
|
||||
@ -94,8 +94,9 @@ docs_dir: site
|
||||
site_dir: out
|
||||
|
||||
theme:
|
||||
font: false
|
||||
logo: https://clan.lol/static/logo/clan-white.png
|
||||
favicon: https://clan.lol/static/logo/clan-dark.png
|
||||
favicon: https://clan.lol/static/dark-favicon/128x128.png
|
||||
name: material
|
||||
features:
|
||||
- navigation.instant
|
||||
@ -104,9 +105,8 @@ theme:
|
||||
- content.code.copy
|
||||
- content.tabs.link
|
||||
icon:
|
||||
repo: fontawesome/brands/git
|
||||
font:
|
||||
code: Roboto Mono
|
||||
repo: fontawesome/brands/git-alt
|
||||
custom_dir: overrides
|
||||
|
||||
palette:
|
||||
# Palette toggle for light mode
|
||||
@ -128,8 +128,7 @@ theme:
|
||||
name: Switch to light mode
|
||||
|
||||
extra_css:
|
||||
- static/asciinema-player/custom-theme.css
|
||||
- static/asciinema-player/asciinema-player.css
|
||||
- static/extra.css
|
||||
|
||||
extra:
|
||||
social:
|
||||
@ -142,7 +141,6 @@ extra:
|
||||
- icon: fontawesome/solid/rss
|
||||
link: /feed_rss_created.xml
|
||||
|
||||
|
||||
plugins:
|
||||
- search
|
||||
- blog
|
||||
|
@ -4,6 +4,8 @@
|
||||
clan-cli-docs,
|
||||
asciinema-player-js,
|
||||
asciinema-player-css,
|
||||
roboto,
|
||||
fira-code,
|
||||
...
|
||||
}:
|
||||
let
|
||||
@ -33,6 +35,10 @@ pkgs.stdenv.mkDerivation {
|
||||
mkdir -p ./site/static/asciinema-player
|
||||
ln -snf ${asciinema-player-js} ./site/static/asciinema-player/asciinema-player.min.js
|
||||
ln -snf ${asciinema-player-css} ./site/static/asciinema-player/asciinema-player.css
|
||||
|
||||
# Link to fonts
|
||||
ln -snf ${roboto}/share/fonts/truetype/Roboto-Regular.ttf ./site/static/
|
||||
ln -snf ${fira-code}/share/fonts/truetype/FiraCode-VF.ttf ./site/static/
|
||||
'';
|
||||
|
||||
buildPhase = ''
|
||||
|
@ -40,13 +40,14 @@ def sanitize(text: str) -> str:
|
||||
return text.replace(">", "\\>")
|
||||
|
||||
|
||||
def replace_store_path(text: str) -> Path:
|
||||
def replace_store_path(text: str) -> tuple[str, str]:
|
||||
res = text
|
||||
if text.startswith("/nix/store/"):
|
||||
res = "https://git.clan.lol/clan/clan-core/src/branch/main/" + str(
|
||||
Path(*Path(text).parts[4:])
|
||||
)
|
||||
return Path(res)
|
||||
name = Path(res).name
|
||||
return (res, name)
|
||||
|
||||
|
||||
def render_option_header(name: str) -> str:
|
||||
@ -108,9 +109,10 @@ def render_option(name: str, option: dict[str, Any], level: int = 3) -> str:
|
||||
"""
|
||||
|
||||
decls = option.get("declarations", [])
|
||||
source_path = replace_store_path(decls[0])
|
||||
source_path, name = replace_store_path(decls[0])
|
||||
print(source_path, name)
|
||||
res += f"""
|
||||
:simple-git: [{source_path.name}]({source_path})
|
||||
:simple-git: [{name}]({source_path})
|
||||
"""
|
||||
res += "\n"
|
||||
|
||||
@ -160,7 +162,7 @@ def produce_clan_core_docs() -> None:
|
||||
for option_name, info in options.items():
|
||||
outfile = f"{module_name}/index.md"
|
||||
|
||||
# Create seperate files for nested options
|
||||
# Create separate files for nested options
|
||||
if len(option_name.split(".")) <= 2:
|
||||
# i.e. clan-core.clanDir
|
||||
output = core_outputs.get(
|
||||
|
@ -5,6 +5,8 @@
|
||||
clan-cli-docs,
|
||||
asciinema-player-js,
|
||||
asciinema-player-css,
|
||||
roboto,
|
||||
fira-code,
|
||||
...
|
||||
}:
|
||||
pkgs.mkShell {
|
||||
@ -18,7 +20,12 @@ pkgs.mkShell {
|
||||
echo "Generated API documentation in './site/reference/' "
|
||||
|
||||
mkdir -p ./site/static/asciinema-player
|
||||
|
||||
ln -snf ${asciinema-player-js} ./site/static/asciinema-player/asciinema-player.min.js
|
||||
ln -snf ${asciinema-player-css} ./site/static/asciinema-player/asciinema-player.css
|
||||
|
||||
# Link to fonts
|
||||
ln -snf ${roboto}/share/fonts/truetype/Roboto-Regular.ttf ./site/static/
|
||||
ln -snf ${fira-code}/share/fonts/truetype/FiraCode-VF.ttf ./site/static/
|
||||
'';
|
||||
}
|
||||
|
12
docs/overrides/main.html
Normal file
12
docs/overrides/main.html
Normal file
@ -0,0 +1,12 @@
|
||||
{% 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 %}
|
@ -42,13 +42,22 @@ sudo umount /dev/sdb1
|
||||
=== "**Linux OS**"
|
||||
### Step 2. Flash Custom Installer
|
||||
|
||||
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.
|
||||
Using clan flash enables the inclusion of ssh public keys.
|
||||
It also allows to set language and keymap currently in the installer image.
|
||||
|
||||
```bash
|
||||
clan --flake git+https://git.clan.lol/clan/clan-core flash flash-installer --disk main /dev/sd<X>
|
||||
clan flash --flake git+https://git.clan.lol/clan/clan-core \
|
||||
--ssh-pubkey $HOME/.ssh/id_ed25519.pub \
|
||||
--keymap en \
|
||||
--language en \
|
||||
--disk main /dev/sd<X> \
|
||||
flash-installer
|
||||
```
|
||||
|
||||
The `--ssh-pubkey`, `--language` and `--keymap` are optional.
|
||||
Replace `$HOME/.ssh/id_ed25519.pub` with a path to your SSH public key.
|
||||
If you do not have an ssh key yet, you can generate one with `ssh-keygen -t ed25519` command.
|
||||
|
||||
!!! Danger "Specifying the wrong device can lead to unrecoverable data loss."
|
||||
|
||||
The `clan flash` utility will erase the disk. Make sure to specify the correct device
|
||||
|
@ -4,10 +4,15 @@ Clan enables encryption of secrets (such as passwords & keys) ensuring security
|
||||
|
||||
Clan utilizes the [sops](https://github.com/getsops/sops) format and integrates with [sops-nix](https://github.com/Mic92/sops-nix) on NixOS machines.
|
||||
|
||||
This guide will walk you through:
|
||||
|
||||
### Create Your Master Keypair
|
||||
- **Creating a Keypair for Your User**: Learn how to generate a keypair for $USER to securely control all secrets.
|
||||
- **Creating Your First Secret**: Step-by-step instructions on creating your initial secret.
|
||||
- **Assigning Machine Access to the Secret**: Understand how to grant a machine access to the newly created secret.
|
||||
|
||||
To get started, you'll need to create **Your master keypair**.
|
||||
## Create Your Admin Keypair
|
||||
|
||||
To get started, you'll need to create **Your admin keypair**.
|
||||
|
||||
!!! info
|
||||
Don't worry — if you've already made one before, this step won't change or overwrite it.
|
||||
@ -27,7 +32,7 @@ Also add your age public key to the repository with 'clan secrets users add YOUR
|
||||
|
||||
!!! warning
|
||||
Make sure to keep a safe backup of the private key you've just created.
|
||||
If it's lost, you won't be able to get to your secrets anymore because they all need the master key to be unlocked.
|
||||
If it's lost, you won't be able to get to your secrets anymore because they all need the admin key to be unlocked.
|
||||
|
||||
!!! note
|
||||
It's safe to add any secrets created by the clan CLI and placed in your repository to version control systems like `git`.
|
||||
@ -35,7 +40,7 @@ Also add your age public key to the repository with 'clan secrets users add YOUR
|
||||
### Add Your Public Key
|
||||
|
||||
```bash
|
||||
clan secrets users add <your_username> <your_public_key>
|
||||
clan secrets users add $USER <your_public_key>
|
||||
```
|
||||
|
||||
It's best to choose the same username as on your Setup/Admin Machine that you use to control the deployment with.
|
||||
@ -101,17 +106,20 @@ In your nixos configuration you can get a path to secrets like this `config.sops
|
||||
|
||||
### Assigning Access
|
||||
|
||||
By default, secrets are encrypted for your key. To specify which users and machines can access a secret:
|
||||
When using `clan secrets set <secret>` without arguments, secrets are encrypted for the key of the user named like your current $USER.
|
||||
|
||||
```bash
|
||||
clan secrets set --machine <machine1> --machine <machine2> --user <user1> --user <user2> <secret_name>
|
||||
```
|
||||
You can also just add machines/users to existing secrets:
|
||||
To add machines/users to an existing secret use:
|
||||
|
||||
```bash
|
||||
clan secrets machines add-secret <machine_name> <secret_name>
|
||||
```
|
||||
|
||||
Alternatively specify users and machines while creating a secret:
|
||||
|
||||
```bash
|
||||
clan secrets set --machine <machine1> --machine <machine2> --user <user1> --user <user2> <secret_name>
|
||||
```
|
||||
|
||||
## Advanced
|
||||
|
||||
In this section we go into more advanced secret management topics.
|
||||
@ -183,11 +191,9 @@ Since our clan secret module will auto-import secrets that are encrypted for a p
|
||||
you can now remove `sops.secrets.<secrets> = { };` unless you need to specify more options for the secret like owner/group of the secret file.
|
||||
|
||||
|
||||
|
||||
## Indepth Explanation
|
||||
|
||||
|
||||
|
||||
The secrets system conceptually knows two different entities:
|
||||
|
||||
- **Machine**: consumes secrets
|
||||
|
13
docs/site/static/extra.css
Normal file
13
docs/site/static/extra.css
Normal file
@ -0,0 +1,13 @@
|
||||
@font-face {
|
||||
font-family: "Roboto";
|
||||
src: url(./Roboto-Regular.ttf) format('truetype');
|
||||
}
|
||||
@font-face {
|
||||
font-family: "Fira Code";
|
||||
src: url(./FiraCode-VF.ttf) format('truetype');
|
||||
}
|
||||
|
||||
:root {
|
||||
--md-text-font: "Roboto";
|
||||
--md-code-font: "Fira Code";
|
||||
}
|
65
flake.lock
65
flake.lock
@ -7,11 +7,11 @@
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1716394172,
|
||||
"narHash": "sha256-B+pNhV8GFeCj9/MoH+qtGqKbgv6fU4hGaw2+NoYYtB0=",
|
||||
"lastModified": 1717177033,
|
||||
"narHash": "sha256-G3CZJafCO8WDy3dyA2EhpUJEmzd5gMJ2IdItAg0Hijw=",
|
||||
"owner": "nix-community",
|
||||
"repo": "disko",
|
||||
"rev": "23c63fb09334c3e8958b57e2ddc3870b75b9111d",
|
||||
"rev": "0274af4c92531ebfba4a5bd493251a143bc51f3c",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@ -27,11 +27,11 @@
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1715865404,
|
||||
"narHash": "sha256-/GJvTdTpuDjNn84j82cU6bXztE0MSkdnTWClUCRub78=",
|
||||
"lastModified": 1717285511,
|
||||
"narHash": "sha256-iKzJcpdXih14qYVcZ9QC9XuZYnPc6T8YImb6dX166kw=",
|
||||
"owner": "hercules-ci",
|
||||
"repo": "flake-parts",
|
||||
"rev": "8dc45382d5206bd292f9c2768b8058a8fd8311d9",
|
||||
"rev": "2a55567fcf15b1b1c7ed712a2c6fadaec7412ea8",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@ -40,22 +40,6 @@
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"git-hooks": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1716413087,
|
||||
"narHash": "sha256-nSTIB7JeJGBGsvtqlyfhUByh/isyK1nfOq2YMxUOFJQ=",
|
||||
"owner": "fricklerhandwerk",
|
||||
"repo": "git-hooks",
|
||||
"rev": "99a78fcf7dc03ba7b1d5c00af109c1e28ced3490",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "fricklerhandwerk",
|
||||
"repo": "git-hooks",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixlib": {
|
||||
"locked": {
|
||||
"lastModified": 1712450863,
|
||||
@ -73,11 +57,11 @@
|
||||
},
|
||||
"nixos-2311": {
|
||||
"locked": {
|
||||
"lastModified": 1715818734,
|
||||
"narHash": "sha256-WvAJWCwPj/6quKcsgsvQYyZRxV8ho/yUzj0HZQ34DVU=",
|
||||
"lastModified": 1717017538,
|
||||
"narHash": "sha256-S5kltvDDfNQM3xx9XcvzKEOyN2qk8Sa+aSOLqZ+1Ujc=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "95742536dc6debb5a8b8b78b27001c38f369f1e7",
|
||||
"rev": "64e468fd2652105710d86cd2ae3e65a5a6d58dec",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@ -95,11 +79,11 @@
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1716123454,
|
||||
"narHash": "sha256-U2o4UPM/UsEyIX2p11+YEQgR9HY3PmjZ2mRl/x5e4xo=",
|
||||
"lastModified": 1716210724,
|
||||
"narHash": "sha256-iqQa3omRcHGpWb1ds75jS9ruA5R39FTmAkeR3J+ve1w=",
|
||||
"owner": "nix-community",
|
||||
"repo": "nixos-generators",
|
||||
"rev": "a63e0c83dd83fe28cc571b97129e13373436bd82",
|
||||
"rev": "d14b286322c7f4f897ca4b1726ce38cb68596c94",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@ -116,11 +100,11 @@
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1716132123,
|
||||
"narHash": "sha256-rATSWbPaKQfZGaemu0tHL2xfCzVIVwpuTjk+KSBC+k4=",
|
||||
"lastModified": 1717040312,
|
||||
"narHash": "sha256-yI/en4IxuCEClIUpIs3QTyYCCtmSPLOhwLJclfNwdeg=",
|
||||
"owner": "nix-community",
|
||||
"repo": "nixos-images",
|
||||
"rev": "8c9cab8c44434c12dafc465fbf61a710c5bceb08",
|
||||
"rev": "47bfb55316e105390dd761e0b6e8e0be09462b67",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@ -131,11 +115,11 @@
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1716127062,
|
||||
"narHash": "sha256-2rk8FqB/iQV2d0vQLs684/Tj5PUHaS1sFwG7fng5vXE=",
|
||||
"lastModified": 1717298511,
|
||||
"narHash": "sha256-9sXuJn/nL+9ImeYtlspTvjt83z1wIgU+9AwfNbnq+tI=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "8a2555763c48e2410054de3f52f7310ce3241ec5",
|
||||
"rev": "6634a0509e9e81e980b129435fbbec518ab246d0",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@ -149,7 +133,6 @@
|
||||
"inputs": {
|
||||
"disko": "disko",
|
||||
"flake-parts": "flake-parts",
|
||||
"git-hooks": "git-hooks",
|
||||
"nixos-generators": "nixos-generators",
|
||||
"nixos-images": "nixos-images",
|
||||
"nixpkgs": "nixpkgs",
|
||||
@ -165,11 +148,11 @@
|
||||
"nixpkgs-stable": []
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1716087663,
|
||||
"narHash": "sha256-zuSAGlx8Qk0OILGCC2GUyZ58/SJ5R3GZdeUNQ6IS0fQ=",
|
||||
"lastModified": 1717297459,
|
||||
"narHash": "sha256-cZC2f68w5UrJ1f+2NWGV9Gx0dEYmxwomWN2B0lx0QRA=",
|
||||
"owner": "Mic92",
|
||||
"repo": "sops-nix",
|
||||
"rev": "0bf1808e70ce80046b0cff821c019df2b19aabf5",
|
||||
"rev": "ab2a43b0d21d1d37d4d5726a892f714eaeb4b075",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@ -185,11 +168,11 @@
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1715940852,
|
||||
"narHash": "sha256-wJqHMg/K6X3JGAE9YLM0LsuKrKb4XiBeVaoeMNlReZg=",
|
||||
"lastModified": 1717278143,
|
||||
"narHash": "sha256-u10aDdYrpiGOLoxzY/mJ9llST9yO8Q7K/UlROoNxzDw=",
|
||||
"owner": "numtide",
|
||||
"repo": "treefmt-nix",
|
||||
"rev": "2fba33a182602b9d49f0b2440513e5ee091d838b",
|
||||
"rev": "3eb96ca1ae9edf792a8e0963cc92fddfa5a87706",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
@ -21,15 +21,17 @@
|
||||
flake-parts.inputs.nixpkgs-lib.follows = "nixpkgs";
|
||||
treefmt-nix.url = "github:numtide/treefmt-nix";
|
||||
treefmt-nix.inputs.nixpkgs.follows = "nixpkgs";
|
||||
git-hooks.url = "github:fricklerhandwerk/git-hooks";
|
||||
git-hooks.flake = false;
|
||||
};
|
||||
|
||||
outputs =
|
||||
inputs@{ flake-parts, ... }:
|
||||
inputs@{ flake-parts, self, ... }:
|
||||
flake-parts.lib.mkFlake { inherit inputs; } (
|
||||
{ ... }:
|
||||
{
|
||||
clan = {
|
||||
# meta.name = "clan-core";
|
||||
directory = self;
|
||||
};
|
||||
systems = [
|
||||
"x86_64-linux"
|
||||
"aarch64-linux"
|
||||
|
@ -17,6 +17,33 @@ 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;
|
||||
@ -33,15 +60,27 @@ in
|
||||
default = { };
|
||||
description = "Allows to include machine-specific modules i.e. machines.\${name} = { ... }";
|
||||
};
|
||||
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";
|
||||
|
||||
# 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";
|
||||
};
|
||||
};
|
||||
|
||||
pkgsForSystem = mkOption {
|
||||
type = types.functionTo types.raw;
|
||||
default = _system: null;
|
||||
@ -52,6 +91,7 @@ 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); };
|
||||
@ -65,9 +105,8 @@ in
|
||||
directory
|
||||
specialArgs
|
||||
machines
|
||||
clanName
|
||||
clanIcon
|
||||
pkgsForSystem
|
||||
meta
|
||||
;
|
||||
};
|
||||
};
|
||||
|
@ -7,16 +7,58 @@
|
||||
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} = { ... }
|
||||
clanName, # Needs to be (globally) unique, as this determines the folder name where the flake gets downloaded to.
|
||||
# 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
|
||||
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
|
||||
@ -58,11 +100,15 @@ let
|
||||
(machines.${name} or { })
|
||||
(
|
||||
{
|
||||
networking.hostName = lib.mkDefault name;
|
||||
clanCore.clanName = clanName;
|
||||
clanCore.clanIcon = clanIcon;
|
||||
# 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;
|
||||
nixpkgs.hostPlatform = lib.mkDefault system;
|
||||
|
||||
# speeds up nix commands by using the nixpkgs from the host system (especially useful in VMs)
|
||||
@ -127,10 +173,15 @@ 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 (
|
||||
|
@ -122,7 +122,7 @@ in
|
||||
cores = lib.mkOption {
|
||||
type = lib.types.ints.positive;
|
||||
default = 1;
|
||||
description = lib.mdDoc ''
|
||||
description = ''
|
||||
Specify the number of cores the guest is permitted to use.
|
||||
The number can be higher than the available cores on the
|
||||
host system.
|
||||
@ -132,7 +132,7 @@ in
|
||||
memorySize = lib.mkOption {
|
||||
type = lib.types.ints.positive;
|
||||
default = 1024;
|
||||
description = lib.mdDoc ''
|
||||
description = ''
|
||||
The memory size in megabytes of the virtual machine.
|
||||
'';
|
||||
};
|
||||
@ -140,7 +140,7 @@ in
|
||||
graphics = lib.mkOption {
|
||||
type = lib.types.bool;
|
||||
default = true;
|
||||
description = lib.mdDoc ''
|
||||
description = ''
|
||||
Whether to run QEMU with a graphics window, or in nographic mode.
|
||||
Serial console will be enabled on both settings, but this will
|
||||
change the preferred console.
|
||||
@ -150,7 +150,7 @@ in
|
||||
waypipe = lib.mkOption {
|
||||
type = lib.types.bool;
|
||||
default = false;
|
||||
description = lib.mdDoc ''
|
||||
description = ''
|
||||
Whether to use waypipe for native wayland passthrough, or not.
|
||||
'';
|
||||
};
|
||||
|
@ -81,7 +81,7 @@ in
|
||||
};
|
||||
};
|
||||
settings = lib.mkOption {
|
||||
description = lib.mdDoc "override the network config in /var/lib/zerotier/bla/$network.json";
|
||||
description = "override the network config in /var/lib/zerotier/bla/$network.json";
|
||||
type = lib.types.submodule { freeformType = (pkgs.formats.json { }).type; };
|
||||
};
|
||||
};
|
||||
|
@ -51,9 +51,14 @@ class AppendOptionAction(argparse.Action):
|
||||
lst.append(values[1])
|
||||
|
||||
|
||||
def create_parser(prog: str | None = None) -> argparse.ArgumentParser:
|
||||
parser = argparse.ArgumentParser(prog=prog, description="cLAN tool")
|
||||
def flake_path(arg: str) -> str | Path:
|
||||
flake_dir = Path(arg).resolve()
|
||||
if flake_dir.exists() and flake_dir.is_dir():
|
||||
return flake_dir
|
||||
return arg
|
||||
|
||||
|
||||
def add_common_flags(parser: argparse.ArgumentParser) -> None:
|
||||
parser.add_argument(
|
||||
"--debug",
|
||||
help="Enable debug logging",
|
||||
@ -70,12 +75,6 @@ def create_parser(prog: str | None = None) -> argparse.ArgumentParser:
|
||||
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",
|
||||
@ -84,51 +83,224 @@ def create_parser(prog: str | None = None) -> argparse.ArgumentParser:
|
||||
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(
|
||||
"backups",
|
||||
help="manage backups of clan machines",
|
||||
description="manage backups of clan machines",
|
||||
epilog=(
|
||||
"""
|
||||
This subcommand provides an interface to backups that clan machines expose.
|
||||
|
||||
Examples:
|
||||
|
||||
$ clan backups list [MACHINE]
|
||||
List backups for the machine [MACHINE]
|
||||
|
||||
$ clan backups create [MACHINE]
|
||||
Create a backup for the machine [MACHINE].
|
||||
|
||||
$ clan backups restore [MACHINE] [PROVIDER] [NAME]
|
||||
The backup to restore for the machine [MACHINE] with the configured [PROVIDER]
|
||||
with the name [NAME].
|
||||
|
||||
For more detailed information, visit: https://docs.clan.lol/getting-started/backups/
|
||||
"""
|
||||
),
|
||||
formatter_class=argparse.RawTextHelpFormatter,
|
||||
)
|
||||
backups.register_parser(parser_backups)
|
||||
|
||||
parser_flake = subparsers.add_parser(
|
||||
"flakes", help="create a clan flake inside the current directory"
|
||||
"flakes",
|
||||
help="create a clan flake inside the current directory",
|
||||
description="create a clan flake inside the current directory",
|
||||
epilog=(
|
||||
"""
|
||||
Examples:
|
||||
$ clan flakes create [DIR]
|
||||
Will create a new clan flake in the specified directory and create it if it
|
||||
doesn't exist yet. The flake will be created from a default template.
|
||||
|
||||
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="set nixos configuration")
|
||||
parser_config = subparsers.add_parser(
|
||||
"config",
|
||||
help="read a nixos configuration option",
|
||||
description="read a nixos configuration option",
|
||||
epilog=(
|
||||
"""
|
||||
"""
|
||||
),
|
||||
formatter_class=argparse.RawTextHelpFormatter,
|
||||
)
|
||||
config.register_parser(parser_config)
|
||||
|
||||
parser_ssh = subparsers.add_parser("ssh", help="ssh to a remote machine")
|
||||
parser_ssh = subparsers.add_parser(
|
||||
"ssh",
|
||||
help="ssh to a remote machine",
|
||||
description="ssh to a remote machine",
|
||||
epilog=(
|
||||
"""
|
||||
This subcommand allows seamless ssh access to the nixos-image builders.
|
||||
|
||||
Examples:
|
||||
|
||||
$ clan ssh [ssh_args ...] --json [JSON]
|
||||
Will ssh in to the machine based on the deployment information contained in
|
||||
the json string. [JSON] can either be a json formatted string itself, or point
|
||||
towards a file containing the deployment information
|
||||
|
||||
For more detailed information, visit: https://docs.clan.lol/getting-started/deploy
|
||||
"""
|
||||
),
|
||||
formatter_class=argparse.RawTextHelpFormatter,
|
||||
)
|
||||
ssh_cli.register_parser(parser_ssh)
|
||||
|
||||
parser_secrets = subparsers.add_parser("secrets", help="manage secrets")
|
||||
parser_secrets = subparsers.add_parser(
|
||||
"secrets",
|
||||
help="manage secrets",
|
||||
description="manage secrets",
|
||||
epilog=(
|
||||
"""
|
||||
This subcommand provides an interface to secret facts.
|
||||
|
||||
Examples:
|
||||
|
||||
$ clan secrets list [regex]
|
||||
Will list secrets for all managed machines.
|
||||
It accepts an optional regex, allowing easy filtering of returned secrets.
|
||||
|
||||
$ clan secrets get [SECRET]
|
||||
Will display the content of the specified secret.
|
||||
|
||||
For more detailed information, visit: https://docs.clan.lol/getting-started/secrets/
|
||||
"""
|
||||
),
|
||||
formatter_class=argparse.RawTextHelpFormatter,
|
||||
)
|
||||
secrets.register_parser(parser_secrets)
|
||||
|
||||
parser_facts = subparsers.add_parser("facts", help="manage facts")
|
||||
parser_facts = subparsers.add_parser(
|
||||
"facts",
|
||||
help="manage facts",
|
||||
description="manage facts",
|
||||
epilog=(
|
||||
"""
|
||||
|
||||
This subcommand provides an interface to facts of clan machines.
|
||||
Facts are artifacts that a service can generate.
|
||||
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.
|
||||
|
||||
A service is an included clan-module that implements facts generation functionality.
|
||||
For example the zerotier module will generate private and public facts.
|
||||
In this case the public fact will be the resulting zerotier-ip of the machine.
|
||||
The secret fact will be the zerotier-identity-secret, which is used by zerotier
|
||||
to prove the machine has control of the zerotier-ip.
|
||||
|
||||
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
|
||||
of the facts for a machine in place.
|
||||
|
||||
For more detailed information, visit: https://docs.clan.lol/getting-started/secrets/
|
||||
"""
|
||||
),
|
||||
formatter_class=argparse.RawTextHelpFormatter,
|
||||
)
|
||||
facts.register_parser(parser_facts)
|
||||
|
||||
parser_machine = subparsers.add_parser(
|
||||
"machines", help="manage machines and their configuration"
|
||||
"machines",
|
||||
help="manage machines and their configuration",
|
||||
description="manage machines and their configuration",
|
||||
epilog=(
|
||||
"""
|
||||
This subcommand provides an interface to machines managed by clan.
|
||||
|
||||
Examples:
|
||||
|
||||
$ clan machines list
|
||||
List all the machines managed by clan.
|
||||
|
||||
$ clan machines update [MACHINES]
|
||||
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]
|
||||
Will install the specified machine [MACHINE], to the specified [TARGET_HOST].
|
||||
|
||||
For more detailed information, visit: https://docs.clan.lol/getting-started/deploy
|
||||
"""
|
||||
),
|
||||
formatter_class=argparse.RawTextHelpFormatter,
|
||||
)
|
||||
machines.register_parser(parser_machine)
|
||||
|
||||
parser_vms = subparsers.add_parser("vms", help="manage virtual machines")
|
||||
parser_vms = subparsers.add_parser(
|
||||
"vms", help="manage virtual machines", description="manage virtual machines"
|
||||
)
|
||||
vms.register_parser(parser_vms)
|
||||
|
||||
parser_history = subparsers.add_parser("history", help="manage history")
|
||||
parser_history = subparsers.add_parser(
|
||||
"history",
|
||||
help="manage history",
|
||||
description="manage history",
|
||||
)
|
||||
history.register_parser(parser_history)
|
||||
|
||||
parser_flash = subparsers.add_parser(
|
||||
"flash", help="flash machines to usb sticks or into isos"
|
||||
"flash",
|
||||
help="flash machines to usb sticks or into isos",
|
||||
description="flash machines to usb sticks or into isos",
|
||||
)
|
||||
flash.register_parser(parser_flash)
|
||||
|
||||
if argcomplete:
|
||||
argcomplete.autocomplete(parser)
|
||||
|
||||
register_common_flags(parser)
|
||||
|
||||
return parser
|
||||
|
||||
|
||||
|
@ -2,6 +2,7 @@ import argparse
|
||||
import json
|
||||
import logging
|
||||
|
||||
from ..completions import add_dynamic_completer, complete_machines
|
||||
from ..errors import ClanError
|
||||
from ..machines.machines import Machine
|
||||
|
||||
@ -33,13 +34,17 @@ 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:
|
||||
parser.add_argument(
|
||||
machines_parser = 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)
|
||||
|
@ -3,6 +3,7 @@ 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
|
||||
|
||||
@ -48,6 +49,8 @@ def list_backups(machine: Machine, provider: str | None = None) -> list[Backup]:
|
||||
|
||||
|
||||
def list_command(args: argparse.Namespace) -> None:
|
||||
if args.flake is None:
|
||||
raise ClanError("Could not find clan flake toplevel directory")
|
||||
machine = Machine(name=args.machine, flake=args.flake)
|
||||
backups = list_backups(machine=machine, provider=args.provider)
|
||||
for backup in backups:
|
||||
@ -55,8 +58,9 @@ def list_command(args: argparse.Namespace) -> None:
|
||||
|
||||
|
||||
def register_list_parser(parser: argparse.ArgumentParser) -> None:
|
||||
parser.add_argument(
|
||||
machines_parser = 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)
|
||||
|
@ -62,6 +62,8 @@ def restore_backup(
|
||||
|
||||
|
||||
def restore_command(args: argparse.Namespace) -> None:
|
||||
if args.flake is None:
|
||||
raise ClanError("Could not find clan flake toplevel directory")
|
||||
machine = Machine(name=args.machine, flake=args.flake)
|
||||
restore_backup(
|
||||
machine=machine,
|
||||
|
@ -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"), stderr_buf.decode("utf-8")
|
||||
return stdout_buf.decode("utf-8", "replace"), stderr_buf.decode("utf-8", "replace")
|
||||
|
||||
|
||||
class TimeTable:
|
||||
@ -101,13 +101,19 @@ 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:
|
||||
glog.debug(f"$: {shlex.join(cmd)} \nCaller: {get_caller()}")
|
||||
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()}")
|
||||
tstart = datetime.now()
|
||||
|
||||
# Start the subprocess
|
||||
@ -120,7 +126,10 @@ def run(
|
||||
)
|
||||
stdout_buf, stderr_buf = handle_output(process, log)
|
||||
|
||||
rc = process.wait()
|
||||
if input:
|
||||
process.communicate(input)
|
||||
else:
|
||||
process.wait()
|
||||
tend = datetime.now()
|
||||
|
||||
global TIME_TABLE
|
||||
@ -136,7 +145,27 @@ def run(
|
||||
msg=error_msg,
|
||||
)
|
||||
|
||||
if check and rc != 0:
|
||||
if check and process.returncode != 0:
|
||||
raise ClanCmdError(cmd_out)
|
||||
|
||||
return cmd_out
|
||||
|
||||
|
||||
def run_no_stdout(
|
||||
cmd: list[str],
|
||||
*,
|
||||
env: dict[str, str] | None = None,
|
||||
cwd: Path = Path.cwd(),
|
||||
log: Log = Log.STDERR,
|
||||
check: bool = True,
|
||||
error_msg: str | None = None,
|
||||
) -> CmdOut:
|
||||
"""
|
||||
Like run, but automatically suppresses stdout, if not in DEBUG log level.
|
||||
If in DEBUG log level the stdout of commands will be shown.
|
||||
"""
|
||||
if logging.getLogger(__name__.split(".")[0]).isEnabledFor(logging.DEBUG):
|
||||
return run(cmd, env=env, log=log, check=check, error_msg=error_msg)
|
||||
else:
|
||||
log = Log.NONE
|
||||
return run(cmd, env=env, log=log, check=check, error_msg=error_msg)
|
||||
|
157
pkgs/clan-cli/clan_cli/completions.py
Normal file
157
pkgs/clan-cli/clan_cli/completions.py
Normal file
@ -0,0 +1,157 @@
|
||||
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]
|
@ -150,6 +150,15 @@ 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(
|
||||
@ -307,7 +316,7 @@ def register_parser(
|
||||
)
|
||||
|
||||
# inject callback function to process the input later
|
||||
parser.set_defaults(func=get_or_set_option)
|
||||
parser.set_defaults(func=get_option)
|
||||
parser.add_argument(
|
||||
"--machine",
|
||||
"-m",
|
||||
@ -345,13 +354,6 @@ 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:
|
||||
|
@ -16,16 +16,116 @@ def register_parser(parser: argparse.ArgumentParser) -> None:
|
||||
required=True,
|
||||
)
|
||||
|
||||
check_parser = subparser.add_parser("check", help="check if facts are up to date")
|
||||
check_parser = subparser.add_parser(
|
||||
"check",
|
||||
help="check if facts are up to date",
|
||||
epilog=(
|
||||
"""
|
||||
This subcommand allows checking if all facts are up to date.
|
||||
|
||||
Examples:
|
||||
|
||||
$ clan facts check [MACHINE]
|
||||
Will check facts for the specified machine.
|
||||
|
||||
|
||||
For more detailed information, visit: https://docs.clan.lol/getting-started/secrets/
|
||||
"""
|
||||
),
|
||||
formatter_class=argparse.RawTextHelpFormatter,
|
||||
)
|
||||
register_check_parser(check_parser)
|
||||
|
||||
list_parser = subparser.add_parser("list", help="list all facts")
|
||||
list_parser = subparser.add_parser(
|
||||
"list",
|
||||
help="list all facts",
|
||||
epilog=(
|
||||
"""
|
||||
This subcommand allows listing all public facts for a specific machine.
|
||||
|
||||
The resulting list will be a json string with the name of the fact as its key
|
||||
and the fact itself as it's value.
|
||||
|
||||
This is how an example output might look like:
|
||||
```
|
||||
{
|
||||
"[FACT_NAME]": "[FACT]"
|
||||
}
|
||||
```
|
||||
|
||||
Examples:
|
||||
|
||||
$ clan facts list [MACHINE]
|
||||
Will list facts for the specified machine.
|
||||
|
||||
|
||||
For more detailed information, visit: https://docs.clan.lol/getting-started/secrets/
|
||||
"""
|
||||
),
|
||||
formatter_class=argparse.RawTextHelpFormatter,
|
||||
)
|
||||
register_list_parser(list_parser)
|
||||
|
||||
parser_generate = subparser.add_parser(
|
||||
"generate", help="generate secrets for machines if they don't exist yet"
|
||||
"generate",
|
||||
help="generate public and secret facts for machines",
|
||||
epilog=(
|
||||
"""
|
||||
This subcommand allows control of the generation of facts.
|
||||
Often this function will be invoked automatically on deploying machines,
|
||||
but there are situations the user may want to have more granular control,
|
||||
especially for the regeneration of certain services.
|
||||
|
||||
A service is an included clan-module that implements facts generation functionality.
|
||||
For example the zerotier module will generate private and public facts.
|
||||
In this case the public fact will be the resulting zerotier-ip of the machine.
|
||||
The secret fact will be the zerotier-identity-secret, which is used by zerotier
|
||||
to prove the machine has control of the zerotier-ip.
|
||||
|
||||
|
||||
Examples:
|
||||
|
||||
$ clan facts generate
|
||||
Will generate facts for all machines.
|
||||
|
||||
$ clan facts generate [MACHINE]
|
||||
Will generate facts for the specified machine.
|
||||
|
||||
$ clan facts generate [MACHINE] --service [SERVICE]
|
||||
Will generate facts for the specified machine for the specified service.
|
||||
|
||||
$ 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
|
||||
of the facts for a machine in place.
|
||||
|
||||
For more detailed information, visit: https://docs.clan.lol/getting-started/secrets/
|
||||
"""
|
||||
),
|
||||
formatter_class=argparse.RawTextHelpFormatter,
|
||||
)
|
||||
register_generate_parser(parser_generate)
|
||||
|
||||
parser_upload = subparser.add_parser("upload", help="upload secrets for machines")
|
||||
parser_upload = subparser.add_parser(
|
||||
"upload",
|
||||
help="upload secrets for machines",
|
||||
epilog=(
|
||||
"""
|
||||
This subcommand allows uploading secrets to remote machines.
|
||||
|
||||
If using sops as a secret backend it will upload the private key to the machine.
|
||||
If using password store it uploads all the secrets you manage to the machine.
|
||||
|
||||
The default backend is sops.
|
||||
|
||||
Examples:
|
||||
|
||||
$ clan facts upload [MACHINE]
|
||||
Will upload secrets to a specific machine.
|
||||
|
||||
For more detailed information, visit: https://docs.clan.lol/getting-started/secrets/
|
||||
"""
|
||||
),
|
||||
formatter_class=argparse.RawTextHelpFormatter,
|
||||
)
|
||||
register_upload_parser(parser_upload)
|
||||
|
@ -2,6 +2,7 @@ import argparse
|
||||
import importlib
|
||||
import logging
|
||||
|
||||
from ..completions import add_dynamic_completer, complete_machines
|
||||
from ..machines.machines import Machine
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
@ -54,10 +55,12 @@ def check_command(args: argparse.Namespace) -> None:
|
||||
|
||||
|
||||
def register_check_parser(parser: argparse.ArgumentParser) -> None:
|
||||
parser.add_argument(
|
||||
machines_parser = 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",
|
||||
|
@ -9,6 +9,11 @@ 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
|
||||
@ -27,6 +32,7 @@ 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
|
||||
|
||||
|
||||
@ -209,26 +215,30 @@ def generate_facts(
|
||||
|
||||
def generate_command(args: argparse.Namespace) -> None:
|
||||
if len(args.machines) == 0:
|
||||
machines = get_all_machines(args.flake)
|
||||
machines = get_all_machines(args.flake, args.option)
|
||||
else:
|
||||
machines = get_selected_machines(args.flake, args.machines)
|
||||
machines = get_selected_machines(args.flake, args.option, args.machines)
|
||||
generate_facts(machines, args.service, args.regenerate)
|
||||
|
||||
|
||||
def register_generate_parser(parser: argparse.ArgumentParser) -> None:
|
||||
parser.add_argument(
|
||||
machines_parser = parser.add_argument(
|
||||
"machines",
|
||||
type=str,
|
||||
help="machine to generate facts for. if empty, generate facts for all machines",
|
||||
nargs="*",
|
||||
default=[],
|
||||
)
|
||||
parser.add_argument(
|
||||
add_dynamic_completer(machines_parser, complete_machines)
|
||||
|
||||
service_parser = 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,
|
||||
|
@ -3,6 +3,7 @@ import importlib
|
||||
import json
|
||||
import logging
|
||||
|
||||
from ..completions import add_dynamic_completer, complete_machines
|
||||
from ..machines.machines import Machine
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
@ -37,8 +38,10 @@ def get_command(args: argparse.Namespace) -> None:
|
||||
|
||||
|
||||
def register_list_parser(parser: argparse.ArgumentParser) -> None:
|
||||
parser.add_argument(
|
||||
machines_parser = parser.add_argument(
|
||||
"machine",
|
||||
help="The machine to print facts for",
|
||||
)
|
||||
add_dynamic_completer(machines_parser, complete_machines)
|
||||
|
||||
parser.set_defaults(func=get_command)
|
||||
|
@ -2,6 +2,7 @@ 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
|
||||
|
||||
@ -15,25 +16,25 @@ class SecretStore(SecretStoreBase):
|
||||
def set(
|
||||
self, service: str, name: str, value: bytes, groups: list[str]
|
||||
) -> Path | None:
|
||||
subprocess.run(
|
||||
run(
|
||||
nix_shell(
|
||||
["nixpkgs#pass"],
|
||||
["pass", "insert", "-m", f"machines/{self.machine.name}/{name}"],
|
||||
),
|
||||
input=value,
|
||||
check=True,
|
||||
log=Log.BOTH,
|
||||
error_msg=f"Failed to insert secret {name}",
|
||||
)
|
||||
return None # we manage the files outside of the git repo
|
||||
|
||||
def get(self, service: str, name: str) -> bytes:
|
||||
return subprocess.run(
|
||||
return run(
|
||||
nix_shell(
|
||||
["nixpkgs#pass"],
|
||||
["pass", "show", f"machines/{self.machine.name}/{name}"],
|
||||
),
|
||||
check=True,
|
||||
stdout=subprocess.PIPE,
|
||||
).stdout
|
||||
error_msg=f"Failed to get secret {name}",
|
||||
).stdout.encode("utf-8")
|
||||
|
||||
def exists(self, service: str, name: str) -> bool:
|
||||
password_store = os.environ.get(
|
||||
@ -48,7 +49,7 @@ class SecretStore(SecretStoreBase):
|
||||
)
|
||||
hashes = []
|
||||
hashes.append(
|
||||
subprocess.run(
|
||||
run(
|
||||
nix_shell(
|
||||
["nixpkgs#git"],
|
||||
[
|
||||
@ -61,13 +62,15 @@ class SecretStore(SecretStoreBase):
|
||||
f"machines/{self.machine.name}",
|
||||
],
|
||||
),
|
||||
stdout=subprocess.PIPE,
|
||||
).stdout.strip()
|
||||
check=False,
|
||||
)
|
||||
.stdout.encode("utf-8")
|
||||
.strip()
|
||||
)
|
||||
for symlink in Path(password_store).glob(f"machines/{self.machine.name}/**/*"):
|
||||
if symlink.is_symlink():
|
||||
hashes.append(
|
||||
subprocess.run(
|
||||
run(
|
||||
nix_shell(
|
||||
["nixpkgs#git"],
|
||||
[
|
||||
@ -80,8 +83,10 @@ class SecretStore(SecretStoreBase):
|
||||
str(symlink),
|
||||
],
|
||||
),
|
||||
stdout=subprocess.PIPE,
|
||||
).stdout.strip()
|
||||
check=False,
|
||||
)
|
||||
.stdout.encode("utf-8")
|
||||
.strip()
|
||||
)
|
||||
|
||||
# we sort the hashes to make sure that the order is always the same
|
||||
|
@ -5,6 +5,7 @@ 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
|
||||
|
||||
@ -32,6 +33,8 @@ 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}/",
|
||||
],
|
||||
@ -46,8 +49,10 @@ def upload_command(args: argparse.Namespace) -> None:
|
||||
|
||||
|
||||
def register_upload_parser(parser: argparse.ArgumentParser) -> None:
|
||||
parser.add_argument(
|
||||
machines_parser = parser.add_argument(
|
||||
"machine",
|
||||
help="The machine to upload secrets to",
|
||||
)
|
||||
add_dynamic_completer(machines_parser, complete_machines)
|
||||
|
||||
parser.set_defaults(func=upload_command)
|
||||
|
@ -53,7 +53,7 @@ def inspect_flake(flake_url: str | Path, machine_name: str) -> FlakeConfig:
|
||||
gcroot_icon: Path = machine_gcroot(flake_url=str(flake_url)) / vm.machine_name
|
||||
nix_add_to_gcroots(vm.machine_icon, gcroot_icon)
|
||||
|
||||
# Get the cLAN name
|
||||
# Get the Clan name
|
||||
cmd = nix_eval(
|
||||
[
|
||||
f'{flake_url}#clanInternals.machines."{system}"."{machine_name}".config.clanCore.clanName'
|
||||
@ -70,7 +70,7 @@ def inspect_flake(flake_url: str | Path, machine_name: str) -> FlakeConfig:
|
||||
)
|
||||
res = run_cmd(cmd)
|
||||
|
||||
# If the icon is null, no icon is set for this cLAN
|
||||
# If the icon is null, no icon is set for this Clan
|
||||
if res == "null":
|
||||
icon_path = None
|
||||
else:
|
||||
@ -113,7 +113,7 @@ def inspect_command(args: argparse.Namespace) -> None:
|
||||
res = inspect_flake(
|
||||
flake_url=inspect_options.flake, machine_name=inspect_options.machine
|
||||
)
|
||||
print("cLAN name:", res.clan_name)
|
||||
print("Clan name:", res.clan_name)
|
||||
print("Icon:", res.icon)
|
||||
print("Description:", res.description)
|
||||
print("Last updated:", res.last_updated)
|
||||
|
@ -3,7 +3,6 @@ import importlib
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import textwrap
|
||||
from collections.abc import Sequence
|
||||
@ -13,6 +12,7 @@ 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,63 +21,6 @@ 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,
|
||||
*,
|
||||
@ -85,7 +28,9 @@ def flash_machine(
|
||||
disks: dict[str, str],
|
||||
system_config: dict[str, Any],
|
||||
dry_run: bool,
|
||||
write_efi_boot_entries: bool,
|
||||
debug: bool,
|
||||
extra_args: list[str] = [],
|
||||
) -> None:
|
||||
secret_facts_module = importlib.import_module(machine.secret_facts_module)
|
||||
secret_facts_store: SecretStoreBase = secret_facts_module.SecretStore(
|
||||
@ -112,6 +57,8 @@ def flash_machine(
|
||||
disko_install.append("sudo")
|
||||
|
||||
disko_install.append("disko-install")
|
||||
if write_efi_boot_entries:
|
||||
disko_install.append("--write-efi-boot-entries")
|
||||
if dry_run:
|
||||
disko_install.append("--dry-run")
|
||||
if debug:
|
||||
@ -128,6 +75,8 @@ def flash_machine(
|
||||
json.dumps(system_config),
|
||||
]
|
||||
)
|
||||
disko_install.extend(["--option", "dry-run", "true"])
|
||||
disko_install.extend(extra_args)
|
||||
|
||||
cmd = nix_shell(
|
||||
["nixpkgs#disko"],
|
||||
@ -148,6 +97,8 @@ class FlashOptions:
|
||||
mode: str
|
||||
language: str
|
||||
keymap: str
|
||||
write_efi_boot_entries: bool
|
||||
nix_options: list[str]
|
||||
|
||||
|
||||
class AppendDiskAction(argparse.Action):
|
||||
@ -176,8 +127,10 @@ def flash_command(args: argparse.Namespace) -> None:
|
||||
confirm=not args.yes,
|
||||
debug=args.debug,
|
||||
mode=args.mode,
|
||||
language=args.lang,
|
||||
language=args.language,
|
||||
keymap=args.keymap,
|
||||
write_efi_boot_entries=args.write_efi_boot_entries,
|
||||
nix_options=args.option,
|
||||
)
|
||||
|
||||
machine = Machine(opts.machine, flake=opts.flake)
|
||||
@ -191,40 +144,22 @@ def flash_command(args: argparse.Namespace) -> None:
|
||||
if ask != "y":
|
||||
return
|
||||
|
||||
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")
|
||||
|
||||
localectl = get_keymap_and_locale()
|
||||
extra_config = {
|
||||
"users": {
|
||||
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}}}}
|
||||
},
|
||||
"console": {
|
||||
"keyMap": opts.keymap if opts.keymap else localectl["keymap"],
|
||||
},
|
||||
"i18n": {
|
||||
"defaultLocale": opts.language if opts.language else localectl["locale"],
|
||||
},
|
||||
}
|
||||
}
|
||||
if opts.keymap:
|
||||
extra_config["console"] = {"keyMap": opts.keymap}
|
||||
|
||||
if opts.language:
|
||||
extra_config["i18n"] = {"defaultLocale": opts.language}
|
||||
|
||||
flash_machine(
|
||||
machine,
|
||||
@ -233,15 +168,19 @@ 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:
|
||||
parser.add_argument(
|
||||
machines_parser = parser.add_argument(
|
||||
"machine",
|
||||
type=str,
|
||||
help="machine to install",
|
||||
)
|
||||
add_dynamic_completer(machines_parser, complete_machines)
|
||||
|
||||
parser.add_argument(
|
||||
"--disk",
|
||||
type=str,
|
||||
@ -251,12 +190,14 @@ def register_parser(parser: argparse.ArgumentParser) -> None:
|
||||
help="device to flash to",
|
||||
default={},
|
||||
)
|
||||
mode_help = textwrap.dedent("""\
|
||||
mode_help = textwrap.dedent(
|
||||
"""\
|
||||
Specify the mode of operation. Valid modes are: format, mount."
|
||||
Format will format the disk before installing.
|
||||
Mount will mount the disk before installing.
|
||||
Mount is useful for updating an existing system without losing data.
|
||||
""")
|
||||
"""
|
||||
)
|
||||
parser.add_argument(
|
||||
"--mode",
|
||||
type=str,
|
||||
@ -272,7 +213,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(
|
||||
"--lang",
|
||||
"--language",
|
||||
type=str,
|
||||
help="system language",
|
||||
)
|
||||
@ -293,4 +234,16 @@ def register_parser(parser: argparse.ArgumentParser) -> None:
|
||||
default=False,
|
||||
action="store_true",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--write-efi-boot-entries",
|
||||
help=textwrap.dedent(
|
||||
"""
|
||||
Write EFI boot entries to the NVRAM of the system for the installed system.
|
||||
Specify this option if you plan to boot from this disk on the current machine,
|
||||
but not if you plan to move the disk to another machine.
|
||||
"""
|
||||
).strip(),
|
||||
default=False,
|
||||
action="store_true",
|
||||
)
|
||||
parser.set_defaults(func=flash_command)
|
||||
|
@ -5,6 +5,7 @@ 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
|
||||
|
||||
|
||||
@ -17,7 +18,26 @@ def register_parser(parser: argparse.ArgumentParser) -> None:
|
||||
required=True,
|
||||
)
|
||||
|
||||
update_parser = subparser.add_parser("update", help="Update a machine")
|
||||
update_parser = subparser.add_parser(
|
||||
"update",
|
||||
help="Update a machine",
|
||||
epilog=(
|
||||
"""
|
||||
This subcommand provides an interface to update machines managed by clan.
|
||||
|
||||
Examples:
|
||||
|
||||
$ clan machines update [MACHINES]
|
||||
Will update the specified machine [MACHINE], if [MACHINE] is omitted, the command
|
||||
will attempt to update every configured machine.
|
||||
To exclude machines being updated `clan.deployment.requireExplicitUpdate = true;`
|
||||
can be set in the machine config.
|
||||
|
||||
For more detailed information, visit: https://docs.clan.lol/getting-started/deploy
|
||||
"""
|
||||
),
|
||||
formatter_class=argparse.RawTextHelpFormatter,
|
||||
)
|
||||
register_update_parser(update_parser)
|
||||
|
||||
create_parser = subparser.add_parser("create", help="Create a machine")
|
||||
@ -26,9 +46,34 @@ def register_parser(parser: argparse.ArgumentParser) -> None:
|
||||
delete_parser = subparser.add_parser("delete", help="Delete a machine")
|
||||
register_delete_parser(delete_parser)
|
||||
|
||||
list_parser = subparser.add_parser("list", help="List machines")
|
||||
list_parser = subparser.add_parser(
|
||||
"list",
|
||||
help="List machines",
|
||||
epilog=(
|
||||
"""
|
||||
This subcommand lists all machines managed by this clan.
|
||||
|
||||
Examples:
|
||||
|
||||
$ clan machines list
|
||||
Lists all the machines and their descriptions.
|
||||
"""
|
||||
),
|
||||
formatter_class=argparse.RawTextHelpFormatter,
|
||||
)
|
||||
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",
|
||||
@ -37,5 +82,23 @@ def register_parser(parser: argparse.ArgumentParser) -> None:
|
||||
The target must be a Linux based system reachable via SSH.
|
||||
Installing a machine means overwriting the target's disk.
|
||||
""",
|
||||
epilog=(
|
||||
"""
|
||||
This subcommand provides an interface to install machines managed by clan.
|
||||
|
||||
Examples:
|
||||
|
||||
$ clan machines install [MACHINE] [TARGET_HOST]
|
||||
Will install the specified machine [MACHINE], to the specified [TARGET_HOST].
|
||||
|
||||
$ clan machines install [MACHINE] --json [JSON]
|
||||
Will install the specified machine [MACHINE] to the host exposed by
|
||||
the deployment information of the [JSON] deployment string.
|
||||
|
||||
For information on how to set up the installer see: https://docs.clan.lol/getting-started/installer/
|
||||
For more detailed information, visit: https://docs.clan.lol/getting-started/deploy
|
||||
"""
|
||||
),
|
||||
formatter_class=argparse.RawTextHelpFormatter,
|
||||
)
|
||||
register_install_parser(install_parser)
|
||||
|
@ -1,6 +1,7 @@
|
||||
import argparse
|
||||
import shutil
|
||||
|
||||
from ..completions import add_dynamic_completer, complete_machines
|
||||
from ..dirs import specific_machine_dir
|
||||
from ..errors import ClanError
|
||||
|
||||
@ -14,5 +15,7 @@ def delete_command(args: argparse.Namespace) -> None:
|
||||
|
||||
|
||||
def register_delete_parser(parser: argparse.ArgumentParser) -> None:
|
||||
parser.add_argument("host", type=str)
|
||||
machines_parser = parser.add_argument("host", type=str)
|
||||
add_dynamic_completer(machines_parser, complete_machines)
|
||||
|
||||
parser.set_defaults(func=delete_command)
|
||||
|
@ -8,6 +8,7 @@ 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
|
||||
@ -26,6 +27,7 @@ def install_nixos(
|
||||
debug: bool = False,
|
||||
password: str | None = None,
|
||||
no_reboot: bool = False,
|
||||
extra_args: list[str] = [],
|
||||
) -> None:
|
||||
secret_facts_module = importlib.import_module(machine.secret_facts_module)
|
||||
log.info(f"installing {machine.name}")
|
||||
@ -56,6 +58,7 @@ def install_nixos(
|
||||
f"{machine.flake}#{machine.name}",
|
||||
"--extra-files",
|
||||
str(tmpdir),
|
||||
*extra_args,
|
||||
]
|
||||
|
||||
if no_reboot:
|
||||
@ -95,6 +98,7 @@ class InstallOptions:
|
||||
debug: bool
|
||||
no_reboot: bool
|
||||
json_ssh_deploy: dict[str, str] | None
|
||||
nix_options: list[str]
|
||||
|
||||
|
||||
def install_command(args: argparse.Namespace) -> None:
|
||||
@ -127,6 +131,7 @@ def install_command(args: argparse.Namespace) -> None:
|
||||
debug=args.debug,
|
||||
no_reboot=args.no_reboot,
|
||||
json_ssh_deploy=json_ssh_deploy,
|
||||
nix_options=args.option,
|
||||
)
|
||||
machine = Machine(opts.machine, flake=opts.flake)
|
||||
machine.target_host_address = opts.target_host
|
||||
@ -142,6 +147,7 @@ def install_command(args: argparse.Namespace) -> None:
|
||||
debug=opts.debug,
|
||||
password=password,
|
||||
no_reboot=opts.no_reboot,
|
||||
extra_args=opts.nix_options,
|
||||
)
|
||||
|
||||
|
||||
@ -183,11 +189,14 @@ def register_install_parser(parser: argparse.ArgumentParser) -> None:
|
||||
help="do not ask for confirmation",
|
||||
default=False,
|
||||
)
|
||||
parser.add_argument(
|
||||
|
||||
machines_parser = parser.add_argument(
|
||||
"machine",
|
||||
type=str,
|
||||
help="machine to install",
|
||||
)
|
||||
add_dynamic_completer(machines_parser, complete_machines)
|
||||
|
||||
parser.add_argument(
|
||||
"target_host",
|
||||
type=str,
|
||||
|
@ -7,7 +7,7 @@ from .machines import Machine
|
||||
|
||||
|
||||
# function to speedup eval if we want to evauluate all machines
|
||||
def get_all_machines(flake_dir: Path) -> list[Machine]:
|
||||
def get_all_machines(flake_dir: Path, nix_options: list[str]) -> list[Machine]:
|
||||
config = nix_config()
|
||||
system = config["system"]
|
||||
json_path = run(
|
||||
@ -19,13 +19,20 @@ def get_all_machines(flake_dir: Path) -> list[Machine]:
|
||||
machines = []
|
||||
for name, machine_data in machines_json.items():
|
||||
machines.append(
|
||||
Machine(name=name, flake=flake_dir, deployment_info=machine_data)
|
||||
Machine(
|
||||
name=name,
|
||||
flake=flake_dir,
|
||||
deployment_info=machine_data,
|
||||
nix_options=nix_options,
|
||||
)
|
||||
)
|
||||
return machines
|
||||
|
||||
|
||||
def get_selected_machines(flake_dir: Path, machine_names: list[str]) -> list[Machine]:
|
||||
def get_selected_machines(
|
||||
flake_dir: Path, nix_options: list[str], machine_names: list[str]
|
||||
) -> list[Machine]:
|
||||
machines = []
|
||||
for name in machine_names:
|
||||
machines.append(Machine(name=name, flake=flake_dir))
|
||||
machines.append(Machine(name=name, flake=flake_dir, nix_options=nix_options))
|
||||
return machines
|
||||
|
@ -1,66 +1,39 @@
|
||||
import argparse
|
||||
import dataclasses
|
||||
import json
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
||||
from clan_cli.api import API
|
||||
|
||||
from ..cmd import Log, run
|
||||
from ..cmd import run_no_stdout
|
||||
from ..nix import nix_config, nix_eval
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@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) -> dict[str, MachineInfo]:
|
||||
def list_machines(flake_url: str | Path, debug: bool) -> list[str]:
|
||||
config = nix_config()
|
||||
system = config["system"]
|
||||
cmd = nix_eval(
|
||||
[
|
||||
f"{flake_url}#clanInternals.machines.{system}",
|
||||
"--apply",
|
||||
"""builtins.mapAttrs (name: attrs: {
|
||||
inherit (attrs.config.clanCore) machineDescription machineIcon machineName;
|
||||
})""",
|
||||
"builtins.attrNames",
|
||||
"--json",
|
||||
]
|
||||
)
|
||||
|
||||
if not debug:
|
||||
proc = run(cmd, log=Log.NONE)
|
||||
else:
|
||||
proc = run(cmd)
|
||||
proc = run_no_stdout(cmd)
|
||||
|
||||
res = proc.stdout.strip()
|
||||
machines_dict = json.loads(res)
|
||||
|
||||
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()
|
||||
}
|
||||
return json.loads(res)
|
||||
|
||||
|
||||
def list_command(args: argparse.Namespace) -> None:
|
||||
flake_path = Path(args.flake).resolve()
|
||||
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)
|
||||
for name in list_machines(flake_path, args.debug):
|
||||
print(name)
|
||||
|
||||
|
||||
def register_list_parser(parser: argparse.ArgumentParser) -> None:
|
||||
|
@ -10,7 +10,7 @@ from clan_cli.clan_uri import ClanURI, MachineData
|
||||
from clan_cli.dirs import vm_state_dir
|
||||
from clan_cli.qemu.qmp import QEMUMonitorProtocol
|
||||
|
||||
from ..cmd import run
|
||||
from ..cmd import run_no_stdout
|
||||
from ..errors import ClanError
|
||||
from ..nix import nix_build, nix_config, nix_eval, nix_metadata
|
||||
from ..ssh import Host, parse_deployment_address
|
||||
@ -41,9 +41,10 @@ class QMPWrapper:
|
||||
|
||||
|
||||
class Machine:
|
||||
flake: str | Path
|
||||
name: str
|
||||
flake: str | Path
|
||||
data: MachineData
|
||||
nix_options: list[str]
|
||||
eval_cache: dict[str, str]
|
||||
build_cache: dict[str, Path]
|
||||
_flake_path: Path | None
|
||||
@ -55,6 +56,7 @@ class Machine:
|
||||
name: str,
|
||||
flake: Path | str,
|
||||
deployment_info: dict | None = None,
|
||||
nix_options: list[str] = [],
|
||||
machine: MachineData | None = None,
|
||||
) -> None:
|
||||
"""
|
||||
@ -76,6 +78,7 @@ class Machine:
|
||||
self.build_cache: dict[str, Path] = {}
|
||||
self._flake_path: Path | None = None
|
||||
self._deployment_info: None | dict = deployment_info
|
||||
self.nix_options = nix_options
|
||||
|
||||
state_dir = vm_state_dir(flake_url=str(self.flake), vm_name=self.data.name)
|
||||
|
||||
@ -197,7 +200,7 @@ class Machine:
|
||||
config_json.flush()
|
||||
|
||||
file_info = json.loads(
|
||||
run(
|
||||
run_no_stdout(
|
||||
nix_eval(
|
||||
[
|
||||
"--impure",
|
||||
@ -242,15 +245,15 @@ class Machine:
|
||||
flake = f"path:{self.flake_dir}"
|
||||
|
||||
args += [
|
||||
f'{flake}#clanInternals.machines."{system}".{self.data.name}.{attr}',
|
||||
*nix_options,
|
||||
f'{flake}#clanInternals.machines."{system}".{self.data.name}.{attr}'
|
||||
]
|
||||
args += nix_options + self.nix_options
|
||||
|
||||
if method == "eval":
|
||||
output = run(nix_eval(args)).stdout.strip()
|
||||
output = run_no_stdout(nix_eval(args)).stdout.strip()
|
||||
return output
|
||||
elif method == "build":
|
||||
outpath = run(nix_build(args)).stdout.strip()
|
||||
outpath = run_no_stdout(nix_build(args)).stdout.strip()
|
||||
return Path(outpath)
|
||||
else:
|
||||
raise ValueError(f"Unknown method {method}")
|
||||
|
60
pkgs/clan-cli/clan_cli/machines/show.py
Normal file
60
pkgs/clan-cli/clan_cli/machines/show.py
Normal file
@ -0,0 +1,60 @@
|
||||
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)
|
@ -3,9 +3,10 @@ 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
|
||||
@ -53,11 +54,7 @@ def upload_sources(
|
||||
path,
|
||||
]
|
||||
)
|
||||
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}"
|
||||
)
|
||||
run(cmd, env=env, error_msg="failed to upload sources")
|
||||
return path
|
||||
|
||||
# Slow path: we need to upload all sources to the remote machine
|
||||
@ -73,16 +70,13 @@ def upload_sources(
|
||||
]
|
||||
)
|
||||
log.info("run %s", shlex.join(cmd))
|
||||
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}"
|
||||
)
|
||||
proc = run(cmd, error_msg="failed to upload sources")
|
||||
|
||||
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.decode('utf-8', 'replace')}"
|
||||
f"failed to parse output of {shlex.join(cmd)}: {e}\nGot: {proc.stdout}"
|
||||
)
|
||||
|
||||
|
||||
@ -110,11 +104,9 @@ def deploy_nixos(machines: MachineGroup) -> None:
|
||||
|
||||
ssh_arg += " -i " + host.key if host.key else ""
|
||||
|
||||
extra_args = host.meta.get("extra_args", [])
|
||||
cmd = [
|
||||
"nixos-rebuild",
|
||||
"switch",
|
||||
*extra_args,
|
||||
"--fast",
|
||||
"--option",
|
||||
"keep-going",
|
||||
@ -124,6 +116,7 @@ def deploy_nixos(machines: MachineGroup) -> None:
|
||||
"true",
|
||||
"--build-host",
|
||||
"",
|
||||
*machine.nix_options,
|
||||
"--flake",
|
||||
f"{path}#{machine.name}",
|
||||
]
|
||||
@ -143,7 +136,9 @@ def update(args: argparse.Namespace) -> None:
|
||||
raise ClanError("Could not find clan flake toplevel directory")
|
||||
machines = []
|
||||
if len(args.machines) == 1 and args.target_host is not None:
|
||||
machine = Machine(name=args.machines[0], flake=args.flake)
|
||||
machine = Machine(
|
||||
name=args.machines[0], flake=args.flake, nix_options=args.option
|
||||
)
|
||||
machine.target_host_address = args.target_host
|
||||
machines.append(machine)
|
||||
|
||||
@ -153,7 +148,7 @@ def update(args: argparse.Namespace) -> None:
|
||||
else:
|
||||
if len(args.machines) == 0:
|
||||
ignored_machines = []
|
||||
for machine in get_all_machines(args.flake):
|
||||
for machine in get_all_machines(args.flake, args.option):
|
||||
if machine.deployment_info.get("requireExplicitUpdate", False):
|
||||
continue
|
||||
try:
|
||||
@ -173,13 +168,13 @@ def update(args: argparse.Namespace) -> None:
|
||||
print(machine, file=sys.stderr)
|
||||
|
||||
else:
|
||||
machines = get_selected_machines(args.flake, args.machines)
|
||||
machines = get_selected_machines(args.flake, args.option, args.machines)
|
||||
|
||||
deploy_nixos(MachineGroup(machines))
|
||||
|
||||
|
||||
def register_update_parser(parser: argparse.ArgumentParser) -> None:
|
||||
parser.add_argument(
|
||||
machines_parser = parser.add_argument(
|
||||
"machines",
|
||||
type=str,
|
||||
nargs="*",
|
||||
@ -187,6 +182,9 @@ 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,
|
||||
|
@ -106,7 +106,7 @@ def nix_shell(packages: list[str], cmd: list[str]) -> list[str]:
|
||||
if os.environ.get("IN_NIX_SANDBOX"):
|
||||
return cmd
|
||||
return [
|
||||
*nix_command(["shell", "--offline", "--inputs-from", f"{nixpkgs_flake()!s}"]),
|
||||
*nix_command(["shell", "--inputs-from", f"{nixpkgs_flake()!s}"]),
|
||||
*packages,
|
||||
"-c",
|
||||
*cmd,
|
||||
|
@ -1,6 +1,7 @@
|
||||
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
|
||||
@ -147,25 +148,28 @@ def register_machines_parser(parser: argparse.ArgumentParser) -> None:
|
||||
|
||||
# Parser
|
||||
get_parser = subparser.add_parser("get", help="get a machine public key")
|
||||
get_parser.add_argument(
|
||||
get_machine_parser = 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_parser.add_argument(
|
||||
remove_machine_parser = 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"
|
||||
)
|
||||
add_secret_parser.add_argument(
|
||||
machine_add_secret_parser = 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
|
||||
)
|
||||
@ -175,9 +179,10 @@ 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"
|
||||
)
|
||||
remove_secret_parser.add_argument(
|
||||
"machine", help="the name of the group", type=machine_name_type
|
||||
machine_remove_parser = remove_secret_parser.add_argument(
|
||||
"machine", help="the name of the machine", 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
|
||||
)
|
||||
|
@ -9,6 +9,7 @@ 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 (
|
||||
@ -153,8 +154,12 @@ def remove_command(args: argparse.Namespace) -> None:
|
||||
remove_secret(Path(args.flake), args.secret)
|
||||
|
||||
|
||||
def add_secret_argument(parser: argparse.ArgumentParser) -> None:
|
||||
parser.add_argument("secret", help="the name of the secret", type=secret_name_type)
|
||||
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 machines_folder(flake_dir: Path, group: str) -> Path:
|
||||
@ -323,11 +328,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)
|
||||
add_secret_argument(parser_get, True)
|
||||
parser_get.set_defaults(func=get_command)
|
||||
|
||||
parser_set = subparser.add_parser("set", help="set a secret")
|
||||
add_secret_argument(parser_set)
|
||||
add_secret_argument(parser_set, False)
|
||||
parser_set.add_argument(
|
||||
"--group",
|
||||
type=str,
|
||||
@ -359,10 +364,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)
|
||||
add_secret_argument(parser_rename, True)
|
||||
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)
|
||||
add_secret_argument(parser_remove, True)
|
||||
parser_remove.set_defaults(func=remove_command)
|
||||
|
@ -29,6 +29,7 @@
|
||||
mypy,
|
||||
nixpkgs,
|
||||
clan-core-path,
|
||||
gitMinimal,
|
||||
}:
|
||||
let
|
||||
# Dependencies that are directly used in the project
|
||||
@ -83,7 +84,7 @@ let
|
||||
cp -r ${./.} $out
|
||||
chmod -R +w $out
|
||||
rm $out/clan_cli/config/jsonschema
|
||||
ln -s ${nixpkgs'} $out/clan_cli/nixpkgs
|
||||
ln -sf ${nixpkgs'} $out/clan_cli/nixpkgs
|
||||
cp -r ${../../lib/jsonschema} $out/clan_cli/config/jsonschema
|
||||
'';
|
||||
|
||||
@ -101,7 +102,7 @@ let
|
||||
outputs = _inputs: { };
|
||||
}
|
||||
EOF
|
||||
ln -s ${nixpkgs} $out/path
|
||||
ln -sf ${nixpkgs} $out/path
|
||||
nix flake update $out \
|
||||
--store ./. \
|
||||
--extra-experimental-features 'nix-command flakes'
|
||||
@ -113,7 +114,13 @@ python3.pkgs.buildPythonApplication {
|
||||
format = "pyproject";
|
||||
|
||||
# Arguments for the wrapper to unset LD_LIBRARY_PATH to avoid glibc version issues
|
||||
makeWrapperArgs = [ "--unset LD_LIBRARY_PATH" ];
|
||||
makeWrapperArgs = [
|
||||
"--unset LD_LIBRARY_PATH"
|
||||
"--suffix"
|
||||
"PATH"
|
||||
":"
|
||||
"${gitMinimal}/bin/git"
|
||||
];
|
||||
|
||||
# Build-time dependencies.
|
||||
nativeBuildInputs = [
|
||||
|
@ -22,7 +22,7 @@ class Option:
|
||||
md_li += indent_next(
|
||||
f"\n{self.description.strip()}" if self.description else ""
|
||||
)
|
||||
md_li += indent_next(f"\n{self.epilog.strip()}" if self.epilog else "")
|
||||
# md_li += indent_next(f"\n{self.epilog.strip()}" if self.epilog else "")
|
||||
|
||||
return md_li
|
||||
|
||||
@ -82,13 +82,54 @@ class Category:
|
||||
md_li += indent_all(
|
||||
f"{self.description.strip()}\n" if self.description else "", 4
|
||||
)
|
||||
md_li += "\n"
|
||||
md_li += indent_all(f"{self.epilog.strip()}\n" if self.epilog else "", 4)
|
||||
md_li += "\n"
|
||||
|
||||
return md_li
|
||||
|
||||
|
||||
def epilog_to_md(text: str) -> str:
|
||||
"""
|
||||
Convert the epilog to md
|
||||
"""
|
||||
after_examples = False
|
||||
md = ""
|
||||
# md += text
|
||||
for line in text.split("\n"):
|
||||
if line.strip() == "Examples:":
|
||||
after_examples = True
|
||||
md += "### Examples"
|
||||
md += "\n"
|
||||
else:
|
||||
if after_examples:
|
||||
if line.strip().startswith("$"):
|
||||
md += f"`{line}`"
|
||||
md += "\n"
|
||||
md += "\n"
|
||||
else:
|
||||
if contains_https_link(line):
|
||||
line = convert_to_markdown_link(line)
|
||||
md += line
|
||||
md += "\n"
|
||||
else:
|
||||
md += line
|
||||
md += "\n"
|
||||
return md
|
||||
|
||||
|
||||
import re
|
||||
|
||||
|
||||
def contains_https_link(line: str) -> bool:
|
||||
pattern = r"https://\S+"
|
||||
return re.search(pattern, line) is not None
|
||||
|
||||
|
||||
def convert_to_markdown_link(line: str) -> str:
|
||||
pattern = r"(https://\S+)"
|
||||
|
||||
# Replacement pattern to convert it to a Markdown link
|
||||
return re.sub(pattern, r"[\1](\1)", line)
|
||||
|
||||
|
||||
def indent_next(text: str, indent_size: int = 4) -> str:
|
||||
"""
|
||||
Indent all lines in a string except the first line.
|
||||
@ -136,7 +177,7 @@ def get_subcommands(
|
||||
continue
|
||||
|
||||
if isinstance(action, argparse._SubParsersAction):
|
||||
continue # Subparsers handled sperately
|
||||
continue # Subparsers handled separately
|
||||
|
||||
option_strings = ", ".join(action.option_strings)
|
||||
if option_strings:
|
||||
@ -178,7 +219,7 @@ def get_subcommands(
|
||||
Category(
|
||||
title=f"{parent} {name}",
|
||||
description=subparser.description,
|
||||
epilog=subparser.epilog,
|
||||
# epilog=subparser.epilog,
|
||||
level=level,
|
||||
options=_options,
|
||||
positionals=_positionals,
|
||||
@ -222,6 +263,7 @@ def collect_commands() -> list[Category]:
|
||||
options=_options,
|
||||
positionals=_positionals,
|
||||
subcommands=_subcommands,
|
||||
epilog=subparser.epilog,
|
||||
level=1,
|
||||
)
|
||||
)
|
||||
@ -280,6 +322,7 @@ def build_command_reference() -> None:
|
||||
markdown = files.get(folder / f"{filename}.md", "")
|
||||
|
||||
markdown += f"{'#'*(cmd.level)} {cmd.title.capitalize()}\n\n"
|
||||
|
||||
markdown += f"{cmd.description}\n\n" if cmd.description else ""
|
||||
|
||||
# usage: clan vms run [-h] machine
|
||||
@ -320,6 +363,8 @@ def build_command_reference() -> None:
|
||||
markdown += indent_all(commands_fmt)
|
||||
markdown += "\n"
|
||||
|
||||
markdown += f"{epilog_to_md(cmd.epilog)}\n\n" if cmd.epilog else ""
|
||||
|
||||
files[folder / f"{filename}.md"] = markdown
|
||||
|
||||
for fname, content in files.items():
|
||||
|
@ -11,10 +11,10 @@ def test_backups(
|
||||
|
||||
cli.run(
|
||||
[
|
||||
"--flake",
|
||||
str(test_flake_with_core.path),
|
||||
"backups",
|
||||
"list",
|
||||
"--flake",
|
||||
str(test_flake_with_core.path),
|
||||
"vm1",
|
||||
]
|
||||
)
|
||||
|
@ -1,7 +1,4 @@
|
||||
import json
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
from cli import Cli
|
||||
@ -14,46 +11,6 @@ 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,
|
||||
@ -62,25 +19,14 @@ 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",
|
||||
@ -88,7 +34,7 @@ def test_configure_machine(
|
||||
)
|
||||
|
||||
# read the output
|
||||
assert capsys.readouterr().out == "true\n"
|
||||
assert capsys.readouterr().out == "false\n"
|
||||
|
||||
|
||||
def test_walk_jsonschema_all_types() -> None:
|
||||
|
@ -17,10 +17,12 @@ def test_create_flake(
|
||||
capsys: pytest.CaptureFixture,
|
||||
temporary_home: Path,
|
||||
cli: Cli,
|
||||
clan_core: Path,
|
||||
) -> None:
|
||||
flake_dir = temporary_home / "test-flake"
|
||||
|
||||
cli.run(["flakes", "create", str(flake_dir)])
|
||||
url = f"{clan_core}#default"
|
||||
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"])
|
||||
@ -47,16 +49,34 @@ def test_create_flake(
|
||||
flake_outputs["nixosConfigurations"]["machine1"]
|
||||
except KeyError:
|
||||
pytest.fail("nixosConfigurations.machine1 not found in flake outputs")
|
||||
# configure machine1
|
||||
capsys.readouterr()
|
||||
cli.run(["config", "--machine", "machine1", "services.openssh.enable", ""])
|
||||
capsys.readouterr()
|
||||
cli.run(
|
||||
[
|
||||
"config",
|
||||
"--machine",
|
||||
"machine1",
|
||||
"services.openssh.enable",
|
||||
"true",
|
||||
]
|
||||
|
||||
|
||||
@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,
|
||||
)
|
||||
flake_outputs = json.loads(flake_show.stdout)
|
||||
try:
|
||||
flake_outputs["nixosConfigurations"]["machine1"]
|
||||
except KeyError:
|
||||
pytest.fail("nixosConfigurations.machine1 not found in flake outputs")
|
||||
|
@ -15,10 +15,10 @@ def test_flakes_inspect(
|
||||
cli = Cli()
|
||||
cli.run(
|
||||
[
|
||||
"--flake",
|
||||
str(test_flake_with_core.path),
|
||||
"flakes",
|
||||
"inspect",
|
||||
"--flake",
|
||||
str(test_flake_with_core.path),
|
||||
"--machine",
|
||||
"vm1",
|
||||
]
|
||||
|
@ -21,55 +21,55 @@ def test_import_sops(
|
||||
monkeypatch.setenv("SOPS_AGE_KEY", age_keys[1].privkey)
|
||||
cli.run(
|
||||
[
|
||||
"--flake",
|
||||
str(test_flake.path),
|
||||
"secrets",
|
||||
"machines",
|
||||
"add",
|
||||
"--flake",
|
||||
str(test_flake.path),
|
||||
"machine1",
|
||||
age_keys[0].pubkey,
|
||||
]
|
||||
)
|
||||
cli.run(
|
||||
[
|
||||
"--flake",
|
||||
str(test_flake.path),
|
||||
"secrets",
|
||||
"users",
|
||||
"add",
|
||||
"--flake",
|
||||
str(test_flake.path),
|
||||
"user1",
|
||||
age_keys[1].pubkey,
|
||||
]
|
||||
)
|
||||
cli.run(
|
||||
[
|
||||
"--flake",
|
||||
str(test_flake.path),
|
||||
"secrets",
|
||||
"users",
|
||||
"add",
|
||||
"--flake",
|
||||
str(test_flake.path),
|
||||
"user2",
|
||||
age_keys[2].pubkey,
|
||||
]
|
||||
)
|
||||
cli.run(
|
||||
[
|
||||
"--flake",
|
||||
str(test_flake.path),
|
||||
"secrets",
|
||||
"groups",
|
||||
"add-user",
|
||||
"--flake",
|
||||
str(test_flake.path),
|
||||
"group1",
|
||||
"user1",
|
||||
]
|
||||
)
|
||||
cli.run(
|
||||
[
|
||||
"--flake",
|
||||
str(test_flake.path),
|
||||
"secrets",
|
||||
"groups",
|
||||
"add-user",
|
||||
"--flake",
|
||||
str(test_flake.path),
|
||||
"group1",
|
||||
"user2",
|
||||
]
|
||||
@ -78,10 +78,10 @@ def test_import_sops(
|
||||
# To edit:
|
||||
# SOPS_AGE_KEY=AGE-SECRET-KEY-1U5ENXZQAY62NC78Y2WC0SEGRRMAEEKH79EYY5TH4GPFWJKEAY0USZ6X7YQ sops --age age14tva0txcrl0zes05x7gkx56qd6wd9q3nwecjac74xxzz4l47r44sv3fz62 ./data/secrets.yaml
|
||||
cmd = [
|
||||
"--flake",
|
||||
str(test_flake.path),
|
||||
"secrets",
|
||||
"import-sops",
|
||||
"--flake",
|
||||
str(test_flake.path),
|
||||
"--group",
|
||||
"group1",
|
||||
"--machine",
|
||||
@ -91,10 +91,10 @@ def test_import_sops(
|
||||
|
||||
cli.run(cmd)
|
||||
capsys.readouterr()
|
||||
cli.run(["--flake", str(test_flake.path), "secrets", "users", "list"])
|
||||
cli.run(["secrets", "users", "list", "--flake", str(test_flake.path)])
|
||||
users = sorted(capsys.readouterr().out.rstrip().split())
|
||||
assert users == ["user1", "user2"]
|
||||
|
||||
capsys.readouterr()
|
||||
cli.run(["--flake", str(test_flake.path), "secrets", "get", "secret-key"])
|
||||
cli.run(["secrets", "get", "--flake", str(test_flake.path), "secret-key"])
|
||||
assert capsys.readouterr().out == "secret-value"
|
||||
|
@ -9,11 +9,11 @@ def test_machine_subcommands(
|
||||
) -> None:
|
||||
cli = Cli()
|
||||
cli.run(
|
||||
["--flake", str(test_flake_with_core.path), "machines", "create", "machine1"]
|
||||
["machines", "create", "--flake", str(test_flake_with_core.path), "machine1"]
|
||||
)
|
||||
|
||||
capsys.readouterr()
|
||||
cli.run(["--flake", str(test_flake_with_core.path), "machines", "list"])
|
||||
cli.run(["machines", "list", "--flake", str(test_flake_with_core.path)])
|
||||
|
||||
out = capsys.readouterr()
|
||||
|
||||
@ -21,12 +21,19 @@ 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(
|
||||
["--flake", str(test_flake_with_core.path), "machines", "delete", "machine1"]
|
||||
["machines", "delete", "--flake", str(test_flake_with_core.path), "machine1"]
|
||||
)
|
||||
|
||||
capsys.readouterr()
|
||||
cli.run(["--flake", str(test_flake_with_core.path), "machines", "list"])
|
||||
cli.run(["machines", "list", "--flake", str(test_flake_with_core.path)])
|
||||
out = capsys.readouterr()
|
||||
|
||||
assert "machine1" not in out.out
|
||||
|
@ -27,11 +27,11 @@ def _test_identities(
|
||||
|
||||
cli.run(
|
||||
[
|
||||
"--flake",
|
||||
str(test_flake.path),
|
||||
"secrets",
|
||||
what,
|
||||
"add",
|
||||
"--flake",
|
||||
str(test_flake.path),
|
||||
"foo",
|
||||
age_keys[0].pubkey,
|
||||
]
|
||||
@ -41,11 +41,11 @@ def _test_identities(
|
||||
with pytest.raises(ClanError): # raises "foo already exists"
|
||||
cli.run(
|
||||
[
|
||||
"--flake",
|
||||
str(test_flake.path),
|
||||
"secrets",
|
||||
what,
|
||||
"add",
|
||||
"--flake",
|
||||
str(test_flake.path),
|
||||
"foo",
|
||||
age_keys[0].pubkey,
|
||||
]
|
||||
@ -54,11 +54,11 @@ def _test_identities(
|
||||
# rotate the key
|
||||
cli.run(
|
||||
[
|
||||
"--flake",
|
||||
str(test_flake.path),
|
||||
"secrets",
|
||||
what,
|
||||
"add",
|
||||
"--flake",
|
||||
str(test_flake.path),
|
||||
"-f",
|
||||
"foo",
|
||||
age_keys[1].privkey,
|
||||
@ -68,11 +68,11 @@ def _test_identities(
|
||||
capsys.readouterr() # empty the buffer
|
||||
cli.run(
|
||||
[
|
||||
"--flake",
|
||||
str(test_flake.path),
|
||||
"secrets",
|
||||
what,
|
||||
"get",
|
||||
"--flake",
|
||||
str(test_flake.path),
|
||||
"foo",
|
||||
]
|
||||
)
|
||||
@ -80,18 +80,18 @@ def _test_identities(
|
||||
assert age_keys[1].pubkey in out.out
|
||||
|
||||
capsys.readouterr() # empty the buffer
|
||||
cli.run(["--flake", str(test_flake.path), "secrets", what, "list"])
|
||||
cli.run(["secrets", what, "list", "--flake", str(test_flake.path)])
|
||||
out = capsys.readouterr() # empty the buffer
|
||||
assert "foo" in out.out
|
||||
|
||||
cli.run(["--flake", str(test_flake.path), "secrets", what, "remove", "foo"])
|
||||
cli.run(["secrets", what, "remove", "--flake", str(test_flake.path), "foo"])
|
||||
assert not (sops_folder / what / "foo" / "key.json").exists()
|
||||
|
||||
with pytest.raises(ClanError): # already removed
|
||||
cli.run(["--flake", str(test_flake.path), "secrets", what, "remove", "foo"])
|
||||
cli.run(["secrets", what, "remove", "--flake", str(test_flake.path), "foo"])
|
||||
|
||||
capsys.readouterr()
|
||||
cli.run(["--flake", str(test_flake.path), "secrets", what, "list"])
|
||||
cli.run(["secrets", what, "list", "--flake", str(test_flake.path)])
|
||||
out = capsys.readouterr()
|
||||
assert "foo" not in out.out
|
||||
|
||||
@ -113,17 +113,17 @@ def test_groups(
|
||||
) -> None:
|
||||
cli = Cli()
|
||||
capsys.readouterr() # empty the buffer
|
||||
cli.run(["--flake", str(test_flake.path), "secrets", "groups", "list"])
|
||||
cli.run(["secrets", "groups", "list", "--flake", str(test_flake.path)])
|
||||
assert capsys.readouterr().out == ""
|
||||
|
||||
with pytest.raises(ClanError): # machine does not exist yet
|
||||
cli.run(
|
||||
[
|
||||
"--flake",
|
||||
str(test_flake.path),
|
||||
"secrets",
|
||||
"groups",
|
||||
"add-machine",
|
||||
"--flake",
|
||||
str(test_flake.path),
|
||||
"group1",
|
||||
"machine1",
|
||||
]
|
||||
@ -131,33 +131,33 @@ def test_groups(
|
||||
with pytest.raises(ClanError): # user does not exist yet
|
||||
cli.run(
|
||||
[
|
||||
"--flake",
|
||||
str(test_flake.path),
|
||||
"secrets",
|
||||
"groups",
|
||||
"add-user",
|
||||
"--flake",
|
||||
str(test_flake.path),
|
||||
"groupb1",
|
||||
"user1",
|
||||
]
|
||||
)
|
||||
cli.run(
|
||||
[
|
||||
"--flake",
|
||||
str(test_flake.path),
|
||||
"secrets",
|
||||
"machines",
|
||||
"add",
|
||||
"--flake",
|
||||
str(test_flake.path),
|
||||
"machine1",
|
||||
age_keys[0].pubkey,
|
||||
]
|
||||
)
|
||||
cli.run(
|
||||
[
|
||||
"--flake",
|
||||
str(test_flake.path),
|
||||
"secrets",
|
||||
"groups",
|
||||
"add-machine",
|
||||
"--flake",
|
||||
str(test_flake.path),
|
||||
"group1",
|
||||
"machine1",
|
||||
]
|
||||
@ -166,11 +166,11 @@ def test_groups(
|
||||
# Should this fail?
|
||||
cli.run(
|
||||
[
|
||||
"--flake",
|
||||
str(test_flake.path),
|
||||
"secrets",
|
||||
"groups",
|
||||
"add-machine",
|
||||
"--flake",
|
||||
str(test_flake.path),
|
||||
"group1",
|
||||
"machine1",
|
||||
]
|
||||
@ -178,51 +178,51 @@ def test_groups(
|
||||
|
||||
cli.run(
|
||||
[
|
||||
"--flake",
|
||||
str(test_flake.path),
|
||||
"secrets",
|
||||
"users",
|
||||
"add",
|
||||
"--flake",
|
||||
str(test_flake.path),
|
||||
"user1",
|
||||
age_keys[0].pubkey,
|
||||
]
|
||||
)
|
||||
cli.run(
|
||||
[
|
||||
"--flake",
|
||||
str(test_flake.path),
|
||||
"secrets",
|
||||
"groups",
|
||||
"add-user",
|
||||
"--flake",
|
||||
str(test_flake.path),
|
||||
"group1",
|
||||
"user1",
|
||||
]
|
||||
)
|
||||
|
||||
capsys.readouterr() # empty the buffer
|
||||
cli.run(["--flake", str(test_flake.path), "secrets", "groups", "list"])
|
||||
cli.run(["secrets", "groups", "list", "--flake", str(test_flake.path)])
|
||||
out = capsys.readouterr().out
|
||||
assert "user1" in out
|
||||
assert "machine1" in out
|
||||
|
||||
cli.run(
|
||||
[
|
||||
"--flake",
|
||||
str(test_flake.path),
|
||||
"secrets",
|
||||
"groups",
|
||||
"remove-user",
|
||||
"--flake",
|
||||
str(test_flake.path),
|
||||
"group1",
|
||||
"user1",
|
||||
]
|
||||
)
|
||||
cli.run(
|
||||
[
|
||||
"--flake",
|
||||
str(test_flake.path),
|
||||
"secrets",
|
||||
"groups",
|
||||
"remove-machine",
|
||||
"--flake",
|
||||
str(test_flake.path),
|
||||
"group1",
|
||||
"machine1",
|
||||
]
|
||||
@ -251,90 +251,90 @@ def test_secrets(
|
||||
) -> None:
|
||||
cli = Cli()
|
||||
capsys.readouterr() # empty the buffer
|
||||
cli.run(["--flake", str(test_flake.path), "secrets", "list"])
|
||||
cli.run(["secrets", "list", "--flake", str(test_flake.path)])
|
||||
assert capsys.readouterr().out == ""
|
||||
|
||||
monkeypatch.setenv("SOPS_NIX_SECRET", "foo")
|
||||
monkeypatch.setenv("SOPS_AGE_KEY_FILE", str(test_flake.path / ".." / "age.key"))
|
||||
cli.run(["--flake", str(test_flake.path), "secrets", "key", "generate"])
|
||||
cli.run(["secrets", "key", "generate", "--flake", str(test_flake.path)])
|
||||
capsys.readouterr() # empty the buffer
|
||||
cli.run(["--flake", str(test_flake.path), "secrets", "key", "show"])
|
||||
cli.run(["secrets", "key", "show", "--flake", str(test_flake.path)])
|
||||
key = capsys.readouterr().out
|
||||
assert key.startswith("age1")
|
||||
cli.run(
|
||||
["--flake", str(test_flake.path), "secrets", "users", "add", "testuser", key]
|
||||
["secrets", "users", "add", "--flake", str(test_flake.path), "testuser", key]
|
||||
)
|
||||
|
||||
with pytest.raises(ClanError): # does not exist yet
|
||||
cli.run(["--flake", str(test_flake.path), "secrets", "get", "nonexisting"])
|
||||
cli.run(["--flake", str(test_flake.path), "secrets", "set", "initialkey"])
|
||||
cli.run(["secrets", "get", "--flake", str(test_flake.path), "nonexisting"])
|
||||
cli.run(["secrets", "set", "--flake", str(test_flake.path), "initialkey"])
|
||||
capsys.readouterr()
|
||||
cli.run(["--flake", str(test_flake.path), "secrets", "get", "initialkey"])
|
||||
cli.run(["secrets", "get", "--flake", str(test_flake.path), "initialkey"])
|
||||
assert capsys.readouterr().out == "foo"
|
||||
capsys.readouterr()
|
||||
cli.run(["--flake", str(test_flake.path), "secrets", "users", "list"])
|
||||
cli.run(["secrets", "users", "list", "--flake", str(test_flake.path)])
|
||||
users = capsys.readouterr().out.rstrip().split("\n")
|
||||
assert len(users) == 1, f"users: {users}"
|
||||
owner = users[0]
|
||||
|
||||
monkeypatch.setenv("EDITOR", "cat")
|
||||
cli.run(["--flake", str(test_flake.path), "secrets", "set", "--edit", "initialkey"])
|
||||
cli.run(["secrets", "set", "--edit", "--flake", str(test_flake.path), "initialkey"])
|
||||
monkeypatch.delenv("EDITOR")
|
||||
|
||||
cli.run(["--flake", str(test_flake.path), "secrets", "rename", "initialkey", "key"])
|
||||
cli.run(["secrets", "rename", "--flake", str(test_flake.path), "initialkey", "key"])
|
||||
|
||||
capsys.readouterr() # empty the buffer
|
||||
cli.run(["--flake", str(test_flake.path), "secrets", "list"])
|
||||
cli.run(["secrets", "list", "--flake", str(test_flake.path)])
|
||||
assert capsys.readouterr().out == "key\n"
|
||||
|
||||
capsys.readouterr() # empty the buffer
|
||||
cli.run(["--flake", str(test_flake.path), "secrets", "list", "nonexisting"])
|
||||
cli.run(["secrets", "list", "--flake", str(test_flake.path), "nonexisting"])
|
||||
assert capsys.readouterr().out == ""
|
||||
|
||||
capsys.readouterr() # empty the buffer
|
||||
cli.run(["--flake", str(test_flake.path), "secrets", "list", "key"])
|
||||
cli.run(["secrets", "list", "--flake", str(test_flake.path), "key"])
|
||||
assert capsys.readouterr().out == "key\n"
|
||||
|
||||
cli.run(
|
||||
[
|
||||
"--flake",
|
||||
str(test_flake.path),
|
||||
"secrets",
|
||||
"machines",
|
||||
"add",
|
||||
"--flake",
|
||||
str(test_flake.path),
|
||||
"machine1",
|
||||
age_keys[1].pubkey,
|
||||
]
|
||||
)
|
||||
cli.run(
|
||||
[
|
||||
"--flake",
|
||||
str(test_flake.path),
|
||||
"secrets",
|
||||
"machines",
|
||||
"add-secret",
|
||||
"--flake",
|
||||
str(test_flake.path),
|
||||
"machine1",
|
||||
"key",
|
||||
]
|
||||
)
|
||||
capsys.readouterr()
|
||||
cli.run(["--flake", str(test_flake.path), "secrets", "machines", "list"])
|
||||
cli.run(["secrets", "machines", "list", "--flake", str(test_flake.path)])
|
||||
assert capsys.readouterr().out == "machine1\n"
|
||||
|
||||
with use_key(age_keys[1].privkey, monkeypatch):
|
||||
capsys.readouterr()
|
||||
cli.run(["--flake", str(test_flake.path), "secrets", "get", "key"])
|
||||
cli.run(["secrets", "get", "--flake", str(test_flake.path), "key"])
|
||||
|
||||
assert capsys.readouterr().out == "foo"
|
||||
|
||||
# rotate machines key
|
||||
cli.run(
|
||||
[
|
||||
"--flake",
|
||||
str(test_flake.path),
|
||||
"secrets",
|
||||
"machines",
|
||||
"add",
|
||||
"--flake",
|
||||
str(test_flake.path),
|
||||
"-f",
|
||||
"machine1",
|
||||
age_keys[0].privkey,
|
||||
@ -344,17 +344,17 @@ def test_secrets(
|
||||
# should also rotate the encrypted secret
|
||||
with use_key(age_keys[0].privkey, monkeypatch):
|
||||
capsys.readouterr()
|
||||
cli.run(["--flake", str(test_flake.path), "secrets", "get", "key"])
|
||||
cli.run(["secrets", "get", "--flake", str(test_flake.path), "key"])
|
||||
|
||||
assert capsys.readouterr().out == "foo"
|
||||
|
||||
cli.run(
|
||||
[
|
||||
"--flake",
|
||||
str(test_flake.path),
|
||||
"secrets",
|
||||
"machines",
|
||||
"remove-secret",
|
||||
"--flake",
|
||||
str(test_flake.path),
|
||||
"machine1",
|
||||
"key",
|
||||
]
|
||||
@ -362,37 +362,37 @@ def test_secrets(
|
||||
|
||||
cli.run(
|
||||
[
|
||||
"--flake",
|
||||
str(test_flake.path),
|
||||
"secrets",
|
||||
"users",
|
||||
"add",
|
||||
"--flake",
|
||||
str(test_flake.path),
|
||||
"user1",
|
||||
age_keys[1].pubkey,
|
||||
]
|
||||
)
|
||||
cli.run(
|
||||
[
|
||||
"--flake",
|
||||
str(test_flake.path),
|
||||
"secrets",
|
||||
"users",
|
||||
"add-secret",
|
||||
"--flake",
|
||||
str(test_flake.path),
|
||||
"user1",
|
||||
"key",
|
||||
]
|
||||
)
|
||||
capsys.readouterr()
|
||||
with use_key(age_keys[1].privkey, monkeypatch):
|
||||
cli.run(["--flake", str(test_flake.path), "secrets", "get", "key"])
|
||||
cli.run(["secrets", "get", "--flake", str(test_flake.path), "key"])
|
||||
assert capsys.readouterr().out == "foo"
|
||||
cli.run(
|
||||
[
|
||||
"--flake",
|
||||
str(test_flake.path),
|
||||
"secrets",
|
||||
"users",
|
||||
"remove-secret",
|
||||
"--flake",
|
||||
str(test_flake.path),
|
||||
"user1",
|
||||
"key",
|
||||
]
|
||||
@ -401,44 +401,44 @@ def test_secrets(
|
||||
with pytest.raises(ClanError): # does not exist yet
|
||||
cli.run(
|
||||
[
|
||||
"--flake",
|
||||
str(test_flake.path),
|
||||
"secrets",
|
||||
"groups",
|
||||
"add-secret",
|
||||
"--flake",
|
||||
str(test_flake.path),
|
||||
"admin-group",
|
||||
"key",
|
||||
]
|
||||
)
|
||||
cli.run(
|
||||
[
|
||||
"--flake",
|
||||
str(test_flake.path),
|
||||
"secrets",
|
||||
"groups",
|
||||
"add-user",
|
||||
"--flake",
|
||||
str(test_flake.path),
|
||||
"admin-group",
|
||||
"user1",
|
||||
]
|
||||
)
|
||||
cli.run(
|
||||
[
|
||||
"--flake",
|
||||
str(test_flake.path),
|
||||
"secrets",
|
||||
"groups",
|
||||
"add-user",
|
||||
"--flake",
|
||||
str(test_flake.path),
|
||||
"admin-group",
|
||||
owner,
|
||||
]
|
||||
)
|
||||
cli.run(
|
||||
[
|
||||
"--flake",
|
||||
str(test_flake.path),
|
||||
"secrets",
|
||||
"groups",
|
||||
"add-secret",
|
||||
"--flake",
|
||||
str(test_flake.path),
|
||||
"admin-group",
|
||||
"key",
|
||||
]
|
||||
@ -447,10 +447,10 @@ def test_secrets(
|
||||
capsys.readouterr() # empty the buffer
|
||||
cli.run(
|
||||
[
|
||||
"--flake",
|
||||
str(test_flake.path),
|
||||
"secrets",
|
||||
"set",
|
||||
"--flake",
|
||||
str(test_flake.path),
|
||||
"--group",
|
||||
"admin-group",
|
||||
"key2",
|
||||
@ -459,28 +459,28 @@ def test_secrets(
|
||||
|
||||
with use_key(age_keys[1].privkey, monkeypatch):
|
||||
capsys.readouterr()
|
||||
cli.run(["--flake", str(test_flake.path), "secrets", "get", "key"])
|
||||
cli.run(["secrets", "get", "--flake", str(test_flake.path), "key"])
|
||||
assert capsys.readouterr().out == "foo"
|
||||
|
||||
# extend group will update secrets
|
||||
cli.run(
|
||||
[
|
||||
"--flake",
|
||||
str(test_flake.path),
|
||||
"secrets",
|
||||
"users",
|
||||
"add",
|
||||
"--flake",
|
||||
str(test_flake.path),
|
||||
"user2",
|
||||
age_keys[2].pubkey,
|
||||
]
|
||||
)
|
||||
cli.run(
|
||||
[
|
||||
"--flake",
|
||||
str(test_flake.path),
|
||||
"secrets",
|
||||
"groups",
|
||||
"add-user",
|
||||
"--flake",
|
||||
str(test_flake.path),
|
||||
"admin-group",
|
||||
"user2",
|
||||
]
|
||||
@ -488,16 +488,16 @@ def test_secrets(
|
||||
|
||||
with use_key(age_keys[2].privkey, monkeypatch): # user2
|
||||
capsys.readouterr()
|
||||
cli.run(["--flake", str(test_flake.path), "secrets", "get", "key"])
|
||||
cli.run(["secrets", "get", "--flake", str(test_flake.path), "key"])
|
||||
assert capsys.readouterr().out == "foo"
|
||||
|
||||
cli.run(
|
||||
[
|
||||
"--flake",
|
||||
str(test_flake.path),
|
||||
"secrets",
|
||||
"groups",
|
||||
"remove-user",
|
||||
"--flake",
|
||||
str(test_flake.path),
|
||||
"admin-group",
|
||||
"user2",
|
||||
]
|
||||
@ -505,24 +505,24 @@ def test_secrets(
|
||||
with pytest.raises(ClanError), use_key(age_keys[2].privkey, monkeypatch):
|
||||
# user2 is not in the group anymore
|
||||
capsys.readouterr()
|
||||
cli.run(["--flake", str(test_flake.path), "secrets", "get", "key"])
|
||||
cli.run(["secrets", "get", "--flake", str(test_flake.path), "key"])
|
||||
print(capsys.readouterr().out)
|
||||
|
||||
cli.run(
|
||||
[
|
||||
"--flake",
|
||||
str(test_flake.path),
|
||||
"secrets",
|
||||
"groups",
|
||||
"remove-secret",
|
||||
"--flake",
|
||||
str(test_flake.path),
|
||||
"admin-group",
|
||||
"key",
|
||||
]
|
||||
)
|
||||
|
||||
cli.run(["--flake", str(test_flake.path), "secrets", "remove", "key"])
|
||||
cli.run(["--flake", str(test_flake.path), "secrets", "remove", "key2"])
|
||||
cli.run(["secrets", "remove", "--flake", str(test_flake.path), "key"])
|
||||
cli.run(["secrets", "remove", "--flake", str(test_flake.path), "key2"])
|
||||
|
||||
capsys.readouterr() # empty the buffer
|
||||
cli.run(["--flake", str(test_flake.path), "secrets", "list"])
|
||||
cli.run(["secrets", "list", "--flake", str(test_flake.path)])
|
||||
assert capsys.readouterr().out == ""
|
||||
|
@ -24,27 +24,27 @@ def test_generate_secret(
|
||||
cli = Cli()
|
||||
cli.run(
|
||||
[
|
||||
"--flake",
|
||||
str(test_flake_with_core.path),
|
||||
"secrets",
|
||||
"users",
|
||||
"add",
|
||||
"--flake",
|
||||
str(test_flake_with_core.path),
|
||||
"user1",
|
||||
age_keys[0].pubkey,
|
||||
]
|
||||
)
|
||||
cli.run(
|
||||
[
|
||||
"--flake",
|
||||
str(test_flake_with_core.path),
|
||||
"secrets",
|
||||
"groups",
|
||||
"add-user",
|
||||
"--flake",
|
||||
str(test_flake_with_core.path),
|
||||
"admins",
|
||||
"user1",
|
||||
]
|
||||
)
|
||||
cmd = ["--flake", str(test_flake_with_core.path), "facts", "generate", "vm1"]
|
||||
cmd = ["facts", "generate", "--flake", str(test_flake_with_core.path), "vm1"]
|
||||
cli.run(cmd)
|
||||
has_secret(test_flake_with_core.path, "vm1-age.key")
|
||||
has_secret(test_flake_with_core.path, "vm1-zerotier-identity-secret")
|
||||
@ -60,7 +60,7 @@ def test_generate_secret(
|
||||
secret1_mtime = identity_secret.lstat().st_mtime_ns
|
||||
|
||||
# test idempotency for vm1 and also generate for vm2
|
||||
cli.run(["facts", "generate"])
|
||||
cli.run(["facts", "generate", "--flake", str(test_flake_with_core.path)])
|
||||
assert age_key.lstat().st_mtime_ns == age_key_mtime
|
||||
assert identity_secret.lstat().st_mtime_ns == secret1_mtime
|
||||
|
||||
|
@ -23,11 +23,11 @@ def test_secrets_upload(
|
||||
cli = Cli()
|
||||
cli.run(
|
||||
[
|
||||
"--flake",
|
||||
str(test_flake_with_core.path),
|
||||
"secrets",
|
||||
"users",
|
||||
"add",
|
||||
"--flake",
|
||||
str(test_flake_with_core.path),
|
||||
"user1",
|
||||
age_keys[0].pubkey,
|
||||
]
|
||||
@ -35,18 +35,18 @@ def test_secrets_upload(
|
||||
|
||||
cli.run(
|
||||
[
|
||||
"--flake",
|
||||
str(test_flake_with_core.path),
|
||||
"secrets",
|
||||
"machines",
|
||||
"add",
|
||||
"--flake",
|
||||
str(test_flake_with_core.path),
|
||||
"vm1",
|
||||
age_keys[1].pubkey,
|
||||
]
|
||||
)
|
||||
monkeypatch.setenv("SOPS_NIX_SECRET", age_keys[0].privkey)
|
||||
cli.run(
|
||||
["--flake", str(test_flake_with_core.path), "secrets", "set", "vm1-age.key"]
|
||||
["secrets", "set", "--flake", str(test_flake_with_core.path), "vm1-age.key"]
|
||||
)
|
||||
|
||||
flake = test_flake_with_core.path.joinpath("flake.nix")
|
||||
@ -55,7 +55,7 @@ def test_secrets_upload(
|
||||
new_text = flake.read_text().replace("__CLAN_TARGET_ADDRESS__", addr)
|
||||
|
||||
flake.write_text(new_text)
|
||||
cli.run(["--flake", str(test_flake_with_core.path), "facts", "upload", "vm1"])
|
||||
cli.run(["facts", "upload", "--flake", str(test_flake_with_core.path), "vm1"])
|
||||
|
||||
# the flake defines this path as the location where the sops key should be installed
|
||||
sops_key = test_flake_with_core.path.joinpath("key.txt")
|
||||
|
@ -86,7 +86,7 @@ def test_inspect(
|
||||
test_flake_with_core: FlakeForTest, capsys: pytest.CaptureFixture
|
||||
) -> None:
|
||||
cli = Cli()
|
||||
cli.run(["--flake", str(test_flake_with_core.path), "vms", "inspect", "vm1"])
|
||||
cli.run(["vms", "inspect", "--flake", str(test_flake_with_core.path), "vm1"])
|
||||
out = capsys.readouterr() # empty the buffer
|
||||
assert "Cores" in out.out
|
||||
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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()
|
||||
|
||||
##################################
|
||||
# #
|
||||
|
@ -46,6 +46,12 @@ 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
|
||||
|
@ -26,7 +26,7 @@ log = logging.getLogger(__name__)
|
||||
class MainWindow(Adw.ApplicationWindow):
|
||||
def __init__(self, config: ClanConfig) -> None:
|
||||
super().__init__()
|
||||
self.set_title("cLAN Manager")
|
||||
self.set_title("Clan Manager")
|
||||
self.set_default_size(980, 850)
|
||||
|
||||
overlay = ToastOverlay.use().overlay
|
||||
|
@ -27,7 +27,7 @@ let
|
||||
name = "org.clan.vm-manager";
|
||||
exec = "clan-vm-manager %u";
|
||||
icon = ./clan_vm_manager/assets/clan_white.png;
|
||||
desktopName = "cLAN Manager";
|
||||
desktopName = "Clan Manager";
|
||||
startupWMClass = "clan";
|
||||
mimeTypes = [ "x-scheme-handler/clan" ];
|
||||
};
|
||||
|
40
pkgs/editor/clan-edit-codium.nix
Normal file
40
pkgs/editor/clan-edit-codium.nix
Normal file
@ -0,0 +1,40 @@
|
||||
{
|
||||
lib,
|
||||
coreutils,
|
||||
nil,
|
||||
nixd,
|
||||
nixpkgs-fmt,
|
||||
direnv,
|
||||
vscode-extensions,
|
||||
vscode-with-extensions,
|
||||
vscodium,
|
||||
writeShellApplication,
|
||||
}:
|
||||
let
|
||||
codium = vscode-with-extensions.override {
|
||||
vscode = vscodium;
|
||||
vscodeExtensions = [
|
||||
vscode-extensions.jnoortheen.nix-ide
|
||||
vscode-extensions.mkhl.direnv
|
||||
];
|
||||
};
|
||||
in
|
||||
writeShellApplication {
|
||||
name = "clan-edit-codium";
|
||||
runtimeInputs = [
|
||||
coreutils
|
||||
nil
|
||||
nixd
|
||||
nixpkgs-fmt
|
||||
direnv
|
||||
];
|
||||
text = ''
|
||||
set -eux
|
||||
DATA_DIR="''${XDG_CACHE_HOME:-$HOME/.cache}/clan-edit-codium"
|
||||
SETTINGS="$DATA_DIR"/User/settings.json
|
||||
${coreutils}/bin/mkdir -p "$DATA_DIR/User"
|
||||
cat ${./settings.json} > "$SETTINGS"
|
||||
|
||||
exec ${lib.getExe codium} --user-data-dir "$DATA_DIR" "$@"
|
||||
'';
|
||||
}
|
4
pkgs/editor/default.nix
Normal file
4
pkgs/editor/default.nix
Normal file
@ -0,0 +1,4 @@
|
||||
{ pkgs }:
|
||||
{
|
||||
clan-edit-codium = pkgs.callPackage ./clan-edit-codium.nix;
|
||||
}
|
23
pkgs/editor/settings.json
Normal file
23
pkgs/editor/settings.json
Normal file
@ -0,0 +1,23 @@
|
||||
{
|
||||
"security.workspace.trust.enabled": false,
|
||||
"nix.enableLanguageServer": true,
|
||||
"nix.serverPath": "nixd",
|
||||
"nix.formatterPath": "nixpkgs-fmt",
|
||||
"nix.serverSettings": {
|
||||
"nixd": {
|
||||
"formatting": {
|
||||
"command": "nixpkgs-fmt"
|
||||
},
|
||||
"options": {
|
||||
"nixos": {
|
||||
"expr": "(let pkgs = import <nixpkgs> { }; in (pkgs.lib.evalModules { modules
|
||||
= (import <nixpkgs/nixos/modules/module-list.nix>) ++ [ ({...}: {
|
||||
nixpkgs.hostPlatform = builtins.currentSystem;} ) ] ; })).options"
|
||||
},
|
||||
"home-manager": {
|
||||
"expr": "(builtins.getFlake \"github:nix-community/home-manager\").homeConfigurations.<name>.options"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -7,6 +7,7 @@
|
||||
./installer/flake-module.nix
|
||||
./schemas/flake-module.nix
|
||||
./webview-ui/flake-module.nix
|
||||
./gui-installer/flake-module.nix
|
||||
];
|
||||
|
||||
perSystem =
|
||||
@ -25,9 +26,9 @@
|
||||
moonlight-sunshine-accept = pkgs.callPackage ./moonlight-sunshine-accept { };
|
||||
merge-after-ci = pkgs.callPackage ./merge-after-ci { inherit (config.packages) tea-create-pr; };
|
||||
pending-reviews = pkgs.callPackage ./pending-reviews { };
|
||||
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 // {
|
||||
|
33
pkgs/gui-installer/flake-module.nix
Normal file
33
pkgs/gui-installer/flake-module.nix
Normal file
@ -0,0 +1,33 @@
|
||||
{
|
||||
perSystem =
|
||||
{ pkgs, ... }:
|
||||
let
|
||||
nfpmConfig = pkgs.writeText "clan-nfpm-config.yaml" (
|
||||
|
||||
builtins.toJSON {
|
||||
name = "clan-gui-installer";
|
||||
contents = [
|
||||
{
|
||||
src = "${./gui-installer.sh}";
|
||||
dst = "/usr/bin/clan-vm-manager";
|
||||
}
|
||||
];
|
||||
}
|
||||
);
|
||||
installerFor =
|
||||
packager:
|
||||
pkgs.runCommand "gui-installer" { } ''
|
||||
mkdir build
|
||||
cd build
|
||||
${pkgs.nfpm}/bin/nfpm package --config ${nfpmConfig} --packager ${packager}
|
||||
mkdir $out
|
||||
mv * $out/
|
||||
'';
|
||||
in
|
||||
{
|
||||
packages.gui-installer-apk = installerFor "apk";
|
||||
packages.gui-installer-archlinux = installerFor "archlinux";
|
||||
packages.gui-installer-deb = installerFor "deb";
|
||||
packages.gui-installer-rpm = installerFor "rpm";
|
||||
};
|
||||
}
|
66
pkgs/gui-installer/gui-installer.sh
Executable file
66
pkgs/gui-installer/gui-installer.sh
Executable file
@ -0,0 +1,66 @@
|
||||
#! /bin/sh
|
||||
|
||||
# create temp dir and ensure it is always cleaned
|
||||
trap 'clean_temp_dir' EXIT
|
||||
temp_dir=$(mktemp -d)
|
||||
|
||||
clean_temp_dir() {
|
||||
rm -rf "$temp_dir"
|
||||
}
|
||||
|
||||
is_installed() {
|
||||
name=$1
|
||||
if [ -n "$(command -v "$name")" ]; then
|
||||
return 0
|
||||
else
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
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
|
||||
NIX_INSTALLER_DIAGNOSTIC_ENDPOINT="" sh "$temp_dir"/install_nix.sh install
|
||||
}
|
||||
|
||||
ask_then_install_nix() {
|
||||
echo "Clan requires Nix to be installed. Would you like to install it now? (y/n)"
|
||||
read -r response
|
||||
if [ "$response" = "y" ]; then
|
||||
install_nix
|
||||
else
|
||||
echo "Clan cannot run without Nix. Exiting."
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
ensure_nix_installed() {
|
||||
if ! is_installed nix; 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 -- "$@"
|
||||
}
|
||||
|
||||
main() {
|
||||
ensure_nix_installed
|
||||
start_clan_gui "$@"
|
||||
}
|
||||
|
||||
main "$@"
|
@ -98,9 +98,6 @@ 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
Normal file → Executable file
26
pkgs/merge-after-ci/merge-after-ci.py
Normal file → Executable file
@ -1,4 +1,5 @@
|
||||
import argparse
|
||||
import shlex
|
||||
import subprocess
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
@ -11,14 +12,17 @@ args = parser.parse_args()
|
||||
if not args.reviewers and not args.no_review:
|
||||
parser.error("either --reviewers or --no-review must be given")
|
||||
|
||||
subprocess.run(
|
||||
[
|
||||
"tea-create-pr",
|
||||
"origin",
|
||||
"main",
|
||||
"--assignees",
|
||||
",".join(["clan-bot", *args.reviewers]),
|
||||
*(["--labels", "needs-review"] if not args.no_review else []),
|
||||
*args.args,
|
||||
]
|
||||
)
|
||||
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)
|
||||
|
49
pkgs/tea-create-pr/script.sh
Normal file → Executable file
49
pkgs/tea-create-pr/script.sh
Normal file → Executable file
@ -1,17 +1,46 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
remoteName="${1:-origin}"
|
||||
targetBranch="${2:-main}"
|
||||
shift && shift
|
||||
remoteFork="${1:-origin}"
|
||||
remoteUpstream="${2:-upstream}"
|
||||
targetBranch="${3:-main}"
|
||||
shift && shift && shift
|
||||
TMPDIR="$(mktemp -d)"
|
||||
currentBranch="$(git rev-parse --abbrev-ref HEAD)"
|
||||
user="$(tea login list -o simple | cut -d" " -f4 | head -n1)"
|
||||
user_unparsed="$(tea whoami)"
|
||||
user="$(echo "$user_unparsed" | tr -d '\n' | cut -f4 -d' ')"
|
||||
tempRemoteBranch="$user-$currentBranch"
|
||||
root_dir=$(git rev-parse --show-toplevel)
|
||||
|
||||
nix fmt -- --fail-on-change
|
||||
|
||||
git log --reverse --pretty="format:%s%n%n%b%n%n" "$remoteName/$targetBranch..HEAD" > "$TMPDIR"/commit-msg
|
||||
# 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
|
||||
|
||||
|
||||
$EDITOR "$TMPDIR"/commit-msg
|
||||
|
||||
@ -23,11 +52,13 @@ rest=$(echo "$COMMIT_MSG" | tail -n+2)
|
||||
if [[ "$firstLine" == "$rest" ]]; then
|
||||
rest=""
|
||||
fi
|
||||
git push --force -u "$remoteName" HEAD:refs/heads/"$tempRemoteBranch"
|
||||
|
||||
git push --force -u "$remoteFork" HEAD:refs/heads/"$tempRemoteBranch"
|
||||
|
||||
|
||||
tea pr create \
|
||||
--repo "$repo" \
|
||||
--head "$user:$tempRemoteBranch" \
|
||||
--title "$firstLine" \
|
||||
--description "$rest" \
|
||||
--head "$tempRemoteBranch" \
|
||||
--base "$targetBranch" \
|
||||
"$@"
|
||||
|
@ -1,35 +0,0 @@
|
||||
{
|
||||
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
|
||||
]);
|
||||
})
|
@ -19,6 +19,7 @@ export default tseslint.config(
|
||||
"error",
|
||||
{
|
||||
callees: ["cx"],
|
||||
whitelist: ["material-icons"],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
@ -5,6 +5,9 @@ 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);
|
||||
@ -25,19 +28,48 @@ fs.readFile(manifestPath, { encoding: "utf8" }, (err, data) => {
|
||||
|
||||
// Add linked stylesheets
|
||||
assets.forEach((asset) => {
|
||||
asset.css.forEach((cssEntry) => {
|
||||
htmlContent += `\n <link rel="stylesheet" href="${cssEntry}">`;
|
||||
});
|
||||
// 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}">`;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
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>`;
|
||||
}
|
||||
});
|
||||
|
16
pkgs/webview-ui/app/mock.ts
Normal file
16
pkgs/webview-ui/app/mock.ts
Normal file
@ -0,0 +1,16 @@
|
||||
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 };
|
227
pkgs/webview-ui/app/package-lock.json
generated
227
pkgs/webview-ui/app/package-lock.json
generated
@ -9,10 +9,12 @@
|
||||
"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",
|
||||
@ -20,8 +22,10 @@
|
||||
"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",
|
||||
@ -1482,6 +1486,34 @@
|
||||
"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",
|
||||
@ -1990,6 +2022,12 @@
|
||||
"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",
|
||||
@ -2183,6 +2221,12 @@
|
||||
"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",
|
||||
@ -2623,6 +2667,19 @@
|
||||
"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",
|
||||
@ -2802,6 +2859,12 @@
|
||||
"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",
|
||||
@ -3197,6 +3260,53 @@
|
||||
"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",
|
||||
@ -3234,6 +3344,15 @@
|
||||
"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",
|
||||
@ -3314,6 +3433,18 @@
|
||||
"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",
|
||||
@ -3366,6 +3497,11 @@
|
||||
"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",
|
||||
@ -3551,6 +3687,15 @@
|
||||
"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",
|
||||
@ -3867,6 +4012,73 @@
|
||||
"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",
|
||||
@ -4289,6 +4501,12 @@
|
||||
"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",
|
||||
@ -5008,6 +5226,15 @@
|
||||
"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",
|
||||
|
@ -13,24 +13,28 @@
|
||||
"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",
|
||||
"eslint-plugin-tailwindcss": "^3.17.0"
|
||||
"vite-plugin-solid": "^2.8.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"material-icons": "^1.13.12",
|
||||
"solid-js": "^1.8.11"
|
||||
}
|
||||
}
|
||||
|
@ -1,59 +1,18 @@
|
||||
import { Match, Switch, createSignal, type Component } from "solid-js";
|
||||
import { createSignal, type Component } from "solid-js";
|
||||
import { CountProvider } from "./Config";
|
||||
// import { Nested } from "./nested";
|
||||
import { Layout } from "./layout/layout";
|
||||
import cx from "classnames";
|
||||
import { Nested } from "./nested";
|
||||
import { Route, Router } from "./Routes";
|
||||
|
||||
type Route = "home" | "machines";
|
||||
// Global state
|
||||
const [route, setRoute] = createSignal<Route>("machines");
|
||||
|
||||
export { route, setRoute };
|
||||
|
||||
const App: Component = () => {
|
||||
const [route, setRoute] = createSignal<Route>("home");
|
||||
return (
|
||||
<CountProvider>
|
||||
<Layout>
|
||||
<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>
|
||||
<Router route={route} />
|
||||
</Layout>
|
||||
</CountProvider>
|
||||
);
|
||||
|
@ -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
|
||||
|
32
pkgs/webview-ui/app/src/Routes.tsx
Normal file
32
pkgs/webview-ui/app/src/Routes.tsx
Normal file
@ -0,0 +1,32 @@
|
||||
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>
|
||||
);
|
||||
};
|
33
pkgs/webview-ui/app/src/Sidebar.tsx
Normal file
33
pkgs/webview-ui/app/src/Sidebar.tsx
Normal file
@ -0,0 +1,33 @@
|
||||
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>
|
||||
);
|
||||
};
|
@ -1,3 +1,5 @@
|
||||
@import 'material-icons/iconfont/filled.css';
|
||||
/* List of icons: https://marella.me/material-icons/demo/ */
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
@ -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,5 +14,26 @@ 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!);
|
||||
|
19
pkgs/webview-ui/app/src/layout/header.tsx
Normal file
19
pkgs/webview-ui/app/src/layout/header.tsx
Normal file
@ -0,0 +1,19 @@
|
||||
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>
|
||||
);
|
||||
};
|
@ -1,9 +1,35 @@
|
||||
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="grid grid-cols-8">{props.children}</div>;
|
||||
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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -1,39 +0,0 @@
|
||||
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>
|
||||
);
|
||||
};
|
13
pkgs/webview-ui/app/src/routes/colors/view.tsx
Normal file
13
pkgs/webview-ui/app/src/routes/colors/view.tsx
Normal file
@ -0,0 +1,13 @@
|
||||
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>
|
||||
);
|
||||
};
|
78
pkgs/webview-ui/app/src/routes/machines/view.tsx
Normal file
78
pkgs/webview-ui/app/src/routes/machines/view.tsx
Normal file
@ -0,0 +1,78 @@
|
||||
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>
|
||||
);
|
||||
};
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user