Move all files to root
This commit is contained in:
114
scripts/generate-crds.js
Normal file
114
scripts/generate-crds.js
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const https = require('https');
|
||||||
|
const crypto = require('crypto');
|
||||||
|
const { execSync } = require('child_process');
|
||||||
|
|
||||||
|
const PROJECT_ROOT = process.cwd();
|
||||||
|
|
||||||
|
const OUTPUT_DIR = path.join(PROJECT_ROOT, 'generated/crds');
|
||||||
|
const TEMP_DIR = path.join(PROJECT_ROOT, 'temp_crds');
|
||||||
|
const CHECKSUM_FILE = path.join(PROJECT_ROOT, '.crd2pulumi-checksum');
|
||||||
|
|
||||||
|
const GW_VERSION = 'v1.1.0';
|
||||||
|
const GW_URL = `https://github.com/kubernetes-sigs/gateway-api/releases/download/${GW_VERSION}/experimental-install.yaml`;
|
||||||
|
const GW_FILE = 'gateway-api.yaml';
|
||||||
|
|
||||||
|
const CM_VERSION = 'v1.15.0';
|
||||||
|
const CM_URL = `https://github.com/cert-manager/cert-manager/releases/download/${CM_VERSION}/cert-manager.crds.yaml`;
|
||||||
|
const CM_FILE = 'cert-manager.yaml';
|
||||||
|
|
||||||
|
const downloadFile = (url, filename) => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const destPath = path.join(TEMP_DIR, filename);
|
||||||
|
const file = fs.createWriteStream(destPath);
|
||||||
|
|
||||||
|
const request = (uri) => {
|
||||||
|
https.get(uri, (response) => {
|
||||||
|
if (response.statusCode === 301 || response.statusCode === 302) {
|
||||||
|
return request(response.headers.location);
|
||||||
|
}
|
||||||
|
if (response.statusCode !== 200) {
|
||||||
|
reject(new Error(`Failed to download ${uri}: ${response.statusCode}`));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.log(`Downloading ${filename}...`);
|
||||||
|
response.pipe(file);
|
||||||
|
file.on('finish', () => {
|
||||||
|
file.close();
|
||||||
|
resolve(destPath);
|
||||||
|
});
|
||||||
|
}).on('error', (err) => {
|
||||||
|
fs.unlink(destPath, () => {});
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
request(url);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const computeHash = (filePaths) => {
|
||||||
|
const hash = crypto.createHash('sha256');
|
||||||
|
filePaths.sort().forEach(fp => hash.update(fs.readFileSync(fp)));
|
||||||
|
return hash.digest('hex');
|
||||||
|
};
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
if (fs.existsSync(TEMP_DIR)) fs.rmSync(TEMP_DIR, { recursive: true, force: true });
|
||||||
|
fs.mkdirSync(TEMP_DIR);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const gwPath = await downloadFile(GW_URL, GW_FILE);
|
||||||
|
const cmPath = await downloadFile(CM_URL, CM_FILE);
|
||||||
|
const allFiles = [gwPath, cmPath];
|
||||||
|
const newHash = computeHash(allFiles);
|
||||||
|
|
||||||
|
let oldHash = null;
|
||||||
|
if (fs.existsSync(CHECKSUM_FILE)) {
|
||||||
|
oldHash = fs.readFileSync(CHECKSUM_FILE, 'utf8').trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (oldHash === newHash && fs.existsSync(OUTPUT_DIR)) {
|
||||||
|
console.log('CRDs unchanged. Skipping.');
|
||||||
|
} else {
|
||||||
|
console.log('Regenerating CRDs...');
|
||||||
|
if (fs.existsSync(OUTPUT_DIR)) fs.rmSync(OUTPUT_DIR, { recursive: true, force: true });
|
||||||
|
fs.mkdirSync(OUTPUT_DIR, { recursive: true });
|
||||||
|
execSync(`crd2pulumi --nodejsPath "${OUTPUT_DIR}" --force "${gwPath}" "${cmPath}"`, { stdio: 'inherit' });
|
||||||
|
|
||||||
|
const pkgPath = path.join(OUTPUT_DIR, 'package.json');
|
||||||
|
if (fs.existsSync(pkgPath)) {
|
||||||
|
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
||||||
|
if (pkg.scripts) delete pkg.scripts;
|
||||||
|
fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
const tsconfig = {
|
||||||
|
compilerOptions: {
|
||||||
|
target: "es2020",
|
||||||
|
module: "commonjs",
|
||||||
|
moduleResolution: "node",
|
||||||
|
declaration: true,
|
||||||
|
skipLibCheck: true,
|
||||||
|
},
|
||||||
|
include: ["**/*.ts"],
|
||||||
|
exclude: ["node_modules"]
|
||||||
|
};
|
||||||
|
fs.writeFileSync(path.join(OUTPUT_DIR, 'tsconfig.json'), JSON.stringify(tsconfig, null, 2));
|
||||||
|
|
||||||
|
try {
|
||||||
|
execSync('npx tsc', { cwd: OUTPUT_DIR, stdio: 'inherit' });
|
||||||
|
} catch (e) { console.warn("TSC warnings ignored."); }
|
||||||
|
|
||||||
|
fs.writeFileSync(CHECKSUM_FILE, newHash);
|
||||||
|
console.log(`Success!`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
process.exit(1);
|
||||||
|
} finally {
|
||||||
|
if (fs.existsSync(TEMP_DIR)) fs.rmSync(TEMP_DIR, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main();
|
||||||
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