Skip to content

Instantly share code, notes, and snippets.

@alpeware
Created November 21, 2017 09:48
Show Gist options
  • Save alpeware/f18bef80cb521347d22b85f1ad07ada7 to your computer and use it in GitHub Desktop.
Save alpeware/f18bef80cb521347d22b85f1ad07ada7 to your computer and use it in GitHub Desktop.
JAQ - Clojure on Google App Engine Java8 Beta Standard Environment [WORK IN PROGRESS]

alpeware/jaq

JAQ is a highly opinionated set of tools to target Google's App Engine Java Standard Environment for Clojure development and deployment.

THIS IS WORK IN PROGRESS

Usage

First time

  • Create local project using project.clj and dev_server.clj above (adapt path and namespaces as needed for now)
  • Build Docker image: make build

Development

  • Start docker container: make clj
  • In docker container: lein dev-server

During Development

  • Web server: http://localhost:7776/
  • Repl: localhost 1002 SOCKET REPL NOT A NREPL
  • Calling App Engine services: call (remote!) once per REPL session to make services accessible

TODO

  • Deployment to App Engine through the REPL
  • Compile class files directly without roundtrip through lein compile -> lein uberwar -> class files

License

Copyright (c) 2017 Alpeware

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

<?xml version="1.0" encoding="utf-8"?>
<appengine-web-app xmlns="http://appengine.google.com/ns/1.0">
<application>alpeware-top-9</application>
<runtime>java8</runtime>
<version>1</version>
<threadsafe>true</threadsafe>
<url-stream-handler>urlfetch</url-stream-handler>
<automatic-scaling>
<max-concurrent-requests>80</max-concurrent-requests>
<max-idle-instances>1</max-idle-instances>
<min-idle-instances>automatic</min-idle-instances>
<max-pending-latency>15s</max-pending-latency>
<min-pending-latency>10s</min-pending-latency>
</automatic-scaling>
<static-files>
<include path="/static/**/*" />
<include path="/**.html" />
<exclude path="/data/**/*" />
</static-files>
<system-properties>
<property name="appengine.api.urlfetch.defaultDeadline" value="60"/>
</system-properties>
</appengine-web-app>
(ns ^{:author "Alpeware"}
top9.core
(:require
[bidi.ring :refer (make-handler)]
;; [clojure.tools.logging :as log]
[taoensso.timbre :as log]
[ring.middleware.defaults :refer [wrap-defaults site-defaults]]
[ring.middleware.nested-params :as nested-params]
[ring.middleware.keyword-params :as keyword-params]
[ring.middleware.json :only [wrap-json-body]]
[ring.middleware.session :as session]
[ring.middleware.params :as params]
[ring.middleware.content-type :refer [wrap-content-type]]
[ring.middleware.not-modified :refer [wrap-not-modified]]
[ring.middleware.format :refer [wrap-restful-format]]
[ring.middleware.file :refer [wrap-file]]
[ring.middleware.resource :refer [wrap-resource]]
[ring.util.servlet :refer [defservice]]
[ring.util.response :as res])
(:import
[com.google.appengine.tools KickStart]
[com.google.appengine.tools.development DevAppServerMain]
[com.google.appengine.api.utils SystemProperty]
[com.google.appengine.api.memcache
MemcacheService
MemcacheServiceFactory]
[com.google.appengine.tools.remoteapi
RemoteApiInstaller
RemoteApiOptions]))
;; utils
(defn- property-or [property alternative]
(or (.get property) alternative))
(defn environment []
(property-or SystemProperty/environment "Development"))
(defn prod? []
(not= (environment) "Development"))
(defn application-id []
(property-or SystemProperty/applicationId "localhost"))
(defn sdk-version []
(property-or SystemProperty/version "Google App Engine/1.x.x"))
(defn remote! []
(-> (RemoteApiInstaller.)
(.install (-> (RemoteApiOptions.)
(.server "localhost" 3000)
(.useDevelopmentServerCredential)))))
(def NREPL-PORT 10010)
(defn repl-server []
(clojure.core.server/start-server
{:address "0.0.0.0"
:port NREPL-PORT
:name "repl"
:accept 'clojure.core.server/repl}))
(defn init []
(log/info "starting")
(repl-server))
(defn destroy []
(log/info "shutting down"))
(defn status-handler [request]
(->
(res/response "{\"status\": \"ok\"}")
(res/content-type "application/json")))
(def routes ["" [["/" :status]
["/status" :status]
["/favicon.ico" :status]
["/foo" :status]]])
(def handlers {:index #'status-handler
:status #'status-handler})
(defn config []
(-> site-defaults
(assoc-in [:security :anti-forgery] false)
(assoc-in [:security :xss-protection :enable?] false)))
(def app
(->
(wrap-defaults (make-handler routes #'handlers) (config))
(wrap-restful-format)
(wrap-content-type)
(wrap-not-modified)))
(defservice app)
(ns ^{:author "Alpeware"}
top9.dev-server
(:gen-class)
(:require
;;[clojure.tools.logging :as log]
[taoensso.timbre :as log]
[clojure.java.io :as io]
[top9.core :as core])
(:import
[com.google.appengine.tools KickStart]
[com.google.appengine.tools.util Action]
[com.google.appengine.tools.development DevAppServerMain DevAppServerFactory]
[com.google.appengine.api.memcache
MemcacheService
MemcacheServiceFactory]
[com.google.appengine.tools.remoteapi
RemoteApiInstaller
RemoteApiOptions]
[net.lingala.zip4j.core ZipFile]
[net.lingala.zip4j.exception ZipException]))
(defn delete-recursively [fname]
(doseq [f (reverse (file-seq (clojure.java.io/file fname)))]
(clojure.java.io/delete-file f)))
(defn copy-file [source-path dest-path]
(io/copy (io/file source-path) (io/file dest-path)))
(defn unzip-file
[zip dest]
(try
(-> (ZipFile. zip)
(.extractAll dest))
(catch ZipException e
(log/info "Error" e)
(.printStackTrace e)
e)))
(defn start
"Start the dev server"
[sdk-version target]
(let [root-path (str "/opt/target/appengine-java-sdk-" sdk-version)
args ["--address=0.0.0.0"
"--port=3000"
"--runtime=java8"
"--generated_dir=/opt/data"
"--default_gcs_bucket=staging.alpeware-top-9.appspot.com"
"./target/war"]
_ (.setProperty (System/getProperties) "appengine.sdk.root" root-path)
_ (.setProperty (System/getProperties) "appengine.api.urlfetch.defaultDeadline" "60")]
(future (DevAppServerMain/main (into-array String args)))))
(def sdk-version "1.9.59")
(def target-dir "./target")
(def target (str target-dir "/appengine-java-sdk-" sdk-version))
(defn run []
(start sdk-version target))
(defn -main
[]
(let [path (str "./.m2/com/google/appengine/appengine-java-sdk/" sdk-version "/appengine-java-sdk-" sdk-version ".zip")]
(when-not (.exists (clojure.java.io/as-file target))
(log/info "Unpacking appengine SDK")
(unzip-file path target-dir))
(log/info "Unpacking war")
(unzip-file "./target/uberjar/top9-0.1.0-standalone.war" "./target/war")
(log/info "Copying appengine-web.xml")
(copy-file "./war-resources/appengine-web.xml" "./target/war/WEB-INF/appengine-web.xml")
(copy-file "./war-resources/datastore-indexes.xml" "./target/war/WEB-INF/datastore-indexes.xml")
(log/info "starting dev server for SDK version" sdk-version)
(start sdk-version target)))
###
# Main container
#
# (C) 2016, Alpeware
###
ARG BASE_IMAGE
FROM "${BASE_IMAGE}"
ARG USER
ARG USER_ID
ARG GROUP_ID
ARG WEB_PORT
ARG REPL_PORT
ARG BS_CMD
RUN addgroup --system --gid "${GROUP_ID}" "${USER}"
RUN adduser --uid "${USER_ID}" --gid "${GROUP_ID}" --disabled-password --gecos '' "${USER}"
RUN usermod -aG sudo "${USER}"
RUN echo "root:docker" | chpasswd
USER "${USER}"
WORKDIR /opt/src
EXPOSE "${WEB_PORT}"
EXPOSE "${REPL_PORT}"
CMD [BS_CMD]
###
# Makefile
#
# (C) 2016, Alpeware
###
.PHONY: all build exec bash clj cljs
BASE_IMAGE=clojure:latest
IMAGE_ORG=alpeware
IMAGE_NAME=clojure
IMAGE_VERSION=1.0
IMAGE_FULL_NAME=${IMAGE_ORG}/${IMAGE_NAME}:${IMAGE_VERSION}
CONTAINER_NAME=${IMAGE_ORG}_${IMAGE_NAME}
VIRTUAL_HOST="www.alpeware.com"
USER=$(shell whoami)
USER_ID=$(shell id -u)
GROUP_ID=$(shell id -g)
LOCAL_WEB_PORT=7776
CONTAINER_WEB_PORT=3000
LOCAL_CLJS_REPL_PORT=10021
LOCAL_REPL_PORT=10020
CONTAINER_REPL_PORT=10010
LOCAL_FIGWHEEL_PORT=3449
CONTAINER_FIGWHEEL_PORT=3449
BS_CMD=bash
PWD=$(shell pwd)
WORK_DIR=/opt
build:
docker build -t ${IMAGE_FULL_NAME} --build-arg BASE_IMAGE=${BASE_IMAGE} --build-arg USER=${USER} --build-arg USER_ID=${USER_ID} --build-arg GROUP_ID=${GROUP_ID} --build-arg BS_CMD=${BS_CMD} --build-arg WEB_PORT=${CONTAINER_WEB_PORT} --build-arg REPL_PORT=${CONTAINER_REPL_PORT} .
exec:
docker exec -it ${CONTAINER_NAME} bash
bash:
docker run -it --privileged -v ${PWD}:${WORK_DIR} -v /home/${USER}/.ssh:/home/${USER}/.ssh -w ${WORK_DIR} -p ${LOCAL_WEB_PORT}:${CONTAINER_WEB_PORT} -p ${LOCAL_REPL_PORT}:${CONTAINER_REPL_PORT} -p ${LOCAL_FIGWHEEL_PORT}:${CONTAINER_FIGWHEEL_PORT} --rm --name ${CONTAINER_NAME} ${IMAGE_FULL_NAME} bash
cljs:
docker run -it --privileged -v ${PWD}:${WORK_DIR} -v /home/${USER}/.ssh:/home/${USER}/.ssh -w ${WORK_DIR} -p ${LOCAL_CLJS_REPL_PORT}:${CONTAINER_REPL_PORT} -p ${LOCAL_FIGWHEEL_PORT}:${CONTAINER_FIGWHEEL_PORT} --rm --name ${CONTAINER_NAME} ${IMAGE_FULL_NAME} lein start-repl
clj:
docker run -it --privileged -v ${PWD}:${WORK_DIR} -v /home/${USER}/.ssh:/home/${USER}/.ssh -w ${WORK_DIR} -p ${LOCAL_WEB_PORT}:${CONTAINER_WEB_PORT} -p ${LOCAL_CLJ_REPL_PORT}:${CONTAINER_REPL_PORT} --rm --name ${CONTAINER_NAME} ${IMAGE_FULL_NAME} bash
(def sdk-version "1.9.59")
(defproject top9 "0.1.0"
:description "JAQ - Basic setup to do Clojure development targeting Google's App Engine Java 8 Standard Environment"
:url "https://www.alpeware.com/"
:license {:name "MIT"
:url "https://opensource.org/licenses/MIT"}
:local-repo ".m2"
:dependencies [[org.clojure/clojure "1.9.0-RC1"]
[org.clojure/clojurescript "1.9.946" :exclusions [org.clojure/clojure
com.google.guava/guava
com.google.code.findbugs/jsr305]]
[org.clojure/tools.logging "0.4.0"]
[org.clojure/data.json "0.2.6"]
[org.clojure/core.async "0.3.443"]
[org.clojure/data.xml "0.0.8"]
[com.google.appengine/appengine-java-sdk ~sdk-version :extension "zip"]
[com.google.appengine/appengine-api-1.0-sdk ~sdk-version]
[com.google.appengine/appengine-api-labs ~sdk-version]
[com.google.appengine/appengine-remote-api ~sdk-version]
[com.google.appengine/appengine-tools-sdk ~sdk-version]
[com.google.api-client/google-api-client-appengine "1.23.0"]
[com.google.api-client/google-api-client "1.23.0"]
[net.lingala.zip4j/zip4j "1.3.2"]
[com.taoensso/timbre "4.10.0"]
[bidi "2.1.2"]
[ring "1.6.3"]
[ring/ring-core "1.6.3"]
[ring/ring-defaults "0.3.1"]
[ring/ring-json "0.4.0"]
[ring-middleware-format "0.7.2" :exclusions [[cheshire]
[com.fasterxml.jackson.core/jackson-core]
[commons-codec]
[com.fasterxml.jackson.dataformat/jackson-dataformat-cbor]]]
[clj-http-lite "0.3.0"]]
:plugins
[[lein-pprint "1.2.0"]
[lein-ancient "0.6.14" :exclusions [org.apache.httpcomponents/httpcore]]
[lein-cljsbuild "1.1.7" :exclusions [[org.clojure/clojure]]]
[lein-figwheel "0.5.14"]
[lein-localrepo "0.5.4"]
[lein-ring "0.12.1"]
[lein-unpack-resources "0.1.1"]]
:sdk-version ~sdk-version
:unpack-resources {:resource [com.google.appengine/appengine-java-sdk ~sdk-version :extension "zip"]
:extract-path "./target/lib"}
:ring {:handler top9.core/app
:init top9.core/init
:destroy top9.core/destroy
:web-xml "./war-resources/web.xml"}
:aot [top9.core]
:main top9.dev-server
:resource-paths ["resources"]
:target-path "target/%s"
:repl-options {:port 10010
:host "0.0.0.0"}
:aliases {"dev-server" ["do" "clean," "ring" "uberwar," "trampoline" "run" "-m" "top9.dev-server" :project/sdk-version]})
<?xml version="1.0" encoding="UTF-8"?><web-app xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" version="2.5">
<listener>
<listener-class>top9.listener</listener-class>
</listener>
<servlet>
<servlet-name>top9.core/app servlet</servlet-name>
<servlet-class>top9.servlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>top9.core/app servlet</servlet-name>
<url-pattern>/*</url-pattern>
</servlet-mapping>
<servlet>
<display-name>Remote API Servlet</display-name>
<servlet-name>RemoteApiServlet</servlet-name>
<servlet-class>com.google.apphosting.utils.remoteapi.RemoteApiServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>RemoteApiServlet</servlet-name>
<url-pattern>/remote_api</url-pattern>
</servlet-mapping>
</web-app>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment