Compare commits

..

1 commit

Author SHA1 Message Date
748948bfbf WIP generic-device-plugin 2025-01-03 21:39:15 +01:00
61 changed files with 2012 additions and 1894 deletions

1
.gitignore vendored
View file

@ -1,4 +1,3 @@
.direnv
.pre-commit-config.yaml
result
.manifests

108
README.md
View file

@ -5,82 +5,74 @@ We use [Kubenix](https://kubenix.org/) to write Kubernetes deployments in Nix!
## Images used
Legend:
- ✨: Image built with Nix (including
[NixNG](https://github.com/nix-community/NixNG))
- ✨: Image built with Nix (including [NixNG](https://github.com/nix-community/NixNG))
- ✅: Official image or trusted publisher
- 🫤: Unofficial image
| Status | Image | Comments |
| ------ | ---------------------------------------------- | --------------------------------------------------------- |
| ✨ | `nixng-blog` | |
| ✨ | `nixng-dnsmasq` | |
| ✨ | `nixng-attic` | |
| ✨ | `nixng-ntfy-sh` | |
| ✨ | `nixng-radicale` | |
| ✨ | `nixng-jellyseerr` | |
| ✨ | `nixng-radarr` | |
| ✨ | `nixng-sonarr` | |
| ✨ | `nixng-bazarr` | |
| ✨ | `nixng-prowlarr` | |
| ✨ | `nixng-deluge` | |
| ✨ | `nixng-mealie` | |
| ✨ | `nixng-atuin` | |
| ✅ | `jellyfin/jellyfin` | |
| ✅ | `postgres:14` | Database for Atuin |
| ✅ | `ghcr.io/paperless-ngx/paperless-ngx` | |
| ✅ | `docker.io/library/redis:7` | Database for Paperless-ngx |
| ✅ | `nextcloud` | |
| ✅ | `postgres:15` | Database for Attic, Nextcloud, Paperless-ngx and Hedgedoc |
| ✅ | `inbucket/inbucket` | |
| ✅ | `lscr.io/linuxserver/syncthing` | |
| ✅ | `codeberg.org/forgejo/forgejo` | |
| ✅ | `ghcr.io/immich-app/immich-server` | |
| ✅ | `ghcr.io/immich-app/immich-machine-learning` | |
| ✅ | `docker.io/redis:6.2-alpine` | Database for Immich |
| ✅ | `docker.io/tensorchord/pgvecto-rs:pg14-v0.2.0` | Database for Immich |
| ✅ | `tombursch/kitchenowl` | |
| ✅ | `freshrss/freshrss` | |
| ✅ | `ubuntu/bind9` | |
| ✅ | `quay.io/hedgedoc/hedgedoc` | |
| 🫤 | `teddysun/kms` | |
| 🫤 | `mpepping/cyberchef` | |
| Status | Image | Comments |
| --- | --- | --- |
| ✨ | `nixng-blog` | |
| ✨ | `nixng-dnsmasq` | |
| ✨ | `nixng-attic` | |
| ✨ | `nixng-ntfy-sh` | |
| ✨ | `nixng-radicale` | |
| ✨ | `nixng-jellyseerr` | |
| ✨ | `nixng-radarr` | |
| ✨ | `nixng-sonarr` | |
| ✨ | `nixng-bazarr` | |
| ✨ | `nixng-prowlarr` | |
| ✅ | `jellyfin/jellyfin` | |
| ✅ | `linuxserver/deluge` | |
| ✅ | `ghcr.io/atuinsh/atuin` | |
| ✅ | `postgres:14` | Database for Atuin |
| ✅ | `ghcr.io/paperless-ngx/paperless-ngx` | |
| ✅ | `docker.io/library/redis:7` | Database for Paperless-ngx |
| ✅ | `nextcloud` | |
| ✅ | `postgres:15` | Database for Attic, Nextcloud, Paperless-ngx and Hedgedoc |
| ✅ | `inbucket/inbucket` | |
| ✅ | `lscr.io/linuxserver/syncthing` | |
| ✅ | `codeberg.org/forgejo/forgejo` | |
| ✅ | `pihole/pihole` | |
| ✅ | `ghcr.io/immich-app/immich-server` | |
| ✅ | `ghcr.io/immich-app/immich-machine-learning` | |
| ✅ | `docker.io/redis:6.2-alpine` | Database for Immich |
| ✅ | `docker.io/tensorchord/pgvecto-rs:pg14-v0.2.0` | Database for Immich |
| ✅ | `tombursch/kitchenowl` | |
| ✅ | `freshrss/freshrss` | |
| ✅ | `ubuntu/bind9` | |
| ✅ | `quay.io/hedgedoc/hedgedoc` | |
| 🫤 | `itzg/minecraft-server` | |
| 🫤 | `teddysun/kms` | |
| 🫤 | `mpepping/cyberchef` | |
## Acknowledgements
- [dns.nix](https://github.com/kirelagin/dns.nix): A Nix DSL for defining DNS
zones
- [flake-utils](https://github.com/numtide/flake-utils): Handy utilities to
develop Nix flakes
- [kubenix](https://kubenix.org/): Declare and deploy Kubernetes resources using
Nix
- [dns.nix](https://github.com/kirelagin/dns.nix): A Nix DSL for defining DNS zones
- [flake-utils](https://github.com/numtide/flake-utils): Handy utilities to develop Nix flakes
- [kubenix](https://kubenix.org/): Declare and deploy Kubernetes resources using Nix
- [nixhelm](https://github.com/farcaller/nixhelm): Nix-digestible Helm charts
- [sops-nix](https://github.com/Mic92/sops-nix): Sops secret management for Nix
## Prerequisites
To deploy to the Kubernetes cluster, first make sure you have an admin account
on the cluster. You can generate this using
`nix run '.#gen-k3s-cert' <username> <servername> ~/.kube`, assuming you have
SSH access to the master node. This puts a private key, signed certificate and a
kubeconfig in the kubeconfig directory
To deploy to the Kubernetes cluster, first make sure you have an admin account on the cluster.
You can generate this using `nix run '.#gen-k3s-cert' <username> <servername> ~/.kube`, assuming you have SSH access to the master node.
This puts a private key, signed certificate and a kubeconfig in the kubeconfig directory
## Bootstrapping
We are now ready to deploy to the Kubernetes cluster. Deployments are done
through an experimental Kubernetes feature called
[ApplySets](https://kubernetes.io/docs/tasks/manage-kubernetes-objects/declarative-config/#how-to-delete-objects).
We are now ready to deploy to the Kubernetes cluster.
Deployments are done through an experimental Kubernetes feature called [ApplySets](https://kubernetes.io/docs/tasks/manage-kubernetes-objects/declarative-config/#how-to-delete-objects).
Each applyset is responsible for a set number of resources within a namespace.
If the cluster has not been initialized yet, we must bootstrap it first. Run
these deployments:
If the cluster has not been initialized yet, we must bootstrap it first.
Run these deployments:
- `nix run '.#bootstrap-default-deploy'`
- `nix run '.#bootstrap-kube-system-deploy'`
## Deployment
Now the cluster has been initialized and we can deploy applications. To explore
which applications we can deploy, run `nix flake show`. Then, for each
application, run `nix run '.#<application>-deploy'`. Or, if you're lazy:
`nix flake show --json | jq -r '.packages."x86_64-linux"|keys[]' | grep -- -deploy | xargs -I{} nix run ".#{}"`.
Now the cluster has been initialized and we can deploy applications.
To explore which applications we can deploy, run `nix flake show`.
Then, for each application, run `nix run '.#<application>-deploy'`.
Or, if you're lazy: `nix flake show --json | jq -r '.packages."x86_64-linux"|keys[]' | grep -- -deploy | xargs -I{} nix run ".#{}"`.

View file

@ -2,31 +2,11 @@
set -euo pipefail
CREATE_LOCAL_GCROOT=false
while [[ "$#" -gt 0 ]]; do
case "$1" in
--help)
echo "Use --create-local-gcroot to create local GC root"
exit 0
;;
--create-local-gcroot)
CREATE_LOCAL_GCROOT=true
shift
;;
*)
echo "Unknown option: $1"
exit 1
;;
esac
done
first_server="${SERVERS%% *}"
previous_manifest=$(
envsubst <<EOF | ssh -T "root@$first_server"
if [[ -f "$GCROOTDIR/$NAME.yml" ]]; then
cat "$GCROOTDIR/$NAME.yml"
fi
previous_manifest=$(ssh -T "root@$first_server" << EOF
if [[ -f "$GCROOTDIR/${NAME}.yml" ]]; then
cat "$GCROOTDIR/${NAME}.yml"
fi
EOF
)
@ -34,35 +14,30 @@ set +e
if [ -z "$previous_manifest" ]; then
echo No previous manifest found!
else
$DYFF between <(echo "$previous_manifest") "$MANIFEST" \
$DYFF between <(echo $previous_manifest) $MANIFEST \
--exclude-regexp metadata.labels.kubenix/hash \
--exclude-regexp labels.kubenix/hash \
--set-exit-code
--set-exit-code
if [ $? -eq 0 ]; then
exit 0
fi
fi
set -e
read -r -p "Continue? " _
read -r -p "Continue? " response
echo Uploading closure...
for server in $SERVERS; do
echo Uploading closure to "$server"...
nix copy --to "ssh://root@$server" "$MANIFEST"
echo Uploading closure to $server...
nix copy --to "ssh://root@$server.dmz" $MANIFEST
ssh "root@$server.dmz" "mkdir -p $GCROOTDIR && ln -sf $MANIFEST $GCROOTDIR/${NAME}.yml"
done
echo Applying Kubernetes manifest...
export KUBECTL_APPLYSET=true
vals eval -fail-on-missing-key-in-map <"$MANIFEST" |
kubectl apply -f - \
--prune \
--applyset applyset-"$NAME" \
--namespace "$NAMESPACE"
echo Creating GC roots
for server in $SERVERS; do
ssh "root@$server" "mkdir -p $GCROOTDIR && ln -sf $MANIFEST $GCROOTDIR/${NAME}.yml"
done
if $CREATE_LOCAL_GCROOT; then
mkdir -p ./.manifests
ln -sf "$MANIFEST" "./.manifests/${NAME}.yml"
fi
vals eval -fail-on-missing-key-in-map <$MANIFEST | \
kubectl apply -f - \
--prune \
--applyset applyset-$NAME \
--namespace $NAMESPACE

View file

@ -69,6 +69,11 @@
namespace = "syncthing";
};
pihole = {
module.pihole.enable = true;
namespace = "dns";
};
immich = {
module.immich.enable = true;
namespace = "immich";
@ -104,6 +109,11 @@
namespace = "kube-system";
};
minecraft = {
module.minecraft.enable = true;
namespace = "minecraft";
};
tailscale = {
module.tailscale.enable = true;
namespace = "tailscale";
@ -113,14 +123,4 @@
module.ntfy.enable = true;
namespace = "ntfy";
};
authentik = {
module.authentik.enable = true;
namespace = "authentik";
};
mealie = {
module.mealie.enable = true;
namespace = "mealie";
};
}

98
docs/longhorn.md Normal file
View file

@ -0,0 +1,98 @@
# Longhorn notes
## Troubleshooting
```
Multi-Attach error for volume "prowlarr" Volume is already exclusively attached to one node and can't be attached to another
```
I solved the above problem like this:
```
kubectl get volumeattachments | grep prowlarr
csi-f13ee1f46a4acc0d7e4abe8a3c993c7e043e9a55cd7573bda3499085654b493a driver.longhorn.io prowlarr lewis true 3m38s
kubectl delete volumeattachments csi-f13ee1f46a4acc0d7e4abe8a3c993c7e043e9a55cd7573bda3499085654b493a
kubectl rollout restart -n media deployment prowlarr
```
```
driver name driver.longhorn.io not found in the list of registered CSI drivers
```
I solved this by restarting k3s:
```
systemctl restart k3s
```
## Migration from NFS to Longhorn
1. Delete the workload, and delete the PVC and PVC using NFS.
2. Create Longhorn volumes as described below.
3. Copy NFS data from lewis.dmz to local disk.
4. Spin up a temporary pod and mount the Longhorn volume(s) in it:
```nix
{
pods.testje.spec = {
containers.testje = {
image = "nginx";
volumeMounts = [
{
name = "uploads";
mountPath = "/hedgedoc/public/uploads";
}
];
};
volumes = {
uploads.persistentVolumeClaim.claimName = "hedgedoc-uploads";
};
};
}
```
5. Use `kubectl cp` to copy the data from the local disk to the pod.
6. Delete the temporary pod.
7. Be sure to set the group ownership of the mount to the correct GID.
7. Create the workload with updated volume mounts.
8. Delete the data from local disk.
## Creation of new Longhorn volumes
While it seems handy to use a K8s StorageClass for Longhorn, we do *not* want to use that.
If you use a StorageClass, a PV and Longhorn volume will be automatically provisioned.
These will have the name `pvc-<UID of PVC>`, where the UID of the PVC is random.
This makes it hard to restore a backup to a Longhorn volume with the correct name.
Instead, we want to manually create the Longhorn volumes via the web UI.
Then, we can create the PV and PVC as usual using our K8s provisioning tool (e.g. Kubectl/Kubenix).
Follow these actions to create a Volume:
1. Using the Longhorn web UI, create a new Longhorn volume, keeping the following in mind:
- The size can be some more than what we expect to reasonable use. We use storage-overprovisioning, so the total size of volumes can exceed real disk size.
- The number of replicas should be 2.
2. Enable the "backup-nfs" recurring job for the Longhorn volume.
3. Disable the "default" recurring job group for the Longhorn volume.
4. Create the PV, PVC and workload as usual.
## Disaster recovery using Longhorn backups
Backing up Longhorn volumes is very easy, but restoring them is more tricky.
We consider here the case when all our machines are wiped, and all we have left is Longhorn backups.
To restore a backup, perform the following actions:
1. Restore the latest snapshot in the relevant Longhorn backup, keeping the following in mind:
- The name should remain the same (i.e. the one chosen at Longhorn volume creation).
- The number of replicas should be 2.
- Disable recurring jobs.
2. Enable the "backup-nfs" recurring job for the Longhorn volume.
3. Disable the "default" recurring job group for the Longhorn volume.
4. Create the PV, PVC and workload as usual.
## Recovering Longhorn volumes without a Kubernetes cluster
1. Navigate to the Longhorn backupstore location (`/mnt/longhorn/persistent/longhorn-backup/backupstore/volumes` for us).
2. Find the directory for the desired volume: `ls **/**`.
3. Determine the last backup for the volume: `cat volume.cfg | jq '.LastBackupName'`.
4. Find the blocks and the order that form the volume: `cat backups/<name>.cfg | jq '.Blocks'`.
5. Extract each block using lz4: `lz4 -d blocks/XX/YY/XXYY.blk block`.
6. Append the blocks to form the file system: `cat block1 block2 block3 > volume.img`
7. Lastly we need to fix the size of the image. We can simply append zero's to the end until the file is long enough so `fsck.ext4` does not complain anymore.
8. Mount the image: `mount -o loop volume.img /mnt/volume`.

734
flake.lock generated
View file

@ -6,11 +6,11 @@
"nixpkgs": "nixpkgs"
},
"locked": {
"lastModified": 1749306247,
"narHash": "sha256-4IKXo51HPvYB8t3FC7QBhQZGXANCgMrEvdgvBjv2SIk=",
"lastModified": 1735508994,
"narHash": "sha256-SMMX3irZ4Y+0QEAq0mOYEnJIYRe3YnXHrkCSRvdxHxU=",
"ref": "refs/heads/master",
"rev": "ba47dfab6c5e8f3b91db61789cfd4b7b88ce6b02",
"revCount": 27,
"rev": "433c1ef4b5874e2c4782be7322604d17182035ab",
"revCount": 23,
"type": "git",
"url": "https://git.kun.is/pim/blog"
},
@ -19,6 +19,47 @@
"url": "https://git.kun.is/pim/blog"
}
},
"deploy-rs": {
"inputs": {
"flake-compat": "flake-compat_4",
"nixpkgs": "nixpkgs_4",
"utils": "utils"
},
"locked": {
"lastModified": 1727447169,
"narHash": "sha256-3KyjMPUKHkiWhwR91J1YchF6zb6gvckCAY1jOE+ne0U=",
"owner": "serokell",
"repo": "deploy-rs",
"rev": "aa07eb05537d4cd025e2310397a6adcedfe72c76",
"type": "github"
},
"original": {
"owner": "serokell",
"repo": "deploy-rs",
"type": "github"
}
},
"disko": {
"inputs": {
"nixpkgs": [
"servers",
"nixpkgs"
]
},
"locked": {
"lastModified": 1729712798,
"narHash": "sha256-a+Aakkb+amHw4biOZ0iMo8xYl37uUL48YEXIC5PYJ/8=",
"owner": "nix-community",
"repo": "disko",
"rev": "09a776702b004fdf9c41a024e1299d575ee18a7d",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "disko",
"type": "github"
}
},
"dns": {
"inputs": {
"flake-utils": "flake-utils",
@ -27,11 +68,33 @@
]
},
"locked": {
"lastModified": 1737653493,
"narHash": "sha256-qTbv8Pm9WWF63M5Fj0Od9E54/lsbMSQUBHw/s30eFok=",
"lastModified": 1733919067,
"narHash": "sha256-ZsL5pKwEDhcZhVJh+3IwgHus7kSW/N8qOlBscwB6BCI=",
"owner": "kirelagin",
"repo": "dns.nix",
"rev": "96e548ae8bd44883afc5bddb9dacd0502542276d",
"rev": "a23f43f9762aa96d3e35c8eeefa7610bd0cdf456",
"type": "github"
},
"original": {
"owner": "kirelagin",
"repo": "dns.nix",
"type": "github"
}
},
"dns_2": {
"inputs": {
"flake-utils": "flake-utils_5",
"nixpkgs": [
"servers",
"nixpkgs"
]
},
"locked": {
"lastModified": 1726867691,
"narHash": "sha256-IK3r16N9pizf53AipOmrcrcyjVsPJwC4PI5hIqEyKwQ=",
"owner": "kirelagin",
"repo": "dns.nix",
"rev": "a3196708a56dee76186a9415c187473b94e6cbae",
"type": "github"
},
"original": {
@ -88,6 +151,70 @@
"type": "github"
}
},
"flake-compat_4": {
"flake": false,
"locked": {
"lastModified": 1696426674,
"narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=",
"owner": "edolstra",
"repo": "flake-compat",
"rev": "0f9255e01c2351cc7d116c072cb317785dd33b33",
"type": "github"
},
"original": {
"owner": "edolstra",
"repo": "flake-compat",
"type": "github"
}
},
"flake-compat_5": {
"flake": false,
"locked": {
"lastModified": 1696426674,
"narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=",
"owner": "edolstra",
"repo": "flake-compat",
"rev": "0f9255e01c2351cc7d116c072cb317785dd33b33",
"type": "github"
},
"original": {
"owner": "edolstra",
"repo": "flake-compat",
"type": "github"
}
},
"flake-compat_6": {
"flake": false,
"locked": {
"lastModified": 1673956053,
"narHash": "sha256-4gtG9iQuiKITOjNQQeQIpoIB6b16fm+504Ch3sNKLd8=",
"owner": "edolstra",
"repo": "flake-compat",
"rev": "35bb57c0c8d8b62bbfd284272c928ceb64ddbde9",
"type": "github"
},
"original": {
"owner": "edolstra",
"repo": "flake-compat",
"type": "github"
}
},
"flake-compat_7": {
"flake": false,
"locked": {
"lastModified": 1696426674,
"narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=",
"owner": "edolstra",
"repo": "flake-compat",
"rev": "0f9255e01c2351cc7d116c072cb317785dd33b33",
"type": "github"
},
"original": {
"owner": "edolstra",
"repo": "flake-compat",
"type": "github"
}
},
"flake-parts": {
"inputs": {
"nixpkgs-lib": [
@ -109,6 +236,28 @@
"type": "github"
}
},
"flake-parts_2": {
"inputs": {
"nixpkgs-lib": [
"servers",
"nix-snapshotter",
"nixpkgs"
]
},
"locked": {
"lastModified": 1704152458,
"narHash": "sha256-DS+dGw7SKygIWf9w4eNBUZsK+4Ug27NwEWmn2tnbycg=",
"owner": "hercules-ci",
"repo": "flake-parts",
"rev": "88a2cd8166694ba0b6cb374700799cec53aef527",
"type": "github"
},
"original": {
"owner": "hercules-ci",
"repo": "flake-parts",
"type": "github"
}
},
"flake-utils": {
"locked": {
"lastModified": 1614513358,
@ -147,11 +296,11 @@
"systems": "systems_4"
},
"locked": {
"lastModified": 1731533236,
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
"lastModified": 1710146030,
"narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
"rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a",
"type": "github"
},
"original": {
@ -163,6 +312,39 @@
"inputs": {
"systems": "systems_5"
},
"locked": {
"lastModified": 1710146030,
"narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"flake-utils_5": {
"locked": {
"lastModified": 1614513358,
"narHash": "sha256-LakhOx3S1dRjnh0b5Dg3mbZyH0ToC9I8Y2wKSkBaTzU=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "5466c5bbece17adaab2d82fae80b46e807611bf3",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"flake-utils_6": {
"inputs": {
"systems": "systems_8"
},
"locked": {
"lastModified": 1726560853,
"narHash": "sha256-X6rJYSESBVr3hBoH0WbKE5KvhPU5bloyZ2L4K60/fPQ=",
@ -201,14 +383,39 @@
"gitignore": "gitignore",
"nixpkgs": [
"nixpkgs"
]
],
"nixpkgs-stable": "nixpkgs-stable"
},
"locked": {
"lastModified": 1747372754,
"narHash": "sha256-2Y53NGIX2vxfie1rOW0Qb86vjRZ7ngizoo+bnXU9D9k=",
"lastModified": 1734797603,
"narHash": "sha256-ulZN7ps8nBV31SE+dwkDvKIzvN6hroRY8sYOT0w+E28=",
"owner": "cachix",
"repo": "git-hooks.nix",
"rev": "80479b6ec16fefd9c1db3ea13aeb038c60530f46",
"rev": "f0f0dc4920a903c3e08f5bdb9246bb572fcae498",
"type": "github"
},
"original": {
"owner": "cachix",
"repo": "git-hooks.nix",
"type": "github"
}
},
"git-hooks_2": {
"inputs": {
"flake-compat": "flake-compat_5",
"gitignore": "gitignore_2",
"nixpkgs": [
"servers",
"nixpkgs-unstable"
],
"nixpkgs-stable": "nixpkgs-stable_2"
},
"locked": {
"lastModified": 1730302582,
"narHash": "sha256-W1MIJpADXQCgosJZT8qBYLRuZls2KSiKdpnTVdKBuvU=",
"owner": "cachix",
"repo": "git-hooks.nix",
"rev": "af8a16fe5c264f5e9e18bcee2859b40a656876cf",
"type": "github"
},
"original": {
@ -238,6 +445,28 @@
"type": "github"
}
},
"gitignore_2": {
"inputs": {
"nixpkgs": [
"servers",
"git-hooks",
"nixpkgs"
]
},
"locked": {
"lastModified": 1709087332,
"narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=",
"owner": "hercules-ci",
"repo": "gitignore.nix",
"rev": "637db329424fd7e46cf4185293b9cc8c88c95394",
"type": "github"
},
"original": {
"owner": "hercules-ci",
"repo": "gitignore.nix",
"type": "github"
}
},
"globset": {
"inputs": {
"nixpkgs-lib": [
@ -304,14 +533,38 @@
"type": "github"
}
},
"kubenix_2": {
"inputs": {
"flake-compat": "flake-compat_6",
"nixpkgs": [
"servers",
"nixpkgs-unstable"
],
"systems": "systems_9",
"treefmt": "treefmt_2"
},
"locked": {
"lastModified": 1717788185,
"narHash": "sha256-Uc6QSQqJa2lyv/1W4StwoKrjtq7cFjlKNhdrtanToGo=",
"owner": "pizzapim",
"repo": "kubenix",
"rev": "a9590abe23a2f7577bc3271d90955e9ccc2923fe",
"type": "github"
},
"original": {
"owner": "pizzapim",
"repo": "kubenix",
"type": "github"
}
},
"nginx": {
"flake": false,
"locked": {
"lastModified": 1748296742,
"narHash": "sha256-E67DlHlrwomNSnRdjcnyOGhACtFbMz2jwm/glxeOxbg=",
"lastModified": 1735301654,
"narHash": "sha256-PHcSyHYyPUwPAls0BgtnGu2e936vhxW2nt7bQxDyGAQ=",
"owner": "nginx",
"repo": "nginx",
"rev": "5b8a5c08ce28639e788734b2528faad70baa113c",
"rev": "e3a9b6ad08a86e799a3d77da3f2fc507d3c9699e",
"type": "github"
},
"original": {
@ -329,11 +582,11 @@
]
},
"locked": {
"lastModified": 1729742964,
"narHash": "sha256-B4mzTcQ0FZHdpeWcpDYPERtyjJd/NIuaQ9+BV1h+MpA=",
"lastModified": 1703863825,
"narHash": "sha256-rXwqjtwiGKJheXB43ybM8NwWB8rO2dSRrEqes0S7F5Y=",
"owner": "nix-community",
"repo": "nix-github-actions",
"rev": "e04df33f62cdcf93d73e9a04142464753a16db67",
"rev": "5163432afc817cf8bd1f031418d1869e4c9d5547",
"type": "github"
},
"original": {
@ -344,11 +597,11 @@
},
"nix-kube-generators": {
"locked": {
"lastModified": 1729269463,
"narHash": "sha256-8jDDpC99fYl5CSHjZyPwb5PK7nQSknhkpfe8+DXI910=",
"lastModified": 1708155396,
"narHash": "sha256-A/BIeJjiRS7sBYP6tFJa/WHDPHe7DGTCkSEKXttYeAQ=",
"owner": "farcaller",
"repo": "nix-kube-generators",
"rev": "2be4f3cb99e179d9f94e6c8723862421437f8efb",
"rev": "14dbd5e5b40615937900f71d9a9851b59b4d9a88",
"type": "github"
},
"original": {
@ -380,6 +633,29 @@
"type": "github"
}
},
"nix-snapshotter_2": {
"inputs": {
"flake-compat": "flake-compat_7",
"flake-parts": "flake-parts_2",
"nixpkgs": [
"servers",
"nixpkgs-unstable"
]
},
"locked": {
"lastModified": 1729627456,
"narHash": "sha256-TCZdXCmnqCPsd3PjLv/LDSKJhTspLliL0DE+c/XP9BY=",
"owner": "pdtpartners",
"repo": "nix-snapshotter",
"rev": "f2957822a3748c91e678657a1cfd009b0440bbfd",
"type": "github"
},
"original": {
"owner": "pdtpartners",
"repo": "nix-snapshotter",
"type": "github"
}
},
"nixhelm": {
"inputs": {
"flake-utils": "flake-utils_3",
@ -391,11 +667,11 @@
"poetry2nix": "poetry2nix"
},
"locked": {
"lastModified": 1748482293,
"narHash": "sha256-Q3lyAFjJeR2d5kmiiSmb8ni121siv/ByjD2ueg2hUQA=",
"lastModified": 1735607967,
"narHash": "sha256-hdOdhQskvxyPPrf4w/k484xfVEVsqktHjwS0noTRRCw=",
"owner": "farcaller",
"repo": "nixhelm",
"rev": "0ecf1ebc80e65ff8bc37e8c852cac6cc149aea6e",
"rev": "6ce9cfd0e06bbf609af333069b3c4e84cd739755",
"type": "github"
},
"original": {
@ -412,19 +688,73 @@
"treefmt-nix": "treefmt-nix_2"
},
"locked": {
"lastModified": 1746371274,
"narHash": "sha256-7X65+TP0luFpQsA6KV80R05qnWp7NxMaIDryFfJ4MqI=",
"owner": "nix-community",
"lastModified": 1735726395,
"narHash": "sha256-rwhsZuwJzJ825Et7YI73G7+wHiPLFfx3SOnozWZfLJ0=",
"owner": "pizzapim",
"repo": "NixNG",
"rev": "ea02c5dd1f2e80ee7acc47f75ec3439ed480dd6f",
"rev": "dbbbb22d8feed064455a8653a0450b3da5a31424",
"type": "github"
},
"original": {
"owner": "nix-community",
"owner": "pizzapim",
"ref": "kubernetes",
"repo": "NixNG",
"type": "github"
}
},
"nixng_2": {
"inputs": {
"nixpkgs": [
"servers",
"nixpkgs"
]
},
"locked": {
"lastModified": 1726571270,
"narHash": "sha256-LEug48WOL+mmFYtKM57e/oudgjBk2Km5zIP3p27hF8I=",
"owner": "pizzapim",
"repo": "NixNG",
"rev": "9538892da603608f0176d07d33b1265e038c0adf",
"type": "github"
},
"original": {
"owner": "pizzapim",
"ref": "dnsmasq",
"repo": "NixNG",
"type": "github"
}
},
"nixos-facter-modules": {
"locked": {
"lastModified": 1730737399,
"narHash": "sha256-PzJrTMhHb9f46uMxmRD4GjnyVuNqxeyEvxaq7OierUQ=",
"owner": "numtide",
"repo": "nixos-facter-modules",
"rev": "c22b916f629fee6941a2976c62247b0bec68082b",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "nixos-facter-modules",
"type": "github"
}
},
"nixos-hardware": {
"locked": {
"lastModified": 1729742320,
"narHash": "sha256-u3Of8xRkN//me8PU+RucKA59/6RNy4B2jcGAF36P4jI=",
"owner": "NixOS",
"repo": "nixos-hardware",
"rev": "e8a2f6d5513fe7b7d15701b2d05404ffdc3b6dda",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "master",
"repo": "nixos-hardware",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1714076141,
@ -441,13 +771,45 @@
"type": "github"
}
},
"nixpkgs-bazarr": {
"locked": {
"lastModified": 1735086895,
"narHash": "sha256-893hOoQn5t9g0r57N0D8/G5WC4pPaNlprjAYO0TWRxc=",
"owner": "r-ryantm",
"repo": "nixpkgs",
"rev": "89e79d58769436a8cfd0d80ae28012d51134f2f3",
"type": "github"
},
"original": {
"owner": "r-ryantm",
"ref": "auto-update/bazarr",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs-jellyseerr": {
"locked": {
"lastModified": 1735406088,
"narHash": "sha256-Cwah5iXhOJ3cbrPYG5oeSyhQ7F7BsAabHTeezD4elh0=",
"owner": "coonce",
"repo": "nixpkgs",
"rev": "25569750ccc0c692128e667a77585d6d27ff7e57",
"type": "github"
},
"original": {
"owner": "coonce",
"ref": "jellyseerr",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs-master": {
"locked": {
"lastModified": 1748523262,
"narHash": "sha256-QGBY1wNWqtrT/9LYPcrie4e7EC44/CXGljkM2N/puWg=",
"lastModified": 1735935963,
"narHash": "sha256-i6xTJb3sb4BeWypD/DjAmslDzGXZUGU1OFJliaKFuuc=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "9e0df1205d7c2acb4504a25091ee603fd1dd9865",
"rev": "20166d17f391d2e11311baaa74344381fa44e4a0",
"type": "github"
},
"original": {
@ -457,6 +819,86 @@
"type": "github"
}
},
"nixpkgs-radicale": {
"locked": {
"lastModified": 1735496163,
"narHash": "sha256-oqUP98g0eqfzCDA/i88qRIBq4BIyxEk9um7dfNGiw+I=",
"owner": "erictapen",
"repo": "nixpkgs",
"rev": "e14050d0c94dc929543f7e4502fda8539d36536f",
"type": "github"
},
"original": {
"owner": "erictapen",
"ref": "radicale",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs-stable": {
"locked": {
"lastModified": 1730741070,
"narHash": "sha256-edm8WG19kWozJ/GqyYx2VjW99EdhjKwbY3ZwdlPAAlo=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "d063c1dd113c91ab27959ba540c0d9753409edf3",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-24.05",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs-stable_2": {
"locked": {
"lastModified": 1720386169,
"narHash": "sha256-NGKVY4PjzwAa4upkGtAMz1npHGoRzWotlSnVlqI40mo=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "194846768975b7ad2c4988bdb82572c00222c0d7",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-24.05",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs-stable_3": {
"locked": {
"lastModified": 1729357638,
"narHash": "sha256-66RHecx+zohbZwJVEPF7uuwHeqf8rykZTMCTqIrOew4=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "bb8c2cf7ea0dd2e18a52746b2c3a5b0c73b93c22",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "release-24.05",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs-unstable": {
"locked": {
"lastModified": 1729818716,
"narHash": "sha256-XRfkUsxLzFkMn3Tpstio1gNOIQ+2PZPCKbifJ2IXxlw=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "062c4f59744fcffa2e5aa3ef443dc8b4d1674ed6",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixpkgs-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs_2": {
"locked": {
"lastModified": 1726871744,
@ -475,11 +917,11 @@
},
"nixpkgs_3": {
"locked": {
"lastModified": 1748370509,
"narHash": "sha256-QlL8slIgc16W5UaI3w7xHQEP+Qmv/6vSNTpoZrrSlbk=",
"lastModified": 1735471104,
"narHash": "sha256-0q9NGQySwDQc7RhAV2ukfnu7Gxa5/ybJ2ANT8DQrQrs=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "4faa5f5321320e49a78ae7848582f684d64783e9",
"rev": "88195a94f390381c6afcdaa933c2f6ff93959cb4",
"type": "github"
},
"original": {
@ -491,11 +933,43 @@
},
"nixpkgs_4": {
"locked": {
"lastModified": 1747958103,
"narHash": "sha256-qmmFCrfBwSHoWw7cVK4Aj+fns+c54EBP8cGqp/yK410=",
"lastModified": 1702272962,
"narHash": "sha256-D+zHwkwPc6oYQ4G3A1HuadopqRwUY/JkMwHz1YF7j4Q=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "e97b3e4186bcadf0ef1b6be22b8558eab1cdeb5d",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixpkgs-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs_5": {
"locked": {
"lastModified": 1726871744,
"narHash": "sha256-V5LpfdHyQkUF7RfOaDPrZDP+oqz88lTJrMT1+stXNwo=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "fe51d34885f7b5e3e7b59572796e1bcb427eccb1",
"rev": "a1d92660c6b3b7c26fb883500a80ea9d33321be2",
"type": "github"
},
"original": {
"owner": "nixos",
"ref": "nixpkgs-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs_6": {
"locked": {
"lastModified": 1733097829,
"narHash": "sha256-9hbb1rqGelllb4kVUCZ307G2k3/UhmA8PPGBoyuWaSw=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "2c15aa59df0017ca140d9ba302412298ab4bf22a",
"type": "github"
},
"original": {
@ -517,11 +991,11 @@
"treefmt-nix": "treefmt-nix"
},
"locked": {
"lastModified": 1738741221,
"narHash": "sha256-UiTOA89yQV5YNlO1ZAp4IqJUGWOnTyBC83netvt8rQE=",
"lastModified": 1718285706,
"narHash": "sha256-DScsBM+kZvxOva7QegfdtleebMXh30XPxDQr/1IGKYo=",
"owner": "nix-community",
"repo": "poetry2nix",
"rev": "be1fe795035d3d36359ca9135b26dcc5321b31fb",
"rev": "a5be1bbbe0af0266147a88e0ec43b18c722f2bb9",
"type": "github"
},
"original": {
@ -542,8 +1016,67 @@
"nixhelm": "nixhelm",
"nixng": "nixng",
"nixpkgs": "nixpkgs_3",
"nixpkgs-bazarr": "nixpkgs-bazarr",
"nixpkgs-jellyseerr": "nixpkgs-jellyseerr",
"nixpkgs-master": "nixpkgs-master",
"nixpkgs-radicale": "nixpkgs-radicale",
"servers": "servers",
"treefmt-nix": "treefmt-nix_4"
}
},
"servers": {
"inputs": {
"deploy-rs": "deploy-rs",
"disko": "disko",
"dns": "dns_2",
"flake-utils": "flake-utils_6",
"git-hooks": "git-hooks_2",
"kubenix": "kubenix_2",
"nix-snapshotter": "nix-snapshotter_2",
"nixng": "nixng_2",
"nixos-facter-modules": "nixos-facter-modules",
"nixos-hardware": "nixos-hardware",
"nixpkgs": [
"nixpkgs"
],
"nixpkgs-unstable": "nixpkgs-unstable",
"sops-nix": "sops-nix",
"treefmt-nix": "treefmt-nix_3"
},
"locked": {
"lastModified": 1733068232,
"narHash": "sha256-iZJ/cq07OVk2TQy6UV9JaXgLARQqJedmuPIHTtgVeeo=",
"ref": "refs/heads/master",
"rev": "68b79e086c4cc6b850ba12c60f3a978d18bd41b1",
"revCount": 495,
"type": "git",
"url": "https://git.kun.is/home/nixos-servers"
},
"original": {
"type": "git",
"url": "https://git.kun.is/home/nixos-servers"
}
},
"sops-nix": {
"inputs": {
"nixpkgs": [
"servers",
"nixpkgs"
],
"nixpkgs-stable": "nixpkgs-stable_3"
},
"locked": {
"lastModified": 1729775275,
"narHash": "sha256-J2vtHq9sw1wWm0aTMXpEEAzsVCUMZDTEe5kiBYccpLE=",
"owner": "Mic92",
"repo": "sops-nix",
"rev": "78a0e634fc8981d6b564f08b6715c69a755c4c7d",
"type": "github"
},
"original": {
"owner": "Mic92",
"repo": "sops-nix",
"type": "github"
}
},
"systems": {
@ -621,6 +1154,20 @@
}
},
"systems_6": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"id": "systems",
"type": "indirect"
}
},
"systems_7": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
@ -635,6 +1182,35 @@
"type": "github"
}
},
"systems_8": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
},
"systems_9": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"id": "systems",
"type": "indirect"
}
},
"treefmt": {
"inputs": {
"nixpkgs": [
@ -665,11 +1241,11 @@
]
},
"locked": {
"lastModified": 1730120726,
"narHash": "sha256-LqHYIxMrl/1p3/kvm2ir925tZ8DkI0KA10djk8wecSk=",
"lastModified": 1717850719,
"narHash": "sha256-npYqVg+Wk4oxnWrnVG7416fpfrlRhp/lQ6wQ4DHI8YE=",
"owner": "numtide",
"repo": "treefmt-nix",
"rev": "9ef337e492a5555d8e17a51c911ff1f02635be15",
"rev": "4fc1c45a5f50169f9f29f6a98a438fb910b834ed",
"type": "github"
},
"original": {
@ -698,14 +1274,14 @@
},
"treefmt-nix_3": {
"inputs": {
"nixpkgs": "nixpkgs_4"
"nixpkgs": "nixpkgs_5"
},
"locked": {
"lastModified": 1748243702,
"narHash": "sha256-9YzfeN8CB6SzNPyPm2XjRRqSixDopTapaRsnTpXUEY8=",
"lastModified": 1730025913,
"narHash": "sha256-Y9NtFmP8ciLyRsopcCx1tyoaaStKeq+EndwtGCgww7I=",
"owner": "numtide",
"repo": "treefmt-nix",
"rev": "1f3f7b784643d488ba4bf315638b2b0a4c5fb007",
"rev": "bae131e525cc8718da22fbeb8d8c7c43c4ea502a",
"type": "github"
},
"original": {
@ -713,6 +1289,64 @@
"repo": "treefmt-nix",
"type": "github"
}
},
"treefmt-nix_4": {
"inputs": {
"nixpkgs": "nixpkgs_6"
},
"locked": {
"lastModified": 1735649548,
"narHash": "sha256-/4pTzlmABhx26AOTYvFN1OTCxJJL/LBUB49giqoMhJA=",
"owner": "numtide",
"repo": "treefmt-nix",
"rev": "597705118f16d1dcd0fef99707700d13b2b324d7",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "treefmt-nix",
"type": "github"
}
},
"treefmt_2": {
"inputs": {
"nixpkgs": [
"servers",
"kubenix",
"nixpkgs"
]
},
"locked": {
"lastModified": 1688026376,
"narHash": "sha256-qJmkr9BWDpqblk4E9/rCsAEl39y2n4Ycw6KRopvpUcY=",
"owner": "numtide",
"repo": "treefmt-nix",
"rev": "df3f32b0cc253dfc7009b7317e8f0e7ccd70b1cf",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "treefmt-nix",
"type": "github"
}
},
"utils": {
"inputs": {
"systems": "systems_7"
},
"locked": {
"lastModified": 1701680307,
"narHash": "sha256-kAuep2h5ajznlPMD9rnQyffWG8EM/C73lejGofXvdM8=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "4022d587cbbfd70fe950c1e2083a02621806a725",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
}
},
"root": "root",

View file

@ -7,6 +7,9 @@
flake-utils.url = "github:numtide/flake-utils";
treefmt-nix.url = "github:numtide/treefmt-nix";
blog.url = "git+https://git.kun.is/pim/blog";
nixpkgs-jellyseerr.url = "github:coonce/nixpkgs?ref=jellyseerr";
nixpkgs-bazarr.url = "github:r-ryantm/nixpkgs?ref=auto-update/bazarr";
nixpkgs-radicale.url = "github:erictapen/nixpkgs?ref=radicale";
git-hooks = {
url = "github:cachix/git-hooks.nix";
@ -28,8 +31,13 @@
inputs.nixpkgs.follows = "nixpkgs";
};
servers = {
url = "git+https://git.kun.is/home/nixos-servers";
inputs.nixpkgs.follows = "nixpkgs";
};
nixng = {
url = "github:nix-community/NixNG";
url = "github:pizzapim/NixNG/kubernetes";
inputs.nixpkgs.follows = "nixpkgs";
};
@ -52,6 +60,5 @@
./formatter.nix
./shell.nix
./nixng-configurations
./nixng-modules
];
}

View file

@ -1,25 +1,29 @@
_: {
{servers, ...}: let
globals = {
images = {
jellyfin = "jellyfin/jellyfin:10.10.7";
jellyfin = "jellyfin/jellyfin:10.10.3";
deluge = "linuxserver/deluge:2.1.1";
atuin = "ghcr.io/atuinsh/atuin:18.3.0";
postgres14 = "postgres:14";
kms = "teddysun/kms:latest";
paperless = "ghcr.io/paperless-ngx/paperless-ngx:2.16.2";
paperless = "ghcr.io/paperless-ngx/paperless-ngx:2.13.5";
redis7 = "docker.io/library/redis:7";
nextcloud = "nextcloud:30.0.6";
nextcloud = "nextcloud:30.0.2";
postgres15 = "postgres:15";
inbucket = "inbucket/inbucket:edge";
syncthing = "lscr.io/linuxserver/syncthing:1.29.6";
forgejo = "codeberg.org/forgejo/forgejo:11.0.1";
immich = "ghcr.io/immich-app/immich-server:v1.134.0";
immich-machine-learning = "ghcr.io/immich-app/immich-machine-learning:v1.134.0";
immich-redis = "docker.io/valkey/valkey:8-bookworm@sha256:42cba146593a5ea9a622002c1b7cba5da7be248650cbb64ecb9c6c33d29794b1";
immich-postgres = "ghcr.io/immich-app/postgres:14-vectorchord0.3.0-pgvectors0.2.0";
kitchenowl = "tombursch/kitchenowl:v0.6.15";
syncthing = "lscr.io/linuxserver/syncthing:1.28.0";
forgejo = "codeberg.org/forgejo/forgejo:9.0.2";
pihole = "pihole/pihole:2024.07.0";
immich = "ghcr.io/immich-app/immich-server:v1.122.3";
immich-machine-learning = "ghcr.io/immich-app/immich-machine-learning:v1.122.3";
immich-redis = "docker.io/redis:6.2-alpine@sha256:eaba718fecd1196d88533de7ba49bf903ad33664a92debb24660a922ecd9cac8";
immich-postgres = "docker.io/tensorchord/pgvecto-rs:pg14-v0.2.0@sha256:90724186f0a3517cf6914295b5ab410db9ce23190a2d9d0b9dd6463e3fa298f0";
kitchenowl = "tombursch/kitchenowl:v0.6.4";
cyberchef = "mpepping/cyberchef:latest";
freshrss = "freshrss/freshrss:1.26.3";
freshrss = "freshrss/freshrss:1.24.3";
bind9 = "ubuntu/bind9:9.18-22.04_beta";
hedgedoc = "quay.io/hedgedoc/hedgedoc:1.10.3";
hedgedoc = "quay.io/hedgedoc/hedgedoc:1.10.0";
minecraft = "itzg/minecraft-server:latest";
};
nodeLabels = {
@ -31,28 +35,7 @@ _: {
hasMedia = "true";
};
};
routerPublicIPv4 = "89.220.7.89";
bind9Ipv6 = "2a0d:6e00:1a77:30::134";
# Load balancer IPv4
traefikIPv4 = "192.168.30.128";
kmsIPv4 = "192.168.30.129";
inbucketIPv4 = "192.168.30.130";
gitIPv4 = "192.168.30.132";
delugeIPv4 = "192.168.30.133";
bind9IPv4 = "192.168.30.134";
dnsmasqIPv4 = "192.168.30.135";
jellyseerrIPv4 = "192.168.30.137";
syncthingIPv4 = "192.168.30.138";
radarrIPv4 = "192.168.30.140";
prowlarrIPv4 = "192.168.30.141";
sonarrIPv4 = "192.168.30.142";
bazarrIPv4 = "192.168.30.143";
paperlessIPv4 = "192.168.30.144";
radicaleIPv4 = "192.168.30.145";
freshrssIPv4 = "192.168.30.146";
immichIPv4 = "192.168.30.147";
nextcloudIPv4 = "192.168.30.148";
};
in {
globals = globals // servers.globals;
}

View file

@ -8,7 +8,7 @@ inputs @ {
flake-utils.lib.eachDefaultSystem
(system: let
pkgs = nixpkgs.legacyPackages.${system};
inherit (pkgs) lib;
lib = pkgs.lib;
deployScript = (pkgs.writeScriptBin "applyset-deploy.sh" (builtins.readFile ./applyset-deploy.sh)).overrideAttrs (old: {
buildCommand = "${old.buildCommand}\npatchShebangs $out";
});
@ -64,7 +64,7 @@ flake-utils.lib.eachDefaultSystem
pkgs.symlinkJoin
{
name = "applyset-deploy.sh";
paths = [deployScript pkgs.vals pkgs.kubectl pkgs.gettext];
paths = [deployScript pkgs.vals pkgs.kubectl];
buildInputs = [pkgs.makeWrapper];
passthru.manifest = result;
meta.mainProgram = "applyset-deploy.sh";

View file

@ -1,4 +1,5 @@
{
self,
utils,
lib,
config,
@ -40,8 +41,6 @@
};
spec = {
nodeName = "jefke";
containers.attic = {
image = utils.mkNixNGImage "attic";
ports.web.containerPort = 8080;
@ -67,12 +66,8 @@
};
volumes = {
data.persistentVolumeClaim.claimName = "data";
server.secret.secretName = "server";
data.hostPath = {
path = "/mnt/longhorn/persistent/volumes/attic";
type = "Directory";
};
};
securityContext = {
@ -96,8 +91,6 @@
};
spec = {
nodeName = "jefke";
containers.postgres = {
image = globals.images.postgres15;
imagePullPolicy = "IfNotPresent";
@ -122,10 +115,7 @@
];
};
volumes.data.hostPath = {
path = "/mnt/longhorn/persistent/volumes/attic-db";
type = "Directory";
};
volumes.data.persistentVolumeClaim.claimName = "database";
};
};
};
@ -167,6 +157,18 @@
portName = "web";
};
};
longhorn.persistentVolumeClaim = {
data = {
volumeName = "attic";
storage = "15Gi";
};
database = {
volumeName = "attic-db";
storage = "150Mi";
};
};
};
};
}

View file

@ -1,6 +1,5 @@
{
config,
utils,
globals,
lib,
...
@ -30,22 +29,35 @@
metadata.labels.app = "atuin";
spec = {
nodeName = "jefke";
volumes.database.hostPath = {
path = "/mnt/longhorn/persistent/volumes/atuin-db";
type = "Directory";
volumes = {
data.persistentVolumeClaim.claimName = "data";
database.persistentVolumeClaim.claimName = "database";
};
containers = {
atuin = {
image = utils.mkNixNGImage "atuin";
image = globals.images.atuin;
imagePullPolicy = "IfNotPresent";
ports.web.containerPort = 8888;
args = ["server" "start"];
env.ATUIN_DB_URI.valueFrom.secretKeyRef = {
name = "database";
key = "databaseURL";
env = {
ATUIN_HOST.value = "0.0.0.0";
ATUIN_PORT.value = "8888";
ATUIN_OPEN_REGISTRATION.value = "false";
ATUIN_DB_URI.valueFrom.secretKeyRef = {
name = "database";
key = "databaseURL";
};
};
volumeMounts = [
{
name = "data";
mountPath = "/config";
}
];
};
database = {
@ -93,6 +105,18 @@
portName = "web";
};
};
longhorn.persistentVolumeClaim = {
data = {
volumeName = "atuin";
storage = "300Mi";
};
database = {
volumeName = "atuin-db";
storage = "300Mi";
};
};
};
};
}

View file

@ -1,112 +0,0 @@
{
nixhelm,
system,
config,
lib,
...
}: {
options.authentik.enable = lib.mkEnableOption "authentik";
config = lib.mkIf config.authentik.enable {
kubernetes = {
helm.releases.authentik = {
chart = nixhelm.chartsDerivations.${system}.authentik.authentik;
includeCRDs = true;
namespace = "authentik";
values = {
authentik = {
email = {
host = "mail.smtp2go.com";
port = 2525;
from = "Authentik authentik@kun.is";
};
};
postgresql = {
enabled = true;
auth.password = "ref+sops://secrets.yml#/authentik/postgresql_password";
primary = {
persistence.enabled = false;
extraEnvVarsSecret = "postgresql-env";
extraVolumes = [
{
name = "data";
hostPath = {
path = "/mnt/longhorn/persistent/volumes/authentik-db";
type = "Directory";
};
}
];
};
};
redis = {
enabled = true;
master = {
persistence.enabled = false;
extraVolumes = [
{
name = "authentik-redis";
hostPath = {
path = "/mnt/longhorn/persistent/volumes/authentik-redis";
type = "Directory";
};
}
];
extraVolumeMounts = [
{
mountPath = "/data";
name = "authentik-redis";
}
];
};
};
};
};
resources = let
env = {
AUTHENTIK_POSTGRESQL__PASSWORD.value = "ref+sops://secrets.yml#/authentik/postgresql_password";
AUTHENTIK_SECRET_KEY.value = "ref+sops://secrets.yml#/authentik/secret_key";
AUTHENTIK_EMAIL__USERNAME.value = "ref+sops://secrets.yml#/smtp2go/username";
AUTHENTIK_EMAIL__PASSWORD.value = "ref+sops://secrets.yml#/smtp2go/password";
};
in {
secrets.postgresql-env.stringData = {
POSTGRES_PASSWORD = "ref+sops://secrets.yml#/authentik/postgresql_password";
};
deployments = {
authentik-server.spec.template.spec.containers.server.env = env;
authentik-worker.spec.template.spec.containers.worker.env = env;
};
statefulSets.authentik-postgresql.spec.template.spec.nodeName = "atlas";
statefulSets.authentik-redis-master.spec.template.spec.nodeName = "atlas";
};
};
lab = {
ingresses.authentik = {
host = "authentik.kun.is";
service = {
name = "authentik-server";
portName = "http";
};
};
tailscaleIngresses = {
tailscale-authentik = {
host = "authentik";
service = {
name = "authentik-server";
portName = "http";
};
};
};
};
};
}

View file

@ -117,7 +117,6 @@ in {
bind9-udp = {
metadata.annotations = {
"metallb.universe.tf/loadBalancerIPs" = "${globals.bind9IPv4},${globals.bind9Ipv6}";
# "metallb.universe.tf/loadBalancerIPs" = "${globals.bind9IPv4}";
"metallb.universe.tf/allow-shared-ip" = "dns";
};
@ -138,7 +137,6 @@ in {
bind9-tcp = {
metadata.annotations = {
"metallb.universe.tf/loadBalancerIPs" = "${globals.bind9IPv4},${globals.bind9Ipv6}";
# "metallb.universe.tf/loadBalancerIPs" = "${globals.bind9IPv4}";
"metallb.universe.tf/allow-shared-ip" = "dns";
};

View file

@ -5,7 +5,7 @@ with dns.lib.combinators; {
SOA = {
nameServer = "ns1";
adminEmail = "webmaster.kun.is";
serial = 2024041302;
serial = 2024041301;
};
NS = [
@ -24,15 +24,13 @@ with dns.lib.combinators; {
subdomains = rec {
"*".A = [globals.routerPublicIPv4];
ns1 = {
ns = {
A = [globals.routerPublicIPv4];
AAAA = [];
};
ns2 = {
A = ["192.145.57.90"];
AAAA = [];
};
ns1 = ns;
ns2 = ns;
wg = {
A = [globals.routerPublicIPv4];

View file

@ -15,9 +15,33 @@
chart = nixhelm.chartsDerivations.${system}.metallb.metallb;
includeCRDs = true;
};
# argo-workflows = {
# chart = nixhelm.chartsDerivations.${system}.argoproj.argo-workflows;
# includeCRDs = true;
# };
longhorn = {
chart = nixhelm.chartsDerivations.${system}.longhorn.longhorn;
includeCRDs = true;
values = {
persistence.defaultClassReplicaCount = 2;
service.ui.type = "LoadBalancer";
defaultSettings = {
defaultDataPath = "/mnt/longhorn";
storageMinimalAvailablePercentage = 0;
allowRecurringJobWhileVolumeDetached = true;
backupTarget = "nfs://lewis.dmz:/mnt/longhorn/persistent/longhorn-backup";
};
};
};
};
resources = {
services.longhorn-frontend.spec.loadBalancerIP = globals.longhornIPv4;
namespaces = {
static-websites = {};
freshrss = {};
@ -35,21 +59,93 @@
inbucket = {};
dns = {};
media = {};
minecraft = {};
tailscale = {};
ntfy = {};
authentik = {};
mealie = {};
};
nodes =
builtins.mapAttrs
(_name: labels: {
(name: labels: {
metadata.labels = labels;
})
globals.nodeLabels;
recurringJobs.backup-nfs.spec = {
cron = "0 1 * * *"; # One o'clock at night
task = "backup";
retain = 2; # We don't need many, as we also make Borg backups.
concurrency = 1;
};
ipAddressPools.main.spec.addresses = ["192.168.30.128-192.168.30.200" "2a0d:6e00:1a77:30::2-2a0d:6e00:1a77:30:ffff:ffff:ffff:fffe"];
l2Advertisements.main.metadata = {};
persistentVolumes = {
music-syncthing.spec = {
capacity.storage = "1Gi";
accessModes = ["ReadWriteMany"];
nfs = {
server = "lewis.dmz";
path = "/mnt/longhorn/persistent/media/music";
};
};
media-media.spec = {
capacity.storage = "1Gi";
accessModes = ["ReadWriteMany"];
nfs = {
server = "lewis.dmz";
path = "/mnt/longhorn/persistent/media";
};
};
};
};
};
lab = {
longhorn.persistentVolume = {
freshrss.storage = "1Gi";
radicale.storage = "200Mi";
atuin.storage = "300Mi";
atuin-db.storage = "300Mi";
nextcloud.storage = "50Gi";
nextcloud-db.storage = "400Mi";
hedgedoc-uploads.storage = "50Mi";
hedgedoc-db.storage = "100Mi";
kitchenowl.storage = "100Mi";
forgejo.storage = "20Gi";
paperless-data.storage = "10Gi";
paperless-redisdata.storage = "20Mi";
paperless-db.storage = "150Mi";
syncthing.storage = "400Mi";
pihole-data.storage = "750Mi";
pihole-dnsmasq.storage = "16Mi";
immich.storage = "50Gi";
immich-db.storage = "5Gi";
attic.storage = "15Gi";
attic-db.storage = "150Mi";
jellyfin.storage = "5Gi";
transmission.storage = "25Mi";
jellyseerr.storage = "75Mi";
radarr.storage = "300Mi";
prowlarr.storage = "150Mi";
sonarr.storage = "150Mi";
bazarr.storage = "25Mi";
minecraft.storage = "1Gi";
ntfy.storage = "300Mi";
deluge.storage = "500Mi";
};
tailscaleIngresses.tailscale-longhorn = {
host = "longhorn";
service = {
name = "longhorn-frontend";
portName = "http";
};
};
};
};

View file

@ -2,6 +2,7 @@
imports = [
./inbucket.nix
./tailscale-ingress.nix
./longhorn-volume.nix
./ingress.nix
./dummy-types.nix
./dnsmasq.nix
@ -19,6 +20,7 @@
./forgejo
./paperless.nix
./syncthing.nix
./pihole.nix
./immich.nix
./attic.nix
./bind9
@ -26,7 +28,6 @@
./traefik.nix
./tailscale.nix
./ntfy.nix
./authentik.nix
./mealie.nix
./minecraft.nix
];
}

View file

@ -32,11 +32,11 @@
kind = "ClusterIssuer";
};
middlewares = {
attrName = "middlewares";
group = "traefik.io";
version = "v1alpha1";
kind = "Middleware";
recurringJob = {
attrName = "recurringJobs";
group = "longhorn.io";
version = "v1beta1";
kind = "RecurringJob";
};
};
}

View file

@ -7,18 +7,11 @@
"repository.pull-request".DEFAULT_MERGE_STYLE = "merge";
"repository.signing".DEFAULT_TRUST_MODEL = "committer";
ui.DEFAULT_THEME = "forgejo-light";
oauth2 = {
ENABLED = true;
ENABLED = false;
JWT_SECRET = "ref+sops://secrets.yml#/forgejo/jwtSecret";
};
oauth2_client = {
ENABLE_AUTO_REGISTRATION = true;
ACCOUNT_LINKING = "auto";
USERNAME = "email";
};
DEFAULT = {
APP_NAME = "Forgejo: Beyond coding. We forge.";
RUN_MODE = "prod";
@ -92,11 +85,11 @@
};
service = {
DISABLE_REGISTRATION = false;
DISABLE_REGISTRATION = true;
REQUIRE_SIGNIN_VIEW = false;
REGISTER_EMAIL_CONFIRM = false;
ENABLE_NOTIFY_MAIL = false;
ALLOW_ONLY_EXTERNAL_REGISTRATION = true;
ALLOW_ONLY_EXTERNAL_REGISTRATION = false;
ENABLE_CAPTCHA = false;
DEFAULT_KEEP_EMAIL_PRIVATE = true;
DEFAULT_ALLOW_CREATE_ORGANIZATION = true;
@ -105,7 +98,7 @@
};
openid = {
ENABLE_OPENID_SIGNIN = false;
ENABLE_OPENID_SIGNIN = true;
ENABLE_OPENID_SIGNUP = false;
};
}

View file

@ -29,7 +29,6 @@
# This disables services from becoming environmental variables
# to prevent SSH_PORT clashing with Forgejo config.
enableServiceLinks = false;
nodeName = "jefke";
containers.forgejo = {
image = globals.images.forgejo;
@ -59,10 +58,7 @@
};
volumes = {
data.hostPath = {
path = "/mnt/longhorn/persistent/volumes/forgejo";
type = "Directory";
};
data.persistentVolumeClaim.claimName = "data";
config.secret.secretName = "forgejo";
};
};
@ -101,6 +97,11 @@
portName = "web";
};
};
longhorn.persistentVolumeClaim.data = {
volumeName = "forgejo";
storage = "20Gi";
};
};
};
}

View file

@ -26,8 +26,6 @@
metadata.labels.app = "freshrss";
spec = {
nodeName = "atlas";
containers.freshrss = {
image = globals.images.freshrss;
imagePullPolicy = "IfNotPresent";
@ -38,13 +36,6 @@
CRON_MIN.value = "2,32";
ADMIN_EMAIL.value = "pim@kunis.nl";
PUBLISHED_PORT.value = "443";
OIDC_ENABLED.value = "1";
OIDC_PROVIDER_METADATA_URL.value = "https://authentik.kun.is/application/o/freshrss/.well-known/openid-configuration";
OIDC_CLIENT_ID.value = "5J2L7Ufq4KMayQ8qrqxHCslxHWL2SXNMKJmsbbiQ";
OIDC_CLIENT_SECRET.value = "ref+sops://secrets.yml#/authentik/oauth2/freshrss/client_secret";
OIDC_CLIENT_CRYPTO_KEY.value = "ref+sops://secrets.yml#/freshrss/oidc_crypto_key";
OIDC_SCOPES.value = "openid email profile";
OIDC_X_FORWARDED_HEADERS.value = "X-Forwarded-Port X-Forwarded-Proto X-Forwarded-Host";
ADMIN_PASSWORD.valueFrom.secretKeyRef = {
name = "server";
@ -65,10 +56,7 @@
];
};
volumes.data.hostPath = {
path = "/mnt/longhorn/persistent/volumes/freshrss";
type = "Directory";
};
volumes.data.persistentVolumeClaim.claimName = "data";
securityContext = {
fsGroup = 33;
@ -95,6 +83,11 @@
host = "freshrss";
service.name = "server";
};
longhorn.persistentVolumeClaim.data = {
volumeName = "freshrss";
storage = "1Gi";
};
};
};
}

View file

@ -41,8 +41,6 @@
};
spec = {
nodeName = "jefke";
containers.hedgedoc = {
image = globals.images.hedgedoc;
ports.web.containerPort = 3000;
@ -56,17 +54,6 @@
CMD_PROTOCOL_USESSL.value = "true";
CMD_CSP_ENABLE.value = "false";
CMD_OAUTH2_PROVIDERNAME.value = "Authentik";
CMD_OAUTH2_CLIENT_ID.value = "ZF56062l4BPnq2INv2zaO9cEiE6sAj7CrxbWhExj";
CMD_OAUTH2_CLIENT_SECRET.value = "ref+sops://secrets.yml#/authentik/oauth2/hedgedoc/client_secret";
CMD_OAUTH2_SCOPE.value = "openid email profile";
CMD_OAUTH2_USER_PROFILE_URL.value = "https://authentik.kun.is/application/o/userinfo/";
CMD_OAUTH2_TOKEN_URL.value = "https://authentik.kun.is/application/o/token/";
CMD_OAUTH2_AUTHORIZATION_URL.value = "https://authentik.kun.is/application/o/authorize/";
CMD_OAUTH2_USER_PROFILE_USERNAME_ATTR.value = "preferred_username";
CMD_OAUTH2_USER_PROFILE_DISPLAY_NAME_ATTR.value = "name";
CMD_OAUTH2_USER_PROFILE_EMAIL_ATTR.value = "email";
CMD_DB_URL.valueFrom.secretKeyRef = {
name = "hedgedoc";
key = "databaseURL";
@ -92,12 +79,8 @@
};
volumes = {
uploads.persistentVolumeClaim.claimName = "uploads";
config.configMap.name = "hedgedoc-config";
uploads.hostPath = {
path = "/mnt/longhorn/persistent/volumes/hedgedoc-uploads";
type = "Directory";
};
};
securityContext = {
@ -130,8 +113,6 @@
};
spec = {
nodeName = "jefke";
containers.postgres = {
image = globals.images.postgres15;
imagePullPolicy = "IfNotPresent";
@ -156,10 +137,7 @@
];
};
volumes.database.hostPath = {
path = "/mnt/longhorn/persistent/volumes/hedgedoc-db";
type = "Directory";
};
volumes.database.persistentVolumeClaim.claimName = "database";
};
};
};
@ -201,6 +179,18 @@
portName = "web";
};
};
longhorn.persistentVolumeClaim = {
uploads = {
volumeName = "hedgedoc-uploads";
storage = "50Mi";
};
database = {
volumeName = "hedgedoc-db";
storage = "100Mi";
};
};
};
};
}

View file

@ -33,12 +33,7 @@
};
spec = {
nodeName = "jefke";
volumes.data.hostPath = {
path = "/mnt/longhorn/persistent/volumes/immich";
type = "Directory";
};
volumes.data.persistentVolumeClaim.claimName = "data";
enableServiceLinks = false;
@ -94,10 +89,7 @@
};
spec = {
volumes.cache.hostPath = {
path = "/tmp/immich-ml-cache";
type = "DirectoryOrCreate";
};
volumes.cache.persistentVolumeClaim.claimName = "cache";
containers.machine-learning = {
image = globals.images.immich-machine-learning;
@ -169,16 +161,13 @@
};
spec = {
nodeName = "jefke";
volumes.data.hostPath = {
path = "/mnt/longhorn/persistent/volumes/immich-db";
type = "Directory";
};
volumes.data.persistentVolumeClaim.claimName = "database";
containers.postgres = {
image = globals.images.immich-postgres;
imagePullPolicy = "IfNotPresent";
command = ["postgres"];
args = ["-c" "shared_preload_libraries=vectors.so" "-c" "search_path=\"$$user\", public, vectors" "-c" "logging_collector=on" "-c" "max_wal_size=2GB" "-c" "shared_buffers=512MB" "-c" "wal_compression=on"];
ports.postgres.containerPort = 5432;
securityContext.runAsUser = 999;
securityContext.runAsGroup = 999;
@ -259,6 +248,11 @@
};
};
};
persistentVolumeClaims.cache.spec = {
accessModes = ["ReadWriteOnce"];
resources.requests.storage = "5Gi";
};
};
lab = {
@ -266,6 +260,18 @@
host = "immich";
service.name = "server";
};
longhorn.persistentVolumeClaim = {
data = {
volumeName = "immich";
storage = "50Gi";
};
database = {
volumeName = "immich-db";
storage = "5Gi";
};
};
};
};
}

