Skip to content

Extend release spec #46

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion .envrc
Original file line number Diff line number Diff line change
@@ -1 +1,7 @@
export ERL_AFLAGS="-start_epmd false -epmd_module Elixir.ControlNode.Epmd"
# `prevent_overlapping_partition` flag is turned off to prevent the following
# issue,
# [warning] 'global' at node :"node1@host1" requested disconnect from node :"node2@host2" in order to prevent overlapping partitions pid=<0.55.0>
# The above occur because BEAM is trying to work in a clustered mode but for
# control node we start network topology i.e. we don't expect release nodes to
# be connected to one another
export ERL_AFLAGS="-start_epmd false -epmd_module Elixir.ControlNode.Epmd -kernel prevent_overlapping_partitions false"
17 changes: 12 additions & 5 deletions lib/control_node/host.ex
Original file line number Diff line number Diff line change
Expand Up @@ -35,17 +35,17 @@ defmodule ControlNode.Host do
end
end

@spec init_release(SSH.t(), binary, atom) :: :ok | :failure | {:error, any}
def init_release(%SSH{} = host_spec, init_file, command) do
with {:ok, %SSH.ExecStatus{exit_code: 0}} <-
SSH.exec(host_spec, "nohup #{init_file} #{command} &", true) do
@spec init_release(SSH.t(), binary) :: :ok | :failure | {:error, any}
def init_release(%SSH{} = host_spec, exec_binary) do
with {:ok, %SSH.ExecStatus{exit_code: 0}} <- SSH.exec(host_spec, exec_binary, true) do
:ok
end
end

# TODO : check and remove
@spec stop_release(SSH.t(), binary) :: :ok | :failure | {:error, any}
def stop_release(%SSH{} = host_spec, cmd) do
with {:ok, %SSH.ExecStatus{exit_code: 0}} <- SSH.exec(host_spec, "nohup #{cmd} stop") do
with {:ok, %SSH.ExecStatus{exit_code: 0}} <- SSH.exec(host_spec, "#{cmd} stop") do
:ok
end
end
Expand All @@ -70,6 +70,13 @@ defmodule ControlNode.Host do
with {:ok, info} <- epmd_list_names(host_spec) do
disconnect(host_spec)
{:ok, info}
else
# No data was received, this usually implies that EPMD may not be running
# on remote host. So, no beam service is running hence we return empty map
{:error, :no_data} ->
{:ok, %Info{services: %{}}}
other ->
other
end
end

Expand Down
4 changes: 2 additions & 2 deletions lib/control_node/host/ssh.ex
Original file line number Diff line number Diff line change
Expand Up @@ -144,8 +144,8 @@ defmodule ControlNode.Host.SSH do

defp do_exec(ssh_config, commands, skip_eof) when is_list(commands) do
env_vars = to_shell_env_vars(ssh_config.env_vars, :export)
commands = env_vars <> Enum.join(commands, "; ")
do_exec(ssh_config, Enum.join(commands, "; "), skip_eof)
commands = env_vars <> Enum.join(commands, " && ")
do_exec(ssh_config, commands, skip_eof)
end

defp do_exec(ssh_config, script, skip_eof) when is_binary(script) do
Expand Down
2 changes: 1 addition & 1 deletion lib/control_node/namespace.ex
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ defmodule ControlNode.Namespace do
end

def start_link(namespace_spec, release_mod) do
name = :"#{namespace_spec.tag}_#{release_mod.release_name}"
name = release_mod.get_namespace_pname(namespace_spec)
Logger.debug("Starting namespace with name #{name}")
GenServer.start_link(__MODULE__, [namespace_spec, release_mod], name: name)
end
Expand Down
2 changes: 1 addition & 1 deletion lib/control_node/namespace/connect.ex
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ defmodule ControlNode.Namespace.Connect do
def callback_mode, do: :handle_event_function

def handle_event(any, event, state, _data) do
Logger.warn("Unexpected event #{inspect({any, event, state})}")
Logger.warning("Unexpected event #{inspect({any, event, state})}")
{:keep_state_and_data, []}
end
end
4 changes: 2 additions & 2 deletions lib/control_node/namespace/initialize.ex
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ defmodule ControlNode.Namespace.Initialize do
:initialize,
%Workflow.Data{deploy_attempts: deploy_attempts} = data
)
when deploy_attempts >= 5 do
when deploy_attempts >= 3 do
Logger.error("Depoyment attempts exhausted, failed to deploy release version #{version}")
{state, actions} = Namespace.Workflow.next(@state_name, :not_running, :ignore)
data = %Workflow.Data{data | deploy_attempts: 0}
Expand Down Expand Up @@ -95,7 +95,7 @@ defmodule ControlNode.Namespace.Initialize do

