Swap to dynamic parsing of secrets using symbols to delay resolution enabling complete fluid templating of resources
This commit is contained in:
@@ -6,6 +6,7 @@
|
||||
[utils.vault :as vault-utils]
|
||||
["@pulumi/docker" :as docker]
|
||||
["path" :as path]
|
||||
[clojure.walk :as walk]
|
||||
[configs :refer [cfg]]))
|
||||
|
||||
(defn assoc-ins [m path-vals]
|
||||
@@ -35,25 +36,6 @@
|
||||
(merge-by-name a b)
|
||||
:else b))
|
||||
|
||||
(defn generic-make-transformer
|
||||
"Returns a Pulumi-compatible transformer that unwraps Output values via .apply."
|
||||
[f]
|
||||
(fn [{:keys [base-values app-name function-keys]}]
|
||||
(.apply function-keys
|
||||
(fn [smap]
|
||||
(let [m (js->clj smap :keywordize-keys true)
|
||||
updates (f {:app-name app-name
|
||||
:function-keys m})
|
||||
result (clj->js (merge base-values updates))]
|
||||
result)))))
|
||||
|
||||
(defn resolve-template [template values]
|
||||
(clojure.walk/postwalk
|
||||
(fn [x]
|
||||
(if (and (keyword? x) (contains? values x))
|
||||
(get values x)
|
||||
x))
|
||||
template))
|
||||
(defn make-transformer
|
||||
"Given f that takes {:app-name .. :secrets ..}, where :secrets is a plain map
|
||||
(already unwrapped inside .apply), return a Helm transformer."
|
||||
@@ -67,6 +49,75 @@
|
||||
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)
|
||||
@@ -75,72 +126,90 @@
|
||||
:dependsOn dependencies})))
|
||||
|
||||
|
||||
(defn create-secret [provider app-namespace app-name dependencies secret-options]
|
||||
(let [base-args {:metadata {:name (str app-name "-secrets")
|
||||
:namespace app-namespace}}
|
||||
final-args (deep-merge base-args secret-options)]
|
||||
(new-resource (.. k8s -core -v1 -Secret) app-name final-args provider dependencies)))
|
||||
(defn default-ingress [{:keys [app-name app-namespace host image-port ingress-opts]}]
|
||||
{:metadata {:name app-name
|
||||
:namespace app-namespace
|
||||
:annotations {"caddy.ingress.kubernetes.io/snippet"
|
||||
(str "tls {\n issuer cloudflare\n dns cloudflare {env.CLOUDFLARE_API_TOKEN}\n}\n"
|
||||
(:caddy-snippet ingress-opts))}}
|
||||
:spec {:ingressClassName "caddy"
|
||||
:rules [{:host host
|
||||
:http {:paths [{:path "/"
|
||||
:pathType "Prefix"
|
||||
:backend {:service {:name app-name
|
||||
:port {:number image-port}}}}]}}]}})
|
||||
|
||||
(defn create-image [app-name image-options]
|
||||
(defn default-chart [{:keys [app-name app-namespace]}]
|
||||
{:chart app-name
|
||||
:namespace app-namespace
|
||||
:transformations []})
|
||||
|
||||
(defn default-deployment [{:keys [app-name app-namespace app-labels image image-port]}]
|
||||
{:metadata {:namespace app-namespace
|
||||
:name app-name}
|
||||
:spec {:selector {:matchLabels app-labels}
|
||||
:replicas 1
|
||||
:template {:metadata {:labels app-labels}
|
||||
:spec {:containers
|
||||
[{:name app-name
|
||||
:image image
|
||||
:ports [{:containerPort image-port}]}]}}}})
|
||||
|
||||
(defn default-image [{:keys [app-name]}]
|
||||
(let [context-path (.. path (join "." (-> cfg :resource-path)))
|
||||
dockerfile-path (.. path (join context-path (str app-name ".dockerfile")))
|
||||
base-args {:build {:context context-path
|
||||
:dockerfile dockerfile-path}
|
||||
:imageName (str (-> cfg :docker-repo) "/" app-name ":latest")}
|
||||
final-args (deep-merge base-args image-options)]
|
||||
(new (.. docker -Image) app-name (clj->js final-args))))
|
||||
:imageName (str (-> cfg :docker-repo) "/" app-name ":latest")}]
|
||||
base-args))
|
||||
|
||||
(defn create-namespace [provider app-namespace dependencies ns-options]
|
||||
(let [base-args {:metadata {:name app-namespace}}
|
||||
final-args (deep-merge base-args ns-options)]
|
||||
(new-resource (.. k8s -core -v1 -Namespace) app-namespace final-args provider dependencies)))
|
||||
|
||||
(defn create-deployment [provider app-namespace app-name app-labels image-name image-port dependencies deployment-options]
|
||||
(let [base-args {:metadata {:namespace app-namespace
|
||||
:name app-name}
|
||||
:spec {:selector {:matchLabels app-labels}
|
||||
:replicas 1
|
||||
:template {:metadata {:labels app-labels}
|
||||
:spec {:containers
|
||||
[{:name app-name
|
||||
:image image-name
|
||||
:ports [{:containerPort image-port}]}]}}}}
|
||||
final-args (deep-merge base-args deployment-options)]
|
||||
(new-resource (.. k8s -apps -v1 -Deployment) app-name final-args provider dependencies)))
|
||||
(defn default-namespace [{:keys [app-namespace]}]
|
||||
{:metadata {:name app-namespace}})
|
||||
|
||||
(defn create-service [provider app-namespace app-name app-labels image-port dependencies service-options]
|
||||
(let [base-args {:metadata {:namespace app-namespace
|
||||
:name app-name}
|
||||
:spec {:selector app-labels
|
||||
:ports [{:port 80 :targetPort image-port}]}}
|
||||
final-args (deep-merge base-args service-options)]
|
||||
(new-resource (.. k8s -core -v1 -Service) app-name final-args provider dependencies)))
|
||||
(defn default-secret [{:keys [app-name app-namespace]}]
|
||||
{:metadata {:name (str app-name "-secrets")
|
||||
: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]}]
|
||||
{:metadata {:name app-name}})
|
||||
|
||||
|
||||
|
||||
(defn create-ingress [provider app-name dependencies ingress-opts]
|
||||
(new-resource (.. k8s -networking -v1 -Ingress) app-name ingress-opts provider dependencies))
|
||||
|
||||
(defn create-secret [provider app-name dependencies secret-opts]
|
||||
(new-resource (.. k8s -core -v1 -Secret) app-name secret-opts provider dependencies))
|
||||
|
||||
(defn create-image [app-name image-opts]
|
||||
(new (.. docker -Image) app-name (clj->js image-opts)))
|
||||
|
||||
(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 create-chart [provider app-namespace app-name dependencies chart-options]
|
||||
(let [base-args {:chart app-name
|
||||
:namespace app-namespace}
|
||||
final-args (deep-merge base-args chart-options)]
|
||||
(new-resource (.. k8s -helm -v3 -Chart) app-name final-args provider dependencies)))
|
||||
|
||||
(defn create-ingress [provider app-namespace app-name host image-port dependencies ingress-options]
|
||||
(let [base-args {:metadata {:name app-name
|
||||
:namespace app-namespace
|
||||
:annotations {"caddy.ingress.kubernetes.io/snippet"
|
||||
(str "tls {\n dns cloudflare {env.CLOUDFLARE_API_TOKEN}\n}\n" (:caddy-snippet ingress-options))}}
|
||||
:spec
|
||||
{:ingressClassName "caddy"
|
||||
:rules [{:host host
|
||||
:http {:paths [{:path "/"
|
||||
:pathType "Prefix"
|
||||
:backend {:service {:name (or (:service-name ingress-options) app-name)
|
||||
:port {:number image-port}}}}]}}]}}
|
||||
final-args (deep-merge base-args ingress-options)]
|
||||
(new-resource (.. k8s -networking -v1 -Ingress) app-name final-args provider dependencies)))
|
||||
|
||||
(defn create-storage-class [provider app-name dependencies storage-options]
|
||||
(let [base-args {:metadata {:name app-name}}
|
||||
final-args (deep-merge base-args storage-options)]
|
||||
(new-resource (.. k8s -storage -v1 -StorageClass) app-name final-args provider dependencies)))
|
||||
|
||||
(defn deploy-stack
|
||||
"Deploys a versatile stack of K8s resources, including optional Helm charts."
|
||||
@@ -151,7 +220,7 @@
|
||||
{: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]
|
||||
:or {vault-load-yaml false image-port 80}} options
|
||||
app-labels {:app app-name}
|
||||
options (merge options {:app-labels {:app app-name} :image-port image-port})
|
||||
|
||||
prepared-vault-data (when (requested-components :vault-secrets)
|
||||
(vault-utils/prepare {:provider provider
|
||||
@@ -164,42 +233,98 @@
|
||||
|
||||
host (when secrets (.apply secrets #(aget % "host")))
|
||||
|
||||
ns (when (requested-components :namespace)
|
||||
(create-namespace provider app-namespace nil ns-opts))
|
||||
|
||||
docker-image (when (requested-components :docker-image)
|
||||
(create-image app-name image-opts))
|
||||
|
||||
secret (when (requested-components :secret)
|
||||
(create-secret provider app-namespace app-name nil secret-opts))
|
||||
|
||||
storage-class (when (requested-components :storage-class)
|
||||
(create-storage-class provider app-name nil storage-class-opts))
|
||||
|
||||
image (if (some? image) image (str (-> cfg :docker-repo) "/" app-name ":latest"))
|
||||
|
||||
ns
|
||||
(when (requested-components :namespace)
|
||||
(generic-transform
|
||||
(fn [final-args]
|
||||
(create-namespace provider app-namespace nil final-args))
|
||||
ns-opts
|
||||
(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)
|
||||
(create-chart provider app-namespace app-name (vec (filter some? [ns docker-image bind-secrets]))
|
||||
(let [helm-values-fn (get chart-opts :helm-values-fn (fn [ctx] (:base-values ctx)))
|
||||
context {:base-values yaml-values
|
||||
:secrets (if (some? prepared-vault-data) secrets nil)
|
||||
:app-name app-name}
|
||||
calculated-values (helm-values-fn context)
|
||||
transformations-fn (if (get chart-opts :transformations) [(get chart-opts :transformations)] [])]
|
||||
(-> chart-opts
|
||||
(assoc :values calculated-values)
|
||||
(assoc :transformations transformations-fn)))))
|
||||
(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]))
|
||||
final-args))
|
||||
chart-opts
|
||||
chart-base-values
|
||||
secrets
|
||||
options)))
|
||||
|
||||
deployment (when (requested-components :deployment)
|
||||
(create-deployment provider app-namespace app-name app-labels image image-port (vec (filter some? [ns docker-image bind-secrets])) deployment-opts))
|
||||
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)
|
||||
(create-service provider app-namespace app-name app-labels image-port [deployment] service-opts))
|
||||
|
||||
app-dependency (or service chart bind-secrets)
|
||||
|
||||
ingress (when (requested-components :ingress) (create-ingress provider app-namespace app-name host image-port [app-dependency] ingress-opts))
|
||||
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)
|
||||
(exec-fn (assoc options :dependencies (vec (filter some? [chart ns secret storage-class deployment service ingress docker-image])))))]
|
||||
|
||||
|
||||
Reference in New Issue
Block a user