Skip to content

Instantly share code, notes, and snippets.

@libc
Last active August 29, 2015 14:15
Show Gist options
  • Save libc/d5984208954928a8d24a to your computer and use it in GitHub Desktop.
Save libc/d5984208954928a8d24a to your computer and use it in GitHub Desktop.
A simple clojure script that uses route53-infirma

Compilation

  1. Create the app lein new app dns-add
  2. Replace project.clj with the one provided
  3. Add route53-infirma into local repository lein deploy route53-infirma com.amazonaws.services.route53.amazon-route53-infima 1.0.0 amazon-route53-infima-1.0.0.jar
  4. Replace src/dns_add/core.clj with the one provided
  5. Compile with lein uberjar (or run with lein run)
(ns dns-add.core
(:require [clojure.tools.cli :refer [parse-opts]]
[clojure.string :as string]
[clojure.set :as set])
(:gen-class))
(def cli-options
[["-p" "--port PORT" "Health check port number"
:default 80
:parse-fn #(Integer/parseInt %)
:validate [#(< 0 % 0x10000) "Must be a number between 0 and 65536"]]
["-z" "--zone ZONEID" "Zone id"]
["-P" "--path /path/to/check" "Health check path"]
["-H" "--host hostname" "Host name (e.g. proxy1.wkp.io)"
:validate [#(not (nil? (re-find #"[^.]+\.[^.]+\.[^.]{2,}$" %))) "Must have at least 1 subdomian and NOT end with a dot"]]
["-i" "--ip ip" "Public IP for health check"]
[nil "--internal-ip internal-ip" "Internal ip for internal domain"]
["-I" "--instance-id id" "Instance id"]
["-R" "--region eu-west-1" "set region"
:default "eu-west-1"]
[nil "--group-name group" "set server group name"]
["-h" "--help"]])
(def options (atom {}))
(def client (new com.amazonaws.services.route53.AmazonRoute53Client))
(def ec2-client (new com.amazonaws.services.ec2.AmazonEC2Client))
(defn fqdn []
(str (:host @options) "."))
(defn fqdn-internal []
(let [parts (string/split (:host @options) #"\.")
part-num (if (.contains (:host @options) ".staging.") 3 2)
[h t] (split-at (- (count parts) part-num) parts)]
(str (string/join "." (concat h ["internal"] t)) ".")))
(defn group-name []
(if (nil? (:group-name @options))
(:host @options)
(:group-name @options)))
(defn withPages [request getter processor nextPage]
"AWS helper function to get paged resources. cals concat(processor(getter(request)), processor(getter(nextPageRequest)), ...) till isTruncated true"
(let [innerLoop (fn [result request]
(let [response (getter request)
batch (processor response)
new-result (concat result batch)]
(if (.isTruncated response)
(recur (concat result batch) (nextPage request response))
new-result)))]
(innerLoop () request)))
(defn get-health-checks []
(let [healthchecks (withPages
(new com.amazonaws.services.route53.model.ListHealthChecksRequest)
#(.listHealthChecks client %)
#(.getHealthChecks %)
(fn [req resp] (.withMarker req (.getNextMarker resp))))
host-group (str " p:" (group-name) " ")
host-health-checks (filter #(.contains (.getCallerReference %) host-group) healthchecks)]
host-health-checks))
(defn convert-public-hc [hc]
(new com.amazonaws.services.route53.infima.util.HealthCheckedResourceRecord
(.getId hc) (.getIPAddress (.getHealthCheckConfig hc))))
(defn internal-hc? [hc]
(.contains (.getCallerReference hc) " iip:"))
(defn convert-internal-hc [hc]
(let [internal-ip (nth (re-find (re-pattern " iip:([0-9.]+)( |$)") (.getCallerReference hc)) 1)]
(new com.amazonaws.services.route53.infima.util.HealthCheckedResourceRecord
(.getId hc) internal-ip)))
(defn get-existing-records []
(let [request (.. (new com.amazonaws.services.route53.model.ListResourceRecordSetsRequest)
(withHostedZoneId (:zone @options)))
rrs (withPages
request
#(.listResourceRecordSets client %)
#(.getResourceRecordSets %)
(fn [req resp] (.. req (withStartRecordName (.getNextRecordName resp)) (withStartRecordType (.getNextRecordType resp)))))
fqdn-exact (fqdn)
fqdn-internal-exact (fqdn-internal)
fqdn-dot (str "." fqdn-exact)
fqdn-internal-dot (str "." fqdn-internal-exact)
; for example if we're creating proxy1.wkp.io, then blah.proxy1.wkp.io and proxy1.wkp.io are existing records
filtered-rrs (filter #(let [name (.getName %)]
(or (= name fqdn-exact) (. name endsWith fqdn-dot)
(= name fqdn-internal-exact) (. name endsWith fqdn-internal-dot)))
rrs)]
filtered-rrs))
(defn transform-set-identifiers [rrs]
(map (fn [rr]
(.withSetIdentifier rr
(str (.getWeight rr) ":"
(if (= (.getSetIdentifier rr) "leafnode")
(string/join "," (map #(.getValue %) (.getResourceRecords rr)))
(.. rr getAliasTarget getDNSName)))))
rrs))
(defn vulcanize [domain-name health-checks]
(let [cnt (if (= (count health-checks) 1)
2
(count health-checks))
new-records (if (> (count health-checks) 0)
(com.amazonaws.services.route53.infima.RubberTree/vulcanize (:zone @options) domain-name "A" 60 (into () health-checks) cnt)
[])]
new-records))
(defn recreate-rr [drop-health-checks]
(let [existing-records (get-existing-records)
existing-health-checks (get-health-checks)
; remove drop-health-checks from existing-healthchecks
health-checks (filter #(nil? (drop-health-checks (.getId %))) existing-health-checks)
infirma-public-hc (map convert-public-hc health-checks)
infirma-internal-hc (map convert-internal-hc (filter internal-hc? health-checks))
new-records (concat (vulcanize (fqdn) infirma-public-hc) (vulcanize (fqdn-internal) infirma-internal-hc))
new-record-changed-set-id (transform-set-identifiers new-records)
changes (concat (map #(new com.amazonaws.services.route53.model.Change "DELETE" %) existing-records)
(map #(new com.amazonaws.services.route53.model.Change "CREATE" %) new-record-changed-set-id))
change-batch (new com.amazonaws.services.route53.model.ChangeBatch changes)
change-rr-request (new com.amazonaws.services.route53.model.ChangeResourceRecordSetsRequest
(:zone @options)
change-batch)]
(println change-rr-request)
(.changeResourceRecordSets client change-rr-request)))
(defn caller-reference []
(str " p:" (group-name) " i:" (:instance-id @options)
(if (nil? (:internal-ip @options))
""
(str " iip:" (:internal-ip @options)))))
(defn add-health-check []
(let [config (.. (new com.amazonaws.services.route53.model.HealthCheckConfig)
(withIPAddress (:ip @options))
(withPort (int (:port @options)))
(withResourcePath (:path @options))
(withType "HTTP"))
hc (.. (new com.amazonaws.services.route53.model.CreateHealthCheckRequest)
(withHealthCheckConfig config)
(withCallerReference (caller-reference)))
tag (.. (new com.amazonaws.services.route53.model.Tag)
(withKey "Name")
(withValue (str (:region @options) " - " (:host @options) " - " (:instance-id @options) " - " (:ip @options))))
tag-hc (.. (new com.amazonaws.services.route53.model.ChangeTagsForResourceRequest)
(withAddTags (list tag))
(withResourceType "healthcheck"))
create-hc-response (. client createHealthCheck hc)]
(. tag-hc setResourceId (.getId (.getHealthCheck create-hc-response)))
(. client changeTagsForResource tag-hc)
hc))
(defn add-health-check-unless-exists []
(if (not-any? #(= (.getCallerReference %) (caller-reference)) (get-health-checks))
(add-health-check)
nil))
(defn dns-add []
(add-health-check-unless-exists)
(recreate-rr #{}))
(defn drop-health-checks [ids]
(doall (map (fn [id]
(let [request (.. (new com.amazonaws.services.route53.model.DeleteHealthCheckRequest) (withHealthCheckId id))]
(.deleteHealthCheck client request)))
ids)))
(defn dns-drop []
(let [remove-healthchecks (map #(.getId %) (filter #(= (.getIPAddress (.getHealthCheckConfig %)) (:ip @options)) (get-health-checks)))]
(println "Removing healthchecks with id:" remove-healthchecks)
(recreate-rr (set remove-healthchecks))
(drop-health-checks remove-healthchecks)))
(defn check-instance-ids [instance-ids]
(let [request (.. (new com.amazonaws.services.ec2.model.DescribeInstancesRequest)
(withInstanceIds instance-ids))
response (. ec2-client describeInstances request)
alive-instances (mapcat (fn [r] (map #(.getInstanceId %) (.getInstances r))) (.getReservations response))]
alive-instances))
(defn extract-instance-id [hc]
(nth (re-find (re-pattern " i:(i-[^ ]+) ") (.getCallerReference hc)) 1))
(defn find-dead-health-checks []
"gets instance-id from health checks, checks whether they're describe-able, and returns health checks of dead instances"
(let [health-checks (get-health-checks)]
(if (zero? (count health-checks))
()
(let [instance-ids (map #(extract-instance-id %) health-checks)
alive-instance-ids (check-instance-ids instance-ids)
dead-instances (set/difference (set instance-ids) (set alive-instance-ids))
bad-health-checks (filter #(not (nil? (dead-instances (extract-instance-id %)))) health-checks)]
(println "alive instances:" alive-instance-ids)
(map #(.getId %) bad-health-checks)))))
(defn dns-gc []
(let [dead-health-checks (find-dead-health-checks)]
(println "dead healthchecks:" dead-health-checks)
(when (> (count dead-health-checks) 0)
(recreate-rr (set dead-health-checks))
(drop-health-checks dead-health-checks))))
(defn usage [options-summary]
(->> ["This is my program. There are many like it, but this one is mine."
""
"Usage: program-name [options] action"
""
"Options:"
options-summary
""
"Actions:"
" add add healthcheck and resource record"
" drop drop healthcheck and resource record"
" gc garbage collect healthchecks and resource records associated with dead instances"
""
"Please refer to the manual page for more information."]
(string/join \newline)))
(defn error-msg [errors]
(str "The following errors occurred while parsing your command:\n\n"
(string/join \newline errors)))
(defn exit [status msg]
(println msg)
(System/exit status))
(defn -main
"I don't do a whole lot ... yet."
[& args]
(let [{:keys [arguments errors summary] :as parsed-args} (parse-opts args cli-options)]
;; Handle help and error conditions
(cond
(:help @options) (exit 0 (usage summary))
(not= (count arguments) 1) (exit 1 (usage summary))
errors (exit 1 (error-msg errors)))
(reset! options (:options parsed-args))
(let [r (com.amazonaws.regions.RegionUtils/getRegion (:region @options))]
(.setRegion client r)
(.setRegion ec2-client r))
(if (> (count (caller-reference)) 63)
(exit 1 (str "Caller reference has to be smaller than 63 characters, specify group-name please, current is " (caller-reference))))
;; Execute program with options
(case (first arguments)
"add" (dns-add)
"drop" (dns-drop)
"gc" (dns-gc)
(exit 1 (usage summary)))))
(defproject dns-add "0.1.0-SNAPSHOT"
:description "FIXME: write description"
:url "http://example.com/FIXME"
:license {:name "Eclipse Public License"
:url "http://www.eclipse.org/legal/epl-v10.html"}
:repositories [["route53-infirma" "file:route53-infirma"]]
:dependencies [[org.clojure/clojure "1.6.0"]
[com.amazonaws.services.route53/amazon-route53-infima "1.0.0"]
[com.amazonaws/aws-java-sdk "1.9.19"]
[org.apache.httpcomponents/httpclient "4.4"]
[org.clojure/tools.cli "0.3.1"]]
:main ^:skip-aot dns-add.core
:target-path "target/%s"
:profiles {:uberjar {:aot :all}})
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment