diff --git a/deps.edn b/deps.edn index dd343e2..6c2a1d2 100644 --- a/deps.edn +++ b/deps.edn @@ -1,18 +1,15 @@ {:deps {org.clojure/clojure {:mvn/version "1.10.1"} - clj-http {:mvn/version "3.10.0"} - cheshire {:mvn/version "5.9.0"} + org.clojure/tools.cli {:mvn/version "0.4.2"} + cheshire {:mvn/version "5.10.0"} + clj-http {:mvn/version "3.10.0"} + ;; org.martinklepsch/clj-http-lite {:mvn/version "0.4.3"} + org.seleniumhq.selenium/selenium-java {:mvn/version "4.0.0-alpha-4"} org.postgresql/postgresql {:mvn/version "42.2.9"} com.layerware/hugsql-core {:mvn/version "0.5.1"} com.layerware/hugsql-adapter-next-jdbc {:mvn/version "0.5.1"} digest {:mvn/version "1.4.9"} - cljstache {:mvn/version "2.0.4"} - ;; Logging - com.taoensso/timbre {:mvn/version "4.10.0"} - com.fzakaria/slf4j-timbre {:mvn/version "0.3.14"} - org.slf4j/log4j-over-slf4j {:mvn/version "1.7.29"} - org.slf4j/jul-to-slf4j {:mvn/version "1.7.29"} - org.slf4j/jcl-over-slf4j {:mvn/version "1.7.29"}} + cljstache {:mvn/version "2.0.4"}} :paths ["src" "resources"] :mvn/repos {"central" {:url "https://repo1.maven.org/maven2/"} "clojars" {:url "https://repo.clojars.org/"}} diff --git a/src/com/sompani/onboarding.clj b/src/com/sompani/onboarding.clj index 97cd74a..48a5cbb 100644 --- a/src/com/sompani/onboarding.clj +++ b/src/com/sompani/onboarding.clj @@ -1,27 +1,90 @@ (ns com.sompani.onboarding - "A tool to automate the onboarding of new VCs. - - This works by providing `-main` with an EDN file containing the keys - with the necessary info. The EDN file has to contain a map with the - following keys: - - * `:company-name`: the company name as in .talent.careers. - * `:company-domain`: the company domain name as in talent.." - (:require [clojure.java.shell :as sh] - [clojure.java.io :as io] + "A tool to automate the onboarding of new VCs." + (:require [clojure.java.io :as io] [clojure.string :as str] + [clojure.tools.cli :refer [parse-opts]] [clj-http.client :as http] - [cljstache.core :as cljstache] - [taoensso.timbre :as timbre - :refer [log trace debug info warn error fatal report - logf tracef debugf infof warnf errorf fatalf reportf - spy get-env]]) + [cljstache.core :as cljstache]) (:import (java.nio.file Files LinkOption) (java.nio.file.attribute PosixFileAttributeView - FileAttribute))) + FileAttribute) + (java.io BufferedReader File) + (org.openqa.selenium.firefox FirefoxDriver) + (org.openqa.selenium By))) -;; # Webserver configuration +;; # Helper functions +(defn sh + "Launches a process with optional args, returning exit code. + Prints stdout & stderr." + [bin & args] + (let [arg-array ^"[Ljava.lang.String;" (into-array String (cons bin args)) + process (-> (ProcessBuilder. arg-array) + (.redirectErrorStream true) ;; TODO stream stderr to stderr + (.start))] + (with-open [out (io/reader (.getInputStream process))] + (loop [] + (when-let [line (.readLine ^BufferedReader out)] + (println line) + (recur)))) + (.waitFor process))) + +(def driver (FirefoxDriver.)) + +;; # AWS Domain +(declare make-dns-entries!) + +(defn aws-login [& {:keys [aws-username aws-password]}] + (.get driver "https://eu-west-1.console.aws.amazon.com/ses/home?region=eu-west-1") + (.sendKeys (.findElement driver (By/id "resolving_input")) + (into-array String [(or aws-username (System/getenv "AWS_USERNAME"))])) + (.click (.findElement driver (By/id "next_button"))) + (Thread/sleep 15000) + (.sendKeys (.findElement driver (By/id "password")) + (into-array String [(or aws-password (System/getenv "AWS_PASSWORD"))])) + (.click (.findElement driver (By/id "signin_button")))) + +(defn aws-verify-domain-dns [{:keys [company-name]}] + (.click (.findElement driver (By/linkText "Domains"))) + (.click (.findElement driver (By/id "gwt-debug-verifyNewDomain"))) + (.sendKeys (.findElement driver (By/cssSelector "td > .gwt-TextBox")) + (into-array String [(str company-name ".talent.careers")])) + (.click (.findElement driver (By/cssSelector ".gwt-CheckBox input"))) + (.click (.findElement driver (By/xpath "//span[contains(text(),\"Verify This Domain\")]"))) + (Thread/sleep 2000) + (let [rows (.findElements driver (By/cssSelector ".dialogMiddle table[style^=\"table-layout\"] tr[__gwt_subrow=\"0\"]")) + entries (->> + (map #(identity {:name (.getText (.findElement % (By/cssSelector "td:nth-child(1)"))) + :type (.getText (.findElement % (By/cssSelector "td:nth-child(2)"))) + :content (.getText (.findElement % (By/cssSelector "td:nth-child(3)")))}) rows) + (remove #(or (= "" (:name %)) + (= "MX" (:type %)))))] + (.click (.findElement driver (By/xpath "//span[contains(text(), \"Close\")]"))) + (make-dns-entries! entries))) + +(defn aws-verify-domain-mailfrom [{:keys [company-name]}] + (.click (.findElement driver (By/linkText "Domains"))) + (.click (.findElement driver (By/linkText (str company-name ".talent.careers")))) + (.click (.findElement driver (By/xpath "//span[contains(text(),\"MAIL FROM Domain\")]"))) + (.click (.findElement driver (By/xpath "//button[contains(text(),\"Set MAIL FROM Domain\")]"))) + (.sendKeys (.findElement driver (By/cssSelector "td > input")) + (into-array String ["m"])) + (.click (.findElement driver (By/xpath "//span[contains(text(),\"Set MAIL FROM Domain\")]"))) + (Thread/sleep 2000) + (let [rows (.findElements driver (By/cssSelector ".dialogMiddle table[style^=\"table-layout\"] tr[__gwt_subrow=\"0\"]")) + entries (->> (map #(identity {:name (.getText (.findElement % (By/cssSelector "td:nth-child(1)"))) + :type (.getText (.findElement % (By/cssSelector "td:nth-child(2)"))) + :content (.getText (.findElement % (By/cssSelector "td:nth-child(3)")))}) rows))] + (.click (.findElement driver (By/xpath "//span[contains(text(),\"Close\")]"))) + (make-dns-entries! entries))) + +;; # AWS Mail +(defn aws-verify-mail-address [{:keys [company-name]}] + (.click (.findElement driver (By/linkText "Email Addresses"))) + (.click (.findElement driver (By/xpath "//button[contains(text(),\"Verify a New Email Address\")]"))) + (.sendKeys (.findElement driver (By/id "gwt-debug-verifySenderEmailBox")) + (into-array String [(str "info@" company-name ".talent.careers")])) + (.click (.findElement driver (By/xpath "//span[contains(text(),\"Verify This Email Address\")]")))) ;; ## Deployment directory structure @@ -36,6 +99,7 @@ (.setGroup group))) (defn make-dirs! [{:keys [company-name]}] + (printf "Creating deploy directories for %s." company-name) (let [attrs (Files/getFileAttributeView (.toPath (io/file "/srv/http/www.sompani.com")) PosixFileAttributeView (into-array LinkOption [LinkOption/NOFOLLOW_LINKS])) @@ -52,10 +116,11 @@ "/srv/http/%s.talent.careers-uploads" "/srv/http/%s.talent.careers.1234"] :let [dir (.toPath (io/file (format dir-str company-name)))]] + (printf ".") (and (Files/createDirectory dir fattr) - (set-owner dir owner)) - ) + (set-owner dir owner))) + (printf ".") (let [link (.toPath (io/file (format "/srv/http/staging.%s.talent.careers" company-name)))] (and (Files/createSymbolicLink link @@ -64,35 +129,39 @@ (set-owner link owner)) ) + (printf ".") (let [link (.toPath (io/file (format "/srv/http/%s.talent.careers" company-name)))] (and (Files/createSymbolicLink link (.toPath (io/file (format "/srv/http/%s.talent.careers.1234" company-name))) fattr) - (set-owner link owner)) - ))) + (set-owner link owner))) + (printf " ok.\n"))) ;; ## nginx configuration (defn create-nginx-server! [datamap] + (printf "Creating nginx entries for $s." (:company-name datamap)) (doseq [file ["skel.talent.careers" "staging.skel.talent.careers"] :let [in-file (str "resources/nginx/" file) out-file (format "/etc/nginx/servers-available/%s" (str/replace file #"skel" (:company-name datamap)))]] - (info "using template" in-file "to create" out-file) + (printf ".") (spit out-file - (cljstache/render (slurp in-file) datamap)))) + (cljstache/render (slurp in-file) datamap))) + (printf " ok.\n")) ;; ## Certificates (defn generate-certs! [{:keys [company-name company-domain] :as datamap}] - (info - (apply sh/sh (map #(cljstache/render % datamap) - ["certbot" "certonly" "--nginx" - "-d" "staging.{{company-name}}.talent.careers" - "-d" "{{company-name}}.talent.careers" - "-d" "talent.{{company-domain}}" - "-d" "www.talent.{{company-domain}}" - "--cert-name" "{{company-name}}"])))) + (println "Generating certificates...") + (apply sh (map #(cljstache/render % datamap) + ["certbot" "certonly" "--nginx" + "-d" "staging.{{company-name}}.talent.careers" + "-d" "{{company-name}}.talent.careers" + "-d" "talent.{{company-domain}}" + "-d" "www.talent.{{company-domain}}" + "--cert-name" "{{company-name}}"])) + (println "ok.")) ;; # DNS configuration @@ -108,28 +177,53 @@ {:type "TXT" :name (format "m.%s.talent.careers" company-name) :content "v=spf1 include:amazonses.com ~all"} {:type "TXT" :name (format "%s.talent.careers" company-name) :content "v=spf1 redirect=_spf.yandex.net"}]) -(defn make-basic-dns-entries! [{:keys [company-name]}] +(defn make-dns-entries! [entries] (let [uri "https://api.cloudflare.com/client/v4/zones/5cd643e7432d7cf69f44e268e32b5452/dns_records" - headers {"Authorization" (str "Bearer " (System/getenv "CLOUDFLARE_API"))} - entries (basic-dns-entries company-name)] - (debug "Creating DNS entries for" company-name "; Headers: " headers) + headers {"Authorization" (str "Bearer " (System/getenv "CLOUDFLARE_API"))}] (doseq [entry entries] - (info (http/post uri - {:headers headers - :content-type :json - :form-params entry}))))) + (printf ".") + (http/post uri + {:headers headers + :content-type :json + :form-params entry})))) + +(defn make-basic-dns-entries! [{:keys [company-name]}] + (printf "Creating basic DNS entries for %s." company-name) + (make-dns-entries! (basic-dns-entries company-name)) + (println " ok.")) ;; # Main initialization +(def cli-options + [["-d" "--domain VCDOMAIN" "The domain of the VC to onboard" + :default nil + :id :vc-domain] + ["-n" "--name VCNAME" "The name of the VC to onboard" + :default nil + :id :vc-name] + [nil "--deploy-directories" "Create deploy directories"] + [nil "--base-dns" "Create basic DNS entries"] + [nil "--aws-domain" "Create AWS Domain DNS entries and verify new domain"] + [nil "--nginx" "Create nginx entries"] + [nil "--certify" "Generate certificates"] + ["-h" "--help"]]) (defn -main [& args] - (let [datamap (read-string (slurp (nth args 0)))] - (info "Creating deploy directories.") - (make-dirs! datamap) - (info "Creating basic DNS entries.") - (make-basic-dns-entries! datamap) - (info "Creating nginx entries.") - (create-nginx-server! datamap) - (info "Generating certificates.") - (generate-certs! datamap) - (info "Done."))) - + (let [{:keys [vc-domain + vc-name + deploy-directories + base-dns + aws-domain + nginx + certify] + :as options} + (:options (parse-opts args cli-options)) + datamap {:company-name vc-name "company-domain" vc-domain}] + (and deploy-directories (make-dirs! datamap)) + (and base-dns (make-basic-dns-entries! datamap)) + (and aws-domain (do (aws-login) + (Thread/sleep 5000) + (aws-verify-domain-dns datamap) + (aws-verify-domain-mailfrom datamap))) + (and nginx (create-nginx-server! datamap)) + (and certify (generate-certs! datamap)) + (println "Done.")))