Versioning functions can be extremely useful when building systems that are deployed into multiple environments or that change over time. Being able to query an environment for a version of a function enables you to quickly understand why behavior might be different across many environments. Also, having a version associated with the response from each function execution is useful when debugging via logs.

In this post, I will present a two-step process I like that enables both querying version information from an environment and associating version information with responses when working with OpenFaaS and the OpenFaaS Clojure template.

Adding Version to Environments

In order to get version information about a function without actually invoking that function, we must specify the version as metadata associated with the function itself. OpenFaaS provides a simple way to achieve this by specifying an annotation in the stack file.

To add an annotation to a function, just add an annotations section to the function definition and add a version key and value to it. Following is a basic example that applies a version annotation with value 0.1.0.

provider:
  name: faas
  gateway: http://localhost:8080
functions:
  some-function:
    lang: clojure
    handler: ./some-function
    image: some-function:latest
    annotations:
      version: 0.1.0

When the docker image is built using this YAML, the annotation will be applied to the image, and you will be able to query for that information. How you perform the query depends on how you are running OpenFaaS.

If you are running OpenFaaS on Docker Swarm, you will find the version annotation with the docker inspect command. Note that the annotation has a com.openfaas.annotations. prefix.

> docker inspect <container-id> | grep version

"com.openfaas.annotations.version": "0.1.0",

If you are instead running OpenFaaS on kubernetes, you can ask the system to describe the pod running your function. This will give you quite a bit of information about the pod. Among the data you will find a list of annotations, including the version specified in the YAML.

> kubectl describe pod -n openfaas-fn -l faas_function=some-function

<snip>
Annotations:    prometheus.io.scrape: false
                version: 0.1.0
<snip>

Finally, you can get the version information by querying the OpenFaaS gateway directly by using the /system/function/<function-name> endpoint. You will need to use Basic Authorization, otherwise the gateway will return a 401 Unauthorized response. If you followed the getting started article, the authentication data was printed to the console.

In our case, a GET to http://localhost:8080/system/function/some-function will give us the following response, which includes the version under the annotations key.

{
  "name": "some-function",
  "image": "some-function:latest",
  "invocationCount": 0,
  "replicas": 1,
  "envProcess": "",
  "availableReplicas": 1,
  "labels": {
    "com.openfaas.function": "some-function",
    "com.openfaas.uid": "530368257",
    "function": "true"
  },
  "annotations": {
    "version": "0.1.0"
  }
}

Adding Version to Responses

Adding the version to the response is a little more involved than adding an annotation.

When building ring applications, an obvious solution is to use a ring middleware that augments the response with some version information. To aid in providing this information, I have created ring-version, a ring middleware that adds an X-Version header to the response using the Implementation-Version specified in the jar’s manifest.

Quick aside: If you do not like this approach, you still have options! ring-version-header is another middleware that adds an X-Version header in the response. You specify the version when you apply the middleware: (wrap-version-header handler "0.1.0").

Since I have not posted a release of ring-version to Clojars yet, we can pull it in from GitHub directly. Add the following to the :deps map in the deps.edn file.

{ring-version {:git/url "https://github.com/tessellator/ring-version"
               :sha "ce25b84f59a778c509da56db0bc992f795b60955"}}

Now that we have the middleware available, we can use it per usual. Here is a small example that demonstrates its use:

(ns function.core
  (:require [ring.middleware.version :refer [wrap-version-response]]))

(defn handler [req]
  {:status 200
   :body "Hello, Clojure."
   :headers {}})

(def app
  (-> handler
      wrap-version-response))

If you were to deploy this version of the function, invoke it, and check the headers in the response, you would find that the function returns an X-Version response header without a version. This is because we have not yet added the Implementation-Version to the jar file. To add the Implementation-Version, we will use a feature of the template that will apply a custom manifest file to the jar during the build step.

During the build step, the template will check to see if a file named manifest.mf exists in the folder containing the deps.edn file. If there is one, it will apply the key-value pairs in the file to the manifest in the jar file. For our example, the contents of manifest.mf might look like the following:

Implementation-Version: 0.1.0

If you read the logs generated during the build step, you will see a message stating it is applying manifest. If you see this message, you should expect your custom manifest data to be in the final uberjar that is deployed. The message is not printed if a manifest.mf file is not found.

If you deploy the function now and examine the output, you should see the correct version information in the response headers.

> curl -v http://localhost:8080/function/my-function

<snip>
X-Version: 0.1.0
<snip>

Wrapping Up

In this post I demonstrated how I like to version my functions using annotations and the OpenFaaS Clojure template. It is a two-step process, but following it enables querying the environment for current version info as well as getting the version with each function response.

One drawback to this approach is that it requires you to maintain the version into two separate locations: the stack file and manifest.mf. To help mitigate this drawback, I use a script to update the versions together.

Is there a way to streamline this approach? I would love to hear your thoughts and ideas. You can reach me on Twitter or in Slack in the Clojurians and OpenFaaS workspaces.