Skip to content

Commit

Permalink
Merge branch 'release/0.5.0' into stable
Browse files Browse the repository at this point in the history
  • Loading branch information
holetse committed Sep 6, 2017
2 parents 990656c + f7b4d6b commit 0dbeb56
Show file tree
Hide file tree
Showing 20 changed files with 2,364 additions and 83 deletions.
16 changes: 11 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ add additional support.
```elixir
def deps do
[{:distillery, "~> 1.3",
{:bootleg, "~> 0.4"}]
{:bootleg, "~> 0.5"}]
end
```

Expand Down Expand Up @@ -136,7 +136,7 @@ by Bootleg:
* `workspace` - remote path specifying where to perform a build or push a deploy (default `.`)
* `user` - ssh username (default to local user)
* `password` - ssh password
* `identity` - file path of an SSH private key identify file
* `identity` - unencrypted private key file path (passphrases are not supported at this time)
* `port` - ssh port (default `22`)

#### Examples
Expand Down Expand Up @@ -180,7 +180,7 @@ Bootleg extensions may impose restrictions on certain roles, such as restricting
### Roles provided by Bootleg

* `build` - Takes only one host. If a list is given, only the first hosts is
used and a warning may result. If this role isn't set the release packaging will be done locally.
used and a warning may result.
* `app` - Takes a list of hosts, or a string with one host.

## Building and deploying a release
Expand Down Expand Up @@ -382,9 +382,15 @@ remote :app do
end

# filtering - only runs on app hosts with an option of primary set to true
remote :app, primary: true do
remote :app, filter: [primary: true] do
"mix ecto.migrate"
end