View file

@ -21,14 +21,7 @@
containers.inbucket = {
image = globals.images.inbucket;
env = {
INBUCKET_MAILBOXNAMING.value = "full";
INBUCKET_SMTP_DEFAULTACCEPT.value = "false";
INBUCKET_SMTP_ACCEPTDOMAINS.value = "kun.is";
INBUCKET_SMTP_DEFAULTSTORE.value = "false";
INBUCKET_SMTP_STOREDOMAINS.value = "kun.is";
INBUCKET_STORAGE_RETENTIONPERIOD.value = "168h";
};
ports = {
web.containerPort = 9000;
smtp.containerPort = 2500;

View file

@ -47,7 +47,7 @@ in {
rules = [
{
inherit (ingress) host;
host = ingress.host;
http.paths = [
{
@ -55,7 +55,7 @@ in {
pathType = "Prefix";
backend.service = {
inherit (ingress.service) name;
name = ingress.service.name;
port.name = ingress.service.portName;
};
}

View file

@ -26,28 +26,16 @@
metadata.labels.app = "kitchenowl";
spec = {
nodeName = "jefke";
volumes.data.hostPath = {
path = "/mnt/longhorn/persistent/volumes/kitchenowl";
type = "Directory";
};
volumes.data.persistentVolumeClaim.claimName = "data";
containers.kitchenowl = {
image = globals.images.kitchenowl;
ports.web.containerPort = 8080;
imagePullPolicy = "IfNotPresent";
env = {
FRONT_URL.value = "https://boodschappen.kun.is";
OIDC_ISSUER.value = "https://authentik.kun.is/application/o/kitchenowl/";
OIDC_CLIENT_ID.value = "OptR5S9hPix9beuJWFdfNBWRBr2l0nPx7mj8FpB3";
OIDC_CLIENT_SECRET.value = "ref+sops://secrets.yml#/authentik/oauth2/kitchenowl/client_secret";
JWT_SECRET_KEY.valueFrom.secretKeyRef = {
name = "server";
key = "jwtSecretKey";
};
env.JWT_SECRET_KEY.valueFrom.secretKeyRef = {
name = "server";
key = "jwtSecretKey";
};
volumeMounts = [
@ -85,6 +73,11 @@
portName = "web";
};
};
longhorn.persistentVolumeClaim.data = {
volumeName = "kitchenowl";
storage = "100Mi";
};
};
};
}

157
modules/longhorn-volume.nix Normal file
View file

@ -0,0 +1,157 @@
{
lib,
config,
...
}: let
longhornVolumeOpts = {name, ...}: {
options = {
storage = lib.mkOption {
type = lib.types.str;
};
namespace = lib.mkOption {
type = lib.types.str;
default = "default";
};
};
};
longhornPVOpts = {name, ...}: {
options = {
storage = lib.mkOption {
type = lib.types.str;
};
};
};
longhornPVCOpts = {name, ...}: {
options = {
volumeName = lib.mkOption {
type = lib.types.str;
default = name;
};
# TODO: ideally we take this from the longhornPV so we don't duplicate this information.
storage = lib.mkOption {
type = lib.types.str;
};
};
};
in {
options = {
lab.longhornVolumes = lib.mkOption {
type = with lib.types; attrsOf (submodule longhornVolumeOpts);
default = {};
};
lab.longhorn = {
persistentVolume = lib.mkOption {
type = with lib.types; attrsOf (submodule longhornPVOpts);
default = {};
};
persistentVolumeClaim = lib.mkOption {
type = with lib.types; attrsOf (submodule longhornPVCOpts);
default = {};
};
};
};
config = {
kubernetes.resources = {
persistentVolumes =
lib.mergeAttrs
(builtins.mapAttrs
(name: longhornVolume: {
spec = {
accessModes = ["ReadWriteOnce"];
capacity.storage = longhornVolume.storage;
persistentVolumeReclaimPolicy = "Delete";
volumeMode = "Filesystem";
claimRef = {
inherit name;
namespace = longhornVolume.namespace;
};
csi = {
driver = "driver.longhorn.io";
fsType = "ext4";
volumeHandle = name;
volumeAttributes = {
dataLocality = "disabled";
fromBackup = "";
fsType = "ext4";
numberOfReplicas = "2";
staleReplicaTimeout = "30";
unmapMarkSnapChainRemoved = "ignored";
recurringJobSelector = lib.generators.toYAML {} [
{
name = "backup-nfs";
isGroup = false;
}
];
};
};
};
})
config.lab.longhornVolumes)
(builtins.mapAttrs
(name: longhornPV: {
spec = {
accessModes = ["ReadWriteOnce"];
capacity.storage = longhornPV.storage;
persistentVolumeReclaimPolicy = "Delete";
volumeMode = "Filesystem";
csi = {
driver = "driver.longhorn.io";
fsType = "ext4";
volumeHandle = name;
volumeAttributes = {
dataLocality = "disabled";
fromBackup = "";
fsType = "ext4";
numberOfReplicas = "2";
staleReplicaTimeout = "30";
unmapMarkSnapChainRemoved = "ignored";
recurringJobSelector = lib.generators.toYAML {} [
{
name = "backup-nfs";
isGroup = false;
}
];
};
};
};
})
config.lab.longhorn.persistentVolume);
persistentVolumeClaims =
lib.mergeAttrs
(builtins.mapAttrs
(name: longhornVolume: {
spec = {
accessModes = ["ReadWriteOnce"];
resources.requests.storage = longhornVolume.storage;
storageClassName = "";
};
})
config.lab.longhornVolumes)
(builtins.mapAttrs
(name: longhornPVC: {
spec = {
accessModes = ["ReadWriteOnce"];
resources.requests.storage = longhornPVC.storage;
storageClassName = "";
volumeName = longhornPVC.volumeName;
};
})
config.lab.longhorn.persistentVolumeClaim);
};
};
}

View file

@ -1,76 +0,0 @@
{
lib,
config,
utils,
...
}: {
options.mealie.enable = lib.mkEnableOption "mealie";
config = lib.mkIf config.mealie.enable {
kubernetes.resources = {
deployments.mealie.spec = {
selector.matchLabels.app = "mealie";
strategy = {
type = "RollingUpdate";
rollingUpdate = {
maxSurge = 0;
maxUnavailable = 1;
};
};
template = {
metadata.labels.app = "mealie";
spec = {
nodeName = "atlas";
containers.mealie = {
image = utils.mkNixNGImage "mealie";
ports.web.containerPort = 8000;
env = {
SMTP_USER.value = "ref+sops://secrets.yml#/smtp2go/username";
SMTP_PASSWORD.value = "ref+sops://secrets.yml#/smtp2go/password";
OIDC_CLIENT_SECRET.value = "ref+sops://secrets.yml#/authentik/oauth2/mealie/client_secret";
};
volumeMounts = [
{
name = "mealie";
mountPath = "/data";
}
];
};
volumes.mealie.hostPath = {
path = "/mnt/longhorn/persistent/volumes/mealie";
type = "Directory";
};
};
};
};
services.mealie.spec = {
selector.app = "mealie";
ports.web = {
port = 80;
targetPort = "web";
};
};
};
lab = {
ingresses.mealie = {
host = "mealie.kun.is";
service = {
name = "mealie";
portName = "web";
};
};
};
};
}

File diff suppressed because it is too large Load diff

59
modules/minecraft.nix Normal file
View file

@ -0,0 +1,59 @@
{
lib,
config,
globals,
...
}: {
options.minecraft.enable = lib.mkEnableOption "minecraft";
config = lib.mkIf config.minecraft.enable {
kubernetes.resources = {
deployments.minecraft.spec = {
selector.matchLabels.app = "minecraft";
template = {
metadata.labels.app = "minecraft";
spec = {
volumes.data.persistentVolumeClaim.claimName = "data";
containers.minecraft = {
image = globals.images.minecraft;
ports.minecraft.containerPort = 25565;
env.EULA.value = "TRUE";
volumeMounts = [
{
name = "data";
mountPath = "/data";
}
];
};
securityContext = {
fsGroup = 1000;
fsGroupChangePolicy = "OnRootMismatch";
};
};
};
};
services.minecraft.spec = {
type = "LoadBalancer";
loadBalancerIP = globals.minecraftIPv4;
selector.app = "minecraft";
ports.minecraft = {
port = 25565;
targetPort = "minecraft";
};
};
};
lab.longhorn.persistentVolumeClaim.data = {
volumeName = "minecraft";
storage = "1Gi";
};
};
}

View file

@ -33,12 +33,7 @@
};
spec = {
nodeName = "atlas";
volumes.data.hostPath = {
path = "/mnt/longhorn/persistent/volumes/nextcloud";
type = "Directory";
};
volumes.data.persistentVolumeClaim.claimName = "data";
containers.nextcloud = {
image = globals.images.nextcloud;
@ -106,8 +101,6 @@
};
spec = {
nodeName = "atlas";
containers.postgres = {
image = globals.images.postgres15;
imagePullPolicy = "IfNotPresent";
@ -132,10 +125,7 @@
];
};
volumes.database.hostPath = {
path = "/mnt/longhorn/persistent/volumes/nextcloud-db";
type = "Directory";
};
volumes.database.persistentVolumeClaim.claimName = "database";
};
};
};
@ -176,6 +166,18 @@
host = "nextcloud";
service.name = "server";
};
longhorn.persistentVolumeClaim = {
data = {
volumeName = "nextcloud";
storage = "50Gi";
};
database = {
volumeName = "nextcloud-db";
storage = "400Mi";
};
};
};
};
}

View file

@ -24,8 +24,6 @@
metadata.labels.app = "ntfy";
spec = {
nodeName = "jefke";
containers.ntfy = {
image = utils.mkNixNGImage "ntfy";
ports.web.containerPort = 80;
@ -48,30 +46,26 @@
};
volumes = {
data.hostPath = {
path = "/mnt/longhorn/persistent/volumes/ntfy";
type = "Directory";
};
cache.hostPath = {
path = "/tmp/ntfy-cache";
type = "DirectoryOrCreate";
};
attachment-cache.hostPath = {
path = "/tmp/ntfy-attachment-cache";
type = "DirectoryOrCreate";
};
};
securityContext = {
fsGroup = 407;
fsGroupChangePolicy = "Always";
cache.persistentVolumeClaim.claimName = "cache";
attachment-cache.persistentVolumeClaim.claimName = "attachment-cache";
data.persistentVolumeClaim.claimName = "data";
};
};
};
};
persistentVolumeClaims = {
cache.spec = {
accessModes = ["ReadWriteOnce"];
resources.requests.storage = "300Mi";
};
attachment-cache.spec = {
accessModes = ["ReadWriteOnce"];
resources.requests.storage = "500Mi";
};
};
services.ntfy.spec = {
selector.app = "ntfy";
@ -91,6 +85,11 @@
portName = "web";
};
};
longhorn.persistentVolumeClaim.data = {
volumeName = "ntfy";
storage = "300Mi";
};
};
};
}

