Let’s build a Bitbucket add-on in Clojure! – Part 2: Serving our Connect descriptor

Reading Time: 4 minutes

In part 1 of this series we did the fundamental work of building a Twelve Factor HTTP-stack from the ground using Leiningen, RingCompojure, and Immutant. However, that was just the foundations, and now we’re ready to start adding the necessary tooling to produce a full Atlassian Connect for Bitbucket application. This will include templating and introduce how to specify and authenticate our Connect add-on via its descriptor.

Connect essentials; serving a descriptor

All Connect add-ons need to serve up a descriptor. This JSON file provides the add-on’s location and identifying information along with the permissions it requires, its API, and other metadata such as a name and description.

As we want our add-on to insert run-time information into this descriptor before it goes out we’re going to need a templating tool. In the Clojure world templating tools generally fall into two groups: those that take Clojure s-expressions and output a given format (usually a tree-oriented one such as HTML or JSON), or those that operate on marked-up files/data. The latter is what most people think of when talking about templating, and it’s what makes the most sense for generating our descriptor, which is largely static. However Ring and Clojure HTTP clients can be configured to automatically convert between JSON and Clojure data-structures, which will come in useful later.

There are quite a few markup-style templating engines for Clojure. Most of them aim to be compatible with defacto standards from other ecosystems, such as Handlebars (Javascript) or ERB (Ruby). In this case I’ve chosen to use Selmer, which is closely related to Django‘s templating system, but you can use an alternative one if you prefer.

The descriptor we need for this project looks like this (template injections are delimited by {{/}}):


{
    "key": "hello-connect",
    "name": "Hello Connect",
    "description": "An example Clojure add-on for Bitbucket",
    "vendor": {
        "name": "Angry Nerds",
        "url": "https://www.atlassian.com/angrynerds"
    },
    "baseUrl": "{{base-url}}",
    "authentication": {
        "type": "jwt"
    },
    "lifecycle": {
        "installed": "/installed",
        "uninstalled": "/uninstalled"
    },
    "modules": {
        "oauthConsumer": {
            "clientId": "{{oauth-key}}"
        },
        "webhooks": [
            {
                "event": "*",
                "url": "/webhook"
            }
        ],
        "webPanel": [
            {
                "url": "/connect-example?repoPath={repo_path}",
                "name": {
                    "value": "Example Web Panel"
                },
                "location": "org.bitbucket.repository.overview.informationPanel",
                "key": "example-web-panel"
            }
        ]
    },
    "scopes": ["account", "repository"],
    "contexts": ["account"]
}

As you can see we need we’re going to inject two variables: base-url and oauth-key. The base-url is where the add-on will be running (e.g. your ngrok tunnel if running it locally). The oauth-key is the key that uniquely identifies who controls this add-on. This needs to be generated within Bitbucket via Manage account > OAuth > Add consumer; see the Bitbucket Connect getting started guide or the later installments in this series for more details on using ngrok and OAuth to develop Connect add-ons.

Setting our variables

As mentioned previously, we’re building this as a Twelve Factor application. This means we want to define our runtime information in the environment and then extract them with the environ library we used earlier to configure the HTTP server. In production we would set the variables using the system environment via export or similar. However in development this can be a pain. Luckily environ also supplies some methods to supply these during development. The simplest is to add the file .lein-envwith a dictionary of your variables; in our case it would look like:


{:base-url "OVERRIDE",
 :oauth-key "OVERRIDE"}

If you don’t want this checked into to git just add it to your .gitignore. For a more flexible system, environ supplies a Leiningen plugin; add the following to your project.clj :plugins list:


[lein-environ "1.0.1"]

This allows us to create an :env entry the same as the file above but in the Leiningen :dev profile which will set the environment. You can also add this to your profiles.clj; see the profiles documentation for more information.

Rendering our descriptor

Before we can render the descriptor we need to make it available to the runtime. In the JVM world this usually means adding it to the resources path. Create a new directory in the base of our project called resources, and another under that called views. Place the above descriptor template into a file called atlassian-connect.json.selmer. We tell Leiningen about this resources directory by adding the following entry to our project.clj:


:resource-paths ["resources"]

Leiningen will then place that directory on the JVM classpath.

Now we can have Selmer render the content and Ring/Immutant serve it. Add the following functions to our handler.clj:


(defn gen-descriptor []
  ;; Fetch configuration from the environment (see `environ` docs)
  (let [ctx {:base-url (env :base-url)
             :oauth-key (env :oauth-key)}]
    (render-file "views/atlassian-connect.json.selmer" ctx)))
(defn gen-descriptor-reply []
  (log/info "Received descriptor request")
  {:status 200
   :headers {"Content-Type" "application/json; charset=utf-8"}
   :body (gen-descriptor)})

This first function extracts the necessary variables from the environment and uses them to render the template we created (you’ll need to add [selmer.parser :as selmer] to your :require list). The second function wraps the resulting output in Ring response map. This is what we’ll pass back to the HTTP server to return.

Serving our descriptor

The last step is to add a route to retrieve the file. We’ll have both / and /atlassian-connect.json serve this up by default. To do this return to the defroutes section of handler.clj and replace the existing “Hello Connect” route with the following:


(GET  "/" [] (response/redirect "/atlassian-connect.json"))
(GET  "/atlassian-connect.json" []
      (gen-descriptor-reply))

Now we can start up our server with lein run and go to http://localhost:3000/. If everything is working OK you should be redirected to http://localhost:3000/atlassian-connect.json and see the rendered version of our Connect descriptor.

The code

The code for this part of the tutorial series is available in this tag in the accompanying Bitbucket repository. There will also code appearing there for the later parts as I work on them if you want to skip ahead.

Next time…

Now that we have a descriptor we can start adding actual functionality to our add-on. You may notice that our descriptor above contains references to a lifecycle. This API allows us to Bitbucket to initiate a relationship between a user and our add-on by providing information such as user metadata and a unique identifying key. Next time we’ll look more closely at how to respond to these calls, and after that we’ll look into communicating with Bitbucket using a client-side Javascript channel or server-to-server calls.