1
0
forked from clan/clan-core

Compare commits

...

43 Commits

Author SHA1 Message Date
0027c46313 Merge pull request 'zerotier-static-peers: use correct exclusion source' (#1548) from kenji/clan-core:modules/fix/static into main
Reviewed-on: clan/clan-core#1548
2024-06-03 21:24:29 +00:00
ca2001040b zerotier-static-peers: use correct exclusion source 2024-06-03 22:53:44 +02:00
d6725100ac Merge pull request 'zerotier-static-peers: add guard condition' (#1547) from kenji/clan-core:modules/add/zerotier-guard into main
Reviewed-on: clan/clan-core#1547
2024-06-03 20:47:03 +00:00
503ce29c84 zerotier-static-peers: add guard condition 2024-06-03 22:42:04 +02:00
87444cd2b8 Merge pull request 'clan: add dyncamic completions for secrets' (#1546) from kenji/clan-core:kenji-clan/secrets-dynamic/add-completion into main
Reviewed-on: clan/clan-core#1546
2024-06-03 19:55:12 +00:00
31eca9e8bc clan: add dyncamic completions for secrets 2024-06-03 21:47:14 +02:00
822afe08b5 Merge pull request 'clan: add dynamic machine completions to clan secrets subcommands' (#1545) from clan/secrets/add-completions into main
Reviewed-on: clan/clan-core#1545
2024-06-03 15:42:37 +00:00
cfb78b0edb clan: add dynamic machine completions to clan secrets subcommands 2024-06-03 17:32:33 +02:00
65fd7d3efe Merge pull request 'clan: add dynamic completion to clan machines show' (#1544) from kenji-clan/machine-show/add-commpletion into main
Reviewed-on: clan/clan-core#1544
2024-06-03 15:15:45 +00:00
e8241fb7c9 clan: add dynamic completion to clan machines show 2024-06-03 17:06:03 +02:00
259d51bdc8 Merge pull request 'clan.static-hosts: excludeHosts should be empty if topLevelDomain is defined.' (#1538) from mrvandalo/clan-core:feature/static-hosts-exclude-nothing-when-tld-is-given into main
Reviewed-on: clan/clan-core#1538
Reviewed-by: kenji <aks.kenji@protonmail.com>
2024-06-03 10:44:41 +00:00
f6fb52afbf clan.static-hosts: excludeHosts should be empty if topLevelDomain is defined. 2024-06-03 10:44:41 +00:00
8089b87bbb Merge pull request 'Revert "clan-cli: cmd.py uses pseudo terminal now. Remove tty.py. Refactor password_store.py to use cmd.py."' (#1543) from lassulus/clan-core:lassulus-HEAD into main 2024-06-03 10:30:50 +00:00
578162425d Revert "clan-cli: cmd.py uses pseudo terminal now. Remove tty.py. Refactor password_store.py to use cmd.py."
This reverts commit ba86b49952.
2024-06-03 12:25:20 +02:00
dbad63f155 Merge pull request 'clan_cli secrets_upload: fix permissions' (#1542) from lassulus/clan-core:lassulus-HEAD into main 2024-06-03 08:58:49 +00:00
da8a733899 clan_cli secrets_upload: fix permissions 2024-06-03 10:52:18 +02:00
8f58f1998d Merge pull request 'Automatic flake update - 2024-06-03T00:00+00:00' (#1540) from flake-update-2024-06-03 into main 2024-06-03 00:05:17 +00:00
Clan Merge Bot
c43fe5187f update flake lock - 2024-06-03T00:00+00:00
Flake lock file updates:

• Updated input 'disko':
    'github:nix-community/disko/10986091e47fb1180620b78438512b294b7e8f67' (2024-05-27)
  → 'github:nix-community/disko/0274af4c92531ebfba4a5bd493251a143bc51f3c' (2024-05-31)
• Updated input 'flake-parts':
    'github:hercules-ci/flake-parts/8dc45382d5206bd292f9c2768b8058a8fd8311d9' (2024-05-16)
  → 'github:hercules-ci/flake-parts/2a55567fcf15b1b1c7ed712a2c6fadaec7412ea8' (2024-06-01)
• Updated input 'nixos-images':
    'github:nix-community/nixos-images/2478833ef8cc6de3d9e331f53b6f3682e425f207' (2024-05-27)
  → 'github:nix-community/nixos-images/47bfb55316e105390dd761e0b6e8e0be09462b67' (2024-05-30)
• Updated input 'nixos-images/nixos-2311':
    'github:NixOS/nixpkgs/0c007b36981bdbd69ccf0c7df30a174e63660667' (2024-05-26)
  → 'github:NixOS/nixpkgs/64e468fd2652105710d86cd2ae3e65a5a6d58dec' (2024-05-29)
• Updated input 'nixpkgs':
    'github:NixOS/nixpkgs/4ae13643e7f2cd4bc6555fce074865d9d14e7c24' (2024-05-28)
  → 'github:NixOS/nixpkgs/6634a0509e9e81e980b129435fbbec518ab246d0' (2024-06-02)
• Updated input 'sops-nix':
    'github:Mic92/sops-nix/962797a8d7f15ed7033031731d0bb77244839960' (2024-05-26)
  → 'github:Mic92/sops-nix/ab2a43b0d21d1d37d4d5726a892f714eaeb4b075' (2024-06-02)
• Updated input 'treefmt-nix':
    'github:numtide/treefmt-nix/2fba33a182602b9d49f0b2440513e5ee091d838b' (2024-05-17)
  → 'github:numtide/treefmt-nix/3eb96ca1ae9edf792a8e0963cc92fddfa5a87706' (2024-06-01)
2024-06-03 00:00:22 +00:00
0993fe45f6 Merge pull request 'clan-cli: cmd.py uses pseudo terminal now. Remove tty.py. Refactor password_store.py to use cmd.py' (#1536) from Qubasa/clan-core:Qubasa-main into main 2024-06-02 14:56:41 +00:00
ba86b49952 clan-cli: cmd.py uses pseudo terminal now. Remove tty.py. Refactor password_store.py to use cmd.py. 2024-06-02 16:52:31 +02:00
0b34c340fc Merge pull request 'clan-cli: Refactor machines/update.py to cmd.run' (#1535) from Qubasa/clan-core:Qubasa-main into main 2024-06-02 08:04:18 +00:00
d513f66170 clan-cli: Refactor machines/update.py to cmd.run 2024-06-02 10:00:23 +02:00
320fb776ea Merge pull request 'clan-cli: Add input arg to cmd.run. Replace subprocess.run in password_store' (#1533) from Qubasa/clan-core:Qubasa-main into main 2024-06-02 07:57:18 +00:00
1a39957dbb clan-cli: Refactor subprocess.run to cmd.run. tea-create-pr: Fix missing fail-on-change for treefmt 2024-06-02 09:53:24 +02:00
b5abe4025a Merge pull request 'docs: Add meta tags for link preview and fix js loading issue.' (#1531) from Qubasa/clan-core:Qubasa-main into main 2024-06-01 20:23:30 +00:00
55f4dcc460 docs: Add meta tags for link preview and fix js loading issue. 2024-06-01 22:19:37 +02:00
ef4a83f739 Merge pull request 'clan-core: add clan meta for ui usage' (#1529) from hsjobeki-main into main
Reviewed-on: clan/clan-core#1529
2024-05-31 16:26:46 +00:00
133f2b705f clan-core: add template to impure tests 2024-05-31 16:26:46 +00:00
83fe58e003 clan-core: add clan meta for ui usage 2024-05-31 16:26:46 +00:00
481f926b17 Merge pull request 'split list machines into show machine command' (#1521) from machines-show into main 2024-05-31 15:00:03 +00:00
788eae432a split list machines into show machine command 2024-05-31 16:56:09 +02:00
b7936c4ed2 Merge pull request 'upgrade nix in development to latest' (#1528) from nix-latest into main 2024-05-31 14:41:21 +00:00
750c8df003 upgrade nix in development to latest
Better error messages!!
2024-05-31 16:37:07 +02:00
276c39aba4 Merge pull request 'Contributing.md: Fix incorrect formating.' (#1527) from Qubasa/clan-core:Qubasa-main into main 2024-05-31 14:02:18 +00:00
90e25eeb76 Contributing.md: Fix incorrect formating. 2024-05-31 15:58:29 +02:00
56676701ae Merge pull request 'clan: add dynamic completions for fact generation services' (#1525) from a-kenji-clan/complete-services into main 2024-05-31 13:25:15 +00:00
bcccf301f0 clan: add dynamic completions for fact generation services 2024-05-31 15:21:07 +02:00
e343ba5635 Merge pull request 'Contributing.md: Explain merge-after-ci for externals.' (#1524) from Qubasa/clan-core:Qubasa-main into main 2024-05-31 12:02:02 +00:00
66fe5ec4fd Contributing.md: Explain merge-after-ci for externals. 2024-05-31 13:58:13 +02:00
f2a884ec30 Merge pull request 'clan: add completion timeout as static' (#1523) from a-kenji-clan/completions into main 2024-05-31 11:10:52 +00:00
d31aa7cf88 clan: add completion timeout as static 2024-05-31 13:06:46 +02:00
9f19a8e605 Merge pull request 'clan: add dynamic completions' (#1522) from a-kenji-clan/cli/init-dynamic-completions into main 2024-05-31 11:00:50 +00:00
23ef39a2d9 clan: add dynamic completions
Add dynamic completion scaffolding to the clan `cli`.
Also add a dynamic completion mechanism for machines for commands that
have machines as their sole argument.

More intricate dynamic completions will be implemented in follow up
PR's.
2024-05-31 12:55:41 +02:00
42 changed files with 650 additions and 185 deletions

View File

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

View File

@ -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

View File

@ -3,7 +3,8 @@
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 {

View File

@ -20,7 +20,7 @@ let
if builtins.pathExists fullPath then builtins.readFile fullPath else null
) machines;
networkIds = lib.filter (machine: machine != null) networkIdsUnchecked;
networkId = 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' (

View File

@ -27,7 +27,8 @@
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

View File

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

View File

@ -16,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>"""

View File

@ -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
@ -105,7 +105,8 @@ theme:
- content.code.copy
- content.tabs.link
icon:
repo: fontawesome/brands/git
repo: fontawesome/brands/git-alt
custom_dir: overrides
palette:
# Palette toggle for light mode
@ -128,8 +129,6 @@ theme:
extra_css:
- static/extra.css
- static/asciinema-player/custom-theme.css
- static/asciinema-player/asciinema-player.css
extra:
social:

12
docs/overrides/main.html Normal file
View 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 %}

View File

@ -7,11 +7,11 @@
]
},
"locked": {
"lastModified": 1716773194,
"narHash": "sha256-rskkGmWlvYFb+CXedBiL8eWEuED0Es0XR4CkJ11RQKY=",
"lastModified": 1717177033,
"narHash": "sha256-G3CZJafCO8WDy3dyA2EhpUJEmzd5gMJ2IdItAg0Hijw=",
"owner": "nix-community",
"repo": "disko",
"rev": "10986091e47fb1180620b78438512b294b7e8f67",
"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": {
@ -57,11 +57,11 @@
},
"nixos-2311": {
"locked": {
"lastModified": 1716767563,
"narHash": "sha256-xaSLDTqKIU55HsCkDnzFKmPiJO2z1xAAvrhUlwlmT2M=",
"lastModified": 1717017538,
"narHash": "sha256-S5kltvDDfNQM3xx9XcvzKEOyN2qk8Sa+aSOLqZ+1Ujc=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "0c007b36981bdbd69ccf0c7df30a174e63660667",
"rev": "64e468fd2652105710d86cd2ae3e65a5a6d58dec",
"type": "github"
},
"original": {
@ -100,11 +100,11 @@
]
},
"locked": {
"lastModified": 1716786664,
"narHash": "sha256-iszhOLhxnv+TX/XM2gAX4LhTCoMzLuG51ObZq/eyDx8=",
"lastModified": 1717040312,
"narHash": "sha256-yI/en4IxuCEClIUpIs3QTyYCCtmSPLOhwLJclfNwdeg=",
"owner": "nix-community",
"repo": "nixos-images",
"rev": "2478833ef8cc6de3d9e331f53b6f3682e425f207",
"rev": "47bfb55316e105390dd761e0b6e8e0be09462b67",
"type": "github"
},
"original": {
@ -115,11 +115,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1716924492,
"narHash": "sha256-9/Ro5/MfI+PNMF8jzh7+gXDPUHeOzL1e/iw3p4z6Ttc=",
"lastModified": 1717298511,
"narHash": "sha256-9sXuJn/nL+9ImeYtlspTvjt83z1wIgU+9AwfNbnq+tI=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "4ae13643e7f2cd4bc6555fce074865d9d14e7c24",
"rev": "6634a0509e9e81e980b129435fbbec518ab246d0",
"type": "github"
},
"original": {
@ -148,11 +148,11 @@
"nixpkgs-stable": []
},
"locked": {
"lastModified": 1716692524,
"narHash": "sha256-sALodaA7Zkp/JD6ehgwc0UCBrSBfB4cX66uFGTsqeFU=",
"lastModified": 1717297459,
"narHash": "sha256-cZC2f68w5UrJ1f+2NWGV9Gx0dEYmxwomWN2B0lx0QRA=",
"owner": "Mic92",
"repo": "sops-nix",
"rev": "962797a8d7f15ed7033031731d0bb77244839960",
"rev": "ab2a43b0d21d1d37d4d5726a892f714eaeb4b075",
"type": "github"
},
"original": {
@ -168,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": {

View File

@ -24,10 +24,14 @@
};
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"

View File

@ -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
;
};
};

View File

@ -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 (

View File

@ -152,6 +152,7 @@ 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(

View File

@ -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
@ -40,8 +41,10 @@ def create_command(args: argparse.Namespace) -> None:
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)

View File

@ -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
@ -57,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)

View File

@ -56,7 +56,7 @@ def handle_output(process: subprocess.Popen, log: Log) -> tuple[str, str]:
sys.stderr.buffer.write(ret)
sys.stderr.flush()
stderr_buf += ret
return stdout_buf.decode("utf-8"), 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,7 @@ def run(
msg=error_msg,
)
if check and rc != 0:
if check and process.returncode != 0:
raise ClanCmdError(cmd_out)
return cmd_out

View 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]

View File

@ -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",

View File

@ -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
@ -216,19 +222,23 @@ def generate_command(args: argparse.Namespace) -> None:
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,

View File

@ -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)

View File

@ -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

View File

@ -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)

View File

@ -12,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
@ -173,11 +174,13 @@ def flash_command(args: argparse.Namespace) -> None:
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,

View File

@ -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
@ -62,6 +63,17 @@ Examples:
)
register_list_parser(list_parser)
show_parser = subparser.add_parser(
"show",
help="Show a machine",
epilog=(
"""
This subcommand shows the details of a machine managed by this clan like icon, description, etc
"""
),
)
register_show_parser(show_parser)
install_parser = subparser.add_parser(
"install",
help="Install a machine",

View File

@ -1,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)

View File

@ -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
@ -188,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,

View File

@ -1,5 +1,4 @@
import argparse
import dataclasses
import json
import logging
from pathlib import Path
@ -12,24 +11,15 @@ 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",
]
)
@ -37,27 +27,13 @@ def list_machines(flake_url: str | Path, debug: bool) -> dict[str, MachineInfo]:
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:

View 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)

View File

@ -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}"
)
@ -180,7 +174,7 @@ def update(args: argparse.Namespace) -> None:
def register_update_parser(parser: argparse.ArgumentParser) -> None:
parser.add_argument(
machines_parser = parser.add_argument(
"machines",
type=str,
nargs="*",
@ -188,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,

View File

@ -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
)

View File

@ -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)

View File

@ -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,3 +49,34 @@ def test_create_flake(
flake_outputs["nixosConfigurations"]["machine1"]
except KeyError:
pytest.fail("nixosConfigurations.machine1 not found in flake outputs")
@pytest.mark.impure
def test_ui_template(
monkeypatch: pytest.MonkeyPatch,
capsys: pytest.CaptureFixture,
temporary_home: Path,
cli: Cli,
clan_core: Path,
) -> None:
flake_dir = temporary_home / "test-flake"
url = f"{clan_core}#empty"
cli.run(["flakes", "create", str(flake_dir), f"--url={url}"])
assert (flake_dir / ".clan-flake").exists()
monkeypatch.chdir(flake_dir)
cli.run(["machines", "create", "machine1"])
capsys.readouterr() # flush cache
cli.run(["machines", "list"])
assert "machine1" in capsys.readouterr().out
flake_show = subprocess.run(
["nix", "flake", "show", "--json"],
check=True,
capture_output=True,
text=True,
)
flake_outputs = json.loads(flake_show.stdout)
try:
flake_outputs["nixosConfigurations"]["machine1"]
except KeyError:
pytest.fail("nixosConfigurations.machine1 not found in flake outputs")

View File

@ -21,6 +21,13 @@ def test_machine_subcommands(
assert "vm1" in out.out
assert "vm2" in out.out
capsys.readouterr()
cli.run(["machines", "show", "--flake", str(test_flake_with_core.path), "machine1"])
out = capsys.readouterr()
assert "machine1" in out.out
assert "Description" in out.out
print(out)
cli.run(
["machines", "delete", "--flake", str(test_flake_with_core.path), "machine1"]
)

View File

@ -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

View File

@ -30,7 +30,7 @@ if ! check_remote "$remoteUpstream"; then
exit 1
fi
treefmt -C "$root_dir"
treefmt --fail-on-change -C "$root_dir"
upstream_url=$(git remote get-url "$remoteUpstream")
set -x
@ -61,4 +61,4 @@ tea pr create \
--head "$user:$tempRemoteBranch" \
--title "$firstLine" \
--description "$rest" \
"$@"
"$@"

View File

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

View File

@ -5,8 +5,6 @@ import { route } from "@/src/App";
export const MachineListView: Component = () => {
const [{ machines, loading }, { getMachines }] = useCountContext();
const list = () => Object.values(machines());
createEffect(() => {
if (route() === "machines") getMachines();
});
@ -34,12 +32,12 @@ export const MachineListView: Component = () => {
</div>
</div>
</Match>
<Match when={!loading() && Object.entries(machines()).length === 0}>
<Match when={!loading() && machines().length === 0}>
No machines found
</Match>
<Match when={!loading()}>
<ul>
<For each={list()}>
<For each={machines()}>
{(entry) => (
<li>
<div class="card card-side m-2 bg-base-100 shadow-lg">
@ -50,7 +48,8 @@ export const MachineListView: Component = () => {
</figure>
<div class="card-body flex-row justify-between">
<div class="flex flex-col">
<h2 class="card-title">{entry.machine_name}</h2>
<h2 class="card-title">{entry}</h2>
{/*
<p
classList={{
"text-gray-400": !entry.machine_description,
@ -59,6 +58,7 @@ export const MachineListView: Component = () => {
>
{entry.machine_description || "No description"}
</p>
*/}
</div>
<div>
<button class="btn btn-ghost">

View File

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

View File

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

16
templates/empty/flake.nix Normal file
View File

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

View File

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