From df4b297415d46ae02046f155eb7d5553c2063105 Mon Sep 17 00:00:00 2001 From: Vanshdeep Singh Date: Sat, 30 Nov 2024 17:38:54 -0500 Subject: [PATCH 1/6] Extend release spec --- lib/control_node/registry.ex | 7 +++++++ lib/control_node/release.ex | 22 ++++++++++++++++++++-- 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/lib/control_node/registry.ex b/lib/control_node/registry.ex index dd7363e..4ab0038 100644 --- a/lib/control_node/registry.ex +++ b/lib/control_node/registry.ex @@ -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 """ diff --git a/lib/control_node/release.ex b/lib/control_node/release.ex index b3d3caa..595dd4b 100644 --- a/lib/control_node/release.ex +++ b/lib/control_node/release.ex @@ -35,11 +35,15 @@ defmodule ControlNode.Release do name: atom, base_path: String.t(), start_timeout: integer, + deploy_func: :default | function(), + init_func: :default | :noop 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 @@ -373,7 +377,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") @@ -382,10 +386,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 + 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 + Host.init_release(host_spec, init_file, :start) + end + + defp init_release(%Spec{init_func: init_func}, host_spec, init_file) do + init_func.(host_spec, init_file) + 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}") From 5ea5beb0d779ceea013ca7a59231f2b3cf91de6a Mon Sep 17 00:00:00 2001 From: Vanshdeep Singh Date: Wed, 4 Dec 2024 01:04:09 -0500 Subject: [PATCH 2/6] Handle custome RELEASE_NAME --- lib/control_node/namespace/initialize.ex | 2 +- lib/control_node/release.ex | 31 ++++++++++++++++++------ 2 files changed, 24 insertions(+), 9 deletions(-) diff --git a/lib/control_node/namespace/initialize.ex b/lib/control_node/namespace/initialize.ex index 0aae3c3..613733c 100644 --- a/lib/control_node/namespace/initialize.ex +++ b/lib/control_node/namespace/initialize.ex @@ -95,7 +95,7 @@ defmodule ControlNode.Namespace.Initialize do {:running, 0} else - Logger.warn("Release state loaded, expected version #{version} found #{current_version}") + Logger.warn("Release state loaded, expected version #{version} found #{current_version || "err_not_deployed"}") {:partially_running, data.deploy_attempts} end diff --git a/lib/control_node/release.ex b/lib/control_node/release.ex index 595dd4b..1919a0d 100644 --- a/lib/control_node/release.ex +++ b/lib/control_node/release.ex @@ -36,7 +36,7 @@ defmodule ControlNode.Release do base_path: String.t(), start_timeout: integer, deploy_func: :default | function(), - init_func: :default | :noop + init_func: :default | :noop, health_check_spec: HealthCheckSpec.t() } defstruct name: nil, @@ -193,14 +193,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 @@ -334,10 +337,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 @@ -444,8 +449,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_var could be nil + sname = Map.get(env_vars || %{}, :RELEASE_NAME, release_spec.name) + + {:ok, :"#{sname}@#{host_spec.hostname}"} + end end From ff25b608eb1172c856f285506d9dd9992591eb37 Mon Sep 17 00:00:00 2001 From: Vanshdeep Singh Date: Wed, 4 Dec 2024 18:20:02 -0500 Subject: [PATCH 3/6] Fix spec and add test --- lib/control_node/release.ex | 4 ++-- test/control_node/release_test.exs | 21 +++++++++++++++++++++ 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/lib/control_node/release.ex b/lib/control_node/release.ex index 1919a0d..86244da 100644 --- a/lib/control_node/release.ex +++ b/lib/control_node/release.ex @@ -36,7 +36,7 @@ defmodule ControlNode.Release do base_path: String.t(), start_timeout: integer, deploy_func: :default | function(), - init_func: :default | :noop, + init_func: :default | function(), health_check_spec: HealthCheckSpec.t() } defstruct name: nil, @@ -458,7 +458,7 @@ defmodule ControlNode.Release do 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_var could be nil + # - env_vars could be nil sname = Map.get(env_vars || %{}, :RELEASE_NAME, release_spec.name) {:ok, :"#{sname}@#{host_spec.hostname}"} diff --git a/test/control_node/release_test.exs b/test/control_node/release_test.exs index 7acaf12..1b080dd 100644 --- a/test/control_node/release_test.exs +++ b/test/control_node/release_test.exs @@ -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 From 78bb9018a73c4465a4f0c3297f93afa35cacef15 Mon Sep 17 00:00:00 2001 From: Vanshdeep Singh Date: Thu, 19 Dec 2024 18:10:32 -0500 Subject: [PATCH 4/6] Improve API interfaces, Elixir 1.16 support --- lib/control_node/host.ex | 10 ++++----- lib/control_node/namespace.ex | 2 +- lib/control_node/namespace/connect.ex | 2 +- lib/control_node/namespace/initialize.ex | 2 +- lib/control_node/namespace/manage.ex | 4 ++-- lib/control_node/namespace/observe.ex | 2 +- lib/control_node/release.ex | 28 ++++++++++++++---------- 7 files changed, 28 insertions(+), 22 deletions(-) diff --git a/lib/control_node/host.ex b/lib/control_node/host.ex index c04aacf..269dd23 100644 --- a/lib/control_node/host.ex +++ b/lib/control_node/host.ex @@ -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 diff --git a/lib/control_node/namespace.ex b/lib/control_node/namespace.ex index 8a952fe..63b9eeb 100644 --- a/lib/control_node/namespace.ex +++ b/lib/control_node/namespace.ex @@ -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 diff --git a/lib/control_node/namespace/connect.ex b/lib/control_node/namespace/connect.ex index 5fe4809..db5641c 100644 --- a/lib/control_node/namespace/connect.ex +++ b/lib/control_node/namespace/connect.ex @@ -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 diff --git a/lib/control_node/namespace/initialize.ex b/lib/control_node/namespace/initialize.ex index 613733c..a84ec28 100644 --- a/lib/control_node/namespace/initialize.ex +++ b/lib/control_node/namespace/initialize.ex @@ -95,7 +95,7 @@ defmodule ControlNode.Namespace.Initialize do {:running, 0} else - Logger.warn("Release state loaded, expected version #{version} found #{current_version || "err_not_deployed"}") + Logger.warning("Release state loaded, expected version #{version} found #{current_version || "err_not_deployed"}") {:partially_running, data.deploy_attempts} end diff --git a/lib/control_node/namespace/manage.ex b/lib/control_node/namespace/manage.ex index 5d3707f..2c2ebeb 100644 --- a/lib/control_node/namespace/manage.ex +++ b/lib/control_node/namespace/manage.ex @@ -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 @@ -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 diff --git a/lib/control_node/namespace/observe.ex b/lib/control_node/namespace/observe.ex index 72d0f61..775fc09 100644 --- a/lib/control_node/namespace/observe.ex +++ b/lib/control_node/namespace/observe.ex @@ -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 diff --git a/lib/control_node/release.ex b/lib/control_node/release.ex index 86244da..61b47d4 100644 --- a/lib/control_node/release.ex +++ b/lib/control_node/release.ex @@ -106,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 @@ -232,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}" ) @@ -306,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 @@ -318,6 +323,7 @@ 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 @@ -325,8 +331,12 @@ defmodule ControlNode.Release do 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} @@ -402,19 +412,15 @@ defmodule ControlNode.Release do end defp init_release(%Spec{init_func: :default}, host_spec, init_file) do - Host.init_release(host_spec, init_file, :start) + # 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 - @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) - end - defp connect_and_monitor(release_spec, host_spec, cookie) do connect(release_spec, host_spec, cookie, true) end From 7a202ff642f87f05a98c3b5190652b386c998c40 Mon Sep 17 00:00:00 2001 From: Vanshdeep Singh Date: Sat, 14 Jun 2025 15:56:46 -0400 Subject: [PATCH 5/6] Update envrc settings, deploy_attempts --- .envrc | 8 +++++++- lib/control_node/host.ex | 7 +++++++ lib/control_node/host/ssh.ex | 4 ++-- lib/control_node/namespace/initialize.ex | 2 +- 4 files changed, 17 insertions(+), 4 deletions(-) diff --git a/.envrc b/.envrc index 0cfc5c0..f89b381 100644 --- a/.envrc +++ b/.envrc @@ -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" diff --git a/lib/control_node/host.ex b/lib/control_node/host.ex index 269dd23..d2730a3 100644 --- a/lib/control_node/host.ex +++ b/lib/control_node/host.ex @@ -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 diff --git a/lib/control_node/host/ssh.ex b/lib/control_node/host/ssh.ex index a1fc5f7..6c49116 100644 --- a/lib/control_node/host/ssh.ex +++ b/lib/control_node/host/ssh.ex @@ -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 diff --git a/lib/control_node/namespace/initialize.ex b/lib/control_node/namespace/initialize.ex index a84ec28..66afc83 100644 --- a/lib/control_node/namespace/initialize.ex +++ b/lib/control_node/namespace/initialize.ex @@ -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} From d0691d6bc97a8e9ecc44fee534e2b8aa95de748f Mon Sep 17 00:00:00 2001 From: Vanshdeep Singh Date: Thu, 28 Aug 2025 14:57:58 -0400 Subject: [PATCH 6/6] Support dynamic env vars --- lib/control_node/epmd.ex | 14 +++++++++----- lib/control_node/host.ex | 4 ++-- lib/control_node/host/ssh.ex | 30 ++++++++++++++++++++---------- lib/control_node/namespace.ex | 11 ++++++++++- lib/control_node/release.ex | 4 ++-- 5 files changed, 43 insertions(+), 20 deletions(-) diff --git a/lib/control_node/epmd.ex b/lib/control_node/epmd.ex index 101acb8..673b133 100644 --- a/lib/control_node/epmd.ex +++ b/lib/control_node/epmd.ex @@ -23,11 +23,15 @@ defmodule ControlNode.Epmd do def address_please(name, host, _address_family) do key = {"#{name}", "#{host}"} - [{_, port}] = :ets.lookup(:control_node_epmd, key) - # The distribution protocol version number has been 5 ever since - # Erlang/OTP R6. - version = 5 - {:ok, {127, 0, 0, 1}, port, version} + case :ets.lookup(:control_node_epmd, key) do + [{_, port}] -> + # The distribution protocol version number has been 5 ever since + # Erlang/OTP R6. + version = 5 + {:ok, {127, 0, 0, 1}, port, version} + _ -> + {:error, :not_found} + end end def register_release(name, host, port) do diff --git a/lib/control_node/host.ex b/lib/control_node/host.ex index d2730a3..9c914d5 100644 --- a/lib/control_node/host.ex +++ b/lib/control_node/host.ex @@ -37,7 +37,7 @@ defmodule ControlNode.Host 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 + with {:ok, %SSH.ExecStatus{exit_code: 0}} <- SSH.exec(host_spec, exec_binary, skip_eof: true) do :ok end end @@ -58,7 +58,7 @@ defmodule ControlNode.Host do @spec hostname(SSH.t()) :: {:ok, binary} def hostname(%SSH{} = host_spec) do with {:ok, %SSH.ExecStatus{exit_status: :success, message: [hostname]}} <- - SSH.exec(host_spec, "hostname") do + SSH.exec(host_spec, "hostname", skip_env_vars: true) do {:ok, %SSH{host_spec | hostname: String.trim(hostname)}} end end diff --git a/lib/control_node/host/ssh.ex b/lib/control_node/host/ssh.ex index 6c49116..2fc3e8c 100644 --- a/lib/control_node/host/ssh.ex +++ b/lib/control_node/host/ssh.ex @@ -25,7 +25,9 @@ defmodule ControlNode.Host.SSH do * `:user` : SSH user name * `:private_key_dir` : Path to the `.ssh` folder (eg. `/home/user/.ssh`) * `via_ssh_agent`: Use SSH Agent for authentication (default `false`) - * `env_vars`: Define env vars (key, value) to be passed when running a command on the remote host + * `env_vars`: Define env vars (key, value) to be passed when running a command on the remote host. + NOTE: `value` can be data or function with signature `fn (%ControlNode.Host.SSH{}) -> ... end` + and must return computed data """ @type t :: %__MODULE__{ host: binary, @@ -35,7 +37,8 @@ defmodule ControlNode.Host.SSH do private_key_dir: binary, conn: :ssh.connection_ref(), hostname: binary, - via_ssh_agent: boolean + via_ssh_agent: boolean, + env_vars: Map.t() | nil } @timeout :infinity @@ -127,9 +130,12 @@ defmodule ControlNode.Host.SSH do be set to `true`. This enable `exec` to return `ExecStatus` while the command is left running on host. """ - @spec exec(t, list | binary) :: {:ok, ExecStatus.t()} | :failure | {:error, any} - def exec(ssh_config, commands, skip_eof \\ false) do - env_vars = to_shell_env_vars(ssh_config.env_vars, :inline) + @spec exec(t, list | binary, list) :: {:ok, ExecStatus.t()} | :failure | {:error, any} + def exec(ssh_config, commands, opts \\ []) do + skip_eof = Keyword.get(opts, :skip_eof, false) + skip_env_vars = Keyword.get(opts, :skip_env_vars, false) + + env_vars = not skip_env_vars && to_shell_env_vars(ssh_config, :inline) || "" Logger.debug("Processed env var", env_vars: env_vars) script = @@ -143,7 +149,7 @@ defmodule ControlNode.Host.SSH do end defp do_exec(ssh_config, commands, skip_eof) when is_list(commands) do - env_vars = to_shell_env_vars(ssh_config.env_vars, :export) + env_vars = to_shell_env_vars(ssh_config, :export) commands = env_vars <> Enum.join(commands, " && ") do_exec(ssh_config, commands, skip_eof) end @@ -163,18 +169,22 @@ defmodule ControlNode.Host.SSH do end end - @spec to_shell_env_vars(Map.t() | nil, :inline | :export) :: String.t() - defp to_shell_env_vars(nil, _), do: "" + @spec to_shell_env_vars(t, :inline | :export) :: String.t() + defp to_shell_env_vars(%__MODULE__{env_vars: nil}, _), do: "" - defp to_shell_env_vars(env_vars, :inline) do + defp to_shell_env_vars(%__MODULE__{env_vars: env_vars} = ssh_config, :inline) do Enum.map(env_vars, fn {key, value} -> + value = is_function(value) && value.(%__MODULE__{ssh_config | conn: nil}) || value + "#{key}='#{value}'" end) |> Enum.join(" ") end - defp to_shell_env_vars(env_vars, :export) do + defp to_shell_env_vars(%__MODULE__{env_vars: env_vars} = ssh_config, :export) do Enum.map(env_vars, fn {key, value} -> + value = is_function(value) && value.(%__MODULE__{ssh_config | conn: nil}) || value + "export #{key}=#{value}" end) |> Enum.join("; ") diff --git a/lib/control_node/namespace.ex b/lib/control_node/namespace.ex index 63b9eeb..a80c1c0 100644 --- a/lib/control_node/namespace.ex +++ b/lib/control_node/namespace.ex @@ -49,8 +49,12 @@ defmodule ControlNode.Namespace do GenServer.call(namespace_pid, :current_version) end + def update_namespace_spec(%ControlNode.Namespace.Spec{} = spec) do + GenServer.call(spec, {:update_namespace_spec, spec}) + end + def start_link(namespace_spec, release_mod) do - name = release_mod.get_namespace_pname(namespace_spec) + name = release_mod.get_namespace_id(namespace_spec) Logger.debug("Starting namespace with name #{name}") GenServer.start_link(__MODULE__, [namespace_spec, release_mod], name: name) end @@ -86,6 +90,11 @@ defmodule ControlNode.Namespace do {:reply, {:ok, version_list}, state} end + @impl true + def handle_call({:update_namespace_spec, spec}, _from, state) do + {:reply, :ok, %{state | spec: spec}} + end + @impl true def handle_cast({:deploy, version}, %{spec: namespace_spec, release_mod: release_mod} = state) do Enum.map(namespace_spec.hosts, fn host_spec -> diff --git a/lib/control_node/release.ex b/lib/control_node/release.ex index 61b47d4..664c5bc 100644 --- a/lib/control_node/release.ex +++ b/lib/control_node/release.ex @@ -106,8 +106,8 @@ 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 + @spec get_namespace_id(Namespace.Spec.t()) :: :atom + def get_namespace_id(%Namespace.Spec{} = namespace_spec) do :"#{namespace_spec.tag}_#{@release_name}" end