Skip to content

Instantly share code, notes, and snippets.

@xfthhxk
Last active January 8, 2024 14:14
Show Gist options
  • Save xfthhxk/01ef9b489094e5f7abbae7e8cd43784c to your computer and use it in GitHub Desktop.
Save xfthhxk/01ef9b489094e5f7abbae7e8cd43784c to your computer and use it in GitHub Desktop.
Project setup with babashka & spire
#!/usr/bin/env bb
;; NB. it can be helpful to do `sudo id` on the cmdline before running this script so that the privileged commands work
;; Spire doesn't yet have the ability to ask for a password when sudo is used
;;
;;
;; This script is an example of how to use babashka and spire to automate project setup for local development.
;; Sets up the local development environment so that necessary software is installed and https can be used.
;; You would tailor it to your needs.
;;
;;
;; Installs:
;; - needed OS packages
;; - Docker
;; - gcloud CLI
;; - root CAs generated by mkcert
;; - NVM && node
;; - SDKMAN && jvm
;; - Clojure CLI
;;
;; Updates:
;; - `/etc/hosts` maps `+local-domain+` to 127.0.0.1
;; - babashka tab completion
;; - gcloud tab completion
;; - installs a dev env shell function
(require '[babashka.pods])
(babashka.pods/load-pod 'epiccastle/spire "0.1.0-alpha.17")
(ns project-setup
(:require [pod.epiccastle.spire.module.apt :as apt]
[pod.epiccastle.spire.module.apt-key :as apt-key]
[pod.epiccastle.spire.module.apt-repo :as apt-repo]
[pod.epiccastle.spire.module.attrs :as attrs]
[pod.epiccastle.spire.facts :as facts]
[pod.epiccastle.spire.module.shell :as shell]
[pod.epiccastle.spire.module.sudo :as sudo]
[pod.epiccastle.spire.module.mkdir :as mkdir]
[babashka.fs :as fs]
[clojure.string :as str]))
(def +docker-keyring+ "/etc/apt/keyrings/docker.gpg")
(def +gcp-keyring+ "/usr/share/keyrings/cloud.google.gpg")
(def +java-version+ "21.0.1-tem")
(def +node-version+ "20.3.0")
(def +local-domain+ "app.local.example.com")
(def +mkcert-domains+ ["localhost" +local-domain+])
(def +system-facts+ (facts/fetch-facts))
(def +system-codename+ (get-in +system-facts+ [:system :codename]))
(def +shell+ (get-in +system-facts+ [:system :shell]))
(def +username+ (get-in +system-facts+ [:user :uid :name]))
(def +home-dir+ (str "/home/" +username+))
(def +bin-dir+ (str +home-dir+ "/bin"))
(def +rc-file+ (str +home-dir+ "/" (case +shell+
:zsh ".zshrc"
".bashrc")))
(def +mkcert+ (str +bin-dir+ "/mkcert"))
(def +mkcert-dir+ (str +home-dir+ "/.local/share/mkcert"))
(defn wsl?
[]
(let [s (str/lower-case (get-in +system-facts+ [:uname :string]))]
(and (str/includes? s "microsoft")
(str/includes? s "wsl"))))
(defn ensure-file-content!
[{:keys [path present? content custom]}]
(let [file-str (slurp path)]
(when-not (present? file-str)
;; spit won't work when sudo is required so use shell
(cond
content (shell/shell {:cmd (format "echo '%s' >> %s" content path)})
custom (shell/shell {:cmd (format "echo '%s' > %s" (custom file-str) path)})))))
(def +zshrc-bb-tasks-autocomplete+ "
_bb_tasks() {
local matches=(`bb tasks |tail -n +3 |cut -f1 -d ' '`)
compadd -a matches
_files # autocomplete filenames as well
}
compdef _bb_tasks bb")
(def +bash-bb-tasks-autocomplete+ "
_bb_tasks() {
COMPREPLY=( $(compgen -W \"$(bb tasks |tail -n +3 |cut -f1 -d ' ')\" -- ${COMP_WORDS[COMP_CWORD]}) );
}
# autocomplete filenames as well
complete -f -F _bb_tasks bb")
(def +dev-env-activate+ "
function dev_env {
gcloud config configurations activate project-dev
unset GOOGLE_APPLICATION_CREDENTIALS
unset GOOGLE_PROJECT
alias db-local='psql -h 127.0.0.1 -U db-user -p 5400 -d db-dev'
}")
(defn bb-tasks-autocomplete
[shell]
(case shell
:zsh +zshrc-bb-tasks-autocomplete+
+bash-bb-tasks-autocomplete+))
(defn ensure-bb-tasks-autocomplete!
[]
(ensure-file-content!
{:path +rc-file+
:present? (fn [s] (str/includes? s "_bb_tasks"))
:content (str "\n" (bb-tasks-autocomplete +shell+))}))
(defn ensure-gcloud-autocomplete!
[]
(let [file "/usr/lib/google-cloud-sdk/completion.bash.inc"]
(ensure-file-content!
{:path +rc-file+
:present? (fn [s] (str/includes? s file))
:content (format "\nif [ -f '%s' ]; then source '%s'; fi\n" file file)})))
(defn ensure-dev-env-fn!
[]
(ensure-file-content!
{:path +rc-file+
:present? (fn [s] (str/includes? s "function dev_env"))
:content +dev-env-activate+}))
(defn ensure-hosts-mapping!
[]
(sudo/sudo
(ensure-file-content!
{:path "/etc/hosts"
:present? (fn [s] (str/includes? s +local-domain+))
:custom (fn [s]
(->> (str/split-lines s)
(map (fn [s]
(if-not (str/includes? s "127.0.0.1")
s
(str s " " +local-domain+))))
(str/join \newline)))})))
(defn download-mkcert!
[]
(let [cmd (format "curl --silent --location --output %s 'https://dl.filippo.io/mkcert/latest?for=linux/amd64'" +mkcert+)]
(when-not (fs/exists? +mkcert+)
(shell/shell {:cmd cmd})
(attrs/attrs {:path +mkcert+ :mode "u+x"}))))
(defn setup-mkcert!
[]
(let [local-pem "./dev/docker/nginx/ssl/local-dev.pem"
local-key "./dev/docker/nginx/ssl/local-dev-key.pem"
domains (str/join " " +mkcert-domains+)
gen-cert-cmd (format "%s -cert-file %s -key-file %s %s" +mkcert+ local-pem local-key domains)]
(when-not (fs/exists? local-pem)
(sudo/sudo
;; install the root certs to allow mkcert as a CA
(shell/shell {:cmd (str +mkcert+ " -install")}))
(shell/shell {:cmd gen-cert-cmd}))))
(defn export-mkcert-root-ca!
[]
(let [p12 (str +mkcert-dir+ "/rootCA.p12")
cmd "openssl pkcs12 -export -out rootCA.p12 -in rootCA.pem -inkey rootCA-key.pem"]
(when-not (fs/exists? p12)
(shell/shell {:dir +mkcert-dir+ :cmd cmd}))))
(defn install-os-packages!
[]
(sudo/sudo
(apt/apt :update)
(apt/apt :install ["ca-certificates" "gnupg" "curl" "apt-transport-https"])
(apt-key/apt-key :present {:public-key-url "https://download.docker.com/linux/ubuntu/gpg"
:keyring +docker-keyring+})
(attrs/attrs {:path +docker-keyring+
:mode "a+r"})
(apt-repo/apt-repo :present {:repo (format "deb [arch=amd64 signed-by=%s] https://download.docker.com/linux/ubuntu %s stable"
+docker-keyring+ (name +system-codename+))
:filename "docker"})
(apt-key/apt-key :present {:public-key-url "https://packages.cloud.google.com/apt/doc/apt-key.gpg"
:keyring +gcp-keyring+})
(apt-repo/apt-repo :present {:repo (format "deb [signed-by=%s] https://packages.cloud.google.com/apt cloud-sdk main" +gcp-keyring+)
:filename "google-cloud-sdk"})
(apt/apt :update)
(apt/apt :install ["docker-ce" "docker-ce-cli" "containerd.io" "docker-buildx-plugin" "docker-compose-plugin"
"libnss3-tools" ;; required by mkcert
"google-cloud-cli"])))
(defn do-all-steps!
[]
(mkdir/mkdir {:path +bin-dir+
:owner +username+
:group +username+
:mode "u=rwx,go=rx"})
(shell/shell {:dir ".git/hooks"
:cmd "ln -s -f ../../dev/git/hooks/pre-commit ."})
(ensure-hosts-mapping!)
(install-os-packages!)
(sudo/sudo
(shell/shell {:cmd (format "usermod -aG docker %s" +username+)}))
(download-mkcert!)
(setup-mkcert!)
;; install nvm
(shell/shell {:cmd (format "curl --silent https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash")})
;; nvm is a shell function
(shell/shell {:cmd (format "source %s/.nvm/nvm.sh && nvm install %s && nvm alias default %s" +home-dir+ +node-version+ +node-version+)})
(shell/shell {:cmd "npm install"})
;; sdk is apparently a shell function so have to source sdkman-init
(shell/shell {:cmd "curl --silent https://get.sdkman.io | bash"})
(shell/shell {:cmd (format "source ~/.sdkman/bin/sdkman-init.sh && sdk install java %s" +java-version+)})
(sudo/sudo
(shell/shell {:cmd "curl --silent https://github.com/clojure/brew-install/releases/latest/download/linux-install.sh | bash"}))
(ensure-dev-env-fn!)
(ensure-bb-tasks-autocomplete!)
(ensure-gcloud-autocomplete!))
(do-all-steps!)
(when (wsl?)
(export-mkcert-root-ca!)
(println (format "Ensure %s/rootCA.p12 is installed on windows using mmc.exe" +mkcert-dir+)))
(run! println ["********************"
"Done!"
(format "You may wish to `source %s`" +rc-file+)
"********************"])
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment