forked from clan/clan-core
Compare commits
43 Commits
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 |
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -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' (
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
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
|
||||
@ -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
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
flake.lock
42
flake.lock
@ -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": {
|
||||
|
@ -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"
|
||||
|
@ -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 (
|
||||
|
@ -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(
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
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]
|
@ -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
|
||||
|
||||
|
||||
@ -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,
|
||||
|
@ -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)
|
||||
|
@ -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,
|
||||
|
@ -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",
|
||||
|
@ -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
|
||||
@ -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,
|
||||
|
@ -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:
|
||||
|
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}"
|
||||
)
|
||||
|
||||
|
||||
@ -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,
|
||||
|
@ -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)
|
||||
|
@ -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")
|
||||
|
@ -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"]
|
||||
)
|
||||
|
@ -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
|
||||
|
@ -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" \
|
||||
"$@"
|
||||
"$@"
|
||||
|
@ -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
|
||||
|
@ -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">
|
||||
|
2
templates/empty/.clan-flake
Normal file
2
templates/empty/.clan-flake
Normal file
@ -0,0 +1,2 @@
|
||||
# DO NOT DELETE
|
||||
# This file is used by the clan cli to discover a clan flake
|
5
templates/empty/clan/meta.json
Normal file
5
templates/empty/clan/meta.json
Normal 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
16
templates/empty/flake.nix
Normal 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;
|
||||
};
|
||||
}
|
@ -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;
|
||||
};
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user