├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── cljs-lambda ├── README.md ├── codox-transforms.edn ├── doc │ ├── introduction.md │ └── testing.md ├── project.clj ├── src-deps │ ├── cljs_lambda │ │ └── externs │ │ │ └── context.js │ └── deps.cljs ├── src │ └── cljs_lambda │ │ ├── aws │ │ └── event.cljc │ │ ├── context.cljs │ │ ├── local.cljs │ │ ├── macros.cljc │ │ └── util.cljs └── test │ └── cljs_lambda │ └── test │ ├── help.cljc │ ├── macros.cljs │ ├── runner.cljs │ └── util.cljs ├── example ├── README.md ├── project.clj ├── src │ └── example │ │ └── core.cljs ├── static │ └── config.edn └── test │ └── example │ ├── core_test.cljs │ └── test_runner.cljs ├── plugin ├── README.md ├── project.clj ├── resources │ ├── default-iam-policy.json │ ├── default-iam-role.json │ ├── index-advanced.mustache │ ├── index-none.mustache │ └── index-simple.mustache └── src │ └── leiningen │ ├── cljs_lambda.clj │ └── cljs_lambda │ ├── args.clj │ ├── aws.clj │ ├── logging.clj │ └── zip_tedium.clj ├── templates ├── cljs-lambda │ ├── project.clj │ └── src │ │ └── leiningen │ │ └── new │ │ ├── cljs_lambda.clj │ │ └── cljs_lambda │ │ ├── README.md │ │ ├── config.edn │ │ ├── core.cljs │ │ ├── core_test.cljs │ │ ├── gitignore │ │ ├── project.clj │ │ └── test_runner.cljs └── serverless │ ├── project.clj │ └── src │ └── leiningen │ └── new │ ├── serverless_cljs.clj │ └── serverless_cljs │ ├── README.md │ ├── core.cljs │ ├── gitignore │ ├── project.clj │ └── serverless.yml └── travis ├── delete-function.sh ├── get-function.sh └── run.sh /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | classes 3 | checkouts 4 | pom.xml 5 | pom.xml.asc 6 | *.jar 7 | *.class 8 | .lein-* 9 | .nrepl-port 10 | .hgignore 11 | .repl 12 | node_modules 13 | .hg/ 14 | out 15 | .idea 16 | *.iml 17 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: clojure 2 | sudo: required 3 | dist: trusty 4 | jdk: 5 | - oraclejdk8 6 | before_install: 7 | - sudo apt-get update 8 | - sudo pip install awscli 9 | - which parallel || sudo apt-get install parallel 10 | - nvm install 5 11 | - pushd cljs-lambda; lein install; popd 12 | - pushd plugin; lein install; popd 13 | - pushd templates/cljs-lambda; lein install; popd 14 | - pushd templates/serverless; lein install; popd 15 | - lein new cljs-lambda $PROJECT_DIR $SNAPSHOT 16 | - cp -r travis/* $PROJECT_DIR 17 | - pushd cljs-lambda 18 | script: lein doo node test once && popd && pushd $PROJECT_DIR && ./run.sh 19 | env: 20 | matrix: 21 | - PROJECT_DIR=test-project-snapshot SNAPSHOT=--snapshot 22 | global: 23 | - secure: "I+sPV+UgjqN1ttcvzg4FnTdLhth9rrxqxq+j4+RwV5kx4LQhNcMWFoZJYcGeWxBuBa5ppozPiBl1SeDLyDT8j1ybjXIlhBo13WeoNtvtinvt5xFMJnQwU7LfGwQQStuxhjOVWVtFfpgcCqiulpecihb+Mx73JRJ94WSLhF40hxbSLBGZfr3rwVnn7xIBWnzrMoDwTk9wpGuo2RDztI56J+mows+4Bzw30bYfCU/Gq/fT+1y2rNs1vIBEKGrmNxlr97Bo+l1DNDBlSkpJ0YxF8uLHAfGMO9EznT+sS+hFcfs15Z/9JvvC3iKKiu/UhShsIK2NSF/l2J3ieVCM1b+rHiEgqvuZBfeyRTvAglsEsQ3cvpq6wZuqteb+71nNddoI3C7lzpivyGtfQzB+v8Av++xzAvTClv28+yNOqpNl2cxDdBiDd1W7tX8cDN5PPD+qUJD6pGE2XdKMSv6nqSV/BHam/z4SHcHvXpskQfBFapF+DJRgWP04XJf6SqDZ0U/zRdbB2ubgFy1PYrIk4or2Cz4xXl6+gvxyMGI2uGxpKN+Dv3aT9spEYeLIboEmQV6itK6/KZeNVXTIxc/C6DypCwzF5nBF8Wx6ZZ2NFINqkR/gLY2sQnhHiapNIBxcY1FqvOrJQ5DzM0eGX5hjPN09PhByhU1rZu9DoRFyGdc/qh8=" 24 | - secure: "M4lvtVc5IbosiZoxzxTcol+VgPQDrU+OGzTSvQEW3bgLu4aAh71f9dJME6doxlzz3INNBQobjJOc4vH8xfHkPzYJgpT0uyvdMPb7E6KmiFJkgwr5WY5D/7OGGio7WqhToNqN/tb0kM+2R2WPikO2+EFgNd3OQpXbRZUswzXqHXrf+UFun08PNrdLjCYHeMJzUvEo7dua8wkcvjC7h3nstsaz1ebXGMB7ewZFg1GMuosXAid0gN/v2s5QihXWK6x+kbnsdKiTJDSQOCmX5/g1qx2oCMUlpNQT+LxeZ3aUJXlnRBNYd06z8uektzL9rrbIza8h4oZ7WsXKqBobsEmANTSRFAfee4nUZ5nnEfGSpPKMYuvdysFh5c+Xjmr1f9o7wYLk6/zxsQOHJTilj1mObHTprjZuK9E6SG+Snt4QlI3DUDjhWT2m+BtMGQs118P7EpLJRNni6bhmX6no6f/1P6JD4v4D7DUTh1zkhbPIenI1r33htZyU0g3SAVOZRrWXpUzddSo6Thp/19bpazklFeqQafLxtaRItzeJwaRg4tsKlJr5UXPxmHtT7uFY5esJKbk2vHji7UB0h5+zmYfx6/3jcDNnFtGUjgtdULX3D5zgD/MIQZ4SrHfpbFfyfMRDsKhSJ/V6NFUjGQagusyRO036pQueJi60Krs1a5TIAe4=" 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # cljs-lambda 2 | 3 | [![Build Status](https://travis-ci.org/nervous-systems/cljs-lambda.svg?branch=master)](https://travis-ci.org/nervous-systems/cljs-lambda) 4 | 5 | [AWS Lambda](http://aws.amazon.com/documentation/lambda/) is a service which 6 | allows named functions to be directly invoked (via a client API), have their 7 | execution triggered by a variety of AWS events (S3 upload, DynamoDB activity, 8 | etc.) or to serve as HTTP endpoints (via [API 9 | Gateway](https://aws.amazon.com/api-gateway/)). 10 | 11 | This README serves to document a [Leiningen 12 | plugin](https://github.com/nervous-systems/cljs-lambda/tree/master/plugin) 13 | (`lein-cljs-lambda`), template (`cljs-lambda`) and [small 14 | library](https://nervous.io/doc/cljs-lambda/) (`cljs-lambda`) to facilitate the 15 | writing, deployment & invocation of Clojurescript Lambda functions. 16 | 17 | The plugin can deploy functions itself, or the 18 | excellent [Serverless](http://serverless.com) framework can be used, 19 | via 20 | [serverless-cljs-plugin](https://www.npmjs.com/package/serverless-cljs-plugin). 21 | 22 | ## Benefits 23 | 24 | - Low instance warmup penalty 25 | - Use promises, or asynchronous channels for deferred completion 26 | - `:optimizations` `:advanced` support, for smaller zip files* 27 | - Utilities for [testing Lambda entrypoints](https://nervous.io/doc/cljs-lambda/testing.html) off of EC2 28 | - Function publishing/versioning 29 | - [Serverless](http://serverless.com) integration 30 | 31 | _N.B. If using advanced compilation alongside Node's standard library, 32 | something like 33 | [cljs-nodejs-externs](https://github.com/nervous-systems/cljs-nodejs-externs) 34 | will be required_ 35 | 36 | ## Status 37 | 38 | This collection of projects is used extensively in production, for important 39 | pieces of infrastructure. While efforts are made to ensure backward 40 | compatibility in the Leiningen plugin, the `cljs-lambda` API is subject to 41 | breaking changes. 42 | 43 | ### Recent Changes 44 | 45 | - `io.nervous/lein-cljs-lambda` 0.6.0 defaults the runtime of deployed functions 46 | to `nodejs4.3`, unless this is overriden with `:runtime` in the fn-spec or on 47 | the command-line. While your functions will be backwards compatible, your AWS 48 | CLI installation may require updating to support this change. 49 | 50 | # Coordinates 51 | 52 | ## [Plugin](https://github.com/nervous-systems/cljs-lambda/tree/master/plugin) 53 | 54 | [![Clojars 55 | Project](http://clojars.org/io.nervous/lein-cljs-lambda/latest-version.svg)](http://clojars.org/io.nervous/lein-cljs-lambda) 56 | 57 | ## [Library](https://github.com/nervous-systems/cljs-lambda/tree/master/cljs-lambda) 58 | 59 | [![Clojars Project](http://clojars.org/io.nervous/cljs-lambda/latest-version.svg)](http://clojars.org/io.nervous/cljs-lambda) 60 | 61 | # Get Started 62 | 63 | ```sh 64 | $ lein new cljs-lambda my-lambda-project 65 | $ cd my-lambda-project 66 | $ lein cljs-lambda default-iam-role 67 | $ lein cljs-lambda deploy 68 | ### 500ms delay via a promise (try also "delay-channel" and "delay-fail") 69 | $ lein cljs-lambda invoke work-magic \ 70 | '{"spell": "delay-promise", "msecs": 500, "magic-word": "my-lambda-project-token"}' 71 | ... {:waited 500} 72 | ### Get environment varibles 73 | $ lein cljs-lambda invoke work-magic \ 74 | '{"spell": "echo-env", "magic-word": "my-lambda-project-token"}' 75 | ... 76 | $ lein cljs-lambda update-config work-magic :memory-size 256 :timeout 66 77 | ``` 78 | 79 | ## Serverless 80 | 81 | To generate a minimal project: 82 | 83 | ```sh 84 | $ lein new serverless-cljs my-lambda-project 85 | ``` 86 | 87 | # Documentation 88 | - [lein-cljs-lambda plugin README](https://github.com/nervous-systems/cljs-lambda/tree/master/plugin) / [reference](https://github.com/nervous-systems/cljs-lambda/wiki/Plugin-Reference) 89 | - [cljs-lambda library API docs](https://nervous.io/doc/cljs-lambda/) 90 | 91 | ## Older 92 | - [Clojurescript/Node on AWS Lambda](https://nervous.io/clojure/clojurescript/aws/lambda/node/lein/2015/07/05/lambda/) (blog post) 93 | - [Chasing Chemtrails w/ Clojurescript](https://nervous.io/clojure/clojurescript/node/aws/2015/08/09/chemtrails/) (blog post) 94 | 95 | ## Other 96 | - [Example project](https://github.com/nervous-systems/cljs-lambda/tree/master/example/) (generated from template) 97 | 98 | # Function Examples 99 | 100 | (Using promises) 101 | 102 | ```clojure 103 | (deflambda slowly-attack [{target :name} ctx] 104 | (p/delay 1000 {:to target :data "This is an attack"})) 105 | ``` 106 | 107 | ## AWS Integration With [eulalie](https://github.com/nervous-systems/eulalie) 108 | 109 | (Using core.async) 110 | 111 | This function retrieves the name it was invoked under, then attempts to invoke 112 | itself in order to recursively compute the factorial of its input: 113 | 114 | ```clojure 115 | (deflambda fac [n {:keys [function-name] :as ctx}] 116 | (go 117 | (if (<= n 1) 118 | n 119 | (let [[tag result] (clj data conversion utilities, an exported 12 | Clojurescript function can easily serve as a Lambda handler. Arranging compiled 13 | Clojurescript into a deployable zip file is outside the scope of this library, 14 | and is handled by the excellent [cljs-lambda Leiningen 15 | plugin](https://github.com/nervous-systems/cljs-lambda#plugin-overview). 16 | 17 | The cljs-lambda _library_ is focused on simplifying the definition of 18 | Clojurescript Lambda handlers -- EDN representations of event/context objects, 19 | eliminating the need for knowledge of the deployment target within generic data 20 | processing code, etc. 21 | 22 | # Simple Example 23 | 24 | ```clojure 25 | (ns project.ns 26 | (:require [cljs-lambda.macros :refer-macros [deflambda]] 27 | [promesa.core :as p])) 28 | 29 | (deflambda wait [n ctx] 30 | (p/delay n {:waited n})) 31 | ``` 32 | 33 | Let's imagine we're invoking this from the command line: 34 | 35 | ```shell 36 | $ aws lambda invoke --function-name wait --payload 66 output.json 37 | ``` 38 | 39 | _N.B. there's not necessarily any correspondence between a handler name and the 40 | Lambda function's name (`--function-name`, above), but we're assuming they're 41 | the same here._ 42 | 43 | ## Input 44 | 45 | In this case, our `deflambda wait`, if correctly deployed, is going to receive 46 | the Clojurescript number `66` as its first (`event`) argument, and as the second 47 | (`ctx`) arg, a record containing something like: 48 | 49 | ```clojure 50 | {:function-name "wait" 51 | :log-group-name "/aws/lambda/wait" 52 | :log-stream-name "2016/03..." 53 | :aws-request-id "4cb18..." 54 | ...} 55 | ``` 56 | 57 | _N.B. The `event` argument is constructed by calling `(js->clj event 58 | :keywordize-keys true)` on the Javascript object received from the Lambda 59 | instructure._ 60 | 61 | [[deflambda]] is an extremely simple wrapper around [[async-lambda-fn]] - the above 62 | example being exactly equivalent to: 63 | 64 | ```clojure 65 | (def ^:export wait 66 | (async-lambda-fn 67 | (fn [n ctx] 68 | (p/delay n {:waited n})))) 69 | ``` 70 | 71 | Aside from some trivial input coercion, `async-lambda-fn` is mostly concerned 72 | with interpreting the wrapped function's body - in this case, a 73 | [promesa](https://github.com/funcool/promesa)/[Bluebird](http://bluebirdjs.com/docs/api-reference.html) 74 | Promise. 75 | 76 | ### A Note on Promises 77 | 78 | Promises are an effective representation of Lambda handler results, insofar 79 | as they're capable of unambiguously representing a single deferred success or 80 | failure value. `core.async` channels may also be returned by `cljs-lambda` 81 | functions, though they're a less natural fit. 82 | 83 | The Node version available on AWS Lambda is `v0.10.36`, at the time of writing 84 | -- which predates the inclusion of an ES6 Promise object as a Node global. The 85 | [promesa](https://github.com/funcool/promesa) Clojure/script library is a 86 | dependency of `cljs-lambda`, and packages the widely used 87 | [Bluebird](http://bluebirdjs.com/docs/api-reference.html) Promise 88 | implementation, providing a natural Clojurescript wrapper around its 89 | functionality. 90 | 91 | # Output 92 | 93 | The promise returned by our example'll fire in `n` seconds, with the value 94 | `{:waited n}` - which'll be serialized to JSON, and returned to the caller as 95 | `{"waited" 66}`. 96 | 97 | In Javascript, an explicit `context.done` method is called to signify 98 | completion -- we're turning that inside-out, and completing the invocation when 99 | the handler's Promise is resolved/rejected. 100 | 101 | _N.B. `cljs-lambda` also provides a [[context/done!]] function -- which can be 102 | called at any time to halt execution -- but passing the context object deep into 103 | what'd otherwise be generic Clojurescript code may not be the most comfortable 104 | approach._ 105 | 106 | # Errors 107 | 108 | We can signal an error to the caller by returning a rejected promise from our 109 | handler: 110 | 111 | ```clojure 112 | (deflambda error [message ctx] 113 | (p/rejected (js/Error. message))) 114 | ``` 115 | 116 | Or by synchronously throwing a `js/Error`: 117 | 118 | ```clojure 119 | (deflambda wait [n ctx] 120 | (when (zero? n) 121 | (throw (js/Error. "Sorry, but I can't help you"))) 122 | (p/delay n {:waited n})) 123 | ``` 124 | 125 | The same two examples with `core.async`: 126 | 127 | ```clojure 128 | (deflambda error [message ctx] 129 | (go (js/Error. message))) 130 | 131 | (deflambda wait [n ctx] 132 | (when (zero? n) 133 | (throw (js/Error. "Sorry, but I can't help you"))) 134 | (go 135 | ( 27 | (channel testable 5) ;; => 28 | ``` 29 | 30 | ### Example 31 | 32 | ```clojure 33 | (def fs (.promisifyAll promesa/Promise (nodejs/require "fs"))) 34 | 35 | (deflambda read-file [path ctx] 36 | (.readFileAsync fs path)) 37 | 38 | (invoke read-file "static/config.edn") ;; => 39 | ``` 40 | 41 | ### Example 42 | 43 | ```clojure 44 | (deflambda identify [caller-name {my-name :function-name}] 45 | (str "Hi " caller-name " I'm " my-name)) 46 | 47 | ;; The default context values aren't particularly exciting 48 | (invoke identify "Mary") => ;; 49 | ;; But custom values may be supplied 50 | (invoke identify "Mary" (->context {:function-name "identify"})) 51 | ;; => ;; 52 | ``` 53 | 54 | ## Environment Variables 55 | 56 | When testing functions which retrieve environment variables via 57 | [[cljs-lambda.context/env]], alternate values may be supplied in a test's 58 | context object. 59 | 60 | ```clojure 61 | (deflambda env [k ctx] 62 | (ctx/env ctx k)) 63 | 64 | (invoke read-file "USER" (->context {:env {"USER" "moe"}})) 65 | ``` 66 | 67 | When deployed, `ctx/env` would read the value from Node's `process.env`. 68 | -------------------------------------------------------------------------------- /cljs-lambda/project.clj: -------------------------------------------------------------------------------- 1 | (defproject io.nervous/cljs-lambda "0.3.5" 2 | :description "Clojurescript AWS Lambda utilities" 3 | :url "https://github.com/nervous-systems/cljs-lambda" 4 | :license {:name "Unlicense" :url "http://unlicense.org/UNLICENSE"} 5 | :scm {:name "git" :url "https://github.com/nervous-systems/cljs-lambda"} 6 | :dependencies [[org.clojure/clojure "1.8.0"] 7 | [org.clojure/clojurescript "1.8.51"] 8 | [org.clojure/core.async "0.2.395"] 9 | [org.clojure/tools.macro "0.1.2"] 10 | [camel-snake-kebab "0.4.0"] 11 | [funcool/promesa "1.6.0"] 12 | [io.nervous/cljs-nodejs-externs "0.2.0"]] 13 | :plugins [[lein-doo "0.1.7"] 14 | [lein-npm "0.6.2"] 15 | [lein-cljsbuild "1.1.4"] 16 | [lein-codox "0.10.2"]] 17 | ;; Codox can't deal w/ deps.cljs, so isolate externs 18 | :source-paths ["src" "src-deps"] 19 | :profiles {:codox {:source-paths ^:replace ["src"]} 20 | :dev {:dependencies [[io.nervous/codox-nervous-theme "0.1.0"]]}} 21 | :cljsbuild 22 | {:builds [{:id "test" 23 | :source-paths ["src" "test"] 24 | :compiler {:output-to "target/test/cljs-lambda.js" 25 | :output-dir "target/test" 26 | :target :nodejs 27 | :optimizations :none 28 | :main cljs-lambda.test.runner}}]} 29 | :codox 30 | {:metadata {:doc/format :markdown} 31 | :themes [:default [:nervous {:nervous/github "https://github.com/nervous-systems/cljs-lambda/"}]] 32 | :language :clojurescript 33 | :source-uri ~(str "https://github.com/nervous-systems/cljs-lambda/" 34 | "blob/master/cljs-lambda/{filepath}#L{line}")}) 35 | -------------------------------------------------------------------------------- /cljs-lambda/src-deps/cljs_lambda/externs/context.js: -------------------------------------------------------------------------------- 1 | var context = {}; 2 | 3 | context.awsRequestId; 4 | context.clientContext; 5 | context.logGroupName; 6 | context.logStreamName; 7 | context.functionName; 8 | context.callbackWaitsForEmptyEventLoop; 9 | 10 | context.getMemoryLimitInMB = function() {}; 11 | context.getFunctionName = function() {}; 12 | context.getAwsRequestId = function() {}; 13 | context.getLogStreamName = function() {}; 14 | context.getClientContext = function() {}; 15 | context.getIdentity = function() {}; 16 | context.getRemainingTimeInMillis = function() {}; 17 | context.getLogger = function() {}; 18 | 19 | context.succeed = function(result) {}; 20 | context.fail = function(error) {}; 21 | context.done = function(error, result) {}; 22 | -------------------------------------------------------------------------------- /cljs-lambda/src-deps/deps.cljs: -------------------------------------------------------------------------------- 1 | {:externs ["cljs_lambda/externs/context.js"]} 2 | -------------------------------------------------------------------------------- /cljs-lambda/src/cljs_lambda/aws/event.cljc: -------------------------------------------------------------------------------- 1 | (ns cljs-lambda.aws.event 2 | "Utility functionality for converting AWS event inputs & outputs to/from EDN. 3 | 4 | - `from-aws` handles `:aws.event/type` values of `:api-gateway`, 5 | `:notification` (SNS & S3), and `:scheduled`. 6 | - `to-aws` can handle `:api-gateway`." 7 | (:require [camel-snake-kebab.core :as csk] 8 | [camel-snake-kebab.extras :as csk.extras] 9 | [clojure.set :as set] 10 | [clojure.string :as str])) 11 | 12 | (defmulti from-aws 13 | "Interpret input map `event` as an AWS event input. The map's 14 | `:aws.event/type` key will be used to inform transformations." 15 | :aws.event/type) 16 | 17 | (def ^:private ag-renames 18 | {:queryStringParameters :query 19 | :statusCode :status 20 | :httpMethod :method 21 | :resourcePath :path 22 | :isBase64Encoded :base64? 23 | :requestContext :context}) 24 | 25 | (defn- apply-keys [m f] 26 | (csk.extras/transform-keys f m)) 27 | 28 | (defn- lower-case-k [k] 29 | (-> k name str/lower-case keyword)) 30 | 31 | (defn- unmethod [s] 32 | (some-> s csk/->kebab-case-keyword)) 33 | 34 | (defn- rekey [k renames] 35 | (if-let [k (renames k)] 36 | k 37 | (cond-> k (not (namespace k)) csk/->kebab-case-keyword))) 38 | 39 | (defn- unfuck 40 | ([m] 41 | (unfuck m #{} {})) 42 | ([m ignore? renames] 43 | (persistent! 44 | (reduce-kv 45 | (fn [m k v] 46 | (let [k (rekey k renames)] 47 | (if (nil? v) 48 | m 49 | (assoc! m 50 | k (cond (ignore? k) v 51 | (map? v) (unfuck v ignore? renames) 52 | :else v))))) 53 | (transient {}) 54 | m)))) 55 | 56 | (defmethod from-aws :api-gateway [m] 57 | (-> m 58 | (unfuck #{:headers :query} ag-renames) 59 | (update :method unmethod) 60 | (update :headers apply-keys lower-case-k) 61 | (update-in [:context :method] unmethod))) 62 | 63 | (defn- source->key [s & [{delim :delim :or {delim ":"}}]] 64 | (when s 65 | (if-let [i (str/index-of s delim)] 66 | (keyword (subs s 0 i) (subs s (inc i))) 67 | (keyword s)))) 68 | 69 | (defmulti ^:no-doc notification->cljs :source) 70 | 71 | (defn- sns-attrs->cljs [m] 72 | (into {} 73 | (for [[k attr] m] 74 | [k {:type (csk/->kebab-case-keyword (attr :Type)) 75 | :value (attr :Value)}]))) 76 | 77 | (defmethod notification->cljs :aws/sns [{sns :Sns :as m}] 78 | (let [sns (-> (unfuck sns #{:attrs} {:MessageAttributes :attrs 79 | :TopicArn :topic 80 | :MessageId :id}) 81 | (update :type csk/->kebab-case-keyword) 82 | (update :attrs sns-attrs->cljs))] 83 | (-> m 84 | (dissoc :Sns) 85 | (assoc :sns sns)))) 86 | 87 | (defn- update-when [m k f] 88 | (cond-> m 89 | (not (nil? (m k))) (update k f))) 90 | 91 | (let [renames {:responseElements :response 92 | :requestParameters :request 93 | :userIdentity :user 94 | :eTag :etag 95 | :s3 :s3 96 | :s3SchemaVersion :s3-schema-version}] 97 | (defmethod notification->cljs :aws/s3 [m] 98 | (-> m 99 | (unfuck #{} renames) 100 | (update-when :region keyword)))) 101 | 102 | (let [renames {:EventSource :source 103 | :eventSource :source 104 | :awsRegion :region 105 | :EventVersion :version 106 | :EventSubscriptionArn :subscription}] 107 | (defmethod from-aws :notification [{records :Records}] 108 | {:records (into [] 109 | (for [record records] 110 | (-> record 111 | (set/rename-keys renames) 112 | (update :source source->key) 113 | notification->cljs))) 114 | :aws.event/type :notification})) 115 | 116 | (defmethod from-aws :scheduled [m] 117 | (-> m 118 | (update :source source->key {:delim "."}) 119 | (update :region keyword))) 120 | 121 | (defmulti to-aws* 122 | "Interpret input map `event` as an AWS event output. The map's 123 | `:aws.event/type` key will be used to inform transformations." 124 | :aws.event/type) 125 | 126 | (let [ag-renames (set/map-invert ag-renames)] 127 | (defmethod to-aws* :api-gateway [m] 128 | (set/rename-keys m ag-renames))) 129 | 130 | (defn to-aws 131 | "Inverse of [[from-aws]], for response/output events. Defers 132 | to [[to-aws*]], and removes `:aws.event/type`, on the assumption that the 133 | returned map will be passed to AWS." 134 | [{:keys [aws.event/type] :as event}] 135 | (-> (to-aws* event) 136 | (dissoc :aws.event/type))) 137 | -------------------------------------------------------------------------------- /cljs-lambda/src/cljs_lambda/context.cljs: -------------------------------------------------------------------------------- 1 | (ns cljs-lambda.context 2 | "Representation & manipulation of Lambda-handler execution context. 3 | 4 | Contexts are represented as records with keys: 5 | 6 | * `:aws-request-id` 7 | * `:client-context` 8 | * `:log-group-name` 9 | * `:log-stream-name` 10 | * `:function-name` 11 | * `:function-arn` 12 | * `:identity` (optional) 13 | * `:handler-callback` (optional) 14 | ") 15 | 16 | (defn- json->edn [json] 17 | (js->clj (js/JSON.parse (js/JSON.stringify json)))) 18 | 19 | (defprotocol ContextHandle 20 | (-done! 21 | [this err result] 22 | "See [[done!]]") 23 | (msecs-remaining 24 | [this] 25 | "The number of milliseconds remaining until the timeout of the invocation 26 | associated with this context.") 27 | (environment 28 | [this] 29 | "Retrieve a map of environment variables.") 30 | (waits? 31 | [this] 32 | "By default, the callback will wait until the Node.js runtime event loop is 33 | empty before freezing the process and returning the results to the caller. 34 | You can set this property to false to request AWS Lambda to freeze the 35 | process soon after the callback is called, even if there are events in the 36 | event loop.") 37 | (set-wait! 38 | [this tf] 39 | "Set the callback-waits")) 40 | 41 | (defrecord ^:no-doc LambdaContext [js-handle] 42 | ContextHandle 43 | (-done! [this err result] 44 | (.done js-handle err result)) 45 | (msecs-remaining [this] 46 | (.getRemainingTimeInMillis js-handle)) 47 | (environment [this] 48 | (json->edn js/process.env)) 49 | (waits? [this] 50 | (.-callbackWaitsForEmptyEventLoop js-handle)) 51 | (set-wait! [this tf] 52 | (set! (.-callbackWaitsForEmptyEventLoop js-handle) tf))) 53 | 54 | (defn waits-on-event-loop? 55 | [ctx] 56 | "Returns state of context property callbackWaitsForEmptyEventLoop" 57 | (waits? ctx)) 58 | 59 | (defn set-wait-on-event-loop! 60 | "Set the context property callbackWaitsForEmptyEventLoop" 61 | [ctx tf] 62 | (set-wait! ctx tf)) 63 | 64 | (defn env 65 | "Retrieve an environment variable by name, defaulting to `nil` if not found. 66 | 67 | ```clojure 68 | (env ctx \"USER\") 69 | (env ctx :USER) 70 | (env ctx 'USER) 71 | ```" 72 | [ctx k] 73 | (get (environment ctx) (name k))) 74 | 75 | (defn done! 76 | "Terminate execution of the handler associated w/ the given context, conveying 77 | the given error (if non-nil), or the given success result (if non-nil). No 78 | arguments communicates generic success. 79 | 80 | ```clojure 81 | (deflambda quick [_ ctx] 82 | (ctx/done! ctx)) 83 | ```" 84 | [ctx & [err result]] 85 | (-done! ctx (clj->js err) (clj->js result))) 86 | 87 | (defn fail! 88 | "Trivial wrapper around [[done!]] 89 | 90 | Terminate execution of the handler associated w/ the given context, conveying 91 | the given error, if non-nil - otherwise mark the execution as failed w/ no 92 | specific error. 93 | 94 | ```clojure 95 | (deflambda purchase [item-name ctx] 96 | (ctx/fail! ctx (js/Error. (str \"Sorry, no more \" item-name)))) 97 | ```" 98 | [ctx & [err]] 99 | (done! ctx err nil)) 100 | 101 | (defn succeed! 102 | "Trivial wrapper around [[done!]] 103 | 104 | Terminate execution of the handler associated w/ the given context, conveying 105 | the given JSON-serializable success value, if non-nil - otherwise mark the 106 | execution as successful w/ no specific result. 107 | 108 | ```clojure 109 | (deflambda purchase [item-name ctx] 110 | (ctx/succeed! ctx \"You bought something\")) 111 | ```" 112 | [ctx & [result]] 113 | (done! ctx nil result)) 114 | 115 | (def ^:no-doc context-keys 116 | {:aws-request-id "awsRequestId" 117 | :client-context "clientContext" 118 | :log-group-name "logGroupName" 119 | :log-stream-name "logStreamName" 120 | :function-name "functionName" 121 | :function-arn "invokedFunctionArn"}) 122 | 123 | (defn- identity-map [js-context] 124 | (when-let [id (.. js-context -identity)] 125 | {:cognito-id (.. id -cognitoIdentityId) 126 | :cognito-pool (.. id -cognitoIdentityPoolId)})) 127 | 128 | (defn ^:no-doc ->context [js-context] 129 | (let [id (identity-map js-context) 130 | cb (.. js-context -handler-callback)] 131 | (cond-> (into (->LambdaContext js-context) 132 | (for [[us them] context-keys] 133 | [us (aget js-context them)])) 134 | id (assoc :identity id) 135 | cb (assoc :handler-callback cb)))) 136 | -------------------------------------------------------------------------------- /cljs-lambda/src/cljs_lambda/local.cljs: -------------------------------------------------------------------------------- 1 | (ns cljs-lambda.local 2 | "Utilities for the local (e.g. automated tests, REPL interactions) invocation 3 | of Lambda handlers. Local invocation is accomplished by passing handlers a 4 | stub context object which records completion signals." 5 | (:require [promesa.core :as p] 6 | [cljs-lambda.context :as ctx] 7 | [cljs.core.async :as async]) 8 | (:require-macros [cljs.core.async.macros :refer [go]])) 9 | 10 | (defrecord ^:no-doc LocalContext [result-channel env] 11 | ctx/ContextHandle 12 | (-done! [this err result] 13 | (async/put! 14 | result-channel [(js->clj err :keywordize-keys true) 15 | (js->clj result :keywordize-keys true)])) 16 | (msecs-remaining [this] 17 | -1) 18 | (environment [this] 19 | env) 20 | (waits? [this] 21 | true) 22 | (set-wait! [this tf] 23 | tf)) 24 | 25 | (defn- stringify-keys 26 | "Shallowly un-keyword/un-symbol the keys in m" 27 | [m] 28 | (into {} 29 | (for [[k v] m] 30 | [(name k) v]))) 31 | 32 | (defn ->context 33 | "Create a `context` object for use w/ [[invoke]], [[channel]]. This is 34 | helpful if your want to take advantage of `key-overrides` to supply different 35 | context values for an invocation -- otherwise, no need to use directly. 36 | 37 | ```clojure 38 | (invoke wait {:msecs 17} (->context {:function-name \"wait\"})) 39 | ``` 40 | 41 | By default, the values for the context keys will match the key names, more or 42 | less, e.g. `{:function-name \"functionName\"}`." 43 | [& [key-overrides]] 44 | (map->LocalContext 45 | (merge ctx/context-keys 46 | {:result-channel (async/promise-chan)} 47 | (update key-overrides :env stringify-keys)))) 48 | 49 | (defn- channel->promise [ch] 50 | (p/promise 51 | (fn [resolve reject] 52 | (go 53 | (let [[err result] ( 69 | ```" 70 | ([f] (invoke f nil)) 71 | ([f event] (invoke f event (->context))) 72 | ([f event {:keys [result-channel] :as ctx}] 73 | (let [promise (channel->promise result-channel)] 74 | (f event ctx) 75 | promise))) 76 | 77 | (defn channel 78 | "Identical semantics to [[invoke]], though the return value is a `core.async` 79 | channel containing either `[:succeed ]` or `[:fail ]`. 80 | 81 | ```clojure 82 | (deflambda please-repeat [[n x] ctx] 83 | (promesa/resolved (repeat n x))) 84 | 85 | (invoke please-repeat [3 :x]) 86 | ;; => 87 | ```" 88 | ([f] (channel f nil)) 89 | ([f event] (channel f event (->context))) 90 | ([f event {input-channel :result-channel :as ctx}] 91 | (f event ctx) 92 | (go 93 | (let [[err result] ( event :headers :content-type)} 44 | :body (event :body)}) 45 | ```" 46 | [name & body] 47 | (let [[name [bindings & body]] (macro/name-with-attributes name body)] 48 | `(def ~(vary-meta name assoc :export true) 49 | (cljs-lambda.util/async-lambda-fn 50 | (fn [event# & args#] 51 | (let [event# (-> (assoc event# :aws.event/type :api-gateway) 52 | cljs-lambda.aws.event/from-aws)] 53 | (p/then (apply cljs-lambda.util/invoke-async 54 | (fn ~bindings ~@body) 55 | (conj args# event#)) 56 | (comp cljs-lambda.aws.event/to-aws 57 | #(assoc % :aws.event/type :api-gateway)))))))))) 58 | -------------------------------------------------------------------------------- /cljs-lambda/src/cljs_lambda/util.cljs: -------------------------------------------------------------------------------- 1 | (ns cljs-lambda.util 2 | (:require [cljs.nodejs :as nodejs] 3 | [cljs.core.async :as async :refer [!]] 4 | [cljs.core.async.impl.protocols :as async-p] 5 | [cljs-lambda.context :as ctx] 6 | [promesa.core :as p]) 7 | (:require-macros [cljs.core.async.macros :refer [go]])) 8 | 9 | (nodejs/enable-util-print!) 10 | (set! *main-cli-fn* identity) 11 | 12 | (defn wrap-lambda-fn 13 | "Prepare a two-arg (event, context) function for exposure as a Lambda handler. 14 | The returned function will convert the event (Javascript Object) into a 15 | Clojurescript map with keyword keys, and turn the context into a record having 16 | keys `:aws-request-id`, `:client-context`, `:log-group-name`, 17 | `:log-stream-name` & `:function-name` - suitable for manipulation 18 | by [[context/done!]] etc." 19 | [f & [{parse-input? :parse-input? :or {parse-input? true}}]] 20 | (fn [event ctx & [cb]] 21 | (when (fn? cb) 22 | (set! (.. ctx -handler-callback) 23 | (fn [err & [value]] 24 | (cb (clj->js err) (clj->js value))))) 25 | (f (if parse-input? (js->clj event :keywordize-keys true) event) 26 | (cond-> ctx 27 | (not (satisfies? ctx/ContextHandle ctx)) ctx/->context)))) 28 | 29 | (defn- chan? [x] 30 | (satisfies? async-p/ReadPort x)) 31 | 32 | (defn- error? [x] 33 | (instance? js/Error x)) 34 | 35 | (defn ^:no-doc invoke-async [f & args] 36 | (p/promise 37 | (fn [resolve reject] 38 | (let [handle #(if (error? %) (reject %) (resolve %))] 39 | (try 40 | (let [result (apply f args)] 41 | (cond (p/promise? result) (p/branch result resolve reject) 42 | (chan? result) (go (handle ( (fn [event ctx] (p/rejected (js/Error.))) 56 | (handle-errors (fn [e event ctx] \"Success\")) 57 | async-lambda-fn)) 58 | ```" 59 | [f error-handler] 60 | (fn [event context] 61 | (p/catch 62 | (invoke-async f event context) 63 | #(invoke-async error-handler % event context)))) 64 | 65 | (defn async-lambda-fn 66 | "Repurpose the two-arg (event, context) asynchronous function `f` as a Lambda 67 | handler. The function's result determines the invocation's success at the 68 | Lambda level, without the requirement of using 69 | Lambda-specific ([[context/fail!]], etc.) functionality within the body. 70 | Optional error handler behaves as [[handle-errors]]. 71 | 72 | If the handler was passed a callback by the Lambda harness, that function will 73 | be used to signal completion, over the the context methods. 74 | 75 | Success: 76 | 77 | * Returns successful Promesa/Bluebird promise 78 | * Returns `core.async` channel containing non-`js/Error` 79 | * Synchronously returns arbitrary object 80 | 81 | ```clojure 82 | (def ^:export wait 83 | (async-lambda-fn 84 | (fn [{n :msecs} ctx] 85 | (promesa/delay n :waited)))) 86 | ``` 87 | 88 | Failure: 89 | 90 | * Returns rejected Promesa/Bluebird promise 91 | * Returns `core.async` channel containing `js/Error` 92 | * Synchronously throws `js/Error` 93 | 94 | ```clojure 95 | (def ^:export blow-up 96 | (async-lambda-fn 97 | (fn [_ ctx] 98 | (go 99 | ( f error-handler (handle-errors error-handler))] 107 | (wrap-lambda-fn 108 | (fn [event ctx] 109 | (let [cb (or (:handler-callback ctx) (partial ctx/done! ctx))] 110 | (-> (invoke-async f event ctx) 111 | (p/branch (partial cb nil) cb)))) 112 | opts))) 113 | -------------------------------------------------------------------------------- /cljs-lambda/test/cljs_lambda/test/help.cljc: -------------------------------------------------------------------------------- 1 | (ns cljs-lambda.test.help 2 | #? (:cljs (:require-macros [cljs-lambda.test.help]))) 3 | 4 | #? (:clj 5 | (defmacro deftest-async [test-name & body] 6 | `(cljs.test/deftest ~test-name 7 | (let [result# (do ~@body)] 8 | (cljs.test/async 9 | done# 10 | (promesa.core/branch result# 11 | done# 12 | (fn [e#] 13 | (println (.. e# -stack)) 14 | (cljs.test/is (not e#)) 15 | (done#)))))))) 16 | -------------------------------------------------------------------------------- /cljs-lambda/test/cljs_lambda/test/macros.cljs: -------------------------------------------------------------------------------- 1 | (ns cljs-lambda.test.macros 2 | (:require [cljs-lambda.context :as ctx] 3 | [cljs-lambda.macros :as macros] 4 | [cljs-lambda.local :refer [invoke]] 5 | [cljs.test :refer-macros [deftest is]] 6 | [promesa.core :as p]) 7 | (:require-macros [cljs-lambda.test.help :refer [deftest-async]])) 8 | 9 | (macros/deflambda def-wrappers-are-evil [event ctx] 10 | (ctx/done! ctx nil event)) 11 | 12 | (deftest-async deflambda 13 | (let [event [1 2 "hello"]] 14 | (p/then (invoke def-wrappers-are-evil event) 15 | #(is (= % event))))) 16 | 17 | (deftest deflambda-exports 18 | (is (-> #'def-wrappers-are-evil meta :export))) 19 | -------------------------------------------------------------------------------- /cljs-lambda/test/cljs_lambda/test/runner.cljs: -------------------------------------------------------------------------------- 1 | (ns cljs-lambda.test.runner 2 | (:require [doo.runner :refer-macros [doo-tests]] 3 | [cljs-lambda.test.util] 4 | [cljs-lambda.test.macros])) 5 | 6 | (doo-tests 'cljs-lambda.test.util 7 | 'cljs-lambda.test.macros) 8 | -------------------------------------------------------------------------------- /cljs-lambda/test/cljs_lambda/test/util.cljs: -------------------------------------------------------------------------------- 1 | (ns cljs-lambda.test.util 2 | (:require [cljs-lambda.util :as lambda] 3 | [cljs-lambda.local :as local :refer [invoke]] 4 | [cljs-lambda.context :as ctx] 5 | [cljs.test :refer-macros [deftest is]] 6 | [promesa.core :as p]) 7 | (:require-macros [cljs.core.async.macros :refer [go]] 8 | [cljs-lambda.test.help :refer [deftest-async]])) 9 | 10 | (defn will= [x] 11 | #(is (= % x))) 12 | 13 | (defn underlying-fn [[tag value] context] 14 | (case tag 15 | :fail (ctx/fail! context value) 16 | :succeed (ctx/succeed! context value) 17 | :chan (go value) 18 | :fail-promise (p/rejected value) 19 | :succeed-promise (p/resolved value))) 20 | 21 | (def lambda-fn (lambda/async-lambda-fn underlying-fn)) 22 | 23 | (defn catches [p catch] 24 | (p/branch p #(is false (str "Success branch reached w/" %)) catch)) 25 | 26 | (deftest-async fail-promise 27 | (let [value (js/Error "This isn't an actual error")] 28 | (catches 29 | (invoke lambda-fn [:fail-promise value]) 30 | (will= value)))) 31 | 32 | (deftest-async succeed-promise 33 | (let [value "OK, ok. OK"] 34 | (p/then 35 | (invoke lambda-fn [:succeed-promise value]) 36 | (will= value)))) 37 | 38 | (deftest-async fail 39 | (let [error (js/Error. "ayy lmao")] 40 | (catches 41 | (invoke lambda-fn [:fail error]) 42 | (will= error)))) 43 | 44 | (deftest-async bail 45 | (let [error (js/Error. "3 eggs") 46 | f (lambda/async-lambda-fn 47 | (fn [_ ctx] 48 | (ctx/fail! ctx error)))] 49 | (catches 50 | (invoke f) 51 | (will= error)))) 52 | 53 | (deftest-async bail-indirect 54 | (let [error (js/Error. "17 eggs") 55 | f (lambda/async-lambda-fn 56 | (fn [_ ctx] 57 | (go 58 | (ctx/fail! ctx error) 59 | "Some other value")))] 60 | (catches 61 | (invoke f) 62 | (will= error)))) 63 | 64 | (deftest-async bail-eternal 65 | (let [error (js/Error. "12 eggs") 66 | f (lambda/async-lambda-fn 67 | (fn [_ ctx] 68 | (p/promise 69 | (fn [_ _] 70 | (js/setTimeout #(ctx/fail! ctx error) 50)))))] 71 | (catches 72 | (invoke f) 73 | (will= error)))) 74 | 75 | (deftest-async done 76 | (let [f (lambda/async-lambda-fn 77 | (fn [_ ctx] 78 | (ctx/done! ctx nil "deftest-async done")))] 79 | (p/then 80 | (invoke f) 81 | (will= "deftest-async done")))) 82 | 83 | (deftest-async done-error 84 | (let [error (js/Error. "deftest-async done-error") 85 | f (lambda/async-lambda-fn 86 | (fn [_ ctx] 87 | (ctx/done! ctx error)))] 88 | (catches 89 | (invoke f) 90 | (will= error)))) 91 | 92 | (deftest-async succeed 93 | (p/then 94 | (invoke lambda-fn [:succeed "ayy lmao"]) 95 | (will= "ayy lmao"))) 96 | 97 | (deftest-async handle-errors 98 | (let [f (-> #(throw (js/Error. "ERROR HANDLER IN PLACE")) 99 | (lambda/handle-errors (constantly "Success")) 100 | lambda/async-lambda-fn)] 101 | (p/then (invoke f) 102 | (will= "Success")))) 103 | 104 | (deftest-async error-handler 105 | (let [error (js/Error. "Porcupine Z") 106 | event {:X 'y} 107 | f (lambda/async-lambda-fn 108 | #(throw error) 109 | {:error-handler 110 | (fn [error* event* ctx*] 111 | (is (= error error*)) 112 | (is (= event event*)) 113 | (p/resolved "Porcupine X"))})] 114 | (p/then (invoke f event) 115 | (will= "Porcupine X")))) 116 | 117 | (deftest-async error-handler-error 118 | (let [error (js/Error. "Porcupine Z") 119 | error-in (js/Error. "Scissors") 120 | f (lambda/async-lambda-fn 121 | #(throw error) 122 | {:error-handler #(throw error-in)})] 123 | (catches (invoke f) (will= error-in)))) 124 | 125 | (deftest-async error-handler-skipped 126 | (let [f (lambda/async-lambda-fn 127 | (constantly "Everything's OK!") 128 | {:error-handler #(throw (js/Error. "Wilderness"))})] 129 | (p/then (invoke f) 130 | (will= "Everything's OK!")))) 131 | 132 | (deftest-async msecs-remaining 133 | (let [f (lambda/async-lambda-fn 134 | (fn [_ ctx] 135 | (ctx/msecs-remaining ctx)))] 136 | (p/then (invoke f) 137 | (will= -1)))) 138 | 139 | (deftest-async env 140 | (let [f (lambda/async-lambda-fn 141 | (fn [_ ctx] 142 | (ctx/env ctx :ENV_VAR))) 143 | ctx (local/->context {:env {'ENV_VAR "deftest-async env"}})] 144 | (p/then (invoke f nil ctx) 145 | (will= "deftest-async env")))) 146 | 147 | (deftest-async 148 | waits 149 | (let [f (lambda/async-lambda-fn 150 | (fn [_ ctx] 151 | (ctx/waits-on-event-loop? ctx)))] 152 | (p/then 153 | (invoke f) 154 | (will= true)))) 155 | 156 | (deftest-async 157 | set-waits 158 | (let [f (lambda/async-lambda-fn 159 | (fn [_ ctx] 160 | (ctx/set-wait-on-event-loop! ctx false)))] 161 | (p/then 162 | (invoke f) 163 | (will= false)))) 164 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | # example 2 | 3 | ## Deploying 4 | 5 | Run `lein cljs-lambda default-iam-role` if you don't have yet have suitable 6 | execution role to place in your project file. This command will create an IAM 7 | role under your default (or specified) AWS CLI profile, and modify your project 8 | file to specify it as the execution default. 9 | 10 | Otherwise, add an IAM role ARN under the function's `:role` key in the 11 | `:functions` vector of your profile file, or in `:cljs-lambda` -> `:defaults` -> 12 | `:role`. 13 | 14 | Then: 15 | 16 | ```sh 17 | $ lein cljs-lambda deploy 18 | $ lein cljs-lambda invoke work-magic ... 19 | ``` 20 | 21 | ## Testing 22 | 23 | ```sh 24 | lein doo node example-test 25 | ``` 26 | 27 | Doo is provided to avoid including code to set the process exit code after a 28 | test run. 29 | -------------------------------------------------------------------------------- /example/project.clj: -------------------------------------------------------------------------------- 1 | (defproject example "0.1.0-SNAPSHOT" 2 | :description "FIXME" 3 | :url "http://please.FIXME" 4 | :dependencies [[org.clojure/clojure "1.8.0"] 5 | [org.clojure/clojurescript "1.8.51"] 6 | [org.clojure/core.async "0.2.395"] 7 | [io.nervous/cljs-lambda "0.3.5"]] 8 | :plugins [[lein-cljsbuild "1.1.4"] 9 | [lein-npm "0.6.0"] 10 | [lein-doo "0.1.7"] 11 | [io.nervous/lein-cljs-lambda "0.6.6"]] 12 | :npm {:dependencies [[source-map-support "0.4.0"]]} 13 | :source-paths ["src"] 14 | :cljs-lambda 15 | {:defaults {:role "FIXME"} 16 | :resource-dirs ["static"] 17 | :functions 18 | [{:name "work-magic" 19 | :invoke example.core/work-magic}]} 20 | :cljsbuild 21 | {:builds [{:id "example" 22 | :source-paths ["src"] 23 | :compiler {:output-to "target/example/example.js" 24 | :output-dir "target/example" 25 | :source-map true 26 | :target :nodejs 27 | :language-in :ecmascript5 28 | :optimizations :none}} 29 | {:id "example-test" 30 | :source-paths ["src" "test"] 31 | :compiler {:output-to "target/example-test/example.js" 32 | :output-dir "target/example-test" 33 | :target :nodejs 34 | :language-in :ecmascript5 35 | :optimizations :none 36 | :main example.test-runner}}]}) 37 | -------------------------------------------------------------------------------- /example/src/example/core.cljs: -------------------------------------------------------------------------------- 1 | (ns example.core 2 | (:require [cljs-lambda.util :as lambda] 3 | [cljs-lambda.context :as ctx] 4 | [cljs-lambda.macros :refer-macros [deflambda]] 5 | [cljs.reader :refer [read-string]] 6 | [cljs.nodejs :as nodejs] 7 | [cljs.core.async :as async] 8 | [promesa.core :as p]) 9 | (:require-macros [cljs.core.async.macros :refer [go]])) 10 | 11 | (def config 12 | (-> (nodejs/require "fs") 13 | (.readFileSync "static/config.edn" "UTF-8") 14 | read-string)) 15 | 16 | (defmulti cast-async-spell (fn [{spell :spell} ctx] (keyword spell))) 17 | 18 | (defmethod cast-async-spell :delay-channel 19 | [{:keys [msecs] :or {msecs 1000}} ctx] 20 | (go 21 | ( p 13 | (p/catch #(is (not %))) 14 | (p/then done)))) 15 | 16 | (defn with-some-error [p] 17 | (p/branch p 18 | #(is false "Expected error") 19 | (constantly nil))) 20 | 21 | (deftest echo 22 | (-> (invoke work-magic {:magic-word "not the magic word"}) 23 | with-some-error 24 | with-promised-completion)) 25 | 26 | (def delay-channel-req 27 | {:magic-word (:magic-word config) 28 | :spell :delay-channel 29 | :msecs 2}) 30 | 31 | (deftest delay-channel-spell 32 | (with-promised-completion 33 | (alet [{:keys [waited]} (await (invoke work-magic delay-channel-req))] 34 | (is (= waited 2))))) 35 | 36 | (deftest delay-channel-spell-go 37 | (cljs.test/async 38 | done 39 | (go 40 | (let [[tag response] ( (invoke work-magic 47 | {:magic-word (:magic-word config) 48 | :spell :delay-fail 49 | :msecs 3}) 50 | with-some-error 51 | with-promised-completion)) 52 | -------------------------------------------------------------------------------- /example/test/example/test_runner.cljs: -------------------------------------------------------------------------------- 1 | (ns example.test-runner 2 | (:require [doo.runner :refer-macros [doo-tests]] 3 | [example.core-test] 4 | [cljs.nodejs :as nodejs])) 5 | 6 | (try 7 | (.install (nodejs/require "source-map-support")) 8 | (catch :default _)) 9 | 10 | (doo-tests 11 | 'example.core-test) 12 | -------------------------------------------------------------------------------- /plugin/README.md: -------------------------------------------------------------------------------- 1 | # lein-cljs-lambda 2 | 3 | The `lein-cljs-lambda` plugin enables the declaration of Lambda-deployable 4 | functions from Leiningen project files. 5 | 6 | [![Clojars 7 | Project](http://clojars.org/io.nervous/lein-cljs-lambda/latest-version.svg)](http://clojars.org/io.nervous/lein-cljs-lambda) 8 | 9 | Using this project will require a recent [Node](https://nodejs.org/) runtime, 10 | and a properly-configured (`aws configure`) [AWS 11 | CLI](https://github.com/aws/aws-cli) installation **>= 1.7.31**. Please run 12 | `pip install --upgrade awscli` if you're using an older version (`aws 13 | --version`). 14 | 15 | ```sh 16 | $ lein new cljs-lambda my-lambda-project 17 | $ cd my-lambda-project 18 | $ lein cljs-lambda default-iam-role 19 | $ lein cljs-lambda deploy 20 | ### 500ms delay via a promise (try also "delay-channel" and "delay-fail") 21 | $ lein cljs-lambda invoke work-magic \ 22 | '{"spell": "delay-promise", "msecs": 500, "magic-word": "my-lambda-project-token"}' 23 | ... {:waited 500} 24 | ### Get environment varibles 25 | $ lein cljs-lambda invoke work-magic \ 26 | '{"spell": "echo-env", "magic-word": "my-lambda-project-token"}' 27 | ... 28 | $ lein cljs-lambda update-config work-magic :memory-size 256 :timeout 66 29 | ``` 30 | 31 | # project.clj Excerpt 32 | 33 | ```clojure 34 | {... 35 | :cljs-lambda 36 | {:defaults {:role "arn:aws:iam::151963828411:role/..."} 37 | :resource-dirs ["config"] 38 | :managed-deps false 39 | :region ... ;; This'll default to your AWS CLI profile's region 40 | :functions 41 | [{:name "dog-bark" 42 | :invoke cljs-lambda-example.dog/bark} 43 | {:name "cat-meow" 44 | :invoke cljs-lambda-example.cat/meow}]}} 45 | ``` 46 | 47 | The wiki's [plugin 48 | reference](https://github.com/nervous-systems/cljs-lambda/wiki/Plugin-Reference) 49 | has more details. 50 | 51 | # Function Configuration 52 | 53 | The `:functions` vector within a `:cljs-lambda` map is comprised of maps, each 54 | describing a function which'll be deployed when `lein cljs-lambda deploy` is 55 | invoked. An example: 56 | 57 | ```clojure 58 | {:name "the-lambda-function-name" 59 | :invoke my-cljs-namespace.module/fn 60 | :role "arn:..." 61 | ;; Optional w/ defaults: 62 | :region ... ;; Defers to [:cljs-lambda :region] or AWS CLI 63 | :description nil 64 | :create true 65 | :timeout 3 ;; seconds 66 | :memory-size 128 ;; MB 67 | :vpc {:subnets [] :security-groups []} 68 | :dead-letter "arn:..." 69 | :env {"VAR_A" "VALUE_A" 70 | "VAR_B" "VALUE_B"}} 71 | ``` 72 | 73 | **NOTE:** Environment variables are case sensitive. You can provide `VAR_A` and `VAR_a`. Just be careful. 74 | 75 | The wiki's [plugin 76 | reference](https://github.com/nervous-systems/cljs-lambda/wiki/Plugin-Reference) 77 | has more details. 78 | 79 | ## Building Your Project 80 | 81 | `lein-cljs-lambda` supports either invoking the Clojurescript compiler directly, 82 | or using `cljsbuild`. If neither `:compiler` nor `:cljs-build-id` are present in 83 | a project's `:cljs-lambda` map, cljsbuild is assumed, and the default/first 84 | build will be used. 85 | 86 | - Source map support will be enabled if the `:source-map` key of the active build 87 | is `true`. 88 | - If `:optimizations` is set to `:advanced` on the active build, the zip output 89 | will be structured accordingly (i.e. it'll only contain `index.js` the single 90 | compiler output file, and the source map, if any). 91 | - With `:advanced`, `*main-cli-fn*` is required to be set (i.e. `(set! *main-cli-fn* identity)`) 92 | -------------------------------------------------------------------------------- /plugin/project.clj: -------------------------------------------------------------------------------- 1 | (defproject io.nervous/lein-cljs-lambda "0.6.6" 2 | :description "Deploying Clojurescript functions to AWS Lambda" 3 | :url "https://github.com/nervous-systems/cljs-lambda" 4 | :license {:name "Unlicense" :url "http://unlicense.org/UNLICENSE"} 5 | :dependencies [[lein-cljsbuild "1.1.4"] 6 | [lein-npm "0.6.2"] 7 | [base64-clj "0.1.1"] 8 | [de.ubercode.clostache/clostache "1.4.0"] 9 | [org.apache.commons/commons-compress "1.11"]] 10 | :exclusions [org.clojure/clojure] 11 | :eval-in-leiningen true) 12 | -------------------------------------------------------------------------------- /plugin/resources/default-iam-policy.json: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "2012-10-17", 3 | "Statement": [ 4 | { 5 | "Effect": "Allow", 6 | "Action": [ 7 | "logs:CreateLogGroup", 8 | "logs:CreateLogStream", 9 | "logs:PutLogEvents" 10 | ], 11 | "Resource": "arn:aws:logs:*:*:*" 12 | } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /plugin/resources/default-iam-role.json: -------------------------------------------------------------------------------- 1 | {"Version": "2012-10-17", 2 | "Statement": 3 | [{"Sid": "", 4 | "Effect": "Allow", 5 | "Principal": { 6 | "Service": "lambda.amazonaws.com"}, 7 | "Action": "sts:AssumeRole"}]} 8 | -------------------------------------------------------------------------------- /plugin/resources/index-advanced.mustache: -------------------------------------------------------------------------------- 1 | {{#source-map}} 2 | try { 3 | require("source-map-support").install(); 4 | } catch(err) { 5 | } 6 | {{/source-map}} 7 | 8 | {{#env}} 9 | process.env.{{key}} = "{{value}}"; 10 | {{/#env}} 11 | 12 | require("./{{output-to}}"); 13 | 14 | {{#module}} 15 | {{#function}} 16 | exports.{{export}} = {{js-name}}; 17 | {{/function}} 18 | {{/module}} 19 | -------------------------------------------------------------------------------- /plugin/resources/index-none.mustache: -------------------------------------------------------------------------------- 1 | {{#source-map}} 2 | try { 3 | require("source-map-support").install(); 4 | } catch(err) { 5 | } 6 | {{/source-map}} 7 | 8 | {{#env}} 9 | process.env.{{key}} = "{{value}}"; 10 | {{/#env}} 11 | 12 | require("./{{output-dir}}/goog/bootstrap/nodejs.js"); 13 | 14 | // It's not clear why this is necessary 15 | goog.global.CLOSURE_UNCOMPILED_DEFINES = {"cljs.core._STAR_target_STAR_":"nodejs"}; 16 | 17 | require("./{{output-to}}"); 18 | 19 | {{#module}} 20 | goog.require("{{name}}"); 21 | 22 | {{#function}} 23 | exports.{{export}} = {{js-name}}; 24 | {{/function}} 25 | {{/module}} 26 | -------------------------------------------------------------------------------- /plugin/resources/index-simple.mustache: -------------------------------------------------------------------------------- 1 | {{#source-map}} 2 | try { 3 | require("source-map-support").install(); 4 | } catch(err) { 5 | } 6 | {{/source-map}} 7 | 8 | {{#env}} 9 | process.env.{{key}} = "{{value}}"; 10 | {{/#env}} 11 | 12 | var __CLJS_LAMBDA_NS_ROOT = require("./{{output-to}}"); 13 | 14 | {{#module}} 15 | {{#function}} 16 | exports.{{export}} = __CLJS_LAMBDA_NS_ROOT.{{js-name}}; 17 | {{/function}} 18 | {{/module}} 19 | -------------------------------------------------------------------------------- /plugin/src/leiningen/cljs_lambda.clj: -------------------------------------------------------------------------------- 1 | (ns leiningen.cljs-lambda 2 | (:require [clojure.java.io :as io] 3 | [clojure.string :as str] 4 | [cheshire.core :as json] 5 | [leiningen.core.main :as main] 6 | [leiningen.core.eval :as eval] 7 | [leiningen.cljs-lambda.zip-tedium :refer [write-zip]] 8 | [leiningen.cljs-lambda.aws :as aws] 9 | [leiningen.cljs-lambda.logging :as logging :refer [log]] 10 | [leiningen.cljs-lambda.args :as args] 11 | [leiningen.npm :as npm] 12 | [leiningen.cljsbuild :as cljsbuild] 13 | [leiningen.change :as change] 14 | [leiningen.cljsbuild.config :as cljsbuild.config] 15 | [clostache.parser]) 16 | (:import [java.io File])) 17 | 18 | (defn- export-name [sym] 19 | (str/replace (munge sym) #"\." "_")) 20 | 21 | (defn- fns->module-template [fns] 22 | (for [[ns fns] (group-by namespace (map :invoke fns))] 23 | {:name (munge ns) 24 | :function 25 | (for [f fns] 26 | ;; This is Clojure's munge, which isn't always going to be right 27 | {:export (export-name f) 28 | :js-name (str (munge ns) "." (munge (name f)))})})) 29 | 30 | (defn- generate-index [env {:keys [optimizations source-map] :as compiler-opts} fns] 31 | (let [template (slurp (io/resource 32 | (str "index-" (name optimizations) ".mustache")))] 33 | (clostache.parser/render 34 | template 35 | (assoc compiler-opts 36 | :source-map (when source-map true) 37 | :module (fns->module-template fns) 38 | :env (for [[k v] env] 39 | {:key k :value v}))))) 40 | 41 | (defn- write-index [output-dir s] 42 | (let [file (io/file output-dir "index.js")] 43 | (log :verbose "Writing index to" (.getAbsolutePath file)) 44 | (with-open [w (io/writer file)] 45 | (.write w s)) 46 | (.getPath file))) 47 | 48 | (def default-defaults {:create true :runtime "nodejs4.3"}) 49 | 50 | (defn- extract-build [{:keys [cljs-lambda] :as proj}] 51 | (when-not (:compiler cljs-lambda) 52 | (let [cljs-build-id (:cljs-build-id cljs-lambda) 53 | {:keys [builds]} (cljsbuild.config/extract-options proj) 54 | [build] (if-not cljs-build-id 55 | builds 56 | (filter #(= (:id %) cljs-build-id) builds))] 57 | (cond (not build) (leiningen.core.main/abort "Can't find cljsbuild build") 58 | (build :main) (leiningen.core.main/abort "Can't deploy build w/ :main") 59 | :else build)))) 60 | 61 | (defn- extract-compile-opts [proj] 62 | {:post [(or (nil? %) (and (coll? (:inputs %)) (map? (:options %))))]} 63 | (some-> proj 64 | :cljs-lambda 65 | :compiler 66 | (update :options (fn [{:keys [optimizations] :as options}] 67 | (if (= :advanced optimizations) 68 | (merge {:output-wrapper true} options) 69 | options))))) 70 | 71 | (def fn-keys 72 | #{:name :create :region :memory-size :role :invoke :description :timeout 73 | :publish :alias :runtime}) 74 | 75 | (defn- augment-fn [{:keys [defaults]} cli-kws fn-spec] 76 | (merge default-defaults 77 | defaults 78 | fn-spec 79 | (select-keys cli-kws fn-keys) 80 | {:handler (str "index." (export-name (:invoke fn-spec)))})) 81 | 82 | (defn- verify-fn-args [{:keys [functions]} {:keys [alias publish]}] 83 | (when alias 84 | (when-not publish 85 | (leiningen.core.main/abort "Can't alias unpublished function")))) 86 | 87 | (defn- augment-fns [cljs-lambda keep-fns cli-kws] 88 | (let [keep-fns (into #{} keep-fns) 89 | fn-pred (if (empty? keep-fns) 90 | (constantly true) 91 | #(keep-fns (:name %)))] 92 | (verify-fn-args cljs-lambda cli-kws) 93 | (->> cljs-lambda 94 | :functions 95 | (filter fn-pred) 96 | (map #(augment-fn cljs-lambda cli-kws %))))) 97 | 98 | (def meta-defaults {:parallel 5}) 99 | 100 | (defn- augment-project 101 | [{:keys [cljs-lambda] :as project} function-names cli-kws] 102 | (let [build (extract-build project) 103 | cljsc (extract-compile-opts project) 104 | meta-config (select-keys 105 | (merge meta-defaults cljs-lambda cli-kws) 106 | #{:region :aws-profile :parallel}) 107 | opts (assoc cljs-lambda 108 | :meta-config meta-config 109 | :positional-args function-names 110 | :keyword-args cli-kws 111 | :functions (augment-fns cljs-lambda function-names cli-kws))] 112 | (assert (and (or build cljsc) (not (and build cljsc)))) 113 | (assoc project 114 | :cljs-lambda 115 | (cond-> opts 116 | build (assoc :cljs-build build :compiler (build :compiler)) 117 | cljsc (assoc :cljsc cljsc :compiler (cljsc :options)))))) 118 | 119 | (defn- ->string-matcher [x] 120 | (cond 121 | (or (symbol? x) (string? x) (keyword? x)) #(= (name x) %) 122 | (instance? java.util.regex.Pattern x) #(re-find x %))) 123 | 124 | (defn capture-env [{capture :capture set-vars :set}] 125 | (let [capture? (if (not-empty capture) 126 | (apply some-fn (map ->string-matcher capture)) 127 | (constantly false)) 128 | env (filter (comp capture? key) (System/getenv))] 129 | (merge (into {} env) 130 | (into {} 131 | (for [[k v] set-vars] 132 | [(name k) v]))))) 133 | 134 | (defn- compile-cljs [proj] 135 | (if-let [opts (-> proj :cljs-lambda :cljsc)] 136 | (do 137 | (log :verbose "Invoking Clojurescript compiler w/ inputs " (opts :inputs)) 138 | (eval/eval-in-project 139 | proj 140 | `(cljs.build.api/build (apply cljs.build.api/inputs ~(opts :inputs)) ~(opts :options)) 141 | `(require '[cljs.build.api]))) 142 | (cljsbuild/cljsbuild proj "once" (-> proj :cljs-lambda :cljs-build :id)))) 143 | 144 | (defn build 145 | "Write a zip file suitable for Lambda deployment" 146 | [{{:keys [functions resource-dirs env] :as opts} :cljs-lambda 147 | :as project}] 148 | (if (or (opts :managed-deps) (-> opts :keyword-args :managed-deps)) 149 | (log :verbose "Note: You set the :managed-deps options to true, so 150 | dependencies won't be handled automatically.") 151 | (if (.exists (io/as-file "package.json")) 152 | (main/abort "Your project already has a project.json file. Please remove 153 | it and use :npm {:dependencies [...]} option instead, or set :manage-deps 154 | to true and manage your dependencies manually.") 155 | (log :verbose 156 | (with-out-str 157 | (npm/npm project "install"))))) 158 | (with-out-str 159 | (compile-cljs project)) 160 | (let [project-name (-> project :name name) 161 | index-path (->> functions 162 | (generate-index (capture-env env) (opts :compiler)) 163 | (write-index (-> opts :compiler :output-dir)))] 164 | (write-zip 165 | (opts :compiler) 166 | {:project-name project-name 167 | :index-path index-path 168 | :resource-dirs resource-dirs 169 | :zip-name (str project-name ".zip") 170 | :force-path (-> opts :keyword-args :output) 171 | :print-files (-> opts :keyword-args :print-files)}))) 172 | 173 | (defn deploy 174 | "Build & deploy a zip file to Lambda, exposing the specified functions" 175 | [{:keys [cljsbuild cljs-lambda] :as project}] 176 | (let [zip-path (build project) 177 | {{:keys [output-dir output-to]} :compiler} cljsbuild] 178 | (aws/deploy! zip-path cljs-lambda))) 179 | 180 | (defn update-config 181 | "Write function configs from project.clj to Lambda" 182 | [{:keys [cljs-lambda] :as project}] 183 | (aws/update-configs! cljs-lambda)) 184 | 185 | (defn dump-config 186 | "Dump effective function configs" 187 | [{{:keys [keyword-args] :as cljs-lambda} :cljs-lambda}] 188 | (-> cljs-lambda 189 | (select-keys [:functions]) 190 | (json/generate-string (select-keys keyword-args [:pretty])) 191 | println)) 192 | 193 | (defn invoke 194 | "Invoke the named Lambda function" 195 | [{{:keys [positional-args] :as cljs-lambda} :cljs-lambda}] 196 | (let [[fn-name payload] positional-args] 197 | (aws/invoke! fn-name payload cljs-lambda))) 198 | 199 | (defn default-iam-role 200 | "Install a Lambda-compatible IAM role, and stick it in project.clj" 201 | [{:keys [:cljs-lambda] :as project}] 202 | (let [arn (aws/install-iam-role! 203 | :cljs-lambda-default 204 | (slurp (io/resource "default-iam-role.json")) 205 | (slurp (io/resource "default-iam-policy.json")))] 206 | (println arn) 207 | (change/change project [:cljs-lambda :defaults] 208 | (fn [m & _] (assoc m :role arn))))) 209 | 210 | (defn create-alias 211 | "Create a remote alias for the given function/version" 212 | [project] 213 | (apply aws/create-alias! (-> project :cljs-lambda :positional-args))) 214 | 215 | (def task->fn 216 | {"alias" create-alias 217 | "build" build 218 | "deploy" deploy 219 | "invoke" invoke 220 | "default-iam-role" default-iam-role 221 | "update-config" update-config 222 | "dump-config" dump-config}) 223 | 224 | (defn cljs-lambda 225 | "Build & deploy AWS Lambda functions" 226 | {:help-arglists '([alias build deploy update-config dump-config invoke default-iam-role]) 227 | :subtasks [#'create-alias #'build #'deploy #'update-config #'dump-config #'invoke #'default-iam-role]} 228 | 229 | ([project] (println (leiningen.help/help-for cljs-lambda))) 230 | 231 | ([project subtask & args] 232 | (if-let [subtask-fn (task->fn subtask)] 233 | (let [[pos kw] (args/split-args args #{:publish :quiet :managed-deps :print-files :pretty}) 234 | [kw quiet] [(dissoc kw :quiet) (kw :quiet)] 235 | project (augment-project project pos kw) 236 | meta-config (-> project :cljs-lambda :meta-config)] 237 | (binding [args/*region* (meta-config :region) 238 | args/*aws-profile* (meta-config :aws-profile) 239 | logging/*log-level* (if quiet :error :verbose)] 240 | (subtask-fn project))) 241 | (println (leiningen.help/help-for cljs-lambda))))) 242 | -------------------------------------------------------------------------------- /plugin/src/leiningen/cljs_lambda/args.clj: -------------------------------------------------------------------------------- 1 | (ns leiningen.cljs-lambda.args) 2 | 3 | (def ^:dynamic *region* nil) 4 | (def ^:dynamic *aws-profile* nil) 5 | 6 | (let [coercions {:create #(Boolean/parseBoolean %) 7 | :timeout #(Integer/parseInt %) 8 | :memory-size #(Integer/parseInt %) 9 | :parallel #(Integer/parseInt %)} 10 | ->arg #(keyword (subs % 1))] 11 | (defn split-args [l bool-arg?] 12 | (loop [pos [] kw {} [k & l] l] 13 | (cond (not k) [pos kw] 14 | (not= \: (first k)) (recur (conj pos k) kw l) 15 | :else (let [arg (->arg k)] 16 | (if (bool-arg? arg) 17 | (recur pos (assoc kw arg true) l) 18 | (let [[v & l] l 19 | coerce (coercions arg identity)] 20 | (recur pos (assoc kw arg (coerce v)) l)))))))) 21 | -------------------------------------------------------------------------------- /plugin/src/leiningen/cljs_lambda/aws.clj: -------------------------------------------------------------------------------- 1 | (ns leiningen.cljs-lambda.aws 2 | (:require [leiningen.cljs-lambda.logging :as logging :refer [log]] 3 | [leiningen.cljs-lambda.args :as args] 4 | [cheshire.core :as json] 5 | [clojure.java.io :as io] 6 | [clojure.java.shell :as shell] 7 | [clojure.set :as set] 8 | [clojure.string :as str] 9 | [base64-clj.core :as base64] 10 | [clojure.pprint :as pprint] 11 | [clojure.string :as string]) 12 | (:import [java.io File] 13 | [java.util.concurrent Executors])) 14 | 15 | (defn abs-path [^File f] (.getAbsolutePath f)) 16 | 17 | (defmacro with-meta-config [config & body] 18 | `(let [{aws-profile# :aws-profile region# :region} ~config] 19 | (binding [args/*aws-profile* (or aws-profile# args/*aws-profile*) 20 | args/*region* (or region# args/*region*)] 21 | ~@body))) 22 | 23 | (defn meta-config [] 24 | (cond-> {} 25 | args/*region* (assoc :region (name args/*region*)) 26 | args/*aws-profile* (assoc :profile (name args/*aws-profile*)))) 27 | 28 | (defmulti ->cli-arg-value 29 | (fn [k v] k)) 30 | 31 | (defmethod ->cli-arg-value :vpc-config [k v] 32 | (let [subnets (:subnets v) 33 | security-groups (:security-groups v)] 34 | (string/join 35 | ["SubnetIds=[" 36 | (string/join "," subnets) 37 | "],SecurityGroupIds=[" 38 | (string/join "," security-groups) 39 | "]"]))) 40 | 41 | (defmethod ->cli-arg-value :environment [k v] 42 | (str 43 | "Variables={" 44 | (string/join 45 | "," 46 | (for [[k v] v] 47 | (str (name k) "=" v))) 48 | "}")) 49 | 50 | (defmethod ->cli-arg-value :dead-letter-config [k v] 51 | (str "TargetArn=" v)) 52 | 53 | (defmethod ->cli-arg-value :default [k v] 54 | (if (keyword? v) (name v) (str v))) 55 | 56 | (defn ->cli-arg [k v] 57 | [(str "--" (name k)) 58 | (->cli-arg-value k v)]) 59 | 60 | (defn ->cli-args [m & [positional {:keys [preserve-names?]}]] 61 | (let [m (cond-> (merge (meta-config) m) 62 | (not preserve-names?) 63 | (set/rename-keys {:name :function-name :vpc :vpc-config :dead-letter :dead-letter-config :env :environment})) 64 | args (flatten 65 | (for [[k v] m] 66 | (->cli-arg k v)))] 67 | (cond->> args positional (into positional)))) 68 | 69 | (defn aws-cli! [service cmd args & [{:keys [fatal?] :or {fatal? true}}]] 70 | (apply log :verbose "aws" service (name cmd) args) 71 | (let [{:keys [exit err] :as r} 72 | (apply shell/sh "aws" service (name cmd) args)] 73 | (if (and fatal? (not (zero? exit))) 74 | (leiningen.core.main/abort err) 75 | r))) 76 | 77 | (def lambda-cli! (partial aws-cli! "lambda")) 78 | 79 | (def fn-config-args 80 | #{:name :role :handler :description :timeout :memory-size :runtime :vpc :dead-letter :env}) 81 | 82 | (def fn-spec-defaults 83 | {:vpc {:subnets [] :security-groups []} :dead-letter "" :env {}}) 84 | 85 | (def create-function-args 86 | (into fn-config-args 87 | #{:zip-file :output :query})) 88 | 89 | (def update-function-code-args 90 | (remove #{:vpc :dead-letter :env} create-function-args)) 91 | 92 | (defn fn-spec->cli-args [fn-args {:keys [publish] :as fn-spec}] 93 | (let [args (merge {:output "text" :query "Version"} fn-spec)] 94 | (-> args 95 | (select-keys fn-args) 96 | (->cli-args (cond-> [] publish (conj "--publish")))))) 97 | 98 | (defn update-alias! [alias-name fn-name version] 99 | (lambda-cli! 100 | :update-alias 101 | (->cli-args 102 | {:function-name fn-name 103 | :name alias-name 104 | :function-version version} 105 | nil 106 | {:preserve-names? true}))) 107 | 108 | (defn create-alias! [alias-name fn-name version] 109 | (lambda-cli! 110 | :create-alias 111 | (->cli-args 112 | {:function-name fn-name 113 | :name alias-name 114 | :function-version version} 115 | nil 116 | {:preserve-names? true}) 117 | {:fatal? false})) 118 | 119 | (def default-runtime "nodejs4.3") 120 | 121 | (defn- create-function! [fn-spec zip-path] 122 | (let [args (fn-spec->cli-args 123 | create-function-args 124 | (merge {:runtime default-runtime} fn-spec {:zip-file zip-path}))] 125 | (-> (lambda-cli! :create-function args) :out str/trim))) 126 | 127 | (defn- update-function-config! [fn-spec] 128 | (lambda-cli! 129 | :update-function-configuration 130 | (-> (merge fn-spec-defaults fn-spec) 131 | (select-keys fn-config-args) 132 | ->cli-args))) 133 | 134 | (defn- update-function-code! 135 | [{:keys [name publish] :as fn-spec} zip-path] 136 | (let [args (merge (fn-spec->cli-args 137 | update-function-code-args 138 | {:name name :zip-file zip-path :publish publish}))] 139 | (-> (lambda-cli! :update-function-code args) 140 | :out 141 | str/trim))) 142 | 143 | (defn- get-function-configuration! [{:keys [name]}] 144 | (let [{:keys [exit out]} (lambda-cli! 145 | :get-function-configuration 146 | (->cli-args {:name name}) 147 | {:fatal? false})] 148 | (case exit 149 | 255 nil 150 | 0 (json/parse-string out)))) 151 | 152 | (defn normalize-config [config] 153 | (-> config 154 | (update-in [:vpc :subnets] sort) 155 | (update-in [:vpc :security-groups] sort) 156 | (update-in [:env] clojure.walk/stringify-keys))) 157 | 158 | (defn remote-config->local-config [remote] 159 | (let [remote (set/rename-keys remote {"FunctionName" :name 160 | "VpcConfig" :vpc 161 | "DeadLetterConfig" :dead-letter 162 | "Environment" :env 163 | "Description" :description 164 | "Timeout" :timeout 165 | "Handler" :handler 166 | "Runtime" :runtime 167 | "MemorySize" :memory-size 168 | "Version" :version 169 | "Role" :role})] 170 | (merge 171 | remote 172 | (if-let [vpc (:vpc remote)] 173 | (assoc remote :vpc 174 | (-> vpc 175 | (select-keys #{"SubnetIds" "SecurityGroupIds"}) 176 | (set/rename-keys {"SubnetIds" :subnets "SecurityGroupIds" :security-groups})))) 177 | {:dead-letter (get-in remote [:dead-letter "TargetArn"] "")} 178 | {:env (-> (get-in remote [:env "Variables"] {}))}))) 179 | 180 | (defn same-config? [remote local] 181 | (let [remote (-> remote remote-config->local-config normalize-config) 182 | local (-> (merge fn-spec-defaults local) (select-keys fn-config-args) normalize-config)] 183 | (= (select-keys remote (keys local)) local))) 184 | 185 | (defn- deploy-function! 186 | [zip-path {fn-name :name create? :create :as fn-spec}] 187 | (if-let [remote-config (get-function-configuration! fn-spec)] 188 | (do 189 | (when-not (same-config? remote-config fn-spec) 190 | (update-function-config! fn-spec)) 191 | (update-function-code! fn-spec zip-path)) 192 | (if create? 193 | (create-function! fn-spec zip-path) 194 | (leiningen.core.main/abort 195 | "Function" fn-name "doesn't exist & :create not set")))) 196 | 197 | (defn- do-functions! [process! {:keys [functions] :as cljs-lambda}] 198 | (when (not-empty functions) 199 | (let [parallel (-> cljs-lambda :meta-config :parallel) 200 | service (Executors/newFixedThreadPool parallel) 201 | bindings (get-thread-bindings) 202 | futures (.invokeAll 203 | service 204 | (for [f functions] 205 | #(with-bindings* bindings process! f)))] 206 | (.shutdown service) 207 | (doseq [f futures] 208 | (.get f))))) 209 | 210 | (defn deploy! [zip-path cljs-lambda] 211 | (do-functions! 212 | (fn [{fn-alias :alias fn-name :name :as fn-spec}] 213 | (with-meta-config fn-spec 214 | (let [version (deploy-function! (str "fileb://" zip-path) fn-spec)] 215 | (if fn-alias 216 | (let [{:keys [exit err] :as r} (create-alias! fn-alias fn-name version)] 217 | (when-not (zero? exit) 218 | (if (.contains err "ResourceConflictException") 219 | (update-alias! fn-alias fn-name version) 220 | (leiningen.core.main/abort err)))) 221 | (when version 222 | (println version)))))) 223 | cljs-lambda)) 224 | 225 | (defn update-config! [{fn-name :name :as fn-spec}] 226 | (with-meta-config fn-spec 227 | (if-let [remote-config (get-function-configuration! fn-spec)] 228 | (when-not (same-config? remote-config fn-spec) 229 | (update-function-config! fn-spec)) 230 | (leiningen.core.main/abort fn-name "doesn't exist & can't create")))) 231 | 232 | (defn update-configs! [cljs-lambda] 233 | (do-functions! update-config! cljs-lambda)) 234 | 235 | (defn invoke! [fn-name payload {:keys [keyword-args] :as cljs-lambda}] 236 | (let [out-file (File/createTempFile "lambda-output" ".json") 237 | out-path (abs-path out-file) 238 | qualifier (some keyword-args [:qualifier :version]) 239 | optional (cond-> {} qualifier (assoc :qualifier qualifier)) 240 | args (-> {:function-name fn-name 241 | :payload payload 242 | :log-type "Tail" 243 | :query "LogResult" 244 | :output "text"} 245 | (merge optional) 246 | (->cli-args [out-path])) 247 | {logs :out} (lambda-cli! :invoke args)] 248 | (log :verbose (base64/decode (str/trim logs))) 249 | (let [output (slurp out-path)] 250 | (pprint/pprint 251 | (try 252 | (json/parse-string output true) 253 | (catch Exception e 254 | [:not-json output])))))) 255 | 256 | (defn- get-role-arn! [role-name] 257 | (let [args (->cli-args {:role-name role-name :output "text" :query "Role.Arn"}) 258 | {:keys [exit out]} 259 | (aws-cli! "iam" "get-role" args {:fatal? false})] 260 | (when (zero? exit) 261 | (str/trim out)))) 262 | 263 | (defn- assume-role-policy-doc! [role-name file-path] 264 | (-> (aws-cli! 265 | "iam" "create-role" 266 | (->cli-args 267 | {:role-name role-name 268 | :assume-role-policy-document (str "file://" file-path) 269 | :output "text" 270 | :query "Role.Arn"})) 271 | :out 272 | str/trim)) 273 | 274 | (defn- put-role-policy! [role-name file-path] 275 | (let [args (->cli-args 276 | {:role-name role-name 277 | :policy-name role-name 278 | :policy-document (str "file://" file-path)})] 279 | (aws-cli! "iam" "put-role-policy" args))) 280 | 281 | (defn install-iam-role! [role-name role policy] 282 | (if-let [role-arn (get-role-arn! role-name)] 283 | role-arn 284 | (let [role-tmp-file (File/createTempFile "iam-role" nil) 285 | policy-tmp-file (File/createTempFile "iam-policy" nil)] 286 | (spit role-tmp-file role) 287 | (spit policy-tmp-file policy) 288 | (let [role-arn (assume-role-policy-doc! role-name (abs-path role-tmp-file))] 289 | (put-role-policy! role-name (abs-path policy-tmp-file)) 290 | (.delete role-tmp-file) 291 | (.delete policy-tmp-file) 292 | role-arn)))) 293 | -------------------------------------------------------------------------------- /plugin/src/leiningen/cljs_lambda/logging.clj: -------------------------------------------------------------------------------- 1 | (ns leiningen.cljs-lambda.logging) 2 | 3 | (def ^:private log-levels {:error 1 :verbose 0}) 4 | 5 | (def ^:dynamic *log-level* :verbose) 6 | 7 | (defn log [level & args] 8 | (when (<= (log-levels *log-level*) (log-levels level)) 9 | (binding [*out* *err*] 10 | (apply println args)))) 11 | -------------------------------------------------------------------------------- /plugin/src/leiningen/cljs_lambda/zip_tedium.clj: -------------------------------------------------------------------------------- 1 | (ns leiningen.cljs-lambda.zip-tedium 2 | (:require [leiningen.cljs-lambda.logging :refer [log]] 3 | [clojure.java.io :as io]) 4 | (:import [java.io File] 5 | [java.nio.file Files LinkOption] 6 | [java.nio.file.attribute PosixFilePermission] 7 | [org.apache.commons.compress.archivers.zip 8 | ZipArchiveEntry 9 | ZipArchiveOutputStream])) 10 | 11 | ;; I don't have time to explain 12 | (def ^:dynamic *print-files* false) 13 | 14 | (defn- get-posix-mode [file] 15 | (let [no-follow (into-array [LinkOption/NOFOLLOW_LINKS]) 16 | perms (Files/getPosixFilePermissions (.toPath file) no-follow)] 17 | (cond-> 0 18 | (contains? perms PosixFilePermission/OWNER_READ) (bit-set 8) 19 | (contains? perms PosixFilePermission/OWNER_WRITE) (bit-set 7) 20 | (contains? perms PosixFilePermission/OWNER_EXECUTE) (bit-set 6) 21 | (contains? perms PosixFilePermission/GROUP_READ) (bit-set 5) 22 | (contains? perms PosixFilePermission/GROUP_WRITE) (bit-set 4) 23 | (contains? perms PosixFilePermission/GROUP_EXECUTE) (bit-set 3) 24 | (contains? perms PosixFilePermission/OTHERS_READ) (bit-set 2) 25 | (contains? perms PosixFilePermission/OTHERS_WRITE) (bit-set 1) 26 | (contains? perms PosixFilePermission/OTHERS_EXECUTE) (bit-set 0)))) 27 | 28 | (defn- zip-entry [zip-stream file & [path]] 29 | (let [path (or path (.getPath file)) 30 | entry (ZipArchiveEntry. file path)] 31 | (when *print-files* 32 | (println (.getAbsolutePath file)) 33 | (println path)) 34 | (.setUnixMode entry (get-posix-mode file)) 35 | (.putArchiveEntry zip-stream entry) 36 | (io/copy file zip-stream) 37 | (.closeArchiveEntry zip-stream))) 38 | 39 | (defn extension [file] 40 | (let [path (.getPath file)] 41 | (when-let [i (.lastIndexOf path ".")] 42 | (subs path (inc i))))) 43 | 44 | (defn- zip-below [zip-stream dir] 45 | (log :verbose "Adding files from" (.getAbsolutePath dir)) 46 | (doseq [file (file-seq dir)] 47 | (when (and (.isFile file) 48 | (not= (extension file) "zip")) 49 | (zip-entry zip-stream file)))) 50 | 51 | (defn- zip-resources [zip-stream dir] 52 | (let [prefix (.. (io/file (.getAbsolutePath dir)) getParentFile getAbsolutePath)] 53 | (doseq [file (rest (file-seq dir))] 54 | (let [path (subs (.getAbsolutePath file) (inc (count prefix)))] 55 | (when-not (.isDirectory file) 56 | (zip-entry zip-stream file path)))))) 57 | 58 | (defmulti stuff-zip 59 | (fn [_ {:keys [optimizations]} _] 60 | (if (#{:simple :advanced} optimizations) 61 | :single-file 62 | :default))) 63 | 64 | (defmethod stuff-zip :default [zip-stream {:keys [output-dir]} {:keys [index-path]}] 65 | (zip-entry zip-stream (io/file index-path) "index.js") 66 | (zip-below zip-stream (io/file output-dir)) 67 | (zip-below zip-stream (io/file "node_modules"))) 68 | 69 | (defmethod stuff-zip :single-file [zip-stream {:keys [output-to source-map]} {:keys [index-path]}] 70 | (zip-entry zip-stream (io/file index-path) "index.js") 71 | (zip-entry zip-stream (io/file output-to)) 72 | (when (string? source-map) 73 | (zip-entry zip-stream (io/file source-map))) 74 | (zip-below zip-stream (io/file "node_modules"))) 75 | 76 | (defn write-zip [{:keys [output-dir] :as compiler-opts} 77 | {:keys [project-name zip-name resource-dirs] :as spec}] 78 | (let [zip-file (if (spec :force-path) 79 | (io/file (spec :force-path)) 80 | (io/file output-dir zip-name)) 81 | path (.getAbsolutePath zip-file)] 82 | (log :verbose "Writing zip to" path) 83 | (.delete zip-file) 84 | (let [zip-stream (ZipArchiveOutputStream. zip-file)] 85 | (binding [*print-files* (spec :print-files)] 86 | (stuff-zip zip-stream compiler-opts spec) 87 | (doseq [d resource-dirs] 88 | (zip-resources zip-stream (io/file d)))) 89 | (.close zip-stream) 90 | path))) 91 | -------------------------------------------------------------------------------- /templates/cljs-lambda/project.clj: -------------------------------------------------------------------------------- 1 | (defproject cljs-lambda/lein-template "0.4.10" 2 | :description "Clojurescript on AWS Lambda" 3 | :url "https://github.com/nervous-systems/cljs-lambda" 4 | :license {:name "Unlicense" :url "http://unlicense.org/UNLICENSE"} 5 | :scm {:name "git" :url "https://github.com/nervous-systems/cljs-lambda"} 6 | :deploy-repositories [["clojars" {:creds :gpg}]] 7 | :signing {:gpg-key "moe@nervous.io"} 8 | :eval-in-leiningen true) 9 | -------------------------------------------------------------------------------- /templates/cljs-lambda/src/leiningen/new/cljs_lambda.clj: -------------------------------------------------------------------------------- 1 | (ns leiningen.new.cljs-lambda 2 | (:require [leiningen.new.templates :refer [renderer name-to-path ->files]] 3 | [leiningen.core.main :as main])) 4 | 5 | (def render (renderer "cljs-lambda")) 6 | 7 | (defn cljs-lambda 8 | [name] 9 | (let [data {:name name 10 | :sanitized (name-to-path name)}] 11 | (main/info "Generating fresh 'lein new' cljs-lambda project.") 12 | (->files data 13 | ["project.clj" (render "project.clj" data)] 14 | ["README.md" (render "README.md" data)] 15 | ["src/{{sanitized}}/core.cljs" (render "core.cljs" data)] 16 | ["test/{{sanitized}}/core_test.cljs" (render "core_test.cljs" data)] 17 | ["static/config.edn" (render "config.edn" data)] 18 | [".gitignore" (render "gitignore" data)] 19 | 20 | ["test/{{sanitized}}/test_runner.cljs" (render "test_runner.cljs" data)]))) 21 | -------------------------------------------------------------------------------- /templates/cljs-lambda/src/leiningen/new/cljs_lambda/README.md: -------------------------------------------------------------------------------- 1 | # {{name}} 2 | 3 | ## Deploying 4 | 5 | Run `lein cljs-lambda default-iam-role` if you don't have yet have suitable 6 | execution role to place in your project file. This command will create an IAM 7 | role under your default (or specified) AWS CLI profile, and modify your project 8 | file to specify it as the execution default. 9 | 10 | Otherwise, add an IAM role ARN under the function's `:role` key in the 11 | `:functions` vector of your profile file, or in `:cljs-lambda` -> `:defaults` -> 12 | `:role`. 13 | 14 | Then: 15 | 16 | ```sh 17 | $ lein cljs-lambda deploy 18 | $ lein cljs-lambda invoke work-magic ... 19 | ``` 20 | 21 | ## Testing 22 | 23 | ```sh 24 | lein doo node {{name}}-test 25 | ``` 26 | 27 | Doo is provided to avoid including code to set the process exit code after a 28 | test run. 29 | -------------------------------------------------------------------------------- /templates/cljs-lambda/src/leiningen/new/cljs_lambda/config.edn: -------------------------------------------------------------------------------- 1 | {:magic-word "{{name}}-token"} 2 | -------------------------------------------------------------------------------- /templates/cljs-lambda/src/leiningen/new/cljs_lambda/core.cljs: -------------------------------------------------------------------------------- 1 | (ns {{name}}.core 2 | (:require [cljs-lambda.util :as lambda] 3 | [cljs-lambda.context :as ctx] 4 | [cljs-lambda.macros :refer-macros [deflambda]] 5 | [cljs.reader :refer [read-string]] 6 | [cljs.nodejs :as nodejs] 7 | [cljs.core.async :as async] 8 | [promesa.core :as p]) 9 | (:require-macros [cljs.core.async.macros :refer [go]])) 10 | 11 | (def config 12 | (-> (nodejs/require "fs") 13 | (.readFileSync "static/config.edn" "UTF-8") 14 | read-string)) 15 | 16 | (defmulti cast-async-spell (fn [{spell :spell} ctx] (keyword spell))) 17 | 18 | (defmethod cast-async-spell :delay-channel 19 | [{:keys [msecs] :or {msecs 1000}} ctx] 20 | (go 21 | ( p 13 | (p/catch #(is (not %))) 14 | (p/then done)))) 15 | 16 | (defn with-some-error [p] 17 | (p/branch p 18 | #(is false "Expected error") 19 | (constantly nil))) 20 | 21 | (deftest echo 22 | (-> (invoke work-magic {:magic-word "not the magic word"}) 23 | with-some-error 24 | with-promised-completion)) 25 | 26 | (def delay-channel-req 27 | {:magic-word (:magic-word config) 28 | :spell :delay-channel 29 | :msecs 2}) 30 | 31 | (deftest delay-channel-spell 32 | (with-promised-completion 33 | (alet [{:keys [waited]} (await (invoke work-magic delay-channel-req))] 34 | (is (= waited 2))))) 35 | 36 | (deftest delay-channel-spell-go 37 | (cljs.test/async 38 | done 39 | (go 40 | (let [[tag response] ( (invoke work-magic 47 | {:magic-word (:magic-word config) 48 | :spell :delay-fail 49 | :msecs 3}) 50 | with-some-error 51 | with-promised-completion)) 52 | -------------------------------------------------------------------------------- /templates/cljs-lambda/src/leiningen/new/cljs_lambda/gitignore: -------------------------------------------------------------------------------- 1 | pom.xml 2 | *jar 3 | /lib/ 4 | /classes/ 5 | /out/ 6 | /target/ 7 | .lein* 8 | /node_modules 9 | -------------------------------------------------------------------------------- /templates/cljs-lambda/src/leiningen/new/cljs_lambda/project.clj: -------------------------------------------------------------------------------- 1 | (defproject {{name}} "0.1.0-SNAPSHOT" 2 | :description "FIXME" 3 | :url "http://please.FIXME" 4 | :dependencies [[org.clojure/clojure "1.8.0"] 5 | [org.clojure/clojurescript "1.8.51"] 6 | [org.clojure/core.async "0.2.395"] 7 | [io.nervous/cljs-lambda "0.3.5"]] 8 | :plugins [[lein-cljsbuild "1.1.4"] 9 | [lein-npm "0.6.0"] 10 | [lein-doo "0.1.7"] 11 | [io.nervous/lein-cljs-lambda "0.6.6"]] 12 | :npm {:dependencies [[source-map-support "0.4.0"]]} 13 | :source-paths ["src"] 14 | :cljs-lambda 15 | {:defaults {:role "FIXME"} 16 | :resource-dirs ["static"] 17 | :functions 18 | [{:name "work-magic" 19 | :invoke {{name}}.core/work-magic}]} 20 | :cljsbuild 21 | {:builds [{:id "{{name}}" 22 | :source-paths ["src"] 23 | :compiler {:output-to "target/{{name}}/{{sanitized}}.js" 24 | :output-dir "target/{{name}}" 25 | :source-map true 26 | :process-shim false 27 | :target :nodejs 28 | :language-in :ecmascript5 29 | :optimizations :none}} 30 | {:id "{{name}}-test" 31 | :source-paths ["src" "test"] 32 | :compiler {:output-to "target/{{name}}-test/{{sanitized}}.js" 33 | :output-dir "target/{{name}}-test" 34 | :target :nodejs 35 | :language-in :ecmascript5 36 | :optimizations :none 37 | :main {{name}}.test-runner}}]}) 38 | -------------------------------------------------------------------------------- /templates/cljs-lambda/src/leiningen/new/cljs_lambda/test_runner.cljs: -------------------------------------------------------------------------------- 1 | (ns {{name}}.test-runner 2 | (:require [doo.runner :refer-macros [doo-tests]] 3 | [{{name}}.core-test] 4 | [cljs.nodejs :as nodejs])) 5 | 6 | (try 7 | (.install (nodejs/require "source-map-support")) 8 | (catch :default _)) 9 | 10 | (doo-tests 11 | '{{name}}.core-test) 12 | -------------------------------------------------------------------------------- /templates/serverless/project.clj: -------------------------------------------------------------------------------- 1 | (defproject serverless-cljs/lein-template "0.1.6" 2 | :description "Clojurescript on AWS Lambda via Serverless" 3 | :url "https://github.com/nervous-systems/cljs-lambda" 4 | :license {:name "Unlicense" :url "http://unlicense.org/UNLICENSE"} 5 | :scm {:name "git" :url "https://github.com/nervous-systems/cljs-lambda"} 6 | :eval-in-leiningen true) 7 | -------------------------------------------------------------------------------- /templates/serverless/src/leiningen/new/serverless_cljs.clj: -------------------------------------------------------------------------------- 1 | (ns leiningen.new.serverless-cljs 2 | (:require [leiningen.new.templates :refer [renderer name-to-path ->files]] 3 | [leiningen.core.main :as main])) 4 | 5 | (def render (renderer "serverless-cljs")) 6 | 7 | (defn serverless-cljs 8 | [name] 9 | (let [data {:name name 10 | :sanitized (name-to-path name)}] 11 | (main/info "Generating fresh 'lein new' serverless-cljs project.") 12 | (->files data 13 | ["project.clj" (render "project.clj" data)] 14 | ["serverless.yml" (render "serverless.yml" data)] 15 | ["README.md" (render "README.md" data)] 16 | ["src/{{sanitized}}/core.cljs" (render "core.cljs" data)] 17 | [".gitignore" (render "gitignore" data)]))) 18 | -------------------------------------------------------------------------------- /templates/serverless/src/leiningen/new/serverless_cljs/README.md: -------------------------------------------------------------------------------- 1 | # {{name}} 2 | 3 | # Install Dependencies 4 | 5 | ```shell 6 | $ lein deps 7 | ``` 8 | 9 | # Deploy 10 | 11 | ```shell 12 | $ serverless deploy 13 | ``` 14 | 15 | # Redeploy Function 16 | 17 | ``` 18 | $ serverless deploy function -f echo 19 | ``` 20 | 21 | # Invoke 22 | 23 | ```shell 24 | $ curl -X POST -H 'Content-Type: application/json' -d '{"body": "Hi"}' 25 | ``` 26 | -------------------------------------------------------------------------------- /templates/serverless/src/leiningen/new/serverless_cljs/core.cljs: -------------------------------------------------------------------------------- 1 | (ns {{name}}.core 2 | (:require [cljs-lambda.macros :refer-macros [defgateway]])) 3 | 4 | (defgateway echo [event ctx] 5 | {:status 200 6 | :headers {:content-type (-> event :headers :content-type)} 7 | :body (event :body)}) 8 | -------------------------------------------------------------------------------- /templates/serverless/src/leiningen/new/serverless_cljs/gitignore: -------------------------------------------------------------------------------- 1 | pom.xml 2 | *jar 3 | /lib/ 4 | /classes/ 5 | /out/ 6 | /target/ 7 | .lein* 8 | /node_modules 9 | .serverless/ 10 | -------------------------------------------------------------------------------- /templates/serverless/src/leiningen/new/serverless_cljs/project.clj: -------------------------------------------------------------------------------- 1 | (defproject {{name}} "0.1.0-SNAPSHOT" 2 | :dependencies [[org.clojure/clojure "1.9.0"] 3 | [org.clojure/clojurescript "1.10.312"] 4 | [io.nervous/cljs-lambda "0.3.5"]] 5 | :plugins [[lein-npm "0.6.2"] 6 | [io.nervous/lein-cljs-lambda "0.6.6"]] 7 | :npm {:dependencies [[serverless-cljs-plugin "0.1.2"]]} 8 | :cljs-lambda {:compiler 9 | {:inputs ["src"] 10 | :options {:output-to "target/{{name}}/{{sanitized}}.js" 11 | :output-dir "target/{{name}}" 12 | :target :nodejs 13 | :language-in :ecmascript5 14 | :optimizations :simple}}}) 15 | -------------------------------------------------------------------------------- /templates/serverless/src/leiningen/new/serverless_cljs/serverless.yml: -------------------------------------------------------------------------------- 1 | service: {{name}} 2 | 3 | provider: 4 | name: aws 5 | runtime: nodejs8.10 6 | 7 | functions: 8 | echo: 9 | cljs: {{name}}.core/echo 10 | events: 11 | - http: 12 | path: echo 13 | method: post 14 | 15 | plugins: 16 | - serverless-cljs-plugin 17 | -------------------------------------------------------------------------------- /travis/delete-function.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | if [[ ! -z $2 ]]; then REGION+="--region $2"; fi 3 | aws lambda delete-function --function-name $1 $REGION > /dev/null 2>&1 || true 4 | -------------------------------------------------------------------------------- /travis/get-function.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | if [[ ! -z $2 ]]; then REGION+="--region $2"; fi 3 | aws lambda get-function --function-name $1 $REGION > /dev/null 2>&1 4 | -------------------------------------------------------------------------------- /travis/run.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # It'd be nice to have some unit tests etc. for the junk in aws.clj, 4 | # but the project map manipulation / cljsbuild integration is 5 | # sufficiently complex and Leiningen-entangled that I don't feel like 6 | # this is a terrible idea 7 | set -e 8 | set -o pipefail 9 | 10 | lein doo node ${PROJECT_DIR}-test once 11 | 12 | if [ ! -z "$AWS_SECRET_ACCESS_KEY" ] ; then 13 | FN_NAME=work-magic-${PROJECT_DIR} 14 | INVOKE="lein cljs-lambda invoke $FN_NAME" 15 | export AWS_DEFAULT_REGION=us-east-1 16 | 17 | lein cljs-lambda default-iam-role :quiet > /dev/null & 18 | 19 | echo "Delete functions in us-east-1 and us-west-2, if they exist" 20 | parallel ./delete-function.sh $FN_NAME ::: "" us-west-2 21 | 22 | wait 23 | echo "Deploy into us-west-2" 24 | lein cljs-lambda deploy work-magic :name $FN_NAME :region us-west-2 :quiet 25 | 26 | echo "Assert function doesn't exist in us-east-1" 27 | (echo "! ./get-function.sh $FN_NAME"; 28 | echo "./get-function.sh $FN_NAME us-west-2") | parallel 29 | 30 | echo "Assert that we can invoke the function in us-west-2 and get stripped output" 31 | FN_OUT=$($INVOKE \ 32 | "{\"magic-word\": \"${PROJECT_DIR}-token\", \"spell\": \"delay-promise\"}" \ 33 | :region us-west-2 :quiet) 34 | if [[ $FN_OUT != \{:waited* ]]; then 35 | echo "Failed to retrieve invocation output $FN_OUT" 36 | exit 1 37 | fi 38 | 39 | echo "Assert that we can update the function's memory size" 40 | lein cljs-lambda update-config :name $FN_NAME :memory-size 512 :region us-west-2 :quiet 41 | FN_MEM_SIZE=$(aws lambda get-function-configuration \ 42 | --function-name $FN_NAME --query MemorySize --region us-west-2) 43 | if [[ $FN_MEM_SIZE != 512 ]]; then 44 | echo "Memory size doesn't match $FN_MEM_SIZE" 45 | exit 1 46 | fi 47 | 48 | echo "Deploy & publish an alias of the function into us-east-1" 49 | lein cljs-lambda deploy work-magic :name $FN_NAME :publish :alias jolly-roger :quiet 50 | 51 | echo "Assert that we can invoke the alias" 52 | (echo " $INVOKE :version jolly-roger :quiet"; 53 | echo "! $INVOKE :version go-home-roger :quiet") | parallel 54 | 55 | echo "Re-publish w/out aliasing, and assert we get a non-empty version" 56 | FN_VERSION=$(lein cljs-lambda deploy work-magic :name $FN_NAME :publish :quiet) 57 | if [[ -z $FN_VERSION ]]; then 58 | echo "Didn't get a version" 59 | exit 1 60 | fi 61 | 62 | echo "Create another alias, w/out publishing" 63 | lein cljs-lambda alias go-home-roger $FN_NAME $FN_VERSION 64 | 65 | echo "Assert we can invoke the previous alias using :qualifier & partial ARN" 66 | (echo "$INVOKE :qualifier $FN_VERSION :quiet"; 67 | echo "$INVOKE:$FN_VERSION :quiet") | parallel 68 | else 69 | echo "No AWS key, exiting quietly" 70 | fi 71 | --------------------------------------------------------------------------------