View file

@ -36,12 +36,7 @@
};
spec = {
nodeName = "jefke";
volumes.data.hostPath = {
path = "/mnt/longhorn/persistent/volumes/paperless-data";
type = "Directory";
};
volumes.data.persistentVolumeClaim.claimName = "data";
containers.paperless = {
image = globals.images.paperless;
@ -62,25 +57,6 @@
PAPERLESS_OCR_LANGUAGE.value = "nld";
USERMAP_UID.value = "33";
USERMAP_GID.value = "33";
PAPERLESS_APPS.value = "allauth.socialaccount.providers.openid_connect";
PAPERLESS_SOCIALACCOUNT_PROVIDERS.value = ''
{
"openid_connect": {
"APPS": [
{
"provider_id": "authentik",
"name": "Authentik",
"client_id": "z5PlhxTB1eXJ9L39Ix2BfhLV72TbF3vbGJZUtyBJ",
"secret": "ref+sops://secrets.yml#/authentik/oauth2/paperless-ngx/client_secret+",
"settings": {
"server_url": "https://authentik.kun.is/application/o/paperless-ngx/.well-known/openid-configuration"
}
}
],
"OAUTH_PKCE_ENABLED": "True"
}
}
'';
PAPERLESS_DBPASS.valueFrom.secretKeyRef = {
name = "database";
@ -131,12 +107,7 @@
};
spec = {
nodeName = "jefke";
volumes.data.hostPath = {
path = "/mnt/longhorn/persistent/volumes/paperless-redisdata";
type = "Directory";
};
volumes.data.persistentVolumeClaim.claimName = "redisdata";
containers.redis = {
image = globals.images.redis7;
@ -181,13 +152,6 @@
};
spec = {
nodeName = "jefke";
volumes.data.hostPath = {
path = "/mnt/longhorn/persistent/volumes/paperless-db";
type = "Directory";
};
containers.postgres = {
image = globals.images.postgres15;
ports.postgres.containerPort = 5432;
@ -211,6 +175,8 @@
}
];
};
volumes.data.persistentVolumeClaim.claimName = "database";
};
};
};
@ -263,6 +229,23 @@
host = "paperless";
service.name = "web";
};
longhorn.persistentVolumeClaim = {
data = {
volumeName = "paperless-data";
storage = "10Gi";
};
redisdata = {
volumeName = "paperless-redisdata";
storage = "20Mi";
};
database = {
volumeName = "paperless-db";
storage = "150Mi";
};
};
};
};
}

