This commit is contained in:
2026-05-05 00:26:41 +02:00
commit 7587c86541
32 changed files with 33914 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
/.terraform/providers/registry.opentofu.org

102
.terraform.lock.hcl generated Normal file
View File

@@ -0,0 +1,102 @@
# This file is maintained automatically by "tofu init".
# Manual edits may be lost in future updates.
provider "registry.opentofu.org/camptocamp/pass" {
version = "2.1.1"
constraints = "2.1.1"
hashes = [
"h1:b26FFbTyOIKCeqpAjnBzLZU1S9jU7XE9U+3+NOxtAq0=",
"zh:2ec5926b48526bc23b99f26dd18138811f6b1f1f81125bce75e841020b58350c",
"zh:32448b55d615ffc4924aab08c5fdea57102cc0f2752003e81367e7d05d42ce9a",
"zh:35c2d4e4551486b3e3befcf5efcf56f94deae0e3293fe8634365e4fb8b632598",
"zh:4d7932ddc0bb0a4f429ce6213d8139c05e54ce0c8f1a60d0a42e84f0dcccf334",
"zh:4f44287a8cab55ad339a04422496f4b607ce257aaa8a8cf1e4bd459f5ac6dbbb",
"zh:6e8f181c187b5c070f6cba458eeabcefb5a8e110a414b349c2f94ccc5952ca64",
"zh:79ae6e5c34221ee6324d57bec033d336f72b96521560ca3ba017c90560e337ba",
"zh:9ff86045990bdac860d9535c2abaaa80794451910f353c8bf71bcc5a6d594fc2",
"zh:c76ed4b72ca17e24b201a2f3365bd0d0ea22f6e93621e61371f1322c1ff33806",
"zh:dc3f2410d08af24d58c3fb5357ec9a8cf74fd02b63f01ff52379b03f147c0b08",
"zh:de0c0ceb0ef2af78fe402ffa860ec1fbc08ff91b18342f17cb55100934ea2c4b",
]
}
provider "registry.opentofu.org/gavinbunney/kubectl" {
version = "1.19.0"
constraints = ">= 1.14.0"
hashes = [
"h1:9QkxPjp0x5FZFfJbE+B7hBOoads9gmdfj9aYu5N4Sfc=",
"zh:1dec8766336ac5b00b3d8f62e3fff6390f5f60699c9299920fc9861a76f00c71",
"zh:43f101b56b58d7fead6a511728b4e09f7c41dc2e3963f59cf1c146c4767c6cb7",
"zh:4c4fbaa44f60e722f25cc05ee11dfaec282893c5c0ffa27bc88c382dbfbaa35c",
"zh:51dd23238b7b677b8a1abbfcc7deec53ffa5ec79e58e3b54d6be334d3d01bc0e",
"zh:5afc2ebc75b9d708730dbabdc8f94dd559d7f2fc5a31c5101358bd8d016916ba",
"zh:6be6e72d4663776390a82a37e34f7359f726d0120df622f4a2b46619338a168e",
"zh:72642d5fcf1e3febb6e5d4ae7b592bb9ff3cb220af041dbda893588e4bf30c0c",
"zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425",
"zh:a1da03e3239867b35812ee031a1060fed6e8d8e458e2eaca48b5dd51b35f56f7",
"zh:b98b6a6728fe277fcd133bdfa7237bd733eae233f09653523f14460f608f8ba2",
"zh:bb8b071d0437f4767695c6158a3cb70df9f52e377c67019971d888b99147511f",
"zh:dc89ce4b63bfef708ec29c17e85ad0232a1794336dc54dd88c3ba0b77e764f71",
"zh:dd7dd18f1f8218c6cd19592288fde32dccc743cde05b9feeb2883f37c2ff4b4e",
"zh:ec4bd5ab3872dedb39fe528319b4bba609306e12ee90971495f109e142d66310",
"zh:f610ead42f724c82f5463e0e71fa735a11ffb6101880665d93f48b4a67b9ad82",
]
}
provider "registry.opentofu.org/hashicorp/kubernetes" {
version = "2.7.1"
constraints = "2.7.1"
hashes = [
"h1:6RosjiNWl8D02NLAemgFVus9LbbXCG9ISSlWZ/B6a+s=",
"zh:03a7020ea2360f88f67ae9a3130813a2842ea3ae9efbb2202c0e5f471ede0f60",
"zh:20fe4f0ec63f11e66c781cfe2ab4e4e1c6d6523419cc961407b9797fb46eb6e9",
"zh:2bf6709033c845a324e5f0675662fbcd44c67d20488d5ccc04b689a179d3e381",
"zh:3c4c908735c8478fb45ef476170b0c77db494c7b0c9b6b1e18d4257acf1f04fa",
"zh:71e228d78876a71eda78ece4569e9710d3a27f0520b75511cd8ec39b6469b40e",
"zh:75a8f1bcc02acb39a8dd5ca0c2f0da5577c9b87e59a6e89a9e550bc61b4451b3",
"zh:884542de0d78c76fae74c2a05bef69e9226fcc28f4d1d395f29997ee8226845c",
"zh:a49114029dcd14134d74f227e7377531f77f39de09beb0b8c96e9bc0611b42ce",
"zh:f836355067e90fc35ebdd1a9dcc1e0533aecd5f827c38a4664862f23542c2175",
]
}
provider "registry.opentofu.org/kbst/kustomization" {
version = "0.9.6"
constraints = "0.9.6"
hashes = [
"h1:8Yh5MXSaSbKDO65cm/hqHc9LpKORNITWVWHOYqIjD0g=",
"zh:13d4d5510b02dcbde4e0e94b9541832913a2ae3c24b93e477c3963357774f0d7",
"zh:1e0bae2bffc2045b7f6d50e02ef519568d71ccf883cf7829b27bd45e813c4a8a",
"zh:24daa001846ef13b76fe35832e40d103fe8e6d0bbdb7e2a9623c3b0ef5d8f18e",
"zh:350b386fa0f3963aa1c245472ddf171b686514ce31d7229f8d09bca95353de1f",
"zh:4b94999e2b3d72f5835ec2f940ef911b6221b69c7bf769f1d2f2d86fbfa04fbe",
"zh:4e53a0b2cc8f3ad3defe7565cb228b09ddd394313ecbb2c81cfc378e6249e0b7",
"zh:7fbde5cdaec981ec958e680d66f35b664470d7ef0c74e8d79ab5b104bc2de105",
"zh:92273e9aa1319ee829ba608a7734aa4e334d8ebca1692eb23ca08b1187714ced",
"zh:c3165710124f6fd7ed3cda5ccac26d926070ff61f7650843d4dffe60379b78c6",
"zh:c8fd90b353a337b0c3d0e3dcba3bbbd1cc2e4f2548198b0a051912f53f91fab9",
"zh:cebce7b15cf666d66421f6ee56577af99845778a54d4485ee0cce593aaa82cda",
"zh:d184afeccedc579dd6412ba2521af86aea117dc2ddb60b2914263db7af1f0d1e",
"zh:dbc2ce52f067d36485630afba4ee606c85361ba7e7d7f424b1da9f498c095cf1",
"zh:f397be4632e06e4766cc38607a93b4606941a1940afc8a48aa5222384886fa82",
]
}
provider "registry.opentofu.org/valodim/desec" {
version = "0.6.1"
constraints = "0.6.1"
hashes = [
"h1:cXLLxqkdJoiW+p/aDEBH6KK+laPM+RtWyYxd8IZBBN8=",
"zh:08fb06d0654d638ada1e2be948ff1070ff73314d65416a8c50775f6faa3c4730",
"zh:141baab7ef9c63dae9fa8edadfae19fcede4bde0132d576d26010dfddeac3fa3",
"zh:2f41c0ef69de1d75efb567bb54b132ce238dd0cca9f42fd71a67db44b0927d5a",
"zh:305d5137b7003701792a92242dd6010ad61db4fe69d94c9e00a1ae37332a0543",
"zh:70c86277d8929e6af2400fead92f5998f9b1dabddf7e57c85d9c5ccd29fcfe92",
"zh:79163bb3b16df4ebafc2128a2d65daf1f5d85d0d3831577e243094cb34e4fa67",
"zh:8dc0b89e311ca1c7a3c0621b265274b1590fbc0839a66677e0232a61c3ffb78e",
"zh:bc57f2f11a0f8e29387ba852474fe48f2a739d86dcd8f2aad545881e9d763912",
"zh:c0a3cab6728d52fdf5e3147c9033d838f4cd5873aae2b16a12c7027d825737f1",
"zh:dac69febd0e4b7a0aa78ab926a10f8c550f3d85fe180c255912308d2a484b3af",
"zh:ff517fd13df0a539c1e41615daea66fa56ac0e46250bf20a423a36e459cf13be",
]
}

