Building upon the the first version of Gatling. We're adding functionality to handle event hooks for each action in the deployment process
Click here to read part 1 first.
Gatling is a deployment tool in for Phoenix apps. In this post, we'll walk through how I updated gatling to add a lot more flexibility to the deployment process.
First, let's walk through all the steps gatling takes to execute the mix gatling.upgrade
task and the elixir functions that execute them. Remember, this is triggered by a a git
post-update
hook on the server.
$ mix deps.get -> mix_deps_get
$ mix compile -> mix_compile
$ mix digest -> mix_digest
$ mix release -> mix_release
$ mkdir /path/to/deploy -> make_upgrade_dir
$ cp -r release /path/to/deploy -> copy_release_to_upgrade
$ sudo service <project> upgrade -> upgrade_service
# configure nginx -> configure_nginx
All these functions are already defined and execute on the server. But I wanted the ability to execute some elixir code before and/or after each of these steps and I wanted to configure this in each app I deployed as each app my require different steps.
I wanted to put a file called ./upgrade.ex
in my project. And when I deploy with a git push, that file would be picked up by Gatling to execute any hooks I defined.
Here is an example of one of these hooks:
#./deploy.ex
defmodule MyProject.UpgradeHooks do
def before_mix_deps_get(_env) do
#do some work
#log some things
#track some things
end
end
With this above example, every time we do a git push, Gatling would find the ./upgrade.ex
and call before_mix_deps_get
right before the default mix_deps_get
function.
How it's made
Now we know the desired functionality, lets walk throught the implementation.
Here is the (abbreviated) original verion of the Mix.Tasks.Gatling.Upgrade
module:
defmodule Mix.Tasks.Gatling.Upgrade do
def upgrade(project) do
Gatling.env(project)
|> mix_deps_get
|> mix_compile
|> mix_digest
|> mix_release
|> make_upgrade_dir
|> copy_release_to_upgrade
|> upgrade_service
|> configure_nginx
end
end
We want to have a wrapper function around each of these that will hook in a before and after function. Let's just use call
. So our upgrade function will now look lik this:
defmodule Mix.Tasks.Gatling.Upgrade do
def upgrade(project) do
Gatling.env(project)
|> call(:mix_deps_get)
|> call(:mix_compile)
|> call(:mix_digest)
|> call(:mix_release)
|> call(:make_upgrade_dir)
|> call(:copy_release_to_upgrade)
|> call(:upgrade_service)
|> call(:configure_nginx)
end
end
call/1
Let's see how call
works:
1 def callback(env, action, type) do
2 module = env.upgrade_callback_module
3 callback_action = [type, action]
4 |> Enum.map(&to_string/1)
5 |> Enum.join("_")
6 |> String.to_atom()
7
8 if function_exported?(module, callback_action, 1) do
9 apply(module, callback_action, [env])
10 end
11
12 nil
13 end
14
15 def call(env, action) do
16 callback(env, action, :before)-
17 apply(__MODULE__, action, [env])
18 callback(env, action, :after)
19 env
20 end
Starting with callback/3
on line:1
we take in the following parameters:
env
=> A struct with all the information we need for a deployaction
=> An atom of the function we want to call e.g.:mix_deps_get
type
=> An atom of the callback type. Either:before
or:after
line:2
Assign our callback module which was previously loaded from the file./upgrade.ex
line:3 - 6
Assign our callback function.:before_mix_deps_get
line:8-10
If the function exists in the module we defined in.upgrade.ex
, then execute it now.line:12
Return nil. We don't want this callback function to be able to change the currentenv
in any way. So we return nil to express that.
In our call/2
function we take in the following parameters
env
=> A struct with all the information we need for a deployaction
=> An atom of the function we want to call e.g.:mix_deps_get
You'll notice these arguments are the same as callback/3
just without a type.
line:16
Execute the before action callback e.gbefore_mix_compile
line:17
Execute the actual action e.g.mix_compile
line:18
Execute the after action callback e.g.after_mix_compile
line:20
Return theenv
struct so it can be used by the next function in the pipeline.
And that's it! It's actually quite simple. There is still one part missing though. How did that env.upgrade_callback_module
get in there. How is Gatling loading our ./upgrade.ex
file and using it in its own project?
Loading code from another project
Before we start loading code, lets look at hour our env
is created.
First we have our %Gatling.Env{}
struct. For brevity's sake, we're only seeing the relavent attributes here:
defmodule Gatling.Env do
defstruct ~w[
#...
upgrade_callback_module
#...
]s
end
Our Gatling
module is what actually populates this struct:
1 defmodule Gatling do
2 def env(project) do
3 %Gatling.Env{
4 #...
5 :upgrade_callback_module => callback_module(project, task: "upgrade"),
6 #...
7 }
8 end
9
10 def callback_module(project, [task: task_name]) do
11 callback_path = Path.join(path/to/project, "#{task_name}.ex")
12 if File.exists? callback_path do
13 Code.load_file(callback_path) |> List.first() |> elem(0)
14 else
15 nil
16 end
17 end
18 end
When we call Gatling.env
we populate upgrade_callback_module
with callback_module/2
:
line:11
build the path that points to the deployed project and assign itline:12-16
If the file exists, use theCode
module to load the elixir file into the Gatling runtime
When we call Gatling.env.upgrade_callback_module
we'll have assess to the module we defined in ~/upgrade.ex
which (again) looks like this:
defmodule MyProject.UpgradeHooks do
def before_mix_deps_get(_env) do
#do some work
#log some things
#track some things
end
end
And that's it! That's all the moving parts and in my opinion, it's quite a minimal effort to gain the flexablity I desired. This concludes part 2 of this blog serires. Next, we'll look into migrating Gatling's underlying dependency exrm with it's successor - Distillery