Skip to content

Commit

Permalink
Merge pull request #10 from SmartColumbusOS/composing
Browse files Browse the repository at this point in the history
Composing
  • Loading branch information
jeffgrunewald authored Mar 14, 2019
2 parents de8a4d2 + 50026d0 commit 68383ec
Show file tree
Hide file tree
Showing 28 changed files with 562 additions and 411 deletions.
40 changes: 19 additions & 21 deletions config/dev.exs
Original file line number Diff line number Diff line change
@@ -1,25 +1,23 @@
use Mix.Config

config :divo,
divo: [
kafka: %{
image: "wurstmeister/kafka:latest",
env: [
val1: "foo",
val2: "bar"
],
ports: [
{9092, 9092}
]
},
redis: %{
image: "redis:5.0.3",
command: "redis start --foreground",
ports: [
{6379, 6379}
],
volumes: [
{"/tmp", "/opt/redis"}
]
divo: %{
version: "3.4",
services: %{
redis: %{
image: "redis:5.0.3",
command: ["redis", "start", "--foreground"],
ports: ["2181:2181"],
volumes: ["/tmp:/opt/redis"]
},
kafka: %{
image: "wurstmeister/kafka",
depends_on: ["zookeeper"],
ports: ["9094:9094"],
environment: [
"VAL1=foo",
"VAL2=bar"
]
}
}
]
}
49 changes: 23 additions & 26 deletions config/test.exs
Original file line number Diff line number Diff line change
@@ -1,31 +1,28 @@
use Mix.Config

config :divo,
divo: [
busybox: %{
image: "busybox:latest",
env: [
val1: "foo",
val2: "bar"
],
ports: [
{9092, 9092}
],
command: "ls",
wait_for: %{
log: "home",
dwell: 400
divo: %{
version: "3.4",
services: %{
busybox: %{
image: "busybox:latest",
environment: [
"VAL1=foo",
"VAL2=bar"
],
ports: ["8888:8888"],
command: "sleep 1000",
healthcheck: %{test: ["CMD-SHELL", "ls / | grep home || exit 1"]}
},
alpine: %{
image: "alpine:latest",
environment: [
"VAL1=foo",
"VAL2=bar"
],
ports: ["5432:5432"],
command: ~S{ls /home && echo "Yodel"}
}
},
alpine: %{
image: "alpine:latest",
env: [
val1: "foo",
val2: "bar"
],
ports: [
{9092, 9092}
],
command: ~S{ls /home && echo "Yodel"}
}
]
},
divo_wait: [dwell: 700, max_tries: 50]
42 changes: 41 additions & 1 deletion lib/divo.ex
Original file line number Diff line number Diff line change
@@ -1,3 +1,43 @@
defmodule Divo do
@moduledoc false
@moduledoc """
A library for incorporating docker-compose files or
compose-compliant map structures defined in application
config as dependency orchestration and management for
integration testing Elixir apps with external services
represented by the container services managed by divo.
"""

defdelegate run(opts), to: Divo.Compose, as: :run
defdelegate stop(), to: Divo.Compose, as: :stop
defdelegate kill(), to: Divo.Compose, as: :kill

@doc """
Implements a macro for including directly in integration
test files. Add `use Divo` to an integration test file to
automatically add the Start and Kill commands for your
dependent service definitions to a `setup_all` block of
your tests.
"""
defmacro __using__(opts \\ []) do
auto_start = Keyword.get(opts, :auto_start, true)

quote do
import Divo.Compose

setup_all do
Divo.Compose.run(unquote(opts))

app = Mix.Project.config() |> Keyword.get(:app)
if unquote(auto_start), do: Application.ensure_all_started(app)

on_exit(fn ->
if unquote(auto_start), do: Application.stop(app)

Divo.Compose.kill()
end)

:ok
end
end
end
end
24 changes: 0 additions & 24 deletions lib/divo/cmd.ex

This file was deleted.

121 changes: 121 additions & 0 deletions lib/divo/compose.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
defmodule Divo.Compose do
@moduledoc """
Implements the basic docker-compose commands for running from
your mix tasks. Run creates and starts the container services. Stop
will only stop the containers but leave them present on the system
for debugging and introspection if needed. Kill will stop any running
containers and remove all containers regardless of their current state.
These operations only apply to services managed by Divo, i.e. defined in
your Mix.env file under the :myapp, :divo key.
"""
require Logger
alias Divo.{File, Helper, Validate}

def run(opts \\ []) do
services = get_services(opts)

(["up", "--detach"] ++ services)
|> execute()

await()
end

def stop() do
execute("stop")
end

def kill() do
execute("down")
end

defp execute(action) do
app =
Helper.fetch_name()
|> to_string()

file =
Helper.fetch_config()
|> File.ensure_file()

args =
(["--project-name", app, "--file", file] ++ [action])
|> List.flatten()

Validate.validate(file)

System.cmd("docker-compose", args, stderr_to_stdout: true)
|> log_compose()
end

defp log_compose({message, 0}), do: Logger.info(message)
defp log_compose({message, code}), do: Logger.error("Docker Compose exited with code: #{code}. #{message}")

defp get_services(opts) do
case Keyword.get(opts, :services) do
nil -> []
defined -> Enum.map(defined, &to_string(&1))
end
end

defp await() do
fetch_containers()
|> Enum.filter(&health_defined?/1)
|> Enum.map(&await_healthy/1)
end

defp await_healthy(container) do
wait_config =
Helper.fetch_name()
|> Application.get_env(:divo_wait, dwell: 500, max_tries: 10)

dwell = Keyword.get(wait_config, :dwell)
tries = Keyword.get(wait_config, :max_tries)

Patiently.wait_for!(
check_health(container),
dwell: dwell,
max_tries: tries
)
end

defp check_health(container) do
fn ->
Logger.info("Checking #{container} is healthy...")

container
|> health_status()
|> case do
"healthy" ->
Logger.info("Service #{container} ready!")
true

_ ->
false
end
end
end

defp fetch_containers() do
{containers, _} = System.cmd("docker", ["ps", "--quiet"])

String.split(containers, "\n", trim: true)
end

defp health_defined?(container) do
{health, _} = System.cmd("docker", ["inspect", "--format", "{{json .State.Health}}", container])

health
|> Jason.decode!()
|> case do
nil -> false
_ -> true
end
end

defp health_status(container) do
{status, _} = System.cmd("docker", ["inspect", "--format", "{{json .State.Health.Status}}", container])

Jason.decode!(status)
end
end
40 changes: 40 additions & 0 deletions lib/divo/file.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
defmodule Divo.File do
@moduledoc """
Constructs the ad hoc docker-compose file used by
Divo to run docker dependency services based on
config in the app environment (Mix.env()) file.
"""
require Logger
alias Divo.Helper

def file_name() do
app = Helper.fetch_name()

case System.get_env("TMPDIR") do
nil -> "/tmp/#{app}.compose"
defined -> "#{defined}/#{app}.compose"
end
end

def ensure_file(app_config) when is_binary(app_config) do
Logger.info("Using : #{app_config}")

app_config
end

def ensure_file(app_config) when is_map(app_config) do
file = file_name()

Logger.info("Generating : #{file}")

app_config
|> Jason.encode!()
|> write(file)

file
end

defp write(content, path) do
File.write!(path, content)
end
end
19 changes: 19 additions & 0 deletions lib/divo/helper.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
defmodule Divo.Helper do
@moduledoc """
Extract common key-fetching functionality used by all of the
mix tasks to construct the proper arguments to the docker
commands.
"""

def fetch_name() do
Mix.Project.config()[:app]
end

def fetch_config() do
with {:ok, config} <- Application.fetch_env(fetch_name(), :divo) do
config
else
:error -> raise ArgumentError, message: "no services were defined in application config"
end
end
end
30 changes: 0 additions & 30 deletions lib/divo/integration.ex

This file was deleted.

Loading

0 comments on commit 68383ec

Please sign in to comment.