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_gettype=> An atom of the callback type. Either:beforeor:after
line:2Assign our callback module which was previously loaded from the file./upgrade.exline:3 - 6Assign our callback function.:before_mix_deps_getline:8-10If the function exists in the module we defined in.upgrade.ex, then execute it now.line:12Return nil. We don't want this callback function to be able to change the currentenvin 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:16Execute the before action callback e.gbefore_mix_compileline:17Execute the actual action e.g.mix_compileline:18Execute the after action callback e.g.after_mix_compileline:20Return theenvstruct 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:11build the path that points to the deployed project and assign itline:12-16If the file exists, use theCodemodule 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