Revise to be more modular and reduce code reuse
This commit is contained in:
@@ -4,134 +4,21 @@
|
|||||||
["@pulumi/pulumi" :as pulumi]
|
["@pulumi/pulumi" :as pulumi]
|
||||||
["@pulumi/vault" :as vault]
|
["@pulumi/vault" :as vault]
|
||||||
[utils.vault :as vault-utils]
|
[utils.vault :as vault-utils]
|
||||||
|
[utils.general :refer [generic-transform deep-merge new-resource]]
|
||||||
["@pulumi/docker" :as docker]
|
["@pulumi/docker" :as docker]
|
||||||
["path" :as path]
|
["path" :as path]
|
||||||
[clojure.walk :as walk]
|
|
||||||
[configs :refer [cfg]]))
|
[configs :refer [cfg]]))
|
||||||
|
|
||||||
(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)))]
|
|
||||||
(creator-fn final-args))))))
|
|
||||||
|
|
||||||
(defn new-resource [resource-type resource-name final-args provider dependencies]
|
|
||||||
(new resource-type resource-name
|
|
||||||
(clj->js final-args)
|
|
||||||
(clj->js {:provider provider
|
|
||||||
:enableServerSideApply false
|
|
||||||
:dependsOn dependencies})))
|
|
||||||
|
|
||||||
|
|
||||||
(defn default-ingress [{:keys [app-name app-namespace host image-port ingress-opts]}]
|
(defn default-ingress [{:keys [app-name app-namespace host image-port ingress-opts]}]
|
||||||
{:metadata {:name app-name
|
{:metadata {:name app-name
|
||||||
:namespace app-namespace
|
:namespace app-namespace
|
||||||
:annotations {"caddy.ingress.kubernetes.io/snippet"
|
:annotations {"caddy.ingress.kubernetes.io/tls.issuer" "cloudflare"
|
||||||
(str "tls {\n issuer cloudflare\n dns cloudflare {env.CLOUDFLARE_API_TOKEN}\n}\n"
|
"caddy.ingress.kubernetes.io/tls.dns_provider" "cloudflare"
|
||||||
(:caddy-snippet ingress-opts))}}
|
"caddy.ingress.kubernetes.io/tls.dns_provider.credentials_secret" "caddy-ingress-controller-secrets"
|
||||||
|
"caddy.ingress.kubernetes.io/tls.dns_provider.credentials_secret_namespace" "caddy-system"
|
||||||
|
"caddy.ingress.kubernetes.io/tls.issuer.acme_ca" "https://acme-v02.api.letsencrypt.org/directory"
|
||||||
|
"caddy.ingress.kubernetes.io/snippet" (:caddy-snippet ingress-opts)}}
|
||||||
:spec {:ingressClassName "caddy"
|
:spec {:ingressClassName "caddy"
|
||||||
:rules [{:host host
|
:rules [{:host host
|
||||||
:http {:paths [{:path "/"
|
:http {:paths [{:path "/"
|
||||||
@@ -144,6 +31,17 @@
|
|||||||
:namespace app-namespace
|
:namespace app-namespace
|
||||||
:transformations []})
|
:transformations []})
|
||||||
|
|
||||||
|
(defn default-config-map [{:keys [app-name app-namespace]}]
|
||||||
|
{:metadata {:namespace app-namespace
|
||||||
|
:name app-name}
|
||||||
|
:data {}})
|
||||||
|
|
||||||
|
(defn default-service [{:keys [app-name app-namespace app-labels image-port]}]
|
||||||
|
{:metadata {:namespace app-namespace
|
||||||
|
:name app-name}
|
||||||
|
:spec {:selector app-labels
|
||||||
|
:ports [{:port 80 :targetPort image-port}]}})
|
||||||
|
|
||||||
(defn default-deployment [{:keys [app-name app-namespace app-labels image image-port]}]
|
(defn default-deployment [{:keys [app-name app-namespace app-labels image image-port]}]
|
||||||
{:metadata {:namespace app-namespace
|
{:metadata {:namespace app-namespace
|
||||||
:name app-name}
|
:name app-name}
|
||||||
@@ -171,45 +69,43 @@
|
|||||||
{:metadata {:name (str app-name "-secrets")
|
{:metadata {:name (str app-name "-secrets")
|
||||||
:namespace app-namespace}})
|
:namespace app-namespace}})
|
||||||
|
|
||||||
(defn default-service [{:keys [app-name app-namespace app-labels image-port]}]
|
|
||||||
{:metadata {:namespace app-namespace
|
|
||||||
:name app-name}
|
|
||||||
:spec {:selector app-labels
|
|
||||||
:ports [{:port 80 :targetPort image-port}]}})
|
|
||||||
|
|
||||||
(defn default-storage-class [{:keys [app-name]}]
|
(defn default-storage-class [{:keys [app-name]}]
|
||||||
{:metadata {:name app-name}})
|
{:metadata {:name app-name}})
|
||||||
|
|
||||||
|
(defn create-resource [resource-type provider app-name dependencies opts]
|
||||||
|
(let [resource-class (case resource-type
|
||||||
|
:docker-image (.. docker -Image)
|
||||||
|
:ingress (.. k8s -networking -v1 -Ingress)
|
||||||
|
:secret (.. k8s -core -v1 -Secret)
|
||||||
|
:namespace (.. k8s -core -v1 -Namespace)
|
||||||
|
:deployment (.. k8s -apps -v1 -Deployment)
|
||||||
|
:service (.. k8s -core -v1 -Service)
|
||||||
|
:chart (.. k8s -helm -v3 -Chart)
|
||||||
|
:config-map (.. k8s -core -v1 -ConfigMap)
|
||||||
|
:storage-class (.. k8s -storage -v1 -StorageClass)
|
||||||
|
(throw (js/Error. (str "Unknown resource type: " resource-type))))]
|
||||||
|
(new-resource resource-class app-name opts provider dependencies)))
|
||||||
|
|
||||||
|
(defn create-component
|
||||||
|
"Checks if a component is requested and, if so, creates it using generic-transform."
|
||||||
|
[requested-components
|
||||||
|
resource-type
|
||||||
|
provider
|
||||||
|
app-name
|
||||||
|
dependencies
|
||||||
|
component-opts
|
||||||
|
defaults
|
||||||
|
secrets
|
||||||
|
options]
|
||||||
|
|
||||||
(defn create-ingress [provider app-name dependencies ingress-opts]
|
(when (requested-components resource-type)
|
||||||
(new-resource (.. k8s -networking -v1 -Ingress) app-name ingress-opts provider dependencies))
|
(generic-transform
|
||||||
|
(fn [final-args]
|
||||||
(defn create-secret [provider app-name dependencies secret-opts]
|
(create-resource resource-type provider app-name dependencies final-args))
|
||||||
(new-resource (.. k8s -core -v1 -Secret) app-name secret-opts provider dependencies))
|
component-opts
|
||||||
|
defaults
|
||||||
(defn create-image [app-name image-opts]
|
secrets
|
||||||
(new (.. docker -Image) app-name (clj->js image-opts)))
|
options)))
|
||||||
|
|
||||||
(defn create-namespace [provider app-namespace dependencies ns-options]
|
|
||||||
(new-resource (.. k8s -core -v1 -Namespace) app-namespace ns-options provider dependencies))
|
|
||||||
|
|
||||||
(defn create-deployment [provider app-name dependencies deployment-opts]
|
|
||||||
(new-resource (.. k8s -apps -v1 -Deployment) app-name deployment-opts provider dependencies))
|
|
||||||
|
|
||||||
(defn create-service [provider app-name dependencies service-opts]
|
|
||||||
(new-resource (.. k8s -core -v1 -Service) app-name service-opts provider dependencies))
|
|
||||||
|
|
||||||
(defn create-chart [provider app-name dependencies chart-opts]
|
|
||||||
(new-resource (.. k8s -helm -v3 -Chart) app-name chart-opts provider dependencies))
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
(defn create-storage-class [provider app-name dependencies storage-opts]
|
|
||||||
(new-resource (.. k8s -storage -v1 -StorageClass) app-name storage-opts provider dependencies))
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
(defn deploy-stack
|
(defn deploy-stack
|
||||||
"Deploys a versatile stack of K8s resources, including optional Helm charts."
|
"Deploys a versatile stack of K8s resources, including optional Helm charts."
|
||||||
@@ -218,7 +114,7 @@
|
|||||||
requested-components (set component-kws)
|
requested-components (set component-kws)
|
||||||
|
|
||||||
{:keys [provider vault-provider pulumi-cfg app-namespace app-name image image-port vault-load-yaml exec-fn
|
{:keys [provider vault-provider pulumi-cfg app-namespace app-name image image-port vault-load-yaml exec-fn
|
||||||
storage-class-opts secret-opts ns-opts image-opts ingress-opts service-opts deployment-opts chart-opts]
|
storage-class-opts secret-opts config-map-opts ns-opts image-opts ingress-opts service-opts deployment-opts chart-opts]
|
||||||
:or {vault-load-yaml false image-port 80}} options
|
:or {vault-load-yaml false image-port 80}} options
|
||||||
options (merge options {:app-labels {:app app-name} :image-port image-port})
|
options (merge options {:app-labels {:app app-name} :image-port image-port})
|
||||||
|
|
||||||
@@ -230,104 +126,26 @@
|
|||||||
:load-yaml vault-load-yaml}))
|
:load-yaml vault-load-yaml}))
|
||||||
|
|
||||||
{:keys [secrets yaml-values bind-secrets]} (or prepared-vault-data {:secrets nil :yaml-values nil :bind-secrets nil})
|
{:keys [secrets yaml-values bind-secrets]} (or prepared-vault-data {:secrets nil :yaml-values nil :bind-secrets nil})
|
||||||
|
|
||||||
host (when secrets (.apply secrets #(aget % "host")))
|
host (when secrets (.apply secrets #(aget % "host")))
|
||||||
|
|
||||||
image (if (some? image) image (str (-> cfg :docker-repo) "/" app-name ":latest"))
|
ns (create-component requested-components :namespace provider app-namespace nil ns-opts (default-namespace options) secrets options)
|
||||||
|
docker-image (create-component requested-components :docker-image nil nil nil image-opts (default-image options) secrets options)
|
||||||
ns
|
secret (create-component requested-components :secret provider app-name nil secret-opts (default-secret options) secrets options)
|
||||||
(when (requested-components :namespace)
|
config-map (create-component requested-components :config-map provider app-name nil config-map-opts (default-config-map options) secrets options)
|
||||||
(generic-transform
|
storage-class (create-component requested-components :storage-class provider app-name nil storage-class-opts (default-storage-class options) secrets options)
|
||||||
(fn [final-args]
|
deployment (create-component requested-components :deployment provider app-name (vec (filter some? [ns docker-image bind-secrets])) deployment-opts (default-deployment options) secrets options)
|
||||||
(create-namespace provider app-namespace nil final-args))
|
service (create-component requested-components :service provider app-name (vec (filter some? [ns deployment bind-secrets])) service-opts (default-service options) secrets options)
|
||||||
ns-opts
|
chart (create-component requested-components :chart provider app-name
|
||||||
(default-namespace options)
|
|
||||||
secrets
|
|
||||||
options))
|
|
||||||
|
|
||||||
docker-image
|
|
||||||
(when (requested-components :docker-image)
|
|
||||||
(generic-transform
|
|
||||||
(fn [final-args]
|
|
||||||
(create-image app-name final-args))
|
|
||||||
image-opts
|
|
||||||
(default-image options)
|
|
||||||
secrets
|
|
||||||
options))
|
|
||||||
|
|
||||||
secret
|
|
||||||
(when (requested-components :secret)
|
|
||||||
(generic-transform
|
|
||||||
(fn [final-args]
|
|
||||||
(create-secret provider app-name nil
|
|
||||||
final-args))
|
|
||||||
storage-class-opts
|
|
||||||
(default-secret options)
|
|
||||||
secrets
|
|
||||||
options))
|
|
||||||
|
|
||||||
storage-class
|
|
||||||
(when (requested-components :storage-class)
|
|
||||||
(generic-transform
|
|
||||||
(fn [final-args]
|
|
||||||
(create-storage-class provider app-name nil
|
|
||||||
final-args))
|
|
||||||
storage-class-opts
|
|
||||||
(default-storage-class options)
|
|
||||||
secrets
|
|
||||||
options))
|
|
||||||
|
|
||||||
chart (when (requested-components :chart)
|
|
||||||
(let [chart-base-values (deep-merge (default-chart options)
|
|
||||||
(update-in chart-opts [:values] #(deep-merge % (or yaml-values {}))))]
|
|
||||||
(generic-transform
|
|
||||||
(fn [final-args]
|
|
||||||
(create-chart provider app-name
|
|
||||||
(vec (filter some? [ns docker-image bind-secrets]))
|
(vec (filter some? [ns docker-image bind-secrets]))
|
||||||
final-args))
|
|
||||||
chart-opts
|
chart-opts
|
||||||
chart-base-values
|
(deep-merge (default-chart options)
|
||||||
|
(update-in chart-opts [:values] #(deep-merge % (or yaml-values {}))))
|
||||||
secrets
|
secrets
|
||||||
options)))
|
options)
|
||||||
|
ingress (create-component requested-components :ingress provider app-name (vec (filter some? [service chart bind-secrets])) ingress-opts (default-ingress (assoc options :host host)) secrets options)
|
||||||
deployment
|
|
||||||
(when (requested-components :deployment)
|
|
||||||
(generic-transform
|
|
||||||
(fn [final-args]
|
|
||||||
(create-deployment provider app-name
|
|
||||||
(vec (filter some? [ns docker-image bind-secrets]))
|
|
||||||
final-args))
|
|
||||||
deployment-opts
|
|
||||||
(default-deployment options)
|
|
||||||
secrets
|
|
||||||
options))
|
|
||||||
|
|
||||||
service
|
|
||||||
(when (requested-components :service)
|
|
||||||
(generic-transform
|
|
||||||
(fn [final-args]
|
|
||||||
(create-service provider app-name
|
|
||||||
(vec (filter some? [ns deployment bind-secrets]))
|
|
||||||
final-args))
|
|
||||||
service-opts
|
|
||||||
(default-service options)
|
|
||||||
secrets
|
|
||||||
options))
|
|
||||||
|
|
||||||
ingress (when (requested-components :ingress)
|
|
||||||
(generic-transform
|
|
||||||
(fn [final-args]
|
|
||||||
(create-ingress provider app-name
|
|
||||||
(vec (filter some? [service chart bind-secrets]))
|
|
||||||
final-args))
|
|
||||||
ingress-opts
|
|
||||||
(default-ingress (assoc options :host host))
|
|
||||||
secrets
|
|
||||||
options))
|
|
||||||
|
|
||||||
execute (when (requested-components :execute)
|
execute (when (requested-components :execute)
|
||||||
(exec-fn (assoc options :dependencies (vec (filter some? [chart ns secret storage-class deployment service ingress docker-image])))))]
|
(exec-fn (assoc options :dependencies (vec (filter some? [chart ns secret storage-class deployment service ingress docker-image])))))]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
{:namespace ns, :vault-secrets prepared-vault-data, :secret secret, :docker-image docker-image :storage-class storage-class, :chart chart, :deployment deployment, :service service, :ingress ingress :execute execute}))
|
{:namespace ns, :vault-secrets prepared-vault-data, :secret secret, :config-map config-map, :docker-image docker-image :storage-class storage-class, :chart chart, :deployment deployment, :service service, :ingress ingress :execute execute}))
|
||||||
Reference in New Issue
Block a user