9
https-redirect.yaml Normal file
View File

@@ -0,0 +1,9 @@
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: redirect-https
spec:
redirectScheme:
scheme: https
permanent: true

6
kustomization.yaml Normal file
View File

@@ -0,0 +1,6 @@
resources:
- ./manifests/devops/
- ./manifests/s3/
- ./manifests/jellyfin/
- ./manifests/cert-manager/
- ./manifests/argo/

19
letencrypt.yaml Normal file
View File

@@ -0,0 +1,19 @@
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-prod
spec:
acme:
email: tls@milasholsting.dk
server: https://acme-v02.api.letsencrypt.org/directory
privateKeySecretRef:
name: letsencrypt-prod
solvers:
- dns01:
webhook:
groupName: acme.hetzner.com
solverName: hetzner
config:
tokenSecretKeyRef:
name: hetzner-secret
key: api-token

11
main.tf Normal file
View File

@@ -0,0 +1,11 @@
data "kustomization_build" "main" {
path = "."
}
resource "kubectl_manifest" "main" {
for_each = data.kustomization_build.main.manifests
yaml_body = each.value
server_side_apply = true
force_conflicts = true
}

View File

@@ -0,0 +1,10 @@
# https://kubernetes.io/docs/concepts/configuration/configmap/
apiVersion: v1
kind: ConfigMap
metadata:
name: argocd-cmd-params-cm
namespace: argocd
data:
server.insecure: true
---

