diff --git a/src/main/utils/execution/general.clj b/src/main/utils/execution/general.clj deleted file mode 100644 index 8c0c65a..0000000 --- a/src/main/utils/execution/general.clj +++ /dev/null @@ -1,82 +0,0 @@ -(ns utils.execution.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))) - diff --git a/src/main/utils/execution/general.cljs b/src/main/utils/execution/general.cljs deleted file mode 100644 index 8e584cd..0000000 --- a/src/main/utils/execution/general.cljs +++ /dev/null @@ -1,232 +0,0 @@ -(ns utils.execution.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))) diff --git a/src/main/utils/execution/providers.cljs b/src/main/utils/execution/providers.cljs deleted file mode 100644 index 35c0afc..0000000 --- a/src/main/utils/execution/providers.cljs +++ /dev/null @@ -1,130 +0,0 @@ -(ns utils.execution.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.execution.general :refer [resolve-template]] - [utils.providers.k8s :as k8s-utils] - [utils.providers.harbor :as harbor-utils] - [utils.providers.docker :as docker-utils] [utils.providers.vault :as vault-utils] - [utils.execution.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-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}))))))))) - - -(defn execute [configs exports] - (-> - (let [pulumi-cfg (pulumi/Config.)] - (provider-apply pulumi-cfg configs)) - (exports) - (clj->js))) \ No newline at end of file diff --git a/src/main/utils/execution/safe_fns.cljs b/src/main/utils/execution/safe_fns.cljs deleted file mode 100644 index b7f4e82..0000000 --- a/src/main/utils/execution/safe_fns.cljs +++ /dev/null @@ -1,48 +0,0 @@ -(ns utils.execution.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 %))}) \ No newline at end of file diff --git a/src/main/utils/execution/stack_processor.cljs b/src/main/utils/execution/stack_processor.cljs deleted file mode 100644 index f2b36b1..0000000 --- a/src/main/utils/execution/stack_processor.cljs +++ /dev/null @@ -1,396 +0,0 @@ -(ns utils.execution.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.providers.defaults :as default] - [utils.providers.vault :as vault-utils] - [utils.execution.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.providers.k8s :as k8s-utils] - [utils.providers.harbor :as harbor-utils] - [utils.providers.docker :as docker-utils] - [utils.execution.safe-fns :refer [safe-fns]]) - (:require-macros [utils.execution.general :refer [p-> build-registry]])) - -(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))) - - - diff --git a/src/main/utils/providers/defaults.cljs b/src/main/utils/providers/defaults.cljs deleted file mode 100644 index e55a41e..0000000 --- a/src/main/utils/providers/defaults.cljs +++ /dev/null @@ -1,12 +0,0 @@ -(ns utils.providers.defaults - (:require ["path" :as path] - [configs :refer [cfg]] - [utils.providers.k8s :as k8s] - [utils.providers.harbor :as harbor] - [utils.providers.docker :as docker])) - - -(def defaults - {:k8s k8s/defaults - :harbor harbor/defaults - :docker docker/defaults}) diff --git a/src/main/utils/providers/docker.cljs b/src/main/utils/providers/docker.cljs deleted file mode 100644 index ae4a766..0000000 --- a/src/main/utils/providers/docker.cljs +++ /dev/null @@ -1,25 +0,0 @@ -(ns utils.providers.docker - (:require - [utils.execution.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]}}}) \ No newline at end of file diff --git a/src/main/utils/providers/harbor.cljs b/src/main/utils/providers/harbor.cljs deleted file mode 100644 index f50ce14..0000000 --- a/src/main/utils/providers/harbor.cljs +++ /dev/null @@ -1,36 +0,0 @@ -(ns utils.providers.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}}}) \ No newline at end of file diff --git a/src/main/utils/providers/k8s.cljs b/src/main/utils/providers/k8s.cljs deleted file mode 100644 index 091612a..0000000 --- a/src/main/utils/providers/k8s.cljs +++ /dev/null @@ -1,198 +0,0 @@ -(ns utils.providers.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]))))) \ No newline at end of file diff --git a/src/main/utils/providers/vault.cljs b/src/main/utils/providers/vault.cljs deleted file mode 100644 index e8c1f88..0000000 --- a/src/main/utils/providers/vault.cljs +++ /dev/null @@ -1,71 +0,0 @@ -(ns utils.providers.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}})