Skip to content

Commit 7dcd2b0

Browse files
holetserjanja
authored andcommitted
Add custom working directory support to remote/3. (#174)
1 parent 2e7eb1d commit 7dcd2b0

File tree

6 files changed

+98
-28
lines changed

6 files changed

+98
-28
lines changed

README.md

+7-1
Original file line numberDiff line numberDiff line change
@@ -382,9 +382,15 @@ remote :app do
382382
end
383383

384384
# filtering - only runs on app hosts with an option of primary set to true
385-
remote :app, primary: true do
385+
remote :app, filter: [primary: true] do
386386
"mix ecto.migrate"
387387
end
388+
389+
# change working directory - creates a file `/tmp/foo`, regardless of the role
390+
# workspace configuration
391+
remote :app, cd: "/tmp" do
392+
"touch ./foo"
393+
end
388394
```
389395

390396
## Phoenix Support

lib/bootleg/config.ex

+22-10
Original file line numberDiff line numberDiff line change
@@ -338,18 +338,18 @@ defmodule Bootleg.Config do
338338
@doc """
339339
Executes commands on all remote hosts within a role.
340340
341-
This is equivalent to calling `remote/3` with a `filter` of `[]`.
341+
This is equivalent to calling `remote/3` with an `options` of `[]`.
342342
"""
343343
defmacro remote(role, lines) do
344344
quote do: remote(unquote(role), [], unquote(lines))
345345
end
346346

347-
defmacro remote(role, filter, do: {:__block__, _, lines}) do
348-
quote do: remote(unquote(role), unquote(filter), unquote(lines))
347+
defmacro remote(role, options, do: {:__block__, _, lines}) do
348+
quote do: remote(unquote(role), unquote(options), unquote(lines))
349349
end
350350

351-
defmacro remote(role, filter, do: lines) do
352-
quote do: remote(unquote(role), unquote(filter), unquote(lines))
351+
defmacro remote(role, options, do: lines) do
352+
quote do: remote(unquote(role), unquote(options), unquote(lines))
353353
end
354354

355355
@doc """
@@ -363,9 +363,16 @@ defmodule Bootleg.Config do
363363
used as a command. Each command will be simulataneously executed on all hosts in the role. Once
364364
all hosts have finished executing the command, the next command in the list will be sent.
365365
366-
`filter` is an optional `Keyword` list of host options to filter with. Any host whose options match
366+
`options` is an optional `Keyword` list of options to customize the remote invocation. Currently two
367+
keys are supported:
368+
369+
* `filter` takes a `Keyword` list of host options to filter with. Any host whose options match
367370
the filter will be included in the remote execution. A host matches if it has all of the filtering
368371
options defined and the values match (via `==/2`) the filter.
372+
* `cd` changes the working directory of the remote shell prior to executing the remote
373+
commands. The options takes either an absolute or relative path, with relative paths being
374+
defined relative to the workspace configured for the role, or the default working directory
375+
of the shell if no workspace is defined.
369376
370377
`role` can be a single role, a list of roles, or the special role `:all` (all roles). If the same host
371378
exists in multiple roles, the commands will be run once for each role where the host shows up. In the
@@ -394,19 +401,24 @@ defmodule Bootleg.Config do
394401
395402
# only runs on `host1.example.com`
396403
role :build, "host2.example.com"
397-
role :build, "host1.example.com", primary: true, another_attr: :cat
404+
role :build, "host1.example.com", filter: [primary: true, another_attr: :cat]
398405
399-
remote :build, primary: true do
406+
remote :build, filter: [primary: true] do
407+
"hostname"
408+
end
409+
410+
# runs on `host1.example.com` inside the `tmp` directory found in the workspace
411+
remote :build, filter: [primary: true], cd: "tmp/" do
400412
"hostname"
401413
end
402414
```
403415
"""
404-
defmacro remote(role, filter, lines) do
416+
defmacro remote(role, options, lines) do
405417
roles = unpack_role(role)
406418
quote bind_quoted: binding() do
407419
Enum.reduce(roles, [], fn role, outputs ->
408420
role
409-
|> SSH.init([], filter)
421+
|> SSH.init([cd: options[:cd]], Keyword.get(options, :filter, []))
410422
|> SSH.run!(lines)
411423
|> SSH.merge_run_results(outputs)
412424
end)

lib/bootleg/ssh.ex

+12
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ defmodule Bootleg.SSH do
3737
def init(hosts, options) do
3838
workspace = Keyword.get(options, :workspace, ".")
3939
create_workspace = Keyword.get(options, :create_workspace, true)
40+
working_directory = Keyword.get(options, :cd)
4041
UI.puts "Creating remote context at '#{workspace}'"
4142

4243
:ssh.start()
@@ -46,6 +47,7 @@ defmodule Bootleg.SSH do
4647
|> Enum.map(&ssh_host_options/1)
4748
|> SSHKit.context()
4849
|> validate_workspace(workspace, create_workspace)
50+
|> working_directory(working_directory)
4951
end
5052

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

89+
defp working_directory(context, path) when path == "." or path == false or is_nil(path) do
90+
context
91+
end
92+
defp working_directory(context, path) do
93+
case Path.type(path) do
94+
:absolute -> %Context{context | path: path}
95+
_ -> %Context{context | path: Path.join(context.path, path)}
96+
end
97+
end
98+
8799
defp capture(message, {buffer, status} = state, host) do
88100
next = case message do
89101
{:data, _, 0, data} ->

test/bootleg/config_functional_test.exs

+9-6
Original file line numberDiff line numberDiff line change
@@ -136,19 +136,22 @@ defmodule Bootleg.ConfigFunctionalTest do
136136
end
137137

138138
@tag boot: 3
139-
test "remote/3 filtering" do
139+
test "remote/3 options" do
140140
capture_io(fn ->
141141
use Bootleg.Config
142142

143-
assert [{:ok, out_0, 0, _}] = remote :build, [foo: 0], "hostname"
144-
assert [{:ok, out_1, 0, _}] = remote :build, [foo: 1], do: "hostname"
143+
assert [{:ok, out_0, 0, _}] = remote :build, [filter: [foo: 0]], "hostname"
144+
assert [{:ok, out_1, 0, _}] = remote :build, [filter: [foo: 1]], do: "hostname"
145145
assert out_1 != out_0
146146

147-
assert [] = remote :build, [foo: :bar], "hostname"
148-
assert [{:ok, out_all, 0, _}] = remote :all, [foo: :bar], "hostname"
147+
assert [] = remote :build, [filter: [foo: :bar]], "hostname"
148+
assert [{:ok, out_all, 0, _}] = remote :all, [filter: [foo: :bar]], "hostname"
149149
assert out_1 != out_0 != out_all
150150

151-
remote :all, [foo: :bar] do "hostname" end
151+
remote :all, filter: [foo: :bar] do "hostname" end
152+
153+
[{:ok, [stdout: "/tmp\n"], 0, _}] = remote :app, cd: "/tmp" do "pwd" end
154+
[{:ok, [stdout: "/home\n"], 0, _}] = remote :app, cd: "../.." do "pwd" end
152155
end)
153156
end
154157

test/bootleg/config_test.exs

+31-10
Original file line numberDiff line numberDiff line change
@@ -447,48 +447,69 @@ defmodule Bootleg.ConfigTest do
447447
assert called SSH.run!({:bar}, ["echo Multi Hello", "echo Multi World!"])
448448
end
449449

450-
test_with_mock "remote/3 filtering", SSH, [:passthrough], [
450+
test_with_mock "remote/3", SSH, [:passthrough], [
451451
init: fn(role, options, filter) -> {role, options, filter} end,
452452
run!: fn(_, _cmd) -> [:ok] end
453453
] do
454454
use Bootleg.Config
455455

456456
task :remote_test_role_one_line_filtered do
457-
remote :one_line, [a_filter: true], "echo Multi Hello"
457+
remote :one_line, [filter: [a_filter: true]], "echo Multi Hello"
458458
end
459459

460460
task :remote_test_role_inline_filtered do
461-
remote :inline, [b_filter: true], do: "echo Multi Hello"
461+
remote :inline, [filter: [b_filter: true]], do: "echo Multi Hello"
462462
end
463463

464464
task :remote_test_role_filtered do
465-
remote :car, passenger: true do
465+
remote :car, filter: [passenger: true] do
466466
"echo Multi Hello"
467467
end
468468
end
469469

470470
task :remote_test_roles_filtered do
471-
remote [:foo, :bar], primary: true do
471+
remote [:foo, :bar], filter: [primary: true] do
472472
"echo Multi Hello"
473473
end
474474
end
475475

476+
task :remote_working_directory_option do
477+
remote :foo, cd: "/bar" do "echo bar!" end
478+
end
479+
480+
task :remote_working_directory_option_nil do
481+
remote :foo, cd: nil do "echo bar!" end
482+
end
483+
484+
task :remote_working_directory_option_none do
485+
remote :foo do "echo bar!" end
486+
end
487+
476488
invoke :remote_test_role_one_line_filtered
477489

478-
assert called SSH.init(:one_line, [], a_filter: true)
490+
assert called SSH.init(:one_line, :_, a_filter: true)
479491

480492
invoke :remote_test_role_inline_filtered
481493

482-
assert called SSH.init(:inline, [], b_filter: true)
494+
assert called SSH.init(:inline, :_, b_filter: true)
483495

484496
invoke :remote_test_role_filtered
485497

486-
assert called SSH.init(:car, [], passenger: true)
498+
assert called SSH.init(:car, :_, passenger: true)
487499

488500
invoke :remote_test_roles_filtered
489501

490-
assert called SSH.init(:foo, [], [primary: true])
491-
assert called SSH.init(:bar, [], [primary: true])
502+
assert called SSH.init(:foo, :_, [primary: true])
503+
assert called SSH.init(:bar, :_, [primary: true])
504+
505+
invoke :remote_working_directory_option
506+
assert called SSH.init(:foo, [cd: "/bar"], [])
507+
508+
invoke :remote_working_directory_option_nil
509+
assert called SSH.init(:foo, [cd: nil], [])
510+
511+
invoke :remote_working_directory_option_none
512+
assert called SSH.init(:foo, [cd: nil], [])
492513
end
493514

494515
test_with_mock "upload/3", SSH, [:passthrough], [

test/bootleg/ssh_functional_test.exs

+17-1
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,6 @@ defmodule Bootleg.SSHFunctionalTest do
8585

8686
@tag boot: 2
8787
test "init/3 host filtering for roles", %{hosts: [host_1, host_2]} do
88-
# credo:disable-for-next-line Credo.Check.Consistency.MultiAliasImportRequireUse
8988
use Bootleg.Config
9089

9190
ip_1 = host_1.ip
@@ -100,6 +99,23 @@ defmodule Bootleg.SSHFunctionalTest do
10099
end)
101100
end
102101

102+
test "init/3 working directory option", %{hosts: [host]} do
103+
# credo:disable-for-next-line Credo.Check.Consistency.MultiAliasImportRequireUse
104+
use Bootleg.Config
105+
106+
role :valid_workspace, host.ip, port: host.port, user: host.user,
107+
workspace: "./woo/bar", silently_accept_hosts: true, identity: host.private_key_path
108+
role :bad_workspace, host.ip, port: host.port, user: host.user,
109+
workspace: "/woo/bar", silently_accept_hosts: true, identity: host.private_key_path
110+
capture_io(fn ->
111+
assert %SSHKitContext{path: "/foo"} = SSH.init(:valid_workspace, [cd: "/foo"])
112+
assert %SSHKitContext{path: "./woo/bar/foo"} = SSH.init(:valid_workspace, [cd: "foo"])
113+
assert %SSHKitContext{path: "./woo/bar"} = SSH.init(:valid_workspace, [cd: nil])
114+
assert_raise SSHError, fn -> SSH.init(:bad_workspace, [cd: "/foo"]) end
115+
assert_raise SSHError, fn -> SSH.init(:bad_workspace, [cd: "foo"]) end
116+
end)
117+
end
118+
103119
test "run!/2 raises an error if the host refuses the connection", %{hosts: [host]} do
104120
capture_io(fn ->
105121
conn = SSHKit.context(SSHKit.host(host.ip))

0 commit comments

Comments
 (0)