View File

12
manifests/argo/cert.yaml Normal file
View File

@@ -0,0 +1,12 @@
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: argo-tls
namespace: argocd
spec:
secretName: argo-tls
issuerRef:
name: letsencrypt-prod
kind: ClusterIssuer
dnsNames:
- argo.milasholsting.dk

View File

@@ -0,0 +1,27 @@
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: argocd-server
namespace: argocd
spec:
entryPoints:
- websecure
routes:
- kind: Rule
match: Host(`argo.milasholsting.dk`)
priority: 10
services:
- name: argocd-server
port: 80
- kind: Rule
match: Host(`argo.milasholsting.dk`) && Header(`Content-Type`, `application/grpc`)
priority: 11
services:
- name: argocd-server
port: 80
scheme: h2c
tls:
secretName: argo-tls
domains:
- main: argo.milasholsting.dk

33375
manifests/argo/install.yaml Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,8 @@
namespace: argocd
resources:
- ./install.yaml
- ./namespace.yaml
- ./ingress.yaml
- ./cert.yaml
- ./argo-cmd-params.yaml

View File

@@ -0,0 +1,4 @@
apiVersion: v1
kind: Namespace
metadata:
name: argocd

View File

@@ -0,0 +1,12 @@
apiVersion: helm.cattle.io/v1
kind: HelmChart
metadata:
name: hetzer-cert-manager
namespace: cert-manager
spec:
repo: https://charts.hetzner.cloud
chart: cert-manager-webhook-hetzner
targetNamespace: cert-manager
version: 0.7.0
valuesContent: |-