117
modules/pihole.nix Normal file
View file

@ -0,0 +1,117 @@
{
globals,
config,
lib,
...
}: {
options.pihole.enable = lib.mkEnableOption "pihole";
config = lib.mkIf config.pihole.enable {
kubernetes.resources = {
secrets.pihole.stringData.webPassword = "ref+sops://secrets.yml#/pihole/password";
deployments.pihole.spec = {
selector.matchLabels.app = "pihole";
strategy = {
type = "RollingUpdate";
rollingUpdate = {
maxSurge = 0;
maxUnavailable = 1;
};
};
template = {
metadata.labels.app = "pihole";
spec = {
containers.pihole = {
image = globals.images.pihole;
env = {
TZ.value = "Europe/Amsterdam";
PIHOLE_DNS_.value = "192.168.30.1";
WEBPASSWORD.valueFrom.secretKeyRef = {
name = "pihole";
key = "webPassword";
};
};
ports = {
web.containerPort = 80;
dns = {
containerPort = 53;
protocol = "UDP";
};
};
volumeMounts = [
{
name = "data";
mountPath = "/etc/pihole";
}
{
name = "dnsmasq";
mountPath = "/etc/dnsmasq.d";
}
];
};
volumes = {
data.persistentVolumeClaim.claimName = "pihole-data";
dnsmasq.persistentVolumeClaim.claimName = "pihole-dnsmasq";
};
securityContext = {
fsGroup = 1000;
fsGroupChangePolicy = "OnRootMismatch";
};
};
};
};
services = {
pihole.spec = {
type = "LoadBalancer";
loadBalancerIP = globals.piholeIPv4;
selector.app = "pihole";
ports = {
dns = {
protocol = "UDP";
port = 53;
targetPort = "dns";
};
web = {
port = 80;
targetPort = "web";
};
};
};
};
};
lab = {
longhorn.persistentVolumeClaim = {
pihole-data = {
volumeName = "pihole-data";
storage = "750Mi";
};
pihole-dnsmasq = {
volumeName = "pihole-dnsmasq";
storage = "16Mi";
};
};
tailscaleIngresses.tailscale-pihole = {
host = "pihole";
service.name = "pihole";
};
};
};
}

