Skip to content

Commit 27c8b3b

Browse files
committed
Merge branch 'release/0.3.0' into stable
2 parents c0ed4d8 + 0ab657c commit 27c8b3b

20 files changed

+667
-60
lines changed

CODE_OF_CONDUCT.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ further defined and clarified by project maintainers.
5555
## Enforcement
5656

5757
Instances of abusive, harassing, or otherwise unacceptable behavior may be
58-
reported by contacting the project team at [INSERT EMAIL ADDRESS]. All
58+
reported by contacting the project team at `[email protected]`. All
5959
complaints will be reviewed and investigated and will result in a response that
6060
is deemed necessary and appropriate to the circumstances. The project team is
6161
obligated to maintain confidentiality with regard to the reporter of an incident.

CONTRIBUTING.md

+68
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
# Contributing
2+
3+
You want to contribute? Awesome! We'd love the help. If you have an idea already, great. If not,
4+
take a look at our [issue tracker][issues] and see if anything appeals. More tests and
5+
documentation are always appreciated too.
6+
7+
## Getting Started
8+
9+
1. Fork the repository
10+
2. Make sure the tests pass locally _before_ you start developing.
11+
3. Write a test or two that cover your feature/bug/refactor (not needed for documentation-only changes)
12+
4. Make your test pass by adding that slick new code.
13+
5. Add documentation for your change (if appropriate)
14+
6. Run `mix credo --strict` and `mix dialyzer` to ensure you haven't missed any coding standards.
15+
7. Commit your changes and open a [pull request][pulls].
16+
8. Wait for a speedy review of your change! Thank you!
17+
18+
## Development Dependencies
19+
20+
In order to run the functional tests, you need to have [Docker][docker] installed locally. The
21+
community edition is fine, but you'll want to avoid the old versions that require a VM.
22+
23+
## Code Standards
24+
25+
Most of the code standards can be found in `.credo.exs`, and will be checked automatically by the
26+
CI process. When in doubt, follow the standards in the file you are changing. Terse but descriptive
27+
variable and function names make us happy. The standard Elixir guide on [writing documentation][writing-docs]
28+
has some good tips on names. Documentation for new public functions is expected, as are tests for
29+
any code change.
30+
31+
Good commit messages and PR descriptions are also important. See our guide on
32+
[commit messages](https://github.com/labzero/guides/blob/master/process/commit_guide.md) for more details.
33+
34+
## Testing
35+
36+
Good tests are arguably more important than good code, so please take a moment to make sure
37+
you have a few with your PR. Try to avoid mock-only tests, as they can get out of sync with reality
38+
fairly easily. They are great for doing basic unit testing though! You'll see we use
39+
[mock](https://github.com/jjh42/mock) as our mocking framework of choice.
40+
41+
Functional tests are much more reliable with a tool like Bootleg, and there are plenty of examples
42+
in the project. `Bootleg.FunctionalCase` provides a simple interface for writing [Docker][docker]
43+
based functional tests. By default each test case will get a single docker container provisioned,
44+
and the details will be passed to `setup` under the key `hosts`. You can request more containers
45+
using `@tag boot: 2` where `2` is the number of containers you'd like. During test development it's
46+
often helpful to have the containers left running after the tests finish, and you can request that
47+
by setting the `ENV` variable `TEST_LEAVE_CONTAINER` when running your tests. It's best to limit how
48+
many tests are run in that case, or you may kill your machine with too many docker containers at once.
49+
50+
If you need a project to test against (this a deployment tool after all), take a look at
51+
`Bootleg.Fixtures.inflate_project/1`. It will take any of the fixture projects and create a new
52+
instance for use during testing. The `test/fixtures` directory contains all the currently available
53+
fixture projects. Instances of projects created via `inflate_project/1` will be cleaned up when the
54+
test suite exits, but you can suppress that by setting `TEST_LEAVE_TEMP` in the `ENV`. Fixtures are
55+
always inflated to your OS temporary directory.
56+
57+
## Contact
58+
59+
You can reach the core Bootleg team in [#deployment](https://elixir-lang.slack.com/messages/C0LH49EPQ)
60+
or [#bootleg](https://elixir-lang.slack.com/messages/C6D2BQY4R) on Elixir Slack. We are also reachable
61+
via email at `[email protected]`. Don't hesitate to get in touch, we'd love to hear from you.
62+
63+
Use the [issue tracker][issues] for bug reports or feature requests.
64+
65+
[issues]: https://github.com/labzero/bootleg/issues
66+
[pulls]: https://github.com/labzero/bootleg/pulls
67+
[writing-docs]: http://elixir-lang.org/docs/stable/elixir/writing-documentation.html
68+
[docker]: https://www.docker.com/

README.md

+22-4
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ add additional support.
1515
```elixir
1616
def deps do
1717
[{:distillery, "~> 1.3",
18-
{:bootleg, "~> 0.1.0"}]
18+
{:bootleg, "~> 0.3"}]
1919
end
2020
```
2121

@@ -151,6 +151,9 @@ Alternatively the above commands can be rolled into one with:
151151
mix bootleg.update production
152152
```
153153

154+
Note that `bootleg.update` will stop any running nodes and then perform a cold start. The stop is performed with
155+
the task `stop_silent`, which differs from `stop` in that it does not fail if the node is already stopped.
156+
154157
## Admin Commands
155158

156159
Bootleg has a set of commands to check up on your running nodes:
@@ -162,6 +165,15 @@ mix bootleg.stop production # Stops a deployed release.
162165
mix bootleg.ping production # Check status of running nodes
163166
```
164167

168+
## Other Comamnds
169+
170+
Bootleg has a few utility commands to help streamline its usage:
171+
172+
```console
173+
mix bootleg.init # Initializes a project for use with Bootleg
174+
mix bootleg.invoke <task> # Calls an arbitrary Bootleg task
175+
```
176+
165177
## Hooks
166178

167179
Hooks may be defined by the user in order to perform additional (or exceptional)
@@ -287,7 +299,8 @@ end
287299

288300
The workhorse of the Bootleg DSL is `remote`: it executes shell commands on remote servers and returns
289301
the results. It takes a role and a block of commands to execute. The commands are executed on all servers
290-
belonging to the role, and raises an `SSHError` if an error is encountered.
302+
belonging to the role, and raises an `SSHError` if an error is encountered. Optionally, a list of options
303+
can be provided to filter the hosts where the commands are run.
291304

292305
```elixir
293306
use Bootleg.Config
@@ -312,6 +325,11 @@ end
312325
remote :app do
313326
"false"
314327
end
328+
329+
# filtering - only runs on app hosts with an option of primary set to true
330+
remote :app, primary: true do
331+
"mix ecto.migrate"
332+
end
315333
```
316334

317335
## Phoenix Support
@@ -326,8 +344,8 @@ for building phoenix releases.
326344
# mix.exs
327345
def deps do
328346
[{:distillery, "~> 1.3"},
329-
{:bootleg, "~> 0.2.0"},
330-
{:bootleg_phoenix, "~> 0.1.0"}]
347+
{:bootleg, "~> 0.3"},
348+
{:bootleg_phoenix, "~> 0.1"}]
331349
end
332350
```
333351

lib/bootleg/config.ex

+108-11
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ defmodule Bootleg.Config do
99
defmacro __using__(_) do
1010
quote do
1111
import Bootleg.Config, only: [role: 2, role: 3, config: 2, config: 0, before_task: 2,
12-
after_task: 2, invoke: 1, task: 2, remote: 1, remote: 2, load: 1]
12+
after_task: 2, invoke: 1, task: 2, remote: 1, remote: 2, remote: 3, load: 1, upload: 3]
1313
end
1414
end
1515

@@ -22,7 +22,7 @@ defmodule Bootleg.Config do
2222
2323
`name` is the name of the role, and is globally unique. Calling `role/3` multiple times with
2424
the same name will result in the host lists being merged. If the same host shows up mutliple
25-
times, it will have its `options` merged.
25+
times, it will have its `options` merged. The name `:all` is reserved and cannot be used here.
2626
2727
`hosts` can be a single hostname, or a `List` of hostnames.
2828
@@ -41,6 +41,9 @@ defmodule Bootleg.Config do
4141
"""
4242
defmacro role(name, hosts, options \\ []) do
4343
# user is in the role options for scm
44+
if name == :all do
45+
raise ArgumentError, ":all is reserved by bootleg and refers to all defined roles."
46+
end
4447
user = Keyword.get(options, :user, System.get_env("USER"))
4548
ssh_options = Enum.filter(options, &Enum.member?(SSH.supported_options, elem(&1, 0)) == true)
4649
role_options =
@@ -325,11 +328,28 @@ defmodule Bootleg.Config do
325328
end
326329

327330
defmacro remote(role, do: {:__block__, _, lines}) do
328-
quote do: remote(unquote(role), unquote(lines))
331+
quote do: remote(unquote(role), [], unquote(lines))
329332
end
330333

331334
defmacro remote(role, do: lines) do
332-
quote do: remote(unquote(role), unquote(lines))
335+
quote do: remote(unquote(role), [], unquote(lines))
336+
end
337+
338+
@doc """
339+
Executes commands on all remote hosts within a role.
340+
341+
This is equivalent to calling `remote/3` with a `filter` of `[]`.
342+
"""
343+
defmacro remote(role, lines) do
344+
quote do: remote(unquote(role), [], unquote(lines))
345+
end
346+
347+
defmacro remote(role, filter, do: {:__block__, _, lines}) do
348+
quote do: remote(unquote(role), unquote(filter), unquote(lines))
349+
end
350+
351+
defmacro remote(role, filter, do: lines) do
352+
quote do: remote(unquote(role), unquote(filter), unquote(lines))
333353
end
334354

335355
@doc """
@@ -343,6 +363,10 @@ defmodule Bootleg.Config do
343363
used as a command. Each command will be simulataneously executed on all hosts in the role. Once
344364
all hosts have finished executing the command, the next command in the list will be sent.
345365
366+
`filter` is an optional `Keyword` list of host options to filter with. Any host whose options match
367+
the filter will be included in the remote execution. A host matches if it has all of the filtering
368+
options defined and the values match (via `==/2`) the filter.
369+
346370
`role` can be a single role, a list of roles, or the special role `:all` (all roles). If the same host
347371
exists in multiple roles, the commands will be run once for each role where the host shows up. In the
348372
case of multiple roles, each role is processed sequentially.
@@ -367,18 +391,22 @@ defmodule Bootleg.Config do
367391
368392
# runs for hosts found in :build first, then for hosts in :app
369393
remote [:build, :app], do: "hostname"
394+
395+
# only runs on `host1.example.com`
396+
role :build, "host2.example.com"
397+
role :build, "host1.example.com", primary: true, another_attr: :cat
398+
399+
remote :build, primary: true do
400+
"hostname"
401+
end
370402
```
371403
"""
372-
defmacro remote(role, lines) do
373-
roles = if role == :all do
374-
quote do: Keyword.keys(Bootleg.Config.Agent.get(:roles))
375-
else
376-
quote do: List.wrap(unquote(role))
377-
end
404+
defmacro remote(role, filter, lines) do
405+
roles = unpack_role(role)
378406
quote bind_quoted: binding() do
379407
Enum.reduce(roles, [], fn role, outputs ->
380408
role
381-
|> SSH.init
409+
|> SSH.init([], filter)
382410
|> SSH.run!(lines)
383411
|> SSH.merge_run_results(outputs)
384412
end)
@@ -401,6 +429,56 @@ defmodule Bootleg.Config do
401429
end
402430
end
403431

432+
@doc """
433+
Uploads a local file to remote hosts.
434+
435+
Uploading works much like `remote/3`, but instead of transferring shell commands over SSH,
436+
it transfers files via SCP. The remote host does need to support SCP, which should be provided
437+
by most SSH implementations automatically.
438+
439+
`role` can either be a single role name, a list of roles, or a list of roles and filter
440+
attributes. The special `:all` role is also supported. See `remote/3` for details.
441+
442+
`local_path` can either be a file or directory found on the local machine. If its a directory,
443+
the entire directory will be recursively copied to the remote hosts. Relative paths are resolved
444+
relative to the root of the local project.
445+
446+
`remote_path` is the file or directory where the transfered files should be placed. The semantics
447+
of how `remote_path` is treated vary depending on what `local_path` refers to. If `local_path` points
448+
to a file, `remote_path` is treated as a file unless it's `.` or ends in `/`, in which case it's
449+
treated as a directory and the filename of the local file will be used. If `local_path` is a directory,
450+
`remote_path` is treated as a directory as well. Relative paths are resolved relative to the projects
451+
remote `workspace`. Missing directories are not implicilty created.
452+
453+
The files on the remote server are created using the authenticating user's `uid`/`gid` and `umask`.
454+
455+
```
456+
use Bootleg.Config
457+
458+
# copies ./my_file to ./new_name on the remote host
459+
upload :app, "my_file", "new_name"
460+
461+
# copies ./my_file to ./a_dir/my_file on the remote host. ./a_dir must already exist
462+
upload :app, "my_file", "a_dir/"
463+
464+
# recursively copies ./some_dir to ./new_dir on the remote host. ./new_dir will be created if missing
465+
upload :app, "some_dir", "new_dir"
466+
467+
# copies ./my_file to /tmp/foo on the remote host
468+
upload :app, "my_file", "/tmp/foo"
469+
"""
470+
defmacro upload(role, local_path, remote_path) do
471+
{roles, filters} = split_roles_and_filters(role)
472+
roles = unpack_role(roles)
473+
quote bind_quoted: binding() do
474+
Enum.each(roles, fn role ->
475+
role
476+
|> SSH.init([], filters)
477+
|> SSH.upload(local_path, remote_path)
478+
end)
479+
end
480+
end
481+
404482
@doc false
405483
@spec get_config(atom, any) :: any
406484
def get_config(key, default \\ nil) do
@@ -418,4 +496,23 @@ defmodule Bootleg.Config do
418496
def version do
419497
get_config(:version, Project.config[:version])
420498
end
499+
500+
@doc false
501+
@spec split_roles_and_filters(atom | keyword) :: {[atom], keyword}
502+
defp split_roles_and_filters(role) do
503+
role
504+
|> List.wrap
505+
|> Enum.split_while(fn term -> !is_tuple(term) end)
506+
end
507+
508+
@doc false
509+
@spec unpack_role(atom | keyword) :: tuple
510+
defp unpack_role(role) do
511+
wrapped_role = List.wrap(role)
512+
if Enum.any?(wrapped_role, fn role -> role == :all end) do
513+
quote do: Keyword.keys(Bootleg.Config.Agent.get(:roles))
514+
else
515+
quote do: unquote(wrapped_role)
516+
end
517+
end
421518
end

0 commit comments

Comments
 (0)