View File

@@ -0,0 +1,4 @@
resources:
- ./hetzner-cert-manager.yaml
- ./secret.yaml
- ./rbac.yaml

View File

@@ -0,0 +1,24 @@
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: cert-manager-webhook-hetzner:solver
rules:
- apiGroups:
- acme.hetzner.com
resources:
- '*'
verbs:
- 'create'
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: cert-manager-webhook-hetzner:solver
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: cert-manager-webhook-hetzner:solver
subjects:
- name: cert-manager
namespace: cert-manager
kind: ServiceAccount

View File

@@ -0,0 +1,8 @@
apiVersion: v1
kind: Secret
metadata:
name: hetzner-secret
namespace: cert-manager
type: Opaque
stringData:
api-token: "Sv4A7eb7nIqsX92vyN78czp4tzqlNP7T8EiocupF5oxY7K6ZMMGGFrgTKmJPs77C"

View File

@@ -0,0 +1,14 @@
# https://kubernetes.io/docs/concepts/configuration/secret/
apiVersion: v1
kind: Secret
metadata:
name: gitea-admin
namespace: devops
type: Opaque
stringData:
username: admin
password: SuperSecertPassword1234
# Example:
# password: {{ .Values.password | b64enc }}
---

View File

@@ -0,0 +1,50 @@
apiVersion: helm.cattle.io/v1
kind: HelmChart
metadata:
name: gitea
namespace: devops
spec:
repo: https://dl.gitea.io/charts/
chart: gitea
targetNamespace: devops
version: 12.5.3
valuesContent: |-
image:
registry: docker.io
repository: commitgo/gitea-ee
tag: 25.4.3
rootless: true
pullPolicy: IfNotPresent
service:
http:
type: ClusterIP
ssh:
type: ClusterIP
ingress:
enabled: true
annotations:
kubernetes.io/ingress.class: traefik
hosts:
- host: gitea.milasholsting.dk
paths:
- path: /
pathType: Prefix
tls:
- secretName: gitea-tls
hosts:
- gitea.milasholsting.dk
gitea:
admin:
existingSecret: gitea-admin
config:
server:
DOMAIN: gitea.milasholsting.dk
ROOT_URL: https://gitea.milasholsting.dk/
persistence:
size: 20Gi
storageClass: local-path

View File

@@ -0,0 +1,46 @@
apiVersion: helm.cattle.io/v1
kind: HelmChartConfig
metadata:
name: gitea
namespace: devops
spec:
valuesContent: |-
image:
registry: docker.io
repository: commitgo/gitea-ee
tag: 25.4.3
rootless: true
pullPolicy: IfNotPresent
service:
http:
type: ClusterIP
ssh:
type: ClusterIP
ingress:
enabled: true
annotations:
kubernetes.io/ingress.class: traefik
hosts:
- host: gitea.milasholsting.dk
paths:
- path: /
pathType: Prefix
tls:
- secretName: gitea-tls
hosts:
- gitea.milasholsting.dk
gitea:
admin:
existingSecret: gitea-admin
config:
server:
DOMAIN: gitea.milasholsting.dk
ROOT_URL: https://gitea.milasholsting.dk/
persistence:
size: 20Gi
storageClass: local-path

View File

@@ -0,0 +1,6 @@
namespace: devops
resources:
- ./namespace.yaml
- ./gitea-admin-secret.yaml
- ./gitea.yaml

View File

@@ -0,0 +1,4 @@
apiVersion: v1
kind: Namespace
metadata:
name: devops

13
manifests/s3/cert.yaml Normal file
View File