# change working directory - creates a file `/tmp/foo`, regardless of the role
# workspace configuration
remote :app, cd: "/tmp" do
"touch ./foo"
end
```

## Phoenix Support
Expand All @@ -399,7 +405,7 @@ for building phoenix releases.
# mix.exs
def deps do
[{:distillery, "~> 1.3"},
{:bootleg, "~> 0.4"},
{:bootleg, "~> 0.5"},
{:bootleg_phoenix, "~> 0.1"}]
end
```
Expand Down
54 changes: 41 additions & 13 deletions lib/bootleg/config.ex
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ defmodule Bootleg.Config do
line = caller.line()
quote do
hook_number = Bootleg.Config.Agent.increment(:next_hook_number)
module_name = String.to_atom("Elixir.Bootleg.Tasks.DynamicCallbacks." <>
module_name = String.to_atom("Elixir.Bootleg.DynamicCallbacks." <>
String.capitalize("#{unquote(position)}") <> String.capitalize("#{unquote(task)}") <>
"#{hook_number}")
defmodule module_name do
Expand Down Expand Up @@ -338,18 +338,18 @@ defmodule Bootleg.Config do
@doc """
Executes commands on all remote hosts within a role.
This is equivalent to calling `remote/3` with a `filter` of `[]`.
This is equivalent to calling `remote/3` with an `options` of `[]`.
"""
defmacro remote(role, lines) do
quote do: remote(unquote(role), [], unquote(lines))
end

defmacro remote(role, filter, do: {:__block__, _, lines}) do
quote do: remote(unquote(role), unquote(filter), unquote(lines))
defmacro remote(role, options, do: {:__block__, _, lines}) do
quote do: remote(unquote(role), unquote(options), unquote(lines))
end

defmacro remote(role, filter, do: lines) do
quote do: remote(unquote(role), unquote(filter), unquote(lines))
defmacro remote(role, options, do: lines) do
quote do: remote(unquote(role), unquote(options), unquote(lines))
end

@doc """
Expand All @@ -363,9 +363,16 @@ defmodule Bootleg.Config do
used as a command. Each command will be simulataneously executed on all hosts in the role. Once
all hosts have finished executing the command, the next command in the list will be sent.
`filter` is an optional `Keyword` list of host options to filter with. Any host whose options match
`options` is an optional `Keyword` list of options to customize the remote invocation. Currently two
keys are supported:
* `filter` takes a `Keyword` list of host options to filter with. Any host whose options match
the filter will be included in the remote execution. A host matches if it has all of the filtering
options defined and the values match (via `==/2`) the filter.
* `cd` changes the working directory of the remote shell prior to executing the remote
commands. The options takes either an absolute or relative path, with relative paths being
defined relative to the workspace configured for the role, or the default working directory
of the shell if no workspace is defined.
`role` can be a single role, a list of roles, or the special role `:all` (all roles). If the same host
exists in multiple roles, the commands will be run once for each role where the host shows up. In the
Expand Down Expand Up @@ -394,19 +401,24 @@ defmodule Bootleg.Config do
# only runs on `host1.example.com`
role :build, "host2.example.com"
role :build, "host1.example.com", primary: true, another_attr: :cat
role :build, "host1.example.com", filter: [primary: true, another_attr: :cat]
remote :build, primary: true do
remote :build, filter: [primary: true] do
"hostname"
end
# runs on `host1.example.com` inside the `tmp` directory found in the workspace
remote :build, filter: [primary: true], cd: "tmp/" do
"hostname"
end
```
"""
defmacro remote(role, filter, lines) do
defmacro remote(role, options, lines) do
roles = unpack_role(role)
quote bind_quoted: binding() do
Enum.reduce(roles, [], fn role, outputs ->
role
|> SSH.init([], filter)
|> SSH.init([cd: options[:cd]], Keyword.get(options, :filter, []))
|> SSH.run!(lines)
|> SSH.merge_run_results(outputs)
end)
Expand Down Expand Up @@ -488,13 +500,29 @@ defmodule Bootleg.Config do
@doc false
@spec app() :: any
def app do
get_config(:app, Project.config[:app])
:config
|> Bootleg.Config.Agent.get()
|> Keyword.get_lazy(:app, fn -> cache_project_config(:app) end)
end

@doc false
@spec version() :: any
def version do
get_config(:version, Project.config[:version])
:config
|> Bootleg.Config.Agent.get()
|> Keyword.get_lazy(:version, fn -> cache_project_config(:version) end)
end

@doc false
@spec cache_project_config(atom) :: any
def cache_project_config(prop) do
unless Project.umbrella? do
val = Project.config[prop]
Bootleg.Config.Agent.merge(:config, prop, val)
val
else
nil
end
end

@doc false
Expand Down
3 changes: 2 additions & 1 deletion lib/bootleg/config/agent.ex
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,8 @@ defmodule Bootleg.Config.Agent do
receive do
{:DOWN, ^ref, :process, _pid, _reason} ->
Enum.each(:code.all_loaded(), fn {module, _file} ->
if String.starts_with?(Atom.to_string(module), "Elixir.Bootleg.DynamicTasks.") do
if String.starts_with?(Atom.to_string(module), "Elixir.Bootleg.DynamicTasks.") ||
String.starts_with?(Atom.to_string(module), "Elixir.Bootleg.DynamicCallbacks.")do
unload_code(module)
end
end)
Expand Down
18 changes: 15 additions & 3 deletions lib/bootleg/ssh.ex
Original file line number Diff line number Diff line change
Expand Up @@ -37,15 +37,17 @@ defmodule Bootleg.SSH do
def init(hosts, options) do
workspace = Keyword.get(options, :workspace, ".")
create_workspace = Keyword.get(options, :create_workspace, true)
working_directory = Keyword.get(options, :cd)
UI.puts "Creating remote context at '#{workspace}'"

:ssh.start()

hosts
|> List.wrap
|> List.wrap()
|> Enum.map(&ssh_host_options/1)
|> SSHKit.context
|> SSHKit.context()
|> validate_workspace(workspace, create_workspace)
|> working_directory(working_directory)
end

def ssh_host_options(%Host{} = host) do
Expand Down Expand Up @@ -84,6 +86,16 @@ defmodule Bootleg.SSH do
SSHKit.path context, workspace
end

defp working_directory(context, path) when path == "." or path == false or is_nil(path) do
context
end
defp working_directory(context, path) do
case Path.type(path) do
:absolute -> %Context{context | path: path}
_ -> %Context{context | path: Path.join(context.path, path)}
end
end

defp capture(message, {buffer, status} = state, host) do
next = case message do
{:data, _, 0, data} ->
Expand Down Expand Up @@ -147,7 +159,7 @@ defmodule Bootleg.SSH do
def ssh_opt(option), do: option

@ssh_options ~w(user password port key_cb auth_methods connection_timeout id_string
idle_time silently_accept_hosts user_dir timeout connection_timeout identity)a
idle_time silently_accept_hosts user_dir timeout connection_timeout identity quiet_mode)a
def supported_options do
@ssh_options
end
Expand Down
12 changes: 12 additions & 0 deletions lib/bootleg/tasks/build.exs
Original file line number Diff line number Diff line change
@@ -1,10 +1,22 @@
alias Bootleg.{UI, Config}
use Bootleg.Config

task :verify_config do
if Config.app() == nil || Config.version() == nil do
raise "Error: app or version to deploy is not set.\n"
<> "Usually these are automatically picked up from Mix.Project.\n"
<> "If this is an umbrella app, you must set these in your deploy.exs, e.g.:\n"
<> "# config(:app, :myapp)\n"
<> "# config(:version, \"0.0.1\")"
end
end

task :build do
Bootleg.Strategies.Build.Distillery.build()
end

before_task :build, :verify_config

