Compare commits
46 Commits
d1b81ce0db
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
0f90778b53
|
|||
|
dec32b9766
|
|||
|
64c13da521
|
|||
|
13f8c64f29
|
|||
|
27681c3ff5
|
|||
|
f83dca42ea
|
|||
|
94e550787e
|
|||
|
508d5a3525
|
|||
|
a42e02514e
|
|||
|
413f16fb6f
|
|||
|
c85cf06186
|
|||
|
63f9d3418c
|
|||
|
46460039af
|
|||
|
b9709dd655
|
|||
|
165eee9dd7
|
|||
|
fd28865071
|
|||
|
51afe1240d
|
|||
|
74909b9cd4
|
|||
|
bd4dd7ba23
|
|||
|
ae407c99c1
|
|||
|
d086bb61ed
|
|||
|
4d003329c7
|
|||
|
80ae32d799
|
|||
|
58ead2bb23
|
|||
|
5c3252746f
|
|||
|
68588f5d72
|
|||
|
ee0cb47e34
|
|||
|
c9866fd424
|
|||
|
b1ecb9f021
|
|||
|
da17ccc446
|
|||
|
73f8cb91c4
|
|||
|
29cff3bf84
|
|||
|
dd904c151d
|
|||
|
b02a06faa7
|
|||
|
7b76ffd34f
|
|||
|
4f79df9bf2
|
|||
|
7cfa0e6375
|
|||
|
3005a07e28
|
|||
|
8fa31858b4
|
|||
|
120e38fd46
|
|||
|
af6952fb1e
|
|||
|
c5dc30dd4a
|
|||
|
3d77a1478a
|
|||
|
f54c95980f
|
|||
|
f89a7e1813
|
|||
|
0993820675
|
@@ -6,12 +6,13 @@ return {
|
||||
lua = { "stylua" },
|
||||
luau = { "stylua" },
|
||||
rust = { "rustfmt", lsp_format = "fallback" },
|
||||
javascript = { "prettier" },
|
||||
typescript = { "prettier" },
|
||||
json = { "prettier" },
|
||||
tsx = { "prettier" },
|
||||
javascript = { "prettierd" },
|
||||
typescript = { "prettierd" },
|
||||
json = { "prettierd" },
|
||||
tsx = { "prettierd" },
|
||||
nix = { "nixfmt" },
|
||||
go = { "gofmt" },
|
||||
svelte = { "prettierd" },
|
||||
},
|
||||
format_on_save = {
|
||||
-- These options will be passed to conform.format()
|
||||
|
||||
@@ -69,6 +69,16 @@ local function setup_java()
|
||||
})
|
||||
end
|
||||
|
||||
local function setup_svelte()
|
||||
require("lspconfig").svelte.setup({
|
||||
root_dir = require("lspconfig.util").root_pattern("svelte.config.js", "svelte.config.ts", "package.json"),
|
||||
})
|
||||
end
|
||||
|
||||
local function setup_qml()
|
||||
require("lspconfig").qmlls.setup({})
|
||||
end
|
||||
|
||||
return {
|
||||
{
|
||||
"neovim/nvim-lspconfig",
|
||||
@@ -101,14 +111,18 @@ return {
|
||||
"jdtls",
|
||||
"clangd",
|
||||
"cmake",
|
||||
"cssls",
|
||||
"qmlls",
|
||||
},
|
||||
automatic_enable = { exclude = { "luau_lsp", "lua_ls" } },
|
||||
automatic_enable = { exclude = { "luau_lsp", "lua_ls", "svelte" } },
|
||||
})
|
||||
setup_luau()
|
||||
setup_lua()
|
||||
setup_ts()
|
||||
setup_nix()
|
||||
setup_java()
|
||||
setup_svelte()
|
||||
setup_qml()
|
||||
end,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ return {
|
||||
"html",
|
||||
"templ",
|
||||
"go",
|
||||
"qmljs",
|
||||
},
|
||||
auto_install = true,
|
||||
highlight = {
|
||||
|
||||
1
.config/quickshell/.qmlls.ini
Symbolic link
1
.config/quickshell/.qmlls.ini
Symbolic link
@@ -0,0 +1 @@
|
||||
/run/user/1000/quickshell/vfs/d51aaef3451280dfdf2f7cf03412fd13/.qmlls.ini
|
||||
52
.config/quickshell/Bar.qml
Normal file
52
.config/quickshell/Bar.qml
Normal file
@@ -0,0 +1,52 @@
|
||||
//qmllint disable unqualified
|
||||
//qmllint disable unused-imports
|
||||
//qmllint disable uncreatable-type
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import "components"
|
||||
|
||||
PanelWindow {
|
||||
id: root
|
||||
anchors.top: true
|
||||
anchors.left: true
|
||||
anchors.right: true
|
||||
implicitHeight: 34
|
||||
color: "#1a1b26"
|
||||
required property var modelData
|
||||
screen: modelData
|
||||
|
||||
property int fontSize: 18
|
||||
property color colBg: "#1a1b26"
|
||||
property color colFg: "#a9b1d6"
|
||||
property color colMuted: "#444b6a"
|
||||
property color colCyan: "#0db9d7"
|
||||
property color colPurple: "#ad8ee6"
|
||||
property color colRed: "#f7768e"
|
||||
property color colYellow: "#e0af68"
|
||||
property color colBlue: "#7aa2f7"
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
|
||||
Rectangle {
|
||||
Layout.preferredWidth: 30
|
||||
Layout.preferredHeight: 28
|
||||
color: "transparent"
|
||||
|
||||
Image {
|
||||
anchors.fill: parent
|
||||
source: "file:///home/luca/dotfiles/.config/quickshell/icons/nixos.png"
|
||||
fillMode: Image.PreserveAspectFit
|
||||
}
|
||||
}
|
||||
|
||||
Workspaces {}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
Clock {}
|
||||
}
|
||||
}
|
||||
34
.config/quickshell/components/Clock.qml
Normal file
34
.config/quickshell/components/Clock.qml
Normal file
@@ -0,0 +1,34 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
Item {
|
||||
id: clockRoot
|
||||
implicitWidth: timeText.implicitWidth
|
||||
implicitHeight: timeText.implicitHeight
|
||||
|
||||
property string dateTime: ""
|
||||
|
||||
function updateTime() {
|
||||
dateTime = Qt.formatDateTime(new Date(), "ddd, MMM dd | HH:mm:ss");
|
||||
}
|
||||
|
||||
Timer {
|
||||
interval: 1000
|
||||
running: true
|
||||
repeat: true
|
||||
triggeredOnStart: true
|
||||
onTriggered: clockRoot.updateTime()
|
||||
}
|
||||
|
||||
Text {
|
||||
id: timeText
|
||||
text: clockRoot.dateTime
|
||||
color: "#a9b1d6"
|
||||
font {
|
||||
pixelSize: 16
|
||||
family: "Comic Relief"
|
||||
bold: true
|
||||
}
|
||||
anchors.centerIn: parent
|
||||
}
|
||||
}
|
||||
84
.config/quickshell/components/Workspaces.qml
Normal file
84
.config/quickshell/components/Workspaces.qml
Normal file
@@ -0,0 +1,84 @@
|
||||
//qmllint disable unqualified
|
||||
//qmllint disable unused-imports
|
||||
//qmllint disable uncreatable-type
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Hyprland
|
||||
import Quickshell.Io
|
||||
|
||||
Repeater {
|
||||
model: Hyprland.workspaces.values
|
||||
|
||||
delegate: Rectangle {
|
||||
id: workspaceComponent
|
||||
Layout.preferredWidth: 30
|
||||
Layout.preferredHeight: parent.height
|
||||
color: "transparent"
|
||||
|
||||
property bool isActive: modelData.focused
|
||||
property int lastActive: 1
|
||||
property int wsId: modelData.id
|
||||
property bool isHovered: mouseHandler.containsMouse
|
||||
|
||||
states: [
|
||||
State {
|
||||
name: "active"
|
||||
when: isActive
|
||||
PropertyChanges {
|
||||
target: workspaceComponent
|
||||
color: Qt.rgba(1, 1, 1, 0.2)
|
||||
}
|
||||
PropertyChanges {
|
||||
target: underline
|
||||
color: root.colPurple
|
||||
}
|
||||
},
|
||||
State {
|
||||
name: "hovered"
|
||||
when: isHovered && !isActive
|
||||
PropertyChanges {
|
||||
target: workspaceComponent
|
||||
color: Qt.rgba(1, 1, 1, 0.1)
|
||||
}
|
||||
PropertyChanges {
|
||||
target: underline
|
||||
color: "#7aa2f7"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
transitions: Transition {
|
||||
ColorAnimation {
|
||||
duration: 200
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
text: modelData.id
|
||||
color: isActive ? "#a9b1d6" : "#7aa2f7"
|
||||
font {
|
||||
pixelSize: root.fontSize
|
||||
bold: true
|
||||
family: "Comic Relief"
|
||||
}
|
||||
anchors.centerIn: parent
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: underline
|
||||
width: 30
|
||||
height: 3
|
||||
color: root.colBg
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: mouseHandler
|
||||
hoverEnabled: true
|
||||
anchors.fill: parent
|
||||
onClicked: Hyprland.dispatch("workspace " + modelData.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
BIN
.config/quickshell/icons/nixos.png
Normal file
BIN
.config/quickshell/icons/nixos.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 97 KiB |
15
.config/quickshell/shell.qml
Normal file
15
.config/quickshell/shell.qml
Normal file
@@ -0,0 +1,15 @@
|
||||
//qmllint disable unqualified
|
||||
//qmllint disable unused-imports
|
||||
//qmllint disable uncreatable-type
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
|
||||
Scope {
|
||||
Variants {
|
||||
model: Quickshell.screens
|
||||
delegate: Component {
|
||||
Bar {}
|
||||
}
|
||||
}
|
||||
}
|
||||
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
nix/homelab/target
|
||||
@@ -1,5 +1,5 @@
|
||||
keys:
|
||||
- &luca age1qu9y0dn5a704dggwmpaaurxqrhxm0qn8czgv5phka56y48sw7u8qkyn637
|
||||
- &luca age13rqgrxh0fm23n3krf6v7yrrlnhhvs8256cusxqfs2l5xz8rgavssdhte4r
|
||||
|
||||
creation_rules:
|
||||
- path_regex: secrets/[^/]+\.(yaml|json|env|ini)$
|
||||
|
||||
1
aliases/happly.sh
Executable file
1
aliases/happly.sh
Executable file
@@ -0,0 +1 @@
|
||||
helmfile apply -f ~/dotfiles/nix/homelab/helm "$@"
|
||||
1
aliases/kapply.sh
Executable file
1
aliases/kapply.sh
Executable file
@@ -0,0 +1 @@
|
||||
kubectl apply -k ~/dotfiles/nix/homelab/kustomize
|
||||
60
nix/flake.lock
generated
60
nix/flake.lock
generated
@@ -81,6 +81,27 @@
|
||||
},
|
||||
"parent": []
|
||||
},
|
||||
"fenix": {
|
||||
"inputs": {
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
],
|
||||
"rust-analyzer-src": "rust-analyzer-src"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1768632427,
|
||||
"narHash": "sha256-Y6kP10exkn5UiK9ead2Gky8TFsFZSsyT4f69DMKm0Wo=",
|
||||
"owner": "nix-community",
|
||||
"repo": "fenix",
|
||||
"rev": "edd560269f0d9ad75bd3da292ce4d9d27efdd22a",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-community",
|
||||
"repo": "fenix",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-compat": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
@@ -186,17 +207,56 @@
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"quickshell": {
|
||||
"inputs": {
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1768689040,
|
||||
"narHash": "sha256-Tlnr5BulJcMers/cb+YvmBQW4nKHjdKo9loInJkyO2k=",
|
||||
"ref": "refs/heads/master",
|
||||
"rev": "7a427ce1979ce7447e885c4f30129b40f3d466f5",
|
||||
"revCount": 729,
|
||||
"type": "git",
|
||||
"url": "https://git.outfoxxed.me/outfoxxed/quickshell"
|
||||
},
|
||||
"original": {
|
||||
"type": "git",
|
||||
"url": "https://git.outfoxxed.me/outfoxxed/quickshell"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"custom-fonts": "custom-fonts",
|
||||
"fenix": "fenix",
|
||||
"home-manager": "home-manager",
|
||||
"nixos-wsl": "nixos-wsl",
|
||||
"nixpkgs": "nixpkgs_2",
|
||||
"nixpkgs-before": "nixpkgs-before",
|
||||
"quickshell": "quickshell",
|
||||
"sops-nix": "sops-nix",
|
||||
"status-bar": "status-bar"
|
||||
}
|
||||
},
|
||||
"rust-analyzer-src": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1768468158,
|
||||
"narHash": "sha256-DfifO/Se9ogmp5rxe/OwmRIz20/w6BsbWC1s4kL1Bzc=",
|
||||
"owner": "rust-lang",
|
||||
"repo": "rust-analyzer",
|
||||
"rev": "adbff8baedae53f9955fe60c0d470ecd77b4f548",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "rust-lang",
|
||||
"ref": "nightly",
|
||||
"repo": "rust-analyzer",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"sops-nix": {
|
||||
"inputs": {
|
||||
"nixpkgs": [
|
||||
|
||||
@@ -21,6 +21,15 @@
|
||||
url = "github:Mic92/sops-nix";
|
||||
inputs.nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
|
||||
fenix = {
|
||||
url = "github:nix-community/fenix";
|
||||
inputs.nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
quickshell = {
|
||||
url = "git+https://git.outfoxxed.me/outfoxxed/quickshell";
|
||||
inputs.nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
};
|
||||
|
||||
outputs =
|
||||
@@ -70,6 +79,7 @@
|
||||
inherit inputs;
|
||||
meta = {
|
||||
hostname = host.name;
|
||||
architecture = host.architecture;
|
||||
};
|
||||
pkgs-before = import inputs.nixpkgs-before { system = host.architecture; };
|
||||
};
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
{ config, pkgs, ... }:
|
||||
|
||||
{
|
||||
home.username = "luca";
|
||||
home.homeDirectory = "/home/luca";
|
||||
|
||||
programs = {
|
||||
git = import ./git.nix;
|
||||
alacritty = {
|
||||
enable = true;
|
||||
settings.window.opacity = 0.6;
|
||||
};
|
||||
zsh = {
|
||||
enable = true;
|
||||
enableCompletion = true;
|
||||
autosuggestion.enable = true;
|
||||
};
|
||||
};
|
||||
|
||||
services.picom = {
|
||||
enable = true;
|
||||
vSync = true;
|
||||
};
|
||||
|
||||
xsession.windowManager.i3 = {
|
||||
enable = true;
|
||||
config = {
|
||||
modifier = "Mod4";
|
||||
defaultWorkspace = "workspace number 1";
|
||||
terminal = "alacritty";
|
||||
};
|
||||
};
|
||||
|
||||
home.stateVersion = "24.11";
|
||||
|
||||
programs.home-manager.enable = true;
|
||||
|
||||
}
|
||||
3291
nix/homelab/Cargo.lock
generated
Normal file
3291
nix/homelab/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
27
nix/homelab/Cargo.toml
Normal file
27
nix/homelab/Cargo.toml
Normal file
@@ -0,0 +1,27 @@
|
||||
[package]
|
||||
name = "homelab"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.100"
|
||||
askama = "0.15.1"
|
||||
clap = { version = "4.5.53", features = ["derive"] }
|
||||
nom = "8.0.0"
|
||||
reqwest = { version = "0.13.1", features = ["json"] }
|
||||
serde = { version = "1.0.228", features = ["serde_derive"] }
|
||||
ssh2 = "0.9.5"
|
||||
thiserror = "2.0.17"
|
||||
tokio = { version = "1.49.0", features = ["macros", "rt"] }
|
||||
toml = { version = "0.9.10", optional = true }
|
||||
urlencoding = "2"
|
||||
|
||||
[workspace]
|
||||
members = [
|
||||
".",
|
||||
"cli"
|
||||
]
|
||||
|
||||
[features]
|
||||
default = ["file-config"]
|
||||
file-config = ["dep:toml"]
|
||||
2
nix/homelab/api/.gitignore
vendored
Normal file
2
nix/homelab/api/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
target
|
||||
.env
|
||||
2842
nix/homelab/api/Cargo.lock
generated
Normal file
2842
nix/homelab/api/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
19
nix/homelab/api/Cargo.toml
Normal file
19
nix/homelab/api/Cargo.toml
Normal file
@@ -0,0 +1,19 @@
|
||||
[package]
|
||||
name = "homelab-api"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
actix-web = "4.12.1"
|
||||
dotenvy = "0.15.7"
|
||||
nom = "8.0.0"
|
||||
rcon = { version = "0.6.0", features = ["rt-tokio"] }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
thiserror = "2.0.17"
|
||||
tokio = "1.49.0"
|
||||
tokio-util = { version = "0.7", features = ["io"] }
|
||||
kube = { version = "2.0.1", features = ["client", "runtime", "ws"] }
|
||||
k8s-openapi = { version = "0.26", features = ["v1_32"] }
|
||||
chrono = "0.4"
|
||||
futures = "0.3"
|
||||
27
nix/homelab/api/flake.lock
generated
Normal file
27
nix/homelab/api/flake.lock
generated
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"nodes": {
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1767799921,
|
||||
"narHash": "sha256-r4GVX+FToWVE2My8VVZH4V0pTIpnu2ZE8/Z4uxGEMBE=",
|
||||
"owner": "nixos",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "d351d0653aeb7877273920cd3e823994e7579b0b",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nixos",
|
||||
"ref": "nixos-25.11",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"nixpkgs": "nixpkgs"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
||||
38
nix/homelab/api/flake.nix
Normal file
38
nix/homelab/api/flake.nix
Normal file
@@ -0,0 +1,38 @@
|
||||
{
|
||||
description = "Homelab api";
|
||||
|
||||
inputs = {
|
||||
nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-25.11";
|
||||
};
|
||||
|
||||
outputs =
|
||||
{ nixpkgs, ... }@inputs:
|
||||
let
|
||||
systems = [
|
||||
"x86_64-linux"
|
||||
"aarch64-linux"
|
||||
];
|
||||
forAllSystems =
|
||||
f:
|
||||
nixpkgs.lib.genAttrs systems (
|
||||
system:
|
||||
f {
|
||||
inherit system;
|
||||
pkgs = nixpkgs.legacyPackages.${system};
|
||||
}
|
||||
);
|
||||
in
|
||||
{
|
||||
devShells = forAllSystems (
|
||||
{ system, pkgs }:
|
||||
{
|
||||
default = pkgs.mkShell {
|
||||
buildInputs = with pkgs; [
|
||||
openssl
|
||||
pkgconf
|
||||
];
|
||||
};
|
||||
}
|
||||
);
|
||||
};
|
||||
}
|
||||
36
nix/homelab/api/src/endpoints/kubernetes.rs
Normal file
36
nix/homelab/api/src/endpoints/kubernetes.rs
Normal file
@@ -0,0 +1,36 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::AppState;
|
||||
use crate::error::Result;
|
||||
use crate::kubernetes;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct RestoreRequest {
|
||||
pub backup_file: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct RestoreResponse {
|
||||
pub server: String,
|
||||
pub job_name: String,
|
||||
pub backup_file: String,
|
||||
pub status: String,
|
||||
}
|
||||
|
||||
/// POST /api/minecraft/{server}/restore
|
||||
pub async fn create_restore(
|
||||
app_state: web::Data<AppState>,
|
||||
path: web::Path<ServerPath>,
|
||||
body: web::Json<RestoreRequest>,
|
||||
) -> Result<HttpResponse> {
|
||||
let job_name =
|
||||
kubernetes::create_restore_job(&app_state.kube, &path.server, &body.backup_file).await?;
|
||||
|
||||
Ok(HttpResponse::Created().json(RestoreResponse {
|
||||
server: path.server.clone(),
|
||||
job_name,
|
||||
backup_file: body.backup_file.clone(),
|
||||
status: "created".to_string(),
|
||||
}))
|
||||
}
|
||||
5
nix/homelab/api/src/endpoints/mod.rs
Normal file
5
nix/homelab/api/src/endpoints/mod.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
pub mod server_stats;
|
||||
pub mod server_uptime;
|
||||
pub mod world_size;
|
||||
|
||||
pub(self) const MINECRAFT_NAMESPACE: &str = "minecraft";
|
||||
36
nix/homelab/api/src/endpoints/server_stats.rs
Normal file
36
nix/homelab/api/src/endpoints/server_stats.rs
Normal file
@@ -0,0 +1,36 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::{
|
||||
AppState,
|
||||
endpoints::server_uptime::{PodUptime, get_pod_uptime},
|
||||
error::Result,
|
||||
rcon::parse_online_list,
|
||||
};
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct ServerStats {
|
||||
pub status: String,
|
||||
pub players_online: u16,
|
||||
pub max_players: u16,
|
||||
pub uptime: PodUptime,
|
||||
pub world_size: Option<String>,
|
||||
}
|
||||
|
||||
pub async fn get_server_stats(app_state: web::Data<AppState>) -> Result<HttpResponse> {
|
||||
let list_response = app_state.rcon.cmd("list").await?;
|
||||
|
||||
let (_, (players_online, max_players)) =
|
||||
parse_online_list(&list_response).map_err(|e| crate::error::Error::Parse(e.to_string()))?;
|
||||
let uptime = get_pod_uptime(&app_state.kube, "main").await?;
|
||||
|
||||
let stats = ServerStats {
|
||||
status: "Online".to_string(),
|
||||
players_online,
|
||||
max_players,
|
||||
uptime,
|
||||
world_size: None,
|
||||
};
|
||||
|
||||
Ok(HttpResponse::Ok().json(stats))
|
||||
}
|
||||
56
nix/homelab/api/src/endpoints/server_uptime.rs
Normal file
56
nix/homelab/api/src/endpoints/server_uptime.rs
Normal file
@@ -0,0 +1,56 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
use k8s_openapi::api::core::v1::Pod;
|
||||
use kube::{Api, Client, api::ListParams};
|
||||
|
||||
use crate::{
|
||||
AppState,
|
||||
endpoints::MINECRAFT_NAMESPACE,
|
||||
error::{Error, Result},
|
||||
};
|
||||
|
||||
#[derive(serde::Serialize)]
|
||||
pub(super) struct PodUptime {
|
||||
pub seconds: i64,
|
||||
pub started_at: String,
|
||||
}
|
||||
|
||||
pub async fn get_uptime(
|
||||
app_state: web::Data<AppState>,
|
||||
path: web::Path<String>,
|
||||
) -> Result<HttpResponse> {
|
||||
let server = path.into_inner();
|
||||
let uptime = get_pod_uptime(&app_state.kube, &server).await?;
|
||||
|
||||
Ok(HttpResponse::Ok().json(uptime))
|
||||
}
|
||||
|
||||
pub(super) async fn find_minecraft_pod(client: &Client, server_name: &str) -> Result<Pod> {
|
||||
let pods: Api<Pod> = Api::namespaced(client.clone(), MINECRAFT_NAMESPACE);
|
||||
|
||||
let label_selector = format!("app=minecraft-{server_name}");
|
||||
let lp = ListParams::default().labels(&label_selector);
|
||||
|
||||
let pod_list = pods.list(&lp).await?;
|
||||
|
||||
pod_list
|
||||
.items
|
||||
.into_iter()
|
||||
.next()
|
||||
.ok_or_else(|| Error::PodNotFound(format!("minecraft-{server_name}")))
|
||||
}
|
||||
|
||||
pub(super) async fn get_pod_uptime(client: &Client, server_name: &str) -> Result<PodUptime> {
|
||||
let pod = find_minecraft_pod(client, server_name).await?;
|
||||
|
||||
let start_time = pod
|
||||
.status
|
||||
.and_then(|s| s.start_time)
|
||||
.ok_or_else(|| Error::PodExec("pod has no start time".into()))?;
|
||||
let now = chrono::Utc::now();
|
||||
let duration = now.signed_duration_since(&start_time.0);
|
||||
|
||||
Ok(PodUptime {
|
||||
seconds: duration.num_seconds(),
|
||||
started_at: start_time.0.to_rfc3339(),
|
||||
})
|
||||
}
|
||||
56
nix/homelab/api/src/endpoints/world_size.rs
Normal file
56
nix/homelab/api/src/endpoints/world_size.rs
Normal file
@@ -0,0 +1,56 @@
|
||||
use actix_web::{HttpResponse, web};
|
||||
use k8s_openapi::api::core::v1::Pod;
|
||||
use kube::{Api, Client};
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::{
|
||||
AppState,
|
||||
endpoints::{MINECRAFT_NAMESPACE, server_uptime::find_minecraft_pod},
|
||||
error::{Error, Result},
|
||||
};
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct WorldSizeResponse {
|
||||
pub server: String,
|
||||
pub size: String,
|
||||
}
|
||||
|
||||
pub async fn get_world_size(
|
||||
app_state: web::Data<AppState>,
|
||||
path: web::Path<String>,
|
||||
) -> Result<HttpResponse> {
|
||||
let server = path.into_inner();
|
||||
let size = get_size_inner(&app_state.kube, &server).await?;
|
||||
|
||||
Ok(HttpResponse::Ok().json(WorldSizeResponse {
|
||||
server: server.clone(),
|
||||
size,
|
||||
}))
|
||||
}
|
||||
|
||||
pub async fn get_size_inner(client: &Client, server_name: &str) -> Result<String> {
|
||||
let pod = find_minecraft_pod(client, server_name).await?;
|
||||
let pod_name = pod
|
||||
.metadata
|
||||
.name
|
||||
.ok_or_else(|| Error::PodNotFound("no pod name".into()))?;
|
||||
|
||||
let output = exec_in_pod(client, &pod_name, vec!["du", "-sh", "/data"]).await?;
|
||||
|
||||
let size = output
|
||||
.split_whitespace()
|
||||
.next()
|
||||
.unwrap_or("unknown")
|
||||
.to_string();
|
||||
|
||||
Ok(size)
|
||||
}
|
||||
|
||||
async fn exec_in_pod(client: &Client, pod_name: &str, command: &str) {
|
||||
let pods: Api<Pod> = Api::namespaced(client.clone(), MINECRAFT_NAMESPACE);
|
||||
|
||||
const params = AttachParams {
|
||||
stdin: true,
|
||||
..Default::default()
|
||||
};
|
||||
}
|
||||
33
nix/homelab/api/src/error.rs
Normal file
33
nix/homelab/api/src/error.rs
Normal file
@@ -0,0 +1,33 @@
|
||||
use actix_web::{HttpResponse, ResponseError, body::BoxBody, http::StatusCode};
|
||||
use thiserror::Error;
|
||||
|
||||
pub type Result<T> = core::result::Result<T, Error>;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum Error {
|
||||
#[error("rcon error: {0}")]
|
||||
Rcon(#[from] rcon::Error),
|
||||
#[error("parse error: {0}")]
|
||||
Parse(String),
|
||||
#[error("kubernetes error: {0}")]
|
||||
Kube(#[from] kube::Error),
|
||||
#[error("pod not found: {0}")]
|
||||
PodNotFound(String),
|
||||
#[error("pod exec error: {0}")]
|
||||
PodExec(String),
|
||||
}
|
||||
|
||||
impl ResponseError for Error {
|
||||
fn status_code(&self) -> StatusCode {
|
||||
match self {
|
||||
Self::Rcon(_) => StatusCode::SERVICE_UNAVAILABLE,
|
||||
Self::PodNotFound(_) => StatusCode::NOT_FOUND,
|
||||
Self::Kube(_) | Self::PodExec(_) => StatusCode::BAD_GATEWAY,
|
||||
_ => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
}
|
||||
}
|
||||
|
||||
fn error_response(&self) -> HttpResponse<BoxBody> {
|
||||
HttpResponse::build(self.status_code()).body(self.to_string())
|
||||
}
|
||||
}
|
||||
219
nix/homelab/api/src/kubernetes.rs
Normal file
219
nix/homelab/api/src/kubernetes.rs
Normal file
@@ -0,0 +1,219 @@
|
||||
use futures::TryStreamExt;
|
||||
use k8s_openapi::api::batch::v1::Job;
|
||||
use k8s_openapi::api::core::v1::Pod;
|
||||
use kube::Client;
|
||||
use kube::api::{Api, AttachParams, ListParams, PostParams};
|
||||
|
||||
use crate::error::{Error, Result};
|
||||
|
||||
const NFS_SERVER: &str = "192.168.27.2";
|
||||
const NFS_BACKUP_PATH: &str = "/backup/minecraft";
|
||||
|
||||
/// Find the Minecraft server pod by server name (e.g., "main", "creative")
|
||||
|
||||
/// Execute a command in a pod and return stdout
|
||||
pub async fn exec_in_pod(client: &Client, pod_name: &str, command: Vec<&str>) -> Result<String> {
|
||||
let pods: Api<Pod> = Api::namespaced(client.clone(), MINECRAFT_NAMESPACE);
|
||||
|
||||
let ap = AttachParams {
|
||||
stdout: true,
|
||||
stderr: true,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let mut attached = pods.exec(pod_name, command, &ap).await?;
|
||||
|
||||
let mut stdout_str = String::new();
|
||||
if let Some(stdout) = attached.stdout() {
|
||||
let bytes: Vec<u8> = tokio_util::io::ReaderStream::new(stdout)
|
||||
.try_collect::<Vec<_>>()
|
||||
.await
|
||||
.map_err(|e| Error::PodExec(e.to_string()))?
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.collect();
|
||||
stdout_str = String::from_utf8_lossy(&bytes).to_string();
|
||||
}
|
||||
|
||||
// Wait for exec to finish
|
||||
attached
|
||||
.join()
|
||||
.await
|
||||
.map_err(|e| Error::PodExec(e.to_string()))?;
|
||||
|
||||
Ok(stdout_str.trim().to_string())
|
||||
}
|
||||
|
||||
/// Get the world size by running du -sh /data in the pod
|
||||
/// Get pod uptime by calculating time since pod started
|
||||
|
||||
/// Create a restore job for a Minecraft server
|
||||
pub async fn create_restore_job(
|
||||
client: &Client,
|
||||
server_name: &str,
|
||||
backup_file: &str,
|
||||
) -> Result<String> {
|
||||
let jobs: Api<Job> = Api::namespaced(client.clone(), MINECRAFT_NAMESPACE);
|
||||
|
||||
let job = build_restore_job(server_name, backup_file);
|
||||
let job_name = job
|
||||
.metadata
|
||||
.name
|
||||
.clone()
|
||||
.unwrap_or_else(|| "unknown".into());
|
||||
|
||||
jobs.create(&PostParams::default(), &job).await?;
|
||||
|
||||
Ok(job_name)
|
||||
}
|
||||
|
||||
fn build_restore_job(server_name: &str, backup_file: &str) -> Job {
|
||||
use k8s_openapi::api::core::v1::{
|
||||
Container, EnvVar, NFSVolumeSource, PersistentVolumeClaimVolumeSource, PodSecurityContext,
|
||||
PodSpec, PodTemplateSpec, Volume, VolumeMount,
|
||||
};
|
||||
|
||||
let job_name = format!(
|
||||
"minecraft-restore-{server_name}-{}",
|
||||
chrono::Utc::now().timestamp()
|
||||
);
|
||||
|
||||
let restore_script = r#"
|
||||
set -e
|
||||
|
||||
echo "=========================================="
|
||||
echo "Minecraft World Restore Job"
|
||||
echo "=========================================="
|
||||
echo ""
|
||||
|
||||
SERVER_NAME="${SERVER_NAME}"
|
||||
BACKUP_FILE="${BACKUP_FILE:-latest.tgz}"
|
||||
BACKUP_PATH="/backups/${BACKUP_FILE}"
|
||||
DATA_DIR="/data"
|
||||
|
||||
echo "Configuration:"
|
||||
echo " Server: ${SERVER_NAME}"
|
||||
echo " Backup file: ${BACKUP_FILE}"
|
||||
echo " Backup path: ${BACKUP_PATH}"
|
||||
echo " Data directory: ${DATA_DIR}"
|
||||
echo ""
|
||||
|
||||
if [ ! -f "${BACKUP_PATH}" ]; then
|
||||
echo "ERROR: Backup file not found: ${BACKUP_PATH}"
|
||||
echo ""
|
||||
echo "Available backups:"
|
||||
ls -lh /backups/ | grep "minecraft-${SERVER_NAME}" || echo " (none found)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Backup file found"
|
||||
echo " Size: $(du -hL ${BACKUP_PATH} | cut -f1)"
|
||||
echo ""
|
||||
|
||||
if [ -d "${DATA_DIR}/world" ]; then
|
||||
echo "WARNING: Existing world data found!"
|
||||
echo "Removing existing world data..."
|
||||
rm -rf "${DATA_DIR}/world" "${DATA_DIR}/world_nether" "${DATA_DIR}/world_the_end"
|
||||
echo "Old world data removed"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
echo "Extracting backup..."
|
||||
tar -xzf "${BACKUP_PATH}" -C "${DATA_DIR}/"
|
||||
|
||||
echo ""
|
||||
echo "=========================================="
|
||||
echo "Restore Complete!"
|
||||
echo "=========================================="
|
||||
"#;
|
||||
|
||||
Job {
|
||||
metadata: kube::core::ObjectMeta {
|
||||
name: Some(job_name),
|
||||
namespace: Some(MINECRAFT_NAMESPACE.to_string()),
|
||||
labels: Some(
|
||||
[("app".to_string(), "minecraft-restore".to_string())]
|
||||
.into_iter()
|
||||
.collect(),
|
||||
),
|
||||
..Default::default()
|
||||
},
|
||||
spec: Some(k8s_openapi::api::batch::v1::JobSpec {
|
||||
backoff_limit: Some(0),
|
||||
ttl_seconds_after_finished: Some(3600),
|
||||
template: PodTemplateSpec {
|
||||
metadata: Some(kube::core::ObjectMeta {
|
||||
labels: Some(
|
||||
[("app".to_string(), "minecraft-restore".to_string())]
|
||||
.into_iter()
|
||||
.collect(),
|
||||
),
|
||||
..Default::default()
|
||||
}),
|
||||
spec: Some(PodSpec {
|
||||
restart_policy: Some("Never".to_string()),
|
||||
security_context: Some(PodSecurityContext {
|
||||
fs_group: Some(2000),
|
||||
run_as_user: Some(1000),
|
||||
run_as_group: Some(3000),
|
||||
..Default::default()
|
||||
}),
|
||||
containers: vec![Container {
|
||||
name: "restore".to_string(),
|
||||
image: Some("busybox:latest".to_string()),
|
||||
command: Some(vec!["sh".to_string(), "-c".to_string()]),
|
||||
args: Some(vec![restore_script.to_string()]),
|
||||
env: Some(vec![
|
||||
EnvVar {
|
||||
name: "SERVER_NAME".to_string(),
|
||||
value: Some(server_name.to_string()),
|
||||
..Default::default()
|
||||
},
|
||||
EnvVar {
|
||||
name: "BACKUP_FILE".to_string(),
|
||||
value: Some(backup_file.to_string()),
|
||||
..Default::default()
|
||||
},
|
||||
]),
|
||||
volume_mounts: Some(vec![
|
||||
VolumeMount {
|
||||
name: "data".to_string(),
|
||||
mount_path: "/data".to_string(),
|
||||
..Default::default()
|
||||
},
|
||||
VolumeMount {
|
||||
name: "backups".to_string(),
|
||||
mount_path: "/backups".to_string(),
|
||||
read_only: Some(true),
|
||||
..Default::default()
|
||||
},
|
||||
]),
|
||||
..Default::default()
|
||||
}],
|
||||
volumes: Some(vec![
|
||||
Volume {
|
||||
name: "data".to_string(),
|
||||
persistent_volume_claim: Some(PersistentVolumeClaimVolumeSource {
|
||||
claim_name: format!("minecraft-{server_name}-datadir"),
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
},
|
||||
Volume {
|
||||
name: "backups".to_string(),
|
||||
nfs: Some(NFSVolumeSource {
|
||||
server: NFS_SERVER.to_string(),
|
||||
path: NFS_BACKUP_PATH.to_string(),
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
},
|
||||
]),
|
||||
..Default::default()
|
||||
}),
|
||||
},
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
83
nix/homelab/api/src/main.rs
Normal file
83
nix/homelab/api/src/main.rs
Normal file
@@ -0,0 +1,83 @@
|
||||
mod endpoints;
|
||||
mod error;
|
||||
mod rcon;
|
||||
|
||||
use std::env;
|
||||
|
||||
use actix_web::{App, HttpServer, web};
|
||||
use kube::Client;
|
||||
|
||||
use crate::rcon::RconClient;
|
||||
|
||||
pub struct AppState {
|
||||
rcon: RconClient,
|
||||
kube: Client,
|
||||
}
|
||||
|
||||
struct Env {
|
||||
rcon_password: String,
|
||||
}
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
fn load_env() -> Env {
|
||||
dotenvy::dotenv().ok();
|
||||
Env {
|
||||
rcon_password: env::var("RCON_PASSWORD")
|
||||
.expect("environment variable RCON_PASSWORD must be set"),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(debug_assertions))]
|
||||
fn load_env() -> Env {
|
||||
Env {
|
||||
rcon_password: env::var("RCON_PASSWORD")
|
||||
.expect("environment variable RCON_PASSWORD must be set"),
|
||||
}
|
||||
}
|
||||
|
||||
#[actix_web::main]
|
||||
async fn main() -> std::io::Result<()> {
|
||||
let env = load_env();
|
||||
|
||||
// Initialize Kubernetes client
|
||||
// Uses in-cluster config when running in k3s, falls back to ~/.kube/config locally
|
||||
let kube_client = Client::try_default()
|
||||
.await
|
||||
.expect("failed to create Kubernetes client");
|
||||
|
||||
let app_state = web::Data::new(AppState {
|
||||
rcon: RconClient::new(env.rcon_password),
|
||||
kube: kube_client,
|
||||
});
|
||||
|
||||
HttpServer::new(move || {
|
||||
App::new()
|
||||
.app_data(app_state.clone())
|
||||
.route(
|
||||
"/",
|
||||
web::get()
|
||||
.to(async || concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"))),
|
||||
)
|
||||
.service(
|
||||
web::scope("/api")
|
||||
.route(
|
||||
"/minecraft-server-stats",
|
||||
web::get().to(endpoints::server_stats::get_server_stats),
|
||||
)
|
||||
// .route(
|
||||
// "/minecraft/{server}/world-size",
|
||||
// web::get().to(endpoints::kubernetes::get_world_size),
|
||||
// )
|
||||
.route(
|
||||
"/minecraft/{server}/uptime",
|
||||
web::get().to(endpoints::server_uptime::get_uptime),
|
||||
), // .route(
|
||||
// "/minecraft/{server}/restore",
|
||||
// web::post().to(endpoints::kubernetes::create_restore),
|
||||
// ),
|
||||
)
|
||||
})
|
||||
.bind(("127.0.0.1", 8080))?
|
||||
.run()
|
||||
.await
|
||||
}
|
||||
53
nix/homelab/api/src/rcon.rs
Normal file
53
nix/homelab/api/src/rcon.rs
Normal file
@@ -0,0 +1,53 @@
|
||||
use nom::{
|
||||
IResult, Parser, bytes::complete::tag, character::complete::digit1, combinator::map_res,
|
||||
sequence::preceded,
|
||||
};
|
||||
use rcon::Connection;
|
||||
use tokio::{net::TcpStream, sync::Mutex};
|
||||
|
||||
use crate::error::Result;
|
||||
|
||||
fn parse_u16(input: &str) -> IResult<&str, u16> {
|
||||
map_res(digit1, |s: &str| s.parse::<u16>()).parse(input)
|
||||
}
|
||||
|
||||
pub fn parse_online_list(input: &str) -> IResult<&str, (u16, u16)> {
|
||||
let (remaining, (online, max)) = (
|
||||
preceded(tag("There are "), parse_u16),
|
||||
preceded(tag(" of a max of "), parse_u16),
|
||||
)
|
||||
.parse(input)?;
|
||||
Ok((remaining, (online, max)))
|
||||
}
|
||||
|
||||
pub struct RconClient {
|
||||
connection: Mutex<Option<Connection<TcpStream>>>,
|
||||
rcon_password: String,
|
||||
}
|
||||
|
||||
impl RconClient {
|
||||
pub fn new(rcon_password: String) -> Self {
|
||||
Self {
|
||||
connection: None.into(),
|
||||
rcon_password,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn cmd(&self, command: &str) -> Result<String> {
|
||||
let mut connection = self.connection.lock().await;
|
||||
|
||||
if connection.is_none() {
|
||||
let conn = create_connection(&self.rcon_password).await?;
|
||||
*connection = Some(conn)
|
||||
}
|
||||
|
||||
Ok(connection.as_mut().unwrap().cmd(command).await?)
|
||||
}
|
||||
}
|
||||
|
||||
async fn create_connection(rcon_password: &str) -> Result<Connection<TcpStream>> {
|
||||
Ok(Connection::<TcpStream>::builder()
|
||||
.enable_minecraft_quirks(true)
|
||||
.connect("192.168.27.12:25575", rcon_password)
|
||||
.await?)
|
||||
}
|
||||
2
nix/homelab/askama.toml
Normal file
2
nix/homelab/askama.toml
Normal file
@@ -0,0 +1,2 @@
|
||||
[general]
|
||||
dirs = ["src/templates"]
|
||||
21
nix/homelab/cli/Cargo.toml
Normal file
21
nix/homelab/cli/Cargo.toml
Normal file
@@ -0,0 +1,21 @@
|
||||
[package]
|
||||
name = "homelab-cli"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.100"
|
||||
askama = "0.15.1"
|
||||
chrono = "0.4.43"
|
||||
clap = { version = "4.5.54", features = ["derive"] }
|
||||
dialoguer = "0.12.0"
|
||||
futures = "0.3.31"
|
||||
indicatif = "0.18.3"
|
||||
k8s-openapi = { version = "0.27.0", features = ["latest", "schemars", "v1_35"] }
|
||||
kube = { version = "3.0.0", features = ["runtime", "derive"] }
|
||||
schemars = "1.2.0"
|
||||
serde_json = "1.0.149"
|
||||
serde_yaml = "0.9.34"
|
||||
thiserror = "2.0.18"
|
||||
thiserror-ext = "0.3.0"
|
||||
tokio = { version = "1.49.0", features = ["macros", "rt"] }
|
||||
3
nix/homelab/cli/askama.toml
Normal file
3
nix/homelab/cli/askama.toml
Normal file
@@ -0,0 +1,3 @@
|
||||
[[escaper]]
|
||||
path = "askama::filters::Text"
|
||||
extensions = ["yaml"]
|
||||
144
nix/homelab/cli/src/commands/minecraft.rs
Normal file
144
nix/homelab/cli/src/commands/minecraft.rs
Normal file
@@ -0,0 +1,144 @@
|
||||
use askama::Template;
|
||||
use clap::{Args, Subcommand};
|
||||
use futures::{AsyncBufReadExt, StreamExt, TryStreamExt};
|
||||
use k8s_openapi::api::apps::v1::Deployment;
|
||||
use k8s_openapi::api::batch::v1::Job;
|
||||
use k8s_openapi::api::core::v1::Pod;
|
||||
use kube::api::{Api, LogParams, Patch, PatchParams, PostParams};
|
||||
use kube::runtime::{WatchStreamExt, watcher};
|
||||
use serde_json::json;
|
||||
|
||||
use crate::State;
|
||||
use crate::error::{ErrorKind, Result};
|
||||
use crate::reporter::Reporter;
|
||||
|
||||
const NAMESPACE: &str = "minecraft";
|
||||
|
||||
#[derive(Debug, Args)]
|
||||
pub struct MinecraftCommand {
|
||||
#[command(subcommand)]
|
||||
command: MinecraftSubcommand,
|
||||
}
|
||||
|
||||
#[derive(Debug, Subcommand)]
|
||||
enum MinecraftSubcommand {
|
||||
/// Backup a minecraft world
|
||||
Backup {
|
||||
/// the world to backup
|
||||
#[arg(short, long)]
|
||||
world: String,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "backup-job.yaml")]
|
||||
struct BackupJobTemplate<'a> {
|
||||
world: &'a str,
|
||||
}
|
||||
|
||||
impl MinecraftCommand {
|
||||
pub async fn run(&self, state: State) -> Result<()> {
|
||||
match &self.command {
|
||||
MinecraftSubcommand::Backup { world } => backup_world(state, world).await,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn backup_world(state: State, world: &str) -> Result<()> {
|
||||
let reporter = Reporter::new();
|
||||
let job_name = format!("minecraft-{}-backup", world);
|
||||
|
||||
reporter.log(format!("Scaling deployment minecraft-{world}"));
|
||||
scale_deployment(&state.client, NAMESPACE, &format!("minecraft-{world}"), 0).await?;
|
||||
|
||||
reporter.status("Creating backup job...");
|
||||
|
||||
let job = build_backup_job(world)?;
|
||||
let jobs: Api<Job> = Api::namespaced(state.client.clone(), NAMESPACE);
|
||||
jobs.create(&PostParams::default(), &job).await?;
|
||||
|
||||
reporter.status("Waiting for pod to start...");
|
||||
|
||||
let pods: Api<Pod> = Api::namespaced(state.client.clone(), NAMESPACE);
|
||||
let pod_name = wait_for_job_pod(&pods, &job_name).await?;
|
||||
|
||||
reporter.status("Running backup...");
|
||||
|
||||
stream_pod_logs(&pods, &pod_name, &reporter).await?;
|
||||
|
||||
let job = jobs.get(&job_name).await?;
|
||||
let status = job.status.as_ref();
|
||||
let succeeded = status.and_then(|s| s.succeeded).unwrap_or(0);
|
||||
let failed = status.and_then(|s| s.failed).unwrap_or(0);
|
||||
|
||||
reporter.log(format!("Scaling deployment minecraft-{world}, replicas: 1"));
|
||||
scale_deployment(&state.client, NAMESPACE, &format!("minecraft-{world}"), 1).await?;
|
||||
if succeeded > 0 {
|
||||
reporter.success("Backup complete");
|
||||
Ok(())
|
||||
} else if failed > 0 {
|
||||
reporter.fail("Backup job failed");
|
||||
Err(ErrorKind::BackupFailed("Job failed".to_string()).into())
|
||||
} else {
|
||||
reporter.fail("Backup job status unknown");
|
||||
Err(ErrorKind::BackupFailed("Unknown status".to_string()).into())
|
||||
}
|
||||
}
|
||||
|
||||
async fn scale_deployment(
|
||||
client: &kube::Client,
|
||||
namespace: &str,
|
||||
name: &str,
|
||||
replicas: i32,
|
||||
) -> Result<()> {
|
||||
let deployments: Api<Deployment> = Api::namespaced(client.clone(), namespace);
|
||||
let patch = json!({ "spec": { "replicas": replicas } });
|
||||
deployments
|
||||
.patch(name, &PatchParams::default(), &Patch::Merge(&patch))
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn wait_for_job_pod(pods: &Api<Pod>, job_name: &str) -> Result<String> {
|
||||
let label_selector = format!("job-name={}", job_name);
|
||||
let config = watcher::Config::default().labels(&label_selector);
|
||||
|
||||
let mut stream = watcher(pods.clone(), config).applied_objects().boxed();
|
||||
|
||||
while let Some(pod) = stream.try_next().await? {
|
||||
let name = pod.metadata.name.as_deref().unwrap_or_default();
|
||||
let phase = pod
|
||||
.status
|
||||
.as_ref()
|
||||
.and_then(|s| s.phase.as_deref())
|
||||
.unwrap_or_default();
|
||||
|
||||
if phase == "Running" || phase == "Succeeded" || phase == "Failed" {
|
||||
return Ok(name.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
Err(ErrorKind::BackupFailed("Pod never started".to_string()).into())
|
||||
}
|
||||
|
||||
fn build_backup_job(world: &str) -> Result<Job> {
|
||||
let template = BackupJobTemplate { world };
|
||||
let yaml = template.render()?;
|
||||
Ok(serde_yaml::from_str(&yaml)?)
|
||||
}
|
||||
|
||||
async fn stream_pod_logs(pods: &Api<Pod>, pod_name: &str, reporter: &Reporter) -> Result<()> {
|
||||
let params = LogParams {
|
||||
follow: true,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let stream = pods.log_stream(pod_name, ¶ms).await?;
|
||||
let mut lines = stream.lines();
|
||||
|
||||
while let Some(line) = lines.try_next().await? {
|
||||
reporter.log_event(&line);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
8
nix/homelab/cli/src/commands/mod.rs
Normal file
8
nix/homelab/cli/src/commands/mod.rs
Normal file
@@ -0,0 +1,8 @@
|
||||
use clap::Subcommand;
|
||||
mod minecraft;
|
||||
|
||||
#[derive(Subcommand, Debug)]
|
||||
pub enum Commands {
|
||||
/// minecraft management
|
||||
Minecraft(minecraft::MinecraftCommand),
|
||||
}
|
||||
20
nix/homelab/cli/src/error.rs
Normal file
20
nix/homelab/cli/src/error.rs
Normal file
@@ -0,0 +1,20 @@
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Error, Debug, thiserror_ext::Box)]
|
||||
#[thiserror_ext(newtype(name = Error))]
|
||||
pub enum ErrorKind {
|
||||
#[error("kube error: {0}")]
|
||||
Kube(#[from] kube::Error),
|
||||
#[error("watcher error: {0}")]
|
||||
Watcher(#[from] kube::runtime::watcher::Error),
|
||||
#[error("io error: {0}")]
|
||||
Io(#[from] std::io::Error),
|
||||
#[error("backup failed: {0}")]
|
||||
BackupFailed(String),
|
||||
#[error("template error: {0}")]
|
||||
Template(#[from] askama::Error),
|
||||
#[error("error deserializing yaml: {0}")]
|
||||
Yaml(#[from] serde_yaml::Error),
|
||||
}
|
||||
|
||||
pub type Result<T> = core::result::Result<T, Error>;
|
||||
57
nix/homelab/cli/src/main.rs
Normal file
57
nix/homelab/cli/src/main.rs
Normal file
@@ -0,0 +1,57 @@
|
||||
mod commands;
|
||||
mod error;
|
||||
mod reporter;
|
||||
|
||||
use std::{ops::Deref, sync::Arc};
|
||||
|
||||
use clap::{CommandFactory, Parser};
|
||||
|
||||
use crate::{commands::Commands, error::Result};
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(version, long_about = "homelab cli")]
|
||||
struct Cli {
|
||||
#[command(subcommand)]
|
||||
command: Option<Commands>,
|
||||
}
|
||||
|
||||
struct AppState {
|
||||
client: kube::Client,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct State(Arc<AppState>);
|
||||
|
||||
impl State {
|
||||
pub fn new(inner: AppState) -> Self {
|
||||
Self(Arc::new(inner))
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for State {
|
||||
type Target = AppState;
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl AppState {
|
||||
pub async fn new() -> Result<Self> {
|
||||
Ok(Self {
|
||||
client: kube::Client::try_default().await?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::main(flavor = "current_thread")]
|
||||
async fn main() -> anyhow::Result<()> {
|
||||
let cli = Cli::parse();
|
||||
let app_state = State::new(AppState::new().await?);
|
||||
|
||||
match cli.command {
|
||||
Some(Commands::Minecraft(cmd)) => cmd.run(app_state.clone()).await?,
|
||||
_ => Cli::command().print_long_help()?,
|
||||
};
|
||||
|
||||
Ok(())
|
||||
}
|
||||
52
nix/homelab/cli/src/reporter.rs
Normal file
52
nix/homelab/cli/src/reporter.rs
Normal file
@@ -0,0 +1,52 @@
|
||||
use std::fmt::Display;
|
||||
|
||||
use indicatif::{ProgressBar, ProgressStyle};
|
||||
|
||||
pub struct Reporter {
|
||||
spinner: ProgressBar,
|
||||
}
|
||||
|
||||
pub const TICK_CHARS: &str = "⣷⣯⣟⡿⢿⣻⣽⣾";
|
||||
|
||||
impl Reporter {
|
||||
pub fn new() -> Self {
|
||||
let spinner = ProgressBar::new_spinner();
|
||||
spinner.set_style(
|
||||
ProgressStyle::with_template(
|
||||
"{prefix:.dim}{msg:>8.214/yellow} {spinner} [{elapsed_precise}]",
|
||||
)
|
||||
.unwrap()
|
||||
.tick_chars(TICK_CHARS),
|
||||
);
|
||||
spinner.enable_steady_tick(std::time::Duration::from_millis(100));
|
||||
Self { spinner }
|
||||
}
|
||||
|
||||
pub fn status(&self, msg: impl Into<String>) {
|
||||
self.spinner.set_message(msg.into());
|
||||
}
|
||||
|
||||
pub fn log_event(&self, line: &str) {
|
||||
self.spinner.suspend(|| {
|
||||
println!(" │ {}", line);
|
||||
});
|
||||
}
|
||||
|
||||
pub fn log<T: Display>(&self, text: T) {
|
||||
self.spinner.suspend(|| println!("{}", text))
|
||||
}
|
||||
|
||||
pub fn success(&self, msg: &str) {
|
||||
self.spinner.finish_with_message(format!("✓ {}", msg));
|
||||
}
|
||||
|
||||
pub fn fail(&self, msg: &str) {
|
||||
self.spinner.finish_with_message(format!("✗ {}", msg));
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Reporter {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
40
nix/homelab/cli/templates/backup-job.yaml
Normal file
40
nix/homelab/cli/templates/backup-job.yaml
Normal file
@@ -0,0 +1,40 @@
|
||||
apiVersion: batch/v1
|
||||
kind: Job
|
||||
metadata:
|
||||
name: minecraft-{{ world }}-backup
|
||||
namespace: minecraft
|
||||
labels:
|
||||
app: minecraft-backup
|
||||
world: {{ world }}
|
||||
spec:
|
||||
ttlSecondsAfterFinished: 300
|
||||
backoffLimit: 0
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
job-name: minecraft-{{ world }}-backup
|
||||
spec:
|
||||
restartPolicy: Never
|
||||
containers:
|
||||
- name: backup
|
||||
image: busybox
|
||||
command:
|
||||
- "sh"
|
||||
- "-c"
|
||||
- |
|
||||
tar -czvf /backups/minecraft-{{ world }}-manual.tar.gz -C /data .
|
||||
volumeMounts:
|
||||
- name: data
|
||||
mountPath: /data
|
||||
readOnly: true
|
||||
- name: backups
|
||||
mountPath: /backups
|
||||
volumes:
|
||||
- name: data
|
||||
persistentVolumeClaim:
|
||||
claimName: minecraft-{{ world }}-datadir
|
||||
readOnly: true
|
||||
- name: backups
|
||||
nfs:
|
||||
server: 192.168.27.2
|
||||
path: /backup/minecraft
|
||||
1
nix/homelab/cli/templates/sync.yaml
Normal file
1
nix/homelab/cli/templates/sync.yaml
Normal file
@@ -0,0 +1 @@
|
||||
apiVersion: batch/v1
|
||||
130
nix/homelab/config.toml
Normal file
130
nix/homelab/config.toml
Normal file
@@ -0,0 +1,130 @@
|
||||
routes = [
|
||||
{
|
||||
name = "gitea",
|
||||
hostname = "git",
|
||||
namespace = "git",
|
||||
service = "gitea-http",
|
||||
port = 3000,
|
||||
private = false
|
||||
},
|
||||
{
|
||||
kind = "TCP",
|
||||
name = "gitea-ssh",
|
||||
namespace = "git",
|
||||
entrypoint = "ssh",
|
||||
service = "gitea-ssh",
|
||||
port = 22,
|
||||
private = false
|
||||
},
|
||||
{
|
||||
name = "home-assistant",
|
||||
namespace = "home",
|
||||
port = 8080,
|
||||
private = true
|
||||
},
|
||||
{
|
||||
name = "sonarr",
|
||||
namespace = "media",
|
||||
port = 8989,
|
||||
private = true
|
||||
},
|
||||
{
|
||||
name = "longhorn",
|
||||
hostname = "storage",
|
||||
namespace = "longhorn-system",
|
||||
service = "longhorn-frontend",
|
||||
port = 80,
|
||||
private = true
|
||||
},
|
||||
{
|
||||
name = "pihole",
|
||||
namespace = "pihole-system",
|
||||
service = "pihole-web",
|
||||
port = 80,
|
||||
private = true
|
||||
},
|
||||
{
|
||||
kind = "TCP",
|
||||
name = "minecraft-router",
|
||||
hostname = "minecraft",
|
||||
namespace = "minecraft",
|
||||
entrypoint = "minecraft",
|
||||
service = "minecraft-router-mc-router",
|
||||
port = 25565,
|
||||
private = false
|
||||
},
|
||||
{
|
||||
kind = "TCP",
|
||||
name = "minecraft-rcon",
|
||||
namespace = "minecraft",
|
||||
entrypoint = "rcon",
|
||||
service = "minecraft-main-rcon",
|
||||
port = 25575,
|
||||
private = true
|
||||
},
|
||||
{
|
||||
name = "prowlarr",
|
||||
namespace = "media",
|
||||
port = 9696,
|
||||
private = true
|
||||
},
|
||||
{
|
||||
name = "radarr",
|
||||
namespace = "media",
|
||||
port = 7878,
|
||||
private = true
|
||||
},
|
||||
{
|
||||
name = "qbittorrent",
|
||||
hostname = "qbit",
|
||||
namespace = "media",
|
||||
port = 8080,
|
||||
private = true
|
||||
},
|
||||
{
|
||||
name = "flaresolverr",
|
||||
hostname = "flare",
|
||||
namespace = "media",
|
||||
port = 8191,
|
||||
private = true
|
||||
},
|
||||
{
|
||||
name = "jellyfin",
|
||||
hostname = "media",
|
||||
namespace = "media",
|
||||
port = 8096,
|
||||
private = true
|
||||
},
|
||||
{
|
||||
name = "grafana",
|
||||
namespace = "monitoring",
|
||||
service = "prometheus-stack-grafana",
|
||||
port = 80,
|
||||
private = true
|
||||
},
|
||||
{
|
||||
name = "mesh",
|
||||
namespace = "networking",
|
||||
service = "headscale",
|
||||
port = 8080,
|
||||
private = false
|
||||
}
|
||||
]
|
||||
|
||||
[pihole]
|
||||
url = "https://pihole.lucalise.ca"
|
||||
password_file = "/run/secrets/pihole_password"
|
||||
extra_hosts = [
|
||||
"192.168.27.12 mc-rocket.privatedns.org",
|
||||
"192.168.27.12 mc-rocket-creative.privatedns.org",
|
||||
"192.168.27.12 mc-rocket-creative.duckdns.org",
|
||||
"192.168.27.12 git.lucalise.ca",
|
||||
"192.168.27.2 rufus",
|
||||
"192.168.27.11 kube"
|
||||
]
|
||||
|
||||
[router]
|
||||
host = "192.168.15.1:22"
|
||||
user = "luca"
|
||||
key_path = "/home/luca/.ssh/id_ed25519"
|
||||
lease_file = "/var/dhcpd/var/db/dhcpd.leases"
|
||||
@@ -20,6 +20,16 @@
|
||||
architecture = "x86_64-linux";
|
||||
}
|
||||
];
|
||||
systems = [ "x86_64-linux" ];
|
||||
forAllSystems =
|
||||
f:
|
||||
nixpkgs.lib.genAttrs systems (
|
||||
system:
|
||||
f {
|
||||
inherit system;
|
||||
pkgs = nixpkgs.legacyPackages.${system};
|
||||
}
|
||||
);
|
||||
in
|
||||
{
|
||||
nixosConfigurations = builtins.listToAttrs (
|
||||
@@ -49,5 +59,16 @@
|
||||
};
|
||||
}) nodes
|
||||
);
|
||||
devShells = forAllSystems (
|
||||
{ system, pkgs }:
|
||||
{
|
||||
default = pkgs.mkShell {
|
||||
buildInputs = with pkgs; [
|
||||
openssl
|
||||
pkgconf
|
||||
];
|
||||
};
|
||||
}
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -30,6 +30,13 @@ releases:
|
||||
- crds:
|
||||
enabled: true
|
||||
|
||||
- name: prometheus-stack
|
||||
chart: oci://ghcr.io/prometheus-community/charts/kube-prometheus-stack
|
||||
namespace: monitoring
|
||||
version: 80.9.2
|
||||
values:
|
||||
- values/prometheus.yaml
|
||||
|
||||
# Git
|
||||
- name: gitea
|
||||
namespace: git
|
||||
@@ -38,6 +45,13 @@ releases:
|
||||
values:
|
||||
- values/gitea.yaml
|
||||
|
||||
- name: gitea-runners
|
||||
namespace: git
|
||||
chart: gitea-charts/actions
|
||||
version: 0.0.2
|
||||
values:
|
||||
- values/gitea-runners.yaml
|
||||
|
||||
# Storage
|
||||
- name: longhorn
|
||||
namespace: longhorn-system
|
||||
@@ -46,6 +60,8 @@ releases:
|
||||
values:
|
||||
- defaultSettings:
|
||||
defaultReplicaCount: 1
|
||||
- defaultBackupStore:
|
||||
backupTarget: nfs://192.168.27.2:/backup/longhorn
|
||||
- persistence:
|
||||
defaultClassReplicaCount: 1
|
||||
|
||||
@@ -56,6 +72,14 @@ releases:
|
||||
values:
|
||||
- values/pihole.yaml
|
||||
|
||||
# Media
|
||||
- name: jellyfin
|
||||
namespace: media
|
||||
chart: jellyfin/jellyfin
|
||||
version: 2.7.0
|
||||
values:
|
||||
- values/media/jellyfin.yaml
|
||||
|
||||
# Minecraft
|
||||
- name: minecraft-router
|
||||
namespace: minecraft
|
||||
@@ -78,6 +102,13 @@ releases:
|
||||
values:
|
||||
- values/minecraft/creative.yaml
|
||||
|
||||
- name: minecraft-old
|
||||
namespace: minecraft
|
||||
chart: minecraft-charts/minecraft
|
||||
version: 5.0.0
|
||||
values:
|
||||
- values/minecraft/old.yaml
|
||||
|
||||
- name: home-assistant
|
||||
namespace: home
|
||||
chart: home-assistant/home-assistant
|
||||
|
||||
5
nix/homelab/helm/values/gitea-runners.yaml
Normal file
5
nix/homelab/helm/values/gitea-runners.yaml
Normal file
@@ -0,0 +1,5 @@
|
||||
enabled: true
|
||||
statefulset:
|
||||
nodeSelector:
|
||||
kubernetes.io/hostname: rufus
|
||||
giteaRootURL: https://git.lucalise.ca
|
||||
@@ -13,10 +13,13 @@ gitea:
|
||||
config:
|
||||
repository:
|
||||
ROOT: /mnt/git-data/git/repositories
|
||||
server:
|
||||
ROOT_URL: https://git.lucalise.ca
|
||||
SSH_DOMAIN: git.lucalise.ca
|
||||
database:
|
||||
DB_TYPE: sqlite3
|
||||
session:
|
||||
PROVIDER: memory
|
||||
PROVIDER: db
|
||||
cache:
|
||||
ADAPTER: memory
|
||||
queue:
|
||||
|
||||
21
nix/homelab/helm/values/media/jellyfin.yaml
Normal file
21
nix/homelab/helm/values/media/jellyfin.yaml
Normal file
@@ -0,0 +1,21 @@
|
||||
persistence:
|
||||
media:
|
||||
enabled: false
|
||||
|
||||
resources:
|
||||
requests:
|
||||
cpu: 200m
|
||||
memory: 512Mi
|
||||
limits:
|
||||
cpu: 1
|
||||
memory: 2Gi
|
||||
|
||||
volumeMounts:
|
||||
- name: data
|
||||
mountPath: /mnt/data
|
||||
|
||||
volumes:
|
||||
- name: data
|
||||
nfs:
|
||||
server: 192.168.27.2
|
||||
path: /data
|
||||
@@ -3,23 +3,37 @@ resources:
|
||||
cpu: 1
|
||||
memory: 500Mi
|
||||
limits:
|
||||
memory: 4Gi
|
||||
memory: 5Gi
|
||||
cpu: 2000m
|
||||
|
||||
minecraftServer:
|
||||
eula: "TRUE"
|
||||
type: "PAPER"
|
||||
type: "FABRIC"
|
||||
version: "1.21.11"
|
||||
difficulty: hard
|
||||
motd: "A Minecraft Server."
|
||||
gameMode: creative
|
||||
memory: 4G
|
||||
memory: 5G
|
||||
rcon:
|
||||
enabled: true
|
||||
withGeneratedPassword: false
|
||||
port: 25575
|
||||
existingSecret: rcon-credentials
|
||||
secretKey: rcon-password
|
||||
modrinth:
|
||||
projects:
|
||||
- fabric-api
|
||||
- tree-vein-miner
|
||||
- lithium
|
||||
- servux
|
||||
- ferrite-core
|
||||
- carpet
|
||||
- elytra-trims
|
||||
- fabric-language-kotlin
|
||||
- c2me-fabric
|
||||
- scalablelux
|
||||
- axiom
|
||||
|
||||
|
||||
nodeSelector:
|
||||
kubernetes.io/hostname: kube
|
||||
|
||||
@@ -3,22 +3,35 @@ resources:
|
||||
cpu: 1
|
||||
memory: 500Mi
|
||||
limits:
|
||||
memory: 6Gi
|
||||
memory: 7Gi
|
||||
cpu: 2500m
|
||||
|
||||
minecraftServer:
|
||||
eula: "TRUE"
|
||||
type: "PAPER"
|
||||
type: "FABRIC"
|
||||
version: "1.21.11"
|
||||
difficulty: hard
|
||||
motd: "A Minecraft Server."
|
||||
memory: 6G
|
||||
memory: 7G
|
||||
rcon:
|
||||
enabled: true
|
||||
withGeneratedPassword: false
|
||||
port: 25575
|
||||
existingSecret: rcon-credentials
|
||||
secretKey: rcon-password
|
||||
modrinth:
|
||||
projects:
|
||||
- fabric-api
|
||||
- tree-vein-miner
|
||||
- lithium
|
||||
- servux
|
||||
- ferrite-core
|
||||
- carpet
|
||||
- elytra-trims
|
||||
- fabric-language-kotlin
|
||||
- c2me-fabric
|
||||
- scalablelux
|
||||
- no-chat-reports
|
||||
|
||||
nodeSelector:
|
||||
kubernetes.io/hostname: kube
|
||||
|
||||
29
nix/homelab/helm/values/minecraft/old.yaml
Normal file
29
nix/homelab/helm/values/minecraft/old.yaml
Normal file
@@ -0,0 +1,29 @@
|
||||
resources:
|
||||
requests:
|
||||
cpu: 500m
|
||||
memory: 512Mi
|
||||
limits:
|
||||
memory: 2Gi
|
||||
cpu: 1
|
||||
|
||||
minecraftServer:
|
||||
eula: "TRUE"
|
||||
type: "VANILLA"
|
||||
version: "1.7.10"
|
||||
difficulty: hard
|
||||
motd: "A Minecraft Server."
|
||||
memory: 4G
|
||||
rcon:
|
||||
enabled: true
|
||||
withGeneratedPassword: false
|
||||
port: 25575
|
||||
existingSecret: rcon-credentials
|
||||
secretKey: rcon-password
|
||||
|
||||
nodeSelector:
|
||||
kubernetes.io/hostname: rufus
|
||||
|
||||
persistence:
|
||||
dataDir:
|
||||
enabled: true
|
||||
Size: 2Gi
|
||||
@@ -8,9 +8,12 @@ resources:
|
||||
|
||||
minecraftRouter:
|
||||
mappings:
|
||||
- externalHostname: "mc-rocket.duckdns.org"
|
||||
- externalHostname: "mc-rocket.privatedns.org"
|
||||
host: "minecraft-main"
|
||||
port: 25565
|
||||
- externalHostname: "mc-rocket-creative.duckdns.org"
|
||||
- externalHostname: "mc-rocket-creative.privatedns.org"
|
||||
host: "minecraft-creative"
|
||||
port: 25565
|
||||
- externalHostname: "mc-rocket-old.privatedns.org"
|
||||
host: "minecraft-old"
|
||||
port: 25565
|
||||
|
||||
@@ -8,6 +8,12 @@ serviceWeb:
|
||||
https:
|
||||
enabled: false
|
||||
|
||||
serviceDns:
|
||||
type: LoadBalancer
|
||||
mixedService: true
|
||||
annotations:
|
||||
metallb.universe.tf/loadBalancerIPs: "192.168.27.13"
|
||||
|
||||
resources:
|
||||
requests:
|
||||
cpu: 100m
|
||||
|
||||
58
nix/homelab/helm/values/prometheus.yaml
Normal file
58
nix/homelab/helm/values/prometheus.yaml
Normal file
@@ -0,0 +1,58 @@
|
||||
# Install Prometheus Operator CRDs
|
||||
crds:
|
||||
enabled: true
|
||||
|
||||
alertmanager:
|
||||
enabled: false
|
||||
|
||||
prometheus:
|
||||
prometheusSpec:
|
||||
storageSpec:
|
||||
volumeClaimTemplate:
|
||||
spec:
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
resources:
|
||||
requests:
|
||||
storage: 20Gi
|
||||
|
||||
retention: 15d
|
||||
retentionSize: "18GB"
|
||||
|
||||
resources:
|
||||
requests:
|
||||
cpu: 100m
|
||||
memory: 512Mi
|
||||
limits:
|
||||
cpu: 500m
|
||||
memory: 2Gi
|
||||
|
||||
grafana:
|
||||
enabled: true
|
||||
|
||||
persistence:
|
||||
enabled: true
|
||||
size: 5Gi
|
||||
|
||||
resources:
|
||||
requests:
|
||||
cpu: 50m
|
||||
memory: 128Mi
|
||||
limits:
|
||||
cpu: 300m
|
||||
memory: 512Mi
|
||||
|
||||
nodeExporter:
|
||||
enabled: true
|
||||
|
||||
kubeStateMetrics:
|
||||
enabled: true
|
||||
|
||||
kubeEtcd:
|
||||
enabled: false
|
||||
kubeControllerManager:
|
||||
enabled: false
|
||||
kubeScheduler:
|
||||
enabled: false
|
||||
kubeProxy:
|
||||
enabled: false
|
||||
33
nix/homelab/kustomize/backup-job-manual.yaml
Normal file
33
nix/homelab/kustomize/backup-job-manual.yaml
Normal file
@@ -0,0 +1,33 @@
|
||||
apiVersion: batch/v1
|
||||
kind: Job
|
||||
metadata:
|
||||
name: minecraft-backup-manual
|
||||
namespace: minecraft
|
||||
spec:
|
||||
template:
|
||||
spec:
|
||||
restartPolicy: Never
|
||||
containers:
|
||||
- name: backup
|
||||
image: busybox
|
||||
command:
|
||||
- sh
|
||||
- -c
|
||||
- |
|
||||
set -e
|
||||
BACKUP_FILE=/backups/manual/minecraft-main-manual.tar.gz
|
||||
echo "creating backup: ${BACKUP_FILE}"
|
||||
tar -czf "${BACKUP_FILE}" -C /data .
|
||||
volumeMounts:
|
||||
- name: data
|
||||
mountPath: /data
|
||||
- name: backups
|
||||
mountPath: /backups
|
||||
volumes:
|
||||
- name: data
|
||||
persistentVolumeClaim:
|
||||
claimName: minecraft-main-datadir
|
||||
- name: backups
|
||||
nfs:
|
||||
server: 192.168.27.2
|
||||
path: /backup/minecraft
|
||||
18
nix/homelab/kustomize/headscale-migrate.yaml
Normal file
18
nix/homelab/kustomize/headscale-migrate.yaml
Normal file
@@ -0,0 +1,18 @@
|
||||
apiVersion: v1
|
||||
kind: Pod
|
||||
metadata:
|
||||
name: headscale-migrate
|
||||
namespace: networking
|
||||
spec:
|
||||
restartPolicy: Never
|
||||
containers:
|
||||
- name: migrate
|
||||
image: nouchka/sqlite3
|
||||
command: ["sleep", "infinity"]
|
||||
volumeMounts:
|
||||
- name: data
|
||||
mountPath: /var/lib/headscale
|
||||
volumes:
|
||||
- name: data
|
||||
persistentVolumeClaim:
|
||||
claimName: headscale-data
|
||||
@@ -5,13 +5,16 @@ resources:
|
||||
- ./metallb/pool.yaml
|
||||
- ./traefik/config.yaml
|
||||
- ./traefik/private-networks.yaml
|
||||
- ./traefik/private-networks-tcp.yaml
|
||||
- ./traefik/chains.yaml
|
||||
- ./cert-manager/config.yaml
|
||||
- ./routes/minecraft.yaml
|
||||
- ./routes/gitea/ssh.yaml
|
||||
- ./routes/gitea/http.yaml
|
||||
- ./routes/longhorn.yaml
|
||||
- ./routes/home-assistant.yaml
|
||||
- ./routes/consul-media.yaml
|
||||
- ./routes/consul-vaultwarden.yaml
|
||||
- ./routes/pihole.yaml
|
||||
- ./routes.yaml
|
||||
|
||||
- ./media/sonarr.yaml
|
||||
- ./media/prowlarr.yaml
|
||||
- ./media/radarr.yaml
|
||||
- ./media/qbittorrent.yaml
|
||||
- ./media/flaresolverr.yaml
|
||||
|
||||
- ./networking/headscale/config.yaml
|
||||
- ./networking/headscale/headscale.yaml
|
||||
|
||||
76
nix/homelab/kustomize/media/flaresolverr.yaml
Normal file
76
nix/homelab/kustomize/media/flaresolverr.yaml
Normal file
@@ -0,0 +1,76 @@
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: flaresolverr-config
|
||||
namespace: media
|
||||
spec:
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
resources:
|
||||
requests:
|
||||
storage: 1Gi
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: flaresolverr
|
||||
namespace: media
|
||||
labels:
|
||||
app: flaresolverr
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: flaresolverr
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: flaresolverr
|
||||
spec:
|
||||
containers:
|
||||
- name: flaresolverr
|
||||
image: ghcr.io/flaresolverr/flaresolverr
|
||||
ports:
|
||||
- containerPort: 8191
|
||||
name: http
|
||||
env:
|
||||
- name: TZ
|
||||
value: America/Vancouver
|
||||
volumeMounts:
|
||||
- name: config
|
||||
mountPath: /config
|
||||
resources:
|
||||
requests:
|
||||
cpu: 100m
|
||||
memory: 256Mi
|
||||
limits:
|
||||
cpu: 1
|
||||
memory: 4Gi
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /health
|
||||
port: 8191
|
||||
initialDelaySeconds: 30
|
||||
periodSeconds: 30
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /health
|
||||
port: 8191
|
||||
initialDelaySeconds: 5
|
||||
volumes:
|
||||
- name: config
|
||||
persistentVolumeClaim:
|
||||
claimName: flaresolverr-config
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: flaresolverr
|
||||
namespace: media
|
||||
spec:
|
||||
selector:
|
||||
app: flaresolverr
|
||||
ports:
|
||||
- port: 8191
|
||||
targetPort: 8191
|
||||
name: http
|
||||
84
nix/homelab/kustomize/media/prowlarr.yaml
Normal file
84
nix/homelab/kustomize/media/prowlarr.yaml
Normal file
@@ -0,0 +1,84 @@
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: prowlarr-config
|
||||
namespace: media
|
||||
spec:
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
resources:
|
||||
requests:
|
||||
storage: 1Gi
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: prowlarr
|
||||
namespace: media
|
||||
labels:
|
||||
app: prowlarr
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: prowlarr
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: prowlarr
|
||||
spec:
|
||||
containers:
|
||||
- name: prowlarr
|
||||
image: lscr.io/linuxserver/prowlarr
|
||||
ports:
|
||||
- containerPort: 9696
|
||||
name: http
|
||||
env:
|
||||
- name: PUID
|
||||
value: "1000"
|
||||
- name: PGID
|
||||
value: "1000"
|
||||
- name: TZ
|
||||
value: "America/Vancouver"
|
||||
volumeMounts:
|
||||
- name: config
|
||||
mountPath: /config
|
||||
resources:
|
||||
requests:
|
||||
cpu: 100m
|
||||
memory: 256Mi
|
||||
limits:
|
||||
cpu: 1
|
||||
memory: 1Gi
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /ping
|
||||
port: 9696
|
||||
initialDelaySeconds: 30
|
||||
periodSeconds: 30
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /ping
|
||||
port: 9696
|
||||
initialDelaySeconds: 10
|
||||
periodSeconds: 10
|
||||
volumes:
|
||||
- name: config
|
||||
persistentVolumeClaim:
|
||||
claimName: prowlarr-config
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: prowlarr
|
||||
namespace: media
|
||||
labels:
|
||||
app: prowlarr
|
||||
spec:
|
||||
selector:
|
||||
app: prowlarr
|
||||
ports:
|
||||
- port: 9696
|
||||
targetPort: 9696
|
||||
protocol: TCP
|
||||
name: http
|
||||
141
nix/homelab/kustomize/media/qbittorrent.yaml
Normal file
141
nix/homelab/kustomize/media/qbittorrent.yaml
Normal file
@@ -0,0 +1,141 @@
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: qbittorrent-config
|
||||
namespace: media
|
||||
spec:
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
resources:
|
||||
requests:
|
||||
storage: 1Gi
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: qbittorrent
|
||||
namespace: media
|
||||
labels:
|
||||
app: qbittorrent
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: qbittorrent
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: qbittorrent
|
||||
annotations:
|
||||
kubectl.kubernetes.io/default-container: qbittorrent
|
||||
spec:
|
||||
containers:
|
||||
- name: gluetun
|
||||
image: qmcgaw/gluetun
|
||||
securityContext:
|
||||
capabilities:
|
||||
add:
|
||||
- NET_ADMIN
|
||||
env:
|
||||
- name: VPN_SERVICE_PROVIDER
|
||||
value: "custom"
|
||||
- name: VPN_TYPE
|
||||
value: "wireguard"
|
||||
- name: TZ
|
||||
value: "America/Vancouver"
|
||||
- name: FIREWALL
|
||||
value: "on"
|
||||
- name: FIREWALL_OUTBOUND_SUBNETS
|
||||
value: "10.0.0.0/8,172.16.0.0/12,192.168.0.0/16"
|
||||
- name: DNS_KEEP_NAMESERVER
|
||||
value: "on"
|
||||
- name: LOG_LEVEL
|
||||
value: "info"
|
||||
volumeMounts:
|
||||
- name: wireguard-config
|
||||
mountPath: /gluetun/wireguard/wg0.conf
|
||||
subPath: wg0.conf
|
||||
readOnly: true
|
||||
- name: tun
|
||||
mountPath: /dev/net/tun
|
||||
resources:
|
||||
requests:
|
||||
cpu: 50m
|
||||
memory: 128Mi
|
||||
limits:
|
||||
cpu: 500m
|
||||
memory: 512Mi
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /v1/vpn/status
|
||||
port: 8000
|
||||
initialDelaySeconds: 30
|
||||
periodSeconds: 60
|
||||
timeoutSeconds: 10
|
||||
failureThreshold: 3
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /v1/vpn/status
|
||||
port: 8000
|
||||
initialDelaySeconds: 10
|
||||
periodSeconds: 60
|
||||
timeoutSeconds: 5
|
||||
|
||||
- name: qbittorrent
|
||||
image: lscr.io/linuxserver/qbittorrent
|
||||
ports:
|
||||
- containerPort: 8080
|
||||
name: http
|
||||
env:
|
||||
- name: PUID
|
||||
value: "1000"
|
||||
- name: PGID
|
||||
value: "1000"
|
||||
- name: TZ
|
||||
value: "America/Vancouver"
|
||||
- name: WEBUI_PORT
|
||||
value: "8080"
|
||||
volumeMounts:
|
||||
- name: config
|
||||
mountPath: /config
|
||||
- name: data
|
||||
mountPath: /mnt/data
|
||||
resources:
|
||||
requests:
|
||||
cpu: 100m
|
||||
memory: 256Mi
|
||||
limits:
|
||||
cpu: 2
|
||||
memory: 2Gi
|
||||
|
||||
volumes:
|
||||
- name: config
|
||||
persistentVolumeClaim:
|
||||
claimName: qbittorrent-config
|
||||
- name: data
|
||||
nfs:
|
||||
server: 192.168.27.2
|
||||
path: /data
|
||||
- name: wireguard-config
|
||||
secret:
|
||||
secretName: wireguard-config
|
||||
- name: tun
|
||||
hostPath:
|
||||
path: /dev/net/tun
|
||||
type: CharDevice
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: qbittorrent
|
||||
namespace: media
|
||||
labels:
|
||||
app: qbittorrent
|
||||
spec:
|
||||
selector:
|
||||
app: qbittorrent
|
||||
ports:
|
||||
- port: 8080
|
||||
targetPort: 8080
|
||||
protocol: TCP
|
||||
name: http
|
||||
90
nix/homelab/kustomize/media/radarr.yaml
Normal file
90
nix/homelab/kustomize/media/radarr.yaml
Normal file
@@ -0,0 +1,90 @@
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: radarr-config
|
||||
namespace: media
|
||||
spec:
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
resources:
|
||||
requests:
|
||||
storage: 1Gi
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: radarr
|
||||
namespace: media
|
||||
labels:
|
||||
app: radarr
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: radarr
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: radarr
|
||||
spec:
|
||||
containers:
|
||||
- name: radarr
|
||||
image: lscr.io/linuxserver/radarr
|
||||
ports:
|
||||
- containerPort: 7878
|
||||
name: http
|
||||
env:
|
||||
- name: PUID
|
||||
value: "1000"
|
||||
- name: PGID
|
||||
value: "1000"
|
||||
- name: TZ
|
||||
value: "America/Vancouver"
|
||||
volumeMounts:
|
||||
- name: config
|
||||
mountPath: /config
|
||||
- name: data
|
||||
mountPath: /mnt/data
|
||||
resources:
|
||||
requests:
|
||||
cpu: 100m
|
||||
memory: 256Mi
|
||||
limits:
|
||||
cpu: 1
|
||||
memory: 1Gi
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /ping
|
||||
port: 7878
|
||||
initialDelaySeconds: 30
|
||||
periodSeconds: 30
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /ping
|
||||
port: 7878
|
||||
initialDelaySeconds: 10
|
||||
periodSeconds: 10
|
||||
volumes:
|
||||
- name: config
|
||||
persistentVolumeClaim:
|
||||
claimName: radarr-config
|
||||
- name: data
|
||||
nfs:
|
||||
server: 192.168.27.2
|
||||
path: /data
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: radarr
|
||||
namespace: media
|
||||
labels:
|
||||
app: radarr
|
||||
spec:
|
||||
selector:
|
||||
app: radarr
|
||||
ports:
|
||||
- port: 7878
|
||||
targetPort: 7878
|
||||
protocol: TCP
|
||||
name: http
|
||||
98
nix/homelab/kustomize/media/sonarr.yaml
Normal file
98
nix/homelab/kustomize/media/sonarr.yaml
Normal file
@@ -0,0 +1,98 @@
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: media
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: sonarr-config
|
||||
namespace: media
|
||||
spec:
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
resources:
|
||||
requests:
|
||||
storage: 1Gi
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: sonarr
|
||||
namespace: media
|
||||
labels:
|
||||
app: sonarr
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: sonarr
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: sonarr
|
||||
spec:
|
||||
containers:
|
||||
- name: sonarr
|
||||
image: lscr.io/linuxserver/sonarr:4.0.16
|
||||
securityContext:
|
||||
runAsUser: 0
|
||||
runAsGroup: 0
|
||||
ports:
|
||||
- containerPort: 8989
|
||||
name: http
|
||||
env:
|
||||
- name: PUID
|
||||
value: "0"
|
||||
- name: PGID
|
||||
value: "0"
|
||||
- name: TZ
|
||||
value: "America/Vancouver"
|
||||
volumeMounts:
|
||||
- name: config
|
||||
mountPath: /config
|
||||
- name: data
|
||||
mountPath: /mnt/data
|
||||
resources:
|
||||
requests:
|
||||
cpu: 100m
|
||||
memory: 256Mi
|
||||
limits:
|
||||
cpu: 1
|
||||
memory: 1Gi
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /ping
|
||||
port: 8989
|
||||
initialDelaySeconds: 30
|
||||
periodSeconds: 30
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /ping
|
||||
port: 8989
|
||||
initialDelaySeconds: 10
|
||||
periodSeconds: 10
|
||||
volumes:
|
||||
- name: config
|
||||
persistentVolumeClaim:
|
||||
claimName: sonarr-config
|
||||
- name: data
|
||||
nfs:
|
||||
server: 192.168.27.2
|
||||
path: /data
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: sonarr
|
||||
namespace: media
|
||||
labels:
|
||||
app: sonarr
|
||||
spec:
|
||||
selector:
|
||||
app: sonarr
|
||||
ports:
|
||||
- port: 8989
|
||||
targetPort: 8989
|
||||
protocol: TCP
|
||||
name: http
|
||||
@@ -5,17 +5,7 @@ metadata:
|
||||
namespace: metallb-system
|
||||
spec:
|
||||
addresses:
|
||||
- 192.168.18.31-192.168.18.61
|
||||
---
|
||||
apiVersion: metallb.io/v1beta1
|
||||
kind: IPAddressPool
|
||||
metadata:
|
||||
name: rufus-pool
|
||||
namespace: metallb-system
|
||||
spec:
|
||||
addresses:
|
||||
- 192.168.27.10-192.168.27.30
|
||||
autoAssign: false
|
||||
- 192.168.27.12-192.168.27.40
|
||||
---
|
||||
apiVersion: metallb.io/v1beta1
|
||||
kind: L2Advertisement
|
||||
@@ -25,15 +15,3 @@ metadata:
|
||||
spec:
|
||||
ipAddressPools:
|
||||
- pool
|
||||
---
|
||||
apiVersion: metallb.io/v1beta1
|
||||
kind: L2Advertisement
|
||||
metadata:
|
||||
name: rufus-advertisement
|
||||
namespace: metallb-system
|
||||
spec:
|
||||
ipAddressPools:
|
||||
- rufus-pool
|
||||
nodeSelectors:
|
||||
- matchLabels:
|
||||
kubernetes.io/hostname: rufus
|
||||
|
||||
50
nix/homelab/kustomize/networking/headscale/config.yaml
Normal file
50
nix/homelab/kustomize/networking/headscale/config.yaml
Normal file
@@ -0,0 +1,50 @@
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: headscale-config
|
||||
namespace: networking
|
||||
data:
|
||||
acl.json: |
|
||||
{
|
||||
"tagOwners": {
|
||||
"tag:personal": ["lucalise@"],
|
||||
},
|
||||
"acls": [
|
||||
{"action": "accept", "src": ["tag:personal"], "dst": ["tag:personal:*"]},
|
||||
{"action": "accept", "src": ["tag:personal"], "dst": ["autogroup:internet:*"]},
|
||||
{"action": "accept", "src": ["tag:personal"], "dst": ["192.168.15.0/27:*", "192.168.27.0/24:*", "192.168.20.0/26:*"]}
|
||||
]
|
||||
}
|
||||
config.yaml: |
|
||||
server_url: https://mesh.lucalise.ca
|
||||
listen_addr: 0.0.0.0:8080
|
||||
metrics_listen_addr: 0.0.0.0:9090
|
||||
|
||||
noise:
|
||||
private_key_path: /var/lib/headscale/noise_private.key
|
||||
|
||||
prefixes:
|
||||
v4: 10.100.0.0/24
|
||||
v6: fd7a:115c:a1e0::/48
|
||||
|
||||
database:
|
||||
type: sqlite3
|
||||
sqlite:
|
||||
path: /var/lib/headscale/db.sqlite
|
||||
policy:
|
||||
path: /etc/headscale/acl.json
|
||||
|
||||
dns:
|
||||
override_local_dns: false
|
||||
base_domain: m.net
|
||||
|
||||
derp:
|
||||
server:
|
||||
enabled: false
|
||||
urls:
|
||||
- https://controlplane.tailscale.com/derpmap/default
|
||||
auto_update_enabled: true
|
||||
update_frequency: 24h
|
||||
|
||||
log:
|
||||
level: info
|
||||
88
nix/homelab/kustomize/networking/headscale/headscale.yaml
Normal file
88
nix/homelab/kustomize/networking/headscale/headscale.yaml
Normal file
@@ -0,0 +1,88 @@
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: headscale-data
|
||||
namespace: networking
|
||||
spec:
|
||||
accessModes:
|
||||
- ReadWriteMany
|
||||
resources:
|
||||
requests:
|
||||
storage: 2Gi
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: headscale
|
||||
namespace: networking
|
||||
labels:
|
||||
app: headscale
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: headscale
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: headscale
|
||||
spec:
|
||||
containers:
|
||||
- name: headscale
|
||||
image: docker.io/headscale/headscale
|
||||
command: ["headscale", "serve"]
|
||||
ports:
|
||||
- containerPort: 8080
|
||||
name: http
|
||||
- containerPort: 9090
|
||||
resources:
|
||||
requests:
|
||||
cpu: 100m
|
||||
memory: 256Mi
|
||||
limits:
|
||||
cpu: 512m
|
||||
memory: 1Gi
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /health
|
||||
port: http
|
||||
initialDelaySeconds: 10
|
||||
periodSeconds: 30
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /health
|
||||
port: http
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 10
|
||||
volumeMounts:
|
||||
- name: headscale-data
|
||||
mountPath: /var/lib/headscale
|
||||
- name: headscale-config
|
||||
mountPath: /etc/headscale/config.yaml
|
||||
subPath: config.yaml
|
||||
- name: headscale-config
|
||||
mountPath: /etc/headscale/acl.json
|
||||
subPath: acl.json
|
||||
volumes:
|
||||
- name: headscale-data
|
||||
persistentVolumeClaim:
|
||||
claimName: headscale-data
|
||||
- name: headscale-config
|
||||
configMap:
|
||||
name: headscale-config
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: headscale
|
||||
namespace: networking
|
||||
labels:
|
||||
app: headscale
|
||||
spec:
|
||||
selector:
|
||||
app: headscale
|
||||
ports:
|
||||
- port: 8080
|
||||
targetPort: http
|
||||
protocol: TCP
|
||||
name: http
|
||||
296
nix/homelab/kustomize/routes.yaml
Normal file
296
nix/homelab/kustomize/routes.yaml
Normal file
@@ -0,0 +1,296 @@
|
||||
apiVersion: gateway.networking.k8s.io/v1
|
||||
kind: HTTPRoute
|
||||
metadata:
|
||||
name: gitea
|
||||
namespace: git
|
||||
spec:
|
||||
parentRefs:
|
||||
- name: traefik-gateway
|
||||
namespace: kube-system
|
||||
hostnames:
|
||||
- git.lucalise.ca
|
||||
rules:
|
||||
- backendRefs:
|
||||
- name: gitea-http
|
||||
port: 3000
|
||||
---
|
||||
apiVersion: traefik.io/v1alpha1
|
||||
kind: IngressRouteTCP
|
||||
metadata:
|
||||
name: gitea-ssh
|
||||
namespace: git
|
||||
spec:
|
||||
entryPoints:
|
||||
- ssh
|
||||
routes:
|
||||
- match: HostSNI(`*`)
|
||||
services:
|
||||
- name: gitea-ssh
|
||||
port: 22
|
||||
---
|
||||
apiVersion: gateway.networking.k8s.io/v1
|
||||
kind: HTTPRoute
|
||||
metadata:
|
||||
name: home-assistant
|
||||
namespace: home
|
||||
spec:
|
||||
parentRefs:
|
||||
- name: traefik-gateway
|
||||
namespace: kube-system
|
||||
hostnames:
|
||||
- home-assistant.lucalise.ca
|
||||
rules:
|
||||
- backendRefs:
|
||||
- name: home-assistant
|
||||
port: 8080
|
||||
filters:
|
||||
- type: ExtensionRef
|
||||
extensionRef:
|
||||
group: traefik.io
|
||||
kind: Middleware
|
||||
name: private-networks
|
||||
---
|
||||
apiVersion: gateway.networking.k8s.io/v1
|
||||
kind: HTTPRoute
|
||||
metadata:
|
||||
name: sonarr
|
||||
namespace: media
|
||||
spec:
|
||||
parentRefs:
|
||||
- name: traefik-gateway
|
||||
namespace: kube-system
|
||||
hostnames:
|
||||
- sonarr.lucalise.ca
|
||||
rules:
|
||||
- backendRefs:
|
||||
- name: sonarr
|
||||
port: 8989
|
||||
filters:
|
||||
- type: ExtensionRef
|
||||
extensionRef:
|
||||
group: traefik.io
|
||||
kind: Middleware
|
||||
name: private-networks
|
||||
---
|
||||
apiVersion: gateway.networking.k8s.io/v1
|
||||
kind: HTTPRoute
|
||||
metadata:
|
||||
name: longhorn
|
||||
namespace: longhorn-system
|
||||
spec:
|
||||
parentRefs:
|
||||
- name: traefik-gateway
|
||||
namespace: kube-system
|
||||
hostnames:
|
||||
- storage.lucalise.ca
|
||||
rules:
|
||||
- backendRefs:
|
||||
- name: longhorn-frontend
|
||||
port: 80
|
||||
filters:
|
||||
- type: ExtensionRef
|
||||
extensionRef:
|
||||
group: traefik.io
|
||||
kind: Middleware
|
||||
name: private-networks
|
||||
---
|
||||
apiVersion: gateway.networking.k8s.io/v1
|
||||
kind: HTTPRoute
|
||||
metadata:
|
||||
name: pihole
|
||||
namespace: pihole-system
|
||||
spec:
|
||||
parentRefs:
|
||||
- name: traefik-gateway
|
||||
namespace: kube-system
|
||||
hostnames:
|
||||
- pihole.lucalise.ca
|
||||
rules:
|
||||
- backendRefs:
|
||||
- name: pihole-web
|
||||
port: 80
|
||||
filters:
|
||||
- type: ExtensionRef
|
||||
extensionRef:
|
||||
group: traefik.io
|
||||
kind: Middleware
|
||||
name: private-networks
|
||||
---
|
||||
apiVersion: traefik.io/v1alpha1
|
||||
kind: IngressRouteTCP
|
||||
metadata:
|
||||
name: minecraft-router
|
||||
namespace: minecraft
|
||||
spec:
|
||||
entryPoints:
|
||||
- minecraft
|
||||
routes:
|
||||
- match: HostSNI(`*`)
|
||||
services:
|
||||
- name: minecraft-router-mc-router
|
||||
port: 25565
|
||||
---
|
||||
apiVersion: traefik.io/v1alpha1
|
||||
kind: IngressRouteTCP
|
||||
metadata:
|
||||
name: minecraft-rcon
|
||||
namespace: minecraft
|
||||
spec:
|
||||
entryPoints:
|
||||
- rcon
|
||||
routes:
|
||||
- match: HostSNI(`*`)
|
||||
middlewares:
|
||||
- name: private-networks-tcp
|
||||
namespace: kube-system
|
||||
services:
|
||||
- name: minecraft-main-rcon
|
||||
port: 25575
|
||||
---
|
||||
apiVersion: gateway.networking.k8s.io/v1
|
||||
kind: HTTPRoute
|
||||
metadata:
|
||||
name: prowlarr
|
||||
namespace: media
|
||||
spec:
|
||||
parentRefs:
|
||||
- name: traefik-gateway
|
||||
namespace: kube-system
|
||||
hostnames:
|
||||
- prowlarr.lucalise.ca
|
||||
rules:
|
||||
- backendRefs:
|
||||
- name: prowlarr
|
||||
port: 9696
|
||||
filters:
|
||||
- type: ExtensionRef
|
||||
extensionRef:
|
||||
group: traefik.io
|
||||
kind: Middleware
|
||||
name: private-networks
|
||||
---
|
||||
apiVersion: gateway.networking.k8s.io/v1
|
||||
kind: HTTPRoute
|
||||
metadata:
|
||||
name: radarr
|
||||
namespace: media
|
||||
spec:
|
||||
parentRefs:
|
||||
- name: traefik-gateway
|
||||
namespace: kube-system
|
||||
hostnames:
|
||||
- radarr.lucalise.ca
|
||||
rules:
|
||||
- backendRefs:
|
||||
- name: radarr
|
||||
port: 7878
|
||||
filters:
|
||||
- type: ExtensionRef
|
||||
extensionRef:
|
||||
group: traefik.io
|
||||
kind: Middleware
|
||||
name: private-networks
|
||||
---
|
||||
apiVersion: gateway.networking.k8s.io/v1
|
||||
kind: HTTPRoute
|
||||
metadata:
|
||||
name: qbittorrent
|
||||
namespace: media
|
||||
spec:
|
||||
parentRefs:
|
||||
- name: traefik-gateway
|
||||
namespace: kube-system
|
||||
hostnames:
|
||||
- qbit.lucalise.ca
|
||||
rules:
|
||||
- backendRefs:
|
||||
- name: qbittorrent
|
||||
port: 8080
|
||||
filters:
|
||||
- type: ExtensionRef
|
||||
extensionRef:
|
||||
group: traefik.io
|
||||
kind: Middleware
|
||||
name: private-networks
|
||||
---
|
||||
apiVersion: gateway.networking.k8s.io/v1
|
||||
kind: HTTPRoute
|
||||
metadata:
|
||||
name: flaresolverr
|
||||
namespace: media
|
||||
spec:
|
||||
parentRefs:
|
||||
- name: traefik-gateway
|
||||
namespace: kube-system
|
||||
hostnames:
|
||||
- flare.lucalise.ca
|
||||
rules:
|
||||
- backendRefs:
|
||||
- name: flaresolverr
|
||||
port: 8191
|
||||
filters:
|
||||
- type: ExtensionRef
|
||||
extensionRef:
|
||||
group: traefik.io
|
||||
kind: Middleware
|
||||
name: private-networks
|
||||
---
|
||||
apiVersion: gateway.networking.k8s.io/v1
|
||||
kind: HTTPRoute
|
||||
metadata:
|
||||
name: jellyfin
|
||||
namespace: media
|
||||
spec:
|
||||
parentRefs:
|
||||
- name: traefik-gateway
|
||||
namespace: kube-system
|
||||
hostnames:
|
||||
- media.lucalise.ca
|
||||
rules:
|
||||
- backendRefs:
|
||||
- name: jellyfin
|
||||
port: 8096
|
||||
filters:
|
||||
- type: ExtensionRef
|
||||
extensionRef:
|
||||
group: traefik.io
|
||||
kind: Middleware
|
||||
name: private-networks
|
||||
---
|
||||
apiVersion: gateway.networking.k8s.io/v1
|
||||
kind: HTTPRoute
|
||||
metadata:
|
||||
name: grafana
|
||||
namespace: monitoring
|
||||
spec:
|
||||
parentRefs:
|
||||
- name: traefik-gateway
|
||||
namespace: kube-system
|
||||
hostnames:
|
||||
- grafana.lucalise.ca
|
||||
rules:
|
||||
- backendRefs:
|
||||
- name: prometheus-stack-grafana
|
||||
port: 80
|
||||
filters:
|
||||
- type: ExtensionRef
|
||||
extensionRef:
|
||||
group: traefik.io
|
||||
kind: Middleware
|
||||
name: private-networks
|
||||
---
|
||||
apiVersion: gateway.networking.k8s.io/v1
|
||||
kind: HTTPRoute
|
||||
metadata:
|
||||
name: mesh
|
||||
namespace: networking
|
||||
spec:
|
||||
parentRefs:
|
||||
- name: traefik-gateway
|
||||
namespace: kube-system
|
||||
hostnames:
|
||||
- mesh.lucalise.ca
|
||||
rules:
|
||||
- backendRefs:
|
||||
- name: headscale
|
||||
port: 8080
|
||||
@@ -1,272 +0,0 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: bazarr
|
||||
namespace: media
|
||||
spec:
|
||||
ports:
|
||||
- port: 6767
|
||||
targetPort: 6767
|
||||
protocol: TCP
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Endpoints
|
||||
metadata:
|
||||
name: bazarr
|
||||
namespace: media
|
||||
subsets:
|
||||
- addresses:
|
||||
- ip: 192.168.20.20
|
||||
ports:
|
||||
- port: 6767
|
||||
protocol: TCP
|
||||
---
|
||||
apiVersion: gateway.networking.k8s.io/v1
|
||||
kind: HTTPRoute
|
||||
metadata:
|
||||
name: bazarr
|
||||
namespace: media
|
||||
spec:
|
||||
parentRefs:
|
||||
- name: traefik-gateway
|
||||
namespace: kube-system
|
||||
hostnames:
|
||||
- "bazarr.lucalise.ca"
|
||||
rules:
|
||||
- backendRefs:
|
||||
- name: bazarr
|
||||
port: 6767
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: prowlarr
|
||||
namespace: media
|
||||
spec:
|
||||
ports:
|
||||
- port: 9696
|
||||
targetPort: 9696
|
||||
protocol: TCP
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Endpoints
|
||||
metadata:
|
||||
name: prowlarr
|
||||
namespace: media
|
||||
subsets:
|
||||
- addresses:
|
||||
- ip: 192.168.20.17
|
||||
ports:
|
||||
- port: 9696
|
||||
protocol: TCP
|
||||
---
|
||||
apiVersion: gateway.networking.k8s.io/v1
|
||||
kind: HTTPRoute
|
||||
metadata:
|
||||
name: prowlarr
|
||||
namespace: media
|
||||
spec:
|
||||
parentRefs:
|
||||
- name: traefik-gateway
|
||||
namespace: kube-system
|
||||
hostnames:
|
||||
- "prowlarr.lucalise.ca"
|
||||
rules:
|
||||
- backendRefs:
|
||||
- name: prowlarr
|
||||
port: 9696
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: radarr
|
||||
namespace: media
|
||||
spec:
|
||||
ports:
|
||||
- port: 7878
|
||||
targetPort: 7878
|
||||
protocol: TCP
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Endpoints
|
||||
metadata:
|
||||
name: radarr
|
||||
namespace: media
|
||||
subsets:
|
||||
- addresses:
|
||||
- ip: 192.168.20.15
|
||||
ports:
|
||||
- port: 7878
|
||||
protocol: TCP
|
||||
---
|
||||
apiVersion: gateway.networking.k8s.io/v1
|
||||
kind: HTTPRoute
|
||||
metadata:
|
||||
name: radarr
|
||||
namespace: media
|
||||
spec:
|
||||
parentRefs:
|
||||
- name: traefik-gateway
|
||||
namespace: kube-system
|
||||
hostnames:
|
||||
- "radarr.lucalise.ca"
|
||||
rules:
|
||||
- backendRefs:
|
||||
- name: radarr
|
||||
port: 7878
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: sonarr
|
||||
namespace: media
|
||||
spec:
|
||||
ports:
|
||||
- port: 8989
|
||||
targetPort: 8989
|
||||
protocol: TCP
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Endpoints
|
||||
metadata:
|
||||
name: sonarr
|
||||
namespace: media
|
||||
subsets:
|
||||
- addresses:
|
||||
- ip: 192.168.20.16
|
||||
ports:
|
||||
- port: 8989
|
||||
protocol: TCP
|
||||
---
|
||||
apiVersion: gateway.networking.k8s.io/v1
|
||||
kind: HTTPRoute
|
||||
metadata:
|
||||
name: sonarr
|
||||
namespace: media
|
||||
spec:
|
||||
parentRefs:
|
||||
- name: traefik-gateway
|
||||
namespace: kube-system
|
||||
hostnames:
|
||||
- "sonarr.lucalise.ca"
|
||||
rules:
|
||||
- backendRefs:
|
||||
- name: sonarr
|
||||
port: 8989
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: qbittorrent
|
||||
namespace: media
|
||||
spec:
|
||||
ports:
|
||||
- port: 8090
|
||||
targetPort: 8090
|
||||
protocol: TCP
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Endpoints
|
||||
metadata:
|
||||
name: qbittorrent
|
||||
namespace: media
|
||||
subsets:
|
||||
- addresses:
|
||||
- ip: 192.168.20.6
|
||||
ports:
|
||||
- port: 8090
|
||||
protocol: TCP
|
||||
---
|
||||
apiVersion: gateway.networking.k8s.io/v1
|
||||
kind: HTTPRoute
|
||||
metadata:
|
||||
name: qbittorrent
|
||||
namespace: media
|
||||
spec:
|
||||
parentRefs:
|
||||
- name: traefik-gateway
|
||||
namespace: kube-system
|
||||
hostnames:
|
||||
- "qbit.lucalise.ca"
|
||||
rules:
|
||||
- backendRefs:
|
||||
- name: qbittorrent
|
||||
port: 8090
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: flaresolverr
|
||||
namespace: media
|
||||
spec:
|
||||
ports:
|
||||
- port: 8191
|
||||
targetPort: 8191
|
||||
protocol: TCP
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Endpoints
|
||||
metadata:
|
||||
name: flaresolverr
|
||||
namespace: media
|
||||
subsets:
|
||||
- addresses:
|
||||
- ip: 192.168.20.4
|
||||
ports:
|
||||
- port: 8191
|
||||
protocol: TCP
|
||||
---
|
||||
apiVersion: gateway.networking.k8s.io/v1
|
||||
kind: HTTPRoute
|
||||
metadata:
|
||||
name: flaresolverr
|
||||
namespace: media
|
||||
spec:
|
||||
parentRefs:
|
||||
- name: traefik-gateway
|
||||
namespace: kube-system
|
||||
hostnames:
|
||||
- "flare.lucalise.ca"
|
||||
rules:
|
||||
- backendRefs:
|
||||
- name: flaresolverr
|
||||
port: 8191
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: jellyfin
|
||||
namespace: media
|
||||
spec:
|
||||
ports:
|
||||
- port: 8096
|
||||
targetPort: 8096
|
||||
protocol: TCP
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Endpoints
|
||||
metadata:
|
||||
name: jellyfin
|
||||
namespace: media
|
||||
subsets:
|
||||
- addresses:
|
||||
- ip: 192.168.20.2
|
||||
ports:
|
||||
- port: 8096
|
||||
protocol: TCP
|
||||
---
|
||||
apiVersion: gateway.networking.k8s.io/v1
|
||||
kind: HTTPRoute
|
||||
metadata:
|
||||
name: jellyfin
|
||||
namespace: media
|
||||
spec:
|
||||
parentRefs:
|
||||
- name: traefik-gateway
|
||||
namespace: kube-system
|
||||
hostnames:
|
||||
- "media.lucalise.ca"
|
||||
rules:
|
||||
- backendRefs:
|
||||
- name: jellyfin
|
||||
port: 8096
|
||||
@@ -1,43 +0,0 @@
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: vaultwarden
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: vaultwarden
|
||||
namespace: vaultwarden
|
||||
spec:
|
||||
ports:
|
||||
- port: 8000
|
||||
targetPort: 8000
|
||||
protocol: TCP
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Endpoints
|
||||
metadata:
|
||||
name: vaultwarden
|
||||
namespace: vaultwarden
|
||||
subsets:
|
||||
- addresses:
|
||||
- ip: 192.168.20.22
|
||||
ports:
|
||||
- port: 8000
|
||||
protocol: TCP
|
||||
---
|
||||
apiVersion: gateway.networking.k8s.io/v1
|
||||
kind: HTTPRoute
|
||||
metadata:
|
||||
name: vaultwarden
|
||||
namespace: vaultwarden
|
||||
spec:
|
||||
parentRefs:
|
||||
- name: traefik-gateway
|
||||
namespace: kube-system
|
||||
hostnames:
|
||||
- "vault.lucalise.ca"
|
||||
rules:
|
||||
- backendRefs:
|
||||
- name: vaultwarden
|
||||
port: 8000
|
||||
@@ -1,15 +0,0 @@
|
||||
apiVersion: gateway.networking.k8s.io/v1
|
||||
kind: HTTPRoute
|
||||
metadata:
|
||||
name: gitea
|
||||
namespace: git
|
||||
spec:
|
||||
parentRefs:
|
||||
- name: traefik-gateway
|
||||
namespace: kube-system
|
||||
hostnames:
|
||||
- "git.lucalise.ca"
|
||||
rules:
|
||||
- backendRefs:
|
||||
- name: gitea-http
|
||||
port: 3000
|
||||
@@ -1,13 +0,0 @@
|
||||
apiVersion: traefik.io/v1alpha1
|
||||
kind: IngressRouteTCP
|
||||
metadata:
|
||||
name: gitea-ssh
|
||||
namespace: git
|
||||
spec:
|
||||
entryPoints:
|
||||
- ssh
|
||||
routes:
|
||||
- match: HostSNI(`*`)
|
||||
services:
|
||||
- name: gitea-ssh
|
||||
port: 22
|
||||
@@ -1,21 +0,0 @@
|
||||
apiVersion: gateway.networking.k8s.io/v1
|
||||
kind: HTTPRoute
|
||||
metadata:
|
||||
name: longhorn
|
||||
namespace: longhorn-system
|
||||
spec:
|
||||
parentRefs:
|
||||
- name: traefik-gateway
|
||||
namespace: kube-system
|
||||
hostnames:
|
||||
- "storage.lucalise.ca"
|
||||
rules:
|
||||
- filters:
|
||||
- type: ExtensionRef
|
||||
extensionRef:
|
||||
group: traefik.io
|
||||
kind: Middleware
|
||||
name: private-networks
|
||||
backendRefs:
|
||||
- name: longhorn-frontend
|
||||
port: 80
|
||||
@@ -1,13 +0,0 @@
|
||||
apiVersion: traefik.io/v1alpha1
|
||||
kind: IngressRouteTCP
|
||||
metadata:
|
||||
name: minecraft-router
|
||||
namespace: minecraft
|
||||
spec:
|
||||
entryPoints:
|
||||
- minecraft
|
||||
routes:
|
||||
- match: HostSNI(`*`)
|
||||
services:
|
||||
- name: minecraft-router-mc-router
|
||||
port: 25565
|
||||
@@ -1,21 +0,0 @@
|
||||
apiVersion: gateway.networking.k8s.io/v1
|
||||
kind: HTTPRoute
|
||||
metadata:
|
||||
name: pihole
|
||||
namespace: pihole-system
|
||||
spec:
|
||||
parentRefs:
|
||||
- name: traefik-gateway
|
||||
namespace: kube-system
|
||||
hostnames:
|
||||
- "pihole.lucalise.ca"
|
||||
rules:
|
||||
- filters:
|
||||
- type: ExtensionRef
|
||||
extensionRef:
|
||||
group: traefik.io
|
||||
kind: Middleware
|
||||
name: private-networks
|
||||
backendRefs:
|
||||
- name: pihole-web
|
||||
port: 80
|
||||
@@ -22,6 +22,28 @@ spec:
|
||||
---
|
||||
apiVersion: traefik.io/v1alpha1
|
||||
kind: Middleware
|
||||
metadata:
|
||||
name: private-networks
|
||||
namespace: media
|
||||
spec:
|
||||
chain:
|
||||
middlewares:
|
||||
- name: private-networks
|
||||
namespace: kube-system
|
||||
---
|
||||
apiVersion: traefik.io/v1alpha1
|
||||
kind: Middleware
|
||||
metadata:
|
||||
name: private-networks
|
||||
namespace: monitoring
|
||||
spec:
|
||||
chain:
|
||||
middlewares:
|
||||
- name: private-networks
|
||||
namespace: kube-system
|
||||
---
|
||||
apiVersion: traefik.io/v1alpha1
|
||||
kind: Middleware
|
||||
metadata:
|
||||
name: private-networks
|
||||
namespace: pihole-system
|
||||
|
||||
@@ -9,8 +9,6 @@ spec:
|
||||
kubernetes.io/hostname: rufus
|
||||
|
||||
service:
|
||||
annotations:
|
||||
metallb.universe.tf/address-pool: rufus-pool
|
||||
spec:
|
||||
externalTrafficPolicy: Local
|
||||
|
||||
@@ -38,6 +36,12 @@ spec:
|
||||
default: true
|
||||
exposedPort: 25565
|
||||
protocol: TCP
|
||||
rcon:
|
||||
port: 25575
|
||||
expose:
|
||||
default: true
|
||||
exposedPort: 25575
|
||||
protocol: TCP
|
||||
|
||||
persistence:
|
||||
enabled: true
|
||||
|
||||
12
nix/homelab/kustomize/traefik/private-networks-tcp.yaml
Normal file
12
nix/homelab/kustomize/traefik/private-networks-tcp.yaml
Normal file
@@ -0,0 +1,12 @@
|
||||
apiVersion: traefik.io/v1alpha1
|
||||
kind: MiddlewareTCP
|
||||
metadata:
|
||||
name: private-networks-tcp
|
||||
namespace: kube-system
|
||||
spec:
|
||||
ipAllowList:
|
||||
sourceRange:
|
||||
- 10.0.0.0/8
|
||||
- 172.16.0.0/12
|
||||
- 192.168.0.0/16
|
||||
- 100.64.0.0/10
|
||||
@@ -20,12 +20,6 @@
|
||||
|
||||
networking.hostName = meta.hostname;
|
||||
networking.networkmanager.enable = true;
|
||||
networking.firewall.extraCommands = ''
|
||||
iptables -I INPUT -d 192.168.27.10/32 -s 10.0.0.0/8 -j ACCEPT
|
||||
iptables -I INPUT -d 192.168.27.10/32 -s 172.16.0.0/12 -j ACCEPT
|
||||
iptables -I INPUT -d 192.168.27.10/32 -s 192.168.0.0/16 -j ACCEPT
|
||||
iptables -I INPUT -d 192.168.27.10/32 -j DROP
|
||||
'';
|
||||
|
||||
time.timeZone = "America/Vancouver";
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
[ (modulesPath + "/installer/scan/not-detected.nix")
|
||||
];
|
||||
|
||||
boot.initrd.availableKernelModules = [ "nvme" "xhci_pci" "usb_storage" "sd_mod" ];
|
||||
boot.initrd.availableKernelModules = [ "nvme" "xhci_pci" "usbhid" "usb_storage" "sd_mod" ];
|
||||
boot.initrd.kernelModules = [ "dm-snapshot" ];
|
||||
boot.kernelModules = [ "kvm-amd" ];
|
||||
boot.extraModulePackages = [ ];
|
||||
|
||||
@@ -6,6 +6,7 @@ NAMESPACES=(
|
||||
"home"
|
||||
"longhorn-system"
|
||||
"pihole-system"
|
||||
"media"
|
||||
)
|
||||
|
||||
OUTPUT_FILE="kustomize/traefik/chains.yaml"
|
||||
|
||||
155
nix/homelab/scripts/generate-routes.sh
Executable file
155
nix/homelab/scripts/generate-routes.sh
Executable file
@@ -0,0 +1,155 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -e
|
||||
|
||||
# Route definitions: name:hostname:port:protocol:private
|
||||
# - name: service name (required)
|
||||
# - hostname: custom hostname, use '-' for default (name.lucalise.ca)
|
||||
# - port: service port (required)
|
||||
# - protocol: TCP (default) or UDP
|
||||
# - private: true/false (default false) - adds private-networks middleware
|
||||
ROUTES=(
|
||||
"sonarr:-:8989:TCP:true"
|
||||
"radarr:-:7878:TCP:true"
|
||||
"prowlarr:-:9696:TCP:true"
|
||||
"bazarr:-:6767:TCP:true"
|
||||
"jellyfin:media:8096:TCP:false"
|
||||
"home-assistant:-:8123:TCP:true"
|
||||
)
|
||||
|
||||
DOMAIN="lucalise.ca"
|
||||
OUTPUT_DIR="kustomize/routes"
|
||||
|
||||
generate_http_route() {
|
||||
local name="$1"
|
||||
local hostname="$2"
|
||||
local port="$3"
|
||||
local protocol="$4"
|
||||
local private="$5"
|
||||
|
||||
if [[ -z "$hostname" || "$hostname" == "-" ]]; then
|
||||
hostname="$name"
|
||||
fi
|
||||
|
||||
if [[ -z "$protocol" ]]; then
|
||||
protocol="TCP"
|
||||
fi
|
||||
|
||||
if [[ -z "$private" ]]; then
|
||||
private="false"
|
||||
fi
|
||||
|
||||
local fqdn="${hostname}.${DOMAIN}"
|
||||
|
||||
local filters_section=""
|
||||
if [[ "$private" == "true" ]]; then
|
||||
filters_section=" - filters:
|
||||
- type: ExtensionRef
|
||||
extensionRef:
|
||||
group: traefik.io
|
||||
kind: Middleware
|
||||
name: private-networks
|
||||
backendRefs:"
|
||||
else
|
||||
filters_section=" - backendRefs:"
|
||||
fi
|
||||
|
||||
cat <<EOF
|
||||
apiVersion: gateway.networking.k8s.io/v1
|
||||
kind: HTTPRoute
|
||||
metadata:
|
||||
name: ${name}
|
||||
namespace: media
|
||||
spec:
|
||||
parentRefs:
|
||||
- name: traefik-gateway
|
||||
namespace: kube-system
|
||||
hostnames:
|
||||
- "${fqdn}"
|
||||
rules:
|
||||
${filters_section}
|
||||
- name: ${name}
|
||||
port: ${port}
|
||||
EOF
|
||||
}
|
||||
|
||||
write_kustomization() {
|
||||
local kustomization_file="${OUTPUT_DIR}/../kustomization.yaml"
|
||||
local temp_file=$(mktemp)
|
||||
|
||||
# Collect new route paths
|
||||
local route_paths=()
|
||||
for route in "${ROUTES[@]}"; do
|
||||
IFS=':' read -r name _ _ _ _ <<< "$route"
|
||||
route_paths+=(" - ./routes/${name}.yaml")
|
||||
done
|
||||
|
||||
local in_resources=false
|
||||
local resources_written=false
|
||||
|
||||
while IFS= read -r line; do
|
||||
# Detect resources section
|
||||
if [[ "$line" == "resources:" ]]; then
|
||||
in_resources=true
|
||||
echo "$line" >> "$temp_file"
|
||||
continue
|
||||
fi
|
||||
|
||||
# If in resources section
|
||||
if [[ "$in_resources" == true ]]; then
|
||||
# Check if line is a resource entry (starts with " - ")
|
||||
if [[ "$line" =~ ^[[:space:]]*-[[:space:]] ]]; then
|
||||
# Skip route entries, keep everything else
|
||||
if [[ "$line" =~ \./routes/ ]]; then
|
||||
continue
|
||||
else
|
||||
echo "$line" >> "$temp_file"
|
||||
fi
|
||||
else
|
||||
# End of resources section - write new routes before moving on
|
||||
if [[ "$resources_written" == false ]]; then
|
||||
for route_path in "${route_paths[@]}"; do
|
||||
echo "$route_path" >> "$temp_file"
|
||||
done
|
||||
resources_written=true
|
||||
fi
|
||||
in_resources=false
|
||||
echo "$line" >> "$temp_file"
|
||||
fi
|
||||
else
|
||||
echo "$line" >> "$temp_file"
|
||||
fi
|
||||
done < "$kustomization_file"
|
||||
|
||||
# If file ended while still in resources section, write routes now
|
||||
if [[ "$in_resources" == true && "$resources_written" == false ]]; then
|
||||
for route_path in "${route_paths[@]}"; do
|
||||
echo "$route_path" >> "$temp_file"
|
||||
done
|
||||
fi
|
||||
|
||||
mv "$temp_file" "$kustomization_file"
|
||||
echo "Updated ${kustomization_file} with ${#route_paths[@]} routes"
|
||||
}
|
||||
|
||||
main() {
|
||||
mkdir -p "${OUTPUT_DIR}"
|
||||
|
||||
for route in "${ROUTES[@]}"; do
|
||||
IFS=':' read -r name hostname port protocol private <<< "$route"
|
||||
|
||||
echo "Generating route for ${name}..."
|
||||
|
||||
output_file="${OUTPUT_DIR}/${name}.yaml"
|
||||
generate_http_route "$name" "$hostname" "$port" "$protocol" "$private" > "$output_file"
|
||||
|
||||
echo " -> ${output_file}"
|
||||
done
|
||||
|
||||
echo ""
|
||||
write_kustomization
|
||||
echo ""
|
||||
echo "Done! Generated ${#ROUTES[@]} routes."
|
||||
}
|
||||
|
||||
main "$@"
|
||||
11
nix/homelab/src/commands.rs
Normal file
11
nix/homelab/src/commands.rs
Normal file
@@ -0,0 +1,11 @@
|
||||
pub mod generate_routes;
|
||||
|
||||
use clap::Subcommand;
|
||||
|
||||
#[derive(Subcommand, Debug)]
|
||||
pub enum Commands {
|
||||
/// generate gateway api routes
|
||||
GenerateRoutes,
|
||||
/// sync dns records with pi-hole
|
||||
SyncDNS,
|
||||
}
|
||||
144
nix/homelab/src/commands/generate_routes.rs
Normal file
144
nix/homelab/src/commands/generate_routes.rs
Normal file
@@ -0,0 +1,144 @@
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
use askama::Template;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::HelperError;
|
||||
|
||||
#[derive(Serialize, Deserialize, Default, Debug, Clone)]
|
||||
pub struct Route {
|
||||
#[serde(default)]
|
||||
kind: RouteKind,
|
||||
name: String,
|
||||
hostname: Option<String>,
|
||||
entrypoint: Option<String>,
|
||||
namespace: String,
|
||||
service: Option<String>,
|
||||
port: i16,
|
||||
private: bool,
|
||||
}
|
||||
|
||||
impl Route {
|
||||
fn hostname(&self) -> &str {
|
||||
self.hostname.as_ref().unwrap_or(&self.name)
|
||||
}
|
||||
|
||||
fn service(&self) -> &str {
|
||||
self.service.as_ref().unwrap_or(&self.name)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
||||
enum RouteKind {
|
||||
#[default]
|
||||
HTTP,
|
||||
TCP,
|
||||
}
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "httproute.yaml", escape = "none")]
|
||||
struct HttpRoute<'a> {
|
||||
name: &'a str,
|
||||
namespace: &'a str,
|
||||
hostname: &'a str,
|
||||
service: &'a str,
|
||||
port: i16,
|
||||
private: bool,
|
||||
}
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "ingressroutetcp.yaml", escape = "none")]
|
||||
struct TcpRoute<'a> {
|
||||
name: &'a str,
|
||||
namespace: &'a str,
|
||||
entrypoint: &'a str,
|
||||
service: &'a str,
|
||||
port: i16,
|
||||
private: bool,
|
||||
}
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "http_middleware_chain.yaml", escape = "none")]
|
||||
struct MiddlewareChain<'a> {
|
||||
namespace: &'a str,
|
||||
}
|
||||
|
||||
pub fn generate_routes(routes: &Vec<Route>) -> Result<(), HelperError> {
|
||||
let routes_content = routes.iter().enumerate().try_fold(
|
||||
String::new(),
|
||||
|mut acc, (i, r)| -> Result<_, HelperError> {
|
||||
if i > 0 {
|
||||
acc.push_str("\n---\n");
|
||||
}
|
||||
acc.push_str(&generate_route(r)?);
|
||||
Ok(acc)
|
||||
},
|
||||
)?;
|
||||
let chains = generate_chains(routes)?;
|
||||
std::fs::write("kustomize/routes.yaml", &routes_content)?;
|
||||
std::fs::write("kustomize/traefik/chains.yaml", &chains)?;
|
||||
println!("Wrote: {}", routes_content);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn generate_route(route: &Route) -> Result<String, HelperError> {
|
||||
match route.kind {
|
||||
RouteKind::HTTP => {
|
||||
let template = HttpRoute {
|
||||
name: &route.name,
|
||||
namespace: &route.namespace,
|
||||
hostname: route.hostname(),
|
||||
service: route.service(),
|
||||
port: route.port,
|
||||
private: route.private,
|
||||
};
|
||||
Ok(template.render()?)
|
||||
}
|
||||
RouteKind::TCP => {
|
||||
let entrypoint = route
|
||||
.entrypoint
|
||||
.as_ref()
|
||||
.ok_or(HelperError::TCPEntryPoint(route.name.clone()))?;
|
||||
let template = TcpRoute {
|
||||
name: &route.name,
|
||||
namespace: &route.namespace,
|
||||
entrypoint,
|
||||
service: route.service(),
|
||||
port: route.port,
|
||||
private: route.private,
|
||||
};
|
||||
Ok(template.render()?)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn generate_chains(routes: &[Route]) -> Result<String, HelperError> {
|
||||
let namespaces = routes
|
||||
.iter()
|
||||
.filter_map(|r| r.private.then_some((r.kind.clone(), &r.namespace)))
|
||||
.collect::<BTreeSet<_>>();
|
||||
|
||||
namespaces
|
||||
.iter()
|
||||
.enumerate()
|
||||
.try_fold(String::new(), |mut acc, (i, (kind, namespace))| {
|
||||
match kind {
|
||||
RouteKind::HTTP => {}
|
||||
_ => {
|
||||
return Ok(acc);
|
||||
}
|
||||
}
|
||||
if i > 0 {
|
||||
acc.push_str("\n---\n");
|
||||
}
|
||||
let rendered = match kind {
|
||||
RouteKind::HTTP => Some(MiddlewareChain { namespace }.render()?),
|
||||
_ => None,
|
||||
};
|
||||
if let Some(rendered) = rendered {
|
||||
acc.push_str(&rendered);
|
||||
}
|
||||
Ok(acc)
|
||||
})
|
||||
}
|
||||
87
nix/homelab/src/config.rs
Normal file
87
nix/homelab/src/config.rs
Normal file
@@ -0,0 +1,87 @@
|
||||
#[cfg(not(feature = "file-config"))]
|
||||
use std::{collections::HashSet, env};
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{PiHoleConfig, RouterConfig, commands::generate_routes::Route, error::Result};
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct Config {
|
||||
pub routes: Option<Vec<Route>>,
|
||||
pub pihole: Option<PiHoleConfig>,
|
||||
pub router: Option<RouterConfig>,
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "file-config"))]
|
||||
struct EnvCollector {
|
||||
missing: Vec<&'static str>,
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "file-config"))]
|
||||
impl EnvCollector {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
missing: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn get(&mut self, key: &'static str) -> Option<String> {
|
||||
match env::var(key) {
|
||||
Ok(val) => Some(val),
|
||||
Err(_) => {
|
||||
self.missing.push(key);
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn finish(self) -> Result<()> {
|
||||
if self.missing.is_empty() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(crate::error::Error::MissingEnvVars(self.missing.join(", ")))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "file-config"))]
|
||||
pub fn parse_config() -> Result<Config> {
|
||||
let mut env = EnvCollector::new();
|
||||
|
||||
let pihole_url = env.get("PIHOLE_URL");
|
||||
let pihole_password = env.get("PIHOLE_PASSWORD_FILE");
|
||||
let pihole_hosts = env.get("PIHOLE_EXTRA_HOSTS");
|
||||
let router_host = env.get("ROUTER_HOST");
|
||||
let router_user = env.get("ROUTER_USER");
|
||||
let router_key = env.get("ROUTER_KEY_PATH");
|
||||
let router_lease = env.get("ROUTER_LEASE_FILE");
|
||||
|
||||
env.finish()?;
|
||||
|
||||
Ok(Config {
|
||||
routes: None,
|
||||
pihole: Some(PiHoleConfig {
|
||||
url: pihole_url.unwrap(),
|
||||
password_file: pihole_password.unwrap(),
|
||||
extra_hosts: Some(
|
||||
pihole_hosts
|
||||
.unwrap()
|
||||
.split('\n')
|
||||
.map(String::from)
|
||||
.collect::<HashSet<String>>(),
|
||||
),
|
||||
}),
|
||||
router: Some(RouterConfig {
|
||||
host: router_host.unwrap(),
|
||||
user: router_user.unwrap(),
|
||||
key_path: router_key.unwrap().into(),
|
||||
lease_file: router_lease.unwrap(),
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(feature = "file-config")]
|
||||
pub fn parse_config() -> Result<Config> {
|
||||
let bytes = std::fs::read("./config.toml")?;
|
||||
Ok(toml::from_slice::<Config>(&bytes)?)
|
||||
}
|
||||
20
nix/homelab/src/dns.rs
Normal file
20
nix/homelab/src/dns.rs
Normal file
@@ -0,0 +1,20 @@
|
||||
use crate::{
|
||||
error::Result,
|
||||
lease_parser::{Lease, parse_leases},
|
||||
transport::Transport,
|
||||
};
|
||||
|
||||
pub struct Router<T: Transport> {
|
||||
transport: T,
|
||||
}
|
||||
|
||||
impl<T: Transport> Router<T> {
|
||||
pub fn new(transport: T) -> Self {
|
||||
Self { transport }
|
||||
}
|
||||
|
||||
pub fn dhcp_leases(&self, resource: &str) -> Result<Vec<Lease>> {
|
||||
let raw = self.transport.fetch(resource)?;
|
||||
parse_leases(&raw).map_err(|e| crate::error::Error::Parse(e.to_string()))
|
||||
}
|
||||
}
|
||||
25
nix/homelab/src/error.rs
Normal file
25
nix/homelab/src/error.rs
Normal file
@@ -0,0 +1,25 @@
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum Error {
|
||||
#[error("SSH error: {0}")]
|
||||
SSH(#[from] ssh2::Error),
|
||||
#[error("parse error: {0}")]
|
||||
Parse(String),
|
||||
#[error("IO error: {0}")]
|
||||
IO(#[from] std::io::Error),
|
||||
#[error("command return non 0 exit code: {0}")]
|
||||
ExitCode(i32),
|
||||
#[error("HTTP error: {0}")]
|
||||
Http(#[from] reqwest::Error),
|
||||
#[error("Pi-hole API error: {0}")]
|
||||
PiHole(String),
|
||||
#[cfg(not(feature = "file-config"))]
|
||||
#[error("missing environment variables: {0}")]
|
||||
MissingEnvVars(String),
|
||||
#[cfg(feature = "file-config")]
|
||||
#[error("error parsing toml: {0}")]
|
||||
TomlParse(#[from] toml::de::Error),
|
||||
}
|
||||
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
146
nix/homelab/src/lease_parser.rs
Normal file
146
nix/homelab/src/lease_parser.rs
Normal file
@@ -0,0 +1,146 @@
|
||||
use std::net::IpAddr;
|
||||
|
||||
use nom::{
|
||||
IResult, Parser,
|
||||
branch::alt,
|
||||
bytes::complete::{tag, take_till, take_until, take_while1},
|
||||
character::complete::{char, digit1, multispace1, space0, space1},
|
||||
combinator::{map, map_res, value},
|
||||
multi::many0,
|
||||
sequence::{delimited, preceded, terminated},
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct Lease {
|
||||
pub ip: IpAddr,
|
||||
pub binding_state: BindingState,
|
||||
pub client_hostname: Option<String>,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum BindingState {
|
||||
Active,
|
||||
Free,
|
||||
Abandoned,
|
||||
Backup,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(Debug, Clone)]
|
||||
enum LeaseField {
|
||||
BindingState(BindingState),
|
||||
ClientHostname(String),
|
||||
Other,
|
||||
}
|
||||
|
||||
fn ws(input: &str) -> IResult<&str, ()> {
|
||||
value(
|
||||
(),
|
||||
many0(alt((
|
||||
value((), multispace1),
|
||||
value((), (tag("#"), take_until("\n"), tag("\n"))),
|
||||
))),
|
||||
)
|
||||
.parse(input)
|
||||
}
|
||||
|
||||
fn parse_ip(input: &str) -> IResult<&str, IpAddr> {
|
||||
map_res(
|
||||
take_while1(|c: char| c.is_ascii_digit() || c == '.'),
|
||||
str::parse,
|
||||
)
|
||||
.parse(input)
|
||||
}
|
||||
|
||||
fn parse_field(input: &str) -> IResult<&str, LeaseField> {
|
||||
alt((
|
||||
map(
|
||||
preceded(
|
||||
(tag("client-hostname"), space1),
|
||||
terminated(
|
||||
delimited(char('"'), take_until("\""), char('"')),
|
||||
(space0, char(';')),
|
||||
),
|
||||
),
|
||||
|h: &str| LeaseField::ClientHostname(h.to_string()),
|
||||
),
|
||||
map(
|
||||
preceded(
|
||||
(tag("binding"), space1, tag("state"), space1),
|
||||
terminated(
|
||||
alt((
|
||||
value(BindingState::Active, tag("active")),
|
||||
value(BindingState::Free, tag("free")),
|
||||
value(BindingState::Abandoned, tag("abandoned")),
|
||||
value(BindingState::Backup, tag("backup")),
|
||||
)),
|
||||
(space0, char(';')),
|
||||
),
|
||||
),
|
||||
LeaseField::BindingState,
|
||||
),
|
||||
value(
|
||||
LeaseField::Other,
|
||||
preceded(
|
||||
(tag("uid"), space1),
|
||||
terminated(
|
||||
delimited(char('"'), take_until("\""), char('"')),
|
||||
(space0, char(';')),
|
||||
),
|
||||
),
|
||||
),
|
||||
value(
|
||||
LeaseField::Other,
|
||||
(take_till(|c| c == ';' || c == '}'), char(';')),
|
||||
),
|
||||
))
|
||||
.parse(input)
|
||||
}
|
||||
|
||||
pub fn parse_lease(input: &str) -> IResult<&str, Lease> {
|
||||
let (input, _) = ws(input)?;
|
||||
let (input, _) = tag("lease").parse(input)?;
|
||||
let (input, _) = space1(input)?;
|
||||
let (input, ip) = parse_ip(input)?;
|
||||
let (input, _) = space0(input)?;
|
||||
let (input, _) = char('{').parse(input)?;
|
||||
let (input, _) = ws(input)?;
|
||||
|
||||
let (input, fields) = many0(terminated(parse_field, ws)).parse(input)?;
|
||||
|
||||
let (input, _) = char('}').parse(input)?;
|
||||
let (input, _) = ws(input)?;
|
||||
|
||||
let mut lease = Lease {
|
||||
ip,
|
||||
binding_state: BindingState::Free,
|
||||
client_hostname: None,
|
||||
};
|
||||
|
||||
for field in fields {
|
||||
match field {
|
||||
LeaseField::BindingState(s) => lease.binding_state = s,
|
||||
LeaseField::ClientHostname(h) => lease.client_hostname = Some(h),
|
||||
LeaseField::Other => {}
|
||||
}
|
||||
}
|
||||
|
||||
Ok((input, lease))
|
||||
}
|
||||
|
||||
pub fn parse_leases(input: &str) -> Result<Vec<Lease>, nom::Err<nom::error::Error<&str>>> {
|
||||
let mut leases = Vec::new();
|
||||
let Some(start) = input.find("\nlease ") else {
|
||||
return Ok(leases);
|
||||
};
|
||||
|
||||
let mut remaining = &input[start..];
|
||||
|
||||
while !remaining.is_empty() {
|
||||
let (rest, lease) = parse_lease(remaining)?;
|
||||
leases.push(lease);
|
||||
remaining = rest;
|
||||
}
|
||||
Ok(leases)
|
||||
}
|
||||
135
nix/homelab/src/main.rs
Normal file
135
nix/homelab/src/main.rs
Normal file
@@ -0,0 +1,135 @@
|
||||
mod commands;
|
||||
mod config;
|
||||
mod dns;
|
||||
mod error;
|
||||
mod lease_parser;
|
||||
mod pihole;
|
||||
mod transport;
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::{collections::HashSet, env};
|
||||
|
||||
use anyhow::Context;
|
||||
use clap::{CommandFactory, Parser};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::config::parse_config;
|
||||
use crate::{
|
||||
commands::{
|
||||
Commands,
|
||||
generate_routes::{Route, generate_routes},
|
||||
},
|
||||
dns::Router,
|
||||
lease_parser::{BindingState, Lease},
|
||||
pihole::PiHoleClient,
|
||||
transport::SSHTransport,
|
||||
};
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(version = "0.1.0", about = "Helper for k3s", long_about = None)]
|
||||
struct Cli {
|
||||
#[command(subcommand)]
|
||||
command: Option<Commands>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum HelperError {
|
||||
#[error("error reading file")]
|
||||
ReadFile(#[from] std::io::Error),
|
||||
#[error("entrypoint required for tcproute: {0:?}")]
|
||||
TCPEntryPoint(String),
|
||||
#[error("template rendering error: {0}")]
|
||||
Template(#[from] askama::Error),
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct PiHoleConfig {
|
||||
url: String,
|
||||
password_file: String,
|
||||
extra_hosts: Option<HashSet<String>>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct RouterConfig {
|
||||
host: String,
|
||||
user: String,
|
||||
key_path: PathBuf,
|
||||
lease_file: String,
|
||||
}
|
||||
|
||||
#[tokio::main(flavor = "current_thread")]
|
||||
async fn main() -> anyhow::Result<()> {
|
||||
let cli = Cli::parse();
|
||||
|
||||
match &cli.command {
|
||||
Some(Commands::GenerateRoutes {}) => {
|
||||
let config = parse_config()?;
|
||||
let routes = config
|
||||
.routes
|
||||
.context("routes in config are required for generating route manifests")?;
|
||||
generate_routes(&routes)?;
|
||||
}
|
||||
Some(Commands::SyncDNS {}) => {
|
||||
let config = parse_config()?;
|
||||
let pihole_config = config
|
||||
.pihole
|
||||
.context("pihole configuration is necessary for syncing dns")?;
|
||||
let router_config = config
|
||||
.router
|
||||
.context("router configuration is necessary for syncing dns")?;
|
||||
|
||||
let password = std::fs::read_to_string(&pihole_config.password_file)
|
||||
.context(format!(
|
||||
"failed to read pihole password from {}",
|
||||
pihole_config.password_file
|
||||
))?
|
||||
.trim()
|
||||
.to_string();
|
||||
|
||||
let leases = tokio::task::spawn_blocking(move || -> anyhow::Result<Vec<Lease>> {
|
||||
let r = Router::new(SSHTransport::new(
|
||||
&router_config.host,
|
||||
&router_config.user,
|
||||
&router_config.key_path,
|
||||
)?);
|
||||
let leases = r
|
||||
.dhcp_leases(&router_config.lease_file)?
|
||||
.into_iter()
|
||||
.filter(|l| {
|
||||
l.binding_state == BindingState::Active && l.client_hostname.is_some()
|
||||
})
|
||||
.collect();
|
||||
Ok(leases)
|
||||
})
|
||||
.await??;
|
||||
|
||||
let mut desired = leases
|
||||
.into_iter()
|
||||
.map(|l| {
|
||||
format!(
|
||||
"{} {}",
|
||||
l.ip,
|
||||
l.client_hostname.expect("filtered for Some above")
|
||||
)
|
||||
})
|
||||
.collect::<HashSet<String>>();
|
||||
if let Some(extra_hosts) = pihole_config.extra_hosts {
|
||||
desired.extend(extra_hosts);
|
||||
}
|
||||
|
||||
println!("Found {} active leases with hostnames", desired.len());
|
||||
|
||||
let client = PiHoleClient::new(&pihole_config.url, &password).await?;
|
||||
|
||||
let stats = client.sync_hosts(desired).await?;
|
||||
println!(
|
||||
"Sync complete: added {}, removed {}",
|
||||
stats.added, stats.removed
|
||||
);
|
||||
}
|
||||
None => Cli::command().print_long_help()?,
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
171
nix/homelab/src/pihole.rs
Normal file
171
nix/homelab/src/pihole.rs
Normal file
@@ -0,0 +1,171 @@
|
||||
use std::collections::HashSet;
|
||||
|
||||
use reqwest::Client;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::error::{Error, Result};
|
||||
|
||||
pub struct PiHoleClient {
|
||||
client: Client,
|
||||
base_url: String,
|
||||
sid: String,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct SyncStats {
|
||||
pub added: usize,
|
||||
pub removed: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct AuthResponse {
|
||||
session: Session,
|
||||
error: Option<AuthError>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct Session {
|
||||
valid: bool,
|
||||
sid: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct AuthError {
|
||||
message: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct AuthRequest {
|
||||
password: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct ConfigResponse {
|
||||
config: DnsConfig,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct DnsConfig {
|
||||
dns: DnsHosts,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct DnsHosts {
|
||||
hosts: Vec<String>,
|
||||
}
|
||||
|
||||
impl PiHoleClient {
|
||||
pub async fn new(base_url: &str, password: &str) -> Result<Self> {
|
||||
let client = Client::new();
|
||||
let url = format!("{}/api/auth", base_url.trim_end_matches('/'));
|
||||
|
||||
let response = client
|
||||
.post(&url)
|
||||
.json(&AuthRequest {
|
||||
password: password.to_string(),
|
||||
})
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
let auth: AuthResponse = response.json().await?;
|
||||
|
||||
if !auth.session.valid {
|
||||
return Err(Error::PiHole(format!(
|
||||
"authentication failed: {}",
|
||||
auth.error.unwrap().message
|
||||
)));
|
||||
}
|
||||
|
||||
let sid = auth.session.sid.ok_or_else(|| {
|
||||
Error::PiHole("authentication succeeded but no session ID returned".to_string())
|
||||
})?;
|
||||
|
||||
Ok(Self {
|
||||
client,
|
||||
base_url: base_url.trim_end_matches('/').to_string(),
|
||||
sid,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn get_hosts(&self) -> Result<HashSet<String>> {
|
||||
let url = format!("{}/api/config/dns/hosts", self.base_url);
|
||||
|
||||
let response = self
|
||||
.client
|
||||
.get(&url)
|
||||
.header("X-FTL-SID", &self.sid)
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
if let Err(e) = response.error_for_status_ref() {
|
||||
return Err(Error::PiHole(format!("failed to get hosts: {}", e)));
|
||||
}
|
||||
|
||||
let config: ConfigResponse = response.json().await?;
|
||||
Ok(config.config.dns.hosts.into_iter().collect())
|
||||
}
|
||||
|
||||
pub async fn add_host(&self, entry: &str) -> Result<()> {
|
||||
let encoded = urlencoding::encode(entry);
|
||||
let url = format!("{}/api/config/dns/hosts/{}", self.base_url, encoded);
|
||||
|
||||
let response = self
|
||||
.client
|
||||
.put(&url)
|
||||
.header("X-FTL-SID", &self.sid)
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
if let Err(e) = response.error_for_status_ref() {
|
||||
let body = response.text().await.unwrap_or_default();
|
||||
return Err(Error::PiHole(format!(
|
||||
"failed to add host '{}': {} - {}",
|
||||
entry, e, body
|
||||
)));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn delete_host(&self, entry: &str) -> Result<()> {
|
||||
let encoded = urlencoding::encode(entry);
|
||||
let url = format!("{}/api/config/dns/hosts/{}", self.base_url, encoded);
|
||||
|
||||
let response = self
|
||||
.client
|
||||
.delete(&url)
|
||||
.header("X-FTL-SID", &self.sid)
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
if let Err(e) = response.error_for_status_ref() {
|
||||
let body = response.text().await.unwrap_or_default();
|
||||
return Err(Error::PiHole(format!(
|
||||
"failed to delete host '{}': {} - {}",
|
||||
entry, e, body
|
||||
)));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn sync_hosts(&self, desired: HashSet<String>) -> Result<SyncStats> {
|
||||
let current = self.get_hosts().await?;
|
||||
|
||||
let to_delete = current.difference(&desired).cloned().collect::<Vec<_>>();
|
||||
let to_add = desired.difference(¤t).cloned().collect::<Vec<_>>();
|
||||
|
||||
for entry in &to_delete {
|
||||
self.delete_host(entry).await?;
|
||||
}
|
||||
|
||||
for entry in &to_add {
|
||||
self.add_host(entry).await?;
|
||||
}
|
||||
|
||||
Ok(SyncStats {
|
||||
added: to_add.len(),
|
||||
removed: to_delete.len(),
|
||||
})
|
||||
}
|
||||
}
|
||||
10
nix/homelab/src/templates/http_middleware_chain.yaml
Normal file
10
nix/homelab/src/templates/http_middleware_chain.yaml
Normal file
@@ -0,0 +1,10 @@
|
||||
apiVersion: traefik.io/v1alpha1
|
||||
kind: Middleware
|
||||
metadata:
|
||||
name: private-networks
|
||||
namespace: {{ namespace }}
|
||||
spec:
|
||||
chain:
|
||||
middlewares:
|
||||
- name: private-networks
|
||||
namespace: kube-system
|
||||
@@ -1,21 +1,23 @@
|
||||
apiVersion: gateway.networking.k8s.io/v1
|
||||
kind: HTTPRoute
|
||||
metadata:
|
||||
name: home-assistant
|
||||
namespace: home
|
||||
name: {{ name }}
|
||||
namespace: {{ namespace }}
|
||||
spec:
|
||||
parentRefs:
|
||||
- name: traefik-gateway
|
||||
namespace: kube-system
|
||||
hostnames:
|
||||
- "home-assistant.lucalise.ca"
|
||||
- {{ hostname }}.lucalise.ca
|
||||
rules:
|
||||
- filters:
|
||||
- backendRefs:
|
||||
- name: {{ service }}
|
||||
port: {{ port }}
|
||||
{%- if private %}
|
||||
filters:
|
||||
- type: ExtensionRef
|
||||
extensionRef:
|
||||
group: traefik.io
|
||||
kind: Middleware
|
||||
name: private-networks
|
||||
backendRefs:
|
||||
- name: home-assistant
|
||||
port: 8080
|
||||
{%- endif %}
|
||||
18
nix/homelab/src/templates/ingressroutetcp.yaml
Normal file
18
nix/homelab/src/templates/ingressroutetcp.yaml
Normal file
@@ -0,0 +1,18 @@
|
||||
apiVersion: traefik.io/v1alpha1
|
||||
kind: IngressRouteTCP
|
||||
metadata:
|
||||
name: {{ name }}
|
||||
namespace: {{ namespace }}
|
||||
spec:
|
||||
entryPoints:
|
||||
- {{ entrypoint }}
|
||||
routes:
|
||||
- match: HostSNI(`*`)
|
||||
{%- if private %}
|
||||
middlewares:
|
||||
- name: private-networks-tcp
|
||||
namespace: kube-system
|
||||
{%- endif %}
|
||||
services:
|
||||
- name: {{ service }}
|
||||
port: {{ port }}
|
||||
9
nix/homelab/src/transport.rs
Normal file
9
nix/homelab/src/transport.rs
Normal file
@@ -0,0 +1,9 @@
|
||||
mod ssh;
|
||||
|
||||
pub use ssh::SSHTransport;
|
||||
|
||||
use crate::error::Result;
|
||||
|
||||
pub trait Transport {
|
||||
fn fetch(&self, resource: &str) -> Result<String>;
|
||||
}
|
||||
41
nix/homelab/src/transport/ssh.rs
Normal file
41
nix/homelab/src/transport/ssh.rs
Normal file
@@ -0,0 +1,41 @@
|
||||
use std::{io::Read, net::TcpStream, path::Path};
|
||||
|
||||
use ssh2::Session;
|
||||
|
||||
use crate::{error::Result, transport::Transport};
|
||||
|
||||
pub struct SSHTransport {
|
||||
session: Session,
|
||||
}
|
||||
|
||||
impl SSHTransport {
|
||||
pub fn new(host: &str, user: &str, key_path: &Path) -> Result<Self> {
|
||||
let stream = TcpStream::connect(host)?;
|
||||
|
||||
let mut s = Self {
|
||||
session: Session::new()?,
|
||||
};
|
||||
s.session.set_tcp_stream(stream);
|
||||
s.session.handshake()?;
|
||||
s.session.userauth_pubkey_file(user, None, key_path, None)?;
|
||||
Ok(s)
|
||||
}
|
||||
}
|
||||
|
||||
impl Transport for SSHTransport {
|
||||
fn fetch(&self, resource: &str) -> Result<String> {
|
||||
let mut channel = self.session.channel_session()?;
|
||||
channel.exec(&format!("cat {}", resource))?;
|
||||
|
||||
let mut output = String::new();
|
||||
channel.read_to_string(&mut output)?;
|
||||
channel.wait_close()?;
|
||||
|
||||
let c = channel.exit_status()?;
|
||||
if c != 0 {
|
||||
return Err(crate::error::Error::ExitCode(c));
|
||||
}
|
||||
|
||||
Ok(output)
|
||||
}
|
||||
}
|
||||
27
nix/iso/flake.lock
generated
Normal file
27
nix/iso/flake.lock
generated
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"nodes": {
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1767325753,
|
||||
"narHash": "sha256-yA/CuWyqm+AQo2ivGy6PlYrjZBQm7jfbe461+4HF2fo=",
|
||||
"owner": "nixos",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "64049ca74d63e971b627b5f3178d95642e61cedd",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nixos",
|
||||
"ref": "nixos-25.11",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"nixpkgs": "nixpkgs"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
||||
34
nix/iso/flake.nix
Normal file
34
nix/iso/flake.nix
Normal file
@@ -0,0 +1,34 @@
|
||||
{
|
||||
description = "NixOS ISO";
|
||||
|
||||
inputs = {
|
||||
nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-25.11";
|
||||
};
|
||||
|
||||
outputs =
|
||||
{ nixpkgs, ... }:
|
||||
{
|
||||
nixosConfigurations = {
|
||||
iso = nixpkgs.lib.nixosSystem {
|
||||
system = "x86_64-linux";
|
||||
modules = [
|
||||
"${nixpkgs}/nixos/modules/installer/cd-dvd/installation-cd-minimal.nix"
|
||||
../modules/keys.nix
|
||||
(
|
||||
{
|
||||
config,
|
||||
...
|
||||
}:
|
||||
{
|
||||
users.users = {
|
||||
nixos.openssh.authorizedKeys.keys = config.authorized_ssh;
|
||||
root.openssh.authorizedKeys.keys = config.authorized_ssh;
|
||||
};
|
||||
services.openssh.enable = true;
|
||||
}
|
||||
)
|
||||
];
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -21,6 +21,7 @@
|
||||
roboto
|
||||
roboto-mono
|
||||
open-sans
|
||||
comic-relief
|
||||
];
|
||||
fonts.fontDir.enable = true;
|
||||
commonPackages = with pkgs; [
|
||||
@@ -50,7 +51,6 @@
|
||||
gnumake
|
||||
watchman
|
||||
bat
|
||||
rustup
|
||||
emote
|
||||
pkg-config
|
||||
openssl
|
||||
@@ -59,7 +59,6 @@
|
||||
sops
|
||||
yubikey-personalization
|
||||
yubikey-manager
|
||||
gnupg
|
||||
(pass.withExtensions (exts: with exts; [ pass-import ]))
|
||||
python3
|
||||
jdt-language-server
|
||||
@@ -76,25 +75,44 @@
|
||||
kubectl
|
||||
kubernetes-helm
|
||||
helmfile
|
||||
jless
|
||||
fd
|
||||
dig
|
||||
just
|
||||
];
|
||||
programs.nix-ld.enable = lib.mkDefault true;
|
||||
programs.zsh.enable = lib.mkDefault true;
|
||||
services.openssh.enable = lib.mkDefault true;
|
||||
hardware.enableAllFirmware = true;
|
||||
sops.defaultSopsFile = ../../secrets/sops.yaml;
|
||||
sops.age.sshKeyPaths = [ "/etc/ssh/id_ed25519" ];
|
||||
programs.gnupg.agent = {
|
||||
enable = true;
|
||||
enableSSHSupport = true;
|
||||
enableSSHSupport = false;
|
||||
pinentryPackage = pkgs.pinentry-gtk2;
|
||||
};
|
||||
services.pcscd.enable = true;
|
||||
services.udev.packages = with pkgs; [ yubikey-personalization ];
|
||||
services.udev.packages = with pkgs; [
|
||||
yubikey-personalization
|
||||
yubikey-manager
|
||||
];
|
||||
programs.ssh.startAgent = true;
|
||||
|
||||
programs.neovim = lib.mkDefault {
|
||||
enable = true;
|
||||
defaultEditor = true;
|
||||
vimAlias = true;
|
||||
};
|
||||
|
||||
sops = {
|
||||
defaultSopsFile = ../../secrets/secrets.yaml;
|
||||
age.sshKeyPaths = [ "/home/luca/.ssh/id_ed25519" ];
|
||||
secrets = {
|
||||
"pihole_password" = {
|
||||
owner = "luca";
|
||||
};
|
||||
"k3s_token" = {
|
||||
owner = "luca";
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
@@ -21,5 +21,7 @@
|
||||
./dns.nix
|
||||
./mounts.nix
|
||||
./nfs-mesh.nix
|
||||
./rust.nix
|
||||
# ./networking/wireguard-mesh.nix
|
||||
];
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
lib,
|
||||
config,
|
||||
pkgs-before,
|
||||
inputs,
|
||||
meta,
|
||||
...
|
||||
}:
|
||||
|
||||
@@ -12,6 +14,15 @@
|
||||
};
|
||||
|
||||
config = lib.mkIf config.desktop.enable {
|
||||
i18n.inputMethod = {
|
||||
enable = true;
|
||||
type = "fcitx5";
|
||||
fcitx5.addons = with pkgs; [
|
||||
fcitx5-mozc
|
||||
fcitx5-gtk
|
||||
];
|
||||
};
|
||||
|
||||
environment.systemPackages = with pkgs; [
|
||||
vscode-fhs
|
||||
pavucontrol
|
||||
@@ -50,6 +61,8 @@
|
||||
lm_sensors
|
||||
fanctl
|
||||
waypipe
|
||||
inputs.quickshell.packages.${meta.architecture}.default
|
||||
alacritty
|
||||
];
|
||||
boot.kernelModules = [
|
||||
"iptables"
|
||||
@@ -108,6 +121,14 @@
|
||||
};
|
||||
xdg.configFile = {
|
||||
"hypr/hyprlock.conf".source = ../../custom/hyprlock/hyprlock.conf;
|
||||
"fcitx5/config".text = ''
|
||||
[Hotkey]
|
||||
TriggerKeys=
|
||||
EnumerateWithTriggerKeys=True
|
||||
EnumerateForwardKeys=
|
||||
EnumerateBackwardKeys=
|
||||
EnumerateSkipFirst=False
|
||||
'';
|
||||
};
|
||||
services.dunst = {
|
||||
enable = true;
|
||||
@@ -202,9 +223,15 @@
|
||||
"$mod, k, movefocus, u"
|
||||
"$mod, j, movefocus, d"
|
||||
"$mod, Space, togglesplit"
|
||||
"$mod SHIFT, h, movewindow, l"
|
||||
"$mod SHIFT, l, movewindow, r"
|
||||
"$mod SHIFT, v, exec, bash -c ~/dotfiles/scripts/copy.sh"
|
||||
"$mod SHIFT, s, exec, bash -c ~/dotfiles/scripts/screenshot.sh"
|
||||
"$mod, p, exec, bash -c ~/dotfiles/scripts/project.sh"
|
||||
"$mod SHIFT, k, exec, bash -c ~/dotfiles/scripts/layout.sh"
|
||||
"$mod SHIFT, j, exec, fcitx5-remote -t"
|
||||
"$mod CTRL, h, focusmonitor, l"
|
||||
"$mod CTRL, l, focusmonitor, r"
|
||||
|
||||
"$mod, 0, workspace, 10"
|
||||
"$mod SHIFT, 0, movetoworkspacesilent, 10"
|
||||
@@ -235,14 +262,14 @@
|
||||
",XF86MonBrightnessDown, exec, bash -c 'brightnessctl s 5%- && perc=$(( \$(brightnessctl get) * 100 / \$(brightnessctl max) )) && notify-send \"Brightness\" -h int:value:\$perc -h string:synchronous:brightness -u low'"
|
||||
];
|
||||
general = {
|
||||
gaps_in = 0;
|
||||
gaps_in = 5;
|
||||
gaps_out = 10;
|
||||
};
|
||||
dwindle = {
|
||||
preserve_split = true;
|
||||
};
|
||||
decoration = {
|
||||
rounding = 0;
|
||||
rounding = 10;
|
||||
blur = {
|
||||
enabled = false;
|
||||
};
|
||||
@@ -252,16 +279,21 @@
|
||||
"XCURSOR_SIZE,24"
|
||||
"LIBVA_DRIVER_NAME,nvidia"
|
||||
"__GLX_VENDOR_LIBRARY_NAME,nvidia"
|
||||
# "GTK_IM_MODULE,fcitx"
|
||||
# "QT_IM_MODULE,fcitx"
|
||||
"XMODIFIERS,@im=fcitx"
|
||||
];
|
||||
exec-once = [
|
||||
"status-bar"
|
||||
# "status-bar"
|
||||
"qs"
|
||||
"wl-clip-persist --clipboard regular"
|
||||
"fcitx5 -d"
|
||||
];
|
||||
monitor = [
|
||||
"eDP-1, 1920x1080, 0x0, 1"
|
||||
];
|
||||
input = {
|
||||
kb_layout = "us";
|
||||
kb_layout = "us,jp";
|
||||
touchpad = {
|
||||
natural_scroll = true;
|
||||
};
|
||||
|
||||
@@ -17,13 +17,6 @@
|
||||
enable = true;
|
||||
dns = "systemd-resolved";
|
||||
};
|
||||
# networking.extraHosts = ''
|
||||
# 75.157.238.86 traefik.lucalise.ca
|
||||
# 75.157.238.86 media.lucalise.ca
|
||||
# 75.157.238.86 git.lucalise.ca
|
||||
# 75.157.238.86 storage.lucalise.ca
|
||||
# 75.157.238.86 home-assistant.lucalise.ca
|
||||
# '';
|
||||
|
||||
services.resolved = {
|
||||
enable = true;
|
||||
@@ -32,13 +25,12 @@
|
||||
"1.0.0.1"
|
||||
];
|
||||
domains = [
|
||||
"consul"
|
||||
"service.consul"
|
||||
"node.consul"
|
||||
"~."
|
||||
];
|
||||
extraConfig = ''
|
||||
[Resolve]
|
||||
DNS=192.168.20.5:8600
|
||||
DNS=192.168.27.13:53 1.1.1.1 1.0.0.1
|
||||
ResolveUnicastSingleLabel=yes
|
||||
'';
|
||||
};
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user