Move all files to root
This commit is contained in:
66
src/main/base.cljs
Normal file
66
src/main/base.cljs
Normal 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
32
src/main/configs.cljs
Normal 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
104
src/main/core.cljs
Normal 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
60
src/main/infra/dns.cljs
Normal 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
172
src/main/infra/init.cljs
Normal 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
317
src/main/infra/openbao.cljs
Normal 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))
|
||||
18
src/main/k8s/add_ons/cert_manager.cljs
Normal file
18
src/main/k8s/add_ons/cert_manager.cljs
Normal 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)}}]}}}
|
||||
})
|
||||
16
src/main/k8s/add_ons/csi_driver/hetzner.cljs
Normal file
16
src/main/k8s/add_ons/csi_driver/hetzner.cljs
Normal 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"}}}}}})
|
||||
26
src/main/k8s/add_ons/csi_driver/wasabi.cljs
Normal file
26
src/main/k8s/add_ons/csi_driver/wasabi.cljs
Normal 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})
|
||||
20
src/main/k8s/add_ons/gateway/traefik.cljs
Normal file
20
src/main/k8s/add_ons/gateway/traefik.cljs
Normal 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)}}})
|
||||
74
src/main/k8s/add_ons/image_registry/harbor.cljs
Normal file
74
src/main/k8s/add_ons/image_registry/harbor.cljs
Normal 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"}}})
|
||||
27
src/main/k8s/add_ons/ingress_controller/caddy.cljs
Normal file
27
src/main/k8s/add_ons/ingress_controller/caddy.cljs
Normal 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}}}}})
|
||||
27
src/main/k8s/add_ons/minio.cljs
Normal file
27
src/main/k8s/add_ons/minio.cljs
Normal 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"}}}})
|
||||
59
src/main/k8s/add_ons/proxy.cljs
Normal file
59
src/main/k8s/add_ons/proxy.cljs
Normal 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}]}}})
|
||||
|
||||
28
src/main/k8s/add_ons/s3proxy.cljs
Normal file
28
src/main/k8s/add_ons/s3proxy.cljs
Normal 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"}}}}}})
|
||||
9
src/main/k8s/add_ons/secret_replicator.cljs
Normal file
9
src/main/k8s/add_ons/secret_replicator.cljs
Normal 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"}}})
|
||||
33
src/main/k8s/preparers/harbor.cljs
Normal file
33
src/main/k8s/preparers/harbor.cljs
Normal 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"}})
|
||||
14
src/main/k8s/services/act_runner/service.cljs
Normal file
14
src/main/k8s/services/act_runner/service.cljs
Normal 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]}}
|
||||
})
|
||||
24
src/main/k8s/services/foundryvtt/service.cljs
Normal file
24
src/main/k8s/services/foundryvtt/service.cljs
Normal 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]}}})
|
||||
25
src/main/k8s/services/gitea/service.cljs
Normal file
25
src/main/k8s/services/gitea/service.cljs
Normal 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]}}})
|
||||
12
src/main/k8s/services/matrix/cinny/service.cljs
Normal file
12
src/main/k8s/services/matrix/cinny/service.cljs
Normal 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")}]}}}}})
|
||||
13
src/main/k8s/services/matrix/database/service.cljs
Normal file
13
src/main/k8s/services/matrix/database/service.cljs
Normal 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")}]}}}}})
|
||||
14
src/main/k8s/services/matrix/element/service.cljs
Normal file
14
src/main/k8s/services/matrix/element/service.cljs
Normal 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")}]}}}}})
|
||||
18
src/main/k8s/services/matrix/element_call/service.cljs
Normal file
18
src/main/k8s/services/matrix/element_call/service.cljs
Normal 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"}}]}}}}})
|
||||
10
src/main/k8s/services/matrix/home_server/service.cljs
Normal file
10
src/main/k8s/services/matrix/home_server/service.cljs
Normal 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")}]}}}}})
|
||||
8
src/main/k8s/services/matrix/livekit_jwt/service.cljs
Normal file
8
src/main/k8s/services/matrix/livekit_jwt/service.cljs
Normal 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")}]}}}}})
|
||||
16
src/main/k8s/services/matrix/livekit_server/service.cljs
Normal file
16
src/main/k8s/services/matrix/livekit_server/service.cljs
Normal 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")}]}}}}})
|
||||
10
src/main/k8s/services/matrix/mautrix_discord/service.cljs
Normal file
10
src/main/k8s/services/matrix/mautrix_discord/service.cljs
Normal 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")}]}}}}})
|
||||
21
src/main/k8s/services/matrix/mmr/service.cljs
Normal file
21
src/main/k8s/services/matrix/mmr/service.cljs
Normal 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
|
||||
14
src/main/k8s/services/matrix/turn/service.cljs
Normal file
14
src/main/k8s/services/matrix/turn/service.cljs
Normal 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")}]}}}}})
|
||||
34
src/main/k8s/services/mesite/service.cljs
Normal file
34
src/main/k8s/services/mesite/service.cljs
Normal 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]}}
|
||||
})
|
||||
17
src/main/k8s/services/nextcloud/service.cljs
Normal file
17
src/main/k8s/services/nextcloud/service.cljs
Normal 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)))}})
|
||||
10
src/main/k8s/services/productive/service.cljs
Normal file
10
src/main/k8s/services/productive/service.cljs
Normal 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]}}
|
||||
})
|
||||
26
src/main/k8s/services/prometheus/service.cljs
Normal file
26
src/main/k8s/services/prometheus/service.cljs
Normal 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"}}}}}}}}}})
|
||||
25
src/main/k8s/services/renovate/service.cljs
Normal file
25
src/main/k8s/services/renovate/service.cljs
Normal 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)))}})
|
||||
76
src/main/service_registries.cljs
Normal file
76
src/main/service_registries.cljs
Normal 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 [])
|
||||
12
src/main/utils/defaults.cljs
Normal file
12
src/main/utils/defaults.cljs
Normal 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})
|
||||
25
src/main/utils/docker.cljs
Normal file
25
src/main/utils/docker.cljs
Normal 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]}}})
|
||||
82
src/main/utils/general.clj
Normal file
82
src/main/utils/general.clj
Normal 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
232
src/main/utils/general.cljs
Normal 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)))
|
||||
36
src/main/utils/harbor.cljs
Normal file
36
src/main/utils/harbor.cljs
Normal 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
198
src/main/utils/k8s.cljs
Normal 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])))))
|
||||
135
src/main/utils/providers.cljs
Normal file
135
src/main/utils/providers.cljs
Normal 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})))))))))
|
||||
48
src/main/utils/safe_fns.cljs
Normal file
48
src/main/utils/safe_fns.cljs
Normal 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 %))})
|
||||
406
src/main/utils/stack_processor.cljs
Normal file
406
src/main/utils/stack_processor.cljs
Normal 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
71
src/main/utils/vault.cljs
Normal 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}})
|
||||
Reference in New Issue
Block a user