{:running, 0}
else
Logger.warn("Release state loaded, expected version #{version} found #{current_version}")
Logger.warning("Release state loaded, expected version #{version} found #{current_version || "err_not_deployed"}")

{:partially_running, data.deploy_attempts}
end
Expand Down
4 changes: 2 additions & 2 deletions lib/control_node/namespace/manage.ex
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ defmodule ControlNode.Namespace.Manage do
data = %Workflow.Data{data | health_check_timer: timer_ref}
{:keep_state, data, []}
else
Logger.warn("Release health check failed")
Logger.warning("Release health check failed")

# TODO: respect max failure count before rebooting the release
if hc_spec.on_failure == :reboot do
Expand Down Expand Up @@ -106,7 +106,7 @@ defmodule ControlNode.Namespace.Manage do
end

def handle_event(any, event, state, _data) do
Logger.warn("Unexpected event #{inspect({any, event, state})}")
Logger.warning("Unexpected event #{inspect({any, event, state})}")
{:keep_state_and_data, []}
end

Expand Down
2 changes: 1 addition & 1 deletion lib/control_node/namespace/observe.ex
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ defmodule ControlNode.Namespace.Observe do
end

def handle_event(any, event, state, _data) do
Logger.warn("Unexpected event #{inspect({any, event, state})}")
Logger.warning("Unexpected event #{inspect({any, event, state})}")
{:keep_state_and_data, []}
end
end
7 changes: 7 additions & 0 deletions lib/control_node/registry.ex
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,13 @@ defmodule ControlNode.Registry do
|> File.read()
end

@doc """
Retrieves application release tar file location
"""
def location(%Local{} = registry_spec, application, version) do
Path.join(registry_spec.path, "#{application}-#{version}.tar.gz")
end

@doc """
Stores application release tar file in the filesystem
"""
Expand Down
73 changes: 56 additions & 17 deletions lib/control_node/release.ex
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,15 @@ defmodule ControlNode.Release do
name: atom,
base_path: String.t(),
start_timeout: integer,
deploy_func: :default | function(),
init_func: :default | function(),
health_check_spec: HealthCheckSpec.t()
}
defstruct name: nil,
base_path: nil,
start_timeout: 5,
deploy_func: :default,
init_func: :default,
health_check_spec: %HealthCheckSpec{}
end

Expand Down Expand Up @@ -102,6 +106,11 @@ defmodule ControlNode.Release do
|> call(:current_version)
end

@spec get_namespace_pname(Namespace.Spec.t()) :: :atom
def get_namespace_pname(%Namespace.Spec{} = namespace_spec) do
:"#{namespace_spec.tag}_#{@release_name}"
end

@doc """
Deploy a new version of the service to the given host

Expand Down Expand Up @@ -189,14 +198,17 @@ defmodule ControlNode.Release do
@spec initialize_state(Release.Spec.t(), ControlNode.Host.SSH.t(), :atom) ::
Release.State.t()
def initialize_state(release_spec, host_spec, cookie) do
with {:ok, %Host.Info{services: services}} <- Host.info(host_spec) do
case Map.get(services, release_spec.name) do
with {:ok, host_spec} <- Host.hostname(host_spec),
{:ok, %Host.Info{services: services}} <- Host.info(host_spec),
{:ok, nodename} <- to_node_name(release_spec, host_spec) do

# Check if the nodename is registered on host
case Map.get(services, to_sname(nodename)) do
nil ->
State.new(host_spec)

service_port ->
with %Host.SSH{} = host_spec <- Host.connect(host_spec),
{:ok, host_spec} <- Host.hostname(host_spec) do
with %Host.SSH{} = host_spec <- Host.connect(host_spec) do
# Setup tunnel to release port on host
# TODO/NOTE/WARN random local port should be used to avoid having a clash
# if the releases use the same port on different hosts
Expand Down Expand Up @@ -225,7 +237,7 @@ defmodule ControlNode.Release do
%State{release_state | version: version, pid: release_pid}

_ ->
Logger.warn(
Logger.warning(
"No version found for release #{release_spec.name} on host #{host_spec.host}"
)

Expand Down Expand Up @@ -299,7 +311,7 @@ defmodule ControlNode.Release do
Node.monitor(node, false)
else
_other ->
Logger.warn("Failed to demonitor node", release_spec: release_spec, host_spec: host_spec)
Logger.warning("Failed to demonitor node", release_spec: release_spec, host_spec: host_spec)
end
end

Expand All @@ -311,15 +323,20 @@ defmodule ControlNode.Release do
end

def is_running?(release_spec, host_spec) do
Logger.debug("Checking if release #{release_spec.name} is running on host", host_spec: host_spec)
case node_info(release_spec, host_spec) do
{:ok, _} -> true
_ -> false
end
end

defp node_info(release_spec, host_spec) do
with {:ok, %Host.Info{services: services}} <- Host.info(host_spec) do
case Map.get(services, release_spec.name) do
with {:ok, %Host.Info{services: services}} <- Host.info(host_spec),
{:ok, nodename} <- to_node_name(release_spec, host_spec) do

Logger.debug("Checking for node #{nodename} on host", host_spec: host_spec)

case Map.get(services, to_sname(nodename)) do
nil ->
{:error, :release_not_running}

Expand All @@ -330,10 +347,12 @@ defmodule ControlNode.Release do
end

defp register_node(release_spec, host_spec, service_port) do
{:ok, nodename} = to_node_name(release_spec, host_spec)

# NOTE: Configure host config for inet
# This config will be used by BEAM to resolve `hostname`
Inet.add_alias_for_localhost(host_spec.hostname)
Epmd.register_release(release_spec.name, host_spec.hostname, service_port)
Epmd.register_release(to_sname(nodename), host_spec.hostname, service_port)
end

defp get_version(release_spec, host_spec) do
Expand Down Expand Up @@ -373,7 +392,7 @@ defmodule ControlNode.Release do
"""
@spec deploy(Spec.t(), Host.SSH.t(), ControlNode.Registry.Local.t(), binary) ::
:ok | {:error, Host.SSH.ExecStatus.t()}
def deploy(%Spec{} = release_spec, host_spec, registry_spec, version) do
def deploy(%Spec{deploy_func: :default} = release_spec, host_spec, registry_spec, version) do
# WARN: may not work if host OS is different from control-node OS
host_release_dir = Path.join(release_spec.base_path, version)
host_release_path = Path.join(host_release_dir, "#{release_spec.name}-#{version}.tar.gz")
Expand All @@ -382,14 +401,24 @@ defmodule ControlNode.Release do
:ok <- Host.upload_file(host_spec, host_release_path, tar_file),
:ok <- Host.extract_tar(host_spec, host_release_path, host_release_dir) do
init_file = Path.join(host_release_dir, "bin/#{release_spec.name}")
Host.init_release(host_spec, init_file, :start)
init_release(release_spec, host_spec, init_file)
end
end

