Move all files to root

This commit is contained in:
2025-11-23 16:12:26 -06:00
parent c3e5976368
commit dfc621a9ff
61 changed files with 1 additions and 0 deletions

66
src/main/base.cljs Normal file
View File

@@ -0,0 +1,66 @@
(ns base
(:require
["@pulumi/pulumi" :as pulumi]
["@pulumi/vault" :as vault]
["@pulumiverse/harbor" :as harbor]
["@pulumi/kubernetes" :as k8s]
[utils.general :as general]
[utils.providers :refer [provider-apply]]
[infra.init :as infra]
[service-registries :refer [base-resources-definition initialize-resources-definition shared-resources-definition preparation-resources-definition deployment-resources-definition]]
)
(:require-macros [utils.general :refer [p->]]))
(defn extended-exports [init]
(let [;;exports (base.build-exports init)
app-outputs (get init :setup)]
#_(assoc exports :nextcloudUrl (.apply app-outputs #(get-in % [:nextcloud :nextcloud-url])))))
(defn mod-apps [pulumi-cfg resource-configs]
"Scans the registry, builds all needed providers, and calls deploy."
(provider-apply resource-configs pulumi-cfg))
(defn mod-init [configs]
(let [pulumi-cfg (pulumi/Config.)]
(mod-apps pulumi-cfg configs)))
(defn mod-quick-deploy [configs exports]
(->
(mod-init configs)
(exports)
(clj->js)))
(defn quick-deploy-base []
(base/mod-quick-deploy
base-resources-definition
(fn [init]
(let [kcfg (p-> init .-cluster "generic:execute" .-kubeconfig)]
#js {:kubeconfig kcfg}))))
(defn quick-deploy-init []
(base/mod-quick-deploy
initialize-resources-definition
(fn [init]
(let [vaultToken (p-> init .-openbao "generic:execute" "root-token")
vaultAddress (p-> init .-openbao "generic:execute" .-address)]
#js {:vaultAddress vaultAddress
:vaultToken vaultToken}))))
(defn quick-deploy-shared []
(base/mod-quick-deploy
shared-resources-definition
(fn [init]
(let [secrets (p-> init .-harbor "vault:prepare" "stringData")]
{:url (p-> secrets .-host #(str "https://" %))
:username (p-> secrets .-username)
:password (p-> secrets .-password)}))))
(defn quick-deploy-prepare []
(base/mod-quick-deploy preparation-resources-definition extended-exports))
(defn quick-deploy-services []
(base/mod-quick-deploy deployment-resources-definition extended-exports))

32
src/main/configs.cljs Normal file
View File

@@ -0,0 +1,32 @@
(ns configs)
(defn get-env [key default] (let [value (aget js/process.env key)]
(if (or (nil? value) (identical? value ""))
default value)))
(def cfg
{:sshKeyName (get-env "SSH_KEY_NAME" nil)
:sshPersonalKeyName (get-env "PERSONAL_KEY_NAME" nil)
:privateKeySsh (.toString (js/Buffer.from (get-env "PRIVATE_KEY" nil) "base64") "utf-8")
:hcloudToken (get-env "HCLOUD_TOKEN" nil)
:wasabiId (get-env "WASABI_ACCESS_KEY" nil)
:wasabiKey (get-env "WASABI_SECRET_KEY" nil)
:apiToken (get-env "CLOUDFLARE_TOKEN" nil)
;; Non-pulumi vals
:resource-path (get-env "RESOURCE_PATH" "resources")
:secrets-json (try
(-> (js/require "path")
(.join js/__dirname ".." "init-secrets.json")
(js/require)
(js->clj :keywordize-keys true))
(catch :default e
(throw (js/Error. (str "Failed to load init-secrets.json: " e)))))
:docker-repo (get-env "DOCKER_REPO" "")
:harbor-repo (get-env "HARBOR_REPO" "")
:public-image-registry-url (get-env "PUBLIC_IMAGE_REGISTRY_URL" "")
:public-image-registry-username (get-env "PUBLIC_IMAGE_REGISTRY_USERNAME" "")
:public-image-registry-password (get-env "PUBLIC_IMAGE_REGISTRY_PASSWORD" "")
:dns-email (get-env "DNS_EMAIL" "")})

104
src/main/core.cljs Normal file
View File

@@ -0,0 +1,104 @@
(ns core
(:require
["@pulumi/pulumi" :as pulumi]
["@pulumi/pulumi/automation" :as pulumi-auto]
["child_process" :as cp]
[promesa.core :as p]
[base :as base]
[configs :refer [cfg]]))
(def base-stack (clj->js {:projectName "hetzner-k3s"
:stackName "base"
:workDir "/home/jaggar/dotfiles/iac"
:program base/quick-deploy-base}))
(def init-stack (clj->js {:projectName "hetzner-k3s"
:stackName "init"
:workDir "/home/jaggar/dotfiles/iac"
:program base/quick-deploy-init}))
(def shared-platform-stack (clj->js {:projectName "hetzner-k3s"
:stackName "shared"
:workDir "/home/jaggar/dotfiles/iac"
:program base/quick-deploy-shared}))
(def prepare-deployment-stack (clj->js {:projectName "hetzner-k3s"
:stackName "prepare"
:workDir "/home/jaggar/dotfiles/iac"
:program base/quick-deploy-prepare}))
(def deployment-stack (clj->js {:projectName "hetzner-k3s"
:stackName "deployment"
:workDir "/home/jaggar/dotfiles/iac"
:program base/quick-deploy-services}))
(defn deploy-stack
([stack-definition configs]
(deploy-stack stack-definition configs 0))
([stack-definition configs post-delay]
(p/let
[stack (.createOrSelectStack pulumi-auto/LocalWorkspace stack-definition)
_ (p/doseq [config configs]
(.setConfig stack (:name config) (clj->js (dissoc config :name))))
_ (.up stack #js {:onOutput println})
outputs (.outputs stack)
_ (p/delay post-delay)]
outputs)))
(defn run []
(p/let [_ (println "Deploying cluster")
base-outputs (deploy-stack base-stack [{:name "hetzner-k3s:sshKeyName" :value (-> cfg :sshKeyName) :secret false}
{:name "hetzner-k3s:sshPersonalKeyName" :value (-> cfg :sshPersonalKeyName) :secret false}
{:name "hcloud:token" :value (-> cfg :hcloudToken) :secret true}
{:name "hetzner-k3s:privateKeySsh" :value (-> cfg :privateKeySsh) :secret true}])
reused-configs [{:name "kubeconfig" :value (-> base-outputs (aget "kubeconfig") (.-value)) :secret true}]
init-outputs (deploy-stack init-stack reused-configs 1000)
port-forward (cp/spawn "kubectl"
#js ["port-forward"
"svc/openbao"
"8200:8200"
"-n"
"vault"])
reused-configs (conj reused-configs {:name "vault:token" :value (-> init-outputs (aget "vaultToken") (.-value)) :secret true})
reused-configs (conj reused-configs {:name "vault:address" :value (-> init-outputs (aget "vaultAddress") (.-value)) :secret true})
shared-outputs (deploy-stack shared-platform-stack
(conj reused-configs {:name "hetzner-k3s:apiToken" :value (-> cfg :apiToken) :secret true})
1000)
prepare-outputs (deploy-stack prepare-deployment-stack reused-configs 3000)
deployment-outputs (deploy-stack deployment-stack reused-configs 2000)
_ (.kill port-forward)]
"All stacks deployed and cleaned up successfully."))
(defn main! []
(-> (run)
(p/then #(println %))
(p/catch #(println "An error occurred:" %))))
;; Checks for changes on the core and prevents deleting the app-stack needlessly.
;; Important for the Openbao vault as it is deployed here and configured on the app-stack generally
;;core-preview-result (.preview core-stack #js {:onOutput println})
;;core-change-summary (js->clj (.-changeSummary core-preview-result) :keywordize-keys true)
#_core-result #_(when (or (zero? (:delete core-change-summary 0))
(pos? (:update core-change-summary 0))
(pos? (:create core-change-summary 0)))
(.up core-stack #js {:onOutput println}))
(defn config-core [stack kubeconfig vault-token vault-address]
(p/do
;;(.setConfig stack "hetzner-k3s:sshKeyName" #js {:value (-> cfg :sshKeyName) :secret false})
;;(.setConfig stack "hetzner-k3s:sshPersonalKeyName" #js {:value (-> cfg :sshPersonalKeyName) :secret false})
;;(.setConfig stack "hetzner-k3s:privateKeySsh" #js {:value (-> cfg :privateKeySsh) :secret true})
(.setConfig stack "kubeconfig" #js {:value kubeconfig :secret true})
(.setConfig stack "vault:token" #js {:value vault-token :secret true})
;;(.setConfig stack "hcloud:token" #js {:value (-> cfg :hcloudToken) :secret true})
(.setConfig stack "vault:address" #js {:value vault-address :secret true})
;;(.setConfig stack "hetzner-k3s:apiToken" #js {:value (-> cfg :apiToken) :secret true})
))

60
src/main/infra/dns.cljs Normal file
View File

@@ -0,0 +1,60 @@
(ns infra.dns
(:require
[clojure.string :as str]
["@pulumi/cloudflare" :as cloudflare]
["@pulumi/command/local" :as local]))
(defn get-record-type
"Determines the DNS record type (A or AAAA) based on IP address format."
[ip-address]
(if (.includes ip-address ":")
"AAAA"
"A"))
(defn- get-node-ips []
(str "kubectl get nodes -o jsonpath='{range .items[*]}{.status.addresses[?(@.type==\"ExternalIP\")].address}{\"\\n\"}{end}'"))
(defn setup-dns [{:keys [pulumi-cfg secrets dependencies]}]
(let [get-node-ips (local/Command.
"get-node-ips"
(clj->js {:create (get-node-ips)
:environment {:KUBECONFIG "./kubeconfig.yaml"}}))
token (.requireSecret pulumi-cfg "apiToken")
cloudflare-provider (new cloudflare/Provider "cloudflare-provider"
(clj->js {:apiToken token}))
node-ips-output (.-stdout get-node-ips)]
(.apply node-ips-output
(fn [command-output]
(let [node-ips (-> command-output
str/split-lines
(->> (map #(first (str/split % #" ")))
(filter seq)))]
(.apply secrets
(fn [secret-data]
(let [hostname-to-zone (js->clj secret-data :keywordize-keys true)]
(vec
(for [[hostname zone-id] hostname-to-zone
[index ip] (map-indexed vector node-ips)
:when (and hostname zone-id ip)]
(new cloudflare/DnsRecord
(str "dns-" (name hostname) "-node-" index)
(clj->js {:zoneId zone-id
:name hostname
:content ip
:type (get-record-type ip)
:ttl 1
:proxied true})
(clj->js {:provider cloudflare-provider})
(clj->js {:dependsOn dependencies}))))))))))))
(def config
{:stack [:vault:prepare :generic:execute]
:app-name "dns"
:no-namespace true
:exec-fn setup-dns})

172
src/main/infra/init.cljs Normal file
View File

@@ -0,0 +1,172 @@
(ns infra.init
(:require ["@pulumi/pulumi" :as pulumi]
["@pulumi/hcloud" :as hcloud]
["@pulumi/command/remote" :as remote]
["@pulumi/command/local" :as local]
["fs" :as fs]))
(defn- setup-master-script []
(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"))
(defn- install-master-script [public-ip]
(str "# Install k3s if not present\n"
"if ! command -v k3s >/dev/null; then\n"
" curl -sfL https://get.k3s.io | INSTALL_K3S_EXEC=\"--disable=traefik --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 [{:keys [pulumi-cfg]}]
(let [ssh-key (.require pulumi-cfg "sshKeyName")
personal-key (.require pulumi-cfg "sshPersonalKeyName")
priv-key (.requireSecret pulumi-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 "tcp" :port "80" :sourceIps ["0.0.0.0/0" "::/0"]}
{:direction "in" :protocol "tcp" :port "443" :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 personal-key]
:firewallIds [(.-id firewall)]}))
master-ip (.-ipv4Address master)
master-conn (clj->js {:host master-ip
:user "root"
:privateKey priv-key})
setup-master
(remote/Command.
"setup-master"
(clj->js {:connection master-conn
:create (.apply setup-master-script)})
(clj->js {:dependsOn [master]}))
install-master
(remote/Command.
"install-master"
(clj->js {:connection master-conn
:create (.apply master-ip install-master-script)})
(clj->js {:dependsOn [setup-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 master-ip
(fn [ip]
(.apply (.-stdout token-cmd)
(fn [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 personal-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 personal-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 worker-de worker-us]}))
label-node
(local/Command.
"label-german-node-alt"
(clj->js
{:create (.apply (.-stdout kubeconfig-cmd)
(fn [kubeconfig]
(.apply (.-name worker-de)
(fn [worker-name]
(let [path "./kubeconfig.yaml"]
(.writeFileSync fs path kubeconfig)
(str
"for i in {1..30}; do "
" if kubectl --kubeconfig=" path " get node " worker-name " > /dev/null 2>&1; then "
" echo 'Node " worker-name " found, proceeding with label.' && "
" kubectl --kubeconfig=" path " label node " worker-name " location=de --overwrite && "
" exit 0; "
" else "
" echo 'Node " worker-name " not ready yet. Waiting 10s... (Attempt: '\"$i\"'/30)'; "
" sleep 10; "
" fi; "
"done; "
"echo 'Error: Timed out waiting for node " worker-name ".' >&2 && "
"exit 1;"))))))})
(clj->js {:dependsOn [kubeconfig-cmd worker-de]}))]
{:kubeconfig (pulumi/secret (.-stdout kubeconfig-cmd))}))
(def config
{:stack [:generic:execute]
:app-name "cluster"
:exec-fn create-cluster})

317
src/main/infra/openbao.cljs Normal file
View File

@@ -0,0 +1,317 @@
(ns infra.openbao
(:require
["@pulumi/pulumi" :as pulumi]
["@pulumi/command/local" :as local]))
(defn- create-wait-for-ready-script [namespace]
"Script to wait for OpenBao pod to exist, then to be running, then for the service to be operational."
(str "#!/bin/bash\n"
"set -e\n\n"
"NAMESPACE=\"" namespace "\"\n"
"MAX_RETRIES=60\n"
"RETRY_INTERVAL=10\n\n"
"## -- NEW SECTION: Wait for the pod to even exist -- ##\n"
"echo 'Waiting for OpenBao pod to be created...'\n"
"POD_FOUND=false\n"
"for i in $(seq 1 $MAX_RETRIES); do\n"
" # Check if a pod with the label exists. We redirect output to /dev/null.\n"
" if kubectl get pod -l app.kubernetes.io/instance=openbao -n \"$NAMESPACE\" -o jsonpath='{.items[0].metadata.name}' >/dev/null 2>&1; then\n"
" echo 'Pod has been created.'\n"
" POD_FOUND=true\n"
" break\n"
" fi\n"
" echo \"Attempt $i/$MAX_RETRIES: Pod not found yet, retrying in $RETRY_INTERVAL seconds...\"\n"
" sleep $RETRY_INTERVAL\n"
"done\n\n"
"if [ \"$POD_FOUND\" = false ]; then\n"
" echo 'Error: Timed out waiting for OpenBao pod to be created.' >&2\n"
" exit 1\n"
"fi\n"
"## -- END NEW SECTION -- ##\n\n"
"echo 'Waiting for OpenBao pod to enter Running state...'\n"
;; Now this command is safe to run because we know the pod exists.
"kubectl wait --for=jsonpath='{.status.phase}'=Running pod -l app.kubernetes.io/instance=openbao -n \"$NAMESPACE\" --timeout=600s\n\n"
"echo 'Pod is Running. Now waiting for OpenBao service to be fully operational...'\n"
"for i in $(seq 1 $MAX_RETRIES); do\n"
" echo \"Attempt $i/$MAX_RETRIES: Checking if OpenBao is responding...\"\n"
" \n"
" # Start a temporary port-forward to test connectivity\n"
" kubectl port-forward -n \"$NAMESPACE\" svc/openbao 8200:8200 &\n"
" PF_PID=$!\n"
" sleep 5 # Give port-forward a moment to establish\n"
" \n"
" # Test if OpenBao health endpoint responds\n"
" if curl -s --max-time 5 http://127.0.0.1:8200/v1/sys/health >/dev/null 2>&1; then\n"
" echo 'OpenBao is responding!'\n"
" kill $PF_PID 2>/dev/null || true\n"
" sleep 2 # Let port-forward cleanup\n"
" exit 0\n"
" fi\n"
" \n"
" kill $PF_PID 2>/dev/null || true\n"
" echo ' (not yet responding, will retry...)'\n"
" sleep $RETRY_INTERVAL\n"
"done\n\n"
"echo 'OpenBao failed to become ready after maximum retries'\n"
"exit 1\n"))
(defn- create-init-script [namespace]
"Robust script to initialize and unseal OpenBao with proper error handling"
(str "#!/bin/bash\n"
"set -e\n\n"
"NAMESPACE=\"" namespace "\"\n"
"BAO_ADDR='http://127.0.0.1:8200'\n"
"PID_FILE=\"/tmp/openbao-pf.pid\"\n\n"
"# Cleanup function\n"
"cleanup() {\n"
" echo 'Cleaning up...'\n"
" if [ -f \"$PID_FILE\" ]; then\n"
" PID=$(cat \"$PID_FILE\")\n"
" kill $PID 2>/dev/null || true\n"
" rm -f \"$PID_FILE\"\n"
" fi\n"
"}\n"
"trap cleanup EXIT\n\n"
"# Start port-forward in background with better error handling\n"
"echo 'Starting port-forward...'\n"
"kubectl port-forward -n \"$NAMESPACE\" svc/openbao 8200:8200 > /tmp/pf.log 2>&1 &\n"
"echo $! > \"$PID_FILE\"\n\n"
"# Wait for port-forward to be ready with timeout\n"
"echo 'Waiting for port-forward to be active...'\n"
"for i in {1..30}; do\n"
" if curl -s --max-time 2 \"$BAO_ADDR/v1/sys/health\" >/dev/null 2>&1; then\n"
" echo 'Port-forward is active!'\n"
" break\n"
" fi\n"
" if [ $i -eq 30 ]; then\n"
" echo 'Port-forward failed to become active'\n"
" echo 'Port-forward log:'\n"
" cat /tmp/pf.log || true\n"
" exit 1\n"
" fi\n"
" printf '.'\n"
" sleep 2\n"
"done\n\n"
"# Check initialization status\n"
"echo 'Checking OpenBao initialization status...'\n"
"HEALTH_RESPONSE=$(curl -s \"$BAO_ADDR/v1/sys/health\" || echo '{}')\n"
"INITIALIZED=$(echo \"$HEALTH_RESPONSE\" | jq -r '.initialized // false')\n"
"SEALED=$(echo \"$HEALTH_RESPONSE\" | jq -r '.sealed // true')\n\n"
"echo \"Current status: initialized=$INITIALIZED, sealed=$SEALED\"\n\n"
"if [ \"$INITIALIZED\" = \"false\" ]; then\n"
" echo 'Initializing OpenBao...'\n"
" \n"
" INIT_RESPONSE=$(curl -s -w '%{http_code}' -X POST \"$BAO_ADDR/v1/sys/init\" \\\n"
" -H 'Content-Type: application/json' \\\n"
" -d '{\"secret_shares\":1,\"secret_threshold\":1}')\n"
" \n"
" HTTP_CODE=${INIT_RESPONSE: -3}\n"
" INIT_DATA=${INIT_RESPONSE%???}\n"
" \n"
" if [ \"$HTTP_CODE\" != \"200\" ]; then\n"
" echo \"Failed to initialize OpenBao. HTTP code: $HTTP_CODE\"\n"
" echo \"Response: $INIT_DATA\"\n"
" exit 1\n"
" fi\n"
" \n"
" UNSEAL_KEY=$(echo \"$INIT_DATA\" | jq -r '.keys_base64[0]')\n"
" ROOT_TOKEN=$(echo \"$INIT_DATA\" | jq -r '.root_token')\n"
" \n"
" if [ \"$UNSEAL_KEY\" = \"null\" ] || [ \"$ROOT_TOKEN\" = \"null\" ]; then\n"
" echo 'Failed to extract keys from initialization response'\n"
" echo \"Response: $INIT_DATA\"\n"
" exit 1\n"
" fi\n"
" \n"
" echo 'OpenBao initialized successfully!'\n"
" \n"
" # Save credentials securely\n"
" echo \"$ROOT_TOKEN\" > /tmp/openbao-root-token\n"
" echo \"$UNSEAL_KEY\" > /tmp/openbao-unseal-key\n"
" chmod 600 /tmp/openbao-root-token /tmp/openbao-unseal-key\n"
" \n"
" echo 'Unsealing OpenBao...'\n"
" UNSEAL_RESPONSE=$(curl -s -w '%{http_code}' -X POST \"$BAO_ADDR/v1/sys/unseal\" \\\n"
" -H 'Content-Type: application/json' \\\n"
" -d \"{\\\"key\\\":\\\"$UNSEAL_KEY\\\"}\")\n"
" \n"
" UNSEAL_HTTP_CODE=${UNSEAL_RESPONSE: -3}\n"
" UNSEAL_DATA=${UNSEAL_RESPONSE%???}\n"
" \n"
" if [ \"$UNSEAL_HTTP_CODE\" != \"200\" ]; then\n"
" echo \"Failed to unseal OpenBao. HTTP code: $UNSEAL_HTTP_CODE\"\n"
" echo \"Response: $UNSEAL_DATA\"\n"
" exit 1\n"
" fi\n"
" \n"
" echo 'OpenBao unsealed successfully!'\n"
" \n"
"elif [ \"$SEALED\" = \"true\" ]; then\n"
" echo '⚠OpenBao is initialized but sealed'\n"
" \n"
" if [ -f \"/tmp/openbao-unseal-key\" ]; then\n"
" echo 'Attempting to unseal with existing key...'\n"
" UNSEAL_KEY=$(cat /tmp/openbao-unseal-key)\n"
" \n"
" curl -s -X POST \"$BAO_ADDR/v1/sys/unseal\" \\\n"
" -H 'Content-Type: application/json' \\\n"
" -d \"{\\\"key\\\":\\\"$UNSEAL_KEY\\\"}\"\n"
" \n"
" echo 'OpenBao unsealed with existing key!'\n"
" else\n"
" echo 'OpenBao is sealed but no unseal key found'\n"
" echo ' Manual intervention required'\n"
" exit 1\n"
" fi\n"
"else\n"
" echo 'OpenBao is already initialized and unsealed!'\n"
" \n"
" # Ensure we have the root token available\n"
" if [ ! -f \"/tmp/openbao-root-token\" ]; then\n"
" echo 'Root token not found locally. OpenBao is ready but you may need to provide the root token manually.'\n"
" fi\n"
"fi\n\n"
"# Final verification\n"
"echo 'Final status verification...'\n"
"FINAL_STATUS=$(curl -s \"$BAO_ADDR/v1/sys/health\")\n"
"FINAL_SEALED=$(echo \"$FINAL_STATUS\" | jq -r '.sealed')\n"
"FINAL_INITIALIZED=$(echo \"$FINAL_STATUS\" | jq -r '.initialized')\n\n"
"if [ \"$FINAL_SEALED\" = \"false\" ] && [ \"$FINAL_INITIALIZED\" = \"true\" ]; then\n"
" echo 'OpenBao is fully ready!'\n"
" echo 'Address: http://127.0.0.1:8200'\n"
" \n"
" if [ -f \"/tmp/openbao-root-token\" ]; then\n"
" echo 'Root token: Available at /tmp/openbao-root-token'\n"
" fi\n"
"else\n"
" echo 'OpenBao is not in the expected ready state'\n"
" echo \"Final status: $FINAL_STATUS\"\n"
" exit 1\n"
"fi\n"))
(defn- create-setup-secrets-script [namespace]
"Script to set up initial secrets after OpenBao is ready"
(str "#!/bin/bash\n"
"set -e\n\n"
"NAMESPACE=\"" namespace "\"\n"
"BAO_ADDR='http://127.0.0.1:8200'\n"
"PID_FILE=\"/tmp/openbao-setup-pf.pid\"\n\n"
"if [ ! -f \"/tmp/openbao-root-token\" ]; then\n"
" echo 'Root token not found. Cannot set up secrets.'\n"
" exit 1\n"
"fi\n\n"
"ROOT_TOKEN=$(cat /tmp/openbao-root-token)\n\n"
"# Cleanup function\n"
"cleanup() {\n"
" if [ -f \"$PID_FILE\" ]; then\n"
" PID=$(cat \"$PID_FILE\")\n"
" kill $PID 2>/dev/null || true\n"
" rm -f \"$PID_FILE\"\n"
" fi\n"
"}\n"
"trap cleanup EXIT\n\n"
"# Start port-forward\n"
"echo 'Starting port-forward for secrets setup...'\n"
"kubectl port-forward -n \"$NAMESPACE\" svc/openbao 8200:8200 > /tmp/setup-pf.log 2>&1 &\n"
"echo $! > \"$PID_FILE\"\n\n"
"# Wait for port-forward\n"
"for i in {1..15}; do\n"
" if curl -s --max-time 2 \"$BAO_ADDR/v1/sys/health\" >/dev/null 2>&1; then\n"
" break\n"
" fi\n"
" if [ $i -eq 15 ]; then\n"
" echo 'Port-forward failed for secrets setup'\n"
" exit 1\n"
" fi\n"
" sleep 2\n"
"done\n\n"
"echo 'Setting up OpenBao secrets...'\n\n"
"# Enable KV secrets engine (ignore error if already exists)\n"
"echo 'Enabling KV secrets engine...'\n"
"curl -s -H \"X-Vault-Token: $ROOT_TOKEN\" \\\n"
" -X POST \"$BAO_ADDR/v1/sys/mounts/secret\" \\\n"
" -d '{\"type\":\"kv-v2\"}' || echo ' (KV engine may already exist)'\n\n"
"echo 'OpenBao secrets setup complete!'\n"))
(defn execute-fn [{:keys [dependencies]}]
(let [wait-ready-command
(new local/Command
"openbao-wait-ready"
(clj->js {:create (create-wait-for-ready-script "vault")
:environment (clj->js {:KUBECONFIG "./kubeconfig.yaml"})})
(clj->js {:dependsOn dependencies}))
init-command
(new local/Command
"openbao-init"
(clj->js {:create (create-init-script "vault")
:environment (clj->js {:KUBECONFIG "./kubeconfig.yaml"})})
(clj->js {:dependsOn [wait-ready-command]}))
setup-secrets-command
(new local/Command
"openbao-setup-secrets"
(clj->js {:create (create-setup-secrets-script "vault")
:environment (clj->js {:KUBECONFIG "./kubeconfig.yaml"})})
(clj->js {:dependsOn [init-command]}))
root-token-command
(new local/Command
"get-root-token"
(clj->js {:create "cat /tmp/openbao-root-token 2>/dev/null || echo 'TOKEN_NOT_FOUND'"})
(clj->js {:dependsOn [setup-secrets-command]}))]
{:root-token (.-stdout root-token-command)
:address "http://127.0.0.1:8200"}))
(def config
{:stack [:k8s:namespace :k8s:chart :generic:execute]
:app-namespace "vault"
:app-name "openbao"
:exec-fn execute-fn
:vault-load-yaml false
:k8s:chart-opts {:fetchOpts {:repo "https://openbao.github.io/openbao-helm"}
:transformations [(fn [props opts]
(let [kind (:kind props)]
(if (= kind "StatefulSet")
{:props props
:opts (assoc opts :skipAwait true)}
{:props props
:opts opts})))]
:values {:ui {:enabled true}
:server {:standalone {:enabled true}
:ha {:enabled false}
:dataStorage {:enabled true
:size "2Gi"
:storageClass "hcloud-volumes"}
:readinessProbe {:enabled true
:path "/v1/sys/health"}
:nodeSelector {:location "de"}}}}})
(defn configure-vault-access
"Configure Pulumi config with OpenBao credentials after deployment"
[openbao-deployment]
(let [config-command
(new local/Command
"configure-pulumi-vault"
(clj->js {:create (.apply (aget openbao-deployment "root_token")
(fn [token]
(if (= token "TOKEN_NOT_FOUND")
"echo 'Warning: Root token not available for Pulumi config'"
(str "pulumi config set vault:address 'http://127.0.0.1:8200'\n"
"pulumi config set --secret vault:token '" token "'\n"
"echo 'Pulumi vault config updated successfully'"))))})
(clj->js {:dependsOn [(aget openbao-deployment "setup_secrets")]}))]
config-command))

View File

@@ -0,0 +1,18 @@
(ns k8s.add-ons.cert-manager)
(def config
{:stack [:vault:prepare [:k8s :secret :chart :cluster-issuer]]
:app-namespace "cert-manager"
:app-name "cert-manager"
:is-prod? true
:k8s:chart-opts {:fetchOpts {:repo "https://charts.jetstack.io"}
:chart "cert-manager"
:version "v1.15.0"
:namespace "cert-manager"
:values {:installCRDs true}}
:k8s:secret-opts {:metadata {:name "api-token-secret"}
:stringData {:apiToken 'token}}
:k8s:cluster-issuer-opts {:spec {:acme {:email 'email
:solvers [{:dns01 {:cloudflare {:apiTokenSecretRef {:name "api-token-secret" :key "apiToken"}}}
:selector {:dnsZones '(parse domains)}}]}}}
})

View File

@@ -0,0 +1,16 @@
(ns k8s.add-ons.csi-driver.hetzner
(:require
[configs :refer [cfg]]))
(def config
{:stack [:k8s:secret :k8s:chart]
:app-namespace "kube-system"
:app-name "hcloud-csi"
:vault-load-yaml false
:k8s:secret-opts {:metadata {:name "hcloud"
:namespace "kube-system"}
:stringData {:token (-> cfg :hcloudToken)}}
:k8s:chart-opts {:fetchOpts {:repo "https://charts.hetzner.cloud"}
:values {:controller {:enabled false
:existingSecret {:name "hcloud-csi-secret"}
:node {:existingSecret {:name "hcloud-csi-secret"}}}}}})

View File

@@ -0,0 +1,26 @@
(ns k8s.add-ons.csi-driver.wasabi
(:require [configs :refer [cfg]]))
(def config
{:stack [:k8s:secret :k8s:chart]
:app-namespace "kube-system"
:no-namespace true
:app-name "wasabi-csi"
:k8s:chart-opts {:chart "csi-s3"
:fetchOpts {:repo "https://yandex-cloud.github.io/k8s-csi-s3/charts"}
:values {:controller {:enabled false
:existingSecret {:name "wasabi-csi-secrets"}
:node {:existingSecret {:name "wasabi-csi-secrets"}}}}
#_:storageClass #_{:create true
:name "csi-s3-sc"
:singleBucket "pulumi-harbor"
:region "us-east-1"
:accessKeyID "something"
:secretAccessKey "something"
;;:bucket "pulumi-harbor"
}}
:k8s:secret-opts {:stringData {:accessKeyID (-> cfg :wasabiId)
:secretAccessKey (-> cfg :wasabiKey)
:endpoint "http://wasabi-proxy.wasabi-proxy.svc.cluster.local"}}
:vault-load-yaml false})

View File

@@ -0,0 +1,20 @@
(ns k8s.add-ons.gateway.traefik)
(def config
{:stack [:vault:prepare [:k8s :secret :chart :gateway :certificates]]
:app-namespace "traefik"
:app-name "traefik"
:is-prod? true
:vault-load-yaml false
:k8s:chart-opts {:fetchOpts {:repo 'repo}
:chart 'chart
:version "37.3.0"
:namespace "traefik"
:values {:providers {:kubernetesGateway {:enabled true}}
:gatewayClass {:enabled true
:name "traefik"}}}
:k8s:gateway-opts
{:metadata {:name "main-gateway"
:namespace "traefik"}
:spec {:gatewayClassName "traefik"
:listeners '(make-listeners domains)}}})

View File

@@ -0,0 +1,74 @@
(ns k8s.add-ons.image-registry.harbor)
(def config
{:stack [:vault:prepare [:k8s :storage-class :chart]]
:app-namespace "harbor"
:app-name "harbor"
:image-port 80
:vault-load-yaml false
:k8s:chart-opts {:fetchOpts {:repo "https://helm.goharbor.io"}
:values {:externalURL '(str "https://" host)
:expose {:type "route"
:tls {:enabled false}
:route {:hosts ['host]
:parentRefs [{:name "main-gateway"
:namespace "traefik"}]}}
:harborAdminPassword 'admin-password
:secretKey 'secret-key
:database {:enabled true
:internal {:password 'db-password}}
:postgresql {:auth {:postgresPassword 'db-password}}
:persistence {:enabled true
:resourcePolicy "keep"
:imageChartStorage {:type "s3"
:redirect {:disable true}
:delete {:enabled true}
:disableredirect true
:s3 {:region 'region
:bucket 'bucket
:secure false
:v4auth true
:accesskey 's3-access-key
:secretkey 's3-secret-key
:regionendpoint 'region-endpoint}}}
:core {:secret 'core-secret
:xsrfKey 'core-xrsf-key
:tokenKey 'core-token-key
:tokenCert 'core-token-cert}
:jobservice {:secret 'jobservice-secret}
:registry {:secret 'registry-secret
:s3 {:region 'region
:bucket 'bucket
:secure false
:forcepathstyle true
:accesskey 's3-access-key
:secretkey 's3-secret-key
:regionendpoint 'region-endpoint}
:upload_purging {:enabled true}
:logLevel "debug"}}
:transformations [(fn [args _opts]
(let [kind (get-in args [:resource :kind])]
(if (some #{kind} ["StatefulSet" "PersistentVolumeClaim" "Ingress"])
(update-in args [:resource :metadata :annotations]
#(assoc (or % {}) "pulumi.com/skipAwait" "true"))
args)))]}
:k8s:storage-class-opts {:provisioner "ru.yandex.s3.csi"
:parameters {"mounter" "geesefs"
"bucket" "pulumi-harbor"
"singleBucket" "pulumi-harbor"
"region" "us-east-1"
"accessKey" "something"
"secretKey" "something"
"accessKeyID" "something"
"secretAccessKey" "something"
"usePathStyle" "true"
"insecureSkipVerify" "true"
"options" "--memory-limit 1000 --dir-mode 0777 --file-mode 0666"
"csi.storage.k8s.io/provisioner-secret-name" "wasabi-csi-secrets"
"csi.storage.k8s.io/provisioner-secret-namespace" "kube-system"
"csi.storage.k8s.io/node-publish-secret-name" "wasabi-csi-secrets"
"csi.storage.k8s.io/node-publish-secret-namespace" "kube-system"
"csi.storage.k8s.io/node-stage-secret-name" "wasabi-csi-secrets"
"csi.storage.k8s.io/node-stage-secret-namespace" "kube-system"
"csi.storage.k8s.io/controller-publish-secret-name" "wasabi-csi-secrets"
"csi.storage.k8s.io/controller-publish-secret-namespace" "kube-system"}}})

View File

@@ -0,0 +1,27 @@
(ns k8s.add-ons.ingress-controller.caddy
(:require [configs :refer [cfg]]))
(def config
{:stack [:vault:prepare :docker:image :k8s:secret :k8s:chart]
:app-namespace "caddy-system"
:app-name "caddy-ingress-controller"
:k8s:image-port 8080
:k8s:vault-load-yaml false
:k8s:image-opts {:imageName '(str repo "/" app-name ":latest")}
:docker:image-opts {:registry {:server (-> cfg :public-image-registry-url)
:username (-> cfg :public-image-registry-username)
:password (-> cfg :public-image-registry-password)}
:tags [(str (-> cfg :public-image-registry-url) "/" (-> cfg :public-image-registry-username) "/" "caddy")]
:push true}
:k8s:chart-opts {:fetchOpts {:repo "https://caddyserver.github.io/ingress"}
:values
{:ingressController
{:deployment {:kind "DaemonSet"}
:daemonSet {:useHostPort true}
:ports {:web {:hostPort 80}
:websecure {:hostPort 443}}
:service {:type "NodePort"
:externalTrafficPolicy "Local"}
:image {:repository 'repo
:tag "latest"}
:config {:email 'email}}}}})

View File

@@ -0,0 +1,27 @@
(ns k8s.add-ons.minio)
(def config
{:stack [:vault-secrets :deployment :service :ingress]
:app-namespace "minio"
:no-namespace true
:app-name "minio"
:image-port 9000
:image "quay.io/minio/minio"
:load-yaml false
:deployment-opts {:spec {:template {:spec {:containers [{:name "minio"
:args ["gateway" "s3"]
:env [{:name "MINIO_ROOT_USER" :valueFrom {:secretKeyRef {:name "minio-secrets"
:key "MINIO_ROOT_USER"}}}
{:name "MINIO_ROOT_PASSWORD" :valueFrom {:secretKeyRef {:name "minio-secrets"
:key "MINIO_ROOT_PASSWORD"}}}
{:name "MINIO_COMPAT"
:value "on"}
{:name "MINIO_S3_URL"
:value "https://s3.wasabisys.com"}
{:name "MINIO_ACCESS_KEY"
:valueFrom {:secretKeyRef {:name "minio-secrets"
:key "MINIO_ACCESS_KEY"}}}
{:name "MINIO_SECRET_KEY"
:valueFrom {:secretKeyRef {:name "minio-secrets"
:key "MINIO_SECRET_KEY"}}}]}]}}
:nodeSelector {"kubernetes.io/hostname" "master-de"}}}})

View File

@@ -0,0 +1,59 @@
(ns k8s.add-ons.proxy
(:require [clojure.string :as str]))
(def wasabi-proxy-caddyfile
(str/join "\n"
[":80 {"
""
" reverse_proxy https://s3.wasabisys.com {"
" flush_interval -1"
" transport http {"
" versions 1.1"
" }"
" header_up -X-Forwarded-For"
" header_up -X-Forwarded-Proto"
" header_up -X-Forwarded-Host"
" header_up -Transfer-Encoding"
" header_up Content-Type {http.request.header.Content-Type}"
" }"
"}"]))
(def config
{:stack [[:k8s :config-map :deployment :service]]
:app-namespace "wasabi-proxy"
:app-name "wasabi-proxy"
:image-port 80
:image "docker.io/library/caddy:2"
:vault-load-yaml false
:k8s:config-map-opts {:data {:Caddyfile wasabi-proxy-caddyfile}}
:k8s:deployment-opts
{:spec
{:template
{:spec
{:containers
[{:name 'app-name
:image "docker.io/library/caddy:2"
:ports [{:containerPort 80}]
:volumeMounts
[{:name "caddyfile-config"
:mountPath "/etc/caddy/Caddyfile"
:subPath "Caddyfile"}
{:name "caddy-data"
:mountPath "/data/caddy"}]}]
:volumes
[{:name "caddyfile-config"
:configMap {:name 'app-name}}
{:name "caddy-data"
:emptyDir {}}]
:nodeSelector {"node-role.kubernetes.io/master" "true"}}}}}
:k8s:service-opts
{:spec
{:ports
[{:port 80
:targetPort 80}]}}})

View File

@@ -0,0 +1,28 @@
(ns k8s.add-ons.s3proxy)
(def config
{:stack [:vault-secrets :deployment :service]
:app-namespace "s3proxy"
:app-name "s3proxy"
:image-port 80
:image "andrewgaul/s3proxy:latest"
:load-yaml false
:deployment-opts
{:spec
{:template
{:spec
{:containers
[{:name "s3proxy"
:env [{:name "S3PROXY_AUTHORIZATION" :value "none"}
{:name "S3PROXY_ENDPOINT" :value "http://0.0.0.0:80"}
;;{:name "S3PROXY_IDENTITY" :value "local-identity"}
;;{:name "S3PROXY_CREDENTIAL" :value "local-credential"}
{:name "JCLOUDS_PROVIDER" :value "s3"}
{:name "JCLOUDS_IDENTITY" :valueFrom {:secretKeyRef {:name "s3proxy-secrets"
:key "S3PROXY_IDENTITY"}}}
{:name "JCLOUDS_CREDENTIAL" :valueFrom {:secretKeyRef {:name "s3proxy-secrets"
:key "S3PROXY_CREDENTIAL"}}}
{:name "JCLOUDS_ENDPOINT" :value "https://s3.wasabisys.com"}
{:name "JCLOUDS_REGION" :value "us-east-1"}
]}]
:nodeSelector {"node-role.kubernetes.io/master" "true"}}}}}})

View File

@@ -0,0 +1,9 @@
(ns k8s.add-ons.secret-replicator)
(def config
{:stack [:k8s:chart]
:image-port 80
:no-namespace true
:app-namespace "kube-system"
:app-name "kubernetes-replicator"
:k8s:chart-opts {:fetchOpts {:repo "https://helm.mittwald.de"}}})

View File

@@ -0,0 +1,33 @@
(ns k8s.preparers.harbor)
(defn execute-fn [env]
(let [docker-string (:docker-json-string env)]
{:docker-string docker-string}))
(def config
{:stack [:vault:retrieve [:harbor :project :robot-account] :k8s:secret]
:no-namespace true
:app-name "apps"
:app-namespace "generic"
:image-port 80
:vault-load-yaml false
:k8s:secret-opts {:metadata
{:name "harbor-creds-secrets"
:namespace "kube-system"
:annotations {"replicator.v1.mittwald.de/replicate-to" "*"}}
:type "kubernetes.io/dockerconfigjson"
:stringData {".dockerconfigjson" '(str "{\"auths\":{\""
host
"\":{\"auth\":\""
(b64e (str (-> :harbor:robot-account .-fullName) ":" (-> :harbor:robot-account .-secret)))
"\"}}}")}}
:harbor:robot-account-opts {:name '(str "kube-" app-name "-robot")
:namespace 'app-name
:level "project"
:permissions [{:kind "project"
:namespace 'app-name
:access [{:action "pull" :resource "repository"}
{:action "list" :resource "repository"}]}]}
:vault:retrieve-opts {:app-name "harbor"
:app-namespace "harbor"}})

View File

@@ -0,0 +1,14 @@
(ns k8s.services.act-runner.service)
(def config
{:stack [:vault:prepare [:k8s :deployment :service]]
:image-port 80
:app-namespace "generic"
:app-name "act-runner"
:k8s:deployment-opts {:spec {:template {:spec {:containers [{:name 'app-name
:envFrom [{:secretRef {:name '(str app-name "-secrets")}}]
:image '(str repo "/" "act_runner" ":latest")
}]}}}}
:k8s:httproute-opts {:spec {::hostnames ['host]}}
})

View File

@@ -0,0 +1,24 @@
(ns k8s.services.foundryvtt.service)
(def config
{:stack [:vault:prepare :harbor:robot-account :docker:image [:k8s :deployment :service :httproute]]
:image-port 30000
:app-namespace "generic"
:app-name "foundry"
:docker:image-opts {:is-local true
:buildArgs {:FOUNDRY_USERNAME 'FOUNDRY_USERNAME
:FOUNDRY_PASSWORD 'FOUNDRY_PASSWORD}
:registry {:server '(str registry-base "/" registry-namespace)
:username '(-> :harbor:robot-account .-name)
:password '(-> :harbor:robot-account .-secret)}
:tags ['(str registry-base "/" registry-namespace "/" app-name)]
:push true}
:k8s:deployment-opts {:spec {:template {:spec {:imagePullSecrets [{:name "harbor-creds-secrets"}]
:containers [{:name 'app-name :image '(str registry-base "/" registry-namespace "/" app-name ":latest")}]}}}}
:harbor:robot-account-opts {:name 'app-name
:permissions [{:kind "project"
:namespace 'registry-namespace
:access [{:action "pull" :resource "repository"}
{:action "push" :resource "repository"}
{:action "list" :resource "repository"}]}]}
:k8s:httproute-opts {:spec {::hostnames ['host]}}})

View File

@@ -0,0 +1,25 @@
(ns k8s.services.gitea.service)
(def config
{:stack [:vault:prepare :k8s:deployment :k8s:service :k8s:httproute]
:image-port 3000
:app-namespace "generic"
:app-name "gitea"
:k8s:deployment-opts {:spec {:template {:spec {:initContainers [
{:name "init-permissions"
:image "busybox:latest"
:command ["sh" "-c" "chown -R 1000:1000 /var/lib/gitea && chown -R 1000:1000 /etc/gitea"]
:volumeMounts [{:name "gitea-data" :mountPath "/var/lib/gitea"}
{:name "gitea-config" :mountPath "/etc/gitea"}]
:securityContext {:runAsUser 0 :runAsGroup 0}}
]
:containers [{:name 'app-name :image '(str repo "/" app-name ":latest-rootless")
:volumeMounts [{:name "gitea-data" :mountPath "/var/lib/gitea"}
{:name "gitea-config" :mountPath "/etc/gitea"}
{:name "timezone" :mountPath "/etc/timezone" :readOnly true}
{:name "localtime" :mountPath "/etc/localtime" :readOnly true}]}]
:volumes [{:name "gitea-data" :hostPath {:path "/opt/gitea/data" :type "DirectoryOrCreate"}}
{:name "gitea-config" :hostPath {:path "/opt/gitea/config" :type "DirectoryOrCreate"}}
{:name "timezone" :hostPath {:path "/etc/timezone" :type "File"}}
{:name "localtime" :hostPath {:path "/etc/localtime" :type "File"}}]}}}}
:k8s:httproute-opts {:spec {::hostnames ['host]}}})

View File

@@ -0,0 +1,12 @@
(ns k8s.services.matrix.cinny.service)
(def config
{:stack [:vault-secrets :docker-image :deployment :service :ingress]
:image-port 80
:app-namespace "matrix"
:app-name "cinny"
:image-opts {:build {:args {:FOUNDRY_USERNAME 'FOUNDRY_USERNAME
:FOUNDRY_PASSWORD 'FOUNDRY_PASSWORD}}
:imageName '(str repo "/" app-name ":latest")}
:deployment-opts {:spec {:template {:spec {:imagePullSecrets [{:name "harbor-creds-secrets"}]
:containers [{:name 'app-name :image '(str repo "/" app-name ":latest")}]}}}}})

View File

@@ -0,0 +1,13 @@
(ns k8s.services.matrix.database.service)
;; env_file:
;; - .env
;; volumes:
;; - ${PWD}/db-data/:/var/lib/postgresql/data/
(def config
{:stack [:deployment :service :ingress]
:image-port 80
:app-namespace "matrix"
:app-name "postgres"
:deployment-opts {:spec {:template {:spec {:containers [{:name 'app-name :image '(str repo "/" 'app-name ":latest")}]}}}}})

View File

@@ -0,0 +1,14 @@
;; volumes:
;; - ./personal/matrix/element-config.json:/app/config.json
;; environment:
;; ELEMENT_WEB_PORT: 3030
(ns k8s.services.matrix.element.service)
(def config
{:stack [:vault-secrets :docker-image :deployment :service :ingress]
:image-port 80
:app-namespace "matrix"
:app-name "element"
:deployment-opts {:spec {:template {:spec {:imagePullSecrets [{:name "harbor-creds-secrets"}]
:containers [{:name 'app-name :image '(str repo "/" app-name ":latest")}]}}}}})

View File

@@ -0,0 +1,18 @@
(ns k8s.services.matrix.element-call.service)
;; volumes:
;; - ./personal/matrix/elementcall/config.json:/app/config.json
(def config
{:stack [:vault-secrets :deployment :service :ingress]
:image-port 80
:app-namespace "matrix"
:app-name "element-call"
:deployment-opts {:spec {:template {:spec {:containers [{:name 'app-name :image '(str repo "/" app-name ":sha-1702b15")
:volumeMounts [{:name "data" :mountPath "/data"}]}]
:initContainers [{:name "init-permissions"
:image "busybox:latest"
:command ["sh" "-c" "chown -R 1000:1000 /data"]
:volumeMounts [{:name "data" :mountPath "/data"}]
:securityContext {:runAsUser 0 :runAsGroup 0}}]
:volumes [{:name "data" :hostPath {:path "/opt/mmr/data" :type "DirectoryOrCreate"}}]}}}}})

View File

@@ -0,0 +1,10 @@
;; homeserver:
;; volumes:
;; - db:/var/lib/conduwuit
(def config
{:stack [:vault-secrets :docker-image :deployment :service :ingress]
:image-port 80
:app-namespace "matrix"
:app-name "tuwunel"
:deployment-opts {:spec {:template {:spec {:containers [{:name 'app-name :image '(str repo "/" 'app-name ":latest")}]}}}}})

View File

@@ -0,0 +1,8 @@
(ns k8s.services.matrix.livekit-jwt.service)
(def config
{:stack [:vault-secrets :docker-image :deployment :service :ingress]
:image-port 80
:app-namespace "matrix"
:app-name "livekit-jwt"
:deployment-opts {:spec {:template {:spec {:containers [{:name 'app-name :image '(str repo "/" lk-jwt-service ":0.2.3")}]}}}}})

View File

@@ -0,0 +1,16 @@
;; livekit:
;; command: --config /etc/livekit.yaml
;; - ./personal/matrix/elementcall/livekit.yaml:/etc/livekit.yaml
;; ports:
;; - 50100-50200:50100-50200/udp
(ns k8s.services.matrix.livekit-server.service)
(def config
{:stack [:vault-secrets :docker-image :deployment :service :ingress]
:image-port 80
:app-namespace "matrix"
:app-name "livekit-server"
:deployment-opts {:spec {:template {:spec {:containers [{:name 'app-name :image '(str repo "/" 'app-name ":latest")}]}}}}})

View File

@@ -0,0 +1,10 @@
;; - ./personal/matrix/discord/data:/data
(ns k8s.services.matrix.mautrix-discord.service)
(def config
{:stack [:vault-secrets :docker-image :deployment :service :ingress]
:image-port 80
:app-namespace "matrix"
:app-name "mautrix-discord"
:deployment-opts {:spec {:template {:spec {:containers [{:name 'app-name :image '(str repo "/" "discord" ":4927a73ce7411f3970803d35c22f0c8c96dc2d7e-amd64")}]}}}}})

View File

@@ -0,0 +1,21 @@
(ns k8s.services.matrix.mmr.service)
(def config
{:stack [:vault-secrets :deployment :service :ingress]
:image-port 80
:app-namespace "matrix"
:app-name "matrix-media-repo"
:deployment-opts {:spec {:template {:spec {:containers [{:name 'app-name :image '(str repo "/" app-name ":v1.3.8")
:volumeMounts [{:name "data" :mountPath "/data"}]}]
:initContainers [{:name "init-permissions"
:image "busybox:latest"
:command ["sh" "-c" "chown -R 1000:1000 /data"]
:volumeMounts [{:name "data" :mountPath "/data"}]
:securityContext {:runAsUser 0 :runAsGroup 0}}]
:volumes [{:name "data" :hostPath {:path "/opt/mmr/data" :type "DirectoryOrCreate"}}]}}}}})
;;
;; - ./personal/matrix/mmr:/data

View File

@@ -0,0 +1,14 @@
(ns k8s.services.matrix.turn.service)
;; - ./personal/matrix/coturn.conf:/etc/coturn/turnserver.conf
(def config
{:stack [:vault-secrets :docker-image :deployment :service :ingress]
:image-port 80
:app-namespace "matrix"
:app-name "coturn"
:image-opts {:build {:args {:FOUNDRY_USERNAME 'FOUNDRY_USERNAME
:FOUNDRY_PASSWORD 'FOUNDRY_PASSWORD}}
:imageName '(str repo "/" app-name ":latest")}
:deployment-opts {:spec {:template {:spec {:imagePullSecrets [{:name "harbor-creds-secrets"}]
:containers [{:name 'app-name :image '(str repo "/" app-name ":latest")}]}}}}})

View File

@@ -0,0 +1,34 @@
(ns k8s.services.mesite.service)
(defn test [env]
(js/console.log env)
(.apply (:test env) #(js/console.log %)))
(def config
{:stack [:vault:prepare
:harbor:robot-account
:docker:image
[:k8s :namespace :deployment :service :httproute]]
:app-name "mesite"
:app-namespace "generic"
:docker:image-opts {:context {:location "https://codeberg.org/Gigia/mesite.git"}
:imageName '(str registry-base "/" registry-namespace "/" app-name ":latest")
:registry {:server '(str registry-base "/" registry-namespace)
:username '(-> :harbor:robot-account .-name)
:password '(-> :harbor:robot-account .-secret)}
:tags ['(str registry-base "/" registry-namespace "/" app-name)]
:push true}
:harbor:robot-account-opts {:name 'app-name
:permissions [{:kind "project"
:namespace 'registry-namespace
:access [{:action "pull" :resource "repository"}
{:action "push" :resource "repository"}
{:action "list" :resource "repository"}]}]}
:k8s:deployment-opts {:spec {:template {:spec {:imagePullSecrets [{:name "harbor-creds-secrets"}]
:containers [{:name 'app-name
:image '(str registry-base "/" registry-namespace "/" app-name ":latest")
:ports [{:containerPort 80}]}]}}}}
:k8s:httproute-opts {:spec {::hostnames ['host]}}
})

View File

@@ -0,0 +1,17 @@
(ns k8s.services.nextcloud.service)
(def config
{:stack [:vault-secrets :chart :ingress]
:app-namespace "nextcloud"
:app-name "nextcloud"
:image-port 8080
:vault-load-yaml true
:chart-opts {:fetchOpts {:repo "https://nextcloud.github.io/helm/"}
:values {:nextcloud {:host 'host
:trustedDomains ['host 'app-name]}}
:transformations (fn [args _opts]
(let [kind (get-in args [:resource :kind])]
(if (some #{kind} ["StatefulSet" "PersistentVolumeClaim" "Ingress"])
(update-in args [:resource :metadata :annotations]
#(assoc (or % {}) "pulumi.com/skipAwait" "true"))
args)))}})

View File

@@ -0,0 +1,10 @@
(ns k8s.services.productive.service)
(def config
{:stack [:vault:prepare [:k8s :deployment :service :httproute]]
:app-namespace "generic"
:app-name "superproductivity"
:image-port 80
:image "docker.io/johannesjo/super-productivity:latest"
:k8s:httproute-opts {:spec {::hostnames ['host]}}
})

View File

@@ -0,0 +1,26 @@
(ns k8s.services.prometheus.service)
(def config
{:stack [:vault-secrets :chart]
:app-namespace "prometheus"
:app-name "prometheus"
:image-port 8080
:vault-load-yaml true
:chart-opts {:chart "kube-prometheus-stack"
:fetchOpts {:repo "https://prometheus-community.github.io/helm-charts"}
:namespace "monitoring"
:values {:grafana {:adminPassword 'password
:ingress {:enabled true
:ingressClassName "caddy"
:hosts ['grafana-host]}
:persistence {:enabled true
:type "pvc"
:storageClassName "hcloud-volumes"
:accessModes ["ReadWriteOnce"]
:size "10Gi"}}
:prometheus {:ingress {:enabled true
:ingressClassName "caddy"
:hosts ['prometheus-host]}
:prometheusSpec {:storageSpec {:volumeClaimTemplate {:spec {:accessModes ["ReadWriteOnce"]
:storageClassName "hcloud-volumes"
:resources {:requests {:storage "50Gi"}}}}}}}}}})

View File

@@ -0,0 +1,25 @@
(ns k8s.services.renovate.service)
;https://docs.renovatebot.com/self-hosted-configuration/
(def config
{:stack [:vault-secrets :chart :cronjob]
:app-namespace "renovate"
:app-name "renovate"
:image-port 8080
:vault-load-yaml true
:chart-opts
{:fetchOpts {:repo "https://docs.renovatebot.com/helm-charts"}
:values
{:renovate
{:config {:platform "github"
:token "vault:renovate/github-token"
:logLevel "info"
:repositories ["your-org/your-repo"]
:onboardingConfig {:extends ["config:base"]}}}}
:transformations
(fn [args _opts]
(let [kind (get-in args [:resource :kind])]
(if (= kind "CronJob")
(update-in args [:resource :spec :jobTemplate :spec :template :metadata :annotations]
#(assoc (or % {}) "pulumi.com/skipAwait" "true"))
args)))}})

View File

@@ -0,0 +1,76 @@
(ns service-registries
(:require
[infra.init :as init]
[infra.openbao :as openbao]
[k8s.add-ons.csi-driver.hetzner :as hetzner-csi]
[infra.dns :as dns]
[infra.buildkit :as buildkit]
[k8s.preparers.harbor :as harbor-prepare]
[k8s.add-ons.ingress-controller.caddy :as caddy]
[k8s.add-ons.gateway.traefik :as traefik]
[k8s.add-ons.cert-manager :as cert-manager]
[k8s.add-ons.csi-driver.wasabi :as wasabi-csi]
[k8s.add-ons.image-registry.harbor :as harbor]
[k8s.add-ons.secret-replicator :as secret-replicator]
[k8s.add-ons.minio :as minio]
[k8s.add-ons.s3proxy :as s3proxy]
[k8s.add-ons.proxy :as proxy]
[k8s.services.nextcloud.service :as nextcloud-service]
[k8s.services.mesite.service :as mesite-service]
[k8s.services.gitea.service :as gitea-service]
[k8s.services.act-runner.service :as act-runner-service]
[k8s.services.foundryvtt.service :as foundryvtt-service]
[k8s.services.productive.service :as productive-service]))
(defn general-provider-output-refs []
{:vault {:stack :init
:outputs ["vaultAddress" "vaultToken"]}
:harbor {:stack :shared
:outputs ["username" "password" "url"]}
:k8s {:stack :base
:outputs ["kubeconfig"]}})
(defn create-resource-definition [resource-configs stack-references provider-external-inputs]
{:resource-configs resource-configs
:stack-references stack-references
:provider-external-inputs provider-external-inputs})
(def base-resources-definition
(create-resource-definition
[init/config]
nil
nil))
(def initialize-resources-definition
(create-resource-definition
[hetzner-csi/config openbao/config]
["base"]
{:k8s {:stack :base
:outputs ["kubeconfig"]}}
))
(def shared-resources-definition
(create-resource-definition
[dns/config wasabi-csi/config proxy/config secret-replicator/config
traefik/config cert-manager/config
harbor/config
]
["base" "init"]
(general-provider-output-refs)))
(def preparation-resources-definition
(create-resource-definition
[harbor-prepare/config]
["base" "init" "shared"]
(general-provider-output-refs)))
(def deployment-resources-definition
(create-resource-definition
[#_buildkit/config #_nextcloud-service/config #_foundryvtt-service/config mesite-service/config #_productive-service/config #_gitea-service/config #_act-runner-service/config]
["base" "init" "shared"]
(general-provider-output-refs)))
(def deployment-matrix-service-registry [])

View File

@@ -0,0 +1,12 @@
(ns utils.defaults
(:require ["path" :as path]
[configs :refer [cfg]]
[utils.k8s :as k8s]
[utils.harbor :as harbor]
[utils.docker :as docker]))
(def defaults
{:k8s k8s/defaults
:harbor harbor/defaults
:docker docker/defaults})

View File

@@ -0,0 +1,25 @@
(ns utils.docker
(:require
[utils.general :refer [generic-transform deep-merge new-resource component-factory resource-factory deploy-stack-factory iterate-stack]]
["@pulumi/docker-build" :as docker]
["path" :as path]
[configs :refer [cfg]]))
(defn image [env]
(let [{:keys [app-name docker:image-opts]} env
context-path (.. path (join "." (-> cfg :resource-path)))
dockerfile-path (.. path (join context-path (str app-name ".dockerfile")))
base-args (if (:is-local docker:image-opts)
{:context {:location context-path}
:dockerfile {:location dockerfile-path}
:imageName (str (-> cfg :docker-repo) "/" app-name ":latest")}
{})]
base-args))
(def defaults
{:image image})
(def component-specs-defs
{:root-sym 'docker
:provider-key :harbor
:resources {:image {:path ['-Image]}}})

View File

@@ -0,0 +1,82 @@
(ns utils.general
(:require
[clojure.walk]))
(defmacro build-registry
"Generates a flat resource registry map from a nested provider definition map."
[definitions]
(let [map-entries
(reduce-kv
(fn [entries provider-group-kw provider-group-def]
(let [{:keys [root-sym provider-key resources]} provider-group-def
provider-ns (name provider-group-kw)]
(reduce-kv
(fn [entries resource-kw resource-def]
(let [{:keys [path defaults-fn defaults-name]} resource-def
resource-name (name resource-kw)
final-key (keyword provider-ns resource-name)
constructor-form (list* '.. root-sym path)
defaults-fn-form
(cond
defaults-fn defaults-fn
defaults-name (let [defaults-fn-sym (symbol "default" (name defaults-name))]
`(fn [env] (~defaults-fn-sym (:options env))))
:else (let [defaults-fn-sym (symbol "default" resource-name)]
`(fn [env] (~defaults-fn-sym (:options env)))))
value-map `{:constructor ~constructor-form
:provider-key ~provider-key
:defaults-fn ~defaults-fn-form}]
(conj entries [final-key value-map])))
entries
resources)))
[]
definitions)]
`~(into {} map-entries)))
(defn- p->-replace-percent
"Walks 'form' and replaces all instances of the symbol '%'
with the value of 'x'."
[form x]
(clojure.walk/postwalk
(fn [sub-form]
(if (= sub-form '%) x sub-form))
form))
(defmacro pulet
"Sequential binding for Pulumi Outputs.
Looks like let*, but each binding is chained."
[bindings & body]
(if (empty? bindings)
`(do ~@body)
(let [[sym val & rest] bindings]
`(p-chain ~val
(fn [~sym]
(pulet [~@rest] ~@body))))))
(defmacro p-> [x & forms]
(let [wrap (fn [acc form]
(cond
(map? form)
`(p-chain ~acc (fn [val#] ~form))
(keyword? form)
`(p-chain ~acc #(get % ~form))
(string? form)
`(p-chain ~acc #(aget % ~form))
(symbol? form)
`(p-chain ~acc #(~form %))
(list? form)
`(p-chain ~acc ~form)
:else
(throw (ex-info "Unsupported form in p->" {:form form}))))]
(reduce wrap x forms)))

232
src/main/utils/general.cljs Normal file
View File

@@ -0,0 +1,232 @@
(ns utils.general (:require [clojure.walk :as walk]))
(defn new-resource [resource-type resource-name final-args provider dependencies]
(let [base-opts (if (or (some? provider) (seq dependencies))
{:enableServerSideApply false} {})
opts (cond-> base-opts
(some? provider) (assoc :provider provider)
(seq dependencies) (assoc :dependsOn dependencies))]
(if (seq opts)
(new resource-type resource-name (clj->js final-args) (clj->js opts))
(new resource-type resource-name (clj->js final-args)))))
(defn assoc-ins [m path-vals]
(reduce (fn [acc [path val]] (assoc-in acc path val)) m path-vals))
(declare deep-merge)
(defn merge-by-name
"Merges two vectors of maps by :name key."
[a b]
(let [a-map (into {} (map #(vector (:name %) %) a))
b-map (into {} (map #(vector (:name %) %) b))
merged (merge-with deep-merge a-map b-map)]
(vec (vals merged))))
(defn deep-merge
"Recursively merges maps and intelligently merges vectors of maps by :name."
[a b]
(cond
(nil? b) a
(and (map? a) (map? b))
(merge-with deep-merge a b)
(and (vector? a) (vector? b)
(every? map? a) (every? map? b)
(some #(contains? % :name) (concat a b)))
(merge-by-name a b)
:else b))
(defn make-transformer
"Given f that takes {:app-name .. :secrets ..}, where :secrets is a plain map
(already unwrapped inside .apply), return a Helm transformer."
[f]
(fn [{:keys [base-values app-name secrets]}]
(.apply secrets
(fn [smap]
(let [m (js->clj smap :keywordize-keys true)
updates (f {:app-name app-name
:secrets m})
after (clj->js (assoc-ins base-values updates))]
after)))))
(defn make-paths [& path-groups]
(mapcat (fn [{:keys [paths backend]}]
(mapv (fn [p]
{:path p
:pathType "Prefix"
:backend {:service backend}})
paths))
path-groups))
(defn generic-make-transformer
"Returns a Pulumi-compatible transformer that unwraps Output values via .apply."
[f {:keys [secrets base-values]}]
(.apply secrets
(fn [smap]
(let [m (js->clj smap :keywordize-keys true)
updates (f {:function-keys m})
result (clj->js (deep-merge base-values updates))]
result))))
(defn safe-parse-int [s]
(let [n (js/parseInt s 10)]
(if (js/isNaN n) nil n)))
(defn string->int? [s]
(and (string? s)
(re-matches #"^-?\d+$" s)))
(defn- coerce-value [v]
(if (string->int? v)
(safe-parse-int v)
v))
;; Whitelist functions for resolving templates. Intended to be extended.
(def ^:private safe-fns
{'str str
'make-paths make-paths})
(defn resolve-template [template values secondary-values]
(walk/postwalk
(fn [x]
(cond
(and (list? x) (contains? safe-fns (first x)))
(apply (get safe-fns (first x)) (rest x))
(symbol? x)
(if (contains? safe-fns x)
x
(let [kw (keyword x)]
(cond
(contains? values x) (coerce-value (get values x))
(contains? values kw) (coerce-value (get values kw))
(contains? secondary-values x) (coerce-value (get secondary-values x))
(contains? secondary-values kw) (coerce-value (get secondary-values kw))
:else x)))
:else x))
template))
(defn generic-transform
"Takes a creator function and executes it with resolved arguments,
handling asynchronicity when secrets are present."
[creator-fn opts base-values secrets options]
(if (nil? secrets)
(let [final-args (clj->js (deep-merge base-values (resolve-template opts {} options)))]
(creator-fn final-args))
(.apply secrets
(fn [smap]
(let [m (js->clj smap :keywordize-keys true)
final-args (clj->js (deep-merge base-values (resolve-template opts m options)))]
;;(js/console.log final-args)
(creator-fn final-args))))))
(defn resource-factory
[component-specs]
(fn [resource-type provider app-name dependencies opts]
(let [spec (get component-specs resource-type)
resource-class (:constructor spec)]
(if resource-class
(new-resource resource-class app-name opts provider dependencies)
(throw (js/Error. (str "Unknown resource type: " resource-type)))))))
(defn component-factory [create-resource]
(fn [requested-components
resource-type
provider
app-name
dependencies
component-opts
defaults
secrets
options]
(when (requested-components resource-type)
(generic-transform
(fn [final-args]
(create-resource resource-type provider app-name dependencies final-args))
component-opts
defaults
secrets
options))))
(defn deploy-stack-factory [func]
(fn [& args]
(let [[component-kws [options]] (split-with keyword? args)
requested-components (set component-kws)]
(func requested-components options))))
(defn iterate-stack
[provider vault-data options secrets requested-components create-component-fn component-specs lifecycle-hooks]
(let [base-components
(reduce
(fn [acc [k {:keys [deps-fn opts-key defaults-fn]}]]
(let [env {:provider provider
:options options
:secrets secrets
:components acc}
app-name (get options :resource-name)
deps (deps-fn env)
opts (get options opts-key)
defaults (defaults-fn env)
component (create-component-fn requested-components k provider app-name deps opts defaults secrets options)]
(assoc acc k component)))
{:vault-secrets vault-data}
(select-keys component-specs requested-components))
final-components
(if lifecycle-hooks
(reduce
(fn [acc k]
(if-let [hook (get lifecycle-hooks k)]
(assoc acc k (hook {:options options
:components acc
:secrets secrets}))
acc))
base-components
requested-components)
base-components)]
final-components))
(defn flatten-resource-groups
"Transforms a nested resource map into a flat, qualified-keyword map.
Example:
(flatten-resource-groups {:k8s {:chart {} :ingress {}}})
=> {:k8s:chart {} :k8s:ingress {}}"
[config]
(into {}
(mapcat
(fn [[k v]]
(if (and (keyword? k) (map? v))
(map (fn [[inner-k inner-v]]
[(keyword (name k) (name inner-k)) inner-v])
v)
[[k v]]))
config)))
(defn- is-output? [x] (some? (and x (.-__pulumiOutput x))))
(defn p-apply-or-resolve
"Runtime helper. If 'v' is an Output, applies 'f' to it.
If 'v' is a plain value, calls 'f' with it."
[v f]
(if (is-output? v)
(.apply v f)
(f v)))
(defn is-output? [x]
(some? (and x (.-__pulumiOutput x))))
(defn p-chain [v f]
(if (is-output? v)
(.apply v f)
(f v)))
(defn p-map [v f]
(p-chain v #(f %)))
(defn p-lift [v]
(if (is-output? v) v (js/Promise.resolve v)))

View File

@@ -0,0 +1,36 @@
(ns utils.harbor
(:require
["@pulumiverse/harbor" :as harbor]))
(defn project [{:keys [app-name]}]
{:name app-name
:public false})
(defn robot-account [{:keys [app-name]}]
{:name (str app-name "-robot")
:level "project"
:permissions [{:kind "project"
:namespace app-name
:access [{:action "push" :resource "repository"}
{:action "pull" :resource "repository"}
{:action "list" :resource "repository"}]}]})
(def defaults
{:project project
:robot-account robot-account})
(def provider-template
{:constructor (.. harbor -Provider)
:name "harbor-provider"
:config {:url 'url
:username 'username
:password 'password}})
(def component-specs-defs
{:root-sym 'harbor
:provider-key :harbor
:resources
{:project {:path ['-Project]}
:robot-account {:path ['-RobotAccount]
:defaults-name 'robot}}})

198
src/main/utils/k8s.cljs Normal file
View File

@@ -0,0 +1,198 @@
(ns utils.k8s (:require ["@pulumi/kubernetes" :as k8s]))
(defn cluster-issuer [{:keys [host is-prod?]}]
{:metadata {:name (if is-prod? "letsencrypt-prod" "letsencrypt-staging")}
:spec {:acme {:email "admin@example.com"
:server (if is-prod? "https://acme-v02.api.letsencrypt.org/directory" "https://acme-staging-v02.api.letsencrypt.org/directory")
:privateKeySecretRef {:name (if is-prod? "account-key-prod" "account-key-staging")}
:solvers [{:dns01 {:cloudflare {:apiTokenSecretRef
{:name "api-token-secret"
:key "apiToken"}}}
:selector {:dnsZones [(or host "example.com")]}}]}}})
(defn certificate
[{:keys [app-name app-namespace host is-prod?]}]
(let [secret-name (str app-name "-cert")
domain (or host (str app-name ".example.com"))
issuer-name (if is-prod?
"letsencrypt-prod"
"letsencrypt-staging")]
{:apiVersion "cert-manager.io/v1"
:kind "Certificate"
:metadata {:name (str app-name "-cert")
:namespace app-namespace}
:spec {:secretName secret-name
:issuerRef {:name issuer-name
:kind "ClusterIssuer"}
:dnsNames [domain]}}))
(defn gateway
[{:keys [app-name]}]
{:apiVersion "gateway.networking.k8s.io/v1"
:kind "Gateway"
:metadata {:name "main-gateway"
:namespace "traefik"}
:spec {:gatewayClassName "traefik"
:listeners
[{:name "http"
:protocol "HTTP"
:port 80}
{:name "https"
:protocol "HTTPS"
:port 443
:tls {:certificateRefs
[{:name (str app-name "-cert")
:kind "Secret"}]}}]}})
(defn httproute [{:keys [app-name app-namespace host]}]
{:apiVersion "gateway.networking.k8s.io/v1"
:kind "HTTPRoute"
:metadata {:name (str app-name "-route")
:namespace app-namespace}
:spec {:parentRefs [{:name "main-gateway"
:namespace "traefik"}]
:hostnames [host]
:rules [{:matches [{:path {:type "PathPrefix"
:value "/"}}]
:backendRefs [{:name app-name
:port 80}]}]}})
(defn ingress [{:keys [app-name app-namespace host]}]
{:metadata {:name app-name
:namespace app-namespace}
:spec {:ingressClassName "caddy"
:rules [{:host host
:http {:paths [{:path "/"
:pathType "Prefix"
:backend {:service {:name app-name
:port {:number 80}}}}]}}]}})
(defn chart [{:keys [app-name app-namespace]}]
{:chart app-name
:namespace app-namespace
:transformations []})
(defn config-map [{:keys [app-name app-namespace]}]
{:metadata {:namespace app-namespace
:name app-name}
:data {}})
(defn service [{:keys [app-name app-namespace image-port]}]
{:metadata {:namespace app-namespace
:name app-name}
:spec {:selector {:app app-name}
:ports [{:port 80 :targetPort image-port}]}})
(defn deployment [{:keys [app-name app-namespace image image-port]}]
{:metadata {:namespace app-namespace
:name app-name}
:spec {:selector {:matchLabels {:app app-name}}
:replicas 1
:template {:metadata {:labels {:app app-name}}
:spec {:containers
[{:name app-name
:image image
:ports [{:containerPort image-port}]}]}}}})
(defn nspace [{:keys [app-namespace]}]
{:metadata {:name app-namespace}})
(defn secret [{:keys [app-name app-namespace]}]
{:metadata {:name (str app-name "-secrets")
:namespace app-namespace}})
(defn storage-class [{:keys [app-name]}]
{:metadata {:name app-name}})
(def defaults
{:ingress ingress
:gateway gateway
:httproute httproute
:certificate certificate
:cluster-issuer cluster-issuer
:chart chart
:config-map config-map
:service service
:deployment deployment
:namespace nspace
:secret secret
:storage-class storage-class})
(def component-specs-defs
{:root-sym 'k8s
:provider-key :k8s
:resources
{:config-map {:path ['-core '-v1 '-ConfigMap]}
:storage-class {:path ['-core '-v1 '-StorageClass]}
:namespace {:path ['-core '-v1 '-Namespace]}
:secret {:path ['-core '-v1 '-Secret]}
:deployment {:path ['-apps '-v1 '-Deployment]}
:service {:path ['-core '-v1 '-Service]}
:ingress {:path ['-networking '-v1 '-Ingress]}
:chart {:path ['-helm '-v3 '-Chart]
:defaults-fn
'(fn [env]
(deep-merge (default/chart (:options env))
(update-in (get-in (:options env) [:k8s:chart-opts]) [:values]
#(deep-merge % (or (:yaml-values (:options env)) {})))))}}})
#_(def component-specs
:k8s:namespace {:constructor (.. k8s -core -v1 -Namespace)
:provider-key :k8s
:defaults-fn (fn [env] (defaults/namespace (:options env)))}
:k8s:secret {:constructor (.. k8s -core -v1 -Secret)
:provider-key :k8s
:defaults-fn (fn [env] (default/secret (:options env)))}
:k8s:deployment {:constructor (.. k8s -apps -v1 -Deployment)
:provider-key :k8s
:defaults-fn (fn [env] (default/deployment (:options env)))}
:k8s:service {:constructor (.. k8s -core -v1 -Service)
:provider-key :k8s
:defaults-fn (fn [env] (default/service (:options env)))}
:k8s:ingress {:constructor (.. k8s -networking -v1 -Ingress)
:provider-key :k8s
:defaults-fn (fn [env] (default/ingress (:options env)))}
:k8s:chart {:constructor (.. k8s -helm -v3 -Chart)
:provider-key :k8s
:defaults-fn (fn [env]
(deep-merge (default/chart (:options env))
(update-in (get-in (:options env) [:k8s:chart-opts]) [:values]
#(deep-merge % (or (:yaml-values (:options env)) {})))))})
(def provider-template
{:constructor (.. k8s -Provider)
:name "k8s-provider"
:config {:kubeconfig 'kubeconfig}})
(defn pre-deploy-rule
"k8s pre-deploy rule: scans the service registry and creates
all unique namespaces. Returns a map of created namespaces
keyed by their name."
[{:keys [resource-configs provider]}]
(let [namespaces (->> resource-configs
(remove #(contains? % :no-namespace))
(map :app-namespace)
(remove nil?)
(set))]
(into {}
(for [ns-name namespaces]
(let [resource-name ns-name
ns-config {:metadata {:name resource-name
:namespace ns-name}}
ns-resource (new (.. k8s -core -v1 -Namespace) resource-name
(clj->js ns-config)
(clj->js {:provider provider}))]
[ns-name ns-resource])))))

View File

@@ -0,0 +1,135 @@
(ns utils.providers
(:require
["@pulumi/pulumi" :as pulumi] ["@pulumi/vault" :as vault] ["@pulumiverse/harbor" :as harbor] ["@pulumi/kubernetes" :as k8s]
[clojure.string :as str] [clojure.walk :as walk]
[utils.general :refer [resolve-template]]
[utils.k8s :as k8s-utils]
[utils.harbor :as harbor-utils]
[utils.docker :as docker-utils] [utils.vault :as vault-utils]
[utils.stack-processor :refer [deploy! component-specs]]))
(defn resolve-provider-template [constructor name config]
{:constructor constructor
:name name
:config config})
(def provider-templates
(into {} (map (fn [[k v]] [k (apply resolve-provider-template (vals v))])
{:vault vault-utils/provider-template
:harbor harbor-utils/provider-template
:k8s k8s-utils/provider-template})))
(defn get-provider-outputs-config []
{:vault {:stack :init
:outputs ["vaultAddress" "vaultToken"]}
:harbor {:stack :shared
:outputs ["username" "password" "url"]}
:k8s {:stack :init
:outputs ["kubeconfig"]}})
#_(defn get-stack-refs []
{:init (new pulumi/StackReference "init")
:shared (new pulumi/StackReference "shared")})
(defn get-stack-refs [stack-ref-array]
(into {}
(map (fn [stack-name]
[(keyword stack-name)
(new pulumi/StackReference stack-name)])
stack-ref-array)))
(defn extract-expanded-keywords [stack]
(let [expand-chain
(fn [chain]
(when (and (sequential? chain) (keyword? (first chain)))
(let [ns (or (namespace (first chain)) (name (first chain)))]
(map #(keyword ns (name %)) (rest chain)))))]
(mapcat (fn [item]
(cond
(and (sequential? item) (keyword? (first item)))
(expand-chain item)
(keyword? item)
[item]
:else
nil))
stack)))
(defn get-all-providers [resource-configs]
(->> resource-configs
(mapcat (comp extract-expanded-keywords :stack))
(map (fn [component-key]
(if-let [ns (namespace component-key)]
(keyword ns)
(let [k-name (name component-key)
parts (str/split k-name #":")]
(when (> (count parts) 1)
(keyword (first parts)))))))
(remove nil?)
(into #{})
vec))
(def provider-rules
{:k8s k8s-utils/pre-deploy-rule})
(defn provider-apply [stack-resources-definition pulumi-cfg]
(let [providers-needed (get-all-providers (:resource-configs stack-resources-definition))
provider-outputs-config (:provider-external-inputs stack-resources-definition)
stack-refs (get-stack-refs (:stack-references stack-resources-definition))
needed-output-configs (select-keys provider-outputs-config providers-needed)
;; At some point we should add the ability for Providers to be passed Pulumi configs or our config map?
;; Cloudflare and others may require or request a token.
outputs-to-fetch (reduce-kv
(fn [acc _provider-key data]
(let [stack-key (:stack data)
stack-ref (get stack-refs stack-key)
outputs (:outputs data)]
(reduce
(fn [m output-name]
(assoc m (keyword output-name) (.getOutput stack-ref output-name)))
acc
outputs)))
{}
needed-output-configs)
all-provider-inputs (pulumi/all (clj->js outputs-to-fetch))]
(.apply all-provider-inputs
(fn [values]
(js/Promise.
(fn [resolve _reject]
(let [resolved-outputs (js->clj values :keywordize-keys true)
instantiated-providers
(reduce
(fn [acc provider-key]
(if-let [template (get provider-templates provider-key)]
(let [constructor (:constructor template)
provider-name (:name template)
resolved-config (resolve-template (:config template) {} resolved-outputs)]
(assoc acc provider-key (new constructor provider-name (clj->js resolved-config))))
acc))
{}
providers-needed)
pre-deploy-results
(reduce-kv
(fn [acc provider-key provider-instance]
(if-let [rule-fn (get provider-rules provider-key)]
(let [rule-results (rule-fn {:resource-configs (:resource-configs stack-resources-definition)
:provider provider-instance})]
(assoc acc provider-key rule-results))
acc))
{}
instantiated-providers)]
(resolve
(deploy!
{:pulumi-cfg pulumi-cfg
:resource-configs (:resource-configs stack-resources-definition)
:all-providers instantiated-providers
:pre-deploy-deps pre-deploy-results})))))))))

View File

@@ -0,0 +1,48 @@
(ns utils.safe-fns)
(defn make-paths [& path-groups]
(mapcat (fn [{:keys [paths backend]}]
(mapv (fn [p]
{:path p
:pathType "Prefix"
:backend {:service backend}})
paths))
path-groups))
(defn make-listeners [domains-or-json]
(let [domains (if (string? domains-or-json)
(js->clj (js/JSON.parse domains-or-json))
domains-or-json)]
(vec
(mapcat
(fn [domain]
(let [clean-name (clojure.string/replace domain #"\." "-")
secret-name (str clean-name "-tls")]
[{:name (str "https-root-" clean-name)
:port 8443
:protocol "HTTPS"
:hostname domain
:tls {:mode "Terminate"
:certificateRefs [{:name secret-name}]}
:allowedRoutes {:namespaces {:from "All"}}
}
{:name (str "https-wild-" clean-name)
:port 8443
:protocol "HTTPS"
:hostname (str "*." domain)
:tls {:mode "Terminate"
:certificateRefs [{:name secret-name}]}
:allowedRoutes {:namespaces {:from "All"}}
}]))
domains))))
(def ^:public safe-fns
{'str str
'b64e (fn [s] (-> (.from js/Buffer s) (.toString "base64")))
'println #(js/console.log %)
'make-paths make-paths
'make-listeners make-listeners
'parse #(js->clj (js/JSON.parse %))})

View File

@@ -0,0 +1,406 @@
(ns utils.stack-processor
(:require
["@pulumi/kubernetes" :as k8s]
["@local/crds/gateway" :as gateway-api]
["@local/crds/cert_manager" :as cert-manager]
["@pulumi/pulumi" :as pulumi]
["@pulumi/vault" :as vault]
["@pulumiverse/harbor" :as harbor]
[utils.defaults :as default]
[utils.vault :as vault-utils]
[utils.general :refer [deep-merge new-resource resource-factory deploy-stack-factory iterate-stack]]
["@pulumi/docker" :as docker]
["@pulumi/docker-build" :as docker-build]
[clojure.walk :as walk]
[clojure.string :as str]
["path" :as path]
[configs :refer [cfg]]
[utils.k8s :as k8s-utils]
[utils.harbor :as harbor-utils]
[utils.docker :as docker-utils]
[utils.safe-fns :refer [safe-fns]])
(:require-macros [utils.general :refer [p-> build-registry]]))
#_(def component-specs-defs
{:k8s k8s-utils/component-specs-defs
:harbor harbor-utils/component-specs-defs
:docker docker-utils/component-specs-defs})
#_(def component-specs (build-registry component-specs-defs))
(defn safe-parse-int [s]
(let [n (js/parseInt s 10)]
(if (js/isNaN n) nil n)))
(defn string->int? [s]
(and (string? s)
(re-matches #"^-?\d+$" s)))
(defn- coerce-value [v]
(if (string->int? v)
(safe-parse-int v)
v))
(defn- is-output? [x] (some? (.-__pulumiOutput x)))
(defn resolve-template
[template secrets-map options-map]
(let [data (merge (js->clj options-map) (js->clj secrets-map))]
(walk/postwalk
(fn [x]
(cond
(and (list? x) (contains? safe-fns (first x)))
(let [f (get safe-fns (first x))
args (rest x)]
(if (some is-output? args)
(.apply (pulumi/all (clj->js args))
(fn [resolved-args]
(apply f (js->clj resolved-args))))
(apply f args)))
(and (list? x) (symbol? (first x)))
(cond
(= (first x) '->)
(let [[_ resource-key & steps] x
resource (get data resource-key)]
(if-not resource x
(reduce (fn [acc step]
(cond
(and (symbol? step) (str/starts-with? (name step) ".-"))
(let [prop-name (subs (name step) 2)]
(aget acc prop-name))
(and (list? step) (= (first step) 'get))
(get acc (second step) (nth step 2 nil))
:else acc))
resource
steps)))
(= (first x) 'get) (get data (second x) (nth x 2 nil))
(= (first x) 'get-in) (get-in data (second x) (nth x 2 nil))
:else x)
(symbol? x)
(if (contains? safe-fns x)
x
(let [kw (keyword x)]
(cond
(contains? data x) (coerce-value (get data x))
(contains? data kw) (coerce-value (get data kw))
:else x)))
:else x))
template)))
(defn generic-transform
"Takes a creator function and executes it with resolved arguments,
handling asynchronicity when secrets are present."
[creator-fn opts base-values secrets options]
(if (nil? secrets)
(let [final-args (clj->js (deep-merge base-values (resolve-template opts {} options)))]
(pulumi/output (creator-fn final-args)))
(.apply secrets
(fn [smap]
(let [m (js->clj smap :keywordize-keys true)
final-args (clj->js (deep-merge base-values (resolve-template opts m options)))]
(creator-fn final-args))))))
(def component-specs
{:vault {:provider-key :vault
:provider-deps [:k8s]}
;; K8s Resources
:k8s:namespace {:constructor (.. k8s -core -v1 -Namespace)
:provider-key :k8s
:defaults-fn (fn [env] ((get-in default/defaults [:k8s :namespace]) (:options env)))}
:k8s:secret {:constructor (.. k8s -core -v1 -Secret)
:provider-key :k8s
:defaults-fn (fn [env] ((get-in default/defaults [:k8s :secret]) (:options env)))}
:k8s:config-map {:constructor (.. k8s -core -v1 -ConfigMap)
:provider-key :k8s
:defaults-fn (fn [env] ((get-in default/defaults [:k8s :config-map]) (:options env)))}
:k8s:deployment {:constructor (.. k8s -apps -v1 -Deployment)
:provider-key :k8s
:defaults-fn (fn [env] ((get-in default/defaults [:k8s :deployment]) (:options env)))}
:k8s:service {:constructor (.. k8s -core -v1 -Service)
:provider-key :k8s
:defaults-fn (fn [env] ((get-in default/defaults [:k8s :service]) (:options env)))}
:k8s:ingress {:constructor (.. k8s -networking -v1 -Ingress)
:provider-key :k8s
:defaults-fn (fn [env] ((get-in default/defaults [:k8s :ingress]) (:options env)))}
:k8s:chart {:constructor (.. k8s -helm -v3 -Chart)
:provider-key :k8s
:defaults-fn (fn [env]
(deep-merge ((get-in default/defaults [:k8s :chart]) (:options env))
(update-in (get-in (:options env) [:k8s:chart-opts]) [:values]
#(deep-merge % (or (:yaml-values (:options env)) {})))))}
:k8s:storage-class {:constructor (.. k8s -storage -v1 -StorageClass)
:provider-key :k8s
:defaults-fn (fn [env] ((get-in default/defaults [:k8s :storage-class]) (:options env)))}
:k8s:pvc {:constructor (.. k8s -storage -v1 -PVC)
:provider-key :k8s
:defaults-fn (fn [env] ((get-in default/defaults [:k8s :pvc]) (:options env)))}
:k8s:gateway {:constructor (.. gateway-api -v1 -Gateway)
:provider-key :k8s
:defaults-fn (fn [env] ((get-in default/defaults [:k8s :gateway]) (:options env)))}
:k8s:httproute {:constructor (.. gateway-api -v1 -HTTPRoute)
:provider-key :k8s
:defaults-fn (fn [env] ((get-in default/defaults [:k8s :httproute]) (:options env)))}
:k8s:cluster-issuer {:constructor (.. cert-manager -v1 -ClusterIssuer)
:provider-key :k8s
:defaults-fn (fn [env] ((get-in default/defaults [:k8s :cluster-issuer]) (:options env)))}
:k8s:certificates
{:constructor (.. cert-manager -v1 -Certificate)
:provider-key :k8s
:defaults-fn (fn [env]
(let [{:keys [app-namespace is-prod?]} (:options env)]
(p-> env :options :vault:prepare "stringData" .-domains
#(vec
(for [domain (js/JSON.parse %)]
(let [clean-name (clojure.string/replace domain #"\." "-")]
{:_suffix clean-name
:metadata {:namespace app-namespace}
:spec {:dnsNames [domain (str "*." domain)]
:secretName (str clean-name "-tls")
:issuerRef {:name (if is-prod? "letsencrypt-prod" "letsencrypt-staging")
:kind "ClusterIssuer"}}}))))))}
:k8s:certificate
{:constructor (.. cert-manager -v1 -Certificate)
:provider-key :k8s
:defaults-fn (fn [env] ((get-in default/defaults [:k8s :certificate]) (:options env)))}
;; Docker Resources
:docker:image {:constructor (.. docker-build -Image)
:provider-key :docker
:defaults-fn (fn [env] ((get-in default/defaults [:docker :image]) (:options env)))}
;; Harbor Resources
:harbor:project {:constructor (.. harbor -Project)
:provider-key :harbor
:defaults-fn (fn [env] ((get-in default/defaults [:harbor :project]) (:options env)))}
:harbor:robot-account {:constructor (.. harbor -RobotAccount)
:provider-key :harbor
:defaults-fn (fn [env] ((get-in default/defaults [:harbor :robot-account]) (:options env)))}})
(defmulti deploy-resource
"Generic resource deployment multimethod.
Dispatches on the fully-qualified resource keyword.
Returns a map of {:resource (the-pulumi-resource) :common-opts-update {map-of-new-state}}."
(fn [dispatch-key _config] dispatch-key))
(defmethod deploy-resource :default
[dispatch-key full-config]
(if-let [spec (get component-specs dispatch-key)]
(let [app-name (:app-name full-config)
dependsOn (:dependsOn full-config)
provider-key (:provider-key spec)
provider (get full-config provider-key)
resource-class (:constructor spec)
opts-key (keyword (str (name dispatch-key) "-opts"))
component-opts (get full-config opts-key)
env {:options full-config :secrets (:secrets full-config) :component-opts component-opts}
raw-defaults (when-let [df (:defaults-fn spec)] (df env))]
(if resource-class
(let [base-creator (fn [final-args suffix]
(let [final-name (if suffix
(str app-name "-" suffix)
app-name)]
(new-resource resource-class
final-name
final-args
provider
dependsOn)))]
{:resource
(p-> raw-defaults
#(let [defaults-list (if (vector? %)
%
[%])
is-multi? (vector? %) resources
(doall
(map-indexed
(fn [idx item]
(let [suffix (cond
(:_suffix item) (:_suffix item)
is-multi? (str idx)
:else nil)
clean-item (dissoc item :_suffix)
item-creator (fn [resolved-args]
(base-creator resolved-args suffix))]
(generic-transform item-creator
component-opts
clean-item
(:secrets env)
full-config)))
defaults-list))]
(if is-multi? resources (first resources))))})
(throw (js/Error. (str "No :constructor found for spec: " dispatch-key)))))
(throw (js/Error. (str "Unknown resource: " dispatch-key)))))
(defmethod deploy-resource :vault:prepare
[_ config]
(let [prepare-opts (get config :vault:prepare-opts {})
defaults {:provider (:k8s config)
:vault-provider (:vault config)
:app-name (:app-name config)
:app-namespace (:app-namespace config)
:load-yaml (get config :vault-load-yaml false)}
final-args (merge defaults prepare-opts)
prepared-vault-data (try
(vault-utils/prepare final-args)
(catch js/Error e
(js/console.error "!!! Error in :vault:prepare :" e)
nil))]
{:common-opts-update prepared-vault-data
:resource (:bind-secrets prepared-vault-data)}))
(defmethod deploy-resource :vault:retrieve
[_ config]
(let [retrieve-opts (get config :vault:retrieve-opts {})
defaults {:vault-provider (:vault config)
:app-name (:app-name config)
:app-namespace (:app-namespace config)}
final-args (merge defaults retrieve-opts)
retrieved-data (try
(vault-utils/retrieve (:vault-provider final-args)
(:app-name final-args)
(:app-namespace final-args))
(catch js/Error e
(js/console.error " Error in :vault:retrieve :" e)
nil))]
{:common-opts-update retrieved-data}))
;; https://www.pulumi.com/docs/iac/concepts/resources/dynamic-providers/
(defmethod deploy-resource :generic:execute
[_ full-config]
(let [app-name (:app-name full-config)
dependsOn (:dependsOn full-config)
component-opts (assoc (:execute-opts full-config)
:pulumi-cfg (:pulumi-cfg full-config)
:secrets (:secrets full-config)
)
defaults {}
exec-fn (:exec-fn full-config)
resource-id (str app-name "-exec")
provider #js {:create (fn [inputs-js]
(js/Promise.
(fn [resolve _reject]
(resolve
#js {:id resource-id
:outs inputs-js}))))
:delete (fn [id old-inputs-js]
(js/Promise.resolve))
:update (fn [id old-inputs-js new-inputs-js]
(js/Promise.
(fn [resolve _reject]
(resolve #js {:outs new-inputs-js}))))}
gen (generic-transform #(clj->js (exec-fn (js->clj % :keywordize-keys true))) component-opts defaults (:secrets full-config) full-config)
creator-fn (fn [inputs]
(pulumi/dynamic.Resource.
provider
resource-id
inputs
(clj->js {:dependsOn (vec dependsOn)})))
resource (.apply gen #(creator-fn %))]
{:resource resource}))
(defn handle-keyword-item [last-resource item config common-opts]
(let [dispatch-key item
depends-on (when last-resource [last-resource])
final-config (merge config common-opts {:dependsOn depends-on})
result-map (deploy-resource dispatch-key final-config)
resource (:resource result-map)
opts-update (:common-opts-update result-map)
resource-update (when resource {dispatch-key resource})]
[resource
resource-update
(merge common-opts opts-update resource-update)]))
(defn handle-list-item [last-resource item config common-opts]
(let [provider-key (first item)
resource-keys (rest item)
nested-result
(reduce
(fn [nested-acc resource-key]
(let [inner-last-resource (get nested-acc :last-resource)
inner-resources-map (get nested-acc :resources)
inner-common-opts (get nested-acc :common-opts)
dispatch-key (keyword (str (name provider-key) ":" (name resource-key)))
[new-resource new-resource-map new-common-opts]
(handle-keyword-item inner-last-resource dispatch-key config inner-common-opts)]
{:last-resource (or new-resource inner-last-resource)
:resources (merge inner-resources-map new-resource-map)
:common-opts new-common-opts}))
{:last-resource last-resource
:resources {}
:common-opts common-opts}
resource-keys)]
[(:last-resource nested-result)
(:resources nested-result)
(:common-opts nested-result)]))
(defn process-stack
"Recursively processes a stack configuration, building a dependency chain.
Returns a map of all created resources keyed by their dispatch keyword."
[stack-items config initial-common-opts]
(let [result
(reduce
(fn [acc item]
(let [{:keys [last-resource common-opts]} acc
[new-resource new-resources new-common-opts]
(if (keyword? item)
(handle-keyword-item last-resource item config common-opts)
(handle-list-item last-resource item config common-opts))]
{:last-resource (or new-resource last-resource)
:resources-map (merge (:resources-map acc) new-resources)
:common-opts new-common-opts}))
{:last-resource nil
:resources-map {}
:common-opts initial-common-opts}
stack-items)]
(:resources-map result)))
(defn deploy! [{:keys [pulumi-cfg resource-configs all-providers]}]
(let [
deployment-results
(into
{}
(for [config resource-configs]
(let [
{:keys [stack app-name]} config
_ (when (nil? config)
(throw (js/Error. "Resource configs contain a nil value!")))
common-opts (merge
all-providers
(select-keys config [:app-name :app-namespace])
{:pulumi-cfg pulumi-cfg})
]
[app-name (process-stack stack config common-opts)])))
]
(clj->js deployment-results)))

71
src/main/utils/vault.cljs Normal file
View File

@@ -0,0 +1,71 @@
(ns utils.vault
(:require
["@pulumi/kubernetes" :as k8s]
["@pulumi/pulumi" :as pulumi]
["@pulumi/vault" :as vault]
["fs" :as fs]
["js-yaml" :as yaml]
["path" :as path]
[configs :refer [cfg]]))
(defn get-secret-val
"Extract a specific key from a Vault secret Output/Promise."
[secret-promise key]
(.then secret-promise #(aget (.-data %) key)))
(defn initialize-mount [vault-provider vault-path service-name]
(let [service-secrets (into {} (get (-> cfg :secrets-json) (keyword service-name)))]
(new (.. vault -generic -Secret)
(str service-name "-secret")
(clj->js {:path (str vault-path)
:dataJson (js/JSON.stringify (clj->js service-secrets))})
(clj->js {:provider vault-provider}))))
(defn prepare
"Prepares common resources and values for a deployment from a single config map."
[config]
(let [{:keys [provider vault-provider app-name app-namespace load-yaml]} config
values-path (.join path js/__dirname ".." (-> cfg :resource-path) (str app-name ".yml"))]
(let [yaml-values (when load-yaml
(js->clj (-> values-path
(fs/readFileSync "utf8")
(yaml/load))
:keywordize-keys true))
{:keys [secrets-data bind-secrets]}
(when vault-provider
(let [vault-path (str "secret/" app-name)
_ (initialize-mount vault-provider vault-path app-name)
secrets (pulumi/output (.getSecret (.-generic vault)
(clj->js {:path vault-path})
(clj->js {:provider vault-provider})))
secrets-data (.apply secrets #(.. % -data))
bind-secrets (when (and provider app-namespace)
(new (.. k8s -core -v1 -Secret) (str app-name "-secrets")
(clj->js {:metadata {:name (str app-name "-secrets")
:namespace app-namespace}
:stringData secrets-data})
(clj->js {:provider provider})))]
{:secrets-data secrets-data
:bind-secrets bind-secrets}))]
{:secrets secrets-data
:yaml-path values-path
:yaml-values yaml-values
:bind-secrets bind-secrets})))
(defn retrieve [vault-provider app-name]
(let [vault-path (str "secret/" app-name)
secrets (pulumi/output (.getSecret (.-generic vault)
(clj->js {:path vault-path})
(clj->js {:provider vault-provider})))
secrets-data (.apply secrets #(.. % -data))]
{:secrets secrets-data}))
(def provider-template
{:constructor (.. vault -Provider)
:name "vault-provider"
:config {:address 'vaultAddress
:token 'vaultToken}})