This commit is contained in:
2025-08-26 17:32:31 -05:00
parent ad65e6f674
commit f1c4accf52
11 changed files with 365 additions and 96 deletions

31
iac/src/js/index.js Normal file
View File

@@ -0,0 +1,31 @@
const k8s = require("@pulumi/kubernetes");
const core = require("./core");
const vault = require("./k8/openbao/openbao");
const nextcloud = require("./k8/nextcloud/nextcloud");
const hetznercsi = require('./k8/csi-drivers/hetzner');
async function main() {
const cluster = core.createCluster();
const appOutputs = cluster.kubeconfig.apply(async (kc) => {
const provider = new k8s.Provider("k8s-dynamic-provider", {
kubeconfig: kc,
});
hetznercsi.deployCsiDriver(provider);
vault.deployVault(provider);
const app = await nextcloud.deployNextcloudApp(kc, provider);
return {
nextcloudUrl: app.nextcloudUrl,
};
});
return {
masterIp: cluster.masterIp,
kubeconfig: cluster.kubeconfig,
nextcloudUrl: appOutputs.nextcloudUrl,
};
}
module.exports = main();

View File

@@ -10,7 +10,7 @@ const yaml = require("js-yaml");
* @param {string} kubeconfig - The kubeconfig content for the cluster.
* @param {k8s.Provider} provider - The Kubernetes provider to deploy resources with.
*/
exports.deployNextcloudApp = async function(kubeconfig, provider) {
exports.deployNextcloudApp = async function(provider) {
const vaultConfig = new pulumi.Config("vault");
const vaultAddress = vaultConfig.require("address");

44
iac/src/main/core.cljs Normal file
View File

@@ -0,0 +1,44 @@
(ns core
(:require
["@pulumi/kubernetes" :as k8s]
[clojure.core.async :refer [go <!]]
[clojure.core.async.interop :refer [<p!]]
[infra.init :as init]
[k8s.csi-driver.hetzner :as hetznercsi]
[k8s.services.openbao.openbao :as vault]
[k8s.services.nextcloud.nextcloud :as nextcloud]
))
(defn app-deployments [provider]
(let [
nextcloud-result (nextcloud/deploy-nextcloud-app provider)
vault-result (vault/deploy-vault provider)
]
{
:nextcloud nextcloud-result
:vault vault-result
}
))
(defn main! []
(let [cluster (init/create-cluster)
app-outputs (.apply (get cluster :kubeconfig)
(fn [kc]
(js/Promise.
(fn [resolve _reject]
(let [provider (k8s/Provider. "k8s-dynamic-provider" #js {:kubeconfig kc})]
(hetznercsi/deploy-csi-driver provider)
(resolve (app-deployments provider)))))))]
(set! (.-exports js/module)
#js {
:kubeconfig (get cluster :kubeconfig)
:masterIp (get cluster :masterIp)
:nextcloudUrl (.apply app-outputs #(get app-outputs :nextcloudUrl))})
#_(set! (.-exports js/module)
#js {:nextcloudUrl (.apply app-outputs (fn [outputs] (.-nextcloudUrl outputs)))})
))

View File

@@ -1,93 +0,0 @@
(ns infra.core
(:require ["@pulumi/pulumi" :as pulumi]
["@pulumi/hcloud" :as hcloud]
["@pulumi/command/remote" :as command]
["@pulumi/kubernetes" :as k8s]))
(def config (pulumi/Config.))
(def ssh-key-name (.require config "sshKeyName"))
(def private-key (.requireSecret config "privateKeySsh"))
(defn install-master-script [public-ip]
(str "if ! command -v k3s >/dev/null; then\n"
" curl -sfL https://get.k3s.io | INSTALL_K3S_EXEC=\"--flannel-backend=wireguard-native --node-external-ip="
public-ip
"\" sh -\n"
"fi"))
(defn install-worker-script [master-ip token]
(pulumi/interpolate
(str "if ! command -v k3s >/dev/null; then\n"
" curl -sfL https://get.k3s.io | K3S_URL=https://"
master-ip
":6443 K3S_TOKEN=\"" token "\" sh -\n"
"fi")))
(defn hcloud-server [name server-type location ssh-key & {:keys [user-data]}]
(hcloud/Server. name
#js {:serverType server-type
:image "ubuntu-22.04"
:location location
:sshKeys #js [ssh-key]
:userData user-data}))
(defn ssh-connection [ip]
#js {:host ip
:user "root"
:privateKey private-key})
(defn main! []
(let [
master (hcloud-server "k3s-master-de" "cx22" "fsn1" ssh-key-name)
master-conn (.apply (.-ipv4Address master) ssh-connection)
install-master (command/Command. "install-master"
#js {:connection master-conn
:create (.apply (.-ipv4Address master)
install-master-script)})
token-cmd (-> (.-stdout install-master)
(.apply (fn [_]
(command/Command. "get-token"
#js {:connection master-conn
:create "cat /var/lib/rancher/k3s/server/node-token"}))))
token-stdout (-> token-cmd (.apply (fn [cmd] (.-stdout cmd))))
worker-script (install-worker-script (.-ipv4Address master) token-stdout)
worker-de (hcloud-server "k3s-worker-de" "cx22" "fsn1" ssh-key-name :user-data worker-script)
worker-us (hcloud-server "k3s-worker-us" "cpx11" "ash" ssh-key-name :user-data worker-script)
kubeconfig-cmd (-> (pulumi/all #js [(.-stdout install-master) (.-ipv4Address master)])
(.apply (fn [[_ master-ip]]
(command/Command. "get-kubeconfig"
#js {:connection master-conn
:create (str "sleep 10 &&" "sed 's/127.0.0.1/" master-ip "/' /etc/rancher/k3s/k3s.yaml")}))))
kubeconfig-stdout (-> kubeconfig-cmd (.apply (fn [cmd] (.-stdout cmd))))
all-workers-ready (pulumi/all #js [(.-urn worker-de) (.-urn worker-us)])
ready-kubeconfig (pulumi/all #js [kubeconfig-stdout all-workers-ready]
(fn [[kc _]] kc))
k8s-provider (k8s/Provider. "k8s-provider"
#js {:kubeconfig ready-kubeconfig})]
(-> (pulumi/all #js [(.-ipv4Address master)
(.-ipv4Address worker-de)
(.-ipv4Address worker-us)
kubeconfig-stdout])
(.apply (fn [[master-ip worker-de-ip worker-us-ip kc]]
(js-obj
"masterIp" master-ip
"workerDeIp" worker-de-ip
"workerUsIp" worker-us-ip
"kubeconfig" (pulumi/secret kc)))))))
(set! (.-main js/module) main!)

View File

@@ -0,0 +1,145 @@
(ns infra.init
(:require ["@pulumi/pulumi" :as pulumi]
["@pulumi/hcloud" :as hcloud]
["@pulumi/command/remote" :as remote]
["@pulumi/command/local" :as local]
["@pulumi/kubernetes" :as k8s]
["fs" :as fs]))
(defn- install-master-script [public-ip]
(str "# Create manifests dir\n"
"mkdir -p /var/lib/rancher/k3s/server/manifests\n\n"
"# Traefik NodePort config\n"
"cat <<EOF > /var/lib/rancher/k3s/server/manifests/traefik-config.yaml\n"
"apiVersion: helm.cattle.io/v1\n"
"kind: HelmChartConfig\n"
"metadata:\n"
" name: traefik\n"
" namespace: kube-system\n"
"spec:\n"
" valuesContent: |-\n"
" service:\n"
" spec:\n"
" type: NodePort\n"
" ports:\n"
" web:\n"
" nodePort: 30080\n"
" websecure:\n"
" nodePort: 30443\n"
"EOF\n\n"
"# Install k3s if not present\n"
"if ! command -v k3s >/dev/null; then\n"
" curl -sfL https://get.k3s.io | INSTALL_K3S_EXEC=\"--flannel-backend=wireguard-native --node-external-ip=" public-ip "\" sh -\n"
"fi\n\n"
"# Wait for node readiness\n"
"until sudo k3s kubectl get node >/dev/null 2>&1; do\n"
" echo 'Waiting for master node...'\n"
" sleep 5\n"
"done\n"))
(defn- install-worker-script [master-ip token]
(str "#!/bin/bash\n"
"exec > /root/k3s-install.log 2>&1\n"
"set -x\n"
"echo '--- Starting worker install ---'\n\n"
"until ping -c1 " master-ip "; do\n"
" echo 'Waiting for master...'\n"
" sleep 2\n"
"done\n\n"
"WORKER_PUBLIC_IP=$(curl -s https://ifconfig.me/ip)\n"
"echo \"Public IP: $WORKER_PUBLIC_IP\"\n\n"
"if ! command -v k3s >/dev/null; then\n"
" curl -sfL https://get.k3s.io | "
"K3S_URL=https://" master-ip ":6443 "
"K3S_TOKEN=\"" token "\" "
"INSTALL_K3S_EXEC=\"--node-external-ip=$WORKER_PUBLIC_IP\" sh -\n"
"fi\n\n"
"echo '--- Finished worker install ---'\n"))
(defn create-cluster []
(let [cfg (pulumi/Config.)
ssh-key (.require cfg "sshKeyName")
priv-key (.requireSecret cfg "privateKeySsh")
firewall (hcloud/Firewall.
"k3s-firewall"
(clj->js {:rules [{:direction "in" :protocol "tcp" :port "22" :sourceIps ["0.0.0.0/0" "::/0"]}
{:direction "in" :protocol "tcp" :port "6443" :sourceIps ["0.0.0.0/0" "::/0"]}
{:direction "in" :protocol "udp" :port "51820" :sourceIps ["0.0.0.0/0" "::/0"]}
{:direction "in" :protocol "icmp" :sourceIps ["0.0.0.0/0" "::/0"]}]}))
master (hcloud/Server.
"k3s-master-de"
(clj->js {:serverType "cx22"
:image "ubuntu-22.04"
:location "fsn1"
:sshKeys [ssh-key]
:firewallIds [(.-id firewall)]}))
master-ip (.-ipv4Address master)
master-conn (clj->js {:host master-ip
:user "root"
:privateKey priv-key})
install-master
(remote/Command.
"install-master"
(clj->js {:connection master-conn
:create (.apply master-ip install-master-script)})
(clj->js {:dependsOn [master]}))
token-cmd
(remote/Command.
"get-token"
(clj->js {:connection master-conn
:create "sudo cat /var/lib/rancher/k3s/server/node-token"})
(clj->js {:dependsOn [install-master]}))
worker-script
(.apply (pulumi/all [master-ip (.-stdout token-cmd)])
(fn [[ip token]] (install-worker-script ip (.trim token))))
worker-de (hcloud/Server.
"k3s-worker-de"
(clj->js {:serverType "cx22"
:image "ubuntu-22.04"
:location "fsn1"
:sshKeys [ssh-key]
:userData worker-script
:firewallIds [(.-id firewall)]}))
worker-us (hcloud/Server.
"k3s-worker-us"
(clj->js {:serverType "cpx11"
:image "ubuntu-22.04"
:location "ash"
:sshKeys [ssh-key]
:userData worker-script
:firewallIds [(.-id firewall)]}))
kubeconfig-cmd
(remote/Command.
"get-kubeconfig"
(clj->js {:connection master-conn
:create (.apply master-ip
(fn [ip]
(str "sudo sed 's/127.0.0.1/" ip "/' /etc/rancher/k3s/k3s.yaml")))})
(clj->js {:dependsOn [install-master]}))
label-node
(local/Command.
"label-german-node"
(clj->js {:create (.apply (pulumi/all [(.-stdout kubeconfig-cmd) (.-name worker-de)])
(fn [[kubeconfig worker-name]]
(let [path "./kubeconfig.yaml"]
(.writeFileSync fs path kubeconfig)
(str "kubectl --kubeconfig=" path
" label node " worker-name
" location=de --overwrite"))))})
(clj->js {:dependsOn [kubeconfig-cmd]}))]
{:masterIp master-ip
:workerDeIp (.-ipv4Address worker-de)
:workerUsIp (.-ipv4Address worker-us)
:kubeconfig (pulumi/secret (.-stdout kubeconfig-cmd))}))

View File

@@ -0,0 +1,32 @@
(ns k8s.csi-driver.hetzner
(:require ["@pulumi/pulumi" :as pulumi]
["@pulumi/kubernetes" :as k8s]))
(defn deploy-csi-driver [provider]
(let [hcloud-config (pulumi/Config. "hcloud")
hcloud-token (.requireSecret hcloud-config "token")
core-v1 (.. k8s -core -v1)
helm-v3 (.. k8s -helm -v3)
csi-secret (core-v1.Secret.
"hcloud-csi-secret"
(clj->js {:metadata {:name "hcloud"
:namespace "kube-system"}
:stringData {:token hcloud-token}})
#js {:provider provider})
secret-name (-> csi-secret .-metadata .-name)
csi-chart (helm-v3.Chart.
"hcloud-csi"
(clj->js {:chart "hcloud-csi"
:fetchOpts {:repo "https://charts.hetzner.cloud"}
:namespace "kube-system"
:values {:controller
{:secret {:enabled false}
:existingSecret {:name secret-name}}
:node
{:existingSecret {:name secret-name}}}})
(clj->js {:provider provider
:dependsOn [csi-secret]}))]
csi-chart))

View File

@@ -0,0 +1,74 @@
(ns k8s.services.nextcloud.nextcloud
(:require
["@pulumi/kubernetes" :as k8s]
["@pulumi/pulumi" :as pulumi]
["@pulumi/vault" :as vault]
["fs" :as fs]
["js-yaml" :as yaml]
["path" :as path]
[clojure.core.async :refer [go]]))
(defn- get-secret-val
"Extract a specific key from a Vault secret Output/Promise."
[secret-promise key]
(.then secret-promise #(aget (.-data %) key)))
(defn deploy-nextcloud-app
"Deploy Nextcloud using Vaultmanaged secrets and a Helm chart."
[provider]
(let [core-v1 (.. k8s -core -v1)
helm-v3 (.. k8s -helm -v3)
vault-cfg (pulumi/Config. "vault")
vault-provider (vault/Provider.
"vault-provider"
(clj->js {:address (.require vault-cfg "address")
:token (.requireSecret vault-cfg "token")}))
nextcloud-secrets (.getSecret (.-generic vault)
(clj->js {:path "secret/nextcloud"})
(clj->js {:provider vault-provider}))
ns ((.. core-v1 -Namespace)
"nextcloud-ns"
(clj->js {:metadata {:name "nextcloud"}})
(clj->js {:provider provider}))
admin-secret ((.. core-v1 -Secret)
"nextcloud-admin-secret"
(clj->js {:metadata {:name "nextcloud-admin-secret"
:namespace (.. ns -metadata -name)}
:stringData {:password (get-secret-val nextcloud-secrets "adminPassword")}})
(clj->js {:provider provider}))
db-secret ((.. core-v1 -Secret)
"nextcloud-db-secret"
(clj->js {:metadata {:name "nextcloud-db-secret"
:namespace (.. ns -metadata -name)}
:stringData {"mariadb-root-password" (get-secret-val nextcloud-secrets "dbPassword")
"mariadb-password" (get-secret-val nextcloud-secrets "dbPassword")}})
(clj->js {:provider provider}))
values-path (.join path js/__dirname "values.yaml")
helm-values (-> values-path
(fs/readFileSync "utf8")
(yaml/load))
_ (aset (aget (aget (aget helm-values "ingress") "hosts") 0)
"host"
(get-secret-val nextcloud-secrets "host"))
chart ((.. helm-v3 -Chart)
"my-nextcloud"
(clj->js {:chart "nextcloud"
:fetchOpts {:repo "https://nextcloud.github.io/helm/"}
:namespace (.. ns -metadata -name)
:values helm-values})
(clj->js {:provider provider
:dependsOn [admin-secret db-secret]}))]
{:namespace ns
:admin-secret admin-secret
:db-secret db-secret
:chart chart
:nextcloud-url (.then nextcloud-secrets
#(str "https://" (aget (.-data %) "host")))}))

View File

@@ -0,0 +1,34 @@
(ns k8s.services.openbao.openbao
(:require
["@pulumi/kubernetes" :as k8s]
["fs" :as fs]
["js-yaml" :as yaml]
["path" :as path]
[clojure.core.async :refer [go]]))
(defn deploy-vault
"Deploy OpenBao via Helm chart on the given Kubernetes provider."
[provider]
(let [core-v1 (.. k8s -core -v1)
helm-v3 (.. k8s -helm -v3)
vault-ns ((.. core-v1 -Namespace)
"vault-ns"
(clj->js {:metadata {:name "vault"}})
(clj->js {:provider provider}))
values-path (.join path js/__dirname "values.yaml")
helm-values (-> values-path
(fs/readFileSync "utf8")
(yaml/load))
chart ((.. helm-v3 -Chart)
"openbao"
(clj->js {:chart "openbao"
:fetchOpts {:repo "https://openbao.github.io/openbao-helm"}
:namespace (.. vault-ns -metadata -name)
:values helm-values})
(clj->js {:provider provider
:dependsOn [vault-ns]}))]
{:namespace vault-ns
:chart chart}))