View file

@ -25,8 +25,6 @@
metadata.labels.app = "radicale";
spec = {
nodeName = "jefke";
containers.radicale = {
image = utils.mkNixNGImage "radicale";
ports.web.containerPort = 5232;
@ -40,10 +38,7 @@
];
};
volumes.data.hostPath = {
path = "/mnt/longhorn/persistent/volumes/radicale";
type = "Directory";
};
volumes.data.persistentVolumeClaim.claimName = "data";
};
};
};
@ -65,6 +60,11 @@
host = "radicale";
service.name = "server";
};
longhorn.persistentVolumeClaim.data = {
volumeName = "radicale";
storage = "200Mi";
};
};
};
}

View file

@ -8,6 +8,8 @@
config = lib.mkIf config.syncthing.enable {
kubernetes.resources = {
serviceAccounts.syncthing = {};
deployments.syncthing.spec = {
selector.matchLabels.app = "syncthing";
@ -24,7 +26,7 @@
metadata.labels.app = "syncthing";
spec = {
nodeName = "jefke";
serviceAccountName = "syncthing";
containers.syncthing = {
image = globals.images.syncthing;
@ -43,22 +45,15 @@
mountPath = "/config";
}
{
name = "keepassxc";
mountPath = "/keepassxc";
name = "music";
mountPath = "/music";
}
];
};
volumes = {
keepassxc.hostPath = {
path = "/mnt/longhorn/persistent/volumes/keepassxc";
type = "Directory";
};
config.hostPath = {
path = "/mnt/longhorn/persistent/volumes/syncthing";
type = "Directory";
};
config.persistentVolumeClaim.claimName = "config";
music.persistentVolumeClaim.claimName = "music";
};
securityContext = {
@ -79,9 +74,21 @@
targetPort = "web";
};
};
persistentVolumeClaims.music.spec = {
accessModes = ["ReadWriteMany"];
storageClassName = "";
resources.requests.storage = "1Mi";
volumeName = "music-syncthing";
};
};
lab = {
longhorn.persistentVolumeClaim.config = {
volumeName = "syncthing";
storage = "400Mi";
};
tailscaleIngresses.tailscale = {
host = "syncthing";
service.name = "syncthing";

View file

@ -42,7 +42,7 @@
pathType = "Prefix";
backend.service = {
inherit (service) name;
name = service.name;
port.name = service.portName;
};
}

View file

@ -20,7 +20,7 @@
ports = {
localsecure = {
port = 8444;
expose.default = true;
expose = true;
exposedPort = 444;
protocol = "TCP";

View file

@ -1,12 +0,0 @@
{
dinit.enable = true;
init.services.atuin.shutdownOnExit = true;
services.atuin = {
enable = true;
settings = {
open_registration = false;
};
};
}

View file

@ -1,23 +1,9 @@
{
lib,
nglib,
config,
...
}: {
{...}: {
dinit.enable = true;
init.services.bazarr = {
shutdownOnExit = true;
group = lib.mkForce "media";
};
init.services.bazarr.shutdownOnExit = true;
services.bazarr = {
enable = true;
configDir = "/config";
};
users.groups.media = nglib.mkDefaultRec {
gid = config.ids.gids.media;
members = ["bazarr"];
};
}

View file

@ -5,6 +5,9 @@
nginx,
blog,
nixpkgs,
nixpkgs-jellyseerr,
nixpkgs-bazarr,
nixpkgs-radicale,
nixpkgs-master,
...
}:
@ -20,9 +23,7 @@ flake-utils.lib.eachDefaultSystem (system: let
bazarr = ./bazarr.nix;
prowlarr = ./prowlarr.nix;
blog = ./blog.nix;
deluge = ./deluge.nix;
mealie = ./mealie.nix;
atuin = ./atuin.nix;
generic-device-plugin = ./generic-device-plugin.nix;
};
in {
nixngConfigurations = builtins.mapAttrs (name: configFile:
@ -37,21 +38,16 @@ in {
};
extraModules = [
self.nixngModules.ids
self.nixngModules.bazarr
self.nixngModules.radicale
self.nixngModules.jellyseerr
self.nixngModules.radarr
self.nixngModules.sonarr
self.nixngModules.prowlarr
self.nixngModules.deluge
self.nixngModules.mealie
self.nixngModules.atuin
{
nixpkgs.overlays = [
(_final: _prev: {
(final: _prev: {
# From master branch
inherit (nixpkgs-master.legacyPackages.${system}) jellyseerr radicale bazarr;
prowlarr = nixpkgs-master.legacyPackages.${system}.prowlarr;
# From forks
bazarr = nixpkgs-bazarr.legacyPackages.${system}.bazarr;
jellyseerr = nixpkgs-jellyseerr.legacyPackages.${system}.jellyseerr;
radicale = nixpkgs-radicale.legacyPackages.${system}.radicale;
})
];
}

View file

@ -1,30 +0,0 @@
{
config,
nglib,
lib,
...
}: {
dinit.enable = true;
init.services = {
deluged = {
shutdownOnExit = true;
group = lib.mkForce "media";
};
deluge-web = {
shutdownOnExit = true;
group = lib.mkForce "media";
};
};
services.deluge = {
enable = true;
configDir = "/config";
web.enable = true;
};
users.groups.media = nglib.mkDefaultRec {
gid = config.ids.gids.media;
members = ["deluge"];
};
}

View file

@ -0,0 +1,10 @@
{globals, ...}: {
dinit.enable = true;
init.services.generic-device-plugin.shutdownOnExit = true;
services.generic-device-plugin = {
enable = true;
settings = {
};
};
}

View file

@ -1,22 +1,9 @@
{
config,
lib,
nglib,
...
}: {
{...}: {
dinit.enable = true;
init.services.jellyseerr = {
shutdownOnExit = true;
group = lib.mkForce "media";
};
init.services.jellyseerr.shutdownOnExit = true;
services.jellyseerr = {
enable = true;
configDir = "/app/config";
};
users.groups.media = nglib.mkDefaultRec {
gid = config.ids.gids.media;
members = ["jellyseerr"];
};
}

View file

@ -1,25 +0,0 @@
{
dinit.enable = true;
init.services.mealie.shutdownOnExit = true;
services.mealie = {
enable = true;
settings = {
DATA_DIR = "/data";
BASE_URL = "https://mealie.kun.is";
ALLOW_SIGNUP = "False";
SMTP_HOST = "mail.smtp2go.com";
SMTP_PORT = "2525";
SMTP_FROM_NAME = "Mealie";
SMTP_AUTH_STRATEGY = "ssl";
SMTP_FROM_EMAIL = "mealie@kun.is";
OIDC_AUTH_ENABLED = "True";
OIDC_CONFIGURATION_URL = "https://authentik.kun.is/application/o/mealie/.well-known/openid-configuration";
OIDC_CLIENT_ID = "lvkHoIPacUXjY4jr9YyEQC7YyhccOH0atbpOiKmG";
OIDC_AUTO_REDIRECT = "True";
OIDC_PROVIDER_NAME = "Authentik";
OIDC_REMEMBER_ME = "True";
};
};
}

View file

@ -1,4 +1,4 @@
{
{...}: {
dinit.enable = true;
init.services.ntfy-sh.shutdownOnExit = true;

View file

@ -1,22 +1,9 @@
{
lib,
nglib,
config,
...
}: {
{...}: {
dinit.enable = true;
init.services.prowlarr = {
shutdownOnExit = true;
group = lib.mkForce "media";
};
init.services.prowlarr.shutdownOnExit = true;
services.prowlarr = {
enable = true;
dataDir = "/config";
};
users.groups.media = nglib.mkDefaultRec {
gid = config.ids.gids.media;
members = ["prowlarr"];
};
}

View file

@ -1,22 +1,9 @@
{
lib,
nglib,
config,
...
}: {
{...}: {
dinit.enable = true;
init.services.radarr = {
shutdownOnExit = true;
group = lib.mkForce "media";
};
init.services.radarr.shutdownOnExit = true;
services.radarr = {
enable = true;
dataDir = "/config";
};
users.groups.media = nglib.mkDefaultRec {
gid = config.ids.gids.media;
members = ["radarr"];
};
}

View file

@ -1,22 +1,9 @@
{
lib,
config,
nglib,
...
}: {
{...}: {
dinit.enable = true;
init.services.sonarr = {
shutdownOnExit = true;
group = lib.mkForce "media";
};
init.services.sonarr.shutdownOnExit = true;
services.sonarr = {
enable = true;
dataDir = "/config";
};
users.groups.media = nglib.mkDefaultRec {
gid = config.ids.gids.media;
members = ["sonarr"];
};
}

View file

@ -1,86 +0,0 @@
{
pkgs,
lib,
nglib,
config,
...
}: let
cfg = config.services.atuin;
cfgInit = config.init.services.atuin;
settingsFormat = pkgs.formats.toml {};
in {
options.services.atuin = {
enable = lib.mkEnableOption "atuin";
package = lib.mkPackageOption pkgs "atuin" {};
settings = lib.mkOption {
type = lib.types.submodule {
freeformType = settingsFormat.type;
options = {
host = lib.mkOption {
type = lib.types.str;
default = "0.0.0.0";
description = "The host to listen on";
};
port = lib.mkOption {
type = lib.types.port;
default = 8888;
description = "The TCP port to listen on";
};
open_registration = lib.mkOption {
type = lib.types.bool;
default = false;
description = "If true, accept new user registrations";
};
db_uri = lib.mkOption {
type = with lib.types; nullOr str;
default = null;
description = "A valid PostgreSQL URI, for saving history";
};
path = lib.mkOption {
type = lib.types.str;
default = "";
description = "A path to prepend to all the routes of the server";
};
};
};
};
};
config = lib.mkIf cfg.enable {
init.services.atuin = {
enabled = true;
user = lib.mkDefault "atuin";
group = lib.mkDefault "atuin";
script = pkgs.writeShellScript "atuin-run" ''
${lib.getExe cfg.package} server start
'';
};
environment = {
systemPackages = [cfg.package];
variables.ATUIN_CONFIG_DIR = let
settingsFile = settingsFormat.generate "server.toml" (lib.filterAttrs (_: v: v != null) cfg.settings);
in
toString (pkgs.writeTextDir "server.toml" (builtins.readFile settingsFile));
};
users.users.${cfgInit.user} = nglib.mkDefaultRec {
description = "atuin";
inherit (cfgInit) group;
createHome = false;
home = "/var/empty";
useDefaultShell = true;
uid = config.ids.uids.atuin;
};
users.groups.${cfgInit.group} = nglib.mkDefaultRec {gid = config.ids.gids.atuin;};
};
}

View file

@ -1,49 +0,0 @@
{
lib,
nglib,
config,
pkgs,
...
}: let
cfg = config.services.bazarr;
cfgInit = config.init.services.bazarr;
in {
options.services.bazarr = {
enable = lib.mkEnableOption "bazarr";
package = lib.mkPackageOption pkgs "bazarr" {};
configDir = lib.mkOption {
description = "Where Bazarr's configuration files are stored.";
type = lib.types.str;
default = "/config";
};
};
config = lib.mkIf cfg.enable {
init.services.bazarr = {
enabled = true;
user = lib.mkDefault "bazarr";
group = lib.mkDefault "bazarr";
script = pkgs.writeShellScript "bazarr-run" ''
umask 0002
${lib.getExe cfg.package} \
--no-update \
--config '${cfg.configDir}'
'';
};
environment.systemPackages = [cfg.package];
users.users.${cfgInit.user} = lib.mkIf (cfgInit.user == "bazarr") (nglib.mkDefaultRec {
description = "bazarr";
group = cfgInit.group;
createHome = false;
home = "/var/empty";
useDefaultShell = true;
uid = config.ids.uids.bazarr;
});
users.groups.${cfgInit.group} = lib.mkIf (cfgInit.group == "bazarr") (nglib.mkDefaultRec {gid = config.ids.gids.bazarr;});
};
}

View file

@ -1,14 +0,0 @@
_: {
nixngModules = {
bazarr = import ./bazarr.nix;
radicale = import ./radicale.nix;
jellyseerr = import ./jellyseerr.nix;
radarr = import ./radarr.nix;
sonarr = import ./sonarr.nix;
prowlarr = import ./prowlarr.nix;
ids = import ./ids.nix;
deluge = import ./deluge.nix;
mealie = import ./mealie.nix;
atuin = import ./atuin.nix;
};
}

View file

@ -1,85 +0,0 @@
{
lib,
nglib,
config,
pkgs,
...
}: let
cfg = config.services.deluge;
cfgInit = config.init.services.deluged;
in {
options.services.deluge = {
enable = lib.mkEnableOption "deluge";
package = lib.mkPackageOption pkgs "deluge-2_x" {};
configDir = lib.mkOption {
type = lib.types.path;
default = "/var/lib/deluge";
description = ''
Directory for Deluge's run-time configuration
'';
};
web = {
enable = lib.mkEnableOption "Deluge web daemon";
port = lib.mkOption {
type = lib.types.port;
default = 8112;
description = ''
Deluge web UI port
'';
};
};
};
config = lib.mkIf cfg.enable {
init.services = {
deluged = {
enabled = true;
user = lib.mkDefault "deluge";
group = lib.mkDefault "deluge";
tmpfiles = with nglib.nottmpfiles.dsl; [(d cfg.configDir "-" cfgInit.user cfgInit.group _ _)];
script = pkgs.writeShellScript "deluged-run" ''
# TODO: make init-level option?
umask 0002
${cfg.package}/bin/deluged \
--do-not-daemonize \
--config ${cfg.configDir}
'';
};
deluge-web = {
enabled = cfg.web.enable;
dependencies = ["deluged"];
user = lib.mkDefault "deluge";
group = lib.mkDefault "deluge";
script = pkgs.writeShellScript "deluge-web-run" ''
${cfg.package}/bin/deluge-web \
--do-not-daemonize \
--port ${toString cfg.web.port} \
--config ${cfg.configDir}
'';
};
};
environment = {
systemPackages = [cfg.package];
variables.PYTHON_EGG_CACHE = "${config.users.users.${cfgInit.user}.home}/.cache";
};
users.users.${cfgInit.user} = lib.mkIf (cfgInit.user == "deluge") (nglib.mkDefaultRec {
description = "deluge";
inherit (cfgInit) group;
createHome = true;
home = "/home/deluge";
useDefaultShell = true;
uid = config.ids.uids.deluge;
});
users.groups.${cfgInit.group} = lib.mkIf (cfgInit.group == "deluge") (nglib.mkDefaultRec {gid = config.ids.gids.deluge;});
};
}

View file

@ -1,28 +0,0 @@
{
ids = {
uids = {
radicale = 408;
jellyseerr = 409;
radarr = 410;
sonarr = 411;
bazarr = 412;
prowlarr = 413;
deluge = 414;
mealie = 415;
atuin = 416;
};
gids = {
media = 51;
radicale = 408;
jellyseerr = 409;
radarr = 410;
sonarr = 411;
bazarr = 412;
prowlarr = 413;
deluge = 414;
mealie = 415;
atuin = 416;
};
};
}

View file

@ -1,65 +0,0 @@
{
lib,
nglib,
pkgs,
config,
...
}: let
cfg = config.services.jellyseerr;
cfgInit = config.init.services.jellyseerr;
in {
options.services.jellyseerr = {
enable = lib.mkEnableOption "jellyseerr";
package = lib.mkPackageOption pkgs "jellyseerr" {};
port = lib.mkOption {
description = ''
The port Jellyseerr should listen on.
'';
type = lib.types.port;
example = 8080;
default = 5055;
};
configDir = lib.mkOption {
description = ''
The directory to save run-time configuration.
'';
type = lib.types.str;
example = "/jellyseerr";
default = "/var/lib/jellyseerr";
};
};
config = lib.mkIf cfg.enable {
init.services.jellyseerr = {
enabled = true;
script = pkgs.writeShellScript "jellyseerr-run" ''
umask 0002
${lib.getExe cfg.package}
'';
user = lib.mkDefault "jellyseerr";
group = lib.mkDefault "jellyseerr";
};
environment = {
systemPackages = [cfg.package];
variables = {
PORT = builtins.toString cfg.port;
CONFIG_DIRECTORY = cfg.configDir;
};
};
users.users.${cfgInit.user} = lib.mkIf (cfgInit.user == "jellyseerr") (nglib.mkDefaultRec {
description = "jellyseerr";
group = cfgInit.group;
createHome = false;
home = "/var/empty";
useDefaultShell = true;
uid = config.ids.uids.jellyseerr;
});
users.groups.${cfgInit.group} = lib.mkIf (cfgInit.group == "jellyseerr") (nglib.mkDefaultRec {gid = config.ids.gids.jellyseerr;});
};
}

View file

@ -1,85 +0,0 @@
{
lib,
nglib,
pkgs,
config,
...
}: let
cfg = config.services.mealie;
cfgInit = config.init.services.mealie;
in {
options.services.mealie = {
enable = lib.mkEnableOption "mealie";
package = lib.mkPackageOption pkgs "mealie" {};
settings = lib.mkOption {
type = lib.types.submodule {
freeformType = with lib.types; attrsOf str;
options = {
PRODUCTION = lib.mkOption {
type = lib.types.str;
default = "true";
};
DATA_DIR = lib.mkOption {
type = with lib.types; nullOr str;
default = null;
};
DB_ENGINE = lib.mkOption {
type = with lib.types; nullOr str;
default = "sqlite";
};
ALEMBIC_CONFIG_FILE = lib.mkOption {
type = with lib.types; nullOr str;
default = "${cfg.package}/alembic.ini";
};
};
};
description = ''
Configuration of the Mealie service.
See [the Mealie documentation](https://nightly.mealie.io/documentation/getting-started/installation/backend-config/) for available options and default values.
'';
default = {};
};
};
config = lib.mkIf cfg.enable {
init.services.mealie = {
enabled = true;
user = lib.mkDefault "mealie";
group = lib.mkDefault "mealie";
tmpfiles = with nglib.nottmpfiles.dsl; lib.optional (cfg.settings.DATA_DIR != null) (d "${cfg.settings.DATA_DIR}" "-" cfgInit.user cfgInit.group "-" _);
execStart =
pkgs.writeShellScript "mealie-run"
(let
# Mealie can only be configured via environmental variables.
# With this, we don't accidentally overwrite env variables set by the user.
extraEnvLines = lib.mapAttrsToList (key: value: ''export ${key}=''${${key}:=${value}}'') cfg.settings;
in ''
${lib.concatStringsSep "\n" extraEnvLines}
${cfg.package}/libexec/init_db
${lib.getExe cfg.package} -b 0.0.0.0:8000
'');
};
environment.systemPackages = [cfg.package];
users.users.${cfgInit.user} = nglib.mkDefaultRec {
description = "mealie";
inherit (cfgInit) group;
createHome = false;
home = "/var/empty";
useDefaultShell = true;
uid = config.ids.uids.mealie;
};
users.groups.${cfgInit.group} = nglib.mkDefaultRec {gid = config.ids.gids.mealie;};
};
}

View file

@ -1,48 +0,0 @@
{
pkgs,
lib,
nglib,
config,
...
}: let
cfg = config.services.prowlarr;
cfgInit = config.init.services.prowlarr;
in {
options.services.prowlarr = {
enable = lib.mkEnableOption "prowlarr";
package = lib.mkPackageOption pkgs "prowlarr" {};
dataDir = lib.mkOption {
description = "Directory to store Prowlarr's data";
type = lib.types.str;
default = "/config";
};
};
config = lib.mkIf cfg.enable {
init.services.prowlarr = {
enabled = true;
user = lib.mkDefault "prowlarr";
group = lib.mkDefault "prowlarr";
script = pkgs.writeShellScript "prowlarr-run" ''
${lib.getExe cfg.package} \
-nobrowser \
-data=${cfg.dataDir}
'';
};
environment.systemPackages = [cfg.package];
users.users.${cfgInit.user} = lib.mkIf (cfgInit.user == "prowlarr") (nglib.mkDefaultRec {
description = "prowlarr";
group = cfgInit.group;
createHome = false;
home = "/var/empty";
useDefaultShell = true;
uid = config.ids.uids.prowlarr;
});
users.groups.${cfgInit.group} = lib.mkIf (cfgInit.group == "prowlarr") (nglib.mkDefaultRec {gid = config.ids.gids.prowlarr;});
};
}

View file

@ -1,47 +0,0 @@
{
config,
lib,
nglib,
pkgs,
...
}: let
cfg = config.services.radarr;
cfgInit = config.init.services.radarr;
in {
options.services.radarr = {
enable = lib.mkEnableOption "radarr";
package = lib.mkPackageOption pkgs "radarr" {};
dataDir = lib.mkOption {
type = lib.types.str;
default = "/var/lib/radarr/.config/Radarr";
description = "The directory where Radarr stores its data files.";
};
};
config = lib.mkIf cfg.enable {
init.services.radarr = {
enabled = true;
user = lib.mkDefault "radarr";
group = lib.mkDefault "radarr";
script = pkgs.writeShellScript "radarr-run.sh" ''
umask 0002
${lib.getExe cfg.package} -nobrowser -data='${cfg.dataDir}'
'';
};
environment.systemPackages = [cfg.package];
users.users.${cfgInit.user} = lib.mkIf (cfgInit.user == "radarr") (nglib.mkDefaultRec {
description = "radarr";
inherit (cfgInit) group;
createHome = false;
home = "/var/empty";
useDefaultShell = true;
uid = config.ids.uids.radarr;
});
users.groups.${cfgInit.group} = lib.mkIf (cfgInit.group == "radarr") (nglib.mkDefaultRec {gid = config.ids.gids.radarr;});
};
}

View file

@ -1,71 +0,0 @@
{
lib,
pkgs,
config,
nglib,
...
}: let
cfg = config.services.radicale;
cfgInit = config.init.services.radicale;
settingsFormat = pkgs.formats.ini {
listToValue = lib.concatMapStringsSep ", " (lib.generators.mkValueStringDefault {});
};
in {
options.services.radicale = {
enable = lib.mkEnableOption "radicale";
package = lib.mkPackageOption pkgs "radicale" {};
settings = lib.mkOption {
type = settingsFormat.type;
default = {};
description = ''
Configuration for Radicale. See
<https://radicale.org/v3.html#configuration>.
'';
example = lib.literalExpression ''
server = {
hosts = [ "0.0.0.0:5232" "[::]:5232" ];
};
auth = {
type = "htpasswd";
htpasswd_filename = "/etc/radicale/users";
htpasswd_encryption = "bcrypt";
};
storage = {
filesystem_folder = "/var/lib/radicale/collections";
};
'';
};
};
config = lib.mkIf cfg.enable (let
configFile = settingsFormat.generate "radicale.ini" cfg.settings;
in {
init.services.radicale = {
enabled = true;
user = lib.mkDefault "radicale";
group = lib.mkDefault "radicale";
script = pkgs.writeShellScript "radicale-run" ''
${cfg.package}/bin/radicale \
--config ${configFile}
'';
};
environment.systemPackages = [cfg.package];
users.users.${cfgInit.user} = nglib.mkDefaultRec {
description = "radicale";
group = cfgInit.group;
createHome = false;
home = "/var/empty";
useDefaultShell = true;
uid = config.ids.uids.radicale;
};
users.groups.${cfgInit.group} = nglib.mkDefaultRec {gid = config.ids.gids.radicale;};
});
}

View file

@ -1,48 +0,0 @@
{
lib,
nglib,
config,
pkgs,
...
}: let
cfg = config.services.sonarr;
cfgInit = config.init.services.sonarr;
in {
options.services.sonarr = {
enable = lib.mkEnableOption "sonarr";
package = lib.mkPackageOption pkgs "sonarr" {};
dataDir = lib.mkOption {
type = lib.types.str;
default = "/var/lib/sonarr/.config/NzbDrone";
description = "The directory where Sonarr stores its data files.";
};
};
config = lib.mkIf cfg.enable {
init.services.sonarr = {
enabled = true;
user = lib.mkDefault "sonarr";
group = lib.mkDefault "sonarr";
tmpfiles = with nglib.nottmpfiles.dsl; [(e "${cfg.dataDir}/logs" "-" cfgInit.user cfgInit.group "7d" _)];
script = pkgs.writeShellScript "sonarr-run" ''
umask 0002
${lib.getExe cfg.package} -nobrowser -data=${cfg.dataDir}
'';
};
environment.systemPackages = [cfg.package];
users.users.${cfgInit.user} = lib.mkIf (cfgInit.user == "sonarr") (nglib.mkDefaultRec {
description = "sonarr";
inherit (cfgInit) group;
createHome = false;
home = "/var/empty";
useDefaultShell = true;
uid = config.ids.uids.sonarr;
});
users.groups.${cfgInit.group} = lib.mkIf (cfgInit.group == "sonarr") (nglib.mkDefaultRec {gid = config.ids.gids.sonarr;});
};
}

View file

@ -13,11 +13,6 @@ if [ -z "$username" ] || [ -z "$host" ]
exit 1
fi
if [ ! -d "$output_path" ]; then
echo "Output directory $output_path does not exist!"
exit 1
fi
# Create a temporary directory
temp=$(mktemp -d)

View file

@ -1,14 +1,11 @@
freshrss:
password: ENC[AES256_GCM,data:ECDPrW+VgO8PY9p2fLIreRETNiRL5ZGnu/PMC7aNj8KaWfyNYL+l3w==,iv:srR/r1EtOpC/CKKrCDKcTLVdMFPAYIJIB1CCg8mS0UU=,tag:YN4PqR5uvPkVskpJWD+91g==,type:str]
oidc_crypto_key: ENC[AES256_GCM,data:dFQKZtFVd5l8W2go6WcK76o7O7hpQWnQKXCGTf9EhSVURvWigv6zzBULie7Y4lkJCsItG8oKmIiCYSy3MhFnU3DJTUJcenm4I7NHyINjvzHOBgUVPXbYQjQhouJwOlPkdqlSKv1f38ItZKNPJebMObZj+kACKbjdik6e6yM40RM=,iv:g6Ygval2qTQwKnrliI+n/r9OxJFePT9MKYyBLU6b3UQ=,tag:kWXTbm2JIR5aL/s4OX2Tqg==,type:str]
pihole:
password: ENC[AES256_GCM,data:MA60825Tl6aYEFVoPgo8k5Vjb9zmIxtPLJriQV1B3P1bOKu1KK7vxQ==,iv:RGZHox8CbJiEEEjMo2k/tNbtjCPy/QY7vOuMN/YNZcg=,tag:yphrq03IKpXM/tSDBLeSgA==,type:str]
hedgedoc:
databaseURL: ENC[AES256_GCM,data:6+IV4TaClIGE1XVkUf7JwXzqx3EvWiIKFx9X5x7QKvQKC7bIieD1ADVeAMQmiQfibnH/YV5TgjNY8Ft+3eX881c3yD+2j7mM+O1fX6taK/BCokDnqhIwTN2qxHsu+mrPcM/Pgg5Zqy8HvUgX8jM=,iv:bCwuNk5CVgK2T5IgLebcKwxwloi6FkWMWhnxwJek1GM=,tag:UDQ0KmRDVlDh35Fjm6eaAA==,type:str]
sessionSecret: ENC[AES256_GCM,data:7FdRjAShjjue1fFwizCgK+94mkbT4ohAPxdyn/8Z8/f2nvGWPZHO/hGexOixbRGLPewJSaMunTMeJL+IzFlGlg==,iv:iz7640b8Mlb6mNps20b+TbphWDEFUbKwKNUXc0kR5NY=,tag:fdEr1tbes1h8VCA/q+0sOw==,type:str]
databasePassword: ENC[AES256_GCM,data:wdRhCluNx2IzgDqouAoIcG6yOWwNLOaEkpqgYEeFvJDZsMC8OUuV7Q==,iv:Csut0c+LRKWD2b4uVuQpGnwwVqnGT6Sk6T/ODlH57Bk=,tag:7bS6a18GyRifq2D6cIheaw==,type:str]
oidc:
client_secret:
password: ENC[AES256_GCM,data:rLkMFkEzCF3+ejGAUliaBMpfOrxL5b3pJVvkblEFIvHupmr9DTS71L4T6/oo616E4IoCuZxqKh0FxhdWZRM4KrEk5YZMbgp9,iv:TbySrCTY+Kps4v/q5maQglm4aOzuUPch2ECBHPp4FYc=,tag:mz71m6JqIemVCypf93hkBQ==,type:str]
digest: ENC[AES256_GCM,data:m+abt7SEXZoeOA+ioB3BmiWN09CDpCWdZ5eNRhShvuUJW7rjbSJAQ2NTZkMTnFoNv6PO6rSUD6lmStFfO/mqVUFvWXBBDMK3xK+ILcgDoZZ1Pnmr19noqTL5OxyYLAtUHRNOohsUBYlZKyv2lFNQlqxk6kOi+fUPitmmsO6BO1nrq2s=,iv:CmC0JLXuT9O29Rtu7Pp8i0h/SyaEAT0bsXQb2LRFAkk=,tag:/IL39BCMuw+bqxv2OaNwhw==,type:str]
nextcloud:
databasePassword: ENC[AES256_GCM,data:jRLgW96FnMEpU0T5z/iQOX/CgjpH2ZykZGd1qGFHK8o=,iv:YrY9IsrlCaiQ8BFYqu+UnOxnvvB/JN4iYfy3vMa3wcw=,tag:41iWc4iVqjdUr02O5CLu7g==,type:str]
paperless:
@ -32,29 +29,6 @@ immich:
tailscale:
clientID: ENC[AES256_GCM,data:O8tTyy55xP85JkbJNR5daB4=,iv:SMj83Sxh7BvPRG3l5TnnpmclO5N2treUQCCJuMy8cO8=,tag:UUSN3bsZvb09cyYN65RQDg==,type:str]
clientSecret: ENC[AES256_GCM,data:c8E/a7McI+wGN9TFJ/yzTSkrhUlISmrNJdjDDMqAQrZ8s5wFEZ+4+h+dtwcjF9Ykj198glgny7cP3HubHVDw,iv:ifaP4NmLRQbYQtJQaMMCMaehosapZ2R3im9ew5h6f9E=,tag:XF+xB94nua8RZlkGxFDFFQ==,type:str]
authentik:
secret_key: ENC[AES256_GCM,data:bbEEpymADAGY/fDNMU7FfzveyK2SBUBCitQLN85tB5C5u32PRsRnOa2MDjKGU4kArnyV/WtJQXT4HJ//nMLVh53Q8BYclWYYVFourEjaajTixkZ2gkAfcMJ1mMaQG09ylrwMhLsZWbeaLFzW6dfPSQOCchvj3VhgvJSXuhNmr90=,iv:aE/DGt/yd9wL5qlWfdyT/9SIsCj7U3GcljArcGIdh9k=,tag:CVeiZITOJ71/jdLzjZjteA==,type:str]
postgresql_password: ENC[AES256_GCM,data:4okPqDzPDnx7ZBFQV2Jtk6SEHTskRd2GVG4XLdpMQrgivKsuhQJf1QAnCWHrjmtg74xdlUy2TdPwTWGd5UiM1G90GwHSzLPSJt6X0IFMxCuq/eyYYbD9w8Wk1pVuvqoluPcjN6WoRJdCzap1QITih8B+oSkTJ/rk84xczsjah4U=,iv:r0dYWcsIqdH8FGuBd2dxAJ1AjRmk6k4QYKq0cnZITk4=,tag:errORaXgO7yJW7ERbmdtRg==,type:str]
oauth2:
freshrss:
client_secret: ENC[AES256_GCM,data:e7wdwWRS8KivGkcWaMgSrUEEuOTHzj1oim+qUcLD35/DA/V6itM2XqVPhqIOXHrf6pOyYgprEv14bEx8zUvtT6iXV4fsEUEWeWTgt4NI3YULtx/t0yVDq9Zc8fN9cIqGxGeig1mcQwmm7vByq58mNJEpcfz46swjN2ATf/CPJQs=,iv:xeNgSKd+g4ne8NLw/2KQjTXSvNkqezOhMn5niuWpD38=,tag:ElOUMg0oZ+q15hCgh/Mzug==,type:str]
hedgedoc:
client_secret: ENC[AES256_GCM,data:hdNQzatO6Pf6mxvfO4h1XrhycKMBUHElEwacGttzByi4JDbIndAwYc2GXdwUmytPMYs/s+lVjcdHhspUFWS01DETWQfnWm/GN73GzW18uj3XyRXqt62HhMf18GvRlOWkGX+jYpUTGGoonYes2xijhD/mNCjxKk5Q+6FVFT2mdJ4=,iv:pScEX6YnoU7HelxmCes8A9vJjPdvFbqbclHYMme8OOE=,tag:FURxphI8IDMvOwB4ahD8hg==,type:str]
paperless-ngx:
client_secret: ENC[AES256_GCM,data:GgF+gQt8olzKUzGMDL6mh6UWDv49OPDH5tB/gboWkFd7Njc1SrSkqf71gQryOcPQ0vpXrh0nK1z6ZjMpmDEA5ohTwWymeLCgwNtJSAMHZ1VlZ2aQZr70r3KtAxKjmTiT5flUYnxS79fCF43BveSMGeAshRCvQmYCdi43sP2E4To=,iv:DzsIRPiMzxaqVrjaHMVKWgOR0asZQzWf8EE1nxRSJmk=,tag:79bo7EzVq9tvL6ap6jfV+Q==,type:str]
forgejo:
client_secret: ENC[AES256_GCM,data:I0LBIrsPuARFEcvu0sKhIbkEYxLhZrwpRfPls3KDARu5rnfwgbJ6AVtfMmcAIM9ISFzXykoyMXossHo1i23N90PsHdl2t580EffhJ+q/UUfCIk7/rX/6CXlcb8WHdab4ymN5r9jEsgD3mAWX55IehU96ZKGRKRhxSIowCIYRhyQ=,iv:1wQDGCDhSu0s+IqXULiHmRiKGTLRvOjwsYaNMCWfkjg=,tag:p1mwks0KP9lhbciTIv3/Dw==,type:str]
immich:
client_secret: ENC[AES256_GCM,data:KrsaLLsjfQsyNQzvQF/pCLj1dhi8tr/OdToY7WczvPUUQKMtSk//oxsiPike/HoVEuCUp+j7UlTfIRPF2xUcPPvw7pkcLhQhcot79aieI1ciIeLZ1Q5svsPrqDBmDY7g65jkzA9vjM9VLTsx4Dx/1vGHDqo7I12qadEQlKAuhhQ=,iv:3icAM7sVe2HlmosbP7VPbcF4SRz/mlbzdQ1gENR9TRs=,tag:O8TCN7NltNpDGoG3T8Ds1w==,type:str]
nextcloud:
client_secret: ENC[AES256_GCM,data:zLejYbfudK/4OquLXPYTv9YOmFpCVfg0KLNkDSDCpFrxroDUAXBCLtYXiGuYkYrD/t7LAzRt+OTq70d7ciuHhBNSLclP2U97BQoXCWscWnxQauRZ+UCABvP+DB9VPQmCwU+uKPrKQ8l51baj+MkpIDdk2lwavpONMU57Zov6N2o=,iv:aQ4bsXUXn177tCxe1kAsSMP9ynEzvDwN0hwFhrT3Nko=,tag:EFcnf6VmyFt2i4+aL56sWw==,type:str]
kitchenowl:
client_secret: ENC[AES256_GCM,data:x4Xsd3d3El59HKBYNV56ah314hYSRhzt46upW34cOopXNHSB3zCDrD46LUa6i8g6V5GJyrMpMfO5mv+b80JrmfHkhGUXZXuTwDNu6ijnO6ZCvC2Bdlo+T0tlkJe25OMCBseJkkC++UBrpKQQTAhyVjnPSVrGVvtY4WtdAw+X/OY=,iv:pOowIhPD7kb2F3ylFzLwNW3BhPZyzoFCGRm2+KCmhno=,tag:GxFI0w06EyGxFwj6Fv4ZLQ==,type:str]
mealie:
client_secret: ENC[AES256_GCM,data:VNEV8a1KZc6XVeRzyBWzuwldTmxEepPRUOEMEM3HKrDIkxcGHDuoLh5P7Ti+jS5rbmua+ET4GPcJTYXR+pO5/cMaxqFONj1D1w9541QPYZNBbTfPM/Zfu8OnzngVsCnnKEtu1bVwflUnmf7F5hHED8zJRe1F9PT/HYA6NCd4ajQ=,iv:58ysTItP8UNnQWwgWRS1dk/K/2dJv3P5wa5rGnz2P/I=,tag:vLGrFldzOey9ANW010GylA==,type:str]
smtp2go:
username: ENC[AES256_GCM,data:BEr7Rq7rlGvfYEpY/ZXnhM2eClnHdqU81A==,iv:dwYD5h+C5bzS9ikUgxQ51+jRQ32TtDy2PhDbd1tpS8Q=,tag:CjjLDz5n4H28qi8jWf9S4w==,type:str]
password: ENC[AES256_GCM,data:Yys6qy6DRYo16+X+Uj9oa9otjaKBnHOtIQ==,iv:G7H9mxsODShFoVlNMwuV8O18NBG/7LTFDFdqnH83YkE=,tag:hSlYp27QMoPZwiKBqyOpKA==,type:str]
sops:
kms: []
gcp_kms: []
@ -79,8 +53,8 @@ sops:
azR0UkJyL0RwUVk4ZzdkSWptcDlWVjAK5FU9B5TBSnV3azO4eCv13T6i3dGGuI68
UgBrVEb1/Fv+4XTjeSEhpiOaH8sNWYoNa3Aa7uTZYlHDRWga2GC7zw==
-----END AGE ENCRYPTED FILE-----
lastmodified: "2025-05-26T20:32:01Z"
mac: ENC[AES256_GCM,data:si28Fu1crF2mYYCJAgN95+G8iJkn4T9wF0Itpi+5cjoSZ2ebxm2wWnVLQ9PwLIkHVF7nNbQM4fWy3eGIWWpexW6ReEc/aGJBLM0L4ho7iFaO1tzWEa5nTyz3QQH8kap1xvqEYgwH9EDkblc4gFpCUDnYbBt9lNcRCZ3JzeYoPxQ=,iv:QHsvuyCCn+9oe5ZQJi2/qDtV7Z2N4JMfqXUEqJkzKH4=,tag:2NGB4VR5bPEZmIC/lYX2VQ==,type:str]
lastmodified: "2024-12-01T13:22:41Z"
mac: ENC[AES256_GCM,data:6UqmxHJC4KWsiQttXFEEG1opPcrGntYj9nlD8m0iBqjc9g/SHxEogpaiYEnriGNXGw0HhRWjrd+JX29Ht4xVeiYqthYX+4rVuIuv+SI7p08hJeIBbIYrfonAJsebbSsynuy9YgyUkNZhoqjZTtuzFU/c4Dh5453RVnuQmu4PZNs=,iv:yA//mqJ0Ft63eRME8A1HBiZ/B0gcVYlS4MaP0LykooU=,tag:0NxU0lVi67N34eDhsT82kQ==,type:str]
pgp: []
unencrypted_suffix: _unencrypted
version: 3.9.4
version: 3.9.1