Skip to content

Instantly share code, notes, and snippets.

@dogenpunk
Forked from olivergeorge/README.md
Created May 8, 2019 03:41
Show Gist options
  • Save dogenpunk/0f291f45baee7ee34c6dbe9d21dedc5a to your computer and use it in GitHub Desktop.
Save dogenpunk/0f291f45baee7ee34c6dbe9d21dedc5a to your computer and use it in GitHub Desktop.
{:allow [arthur.core/hello-world-web
arthur.core/echo-web]
:lambdas {:hello-world-web
{:fn arthur.core/hello-world-web
:integration :api-gateway/proxy
:api-gateway/resources [{:path-part "hello-world"
:methods [{:http-method "ANY"
:authorization-type "NONE"
:integration {:timeout-in-millis 29000}}]}]}
:echo-web
{:fn arthur.core/echo-web
:integration :api-gateway/proxy
:api-gateway/resources [{:path-part "echo"
:methods [{:http-method "ANY"
:authorization-type "NONE"
:integration {:timeout-in-millis 29000}}]}]}}
:app-name "og-condense-sandbox"}
(ns user
(:require [datomic.ion.dev :as ion-dev]
[clojure.java.io :as io]
[clojure.edn :as edn]
[clojure.java.shell :as shell]
[clojure.data.json :as json]
[clojure.string :as string]))
(def ion-config (edn/read-string (slurp "resources/datomic/ion-config.edn")))
(def stage-name "dev")
(defn submap?
([a] (partial submap? a))
([a b] (= a (select-keys b (keys a)))))
(defn find-by [pred ms] (first (filter pred ms)))
(defn aws [& args]
(apply println "aws" args)
(let [ret (apply shell/sh "aws" args)]
(when (= 0 (:exit ret))
(json/read-str (:out ret) :key-fn keyword :eof-error? false))))
(defn stack-outputs [stack]
(into {} (map (juxt (comp keyword :OutputKey) :OutputValue) (:Outputs stack))))
(defn get-rest-apis []
(:items (aws "apigateway" "get-rest-apis")))
(defn create-rest-api []
(aws "apigateway" "create-rest-api"
"--name" (:app-name ion-config)
"--binary-media-types" "*/*"
"--endpoint-configuration" "{\"types\" : [\"REGIONAL\"]}"))
(defn find-rest-api
[]
(find-by (submap? {:name (:app-name ion-config)}) (get-rest-apis)))
(defn upsert-rest-api
[]
(let [rest-apis (get-rest-apis)
name (:app-name ion-config)
matches (filter (submap? {:name name}) rest-apis)
[match & extra-matches] matches]
(when extra-matches
(throw (ex-info "Multiple matching rest-apis" {:name name :matches matches})))
(or match (create-rest-api))))
(defn get-resources
[rest-api]
(:items (aws "apigateway" "get-resources" "--rest-api-id" (:id rest-api))))
(defn sync-rest-api
"Setup REST API based on ion-config via accreate-only changes. Reports if breaking changes are required."
[]
(let [stacks (aws "cloudformation" "describe-stacks")
app-stack (find-by (submap? {:StackName (:app-name ion-config)}) (:Stacks stacks))
[_ _ _ region client-id & _] (string/split (:StackId app-stack) #":")
{:keys [CodeDeployDeploymentGroup]} (stack-outputs app-stack)
rest-api (upsert-rest-api)
resources (get-resources rest-api)
rest-api-id (:id rest-api)
parent-id (:id (find-by (submap? {:path "/"}) resources))]
(doseq [[lambda-name lambda-config] (:lambdas ion-config)
:when (submap? {:integration :api-gateway/proxy} lambda-config)
{:keys [path-part methods]} (:api-gateway/resources lambda-config)]
(let [resource (or (find-by (submap? {:parentId parent-id :pathPart path-part}) resources)
(aws "apigateway" "create-resource"
"--rest-api-id" rest-api-id
"--parent-id" parent-id
"--path-part" path-part))
function-name (str CodeDeployDeploymentGroup "-" (name lambda-name))
function (aws "lambda" "get-function" "--function-name" function-name)
FunctionArn (get-in function [:Configuration :FunctionArn])
]
(let [kvs {}]
(when-not (submap? kvs resource)
(throw (ex-info "resource incompatible with config" {:kvs kvs :resource resource}))))
(doseq [{:keys [http-method authorization-type]} methods]
(if-let [method (aws "apigateway" "get-method"
"--rest-api-id" rest-api-id
"--resource-id" (:id resource)
"--http-method" http-method)]
(let [kvs {:httpMethod http-method :authorizationType authorization-type}]
(when-not (submap? kvs method)
(throw (ex-info "method incompatible with config" {:kvs kvs :method method}))))
(aws "apigateway" "put-method"
"--rest-api-id" rest-api-id
"--resource-id" (:id resource)
"--http-method" http-method
"--authorization-type" authorization-type))
(if-let [integration (aws "apigateway" "get-integration"
"--rest-api-id" rest-api-id
"--resource-id" (:id resource)
"--http-method" http-method)]
(let [kvs {}]
(when-not (submap? kvs integration)
(throw (ex-info "method incompatible with config" {:kvs kvs :integration integration}))))
(let [service_api (str "2015-03-31/functions/" FunctionArn "/invocations") ; TODO: Okay to hardcode this date?
uri (str "arn:aws:apigateway:" region ":lambda:path/" service_api)]
(aws "apigateway" "put-integration"
"--rest-api-id" rest-api-id
"--resource-id" (:id resource)
"--http-method" http-method
"--type" "AWS_PROXY"
"--integration-http-method" "POST"
"--timeout-in-millis" "29000"
"--uri" uri))))
(let [source-arn (str "arn:aws:execute-api:" region ":" client-id ":" rest-api-id "/*/*/" path-part)
statement-id (str "ion-" (hash (str function-name source-arn)))
{:keys [Policy]} (aws "lambda" "get-policy" "--function-name" function-name)
policy-data (when Policy (json/read-str Policy :key-fn keyword))
statement (find-by (submap? {:Sid statement-id}) (:Statement policy-data))]
(if statement
(let [kvs {:Condition {:ArnLike source-arn}
:Resource FunctionArn}]
(when-not (submap? kvs statement)
(throw (ex-info "statement incompatible with config" {:kvs kvs :statement statement}))))
(aws "lambda" "add-permission"
"--function-name" function-name
"--action" "lambda:InvokeFunction"
"--principal" "apigateway.amazonaws.com"
"--statement-id" statement-id
"--source-arn" source-arn)))))))
(defn deploy-api []
(let [rest-api (find-rest-api)]
(aws "apigateway" "create-deployment" "--rest-api-id" (:id rest-api) "--stage-name" stage-name)))
(defn deploy-uri []
(let [stacks (aws "cloudformation" "describe-stacks")
app-stack (find-by (submap? {:StackName (:app-name ion-config)}) (:Stacks stacks))
[_ _ _ region client-id & _] (string/split (:StackId app-stack) #":")
{:keys [id]} (find-rest-api)]
(str "https://" id ".execute-api." region ".amazonaws.com/" stage-name "/")))
(defn compute-group
[]
(let [stacks (aws "cloudformation" "describe-stacks")
app-stack (find-by (submap? {:StackName (:app-name ion-config)}) (:Stacks stacks))
{:keys [CodeDeployDeploymentGroup]} (stack-outputs app-stack)]
CodeDeployDeploymentGroup))
(defn release
"Do push and deploy of app. Supports stable and unstable releases. Returns when deploy finishes running."
[args]
(try
(let [group (compute-group)
push-data (ion-dev/push args)
deploy-args (merge (select-keys args [:creds-profile :region :uname])
(select-keys push-data [:rev])
{:group group})]
(let [deploy-data (ion-dev/deploy deploy-args)
deploy-status-args (merge (select-keys args [:creds-profile :region])
(select-keys deploy-data [:execution-arn]))]
(loop []
(let [status-data (ion-dev/deploy-status deploy-status-args)]
(if (= "RUNNING" (:code-deploy-status status-data))
(do (Thread/sleep 5000) (recur))
status-data)))))
(catch Exception e
{:deploy-status "ERROR"
:message (.getMessage e)})))
(comment
(release {})
(sync-rest-api)
(deploy-api)
(deploy-uri))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment