From f1c4accf52bc5b5074008f69034e9465ff4beaf8 Mon Sep 17 00:00:00 2001 From: GigiaJ Date: Tue, 26 Aug 2025 17:32:31 -0500 Subject: [PATCH] dump --- iac/README.md | 2 + iac/package.json | 1 + iac/shadow-cljs.edn | 3 +- iac/src/js/index.js | 31 ++++ iac/src/js/k8/nextcloud/nextcloud.js | 2 +- iac/src/main/core.cljs | 44 ++++++ iac/src/main/infra/core.cljs | 93 ----------- iac/src/main/infra/init.cljs | 145 ++++++++++++++++++ iac/src/main/k8s/csi_driver/hetzner.cljs | 32 ++++ .../k8s/services/nextcloud/nextcloud.cljs | 74 +++++++++ .../main/k8s/services/openbao/openbao.cljs | 34 ++++ 11 files changed, 365 insertions(+), 96 deletions(-) create mode 100644 iac/src/js/index.js create mode 100644 iac/src/main/core.cljs delete mode 100644 iac/src/main/infra/core.cljs create mode 100644 iac/src/main/infra/init.cljs create mode 100644 iac/src/main/k8s/csi_driver/hetzner.cljs create mode 100644 iac/src/main/k8s/services/nextcloud/nextcloud.cljs create mode 100644 iac/src/main/k8s/services/openbao/openbao.cljs diff --git a/iac/README.md b/iac/README.md index 40c0b90..0c70f38 100644 --- a/iac/README.md +++ b/iac/README.md @@ -9,6 +9,8 @@ I'll try to include any pertinent documentation here in the tooling I use or the #### Upcoming Initially we'll try to migrate our services from a docker compose and into a reproducible and controlled deployment scheme here. I'll also likely break this into its own repo and instead reference it as a submodule in our dotfiles (because it makes far more sense that way). +Since hcloud keeps (seriously, several times) making me wait for verification I've opted to go ahead and rewrite it into Clojurescript. + #### Goals The long term goal is for this to be a mostly uninteractive, to completion set up of my cloud services. Since it'll be IaC should I ever choose down the road to migrate certain ones to local nodes I run then that effort should also be more or less feasible. diff --git a/iac/package.json b/iac/package.json index cfcaa16..5ad2318 100644 --- a/iac/package.json +++ b/iac/package.json @@ -2,6 +2,7 @@ "name": "vultr-k8s", "main": "index.js", "scripts": { + "test": "npx shadow-cljs watch app", "build": "shadow-cljs release app", "pulumi": "pulumi up", "deploy": "npm run build && npm run pulumi" diff --git a/iac/shadow-cljs.edn b/iac/shadow-cljs.edn index 1f7bebe..1357c18 100644 --- a/iac/shadow-cljs.edn +++ b/iac/shadow-cljs.edn @@ -1,7 +1,6 @@ -;; shadow-cljs.edn {:source-paths ["src/main"] :dependencies [] :builds {:app {:target :node-script :output-to "index.js" - :main infra.core/main!}}} \ No newline at end of file + :main core/main!}}} \ No newline at end of file diff --git a/iac/src/js/index.js b/iac/src/js/index.js new file mode 100644 index 0000000..9da64a8 --- /dev/null +++ b/iac/src/js/index.js @@ -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(); diff --git a/iac/src/js/k8/nextcloud/nextcloud.js b/iac/src/js/k8/nextcloud/nextcloud.js index 9f0039c..ff76b3e 100644 --- a/iac/src/js/k8/nextcloud/nextcloud.js +++ b/iac/src/js/k8/nextcloud/nextcloud.js @@ -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"); diff --git a/iac/src/main/core.cljs b/iac/src/main/core.cljs new file mode 100644 index 0000000..375faec --- /dev/null +++ b/iac/src/main/core.cljs @@ -0,0 +1,44 @@ +(ns core + (:require + ["@pulumi/kubernetes" :as k8s] + [clojure.core.async :refer [go /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!) diff --git a/iac/src/main/infra/init.cljs b/iac/src/main/infra/init.cljs new file mode 100644 index 0000000..a0caf64 --- /dev/null +++ b/iac/src/main/infra/init.cljs @@ -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 < /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))})) diff --git a/iac/src/main/k8s/csi_driver/hetzner.cljs b/iac/src/main/k8s/csi_driver/hetzner.cljs new file mode 100644 index 0000000..bf4506f --- /dev/null +++ b/iac/src/main/k8s/csi_driver/hetzner.cljs @@ -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)) diff --git a/iac/src/main/k8s/services/nextcloud/nextcloud.cljs b/iac/src/main/k8s/services/nextcloud/nextcloud.cljs new file mode 100644 index 0000000..3d20c4b --- /dev/null +++ b/iac/src/main/k8s/services/nextcloud/nextcloud.cljs @@ -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 Vault‑managed 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")))})) diff --git a/iac/src/main/k8s/services/openbao/openbao.cljs b/iac/src/main/k8s/services/openbao/openbao.cljs new file mode 100644 index 0000000..a882b17 --- /dev/null +++ b/iac/src/main/k8s/services/openbao/openbao.cljs @@ -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}))