task :generate_release do
UI.info "Generating release"
mix_env = Keyword.get(Config.config(), :mix_env, "prod")
Expand Down
2 changes: 1 addition & 1 deletion lib/mix/tasks/init.ex
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ defmodule Mix.Tasks.Bootleg.Init do
# # mix.exs
# def deps do
# [{:distillery, "~> 1.3"},
# {:bootleg, "~> 0.4"},
# {:bootleg, "~> 0.5"},
# {:bootleg_phoenix, "~> 0.1"}]
# end
# ```
Expand Down
39 changes: 24 additions & 15 deletions lib/ui.ex
Original file line number Diff line number Diff line change
Expand Up @@ -75,13 +75,15 @@ defmodule Bootleg.UI do
"""
def puts_upload(%SSHKit.Context{} = context, local_path, remote_path) do
Enum.each(context.hosts, fn(host) ->
[:bright, :green]
[:reset, :bright, :green]
++ ["[" <> String.pad_trailing(host.name, 10) <> "] "]
++ [:reset, :yellow, "UPLOAD", " "]
++ [:reset, Path.relative_to_cwd(local_path)]
++ [:reset, :yellow, " -> "]
++ [:reset, Path.join(context.path, remote_path)]
|> Bunt.puts()
++ ["\n"]
|> IO.ANSI.format(output_coloring())
|> IO.binwrite()
end)
end

Expand All @@ -90,13 +92,15 @@ defmodule Bootleg.UI do
"""
def puts_download(%SSHKit.Context{} = context, remote_path, local_path) do
Enum.each(context.hosts, fn(host) ->
[:bright, :green]
[:reset, :bright, :green]
++ ["[" <> String.pad_trailing(host.name, 10) <> "] "]
++ [:reset, :yellow, "DOWNLOAD", " "]
++ [:reset, Path.join(context.path, remote_path)]
++ [:reset, :yellow, " -> "]
++ [:reset, Path.relative_to_cwd(local_path)]
|> Bunt.puts()
++ ["\n"]
|> IO.ANSI.format(output_coloring())
|> IO.binwrite()
end)
end

Expand All @@ -114,7 +118,9 @@ defmodule Bootleg.UI do
"""
def puts_send(%SSHKit.Host{} = host, command) do
prefix = "[" <> String.pad_trailing(host.name, 10) <> "] "
Bunt.puts [:bright, :green, prefix, :reset, command]
[:reset, :bright, :green, prefix, :reset, command, "\n"]
|> IO.ANSI.format(output_coloring())
|> IO.binwrite()
end

@doc """
Expand Down Expand Up @@ -154,17 +160,20 @@ defmodule Bootleg.UI do
prefix = "[" <> String.pad_trailing(host.name, 10) <> "] "
text
|> String.split(["\r\n", "\n"])
|> Enum.map(&String.trim_trailing/1)
|> Enum.map(&([:reset, :bright, :blue, prefix, :reset, &1]))
|> drop_last_line()
|> Enum.intersperse("\n")
|> Bunt.puts
|> Enum.map(&format_line(&1, prefix))
|> IO.ANSI.format(output_coloring())
|> IO.binwrite()
end

defp drop_last_line(lines) do
case length(lines) < 2 do
true -> lines
false -> Enum.drop(lines, -1)
end
defp format_line(line, prefix) do
[:reset, :bright, :blue, prefix, :reset, String.trim_trailing(line), "\n"]
end

@doc """
Get configured output coloring enabled
Defaults to true
"""
def output_coloring do
Application.get_env(:bootleg, :output_coloring, true)
end
end
3 changes: 1 addition & 2 deletions mix.exs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
defmodule Bootleg.Mixfile do
use Mix.Project

@version "0.4.0"
@version "0.5.0"
@source "https://github.com/labzero/bootleg"

def project do
Expand Down Expand Up @@ -45,7 +45,6 @@ defmodule Bootleg.Mixfile do
{:dialyxir, "~> 0.5", only: [:dev, :test], runtime: false},
{:ex_doc, "~> 0.16", only: :dev, runtime: false},
{:excoveralls, "~> 0.6", only: :test},
{:bunt, "~> 0.2.0"},
{:mock, "~> 0.2.0", only: :test},
{:junit_formatter, "~> 1.3", only: :test},
{:temp, "~> 0.4.3", only: :test}
Expand Down
15 changes: 9 additions & 6 deletions test/bootleg/config_functional_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -136,19 +136,22 @@ defmodule Bootleg.ConfigFunctionalTest do
end

@tag boot: 3
test "remote/3 filtering" do
test "remote/3 options" do
capture_io(fn ->
use Bootleg.Config

assert [{:ok, out_0, 0, _}] = remote :build, [foo: 0], "hostname"
assert [{:ok, out_1, 0, _}] = remote :build, [foo: 1], do: "hostname"
assert [{:ok, out_0, 0, _}] = remote :build, [filter: [foo: 0]], "hostname"
assert [{:ok, out_1, 0, _}] = remote :build, [filter: [foo: 1]], do: "hostname"
assert out_1 != out_0

assert [] = remote :build, [foo: :bar], "hostname"
assert [{:ok, out_all, 0, _}] = remote :all, [foo: :bar], "hostname"
assert [] = remote :build, [filter: [foo: :bar]], "hostname"
assert [{:ok, out_all, 0, _}] = remote :all, [filter: [foo: :bar]], "hostname"
assert out_1 != out_0 != out_all

remote :all, [foo: :bar] do "hostname" end
remote :all, filter: [foo: :bar] do "hostname" end

[{:ok, [stdout: "/tmp\n"], 0, _}] = remote :app, cd: "/tmp" do "pwd" end
[{:ok, [stdout: "/home\n"], 0, _}] = remote :app, cd: "../.." do "pwd" end
end)
end

Expand Down
Loading

0 comments on commit 0dbeb56

Please sign in to comment.