defmodule Runbox.ScenarioRelease do
@moduledoc group: :internal
@moduledoc """
Module for inspecting a scenario release.
"""
alias Runbox.Scenario
alias Runbox.ScenarioTemplate
alias Runbox.Slave
defmodule SlaveFunc do
require Logger
@moduledoc group: :internal
@moduledoc """
Functions which are called on the scenario slave via RPC from the master
node.
"""
alias Runbox.Scenario.TemplateInspector
alias Runbox.Scenario.Type
@doc """
Retrieves information about scenarios in the release.
Scenarios with invalid manifests are filtered out. However, scenarios with colliding module
names are not.
"""
@spec release_info([module] | nil) :: [Scenario.t()]
def release_info(modules \\ nil) do
modules_to_scenarios(modules || release_modules())
end
@doc """
Returns list of IDs of scenarios in the release.
Scenarios with invalid manifests or mutually colliding manifest module names are filtered out.
"""
@spec scenarios :: [scenario_id :: String.t()]
def scenarios do
release_modules()
|> Enum.filter(&Scenario.manifest_module?/1)
|> resolve_prefix_conflicts()
|> Enum.flat_map(fn manifest_mod ->
with {:ok, manifest} <- scenario_manifest_info(manifest_mod),
:ok <- validate_scenario_type(manifest.type, manifest_mod) do
[manifest.id]
else
:error ->
[]
end
end)
end
@doc """
Returns all modules of the scenario app.
"""
@spec release_modules() :: [module()]
def release_modules do
scenario_app = Application.fetch_env!(:runbox, :scenario_app)
case :application.get_key(scenario_app, :modules) do
{:ok, modules} -> modules
:undefined -> []
end
end
defp resolve_prefix_conflicts(manifest_mods) do
conflicting =
Enum.reduce(manifest_mods, MapSet.new(), &find_conflicting(&1, &2, manifest_mods))
Enum.filter(manifest_mods, fn mod -> mod not in conflicting end)
end
defp find_conflicting(module, conflicting, others) do
if module in conflicting do
conflicting
else
case Enum.filter(others, &prefix_of?(module, &1)) do
[] -> conflicting
new_conflicts -> MapSet.union(conflicting, MapSet.new([module | new_conflicts]))
end
end
end
defp prefix_of?(module, other) do
prefix = Module.split(other)
module_path = Module.split(module)
if List.starts_with?(module_path, prefix) and module != other do
Logger.warning(
"Module #{other} is prefix of module #{module}, which could " <>
"lead to conflicts when detecting templates - they will be excluded"
)
true
else
false
end
end
defp modules_to_scenarios(modules) do
modules
|> Enum.filter(&Scenario.manifest_module?/1)
|> Enum.flat_map(fn manifest_mod ->
with {:ok, manifest} <- scenario_manifest_info(manifest_mod),
:ok <- validate_scenario_type(manifest.type, manifest_mod) do
opts = scenario_opts(manifest_mod)
templates = scenario_templates(modules, manifest_mod, manifest)
[
%Scenario{
manifest: manifest,
opts: opts,
templates: templates
}
]
else
:error ->
[]
end
end)
end
defp scenario_templates(modules, manifest_mod, manifest) do
template_modules = Enum.filter(modules, &Scenario.template_module?(&1, manifest_mod))
%{template_inspector: inspector_mod} = Scenario.get_impl_for(manifest.type)
Enum.flat_map(template_modules, fn mod ->
if valid_template?(inspector_mod, mod) do
info = template_info(inspector_mod, mod)
[%ScenarioTemplate{module: mod, info: info}]
else
[]
end
end)
end
defp validate_scenario_type(type, manifest_mod) do
if type in Type.all_types() do
:ok
else
Logger.error(
"Ignoring manifest module #{inspect(manifest_mod)} because it has invalid type '#{type}'"
)
:error
end
end
defp scenario_manifest_info(manifest_mod) do
{:ok, manifest_mod.get_info()}
catch
kind, value ->
Logger.error(
"Error reading info of manifest #{inspect(manifest_mod)}: #{inspect({kind, value})}"
)
Logger.error("Ignoring manifest module #{inspect(manifest_mod)}")
:error
end
defp scenario_opts(manifest_mod) do
if Code.ensure_loaded?(manifest_mod) && function_exported?(manifest_mod, :on_start, 0) do
%{on_start: {manifest_mod, :on_start, []}}
else
%{}
end
end
defp valid_template?(inspector_mod, template_mod) do
TemplateInspector.valid_template?(inspector_mod, template_mod)
end
defp template_info(inspector_mod, template_mod) do
TemplateInspector.template_info(inspector_mod, template_mod)
end
end
@doc """
Retrieves information about scenarios in a release.
"""
@spec release_info(String.t()) :: {:ok, [Scenario.t()]} | {:error, term()}
def release_info(release_dir) do
Slave.with_slave(release_dir, fn slave ->
Slave.call(slave, SlaveFunc, :release_info, [])
end)
end
end