Compare commits

..

5 Commits

Author SHA1 Message Date
pinpox
998aaec269 wip 2025-07-22 17:39:50 +02:00
pinpox
77bb690b87 wip 2025-07-22 16:57:24 +02:00
pinpox
85e968c4f7 wip 2025-07-22 16:26:42 +02:00
pinpox
f84da1cf62 Add wait_for_file testing helper 2025-07-22 16:23:20 +02:00
pinpox
af8f4f00c2 add syncthing services 2025-07-22 16:00:01 +02:00
165 changed files with 1852 additions and 2894 deletions

View File

@@ -24,7 +24,7 @@ If you're new to Clan and eager to dive in, start with our quickstart guide and
In the Clan ecosystem, security is paramount. Learn how to handle secrets effectively:
- **Secrets Management**: Securely manage secrets by consulting [Vars](https://docs.clan.lol/guides/vars-backend/)<!-- [secrets.md](docs/site/guides/vars-backend.md) -->.
- **Secrets Management**: Securely manage secrets by consulting [secrets](https://docs.clan.lol/guides/getting-started/secrets/)<!-- [secrets.md](docs/site/guides/getting-started/secrets.md) -->.
### Contributing to Clan

View File

@@ -1,6 +0,0 @@
# Dont import this file
# It is only here for backwards compatibility.
# Dont author new modules with this file.
{
imports = [ ./roles/peer.nix ];
}

View File

@@ -1,6 +0,0 @@
{ ... }:
{
imports = [
../shared.nix
];
}

View File

@@ -1,21 +0,0 @@
{ config, lib, ... }:
let
instanceNames = builtins.attrNames config.clan.inventory.services.syncthing;
instanceName = builtins.head instanceNames;
instance = config.clan.inventory.services.syncthing.${instanceName};
introducer = builtins.head instance.roles.introducer.machines;
introducerId = "${config.clan.core.settings.directory}/vars/per-machine/${introducer}/syncthing/id/value";
in
{
imports = [
../shared.nix
];
clan.syncthing.introducer = lib.strings.removeSuffix "\n" (
if builtins.pathExists introducerId then
builtins.readFile introducerId
else
throw "${introducerId} does not exists. Please run `clan vars generate ${introducer}` to generate the introducer device id"
);
}

View File

@@ -1,11 +1,7 @@
---
description = "A secure, file synchronization app for devices over networks, offering a private alternative to cloud services."
features = [ "inventory" ]
[constraints]
roles.introducer.min = 1
roles.introducer.max = 1
---
**Warning**: This module was written with our VM integration in mind likely won't work outside of this context. They will be generalized in future.
## Usage
@@ -26,7 +22,7 @@ We recommend configuring this module as an sync-service through the provided opt
- **Share Folders**: Select folders to share with connected devices and configure permissions and synchronization parameters.
!!! info
Clan automatically discovers other devices. Automatic discovery requires one machine to be an [introducer](#clan.syncthing.introducer)
Clan automatically discovers other devices. Automatic discovery requires one machine to be an [introducer](#roles.introducer)
If that is not the case you can add the other device by its Device ID manually.
You can find and share Device IDs under the "Add Device" button in the Web GUI. (`127.0.0.1:8384`)
@@ -37,4 +33,4 @@ We recommend configuring this module as an sync-service through the provided opt
## Support
- **Documentation**: Extensive documentation is available on the [Syncthing website](https://docs.syncthing.net/).
- **Documentation**: Extensive documentation is available on the [Syncthing website](https://docs.syncthing.net/).

View File

@@ -0,0 +1,174 @@
{
# lib,
# config,
# pkgs,
...
}:
{
_class = "clan.service";
manifest.name = "clan-core/syncthing";
manifest.description = "A secure, file synchronization app for devices over networks";
roles.introducer = {
interface =
{ lib, ... }:
{
options = {
# TODO is this option even needed or used anywhere?
id = lib.mkOption {
description = ''
The ID of the machine.
It is generated automatically by default.
'';
type = lib.types.nullOr lib.types.str;
example = "BABNJY4-G2ICDLF-QQEG7DD-N3OBNGF-BCCOFK6-MV3K7QJ-2WUZHXS-7DTW4AS";
# default = config.clan.core.vars.generators.syncthing.files."id".value;
defaultText = "config.clan.core.vars.generators.syncthing.files.\"id\".value";
};
introducer = lib.mkOption {
description = ''
The introducer for the machine.
'';
type = lib.types.nullOr lib.types.str;
default = null;
};
autoAcceptDevices = lib.mkOption {
description = ''
Auto accept incoming device requests.
Should only be used on the introducer.
'';
type = lib.types.bool;
default = false;
};
autoShares = lib.mkOption {
description = ''
Auto share the following Folders by their ID's with introduced devices.
Should only be used on the introducer.
'';
type = lib.types.listOf lib.types.str;
default = [ ];
example = [
"folder1"
"folder2"
];
};
};
};
perInstance =
{
# instanceName,
roles,
settings,
...
}:
{
nixosModule =
{
...
}:
{
_module.args = {
inherit settings roles;
introducerID = null;
};
imports = [
./shared.nix
];
};
};
};
roles.peer = {
interface =
{ lib, ... }:
{
options = {
# TODO is this option even needed or used anywhere?
id = lib.mkOption {
description = ''
The ID of the machine.
It is generated automatically by default.
'';
type = lib.types.nullOr lib.types.str;
example = "BABNJY4-G2ICDLF-QQEG7DD-N3OBNGF-BCCOFK6-MV3K7QJ-2WUZHXS-7DTW4AS";
# default = config.clan.core.vars.generators.syncthing.files."id".value;
defaultText = "config.clan.core.vars.generators.syncthing.files.\"id\".value";
};
introducer = lib.mkOption {
description = ''
The introducer for the machine.
'';
type = lib.types.nullOr lib.types.str;
default = null;
};
autoAcceptDevices = lib.mkOption {
description = ''
Auto accept incoming device requests.
Should only be used on the introducer.
'';
type = lib.types.bool;
default = false;
};
autoShares = lib.mkOption {
description = ''
Auto share the following Folders by their ID's with introduced devices.
Should only be used on the introducer.
'';
type = lib.types.listOf lib.types.str;
default = [ ];
example = [
"folder1"
"folder2"
];
};
};
};
perInstance =
{
# instanceName,
roles,
settings,
...
}:
{
nixosModule =
{
lib,
config,
...
}:
{
_module.args =
let
introducer = builtins.head (lib.attrNames roles.introducer.machines);
introducerIDPath =
if settings.introducer == null then
"${config.clan.core.settings.directory}/vars/per-machine/${introducer}/syncthing/id/value"
else
"${config.clan.core.settings.directory}/vars/per-machine/${settings.introducer}/syncthing/id/value";
introducerID = lib.strings.removeSuffix "\n" (
if builtins.pathExists introducerIDPath then
builtins.readFile introducerIDPath
else
throw "${introducerIDPath} does not exists. Please run `clan vars generate ${introducer}` to generate the introducer device id"
);
in
{
inherit settings roles introducerID;
};
imports = [
./shared.nix
];
};
};
};
}

View File

@@ -0,0 +1,23 @@
{
self,
lib,
...
}:
let
module = lib.modules.importApply ./default.nix {
inherit (self) packages;
};
in
{
clan.modules.syncthing = module;
perSystem =
{ ... }:
{
clan.nixosTests.syncthing = {
imports = [ ./tests/vm/default.nix ];
clan.modules."@clan/syncthing" = module;
};
};
}

View File

@@ -2,49 +2,11 @@
config,
pkgs,
lib,
settings,
introducerID,
...
}:
{
options.clan.syncthing = {
id = lib.mkOption {
description = ''
The ID of the machine.
It is generated automatically by default.
'';
type = lib.types.nullOr lib.types.str;
example = "BABNJY4-G2ICDLF-QQEG7DD-N3OBNGF-BCCOFK6-MV3K7QJ-2WUZHXS-7DTW4AS";
default = config.clan.core.vars.generators.syncthing.files."id".value;
defaultText = "config.clan.core.vars.generators.syncthing.files.\"id\".value";
};
introducer = lib.mkOption {
description = ''
The introducer for the machine.
'';
type = lib.types.nullOr lib.types.str;
default = null;
};
autoAcceptDevices = lib.mkOption {
description = ''
Auto accept incoming device requests.
Should only be used on the introducer.
'';
type = lib.types.bool;
default = false;
};
autoShares = lib.mkOption {
description = ''
Auto share the following Folders by their ID's with introduced devices.
Should only be used on the introducer.
'';
type = lib.types.listOf lib.types.str;
default = [ ];
example = [
"folder1"
"folder2"
];
};
};
imports = [
{
# Syncthing ports: 8384 for remote access to GUI
@@ -65,7 +27,7 @@
{
assertion = lib.all (
attr: builtins.hasAttr attr config.services.syncthing.settings.folders
) config.clan.syncthing.autoShares;
) settings.autoShares;
message = ''
Syncthing: If you want to AutoShare a folder, you need to have it configured on the sharing device.
'';
@@ -80,12 +42,8 @@
services.syncthing = {
enable = true;
overrideFolders = lib.mkDefault (
if (config.clan.syncthing.introducer == null) then true else false
);
overrideDevices = lib.mkDefault (
if (config.clan.syncthing.introducer == null) then true else false
);
overrideFolders = lib.mkDefault (if (introducerID == null) then true else false);
overrideDevices = lib.mkDefault (if (introducerID == null) then true else false);
key = lib.mkDefault config.clan.core.vars.generators.syncthing.files."key".path or null;
cert = lib.mkDefault config.clan.core.vars.generators.syncthing.files."cert".path or null;
@@ -98,13 +56,13 @@
devices =
{ }
// (
if (config.clan.syncthing.introducer == null) then
if (introducerID == null) then
{ }
else
{
"${config.clan.syncthing.introducer}" = {
"${introducerID}" = {
name = "introducer";
id = config.clan.syncthing.introducer;
id = introducerID;
introducer = true;
autoAcceptFolders = true;
};
@@ -112,6 +70,7 @@
);
};
};
systemd.services.syncthing-auto-accept =
let
baseAddress = "127.0.0.1:8384";
@@ -120,7 +79,7 @@
SharedFolderById = "/rest/config/folders/";
apiKey = config.clan.core.vars.generators.syncthing.files."apikey".path;
in
lib.mkIf config.clan.syncthing.autoAcceptDevices {
lib.mkIf settings.autoAcceptDevices {
description = "Syncthing auto accept devices";
requisite = [ "syncthing.service" ];
after = [ "syncthing.service" ];
@@ -138,7 +97,7 @@
${lib.getExe pkgs.curl} -X POST -d "{\"deviceId\": $ID}" -H "Content-Type: application/json" -H "X-API-Key: $APIKEY" ${baseAddress}${postNewDevice}
# get all shared folders by their ID
for folder in ${builtins.toString config.clan.syncthing.autoShares}; do
for folder in ${builtins.toString settings.autoShares}; do
SHARED_IDS=$(${lib.getExe pkgs.curl} -X GET -H "X-API-Key: $APIKEY" ${baseAddress}${SharedFolderById}"$folder" | ${lib.getExe pkgs.jq} ."devices")
PATCHED_IDS=$(echo $SHARED_IDS | ${lib.getExe pkgs.jq} ".+= [{\"deviceID\": $ID, \"introducedBy\": \"\", \"encryptionPassword\": \"\"}]")
${lib.getExe pkgs.curl} -X PATCH -d "{\"devices\": $PATCHED_IDS}" -H "X-API-Key: $APIKEY" ${baseAddress}${SharedFolderById}"$folder"
@@ -147,7 +106,7 @@
'';
};
systemd.timers.syncthing-auto-accept = lib.mkIf config.clan.syncthing.autoAcceptDevices {
systemd.timers.syncthing-auto-accept = lib.mkIf settings.autoAcceptDevices {
description = "Syncthing Auto Accept";
wantedBy = [ "syncthing-auto-accept.service" ];
@@ -162,7 +121,7 @@
let
apiKey = config.clan.core.vars.generators.syncthing.files."apikey".path;
in
lib.mkIf config.clan.syncthing.autoAcceptDevices {
lib.mkIf settings.autoAcceptDevices {
description = "Set the api key";
after = [ "syncthing-init.service" ];
wantedBy = [ "multi-user.target" ];
@@ -182,7 +141,6 @@
};
clan.core.vars.generators.syncthing = {
migrateFact = "syncthing";
files."key".group = config.services.syncthing.group;
files."key".owner = config.services.syncthing.user;

View File

@@ -0,0 +1,99 @@
{
name = "syncthing";
clan = {
test.useContainers = false;
directory = ./.;
inventory = {
machines.introducer = { };
machines.peer1 = { };
machines.peer2 = { };
instances."test" = {
module.name = "@clan/syncthing";
module.input = "self";
roles.introducer.machines.introducer.settings = {
autoAcceptDevices = true;
autoShares = [ "Shared" ];
};
roles.peer.machines.peer1 = { };
roles.peer.machines.peer2 = { };
};
};
};
nodes = {
# peer2.console.keyMap = "colemak";
peer1.services.syncthing.openDefaultPorts = true;
peer2.services.syncthing.openDefaultPorts = true;
introducer = {
services.syncthing.openDefaultPorts = true;
# For faster Tests
systemd.timers.syncthing-auto-accept.timerConfig = {
OnActiveSec = 1;
OnUnitActiveSec = 1;
};
services.syncthing.settings.folders = {
"Shared" = {
enable = true;
path = "~/Shared";
versioning = {
type = "trashcan";
params = {
cleanoutDays = "30";
};
};
};
};
};
};
testScript = ''
start_all()
# import time
# time.sleep(500000)
introducer.wait_for_unit("syncthing.service")
peer1.wait_for_unit("syncthing.service")
peer2.wait_for_unit("syncthing.service")
# Check that syncthing web interface is accessible
introducer.wait_for_open_port(8384)
peer1.wait_for_open_port(8384)
peer2.wait_for_open_port(8384)
# Basic connectivity test
introducer.succeed("curl -s http://127.0.0.1:8384")
peer1.succeed("curl -s http://127.0.0.1:8384")
peer2.succeed("curl -s http://127.0.0.1:8384")
# Check that folders are created correctly
peer1.execute("ls -la /var/lib/syncthing")
peer2.execute("ls -la /var/lib/syncthing")
peer1.wait_for_file("/var/lib/syncthing/Shared")
peer2.wait_for_file("/var/lib/syncthing/Shared")
# Check file synchronisation from peer1 to peer2
introducer.shutdown()
peer1.execute("echo hello > /var/lib/syncthing/Shared/hello")
# peer2.wait_until_succeeds("timeout 5 cat /var/lib/syncthing/Shared")
peer2.wait_for_file("/var/lib/syncthing/Shared/hello")
out = peer2.succeed("cat /var/lib/syncthing/Shared/hello")
assert "hello" in out
'';
}

View File

@@ -0,0 +1,6 @@
[
{
"publickey": "age1t6s4d0sn3jlzlu7zxuux5fx8xw9xjfr0gqfaur0wessfrxsg35hsqttv2t",
"type": "age"
}
]

View File

@@ -0,0 +1,6 @@
[
{
"publickey": "age1mq8u3x9z8v3zdm59qslxyn33zm0rpjzrd9sr9fjzqwp8d66t9czq626xsd",
"type": "age"
}
]

View File

@@ -0,0 +1,6 @@
[
{
"publickey": "age18mp7v3ck9eg0vqy3y83z5hdhum9rc77ntwal42p30udejkzvky2qjluy4x",
"type": "age"
}
]

View File

@@ -0,0 +1,15 @@
{
"data": "ENC[AES256_GCM,data:JbpTTfHD92NlaUR7xAyJFoqD+4mYDlpE1gdWuCsrMyar8rUzS6vX7i7ymd69K0tPAT/UUZAmNacPFwvjTkZmdv+/719FNBkowrc=,iv:ZHTcm+V1dNZ07kRQEDNFYh8NMMwZ5g5cq0Tg281Aaec=,tag:tjAJRuQrRC0JYhS0tA+VUw==,type:str]",
"sops": {
"age": [
{
"recipient": "age1qm0p4vf9jvcnn43s6l4prk8zn6cx0ep9gzvevxecv729xz540v8qa742eg",
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBJZWV1ZUZlODZ2OXlXbzVX\nSTkzV0VFL1NJVXpzKy9hTy9NN3gzeWxJbzE4CnJZc0JYd2s3ci85aDFuQ3pJbmtT\nSWZGRmREM25nWjhkZ1hGNE0rcVpMbFEKLS0tIDk3aHZ6ZVlaTmhOV3B4b3g5MzV4\nUUNWcXdHcTlVVEw3UGlxQWtBSDdpMk0K6nCih/rHq4vLS/oDz8cbjY8TVVsQmzaW\nivSd3WhpUaRdigyw/u3/5Lmaii1awy2qJdyREbzzUVgJPfoZ87pabw==\n-----END AGE ENCRYPTED FILE-----\n"
}
],
"lastmodified": "2025-07-22T13:55:15Z",
"mac": "ENC[AES256_GCM,data:KoxJwNfRO1SDlgCc5p9+ZDP6rXOAUXG48ousVXKgNfR+qyS9i0FIYjgJxsSxzsYyn0Md7fbbJdX8MEnJZkgkTn0pJ46HfHsD4oiE66AF4pcgdIssTo4BX6RvoqbCdtS6hi6dpyrW7j1PPhwO3DRhaFIO58Nk1fxcVpyATzm8Gyg=,iv:SYotgqC8fA80mmjYZxUM0p+MUGxRYKHCd1pscS3HVt0=,tag:3XrQabwt+KEtk8JLZ4HTPA==,type:str]",
"unencrypted_suffix": "_unencrypted",
"version": "3.10.2"
}
}

View File

@@ -0,0 +1 @@
../../../users/admin

View File

@@ -0,0 +1,15 @@
{
"data": "ENC[AES256_GCM,data:Lr3bPR9MjNvwBYPIQg0D4qIDhRbD++ZOpuGvz093d+DWva1b4h1jhcsnmziOvINZQ3vVpizklkASRWo757FOJLLV1LiXNqiZAbY=,iv:HbsNN9Who9BFTHEUrRVAA5MAkadXVqTGEsq5kTPZdQo=,tag:pGW8oINEGRjDgg1JoHdUEQ==,type:str]",
"sops": {
"age": [
{
"recipient": "age1qm0p4vf9jvcnn43s6l4prk8zn6cx0ep9gzvevxecv729xz540v8qa742eg",
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBBM1Q5UEpvaDQ4K25oTWN3\nbmdPaTdZUTFFR0xUVEttajNXOSt1T3BnTjJnCkE2eUVleXJKckovdkw3ak1xUGZj\nK3hET0E0OU5KYlZLL2daZFNaZ3cyS00KLS0tIHFwMW9LTHNLR1JwaHdiMnc2bWhH\nMWtwMElUYW50ZXlGZG1US1hiWlNoRjAKTg62lhjMCG1uPtxAmq5L7QGwmlwvGnxG\n+qTZHAPAoTUaWtnJfJpueGB1OJbr4HbUH0gN0cBqq7Y0DGIyvGqidA==\n-----END AGE ENCRYPTED FILE-----\n"
}
],
"lastmodified": "2025-07-22T13:55:27Z",
"mac": "ENC[AES256_GCM,data:1fh6Jp+y6jGI3NgZMgGcuVHX3GLXYH9LKbPG4cVwOk1otX94zr0UcrVOSgH3m9J7QpGlFl48HwCfoNZzkRVmX633Px1UZQopOSZLao8Ao7ZcAwP3EmIowwJBC8//pYIhE6JwPlIlRbHOQRDd8HhIM1VwNjc8dUBBX0VyTAgyT4w=,iv:0Z722NhqETyVY+mkerERVw9TmKx0aASdSdYYdNucmCg=,tag:PT56bX50YajhlHQttz+Ffw==,type:str]",
"unencrypted_suffix": "_unencrypted",
"version": "3.10.2"
}
}

View File

@@ -0,0 +1 @@
../../../users/admin

View File

@@ -0,0 +1,15 @@
{
"data": "ENC[AES256_GCM,data:XgQnGe/dmXyir8gDOgcsdYok1d1blDBx+AFDLw+tXBzv5FY1pSbonSuOKmEVWDEmRCR3o1D8qiuUrDsa68D3als69A/bRhYHR+A=,iv:JzNB2VjE+HHAOQXkN3t95wQPiBBj/c93X5JHP8fosHg=,tag:fcAG53Tp+38NAfqSObIa4A==,type:str]",
"sops": {
"age": [
{
"recipient": "age1qm0p4vf9jvcnn43s6l4prk8zn6cx0ep9gzvevxecv729xz540v8qa742eg",
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB6amIrbk9uV1IyQ20zMExo\nL0RrMmxOa3dEOGNmZFhIWXF1Sjc1K3VtdkdFCklLdEVNYnpsclRUdUtyTmVteWhu\nMHpPQmYzSEVldytGazJ1azBnQWJsaEUKLS0tIEZFY0hMNnV1TE9jODdpdy80eXRx\nMDZWSGt1MnhYQWJoMmNoaW1KWGVsVk0Ki0VQLz79+QQeiOri0aBqHEsVessIyjX8\nv3OZAjwMglPNv3j4CIqY/F4sfrAYxKUNB7g0Ui56BZlrG/i68EupAg==\n-----END AGE ENCRYPTED FILE-----\n"
}
],
"lastmodified": "2025-07-22T13:55:39Z",
"mac": "ENC[AES256_GCM,data:081iGB26aJv0067lLJVetcKOyzzaHys6W70hcxB9010kpyU6AkxNt+HCa2lvJNAv+lls4WXgM6WsD/KODkDvbZsP4U3P9sJqY4RbTqJvypN4yjmzogneB7GVOenMQ8ywbm+ILM54nx8Enn6GPm4YX6yTat4WVTFFd+dJYmfBBmM=,iv:rXOqKtBSZvwozT49Zhp6aBpLXWlim7KLRGg6yIb2vkQ=,tag:vmjlvW0PNlHYs7syshJETg==,type:str]",
"unencrypted_suffix": "_unencrypted",
"version": "3.10.2"
}
}

View File

@@ -0,0 +1 @@
../../../users/admin

View File

@@ -0,0 +1,4 @@
{
"publickey": "age1qm0p4vf9jvcnn43s6l4prk8zn6cx0ep9gzvevxecv729xz540v8qa742eg",
"type": "age"
}

View File

@@ -0,0 +1 @@
../../../../../../sops/machines/introducer

View File

@@ -0,0 +1,19 @@
{
"data": "ENC[AES256_GCM,data:/dsV5uaNTBmB82lbzlQHGyRbFeXM9l32igrLcfGG13td,iv:6PB0LqjRtvrSYxeOPN+261VqacGg0jczCyyF4FZQa/I=,tag:4GlcYRhYhHVdSkwDSzcT6Q==,type:str]",
"sops": {
"age": [
{
"recipient": "age1qm0p4vf9jvcnn43s6l4prk8zn6cx0ep9gzvevxecv729xz540v8qa742eg",
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBKYmJIUVhVQ3c3ZnhweUhU\nZnR6THB1Y2RjWExPckIwNEl1SlRGeVJORUVVCkk1ZzZwbGNwc1JQOFRpN2MwT0xG\nZTErNzVmSnhPazJJVHBKUnl6d2lYaVkKLS0tIHJRclM2WEYzQTRwVXRHcUUxSTlt\na1pHamdpdm9hUU9PSndybzdlMWY3NkkKcTnYp/5fUTdiNr0ajJeAPuLwhgWAdlAE\ngZ4soLdZoBFUHSsh0LhEm6jO2DXEKUZY7oLZi1gSZZbPA7PI/5k7OA==\n-----END AGE ENCRYPTED FILE-----\n"
},
{
"recipient": "age1t6s4d0sn3jlzlu7zxuux5fx8xw9xjfr0gqfaur0wessfrxsg35hsqttv2t",
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB3THZibDFMaTZrKzJVWjd3\nd3RScjZVZmNlbitmc2FkRXl0UlVmZlRoM2dJCnlkUW1pa3dHZlNqTklxc3dZTkha\nQUpWck5BcEk4NjRjSXI3TlFzQ0pzVVUKLS0tIFpJdHJzKzVkeGFBcnRvR3pDeFN2\nNFNFaW9kY3JXdzk0Q3hPS29iOFRkK1UKoQSqFLIAeU6aRL+rDQ+oJ9PS6aAtAPeo\n5Kpwoi3KQHVrDDIRBaxvZ2BXObOyU0tBqjzBdrOgtRn+96HmnZ6JDw==\n-----END AGE ENCRYPTED FILE-----\n"
}
],
"lastmodified": "2025-07-22T13:55:15Z",
"mac": "ENC[AES256_GCM,data:4T1+t0hZwN0ThdXoCcNeUiQNj2RosRP6HL6SKmsnECn3pmDZqJEt/0xEQWDeDcvfHyZkJAFbU0RfhYsyz2wtYZwuMDL8WEbHURa1GQ4uKNWfUPPeu88eTwnYsbvtUS6TdRZ26eDoQKDNEed1cy9TfI4Bugdt0rrl6O+su2Ikvyo=,iv:aMLJ4r55UY5cIB1rQNns3jN2U/ZjfFN1vl6ZsXpcBbo=,tag:P2Dv7LeGB1fT4TYHwJgLGw==,type:str]",
"unencrypted_suffix": "_unencrypted",
"version": "3.10.2"
}
}

View File

@@ -0,0 +1 @@
../../../../../../sops/users/admin

View File

@@ -0,0 +1 @@
../../../../../../sops/machines/introducer

View File

@@ -0,0 +1,19 @@
{
"data": "ENC[AES256_GCM,data:6bI+k3BUKfpOnYSZx6ucCEKLvPCC2+LVrHzxQyN7MvLKk43LnKsfSSI8lpp5l8CE8Bpcpq9BqWVU3ZhnwmM4RbiHg/+C7R374KqoV0TLXMwq4lkeVM5oh4On2qBbnxsXtBz6NbJmIiJPPTT5YARdDAdIVUZ21jOj7G5jvm1BbAWojVkrKpb5IpOTmuxSA0CPEQRsvP9NHH+5Btm3J8Py4bHcRpgNeKaFfTrJjtXN2te8hTjJb8sGmFvuOr92Tdh/+O5qeA4SjMXqRJEiJiE+Fc9X9ukrxlZkDwsSn2gXQu7ahGhjOZCvQ/Th9j55/ZPiu5zrypfBIltSfGaMzHTqSECSGozfNSqbAAuu/ybCMmp87HDg9RQ3M/2yYqmSs2cRSB1/jN7ha3LTEnPhPKWnpOmnNvQT/rSX0wyfGtdKVk0bIgEc28wIKANyJC+HPPtENdcmdUhx6q2CRi1h+nFz92zkRGzWySgCcl51WawFDD6KRKLvqpq9LWv/bNl4NOQd307hPZguLLB6/Q/yV29j+XgGrZcmlOnn5NfAlVfF+F6ckebPxDKMg0Z3DD/83DPM18iO5SeDcaNVS3c3KoeSpURWMHr5+2n+qtu3wVvKsPPhAsBV8diwGxp5z00Rr84mHXPfnz56Tr+m/rYLfflryrybxTujcuWFCGgzwMdeaYx4tz7lQAY1XtdKSNTYjhWdDfFbqThP3HVGJsRQshCd+8Q4miEfJzDmUgbrUuoF3VjkMApZ4wULV5BCcLrDYIXkwu1lyDt5fk4G0TyZCP8JvBV3zcVxjAPW86tSHNcX0V56Yd090s51u0iJyM/eqNxoNXa/DWBuLd4jpWJRz2c48pUO1XRgqvZHFmUcO5q7SPL/bCvdpjqaopnOqt3501zGB2q+TGfyZUZ3sqtylTPz4C7tv/hrCzmRCTzEFlfsmUOi08DL/6K65ihe6uXjoH2fB7ypjUy4u5L1XcBIvxSitcSacosJHSUtWcFZ6mWIgVyChTjWRfCAF4eB3aVctW+OmVTeiTcdAfbDiPR5FNyDWPTMnnorjBVZkpU=,iv:RGNo6ipQplIsHL7avQpTgEKMDifKnB5W96vYf6X96cQ=,tag:k8oObQ1m3aVnG94MR7dhHw==,type:str]",
"sops": {
"age": [
{
"recipient": "age1qm0p4vf9jvcnn43s6l4prk8zn6cx0ep9gzvevxecv729xz540v8qa742eg",
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA5NFVRb3cvZEZXdW1xWlBP\nWWNCRjlxYmpPQ1k2aFBmOUR4M0F2ai9Iam5ZCkxzU2dUamZXRmRqV3ZyWTRvdUda\ncTlHa1BuSDQ0em9lMXRwUlc5V2E1WUUKLS0tIDRkamJVZ3NIVlJGRlNrK0EyT09x\nbHVsMzlKVzJGNS8vVVlHRlFJRmUvUlUK1OSBTQi6R3XYEZEQmpGCrMr8m4jCBRiV\nj/G9sCahhTUc0D7E7DTXD3fwfXnZk1bD7buA99f908DT2Bjv4TfeKQ==\n-----END AGE ENCRYPTED FILE-----\n"
},
{
"recipient": "age1t6s4d0sn3jlzlu7zxuux5fx8xw9xjfr0gqfaur0wessfrxsg35hsqttv2t",
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBwZkJFbWhINkxmK3g4RHFN\neko4VTZHVGZOK1E2dFVqbWdVZktnTEdqT1JJCnBraG1acHVrNzlBWmJhNEMya1Fi\ndUw0NDdKakdLcGE1Z0tsSHVvQ0l1NFkKLS0tIDZvek42eXVEbEdWeG1vSENnakQ4\nQzlGaUNKMVhEM2QwY3ZPTW5nYXNLUkUK2laHy33U+hcQMT4jlUOqtVRCy+hNHyaS\nyuSk1i7Am3VeProaXccREjHjYRHn/l/B1oLRQQQT4cLcComxOArz0w==\n-----END AGE ENCRYPTED FILE-----\n"
}
],
"lastmodified": "2025-07-22T13:55:16Z",
"mac": "ENC[AES256_GCM,data:CVyuCoiArDYnoz/GQ0OZ2K0rJ1+Y1xoznp+v4rxAfL22fv3mY29EDw7ByYXxye0ARCD8gBFCpKeUvWbfOPJGfjsfAFT0AxLYFbUONgejYZpVZbnlElfLUSi39ZhaSwcImqe4RLJ2TND/HuJ6jwz0Lb1h8BWOq5/NeF8JJH2tHFo=,iv:n2liiufvBjCjASBIAmOu2Q5IONsX99GFmZw7YDeHJ+4=,tag:0bYMs8k362w7jApCird4yA==,type:str]",
"unencrypted_suffix": "_unencrypted",
"version": "3.10.2"
}
}

View File

@@ -0,0 +1 @@
../../../../../../sops/users/admin

View File

@@ -0,0 +1 @@
UHILKRC-BZSMZT7-CA5ANWX-DJTR55R-NBGUBK4-SOZJG5Z-MONIHKP-ZHFGOAL

View File

@@ -0,0 +1 @@
../../../../../../sops/machines/introducer

View File

@@ -0,0 +1,19 @@
{
"data": "ENC[AES256_GCM,data:YOzFYRjbmtSqzz+gaimswru/Xl3h2bZ0zkkM+QokycDIGhk5wrdlrGNJiaiQk3xJ1JUEbJewmsBFPz0Xcl8Nh2aAj58s0fMg/H0lb6WfF0n7B4u3TZ6urWir4ViUPdVxcS7oxKhJCU2KoqFNpXS/NjRCmi4G12j42VGBStLNxCFZ8EH4oK/nV0tYpJ++meKA9IlQbuEKmAv6Xt7Ry1isW4THl7jzYwE6y7I1ThqudnDtvqguY+Nl27vMmFed+WmpE4jsXjeW7T/CTynafen81uL7gP6xiy/n+3nZNYgutHCTqa1AonITDIJrzRjAPMmYxwv0k1ebYImBCT0wH7rnmbLF7y4EMNFuA3O5nAvpZ8tkkLzooVf0PCYrtWbwsxOm,iv:T6aRMEHnOezxswMnJXBXoHVkVDFtDqvtCRhqvQHLOWU=,tag:2zTGHZWCt6SQrESyC3f9Rg==,type:str]",
"sops": {
"age": [
{
"recipient": "age1qm0p4vf9jvcnn43s6l4prk8zn6cx0ep9gzvevxecv729xz540v8qa742eg",
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBSK2pOU0Zoam52V00vdnFi\nR2hpenpHQ1BDaHlyODZ0cmgvdzlLMTZ6em5VCi9sdG9zdzY0Z1pDQVF3dnNSREdt\nQVVUQ29uaE03SWM3bnA3blRHZWJmUmMKLS0tIGwyNUhVMTJ5SkxudlZaVUJaM0xJ\nSEpoaFZaUlczMkYxWlZhb1RBbUZ3SFUKGmjG+r998QLDJylznGhuqa6magj9x9PM\nly7PlqaoZ1diLuFklqFVExK3cXwvA6xdOScZqd8/P/sEdzAodDuKQw==\n-----END AGE ENCRYPTED FILE-----\n"
},
{
"recipient": "age1t6s4d0sn3jlzlu7zxuux5fx8xw9xjfr0gqfaur0wessfrxsg35hsqttv2t",
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBnMlpRK043Rm0zbTJxUGlv\nbE5mOUhhZDQ3a2UrNVhHZithUTJMRlo2Q2hZCjQ2aUt6VXpuOVE4dVVyVFc0WjBm\nMWI0SHVqYUNjdkQvSXY5QWdFTDRjdGcKLS0tIDBleURzYVBlZVcwQk85YzhOWE1z\nd2ZwYVRMM0dxM3U0RWI0Z2kxSjlETWsK+/m7xmcoXlnfYkRL3RK1VATGY6RtkmHA\nfg+YeAENLT1Mr9SnJCWcodxFicz8hiN5PinjynjqWgfv/xLMuh2G9Q==\n-----END AGE ENCRYPTED FILE-----\n"
}
],
"lastmodified": "2025-07-22T13:55:16Z",
"mac": "ENC[AES256_GCM,data:dKjYJ4ogUztSwuP6gkyXj/gYd3TDztuwivhtRzLYwoJLJ7c4anEeZDA51tslnhdDDXQ101JDEt0tD05wAo1YdLTNg9YR0eAh4wZ/dkJ+U92U7DYEW40YXxy8co/WWW50eYqbD6SnZbZAMjf4SbaH6kqtS2MTqeba2B1G/y8sb94=,iv:Z8tzsvSTRjondcN0Vnc3bcyVlVnpqLYRoaOCe6dLQGQ=,tag:leifNnUovWvO5cqzyN2PNw==,type:str]",
"unencrypted_suffix": "_unencrypted",
"version": "3.10.2"
}
}

View File

@@ -0,0 +1 @@
../../../../../../sops/users/admin

View File

@@ -0,0 +1 @@
../../../../../../sops/machines/peer1

View File

@@ -0,0 +1,19 @@
{
"data": "ENC[AES256_GCM,data:W48JccasfJGc+WJOgGJf+T5OfVYSAfvghuXC7wUqVkkI,iv:Ukhb8+d1FrZPSmerFZUbuL/G2yHpBNSJeRL4nYG2Cm8=,tag:FCf3VI+eBsmrXvbsI62B7w==,type:str]",
"sops": {
"age": [
{
"recipient": "age1mq8u3x9z8v3zdm59qslxyn33zm0rpjzrd9sr9fjzqwp8d66t9czq626xsd",
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBTbkFSTTBYVlNjVjNyYmIx\nSjl3UUp5VVlJZ2xudW8veS94VUVwbS94NHkwCnZ3LzZocFBnYkN3K0wwelhYalM5\nOVRmN3g1Tk5rRTViZXkwT244b1I3bzgKLS0tIEpKdCtiSTdjREV4OVZDbU1rdDk1\nTnZZTEk5OEZlWFRFTTVITkRCTFVFQXMKV8SdppHqwCEIyRTNUxjG7AtGZyVZuKBr\ndXURED8uLw37i2gvAzlUZLzQieV+F//muVF4fFONucBq5wnRskyBTA==\n-----END AGE ENCRYPTED FILE-----\n"
},
{
"recipient": "age1qm0p4vf9jvcnn43s6l4prk8zn6cx0ep9gzvevxecv729xz540v8qa742eg",
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBWb1p5WEM5d3h1RVRpRVVk\nMXArMk9DOTNoYm9yT3pXbWV1YmN4MDFXd2hvClJpUEwwM0pDSUtqZHRvaTI1RlNF\nTkRBYlo4R1lpR0Y1YnRLWGIxNThFMFUKLS0tIGIzeTZ1QVJTZ3VlbjgwN1Z2Y3Bw\nTWxOT2xtVmR0Zm1jeW8wdE1xTEQ0aDAKkMuwZwMPUPO8kGmc7xCElxvpTFfGqag4\nXm6KaoyGNJO5OoWoXgJawynRNyJX2JFGjcI1M1xK4ItYU5NF4Po8ig==\n-----END AGE ENCRYPTED FILE-----\n"
}
],
"lastmodified": "2025-07-22T13:55:27Z",
"mac": "ENC[AES256_GCM,data:uOaOVlLb+qLshDC4Y1KxYFgsPpHmNlfDaaPHcBBvNs7brBfsfZOPwVXqPxj5M9IzhD87l7kbYisaeBCqDKPM+IdZV1C3WstRu70a26onLl+JMXYpJSUljQwZKWnL7e48o4MZqcOtY0ghcAajI2if7nnGdKXIF5djdSLwSWHx9e4=,iv:Alb7j8UVmfLHmMzwBpJ9Aor+lffm3P3DAJaipvX6LH4=,tag:L4pTMxjA6dRhK6nl2Kokhw==,type:str]",
"unencrypted_suffix": "_unencrypted",
"version": "3.10.2"
}
}

View File

@@ -0,0 +1 @@
../../../../../../sops/users/admin

View File

@@ -0,0 +1 @@
../../../../../../sops/machines/peer1

View File

@@ -0,0 +1,19 @@
{
"data": "ENC[AES256_GCM,data:n5rS499IeNmb7or1zApWyDa74HcFhCGv35zn1DsYED8lWGEnmSFMbOUFlKwa3cBWiGRyQ2UjGzdb7lmDmoNLNzd7JB4ARgI5DapVtj+ymn56VIK5GTBOsRzJgtT4lqh3vEu4Spuv290aBHHg+lsPfdTfsJuOgAr4PVgnNY3O6Exz6EZwOSSvbUX5if7G96Y1bemF2Hh1AbdOg5KvB/Uh6mBW17hnUnbqxT7O7fepYmsSLB/qjBJWVivoMtMQWoQL8IkOapmuQYDf8ig+2FMxzcAQ71wTpTlXLGJ/XAuFSx1tJzpLbFCOpvLtGnemoUIB8viBjgfyTqN5iboJvk/WX110609yx0ew/F45CfpBfjQ66iI1shiCidfPm3ZHCO7KSjXzXEmjX5dVFp3xpz58yJH8LwQtrIQB1OQsLKe2ZmrYfjydoDuN9N+A0a6qN6KJUvIGVxNKnQ1rjj8uq4toVhxsSLQrfNkTFPA1Q624rCOu04oz3wd/oPS28war85atfMjmz9JU+Hw5u5qaxgl7UJno1VYknsrrYDIAGPxUwO56t9kHIgOZpNuD6JqDBJAfGJ1oPT+zVIBcNwNg0Z577g0l9rCLNdKInVCVahxZLCVe3LidtxL/bNzvSYHb09RbkJIhdWn7ouYCHwRRWN5kWbUopm2hHVZjRhC2CGJLBZ3+aLkEJzyZGcQCLF6Jc+/659FD8dXdaqfbR/vEKaVhoLL/WGKeo/lZL6c7TaVwDSIIa7dkk/QvGvmWhO+qG8yIs4vciGa06I62P1Y+9DsMrrWu/yqLTSkWtOpKdvIBh/jPOC4t56JEb/ksIvDLKSdrtrtHyRdCkzZ95XQDAM/a2nv+LnH75Yz9q/R0Caex/OSATCFpNcoyeSJwC5sSs6nV/NJ18YynzclSaKmAvMM8XVx8jrtUEp1yIzgBogxTFJ+yTsvEa6y/yCF3i80RrJNIkOKJDR48g7R/RJTufR+YxQj+RjWpgHbkc/Uhb0cR3X5A0ptA2X6Qz+cW9NXr5dA3A4lVsFzkZLzulqPhCxT9+g8OINx2+ayNx/M=,iv:xy7UNpO48flvQE+otJfHUJLhZlOuUueXLP03cDigXFQ=,tag:HeWayOjoZmVa0dr8GPN7Iw==,type:str]",
"sops": {
"age": [
{
"recipient": "age1mq8u3x9z8v3zdm59qslxyn33zm0rpjzrd9sr9fjzqwp8d66t9czq626xsd",
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBITjNQcHMweFY1ekNkdWFs\nak1wcm5td29BcTI1OFBtMnFXcTg4SEJlUTFZCjc1YlovcSsxOUNFWGIxZWVsRU1L\neS9wNE1rWU05WDdSZEQzbEZib2Yxa3cKLS0tICt4TjVTOG1JUENSNW8xNVlkZHkx\nem1EMEI3Skx5R3BmLzVRYjBkK1BRODQKfX1nexjFXyXxGBvDRlssjw3XcwvtGxGC\narZ2/kmF2nETLoNDVcNIxV5KUPohhTwWdlrmPGksquBrUCjElMOVLw==\n-----END AGE ENCRYPTED FILE-----\n"
},
{
"recipient": "age1qm0p4vf9jvcnn43s6l4prk8zn6cx0ep9gzvevxecv729xz540v8qa742eg",
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBwMFZ0clBPaE1Xc1NGRTFK\nWlFGTWErdEtoSUhEeG1jWWNycGZsd1h0S0RrClQyQ1dRWGI4VzZRQXlvd3ErU00w\nTjNnQ2VJZEhpZ0RQdUxBUHpONjVNNjAKLS0tIGF1enFGWnhFL3pCM21nSTlhUUxs\naFQreFI2NzJmZVRZeVBxNVBQYVhTWEkKkGUPL0hOBAX2L7Vezq7Sf1V9Yu//X1/x\nNHndFsjTBcaFbDWCxcZpqVvJ0paKm46Nm+AQyhThR0OGCsKbRJhSlQ==\n-----END AGE ENCRYPTED FILE-----\n"
}
],
"lastmodified": "2025-07-22T13:55:27Z",
"mac": "ENC[AES256_GCM,data:NieKHtjtSL9NsDsFnIU5MN8pVp55r05Qu7Qf9Pde8W/60xkF1A7kML1zpbUbMfp05VS26aIENps8BOyDZhCJgZG/89NLXccGoJTKFLDUyKDqzwCPuEK+enbRm80VQnPpMLxRSMacCl/Qm3QxpQzLuasQvLH1c18BBhy+jpwuFBY=,iv:NbA65YXGIlkWXu9tmDHdvNL4xxlfp2FFFpjckJ86E80=,tag:XX6T/Hs8W0htQyp7DJhahQ==,type:str]",
"unencrypted_suffix": "_unencrypted",
"version": "3.10.2"
}
}

View File

@@ -0,0 +1 @@
../../../../../../sops/users/admin

View File

@@ -0,0 +1 @@
LFXPW4H-IIDB5K4-SZ54YJL-PVRW2Q7-52YPHEI-6VZQOQA-PSAVYPV-QF5BCQA

View File

@@ -0,0 +1 @@
../../../../../../sops/machines/peer1

View File

@@ -0,0 +1,19 @@
{
"data": "ENC[AES256_GCM,data:QnnXrKYiJMtNQlHBBjwZeosvLjcuMwWH+YjRwy3OeaPQ9gJbj2sAUrkoZSqc5jzi577nTympol92bGIRP51TKJ3k/i0cq7P/UyGIWCjPUi5jmJs3tY8rfztlcyiXr0TPYVYyZDp1ErPfFW96xWjfaSgopjScYGRS/NGkaRedQuP39Hwg4rIPpdrkEbPgVIMCHJdvMcPm2a2RmqX1PzoBj+TEQMCFxyuRg2cTVoqyyYv95w19T+5GddvbVDmB9c8OhG4VS9nqfgdL0IJ3TMUnJAC/S1wfj64YFdhrt7lzR00xriht19ZS+HPhLlBjSruZXa4SrSEtdIMfTAXlVwtw5e/LENhiKumyhmUMqlPRJPANbgZTxa6g1Wq3NmGIOQB6,iv:2I5JcaYbEA4W9zLWTTR4j4I1hREDqb7Nh4Wg9AVEaHE=,tag:WiJhtiPjTMN+CgJU+KNmgw==,type:str]",
"sops": {
"age": [
{
"recipient": "age1mq8u3x9z8v3zdm59qslxyn33zm0rpjzrd9sr9fjzqwp8d66t9czq626xsd",
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBXL1pYVmdUclF3dXhlTjJ5\nRFl1WXRrWUwwYUwxMnNvdXdCbFVZRmp0bDFvCnRNd3p1MzV3WHMxUFlkdUt6SHJj\nTHhSYTkzcGZqNnRWMFBPTTVmNGJaa00KLS0tIHNtWkxzdkEwelVqdzRHM2lqT05H\nN1EvdDgrOG5OUXEvdTlwMDJMekdFa3MKzlxMUKl97RGAcilKQuxC34Pzpr9qPsG9\nS5qhziP2xOn+6rXO21/klVQ1pJDZZcsVI+fCFPHTbwJvXvS6VNuLCg==\n-----END AGE ENCRYPTED FILE-----\n"
},
{
"recipient": "age1qm0p4vf9jvcnn43s6l4prk8zn6cx0ep9gzvevxecv729xz540v8qa742eg",
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBXL3ZCelVSd1ZxTXE3TEFs\ndmdEejl2bE5CTXhqVXZnbEVOOUlaYzhNMnlVClJ5K0hmMXVBTzJUVnVybHY0WCtw\na2tnUm5jR0xGN0M0SHVSSzVvejRZQ1EKLS0tIFNnS2EweEs2V3VXVnA5T0hQVWNq\nREFZckQ5cStZamZBdmZ2WHNzUzdQa0kK6774xhCCzaODxymwg2B+3Y6aRoXyBW1G\n7jTYWEsnf9eUCcBZP+jexT3obituZENtque5Ov1zblaGIBYnxi87Fg==\n-----END AGE ENCRYPTED FILE-----\n"
}
],
"lastmodified": "2025-07-22T13:55:27Z",
"mac": "ENC[AES256_GCM,data:HXcgcj09lBf9NPX5htqHZuU3yLKkKMt+RZtABLPFtzsZzXV6nE/ajlHm6+xI7Fo36u1b3KN9QfQ7i70LP2UKUf1L/JTVZiExkDmcfJCluCaq+/bDpY54ICw1jLZ0ckXgMHSTNwJbvM/UbvqdcB0cCt6MzxRUREHjTW4wk08qpPM=,iv:n24kpaCvODWE6+0cjo5fHCkFYHbz/zSjj9wqbVpd/dg=,tag:9x8wboXQB/YrMUU6L8jXqQ==,type:str]",
"unencrypted_suffix": "_unencrypted",
"version": "3.10.2"
}
}

View File

@@ -0,0 +1 @@
../../../../../../sops/users/admin

View File

@@ -0,0 +1 @@
../../../../../../sops/machines/peer2

View File

@@ -0,0 +1,19 @@
{
"data": "ENC[AES256_GCM,data:MMjnYNzHE7QSxrXY7aN8XSFkFwK/lpRhL4qV0vn6cwyn,iv:kB8m6ZhkNta6Y+gsYufvo45B+Q5lHuJBmYN3sk7ALKE=,tag:zbgynC6xhrFGcq+mE2pftA==,type:str]",
"sops": {
"age": [
{
"recipient": "age18mp7v3ck9eg0vqy3y83z5hdhum9rc77ntwal42p30udejkzvky2qjluy4x",
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA4S2FEd2R1Z2hUU3BtSGdZ\nS2I3NjRrNHhGaW9SL3Q3bkJuMFNlM25ya0dNCmhpVmk0eUpWWkpkQWErRUJKdks2\nSzUvMnVQTVJVbC9WeWpkd3VHandrNW8KLS0tIFFWOUlIZFlkYi9NSjJpbXNNWE9I\nMEYxeGJEcGNLaHJOSmR2TytaWDBtaVEKhEfTi3ESLw6I2Uu3ejCGWHMv3LmRvjbg\nZ30CoUmktB7z5/RbdwwiFyV+ijuNq0RPxrlBX0VGEkd/+BrIY4BYxQ==\n-----END AGE ENCRYPTED FILE-----\n"
},
{
"recipient": "age1qm0p4vf9jvcnn43s6l4prk8zn6cx0ep9gzvevxecv729xz540v8qa742eg",
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBwSXpGRkIzU0hwaXBaYXdM\nNTdnZmJTSmVzbVVWZHNCd1VSNjQ4OGdzTkFzClh2Vnl1RWlLbnE0YnVsYjNvZ2hs\nOE04a1QxM0pZVUsvMkpPdlA0Tjdpb28KLS0tIEpMVmFsTVpFcVVvcDdRVk5xZFdS\nMXBTdkt4SlV5UnJTcG4wQU11YjZrS1UKcLyVL6PknecAOLEhjYYbZ38+e+g5jXFd\nJcu7fHEs5O464vgM58SKJQ5m77rGzwqRGh6MjqJTfAl8nPbvw0t2cg==\n-----END AGE ENCRYPTED FILE-----\n"
}
],
"lastmodified": "2025-07-22T13:55:39Z",
"mac": "ENC[AES256_GCM,data:957Dud8YuagJRf8I0pAe4Tpk+En4ac99TAjRVv5eYAQtyDR1iHUwu/NIc0I14wZj7bKP6UqbOntST3jl33xhM2NQGG/FskJppsbS355OirpVfNahqlhcy0Jy5p1L9TikY89SlFgO6kx2eYs67inbk3UXnyeVxAcEIq6qnmh0u60=,iv:rfLaPYlcnBLecIYTIqJbaI2DNERS1MfU1C+7xH51kwY=,tag:5lQ1Ouf40MyB/Q3B6EwMzg==,type:str]",
"unencrypted_suffix": "_unencrypted",
"version": "3.10.2"
}
}

View File

@@ -0,0 +1 @@
../../../../../../sops/users/admin

View File

@@ -0,0 +1 @@
../../../../../../sops/machines/peer2

View File

@@ -0,0 +1,19 @@
{
"data": "ENC[AES256_GCM,data:65FnMTSkJxWZrik97mTIIZBH7Y+8bxTlV/wIA2jTV96oQebGWN6kI3lkR562e9ZVoiB5xyTQayU+NZhgIFEzyC5xAnXmLnKlZ2cvcjDJ51TlAujoHD5+R36tb6FrsBkuki3UI/v0xeWUhJSKpIoPL3/1Tq4l8EWVRR/JTf8fuC06HxwyiXJeMU3CkLpbt/txL2VGM9nNov+CbEZL+WJ99kCuRQ/QPAVglj0AulttXBPztNnUtPZBnBZzbFpxE4nRZ7ufjweVrRYgqYNj1CT0gdOJnyId9uu7vkHpZvGiwB3CaEeG3VyOGidfocmC1Yq/bQw26djx6npuiKoXPEgJmyaoi9DPuOvrvMwuYMpuueZCM7+54fbk7VTKusRtp0iOddSP1pevg1zM/w3NjRnnzgicahzxRf2EEGBRea9P4/znmPMHdbylXp1mnuqGpYZEteweOyFQFkjEmjIASI/ZI04JSgTGs37D3WrpD+Utjh/nVOtmeFHbeqbhk7vngDf++PW9nNM3xNhsALhd1w313agzUffQjRI96zXGj4SkUoPXkO9v9uPY+bZUxP4VncO4FbgxcEkO9YS57LiLp56YUzC/72Z30zsLrCnl2YZTXQ8nc7fvFVqfl61HmUC7xaEckQ0YXxH1yXQj+YTsk1qOVY/AyKjjq33j96de0i4AxYCEgZGU0pTkbT92MNezs7Atz066YIi1YC9YsMMMOXdtlz0FvQIN0WTzSqo1AtuyAUXo+otBX9IeMzX3hQ++E7CuHDMjR+I+jwwouAlrNCcn1Ean48cj6UkhNFHem/fysDL/2OYUZqeUfB7Qn8r0Ffewo55/87J0o2+8zlxdEOWPYiKOsT91G8Lq77OaadZCGFnNWlBrzqUkt+TuUa7UM9ok8uyZLX4R+f+hvIuvp+T+IAcz6r0R+1ndHai59r5gfgBeFxB5qnTPYTgujcdAiEeqfg7Dx0DPKEBiIj5awgsdd057oVf+eEY4Cu/mXuKb70TlbgEvelNmB5MRDlWTRm6ybrbnb7NfPmt5UYaTcPX6l/kP5ek95Q==,iv:uAE/AVleWSNp6kY/anvkJOeUqlfiuAgtHBgry6IqcJc=,tag:+J8jTvKqEULP8GW3IUqyTQ==,type:str]",
"sops": {
"age": [
{
"recipient": "age18mp7v3ck9eg0vqy3y83z5hdhum9rc77ntwal42p30udejkzvky2qjluy4x",
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBqdmdzU3Y2V1MzaE51a2Fj\nbFBIUWF3eXJrTUhKVW1tYzFSeGpad1BwRDBvCk96Z1lIMDg0ZnMrNnJkUzA4VXJ1\nZEtzbGNJU3ZzVmFYMzRBNmo5TnEybUkKLS0tIHErQ2dYZlVuckdML3Job3dwM0hM\nVEVWek40Y0F0bStzd09XenhWaVFIYVkKToXp2wUPBe9LtuLMlBPfpQRq4WHLbnSt\nueOxJvflzQveUhdpuHi6IbtE9HyCwtn6joeOnBTNFxt12Dy/s247Aw==\n-----END AGE ENCRYPTED FILE-----\n"
},
{
"recipient": "age1qm0p4vf9jvcnn43s6l4prk8zn6cx0ep9gzvevxecv729xz540v8qa742eg",
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA3bGpQQ1N0cmRmcURGV3hq\nUGdHOWh0S1VIQkQ2aFFVNXVnZmpFc050S240Ck03Z1NXNDVISTRvNnhkVnFZdzg0\nVFlXZ1g4citlV21PZ2lEbmRhMFhJaUUKLS0tIDIyTXZNTWpIUFhSYXR4VXBUN284\nYTFqWkQza0V3M05lZDlBQzBtWWFlaTAKfJViXG1KULBXTmDC4PLSWRy9LAVchE47\n1rBtT+r/KlbnOTYYEo7zCMagkPhDdPD5WSegmtghUGkXRtqaTZcTBA==\n-----END AGE ENCRYPTED FILE-----\n"
}
],
"lastmodified": "2025-07-22T13:55:39Z",
"mac": "ENC[AES256_GCM,data:HsnTHpXkyqJMlpbKzHvBH7bxg+ssDwXzjA7JTdpioP74gD8u+oPTHJOlgYI4Qbvr04JbZiRy/cko3BTUXj8C6nCV5gyXhDG3O0rkAGd/v3108ff7WiOWbXpNJ5QhLBnG4Ny1/ITfzu7MQtNkvGFnoktyMtvqHkVeJf5IhOuT6Fo=,iv:T1IYJyi8eXGdVE/JQSvyoyU/Iw/RlpO4TQCx1naZXXI=,tag:Z3J/313RG337U9HdXidrCA==,type:str]",
"unencrypted_suffix": "_unencrypted",
"version": "3.10.2"
}
}

View File

@@ -0,0 +1 @@
../../../../../../sops/users/admin

View File

@@ -0,0 +1 @@
5N6ZTHC-FZKLOY7-DJCORO3-BG3C4Q3-KD3PCMP-2DM7EHQ-2FS25FK-DCOTSAI

View File

@@ -0,0 +1 @@
../../../../../../sops/machines/peer2

View File

@@ -0,0 +1,19 @@
{
"data": "ENC[AES256_GCM,data:rDPNgUOmTINCoQnTQMjEXXrkvIYT7Ju6tX56aUA3cjlRGSzGmJb3Vmec58RS2QHmuOPUykydPTuAHxKs+SNZoXbdzDn75gAjS+eGNXGB9H9TFS2+iLpaSC5XTnfSdyzuGVyPRNvceI/C8FIvEauEpJ9dbS2kkTFTD15u2iI6zTmm/Mtvy2C18BwwEtu+m1TWB90oVduX05rrdnxoMjtscBCKR8fz6CbqYrbnpag9AUGUk8Z+cIMYGlSjDRlyaLWkh33aV/TWx/QOzfAWCyCVLMnU500ch0c6l2fg8daKDJJas2jHxo8KnlBIP2b6SLrcEKtlOSaPicizlJ9UDilDqHQfbucEJTgkQ2eEwJWSmXhU3q3ttGm3mxhQgej5FeIW,iv:6P1hFhDmHs4IYeyxVA/lYu2OL0ciaZNPEmMdBudacds=,tag:9MjdSKKjUO3VlTh/QmAPtA==,type:str]",
"sops": {
"age": [
{
"recipient": "age18mp7v3ck9eg0vqy3y83z5hdhum9rc77ntwal42p30udejkzvky2qjluy4x",
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBCUWYyQXVUMUMwaUt4S3RO\nYkNzd3FxU29qYk1wWVlrTUNPK0lKSTU2RFJrCmZaeGg5Nlg5azhaRSs5SzVDZlpV\nKzJuUmlSekYrdzdGQ3JDejNwUmZ3cWMKLS0tIFhGT29pUC9wNlpGaHBMSHROV0lP\nNXNjQytQVDlOQmI1UThUTm5Qd1BmNmsKlvG3ZENmjPUDHRUB2o4RTCueIdtXZjxU\nz3Xg/RWZjaDInneg0z/jXyhKoqLRk1J0h9f/36CHT7RtVHIjtz2KIQ==\n-----END AGE ENCRYPTED FILE-----\n"
},
{
"recipient": "age1qm0p4vf9jvcnn43s6l4prk8zn6cx0ep9gzvevxecv729xz540v8qa742eg",
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB3MWovZlBoVk1HdUhhSXNy\nYXgxZVdIaDhqMTk0dFRCVFptaUE0ZmZ4ZHlBClpDNS85a2U0WE12TXVzOFhDcTRM\nWG1yU0dDaS9pUWVUT3pnWXNxRjlSVmcKLS0tIGxiTWVIREE4SlQ0Mm8xQW0ycnB2\na2Q4WGY2eDl0UGVTdXo1bWhMQjdNNzgKorezVfxaH/Kx3lpX57zF/0gAeCu2NzWX\nAm7RAFeHDJ+w0jIlZa2RM6mXOTKDzaF+h4j27GcLLbXsSRNhlFMqvg==\n-----END AGE ENCRYPTED FILE-----\n"
}
],
"lastmodified": "2025-07-22T13:55:39Z",
"mac": "ENC[AES256_GCM,data:w9GKY762/ikgNU0ypIOzlnXZqnW3tN1Fz+cVCsgPMQhZbf2OIb5zHgQAcZMHJII5+WqPxLE+PThhZ2hstuz0hvvqXF37KvuT5MblcGShCNOCTJiVwLVhhEUQl3c3C9iXKV+A/aG9psagjHAlhxsdv1UKqVPhQ0bgrl92EZZPdwo=,iv:3XuVGnMu2tOLvkjY8G78qk+mJWZShRsgWx7LwU3qaCI=,tag:+tQcEjZttN9cPxAOer4IoQ==,type:str]",
"unencrypted_suffix": "_unencrypted",
"version": "3.10.2"
}
}

View File

@@ -0,0 +1 @@
../../../../../../sops/users/admin

View File

@@ -134,9 +134,9 @@
systemd.services.zerotier-inventory-autoaccept =
let
machines = uniqueStrings (
(lib.optionals (roles ? moon) (lib.attrNames roles.moon.machines))
++ (lib.optionals (roles ? controller) (lib.attrNames roles.controller.machines))
++ (lib.optionals (roles ? peer) (lib.attrNames roles.peer.machines))
(lib.attrNames roles.moon.machines)
++ (lib.attrNames roles.controller.machines)
++ (lib.attrNames roles.peer.machines)
);
networkIps = builtins.foldl' (
ips: name:

View File

@@ -32,33 +32,6 @@ let
};
};
}).config;
testFlakeNoMoon =
(clanLib.clan {
self = { };
directory = ./vm;
machines.jon = {
nixpkgs.hostPlatform = "x86_64-linux";
};
machines.sara = {
nixpkgs.hostPlatform = "x86_64-linux";
};
machines.bam = {
nixpkgs.hostPlatform = "x86_64-linux";
};
modules.zerotier = module;
inventory.instances = {
zerotier = {
module.name = "zerotier";
module.input = "self";
roles.peer.tags.all = { };
roles.controller.machines.bam = { };
};
};
}).config;
in
{
test_peers = {
@@ -100,30 +73,4 @@ in
networkName = "zerotier";
};
};
test_peers_no_moon = {
expr = {
hasNetworkIds = testFlakeNoMoon.nixosConfigurations.jon.config.services.zerotierone.joinNetworks;
isController =
testFlakeNoMoon.nixosConfigurations.jon.config.clan.core.networking.zerotier.controller.enable;
networkName = testFlakeNoMoon.nixosConfigurations.jon.config.clan.core.networking.zerotier.name;
};
expected = {
hasNetworkIds = [ "0e28cb903344475e" ];
isController = false;
networkName = "zerotier";
};
};
test_controller_no_moon = {
expr = {
hasNetworkIds = testFlakeNoMoon.nixosConfigurations.bam.config.services.zerotierone.joinNetworks;
isController =
testFlakeNoMoon.nixosConfigurations.bam.config.clan.core.networking.zerotier.controller.enable;
networkName = testFlakeNoMoon.nixosConfigurations.bam.config.clan.core.networking.zerotier.name;
};
expected = {
hasNetworkIds = [ "0e28cb903344475e" ];
isController = true;
networkName = "zerotier";
};
};
}

View File

@@ -48,25 +48,34 @@ nav:
- Home: index.md
- Guides:
- Getting Started:
- Creating Your First Clan: guides/getting-started/index.md
- Create USB Installer: guides/getting-started/installer.md
- Add Machines: guides/getting-started/add-machines.md
- Add User: guides/getting-started/add-user.md
- Add Services: guides/getting-started/add-services.md
- Deploy Machine: guides/getting-started/deploy.md
- Continuous Integration: guides/getting-started/check.md
- Inventory: guides/inventory.md
- Using Services: guides/clanServices.md
- Backup & Restore: guides/backups.md
- 🚀 Creating Your First Clan: guides/getting-started/index.md
- 📀 Create USB Installer (optional): guides/getting-started/installer.md
- ⚙️ Add Machines: guides/getting-started/add-machines.md
- ⚙️ Add User: guides/getting-started/add-user.md
- ⚙️ Add Services: guides/getting-started/add-services.md
- 🔐 Secrets & Facts: guides/getting-started/secrets.md
- 🚢 Deploy Machine: guides/getting-started/deploy.md
- 🧪 Continuous Integration: guides/getting-started/check.md
- clanServices: guides/clanServices.md
- Disk Encryption: guides/disk-encryption.md
- Vars: guides/vars-backend.md
- Age Plugins: guides/age-plugins.md
- Advanced Secrets: guides/secrets.md
- Machine Autoincludes: guides/more-machines.md
- Mesh VPN: guides/mesh-vpn.md
- Backup & Restore: guides/backups.md
- Vars Backend: guides/vars-backend.md
- Facts Backend: guides/secrets.md
- Adding more machines: guides/more-machines.md
- Target Host: guides/target-host.md
- Zerotier VPN: guides/mesh-vpn.md
- Inventory:
- Inventory: guides/inventory.md
- Secure Boot: guides/secure-boot.md
- Flake-parts: guides/flake-parts.md
- Authoring:
- clanService: guides/authoring/clanServices/index.md
- Disk Template: guides/authoring/templates/disk/disko-templates.md
- clanModule: guides/authoring/clanModules/index.md
- Contributing:
- Contribute: guides/contributing/CONTRIBUTING.md
- Debugging: guides/contributing/debugging.md
- Testing: guides/contributing/testing.md
- Migrations:
- Migrate existing Flakes: guides/migrations/migration-guide.md
- Migrate inventory Services: guides/migrations/migrate-inventory-services.md
@@ -76,84 +85,66 @@ nav:
- Reference:
- Overview: reference/index.md
- Services:
- List:
- Overview: reference/clanServices/index.md
- reference/clanServices/admin.md
- reference/clanServices/borgbackup.md
- reference/clanServices/data-mesher.md
- reference/clanServices/emergency-access.md
- reference/clanServices/garage.md
- reference/clanServices/hello-world.md
- reference/clanServices/importer.md
- reference/clanServices/mycelium.md
- reference/clanServices/packages.md
- reference/clanServices/sshd.md
- reference/clanServices/state-version.md
- reference/clanServices/trusted-nix-caches.md
- reference/clanServices/users.md
- reference/clanServices/wifi.md
- reference/clanServices/zerotier.md
- API: reference/clanServices/clan-service-author-interface.md
- Writing a Service Module: developer/extensions/clanServices/index.md
- Overview: reference/clanServices/index.md
- reference/clanServices/admin.md
- reference/clanServices/borgbackup.md
- reference/clanServices/data-mesher.md
- reference/clanServices/emergency-access.md
- reference/clanServices/garage.md
- reference/clanServices/hello-world.md
- reference/clanServices/importer.md
- reference/clanServices/mycelium.md
- reference/clanServices/packages.md
- reference/clanServices/sshd.md
- reference/clanServices/state-version.md
- reference/clanServices/syncthing.md
- reference/clanServices/trusted-nix-caches.md
- reference/clanServices/users.md
- reference/clanServices/wifi.md
- reference/clanServices/zerotier.md
- Interface for making Services: reference/clanServices/clan-service-author-interface.md
- Modules:
- List:
- Overview: reference/clanModules/index.md
- reference/clanModules/frontmatter/index.md
# TODO: display the docs of the clan.service modules
- reference/clanModules/admin.md
# This is the module overview and should stay at the top
- reference/clanModules/borgbackup-static.md
- reference/clanModules/data-mesher.md
- reference/clanModules/borgbackup.md
- reference/clanModules/deltachat.md
- reference/clanModules/disk-id.md
- reference/clanModules/dyndns.md
- reference/clanModules/ergochat.md
- reference/clanModules/garage.md
- reference/clanModules/heisenbridge.md
- reference/clanModules/importer.md
- reference/clanModules/iwd.md
- reference/clanModules/localbackup.md
- reference/clanModules/localsend.md
- reference/clanModules/matrix-synapse.md
- reference/clanModules/moonlight.md
- reference/clanModules/mumble.md
- reference/clanModules/mycelium.md
- reference/clanModules/nginx.md
- reference/clanModules/packages.md
- reference/clanModules/postgresql.md
- reference/clanModules/root-password.md
- reference/clanModules/single-disk.md
- reference/clanModules/sshd.md
- reference/clanModules/state-version.md
- reference/clanModules/static-hosts.md
- reference/clanModules/sunshine.md
- reference/clanModules/syncthing-static-peers.md
- reference/clanModules/syncthing.md
- reference/clanModules/thelounge.md
- reference/clanModules/trusted-nix-caches.md
- reference/clanModules/user-password.md
- reference/clanModules/auto-upgrade.md
- reference/clanModules/vaultwarden.md
- reference/clanModules/xfce.md
- reference/clanModules/zerotier-static-peers.md
- reference/clanModules/zerotier.md
- reference/clanModules/zt-tcp-relay.md
- Writing a Clan Module: developer/extensions/clanModules/index.md
- Nix API:
- inputs.clan-core.lib.clan: reference/nix-api/clan.md
- config.clan.core:
- Overview: reference/clan.core/index.md
- reference/clan.core/backups.md
- reference/clan.core/deployment.md
- reference/clan.core/facts.md
- reference/clan.core/networking.md
- reference/clan.core/settings.md
- reference/clan.core/sops.md
- reference/clan.core/state.md
- reference/clan.core/vars.md
- Inventory: reference/nix-api/inventory.md
- Overview: reference/clanModules/index.md
- reference/clanModules/frontmatter/index.md
# TODO: display the docs of the clan.service modules
- reference/clanModules/admin.md
# This is the module overview and should stay at the top
- reference/clanModules/borgbackup-static.md
- reference/clanModules/data-mesher.md
- reference/clanModules/borgbackup.md
- reference/clanModules/deltachat.md
- reference/clanModules/disk-id.md
- reference/clanModules/dyndns.md
- reference/clanModules/ergochat.md
- reference/clanModules/garage.md
- reference/clanModules/heisenbridge.md
- reference/clanModules/importer.md
- reference/clanModules/iwd.md
- reference/clanModules/localbackup.md
- reference/clanModules/localsend.md
- reference/clanModules/matrix-synapse.md
- reference/clanModules/moonlight.md
- reference/clanModules/mumble.md
- reference/clanModules/mycelium.md
- reference/clanModules/nginx.md
- reference/clanModules/packages.md
- reference/clanModules/postgresql.md
- reference/clanModules/root-password.md
- reference/clanModules/single-disk.md
- reference/clanModules/sshd.md
- reference/clanModules/state-version.md
- reference/clanModules/static-hosts.md
- reference/clanModules/sunshine.md
- reference/clanModules/syncthing-static-peers.md
- reference/clanModules/thelounge.md
- reference/clanModules/trusted-nix-caches.md
- reference/clanModules/user-password.md
- reference/clanModules/auto-upgrade.md
- reference/clanModules/vaultwarden.md
- reference/clanModules/xfce.md
- reference/clanModules/zerotier-static-peers.md
- reference/clanModules/zerotier.md
- reference/clanModules/zt-tcp-relay.md
- CLI:
- Overview: reference/cli/index.md
@@ -170,7 +161,21 @@ nav:
- reference/cli/templates.md
- reference/cli/vars.md
- reference/cli/vms.md
- NixOS Modules:
- clan.core:
- Overview: reference/clan.core/index.md
- reference/clan.core/backups.md
- reference/clan.core/deployment.md
- reference/clan.core/facts.md
- reference/clan.core/networking.md
- reference/clan.core/settings.md
- reference/clan.core/sops.md
- reference/clan.core/state.md
- reference/clan.core/vars.md
- Nix API:
- clan: reference/nix-api/clan.md
- Inventory: reference/nix-api/inventory.md
- Glossary: reference/glossary.md
- Decisions:
- Architecture Decisions: decisions/README.md
@@ -182,14 +187,8 @@ nav:
- Template: decisions/_template.md
- Options: options.md
- Developer:
- Introduction: developer/index.md
- Dev Setup: developer/contributing/CONTRIBUTING.md
- Writing a Service Module: developer/extensions/clanServices/index.md
- Writing a Clan Module: developer/extensions/clanModules/index.md
- Writing a Disko Template: developer/extensions/templates/disk/disko-templates.md
- Debugging: developer/contributing/debugging.md
- Testing: developer/contributing/testing.md
- Python API: developer/api.md
- Introduction: intern/index.md
- API: intern/api.md
docs_dir: site
site_dir: out
@@ -247,6 +246,3 @@ plugins:
- search
- macros
- redoc-tag
- redirects:
redirect_maps:
guides/getting-started/secrets.md: guides/vars-backend.md

View File

@@ -40,7 +40,6 @@ pkgs.stdenv.mkDerivation {
mkdocs-material
mkdocs-macros
mkdocs-redoc-tag
mkdocs-redirects
]);
configurePhase = ''
pushd docs

View File

@@ -114,6 +114,9 @@
in
{
options = {
_ = mkOption {
type = types.raw;
};
instances.${name} = lib.mkOption {
inherit description;
type = types.submodule {
@@ -146,29 +149,20 @@
};
};
docModules = [
{
inherit self;
}
self.modules.clan.default
{
options.inventory = lib.mkOption {
type = types.submoduleWith {
modules = [
{ noInstanceOptions = true; }
] ++ mapAttrsToList fakeInstanceOptions serviceModules;
};
};
}
];
mkScope = name: modules: {
inherit name;
modules = [
{
_module.args = { inherit clanLib; };
_file = "docs mkScope";
}
{ noInstanceOptions = true; }
../../../lib/modules/inventoryClass/interface.nix
] ++ mapAttrsToList fakeInstanceOptions modules;
urlPrefix = "https://github.com/nix-community/dream2nix/blob/main/";
};
in
{
# Uncomment for debugging
# legacyPackages.docModules = lib.evalModules {
# modules = docModules;
# };
packages = lib.optionalAttrs ((privateInputs ? nuschtos) || (inputs ? nuschtos)) {
docs-options =
(privateInputs.nuschtos or inputs.nuschtos)
@@ -177,13 +171,7 @@
inherit baseHref;
title = "Clan Options";
# scopes = mapAttrsToList mkScope serviceModules;
scopes = [
{
name = "Clan";
modules = docModules;
urlPrefix = "https://git.clan.lol/clan/clan-core/src/branch/main/";
}
];
scopes = [ (mkScope "Clan Inventory" serviceModules) ];
};
};
};

View File

@@ -805,7 +805,7 @@ Typically needed by module authors to define roles, behavior and metadata for di
!!! Note
This is not a user-facing documentation, but rather meant as a reference for *module authors*
See: [clanService Authoring Guide](../../developer/extensions/clanServices/index.md)
See: [clanService Authoring Guide](../../guides/authoring/clanServices/index.md)
"""
# Inventory options are already included under the clan attribute
# We just omitted them in the clan docs, because we want a separate output for the inventory model

View File

@@ -1,59 +0,0 @@
## Using Age Plugins
If you wish to use a key generated using an [age plugin] as your admin key, extra care is needed.
You must **precede your secret key with a comment that contains its corresponding recipient**.
This is usually output as part of the generation process
and is only required because there is no unified mechanism for recovering a recipient from a plugin secret key.
Here is an example:
```title="~/.config/sops/age/keys.txt"
# public key: age1zdy49ek6z60q9r34vf5mmzkx6u43pr9haqdh5lqdg7fh5tpwlfwqea356l
AGE-PLUGIN-FIDO2-HMAC-1QQPQZRFR7ZZ2WCV...
```
!!! note
The comment that precedes the plugin secret key need only contain the recipient.
Any other text is ignored.
In the example above, you can specify `# recipient: age1zdy...`, `# public: age1zdy....` or even
just `# age1zdy....`
You will need to add an entry into your `flake.nix` to ensure that the necessary `age` plugins
are loaded when using Clan:
```nix title="flake.nix"
{
inputs.clan-core.url = "https://git.clan.lol/clan/clan-core/archive/main.tar.gz";
inputs.nixpkgs.follows = "clan-core/nixpkgs";
outputs =
{ self, clan-core, ... }:
let
# Sometimes this attribute set is defined in clan.nix
clan = clan-core.lib.clan {
inherit self;
meta.name = "myclan";
# Add Yubikey and FIDO2 HMAC plugins
# Note: the plugins listed here must be available in nixpkgs.
secrets.age.plugins = [
"age-plugin-yubikey"
"age-plugin-fido2-hmac"
];
machines = {
# elided for brevity
};
};
in
{
inherit (clan) nixosConfigurations nixosModules clanInternals;
# elided for brevity
};
}
```

View File

@@ -267,5 +267,5 @@ The benefit of this approach is that downstream users can override the value of
## Further
- [Reference Documentation for Service Authors](../../../reference/clanServices/clan-service-author-interface.md)
- [Migration Guide from ClanModules to ClanServices](../../../guides/migrations/migrate-inventory-services.md)
- [Migration Guide from ClanModules to ClanServices](../../migrations/migrate-inventory-services.md)
- [Decision that lead to ClanServices](../../../decisions/01-ClanModules.md)

View File

@@ -138,7 +138,7 @@ You can use services exposed by Clans core module library, `clan-core`.
You can also author your own `clanService` modules.
🔗 Learn how to write your own service: [Authoring a clanService](../developer/extensions/clanServices/index.md)
🔗 Learn how to write your own service: [Authoring a clanService](../guides/authoring/clanServices/index.md)
You might expose your service module from your flake — this makes it easy for other people to also use your module in their clan.
@@ -154,6 +154,6 @@ You might expose your service module from your flake — this makes it easy for
## Whats Next?
* [Author your own clanService →](../developer/extensions/clanServices/index.md)
* [Author your own clanService →](../guides/authoring/clanServices/index.md)
* [Migrate from clanModules →](../guides/migrations/migrate-inventory-services.md)
<!-- TODO: * [Understand the architecture →](../explanation/clan-architecture.md) -->

View File

@@ -41,7 +41,7 @@ To learn more: [Guide about clanService](../clanServices.md)
```
1. See [reference/clanServices](../../reference/clanServices/index.md) for all available services and how to configure them.
Or read [authoring/clanServices](../../developer/extensions/clanServices/index.md) if you want to bring your own
Or read [authoring/clanServices](../authoring/clanServices/index.md) if you want to bring your own
2. Replace `__YOUR_CONTROLLER_` with the *name* of your machine.

View File

@@ -57,7 +57,7 @@ For more information see [clanService/users](../../reference/clanServices/users.
Some people like to define a `users` folder in their repository root.
That allows to bind all user specific logic to a single place (`default.nix`)
Which can be imported into individual machines to make the user available on that machine.
Which can be imported into individual machines to make the user avilable on that machine.
```bash
.
@@ -107,7 +107,7 @@ We can use this property of clan services to bind a nixosModule to the user, whi
}
```
1. Type `path` or `string`: Must point to a separate file. Inlining a module is not possible
1. Type `path` or `string`: Must point to a seperate file. Inlining a module is not possible
!!! Note "This is inspiration"
Our community might come up with better solutions soon.

View File

@@ -8,6 +8,7 @@ Now that you have created a machines, added some services and setup secrets. Thi
- [x] RAM > 2GB
- [x] **Two Computers**: You need one computer that you're getting ready (we'll call this the Target Computer) and another one to set it up from (we'll call this the Setup Computer). Make sure both can talk to each other over the network using SSH.
- [x] **Machine configuration**: See our basic [adding and configuring machine guide](./add-machines.md)
- [x] **Initialized secrets**: See [secrets](secrets.md) for how to initialize your secrets.
## Physical Hardware
@@ -17,7 +18,7 @@ Steps:
- Create a NixOS installer image and transfer it to a bootable USB drive as described in the [installer](./installer.md).
- Boot the target machine and connect it to a network that makes it reachable from your setup computer.
- Note down a reachable ip address (*ipv4*, *ipv6* or *tor*)
- Note down a reachable ip adress (*ipv4*, *ipv6* or *tor*)
---
@@ -168,7 +169,7 @@ Re-run the command with the correct disk:
clan templates apply disk single-disk jon --set mainDisk "/dev/disk/by-id/nvme-WD_PC_SN740_SDDQNQD-512G-1201_232557804368"
```
Should now be successful
Should now be succesfull
```shellSession
Applied disk template 'single-disk' to machine 'jon'

View File

@@ -59,7 +59,7 @@ Enter a *name*, confirm with *enter*. A directory with that name will be created
## Explore the Project Structure
Take a look at all project files:
Take a lookg at all project files:
```bash
cd my-clan
@@ -125,10 +125,11 @@ To change the name of your clan edit `meta.name` in the `clan.nix` or `flake.nix
You can continue with **any** of the following steps at your own pace:
- [x] [Install Nix & Clan CLI](./index.md)
- [x] [Initialize Clan](./index.md#add-clan-cli-to-your-shell)
- [x] [Initialize Clan](./index.md#initialize-your-project)
- [ ] [Create USB Installer (optional)](./installer.md)
- [ ] [Add Machines](./add-machines.md)
- [ ] [Add a User](./add-user.md)
- [ ] [Add Services](./add-services.md)
- [ ] [Configure Secrets](./secrets.md)
- [ ] [Deploy](./deploy.md) - Requires configured secrets
- [ ] [Setup CI (optional)](./check.md)

View File

@@ -0,0 +1,179 @@
Setting up secrets is **Required** for any *machine deployments* or *vm runs* - You need to complete the steps: [Create Admin Keypair](#create-your-admin-keypair) and [Add Your Public Key(s)](#add-your-public-keys)
---
Clan enables encryption of secrets (such as passwords & keys) ensuring security and ease-of-use among users.
By default, Clan uses the [sops](https://github.com/getsops/sops) format
and integrates with [sops-nix](https://github.com/Mic92/sops-nix) on NixOS machines.
Clan can also be configured to be used with other secret store [backends](../../reference/clan.core/vars.md#clan.core.vars.settings.secretStore).
This guide will walk you through:
- **Creating a Keypair for Your User**: Learn how to generate a keypair for `$USER` to securely control all secrets.
- **Creating Your First Secret**: Step-by-step instructions on creating your initial secret.
- **Assigning Machine Access to the Secret**: Understand how to grant a machine access to the newly created secret.
## Create Your Admin Keypair
To get started, you'll need to create **your admin keypair**.
!!! info
Don't worry — if you've already made one before, this step won't change or overwrite it.
```bash
clan secrets key generate
```
**Output**:
```{.console, .no-copy}
Public key: age1wkth7uhpkl555g40t8hjsysr20drq286netu8zptw50lmqz7j95sw2t3l7
Generated age private key at '/home/joerg/.config/sops/age/keys.txt' for your user. Please back it up on a secure location or you will lose access to your secrets.
Also add your age public key to the repository with 'clan secrets users add YOUR_USER age1wkth7uhpkl555g40t8hjsysr20drq286netu8zptw50lmqz7j95sw2t3l7' (replace YOUR_USER with your actual username)
```
!!! warning
Make sure to keep a safe backup of the private key you've just created.
If it's lost, you won't be able to get to your secrets anymore because they all need the admin key to be unlocked.
If you already have an [age] secret key and want to use that instead, you can simply edit `~/.config/sops/age/keys.txt`:
```title="~/.config/sops/age/keys.txt"
AGE-SECRET-KEY-13GWMK0KNNKXPTJ8KQ9LPSQZU7G3KU8LZDW474NX3D956GGVFAZRQTAE3F4
```
Alternatively, you can provide your [age] secret key as an environment variable `SOPS_AGE_KEY`, or in a different file
using `SOPS_AGE_KEY_FILE`.
For more information see the [SOPS] guide on [encrypting with age].
!!! note
It's safe to add any secrets created by the clan CLI and placed in your repository to version control systems like `git`.
### Add Your Public Key(s)
```console
clan secrets users add $USER --age-key <your_public_key>
```
It's best to choose the same username as on your Setup/Admin Machine that you use to control the deployment with.
Once run this will create the following files:
```{.console, .no-copy}
sops/
└── users/
└── <your_username>/
└── key.json
```
If you followed the quickstart tutorial all necessary secrets are initialized at this point.
!!! note
You can add multiple age keys for a user by providing multiple `--age-key <your_public_key>` flags:
```console
clan secrets users add $USER \
--age-key <your_public_key_1> \
--age-key <your_public_key_2> \
...
```
### Manage Your Public Key(s)
You can list keys for your user with `clan secrets users get $USER`:
```console
clan secrets users get alice
[
{
"publickey": "age1hrrcspp645qtlj29krjpq66pqg990ejaq0djcms6y6evnmgglv5sq0gewu",
"type": "age",
"username": "alice"
},
{
"publickey": "age13kh4083t3g4x3ktr52nav6h7sy8ynrnky2x58pyp96c5s5nvqytqgmrt79",
"type": "age",
"username": "alice"
}
]
```
To add a new key to your user:
```console
clan secrets users add-key $USER --age-key <your_public_key>
```
To remove a key from your user:
```console
clan secrets users remove-key $USER --age-key <your_public_key>
```
[age]: https://github.com/FiloSottile/age
[age plugin]: https://github.com/FiloSottile/awesome-age?tab=readme-ov-file#plugins
[sops]: https://github.com/getsops/sops
[encrypting with age]: https://github.com/getsops/sops?tab=readme-ov-file#encrypting-using-age
## Further: Using Age Plugins
If you wish to use a key generated using an [age plugin] as your admin key, extra care is needed.
You must **precede your secret key with a comment that contains its corresponding recipient**.
This is usually output as part of the generation process
and is only required because there is no unified mechanism for recovering a recipient from a plugin secret key.
Here is an example:
```title="~/.config/sops/age/keys.txt"
# public key: age1zdy49ek6z60q9r34vf5mmzkx6u43pr9haqdh5lqdg7fh5tpwlfwqea356l
AGE-PLUGIN-FIDO2-HMAC-1QQPQZRFR7ZZ2WCV...
```
!!! note
The comment that precedes the plugin secret key need only contain the recipient.
Any other text is ignored.
In the example above, you can specify `# recipient: age1zdy...`, `# public: age1zdy....` or even
just `# age1zdy....`
You will need to add an entry into your `flake.nix` to ensure that the necessary `age` plugins
are loaded when using Clan:
```nix title="flake.nix"
{
inputs.clan-core.url = "https://git.clan.lol/clan/clan-core/archive/main.tar.gz";
inputs.nixpkgs.follows = "clan-core/nixpkgs";
outputs =
{ self, clan-core, ... }:
let
# Sometimes this attribute set is defined in clan.nix
clan = clan-core.lib.clan {
inherit self;
meta.name = "myclan";
# Add Yubikey and FIDO2 HMAC plugins
# Note: the plugins listed here must be available in nixpkgs.
secrets.age.plugins = [
"age-plugin-yubikey"
"age-plugin-fido2-hmac"
];
machines = {
# elided for brevity
};
};
in
{
inherit (clan) nixosConfigurations nixosModules clanInternals;
# elided for brevity
};
}
```

View File

@@ -1,7 +1,7 @@
# Migrating from using `clanModules` to `clanServices`
**Audience**: This is a guide for **people using `clanModules`**.
If you are a **module author** and need to migrate your modules please consult our **new** [clanServices authoring guide](../../developer/extensions/clanServices/index.md)
If you are a **module author** and need to migrate your modules please consult our **new** [clanServices authoring guide](../authoring/clanServices/index.md)
## What's Changing?
@@ -35,37 +35,6 @@ services = {
};
```
### Complex Example: Multi-service Setup
```nix
# Old format
services = {
borgbackup.production = {
roles.server.machines = [ "backup-server" ];
roles.server.config = {
directory = "/var/backup/borg";
};
roles.client.tags = [ "backup" ];
roles.client.extraModules = [ "nixosModules/borgbackup.nix" ];
};
zerotier.company-network = {
roles.controller.machines = [ "network-controller" ];
roles.moon.machines = [ "moon-1" "moon-2" ];
roles.peer.tags = [ "nixos" ];
};
sshd.internal = {
roles.server.tags = [ "nixos" ];
roles.client.tags = [ "nixos" ];
config.certificate.searchDomains = [
"internal.example.com"
"vpn.example.com"
];
};
};
```
---
## ✅ After: New `instances` Definition with `clanServices`
@@ -101,56 +70,6 @@ instances = {
};
```
### Complex Example Migrated
```nix
# New format
instances = {
borgbackup-production = {
module = {
name = "borgbackup";
input = "clan-core";
};
roles.server.machines."backup-server" = { };
roles.server.settings = {
directory = "/var/backup/borg";
};
roles.client.tags.backup = { };
roles.client.extraModules = [ ../nixosModules/borgbackup.nix ];
};
zerotier-company-network = {
module = {
name = "zerotier";
input = "clan-core";
};
roles.controller.machines."network-controller" = { };
roles.moon.machines."moon-1".settings = {
stableEndpoints = [ "10.0.0.1" "2001:db8::1" ];
};
roles.moon.machines."moon-2".settings = {
stableEndpoints = [ "10.0.0.2" "2001:db8::2" ];
};
roles.peer.tags.nixos = { };
};
sshd-internal = {
module = {
name = "sshd";
input = "clan-core";
};
roles.server.tags.nixos = { };
roles.client.tags.nixos = { };
roles.client.settings = {
certificate.searchDomains = [
"internal.example.com"
"vpn.example.com"
];
};
};
};
```
---
## Steps to Migrate
@@ -212,33 +131,6 @@ roles.default.machines."test-inventory-machine".settings = {
};
```
### Important Type Changes
The new `instances` format uses **attribute sets** instead of **lists** for tags and machines:
```nix
# ❌ Old format (lists)
roles.client.tags = [ "backup" ];
roles.server.machines = [ "blob64" ];
# ✅ New format (attribute sets)
roles.client.tags.backup = { };
roles.server.machines.blob64 = { };
```
### Handling Multiple Machines/Tags
When you need to assign multiple machines or tags to a role:
```nix
# ❌ Old format
roles.moon.machines = [ "eva" "eve" ];
# ✅ New format - each machine gets its own attribute
roles.moon.machines.eva = { };
roles.moon.machines.eve = { };
```
---
!!! Warning
@@ -246,89 +138,8 @@ roles.moon.machines.eve = { };
* `inventory.services` is no longer recommended; use `inventory.instances` instead.
* Module authors should begin exporting service modules under the `clan.modules` attribute of their flake.
## Troubleshooting Common Migration Errors
### Error: "not of type `attribute set of (submodule)`"
This error occurs when using lists instead of attribute sets for tags or machines:
```
error: A definition for option `flake.clan.inventory.instances.borgbackup-blob64.roles.client.tags' is not of type `attribute set of (submodule)'.
```
**Solution**: Convert lists to attribute sets as shown in the "Important Type Changes" section above.
### Error: "unsupported attribute `module`"
This error indicates the module structure is incorrect:
```
error: Module ':anon-4:anon-1' has an unsupported attribute `module'.
```
**Solution**: Ensure the `module` attribute has exactly two fields: `name` and `input`.
### Error: "attribute 'pkgs' missing"
This suggests the instance configuration is trying to use imports incorrectly:
```
error: attribute 'pkgs' missing
```
**Solution**: Use the `module = { name = "..."; input = "..."; }` format instead of `imports`.
### Removed Features
The following features from the old `services` format are no longer supported in `instances`:
- Top-level `config` attribute (use `roles.<role>.settings` instead)
- Direct module imports (use the `module` declaration instead)
### extraModules Support
The `extraModules` attribute is still supported in the new instances format! The key change is how modules are specified:
**Old format (string paths relative to clan root):**
```nix
roles.client.extraModules = [ "nixosModules/borgbackup.nix" ];
```
**New format (NixOS modules):**
```nix
# Direct module reference
roles.client.extraModules = [ ../nixosModules/borgbackup.nix ];
# Or using self
roles.client.extraModules = [ self.nixosModules.borgbackup ];
# Or inline module definition
roles.client.extraModules = [
{ config, ... }: {
# Your module configuration here
}
];
```
The `extraModules` now expects actual **NixOS modules** rather than string paths. This provides better type checking and more flexibility in how modules are specified.
**Alternative: Using @clan/importer**
For scenarios where you need to import modules with specific tag-based targeting, you can also use the dedicated `@clan/importer` service:
```nix
instances = {
my-importer = {
module.name = "@clan/importer";
module.input = "clan-core";
roles.default.tags.my-tag = { };
roles.default.extraModules = [ self.nixosModules.myModule ];
};
};
```
## Further reference
* [Authoring a 'clan.service' module](../../developer/extensions/clanServices/index.md)
* [Authoring a 'clan.service' module](../authoring/clanServices/index.md)
* [ClanServices](../clanServices.md)
* [Inventory Reference](../../reference/nix-api/inventory.md)
* [Inventory Reference](../../reference/nix-api/inventory.md)

View File

@@ -1,141 +1,25 @@
This article provides an overview over the underlying secrets system which is used by [Vars](../guides/vars-backend.md).
Under most circumstances you should use [Vars](../guides/vars-backend.md) directly instead.
If you want to know more about how to save and share passwords in your clan read further!
Consider using `clan secrets` only for managing admin users and groups, as well as a debugging tool.
Manually interacting with secrets via `clan secrets [set|remove]`, etc may break the integrity of your `Vars` state.
---
Clan enables encryption of secrets (such as passwords & keys) ensuring security and ease-of-use among users.
By default, Clan uses the [sops](https://github.com/getsops/sops) format
and integrates with [sops-nix](https://github.com/Mic92/sops-nix) on NixOS machines.
Clan can also be configured to be used with other secret store [backends](../reference/clan.core/vars.md#clan.core.vars.settings.secretStore).
## Create Your Admin Keypair
To get started, you'll need to create **your admin keypair**.
!!! info
Don't worry — if you've already made one before, this step won't change or overwrite it.
```bash
clan secrets key generate
```
**Output**:
```{.console, .no-copy}
Public key: age1wkth7uhpkl555g40t8hjsysr20drq286netu8zptw50lmqz7j95sw2t3l7
Generated age private key at '/home/joerg/.config/sops/age/keys.txt' for your user. Please back it up on a secure location or you will lose access to your secrets.
Also add your age public key to the repository with 'clan secrets users add YOUR_USER age1wkth7uhpkl555g40t8hjsysr20drq286netu8zptw50lmqz7j95sw2t3l7' (replace YOUR_USER with your actual username)
```
!!! warning
Make sure to keep a safe backup of the private key you've just created.
If it's lost, you won't be able to get to your secrets anymore because they all need the admin key to be unlocked.
If you already have an [age] secret key and want to use that instead, you can simply edit `~/.config/sops/age/keys.txt`:
```title="~/.config/sops/age/keys.txt"
AGE-SECRET-KEY-13GWMK0KNNKXPTJ8KQ9LPSQZU7G3KU8LZDW474NX3D956GGVFAZRQTAE3F4
```
Alternatively, you can provide your [age] secret key as an environment variable `SOPS_AGE_KEY`, or in a different file
using `SOPS_AGE_KEY_FILE`.
For more information see the [SOPS] guide on [encrypting with age].
!!! note
It's safe to add any secrets created by the clan CLI and placed in your repository to version control systems like `git`.
## Add Your Public Key(s)
```console
clan secrets users add $USER --age-key <your_public_key>
```
It's best to choose the same username as on your Setup/Admin Machine that you use to control the deployment with.
Once run this will create the following files:
```{.console, .no-copy}
sops/
└── users/
└── <your_username>/
└── key.json
```
If you followed the quickstart tutorial all necessary secrets are initialized at this point.
!!! note
You can add multiple age keys for a user by providing multiple `--age-key <your_public_key>` flags:
```console
clan secrets users add $USER \
--age-key <your_public_key_1> \
--age-key <your_public_key_2> \
...
```
## Manage Your Public Key(s)
You can list keys for your user with `clan secrets users get $USER`:
```console
clan secrets users get alice
[
{
"publickey": "age1hrrcspp645qtlj29krjpq66pqg990ejaq0djcms6y6evnmgglv5sq0gewu",
"type": "age",
"username": "alice"
},
{
"publickey": "age13kh4083t3g4x3ktr52nav6h7sy8ynrnky2x58pyp96c5s5nvqytqgmrt79",
"type": "age",
"username": "alice"
}
]
```
To add a new key to your user:
```console
clan secrets users add-key $USER --age-key <your_public_key>
```
To remove a key from your user:
```console
clan secrets users remove-key $USER --age-key <your_public_key>
```
[age]: https://github.com/FiloSottile/age
[age plugin]: https://github.com/FiloSottile/awesome-age?tab=readme-ov-file#plugins
[sops]: https://github.com/getsops/sops
[encrypting with age]: https://github.com/getsops/sops?tab=readme-ov-file#encrypting-using-age
## Adding a Secret
### Adding a Secret
```shellSession
clan secrets set mysecret
Paste your secret:
```
## Retrieving a Stored Secret
### Retrieving a Stored Secret
```bash
clan secrets get mysecret
```
## List all Secrets
### List all Secrets
```bash
clan secrets list
```
## NixOS integration
### NixOS integration
A NixOS machine will automatically import all secrets that are encrypted for the
current machine. At runtime it will use the host key to decrypt all secrets into
@@ -153,7 +37,7 @@ In your nixos configuration you can get a path to secrets like this `config.sops
}
```
## Assigning Access
### Assigning Access
When using `clan secrets set <secret>` without arguments, secrets are encrypted for the key of the user named like your current $USER.

View File

@@ -4,21 +4,7 @@ hide:
- toc
---
# :material-home: What is Clan?
[Clan](https://clan.lol/) is a peer-to-peer computer management framework that
empowers you to reclaim control over your digital computing experience. Built on
NixOS, Clan provides a unified interface for managing networks of machines with
automated [secret management](./guides/secrets.md), secure [mesh VPN
connectivity](./guides/mesh-vpn.md), and customizable installation images. Whether
you're running a homelab or building decentralized computing infrastructure,
Clan simplifies configuration management while restoring your independence from
closed computing ecosystems.
At the heart of Clan are [Clan Services](./reference/clanServices/index.md) - the core
concept that enables you to add functionality across multiple machines in your
network. While Clan ships with essential core services, you can [create custom
services](./guides/clanServices.md) tailored to your specific needs.
# :material-home: Welcome to **Clan**'s documentation
[Getting Started](./guides/getting-started/index.md){ .md-button }
@@ -52,7 +38,7 @@ services](./guides/clanServices.md) tailored to your specific needs.
Use Clan with [https://flake.parts/]()
- [Contribute](./developer/contributing/CONTRIBUTING.md)
- [Contribute](./guides/contributing/CONTRIBUTING.md)
---

View File

@@ -2,7 +2,6 @@
font-family: "Roboto";
src: url(./Roboto-Regular.ttf) format("truetype");
}
@font-face {
font-family: "Fira Code";
src: url(./FiraCode-VF.ttf) format("truetype");
@@ -21,9 +20,3 @@
.md-nav__item.md-nav__item--section > label > span {
color: var(--md-typeset-a-color);
}
.md-typeset h4 {
margin: 3em 0 0.5em;
font-weight: bold;
color: #7ebae4;
}

12
flake.lock generated
View File

@@ -31,11 +31,11 @@
]
},
"locked": {
"lastModified": 1753140376,
"narHash": "sha256-7lrVrE0jSvZHrxEzvnfHFE/Wkk9DDqb+mYCodI5uuB8=",
"lastModified": 1752718651,
"narHash": "sha256-PkaR0qmyP9q/MDN3uYa+RLeBA0PjvEQiM0rTDDBXkL8=",
"owner": "nix-community",
"repo": "disko",
"rev": "545aba02960caa78a31bd9a8709a0ad4b6320a5c",
"rev": "d5ad4485e6f2edcc06751df65c5e16572877db88",
"type": "github"
},
"original": {
@@ -51,11 +51,11 @@
]
},
"locked": {
"lastModified": 1753121425,
"narHash": "sha256-TVcTNvOeWWk1DXljFxVRp+E0tzG1LhrVjOGGoMHuXio=",
"lastModified": 1751413152,
"narHash": "sha256-Tyw1RjYEsp5scoigs1384gIg6e0GoBVjms4aXFfRssQ=",
"owner": "hercules-ci",
"repo": "flake-parts",
"rev": "644e0fc48951a860279da645ba77fe4a6e814c5e",
"rev": "77826244401ea9de6e3bac47c2db46005e1f30b5",
"type": "github"
},
"original": {

View File

@@ -229,8 +229,8 @@ in
};
inventory = lib.mkOption {
type = types.submoduleWith {
modules = [
type = types.submodule {
imports = [
{
_module.args = { inherit clanLib; };
_file = "clan interface";

View File

@@ -247,7 +247,7 @@ in
{
distributedServices = clanLib.inventory.mapInstances {
inherit (clanConfig) inventory exportsModule;
inherit flakeInputs directory;
inherit flakeInputs;
clanCoreModules = clan-core.clan.modules;
prefix = [ "distributedServices" ];
};

View File

@@ -1,8 +1,4 @@
# Wraps all services in one fixed point module
{
# TODO: consume directly from clan.config
directory,
}:
{
lib,
config,
@@ -33,8 +29,6 @@ in
{
_module.args._ctx = [ name ];
_module.args.exports' = config.exports;
_module.args.directory = directory;
}
)
./service-module.nix
@@ -77,5 +71,8 @@ in
};
default = { };
};
debug = mkOption {
default = lib.mapAttrsToList (_: service: service.exports) config.mappedServices;
};
};
}

View File

@@ -24,7 +24,6 @@ in
flakeInputs,
# The clan inventory
inventory,
directory,
clanCoreModules,
prefix ? [ ],
exportsModule,
@@ -129,7 +128,7 @@ in
_ctx = prefix;
};
modules = [
(import ./all-services-wrapper.nix { inherit directory; })
./all-services-wrapper.nix
] ++ modules;
};

View File

@@ -2,7 +2,6 @@
lib,
config,
_ctx,
directory,
...
}:
let
@@ -213,7 +212,7 @@ in
options.extraModules = lib.mkOption {
default = [ ];
type = types.listOf (types.either types.deferredModule types.str);
type = types.listOf (types.deferredModule);
};
})
];
@@ -756,14 +755,10 @@ in
instanceRes
// {
nixosModule = {
imports =
[
# Result of the applied 'perInstance = {...}: { nixosModule = { ... }; }'
instanceRes.nixosModule
]
++ (map (
s: if builtins.typeOf s == "string" then "${directory}/${s}" else s
) instanceCfg.roles.${roleName}.extraModules);
imports = [
# Result of the applied 'perInstance = {...}: { nixosModule = { ... }; }'
instanceRes.nixosModule
] ++ instanceCfg.roles.${roleName}.extraModules;
};
}

View File

@@ -45,7 +45,6 @@ let
};
in
clanLib.inventory.mapInstances {
directory = ./.;
clanCoreModules = { };
flakeInputs = flakeInputsFixture;
inherit inventory;
@@ -53,7 +52,6 @@ let
};
in
{
extraModules = import ./extraModules.nix { inherit clanLib; };
exports = import ./exports.nix { inherit lib clanLib; };
resolve_module_spec = import ./import_module_spec.nix { inherit lib callInventoryAdapter; };
test_simple =

View File

@@ -1,33 +0,0 @@
{ clanLib }:
let
clan = clanLib.clan {
self = { };
directory = ./.;
machines.jon = {
nixpkgs.hostPlatform = "x86_64-linux";
};
# A module that adds exports perMachine
modules.A = {
manifest.name = "A";
roles.peer = { };
};
inventory = {
instances.A = {
module.input = "self";
roles.peer.tags.all = { };
roles.peer.extraModules = [ ./oneOption.nix ];
};
};
};
in
{
test_1 = {
inherit clan;
expr = clan.config.nixosConfigurations.jon.config.testDebug;
expected = 42;
};
}

View File

@@ -1,6 +0,0 @@
{ lib, ... }:
{
options.testDebug = lib.mkOption {
default = 42;
};
}

View File

@@ -142,7 +142,7 @@ in
- The module MUST have at least `features = [ "inventory" ]` in the frontmatter section.
- The module MUST have a subfolder `roles` with at least one `{roleName}.nix` file.
For further information see: [Module Authoring Guide](../../developer/extensions/clanServices/index.md).
For further information see: [Module Authoring Guide](../../guides/authoring/clanServices/index.md).
???+ example
```nix
@@ -179,8 +179,8 @@ in
map (m: "'${m}'") (lib.attrNames (lib.filterAttrs (n: _v: !builtins.elem n allowedNames) moduleSet))
)}
See: https://docs.clan.lol/developer/extensions/clanServices/
And: https://docs.clan.lol/developer/extensions/clanServices/
See: https://docs.clan.lol/guides/clanServices/
And: https://docs.clan.lol/guides/authoring/clanServices/
'' moduleSet;
};

View File

@@ -7,7 +7,7 @@ from typing import TYPE_CHECKING, Any
from clan_lib.api import ApiResponse
from clan_lib.api.tasks import WebThread
from clan_lib.async_run import set_current_thread_opkey, set_should_cancel
from clan_lib.async_run import set_should_cancel
if TYPE_CHECKING:
from .middleware import Middleware
@@ -98,7 +98,7 @@ class ApiBridge(ABC):
*,
thread_name: str = "ApiBridgeThread",
wait_for_completion: bool = False,
timeout: float = 60.0 * 60, # 1 hour default timeout
timeout: float = 60.0,
) -> None:
"""Process an API request in a separate thread with cancellation support.
@@ -112,7 +112,6 @@ class ApiBridge(ABC):
def thread_task(stop_event: threading.Event) -> None:
set_should_cancel(lambda: stop_event.is_set())
set_current_thread_opkey(op_key)
try:
log.debug(
f"Processing {request.method_name} with args {request.args} "

View File

@@ -9,7 +9,6 @@ gi.require_version("Gtk", "4.0")
from clan_lib.api import ApiError, ErrorDataClass, SuccessDataClass
from clan_lib.api.directory import FileRequest
from clan_lib.async_run import get_current_thread_opkey
from clan_lib.clan.check import check_clan_valid
from clan_lib.flake import Flake
from gi.repository import Gio, GLib, Gtk
@@ -25,7 +24,7 @@ def remove_none(_list: list) -> list:
RESULT: dict[str, SuccessDataClass[list[str] | None] | ErrorDataClass] = {}
def get_clan_folder() -> SuccessDataClass[Flake] | ErrorDataClass:
def get_clan_folder(*, op_key: str) -> SuccessDataClass[Flake] | ErrorDataClass:
"""
Opens the clan folder using the GTK file dialog.
Returns the path to the clan folder or an error if it fails.
@@ -35,10 +34,7 @@ def get_clan_folder() -> SuccessDataClass[Flake] | ErrorDataClass:
title="Select Clan Folder",
initial_folder=str(Path.home()),
)
response = get_system_file(file_request)
op_key = response.op_key
response = get_system_file(file_request, op_key=op_key)
if isinstance(response, ErrorDataClass):
return response
@@ -74,13 +70,8 @@ def get_clan_folder() -> SuccessDataClass[Flake] | ErrorDataClass:
def get_system_file(
file_request: FileRequest,
file_request: FileRequest, *, op_key: str
) -> SuccessDataClass[list[str] | None] | ErrorDataClass:
op_key = get_current_thread_opkey()
if not op_key:
msg = "No operation key found in the current thread context."
raise RuntimeError(msg)
GLib.idle_add(gtk_open_file, file_request, op_key)
while RESULT.get(op_key) is None:

View File

@@ -21,12 +21,18 @@ class ArgumentParsingMiddleware(Middleware):
# Convert dictionary arguments to dataclass instances
reconciled_arguments = {}
for k, v in context.request.args.items():
if k == "op_key":
continue
# Get the expected argument type from the API
arg_class = self.api.get_method_argtype(context.request.method_name, k)
# Convert dictionary to dataclass instance
reconciled_arguments[k] = from_dict(arg_class, v)
# Add op_key to arguments
reconciled_arguments["op_key"] = context.request.op_key
# Create a new request with reconciled arguments
updated_request = BackendRequest(

View File

@@ -1,22 +1,13 @@
import json
import logging
import threading
import uuid
from http.server import BaseHTTPRequestHandler
from pathlib import Path
from typing import TYPE_CHECKING, Any
from urllib.parse import urlparse
from clan_lib.api import (
MethodRegistry,
SuccessDataClass,
dataclass_to_dict,
)
from clan_lib.api import MethodRegistry, SuccessDataClass, dataclass_to_dict
from clan_lib.api.tasks import WebThread
from clan_lib.async_run import (
set_current_thread_opkey,
set_should_cancel,
)
from clan_app.api.api_bridge import ApiBridge, BackendRequest, BackendResponse
@@ -333,34 +324,17 @@ class HttpBridge(ApiBridge, BaseHTTPRequestHandler):
msg = f"Operation key '{op_key}' is already in use. Please try again."
raise ValueError(msg)
def process_request_in_thread(
self,
request: BackendRequest,
*,
thread_name: str = "ApiBridgeThread",
wait_for_completion: bool = False,
timeout: float = 60.0 * 60, # 1 hour default timeout
) -> None:
pass
def _process_api_request_in_thread(
self, api_request: BackendRequest, method_name: str
) -> None:
"""Process the API request in a separate thread."""
stop_event = threading.Event()
request = api_request
op_key = request.op_key or "unknown"
set_should_cancel(lambda: stop_event.is_set())
set_current_thread_opkey(op_key)
curr_thread = threading.current_thread()
self.threads[op_key] = WebThread(thread=curr_thread, stop_event=stop_event)
log.debug(
f"Processing {request.method_name} with args {request.args} "
f"and header {request.header}"
# Use the inherited thread processing method
self.process_request_in_thread(
api_request,
thread_name="HttpThread",
wait_for_completion=True,
timeout=60.0,
)
self.process_request(request)
def log_message(self, format: str, *args: Any) -> None: # noqa: A002
"""Override default logging to use our logger."""

View File

@@ -0,0 +1,39 @@
version: "0.5"
processes:
# App Dev
clan-app-ui:
namespace: "app"
command: |
cd $(git rev-parse --show-toplevel)/pkgs/clan-app/ui-2d
npm install
vite
ready_log_line: "VITE"
clan-app:
namespace: "app"
command: |
cd $(git rev-parse --show-toplevel)/pkgs/clan-app
./bin/clan-app --debug --content-uri http://localhost:3000
depends_on:
clan-app-ui:
condition: "process_log_ready"
is_foreground: true
ready_log_line: "Debug mode enabled"
# Storybook Dev
storybook:
namespace: "storybook"
command: |
cd $(git rev-parse --show-toplevel)/pkgs/clan-app/ui-2d
npm run storybook-dev -- --ci
ready_log_line: "started"
luakit:
namespace: "storybook"
command: "luakit http://localhost:6006"
depends_on:
storybook:
condition: "process_log_ready"

View File

@@ -17,7 +17,6 @@
"@solidjs/router": "^0.15.3",
"@tanstack/eslint-plugin-query": "^5.51.12",
"@tanstack/solid-query": "^5.76.0",
"@tanstack/solid-query-devtools": "^5.83.0",
"solid-js": "^1.9.7",
"solid-toast": "^0.5.0",
"three": "^0.176.0",
@@ -54,6 +53,7 @@
"postcss": "^8.4.38",
"postcss-url": "^10.1.3",
"prettier": "^3.2.5",
"solid-devtools": "^0.34.0",
"storybook": "^9.0.8",
"swagger-ui-dist": "^5.26.2",
"tailwindcss": "^3.4.3",
@@ -360,6 +360,22 @@
"@babel/core": "^7.0.0-0"
}
},
"node_modules/@babel/plugin-syntax-typescript": {
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.27.1.tgz",
"integrity": "sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.27.1"
},
"engines": {
"node": ">=6.9.0"
},
"peerDependencies": {
"@babel/core": "^7.0.0-0"
}
},
"node_modules/@babel/runtime": {
"version": "7.27.6",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.6.tgz",
@@ -1536,6 +1552,13 @@
"node": ">= 8"
}
},
"node_modules/@nothing-but/utils": {
"version": "0.17.0",
"resolved": "https://registry.npmjs.org/@nothing-but/utils/-/utils-0.17.0.tgz",
"integrity": "sha512-TuCHcHLOqDL0SnaAxACfuRHBNRgNJcNn9X0GiH5H3YSDBVquCr3qEIG3FOQAuMyZCbu9w8nk2CHhOsn7IvhIwQ==",
"dev": true,
"license": "MIT"
},
"node_modules/@oxc-resolver/binding-darwin-arm64": {
"version": "11.5.0",
"resolved": "https://registry.npmjs.org/@oxc-resolver/binding-darwin-arm64/-/binding-darwin-arm64-11.5.0.tgz",
@@ -1790,6 +1813,64 @@
"@sinonjs/commons": "^3.0.1"
}
},
"node_modules/@solid-devtools/debugger": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@solid-devtools/debugger/-/debugger-0.28.1.tgz",
"integrity": "sha512-6qIUI6VYkXoRnL8oF5bvh2KgH71qlJ18hNw/mwSyY6v48eb80ZR48/5PDXufUa3q+MBSuYa1uqTMwLewpay9eg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@nothing-but/utils": "~0.17.0",
"@solid-devtools/shared": "^0.20.0",
"@solid-primitives/bounds": "^0.1.1",
"@solid-primitives/event-listener": "^2.4.1",
"@solid-primitives/keyboard": "^1.3.1",
"@solid-primitives/rootless": "^1.5.1",
"@solid-primitives/scheduled": "^1.5.1",
"@solid-primitives/static-store": "^0.1.1",
"@solid-primitives/utils": "^6.3.1"
},
"peerDependencies": {
"solid-js": "^1.9.0"
}
},
"node_modules/@solid-devtools/shared": {
"version": "0.20.0",
"resolved": "https://registry.npmjs.org/@solid-devtools/shared/-/shared-0.20.0.tgz",
"integrity": "sha512-o5TACmUOQsxpzpOKCjbQqGk8wL8PMi+frXG9WNu4Lh3PQVUB6hs95Kl/S8xc++zwcMguUKZJn8h5URUiMOca6Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"@nothing-but/utils": "~0.17.0",
"@solid-primitives/event-listener": "^2.4.1",
"@solid-primitives/media": "^2.3.1",
"@solid-primitives/refs": "^1.1.1",
"@solid-primitives/rootless": "^1.5.1",
"@solid-primitives/scheduled": "^1.5.1",
"@solid-primitives/static-store": "^0.1.1",
"@solid-primitives/styles": "^0.1.1",
"@solid-primitives/utils": "^6.3.1"
},
"peerDependencies": {
"solid-js": "^1.9.0"
}
},
"node_modules/@solid-primitives/bounds": {
"version": "0.1.3",
"resolved": "https://registry.npmjs.org/@solid-primitives/bounds/-/bounds-0.1.3.tgz",
"integrity": "sha512-UbiyKMdSPmtijcEDnYLQL3zzaejpwWDAJJ4Gt5P0hgVs6A72piov0GyNw7V2SroH7NZFwxlYS22YmOr8A5xc1Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"@solid-primitives/event-listener": "^2.4.3",
"@solid-primitives/resize-observer": "^2.1.3",
"@solid-primitives/static-store": "^0.1.2",
"@solid-primitives/utils": "^6.3.2"
},
"peerDependencies": {
"solid-js": "^1.6.12"
}
},
"node_modules/@solid-primitives/event-listener": {
"version": "2.4.3",
"resolved": "https://registry.npmjs.org/@solid-primitives/event-listener/-/event-listener-2.4.3.tgz",
@@ -1802,6 +1883,21 @@
"solid-js": "^1.6.12"
}
},
"node_modules/@solid-primitives/keyboard": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/@solid-primitives/keyboard/-/keyboard-1.3.3.tgz",
"integrity": "sha512-9dQHTTgLBqyAI7aavtO+HnpTVJgWQA1ghBSrmLtMu1SMxLPDuLfuNr+Tk5udb4AL4Ojg7h9JrKOGEEDqsJXWJA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@solid-primitives/event-listener": "^2.4.3",
"@solid-primitives/rootless": "^1.5.2",
"@solid-primitives/utils": "^6.3.2"
},
"peerDependencies": {
"solid-js": "^1.6.12"
}
},
"node_modules/@solid-primitives/keyed": {
"version": "1.5.2",
"resolved": "https://registry.npmjs.org/@solid-primitives/keyed/-/keyed-1.5.2.tgz",
@@ -1889,6 +1985,16 @@
"solid-js": "^1.6.12"
}
},
"node_modules/@solid-primitives/scheduled": {
"version": "1.5.2",
"resolved": "https://registry.npmjs.org/@solid-primitives/scheduled/-/scheduled-1.5.2.tgz",
"integrity": "sha512-/j2igE0xyNaHhj6kMfcUQn5rAVSTLbAX+CDEBm25hSNBmNiHLu2lM7Usj2kJJ5j36D67bE8wR1hBNA8hjtvsQA==",
"dev": true,
"license": "MIT",
"peerDependencies": {
"solid-js": "^1.6.12"
}
},
"node_modules/@solid-primitives/static-store": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/@solid-primitives/static-store/-/static-store-0.1.2.tgz",
@@ -1922,6 +2028,20 @@
}
}
},
"node_modules/@solid-primitives/styles": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/@solid-primitives/styles/-/styles-0.1.2.tgz",
"integrity": "sha512-7iX5K+J5b1PRrbgw3Ki92uvU2LgQ0Kd/QMsrAZxDg5dpUBwMyTijZkA3bbs1ikZsT1oQhS41bTyKbjrXeU0Awg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@solid-primitives/rootless": "^1.5.2",
"@solid-primitives/utils": "^6.3.2"
},
"peerDependencies": {
"solid-js": "^1.6.12"
}
},
"node_modules/@solid-primitives/trigger": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/@solid-primitives/trigger/-/trigger-1.2.2.tgz",
@@ -2161,19 +2281,9 @@
}
},
"node_modules/@tanstack/query-core": {
"version": "5.83.0",
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.83.0.tgz",
"integrity": "sha512-0M8dA+amXUkyz5cVUm/B+zSk3xkQAcuXuz5/Q/LveT4ots2rBpPTZOzd7yJa2Utsf8D2Upl5KyjhHRY+9lB/XA==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
}
},
"node_modules/@tanstack/query-devtools": {
"version": "5.81.2",
"resolved": "https://registry.npmjs.org/@tanstack/query-devtools/-/query-devtools-5.81.2.tgz",
"integrity": "sha512-jCeJcDCwKfoyyBXjXe9+Lo8aTkavygHHsUHAlxQKKaDeyT0qyQNLKl7+UyqYH2dDF6UN/14873IPBHchcsU+Zg==",
"version": "5.81.5",
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.81.5.tgz",
"integrity": "sha512-ZJOgCy/z2qpZXWaj/oxvodDx07XcQa9BF92c0oINjHkoqUPsmm3uG08HpTaviviZ/N9eP1f9CM7mKSEkIo7O1Q==",
"license": "MIT",
"funding": {
"type": "github",
@@ -2181,12 +2291,12 @@
}
},
"node_modules/@tanstack/solid-query": {
"version": "5.83.0",
"resolved": "https://registry.npmjs.org/@tanstack/solid-query/-/solid-query-5.83.0.tgz",
"integrity": "sha512-RF8Tv9+6+Kmzj+EafbTzvzzPq+J5SzHtc1Tz3D2MZ/EvlZTH+GL5q4HNnWK3emg7CB6WzyGnTuERmmWJaZs8/w==",
"version": "5.81.5",
"resolved": "https://registry.npmjs.org/@tanstack/solid-query/-/solid-query-5.81.5.tgz",
"integrity": "sha512-VqVXaxiJIsKA6B45uApF+RUD3g8Roj/vdAuGpHMjR+RyHqlyQ+hOwgmALkzlbkbIaWCQi8CJOvrbU6WOBuMOxA==",
"license": "MIT",
"dependencies": {
"@tanstack/query-core": "5.83.0"
"@tanstack/query-core": "5.81.5"
},
"funding": {
"type": "github",
@@ -2196,23 +2306,6 @@
"solid-js": "^1.6.0"
}
},
"node_modules/@tanstack/solid-query-devtools": {
"version": "5.83.0",
"resolved": "https://registry.npmjs.org/@tanstack/solid-query-devtools/-/solid-query-devtools-5.83.0.tgz",
"integrity": "sha512-Z0wQlAWXz/U2bJ/paMRBTDhMoPnB9Te6GmA21sXnI+nDnAAPZRcPxFBiCgYJS3eFsvbkdRGJwoUSQrdIgy0shg==",
"license": "MIT",
"dependencies": {
"@tanstack/query-devtools": "5.81.2"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
},
"peerDependencies": {
"@tanstack/solid-query": "^5.83.0",
"solid-js": "^1.6.0"
}
},
"node_modules/@testing-library/dom": {
"version": "10.4.0",
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.0.tgz",
@@ -6903,6 +6996,29 @@
"url": "https://github.com/sponsors/cyyynthia"
}
},
"node_modules/solid-devtools": {
"version": "0.34.3",
"resolved": "https://registry.npmjs.org/solid-devtools/-/solid-devtools-0.34.3.tgz",
"integrity": "sha512-ZQua959n+Zu3sLbm9g0IRjYUb1YYlYbu83PWLRoKbSsq0a3ItQNhnS2OBU7rQNmOKZiMexNo9Z3izas9BcOKDg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/core": "^7.27.4",
"@babel/plugin-syntax-typescript": "^7.27.1",
"@babel/types": "^7.27.6",
"@solid-devtools/debugger": "^0.28.1",
"@solid-devtools/shared": "^0.20.0"
},
"peerDependencies": {
"solid-js": "^1.9.0",
"vite": "^2.2.3 || ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0"
},
"peerDependenciesMeta": {
"vite": {
"optional": true
}
}
},
"node_modules/solid-js": {
"version": "1.9.7",
"resolved": "https://registry.npmjs.org/solid-js/-/solid-js-1.9.7.tgz",

View File

@@ -52,6 +52,7 @@
"postcss": "^8.4.38",
"postcss-url": "^10.1.3",
"prettier": "^3.2.5",
"solid-devtools": "^0.34.0",
"storybook": "^9.0.8",
"swagger-ui-dist": "^5.26.2",
"tailwindcss": "^3.4.3",
@@ -72,7 +73,6 @@
"@solidjs/router": "^0.15.3",
"@tanstack/eslint-plugin-query": "^5.51.12",
"@tanstack/solid-query": "^5.76.0",
"@tanstack/solid-query-devtools": "^5.83.0",
"solid-js": "^1.9.7",
"solid-toast": "^0.5.0",
"three": "^0.176.0",

View File

@@ -9,7 +9,6 @@ import { TextInput } from "@/src/components/Form/TextInput";
import { TextArea } from "@/src/components/Form/TextArea";
import { Checkbox } from "@/src/components/Form/Checkbox";
import { FieldProps } from "./Field";
import { HostFileInput } from "@/src/components/Form/HostFileInput";
const FieldsetExamples = (props: FieldsetProps) => (
<div class="flex flex-col gap-8">
@@ -27,7 +26,7 @@ const meta = {
<div
class={cx({
"w-[600px]": (context.args.orientation || "vertical") == "vertical",
"w-[512px]": context.args.orientation == "horizontal",
"w-[1024px]": context.args.orientation == "horizontal",
"bg-inv-acc-3": context.args.inverted,
})}
>
@@ -64,11 +63,6 @@ export const Default: Story = {
label="Bio"
input={{ placeholder: "Tell us a bit about yourself", rows: 8 }}
/>
<HostFileInput
{...props}
label="Profile pic"
onSelectFile={async () => "/home/foo/bar/baz/fizz/buzz/bla/bizz"}
/>
<Checkbox {...props} label="Accept Terms" required={true} />
</>
),

View File

@@ -1,11 +1,5 @@
div.form-field.host-file {
button {
@apply w-fit;
}
&.horizontal {
button {
@apply grow max-w-[18rem];
}
@apply w-1/2;
}
}

View File

@@ -47,10 +47,7 @@ export const HostFileInput = (props: HostFileInputProps) => {
})}
{...props}
>
<Orienter
orientation={props.orientation}
align={props.orientation == "horizontal" ? "center" : "start"}
>
<Orienter orientation={props.orientation} align={"start"}>
<Label
labelComponent={TextField.Label}
descriptionComponent={TextField.Description}

View File

@@ -5,7 +5,7 @@ div.orienter {
}
&.horizontal {
@apply flex-row justify-between gap-0;
@apply flex-row justify-start;
& > div.form-label {
@apply w-1/2 shrink;

View File

@@ -1,5 +1,5 @@
div.modal-content {
@apply min-w-[320px] max-w-[512px];
@apply max-w-[512px];
@apply rounded-md;
/* todo replace with a theme() color */
@@ -12,7 +12,7 @@ div.modal-content {
@apply border border-def-2 rounded-tl-md rounded-tr-md;
@apply border-b-def-3;
& > .modal-title {
& > .title {
@apply mx-auto;
}
}

View File

@@ -26,12 +26,7 @@ export const Modal = (props: ModalProps) => {
<KDialog.Portal mount={props.mount}>
<KDialog.Content class={cx("modal-content", props.class)}>
<div class="header">
<Typography
class="modal-title"
hierarchy="label"
family="mono"
size="xs"
>
<Typography class="title" hierarchy="label" family="mono" size="xs">
{props.title}
</Typography>
<KDialog.CloseButton

View File

@@ -1,157 +0,0 @@
import type { Meta, StoryObj } from "@kachurun/storybook-solid";
import {
createMemoryHistory,
MemoryRouter,
Route,
RouteSectionProps,
} from "@solidjs/router";
import { Sidebar } from "@/src/components/Sidebar/Sidebar";
import { Suspense } from "solid-js";
import { QueryClient, QueryClientProvider } from "@tanstack/solid-query";
import { addClanURI, resetStore } from "@/src/stores/clan";
import { SolidQueryDevtools } from "@tanstack/solid-query-devtools";
import { encodeBase64 } from "@/src/hooks/clan";
const defaultClanURI = "/home/brian/clans/my-clan";
const queryData = {
"/home/brian/clans/my-clan": {
details: {
name: "Brian's Clan",
uri: "/home/brian/clans/my-clan",
},
machines: {
europa: {
name: "Europa",
machineClass: "nixos",
},
ganymede: {
name: "Ganymede",
machineClass: "nixos",
},
},
},
"/home/brian/clans/davhau": {
details: {
name: "Dave's Clan",
uri: "/home/brian/clans/davhau",
},
machines: {
callisto: {
name: "Callisto",
machineClass: "nixos",
},
amalthea: {
name: "Amalthea",
machineClass: "nixos",
},
},
},
"/home/brian/clans/mic92": {
details: {
name: "Mic92's Clan",
uri: "/home/brian/clans/mic92",
},
machines: {
thebe: {
name: "Thebe",
machineClass: "nixos",
},
sponde: {
name: "Sponde",
machineClass: "nixos",
},
},
},
};
const staticSections = [
{
title: "Links",
links: [
{ label: "GitHub", path: "https://github.com/brian-the-dev" },
{ label: "Twitter", path: "https://twitter.com/brian_the_dev" },
{
label: "LinkedIn",
path: "https://www.linkedin.com/in/brian-the-dev/",
},
{
label: "Instagram",
path: "https://www.instagram.com/brian_the_dev/",
},
],
},
];
const meta: Meta<RouteSectionProps> = {
title: "Components/Sidebar",
component: Sidebar,
render: () => {
// set history to point to our test clan
const history = createMemoryHistory();
history.set({ value: `/clans/${encodeBase64(defaultClanURI)}` });
// reset local storage and then add each clan
resetStore();
Object.keys(queryData).forEach((uri) => addClanURI(uri));
return (
<div style="height: 670px;">
<MemoryRouter
history={history}
root={(props) => <Suspense>{props.children}</Suspense>}
>
<Route
path="/clans/:clanURI"
component={() => <Sidebar staticSections={staticSections} />}
>
<Route path="/" />
<Route
path="/machines/:machineID"
component={() => <h1>Machine</h1>}
/>
</Route>
</MemoryRouter>
<SolidQueryDevtools initialIsOpen={true} />
</div>
);
},
};
export default meta;
type Story = StoryObj<RouteSectionProps>;
export const Default: Story = {
args: {},
decorators: [
(Story: StoryObj) => {
const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: false,
staleTime: Infinity,
},
},
});
Object.entries(queryData).forEach(([clanURI, clan]) => {
queryClient.setQueryData(
["clans", encodeBase64(clanURI), "details"],
clan.details,
);
queryClient.setQueryData(
["clans", encodeBase64(clanURI), "machines"],
clan.machines || {},
);
});
return (
<QueryClientProvider client={queryClient}>
<Story />
</QueryClientProvider>
);
},
],
};

View File

@@ -1,28 +0,0 @@
import "./Sidebar.css";
import { SidebarHeader } from "@/src/components/Sidebar/SidebarHeader";
import { SidebarBody } from "@/src/components/Sidebar/SidebarBody";
export interface LinkProps {
path: string;
label?: string;
}
export interface SectionProps {
title: string;
links: LinkProps[];
}
export interface SidebarProps {
staticSections?: SectionProps[];
}
export const Sidebar = (props: SidebarProps) => {
return (
<>
<div class="sidebar">
<SidebarHeader />
<SidebarBody {...props} />
</div>
</>
);
};

View File

@@ -1,107 +0,0 @@
import "./SidebarHeader.css";
import Icon from "@/src/components/Icon/Icon";
import { DropdownMenu } from "@kobalte/core/dropdown-menu";
import { useNavigate } from "@solidjs/router";
import { Typography } from "../Typography/Typography";
import { createSignal, For, Suspense } from "solid-js";
import { useClanListQuery } from "@/src/queries/queries";
import { navigateToClan, useClanURI } from "@/src/hooks/clan";
import { clanURIs } from "@/src/stores/clan";
export const SidebarHeader = () => {
const navigate = useNavigate();
const [open, setOpen] = createSignal(false);
// get information about the current active clan
const clanURI = useClanURI();
const allClans = useClanListQuery(clanURIs());
const activeClan = () => allClans.find(({ data }) => data?.uri === clanURI);
return (
<div class="sidebar-header">
<Suspense fallback={"Loading..."}>
<DropdownMenu open={open()} onOpenChange={setOpen} sameWidth={true}>
<DropdownMenu.Trigger class="dropdown-trigger">
<div class="clan-label">
<div class="clan-icon">
<Typography
hierarchy="label"
size="s"
weight="bold"
inverted={true}
>
{activeClan()?.data?.name.charAt(0).toUpperCase()}
</Typography>
</div>
<Typography
hierarchy="label"
size="s"
weight="bold"
inverted={!open()}
>
{activeClan()?.data?.name}
</Typography>
</div>
<DropdownMenu.Icon>
<Icon icon={"CaretDown"} inverted={!open()} size="0.75rem" />
</DropdownMenu.Icon>
</DropdownMenu.Trigger>
<DropdownMenu.Portal>
<DropdownMenu.Content class="sidebar-dropdown-content">
<DropdownMenu.Item
class="dropdown-item"
onSelect={() => navigateToClan(navigate, clanURI)}
>
<Icon
icon="Settings"
size="0.75rem"
inverted={true}
color="tertiary"
/>
<Typography hierarchy="label" size="xs" weight="medium">
Settings
</Typography>
</DropdownMenu.Item>
<DropdownMenu.Group class="dropdown-group">
<DropdownMenu.GroupLabel class="dropdown-group-label">
<Typography
hierarchy="label"
family="mono"
size="xs"
color="tertiary"
>
YOUR CLANS
</Typography>
</DropdownMenu.GroupLabel>
<div class="dropdown-group-items">
<For each={allClans}>
{(clan) => (
<Suspense fallback={"Loading..."}>
<DropdownMenu.Item
class="dropdown-item"
onSelect={() =>
navigateToClan(navigate, clan.data!.uri)
}
>
<Typography
hierarchy="label"
size="xs"
weight="medium"
>
{clan.data?.name}
</Typography>
</DropdownMenu.Item>
</Suspense>
)}
</For>
</div>
</DropdownMenu.Group>
</DropdownMenu.Content>
</DropdownMenu.Portal>
</DropdownMenu>
</Suspense>
</div>
);
};

Some files were not shown because too many files have changed in this diff Show More