From 2afbc9738ad42a351260c9e0a78dcd7bc203eb25 Mon Sep 17 00:00:00 2001 From: Edward Holets Date: Tue, 19 Sep 2017 13:36:05 -0700 Subject: [PATCH 01/13] Ignore gh-pages branch on CircleCI. (#178) --- .circleci/config.yml | 4 ++++ .gitignore | 2 ++ 2 files changed, 6 insertions(+) diff --git a/.circleci/config.yml b/.circleci/config.yml index 3fd2772..c2aadc7 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1,4 +1,8 @@ version: 2 +general: + branches: + ignore: + - gh-pages jobs: build: machine: true diff --git a/.gitignore b/.gitignore index b6012c7..a8b8557 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,5 @@ erl_crash.dump # Also ignore archive artifacts (built via "mix archive.build"). *.ez +/_site +/.sass-cache From d675759ca74ef95e35f62559225a00b07cdcf246 Mon Sep 17 00:00:00 2001 From: Brien Wankel Date: Thu, 21 Sep 2017 14:36:11 -0700 Subject: [PATCH 02/13] Fix typo in readme (#179) --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 26cb05d..670c5cb 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ Simple deployment and server automation for Elixir. -**Bootleg** is a simple set of commands that attempt to simplify building and deploying elixir applications. The goal of the project is to provide an extensible framework that can support many different deploy scenarios with one common set of commands. +**Bootleg** is a simple set of commands that attempt to simplify building and deploying Elixir applications. The goal of the project is to provide an extensible framework that can support many different deploy scenarios with one common set of commands. Out of the box, Bootleg provides remote build and remote server automation for your existing [Distillery](https://github.com/bitwalker/distillery) releases. Bootleg assumes your project is committed into a `git` repository and some of the build steps use this assumption to handle code in some steps of the build process. If you are using an scm other than git, please consider contributing to Bootleg to @@ -12,7 +12,7 @@ add additional support. ## Installation -```elixir +``` def deps do [{:distillery, "~> 1.3", {:bootleg, "~> 0.5"}] From 0e4655b8350731b4fc460ed4bae82c66991ed833 Mon Sep 17 00:00:00 2001 From: Edward Holets Date: Mon, 25 Sep 2017 14:25:22 -0700 Subject: [PATCH 03/13] Add homepage link and improve source code linking. (#180) * Add link to one pager. * Add proper source code links to documentation. * Avoid 301 on homepage. --- mix.exs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/mix.exs b/mix.exs index e6aa940..932725c 100644 --- a/mix.exs +++ b/mix.exs @@ -3,6 +3,7 @@ defmodule Bootleg.Mixfile do @version "0.5.0" @source "https://github.com/labzero/bootleg" + @homepage "https://labzero.github.io/bootleg/" def project do [app: :bootleg, @@ -13,10 +14,12 @@ defmodule Bootleg.Mixfile do elixirc_paths: elixirc_paths(Mix.env), test_coverage: [tool: ExCoveralls], dialyzer: [plt_add_deps: :transitive, plt_add_apps: [:mix, :sshkit]], - docs: [source_ref: "v#{@version}", main: "readme", extras: ["README.md"]], + docs: [source_ref: "release-#{@version}", main: "readme", extras: ["README.md"]], description: description(), deps: deps(), - package: package() + package: package(), + source_url: @source, + homepage_url: @homepage ] end @@ -58,7 +61,7 @@ defmodule Bootleg.Mixfile do defp package do [maintainers: ["labzero", "Brien Wankel", "Ned Holets", "Rob Adams"], licenses: ["MIT"], - links: %{"GitHub" => @source}] + links: %{"GitHub" => @source, "Homepage" => @homepage}] end defp elixirc_paths(:test), do: ["lib", "web", "test/support"] From 6116555eb260d81b2f02489315a769cc3fa889ff Mon Sep 17 00:00:00 2001 From: Edward Holets Date: Fri, 29 Sep 2017 12:40:18 -0700 Subject: [PATCH 04/13] Ensure only whole lines of SSH output are emitted. (#182) * Ensure only whole lines of SSH output are emitted. * Update UI module to emit UTF-8 data to shell. --- lib/bootleg/ssh.ex | 44 ++++++++++++++++++++++++---- lib/ui.ex | 8 ++--- test/bootleg/ssh_functional_test.exs | 26 +++++++++++++++- test/ui_test.exs | 11 +++++-- 4 files changed, 75 insertions(+), 14 deletions(-) diff --git a/lib/bootleg/ssh.ex b/lib/bootleg/ssh.ex index b57b6d1..cb2c8f7 100644 --- a/lib/bootleg/ssh.ex +++ b/lib/bootleg/ssh.ex @@ -69,7 +69,7 @@ defmodule Bootleg.SSH do end conn - |> SSHKitSSH.run(cmd, fun: &capture(&1, &2, host)) + |> SSHKitSSH.run(cmd, fun: &capture(&1, &2, host), acc: {:cont, {[], nil, %{}}}) |> Tuple.append(host) end @@ -96,14 +96,46 @@ defmodule Bootleg.SSH do end end - defp capture(message, {buffer, status} = state, host) do + @last_new_line ~r/\A(?.*)((?\n)(?[^\n]*))?\z/msU + + defp split_last_line(data) do + %{"bulk" => bulk, "newline" => newline, "remainder" => remainder} = Regex.named_captures(@last_new_line, data) + if newline == "\n" do + {bulk <> "\n", remainder} + else + {"", bulk} + end + end + + defp buffer_complete_lines(data, device, buffer, partial_buffer) do + partial_line = partial_buffer[device] || "" + {bulk, remainder} = split_last_line(partial_line <> data) + new_partial_buffer = Map.put(partial_buffer, device, remainder) + if bulk == "" do + {buffer, new_partial_buffer, bulk} + else + {[{device, bulk} | buffer], new_partial_buffer, bulk} + end + end + + defp empty_partial_buffer(buffer, partial_buffer) do + remainders = Enum.filter(partial_buffer, fn {_, value} -> value && value != "" end) + remainders ++ buffer + end + + defp capture(message, {buffer, status, partial_buffer} = state, host) do next = case message do {:data, _, 0, data} -> + {buffer, partial_buffer, data} = + buffer_complete_lines(data, :stdout, buffer, partial_buffer) UI.puts_recv host, String.trim_trailing(data) - {[{:stdout, data} | buffer], status} - {:data, _, 1, data} -> {[{:stderr, data} | buffer], status} - {:exit_status, _, code} -> {buffer, code} - {:closed, _} -> {:ok, Enum.reverse(buffer), status} + {buffer, status, partial_buffer} + {:data, _, 1, data} -> + {buffer, partial_buffer, _} = buffer_complete_lines(data, :stderr, buffer, partial_buffer) + {buffer, status, partial_buffer} + {:exit_status, _, code} -> {buffer, code, partial_buffer} + {:closed, _} -> + {:ok, Enum.reverse(empty_partial_buffer(buffer, partial_buffer)), status} _ -> state end diff --git a/lib/ui.ex b/lib/ui.ex index 717f993..f66788d 100644 --- a/lib/ui.ex +++ b/lib/ui.ex @@ -83,7 +83,7 @@ defmodule Bootleg.UI do ++ [:reset, Path.join(context.path, remote_path)] ++ ["\n"] |> IO.ANSI.format(output_coloring()) - |> IO.binwrite() + |> IO.write() end) end @@ -100,7 +100,7 @@ defmodule Bootleg.UI do ++ [:reset, Path.relative_to_cwd(local_path)] ++ ["\n"] |> IO.ANSI.format(output_coloring()) - |> IO.binwrite() + |> IO.write() end) end @@ -120,7 +120,7 @@ defmodule Bootleg.UI do prefix = "[" <> String.pad_trailing(host.name, 10) <> "] " [:reset, :bright, :green, prefix, :reset, command, "\n"] |> IO.ANSI.format(output_coloring()) - |> IO.binwrite() + |> IO.write() end @doc """ @@ -162,7 +162,7 @@ defmodule Bootleg.UI do |> String.split(["\r\n", "\n"]) |> Enum.map(&format_line(&1, prefix)) |> IO.ANSI.format(output_coloring()) - |> IO.binwrite() + |> IO.write() end defp format_line(line, prefix) do diff --git a/test/bootleg/ssh_functional_test.exs b/test/bootleg/ssh_functional_test.exs index 56e296a..c5f8f10 100644 --- a/test/bootleg/ssh_functional_test.exs +++ b/test/bootleg/ssh_functional_test.exs @@ -100,7 +100,6 @@ defmodule Bootleg.SSHFunctionalTest do end test "init/3 working directory option", %{hosts: [host]} do - # credo:disable-for-next-line Credo.Check.Consistency.MultiAliasImportRequireUse use Bootleg.Config role :valid_workspace, host.ip, port: host.port, user: host.user, @@ -151,4 +150,29 @@ defmodule Bootleg.SSHFunctionalTest do File.rm!(path) end) end + + @tag ui_verbosity: :silent + test "returns output in whole line increments", %{hosts: [host]} do + # credo:disable-for-next-line Credo.Check.Consistency.MultiAliasImportRequireUse + use Bootleg.Config + + role :node, host.ip, port: host.port, user: host.user, + workspace: "/", silently_accept_hosts: true, identity: host.private_key_path + + n = 100_000 + digest = :crypto.hash_init(:sha256) + checksum = :crypto.hash(:sha256, Enum.join(Enum.map(1..n, fn i -> "#{i}\n" end))) + + capture_io(fn -> + [{:ok, data, 0, _}] = remote :node, "seq 1 #{n}" + {chunk_sums, digest} = Enum.map_reduce(data, digest, fn ({_, bytes}, digest) -> + assert :binary.last(bytes) == 0x0A # ensure the chunk is well formed + digest = :crypto.hash_update(digest, bytes) + {lines, _} = String.split_at(bytes, -1) + {Enum.sum(Enum.map(String.split(lines, "\n"), &String.to_integer/1)), digest} + end) + assert Enum.sum(chunk_sums) == (n * (n + 1)) / 2 # ensure no bytes got shifted + assert :crypto.hash_final(digest) == checksum # ensure no bytes got lost + end) + end end diff --git a/test/ui_test.exs b/test/ui_test.exs index 5e13182..239bc59 100644 --- a/test/ui_test.exs +++ b/test/ui_test.exs @@ -179,11 +179,16 @@ defmodule Bootleg.UITest do end) == "\e[0m\e[1m\e[34m[localhost.1] \e[0mhello world!\n\e[0m" end - test "ssh puts ignores non-UTF8 data", %{conn: conn} do + test "ssh puts does not molest UTF-8 data", %{conn: conn} do file = File.read!("./test/fixtures/encoding/utf8.data") host = List.first(conn.hosts) - assert byte_size(capture_io(fn -> + out = capture_io(fn -> UI.puts_recv(host, file) - end)) == 285_036, "Received data not in expected form." + end) + size = byte_size(out) + char_out = String.to_charlist(out) + assert Enum.count(char_out, fn codepoint -> codepoint === 8216 end) == 2000 + assert Enum.count(char_out, fn codepoint -> codepoint === 8217 end) == 2000 + assert size == 273_036, "Received data not in expected form." end end From cd2516e36b938dbbce468debff686c00102e3d18 Mon Sep 17 00:00:00 2001 From: Edward Holets Date: Tue, 10 Oct 2017 08:50:28 -0700 Subject: [PATCH 05/13] Add target architecture to CircleCI cache config. (#183) --- .circleci/config.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index c2aadc7..423dc80 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -11,16 +11,16 @@ jobs: - checkout - run: echo 'export INSTALL_PATH="$HOME/dependencies";export PATH="$INSTALL_PATH/bin:$PATH";export MIX_ENV=test;export VERSION_CIRCLECI=2' >> $BASH_ENV - restore_cache: - key: environment-cache-{{ checksum ".circleci/config.yml" }}-{{ checksum "script/ci/prepare.sh" }} + key: environment-cache-{{ checksum ".circleci/config.yml" }}-{{ checksum "script/ci/prepare.sh" }}-{{ arch }} - run: name: Install Elixir command: script/ci/prepare.sh - save_cache: - key: environment-cache-{{ checksum ".circleci/config.yml" }}-{{ checksum "script/ci/prepare.sh" }} + key: environment-cache-{{ checksum ".circleci/config.yml" }}-{{ checksum "script/ci/prepare.sh" }}-{{ arch }} paths: - ~/dependencies - restore_cache: - key: dependencies-cache-{{ checksum ".circleci/config.yml" }}-{{ checksum "mix.lock" }} + key: dependencies-cache-{{ checksum ".circleci/config.yml" }}-{{ checksum "mix.lock" }}-{{ arch }} - run: name: Preparing dependencies command: | @@ -31,7 +31,7 @@ jobs: mix dialyzer --plt; no_output_timeout: 10m - save_cache: - key: dependencies-cache-{{ checksum ".circleci/config.yml" }}-{{ checksum "mix.lock" }} + key: dependencies-cache-{{ checksum ".circleci/config.yml" }}-{{ checksum "mix.lock" }}-{{ arch }} paths: - ~/.mix - _build From 4920b80a0707a709554f4c93b94fffefbcd4d9ea Mon Sep 17 00:00:00 2001 From: Edward Holets Date: Tue, 10 Oct 2017 10:28:55 -0700 Subject: [PATCH 06/13] Add `REPLACE_OS_VARS` support. (#186) * Add `REPLACE_OS_VARS` support. * Address buil failure alpine-baselayout 3.0.5-r0. --- README.md | 1 + lib/bootleg/config.ex | 9 ++++++++- lib/bootleg/ssh.ex | 10 ++++++++++ test/bootleg/ssh_functional_test.exs | 19 +++++++++++++++++-- test/support/docker/Dockerfile | 1 - 5 files changed, 36 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 670c5cb..db04e32 100644 --- a/README.md +++ b/README.md @@ -138,6 +138,7 @@ by Bootleg: * `password` - ssh password * `identity` - unencrypted private key file path (passphrases are not supported at this time) * `port` - ssh port (default `22`) + * `replace_os_vars` - controls the `REPLACE_OS_VARS` environment variable used by Distillery for release configuration (default `true`) #### Examples diff --git a/lib/bootleg/config.ex b/lib/bootleg/config.ex index 980e91b..eace99c 100644 --- a/lib/bootleg/config.ex +++ b/lib/bootleg/config.ex @@ -51,7 +51,14 @@ defmodule Bootleg.Config do |> Keyword.put(:user, user) # identity needs to be present in both options lists |> Keyword.put(:identity, ssh_options[:identity]) - |> Enum.filter(fn {_, v} -> v end) + |> Keyword.get_and_update(:identity, fn val -> + if val || Keyword.has_key?(ssh_options, :identity) do + {val, val || ssh_options[:identity]} + else + :pop + end + end) + |> elem(1) quote bind_quoted: binding() do hosts = diff --git a/lib/bootleg/ssh.ex b/lib/bootleg/ssh.ex index cb2c8f7..1917fcf 100644 --- a/lib/bootleg/ssh.ex +++ b/lib/bootleg/ssh.ex @@ -38,6 +38,7 @@ defmodule Bootleg.SSH do workspace = Keyword.get(options, :workspace, ".") create_workspace = Keyword.get(options, :create_workspace, true) working_directory = Keyword.get(options, :cd) + should_replace_os_vars = Keyword.get(options, :replace_os_vars, true) UI.puts "Creating remote context at '#{workspace}'" :ssh.start() @@ -46,6 +47,7 @@ defmodule Bootleg.SSH do |> List.wrap() |> Enum.map(&ssh_host_options/1) |> SSHKit.context() + |> replace_os_vars(should_replace_os_vars) |> validate_workspace(workspace, create_workspace) |> working_directory(working_directory) end @@ -96,6 +98,14 @@ defmodule Bootleg.SSH do end end + defp replace_os_vars(context, true) do + SSHKit.env(context, %{"REPLACE_OS_VARS" => "true"}) + end + + defp replace_os_vars(context, _) do + context + end + @last_new_line ~r/\A(?.*)((?\n)(?[^\n]*))?\z/msU defp split_last_line(data) do diff --git a/test/bootleg/ssh_functional_test.exs b/test/bootleg/ssh_functional_test.exs index c5f8f10..4b79024 100644 --- a/test/bootleg/ssh_functional_test.exs +++ b/test/bootleg/ssh_functional_test.exs @@ -151,9 +151,7 @@ defmodule Bootleg.SSHFunctionalTest do end) end - @tag ui_verbosity: :silent test "returns output in whole line increments", %{hosts: [host]} do - # credo:disable-for-next-line Credo.Check.Consistency.MultiAliasImportRequireUse use Bootleg.Config role :node, host.ip, port: host.port, user: host.user, @@ -175,4 +173,21 @@ defmodule Bootleg.SSHFunctionalTest do assert :crypto.hash_final(digest) == checksum # ensure no bytes got lost end) end + + test "replace os vars", %{hosts: [host]} do + # credo:disable-for-next-line Credo.Check.Consistency.MultiAliasImportRequireUse + use Bootleg.Config + + role :default_replace, host.ip, port: host.port, user: host.user, + workspace: "/", silently_accept_hosts: true, identity: host.private_key_path + + role :no_replace, host.ip, port: host.port, user: host.user, + workspace: "/", silently_accept_hosts: true, identity: host.private_key_path, + replace_os_vars: false + + capture_io(fn -> + assert [{:ok, [stdout: "true"], 0, _}] = remote :default_replace, "echo -n ${REPLACE_OS_VARS}" + assert [{:ok, [], 0, _}] = remote :no_replace, "echo -n ${REPLACE_OS_VARS}" + end) + end end diff --git a/test/support/docker/Dockerfile b/test/support/docker/Dockerfile index 76c6481..3182512 100644 --- a/test/support/docker/Dockerfile +++ b/test/support/docker/Dockerfile @@ -5,7 +5,6 @@ FROM bitwalker/alpine-elixir:latest RUN echo http://dl-cdn.alpinelinux.org/alpine/edge/main >> /etc/apk/repositories RUN apk update -RUN apk upgrade RUN apk add --update --no-cache openssh sudo git perl-utils bash RUN ssh-keygen -A RUN printf "PermitUserEnvironment yes\n" >> /etc/ssh/sshd_config From 3f5cef191372e0bc0e094d3f2a26afbb5a35a669 Mon Sep 17 00:00:00 2001 From: Edward Holets Date: Tue, 10 Oct 2017 10:40:09 -0700 Subject: [PATCH 07/13] Add `config/1` for retrieving configuration values. (#184) * Add `config/1` for retrieving configuration values. * Refactor internal tasks to use `config/1`. * Remove dead Bootleg.Strategies.Manage.Distillery module. --- lib/bootleg/config.ex | 42 +++++++++++++++++++++++++++-- lib/bootleg/tasks/build.exs | 8 +++--- lib/strategies/build/distillery.ex | 6 ++--- lib/strategies/manage/distillery.ex | 36 ------------------------- test/bootleg/config_test.exs | 15 ++++++++++- 5 files changed, 61 insertions(+), 46 deletions(-) delete mode 100644 lib/strategies/manage/distillery.ex diff --git a/lib/bootleg/config.ex b/lib/bootleg/config.ex index eace99c..2c625a5 100644 --- a/lib/bootleg/config.ex +++ b/lib/bootleg/config.ex @@ -8,8 +8,9 @@ defmodule Bootleg.Config do defmacro __using__(_) do quote do - import Bootleg.Config, only: [role: 2, role: 3, config: 2, config: 0, before_task: 2, - after_task: 2, invoke: 1, task: 2, remote: 1, remote: 2, remote: 3, load: 1, upload: 3] + import Bootleg.Config, only: [role: 2, role: 3, config: 2, config: 1, config: 0, + before_task: 2, after_task: 2, invoke: 1, task: 2, remote: 1, remote: 2, + remote: 3, load: 1, upload: 3] end end @@ -101,6 +102,42 @@ defmodule Bootleg.Config do end end + @doc """ + Fetches the value for the supplied key from the Bootleg configuration. If the provided + key is a `Tuple`, the first element is considered the key, the second value is considered + the default value (and returned without altering the config) in case the key has not + been set. This uses the same semantics as `Keyword.get/3`. + + ``` + use Bootleg.Config + config :foo, :bar + + # local_foo will be :bar + local_foo = config :foo + + # local_foo will be :bar still, as :foo already has a value + local_foo = config {:foo, :car} + + # local_hello will be :world, as :hello has not been defined yet + local_hello = config {:hello, :world} + + config :hello, nil + # local_hello will be nil, as :hello has a value of nil now + local_hello = config {:hello, :world} + ``` + """ + defmacro config({key, default}) do + quote bind_quoted: binding() do + Keyword.get(Bootleg.Config.Agent.get(:config), key, default) + end + end + + defmacro config(key) do + quote bind_quoted: binding() do + Keyword.get(Bootleg.Config.Agent.get(:config), key) + end + end + @doc """ Sets `key` in the Bootleg configuration to `value`. @@ -113,6 +150,7 @@ defmodule Bootleg.Config do config :app, :my_cool_app config :version, "1.0.0" + ``` """ defmacro config(key, value) do quote bind_quoted: binding() do diff --git a/lib/bootleg/tasks/build.exs b/lib/bootleg/tasks/build.exs index 727865e..c74c575 100644 --- a/lib/bootleg/tasks/build.exs +++ b/lib/bootleg/tasks/build.exs @@ -19,14 +19,14 @@ before_task :build, :verify_config task :generate_release do UI.info "Generating release" - mix_env = Keyword.get(Config.config(), :mix_env, "prod") + mix_env = config({:mix_env, "prod"}) remote :build do "MIX_ENV=#{mix_env} mix release" end end task :compile do - mix_env = Keyword.get(Config.config(), :mix_env, "prod") + mix_env = config({:mix_env, "prod"}) UI.info "Compiling remote build" remote :build do "MIX_ENV=#{mix_env} mix deps.compile" @@ -35,8 +35,8 @@ task :compile do end task :clean do - locations = config() - |> Keyword.get(:clean_locations, ["*"]) + locations = {:clean_locations, ["*"]} + |> config() |> List.wrap |> Enum.join(" ") if locations != "" do diff --git a/lib/strategies/build/distillery.ex b/lib/strategies/build/distillery.ex index f968fa3..36bd57c 100644 --- a/lib/strategies/build/distillery.ex +++ b/lib/strategies/build/distillery.ex @@ -15,8 +15,8 @@ defmodule Bootleg.Strategies.Build.Distillery do def build do conn = init() - mix_env = Config.get_config(:mix_env, "prod") - refspec = Config.get_config(:refspec, "master") + mix_env = config({:mix_env, "prod"}) + refspec = config({:refspec, "master"}) invoke :clean :ok = git_push(conn, refspec) git_reset_remote(conn, refspec) @@ -50,7 +50,7 @@ defmodule Bootleg.Strategies.Build.Distillery do _ -> "ssh://#{user_host_port}/~/#{workspace}" end - push_options = Config.get_config(:push_options, "-f") + push_options = config({:push_options, "-f"}) git_env = git_env(options) UI.info "Pushing new commits with git to: #{user_host_port}" diff --git a/lib/strategies/manage/distillery.ex b/lib/strategies/manage/distillery.ex deleted file mode 100644 index 4f89703..0000000 --- a/lib/strategies/manage/distillery.ex +++ /dev/null @@ -1,36 +0,0 @@ -defmodule Bootleg.Strategies.Manage.Distillery do - @moduledoc false - - alias Bootleg.{UI, SSH, Config} - - def init do - SSH.init(:app) - end - - def start(conn) do - app_name = Config.app - SSH.run!(conn, "bin/#{app_name} start") - UI.info "#{app_name} started" - {:ok, conn} - end - - def stop(conn) do - app_name = Config.app - SSH.run!(conn, "bin/#{app_name} stop") - UI.info "#{app_name} stopped" - {:ok, conn} - end - - def restart(conn) do - app_name = Config.app - SSH.run!(conn, "bin/#{app_name} restart") - UI.info "#{app_name} restarted" - {:ok, conn} - end - - def ping(conn) do - app_name = Config.app - SSH.run!(conn, "bin/#{app_name} ping") - {:ok, conn} - end -end diff --git a/test/bootleg/config_test.exs b/test/bootleg/config_test.exs index 44a1ac2..9095049 100644 --- a/test/bootleg/config_test.exs +++ b/test/bootleg/config_test.exs @@ -516,7 +516,6 @@ defmodule Bootleg.ConfigTest do init: fn(role, options, filter) -> {role, options, filter} end, upload: fn(_conn, _local, _remote) -> :ok end, ] do - # credo:disable-for-next-line Credo.Check.Consistency.MultiAliasImportRequireUse use Bootleg.Config role :foo, "never-used-foo.example.com" @@ -595,4 +594,18 @@ defmodule Bootleg.ConfigTest do assert called SSH.upload({:foo, [], [db: :mysql]}, "the/local/path", "some/remote/path") assert called SSH.upload({:car, [], [db: :mysql]}, "the/local/path", "some/remote/path") end + + test "config/1" do + # credo:disable-for-next-line Credo.Check.Consistency.MultiAliasImportRequireUse + use Bootleg.Config + + refute config(:foo) + assert config({:foo, :bar}) == :bar + assert config({:foo, :car}) == :car + config(:foo, :war) + assert config(:foo) == :war + assert config({:foo, :bar}) == :war + config(:foo, nil) + assert config({:foo, :bar}) == nil + end end From 9cee90ec2418620d77818d5410ab8755e5aedf00 Mon Sep 17 00:00:00 2001 From: Rob Adams Date: Tue, 10 Oct 2017 11:23:11 -0700 Subject: [PATCH 08/13] Update README, general cleanup (#185) * Update README, general cleanup * Remove extraneous text --- README.md | 65 +++++++++++++++++++++++++++++++++++-------------------- 1 file changed, 41 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index db04e32..58bc048 100644 --- a/README.md +++ b/README.md @@ -4,10 +4,10 @@ Simple deployment and server automation for Elixir. -**Bootleg** is a simple set of commands that attempt to simplify building and deploying Elixir applications. The goal of the project is to provide an extensible framework that can support many different deploy scenarios with one common set of commands. +**Bootleg** is a simple set of commands that attempt to simplify building and deploying Elixir applications. The goal of the project is to provide an extensible framework that can support many different deployment scenarios with one common set of commands. -Out of the box, Bootleg provides remote build and remote server automation for your existing [Distillery](https://github.com/bitwalker/distillery) releases. Bootleg assumes your project is committed into a `git` repository and some of the build steps use this assumption -to handle code in some steps of the build process. If you are using an scm other than git, please consider contributing to Bootleg to +Out of the box, Bootleg provides remote build and remote server automation for your [Distillery](https://github.com/bitwalker/distillery) releases. Bootleg assumes your project is committed into a **git** repository and some of the build steps use this assumption +to handle code within the build process. If you are using another source control management (SCM) tool please consider contributing to Bootleg to add additional support. ## Installation @@ -24,7 +24,6 @@ end In order to build your project, Bootleg requires that your build server be set up to compile Elixir code. Make sure you have already installed Elixir on any build server you define. - ## Quick Start ### Initialize your project @@ -396,11 +395,14 @@ end ## Phoenix Support -Bootleg builds elixir apps, if your application has extra steps required make use of the hooks +If your application has extra steps required, you may make use of the hooks system to add additional functionality. A common case is for building assets for Phoenix -applications. To build phoenix assets during your build, include the additional package -`bootleg_phoenix` to your `deps` list. This will automatically perform the additional steps required -for building phoenix releases. +applications. + +### Using the bootleg_phoenix package + +To run these steps automatically you may include the additional package +`bootleg_phoenix` in your `deps` list. This package provides the build hook commands required to build most Phoenix releases. ```elixir # mix.exs @@ -411,14 +413,32 @@ def deps do end ``` -For more about `bootleg_phoenix` see: https://github.com/labzero/bootleg_phoenix +See also: [labzero/bootleg_phoenix](https://github.com/labzero/bootleg_phoenix). -## Sharing Tasks +### Using your own deploy configuration and hooks -Sharing is a good thing. We love to share, especially awesome code we write. Bootleg supports loading -tasks from packages in a manner very similar to `Mix.Task`. Just define your module under `Bootleg.Tasks`, -`use Bootleg.Task` and pass it a block of Bootleg DSL. The contents will be discovered and executed -automatically at launch. +Similar to how `bootleg_phoenix` is implemented, you can make use of the hooks system to run some commands on the build server around compile time. + +```elixir +task :phoenix_digest do + remote :build do + "npm install" + "./node_modules/brunch/bin/brunch b -p" + "MIX_ENV=prod mix phoenix.digest" + end + UI.info "Phoenix asset digest generated" +end + +after_task :compile, :phoenix_digest +``` + + +## Task Providers + +Sharing is a good thing. Bootleg supports loading +tasks from packages in a manner very similar to `Mix.Task`. + +You can create and share custom tasks by namespacing a module under `Bootleg.Tasks` and passing a block of Bootleg DSL: ```elixir defmodule Bootleg.Tasks.Foo do @@ -432,30 +452,27 @@ defmodule Bootleg.Tasks.Foo do end ``` -See `Bootleg.Task` for more details. +See also: [Bootleg.Task](https://hexdocs.pm/bootleg/Bootleg.Task.html#content) for additional examples. ## Help -If something goes wrong, retry with the `--verbose` option. For detailed information about the Bootleg commands and their options, try `mix bootleg help `. +We're usually around on Slack where you can find us on [elixir-lang's #bootleg channel](http://elixir-lang.slack.com/messages/bootleg/) if you have any questions. + ----- ## Acknowledgments Bootleg makes heavy use of the [bitcrowd/SSHKit.ex](https://github.com/bitcrowd/sshkit.ex) -library under the hood. We would like to acknowledge the effort from the bitcrowd team that went into -creating SSHKit.ex as well as for them prioritizing our requests and providing a chance to collaborate -on ideas for both the SSHKit.ex and Bootleg projects. +library under the hood. We are very appreciative of the efforts of the bitcrowd team for both creating SSHKit.ex and being so attentive to our requests. We're also grateful for the opportunity to collaborate +on ideas for both projects! ## Contributing -We welcome everyone to contribute to Bootleg and help us tackle existing issues! - -Use the [issue tracker][issues] for bug reports or feature requests. -Open a [pull request][pulls] when you are ready to contribute. +We welcome all contributions to Bootleg, whether they're improving the documentation, implementing features, reporting issues or suggesting new features. -If you are planning to contribute documentation, please check +If you'd like to contribute documentation, please check [the best practices for writing documentation][writing-docs]. From 887aa2a478ceb88659b807af8cb7f195e02d0dd6 Mon Sep 17 00:00:00 2001 From: Brien Wankel Date: Thu, 12 Oct 2017 17:59:03 -0700 Subject: [PATCH 09/13] Update ssh_client_key_api dependency to 0.1.1 (#187) --- mix.exs | 2 +- mix.lock | 22 +++++++++++----------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/mix.exs b/mix.exs index 932725c..90609a1 100644 --- a/mix.exs +++ b/mix.exs @@ -43,7 +43,7 @@ defmodule Bootleg.Mixfile do defp deps do [ {:sshkit, "0.0.3"}, - {:ssh_client_key_api, "0.0.1"}, + {:ssh_client_key_api, "~> 0.1"}, {:credo, "~> 0.7", only: [:dev, :test]}, {:dialyxir, "~> 0.5", only: [:dev, :test], runtime: false}, {:ex_doc, "~> 0.16", only: :dev, runtime: false}, diff --git a/mix.lock b/mix.lock index 927cc01..ac3d125 100644 --- a/mix.lock +++ b/mix.lock @@ -1,21 +1,21 @@ %{"bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm"}, - "certifi": {:hex, :certifi, "1.2.1", "c3904f192bd5284e5b13f20db3ceac9626e14eeacfbb492e19583cf0e37b22be", [:rebar3], [], "hexpm"}, - "credo": {:hex, :credo, "0.8.3", "efe6e9078de64cefdd25d8df7a97292e29e63f42a8988990340eaf1f40d93224", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}], "hexpm"}, - "dialyxir": {:hex, :dialyxir, "0.5.0", "5bc543f9c28ecd51b99cc1a685a3c2a1a93216990347f259406a910cf048d1d7", [:mix], [], "hexpm"}, - "earmark": {:hex, :earmark, "1.2.2", "f718159d6b65068e8daeef709ccddae5f7fdc770707d82e7d126f584cd925b74", [:mix], [], "hexpm"}, - "ex_doc": {:hex, :ex_doc, "0.16.2", "3b3e210ebcd85a7c76b4e73f85c5640c011d2a0b2f06dcdf5acdb2ae904e5084", [:mix], [{:earmark, "~> 1.1", [hex: :earmark, repo: "hexpm", optional: false]}], "hexpm"}, - "excoveralls": {:hex, :excoveralls, "0.7.1", "3dd659db19c290692b5e2c4a2365ae6d4488091a1ba58f62dcbdaa0c03da5491", [:mix], [{:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: false]}, {:hackney, ">= 0.12.0", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm"}, + "certifi": {:hex, :certifi, "2.0.0", "a0c0e475107135f76b8c1d5bc7efb33cd3815cb3cf3dea7aefdd174dabead064", [:rebar3], [], "hexpm"}, + "credo": {:hex, :credo, "0.8.8", "990e7844a8d06ebacd88744a55853a83b74270b8a8461c55a4d0334b8e1736c9", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}], "hexpm"}, + "dialyxir": {:hex, :dialyxir, "0.5.1", "b331b091720fd93e878137add264bac4f644e1ddae07a70bf7062c7862c4b952", [:mix], [], "hexpm"}, + "earmark": {:hex, :earmark, "1.2.3", "206eb2e2ac1a794aa5256f3982de7a76bf4579ff91cb28d0e17ea2c9491e46a4", [:mix], [], "hexpm"}, + "ex_doc": {:hex, :ex_doc, "0.17.1", "39f777415e769992e6732d9589dc5846ea587f01412241f4a774664c746affbb", [:mix], [{:earmark, "~> 1.1", [hex: :earmark, repo: "hexpm", optional: false]}], "hexpm"}, + "excoveralls": {:hex, :excoveralls, "0.7.4", "3d84b2f15a0e593159f74b19f83794b464b34817183d27965bdc6c462de014f9", [:mix], [{:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: false]}, {:hackney, ">= 0.12.0", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm"}, "exjsx": {:hex, :exjsx, "4.0.0", "60548841e0212df401e38e63c0078ec57b33e7ea49b032c796ccad8cde794b5c", [:mix], [{:jsx, "~> 2.8.0", [hex: :jsx, repo: "hexpm", optional: false]}], "hexpm"}, - "hackney": {:hex, :hackney, "1.8.6", "21a725db3569b3fb11a6af17d5c5f654052ce9624219f1317e8639183de4a423", [:rebar3], [{:certifi, "1.2.1", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "5.0.2", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "1.0.2", [hex: :mimerl, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.1", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm"}, - "idna": {:hex, :idna, "5.0.2", "ac203208ada855d95dc591a764b6e87259cb0e2a364218f215ad662daa8cd6b4", [:rebar3], [{:unicode_util_compat, "0.2.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm"}, + "hackney": {:hex, :hackney, "1.9.0", "51c506afc0a365868469dcfc79a9d0b94d896ec741cfd5bd338f49a5ec515bfe", [:rebar3], [{:certifi, "2.0.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "5.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "1.0.2", [hex: :mimerl, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.1", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm"}, + "idna": {:hex, :idna, "5.1.0", "d72b4effeb324ad5da3cab1767cb16b17939004e789d8c0ad5b70f3cea20c89a", [:rebar3], [{:unicode_util_compat, "0.3.1", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm"}, "jsx": {:hex, :jsx, "2.8.2", "7acc7d785b5abe8a6e9adbde926a24e481f29956dd8b4df49e3e4e7bcc92a018", [:mix, :rebar3], [], "hexpm"}, "junit_formatter": {:hex, :junit_formatter, "1.3.0", "e4321e3275f48daecadb3116bc814e1a743645f2549c6526b1a32cd6c8dd1833", [:mix], [], "hexpm"}, - "meck": {:hex, :meck, "0.8.7", "ebad16ca23f685b07aed3bc011efff65fbaf28881a8adf925428ef5472d390ee", [:rebar3], [], "hexpm"}, + "meck": {:hex, :meck, "0.8.8", "eeb3efe811d4346e1a7f65b2738abc2ad73cbe1a2c91b5dd909bac2ea0414fa6", [:rebar3], [], "hexpm"}, "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm"}, "mimerl": {:hex, :mimerl, "1.0.2", "993f9b0e084083405ed8252b99460c4f0563e41729ab42d9074fd5e52439be88", [:rebar3], [], "hexpm"}, "mock": {:hex, :mock, "0.2.1", "bfdba786903e77f9c18772dee472d020ceb8ef000783e737725a4c8f54ad28ec", [:mix], [{:meck, "~> 0.8.2", [hex: :meck, repo: "hexpm", optional: false]}], "hexpm"}, - "ssh_client_key_api": {:hex, :ssh_client_key_api, "0.0.1", "521076c1c468275b351cb2c28dd185ba76a07afff7706805add1e0892f0b64a9", [:mix], [], "hexpm"}, + "ssh_client_key_api": {:hex, :ssh_client_key_api, "0.1.1", "eb191625fdca68fc2740de44820e124748d9e27fb75fc85a6f1653371bf9b4ba", [:mix], [], "hexpm"}, "sshkit": {:hex, :sshkit, "0.0.3", "b0d7c3e0a5caed69304ccbc1c97ab0b1fe82a26f0d21ba2a26b52868f834ab5a", [:mix], [], "hexpm"}, "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.1", "28a4d65b7f59893bc2c7de786dec1e1555bd742d336043fe644ae956c3497fbe", [:make, :rebar], [], "hexpm"}, "temp": {:hex, :temp, "0.4.3", "b641c3ce46094839bff110fdb64162536d640d9d47ca2c37add9104a2fa3bd81", [:mix], [], "hexpm"}, - "unicode_util_compat": {:hex, :unicode_util_compat, "0.2.0", "dbbccf6781821b1c0701845eaf966c9b6d83d7c3bfc65ca2b78b88b8678bfa35", [:rebar3], [], "hexpm"}} + "unicode_util_compat": {:hex, :unicode_util_compat, "0.3.1", "a1f612a7b512638634a603c8f401892afbf99b8ce93a45041f8aaca99cadb85e", [:rebar3], [], "hexpm"}} From 8a9f973248f63b905d07e2dde948085dada25a00 Mon Sep 17 00:00:00 2001 From: Brien Wankel Date: Mon, 16 Oct 2017 10:56:39 -0700 Subject: [PATCH 10/13] Update junit_formatter to get rid of deprecation error (#188) * Update junit_formatter to get rid of deprecation error * Resolve rebase conflict --- mix.exs | 2 +- mix.lock | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/mix.exs b/mix.exs index 90609a1..09109ee 100644 --- a/mix.exs +++ b/mix.exs @@ -49,7 +49,7 @@ defmodule Bootleg.Mixfile do {:ex_doc, "~> 0.16", only: :dev, runtime: false}, {:excoveralls, "~> 0.6", only: :test}, {:mock, "~> 0.2.0", only: :test}, - {:junit_formatter, "~> 1.3", only: :test}, + {:junit_formatter, "~> 2.0", only: :test}, {:temp, "~> 0.4.3", only: :test} ] end diff --git a/mix.lock b/mix.lock index ac3d125..47b3caa 100644 --- a/mix.lock +++ b/mix.lock @@ -9,7 +9,7 @@ "hackney": {:hex, :hackney, "1.9.0", "51c506afc0a365868469dcfc79a9d0b94d896ec741cfd5bd338f49a5ec515bfe", [:rebar3], [{:certifi, "2.0.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "5.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "1.0.2", [hex: :mimerl, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.1", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm"}, "idna": {:hex, :idna, "5.1.0", "d72b4effeb324ad5da3cab1767cb16b17939004e789d8c0ad5b70f3cea20c89a", [:rebar3], [{:unicode_util_compat, "0.3.1", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm"}, "jsx": {:hex, :jsx, "2.8.2", "7acc7d785b5abe8a6e9adbde926a24e481f29956dd8b4df49e3e4e7bcc92a018", [:mix, :rebar3], [], "hexpm"}, - "junit_formatter": {:hex, :junit_formatter, "1.3.0", "e4321e3275f48daecadb3116bc814e1a743645f2549c6526b1a32cd6c8dd1833", [:mix], [], "hexpm"}, + "junit_formatter": {:hex, :junit_formatter, "2.0.0", "7312342085d619590588fb5532ac2ec7444280d0b7224e85a25aab868ae0038d", [:mix], [], "hexpm"}, "meck": {:hex, :meck, "0.8.8", "eeb3efe811d4346e1a7f65b2738abc2ad73cbe1a2c91b5dd909bac2ea0414fa6", [:rebar3], [], "hexpm"}, "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm"}, "mimerl": {:hex, :mimerl, "1.0.2", "993f9b0e084083405ed8252b99460c4f0563e41729ab42d9074fd5e52439be88", [:rebar3], [], "hexpm"}, From 0b380782dda3d69c29a036b25dec22b464351eae Mon Sep 17 00:00:00 2001 From: Edward Holets Date: Tue, 17 Oct 2017 13:27:43 -0700 Subject: [PATCH 11/13] Add support for different length result lists to `SSH.merge_run_results/2`. (#190) --- lib/bootleg/ssh.ex | 7 +++++++ test/bootleg/ssh_test.exs | 2 ++ 2 files changed, 9 insertions(+) diff --git a/lib/bootleg/ssh.ex b/lib/bootleg/ssh.ex index 1917fcf..ac408f2 100644 --- a/lib/bootleg/ssh.ex +++ b/lib/bootleg/ssh.ex @@ -215,6 +215,13 @@ defmodule Bootleg.SSH do orig end def merge_run_results(new, orig) when is_list(orig) do + delta = length(new) - length(orig) + entries = List.duplicate([], abs(delta)) + {new, orig} = if delta > 0 do + {new, orig ++ entries} + else + {new ++ entries, orig} + end new |> Enum.zip(orig) |> Enum.map(fn {n, o} -> diff --git a/test/bootleg/ssh_test.exs b/test/bootleg/ssh_test.exs index 1e7aa2a..74763bc 100644 --- a/test/bootleg/ssh_test.exs +++ b/test/bootleg/ssh_test.exs @@ -53,6 +53,8 @@ defmodule Bootleg.SSHTest do assert [1, 2] = SSH.merge_run_results([1, 2], []) assert [[1, 2]] = SSH.merge_run_results([[1, 2]], []) assert [[1, 2]] = SSH.merge_run_results([], [[1, 2]]) + assert [[2, 1], [4]] = SSH.merge_run_results([1], [2, 4]) + assert [[1, 2], [4]] = SSH.merge_run_results([2, 4], [1]) end test "supported_options/0" do From d8b5a7f10e810071f65b50efb87c5b41c1075fa1 Mon Sep 17 00:00:00 2001 From: Edward Holets Date: Thu, 26 Oct 2017 12:27:22 -0700 Subject: [PATCH 12/13] Add `download/3` DSL verb. (#192) --- lib/bootleg/config.ex | 59 ++++++++- lib/bootleg/ssh.ex | 3 +- test/bootleg/config_functional_test.exs | 152 +++++++++++++++++++++++- test/bootleg/config_test.exs | 57 ++++++++- 4 files changed, 265 insertions(+), 6 deletions(-) diff --git a/lib/bootleg/config.ex b/lib/bootleg/config.ex index 2c625a5..9440775 100644 --- a/lib/bootleg/config.ex +++ b/lib/bootleg/config.ex @@ -10,7 +10,7 @@ defmodule Bootleg.Config do quote do import Bootleg.Config, only: [role: 2, role: 3, config: 2, config: 1, config: 0, before_task: 2, after_task: 2, invoke: 1, task: 2, remote: 1, remote: 2, - remote: 3, load: 1, upload: 3] + remote: 3, load: 1, upload: 3, download: 3] end end @@ -444,10 +444,10 @@ defmodule Bootleg.Config do # runs for hosts found in :build first, then for hosts in :app remote [:build, :app], do: "hostname" - # only runs on `host1.example.com` role :build, "host2.example.com" - role :build, "host1.example.com", filter: [primary: true, another_attr: :cat] + role :build, "host1.example.com", primary: true, another_attr: :cat + # only runs on `host1.example.com` remote :build, filter: [primary: true] do "hostname" end @@ -536,6 +536,59 @@ defmodule Bootleg.Config do end end + @doc """ + Downloads files from remote hosts to the local machine. + + Downloading works much like `remote/3`, but instead of transferring shell commands over SSH, + it transfers files via SCP. The remote host does need to support SCP, which should be provided + by most SSH implementations automatically. + + `role` can either be a single role name, a list of roles, or a list of roles and filter + attributes. The special `:all` role is also supported. See `remote/3` for details. Note that + if multiple hosts match, files will be downloaded from all matching hosts, and any duplicate + file names will result in collisions. The exact semantics of how that works are handled by + `SSHKit.SCP`, but in general the file transfered last wins. + + `local_path` is a path to local directory or file where the downloaded files(s) should be placed. + Absolute paths will be respected, relative paths will be resolved relative to the current working + directory of the invoking shell. If the `local_path` does not exist in the local file system, an + attempt will be made to create the missing directory. This does not handle nested directories, + and a `File.Error` will be raised. + + `remote_path` is the file or directory to be copied from the remote hosts. If a directory is + specified, its contents will be recursively copied. Relative paths will be resolved relative to + the remote workspace, absolute paths will be respected. + + The files on the local host are created using the current user's `uid`/`gid` and `umask`. + + ``` + use Bootleg.Config + + # copies ./my_file from the remote host to ./new_name locally + download :app, "my_file", "new_name" + + # copies ./my_file from the remote host to the file ./a_dir/my_file locally + download :app, "my_file", "a_dir" + + # recursively copies ./some_dir on the remote host to ./new_dir locally, ./new_dir + # will be created if missing + download :app, "some_dir", "new_dir" + + # copies /foo/my_file on the remote host to /tmp/foo locally + download :app, "/foo/my_file", "/tmp/foo" + """ + defmacro download(role, remote_path, local_path) do + {roles, filters} = split_roles_and_filters(role) + roles = unpack_role(roles) + quote bind_quoted: binding() do + Enum.each(roles, fn role -> + role + |> SSH.init([], filters) + |> SSH.download(remote_path, local_path) + end) + end + end + @doc false @spec get_config(atom, any) :: any def get_config(key, default \\ nil) do diff --git a/lib/bootleg/ssh.ex b/lib/bootleg/ssh.ex index ac408f2..77d90c2 100644 --- a/lib/bootleg/ssh.ex +++ b/lib/bootleg/ssh.ex @@ -171,8 +171,9 @@ defmodule Bootleg.SSH do def download(conn, remote_path, local_path) do UI.puts_download conn, remote_path, local_path - case SSHKit.download(conn, remote_path, as: local_path) do + case SSHKit.download(conn, remote_path, as: local_path, recursive: true) do [:ok|_] -> :ok + [] -> :ok [{_, msg}|_] -> raise "SCP download error: #{msg}" end end diff --git a/test/bootleg/config_functional_test.exs b/test/bootleg/config_functional_test.exs index 25ebe3f..24ee831 100644 --- a/test/bootleg/config_functional_test.exs +++ b/test/bootleg/config_functional_test.exs @@ -121,6 +121,22 @@ defmodule Bootleg.ConfigFunctionalTest do end) end + @tag boot: 3 + test "remote/2 multiple roles/hosts" do + capture_io(fn -> + use Bootleg.Config + + assert [{:ok, [stdout: host_1], 0, _}, {:ok, [stdout: host_2], 0, _}] = remote :build, do: "hostname" + refute host_1 == host_2 + + assert [ + [{:ok, [stdout: host_1], 0, _}, + {:ok, [stdout: host_2], 0, _}], + [{:ok, [stdout: host_3], 0, _}]] = remote [:build, :app], do: "hostname" + refute host_1 == host_2 == host_3 + end) + end + test "remote/2 fails remotely" do use Bootleg.Config @@ -219,7 +235,6 @@ defmodule Bootleg.ConfigFunctionalTest do invoke :upload_role_filtered assert_raise SSHError, fn -> - # credo:disable-for-next-line Credo.Check.Consistency.MultiAliasImportRequireUse use Bootleg.Config remote :all, do: "grep '^upload_role_filtered$' role_filtered" end @@ -237,4 +252,139 @@ defmodule Bootleg.ConfigFunctionalTest do invoke :upload_preserve_name end) end + + @tag boot: 3 + test "download/3" do + capture_io(fn -> + # credo:disable-for-next-line Credo.Check.Consistency.MultiAliasImportRequireUse + use Bootleg.Config + + path = Temp.mkdir!("download") + + # single file, single role and host + remote :app, do: "echo -n download_single_role >> download_single_role" + download :app, "download_single_role", path + + assert {:ok, "download_single_role"} = File.read(Path.join(path, "download_single_role")) + + # single file, single role, multiple hosts + remote :build, do: "hostname >> download_single_role_multi_host" + [_, {:ok, [stdout: host], 0, _}] = remote :build, do: "hostname" + download :build, "download_single_role_multi_host", path + + assert {:ok, ^host} = File.read(Path.join(path, "download_single_role_multi_host")) + + # single file, multiple roles/hosts + remote [:build, :app], do: "hostname >> download_multi_role" + [[_, _], [{:ok, [stdout: host], 0, _}]] = remote [:build, :app], do: "hostname" + download :build, "download_multi_role", path + + assert {:ok, ^host} = File.read(Path.join(path, "download_multi_role")) + + # single file, :all role (multiple hosts) + remote :all, do: "hostname >> download_all_role" + [[_, _], [{:ok, [stdout: host], 0, _}]] = remote :all, do: "hostname" + download :all, "download_all_role", path + assert {:ok, ^host} = File.read(Path.join(path, "download_all_role")) + + # single file, filtered role + remote :all, [filter: [foo: :bar]], "hostname >> download_role_filtered" + [{:ok, [stdout: host], 0, _}] = remote :all, [filter: [foo: :bar]], "hostname" + download [:all, foo: :bar], "download_role_filtered", path + assert {:ok, ^host} = File.read(Path.join(path, "download_role_filtered")) + + # recursively download directory + remote :app do + "mkdir -p to_download" + "mkdir -p to_download/deep/deeper" + "touch to_download/foo" + "touch to_download/bar" + "hostname >> to_download/hostname" + "uname -a >> to_download/deep/deeper/the_depths" + end + [{:ok, [stdout: host], 0, _}] = remote :app, "hostname" + download :app, "to_download", path + to_download_path = Path.join(path, "to_download") + assert {:ok, ^host} = File.read(Path.join(to_download_path, "hostname")) + assert {:ok, ""} = File.read(Path.join(to_download_path, "foo")) + assert {:ok, ""} = File.read(Path.join(to_download_path, "bar")) + assert {:ok, uname} = to_download_path + |> Path.join("deep") + |> Path.join("deeper") + |> Path.join("the_depths") + |> File.read + assert String.match?(uname, ~r{Linux}) + + # remote absolute path + remote :app, "hostname >> /tmp/download_abs" + [{:ok, [stdout: host], 0, _}] = remote :app, "hostname" + download :app, "/tmp/download_abs", path + assert {:ok, ^host} = File.read(Path.join(path, "download_abs")) + + # remote absolute path with local rename + remote :app, "hostname >> /tmp/download_alt_name" + [{:ok, [stdout: host], 0, _}] = remote :app, "hostname" + download :app, "/tmp/download_alt_name", Path.join(path, "new_name") + assert {:ok, ^host} = File.read(Path.join(path, "new_name")) + + # a single missing directory will be created + remote :app do + "mkdir -p /tmp/download_dir" + "hostname >> /tmp/download_dir/a_file" + end + + [{:ok, [stdout: host], 0, _}] = remote :app, "hostname" + + to_download_path = Path.join(path, "not_a_dir") + download :app, "/tmp/download_dir", to_download_path + assert {:ok, ^host} = File.read(Path.join(to_download_path, "a_file")) + + # nested local directories are not created + remote :app do + "mkdir -p /tmp/download_dir_deep" + "hostname >> /tmp/download_dir_deep/a_file" + end + + to_download_path = path + |> Path.join("not_a_dir_error") + |> Path.join("deeper_still") + assert_raise File.Error, fn -> + download :app, "/tmp/download_dir_deep", to_download_path + end + assert_raise File.Error, fn -> + download :app, "/tmp/download_dir_deep/a_file", to_download_path + end + + # local file is clobbered + remote :app, "hostname >> /tmp/download_dir_local_exists" + [{:ok, [stdout: host], 0, _}] = remote :app, "hostname" + to_download_path = Path.join(path, "i_exist") + File.write!(to_download_path, "some content") + download :app, "/tmp/download_dir_local_exists", to_download_path + assert {:ok, ^host} = File.read(to_download_path) + + # local directories will be respected + remote :app, "hostname >> /tmp/download_dir_exists" + [{:ok, [stdout: host], 0, _}] = remote :app, "hostname" + to_download_path = Path.join(path, "dir_exists") + File.mkdir!(to_download_path) + download :app, "/tmp/download_dir_exists", to_download_path + assert {:ok, ^host} = File.read(Path.join(to_download_path, "download_dir_exists")) + + # trailing slashes are ignored + remote :app, "hostname >> /tmp/download_force_dir" + [{:ok, [stdout: host], 0, _}] = remote :app, "hostname" + to_download_path = Path.join(path, "forced_dir") <> "/" + download :app, "/tmp/download_force_dir", to_download_path + assert {:ok, ^host} = File.read(Path.join(path, "forced_dir")) + + # trailing current directory characters are not respected + remote :app, "hostname >> /tmp/download_force_current_dir" + [{:ok, [stdout: host], 0, _}] = remote :app, "hostname" + to_download_path = Path.join(path, "forced_current_dir") + download :app, "/tmp/download_force_current_dir", to_download_path <> "/." + assert {:error, :enotdir} = File.read(Path.join(to_download_path, "download_force_current_dir")) + assert {:ok, ^host} = File.read(Path.join(path, "forced_current_dir")) + end) + end end diff --git a/test/bootleg/config_test.exs b/test/bootleg/config_test.exs index 9095049..55a2660 100644 --- a/test/bootleg/config_test.exs +++ b/test/bootleg/config_test.exs @@ -596,7 +596,6 @@ defmodule Bootleg.ConfigTest do end test "config/1" do - # credo:disable-for-next-line Credo.Check.Consistency.MultiAliasImportRequireUse use Bootleg.Config refute config(:foo) @@ -608,4 +607,60 @@ defmodule Bootleg.ConfigTest do config(:foo, nil) assert config({:foo, :bar}) == nil end + + test_with_mock "download/3", SSH, [:passthrough], [ + init: fn(role, options, filter) -> {role, options, filter} end, + download: fn(_conn, _remote, _local) -> :ok end, + ] do + # credo:disable-for-next-line Credo.Check.Consistency.MultiAliasImportRequireUse + use Bootleg.Config + + role :foo, "never-used-foo.example.com" + role :car, "never-used-bar.example.com" + + download :foo, "some/remote/path", "the/local/path" + + assert called SSH.init(:foo, [], []) + assert called SSH.download({:foo, [], []}, "some/remote/path", "the/local/path") + + download [:foo, :bar], "some/remote/path", "the/local/path" + + assert called SSH.init(:foo, [], []) + assert called SSH.init(:bar, [], []) + assert called SSH.download({:foo, [], []}, "some/remote/path", "the/local/path") + assert called SSH.download({:bar, [], []}, "some/remote/path", "the/local/path") + + download :all, "some/remote/path", "the/local/path" + + assert called SSH.init(:foo, [], []) + assert called SSH.init(:car, [], []) + assert called SSH.download({:foo, [], []}, "some/remote/path", "the/local/path") + assert called SSH.download({:car, [], []}, "some/remote/path", "the/local/path") + + download [:foo, primary: true], "some/remote/path", "the/local/path" + + assert called SSH.init(:foo, [], [primary: true]) + assert called SSH.download({:foo, [], [primary: true]}, "some/remote/path", "the/local/path") + + download [:foo, :bar, primary: true], "some/remote/path", "the/local/path" + + assert called SSH.init(:foo, [], [primary: true]) + assert called SSH.init(:bar, [], [primary: true]) + assert called SSH.download({:foo, [], [primary: true]}, "some/remote/path", "the/local/path") + assert called SSH.download({:bar, [], [primary: true]}, "some/remote/path", "the/local/path") + + download [:foo, :bar, primary: true, db: :mysql], "some/remote/path", "the/local/path" + + assert called SSH.init(:foo, [], [primary: true, db: :mysql]) + assert called SSH.init(:bar, [], [primary: true, db: :mysql]) + assert called SSH.download({:foo, [], [primary: true, db: :mysql]}, "some/remote/path", "the/local/path") + assert called SSH.download({:bar, [], [primary: true, db: :mysql]}, "some/remote/path", "the/local/path") + + download [:all, db: :mysql], "some/remote/path", "the/local/path" + + assert called SSH.init(:foo, [], [db: :mysql]) + assert called SSH.init(:car, [], [db: :mysql]) + assert called SSH.download({:foo, [], [db: :mysql]}, "some/remote/path", "the/local/path") + assert called SSH.download({:car, [], [db: :mysql]}, "some/remote/path", "the/local/path") + end end From 83bdd5b37d0028307f1ada4866750ea71292c7ae Mon Sep 17 00:00:00 2001 From: Edward Holets Date: Mon, 30 Oct 2017 14:03:21 -0700 Subject: [PATCH 13/13] Rev version to 0.6.0 and update docs. --- README.md | 10 +++++----- lib/mix/tasks/init.ex | 6 +++--- mix.exs | 2 +- mix.lock | 6 +++--- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 58bc048..beb9ef1 100644 --- a/README.md +++ b/README.md @@ -14,8 +14,8 @@ add additional support. ``` def deps do - [{:distillery, "~> 1.3", - {:bootleg, "~> 0.5"}] + [{:distillery, "~> 1.5", + {:bootleg, "~> 0.6"}] end ``` @@ -407,9 +407,9 @@ To run these steps automatically you may include the additional package ```elixir # mix.exs def deps do - [{:distillery, "~> 1.3"}, - {:bootleg, "~> 0.5"}, - {:bootleg_phoenix, "~> 0.1"}] + [{:distillery, "~> 1.5"}, + {:bootleg, "~> 0.6"}, + {:bootleg_phoenix, "~> 0.2"}] end ``` diff --git a/lib/mix/tasks/init.ex b/lib/mix/tasks/init.ex index 4c28cf0..2bce2ae 100644 --- a/lib/mix/tasks/init.ex +++ b/lib/mix/tasks/init.ex @@ -41,9 +41,9 @@ defmodule Mix.Tasks.Bootleg.Init do # ``` # # mix.exs # def deps do - # [{:distillery, "~> 1.3"}, - # {:bootleg, "~> 0.5"}, - # {:bootleg_phoenix, "~> 0.1"}] + # [{:distillery, "~> 1.5"}, + # {:bootleg, "~> 0.6"}, + # {:bootleg_phoenix, "~> 0.2"}] # end # ``` # For more about `bootleg_phoenix` see: https://github.com/labzero/bootleg_phoenix diff --git a/mix.exs b/mix.exs index 09109ee..7c52d79 100644 --- a/mix.exs +++ b/mix.exs @@ -1,7 +1,7 @@ defmodule Bootleg.Mixfile do use Mix.Project - @version "0.5.0" + @version "0.6.0" @source "https://github.com/labzero/bootleg" @homepage "https://labzero.github.io/bootleg/" diff --git a/mix.lock b/mix.lock index 47b3caa..be49c88 100644 --- a/mix.lock +++ b/mix.lock @@ -3,12 +3,12 @@ "credo": {:hex, :credo, "0.8.8", "990e7844a8d06ebacd88744a55853a83b74270b8a8461c55a4d0334b8e1736c9", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}], "hexpm"}, "dialyxir": {:hex, :dialyxir, "0.5.1", "b331b091720fd93e878137add264bac4f644e1ddae07a70bf7062c7862c4b952", [:mix], [], "hexpm"}, "earmark": {:hex, :earmark, "1.2.3", "206eb2e2ac1a794aa5256f3982de7a76bf4579ff91cb28d0e17ea2c9491e46a4", [:mix], [], "hexpm"}, - "ex_doc": {:hex, :ex_doc, "0.17.1", "39f777415e769992e6732d9589dc5846ea587f01412241f4a774664c746affbb", [:mix], [{:earmark, "~> 1.1", [hex: :earmark, repo: "hexpm", optional: false]}], "hexpm"}, + "ex_doc": {:hex, :ex_doc, "0.18.1", "37c69d2ef62f24928c1f4fdc7c724ea04aecfdf500c4329185f8e3649c915baf", [:mix], [{:earmark, "~> 1.1", [hex: :earmark, repo: "hexpm", optional: false]}], "hexpm"}, "excoveralls": {:hex, :excoveralls, "0.7.4", "3d84b2f15a0e593159f74b19f83794b464b34817183d27965bdc6c462de014f9", [:mix], [{:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: false]}, {:hackney, ">= 0.12.0", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm"}, "exjsx": {:hex, :exjsx, "4.0.0", "60548841e0212df401e38e63c0078ec57b33e7ea49b032c796ccad8cde794b5c", [:mix], [{:jsx, "~> 2.8.0", [hex: :jsx, repo: "hexpm", optional: false]}], "hexpm"}, - "hackney": {:hex, :hackney, "1.9.0", "51c506afc0a365868469dcfc79a9d0b94d896ec741cfd5bd338f49a5ec515bfe", [:rebar3], [{:certifi, "2.0.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "5.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "1.0.2", [hex: :mimerl, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.1", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm"}, + "hackney": {:hex, :hackney, "1.10.1", "c38d0ca52ea80254936a32c45bb7eb414e7a96a521b4ce76d00a69753b157f21", [:rebar3], [{:certifi, "2.0.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "5.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "1.0.2", [hex: :mimerl, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.1", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm"}, "idna": {:hex, :idna, "5.1.0", "d72b4effeb324ad5da3cab1767cb16b17939004e789d8c0ad5b70f3cea20c89a", [:rebar3], [{:unicode_util_compat, "0.3.1", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm"}, - "jsx": {:hex, :jsx, "2.8.2", "7acc7d785b5abe8a6e9adbde926a24e481f29956dd8b4df49e3e4e7bcc92a018", [:mix, :rebar3], [], "hexpm"}, + "jsx": {:hex, :jsx, "2.8.3", "a05252d381885240744d955fbe3cf810504eb2567164824e19303ea59eef62cf", [:mix, :rebar3], [], "hexpm"}, "junit_formatter": {:hex, :junit_formatter, "2.0.0", "7312342085d619590588fb5532ac2ec7444280d0b7224e85a25aab868ae0038d", [:mix], [], "hexpm"}, "meck": {:hex, :meck, "0.8.8", "eeb3efe811d4346e1a7f65b2738abc2ad73cbe1a2c91b5dd909bac2ea0414fa6", [:rebar3], [], "hexpm"}, "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm"},