- Create the app
lein new app dns-add
- Replace project.clj with the one provided
- 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
- Replace src/dns_add/core.clj with the one provided
- Compile with
lein uberjar
(or run withlein run
)
Last active
August 29, 2015 14:15
-
-
Save libc/d5984208954928a8d24a to your computer and use it in GitHub Desktop.
A simple clojure script that uses route53-infirma
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(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))))) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(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