@spec start(Spec.t(), State.t()) :: term
def start(release_spec, %State{host: host_spec, release_path: release_path}) do
init_file = Path.join(release_path, "bin/#{release_spec.name}")
Host.init_release(host_spec, init_file, :start)
def deploy(%Spec{deploy_func: deploy_func} = release_spec, host_spec, registry_spec, version) when is_function(deploy_func) do
with {:ok, remote_init_file} <- deploy_func.(release_spec, host_spec, registry_spec, version) do
init_release(release_spec, host_spec, remote_init_file)
end
end

defp init_release(%Spec{init_func: :default}, host_spec, init_file) do
# cmd = "nohup #{init_file} start &"
cmd = "#{init_file} daemon"
Host.init_release(host_spec, cmd)
end

defp init_release(%Spec{init_func: init_func}, host_spec, init_file) do
init_func.(host_spec, init_file)
end

defp connect_and_monitor(release_spec, host_spec, cookie) do
Expand Down Expand Up @@ -426,8 +455,18 @@ defmodule ControlNode.Release do
end
end

def to_sname(nodename) do
Atom.to_string(nodename) |> String.split("@") |> hd() |> String.to_atom()
end

def to_node_name(_release_spec, %Host.SSH{hostname: nil}), do: {:error, :hostname_not_found}

def to_node_name(release_spec, host_spec),
do: {:ok, :"#{release_spec.name}@#{host_spec.hostname}"}
def to_node_name(release_spec, %Host.SSH{env_vars: env_vars} = host_spec) do
# NOTE: If the env_vars for the host defines `RELEASE_NAME` then we should
# take that over the default name
# - env_vars could be nil
sname = Map.get(env_vars || %{}, :RELEASE_NAME, release_spec.name)

{:ok, :"#{sname}@#{host_spec.hostname}"}
end
end
21 changes: 21 additions & 0 deletions test/control_node/release_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,27 @@ defmodule ControlNode.ReleaseTest do
Release.stop(release_spec, release_state)
end

test "Connects to node with custom RELEASE_NAME", %{
release_spec: release_spec,
host_spec: host_spec,
cookie: cookie
} do
custom_release_name = "service-app-dev"
host_spec = %{
host_spec | env_vars: %{RELEASE_NAME: custom_release_name}
}

assert %Release.State{
host: host_spec,
version: "0.1.0",
status: :running,
release_path: "/app/service_app/0.1.0"
} = release_state = Release.initialize_state(release_spec, host_spec, cookie)

assert :pong == Node.ping(:"#{custom_release_name}@#{host_spec.hostname}")
Release.stop(release_spec, release_state)
end

@tag :skip
# NOTE: Not sure what this test was supposed to cover :/
# remember to document next time
Expand Down
Loading