@@ -0,0 +1,13 @@
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: rustfs-cert
namespace: s3storage # Ensure this matches your app's namespace
spec:
secretName: rustfs-tls-cert
issuerRef:
name: letsencrypt-prod # This must match your ClusterIssuer name
kind: ClusterIssuer
dnsNames:
- s3.milasholsting.dk
- console.s3.milasholsting.dk

View File

@@ -0,0 +1,8 @@
namespace: s3storage
resources:
- ./rustfs.yaml
- ./namespace.yaml
- ./rustfs-ingress.yaml
- ./rustfsChartConfig.yaml
# - ./cert.yaml

View File

@@ -0,0 +1,4 @@
apiVersion: v1
kind: Namespace
metadata:
name: s3storage

View File

@@ -0,0 +1,26 @@
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: rustfs-ingress
namespace: s3storage
spec:
entryPoints:
- websecure
routes:
- match: Host(`s3.milasholsting.dk`)
kind: Rule
services:
- name: rustfs-svc
port: 9000
sticky:
cookie:
httpOnly: true
name: s3-routing
secure: true
- match: Host(`console.s3.milasholsting.dk`)
kind: Rule
services:
- name: rustfs-svc
port: 9001
tls:
secretName: rustfs-tls-cert

32
manifests/s3/rustfs.yaml Normal file
View File

@@ -0,0 +1,32 @@
apiVersion: helm.cattle.io/v1
kind: HelmChart
metadata:
name: rustfs
namespace: s3storage
spec:
repo: https://charts.rustfs.com
chart: rustfs
targetNamespace: s3storage
version: 0.0.82
valuesContent: |-
# Standalone mode: single pod with single PVC
mode:
standalone:
enabled: true
distributed:
enabled: false
# Optional: adjust storage sizes (default 256Mi each)
storageclass:
name: local-path
dataStorageSize: 15Gi
logStorageSize: 1Gi
ingress:
enabled: false
# Optional: change default credentials
secret:
rustfs:
access_key: rustfsadmin
secret_key: f82g6toxn5xlwac6cd8bjwfl

View File

@@ -0,0 +1,28 @@
apiVersion: helm.cattle.io/v1
kind: HelmChartConfig
metadata:
name: rustfs
namespace: s3storage
spec:
valuesContent: |-
# Standalone mode: single pod with single PVC
mode:
standalone:
enabled: true
distributed:
enabled: false
# Optional: adjust storage sizes (default 256Mi each)
storageclass:
name: local-path
dataStorageSize: 15Gi
logStorageSize: 1Gi
ingress:
enabled: false
# Optional: change default credentials
secret:
rustfs:
access_key: rustfsadmin
secret_key: f82g6toxn5xlwac6cd8bjwfl

41
provider.tf Normal file
View File

@@ -0,0 +1,41 @@
terraform {
required_providers {
kustomization = {
source = "kbst/kustomization"
version = "0.9.6"
}
kubernetes = {
source = "hashicorp/kubernetes"
version = "2.7.1"
}
kubectl = {
source = "gavinbunney/kubectl"
version = ">= 1.14.0"
}
desec = {
source = "Valodim/desec"
version = "0.6.1"
}
pass = {
source = "camptocamp/pass"
version = "2.1.1"
}
}
}
provider "pass" {}
provider "kubectl" {
config_path = "~/.kube/config"
config_context = "mecha"
}
provider "kubernetes" {
config_path = "~/.kube/config"
config_context = "mecha"
}
provider "kustomization" {
context = "mecha"
kubeconfig_path = "~/.kube/config"
}

8
scripts.ts Normal file
View File

@@ -0,0 +1,8 @@
// check-headers.ts
const domain = "https://jellyfin.milasholsting.dk";
const resp = await fetch(domain);
console.log("--- Header Check ---");
resp.headers.forEach((value, key) => {
console.log(`${key}: ${value}`);
});

1
terraform.tfstate Normal file

File diff suppressed because one or more lines are too long

1
terraform.tfstate.backup Normal file

File diff suppressed because one or more lines are too long